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