-- allium: 1 -- bDS Media Editor View -- Scope: UI content area — media editing surface -- Distilled from: MediaEditor.tsx -- Describes the layout and behaviour of the media editor rendered in -- the main content area when a media tab is active. use "./media.allium" as media use "./post.allium" as post use "./i18n.allium" as i18n -- ─── Media editor ───────────────────────────────────────────── value MediaEditorView { media_id: String is_image: Boolean file_name: String -- originalName, read-only 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 linked_posts: List } value MediaTranslationItem { language: String flag_emoji: String title: String? alt_text: String? caption: String? } value LinkedPostItem { post_id: String title: String } value PostPickerOverlay { search_query: String results: List overflow_count: Integer? -- shown as "and N more" if > 0 } surface PostPickerOverlaySurface { context overlay: PostPickerOverlay exposes: overlay.search_query for result in overlay.results: result.post_id result.title overlay.overflow_count when overlay.overflow_count != null provides: PostPickerResultClicked(media_id, post_id) } value PostPickerResult { post_id: String title: String } config { media_post_picker_max_results: Integer = 10 } surface MediaEditorSurface { context editor: MediaEditorView exposes: editor.file_name editor.mime_type editor.file_size editor.dimensions when editor.dimensions != null editor.is_image editor.title editor.alt_text editor.caption editor.tags editor.author editor.language for t in editor.translations: t.language t.flag_emoji t.title for lp in editor.linked_posts: lp.post_id lp.title provides: MediaAIImageAnalysisRequested(editor.media_id) when editor.is_image MediaDetectLanguageRequested(editor.media_id) MediaTranslateMetadataRequested(editor.media_id, target_language) MediaReplaceFileRequested(editor.media_id) MediaSaveRequested(editor.media_id) MediaDeleteRequested(editor.media_id) MediaLinkToPostRequested(editor.media_id) MediaTranslationEditClicked(editor.media_id, language) MediaTranslationRefreshClicked(editor.media_id, language) MediaUnlinkPostRequested(editor.media_id, post_id) MediaDeleteConfirmed(editor.media_id) TranslationEditModalSaved(editor.media_id, language, title, alt, caption) @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 at full resolution via the /media-file/:id route -- (serves the original file; falls back to the large thumbnail for -- formats browsers cannot display inline) with cache-busting -- timestamp. Never upscaled beyond natural size. The sidebar keeps -- using small thumbnails. -- 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 (opens modal), refresh button. -- No delete action; translations are only removed via the scripting -- API (script.allium bds.media.delete_translation) or when the -- media item itself is deleted. -- "No translations" message when list is empty. @guarantee TranslationEditModal -- Editing a translation opens a modal dialog ("Edit Translation"), not -- an inline form. Hidden language field plus Title, Alt Text, and -- Caption (textarea) inputs. Footer: Cancel and Save buttons. -- Save persists the translation; Cancel/close discards. @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 MediaSave { when: MediaSaveRequested(media_id) let item = media/Media{id: media_id} ensures: UpdateMediaRequested(item, editor_field_changes(media_id)) -- Persists all form fields; the engine update rewrites the -- sidecar and FTS entry (media.allium UpdateMedia, -- engine_side_effects.allium UpdateMediaSideEffects) } rule MediaAIImageAnalysis { when: MediaAIImageAnalysisRequested(media_id) -- Only available for image/* MIME types (editor.is_image guard -- on the provides entry; button hidden for non-images). ensures: AISuggestionRequested(entity_type: "media", entity_id: media_id) -- Shared flow: action_patterns.allium MediaAISuggestionFlow — -- airplane gating, image model over the 448x448 AI thumbnail, -- AISuggestionsModal with title/alt/caption, accepted fields -- trigger an explicit save. } rule MediaDetectLanguage { when: MediaDetectLanguageRequested(media_id) let item = media/Media{id: media_id} requires: active_endpoint_configured -- airplane-mode gating: ai.allium AirplaneModeGating let detected = ai_detect_language(concat(item.title, item.alt, item.caption)) -- title model over the concatenated metadata text ensures: UpdateMediaRequested(item, language_change(detected)) -- persisted immediately — no modal, no confirmation; the -- engine update rewrites the sidecar } rule MediaTranslateMetadata { when: MediaTranslateMetadataRequested(media_id, target_language) let item = media/Media{id: media_id} requires: active_endpoint_configured -- target_language comes from the language picker modal (same -- pattern as post translate). let source_language = item.language ?? ai_detect_language(concat(item.title, item.alt, item.caption)) -- step 1: detect the source language first when unset -- (auto-persisted, as in MediaDetectLanguage) let translated = ai_translate_media_metadata(item, source_language, target_language) -- step 2: translate title, alt, caption via the title model ensures: UpsertMediaTranslationRequested(item, target_language, translated.title, translated.alt, translated.caption) -- creates/updates the translation record and writes the -- translated sidecar {path}.{lang}.meta (media.allium -- UpsertMediaTranslation) } rule MediaReplaceFile { when: MediaReplaceFileRequested(media_id) let item = media/Media{id: media_id} let source = native_file_dialog_selection() -- native file dialog, no MIME type filter; cancel aborts ensures: ReplaceMediaFileRequested(item, source) -- copies over the existing media file path; images regenerate -- thumbnails synchronously (media.allium ReplaceMediaFile) ensures: MediaPreviewRefreshed(media_id) -- preview reloads with a cache-busting timestamp query param } rule MediaDeleteAction { when: MediaDeleteRequested(media_id) let item = media/Media{id: media_id} ensures: ConfirmDeleteModalOpened(media_id, item.linked_posts) -- custom modal (not a native dialog): media display name, -- linked post count and list; Cancel + Delete (destructive -- red style) — action_patterns.allium -- confirmation_assignments: media_delete } rule MediaDeleteExecute { when: MediaDeleteConfirmed(media_id) let item = media/Media{id: media_id} ensures: DeleteMediaRequested(item) -- file, sidecar, thumbnails, translations, post-media links, -- FTS entry: media.allium DeleteMedia + -- engine_side_effects.allium DeleteMediaSideEffects ensures: closeTab(media_id) -- sidebar removes the item reactively } rule MediaLinkToPost { when: MediaLinkToPostRequested(media_id) ensures: PostPickerOverlayOpened(media_id) -- inline overlay near the button (not a modal): search input -- filters unlinked posts by title, at most -- config.media_post_picker_max_results results, "and N more" -- when the total exceeds the limit } rule MediaLinkToPostSelected { when: PostPickerResultClicked(media_id, post_id) let item = media/Media{id: media_id} ensures: MediaLinkedToPost(item, post/Post{id: post_id}) -- creates the post-media link and updates sidecar -- linkedPostIds; the linked posts list refreshes reactively ensures: PostPickerOverlayClosed(media_id) } rule MediaUnlinkPost { when: MediaUnlinkPostRequested(media_id, post_id) let item = media/Media{id: media_id} -- No confirmation (action_patterns.allium -- confirmation_assignments: media_unlink) ensures: MediaUnlinkedFromPost(item, post_id) -- removes the post-media link row and updates sidecar -- linkedPostIds } rule MediaTranslationEdit { when: MediaTranslationEditClicked(media_id, language) ensures: TranslationEditModalOpened(media_id, language) -- "Edit Translation" modal pre-filled with the translation's -- title, alt, and caption; Cancel/close discards } rule MediaTranslationEditSave { when: TranslationEditModalSaved(media_id, language, title, alt, caption) let item = media/Media{id: media_id} ensures: UpsertMediaTranslationRequested(item, language, title, alt, caption) -- persists the DB record + translated sidecar -- {path}.{lang}.meta } rule MediaTranslationRefresh { when: MediaTranslationRefreshClicked(media_id, language) let item = media/Media{id: media_id} requires: active_endpoint_configured let translated = ai_translate_media_metadata(item, item.language, language) -- re-translates from the source language via the title model ensures: UpsertMediaTranslationRequested(item, language, translated.title, translated.alt, translated.caption) -- overwrites the existing translation fields and rewrites the -- translated sidecar }