diff --git a/specs/editor_chat.allium b/specs/editor_chat.allium index c20b7f8..a92d9a9 100644 --- a/specs/editor_chat.allium +++ b/specs/editor_chat.allium @@ -5,55 +5,40 @@ -- Describes the layout and behaviour of the chat panel. +use "./i18n.allium" as i18n + -- ─── 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 +value ChatPanelView { + conversation_id: String? + needs_api_key: Boolean + title: String -- conversation title or "New Chat" + selected_model_id: String? + messages: List + is_streaming: Boolean + input_text: String } --- ─── Model selector dropdown ────────────────────────────────── +value ChatMessage { + role: String -- user | assistant | system + content: String -- user: plain text; assistant: GFM markdown + tool_markers: List + is_streaming: Boolean -- true while accumulating +} + +value ToolMarker { + tool_name: String + args_preview: String -- string args truncated to config.chat_tool_args_max_length + is_complete: Boolean -- checkmark when done, dot when in-progress +} 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 selected_model_id: String? } value ModelProviderGroup { - provider_name: String -- e.g. "OpenAI", "Ollama", "LM Studio" + provider_name: String -- e.g. "OpenAI", "Ollama", "LM Studio" models: List } @@ -64,25 +49,82 @@ value ModelEntry { max_output_tokens: Integer } --- ─── Chat panel actions ───────────────────────────────────── +config { + chat_tool_args_max_length: Integer = 30 + chat_input_max_height: Integer = 200 +} --- 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 +surface ChatPanelSurface { + context panel: ChatPanelView --- 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 + exposes: + panel.needs_api_key + panel.title + panel.selected_model_id + panel.is_streaming + panel.input_text + for msg in panel.messages: + msg.role + msg.content + msg.is_streaming + for tm in msg.tool_markers: + tm.tool_name + tm.args_preview + tm.is_complete --- Stop/Abort: --- Square stop icon button, visible only during active streaming --- Stops current generation, partial message preserved as-is + provides: + ChatSendMessage(panel.conversation_id, panel.input_text) + when panel.input_text != "" and not panel.is_streaming + ChatAbortStreaming(panel.conversation_id) + when panel.is_streaming + ChatSelectModel(panel.conversation_id, model_id) + ChatOpenSettings() + when panel.needs_api_key --- 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 + @guarantee ApiKeyRequiredScreen + -- Shown when needs_api_key is true. + -- Key icon, title, description text, "Open Settings" button. + -- No chat functionality available until API key is set. + + @guarantee HeaderLayout + -- Left: conversation title (or "New Chat"), CSS ellipsis on overflow. + -- Right: model selector button opening dropdown. + + @guarantee ModelSelectorDropdown + -- Dropdown groups models by provider (section headers). + -- Each entry: model display name. + -- Expandable details: context window, max output tokens. + -- Selection is per-conversation override, persisted with conversation. + -- Changing model mid-conversation applies to subsequent messages only. + + @guarantee WelcomeScreen + -- Shown when no messages and not streaming. + -- Robot icon, title, description, 5 tip bullet points. + + @guarantee MessageRendering + -- User messages: plain text. + -- Assistant messages: rendered as GFM Markdown. + -- External images blocked (CSP), shown as links. + -- Tool markers: checkmark (done) or dot (in-progress) icon, + -- tool name, args truncated to config.chat_tool_args_max_length for strings. + -- Streaming: accumulating markdown + tool markers, thinking dots animation. + + @guarantee AutoScroll + -- Message area auto-scrolls to bottom on new messages. + + @guarantee InputArea + -- Abort/Stop button: square stop icon, visible only during streaming. + -- Auto-growing textarea: max config.chat_input_max_height px. + -- Enter sends message. Shift+Enter inserts newline. + -- Send button: up-arrow icon, disabled when input is empty or streaming. + + @guarantee AssistantActionDispatch + -- 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. + + @guarantee TokenTracking + -- Token usage tracked per conversation. + -- Displayed in status bar, not in the chat panel itself. +} diff --git a/specs/editor_media.allium b/specs/editor_media.allium index 889d015..4ef4083 100644 --- a/specs/editor_media.allium +++ b/specs/editor_media.allium @@ -9,73 +9,31 @@ use "./media.allium" as media use "./i18n.allium" as i18n --- ─── External surfaces ────────────────────────────────────── - -surface User { - 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) -} - -- ─── 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? + mime_type: String -- read-only + file_size: String -- formatted, read-only + dimensions: String? -- "W x H" if width/height exist, read-only + title: String? -- editable text input + alt_text: String? -- editable text input + caption: String? -- editable textarea (3 rows) + tags: String? -- comma-separated text input + author: String? -- editable text input + language: String? -- select from supported languages translations: List linked_posts: List } value MediaTranslationItem { language: String + flag_emoji: String title: String? + alt_text: String? + caption: String? } value LinkedPostItem { @@ -83,18 +41,7 @@ value LinkedPostItem { title: String } --- No auto-save; explicit Save button required. --- Replace File opens a native file dialog. --- Delete shows confirmation with references (linked posts). - --- ─── Post picker overlay ──────────────────────────────────── - 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 overflow_count: Integer? -- shown as "and N more" if > 0 @@ -105,6 +52,93 @@ value PostPickerResult { title: String } +config { + media_post_picker_max_results: Integer = 10 +} + +surface MediaEditorSurface { + context editor: MediaEditorView + + exposes: + editor.file_name + editor.mime_type + editor.file_size + editor.dimensions when editor.dimensions != null + editor.is_image + editor.title + editor.alt_text + editor.caption + editor.tags + editor.author + editor.language + for t in editor.translations: + t.language + t.flag_emoji + t.title + for lp in editor.linked_posts: + lp.post_id + lp.title + + provides: + MediaAIImageAnalysisRequested(editor.media_id) + when editor.is_image + MediaDetectLanguageRequested(editor.media_id) + MediaTranslateMetadataRequested(editor.media_id, target_language) + MediaReplaceFileRequested(editor.media_id) + MediaSaveRequested(editor.media_id) + MediaDeleteRequested(editor.media_id) + MediaLinkToPostRequested(editor.media_id) + MediaTranslationEditClicked(editor.media_id, language) + MediaTranslationRefreshClicked(editor.media_id, language) + MediaTranslationDeleteClicked(editor.media_id, language) + MediaUnlinkPostRequested(editor.media_id, post_id) + + @guarantee HeaderLayout + -- Header bar with media display name. + -- Actions (right side): Quick Actions dropdown, Replace File button, + -- Save button, Delete button (danger style). + + @guarantee QuickActionsDropdown + -- Dropdown menu in header with three entries: + -- AI Image Analysis (robot icon) — only shown for image/* MIME types. + -- Detect Language (magnifier icon). + -- Translate Metadata (globe icon). + + @guarantee PreviewArea + -- Images: rendered via bds-media:// protocol with cache-busting timestamp. + -- Non-images: SVG file icon placeholder + original filename text. + + @guarantee MetadataForm + -- Form fields in order: + -- File Name (disabled input), MIME Type (disabled input), + -- Size + Dimensions row (disabled inputs), + -- Title (text input), Alt Text (text input), Caption (textarea, 3 rows), + -- Tags (comma-separated text input), Author (text input), + -- Language (select from supported languages). + + @guarantee TranslationsSection + -- Shown only when language is set. + -- List of existing translations: flag emoji + language name + title. + -- Per-translation actions: click to edit inline, refresh button, delete button. + -- "No translations" message when list is empty. + + @guarantee LinkedPostsSection + -- "Link to Post" button opens inline post picker overlay. + -- List of currently linked posts with document icon. Click navigates to post tab. + -- Per-post unlink button (×). + -- "Not linked to any posts" message when list is empty. + + @guarantee PostPickerOverlay + -- Inline overlay positioned near "Link to Post" button (not a modal). + -- Search input filtering unlinked posts by title. + -- Up to config.media_post_picker_max_results results displayed. + -- "and N more" text when total exceeds limit. + -- Click result: links media to selected post, closes overlay. + + @guarantee NoAutoSave + -- Unlike the post editor, media editor requires explicit Save button. +} + -- ─── Media editor actions ───────────────────────────────────── rule MediaAIImageAnalysis { @@ -161,7 +195,8 @@ 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 + -- Up to config.media_post_picker_max_results results shown + -- "and N more" text if results exceed limit -- Click links media to selected post (updates sidecar linkedPostIds) -- Linked posts list refreshes immediately } diff --git a/specs/editor_misc.allium b/specs/editor_misc.allium index c08657f..e4c1cb3 100644 --- a/specs/editor_misc.allium +++ b/specs/editor_misc.allium @@ -9,29 +9,11 @@ -- warrant their own spec file. use "./tabs.allium" as tabs - --- ─── External surfaces ────────────────────────────────────── - -surface User { - provides: ImportAnalyzeRequested(definition_id, file_path) - provides: ImportExecuteRequested(definition_id) - provides: SiteValidationScanRequested() - provides: SiteValidationApplyRequested(report) - provides: TranslationValidationScanRequested() - provides: TranslationValidationFixRequested(report) - provides: DuplicateSearchRequested() - provides: DuplicatePairDismissed(post_id_a, post_id_b) - provides: DuplicatePairsBatchDismissed(pair_ids) - provides: MenuSaveRequested() - provides: MenuItemAdded(kind, data) - provides: MenuItemDeleted(item_id) - provides: MenuItemMoved(item_id, direction) -} +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 @@ -44,7 +26,6 @@ value Dashboard { } value DashboardStats { - -- Three stat cards side by side: total_posts: Integer published_count: Integer draft_count: Integer @@ -57,10 +38,6 @@ value DashboardStats { } 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 } @@ -71,13 +48,8 @@ value DashboardTimelineMonth { } value DashboardTagCloud { - -- Up to 40 tags, sorted alphabetically - -- Font size scaled 11px-22px based on post count - -- Tags with colours get coloured background with contrast text - -- Hover title shows post count - -- "and N more" text if > 40 tags tags: List - overflow_count: Integer? + overflow_count: Integer? -- "and N more" when > config.dashboard_max_tags } value DashboardTag { @@ -87,8 +59,6 @@ value DashboardTag { } value DashboardCategoryCloud { - -- All categories as badge-like tags - -- Each shows category name + count categories: List } @@ -98,13 +68,10 @@ value DashboardCategory { } 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 + title: String -- "Untitled" as fallback + status: String -- draft | published + date: String -- locale-formatted } config { @@ -115,16 +82,144 @@ config { dashboard_timeline_months: Integer = 12 } +surface DashboardSurface { + context dash: Dashboard + + exposes: + dash.title + dash.subtitle + dash.stats.total_posts + dash.stats.published_count + dash.stats.draft_count + dash.stats.archived_count when dash.stats.archived_count > 0 + dash.stats.media_count + dash.stats.image_count + dash.stats.total_media_size + dash.stats.tag_count + dash.stats.category_count + for m in dash.timeline.months: + m.label + m.year + m.count + for t in dash.tag_cloud.tags: + t.name + t.count + t.color + dash.tag_cloud.overflow_count when dash.tag_cloud.overflow_count != null + for c in dash.category_cloud.categories: + c.name + c.count + for rp in dash.recent_posts: + rp.title + rp.status + rp.date + + provides: + DashboardRecentPostClicked(post_id, single) + DashboardRecentPostClicked(post_id, double) + + @guarantee StatCards + -- Three stat cards side by side. + -- Posts card: total number, breakdown tags (published/drafts/archived if > 0). + -- Media card: count, images count, total size (formatted bytes). + -- Tags card: count, categories count. + + @guarantee TimelineChart + -- Bar chart of posts over last config.dashboard_timeline_months months that have data. + -- Each bar: count label on top, month abbreviation + year below. + -- Bar height proportional to max count. + + @guarantee TagCloud + -- Up to config.dashboard_max_tags tags, sorted alphabetically. + -- Font size scaled config.dashboard_tag_min_font to config.dashboard_tag_max_font px + -- based on post count. + -- Tags with colours get coloured background with contrast text. + -- Hover title shows post count. + -- "and N more" text when overflow_count > 0. + + @guarantee CategoryCloud + -- All categories as badge-like tags. + -- Each shows category name + count. + + @guarantee RecentPosts + -- Last config.dashboard_recent_count posts by updatedAt descending. + -- Each row: title (or "Untitled"), status badge (draft/published), date. + -- Single-click: preview tab. Double-click: pin tab. +} + -- ─── 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. +value MenuEditorView { + items: List +} --- ─── Menu editor actions ──────────────────────────────────── +value MenuTreeItem { + item_id: String + kind: String -- home | page | category_archive | submenu + label: String + children: List + is_home: Boolean -- true for the home item (protected) +} + +surface MenuEditorSurface { + context menu: MenuEditorView + + exposes: + for item in menu.items: + item.kind + item.label + item.children + item.is_home + + provides: + MenuItemAdded(kind, data) + MenuSaveRequested() + MenuItemDeleted(item_id) + when not item.is_home + MenuItemMoved(item_id, direction) + when not item.is_home + + @guarantee HeaderLayout + -- Title + description text. + + @guarantee Toolbar + -- 8 icon buttons with tooltips: + -- Add Entry (+), Save (floppy), Add Category Archive, + -- Move Up, Move Down, Indent, Unindent, Delete. + + @guarantee TreeView + -- Drag-and-drop tree with items showing: + -- Drag handle, kind icon (home/page/category-archive/submenu SVGs), + -- title, selected row highlighting. + -- Nested items indented to show hierarchy. + + @guarantee InlineEditing + -- When creating page item: inline PageInput with search + -- (filters posts in "page" category). + -- When creating category archive: inline CategoryInput with search/create. + -- Escape cancels, selection confirms. + + @guarantee HomeItemProtection + -- Home item cannot be moved, reordered, or deleted. + -- Maximum one home item allowed. + + @guarantee DragDrop + -- Drag handle on each item. + -- Auto-expand collapsed submenus on hover (config.menu_drag_expand_delay ms delay). + -- Drop indicators show target position and nesting level. + + @guarantee MoveDirections + -- 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). +} + +config { + menu_drag_expand_delay: Integer = 450 +} rule MenuAddItem { when: MenuItemAdded(kind, data) @@ -144,11 +239,7 @@ rule MenuSave { rule MenuMoveItem { when: MenuItemMoved(item_id, direction) -- direction = up | down | indent | unindent - -- 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) requires: not is_home_item(item_id) - -- Home item is protected: cannot be moved or deleted ensures: MenuTreeUpdated() } @@ -159,71 +250,187 @@ rule MenuDeleteItem { ensures: MenuTreeUpdated() } --- 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 - -config { - menu_drag_expand_delay: Integer = 450 -} - -- ─── 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). +value MetadataDiffView { + is_scanning: Boolean + active_entity_tab: String -- posts | media | scripts | templates + diff_stats: MetadataDiffStats + field_summaries: List + items: List + orphan_files: List +} --- ─── Metadata diff actions ────────────────────────────────── +value MetadataDiffStats { + total_posts: Integer + published_posts: Integer + draft_posts: Integer + media_files: Integer + scripts: Integer + templates: Integer +} --- 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 +value MetadataDiffFieldSummary { + field_name: String + diff_count: Integer +} --- 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) +value MetadataDiffItem { + entity_name: String + entity_type: String + file_missing: Boolean -- badge shown when file not found + field_diffs: List +} --- 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 +value MetadataDiffField { + field_name: String + db_value: String? + file_value: String? +} + +value MetadataDiffOrphanFile { + file_path: String + entity_type: String +} + +surface MetadataDiffSurface { + context diff: MetadataDiffView + + exposes: + diff.is_scanning + diff.active_entity_tab + diff.diff_stats + for fs in diff.field_summaries: + fs.field_name + fs.diff_count + for item in diff.items: + item.entity_name + item.file_missing + for fd in item.field_diffs: + fd.field_name + fd.db_value + fd.file_value + for orphan in diff.orphan_files: + orphan.file_path + + provides: + MetadataDiffScanRequested() + MetadataDiffSyncFieldToFile(entity_name, field_name) + MetadataDiffSyncFieldToDb(entity_name, field_name) + MetadataDiffSyncAllFieldToFile(field_name) + MetadataDiffSyncAllFieldToDb(field_name) + MetadataDiffImportOrphan(file_path) + + @guarantee HeaderLayout + -- Title + description text. + -- Stats row: 6 stat items (total posts, published, drafts, media, scripts, templates). + + @guarantee ScanAction + -- Scan/Rescan button at top. + -- Progress bar + message during scan. + + @guarantee EntityTabs + -- Tabs: Posts, Media, Scripts, Templates — each with badge count of diffs. + + @guarantee FieldSummaryPills + -- Clickable filter pills per field, each with count. + -- Two bulk sync buttons per pill: DB→File and File→DB (syncs all items for that field). + + @guarantee DiffItemCards + -- Per-item card: header (entity label + file-missing badge), + -- field rows (field name, DB value, file value), + -- two sync buttons per field (DB→File, File→DB). + -- Button disappears after successful sync (field now matches). + + @guarantee OrphanFilesSection + -- Files on disk with no matching DB record shown at bottom of each entity tab. + -- "Import" button creates DB record from file metadata. + + @guarantee NoConfirmation + -- Individual field syncs require no confirmation dialog. +} -- ─── 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. + +value GitDiffView { + diff_id: String + diff_type: String -- file | commit + display_mode: String -- inline | side_by_side (from editor settings) +} + +surface GitDiffSurface { + context diff: GitDiffView + + exposes: + diff.diff_id + diff.diff_type + diff.display_mode + + @guarantee DiffDisplayModes + -- Supports inline and side-by-side diff display modes. + -- Mode comes from editor settings (Diff View Style). + + @guarantee ReadOnly + -- No actions beyond viewing — changes are managed via git sidebar. +} -- ─── Documentation views ───────────────────────────────────── -- documentation: renders DOCUMENTATION.md as styled HTML -- api_documentation: renders API.md as styled HTML --- ─── Site validation view ─────────────────────────────────── +surface DocumentationSurface { + @guarantee MarkdownRendering + -- Renders markdown file as styled HTML. + -- No edit actions. Read-only view. +} --- Compares sitemap.xml to generated HTML files on disk. --- Detects missing pages, orphan HTML, and stale content. +-- ─── Site validation view ─────────────────────────────────── value SiteValidationReport { expected_url_count: Integer existing_html_count: Integer - missing_url_paths: List -- in sitemap, no HTML on disk - extra_url_paths: List -- HTML on disk, not in sitemap - updated_post_url_paths: List -- source .md newer than HTML + missing_url_paths: List -- in sitemap, no HTML on disk + extra_url_paths: List -- HTML on disk, not in sitemap + updated_post_url_paths: List -- source .md newer than HTML } --- Layout: scan button, summary line, three URL list sections. --- Summary: "Expected URLs: N — Existing HTML URLs: N — Missing: N — Extra: N — Updated: N" --- Each section: heading, list of URL paths, or "None found". +surface SiteValidationSurface { + context report: SiteValidationReport + + exposes: + report.expected_url_count + report.existing_html_count + report.missing_url_paths + report.extra_url_paths + report.updated_post_url_paths + + provides: + SiteValidationScanRequested() + SiteValidationApplyRequested(report) + when report.missing_url_paths.count > 0 + or report.extra_url_paths.count > 0 + or report.updated_post_url_paths.count > 0 + + @guarantee SummaryLine + -- "Expected URLs: N — Existing HTML URLs: N — Missing: N — Extra: N — Updated: N" + + @guarantee UrlSections + -- Three sections: Missing URLs, Extra URLs, Updated URLs. + -- Each section: heading + list of URL paths, or "None found". + + @guarantee ApplyAction + -- Apply button disabled when nothing to fix. + -- On apply: renders missing, deletes extra, re-renders updated. + -- Toast: "Validation applied: N rendered, N deleted". +} rule SiteValidationScan { when: SiteValidationScanRequested() @@ -240,15 +447,11 @@ rule SiteValidationApply { -- Deletes extra HTML files, removes empty directories -- Regenerates calendar if anything changed -- Rebuilds search index if anything rendered or deleted - -- Toast: "Validation applied: N rendered, N deleted" ensures: GenerateSiteRequested(sections: report.affected_sections) } -- ─── Translation validation view ─────────────────────────── --- Checks translation integrity across DB rows and filesystem files. --- NOT a matrix view — it is a list of issues found. - value TranslationValidationReport { checked_database_row_count: Integer checked_filesystem_file_count: Integer @@ -271,10 +474,51 @@ value TranslationValidationIssue { file_path: String? } --- Layout: Revalidate + Fix Issues buttons, summary line, two issue sections. --- Summary: "Checked DB rows: N — Checked files: N — Invalid DB rows: N — Invalid files: N" --- Each issue rendered as card with coloured left border. --- Card shows: issue label, source post ID, translation ID, title, languages, file path. +surface TranslationValidationSurface { + context report: TranslationValidationReport + + exposes: + report.checked_database_row_count + report.checked_filesystem_file_count + for issue in report.invalid_database_rows: + issue.issue + issue.translation_id + issue.translation_for + issue.canonical_language + issue.translation_language + issue.title + issue.file_path + for issue in report.invalid_filesystem_files: + issue.issue + issue.file_path + + provides: + TranslationValidationScanRequested() + TranslationValidationFixRequested(report) + when report.invalid_database_rows.count > 0 + or report.invalid_filesystem_files.count > 0 + + @guarantee SummaryLine + -- "Checked DB rows: N — Checked files: N — Invalid DB rows: N — Invalid files: N" + + @guarantee IssueSections + -- Two sections: Database Issues, Filesystem Issues. + -- Each issue rendered as card with coloured left border. + -- Card shows: issue label, source post ID, translation ID, title, languages, file path. + + @guarantee IssueTypes + -- same_language_as_canonical: translation language matches source. + -- do_not_translate_has_translations: source is doNotTranslate. + -- content_in_database: published translation has content in DB. + -- missing_source_post: translationFor references nonexistent post. + + @guarantee FixAction + -- Revalidate button + Fix button (disabled when no issues). + -- Fix: content_in_database -> flush to .md file, set content null. + -- Other issues -> delete DB row or .md file. + -- After fix: automatically re-validates. + -- Toast: "Deleted N DB rows and N files, flushed N translations to disk". +} rule TranslationValidationScan { when: TranslationValidationScanRequested() @@ -290,18 +534,14 @@ rule TranslationValidationFix { -- DB issues: deletes translation row -- Filesystem issues: deletes the .md file -- After fix: automatically re-validates - -- Toast: "Deleted N DB rows and N files, flushed N translations to disk" ensures: TranslationValidationScan } -- ─── Find duplicates view ────────────────────────────────── --- Uses embedding vectors to find semantically similar posts. --- Gate: requires semanticSimilarityEnabled in project metadata. --- If disabled: shows "Semantic similarity is not enabled" message. - value DuplicateSearchResult { pairs: List + has_more: Boolean -- pagination with config.duplicate_page_size per page } value DuplicatePair { @@ -309,23 +549,67 @@ value DuplicatePair { title_a: String post_id_b: String title_b: String - similarity: Decimal -- 0.0 to 1.0 - exact_match: Boolean -- true if titles + content identical + similarity: Decimal -- 0.0 to 1.0 + exact_match: Boolean -- true if titles + content identical } --- Layout: header, count, paginated pair list (500 per page). --- Each pair row: checkbox, post A title (clickable), arrow, post B title (clickable), --- similarity badge ("Exact duplicate" or "N% similar"), Dismiss button. --- Exact matches styled distinctly. Batch controls: Check All, Uncheck All, --- Dismiss Checked (N). Refresh button. +config { + duplicate_similarity_threshold: Decimal = 0.92 + duplicate_page_size: Integer = 500 + duplicate_neighbor_count: Integer = 21 +} + +surface DuplicatesSurface { + context result: DuplicateSearchResult + + exposes: + for pair in result.pairs: + pair.title_a + pair.title_b + pair.similarity + pair.exact_match + result.has_more + + provides: + DuplicateSearchRequested() + DuplicatePairDismissed(post_id_a, post_id_b) + DuplicatePairsBatchDismissed(pair_ids) + DuplicatePostClicked(post_id) + DuplicateShowMoreRequested() + when result.has_more + + @guarantee SemanticSimilarityGate + -- Requires semanticSimilarityEnabled in project metadata. + -- If disabled: shows "Semantic similarity is not enabled" message. + -- No search functionality available when disabled. + + @guarantee ActionsBar + -- Refresh button, Check All, Uncheck All, + -- Dismiss Checked (with count), disabled when none checked. + + @guarantee PairRows + -- Each row: checkbox, post A title (clickable -> opens tab), + -- arrow, post B title (clickable -> opens tab), + -- similarity badge (percentage or "Exact Match"), + -- Dismiss button. + -- Exact matches styled distinctly from similarity matches. + + @guarantee Pagination + -- "Show More" button when has_more is true. + -- config.duplicate_page_size pairs per page. + + @guarantee BatchDismiss + -- Batch insert in chunks of 100. + -- Dismissed pairs excluded from future searches. +} rule DuplicateSearch { when: DuplicateSearchRequested() requires: semantic_similarity_enabled -- Loads USearch vector index for project - -- For each indexed post: search 21 nearest neighbors + -- For each indexed post: search config.duplicate_neighbor_count nearest neighbors -- similarity = max(0, 1 - distance) - -- Filter: similarity >= threshold, exclude dismissed pairs + -- Filter: similarity >= config.duplicate_similarity_threshold, exclude dismissed -- For 100% embedding similarity: load post bodies, compare title+content -- If identical: exact_match = true -- Sort: exact matches first, then descending similarity @@ -336,7 +620,6 @@ rule DuplicateDismiss { when: DuplicatePairDismissed(post_id_a, post_id_b) -- Inserts into dismissed_duplicate_pairs with canonical ID ordering -- Excluded from future searches - -- See schema.allium DismissedDuplicatePair ensures: PairDismissed(post_id_a, post_id_b) } @@ -346,22 +629,139 @@ rule DuplicateBatchDismiss { ensures: for pair in pair_ids: PairDismissed(pair) } -config { - duplicate_similarity_threshold: Decimal = 0.92 - duplicate_page_size: Integer = 500 - duplicate_neighbor_count: Integer = 21 -} - -- ─── 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. +value ImportAnalysisView { + definition_id: String + definition_name: String -- editable name input + uploads_folder_path: String? -- path display + Browse button + wxr_file_path: String? -- path display + Select & Analyze button + is_loading: Boolean + report: ImportAnalysisReport? +} --- ─── Import analysis actions ──────────────────────────────── +value ImportAnalysisReport { + site_info: ImportSiteInfo + post_stats: ImportEntityStats + page_stats: ImportEntityStats + media_stats: ImportMediaStats + category_stats: ImportTaxonomyStats + tag_stats: ImportTaxonomyStats + date_distribution: List + conflicts: List + macros: List +} + +value ImportSiteInfo { + title: String + url: String + language: String + source_file: String +} + +value ImportEntityStats { + new_count: Integer + update_count: Integer + conflict_count: Integer + duplicate_count: Integer +} + +value ImportMediaStats { + new_count: Integer + update_count: Integer + conflict_count: Integer + duplicate_count: Integer + missing_count: Integer +} + +value ImportTaxonomyStats { + existing_count: Integer + mapped_count: Integer + new_count: Integer +} + +value ImportYearDistribution { + year: Integer + post_count: Integer + media_count: Integer +} + +value ImportConflict { + item_type: String -- post | page | media + item_name: String + resolution: String -- import | skip | merge +} + +value ImportMacro { + name: String + usage_count: Integer + parameters: List + validation_status: String -- valid | invalid | unknown +} + +surface ImportAnalysisSurface { + context analysis: ImportAnalysisView + + exposes: + analysis.definition_name + analysis.uploads_folder_path + analysis.wxr_file_path + analysis.is_loading + analysis.report when analysis.report != null + + provides: + ImportAnalyzeRequested(analysis.definition_id, file_path) + when analysis.wxr_file_path != null + ImportExecuteRequested(analysis.definition_id) + when analysis.report != null + ImportConflictResolutionChanged(item_name, resolution) + ImportTaxonomyMappingChanged(source_term, target_term) + ImportAITaxonomyAnalysisRequested(analysis.definition_id) + + @guarantee FileSelectors + -- Uploads folder: path display + Browse button (native folder dialog). + -- WXR file: path display + Select & Analyze button (native file dialog). + + @guarantee LoadingState + -- Spinner + progress step + detail text during analysis. + + @guarantee SiteInfoCard + -- Shows: site title, URL, language, source file path. + + @guarantee StatCards + -- Posts (new/update/conflict/duplicate), Pages (same), + -- Media (new/update/conflict/duplicate/missing), + -- Categories (existing/mapped/new), Tags (existing/mapped/new). + + @guarantee DateDistribution + -- Year-by-year bar charts for posts + media. + + @guarantee ConflictsSection + -- Collapsible. Per-item dropdown: Import/Skip/Merge. + -- Default: Import for new items, Skip for existing matches. + + @guarantee TaxonomySection + -- Collapsible. Category + tag pills. + -- Click pill to map to existing term. Inline edit with suggestion dropdown. + -- AI analyze button (with model selector dropdown). + -- Gate: airplane mode check for AI taxonomy analysis. + + @guarantee MacrosSection + -- Collapsible. Discovered macros with usage counts, parameters, + -- validation status (valid/invalid/unknown badges). + + @guarantee ExecuteAction + -- Execute button shows importable counts (tags, posts, media, pages). + -- Disabled if nothing to import. + -- Progress bar during execution (current/total, phase, detail, ETA). + -- No confirmation dialog — executes immediately. + + @guarantee CreatedEntitiesRefresh + -- Created entities appear in sidebar immediately after execution. +} rule ImportSelectAndAnalyze { when: ImportAnalyzeRequested(definition_id, file_path) @@ -371,17 +771,6 @@ rule ImportSelectAndAnalyze { -- 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 diff --git a/specs/editor_post.allium b/specs/editor_post.allium index 5884e2b..87da771 100644 --- a/specs/editor_post.allium +++ b/specs/editor_post.allium @@ -11,79 +11,16 @@ use "./tabs.allium" as tabs use "./post.allium" as post use "./i18n.allium" as i18n --- ─── External surfaces ────────────────────────────────────── - -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) -} - -- ─── 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 with embedding suggestions), --- 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) --- --- Tag autocomplete: --- Standard: matches existing tags by name prefix (case-insensitive) --- If semanticSimilarityEnabled: also suggests tags from similar posts --- via SuggestTags (see embedding.allium). Finds 10 similar posts, --- collects their tags, weights by similarity, returns top 5. --- Suggestions merged: prefix matches first, then embedding suggestions --- 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 - value PostEditorView { post_id: String header: PostEditorHeader - metadata_expanded: Boolean + metadata: PostEditorMetadata + metadata_expanded: Boolean -- starts expanded when title is empty excerpt_expanded: Boolean editor_mode: String -- visual | markdown | preview + footer: PostEditorFooter } value PostEditorHeader { @@ -93,20 +30,161 @@ value PostEditorHeader { is_auto_saving: Boolean } +value PostEditorMetadata { + title: String -- editable text input + tags: List -- autocomplete chip input + author: String? -- text input + language: String? -- select from supported languages + do_not_translate: Boolean -- checkbox + slug: String -- read-only text input + categories: List -- chip input + template_slug: String? -- select (shown only when templates exist) + post_links: PostLinksPanel + linked_media: List +} + +value PostLinksPanel { + backlinks: List -- posts linking to this post + outlinks: List -- posts this post links to +} + +value PostLinkReference { + post_id: String + title: String +} + +value LinkedMediaItem { + media_id: String + has_thumbnail: Boolean + name: String + sort_order: Integer +} + +value PostEditorFooter { + created_at: String -- locale-formatted date + updated_at: String -- locale-formatted date + published_at: String? -- locale-formatted date, only when post was published +} + +value TranslationFlag { + language: String + flag_emoji: String + status: String -- draft | published + is_active: Boolean -- true when this language is currently being edited +} + +surface PostEditorSurface { + context editor: PostEditorView + + exposes: + editor.header.title + editor.header.is_dirty + editor.header.status + editor.header.is_auto_saving + editor.metadata_expanded + editor.excerpt_expanded + editor.editor_mode + editor.footer.created_at + editor.footer.updated_at + editor.footer.published_at when editor.footer.published_at != null + + provides: + PostAIAnalysisRequested(editor.post_id) + PostTranslateRequested(editor.post_id, target_language) + PostSaved(editor.post_id) + PostPublishRequested(editor.post_id) + when editor.header.status = draft + PostDiscardRequested(editor.post_id) + when editor.header.status = draft + PostDeleteRequested(editor.post_id) + PostInsertLinkRequested(editor.post_id) + when editor.editor_mode = markdown + PostInsertMediaRequested(editor.post_id) + when editor.editor_mode = markdown + PostGalleryRequested(editor.post_id) + ImageDroppedOnEditor(editor.post_id, file_path) + PostLanguageDetectRequested(editor.post_id) + + @guarantee HeaderLayout + -- Header bar with two areas. + -- Left: title text with dirty indicator dot (●) when is_dirty is true. + -- Right: status badge, auto-save indicator (when saving), + -- Quick Actions dropdown, Publish button (only when draft, success style), + -- Discard button (only when draft), Delete button. + + @guarantee QuickActionsDropdown + -- Dropdown menu in header with two entries: + -- AI Analysis (robot icon) — suggests title, excerpt, slug. + -- Translate Post (globe icon) — opens translation modal. + + @guarantee MetadataSection + -- Collapsible section. Starts expanded when title is empty. + -- Two-column layout. + -- Left column: Title, Tags, Author, Language + detect button, + -- Do Not Translate checkbox, Slug (read-only), Categories, + -- Template (select, only when templates exist), PostLinks. + -- Right column: LinkedMediaPanel. + + @guarantee TagAutocomplete + -- Tag input with autocomplete. + -- Standard: prefix match on existing tag names (case-insensitive). + -- When semanticSimilarityEnabled: also suggests tags from 10 similar posts, + -- weighted by similarity, top 5 shown. + -- Merged results: prefix matches first, then embedding suggestions. + + @guarantee TranslationFlagsBar + -- Row of flag emoji buttons inline with metadata toggle. + -- One flag per language: canonical language + each translation. + -- Each flag shows status (draft/published) via CSS class. + -- Active flag highlighted. Click switches editor to that language's draft. + + @guarantee ExcerptSection + -- Collapsible section with textarea (4 rows). + + @guarantee EditorBodyToolbar + -- Toolbar: "Content" label, mode toggle (Visual/Markdown/Preview), + -- action buttons (markdown mode only): Gallery (with media count), + -- Insert Post Link, Insert Media. + + @guarantee EditorModes + -- Visual: rich-text WYSIWYG editor. + -- Markdown: code editor with markdown-with-macros language, + -- highlighting [[macro ...]] syntax. Word wrap on, minimap off, 14px font. + -- Preview: iframe showing rendered preview. + + @guarantee DragDropImages + -- Drop image file onto editor area triggers import chain. + + @guarantee FooterLayout + -- Three date stamps: Created, Updated, Published (only when published_at exists). + -- Locale-formatted dates. + + @guarantee LinkedMediaPanel + -- Right column of metadata showing media items linked to this post. + -- Actions: Import & Link (native file dialog), Link Existing (media picker), + -- Unlink (no confirmation), Reorder (drag handle), Click (opens media tab). +} + config { post_auto_save_delay: Integer = 3000 - -- milliseconds of idle before auto-save triggers + post_content_sample_length: Integer = 2000 } 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 + -- Auto-saves after config.post_auto_save_delay ms of idle in the editor. + -- Also auto-saves on unmount/tab switch. + -- Ctrl/Cmd+S triggers immediate save. + -- Saves: title, content, excerpt, tags, categories, templateSlug, language. } invariant PostDirtyTracking { - -- Compares canonical draft + translation drafts against saved state - -- Dirty indicator shown in header and tab bar + -- Compares canonical draft + translation drafts against saved state. + -- Dirty indicator shown in header (●) and tab bar. +} + +invariant PostEditorModePersistence { + -- Editor mode (visual/markdown/preview) persists per session. + -- Default mode comes from editor settings. } -- ─── Post editor actions ──────────────────────────────────── @@ -115,7 +193,7 @@ rule PostAIAnalysis { when: PostAIAnalysisRequested(post_id) -- Gate: airplane mode check (see action_patterns.allium AIOperationGating) -- Uses title model (not default chat model) - -- Input: post title + excerpt + content (first 2000 chars) + -- Input: post title + excerpt + content (first config.post_content_sample_length chars) -- Response: suggested title, excerpt, slug -- Opens AISuggestionsModal with 3 fields: -- Each field: current value, suggested value, accept checkbox @@ -227,25 +305,3 @@ rule PostLanguageDetect { -- 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 diff --git a/specs/editor_script.allium b/specs/editor_script.allium index 0564015..0abbb9e 100644 --- a/specs/editor_script.allium +++ b/specs/editor_script.allium @@ -6,37 +6,83 @@ -- Describes the layout and behaviour of the script editor. -- See script.allium for entity model. --- ─── External surfaces ────────────────────────────────────── - -surface User { - provides: ScriptSaveRequested(script_id) - provides: ScriptRunRequested(script_id) - provides: ScriptDeleteRequested(script_id) -} +use "./script.allium" as script +use "./i18n.allium" as i18n -- ─── Script editor view ────────────────────────────────────── --- Code editor for Lua scripts (macros, utilities, transforms). +value ScriptEditorView { + script_id: String + title: String -- editable text input + slug: String -- editable text input, auto-generated from title + kind: String -- select: utility | macro | transform + entrypoint: String -- select: dynamically discovered functions, "main" always first + enabled: Boolean -- checkbox + content: String -- code editor content + created_at: String -- locale-formatted date + updated_at: String -- locale-formatted date +} --- 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. +surface ScriptEditorSurface { + context editor: ScriptEditorView + + exposes: + editor.title + editor.slug + editor.kind + editor.entrypoint + editor.enabled + editor.created_at + editor.updated_at + + provides: + ScriptSaveRequested(editor.script_id) + ScriptRunRequested(editor.script_id) + ScriptCheckSyntaxRequested(editor.script_id) + ScriptDeleteRequested(editor.script_id) + + @guarantee HeaderLayout + -- Header bar with script title tab. + -- Actions (right side): Save button, Run button, + -- Check Syntax button, Delete button (danger style). + + @guarantee MetadataRow + -- Two rows of metadata fields above the editor. + -- Row 1: Title (text input), Slug (text input, auto-generated from title). + -- Row 2: Kind (select: utility/macro/transform), + -- Entrypoint (select: dynamically discovered functions with "main" always first), + -- Enabled (checkbox). + + @guarantee EditorBody + -- Code editor with Python syntax highlighting. + -- Toolbar: "Content" label. + -- Syntax errors shown as inline markers in editor gutter. + + @guarantee FooterLayout + -- Created date and Updated date, locale-formatted. +} -- ─── Script editor actions ────────────────────────────────── rule ScriptSave { when: ScriptSaveRequested(script_id) - -- Lua syntax check first (parse validation) + -- Python syntax check first (parse validation via Python runtime) -- If syntax error: blocks save, shows error inline in editor gutter - -- If valid: bumps version, saves to DB + rewrites .lua file - -- See engine_side_effects.allium UpdateTemplateSideEffects (same pattern) + -- If valid: bumps version, saves to DB + rewrites .py file + -- Entrypoint list re-discovered from AST after save +} + +rule ScriptCheckSyntax { + when: ScriptCheckSyntaxRequested(script_id) + -- Validates Python syntax without saving + -- Shows errors inline in editor gutter + -- Success shown as toast or status indicator } rule ScriptRun { when: ScriptRunRequested(script_id) - -- Executes script in Lua runtime - -- Calls configured entrypoint function (default: render) + -- Executes script in Python runtime + -- Calls configured entrypoint function (default: main) -- stdout/stderr directed to Output panel tab -- Output panel auto-opens if not already visible -- Errors shown in Output panel with line numbers @@ -45,6 +91,6 @@ rule ScriptRun { rule ScriptDelete { when: ScriptDeleteRequested(script_id) -- No confirmation dialog - -- Deletes DB record + .lua file on disk + -- Deletes DB record + .py file on disk -- Closes script tab, sidebar removes item } diff --git a/specs/editor_settings.allium b/specs/editor_settings.allium index 6bc2acc..b82dca4 100644 --- a/specs/editor_settings.allium +++ b/specs/editor_settings.allium @@ -5,116 +5,251 @@ -- Describes the layout and behaviour of the settings and style views. --- ─── External surfaces ────────────────────────────────────── - -surface User { - provides: StyleThemeSelected(theme_name) - provides: StyleApplyRequested(theme_name) -} +use "./i18n.allium" as i18n -- ─── Settings view ──────────────────────────────────────────── --- VS Code-style settings page with header + search bar + scrollable sections. --- Search filters sections by keyword match. "No results" with clear button. +value SettingsView { + search_query: String? -- filters sections by keyword match + active_sections: List -- visible sections after search filter +} --- 8 sections (style is a separate tab, not a settings section): +value SettingsProjectSection { + project_name: String -- text input + project_description: String -- textarea (3 rows) + data_path: String -- text input + Browse button + Reset button + public_url: String -- url input + main_language: String -- select + blog_languages: List -- checkboxes (main language disabled) + default_author: String -- text input + max_posts_per_page: Integer -- number input (1-500, default 50) + blogmark_category: String -- select from categories +} --- 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 +value SettingsEditorSection { + default_mode: String -- select: wysiwyg | markdown | preview + diff_view_style: String -- select: inline | side-by-side + wrap_long_lines: Boolean -- checkbox + hide_unchanged_regions: Boolean -- checkbox +} --- 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) +value SettingsCategoryRow { + name: String -- read-only for protected categories + title: String -- editable + render_in_lists: Boolean -- checkbox + show_titles: Boolean -- checkbox + post_template_slug: String? -- select + list_template_slug: String? -- select + is_protected: Boolean -- true for: article, aside, page, picture +} --- 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 +value SettingsAISection { + anthropic_api_key: String? -- masked + Change/Save + mistral_api_key: String? -- masked + Change/Save + ollama_enabled: Boolean -- toggle, shows model capabilities table (tools/vision) + lm_studio_enabled: Boolean -- toggle, shows model capabilities table + offline_mode: Boolean -- toggle, switches between online and airplane endpoint + default_model: String? -- select from active endpoint models + title_model: String? -- select + image_analysis_model: String? -- select (vision-capable only) + system_prompt: String -- textarea (12 rows) + Save + Reset to Default +} --- 4. AI Assistant Settings: --- Two-endpoint configuration (see ai.allium TwoEndpointModel): --- Online endpoint: URL (text), API Key (masked + Change/Save), Model (select) --- Airplane endpoint: URL (text), Model (select) --- Both use OpenAI Chat Completions wire format. --- Refresh Models button per endpoint: fetches model list from endpoint URL --- Title Model (select from active endpoint models) --- Image Analysis Model (select, vision-capable only) --- Offline mode toggle: switches between online and airplane endpoint --- Disabled if airplane endpoint URL is not configured --- System Prompt (textarea 12 rows + Save + Reset to Default) +value SettingsPublishingSection { + ssh_mode: String -- select: scp | rsync + ssh_host: String -- text input + ssh_username: String -- text input + ssh_remote_path: String -- text input +} --- 5. Technology Settings: --- Semantic Similarity (checkbox) +value SettingsMCPSection { + status: String -- port number or "Not running" + agents: List +} --- 6. Publishing Settings (SSH): --- Info: SSH key auth only --- SSH Mode (select: scp/rsync), SSH Host, SSH Username, SSH Remote Path --- Save + Clear buttons +value MCPAgentRow { + agent_name: String -- Claude Code, Claude Desktop, GitHub Copilot, etc. + is_installed: Boolean -- Add/Remove toggle +} --- 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 +invariant SettingsProtectedCategories { + -- Protected categories (article, aside, page, picture) cannot be deleted. + -- Their Remove button is disabled. +} --- 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 +invariant SettingsMCPAgents { + -- MCP section has exactly 7 agent rows in this order: + -- Claude Code, Claude Desktop, GitHub Copilot, Gemini CLI, + -- OpenCode, Mistral Vibe, OpenAI Codex. +} + +config { + settings_max_posts_per_page: Integer = 500 + settings_default_posts_per_page: Integer = 50 + settings_system_prompt_rows: Integer = 12 +} + +surface SettingsViewSurface { + context settings: SettingsView + + exposes: + settings.search_query + settings.active_sections + + provides: + SettingsSearchChanged(query) + SettingsProjectSaved(project_data) + SettingsEditorSaved(editor_data) + SettingsCategoryAdded(category_name) + SettingsCategoryUpdated(category_row) + SettingsCategoryRemoved(category_name) + SettingsCategoriesResetToDefaults() + SettingsAIApiKeySaved(provider, key) + SettingsAIModelRefreshRequested(endpoint) + SettingsAISystemPromptSaved(prompt) + SettingsAISystemPromptReset() + SettingsPublishingSaved(publishing_data) + SettingsPublishingCleared() + SettingsMCPAgentToggled(agent_name) + SettingsRebuildRequested(entity_type) + SettingsRegenerateThumbnailsRequested() + SettingsOpenDataFolderRequested() + StyleThemeSelected(theme_name) + StyleApplyRequested(theme_name) + + @guarantee SearchBar + -- Search input in header filters sections by keyword match. + -- "No results" message with clear button when no sections match. + + @guarantee ProjectSection + -- Section 1: Project Name, Description (textarea 3 rows), Data Path (text + Browse + Reset), + -- Public URL, Main Language (select), Blog Languages (checkbox grid, main disabled), + -- Default Author, Max Posts Per Page (number 1-500), + -- Blogmark Category (select), Blogmark Bookmarklet (copy button), Save button. + + @guarantee BookmarkletCopy + -- Copy button copies bookmarklet JavaScript to clipboard. + -- Bookmarklet uses project's publicUrl to construct POST endpoint. + + @guarantee EditorSection + -- Section 2: Default Editor Mode (select: WYSIWYG/Markdown/Preview), + -- Diff View Style (select: Inline/Side-by-side), + -- Wrap Long Lines (checkbox), Hide Unchanged Regions (checkbox). + + @guarantee ContentCategoriesSection + -- Section 3: Table with columns: 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, validates non-empty and unique. + -- "Reset to Defaults" restores default categories set. + + @guarantee AISection + -- Section 4: Anthropic API key, Mistral API key (both masked + Change/Save), + -- Ollama toggle (with model capabilities table: tools/vision), + -- LM Studio toggle (with capabilities table), + -- Offline mode toggle (with dedicated model selectors for chat/title/image), + -- Default model (with catalog info: max output, context window), + -- Title model, Image analysis model, + -- System prompt (textarea config.settings_system_prompt_rows rows + Save + Reset). + + @guarantee AIApiKeyValidation + -- On Save: test API call to endpoint before persisting. + -- On failure: toast error message, key not saved. + -- On success: key encrypted via SecureKeyStore, masked display shown. + + @guarantee AIModelRefresh + -- Refresh Models button per endpoint fetches model list from URL. + -- Model selector populated from fetched list. + -- Per-model info: max output tokens, context window (when available). + + @guarantee TechnologySection + -- Section 5: Python runtime mode (webworker/main-thread), + -- Semantic Similarity toggle. + + @guarantee PublishingSection + -- Section 6: SSH Mode (scp/rsync), Host, Username, Remote Path. + -- Save + Clear buttons. + + @guarantee MCPSection + -- Section 7: Status badge (port or "Not running"). + -- 7 agent rows with Add/Remove toggle each. + + @guarantee DataMaintenanceSection + -- Section 8: 6 rebuild buttons (Posts from Files, Media from Files, + -- Scripts from Files, Templates from Files, Links, + -- Regenerate Missing Thumbnails). + -- Open Data Folder button. + -- Each rebuild executes immediately (no confirmation). + -- Runs as background task with progress in Tasks panel. + + @guarantee CollapsibleSections + -- All 8 sections are collapsible. + -- Section visibility respects search filter. +} -- ─── Settings view actions ────────────────────────────────── --- API key validation: --- On Save: test API call to online endpoint before persisting --- On validation failure: toast error message, key not saved --- On success: key encrypted via SecureKeyStore, masked display shown - --- Endpoint configuration: --- URL field required for both endpoints --- Refresh Models button fetches model list from endpoint URL --- Model selector populated from fetched model list --- Per-model info: max output tokens, context window (when available) - --- 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 +rule SettingsRebuild { + when: SettingsRebuildRequested(entity_type) + -- entity_type: posts | media | scripts | templates | links | thumbnails + -- 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 +} -- ─── Style view ─────────────────────────────────────────────── --- Simple vertical layout: header, theme picker, controls row, preview. +value StyleView { + themes: List + selected_theme: String? + applied_theme: String? + preview_mode: String -- auto | light | dark +} --- 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. +value StyleTheme { + name: String + accent_color: String -- hex + light_bg_color: String -- hex + dark_bg_color: String -- hex +} --- Controls row: Preview Mode dropdown (Auto/Light/Dark), Apply Theme button. +surface StyleViewSurface { + context style: StyleView --- 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. + exposes: + for t in style.themes: + t.name + t.accent_color + t.light_bg_color + t.dark_bg_color + style.selected_theme + style.applied_theme + style.preview_mode --- Apply Theme persists to project metadata. + provides: + StyleThemeSelected(theme_name) + StyleApplyRequested(style.selected_theme) + when style.selected_theme != style.applied_theme + StylePreviewModeChanged(mode) --- ─── Style view actions ───────────────────────────────────── + @guarantee ThemePicker + -- 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. + + @guarantee ControlsRow + -- Preview Mode dropdown (Auto/Light/Dark). + -- Apply Theme button, disabled when selected_theme matches applied_theme. + + @guarantee LivePreview + -- Iframe at 127.0.0.1:4123/__style-preview with theme and mode query params. + -- Updates live on selection or preview mode change. + + @guarantee PreviewModeLocal + -- Preview mode dropdown controls iframe query param only. + -- Does not persist — local UI state only. +} rule StyleThemeSelect { when: StyleThemeSelected(theme_name) @@ -127,7 +262,3 @@ rule StyleApplyTheme { -- Persists picoTheme to project metadata (meta/project.json) -- See engine_side_effects.allium UpdateProjectMetadataSideEffects } - --- Preview mode dropdown (Auto/Light/Dark): --- Controls iframe query param, live update on change --- Does not persist — local UI state only diff --git a/specs/editor_tags.allium b/specs/editor_tags.allium index f8ea3ac..5eaafc0 100644 --- a/specs/editor_tags.allium +++ b/specs/editor_tags.allium @@ -5,61 +5,105 @@ -- Describes the layout and behaviour of the tags view. +use "./tag.allium" as tag +use "./i18n.allium" as i18n + -- ─── Tags view ──────────────────────────────────────────────── --- Three sections controlled by sidebar TagsNav: +value TagsView { + cloud_tags: List + selected_tags: List -- multi-select from cloud + edit_form: TagEditForm? -- populated when exactly 1 tag selected + merge_source_tags: List -- 2+ selected tags for merge + merge_target: String? -- target tag for merge +} --- 1. Cloud: tag cloud visualization. Tags sized by post count, --- coloured by tag colour. Clickable to select for editing. +value TagCloudItem { + name: String + count: Integer + color: String? +} --- 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 - --- ─── Colour picker popover ────────────────────────────────── +value TagEditForm { + name: String -- editable text input + color: String? -- colour picker value + post_template_slug: String? -- select from available templates +} 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 -- 17 hex colour values + presets: List -- 17 hex colour values custom_hex: String? selected: String? } config { colour_picker_preset_count: Integer = 17 + tag_cloud_min_font: String = "0.85rem" + tag_cloud_max_font: String = "1.8rem" +} + +surface TagsViewSurface { + context view: TagsView + + exposes: + for t in view.cloud_tags: + t.name + t.count + t.color + view.selected_tags + view.edit_form when view.edit_form != null + view.merge_target when view.merge_target != null + + provides: + TagCloudItemClicked(tag_name) + TagCreateRequested(name, color) + TagSaveRequested(name, color, post_template_slug) + when view.edit_form != null + TagDeleteRequested(tag_name) + when view.selected_tags.count = 1 + TagMergeRequested(source_tags, target_tag) + when view.selected_tags.count >= 2 + TagSyncRequested() + + @guarantee CloudSection + -- Tag cloud visualisation (section #1). + -- Tags sized by post count (config.tag_cloud_min_font to config.tag_cloud_max_font). + -- Tags with colours get coloured background with contrast text. + -- Multi-select: click to toggle selection. Selection count shown with clear button. + -- Click selects tag for editing in Manage section. + + @guarantee ManageSection + -- Create/Edit section (section #2). + -- Create form: name input + colour picker + Create button. + -- Edit form (when exactly 1 tag selected): name input, colour picker, + -- post template select dropdown (optional), Save/Cancel buttons. + -- Delete button visible when exactly 1 tag selected. + + @guarantee ColourPicker + -- Inline popover positioned below tag colour field. + -- Grid of config.colour_picker_preset_count preset colour swatches. + -- Below grid: custom hex input field (#RRGGBB). + -- Selection is immediate — no confirm/cancel buttons. + -- Choosing a colour updates the form's colour field live. + + @guarantee MergeSection + -- Merge section (section #3). Requires 2+ selected tags. + -- Target tag select dropdown (single select from selected tags). + -- Merge button opens ConfirmDialog: + -- "Merge N tags into {target}? This cannot be undone." + -- Shows preview of "tags to delete" (all selected except target). + + @guarantee SyncSection + -- Sync/Discover section (section #4). + -- "Discover" button rewrites tags.json from current DB state. + + @guarantee DeleteConfirmation + -- 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. + + @guarantee MergeExecution + -- On merge confirm: background task updates all affected posts, + -- rewrites published .md files, deletes source tags, writes tags.json. } diff --git a/specs/editor_template.allium b/specs/editor_template.allium index 7b1728f..be6b3c1 100644 --- a/specs/editor_template.allium +++ b/specs/editor_template.allium @@ -6,22 +6,56 @@ -- Describes the layout and behaviour of the template editor. -- See template.allium for entity model and Liquid subset. --- ─── External surfaces ────────────────────────────────────── - -surface User { - provides: TemplateSaveRequested(template_id) - provides: TemplateDeleteRequested(template_id) -} +use "./template.allium" as template +use "./i18n.allium" as i18n -- ─── Template editor view ───────────────────────────────────── --- Code editor for Liquid templates (post, list, not_found, partial). +value TemplateEditorView { + template_id: String + title: String -- editable text input + slug: String -- editable text input, auto-generated from title + kind: String -- select: post | list | not_found | partial + enabled: Boolean -- checkbox + content: String -- code editor content + created_at: String -- locale-formatted date + updated_at: String -- locale-formatted date +} --- 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. +surface TemplateEditorSurface { + context editor: TemplateEditorView + + exposes: + editor.title + editor.slug + editor.kind + editor.enabled + editor.created_at + editor.updated_at + + provides: + TemplateSaveRequested(editor.template_id) + TemplateValidateRequested(editor.template_id) + TemplateDeleteRequested(editor.template_id) + + @guarantee HeaderLayout + -- Header bar with template title tab. + -- Actions (right side): Save button, Validate button, + -- Delete button (danger style). + + @guarantee MetadataRow + -- Two rows of metadata fields above the editor. + -- Row 1: Title (text input), Slug (text input, auto-generated from title). + -- Row 2: Kind (select: post/list/not-found/partial), Enabled (checkbox). + + @guarantee EditorBody + -- Code editor with HTML/Liquid syntax highlighting. + -- Toolbar: "Content" label. + -- Syntax errors shown inline in editor on validation. + + @guarantee FooterLayout + -- Created date and Updated date, locale-formatted. +} -- ─── Template editor actions ──────────────────────────────── @@ -33,6 +67,13 @@ rule TemplateSave { -- See engine_side_effects.allium UpdateTemplateSideEffects } +rule TemplateValidate { + when: TemplateValidateRequested(template_id) + -- Validates Liquid syntax without saving + -- Shows errors inline in editor + -- Success shown as toast or status indicator +} + rule TemplateDelete { when: TemplateDeleteRequested(template_id) -- Checks for references: posts using this template, tags with postTemplateSlug