chore: more cleanup in the specs
This commit is contained in:
@@ -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<ModelEntry>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
-- ============================================================================
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<String>
|
||||
|
||||
@@ -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<MediaGridItem> -- 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<ScriptListItem>
|
||||
}
|
||||
|
||||
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<TemplateListItem>
|
||||
}
|
||||
|
||||
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<ChatListItem>
|
||||
}
|
||||
|
||||
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<ImportListItem>
|
||||
}
|
||||
|
||||
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<CalendarMonth>
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user