248 lines
9.7 KiB
Plaintext
248 lines
9.7 KiB
Plaintext
-- allium: 1
|
|
-- bDS Tab System and Editor Routing
|
|
-- Scope: UI navigation (all waves)
|
|
-- Distilled from: tabPolicy.ts, TabBar.tsx, editorRouting.ts, appStore.ts
|
|
|
|
-- Governs the tab bar, tab lifecycle (open/close/pin), editor routing,
|
|
-- and the relationship between tabs and the content area.
|
|
|
|
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.
|
|
-- Plus "dashboard" as the no-tab default view.
|
|
--
|
|
-- Tab types: post, media, settings, style, tags, chat, import,
|
|
-- menu_editor, metadata_diff, git_diff, documentation,
|
|
-- api_documentation, site_validation, translation_validation,
|
|
-- scripts, templates, find_duplicates
|
|
--
|
|
-- Editor routes: all of the above plus "dashboard" (shown when no tab is active).
|
|
-- Route registry is 1:1: every tab type maps to itself as an editor route.
|
|
|
|
-- ─── Tab entity ───────────────────────────────────────────────
|
|
|
|
value Tab {
|
|
type: String -- one of the 17 tab types
|
|
id: String -- singleton: id = type name; entity: external ID
|
|
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.
|
|
-- settings, tags, style, scripts (bare), menu_editor, documentation,
|
|
-- api_documentation, metadata_diff, site_validation,
|
|
-- translation_validation, find_duplicates
|
|
-- Total: 11 singleton types.
|
|
|
|
-- 2. Entity tabs: keyed by external ID, support preview/pin intent.
|
|
-- post (id = postId), media (id = mediaId)
|
|
|
|
-- 3. Script tabs: type = scripts, id = scriptId (NOT the singleton).
|
|
-- Support preview/pin intent.
|
|
|
|
-- 4. Template tabs: type = templates, id = templateId.
|
|
-- Support preview/pin intent.
|
|
|
|
-- 5. Chat tabs: type = chat, id = conversationId. Always pinned (not transient).
|
|
|
|
-- 6. Import tabs: type = import, id = definitionId. Always pinned.
|
|
|
|
-- 7. Git diff tabs: type = git_diff.
|
|
-- File diff: id = "git-diff:{filePath}"
|
|
-- Commit diff: id = "git-diff:commit:{commitHash}"
|
|
-- Support preview/pin intent.
|
|
|
|
-- ─── Open intent ──────────────────────────────────────────────
|
|
|
|
-- preview: transient tab (replaced by next preview of same type)
|
|
-- pin: permanent tab (persists until explicitly closed)
|
|
|
|
rule DeriveTransient {
|
|
when: TabOpening(tab_type, intent)
|
|
if tab_type in singleton_tool_tabs:
|
|
ensures: tab.is_transient = false
|
|
else if tab_type = chat or tab_type = import:
|
|
ensures: tab.is_transient = false
|
|
else:
|
|
ensures: tab.is_transient = (intent = preview)
|
|
}
|
|
|
|
-- ─── Tab lifecycle ────────────────────────────────────────────
|
|
|
|
rule OpenTab {
|
|
when: OpenTabRequested(type, id, intent)
|
|
|
|
-- Dedup: if tab with same (type, id) already exists, activate it.
|
|
-- If intent = pin, also set is_transient = false.
|
|
-- Transient replacement: if opening as transient and a transient tab
|
|
-- of same type exists, replace it with the new tab.
|
|
-- Otherwise: append a new tab.
|
|
-- Always sets active_tab to the opened/reused tab.
|
|
ensures: active_tab = resolved_tab
|
|
}
|
|
|
|
rule OpenTabInBackground {
|
|
when: OpenTabInBackgroundRequested(type, id, intent)
|
|
-- Same dedup/replace logic as OpenTab, but does NOT change active_tab
|
|
}
|
|
|
|
rule CloseTab {
|
|
when: CloseTabRequested(tab)
|
|
ensures: not exists tab
|
|
-- If tab was active: activate next tab at same index, or last tab, or null
|
|
}
|
|
|
|
rule PinTab {
|
|
when: PinTabRequested(tab)
|
|
ensures: tab.is_transient = false
|
|
}
|
|
|
|
rule ClearTabs {
|
|
when: ClearTabsRequested()
|
|
ensures: tabs = empty
|
|
ensures: active_tab = null
|
|
}
|
|
|
|
-- ─── Editor routing ───────────────────────────────────────────
|
|
|
|
-- The editor content area renders a view based on the active tab.
|
|
|
|
rule ResolveEditorRoute {
|
|
when: ActiveTabChanged(active_tab)
|
|
if active_tab = null:
|
|
ensures: editor_route = dashboard
|
|
else:
|
|
ensures: editor_route = active_tab.type
|
|
-- 1:1 mapping; every tab type maps to itself as editor route
|
|
}
|
|
|
|
-- ─── Editor views (what each route renders) ───────────────────
|
|
|
|
-- dashboard: Overview stats, timeline, tag cloud, recent posts
|
|
-- post: Post editor (keyed by postId)
|
|
-- media: Media editor (keyed by mediaId)
|
|
-- settings: Settings view with scrollable sections
|
|
-- style: Pico CSS theme editor
|
|
-- tags: Tag cloud, create/edit, merge sections
|
|
-- chat: AI chat panel (keyed by conversationId)
|
|
-- import: Import analysis view (keyed by definitionId)
|
|
-- menu_editor: OPML menu editor
|
|
-- metadata_diff: DB vs filesystem diff viewer
|
|
-- git_diff: Git diff view (file or commit, keyed by tab id)
|
|
-- documentation: Rendered markdown (DOCUMENTATION.md)
|
|
-- api_documentation: Rendered markdown (API.md)
|
|
-- site_validation: Generated site link/structure validation
|
|
-- translation_validation: Translation completeness checks
|
|
-- scripts: Script editor (keyed by scriptId)
|
|
-- templates: Template editor (keyed by templateId)
|
|
-- find_duplicates: Duplicate post detection via embeddings
|
|
|
|
-- ─── Tab bar rendering ───────────────────────────────────────
|
|
|
|
-- Hidden when no tabs exist.
|
|
-- Horizontal strip with overflow scroll (left/right arrow buttons, 150px per click).
|
|
-- Auto-scrolls to bring active tab into view (10px padding).
|
|
|
|
config {
|
|
tab_min_width: Integer = 100
|
|
tab_max_width: Integer = 160
|
|
tab_scroll_step: Integer = 150
|
|
chat_title_max_length: Integer = 18
|
|
git_hash_display_length: Integer = 7
|
|
}
|
|
|
|
value TabBarItem {
|
|
title: String -- resolved per type (see below); CSS ellipsis at tab_max_width
|
|
is_active: Boolean
|
|
is_transient: Boolean -- italic title when true
|
|
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
|
|
-- scripts: script.title from DB (listens scripts-changed); no JS truncation
|
|
-- templates: template.title from DB (listens templates-changed); no JS truncation
|
|
-- chat: conversation.title, JS-truncated to 18 chars + "..." if over limit
|
|
-- import: definition.name (listens name-updated); no JS truncation
|
|
-- git_diff file: filename only (last path segment); no JS truncation
|
|
-- git_diff commit: "{shortHash} {subject}" (shortHash = 7 chars); fallback: 7-char hash only
|
|
-- singletons: i18n key lookup (common.settings, tabBar.style, etc.)
|
|
-- fallback: i18n:tabBar.unknown
|
|
--
|
|
-- All tab titles are additionally CSS-truncated (text-overflow:ellipsis, white-space:nowrap)
|
|
-- within the tab's max-width of 160px.
|
|
|
|
-- ─── Tab interactions ─────────────────────────────────────────
|
|
|
|
-- Single click on tab: activate it
|
|
-- Double click on tab: if transient, pin it
|
|
-- Middle click on tab: close it
|
|
-- Close button: close the tab
|
|
|
|
-- ─── Dirty tracking ──────────────────────────────────────────
|
|
|
|
invariant DirtyIndicator {
|
|
-- Only post tabs show dirty state
|
|
-- A post tab is dirty when its in-memory content differs from saved
|
|
for tab in tabs:
|
|
tab.is_dirty = (tab.type = post and dirtyPosts.contains(tab.id))
|
|
}
|
|
|
|
-- ─── Tab tooltip ──────────────────────────────────────────────
|
|
|
|
-- Base: tab title
|
|
-- If transient: append " (Preview)"
|
|
-- If dirty: append " * Modified"
|
|
|
|
-- ─── Keyboard shortcuts ──────────────────────────────────────
|
|
|
|
-- Ctrl/Cmd+W: close active tab
|
|
-- Ctrl/Cmd+B: toggle sidebar (see layout.allium)
|
|
|
|
-- ─── Tab state persistence ───────────────────────────────────
|
|
|
|
-- Tab state (list + activeTabId) can be serialized/restored
|
|
-- for session continuity across project switches.
|