Files
RuDS/specs/editors.allium

1143 lines
47 KiB
Plaintext

-- 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,
-- ScriptEditor.tsx, TemplateEditor.tsx, MenuEditorView.tsx,
-- MetadataDiffPanel.tsx, ImportAnalysisView.tsx
-- 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
-- ─── External surfaces ──────────────────────────────────────
-- The user is the external actor who initiates all editor interactions
-- via clicks, keyboard shortcuts, and drag-and-drop. This surface
-- provides the triggers that editor action rules listen for.
surface User {
provides: PostAIAnalysisRequested(post_id)
provides: PostTranslateRequested(post_id, target_language)
provides: PostSaved(post_id)
provides: PostPublishRequested(post_id)
provides: PostDiscardRequested(post_id)
provides: PostDeleteRequested(post_id)
provides: PostInsertLinkRequested(post_id)
provides: PostInsertMediaRequested(post_id)
provides: PostGalleryRequested(post_id)
provides: ImageDroppedOnEditor(post_id, file_path)
provides: PostLanguageDetectRequested(post_id)
provides: MediaAIImageAnalysisRequested(media_id)
provides: MediaDetectLanguageRequested(media_id)
provides: MediaTranslateMetadataRequested(media_id, target_language)
provides: MediaReplaceFileRequested(media_id)
provides: MediaDeleteRequested(media_id)
provides: MediaLinkToPostRequested(media_id)
provides: MediaTranslationEditClicked(media_id, language)
provides: MediaTranslationRefreshClicked(media_id, language)
provides: MediaTranslationDeleteClicked(media_id, language)
provides: StyleThemeSelected(theme_name)
provides: StyleApplyRequested(theme_name)
provides: ScriptSaveRequested(script_id)
provides: ScriptRunRequested(script_id)
provides: ScriptDeleteRequested(script_id)
provides: TemplateSaveRequested(template_id)
provides: TemplateDeleteRequested(template_id)
provides: ImportAnalyzeRequested(definition_id, file_path)
provides: ImportExecuteRequested(definition_id)
}
-- ─── 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
}
-- ─── Post editor actions ────────────────────────────────────
-- Shared modal for presenting AI suggestions with per-field accept/reject.
-- Used by PostAIAnalysis and MediaAIImageAnalysis.
value AISuggestionsModal {
fields: List<AISuggestionField>
-- Layout: title bar ("AI Suggestions"), scrollable field list, button row
-- Each field rendered as a row:
-- Left: checkbox (accept/reject), label
-- Center: current value (read-only, muted), arrow, suggested value (highlighted)
-- Special: slug field checkbox disabled if post was ever published
-- Buttons: Cancel (secondary), Apply Selected (primary)
-- Cancel discards all; Apply writes only accepted fields to entity
}
value AISuggestionField {
label: String
current_value: String
suggested_value: String
accepted: Boolean -- checkbox, default true
locked: Boolean -- if true, checkbox disabled (e.g. published slug)
}
-- ─── Modals and overlays ────────────────────────────────────
-- All modals rendered as centered overlay with backdrop dimming.
-- ESC key or backdrop click closes modal (cancel semantics).
-- Overlays (PostPicker, ColourPicker) are positioned inline near trigger.
value InsertPostLinkModal {
-- Two-tab modal opened by Ctrl/Cmd+K in post editor (markdown mode).
-- Tab 1 - Internal:
-- Search input (debounced 300ms, queries post titles)
-- Results list: post title + status badge (draft/published/archived)
-- If semantic similarity enabled: results ranked by vector similarity
-- Click result: inserts [title](/YYYY/MM/DD/slug) at cursor, closes modal
-- "Create Post" row at bottom of results:
-- Creates new post with search query as title, inserts link to it
-- Tab 2 - External:
-- URL input field (required)
-- Display text input field (optional)
-- Insert button: inserts [text](url) or bare url if no text, closes modal
active_tab: String -- internal | external
search_query: String
results: List<InsertLinkResult>
}
value InsertLinkResult {
post_id: String
title: String
status: String -- draft | published | archived
canonical_url: String -- /YYYY/MM/DD/slug
}
value InsertMediaModal {
-- Grid modal for inserting media references into post content.
-- Search input filtering by media title and original filename.
-- Grid of media items: bds-thumb:// thumbnail (medium 400px), title below.
-- Click item:
-- Images: inserts ![alt](bds-media://id) at cursor
-- Non-images: inserts [originalName](bds-media://id) at cursor
-- Closes modal after insertion.
search_query: String
results: List<InsertMediaResult>
}
value InsertMediaResult {
media_id: String
title: String
original_name: String
is_image: Boolean
thumbnail_url: String? -- bds-thumb:// for images, null for others
}
value LanguagePickerModal {
-- Shown for Translate Post and Translate Media Metadata actions.
-- Lists all configured blogLanguages except source language.
-- Each row: flag emoji, language name, status badge if translation exists.
-- Existing translations show "(draft)" or "(published)" badge.
-- Click selects target language and initiates translation flow.
-- Cancel closes without action.
source_language: String
available_targets: List<LanguageTarget>
}
value LanguageTarget {
code: String
name: String
flag_emoji: String
has_existing_translation: Boolean
existing_status: String? -- draft | published, if translation exists
}
value ConfirmDeleteModal {
-- Custom styled modal for destructive operations with reference info.
-- Used by: MediaDelete (shows linked posts), TagDelete (shows post count).
-- Layout: warning icon, title, entity name, reference section, buttons.
-- Reference section: "This item is referenced by:" + bulleted list.
-- Buttons: Cancel (secondary), Delete (destructive red).
entity_name: String
entity_type: String -- media | tag
reference_count: Integer
reference_list: List<String> -- titles of referencing entities
}
value ConfirmDialog {
-- Custom styled modal for non-delete confirmations.
-- Used by: TagMerge ("Merge N tags into {target}? Cannot be undone.").
-- Layout: title, descriptive message, buttons.
-- Buttons: Cancel (secondary), Confirm (primary).
title: String
message: String
}
-- Native confirm dialogs (via rfd crate) are NOT modelled as values.
-- They are simple yes/no system dialogs with a message string.
-- Used by: PostDelete, PostDiscard, TemplateDelete (with references).
value GalleryOverlay {
-- Full-screen overlay showing all media linked to a post.
-- Opened from Gallery button in post editor toolbar (markdown mode).
-- Image grid: bds-thumb:// thumbnails (medium 400px), 3-4 columns.
-- Click image: opens LightboxView for that image.
-- Close: X button or ESC key.
post_id: String
images: List<GalleryImage>
}
value GalleryImage {
media_id: String
thumbnail_url: String -- bds-thumb://media_id
alt_text: String?
}
value LightboxView {
-- Full-screen image viewer, sub-view of GalleryOverlay.
-- Shows single image at full resolution via bds-media:// protocol.
-- Navigation: left/right arrow buttons, keyboard left/right arrow keys.
-- Close: X button, ESC key, or click outside image area.
-- Header: image title or filename, index counter "3 of 12".
current_index: Integer
total_count: Integer
media_id: String
image_url: String -- bds-media://media_id
alt_text: String?
}
value ColourPickerPopover {
-- Inline popover positioned below tag colour field (tags view manage).
-- Not a modal — does not dim backdrop, no ESC/backdrop close.
-- Closes when clicking outside popover or selecting a colour.
-- Grid of 17 preset colour swatches (4x4 + 1).
-- Below grid: custom hex input field (#RRGGBB).
-- Selection is immediate — no confirm/cancel buttons.
-- Choosing a colour updates the tag form's colour field live.
presets: List<String> -- 17 hex colour values
custom_hex: String?
selected: String?
}
config {
colour_picker_preset_count: Integer = 17
}
value PostPickerOverlay {
-- Inline overlay positioned near "Link to Post" button (media editor).
-- Not a modal — positioned as dropdown below trigger button.
-- Search input filtering posts by title (only unlinked posts shown).
-- Up to 10 results displayed. "and N more" text if total exceeds 10.
-- Click result: links media to selected post, closes overlay.
search_query: String
results: List<PostPickerResult>
overflow_count: Integer? -- shown as "and N more" if > 0
}
value PostPickerResult {
post_id: String
title: String
}
value ModelSelectorDropdown {
-- Dropdown in chat panel header for per-conversation model override.
-- Groups models by provider using section headers.
-- Each entry: model display name.
-- Expandable details per model: context window, max output tokens, pricing.
-- Selected model persisted with conversation record.
-- Changing model mid-conversation applies to subsequent messages only.
groups: List<ModelProviderGroup>
selected_model_id: String?
}
value ModelProviderGroup {
provider_name: String -- e.g. "OpenAI", "Ollama", "LM Studio"
models: List<ModelEntry>
}
value ModelEntry {
model_id: String
display_name: String
context_window: Integer
max_output_tokens: Integer
}
rule PostAIAnalysis {
when: PostAIAnalysisRequested(post_id)
-- Gate: airplane mode check (see flows.allium AIOperationGating)
-- Uses title model (not default chat model)
-- Input: post title + excerpt + content (first 2000 chars)
-- Response: suggested title, excerpt, slug
-- Opens AISuggestionsModal with 3 fields:
-- Each field: current value, suggested value, accept checkbox
-- Slug field locked (no accept checkbox) if post was ever published
-- On confirm: applies only checked fields, triggers auto-save
}
rule PostTranslateAction {
when: PostTranslateRequested(post_id, target_language)
-- Gate: airplane mode check
-- Opens language picker modal:
-- Available target languages from project blogLanguages
-- Existing translations shown with status badge (draft/published)
-- Two sequential AI calls via title model:
-- 1. Translate metadata (title, excerpt) to target language
-- 2. Translate content (full markdown body) to target language
-- Creates/updates translation record in DB
-- If source post is published: transitions source to draft
-- (copies file content back to DB so it can be edited)
}
rule PostAutoTranslateOnSave {
when: PostSaved(post_id)
-- Gate: airplane mode check + auto_translate not disabled (doNotTranslate=false)
-- For each configured blog language missing a translation:
-- Enqueue background translation task (title model)
-- Each task: translate metadata + content, create translation record
-- Cascades to linked media: for each linked media item,
-- translate media metadata for missing languages
-- See flows.allium AutoTranslationChain for full chain
}
rule PostPublishAction {
when: PostPublishRequested(post_id)
-- Implicit save first (awaited) if post is dirty
-- Then calls engine publish (see flows.allium PublishPostSideEffects)
-- Also publishes all translations whose source language is published
-- UI updates: status badge -> published, sidebar section move
}
rule PostDiscardChanges {
when: PostDiscardRequested(post_id)
-- Only available for published posts with pending draft changes
-- Native confirm dialog (rfd): "Discard changes to this post?"
-- On confirm: reads published version from .md file,
-- restores DB to published state (content=null, status=published)
-- Editor reloads with restored content
}
rule PostDeleteAction {
when: PostDeleteRequested(post_id)
-- Native confirm dialog (rfd): "Delete this post?"
-- If published: also deletes .md file and all translation files
-- If never published: only deletes DB record
-- Removes from DB, closes tab, sidebar removes item
-- See flows.allium DeletePostSideEffects
}
rule PostInsertLink {
when: PostInsertLinkRequested(post_id)
-- Keyboard shortcut: Ctrl/Cmd+K
-- Opens InsertModal with two tabs: Internal, External
-- Internal tab:
-- Search input (debounced, queries post titles)
-- Results list: title + status badge (draft/published)
-- If semantic similarity enabled: results ranked by similarity
-- Click inserts markdown link: [title](/YYYY/MM/DD/slug)
-- "Create Post" option at bottom of search results:
-- Creates new post with search query as title
-- Inserts link to newly created post
-- External tab:
-- URL input + optional display text input
-- Inserts: [text](url) or bare url if no display text
}
rule PostInsertMedia {
when: PostInsertMediaRequested(post_id)
-- Opens InsertModal (media search variant)
-- Search input, grid of media items with bds-thumb:// thumbnails
-- Click inserts markdown:
-- Images: ![alt](bds-media://id)
-- Non-images: [originalName](bds-media://id)
}
rule PostGalleryAction {
when: PostGalleryRequested(post_id)
-- Opens gallery overlay showing all media linked to this post
-- Image grid with bds-thumb:// thumbnails
-- Click on image opens lightbox (full-size bds-media:// preview)
-- Lightbox: left/right arrow navigation, close button, ESC to close
}
rule PostDragDropImage {
when: ImageDroppedOnEditor(post_id, file_path)
-- Chain of operations (see flows.allium DragDropImageChain):
-- 1. Import media file -> media record + file copy + sidecar
-- 2. Generate thumbnails (async: small/medium/large/ai)
-- 3. Link media to post (update sidecar linkedPostIds)
-- 4. Insert markdown image at cursor: ![](bds-media://id)
-- 5. If AI available: AI image analysis (async, auto-applied, no modal)
-- 6. If auto-translate enabled: cascade translate media metadata
-- Steps 1-4 synchronous. Steps 5-6 background tasks.
}
rule PostLanguageDetect {
when: PostLanguageDetectRequested(post_id)
-- Gate: airplane mode check
-- Sends content sample to title model
-- Auto-sets post language field (no modal)
-- Triggers auto-save
}
-- Post editor mode switching:
-- Visual mode: rich-text WYSIWYG (TipTap in TS, not yet in Rust)
-- Markdown mode: code editor with markdown-with-macros highlighting
-- Preview mode: iframe with rendered HTML via preview server
-- Mode persists per-session, default from editor settings
-- Language tab switching (translation editing):
-- Flag emoji buttons in metadata bar for canonical + each translation
-- Clicking a flag loads that language's draft content into editor
-- Each flag shows status (draft/published) via CSS class
-- Active flag highlighted
-- LinkedMediaPanel (right column of metadata):
-- Shows media items linked to this post
-- Actions:
-- Import & Link: native file dialog, runs full import + link chain
-- Link Existing: opens media picker modal (search + grid)
-- Unlink: removes media-post link from sidecar (no confirmation)
-- Reorder: drag handle on each item
-- Click: opens media item in its own tab (preview intent)
-- Import & Link triggers: import -> thumbnail -> link -> optional AI analysis
-- ─── 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).
-- ─── Media editor actions ─────────────────────────────────────
rule MediaAIImageAnalysis {
when: MediaAIImageAnalysisRequested(media_id)
-- Gate: airplane mode check (see flows.allium AIOperationGating)
-- Only available for image/* MIME types (button hidden for non-images)
-- Uses image analysis model (vision-capable, not title model)
-- Input: AI-optimized JPEG thumbnail (448x448, generated on import)
-- Response: suggested title, alt text, caption
-- Opens AISuggestionsModal with 3 fields (title, alt, caption)
-- On confirm: applies checked fields, triggers explicit save
}
rule MediaDetectLanguage {
when: MediaDetectLanguageRequested(media_id)
-- Gate: airplane mode check
-- Input: concatenation of title + alt + caption text
-- Response: detected language code
-- Immediately persists to media record (no modal, no confirmation)
-- Triggers sidecar rewrite
}
rule MediaTranslateMetadata {
when: MediaTranslateMetadataRequested(media_id, target_language)
-- Gate: airplane mode check
-- Opens language picker modal (same pattern as post translate)
-- Two-step process:
-- 1. If source language not set: detect it first (auto-persist)
-- 2. Translate title, alt, caption to target language via title model
-- Creates/updates media translation record
-- Writes translated sidecar file: {path}.{lang}.meta
}
rule MediaReplaceFile {
when: MediaReplaceFileRequested(media_id)
-- Opens native file dialog (no MIME type filter)
-- Copies selected file over existing media file path
-- If image: regenerates thumbnails synchronously (awaited)
-- Preview area updates with cache-busting timestamp query param
}
rule MediaDeleteAction {
when: MediaDeleteRequested(media_id)
-- Opens ConfirmDeleteModal (custom modal, not native dialog)
-- Shows: media display name, linked posts count and list
-- Two buttons: Cancel, Delete (destructive red style)
-- On confirm: deletes file, sidecar, thumbnails, all translations,
-- post-media links, FTS index entry
-- Closes media tab, sidebar removes item
-- See flows.allium DeleteMediaSideEffects
}
rule MediaLinkToPost {
when: MediaLinkToPostRequested(media_id)
-- Opens inline post picker overlay (not a modal, positioned near button)
-- Search input filtering unlinked posts by title
-- Up to 10 results shown, "and N more" text if results exceed 10
-- Click links media to selected post (updates sidecar linkedPostIds)
-- Linked posts list refreshes immediately
}
rule MediaTranslationEdit {
when: MediaTranslationEditClicked(media_id, language)
-- Loads translation fields inline (title, alt, caption) for that language
-- Edit in place, save persists to translated sidecar {path}.{lang}.meta
}
rule MediaTranslationRefresh {
when: MediaTranslationRefreshClicked(media_id, language)
-- Gate: airplane mode check
-- Re-translates from source language to target via title model
-- Overwrites existing translation fields
-- Rewrites translated sidecar file
}
rule MediaTranslationDelete {
when: MediaTranslationDeleteClicked(media_id, language)
-- Deletes translation record from DB
-- Deletes translated sidecar file: {path}.{lang}.meta
-- No confirmation dialog
}
-- ─── 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
-- ─── Settings view actions ──────────────────────────────────
-- API key validation:
-- On Save: test API call to provider endpoint before persisting
-- On validation failure: toast error message, key not saved
-- On success: key saved, masked display shown
-- Local provider enable/disable:
-- Enabling Ollama/LM Studio: fetches model list, shows capabilities table
-- Capabilities table columns: model name, chat, title, image (checkmarks)
-- Models auto-fetched on enable; manual refresh via Refresh Models button
-- Model catalog refresh:
-- Refresh button next to Default Model selector
-- Fetches available models from all enabled providers
-- Updates optgroup dropdown with provider groupings
-- Per-model info: max output tokens, context window, pricing
-- Rebuild operations (Data Maintenance section):
-- 6 buttons, each executes immediately (no confirmation dialog)
-- Runs as background task with progress visible in Tasks panel
-- On completion: wholesale replaces store data for that entity type
-- Sidebar and editors re-render with fresh data
-- Category management (Content Settings):
-- Protected categories: article, aside, page, picture (Remove button disabled)
-- Add Category: text input + Add button, validates non-empty and unique
-- Per-category settings: editable inline in table row
-- "Reset to Defaults" restores default categories set
-- MCP per-agent config:
-- Add/Remove toggle per agent row
-- Status badge: shows port number when running, "Not running" otherwise
-- Bookmarklet (Project Settings):
-- Copy button copies bookmarklet JavaScript to clipboard
-- Bookmarklet uses project's publicUrl to construct POST endpoint
-- ─── 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.
-- ─── Style view actions ─────────────────────────────────────
rule StyleThemeSelect {
when: StyleThemeSelected(theme_name)
-- Updates iframe preview immediately (query param change)
-- Does NOT persist until Apply is clicked
}
rule StyleApplyTheme {
when: StyleApplyRequested(theme_name)
-- Persists picoTheme to project metadata (meta/project.json)
-- See flows.allium UpdateProjectMetadataSideEffects
}
-- Preview mode dropdown (Auto/Light/Dark):
-- Controls iframe query param, live update on change
-- Does not persist — local UI state only
-- ─── 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.
-- ─── Tags view actions ──────────────────────────────────────
-- Cloud section:
-- Tags sized by post count (same 11-22px scaling as dashboard)
-- Tags with colours get coloured background
-- Click selects tag: switches to Manage section, loads tag data
-- Manage section:
-- Create new: empty form, Save creates tag + writes tags.json
-- Edit existing: loads tag data into form, Save updates
-- Tag name input, colour picker, template select (optional)
-- Colour picker: 17 preset colour swatches + custom hex input field
-- Delete: opens ConfirmDialog showing post count
-- "This tag is used in N posts. Delete anyway?"
-- On confirm: background task removes tag from all posts,
-- rewrites published .md files, deletes tag, writes tags.json
-- Merge section:
-- Source tag multi-select, target tag single-select
-- Merge button: opens ConfirmDialog
-- "Merge N tags into {target}? This cannot be undone."
-- On confirm: background task updates all affected posts,
-- rewrites published .md files, deletes source tags, writes tags.json
-- Sync (maintenance action):
-- Rewrites tags.json from current DB state
-- ─── 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
}
-- ─── Chat panel actions ─────────────────────────────────────
-- Model selector:
-- Dropdown in header, groups models by provider (optgroup)
-- Selection is per-conversation override, persisted with conversation
-- Changing model mid-conversation applies to subsequent messages only
-- Streaming behaviour:
-- Messages accumulate in real-time (markdown rendered incrementally)
-- Tool call markers: checkmark icon (done) or dot (in-progress),
-- tool name, args truncated to 30 chars for string values
-- Thinking animation (three dots) during LLM processing
-- Stop/Abort:
-- Square stop icon button, visible only during active streaming
-- Stops current generation, partial message preserved as-is
-- Assistant action dispatch:
-- Assistant tool calls can trigger navigation actions:
-- open_post(id), open_media(id), open_settings(), etc.
-- Actions dispatched through store, same as user clicks
-- Navigation actions open tabs with pin intent
-- ─── 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).
-- No actions beyond viewing — changes are managed via git sidebar.
-- ─── Menu editor view ────────────────────────────────────────
-- Visual editor for the OPML navigation menu (meta/menu.opml).
-- See menu.allium for data model.
-- Layout: toolbar at top, tree view of menu items below.
-- Each item row: drag handle, icon, label, type badge, action buttons.
-- Nested items indented to show hierarchy.
-- ─── Menu editor actions ────────────────────────────────────
-- Add item (toolbar buttons):
-- Page entry: opens lazy-loaded page picker (posts with "page" category)
-- Category archive entry: opens lazy-loaded category picker
-- Submenu: creates empty container node for nesting children
-- Home entry: always available, maximum one allowed
-- Save: serializes tree to OPML 2.0, writes meta/menu.opml
-- Move operations (per-item buttons):
-- Up/Down: reorder within same nesting level
-- Indent: nest under previous sibling (becomes child)
-- Unindent: move to parent's level (becomes next sibling of parent)
-- Home item: protected — cannot be moved or deleted
-- Drag-and-drop reorder:
-- Drag handle on each item
-- Auto-expand collapsed submenus on hover (450ms delay)
-- Drop indicators show target position and nesting level
-- Delete: removes item and all children, no confirmation dialog
-- Exception: home item cannot be deleted (button hidden)
-- ─── Metadata diff view ──────────────────────────────────────
-- Shows DB vs filesystem differences for all entity types.
-- See metadata_diff.allium for diff field definitions.
-- Layout: scan button at top, results grouped by entity type below.
-- Each diff entry: entity name, field name, DB value, file value,
-- two sync buttons (DB->File, File->DB).
-- ─── Metadata diff actions ──────────────────────────────────
-- Scan: runs 4 parallel scans (posts, media, scripts, templates)
-- Compares DB records against filesystem files field by field
-- Results grouped by entity type, sorted by entity name
-- Shows count of differences per entity type in section header
-- Per-field sync:
-- DB -> File button: overwrites file field with DB value
-- File -> DB button: overwrites DB field with file value
-- No confirmation dialog for individual field syncs
-- Button disappears after successful sync (field now matches)
-- Orphan file import:
-- Files on disk with no matching DB record shown separately
-- "Import" action creates DB record from file metadata
-- Appears at bottom of each entity type section
-- ─── 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
-- ─── Validation view actions ────────────────────────────────
-- Site validation:
-- Scans generated HTML for broken internal links, missing assets
-- Results: list of issues with file path + line + description
-- No auto-fix actions — informational only
-- Translation validation:
-- Checks translation completeness across configured blog languages
-- Results: matrix of posts x languages, missing translations highlighted
-- No auto-fix actions — informational only
-- ─── Find duplicates view ────────────────────────────────────
-- Uses embedding vectors to find semantically similar posts.
-- Shows candidate pairs with similarity score.
-- See embedding.allium for vector details.
-- ─── Find duplicates actions ────────────────────────────────
-- Uses HNSW index of post embedding vectors for similarity search
-- Shows candidate pairs: post A title, post B title, score (0.0-1.0)
-- Click on either post title opens it in a tab (preview intent)
-- No auto-merge or auto-delete actions
-- ─── Script editor view ──────────────────────────────────────
-- Code editor for Lua scripts (macros, utilities, transforms).
-- See script.allium for entity model.
-- Layout: header bar, code editor, no preview.
-- Header: script title (editable), kind badge, enabled toggle.
-- Editor: code editor with Lua syntax highlighting.
-- Toolbar: Run button, Save button, Delete button, entrypoint selector.
-- ─── Script editor actions ──────────────────────────────────
rule ScriptSave {
when: ScriptSaveRequested(script_id)
-- Lua syntax check first (parse validation)
-- If syntax error: blocks save, shows error inline in editor gutter
-- If valid: bumps version, saves to DB + rewrites .lua file
-- See flows.allium for UpdateScriptSideEffects
}
rule ScriptRun {
when: ScriptRunRequested(script_id)
-- Executes script in Lua runtime
-- Calls configured entrypoint function (default: render)
-- stdout/stderr directed to Output panel tab
-- Output panel auto-opens if not already visible
-- Errors shown in Output panel with line numbers
}
rule ScriptDelete {
when: ScriptDeleteRequested(script_id)
-- No confirmation dialog
-- Deletes DB record + .lua file on disk
-- Closes script tab, sidebar removes item
}
-- ─── Template editor view ─────────────────────────────────────
-- Code editor for Liquid templates (post, list, not_found, partial).
-- See template.allium for entity model and Liquid subset.
-- Layout: header bar, code editor, preview pane.
-- Header: template title (editable), kind badge, enabled toggle, version display.
-- Editor: code editor with Liquid syntax highlighting.
-- Preview: rendered template output in iframe (uses sample post data).
-- Toolbar: Save button, Delete button.
-- ─── Template editor actions ────────────────────────────────
rule TemplateSave {
when: TemplateSaveRequested(template_id)
-- Liquid validation first (parse check for syntax errors)
-- If invalid: blocks save, shows error inline in editor
-- If valid: bumps version, saves to DB + rewrites .liquid file
-- See flows.allium for UpdateTemplateSideEffects
}
rule TemplateDelete {
when: TemplateDeleteRequested(template_id)
-- Checks for references: posts using this template, tags with postTemplateSlug
-- If references exist: native confirm dialog (rfd)
-- "This template is used by N posts and M tags. Force delete?"
-- Force delete: nulls templateSlug on referencing posts,
-- nulls postTemplateSlug on referencing tags
-- If no references: deletes without confirmation
-- Deletes DB record + .liquid file on disk
-- Closes template tab, sidebar removes item
}
-- ─── Import analysis view ───────────────────────────────────
-- Editor for WXR (WordPress eXtended RSS) import definitions.
-- Keyed by import definition ID. Opened as always-pinned tab.
-- Workflow: Select File -> Analyze -> Resolve Conflicts -> Execute
-- Layout: header (definition name), file selector, analysis results,
-- conflict resolution panel, taxonomy mapping, execute button.
-- ─── Import analysis actions ────────────────────────────────
rule ImportSelectAndAnalyze {
when: ImportAnalyzeRequested(definition_id, file_path)
-- Parses WXR XML file
-- Extracts: posts, pages, media, tags, categories, authors
-- Shows summary counts per entity type
-- Identifies conflicts: duplicate slugs, existing categories/tags
}
-- Conflict resolution:
-- Per-item dropdown: Import (create new), Skip (ignore), Merge (combine)
-- Default: Import for new items, Skip for existing matches
-- Taxonomy mapping:
-- Source categories/tags mapped to existing project categories/tags
-- Dropdown per source taxonomy term -> target term or "Create New"
-- AI taxonomy analysis (optional):
-- Gate: airplane mode check
-- Suggests mappings based on name similarity via title model
rule ImportExecute {
when: ImportExecuteRequested(definition_id)
-- No confirmation dialog — executes immediately
-- Processes items per conflict resolution settings
-- Creates posts, media, tags, categories as needed
-- Summary with counts shown in import view on completion
-- Created entities appear in sidebar immediately (store updated)
}