From 579f1cd068286233ae33219bc347c4559d365a05 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 5 Apr 2026 09:22:42 +0200 Subject: [PATCH] chore: added editor specs --- specs/bds.allium | 1 + specs/editors.allium | 415 +++++++++++++++++++++++++++++++++++++ specs/sidebar_views.allium | 74 +++++-- specs/tabs.allium | 37 ++-- 4 files changed, 495 insertions(+), 32 deletions(-) create mode 100644 specs/editors.allium diff --git a/specs/bds.allium b/specs/bds.allium index 5818b16..f083e12 100644 --- a/specs/bds.allium +++ b/specs/bds.allium @@ -30,6 +30,7 @@ use "./i18n.allium" as i18n -- Split localization (UI vs content use "./layout.allium" as layout -- App shell, activity bar, status bar, panels use "./tabs.allium" as tabs -- Tab system, editor routing, preview/pin use "./sidebar_views.allium" as sidebar -- 10 sidebar views, content, behaviour +use "./editors.allium" as editors -- Editor view layouts, dashboard, chat panel -- Integration use "./git.allium" as git -- Git operations, LFS, reconciliation diff --git a/specs/editors.allium b/specs/editors.allium new file mode 100644 index 0000000..6efce6b --- /dev/null +++ b/specs/editors.allium @@ -0,0 +1,415 @@ +-- allium: 1 +-- bDS Editor Views +-- Scope: UI content area (all waves + extensions) +-- Distilled from: PostEditor.tsx, MediaEditor.tsx, Editor.tsx (Dashboard), +-- SettingsView.tsx, StyleView.tsx, ChatPanel.tsx, TagsView + +-- Describes the layout and behaviour of each editor view rendered in +-- the main content area when a tab is active (or dashboard when no tab). +-- Tab routing is in tabs.allium. Sidebar navigation is in sidebar_views.allium. + +use "./tabs.allium" as tabs +use "./post.allium" as post +use "./media.allium" as media +use "./i18n.allium" as i18n + +-- ─── Dashboard (no tab active) ─────────────────────────────── + +-- Shown as default/welcome view when no entity tab is active. +-- Single centered column. + +value Dashboard { + title: String + subtitle: String + stats: DashboardStats + timeline: DashboardTimeline + tag_cloud: DashboardTagCloud + category_cloud: DashboardCategoryCloud + recent_posts: List +} + +value DashboardStats { + -- Three stat cards side by side: + total_posts: Integer + published_count: Integer + draft_count: Integer + archived_count: Integer -- shown only if > 0 + media_count: Integer + image_count: Integer + total_media_size: String -- formatted B/KB/MB/GB + tag_count: Integer + category_count: Integer +} + +value DashboardTimeline { + -- Bar chart of posts over time + -- Last 12 months that have data + -- Each bar: count label on top, month abbreviation + year below + -- Bar height proportional to max count + months: List +} + +value DashboardTimelineMonth { + label: String -- month abbreviation + year: Integer + count: Integer +} + +value DashboardTagCloud { + -- Up to 40 tags, sorted alphabetically + -- Font size scaled 11px-22px based on post count + -- Tags with colours get coloured background with contrast text + -- Hover title shows post count + -- "and N more" text if > 40 tags + tags: List + overflow_count: Integer? +} + +value DashboardTag { + name: String + count: Integer + color: String? +} + +value DashboardCategoryCloud { + -- All categories as badge-like tags + -- Each shows category name + count + categories: List +} + +value DashboardCategory { + name: String + count: Integer +} + +value DashboardRecentPost { + -- Last 5 posts by updatedAt descending + -- Each row: title (or "Untitled"), status badge (draft/published), date + -- Single-click: preview tab, double-click: pin tab + post_id: String + title: String + status: String + date: String +} + +config { + dashboard_max_tags: Integer = 40 + dashboard_tag_min_font: Integer = 11 + dashboard_tag_max_font: Integer = 22 + dashboard_recent_count: Integer = 5 + dashboard_timeline_months: Integer = 12 +} + +-- ─── Post editor ────────────────────────────────────────────── + +-- Single vertical column. Three regions: header, body, footer. + +-- Header bar: +-- Left: post title (or "Untitled") with dirty indicator dot +-- Right: status badge, auto-save indicator, Quick Actions dropdown, Publish/Delete buttons +-- Quick Actions: +-- AI Analysis (suggests title, excerpt, slug) +-- Translate Post (opens translation modal) + +-- Collapsible Metadata section (starts expanded when title is empty): +-- Two-column layout. Left column: metadata fields. Right column: linked media panel. +-- Left column fields: +-- Title (text input), Tags (autocomplete chip input), Author (text input), +-- Language (select + AI detect button), Do Not Translate (checkbox), +-- Slug (read-only), Categories (chip input), Template (select, if templates exist), +-- PostLinks (backlinks and outlinks) +-- Right column: +-- LinkedMediaPanel (media items linked to this post) + +-- Language flags bar (inline with metadata toggle): +-- Row of flag emoji buttons for canonical language + each translation +-- Styles: status class (draft/published), active indicator +-- Clicking switches editor content to that language's draft + +-- Collapsible Excerpt section: +-- Excerpt textarea (4 rows) + +-- Editor Body: +-- Toolbar: "Content" label | mode toggle (Visual/Markdown/Preview) | action buttons +-- Action buttons (markdown mode only): Gallery, Insert Post Link, Insert Media +-- Visual mode: rich-text WYSIWYG editor +-- Markdown mode: code editor with markdown-with-macros language +-- Highlights [[macro ...]] double-bracket syntax +-- Word wrap on, minimap off, 14px font +-- Preview mode: iframe showing rendered preview +-- Supports drag-and-drop of images (imports media, inserts markdown) + +-- Footer: +-- Three date stamps: Created, Updated, Published (published only if publishedAt exists) +-- Locale-formatted dates + +-- Modals: +-- InsertModal for link search (Ctrl+K) and media search +-- AISuggestionsModal for AI-generated title/excerpt/slug +-- Translation modal: select target language, shows existing status +-- Lightbox for content images + +value PostEditorView { + post_id: String + header: PostEditorHeader + metadata_expanded: Boolean + excerpt_expanded: Boolean + editor_mode: String -- visual | markdown | preview +} + +value PostEditorHeader { + title: String -- post title or "Untitled" + is_dirty: Boolean + status: String -- draft | published | archived + is_auto_saving: Boolean +} + +config { + post_auto_save_delay: Integer = 3000 + -- milliseconds of idle before auto-save triggers +} + +invariant PostAutoSave { + -- Auto-saves after 3 seconds of idle in the editor + -- Also auto-saves on unmount/tab switch + -- Ctrl/Cmd+S triggers immediate save +} + +invariant PostDirtyTracking { + -- Compares canonical draft + translation drafts against saved state + -- Dirty indicator shown in header and tab bar +} + +-- ─── Media editor ───────────────────────────────────────────── + +-- Single column: preview area on top, metadata form below. + +-- Header bar: +-- Media display name +-- Actions: Quick Actions dropdown, Replace File, Save, Delete +-- Quick Actions: +-- AI Image Analysis (image/* types only) - suggests title, alt, caption +-- Detect Language (from metadata text) +-- Translate Metadata (opens translation target modal) + +-- Preview area: +-- Images: rendered via bds-media:// protocol with cache-busting timestamp +-- Non-images: SVG file icon placeholder + original filename text + +-- Metadata form fields: +-- File Name (read-only), MIME Type (read-only), +-- Size (formatted as KB) + Dimensions (W x H, only if width/height exist), +-- Title (editable), Alt Text (editable), Caption (textarea 3 rows), +-- Tags (comma-separated text input), Author, Language (select) + +-- Translations section (shown only when language is set): +-- List of existing translations: flag + language name + title +-- Per-translation: click to edit, refresh button, delete button +-- "No translations" message when empty + +-- Linked Posts section: +-- "Link to Post" button opens post picker overlay +-- Post picker: search input, up to 10 unlinked posts, "and N more" if >10 +-- List of currently linked posts (clickable -> navigates), unlink button +-- "Not linked to any posts" when empty + +value MediaEditorView { + media_id: String + is_image: Boolean + file_name: String -- originalName, read-only + mime_type: String + file_size: String -- formatted + dimensions: String? -- "W x H" if available + title: String? + alt_text: String? + caption: String? + tags: String? -- comma-separated + author: String? + language: String? + translations: List + linked_posts: List +} + +value MediaTranslationItem { + language: String + title: String? +} + +value LinkedPostItem { + post_id: String + title: String +} + +-- No auto-save; explicit Save button required. +-- Replace File opens a native file dialog. +-- Delete shows confirmation with references (linked posts). + +-- ─── Settings view ──────────────────────────────────────────── + +-- VS Code-style settings page with header + search bar + scrollable sections. +-- Search filters sections by keyword match. "No results" with clear button. + +-- 8 sections (style is a separate tab, not a settings section): + +-- 1. Project Settings: +-- Project Name (text), Project Description (textarea 3 rows), +-- Data Path (text + Browse + Reset), Public URL (url), +-- Main Language (select), Blog Languages (checkboxes, main disabled), +-- Default Author (text), Max Posts Per Page (number 1-500 default 50), +-- Blogmark Category (select), Blogmark Bookmarklet (copy button), Save + +-- 2. Editor Settings: +-- Default Editor Mode (select: WYSIWYG/Markdown/Preview), +-- Diff View Style (select: Inline/Side-by-side), +-- Wrap Long Lines (checkbox), Hide Unchanged Regions (checkbox) + +-- 3. Content Settings (Categories): +-- Table: Category | Title | Render in Lists | Show Titles | Post Template | List Template | Remove +-- Protected categories: article, aside, page, picture (cannot be deleted) +-- Add Category: text input + Add button. Reset to Defaults button + +-- 4. AI Assistant Settings: +-- API keys: OpenCode, Mistral (masked display + Change, or password input + Save) +-- Local providers: Ollama enable checkbox + capabilities table, +-- LM Studio enable checkbox + capabilities table +-- Offline mode: enable checkbox (disabled if no local providers), +-- selectors for chat/title/image models (local only) +-- Default Model (select with optgroup by provider + refresh), +-- shows: max output tokens, context window, pricing +-- Title Model (select), Image Analysis Model (vision-capable only) +-- System Prompt (textarea 12 rows + Save + Reset to Default) + +-- 5. Technology Settings: +-- Python Runtime Mode (select: Web Worker/Main Thread) +-- Semantic Similarity (checkbox) + +-- 6. Publishing Settings (SSH): +-- Info: SSH key auth only +-- SSH Mode (select: scp/rsync), SSH Host, SSH Username, SSH Remote Path +-- Save + Clear buttons + +-- 7. Data Maintenance: +-- Rebuild buttons (6): Posts from Files, Media from Files, Scripts from Files, +-- Templates from Files, Links, Regenerate Missing Thumbnails +-- Open Data Folder button + +-- 8. MCP Server Settings: +-- Status badge (port or "Not running") +-- Per-agent rows: Claude Code, Claude Desktop, GitHub Copilot, Gemini CLI, +-- OpenCode, Mistral Vibe, OpenAI Codex — each with Add/Remove toggle + +-- ─── Style view ─────────────────────────────────────────────── + +-- Simple vertical layout: header, theme picker, controls row, preview. + +-- Theme Picker: grid of theme buttons (one per Pico CSS theme). +-- Each button: swatch with 3 colour tones (accent, light bg, dark bg), theme name. +-- Selected theme: highlighted with aria-pressed. + +-- Controls row: Preview Mode dropdown (Auto/Light/Dark), Apply Theme button. + +-- Preview: iframe of style preview page at 127.0.0.1:4123/__style-preview +-- with theme and mode query params. Updates live on selection change. + +-- Apply Theme persists to project metadata. + +-- ─── Tags view ──────────────────────────────────────────────── + +-- Three sections controlled by sidebar TagsNav: + +-- 1. Cloud: tag cloud visualization. Tags sized by post count, +-- coloured by tag colour. Clickable to select for editing. + +-- 2. Manage (Create/Edit): tag name input, colour picker, template select. +-- Edit existing tag or create new. Delete button with confirmation. + +-- 3. Merge: source tag select, target tag select, Merge button. +-- Merges all posts from source tag into target, removes source. + +-- ─── Chat panel ─────────────────────────────────────────────── + +-- Three-region vertical layout: header, message area, input area. + +-- API Key Required screen (shown when no API key): +-- Key icon, title, description, "Open Settings" button + +-- Header: +-- Left: conversation title (or "New Chat"), CSS ellipsis on overflow +-- Right: model selector button + dropdown. Groups models by provider. + +-- Message area: +-- Welcome screen (no messages, not streaming): robot icon, tips +-- Message list: each has avatar (user=person, assistant=robot), role label, text +-- User messages: plain text +-- Assistant messages: rendered as GFM Markdown +-- External images blocked (CSP), shown as links +-- Tool markers: checkmark/dot, tool name, truncated args (30 char strings) +-- Streaming: accumulating markdown + tool markers, thinking dots animation +-- Auto-scrolls to bottom on new messages + +-- Input area: +-- Abort/Stop button (only during streaming, square stop icon) +-- Auto-growing textarea (max 200px). Enter sends, Shift+Enter newline +-- Send button (up-arrow icon), disabled when empty or streaming + +-- Token usage tracked per conversation (displayed in status bar, not chat panel). +-- Per-conversation model override via header selector. + +config { + chat_tool_args_max_length: Integer = 30 + chat_input_max_height: Integer = 200 +} + +-- ─── Git diff view ──────────────────────────────────────────── + +-- Renders diff for a file (working tree vs HEAD) or a commit. +-- File diff: id = "git-diff:{filePath}" +-- Commit diff: id = "git-diff:commit:{commitHash}" +-- Supports inline and side-by-side diff display modes (from editor settings). + +-- ─── Menu editor view ──────────────────────────────────────── + +-- Visual editor for the OPML navigation menu (meta/menu.opml). +-- Drag-and-drop reordering of menu items. +-- Item types: page, submenu, category_archive, home. +-- Add/remove items, edit labels and targets. +-- See menu.allium for data model. + +-- ─── Metadata diff view ────────────────────────────────────── + +-- Shows DB vs filesystem differences for all entity types. +-- Each diff entry: entity type, field name, DB value, file value. +-- Actions: apply file value to DB, or apply DB value to file. +-- See metadata_diff.allium for diff field definitions. + +-- ─── Documentation views ───────────────────────────────────── + +-- documentation: renders DOCUMENTATION.md as styled HTML +-- api_documentation: renders API.md as styled HTML + +-- ─── Validation views ──────────────────────────────────────── + +-- site_validation: checks generated site for broken links, missing assets +-- translation_validation: checks translation completeness across languages + +-- ─── Find duplicates view ──────────────────────────────────── + +-- Uses embedding vectors to find semantically similar posts. +-- Shows candidate pairs with similarity score. +-- See embedding.allium for vector details. + +-- ─── Script editor view ────────────────────────────────────── + +-- Code editor for Lua scripts (macros, utilities, transforms). +-- Header: script title, kind badge, enabled toggle. +-- Editor: code editor with Lua syntax highlighting. +-- Toolbar: Run button, entrypoint selector. +-- See script.allium for entity model. + +-- ─── Template editor view ───────────────────────────────────── + +-- Code editor for Liquid templates (post, list, not_found, partial). +-- Header: template title, kind badge, enabled toggle, version. +-- Editor: code editor with Liquid syntax highlighting. +-- Preview: rendered template output in iframe. +-- See template.allium for entity model and Liquid subset. diff --git a/specs/sidebar_views.allium b/specs/sidebar_views.allium index b507f12..7765c5b 100644 --- a/specs/sidebar_views.allium +++ b/specs/sidebar_views.allium @@ -33,13 +33,20 @@ config { -- SidebarEntityList: reusable pattern for scripts, templates, chat, import. -- Shows: header with title + create button, scrollable item list, -- empty state with message + action CTA. +-- All titles and names are CSS-truncated with ellipsis when they overflow +-- the sidebar width. Hard JS truncation limits are noted per view. --- Date formatting for sidebar items: --- Today -> time only (e.g. "2:30 PM") --- Yesterday -> i18n "Yesterday" --- <7 days -> short weekday (e.g. "Mon") --- Older -> "Mon DD" format +-- Relative date formatting for entity list items (chat, scripts, templates, import): +-- Same day (diffDays=0) -> time only via toLocaleTimeString (e.g. "2:30 PM") +-- Yesterday (diffDays=1) -> i18n key sidebar.chat.yesterday +-- <7 days -> short weekday via toLocaleDateString weekday:short (e.g. "Mon") +-- >=7 days -> short month + day via toLocaleDateString month:short day:numeric (e.g. "Feb 10") -- Locale map: en->en-US, de->de-DE, fr->fr-FR, it->it-IT, es->es-ES +-- Fallback locale: en-US + +-- Post/page date formatting (different from relative dates above): +-- toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' }) +-- Example: "Feb 10, 2026" -- ─── 1. Posts view ──────────────────────────────────────────── @@ -64,12 +71,18 @@ value PostsView { value PostListItem { post_id: String type_icon: String -- derived from categories (below) - title: String -- fallback "Untitled" + title: String -- post.title, fallback "Untitled"; CSS ellipsis on overflow language_count: Integer? -- badge shown when availableLanguages.count > 1 - date: String -- drafts/archived: updatedAt; published: publishedAt ?? updatedAt + date: String -- locale-formatted (month day, year) active: Boolean -- highlighted when activeTabId = post.id } +-- Per-item data shown (left to right): +-- [type_icon] [title ...ellipsis] [lang badge] +-- [date] +-- Title is CSS-truncated (white-space:nowrap, text-overflow:ellipsis). +-- Date source: drafts/archived use updatedAt; published use publishedAt ?? updatedAt. + -- Post type icon derivation from categories (first match wins, case-insensitive): -- picture/photo/image -> camera icon -- aside/note/quick -> notepad icon @@ -106,13 +119,24 @@ value MediaView { value MediaGridItem { media_id: String - has_thumbnail: Boolean -- image: custom protocol bds-thumb://{id}; non-image: file icon - name: String -- title truncated 60 chars, fallback originalName + has_thumbnail: Boolean -- image: bds-thumb://{id} protocol; non-image: file icon SVG + name: String -- title truncated to 60 chars + "..." if over; fallback originalName (no truncation) file_size: String -- formatted (B / KB / MB) tooltip: String -- caption ?? originalName active: Boolean -- highlighted when activeTabId = media.id } +-- Per-item data shown in grid cell: +-- [thumbnail or file icon] +-- [name ...CSS ellipsis] +-- [file_size] +-- Name: JS hard limit of 60 chars on title (substring + "..."), originalName not truncated. +-- CSS also applies text-overflow:ellipsis as a safety net. + +config { + media_title_max_length: Integer = 60 +} + rule MediaListClick { when: MediaGridItemClicked(media_id, click_type) if click_type = single: @@ -133,11 +157,15 @@ value ScriptsView { value ScriptListItem { script_id: String - title: String - date: String -- relative date format + title: String -- CSS ellipsis on overflow + date: String -- relative date format (see shared patterns) active: Boolean } +-- Per-item data shown: +-- [title ...CSS ellipsis] [delete x] +-- [relative date] + rule ScriptListClick { when: ScriptListItemClicked(script_id, click_type) if click_type = single: @@ -161,11 +189,13 @@ value TemplatesView { value TemplateListItem { template_id: String - title: String - date: String + title: String -- CSS ellipsis on overflow + date: String -- relative date format active: Boolean } +-- Per-item data shown: same layout as ScriptListItem. + rule TemplateListClick { when: TemplateListItemClicked(template_id, click_type) if click_type = single: @@ -238,10 +268,14 @@ value ChatView { value ChatListItem { conversation_id: String - title: String -- live-updated via onTitleUpdated events + title: String -- live-updated via onTitleUpdated; CSS ellipsis on overflow date: String -- relative date format } +-- Per-item data shown: +-- [title ...CSS ellipsis] [delete x] +-- [relative date] + rule ChatListClick { when: ChatListItemClicked(conversation_id) ensures: OpenTabRequested(type: chat, id: conversation_id, intent: pin) @@ -260,8 +294,8 @@ value ImportView { value ImportListItem { definition_id: String - name: String -- live-updated via onNameUpdated events - date: String + name: String -- live-updated via onNameUpdated; CSS ellipsis on overflow + date: String -- relative date format } rule ImportListClick { @@ -296,17 +330,19 @@ value GitStatusFile { } value GitHistoryEntry { - short_hash: String - subject: String + short_hash: String -- 7 chars + subject: String -- wraps (word-break), not truncated author: String date: String -- locale-formatted - sync_status: String -- synced, local_only, remote_only + sync_status: String -- synced, local_only, remote_only (coloured dots) } -- Action buttons: fetch, pull, push, prune_lfs. All disabled while any action is loading. -- Changes section: file count, commit message input, commit button, file list. +-- File paths in changes list: CSS ellipsis on overflow. -- Changes list polls every 2 seconds in background. -- Remote state refreshes every 30 seconds with auto-fetch. +-- History entries: subject + shortHash + author + date; sync status with coloured legend dots. rule GitFileClick { when: GitStatusFileClicked(file_path, click_type) diff --git a/specs/tabs.allium b/specs/tabs.allium index 2d4fc1a..c917302 100644 --- a/specs/tabs.allium +++ b/specs/tabs.allium @@ -146,24 +146,35 @@ rule ResolveEditorRoute { -- Horizontal strip with overflow scroll (left/right arrow buttons, 150px per click). -- Auto-scrolls to bring active tab into view (10px padding). +config { + tab_min_width: Integer = 100 + tab_max_width: Integer = 160 + tab_scroll_step: Integer = 150 + chat_title_max_length: Integer = 18 + git_hash_display_length: Integer = 7 +} + value TabBarItem { - title: String + title: String -- resolved per type (see below); CSS ellipsis at tab_max_width is_active: Boolean - is_transient: Boolean -- italic title when true - is_dirty: Boolean -- dot indicator, only for post tabs + is_transient: Boolean -- italic title when true + is_dirty: Boolean -- dot indicator, only for post tabs } -- Tab title resolution: --- post: post.title from DB (listens post-updated events) --- media: media.originalName --- scripts: script.title from DB (listens scripts-changed) --- templates: template.title from DB (listens templates-changed) --- chat: conversation.title, truncated to 18 chars --- import: definition.name (listens name-updated) --- git_diff file: filename from path (last path segment) --- git_diff commit: "{shortHash} {subject}" from git history --- singletons: i18n key lookup (common.settings, tabBar.style, etc.) --- fallback: i18n:tabBar.unknown +-- post: post.title from DB (listens post-updated events); no JS truncation +-- media: media.originalName; no JS truncation +-- scripts: script.title from DB (listens scripts-changed); no JS truncation +-- templates: template.title from DB (listens templates-changed); no JS truncation +-- chat: conversation.title, JS-truncated to 18 chars + "..." if over limit +-- import: definition.name (listens name-updated); no JS truncation +-- git_diff file: filename only (last path segment); no JS truncation +-- git_diff commit: "{shortHash} {subject}" (shortHash = 7 chars); fallback: 7-char hash only +-- singletons: i18n key lookup (common.settings, tabBar.style, etc.) +-- fallback: i18n:tabBar.unknown +-- +-- All tab titles are additionally CSS-truncated (text-overflow:ellipsis, white-space:nowrap) +-- within the tab's max-width of 160px. -- ─── Tab interactions ─────────────────────────────────────────