chore: more cleanup in the specs
This commit is contained in:
@@ -37,6 +37,20 @@ value ModelSelectorDropdown {
|
|||||||
selected_model_id: String?
|
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 {
|
value ModelProviderGroup {
|
||||||
provider_name: String -- e.g. "OpenAI", "Ollama", "LM Studio"
|
provider_name: String -- e.g. "OpenAI", "Ollama", "LM Studio"
|
||||||
models: List<ModelEntry>
|
models: List<ModelEntry>
|
||||||
|
|||||||
@@ -47,6 +47,17 @@ value PostPickerOverlay {
|
|||||||
overflow_count: Integer? -- shown as "and N more" if > 0
|
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 {
|
value PostPickerResult {
|
||||||
post_id: String
|
post_id: String
|
||||||
title: String
|
title: String
|
||||||
|
|||||||
@@ -73,6 +73,16 @@ value TranslationFlag {
|
|||||||
is_active: Boolean -- true when this language is currently being edited
|
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 {
|
surface PostEditorSurface {
|
||||||
context editor: PostEditorView
|
context editor: PostEditorView
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ value ColourPickerPopover {
|
|||||||
selected: String?
|
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 {
|
config {
|
||||||
colour_picker_preset_count: Integer = 17
|
colour_picker_preset_count: Integer = 17
|
||||||
tag_cloud_min_font: String = "0.85rem"
|
tag_cloud_min_font: String = "0.85rem"
|
||||||
|
|||||||
@@ -17,6 +17,67 @@ surface FrontmatterPersistenceSurface {
|
|||||||
PublishScriptRequested(script)
|
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
|
-- POST FILE FORMAT
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
|||||||
@@ -49,6 +49,20 @@ entity SiteGeneration {
|
|||||||
generated_files: GeneratedFile with project_id = this.project_id
|
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 {
|
invariant IncrementalByContentHash {
|
||||||
-- Files are only written when content_hash changes
|
-- Files are only written when content_hash changes
|
||||||
-- generatedFileHashes table tracks (projectId, relativePath, contentHash)
|
-- generatedFileHashes table tracks (projectId, relativePath, contentHash)
|
||||||
@@ -61,6 +75,21 @@ invariant MultiLanguageRoutes {
|
|||||||
-- Each language subtree gets its own feeds and archives
|
-- 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
|
-- Core section: root pages, sitemap, RSS, Atom, calendar.json
|
||||||
|
|
||||||
rule GenerateCoreSectionPages {
|
rule GenerateCoreSectionPages {
|
||||||
|
|||||||
@@ -98,6 +98,11 @@ invariant ThemeSwitching {
|
|||||||
-- Uses Pico CSS with configurable themes
|
-- 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 {
|
invariant LocalhostOnly {
|
||||||
-- Preview server binds to 127.0.0.1 only, never 0.0.0.0
|
-- 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)
|
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 {
|
value PostDateFormat {
|
||||||
timestamp: Timestamp
|
timestamp: Timestamp
|
||||||
locale: String
|
locale: String
|
||||||
@@ -67,6 +77,15 @@ value PostDateFormat {
|
|||||||
-- Example: "Feb 10, 2026"
|
-- Example: "Feb 10, 2026"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface PostDateFormatSurface {
|
||||||
|
context format: PostDateFormat
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
format.timestamp
|
||||||
|
format.locale
|
||||||
|
format.display
|
||||||
|
}
|
||||||
|
|
||||||
value PostTypeIcon {
|
value PostTypeIcon {
|
||||||
categories: List<String>
|
categories: List<String>
|
||||||
|
|
||||||
@@ -80,6 +99,14 @@ value PostTypeIcon {
|
|||||||
else: "document"
|
else: "document"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface PostTypeIconSurface {
|
||||||
|
context icon: PostTypeIcon
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
icon.categories
|
||||||
|
icon.icon
|
||||||
|
}
|
||||||
|
|
||||||
invariant SidebarEntityListPattern {
|
invariant SidebarEntityListPattern {
|
||||||
-- Views following this pattern (scripts, templates, chat, import) must provide:
|
-- Views following this pattern (scripts, templates, chat, import) must provide:
|
||||||
-- 1. Header with localised title and create button.
|
-- 1. Header with localised title and create button.
|
||||||
@@ -103,6 +130,22 @@ value PostsView {
|
|||||||
has_more: Boolean -- pagination, 500 per batch
|
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.
|
-- Drafts section always shows all drafts regardless of filters.
|
||||||
-- Published and archived sections respect active filters.
|
-- Published and archived sections respect active filters.
|
||||||
-- "Clear All Filters" button resets search, date, tags, categories.
|
-- "Clear All Filters" button resets search, date, tags, categories.
|
||||||
@@ -175,6 +218,17 @@ value MediaView {
|
|||||||
grid: List<MediaGridItem> -- grid layout (not list)
|
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 {
|
value MediaGridItem {
|
||||||
media_id: String
|
media_id: String
|
||||||
thumbnail_path: String? -- small (150px) thumbnail on disk when image; null for non-image
|
thumbnail_path: String? -- small (150px) thumbnail on disk when image; null for non-image
|
||||||
@@ -240,6 +294,13 @@ value ScriptsView {
|
|||||||
items: List<ScriptListItem>
|
items: List<ScriptListItem>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface ScriptsViewSurface {
|
||||||
|
context view: ScriptsView
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
view.items.count
|
||||||
|
}
|
||||||
|
|
||||||
value ScriptListItem {
|
value ScriptListItem {
|
||||||
script_id: String
|
script_id: String
|
||||||
title: String -- truncated with ellipsis on overflow
|
title: String -- truncated with ellipsis on overflow
|
||||||
@@ -299,6 +360,13 @@ value TemplatesView {
|
|||||||
items: List<TemplateListItem>
|
items: List<TemplateListItem>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface TemplatesViewSurface {
|
||||||
|
context view: TemplatesView
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
view.items.count
|
||||||
|
}
|
||||||
|
|
||||||
value TemplateListItem {
|
value TemplateListItem {
|
||||||
template_id: String
|
template_id: String
|
||||||
title: String -- truncated with ellipsis on overflow
|
title: String -- truncated with ellipsis on overflow
|
||||||
@@ -398,6 +466,13 @@ value SettingsNav {
|
|||||||
active_section: String? -- persisted across sidebar switches
|
active_section: String? -- persisted across sidebar switches
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface SettingsNavSurface {
|
||||||
|
context nav: SettingsNav
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
nav.active_section when nav.active_section != null
|
||||||
|
}
|
||||||
|
|
||||||
rule SettingsNavClick {
|
rule SettingsNavClick {
|
||||||
when: SettingsNavEntryClicked(section)
|
when: SettingsNavEntryClicked(section)
|
||||||
if section = style:
|
if section = style:
|
||||||
@@ -452,6 +527,13 @@ value TagsNav {
|
|||||||
active_section: String? -- persisted
|
active_section: String? -- persisted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface TagsNavSurface {
|
||||||
|
context nav: TagsNav
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
nav.active_section when nav.active_section != null
|
||||||
|
}
|
||||||
|
|
||||||
rule TagsNavClick {
|
rule TagsNavClick {
|
||||||
when: TagsNavEntryClicked(section)
|
when: TagsNavEntryClicked(section)
|
||||||
ensures: OpenTabRequested(type: tags, id: tags, intent: pin)
|
ensures: OpenTabRequested(type: tags, id: tags, intent: pin)
|
||||||
@@ -469,6 +551,14 @@ value ChatView {
|
|||||||
items: List<ChatListItem>
|
items: List<ChatListItem>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface ChatViewSurface {
|
||||||
|
context view: ChatView
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
view.api_ready
|
||||||
|
view.items.count
|
||||||
|
}
|
||||||
|
|
||||||
value ChatListItem {
|
value ChatListItem {
|
||||||
conversation_id: String
|
conversation_id: String
|
||||||
title: String -- live-updated via onTitleUpdated
|
title: String -- live-updated via onTitleUpdated
|
||||||
@@ -514,6 +604,13 @@ value ImportView {
|
|||||||
items: List<ImportListItem>
|
items: List<ImportListItem>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface ImportViewSurface {
|
||||||
|
context view: ImportView
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
view.items.count
|
||||||
|
}
|
||||||
|
|
||||||
value ImportListItem {
|
value ImportListItem {
|
||||||
definition_id: String
|
definition_id: String
|
||||||
name: String -- live-updated via onNameUpdated
|
name: String -- live-updated via onNameUpdated
|
||||||
@@ -571,6 +668,22 @@ value GitActiveView {
|
|||||||
has_more_history: Boolean -- paginated, 20 per page
|
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 {
|
value GitStatusFile {
|
||||||
path: String
|
path: String
|
||||||
status: String -- modified, added, deleted, renamed, etc.
|
status: String -- modified, added, deleted, renamed, etc.
|
||||||
@@ -672,6 +785,14 @@ value CalendarYear {
|
|||||||
months: List<CalendarMonth>
|
months: List<CalendarMonth>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface CalendarYearSurface {
|
||||||
|
context calendar_year: CalendarYear
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
calendar_year.year
|
||||||
|
calendar_year.months.count
|
||||||
|
}
|
||||||
|
|
||||||
value CalendarMonth {
|
value CalendarMonth {
|
||||||
month: Integer -- 1-12
|
month: Integer -- 1-12
|
||||||
count: Integer -- number of items in this month
|
count: Integer -- number of items in this month
|
||||||
|
|||||||
@@ -8,6 +8,25 @@
|
|||||||
|
|
||||||
use "./layout.allium" as layout
|
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 ────────────────────────────────────────────────
|
-- ─── Tab types ────────────────────────────────────────────────
|
||||||
|
|
||||||
-- 17 distinct tab types, each routing to a matching editor view.
|
-- 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)
|
is_transient: Boolean -- true = preview tab (italic title, replaceable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface TabSurface {
|
||||||
|
context tab: Tab
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
tab.type
|
||||||
|
tab.id
|
||||||
|
tab.is_transient
|
||||||
|
}
|
||||||
|
|
||||||
-- ─── Tab categories ───────────────────────────────────────────
|
-- ─── Tab categories ───────────────────────────────────────────
|
||||||
|
|
||||||
-- 1. Singleton tool tabs: always one instance, never transient, id = type name.
|
-- 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
|
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:
|
-- Tab title resolution:
|
||||||
-- post: post.title from DB (listens post-updated events); no JS truncation
|
-- post: post.title from DB (listens post-updated events); no JS truncation
|
||||||
-- media: media.originalName; no JS truncation
|
-- media: media.originalName; no JS truncation
|
||||||
|
|||||||
@@ -21,6 +21,42 @@ surface TemplateRenderingSurface {
|
|||||||
RenderNotFoundPageRequested()
|
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 {
|
value RenderContext {
|
||||||
-- Top-level variables available in all templates
|
-- Top-level variables available in all templates
|
||||||
language: String -- Current language code
|
language: String -- Current language code
|
||||||
|
|||||||
Reference in New Issue
Block a user