chore: break down god-specs
This commit is contained in:
127
specs/action_patterns.allium
Normal file
127
specs/action_patterns.allium
Normal file
@@ -0,0 +1,127 @@
|
||||
-- allium: 1
|
||||
-- bDS Action Patterns and Chains
|
||||
-- Scope: cross-cutting (all waves)
|
||||
-- Distilled from: PostEditor.tsx, MediaEditor.tsx, appStore.ts
|
||||
|
||||
-- Cross-cutting patterns for AI operations, auto-translation,
|
||||
-- drag-and-drop chains, and confirmation dialogs.
|
||||
|
||||
use "./post.allium" as post
|
||||
use "./media.allium" as media
|
||||
|
||||
-- ─── External surfaces ──────────────────────────────────────
|
||||
|
||||
surface UserAction {
|
||||
provides: AISuggestionRequested(entity_type, entity_id)
|
||||
provides: PostSaved(post_id)
|
||||
provides: PostAutoTranslateCompleted(post_id, language)
|
||||
}
|
||||
|
||||
-- ─── 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
|
||||
}
|
||||
|
||||
-- ─── AI suggestions pattern ────────────────────────────
|
||||
|
||||
-- Shared flow used by PostAIAnalysis and MediaAIImageAnalysis.
|
||||
-- See modals.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
|
||||
}
|
||||
|
||||
-- ─── 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
|
||||
}
|
||||
|
||||
-- ─── Drag-and-drop image chain ─────────────────────────
|
||||
|
||||
-- Full chain when image file is dropped on post editor body.
|
||||
-- See editor_post.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  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
|
||||
|
||||
-- ─── 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
|
||||
@@ -30,8 +30,20 @@ use "./i18n.allium" as i18n -- Split localization (UI vs content
|
||||
use "./layout.allium" as layout -- App shell, activity bar, status bar, panels
|
||||
use "./tabs.allium" as tabs -- Tab system, editor routing, preview/pin
|
||||
use "./sidebar_views.allium" as sidebar -- 10 sidebar views, content, behaviour
|
||||
use "./editors.allium" as editors -- Editor view layouts, dashboard, chat panel
|
||||
use "./flows.allium" as flows -- UI/UX flows, save side-effects, event propagation
|
||||
use "./modals.allium" as modals -- Shared modals (AI suggestions, confirm, gallery)
|
||||
use "./editor_post.allium" as editor_post -- Post editor view and actions
|
||||
use "./editor_media.allium" as editor_media -- Media editor view and actions
|
||||
use "./editor_settings.allium" as editor_settings -- Settings + style views
|
||||
use "./editor_tags.allium" as editor_tags -- Tags view and colour picker
|
||||
use "./editor_chat.allium" as editor_chat -- Chat panel and model selector
|
||||
use "./editor_script.allium" as editor_script -- Script editor
|
||||
use "./editor_template.allium" as editor_template -- Template editor
|
||||
use "./editor_misc.allium" as editor_misc -- Dashboard, menu, metadata diff, git diff, etc.
|
||||
|
||||
-- Flows and side-effects
|
||||
use "./ui_data_flow.allium" as ui_data_flow -- Sidebar/editor/tab reactive coordination
|
||||
use "./engine_side_effects.allium" as engine_side_effects -- CRUD side-effect chains
|
||||
use "./action_patterns.allium" as action_patterns -- AI gating, translation chains, confirmations
|
||||
|
||||
-- Integration
|
||||
use "./git.allium" as git -- Git operations, LFS, reconciliation
|
||||
|
||||
88
specs/editor_chat.allium
Normal file
88
specs/editor_chat.allium
Normal file
@@ -0,0 +1,88 @@
|
||||
-- allium: 1
|
||||
-- bDS Chat Panel
|
||||
-- Scope: UI content area — AI chat surface
|
||||
-- Distilled from: ChatPanel.tsx
|
||||
|
||||
-- Describes the layout and behaviour of the chat panel.
|
||||
|
||||
-- ─── 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
|
||||
}
|
||||
|
||||
-- ─── Model selector dropdown ──────────────────────────────────
|
||||
|
||||
value ModelSelectorDropdown {
|
||||
-- Dropdown in chat panel header for per-conversation model override.
|
||||
-- Groups models by provider using section headers.
|
||||
-- Each entry: model display name.
|
||||
-- Expandable details per model: context window, max output tokens, pricing.
|
||||
-- Selected model persisted with conversation record.
|
||||
-- Changing model mid-conversation applies to subsequent messages only.
|
||||
groups: List<ModelProviderGroup>
|
||||
selected_model_id: String?
|
||||
}
|
||||
|
||||
value ModelProviderGroup {
|
||||
provider_name: String -- e.g. "OpenAI", "Ollama", "LM Studio"
|
||||
models: List<ModelEntry>
|
||||
}
|
||||
|
||||
value ModelEntry {
|
||||
model_id: String
|
||||
display_name: String
|
||||
context_window: Integer
|
||||
max_output_tokens: Integer
|
||||
}
|
||||
|
||||
-- ─── 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
|
||||
188
specs/editor_media.allium
Normal file
188
specs/editor_media.allium
Normal file
@@ -0,0 +1,188 @@
|
||||
-- allium: 1
|
||||
-- bDS Media Editor View
|
||||
-- Scope: UI content area — media editing surface
|
||||
-- Distilled from: MediaEditor.tsx
|
||||
|
||||
-- Describes the layout and behaviour of the media editor rendered in
|
||||
-- the main content area when a media tab is active.
|
||||
|
||||
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?
|
||||
translations: List<MediaTranslationItem>
|
||||
linked_posts: List<LinkedPostItem>
|
||||
}
|
||||
|
||||
value MediaTranslationItem {
|
||||
language: String
|
||||
title: String?
|
||||
}
|
||||
|
||||
value LinkedPostItem {
|
||||
post_id: String
|
||||
title: String
|
||||
}
|
||||
|
||||
-- No auto-save; explicit Save button required.
|
||||
-- Replace File opens a native file dialog.
|
||||
-- Delete shows confirmation with references (linked posts).
|
||||
|
||||
-- ─── 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<PostPickerResult>
|
||||
overflow_count: Integer? -- shown as "and N more" if > 0
|
||||
}
|
||||
|
||||
value PostPickerResult {
|
||||
post_id: String
|
||||
title: String
|
||||
}
|
||||
|
||||
-- ─── Media editor actions ─────────────────────────────────────
|
||||
|
||||
rule MediaAIImageAnalysis {
|
||||
when: MediaAIImageAnalysisRequested(media_id)
|
||||
-- Gate: airplane mode check (see action_patterns.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 engine_side_effects.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
|
||||
}
|
||||
247
specs/editor_misc.allium
Normal file
247
specs/editor_misc.allium
Normal file
@@ -0,0 +1,247 @@
|
||||
-- allium: 1
|
||||
-- bDS Miscellaneous Editor Views
|
||||
-- Scope: UI content area — dashboard, menu editor, metadata diff, git diff,
|
||||
-- documentation, validation, find duplicates, import analysis
|
||||
-- Distilled from: Editor.tsx (Dashboard), MenuEditorView.tsx,
|
||||
-- MetadataDiffPanel.tsx, ImportAnalysisView.tsx
|
||||
|
||||
-- Describes the layout and behaviour of smaller editor views that don't
|
||||
-- warrant their own spec file.
|
||||
|
||||
use "./tabs.allium" as tabs
|
||||
|
||||
-- ─── External surfaces ──────────────────────────────────────
|
||||
|
||||
surface User {
|
||||
provides: ImportAnalyzeRequested(definition_id, file_path)
|
||||
provides: ImportExecuteRequested(definition_id)
|
||||
}
|
||||
|
||||
-- ─── Dashboard (no tab active) ───────────────────────────────
|
||||
|
||||
-- Shown as default/welcome view when no entity tab is active.
|
||||
-- Single centered column.
|
||||
|
||||
value Dashboard {
|
||||
title: String
|
||||
subtitle: String
|
||||
stats: DashboardStats
|
||||
timeline: DashboardTimeline
|
||||
tag_cloud: DashboardTagCloud
|
||||
category_cloud: DashboardCategoryCloud
|
||||
recent_posts: List<DashboardRecentPost>
|
||||
}
|
||||
|
||||
value DashboardStats {
|
||||
-- Three stat cards side by side:
|
||||
total_posts: Integer
|
||||
published_count: Integer
|
||||
draft_count: Integer
|
||||
archived_count: Integer -- shown only if > 0
|
||||
media_count: Integer
|
||||
image_count: Integer
|
||||
total_media_size: String -- formatted B/KB/MB/GB
|
||||
tag_count: Integer
|
||||
category_count: Integer
|
||||
}
|
||||
|
||||
value DashboardTimeline {
|
||||
-- Bar chart of posts over time
|
||||
-- Last 12 months that have data
|
||||
-- Each bar: count label on top, month abbreviation + year below
|
||||
-- Bar height proportional to max count
|
||||
months: List<DashboardTimelineMonth>
|
||||
}
|
||||
|
||||
value DashboardTimelineMonth {
|
||||
label: String -- month abbreviation
|
||||
year: Integer
|
||||
count: Integer
|
||||
}
|
||||
|
||||
value DashboardTagCloud {
|
||||
-- Up to 40 tags, sorted alphabetically
|
||||
-- Font size scaled 11px-22px based on post count
|
||||
-- Tags with colours get coloured background with contrast text
|
||||
-- Hover title shows post count
|
||||
-- "and N more" text if > 40 tags
|
||||
tags: List<DashboardTag>
|
||||
overflow_count: Integer?
|
||||
}
|
||||
|
||||
value DashboardTag {
|
||||
name: String
|
||||
count: Integer
|
||||
color: String?
|
||||
}
|
||||
|
||||
value DashboardCategoryCloud {
|
||||
-- All categories as badge-like tags
|
||||
-- Each shows category name + count
|
||||
categories: List<DashboardCategory>
|
||||
}
|
||||
|
||||
value DashboardCategory {
|
||||
name: String
|
||||
count: Integer
|
||||
}
|
||||
|
||||
value DashboardRecentPost {
|
||||
-- Last 5 posts by updatedAt descending
|
||||
-- Each row: title (or "Untitled"), status badge (draft/published), date
|
||||
-- Single-click: preview tab, double-click: pin tab
|
||||
post_id: String
|
||||
title: String
|
||||
status: String
|
||||
date: String
|
||||
}
|
||||
|
||||
config {
|
||||
dashboard_max_tags: Integer = 40
|
||||
dashboard_tag_min_font: Integer = 11
|
||||
dashboard_tag_max_font: Integer = 22
|
||||
dashboard_recent_count: Integer = 5
|
||||
dashboard_timeline_months: Integer = 12
|
||||
}
|
||||
|
||||
-- ─── Menu editor view ────────────────────────────────────────
|
||||
|
||||
-- Visual editor for the OPML navigation menu (meta/menu.opml).
|
||||
-- See menu.allium for data model.
|
||||
|
||||
-- Layout: toolbar at top, tree view of menu items below.
|
||||
-- Each item row: drag handle, icon, label, type badge, action buttons.
|
||||
-- Nested items indented to show hierarchy.
|
||||
|
||||
-- ─── Menu editor actions ────────────────────────────────────
|
||||
|
||||
-- Add item (toolbar buttons):
|
||||
-- Page entry: opens lazy-loaded page picker (posts with "page" category)
|
||||
-- Category archive entry: opens lazy-loaded category picker
|
||||
-- Submenu: creates empty container node for nesting children
|
||||
-- Home entry: always available, maximum one allowed
|
||||
|
||||
-- Save: serializes tree to OPML 2.0, writes meta/menu.opml
|
||||
|
||||
-- Move operations (per-item buttons):
|
||||
-- Up/Down: reorder within same nesting level
|
||||
-- Indent: nest under previous sibling (becomes child)
|
||||
-- Unindent: move to parent's level (becomes next sibling of parent)
|
||||
-- Home item: protected — cannot be moved or deleted
|
||||
|
||||
-- Drag-and-drop reorder:
|
||||
-- Drag handle on each item
|
||||
-- Auto-expand collapsed submenus on hover (450ms delay)
|
||||
-- Drop indicators show target position and nesting level
|
||||
|
||||
-- Delete: removes item and all children, no confirmation dialog
|
||||
-- Exception: home item cannot be deleted (button hidden)
|
||||
|
||||
-- ─── Metadata diff view ──────────────────────────────────────
|
||||
|
||||
-- Shows DB vs filesystem differences for all entity types.
|
||||
-- See metadata_diff.allium for diff field definitions.
|
||||
|
||||
-- Layout: scan button at top, results grouped by entity type below.
|
||||
-- Each diff entry: entity name, field name, DB value, file value,
|
||||
-- two sync buttons (DB->File, File->DB).
|
||||
|
||||
-- ─── Metadata diff actions ──────────────────────────────────
|
||||
|
||||
-- Scan: runs 4 parallel scans (posts, media, scripts, templates)
|
||||
-- Compares DB records against filesystem files field by field
|
||||
-- Results grouped by entity type, sorted by entity name
|
||||
-- Shows count of differences per entity type in section header
|
||||
|
||||
-- Per-field sync:
|
||||
-- DB -> File button: overwrites file field with DB value
|
||||
-- File -> DB button: overwrites DB field with file value
|
||||
-- No confirmation dialog for individual field syncs
|
||||
-- Button disappears after successful sync (field now matches)
|
||||
|
||||
-- Orphan file import:
|
||||
-- Files on disk with no matching DB record shown separately
|
||||
-- "Import" action creates DB record from file metadata
|
||||
-- Appears at bottom of each entity type section
|
||||
|
||||
-- ─── 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.
|
||||
|
||||
-- ─── Documentation views ─────────────────────────────────────
|
||||
|
||||
-- documentation: renders DOCUMENTATION.md as styled HTML
|
||||
-- api_documentation: renders API.md as styled HTML
|
||||
|
||||
-- ─── Validation views ────────────────────────────────────────
|
||||
|
||||
-- site_validation: checks generated site for broken links, missing assets
|
||||
-- translation_validation: checks translation completeness across languages
|
||||
|
||||
-- ─── Validation view actions ────────────────────────────────
|
||||
|
||||
-- Site validation:
|
||||
-- Scans generated HTML for broken internal links, missing assets
|
||||
-- Results: list of issues with file path + line + description
|
||||
-- No auto-fix actions — informational only
|
||||
|
||||
-- Translation validation:
|
||||
-- Checks translation completeness across configured blog languages
|
||||
-- Results: matrix of posts x languages, missing translations highlighted
|
||||
-- No auto-fix actions — informational only
|
||||
|
||||
-- ─── Find duplicates view ────────────────────────────────────
|
||||
|
||||
-- Uses embedding vectors to find semantically similar posts.
|
||||
-- Shows candidate pairs with similarity score.
|
||||
-- See embedding.allium for vector details.
|
||||
|
||||
-- ─── Find duplicates actions ────────────────────────────────
|
||||
|
||||
-- Uses HNSW index of post embedding vectors for similarity search
|
||||
-- Shows candidate pairs: post A title, post B title, score (0.0-1.0)
|
||||
-- Click on either post title opens it in a tab (preview intent)
|
||||
-- No auto-merge or auto-delete actions
|
||||
|
||||
-- ─── 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)
|
||||
}
|
||||
243
specs/editor_post.allium
Normal file
243
specs/editor_post.allium
Normal file
@@ -0,0 +1,243 @@
|
||||
-- allium: 1
|
||||
-- bDS Post Editor View
|
||||
-- Scope: UI content area — post editing surface
|
||||
-- Distilled from: PostEditor.tsx
|
||||
|
||||
-- Describes the layout and behaviour of the post editor rendered in
|
||||
-- the main content area when a post tab is active.
|
||||
-- Tab routing is in tabs.allium. Sidebar navigation is in sidebar_views.allium.
|
||||
|
||||
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), Author (text input),
|
||||
-- Language (select + AI detect button), Do Not Translate (checkbox),
|
||||
-- Slug (read-only), Categories (chip input), Template (select, if templates exist),
|
||||
-- PostLinks (backlinks and outlinks)
|
||||
-- Right column:
|
||||
-- LinkedMediaPanel (media items linked to this post)
|
||||
|
||||
-- Language flags bar (inline with metadata toggle):
|
||||
-- Row of flag emoji buttons for canonical language + each translation
|
||||
-- Styles: status class (draft/published), active indicator
|
||||
-- Clicking switches editor content to that language's draft
|
||||
|
||||
-- Collapsible Excerpt section:
|
||||
-- Excerpt textarea (4 rows)
|
||||
|
||||
-- Editor Body:
|
||||
-- Toolbar: "Content" label | mode toggle (Visual/Markdown/Preview) | action buttons
|
||||
-- Action buttons (markdown mode only): Gallery, Insert Post Link, Insert Media
|
||||
-- Visual mode: rich-text WYSIWYG editor
|
||||
-- Markdown mode: code editor with markdown-with-macros language
|
||||
-- Highlights [[macro ...]] double-bracket syntax
|
||||
-- Word wrap on, minimap off, 14px font
|
||||
-- Preview mode: iframe showing rendered preview
|
||||
-- Supports drag-and-drop of images (imports media, inserts markdown)
|
||||
|
||||
-- Footer:
|
||||
-- Three date stamps: Created, Updated, Published (published only if publishedAt exists)
|
||||
-- Locale-formatted dates
|
||||
|
||||
value PostEditorView {
|
||||
post_id: String
|
||||
header: PostEditorHeader
|
||||
metadata_expanded: Boolean
|
||||
excerpt_expanded: Boolean
|
||||
editor_mode: String -- visual | markdown | preview
|
||||
}
|
||||
|
||||
value PostEditorHeader {
|
||||
title: String -- post title or "Untitled"
|
||||
is_dirty: Boolean
|
||||
status: String -- draft | published | archived
|
||||
is_auto_saving: Boolean
|
||||
}
|
||||
|
||||
config {
|
||||
post_auto_save_delay: Integer = 3000
|
||||
-- milliseconds of idle before auto-save triggers
|
||||
}
|
||||
|
||||
invariant PostAutoSave {
|
||||
-- Auto-saves after 3 seconds of idle in the editor
|
||||
-- Also auto-saves on unmount/tab switch
|
||||
-- Ctrl/Cmd+S triggers immediate save
|
||||
}
|
||||
|
||||
invariant PostDirtyTracking {
|
||||
-- Compares canonical draft + translation drafts against saved state
|
||||
-- Dirty indicator shown in header and tab bar
|
||||
}
|
||||
|
||||
-- ─── Post editor actions ────────────────────────────────────
|
||||
|
||||
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)
|
||||
-- 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 action_patterns.allium AutoTranslationChain for full chain
|
||||
}
|
||||
|
||||
rule PostPublishAction {
|
||||
when: PostPublishRequested(post_id)
|
||||
-- Implicit save first (awaited) if post is dirty
|
||||
-- Then calls engine publish (see engine_side_effects.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 engine_side_effects.allium DeletePostSideEffects
|
||||
}
|
||||
|
||||
rule PostInsertLink {
|
||||
when: PostInsertLinkRequested(post_id)
|
||||
-- Keyboard shortcut: Ctrl/Cmd+K
|
||||
-- Opens InsertPostLinkModal 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 InsertMediaModal (media search variant)
|
||||
-- Search input, grid of media items with bds-thumb:// thumbnails
|
||||
-- Click inserts markdown:
|
||||
-- Images: 
|
||||
-- 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 action_patterns.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: 
|
||||
-- 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
|
||||
50
specs/editor_script.allium
Normal file
50
specs/editor_script.allium
Normal file
@@ -0,0 +1,50 @@
|
||||
-- allium: 1
|
||||
-- bDS Script Editor View
|
||||
-- Scope: UI content area — script editing surface
|
||||
-- Distilled from: ScriptEditor.tsx
|
||||
|
||||
-- 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)
|
||||
}
|
||||
|
||||
-- ─── Script editor view ──────────────────────────────────────
|
||||
|
||||
-- Code editor for Lua scripts (macros, utilities, transforms).
|
||||
|
||||
-- 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 engine_side_effects.allium UpdateTemplateSideEffects (same pattern)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
138
specs/editor_settings.allium
Normal file
138
specs/editor_settings.allium
Normal file
@@ -0,0 +1,138 @@
|
||||
-- allium: 1
|
||||
-- bDS Settings and Style Views
|
||||
-- Scope: UI content area — settings + style editing surfaces
|
||||
-- Distilled from: SettingsView.tsx, StyleView.tsx
|
||||
|
||||
-- Describes the layout and behaviour of the settings and style views.
|
||||
|
||||
-- ─── External surfaces ──────────────────────────────────────
|
||||
|
||||
surface User {
|
||||
provides: StyleThemeSelected(theme_name)
|
||||
provides: StyleApplyRequested(theme_name)
|
||||
}
|
||||
|
||||
-- ─── Settings view ────────────────────────────────────────────
|
||||
|
||||
-- VS Code-style settings page with header + search bar + scrollable sections.
|
||||
-- Search filters sections by keyword match. "No results" with clear button.
|
||||
|
||||
-- 8 sections (style is a separate tab, not a settings section):
|
||||
|
||||
-- 1. Project Settings:
|
||||
-- Project Name (text), Project Description (textarea 3 rows),
|
||||
-- Data Path (text + Browse + Reset), Public URL (url),
|
||||
-- Main Language (select), Blog Languages (checkboxes, main disabled),
|
||||
-- Default Author (text), Max Posts Per Page (number 1-500 default 50),
|
||||
-- Blogmark Category (select), Blogmark Bookmarklet (copy button), Save
|
||||
|
||||
-- 2. Editor Settings:
|
||||
-- Default Editor Mode (select: WYSIWYG/Markdown/Preview),
|
||||
-- Diff View Style (select: Inline/Side-by-side),
|
||||
-- Wrap Long Lines (checkbox), Hide Unchanged Regions (checkbox)
|
||||
|
||||
-- 3. Content Settings (Categories):
|
||||
-- Table: Category | Title | Render in Lists | Show Titles | Post Template | List Template | Remove
|
||||
-- Protected categories: article, aside, page, picture (cannot be deleted)
|
||||
-- Add Category: text input + Add button. Reset to Defaults button
|
||||
|
||||
-- 4. AI Assistant Settings:
|
||||
-- API keys: OpenCode, Mistral (masked display + Change, or password input + Save)
|
||||
-- Local providers: Ollama enable checkbox + capabilities table,
|
||||
-- LM Studio enable checkbox + capabilities table
|
||||
-- Offline mode: enable checkbox (disabled if no local providers),
|
||||
-- selectors for chat/title/image models (local only)
|
||||
-- Default Model (select with optgroup by provider + refresh),
|
||||
-- shows: max output tokens, context window, pricing
|
||||
-- Title Model (select), Image Analysis Model (vision-capable only)
|
||||
-- System Prompt (textarea 12 rows + Save + Reset to Default)
|
||||
|
||||
-- 5. Technology Settings:
|
||||
-- Python Runtime Mode (select: Web Worker/Main Thread)
|
||||
-- Semantic Similarity (checkbox)
|
||||
|
||||
-- 6. Publishing Settings (SSH):
|
||||
-- Info: SSH key auth only
|
||||
-- SSH Mode (select: scp/rsync), SSH Host, SSH Username, SSH Remote Path
|
||||
-- Save + Clear buttons
|
||||
|
||||
-- 7. Data Maintenance:
|
||||
-- Rebuild buttons (6): Posts from Files, Media from Files, Scripts from Files,
|
||||
-- Templates from Files, Links, Regenerate Missing Thumbnails
|
||||
-- Open Data Folder button
|
||||
|
||||
-- 8. MCP Server Settings:
|
||||
-- Status badge (port or "Not running")
|
||||
-- Per-agent rows: Claude Code, Claude Desktop, GitHub Copilot, Gemini CLI,
|
||||
-- OpenCode, Mistral Vibe, OpenAI Codex — each with Add/Remove toggle
|
||||
|
||||
-- ─── Settings view actions ──────────────────────────────────
|
||||
|
||||
-- API key validation:
|
||||
-- On Save: test API call to provider endpoint before persisting
|
||||
-- On validation failure: toast error message, key not saved
|
||||
-- On success: key saved, masked display shown
|
||||
|
||||
-- Local provider enable/disable:
|
||||
-- Enabling Ollama/LM Studio: fetches model list, shows capabilities table
|
||||
-- Capabilities table columns: model name, chat, title, image (checkmarks)
|
||||
-- Models auto-fetched on enable; manual refresh via Refresh Models button
|
||||
|
||||
-- Model catalog refresh:
|
||||
-- Refresh button next to Default Model selector
|
||||
-- Fetches available models from all enabled providers
|
||||
-- Updates optgroup dropdown with provider groupings
|
||||
-- Per-model info: max output tokens, context window, pricing
|
||||
|
||||
-- Rebuild operations (Data Maintenance section):
|
||||
-- 6 buttons, each executes immediately (no confirmation dialog)
|
||||
-- Runs as background task with progress visible in Tasks panel
|
||||
-- On completion: wholesale replaces store data for that entity type
|
||||
-- Sidebar and editors re-render with fresh data
|
||||
|
||||
-- Category management (Content Settings):
|
||||
-- Protected categories: article, aside, page, picture (Remove button disabled)
|
||||
-- Add Category: text input + Add button, validates non-empty and unique
|
||||
-- Per-category settings: editable inline in table row
|
||||
-- "Reset to Defaults" restores default categories set
|
||||
|
||||
-- MCP per-agent config:
|
||||
-- Add/Remove toggle per agent row
|
||||
-- Status badge: shows port number when running, "Not running" otherwise
|
||||
|
||||
-- Bookmarklet (Project Settings):
|
||||
-- Copy button copies bookmarklet JavaScript to clipboard
|
||||
-- Bookmarklet uses project's publicUrl to construct POST endpoint
|
||||
|
||||
-- ─── Style view ───────────────────────────────────────────────
|
||||
|
||||
-- Simple vertical layout: header, theme picker, controls row, preview.
|
||||
|
||||
-- Theme Picker: grid of theme buttons (one per Pico CSS theme).
|
||||
-- Each button: swatch with 3 colour tones (accent, light bg, dark bg), theme name.
|
||||
-- Selected theme: highlighted with aria-pressed.
|
||||
|
||||
-- Controls row: Preview Mode dropdown (Auto/Light/Dark), Apply Theme button.
|
||||
|
||||
-- Preview: iframe of style preview page at 127.0.0.1:4123/__style-preview
|
||||
-- with theme and mode query params. Updates live on selection change.
|
||||
|
||||
-- Apply Theme persists to project metadata.
|
||||
|
||||
-- ─── Style view actions ─────────────────────────────────────
|
||||
|
||||
rule StyleThemeSelect {
|
||||
when: StyleThemeSelected(theme_name)
|
||||
-- Updates iframe preview immediately (query param change)
|
||||
-- Does NOT persist until Apply is clicked
|
||||
}
|
||||
|
||||
rule StyleApplyTheme {
|
||||
when: StyleApplyRequested(theme_name)
|
||||
-- Persists picoTheme to project metadata (meta/project.json)
|
||||
-- See 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
|
||||
65
specs/editor_tags.allium
Normal file
65
specs/editor_tags.allium
Normal file
@@ -0,0 +1,65 @@
|
||||
-- allium: 1
|
||||
-- bDS Tags View
|
||||
-- Scope: UI content area — tag management surface
|
||||
-- Distilled from: TagsView.tsx
|
||||
|
||||
-- Describes the layout and behaviour of the tags view.
|
||||
|
||||
-- ─── Tags view ────────────────────────────────────────────────
|
||||
|
||||
-- Three sections controlled by sidebar TagsNav:
|
||||
|
||||
-- 1. Cloud: tag cloud visualization. Tags sized by post count,
|
||||
-- coloured by tag colour. Clickable to select for editing.
|
||||
|
||||
-- 2. Manage (Create/Edit): tag name input, colour picker, template select.
|
||||
-- Edit existing tag or create new. Delete button with confirmation.
|
||||
|
||||
-- 3. Merge: source tag select, target tag select, Merge button.
|
||||
-- Merges all posts from source tag into target, removes source.
|
||||
|
||||
-- ─── Tags view actions ──────────────────────────────────────
|
||||
|
||||
-- Cloud section:
|
||||
-- Tags sized by post count (same 11-22px scaling as dashboard)
|
||||
-- Tags with colours get coloured background
|
||||
-- Click selects tag: switches to Manage section, loads tag data
|
||||
|
||||
-- Manage section:
|
||||
-- Create new: empty form, Save creates tag + writes tags.json
|
||||
-- Edit existing: loads tag data into form, Save updates
|
||||
-- Tag name input, colour picker, template select (optional)
|
||||
-- Colour picker: 17 preset colour swatches + custom hex input field
|
||||
-- Delete: opens ConfirmDialog showing post count
|
||||
-- "This tag is used in N posts. Delete anyway?"
|
||||
-- On confirm: background task removes tag from all posts,
|
||||
-- rewrites published .md files, deletes tag, writes tags.json
|
||||
|
||||
-- Merge section:
|
||||
-- Source tag multi-select, target tag single-select
|
||||
-- Merge button: opens ConfirmDialog
|
||||
-- "Merge N tags into {target}? This cannot be undone."
|
||||
-- On confirm: background task updates all affected posts,
|
||||
-- rewrites published .md files, deletes source tags, writes tags.json
|
||||
|
||||
-- Sync (maintenance action):
|
||||
-- Rewrites tags.json from current DB state
|
||||
|
||||
-- ─── Colour picker popover ──────────────────────────────────
|
||||
|
||||
value ColourPickerPopover {
|
||||
-- Inline popover positioned below tag colour field (tags view manage).
|
||||
-- Not a modal — does not dim backdrop, no ESC/backdrop close.
|
||||
-- Closes when clicking outside popover or selecting a colour.
|
||||
-- Grid of 17 preset colour swatches (4x4 + 1).
|
||||
-- Below grid: custom hex input field (#RRGGBB).
|
||||
-- Selection is immediate — no confirm/cancel buttons.
|
||||
-- Choosing a colour updates the tag form's colour field live.
|
||||
presets: List<String> -- 17 hex colour values
|
||||
custom_hex: String?
|
||||
selected: String?
|
||||
}
|
||||
|
||||
config {
|
||||
colour_picker_preset_count: Integer = 17
|
||||
}
|
||||
46
specs/editor_template.allium
Normal file
46
specs/editor_template.allium
Normal file
@@ -0,0 +1,46 @@
|
||||
-- allium: 1
|
||||
-- bDS Template Editor View
|
||||
-- Scope: UI content area — template editing surface
|
||||
-- Distilled from: TemplateEditor.tsx
|
||||
|
||||
-- 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)
|
||||
}
|
||||
|
||||
-- ─── Template editor view ─────────────────────────────────────
|
||||
|
||||
-- Code editor for Liquid templates (post, list, not_found, partial).
|
||||
|
||||
-- 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 engine_side_effects.allium 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
|
||||
}
|
||||
1142
specs/editors.allium
1142
specs/editors.allium
File diff suppressed because it is too large
Load Diff
314
specs/engine_side_effects.allium
Normal file
314
specs/engine_side_effects.allium
Normal file
@@ -0,0 +1,314 @@
|
||||
-- allium: 1
|
||||
-- bDS Engine-Level Save Side-Effects
|
||||
-- Scope: cross-cutting (all waves)
|
||||
-- Distilled from: PostEngine.ts, MediaEngine.ts, TemplateEngine.ts,
|
||||
-- ScriptEngine.ts, MetaEngine.ts, TagEngine.ts
|
||||
|
||||
-- When an entity is saved/published/deleted in the engine layer, a chain
|
||||
-- of automatic side-effects fires. These are NOT UI-level concerns —
|
||||
-- they happen in the backend regardless of which UI triggered them.
|
||||
|
||||
use "./post.allium" as post
|
||||
use "./media.allium" as media
|
||||
|
||||
-- ─── External surfaces ──────────────────────────────────────
|
||||
|
||||
-- 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)
|
||||
}
|
||||
|
||||
-- ─── Post operations ─────────────────────────────────────
|
||||
|
||||
rule CreatePostSideEffects {
|
||||
when: PostCreated(post)
|
||||
ensures: FTSIndexUpdated(post)
|
||||
ensures: EmbeddingUpdated(post)
|
||||
-- No file written (draft lives in DB)
|
||||
}
|
||||
|
||||
rule UpdatePostSideEffects {
|
||||
when: PostUpdated(post, changes)
|
||||
-- If post is published and content/metadata changes:
|
||||
-- auto-transition status back to draft
|
||||
-- If slug changed and file exists: rename .md file
|
||||
-- If templateSlug changed on published post: rewrite .md frontmatter
|
||||
ensures: FTSIndexUpdated(post)
|
||||
if changes.content:
|
||||
ensures: PostLinksUpdated(post)
|
||||
-- Parses markdown/HTML links, resolves slugs to post IDs,
|
||||
-- replaces outgoing link rows
|
||||
ensures: EmbeddingUpdated(post)
|
||||
}
|
||||
|
||||
rule PublishPostSideEffects {
|
||||
when: PostPublished(post)
|
||||
ensures: PostFileWritten(post)
|
||||
-- posts/YYYY/MM/{slug}.md with YAML frontmatter
|
||||
if old_file_path != new_file_path:
|
||||
ensures: OldPostFileDeleted(old_file_path)
|
||||
ensures: post.content = null
|
||||
-- Content cleared from DB; lives in filesystem only
|
||||
ensures: FTSIndexUpdated(post)
|
||||
ensures: PostLinksUpdated(post)
|
||||
-- Also publishes all translations:
|
||||
for t in post.translations:
|
||||
ensures: TranslationFileWritten(t)
|
||||
ensures: t.content = null
|
||||
ensures: EmbeddingUpdated(post)
|
||||
}
|
||||
|
||||
rule DeletePostSideEffects {
|
||||
when: PostDeleted(post)
|
||||
if post.file_path != "":
|
||||
ensures: PostFileDeleted(post.file_path)
|
||||
ensures: PostLinksDeleted(post)
|
||||
-- Deletes both source and target link rows
|
||||
for media_link in post.linked_media:
|
||||
ensures: MediaSidecarUpdated(media_link.media_id)
|
||||
-- Removes post from media sidecar's linkedPostIds
|
||||
ensures: FTSIndexDeleted(post)
|
||||
ensures: EmbeddingRemoved(post)
|
||||
}
|
||||
|
||||
rule DiscardPostChangesSideEffects {
|
||||
when: PostChangesDiscarded(post)
|
||||
-- Reads published version from file, restores DB metadata,
|
||||
-- sets content=null, status=published
|
||||
ensures: FTSIndexUpdated(post)
|
||||
}
|
||||
|
||||
-- ─── Media operations ────────────────────────────────────
|
||||
|
||||
rule ImportMediaSideEffects {
|
||||
when: MediaImported(media)
|
||||
ensures: MediaFileWritten(media)
|
||||
-- media/YYYY/MM/{uuid}.{ext}
|
||||
ensures: SidecarFileWritten(media)
|
||||
-- {path}.meta with YAML-like metadata
|
||||
if media.is_image:
|
||||
ensures: ThumbnailsGenerated(media)
|
||||
-- small=150px, medium=400px, large=800px, ai=448x448
|
||||
-- Asynchronous, emits thumbnailsGenerated on completion
|
||||
ensures: FTSIndexUpdated(media)
|
||||
}
|
||||
|
||||
rule UpdateMediaSideEffects {
|
||||
when: MediaUpdated(media, changes)
|
||||
ensures: SidecarFileRewritten(media)
|
||||
-- Preserves fields caller didn't set (linkedPostIds, author)
|
||||
ensures: FTSIndexUpdated(media)
|
||||
}
|
||||
|
||||
rule ReplaceMediaFileSideEffects {
|
||||
when: MediaFileReplaced(media, new_file)
|
||||
-- Copies new file over existing path
|
||||
if media.is_image:
|
||||
ensures: ThumbnailsRegenerated(media)
|
||||
-- Synchronous (awaited), not fire-and-forget
|
||||
}
|
||||
|
||||
rule DeleteMediaSideEffects {
|
||||
when: MediaDeleted(media)
|
||||
ensures: MediaFileDeleted(media)
|
||||
ensures: SidecarFileDeleted(media)
|
||||
ensures: ThumbnailsDeleted(media)
|
||||
ensures: PostMediaLinksDeleted(media)
|
||||
ensures: MediaTranslationsDeleted(media)
|
||||
-- Also deletes all translated sidecar files: {path}.{lang}.meta
|
||||
ensures: FTSIndexDeleted(media)
|
||||
}
|
||||
|
||||
-- ─── Template operations ─────────────────────────────────
|
||||
|
||||
rule CreateTemplateSideEffects {
|
||||
when: TemplateCreated(template)
|
||||
ensures: TemplateFileWritten(template)
|
||||
-- templates/{slug}.liquid with YAML frontmatter
|
||||
}
|
||||
|
||||
rule UpdateTemplateSideEffects {
|
||||
when: TemplateUpdated(template, changes)
|
||||
ensures: template.version = template.version + 1
|
||||
-- DB-first update, then filesystem; rollback DB on filesystem failure
|
||||
if changes.slug:
|
||||
ensures: TemplateFileRenamed(template)
|
||||
ensures: CascadeSlugUpdate(template)
|
||||
-- Updates posts.templateSlug and tags.postTemplateSlug
|
||||
ensures: TemplateFileRewritten(template)
|
||||
}
|
||||
|
||||
rule PublishTemplateSideEffects {
|
||||
when: TemplatePublished(template)
|
||||
ensures: TemplateFileWritten(template)
|
||||
ensures: template.content = null
|
||||
-- Content cleared from DB
|
||||
}
|
||||
|
||||
rule DeleteTemplateSideEffects {
|
||||
when: TemplateDeleted(template, force)
|
||||
if has_references and not force:
|
||||
-- Return without deleting, report reference counts
|
||||
ensures: nothing
|
||||
if force:
|
||||
ensures: ReferencingPostsCleared(template)
|
||||
ensures: ReferencingTagsCleared(template)
|
||||
-- Nulls out templateSlug on posts, postTemplateSlug on tags
|
||||
ensures: TemplateFileDeleted(template)
|
||||
}
|
||||
|
||||
-- ─── Script operations ───────────────────────────────────
|
||||
|
||||
-- Same pattern as templates:
|
||||
-- Create: write .lua file, insert DB
|
||||
-- Update: bump version, rewrite file, update DB
|
||||
-- Publish: write file, clear DB content
|
||||
-- Delete: delete file, delete DB row
|
||||
|
||||
-- ─── Tag operations ──────────────────────────────────────
|
||||
|
||||
rule DeleteTagSideEffects {
|
||||
when: TagDeleted(tag)
|
||||
-- Background task:
|
||||
-- For each post containing this tag:
|
||||
-- Remove tag from post's tags array in DB
|
||||
-- If published: rewrite .md file (syncPublishedPostFile)
|
||||
ensures: TagsJsonWritten()
|
||||
-- meta/tags.json updated
|
||||
}
|
||||
|
||||
rule RenameTagSideEffects {
|
||||
when: TagRenamed(old_name, new_name)
|
||||
-- Background task:
|
||||
-- For each post containing old_name:
|
||||
-- Replace old_name with new_name in tags array
|
||||
-- If published: rewrite .md file
|
||||
ensures: TagsJsonWritten()
|
||||
}
|
||||
|
||||
rule MergeTagsSideEffects {
|
||||
when: TagsMerged(source_tags, target_tag)
|
||||
-- Background task:
|
||||
-- For each source tag, for each post containing it:
|
||||
-- Replace source with target (dedup), update DB
|
||||
-- If published: rewrite .md file
|
||||
-- Delete all source tag rows
|
||||
ensures: TagsJsonWritten()
|
||||
}
|
||||
|
||||
-- ─── Settings/Metadata operations ────────────────────────
|
||||
|
||||
rule UpdateProjectMetadataSideEffects {
|
||||
when: ProjectMetadataUpdated(metadata)
|
||||
ensures: ProjectJsonWritten()
|
||||
-- meta/project.json (atomic write)
|
||||
ensures: CategoryMetaJsonWritten()
|
||||
-- meta/category-meta.json (atomic write)
|
||||
}
|
||||
|
||||
rule AddCategorySideEffects {
|
||||
when: CategoryAdded(name)
|
||||
ensures: ProjectJsonWritten()
|
||||
ensures: CategoryMetaJsonWritten()
|
||||
ensures: CategoriesJsonWritten()
|
||||
-- meta/categories.json
|
||||
}
|
||||
|
||||
rule RemoveCategorySideEffects {
|
||||
when: CategoryRemoved(name)
|
||||
ensures: ProjectJsonWritten()
|
||||
ensures: CategoryMetaJsonWritten()
|
||||
ensures: CategoriesJsonWritten()
|
||||
}
|
||||
|
||||
rule UpdatePublishingPreferencesSideEffects {
|
||||
when: PublishingPreferencesUpdated(prefs)
|
||||
ensures: PublishingJsonWritten()
|
||||
-- meta/publishing.json (atomic write)
|
||||
}
|
||||
|
||||
-- ─── Translation operations ──────────────────────────────
|
||||
|
||||
rule UpsertPostTranslationSideEffects {
|
||||
when: PostTranslationUpserted(translation, source_post)
|
||||
-- If source is published and this is a manual edit (not auto-publish):
|
||||
-- transition source post to draft (copies content from file to DB)
|
||||
if source_post.status = published and translation.is_manual_edit:
|
||||
ensures: source_post.status = draft
|
||||
ensures: source_post.content = read_file(source_post.file_path)
|
||||
-- If both translation and source are published:
|
||||
-- write translation file, clear translation content from DB
|
||||
if translation.status = published and source_post.status = published:
|
||||
ensures: TranslationFileWritten(translation)
|
||||
ensures: translation.content = null
|
||||
ensures: FTSIndexUpdated(source_post)
|
||||
-- FTS includes all translation content for the source post
|
||||
}
|
||||
|
||||
rule UpsertMediaTranslationSideEffects {
|
||||
when: MediaTranslationUpserted(translation, media)
|
||||
ensures: TranslatedSidecarWritten(media, translation.language)
|
||||
-- {path}.{lang}.meta
|
||||
}
|
||||
|
||||
rule DeleteMediaTranslationSideEffects {
|
||||
when: MediaTranslationDeleted(media, language)
|
||||
ensures: TranslatedSidecarDeleted(media, language)
|
||||
}
|
||||
|
||||
-- ─── CLI/MCP notification sync ───────────────────────────
|
||||
|
||||
-- When MCP CLI makes mutations, it writes to db_notifications table.
|
||||
-- NotificationWatcher polls DB file (chokidar, 100ms debounce):
|
||||
-- Reads unseen CLI notifications
|
||||
-- Calls engine.invalidate(entityId) if needed
|
||||
-- Sends entity:changed IPC event to renderer
|
||||
-- Marks rows as seen
|
||||
-- Prunes: >1h processed, >24h unprocessed
|
||||
|
||||
-- ─── Side-effect summary table ───────────────────────────
|
||||
|
||||
-- Operation | File Write | FTS | Links | Thumbs | Sidecar | Embed | JSON Meta
|
||||
-- -------------------|--------------|------|-------|--------|---------|-------|----------
|
||||
-- createPost | no (draft) | yes | no | no | no | yes | no
|
||||
-- updatePost | rename only* | yes | if Δ | no | no | yes | no
|
||||
-- publishPost | .md + trans | yes | yes | no | no | yes | no
|
||||
-- deletePost | delete .md | del | del | no | Δ media | del | no
|
||||
-- importMedia | copy file | yes | no | async | write | no | no
|
||||
-- updateMedia | no | yes | no | no | rewrite | no | no
|
||||
-- replaceMediaFile | overwrite | no | no | regen | no | no | no
|
||||
-- deleteMedia | delete all | del | no | del | del all | no | no
|
||||
-- createTemplate | .liquid | no | no | no | no | no | no
|
||||
-- updateTemplate | rewrite | no | no | no | no | no | no
|
||||
-- deleteTemplate | delete+casc | no | no | no | no | no | no
|
||||
-- deleteTag | sync posts | no | no | no | no | no | tags.json
|
||||
-- renameTag | sync posts | no | no | no | no | no | tags.json
|
||||
-- mergeTags | sync posts | no | no | no | no | no | tags.json
|
||||
-- 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
|
||||
@@ -1,640 +0,0 @@
|
||||
-- allium: 1
|
||||
-- bDS UI/UX Flows and Save Side-Effects
|
||||
-- Scope: cross-cutting (all waves)
|
||||
-- Distilled from: appStore.ts, PostEditor.tsx, MediaEditor.tsx,
|
||||
-- SettingsView.tsx, PostEngine.ts, MediaEngine.ts, TemplateEngine.ts,
|
||||
-- ScriptEngine.ts, MetaEngine.ts, TagEngine.ts, NotificationWatcher.ts
|
||||
|
||||
-- Describes the reactive data flows that connect sidebar, editor, tab bar,
|
||||
-- and backend engine operations. Covers: sidebar->editor, editor->sidebar,
|
||||
-- cross-tab coordination, and automatic side-effects on save/publish/delete.
|
||||
|
||||
use "./layout.allium" as layout
|
||||
use "./tabs.allium" as tabs
|
||||
use "./sidebar_views.allium" as sidebar
|
||||
use "./editors.allium" as editors
|
||||
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
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
-- All UI coordination flows through a shared reactive store (Zustand in TS,
|
||||
-- Iced subscriptions in Rust). There are no direct calls between sidebar
|
||||
-- and editor. Both read from and write to the store; changes propagate
|
||||
-- reactively.
|
||||
|
||||
-- ─── 1.1 Sidebar -> Editor flows ──────────────────────────────
|
||||
|
||||
rule SidebarEntityClick {
|
||||
when: SidebarItemClicked(entity_type, entity_id, click_type)
|
||||
-- Single click: open as transient/preview tab
|
||||
-- Double click: open as pinned tab
|
||||
-- See sidebar_views.allium for per-view click rules
|
||||
-- Effect: Editor mounts the matching view keyed by entity_id
|
||||
-- Effect: Sidebar item highlights based on activeTabId match
|
||||
-- If a prior transient tab of same type exists, it is replaced,
|
||||
-- and the old editor unmounts (triggering auto-save if dirty)
|
||||
}
|
||||
|
||||
rule SidebarCreateEntity {
|
||||
when: SidebarCreateRequested(entity_type)
|
||||
-- Posts/Pages: backend creates post, emits post:created,
|
||||
-- store adds to posts list, sidebar re-renders with new item.
|
||||
-- Does NOT open a tab automatically. User must click to open.
|
||||
-- Sets selectedPostId for highlighting, optionally ensures sidebar visible.
|
||||
-- Scripts/Templates: backend creates, emits event, opens tab immediately.
|
||||
-- Chat: creates conversation, opens as pinned tab.
|
||||
-- Import: creates definition, opens as pinned tab.
|
||||
}
|
||||
|
||||
rule SidebarDeleteEntity {
|
||||
when: SidebarDeleteRequested(entity_type, entity_id)
|
||||
-- Scripts/Templates/Chat/Import: backend deletes entity,
|
||||
-- then closeTab(entity_id) removes its tab,
|
||||
-- activates adjacent tab or shows dashboard.
|
||||
-- Posts/Media: deletion is triggered from the EDITOR (not sidebar).
|
||||
-- See PostEditorDelete / MediaEditorDelete below.
|
||||
}
|
||||
|
||||
invariant SidebarFilterIsolation {
|
||||
-- Sidebar search/filter state is local to the sidebar component.
|
||||
-- Filtering never affects: active tab, editor content, selectedPostId.
|
||||
-- Only the visible list of items changes.
|
||||
}
|
||||
|
||||
-- ─── 1.2 Editor -> Sidebar flows ──────────────────────────────
|
||||
|
||||
rule PostTitleChanged {
|
||||
when: PostSaved(post_id, updated_post)
|
||||
-- Auto-save fires after 3s idle, or on Ctrl+S, or on unmount/tab switch
|
||||
-- Store's posts array is updated with new title/metadata
|
||||
-- Sidebar PostsList re-renders reactively showing new title
|
||||
-- TabBar re-renders showing new title
|
||||
ensures: dirtyPosts.remove(post_id)
|
||||
}
|
||||
|
||||
rule PostStatusChanged {
|
||||
when: PostStatusTransitioned(post_id, old_status, new_status)
|
||||
-- Store's posts array is updated with new status
|
||||
-- Sidebar detects status change (compares prev/current status maps)
|
||||
-- and re-runs search/filter if active
|
||||
-- Post moves between draft/published/archived sections in sidebar
|
||||
}
|
||||
|
||||
rule PostEditorDelete {
|
||||
when: PostDeletedFromEditor(post_id)
|
||||
ensures: store.removePost(post_id)
|
||||
-- Removes from posts array, clears selectedPostId if matching,
|
||||
-- removes from dirtyPosts
|
||||
ensures: closeTab(post_id)
|
||||
-- Sidebar re-renders without the post
|
||||
}
|
||||
|
||||
rule MediaEditorSave {
|
||||
when: MediaSavedFromEditor(media_id, updated_media)
|
||||
ensures: store.updateMedia(media_id, updated_media)
|
||||
-- Sidebar MediaList re-renders with updated metadata
|
||||
}
|
||||
|
||||
rule MediaEditorDelete {
|
||||
when: MediaDeletedFromEditor(media_id)
|
||||
ensures: store.removeMedia(media_id)
|
||||
-- Editor.tsx safety net: detects active media tab references
|
||||
-- non-existent item, calls closeTab(activeTab.id)
|
||||
-- Sidebar re-renders without the media item
|
||||
}
|
||||
|
||||
rule SettingsRebuild {
|
||||
when: SettingsRebuildCompleted(entity_type, new_data)
|
||||
-- Wholesale replacement of posts or media array in store
|
||||
ensures: store.setPosts(new_data) or store.setMedia(new_data)
|
||||
-- Sidebar re-renders entirely with fresh data
|
||||
}
|
||||
|
||||
-- ─── 1.3 Cross-tab coordination ──────────────────────────────
|
||||
|
||||
invariant TabSwitchDoesNotChangeSidebarView {
|
||||
-- Switching tabs does NOT change activeView in the sidebar.
|
||||
-- The sidebar view is controlled exclusively by the Activity Bar.
|
||||
-- Exception: Dashboard post click explicitly sets activeView = "posts".
|
||||
}
|
||||
|
||||
invariant SidebarHighlightFollowsActiveTab {
|
||||
-- Sidebar item highlight is based on activeTabId === entity.id,
|
||||
-- NOT on selectedPostId/selectedMediaId (which are separate concepts
|
||||
-- used only for post creation flow).
|
||||
}
|
||||
|
||||
rule TransientTabReplacement {
|
||||
when: TransientTabBeingReplaced(old_tab, new_tab)
|
||||
-- Old editor unmounts -> cleanup auto-save fires if dirty
|
||||
-- New editor mounts with new entity
|
||||
-- Sidebar highlight shifts to newly active entity
|
||||
}
|
||||
|
||||
rule TabCloseCleanup {
|
||||
when: TabClosed(tab)
|
||||
-- If post tab: PostEditor unmounts, auto-save fires if dirty
|
||||
-- Store activates adjacent tab (prefer right, then left, then null)
|
||||
-- Sidebar highlight updates to new active tab
|
||||
-- If no tabs remain: dashboard shown
|
||||
}
|
||||
|
||||
rule DashboardPostClick {
|
||||
when: DashboardPostClicked(post_id, click_type)
|
||||
-- This is the ONLY place where opening a tab also switches sidebar view
|
||||
ensures: setActiveView("posts")
|
||||
ensures: setSelectedPost(post_id)
|
||||
if click_type = single:
|
||||
ensures: OpenTabRequested(type: post, id: post_id, intent: preview)
|
||||
if click_type = double:
|
||||
ensures: OpenTabRequested(type: post, id: post_id, intent: pin)
|
||||
}
|
||||
|
||||
-- ─── 1.4 Backend -> UI event propagation ─────────────────────
|
||||
|
||||
-- Backend engines emit events via IPC. The renderer listens and updates
|
||||
-- the shared store. Both sidebar and editor re-render reactively.
|
||||
-- Events: post:created, post:updated, post:deleted,
|
||||
-- media:imported, media:updated, media:deleted,
|
||||
-- template:created/updated/deleted, script:created/updated/deleted,
|
||||
-- entity:changed (from CLI/MCP mutations via NotificationWatcher)
|
||||
|
||||
-- TabBar also listens directly for:
|
||||
-- post-updated (title refresh)
|
||||
-- bds:scripts-changed (script title refresh)
|
||||
-- BDS_EVENT_TEMPLATES_CHANGED (template title refresh)
|
||||
-- chat.onTitleUpdated (chat conversation title refresh)
|
||||
-- importDefinitions.onNameUpdated (import name refresh)
|
||||
|
||||
-- Editor.tsx has safety-net useEffect guards that:
|
||||
-- Close media tabs when referenced media no longer exists in store
|
||||
-- Clear selectedPostId/selectedMediaId when entity is gone
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- PART 2: ENGINE-LEVEL SAVE SIDE-EFFECTS
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
-- When an entity is saved/published/deleted in the engine layer, a chain
|
||||
-- of automatic side-effects fires. These are NOT UI-level concerns --
|
||||
-- they happen in the backend regardless of which UI triggered them.
|
||||
|
||||
-- ─── 2.1 Post operations ─────────────────────────────────────
|
||||
|
||||
rule CreatePostSideEffects {
|
||||
when: PostCreated(post)
|
||||
ensures: FTSIndexUpdated(post)
|
||||
ensures: EmbeddingUpdated(post)
|
||||
-- No file written (draft lives in DB)
|
||||
}
|
||||
|
||||
rule UpdatePostSideEffects {
|
||||
when: PostUpdated(post, changes)
|
||||
-- If post is published and content/metadata changes:
|
||||
-- auto-transition status back to draft
|
||||
-- If slug changed and file exists: rename .md file
|
||||
-- If templateSlug changed on published post: rewrite .md frontmatter
|
||||
ensures: FTSIndexUpdated(post)
|
||||
if changes.content:
|
||||
ensures: PostLinksUpdated(post)
|
||||
-- Parses markdown/HTML links, resolves slugs to post IDs,
|
||||
-- replaces outgoing link rows
|
||||
ensures: EmbeddingUpdated(post)
|
||||
}
|
||||
|
||||
rule PublishPostSideEffects {
|
||||
when: PostPublished(post)
|
||||
ensures: PostFileWritten(post)
|
||||
-- posts/YYYY/MM/{slug}.md with YAML frontmatter
|
||||
if old_file_path != new_file_path:
|
||||
ensures: OldPostFileDeleted(old_file_path)
|
||||
ensures: post.content = null
|
||||
-- Content cleared from DB; lives in filesystem only
|
||||
ensures: FTSIndexUpdated(post)
|
||||
ensures: PostLinksUpdated(post)
|
||||
-- Also publishes all translations:
|
||||
for t in post.translations:
|
||||
ensures: TranslationFileWritten(t)
|
||||
ensures: t.content = null
|
||||
ensures: EmbeddingUpdated(post)
|
||||
}
|
||||
|
||||
rule DeletePostSideEffects {
|
||||
when: PostDeleted(post)
|
||||
if post.file_path != "":
|
||||
ensures: PostFileDeleted(post.file_path)
|
||||
ensures: PostLinksDeleted(post)
|
||||
-- Deletes both source and target link rows
|
||||
for media_link in post.linked_media:
|
||||
ensures: MediaSidecarUpdated(media_link.media_id)
|
||||
-- Removes post from media sidecar's linkedPostIds
|
||||
ensures: FTSIndexDeleted(post)
|
||||
ensures: EmbeddingRemoved(post)
|
||||
}
|
||||
|
||||
rule DiscardPostChangesSideEffects {
|
||||
when: PostChangesDiscarded(post)
|
||||
-- Reads published version from file, restores DB metadata,
|
||||
-- sets content=null, status=published
|
||||
ensures: FTSIndexUpdated(post)
|
||||
}
|
||||
|
||||
-- ─── 2.2 Media operations ────────────────────────────────────
|
||||
|
||||
rule ImportMediaSideEffects {
|
||||
when: MediaImported(media)
|
||||
ensures: MediaFileWritten(media)
|
||||
-- media/YYYY/MM/{uuid}.{ext}
|
||||
ensures: SidecarFileWritten(media)
|
||||
-- {path}.meta with YAML-like metadata
|
||||
if media.is_image:
|
||||
ensures: ThumbnailsGenerated(media)
|
||||
-- small=150px, medium=400px, large=800px, ai=448x448
|
||||
-- Asynchronous, emits thumbnailsGenerated on completion
|
||||
ensures: FTSIndexUpdated(media)
|
||||
}
|
||||
|
||||
rule UpdateMediaSideEffects {
|
||||
when: MediaUpdated(media, changes)
|
||||
ensures: SidecarFileRewritten(media)
|
||||
-- Preserves fields caller didn't set (linkedPostIds, author)
|
||||
ensures: FTSIndexUpdated(media)
|
||||
}
|
||||
|
||||
rule ReplaceMediaFileSideEffects {
|
||||
when: MediaFileReplaced(media, new_file)
|
||||
-- Copies new file over existing path
|
||||
if media.is_image:
|
||||
ensures: ThumbnailsRegenerated(media)
|
||||
-- Synchronous (awaited), not fire-and-forget
|
||||
}
|
||||
|
||||
rule DeleteMediaSideEffects {
|
||||
when: MediaDeleted(media)
|
||||
ensures: MediaFileDeleted(media)
|
||||
ensures: SidecarFileDeleted(media)
|
||||
ensures: ThumbnailsDeleted(media)
|
||||
ensures: PostMediaLinksDeleted(media)
|
||||
ensures: MediaTranslationsDeleted(media)
|
||||
-- Also deletes all translated sidecar files: {path}.{lang}.meta
|
||||
ensures: FTSIndexDeleted(media)
|
||||
}
|
||||
|
||||
-- ─── 2.3 Template operations ─────────────────────────────────
|
||||
|
||||
rule CreateTemplateSideEffects {
|
||||
when: TemplateCreated(template)
|
||||
ensures: TemplateFileWritten(template)
|
||||
-- templates/{slug}.liquid with YAML frontmatter
|
||||
}
|
||||
|
||||
rule UpdateTemplateSideEffects {
|
||||
when: TemplateUpdated(template, changes)
|
||||
ensures: template.version = template.version + 1
|
||||
-- DB-first update, then filesystem; rollback DB on filesystem failure
|
||||
if changes.slug:
|
||||
ensures: TemplateFileRenamed(template)
|
||||
ensures: CascadeSlugUpdate(template)
|
||||
-- Updates posts.templateSlug and tags.postTemplateSlug
|
||||
ensures: TemplateFileRewritten(template)
|
||||
}
|
||||
|
||||
rule PublishTemplateSideEffects {
|
||||
when: TemplatePublished(template)
|
||||
ensures: TemplateFileWritten(template)
|
||||
ensures: template.content = null
|
||||
-- Content cleared from DB
|
||||
}
|
||||
|
||||
rule DeleteTemplateSideEffects {
|
||||
when: TemplateDeleted(template, force)
|
||||
if has_references and not force:
|
||||
-- Return without deleting, report reference counts
|
||||
ensures: nothing
|
||||
if force:
|
||||
ensures: ReferencingPostsCleared(template)
|
||||
ensures: ReferencingTagsCleared(template)
|
||||
-- Nulls out templateSlug on posts, postTemplateSlug on tags
|
||||
ensures: TemplateFileDeleted(template)
|
||||
}
|
||||
|
||||
-- ─── 2.4 Script operations ───────────────────────────────────
|
||||
|
||||
-- Same pattern as templates:
|
||||
-- Create: write .lua file, insert DB
|
||||
-- Update: bump version, rewrite file, update DB
|
||||
-- Publish: write file, clear DB content
|
||||
-- Delete: delete file, delete DB row
|
||||
|
||||
-- ─── 2.5 Tag operations ──────────────────────────────────────
|
||||
|
||||
rule DeleteTagSideEffects {
|
||||
when: TagDeleted(tag)
|
||||
-- Background task:
|
||||
-- For each post containing this tag:
|
||||
-- Remove tag from post's tags array in DB
|
||||
-- If published: rewrite .md file (syncPublishedPostFile)
|
||||
ensures: TagsJsonWritten()
|
||||
-- meta/tags.json updated
|
||||
}
|
||||
|
||||
rule RenameTagSideEffects {
|
||||
when: TagRenamed(old_name, new_name)
|
||||
-- Background task:
|
||||
-- For each post containing old_name:
|
||||
-- Replace old_name with new_name in tags array
|
||||
-- If published: rewrite .md file
|
||||
ensures: TagsJsonWritten()
|
||||
}
|
||||
|
||||
rule MergeTagsSideEffects {
|
||||
when: TagsMerged(source_tags, target_tag)
|
||||
-- Background task:
|
||||
-- For each source tag, for each post containing it:
|
||||
-- Replace source with target (dedup), update DB
|
||||
-- If published: rewrite .md file
|
||||
-- Delete all source tag rows
|
||||
ensures: TagsJsonWritten()
|
||||
}
|
||||
|
||||
-- ─── 2.6 Settings/Metadata operations ────────────────────────
|
||||
|
||||
rule UpdateProjectMetadataSideEffects {
|
||||
when: ProjectMetadataUpdated(metadata)
|
||||
ensures: ProjectJsonWritten()
|
||||
-- meta/project.json (atomic write)
|
||||
ensures: CategoryMetaJsonWritten()
|
||||
-- meta/category-meta.json (atomic write)
|
||||
}
|
||||
|
||||
rule AddCategorySideEffects {
|
||||
when: CategoryAdded(name)
|
||||
ensures: ProjectJsonWritten()
|
||||
ensures: CategoryMetaJsonWritten()
|
||||
ensures: CategoriesJsonWritten()
|
||||
-- meta/categories.json
|
||||
}
|
||||
|
||||
rule RemoveCategorySideEffects {
|
||||
when: CategoryRemoved(name)
|
||||
ensures: ProjectJsonWritten()
|
||||
ensures: CategoryMetaJsonWritten()
|
||||
ensures: CategoriesJsonWritten()
|
||||
}
|
||||
|
||||
rule UpdatePublishingPreferencesSideEffects {
|
||||
when: PublishingPreferencesUpdated(prefs)
|
||||
ensures: PublishingJsonWritten()
|
||||
-- meta/publishing.json (atomic write)
|
||||
}
|
||||
|
||||
-- ─── 2.7 Translation operations ──────────────────────────────
|
||||
|
||||
rule UpsertPostTranslationSideEffects {
|
||||
when: PostTranslationUpserted(translation, source_post)
|
||||
-- If source is published and this is a manual edit (not auto-publish):
|
||||
-- transition source post to draft (copies content from file to DB)
|
||||
if source_post.status = published and translation.is_manual_edit:
|
||||
ensures: source_post.status = draft
|
||||
ensures: source_post.content = read_file(source_post.file_path)
|
||||
-- If both translation and source are published:
|
||||
-- write translation file, clear translation content from DB
|
||||
if translation.status = published and source_post.status = published:
|
||||
ensures: TranslationFileWritten(translation)
|
||||
ensures: translation.content = null
|
||||
ensures: FTSIndexUpdated(source_post)
|
||||
-- FTS includes all translation content for the source post
|
||||
}
|
||||
|
||||
rule UpsertMediaTranslationSideEffects {
|
||||
when: MediaTranslationUpserted(translation, media)
|
||||
ensures: TranslatedSidecarWritten(media, translation.language)
|
||||
-- {path}.{lang}.meta
|
||||
}
|
||||
|
||||
rule DeleteMediaTranslationSideEffects {
|
||||
when: MediaTranslationDeleted(media, language)
|
||||
ensures: TranslatedSidecarDeleted(media, language)
|
||||
}
|
||||
|
||||
-- ─── 2.8 CLI/MCP notification sync ───────────────────────────
|
||||
|
||||
-- When MCP CLI makes mutations, it writes to db_notifications table.
|
||||
-- NotificationWatcher polls DB file (chokidar, 100ms debounce):
|
||||
-- Reads unseen CLI notifications
|
||||
-- Calls engine.invalidate(entityId) if needed
|
||||
-- Sends entity:changed IPC event to renderer
|
||||
-- Marks rows as seen
|
||||
-- Prunes: >1h processed, >24h unprocessed
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- PART 3: SIDE-EFFECT SUMMARY TABLE
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
-- Operation | File Write | FTS | Links | Thumbs | Sidecar | Embed | JSON Meta
|
||||
-- -------------------|--------------|------|-------|--------|---------|-------|----------
|
||||
-- createPost | no (draft) | yes | no | no | no | yes | no
|
||||
-- updatePost | rename only* | yes | if Δ | no | no | yes | no
|
||||
-- publishPost | .md + trans | yes | yes | no | no | yes | no
|
||||
-- deletePost | delete .md | del | del | no | Δ media | del | no
|
||||
-- importMedia | copy file | yes | no | async | write | no | no
|
||||
-- updateMedia | no | yes | no | no | rewrite | no | no
|
||||
-- replaceMediaFile | overwrite | no | no | regen | no | no | no
|
||||
-- deleteMedia | delete all | del | no | del | del all | no | no
|
||||
-- createTemplate | .liquid | no | no | no | no | no | no
|
||||
-- updateTemplate | rewrite | no | no | no | no | no | no
|
||||
-- deleteTemplate | delete+casc | no | no | no | no | no | no
|
||||
-- deleteTag | sync posts | no | no | no | no | no | tags.json
|
||||
-- renameTag | sync posts | no | no | no | no | no | tags.json
|
||||
-- mergeTags | sync posts | no | no | no | no | no | tags.json
|
||||
-- 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  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
|
||||
168
specs/modals.allium
Normal file
168
specs/modals.allium
Normal file
@@ -0,0 +1,168 @@
|
||||
-- allium: 1
|
||||
-- bDS Shared Modals
|
||||
-- Scope: UI overlays used across multiple editor views
|
||||
-- Distilled from: PostEditor.tsx, MediaEditor.tsx
|
||||
|
||||
-- Shared modal components used by multiple editors.
|
||||
-- Editor-specific modals live in their respective editor spec files.
|
||||
|
||||
use "./i18n.allium" as i18n
|
||||
|
||||
-- ─── AI Suggestions Modal ────────────────────────────────────
|
||||
|
||||
-- Shared modal for presenting AI suggestions with per-field accept/reject.
|
||||
-- Used by PostAIAnalysis (editor_post.allium) and
|
||||
-- MediaAIImageAnalysis (editor_media.allium).
|
||||
|
||||
value AISuggestionsModal {
|
||||
fields: List<AISuggestionField>
|
||||
-- Layout: title bar ("AI Suggestions"), scrollable field list, button row
|
||||
-- Each field rendered as a row:
|
||||
-- Left: checkbox (accept/reject), label
|
||||
-- Center: current value (read-only, muted), arrow, suggested value (highlighted)
|
||||
-- Special: slug field checkbox disabled if post was ever published
|
||||
-- Buttons: Cancel (secondary), Apply Selected (primary)
|
||||
-- Cancel discards all; Apply writes only accepted fields to entity
|
||||
}
|
||||
|
||||
value AISuggestionField {
|
||||
label: String
|
||||
current_value: String
|
||||
suggested_value: String
|
||||
accepted: Boolean -- checkbox, default true
|
||||
locked: Boolean -- if true, checkbox disabled (e.g. published slug)
|
||||
}
|
||||
|
||||
-- ─── Insert Post Link Modal ──────────────────────────────────
|
||||
|
||||
value InsertPostLinkModal {
|
||||
-- Two-tab modal opened by Ctrl/Cmd+K in post editor (markdown mode).
|
||||
-- Tab 1 - Internal:
|
||||
-- Search input (debounced 300ms, queries post titles)
|
||||
-- Results list: post title + status badge (draft/published/archived)
|
||||
-- If semantic similarity enabled: results ranked by vector similarity
|
||||
-- Click result: inserts [title](/YYYY/MM/DD/slug) at cursor, closes modal
|
||||
-- "Create Post" row at bottom of results:
|
||||
-- Creates new post with search query as title, inserts link to it
|
||||
-- Tab 2 - External:
|
||||
-- URL input field (required)
|
||||
-- Display text input field (optional)
|
||||
-- Insert button: inserts [text](url) or bare url if no text, closes modal
|
||||
active_tab: String -- internal | external
|
||||
search_query: String
|
||||
results: List<InsertLinkResult>
|
||||
}
|
||||
|
||||
value InsertLinkResult {
|
||||
post_id: String
|
||||
title: String
|
||||
status: String -- draft | published | archived
|
||||
canonical_url: String -- /YYYY/MM/DD/slug
|
||||
}
|
||||
|
||||
-- ─── Insert Media Modal ──────────────────────────────────────
|
||||
|
||||
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  at cursor
|
||||
-- Non-images: inserts [originalName](bds-media://id) at cursor
|
||||
-- Closes modal after insertion.
|
||||
search_query: String
|
||||
results: List<InsertMediaResult>
|
||||
}
|
||||
|
||||
value InsertMediaResult {
|
||||
media_id: String
|
||||
title: String
|
||||
original_name: String
|
||||
is_image: Boolean
|
||||
thumbnail_url: String? -- bds-thumb:// for images, null for others
|
||||
}
|
||||
|
||||
-- ─── Language Picker Modal ───────────────────────────────────
|
||||
|
||||
value LanguagePickerModal {
|
||||
-- Shown for Translate Post and Translate Media Metadata actions.
|
||||
-- Lists all configured blogLanguages except source language.
|
||||
-- Each row: flag emoji, language name, status badge if translation exists.
|
||||
-- Existing translations show "(draft)" or "(published)" badge.
|
||||
-- Click selects target language and initiates translation flow.
|
||||
-- Cancel closes without action.
|
||||
source_language: String
|
||||
available_targets: List<LanguageTarget>
|
||||
}
|
||||
|
||||
value LanguageTarget {
|
||||
code: String
|
||||
name: String
|
||||
flag_emoji: String
|
||||
has_existing_translation: Boolean
|
||||
existing_status: String? -- draft | published, if translation exists
|
||||
}
|
||||
|
||||
-- ─── Confirm Delete Modal ────────────────────────────────────
|
||||
|
||||
value ConfirmDeleteModal {
|
||||
-- Custom styled modal for destructive operations with reference info.
|
||||
-- Used by: MediaDelete (shows linked posts), TagDelete (shows post count).
|
||||
-- Layout: warning icon, title, entity name, reference section, buttons.
|
||||
-- Reference section: "This item is referenced by:" + bulleted list.
|
||||
-- Buttons: Cancel (secondary), Delete (destructive red).
|
||||
entity_name: String
|
||||
entity_type: String -- media | tag
|
||||
reference_count: Integer
|
||||
reference_list: List<String> -- titles of referencing entities
|
||||
}
|
||||
|
||||
-- ─── Confirm Dialog ──────────────────────────────────────────
|
||||
|
||||
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).
|
||||
|
||||
-- ─── Gallery Overlay ─────────────────────────────────────────
|
||||
|
||||
value GalleryOverlay {
|
||||
-- Full-screen overlay showing all media linked to a post.
|
||||
-- Opened from Gallery button in post editor toolbar (markdown mode).
|
||||
-- Image grid: bds-thumb:// thumbnails (medium 400px), 3-4 columns.
|
||||
-- Click image: opens LightboxView for that image.
|
||||
-- Close: X button or ESC key.
|
||||
post_id: String
|
||||
images: List<GalleryImage>
|
||||
}
|
||||
|
||||
value GalleryImage {
|
||||
media_id: String
|
||||
thumbnail_url: String -- bds-thumb://media_id
|
||||
alt_text: String?
|
||||
}
|
||||
|
||||
value LightboxView {
|
||||
-- Full-screen image viewer, sub-view of GalleryOverlay.
|
||||
-- Shows single image at full resolution via bds-media:// protocol.
|
||||
-- Navigation: left/right arrow buttons, keyboard left/right arrow keys.
|
||||
-- Close: X button, ESC key, or click outside image area.
|
||||
-- Header: image title or filename, index counter "3 of 12".
|
||||
current_index: Integer
|
||||
total_count: Integer
|
||||
media_id: String
|
||||
image_url: String -- bds-media://media_id
|
||||
alt_text: String?
|
||||
}
|
||||
|
||||
-- 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.
|
||||
203
specs/ui_data_flow.allium
Normal file
203
specs/ui_data_flow.allium
Normal file
@@ -0,0 +1,203 @@
|
||||
-- allium: 1
|
||||
-- bDS UI Data Flow Model
|
||||
-- Scope: cross-cutting (all waves)
|
||||
-- Distilled from: appStore.ts, PostEditor.tsx, MediaEditor.tsx, Editor.tsx,
|
||||
-- Sidebar.tsx, NotificationWatcher.ts
|
||||
|
||||
-- Describes the reactive data flows that connect sidebar, editor, tab bar,
|
||||
-- and backend engine operations. Covers: sidebar->editor, editor->sidebar,
|
||||
-- cross-tab coordination, and backend->UI event propagation.
|
||||
|
||||
use "./layout.allium" as layout
|
||||
use "./tabs.allium" as tabs
|
||||
use "./sidebar_views.allium" as sidebar
|
||||
|
||||
-- ─── 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)
|
||||
}
|
||||
|
||||
-- ─── 1. Sidebar -> Editor flows ──────────────────────────────
|
||||
|
||||
-- All UI coordination flows through a shared reactive store (Zustand in TS,
|
||||
-- Iced subscriptions in Rust). There are no direct calls between sidebar
|
||||
-- and editor. Both read from and write to the store; changes propagate
|
||||
-- reactively.
|
||||
|
||||
rule SidebarEntityClick {
|
||||
when: SidebarItemClicked(entity_type, entity_id, click_type)
|
||||
-- Single click: open as transient/preview tab
|
||||
-- Double click: open as pinned tab
|
||||
-- See sidebar_views.allium for per-view click rules
|
||||
-- Effect: Editor mounts the matching view keyed by entity_id
|
||||
-- Effect: Sidebar item highlights based on activeTabId match
|
||||
-- If a prior transient tab of same type exists, it is replaced,
|
||||
-- and the old editor unmounts (triggering auto-save if dirty)
|
||||
}
|
||||
|
||||
rule SidebarCreateEntity {
|
||||
when: SidebarCreateRequested(entity_type)
|
||||
-- Posts/Pages: backend creates post, emits post:created,
|
||||
-- store adds to posts list, sidebar re-renders with new item.
|
||||
-- Does NOT open a tab automatically. User must click to open.
|
||||
-- Sets selectedPostId for highlighting, optionally ensures sidebar visible.
|
||||
-- Scripts/Templates: backend creates, emits event, opens tab immediately.
|
||||
-- Chat: creates conversation, opens as pinned tab.
|
||||
-- Import: creates definition, opens as pinned tab.
|
||||
}
|
||||
|
||||
rule SidebarDeleteEntity {
|
||||
when: SidebarDeleteRequested(entity_type, entity_id)
|
||||
-- Scripts/Templates/Chat/Import: backend deletes entity,
|
||||
-- then closeTab(entity_id) removes its tab,
|
||||
-- activates adjacent tab or shows dashboard.
|
||||
-- Posts/Media: deletion is triggered from the EDITOR (not sidebar).
|
||||
-- See editor_post.allium PostDeleteAction / editor_media.allium MediaDeleteAction.
|
||||
}
|
||||
|
||||
invariant SidebarFilterIsolation {
|
||||
-- Sidebar search/filter state is local to the sidebar component.
|
||||
-- Filtering never affects: active tab, editor content, selectedPostId.
|
||||
-- Only the visible list of items changes.
|
||||
}
|
||||
|
||||
-- ─── 2. Editor -> Sidebar flows ──────────────────────────────
|
||||
|
||||
rule PostTitleChanged {
|
||||
when: PostSaved(post_id, updated_post)
|
||||
-- Auto-save fires after 3s idle, or on Ctrl+S, or on unmount/tab switch
|
||||
-- Store's posts array is updated with new title/metadata
|
||||
-- Sidebar PostsList re-renders reactively showing new title
|
||||
-- TabBar re-renders showing new title
|
||||
ensures: dirtyPosts.remove(post_id)
|
||||
}
|
||||
|
||||
rule PostStatusChanged {
|
||||
when: PostStatusTransitioned(post_id, old_status, new_status)
|
||||
-- Store's posts array is updated with new status
|
||||
-- Sidebar detects status change (compares prev/current status maps)
|
||||
-- and re-runs search/filter if active
|
||||
-- Post moves between draft/published/archived sections in sidebar
|
||||
}
|
||||
|
||||
rule PostEditorDelete {
|
||||
when: PostDeletedFromEditor(post_id)
|
||||
ensures: store.removePost(post_id)
|
||||
-- Removes from posts array, clears selectedPostId if matching,
|
||||
-- removes from dirtyPosts
|
||||
ensures: closeTab(post_id)
|
||||
-- Sidebar re-renders without the post
|
||||
}
|
||||
|
||||
rule MediaEditorSave {
|
||||
when: MediaSavedFromEditor(media_id, updated_media)
|
||||
ensures: store.updateMedia(media_id, updated_media)
|
||||
-- Sidebar MediaList re-renders with updated metadata
|
||||
}
|
||||
|
||||
rule MediaEditorDelete {
|
||||
when: MediaDeletedFromEditor(media_id)
|
||||
ensures: store.removeMedia(media_id)
|
||||
-- Editor.tsx safety net: detects active media tab references
|
||||
-- non-existent item, calls closeTab(activeTab.id)
|
||||
-- Sidebar re-renders without the media item
|
||||
}
|
||||
|
||||
rule SettingsRebuild {
|
||||
when: SettingsRebuildCompleted(entity_type, new_data)
|
||||
-- Wholesale replacement of posts or media array in store
|
||||
ensures: store.setPosts(new_data) or store.setMedia(new_data)
|
||||
-- Sidebar re-renders entirely with fresh data
|
||||
}
|
||||
|
||||
-- ─── 3. Cross-tab coordination ──────────────────────────────
|
||||
|
||||
invariant TabSwitchDoesNotChangeSidebarView {
|
||||
-- Switching tabs does NOT change activeView in the sidebar.
|
||||
-- The sidebar view is controlled exclusively by the Activity Bar.
|
||||
-- Exception: Dashboard post click explicitly sets activeView = "posts".
|
||||
}
|
||||
|
||||
invariant SidebarHighlightFollowsActiveTab {
|
||||
-- Sidebar item highlight is based on activeTabId === entity.id,
|
||||
-- NOT on selectedPostId/selectedMediaId (which are separate concepts
|
||||
-- used only for post creation flow).
|
||||
}
|
||||
|
||||
rule TransientTabReplacement {
|
||||
when: TransientTabBeingReplaced(old_tab, new_tab)
|
||||
-- Old editor unmounts -> cleanup auto-save fires if dirty
|
||||
-- New editor mounts with new entity
|
||||
-- Sidebar highlight shifts to newly active entity
|
||||
}
|
||||
|
||||
rule TabCloseCleanup {
|
||||
when: TabClosed(tab)
|
||||
-- If post tab: PostEditor unmounts, auto-save fires if dirty
|
||||
-- Store activates adjacent tab (prefer right, then left, then null)
|
||||
-- Sidebar highlight updates to new active tab
|
||||
-- If no tabs remain: dashboard shown
|
||||
}
|
||||
|
||||
rule DashboardPostClick {
|
||||
when: DashboardPostClicked(post_id, click_type)
|
||||
-- This is the ONLY place where opening a tab also switches sidebar view
|
||||
ensures: setActiveView("posts")
|
||||
ensures: setSelectedPost(post_id)
|
||||
if click_type = single:
|
||||
ensures: OpenTabRequested(type: post, id: post_id, intent: preview)
|
||||
if click_type = double:
|
||||
ensures: OpenTabRequested(type: post, id: post_id, intent: pin)
|
||||
}
|
||||
|
||||
-- ─── 4. Backend -> UI event propagation ─────────────────────
|
||||
|
||||
-- Backend engines emit events via IPC. The renderer listens and updates
|
||||
-- the shared store. Both sidebar and editor re-render reactively.
|
||||
-- Events: post:created, post:updated, post:deleted,
|
||||
-- media:imported, media:updated, media:deleted,
|
||||
-- template:created/updated/deleted, script:created/updated/deleted,
|
||||
-- entity:changed (from CLI/MCP mutations via NotificationWatcher)
|
||||
|
||||
-- TabBar also listens directly for:
|
||||
-- post-updated (title refresh)
|
||||
-- bds:scripts-changed (script title refresh)
|
||||
-- BDS_EVENT_TEMPLATES_CHANGED (template title refresh)
|
||||
-- chat.onTitleUpdated (chat conversation title refresh)
|
||||
-- importDefinitions.onNameUpdated (import name refresh)
|
||||
|
||||
-- Editor.tsx has safety-net useEffect guards that:
|
||||
-- Close media tabs when referenced media no longer exists in store
|
||||
-- Clear selectedPostId/selectedMediaId when entity is gone
|
||||
|
||||
-- ─── 5. 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
|
||||
Reference in New Issue
Block a user