345 lines
14 KiB
Plaintext
345 lines
14 KiB
Plaintext
-- allium: 1
|
|
-- bDS Post Editor View
|
|
-- Scope: UI content area — post editing surface
|
|
-- Distilled from: PostEditor.tsx
|
|
|
|
-- Describes the layout and behaviour of the post editor rendered in
|
|
-- the main content area when a post tab is active.
|
|
-- Tab routing is in tabs.allium. Sidebar navigation is in sidebar_views.allium.
|
|
|
|
use "./tabs.allium" as tabs
|
|
use "./post.allium" as post
|
|
use "./i18n.allium" as i18n
|
|
|
|
-- ─── Post editor ──────────────────────────────────────────────
|
|
|
|
value PostEditorView {
|
|
post_id: String
|
|
header: PostEditorHeader
|
|
metadata: PostEditorMetadata
|
|
metadata_expanded: Boolean -- starts expanded when title is empty
|
|
excerpt_expanded: Boolean
|
|
editor_mode: String -- markdown | preview
|
|
footer: PostEditorFooter
|
|
}
|
|
|
|
value PostEditorHeader {
|
|
title: String -- post title or "Untitled"
|
|
is_dirty: Boolean
|
|
status: String -- draft | published | archived
|
|
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 TranslationFlagSurface {
|
|
context flag: TranslationFlag
|
|
|
|
exposes:
|
|
flag.language
|
|
flag.flag_emoji
|
|
flag.status
|
|
flag.is_active
|
|
}
|
|
|
|
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)
|
|
PostDiscardConfirmed(editor.post_id)
|
|
PostDeleteConfirmed(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.
|
|
-- While typing: substring match on existing tag names (case-insensitive),
|
|
-- top 8 shown, plus a "create tag" row when the query is new.
|
|
-- While the query is empty (on focus): semantic suggestions appear under a
|
|
-- "Suggested tags" label — tags drawn from up to 10 similar posts,
|
|
-- weighted by similarity, top 5, excluding tags already on the post.
|
|
-- Requires semanticSimilarityEnabled and a built embedding index;
|
|
-- it is an index read (no model inference) so it works offline and
|
|
-- yields nothing when similarity is disabled or unindexed.
|
|
|
|
@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 (Markdown/Preview),
|
|
-- action buttons (markdown mode only): Gallery (with media count),
|
|
-- Insert Post Link, Insert Media.
|
|
|
|
@guarantee EditorModes
|
|
-- Markdown: code editor with markdown-with-macros language,
|
|
-- highlighting [[macro ...]] syntax. Word wrap on, minimap off, 14px font.
|
|
-- Preview: iframe served by the localhost preview server at the post's
|
|
-- normal generated-site URL, using the shared site renderer. Draft
|
|
-- content comes from DB; published content and assets come from files.
|
|
-- Markdown and Preview are the only editor modes.
|
|
|
|
@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
|
|
post_content_sample_length: Integer = 2000
|
|
}
|
|
|
|
invariant PostAutoSave {
|
|
-- 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.
|
|
}
|
|
|
|
invariant PostEditorModePersistence {
|
|
-- Editor mode (markdown/preview) persists per session.
|
|
-- Default mode comes from editor settings (markdown).
|
|
}
|
|
|
|
-- ─── Post editor actions ────────────────────────────────────
|
|
|
|
rule PostAIAnalysis {
|
|
when: PostAIAnalysisRequested(post_id)
|
|
ensures: AISuggestionRequested(entity_type: "post", entity_id: post_id)
|
|
-- Shared flow: action_patterns.allium PostAISuggestionFlow —
|
|
-- airplane gating, title model (not the default chat model)
|
|
-- over title + excerpt + content sample, AISuggestionsModal
|
|
-- with title/excerpt/slug (slug locked once published),
|
|
-- accepted fields re-arm the auto-save timer.
|
|
}
|
|
|
|
rule PostTranslateAction {
|
|
when: PostTranslateRequested(post_id, target_language)
|
|
let target = post/Post{id: post_id}
|
|
requires: active_endpoint_configured
|
|
-- airplane-mode gating: ai.allium AirplaneModeGating
|
|
requires: target_language in target.project.blog_languages
|
|
-- chosen in the language picker modal; existing translations
|
|
-- are listed with their status badge (draft/published)
|
|
let translated_meta = ai_translate_post_metadata(target, target_language)
|
|
-- AI call 1: title + excerpt via the title model
|
|
let translated_content = ai_translate_post_content(target, target_language)
|
|
-- AI call 2 (sequential): full markdown body via the title model
|
|
ensures: PostTranslationUpserted(
|
|
translation_record(target, target_language, translated_meta, translated_content),
|
|
target)
|
|
-- creates/updates the translation record; a published source
|
|
-- post transitions back to draft with the file content copied
|
|
-- to the DB so it can be edited (engine_side_effects.allium
|
|
-- UpsertPostTranslationSideEffects)
|
|
}
|
|
|
|
-- Auto-translation on manual save is specified once in
|
|
-- action_patterns.allium AutoTranslationChain (triggered by PostSaved),
|
|
-- including the linked-media cascade. It is not duplicated here.
|
|
|
|
rule PostPublishAction {
|
|
when: PostPublishRequested(post_id)
|
|
let target = post/Post{id: post_id}
|
|
ensures:
|
|
if editor_is_dirty(post_id):
|
|
PostSaved(post_id)
|
|
-- implicit save first (awaited)
|
|
ensures: PublishPostRequested(target)
|
|
-- engine publish: file write, translation publishing, FTS,
|
|
-- links (engine_side_effects.allium PublishPostSideEffects)
|
|
-- UI updates reactively: status badge -> published, sidebar
|
|
-- section move (ui_data_flow.allium PostStatusChanged)
|
|
}
|
|
|
|
rule PostDiscardChanges {
|
|
when: PostDiscardRequested(post_id)
|
|
let target = post/Post{id: post_id}
|
|
requires: target.published_at != null
|
|
requires: target.status = draft
|
|
-- only published posts with pending draft changes
|
|
ensures: DiscardConfirmShown(post_id)
|
|
-- system confirm dialog: "Discard changes to this post?"
|
|
-- (action_patterns.allium confirmation_assignments:
|
|
-- post_discard)
|
|
}
|
|
|
|
rule PostDiscardExecute {
|
|
when: PostDiscardConfirmed(post_id)
|
|
let target = post/Post{id: post_id}
|
|
ensures: DiscardPostChangesRequested(target)
|
|
-- engine restores the published state from the .md file:
|
|
-- content = null, status = published
|
|
ensures: EditorReloaded(post_id)
|
|
-- editor reloads with the restored content
|
|
}
|
|
|
|
rule PostDeleteAction {
|
|
when: PostDeleteRequested(post_id)
|
|
ensures: DeleteConfirmShown(post_id)
|
|
-- system confirm dialog: "Delete this post?"
|
|
-- (action_patterns.allium confirmation_assignments: post_delete)
|
|
}
|
|
|
|
rule PostDeleteExecute {
|
|
when: PostDeleteConfirmed(post_id)
|
|
let target = post/Post{id: post_id}
|
|
ensures: DeletePostRequested(target)
|
|
-- published posts also delete the .md file and all translation
|
|
-- files; never-published drafts only the DB record
|
|
-- (engine_side_effects.allium DeletePostSideEffects)
|
|
ensures: closeTab(post_id)
|
|
-- sidebar removes the item reactively (ui_data_flow.allium
|
|
-- PostEditorDelete)
|
|
}
|
|
|
|
rule PostInsertLink {
|
|
when: PostInsertLinkRequested(post_id)
|
|
-- Keyboard shortcut: Ctrl/Cmd+K (markdown mode only, per the
|
|
-- provides guard)
|
|
ensures: InsertPostLinkModalOpened(post_id)
|
|
-- two tabs: Internal (debounced title search, similarity
|
|
-- ranking, Create Post row) and External (URL + display text);
|
|
-- picked entries insert [title](/YYYY/MM/DD/slug) or
|
|
-- [text](url) at the cursor — modals.allium InsertPostLinkModal
|
|
}
|
|
|
|
rule PostInsertMedia {
|
|
when: PostInsertMediaRequested(post_id)
|
|
ensures: InsertMediaModalOpened(post_id)
|
|
-- media search grid with bds-thumb:// thumbnails; picked items
|
|
-- insert  for images or
|
|
-- [originalName](/media/YYYY/MM/file) otherwise —
|
|
-- modals.allium InsertMediaModal
|
|
}
|
|
|
|
rule PostGalleryAction {
|
|
when: PostGalleryRequested(post_id)
|
|
ensures: GalleryOverlayOpened(post_id)
|
|
-- grid of all media linked to this post; clicking an image
|
|
-- opens the lightbox (full-size preview, arrow navigation,
|
|
-- ESC to close) — modals.allium GalleryOverlay / LightboxView
|
|
}
|
|
|
|
rule PostDragDropImage {
|
|
when: ImageDroppedOnEditor(post_id, file_path)
|
|
ensures: ImageFileDroppedOnPostEditor(post_id, file_path)
|
|
-- full chain: action_patterns.allium PostDragDropImageChain
|
|
-- (synchronous import + thumbnails + link + cursor insert,
|
|
-- background AI auto-apply and translation cascade)
|
|
}
|
|
|
|
rule PostLanguageDetect {
|
|
when: PostLanguageDetectRequested(post_id)
|
|
let target = post/Post{id: post_id}
|
|
requires: active_endpoint_configured
|
|
let detected = ai_detect_language(post_suggestion_input(target))
|
|
-- title model over the content sample; no modal
|
|
ensures: target.language = detected
|
|
ensures: PostAutoSaveTimerReset(post_id)
|
|
}
|