chore: added editor specs

This commit is contained in:
2026-04-05 09:22:42 +02:00
parent 041eeb3a4f
commit 579f1cd068
4 changed files with 495 additions and 32 deletions

View File

@@ -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

415
specs/editors.allium Normal file
View File

@@ -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<DashboardRecentPost>
}
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<DashboardTimelineMonth>
}
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<DashboardTag>
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<DashboardCategory>
}
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<MediaTranslationItem>
linked_posts: List<LinkedPostItem>
}
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.

View File

@@ -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)

View File

@@ -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 ─────────────────────────────────────────