diff --git a/specs/editors.allium b/specs/editors.allium index 7c85a2f..506d4c1 100644 --- a/specs/editors.allium +++ b/specs/editors.allium @@ -15,6 +15,44 @@ use "./post.allium" as post use "./media.allium" as media use "./i18n.allium" as i18n +-- ─── External surfaces ────────────────────────────────────── + +-- The user is the external actor who initiates all editor interactions +-- via clicks, keyboard shortcuts, and drag-and-drop. This surface +-- provides the triggers that editor action rules listen for. + +surface User { + provides: PostAIAnalysisRequested(post_id) + provides: PostTranslateRequested(post_id, target_language) + provides: PostSaved(post_id) + provides: PostPublishRequested(post_id) + provides: PostDiscardRequested(post_id) + provides: PostDeleteRequested(post_id) + provides: PostInsertLinkRequested(post_id) + provides: PostInsertMediaRequested(post_id) + provides: PostGalleryRequested(post_id) + provides: ImageDroppedOnEditor(post_id, file_path) + provides: PostLanguageDetectRequested(post_id) + provides: MediaAIImageAnalysisRequested(media_id) + provides: MediaDetectLanguageRequested(media_id) + provides: MediaTranslateMetadataRequested(media_id, target_language) + provides: MediaReplaceFileRequested(media_id) + provides: MediaDeleteRequested(media_id) + provides: MediaLinkToPostRequested(media_id) + provides: MediaTranslationEditClicked(media_id, language) + provides: MediaTranslationRefreshClicked(media_id, language) + provides: MediaTranslationDeleteClicked(media_id, language) + provides: StyleThemeSelected(theme_name) + provides: StyleApplyRequested(theme_name) + provides: ScriptSaveRequested(script_id) + provides: ScriptRunRequested(script_id) + provides: ScriptDeleteRequested(script_id) + provides: TemplateSaveRequested(template_id) + provides: TemplateDeleteRequested(template_id) + provides: ImportAnalyzeRequested(definition_id, file_path) + provides: ImportExecuteRequested(definition_id) +} + -- ─── Dashboard (no tab active) ─────────────────────────────── -- Shown as default/welcome view when no entity tab is active. @@ -188,7 +226,13 @@ invariant PostDirtyTracking { -- Used by PostAIAnalysis and MediaAIImageAnalysis. value AISuggestionsModal { fields: List - -- Confirm applies only accepted fields; Cancel discards all + -- Layout: title bar ("AI Suggestions"), scrollable field list, button row + -- Each field rendered as a row: + -- Left: checkbox (accept/reject), label + -- Center: current value (read-only, muted), arrow, suggested value (highlighted) + -- Special: slug field checkbox disabled if post was ever published + -- Buttons: Cancel (secondary), Apply Selected (primary) + -- Cancel discards all; Apply writes only accepted fields to entity } value AISuggestionField { @@ -196,6 +240,187 @@ value AISuggestionField { current_value: String suggested_value: String accepted: Boolean -- checkbox, default true + locked: Boolean -- if true, checkbox disabled (e.g. published slug) +} + +-- ─── Modals and overlays ──────────────────────────────────── + +-- All modals rendered as centered overlay with backdrop dimming. +-- ESC key or backdrop click closes modal (cancel semantics). +-- Overlays (PostPicker, ColourPicker) are positioned inline near trigger. + +value InsertPostLinkModal { + -- Two-tab modal opened by Ctrl/Cmd+K in post editor (markdown mode). + -- Tab 1 - Internal: + -- Search input (debounced 300ms, queries post titles) + -- Results list: post title + status badge (draft/published/archived) + -- If semantic similarity enabled: results ranked by vector similarity + -- Click result: inserts [title](/YYYY/MM/DD/slug) at cursor, closes modal + -- "Create Post" row at bottom of results: + -- Creates new post with search query as title, inserts link to it + -- Tab 2 - External: + -- URL input field (required) + -- Display text input field (optional) + -- Insert button: inserts [text](url) or bare url if no text, closes modal + active_tab: String -- internal | external + search_query: String + results: List +} + +value InsertLinkResult { + post_id: String + title: String + status: String -- draft | published | archived + canonical_url: String -- /YYYY/MM/DD/slug +} + +value InsertMediaModal { + -- Grid modal for inserting media references into post content. + -- Search input filtering by media title and original filename. + -- Grid of media items: bds-thumb:// thumbnail (medium 400px), title below. + -- Click item: + -- Images: inserts ![alt](bds-media://id) at cursor + -- Non-images: inserts [originalName](bds-media://id) at cursor + -- Closes modal after insertion. + search_query: String + results: List +} + +value InsertMediaResult { + media_id: String + title: String + original_name: String + is_image: Boolean + thumbnail_url: String? -- bds-thumb:// for images, null for others +} + +value LanguagePickerModal { + -- Shown for Translate Post and Translate Media Metadata actions. + -- Lists all configured blogLanguages except source language. + -- Each row: flag emoji, language name, status badge if translation exists. + -- Existing translations show "(draft)" or "(published)" badge. + -- Click selects target language and initiates translation flow. + -- Cancel closes without action. + source_language: String + available_targets: List +} + +value LanguageTarget { + code: String + name: String + flag_emoji: String + has_existing_translation: Boolean + existing_status: String? -- draft | published, if translation exists +} + +value ConfirmDeleteModal { + -- Custom styled modal for destructive operations with reference info. + -- Used by: MediaDelete (shows linked posts), TagDelete (shows post count). + -- Layout: warning icon, title, entity name, reference section, buttons. + -- Reference section: "This item is referenced by:" + bulleted list. + -- Buttons: Cancel (secondary), Delete (destructive red). + entity_name: String + entity_type: String -- media | tag + reference_count: Integer + reference_list: List -- titles of referencing entities +} + +value ConfirmDialog { + -- Custom styled modal for non-delete confirmations. + -- Used by: TagMerge ("Merge N tags into {target}? Cannot be undone."). + -- Layout: title, descriptive message, buttons. + -- Buttons: Cancel (secondary), Confirm (primary). + title: String + message: String +} + +-- Native confirm dialogs (via rfd crate) are NOT modelled as values. +-- They are simple yes/no system dialogs with a message string. +-- Used by: PostDelete, PostDiscard, TemplateDelete (with references). + +value GalleryOverlay { + -- Full-screen overlay showing all media linked to a post. + -- Opened from Gallery button in post editor toolbar (markdown mode). + -- Image grid: bds-thumb:// thumbnails (medium 400px), 3-4 columns. + -- Click image: opens LightboxView for that image. + -- Close: X button or ESC key. + post_id: String + images: List +} + +value GalleryImage { + media_id: String + thumbnail_url: String -- bds-thumb://media_id + alt_text: String? +} + +value LightboxView { + -- Full-screen image viewer, sub-view of GalleryOverlay. + -- Shows single image at full resolution via bds-media:// protocol. + -- Navigation: left/right arrow buttons, keyboard left/right arrow keys. + -- Close: X button, ESC key, or click outside image area. + -- Header: image title or filename, index counter "3 of 12". + current_index: Integer + total_count: Integer + media_id: String + image_url: String -- bds-media://media_id + alt_text: String? +} + +value ColourPickerPopover { + -- Inline popover positioned below tag colour field (tags view manage). + -- Not a modal — does not dim backdrop, no ESC/backdrop close. + -- Closes when clicking outside popover or selecting a colour. + -- Grid of 17 preset colour swatches (4x4 + 1). + -- Below grid: custom hex input field (#RRGGBB). + -- Selection is immediate — no confirm/cancel buttons. + -- Choosing a colour updates the tag form's colour field live. + presets: List -- 17 hex colour values + custom_hex: String? + selected: String? +} + +config { + colour_picker_preset_count: Integer = 17 +} + +value PostPickerOverlay { + -- Inline overlay positioned near "Link to Post" button (media editor). + -- Not a modal — positioned as dropdown below trigger button. + -- Search input filtering posts by title (only unlinked posts shown). + -- Up to 10 results displayed. "and N more" text if total exceeds 10. + -- Click result: links media to selected post, closes overlay. + search_query: String + results: List + overflow_count: Integer? -- shown as "and N more" if > 0 +} + +value PostPickerResult { + post_id: String + title: String +} + +value ModelSelectorDropdown { + -- Dropdown in chat panel header for per-conversation model override. + -- Groups models by provider using section headers. + -- Each entry: model display name. + -- Expandable details per model: context window, max output tokens, pricing. + -- Selected model persisted with conversation record. + -- Changing model mid-conversation applies to subsequent messages only. + groups: List + selected_model_id: String? +} + +value ModelProviderGroup { + provider_name: String -- e.g. "OpenAI", "Ollama", "LM Studio" + models: List +} + +value ModelEntry { + model_id: String + display_name: String + context_window: Integer + max_output_tokens: Integer } rule PostAIAnalysis { diff --git a/specs/flows.allium b/specs/flows.allium index a840769..d738d99 100644 --- a/specs/flows.allium +++ b/specs/flows.allium @@ -17,6 +17,59 @@ use "./post.allium" as post use "./media.allium" as media use "./search.allium" as search +-- ─── External surfaces ────────────────────────────────────── + +-- User-initiated navigation and entity management actions. +-- These triggers originate from sidebar clicks, tab operations, +-- dashboard interactions, and save/delete actions in editors. + +surface UserNavigation { + provides: SidebarItemClicked(entity_type, entity_id, click_type) + provides: SidebarCreateRequested(entity_type) + provides: SidebarDeleteRequested(entity_type, entity_id) + provides: DashboardPostClicked(post_id, click_type) + provides: PostSaved(post_id, updated_post) + provides: PostStatusTransitioned(post_id, old_status, new_status) + provides: PostDeletedFromEditor(post_id) + provides: MediaSavedFromEditor(media_id, updated_media) + provides: MediaDeletedFromEditor(media_id) + provides: SettingsRebuildCompleted(entity_type, new_data) + provides: TransientTabBeingReplaced(old_tab, new_tab) + provides: TabClosed(tab) + provides: AISuggestionRequested(entity_type, entity_id) + provides: PostAutoTranslateCompleted(post_id, language) +} + +-- Engine-level events emitted after backend operations complete. +-- These are NOT direct user actions — they fire as side-effects +-- of user operations processed by the engine layer. + +surface Engine { + provides: PostCreated(post) + provides: PostUpdated(post, changes) + provides: PostPublished(post) + provides: PostDeleted(post) + provides: PostChangesDiscarded(post) + provides: MediaImported(media) + provides: MediaUpdated(media, changes) + provides: MediaFileReplaced(media, new_file) + provides: MediaDeleted(media) + provides: TemplateCreated(template) + provides: TemplateUpdated(template, changes) + provides: TemplatePublished(template) + provides: TemplateDeleted(template, force) + provides: TagDeleted(tag) + provides: TagRenamed(old_name, new_name) + provides: TagsMerged(source_tags, target_tag) + provides: ProjectMetadataUpdated(metadata) + provides: CategoryAdded(name) + provides: CategoryRemoved(name) + provides: PublishingPreferencesUpdated(prefs) + provides: PostTranslationUpserted(translation, source_post) + provides: MediaTranslationUpserted(translation, media) + provides: MediaTranslationDeleted(media, language) +} + -- ═══════════════════════════════════════════════════════════════ -- PART 1: UI DATA FLOW MODEL -- ═══════════════════════════════════════════════════════════════