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