initial commit

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-23 10:42:27 +02:00
commit cd998f24a9
57 changed files with 9751 additions and 0 deletions

251
specs/modals.allium Normal file
View File

@@ -0,0 +1,251 @@
-- allium: 1
-- bDS Shared Modals
-- Scope: UI overlays used across multiple editor views
-- Distilled from: PostEditor.tsx, MediaEditor.tsx
-- Shared modal components used by multiple editors.
-- Editor-specific modals live in their respective editor spec files.
use "./i18n.allium" as i18n
-- ─── AI Suggestions Modal ────────────────────────────────────
-- Shared modal for presenting AI suggestions with per-field accept/reject.
-- Used by PostAIAnalysis (editor_post.allium) and
-- MediaAIImageAnalysis (editor_media.allium).
value AISuggestionsModal {
fields: List<AISuggestionField>
-- Layout: title bar ("AI Suggestions"), scrollable field list, button row
-- Each field rendered as a row:
-- Left: checkbox (accept/reject), label
-- Center: current value (read-only, muted), arrow, suggested value (highlighted)
-- Special: slug field checkbox disabled if post was ever published
-- Buttons: Cancel (secondary), Apply Selected (primary)
-- Cancel discards all; Apply writes only accepted fields to entity
}
surface AISuggestionsModalSurface {
context modal: AISuggestionsModal
exposes:
modal.fields.count
}
value AISuggestionField {
label: String
current_value: String
suggested_value: String
accepted: Boolean -- checkbox, default true
locked: Boolean -- if true, checkbox disabled (e.g. published slug)
}
-- ─── Insert Post Link Modal ──────────────────────────────────
value InsertPostLinkModal {
-- Two-tab modal opened by Ctrl/Cmd+K in post editor (markdown mode).
-- Tab 1 - Internal:
-- Search input (debounced 300ms, queries post titles via FTS)
--
-- Empty query state (search_query < 2 chars):
-- If semanticSimilarityEnabled: shows up to 5 related posts via
-- FindSimilar(current_post, 5) ranked by embedding similarity
-- Else: shows nothing (empty results)
--
-- Active query state (search_query >= 2 chars):
-- Results from FTS title search
-- If semanticSimilarityEnabled: each result augmented with similarity
-- score from ComputeSimilarities(current_post, result_post_ids)
-- Scores displayed as visual indicator per result row
-- Results list: post title + status badge + optional similarity score
--
-- Click result: inserts [title](/YYYY/MM/DD/slug) at cursor, closes modal
-- "Create Post" row at bottom of results:
-- Creates new post with search query as title, inserts link to it
-- Tab 2 - External:
-- URL input field (required)
-- Display text input field (optional)
-- Insert button: inserts [text](url) or bare url if no text, closes modal
active_tab: String -- internal | external
search_query: String
results: List<InsertLinkResult>
related_posts: List<InsertLinkResult> -- similarity-based, shown when query empty
}
surface InsertPostLinkModalSurface {
context modal: InsertPostLinkModal
exposes:
modal.active_tab
modal.search_query
modal.results.count
modal.related_posts.count
}
value InsertLinkResult {
post_id: String
title: String
status: String -- draft | published | archived
canonical_url: String -- /YYYY/MM/DD/slug
similarity_score: Decimal? -- 0.0-1.0, present when embeddings enabled
}
-- ─── Insert Media Modal ──────────────────────────────────────
value InsertMediaModal {
-- Grid modal for inserting media references into post content.
-- Search input filtering by media title and original filename.
-- Grid of media items: bds-thumb:// thumbnail (medium 400px), title below.
-- Click item:
-- Images: inserts ![alt](bds-media://id) at cursor
-- Non-images: inserts [originalName](bds-media://id) at cursor
-- Closes modal after insertion.
search_query: String
results: List<InsertMediaResult>
}
surface InsertMediaModalSurface {
context modal: InsertMediaModal
exposes:
modal.search_query
modal.results.count
}
value InsertMediaResult {
media_id: String
title: String
original_name: String
is_image: Boolean
thumbnail_url: String? -- bds-thumb:// for images, null for others
}
-- ─── Language Picker Modal ───────────────────────────────────
value LanguagePickerModal {
-- Shown for Translate Post and Translate Media Metadata actions.
-- Lists all configured blogLanguages except source language.
-- Each row: flag emoji, language name, status badge if translation exists.
-- Existing translations show "(draft)" or "(published)" badge.
-- Click selects target language and initiates translation flow.
-- Cancel closes without action.
source_language: String
available_targets: List<LanguageTarget>
}
surface LanguagePickerModalSurface {
context modal: LanguagePickerModal
exposes:
modal.source_language
modal.available_targets.count
}
value LanguageTarget {
code: String
name: String
flag_emoji: String
has_existing_translation: Boolean
existing_status: String? -- draft | published, if translation exists
}
-- ─── Confirm Delete Modal ────────────────────────────────────
value ConfirmDeleteModal {
-- Custom styled modal for destructive operations with reference info.
-- Used by: MediaDelete (shows linked posts), TagDelete (shows post count).
-- Layout: warning icon, title, entity name, reference section, buttons.
-- Reference section: "This item is referenced by:" + bulleted list.
-- Buttons: Cancel (secondary), Delete (destructive red).
entity_name: String
entity_type: String -- media | tag
reference_count: Integer
reference_list: List<String> -- titles of referencing entities
}
surface ConfirmDeleteModalSurface {
context modal: ConfirmDeleteModal
exposes:
modal.entity_name
modal.entity_type
modal.reference_count
modal.reference_list
}
-- ─── Confirm Dialog ──────────────────────────────────────────
value ConfirmDialog {
-- Custom styled modal for non-delete confirmations.
-- Used by: TagMerge ("Merge N tags into {target}? Cannot be undone.").
-- Layout: title, descriptive message, buttons.
-- Buttons: Cancel (secondary), Confirm (primary).
title: String
message: String
}
surface ConfirmDialogSurface {
context modal: ConfirmDialog
exposes:
modal.title
modal.message
}
-- System confirm dialogs are NOT modelled as values.
-- They are simple yes/no system dialogs with a message string.
-- Used by: PostDelete, PostDiscard, TemplateDelete (with references).
-- ─── Gallery Overlay ─────────────────────────────────────────
value GalleryOverlay {
-- Full-screen overlay showing all media linked to a post.
-- Opened from Gallery button in post editor toolbar (markdown mode).
-- Image grid: bds-thumb:// thumbnails (medium 400px), 3-4 columns.
-- Click image: opens LightboxView for that image.
-- Close: X button or ESC key.
post_id: String
images: List<GalleryImage>
}
surface GalleryOverlaySurface {
context overlay: GalleryOverlay
exposes:
overlay.post_id
overlay.images.count
}
value GalleryImage {
media_id: String
thumbnail_url: String -- bds-thumb://media_id
alt_text: String?
}
value LightboxView {
-- Full-screen image viewer, sub-view of GalleryOverlay.
-- Shows single image at full resolution via bds-media:// protocol.
-- Navigation: left/right arrow buttons, keyboard left/right arrow keys.
-- Close: X button, ESC key, or click outside image area.
-- Header: image title or filename, index counter "3 of 12".
current_index: Integer
total_count: Integer
media_id: String
image_url: String -- bds-media://media_id
alt_text: String?
}
surface LightboxViewSurface {
context view: LightboxView
exposes:
view.current_index
view.total_count
view.media_id
view.image_url
view.alt_text when view.alt_text != null
}
-- All modals rendered as centered overlay with backdrop dimming.
-- ESC key or backdrop click closes modal (cancel semantics).
-- Overlays (PostPicker, ColourPicker) are positioned inline near trigger.