diff --git a/specs/editors.allium b/specs/editors.allium index 6efce6b..7c85a2f 100644 --- a/specs/editors.allium +++ b/specs/editors.allium @@ -2,7 +2,9 @@ -- 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 +-- 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). @@ -180,6 +182,161 @@ invariant PostDirtyTracking { -- 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 + -- Confirm applies only accepted fields; Cancel discards all +} + +value AISuggestionField { + label: String + current_value: String + suggested_value: String + accepted: Boolean -- checkbox, default true +} + +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. @@ -244,6 +401,88 @@ value LinkedPostItem { -- 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. @@ -298,6 +537,44 @@ value LinkedPostItem { -- 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. @@ -313,6 +590,24 @@ value LinkedPostItem { -- 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: @@ -326,6 +621,33 @@ value LinkedPostItem { -- 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. @@ -360,28 +682,97 @@ config { 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). --- 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. +-- 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. --- 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. +-- 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 @@ -392,24 +783,135 @@ config { -- 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). --- 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. +-- 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). --- 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. + +-- 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) +} diff --git a/specs/flows.allium b/specs/flows.allium index 4a1a28a..a840769 100644 --- a/specs/flows.allium +++ b/specs/flows.allium @@ -453,3 +453,135 @@ rule DeleteMediaTranslationSideEffects { -- updateMetadata | no | no | no | no | no | no | *.json -- addCategory | no | no | no | no | no | no | *.json -- * updatePost rewrites file only when templateSlug changes on published post + +-- ═══════════════════════════════════════════════════════════════ +-- PART 4: ACTION PATTERNS AND CHAINS +-- ═══════════════════════════════════════════════════════════════ + +-- Cross-cutting patterns for AI operations, auto-translation, +-- drag-and-drop chains, confirmation dialogs, and offline gating. + +-- ─── 4.1 AI operation gating ─────────────────────────────── + +invariant AIOperationGating { + -- All AI operations check airplane/offline mode before executing. + -- If offline mode enabled: + -- If local provider configured (Ollama/LM Studio): route to local model + -- Else: show toast "AI unavailable in offline mode", abort operation + -- If online: use configured provider endpoint + -- Two model categories: + -- Title model: text analysis (suggestions, translation, language detect) + -- Image model: vision-capable (image analysis only) + -- Model selection: per-operation default from settings, + -- overrideable per-conversation in chat panel only +} + +-- ─── 4.2 AI suggestions pattern ──────────────────────────── + +-- Shared flow used by PostAIAnalysis and MediaAIImageAnalysis. +-- See editors.allium AISuggestionsModal value type. + +rule AISuggestionFlow { + when: AISuggestionRequested(entity_type, entity_id) + -- 1. Check AIOperationGating (abort if offline and no local model) + -- 2. Send request to appropriate model: + -- Posts: title model, input = title + excerpt + first 2000 chars + -- Media: image model, input = 448x448 AI thumbnail JPEG + -- 3. Parse response into per-field suggestions + -- 4. Open AISuggestionsModal: + -- Each field: label, current value, suggested value, accept checkbox + -- Accept checkboxes default to true + -- Special case: post slug locked (no checkbox) if ever published + -- 5. On Confirm: apply only accepted fields to entity + -- Posts: triggers auto-save (3s timer reset) + -- Media: triggers explicit save + -- 6. On Cancel: discard all suggestions, no changes +} + +-- ─── 4.3 Auto-translation chain ──────────────────────────── + +rule AutoTranslationChain { + when: PostSaved(post_id) + -- Gate: AIOperationGating + post.doNotTranslate must be false + -- Triggered after any post save (auto-save, manual Ctrl+S, or unmount) + -- For each configured blogLanguage missing a translation for this post: + -- 1. Enqueue background task: translate metadata (title, excerpt) + -- via title model + -- 2. Enqueue background task: translate content (full markdown) + -- via title model + -- 3. Create/update translation record in DB + -- Tasks: sequential per language, parallel across languages + -- Progress visible in Tasks panel +} + +rule MediaMetadataTranslationCascade { + when: PostAutoTranslateCompleted(post_id, language) + -- After a post translation completes for a given language: + -- For each media item linked to this post: + -- If media has source language set + -- and no translation exists for {language}: + -- Enqueue background task: translate media metadata + -- (title, alt, caption) via title model + -- Creates translated sidecar file: {path}.{lang}.meta +} + +-- ─── 4.4 Drag-and-drop image chain ───────────────────────── + +-- Full chain when image file is dropped on post editor body. +-- See editors.allium PostDragDropImage for trigger rule. + +-- Synchronous steps (user waits): +-- 1. importMedia(file) -> new media record + file copy + base sidecar +-- 2. generateThumbnails(media) -> async start (small/medium/large/ai) +-- 3. linkMediaToPost(media, post) -> update sidecar linkedPostIds +-- 4. insertMarkdownImage(cursor) -> insert ![](bds-media://id) at cursor + +-- Background steps (non-blocking, results auto-applied): +-- 5. If AI available: aiImageAnalysis(media) +-- Uses image model on 448x448 AI thumbnail +-- Results auto-applied to media metadata (NO modal for drag-drop, +-- unlike manual Quick Action which shows AISuggestionsModal) +-- Triggers sidecar rewrite +-- 6. If auto-translate enabled (post.doNotTranslate=false): +-- For each blogLanguage: translateMediaMetadata(media, lang) +-- Creates translated sidecar files + +-- ─── 4.5 Confirmation dialog patterns ────────────────────── + +-- Four distinct patterns used across the application: + +-- Pattern 1: Native confirm dialog (rfd crate) +-- Simple yes/no system dialog, no custom UI +-- Used by: PostDelete, PostDiscard, TemplateDelete (when references exist) + +-- Pattern 2: ConfirmDeleteModal (custom modal with reference info) +-- Shows entity name, reference counts, linked entity list +-- Two buttons: Cancel, Delete (destructive red style) +-- Used by: MediaDelete (shows linked posts), TagDelete (shows post count) + +-- Pattern 3: ConfirmDialog (custom modal for non-delete confirmations) +-- Shows description of action and consequences +-- Two buttons: Cancel, Confirm +-- Used by: TagMerge ("Merge N tags into {target}? Cannot be undone.") + +-- Pattern 4: No confirmation (immediate execution) +-- Action executes on click, no dialog +-- Used by: all Rebuild operations, ScriptDelete, MenuItemDelete, +-- MetadataDiff per-field sync, ImportExecute, MediaTranslationDelete, +-- MediaUnlink + +-- ─── 4.6 Keyboard shortcut map ───────────────────────────── + +-- All shortcuts use Cmd on macOS, Ctrl on other platforms. + +-- Global: +-- Ctrl/Cmd+B: toggle sidebar visibility +-- Ctrl/Cmd+W: close active tab + +-- Post editor: +-- Ctrl/Cmd+S: save post immediately (resets auto-save timer) +-- Ctrl/Cmd+K: open InsertModal (insert post link) + +-- Sidebar lists: +-- Enter: open selected item as pinned tab +-- Space: open selected item as preview/transient tab