diff --git a/specs/editor_chat.allium b/specs/editor_chat.allium index a92d9a9..8df4567 100644 --- a/specs/editor_chat.allium +++ b/specs/editor_chat.allium @@ -37,6 +37,20 @@ value ModelSelectorDropdown { selected_model_id: String? } +surface ModelSelectorDropdownSurface { + context dropdown: ModelSelectorDropdown + + exposes: + dropdown.selected_model_id when dropdown.selected_model_id != null + for group in dropdown.groups: + group.provider_name + for model in group.models: + model.model_id + model.display_name + model.context_window + model.max_output_tokens +} + value ModelProviderGroup { provider_name: String -- e.g. "OpenAI", "Ollama", "LM Studio" models: List diff --git a/specs/editor_media.allium b/specs/editor_media.allium index 4ef4083..ae2cdd1 100644 --- a/specs/editor_media.allium +++ b/specs/editor_media.allium @@ -47,6 +47,17 @@ value PostPickerOverlay { 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 +} + value PostPickerResult { post_id: String title: String diff --git a/specs/editor_post.allium b/specs/editor_post.allium index 87da771..80727da 100644 --- a/specs/editor_post.allium +++ b/specs/editor_post.allium @@ -73,6 +73,16 @@ value TranslationFlag { 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 diff --git a/specs/editor_tags.allium b/specs/editor_tags.allium index 5eaafc0..394cac6 100644 --- a/specs/editor_tags.allium +++ b/specs/editor_tags.allium @@ -36,6 +36,15 @@ value ColourPickerPopover { selected: String? } +surface ColourPickerPopoverSurface { + context popover: ColourPickerPopover + + exposes: + popover.presets + popover.custom_hex when popover.custom_hex != null + popover.selected when popover.selected != null +} + config { colour_picker_preset_count: Integer = 17 tag_cloud_min_font: String = "0.85rem" diff --git a/specs/frontmatter.allium b/specs/frontmatter.allium index 7afef5d..190f140 100644 --- a/specs/frontmatter.allium +++ b/specs/frontmatter.allium @@ -17,6 +17,67 @@ surface FrontmatterPersistenceSurface { PublishScriptRequested(script) } +surface PostFrontmatterSurface { + context frontmatter: PostFrontmatter + + exposes: + frontmatter.id + frontmatter.title + frontmatter.slug + frontmatter.status + frontmatter.published_at + frontmatter.tags + frontmatter.categories +} + +surface MediaSidecarSurface { + context sidecar: MediaSidecar + + exposes: + sidecar.id + sidecar.original_name + sidecar.mime_type + sidecar.width + sidecar.height + sidecar.updated_at +} + +surface TemplateFrontmatterSurface { + context frontmatter: TemplateFrontmatter + + exposes: + frontmatter.id + frontmatter.slug + frontmatter.kind + frontmatter.enabled + frontmatter.version +} + +surface ScriptFrontmatterSurface { + context frontmatter: ScriptFrontmatter + + exposes: + frontmatter.id + frontmatter.slug + frontmatter.kind + frontmatter.entrypoint + frontmatter.enabled + frontmatter.version +} + +surface MenuOpmlSurface { + context document: MenuOpml + + exposes: + document.header.title + document.header.date_created + document.header.date_modified + for item in document.body: + item.kind + item.label + item.slug +} + -- ============================================================================ -- POST FILE FORMAT -- ============================================================================ diff --git a/specs/generation.allium b/specs/generation.allium index 0279977..bd7119d 100644 --- a/specs/generation.allium +++ b/specs/generation.allium @@ -49,6 +49,20 @@ entity SiteGeneration { generated_files: GeneratedFile with project_id = this.project_id } +surface GenerationStatusSurface { + context generation: SiteGeneration + + exposes: + generation.project_id + generation.base_url + generation.language + generation.blog_languages + generation.max_posts_per_page + generation.pico_theme + generation.sections + generation.generated_files.count +} + invariant IncrementalByContentHash { -- Files are only written when content_hash changes -- generatedFileHashes table tracks (projectId, relativePath, contentHash) @@ -61,6 +75,21 @@ invariant MultiLanguageRoutes { -- Each language subtree gets its own feeds and archives } +invariant CanonicalBaseUrlConfigured { + for generation in SiteGenerations: + generation.base_url != "" +} + +invariant NamedPicoTheme { + for generation in SiteGenerations where generation.pico_theme != null: + generation.pico_theme != "" +} + +invariant GeneratedFilesTracked { + for generation in SiteGenerations: + generation.generated_files.count >= 0 +} + -- Core section: root pages, sitemap, RSS, Atom, calendar.json rule GenerateCoreSectionPages { diff --git a/specs/preview.allium b/specs/preview.allium index 94209b1..cd0bd15 100644 --- a/specs/preview.allium +++ b/specs/preview.allium @@ -98,6 +98,11 @@ invariant ThemeSwitching { -- Uses Pico CSS with configurable themes } +invariant PreviewServerBinding { + for server in PreviewServers where server.is_running: + server.host = config.preview_host and server.port = config.preview_port +} + invariant LocalhostOnly { -- Preview server binds to 127.0.0.1 only, never 0.0.0.0 } diff --git a/specs/sidebar_views.allium b/specs/sidebar_views.allium index 8ad02dd..5805063 100644 --- a/specs/sidebar_views.allium +++ b/specs/sidebar_views.allium @@ -58,6 +58,16 @@ value RelativeDateFormat { else: timestamp.toLocaleDateString(locale, month: short, day: numeric) } +surface RelativeDateFormatSurface { + context format: RelativeDateFormat + + exposes: + format.timestamp + format.locale + format.diff_days + format.display +} + value PostDateFormat { timestamp: Timestamp locale: String @@ -67,6 +77,15 @@ value PostDateFormat { -- Example: "Feb 10, 2026" } +surface PostDateFormatSurface { + context format: PostDateFormat + + exposes: + format.timestamp + format.locale + format.display +} + value PostTypeIcon { categories: List @@ -80,6 +99,14 @@ value PostTypeIcon { else: "document" } +surface PostTypeIconSurface { + context icon: PostTypeIcon + + exposes: + icon.categories + icon.icon +} + invariant SidebarEntityListPattern { -- Views following this pattern (scripts, templates, chat, import) must provide: -- 1. Header with localised title and create button. @@ -103,6 +130,22 @@ value PostsView { has_more: Boolean -- pagination, 500 per batch } +surface PostsViewSurface { + context view: PostsView + + exposes: + view.mode + view.search_query + view.filter_panel_visible + view.calendar_filter when view.calendar_filter != null + view.tag_filter + view.category_filter + view.draft_section.count + view.published_section.count + view.archived_section.count + view.has_more +} + -- Drafts section always shows all drafts regardless of filters. -- Published and archived sections respect active filters. -- "Clear All Filters" button resets search, date, tags, categories. @@ -175,6 +218,17 @@ value MediaView { grid: List -- grid layout (not list) } +surface MediaViewSurface { + context view: MediaView + + exposes: + view.search_query + view.filter_panel_visible + view.calendar_filter when view.calendar_filter != null + view.tag_filter + view.grid.count +} + value MediaGridItem { media_id: String thumbnail_path: String? -- small (150px) thumbnail on disk when image; null for non-image @@ -240,6 +294,13 @@ value ScriptsView { items: List } +surface ScriptsViewSurface { + context view: ScriptsView + + exposes: + view.items.count +} + value ScriptListItem { script_id: String title: String -- truncated with ellipsis on overflow @@ -299,6 +360,13 @@ value TemplatesView { items: List } +surface TemplatesViewSurface { + context view: TemplatesView + + exposes: + view.items.count +} + value TemplateListItem { template_id: String title: String -- truncated with ellipsis on overflow @@ -398,6 +466,13 @@ value SettingsNav { active_section: String? -- persisted across sidebar switches } +surface SettingsNavSurface { + context nav: SettingsNav + + exposes: + nav.active_section when nav.active_section != null +} + rule SettingsNavClick { when: SettingsNavEntryClicked(section) if section = style: @@ -452,6 +527,13 @@ value TagsNav { active_section: String? -- persisted } +surface TagsNavSurface { + context nav: TagsNav + + exposes: + nav.active_section when nav.active_section != null +} + rule TagsNavClick { when: TagsNavEntryClicked(section) ensures: OpenTabRequested(type: tags, id: tags, intent: pin) @@ -469,6 +551,14 @@ value ChatView { items: List } +surface ChatViewSurface { + context view: ChatView + + exposes: + view.api_ready + view.items.count +} + value ChatListItem { conversation_id: String title: String -- live-updated via onTitleUpdated @@ -514,6 +604,13 @@ value ImportView { items: List } +surface ImportViewSurface { + context view: ImportView + + exposes: + view.items.count +} + value ImportListItem { definition_id: String name: String -- live-updated via onNameUpdated @@ -571,6 +668,22 @@ value GitActiveView { has_more_history: Boolean -- paginated, 20 per page } +surface GitActiveViewSurface { + context view: GitActiveView + + exposes: + view.branch + view.upstream when view.upstream != null + view.ahead + view.behind + view.status_files.count + view.history_entries.count + view.has_more_history + + provides: + GitCommitRequested(message) +} + value GitStatusFile { path: String status: String -- modified, added, deleted, renamed, etc. @@ -672,6 +785,14 @@ value CalendarYear { months: List } +surface CalendarYearSurface { + context calendar_year: CalendarYear + + exposes: + calendar_year.year + calendar_year.months.count +} + value CalendarMonth { month: Integer -- 1-12 count: Integer -- number of items in this month diff --git a/specs/tabs.allium b/specs/tabs.allium index c917302..33941d4 100644 --- a/specs/tabs.allium +++ b/specs/tabs.allium @@ -8,6 +8,25 @@ use "./layout.allium" as layout +surface TabControlSurface { + facing _: TabOperator + + provides: + OpenTabRequested(type, id, intent) + OpenTabInBackgroundRequested(type, id, intent) + CloseTabRequested(tab) + PinTabRequested(tab) + ClearTabsRequested() +} + +surface TabRuntimeSurface { + facing _: TabRuntime + + provides: + TabOpening(tab_type, intent) + ActiveTabChanged(active_tab) +} + -- ─── Tab types ──────────────────────────────────────────────── -- 17 distinct tab types, each routing to a matching editor view. @@ -29,6 +48,15 @@ value Tab { is_transient: Boolean -- true = preview tab (italic title, replaceable) } +surface TabSurface { + context tab: Tab + + exposes: + tab.type + tab.id + tab.is_transient +} + -- ─── Tab categories ─────────────────────────────────────────── -- 1. Singleton tool tabs: always one instance, never transient, id = type name. @@ -161,6 +189,16 @@ value TabBarItem { is_dirty: Boolean -- dot indicator, only for post tabs } +surface TabBarItemSurface { + context item: TabBarItem + + exposes: + item.title + item.is_active + item.is_transient + item.is_dirty +} + -- Tab title resolution: -- post: post.title from DB (listens post-updated events); no JS truncation -- media: media.originalName; no JS truncation diff --git a/specs/template_context.allium b/specs/template_context.allium index c7af5f7..6afa81a 100644 --- a/specs/template_context.allium +++ b/specs/template_context.allium @@ -21,6 +21,42 @@ surface TemplateRenderingSurface { RenderNotFoundPageRequested() } +surface RenderContextSurface { + context context: RenderContext + + exposes: + context.language + context.language_prefix + context.page_title + context.pico_stylesheet_href + context.blog_languages + context.alternate_links + context.menu_items + context.post + context.day_blocks + context.archive_context + context.is_list_page + context.prev_page_href + context.next_page_href + context.not_found_message +} + +surface PaginationContextSurface { + context pagination: PaginationContext + + exposes: + pagination.is_first_page + pagination.is_last_page + pagination.has_prev_page + pagination.has_next_page + pagination.prev_page_href + pagination.next_page_href + pagination.current_page + pagination.total_pages + pagination.total_items + pagination.items_per_page +} + value RenderContext { -- Top-level variables available in all templates language: String -- Current language code