chore: extend spec for UI/UX
This commit is contained in:
@@ -26,6 +26,11 @@ use "./publishing.allium" as publishing -- SSH upload (SCP / rsync)
|
|||||||
use "./task.allium" as task -- Background task manager
|
use "./task.allium" as task -- Background task manager
|
||||||
use "./i18n.allium" as i18n -- Split localization (UI vs content)
|
use "./i18n.allium" as i18n -- Split localization (UI vs content)
|
||||||
|
|
||||||
|
-- UI
|
||||||
|
use "./layout.allium" as layout -- App shell, activity bar, status bar, panels
|
||||||
|
use "./tabs.allium" as tabs -- Tab system, editor routing, preview/pin
|
||||||
|
use "./sidebar_views.allium" as sidebar -- 10 sidebar views, content, behaviour
|
||||||
|
|
||||||
-- Integration
|
-- Integration
|
||||||
use "./git.allium" as git -- Git operations, LFS, reconciliation
|
use "./git.allium" as git -- Git operations, LFS, reconciliation
|
||||||
use "./mcp.allium" as mcp -- MCP server (tools, resources, proposals)
|
use "./mcp.allium" as mcp -- MCP server (tools, resources, proposals)
|
||||||
|
|||||||
252
specs/layout.allium
Normal file
252
specs/layout.allium
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
-- allium: 1
|
||||||
|
-- bDS Application Layout
|
||||||
|
-- Scope: UI shell (all waves)
|
||||||
|
-- Distilled from: src/renderer/App.tsx, ActivityBar.tsx, StatusBar.tsx,
|
||||||
|
-- Panel.tsx, WindowTitleBar.tsx, ResizablePanel, Sidebar.tsx
|
||||||
|
|
||||||
|
-- The top-level visual structure of the application window.
|
||||||
|
-- Describes the shell (regions, toggle behaviour, resize constraints)
|
||||||
|
-- but NOT the content of each region (see tabs.allium, sidebar_views.allium).
|
||||||
|
|
||||||
|
use "./i18n.allium" as i18n
|
||||||
|
use "./task.allium" as task
|
||||||
|
|
||||||
|
-- ─── Window shell ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- +------------------------------------------------------------+
|
||||||
|
-- | WindowTitleBar |
|
||||||
|
-- +----+--------+----------------------------+-----------------+
|
||||||
|
-- | A | Side- | TabBar | Assistant |
|
||||||
|
-- | c | bar |----------------------------| Sidebar |
|
||||||
|
-- | t | (resz) | Editor (routed by tab) | |
|
||||||
|
-- | i | |----------------------------+ |
|
||||||
|
-- | v | | Panel (bottom) | |
|
||||||
|
-- | i | | | |
|
||||||
|
-- | t | | | |
|
||||||
|
-- | y | | | |
|
||||||
|
-- +----+--------+----------------------------+-----------------+
|
||||||
|
-- | StatusBar |
|
||||||
|
-- +------------------------------------------------------------+
|
||||||
|
|
||||||
|
value AppShell {
|
||||||
|
title_bar: WindowTitleBar
|
||||||
|
activity_bar: ActivityBar
|
||||||
|
sidebar: ResizableRegion
|
||||||
|
content_area: ContentArea
|
||||||
|
assistant_sidebar: ResizableRegion
|
||||||
|
status_bar: StatusBar
|
||||||
|
}
|
||||||
|
|
||||||
|
value ContentArea {
|
||||||
|
-- tab_bar: see tabs.allium
|
||||||
|
-- editor: routed by active tab; see tabs.allium
|
||||||
|
panel: Panel
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── Resizable regions ────────────────────────────────────────
|
||||||
|
|
||||||
|
config {
|
||||||
|
sidebar_initial_width: Integer = 280
|
||||||
|
sidebar_min_width: Integer = 200
|
||||||
|
sidebar_max_width: Integer = 500
|
||||||
|
assistant_initial_width: Integer = 360
|
||||||
|
assistant_min_width: Integer = 280
|
||||||
|
assistant_max_width: Integer = 640
|
||||||
|
}
|
||||||
|
|
||||||
|
value ResizableRegion {
|
||||||
|
visible: Boolean
|
||||||
|
width: Integer
|
||||||
|
min_width: Integer
|
||||||
|
max_width: Integer
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── Toggle state ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
value ShellVisibility {
|
||||||
|
sidebar_visible: Boolean
|
||||||
|
panel_visible: Boolean
|
||||||
|
assistant_sidebar_visible: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
rule ToggleSidebar {
|
||||||
|
when: ToggleSidebarRequested()
|
||||||
|
ensures: sidebar_visible = not sidebar_visible
|
||||||
|
}
|
||||||
|
|
||||||
|
rule TogglePanel {
|
||||||
|
when: TogglePanelRequested()
|
||||||
|
ensures: panel_visible = not panel_visible
|
||||||
|
}
|
||||||
|
|
||||||
|
rule ToggleAssistantSidebar {
|
||||||
|
when: ToggleAssistantSidebarRequested()
|
||||||
|
ensures: assistant_sidebar_visible = not assistant_sidebar_visible
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── Window title bar ─────────────────────────────────────────
|
||||||
|
|
||||||
|
value WindowTitleBar {
|
||||||
|
-- Platform-adaptive
|
||||||
|
-- menu_bar: rendered only on non-Mac platforms (5 groups: File, Edit, View, Blog, Help)
|
||||||
|
-- Keyboard: Alt opens mnemonics, Alt+letter opens group
|
||||||
|
-- Arrow keys navigate groups and items, Enter/Space activates
|
||||||
|
-- View group hides devTools toggle when not in dev mode
|
||||||
|
title: String -- document.title, fallback "Blogging Desktop Server"
|
||||||
|
-- Three toggle buttons (all platforms): sidebar, panel, assistant
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── Activity bar ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Narrow vertical icon strip at the far-left edge of the window.
|
||||||
|
-- Two groups: top (content views) and bottom (tools).
|
||||||
|
|
||||||
|
value ActivityBar {
|
||||||
|
top_group: List<ActivityButton>
|
||||||
|
bottom_group: List<ActivityButton>
|
||||||
|
}
|
||||||
|
|
||||||
|
value ActivityButton {
|
||||||
|
id: String -- matches SidebarView name
|
||||||
|
label_key: String -- i18n key for tooltip
|
||||||
|
badge: Badge? -- only git has a badge
|
||||||
|
active: Boolean -- highlighted when this view is showing
|
||||||
|
}
|
||||||
|
|
||||||
|
value Badge {
|
||||||
|
count: Integer
|
||||||
|
display: String -- count capped at "99+"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Exhaustive activity list with preserved order
|
||||||
|
-- Top group (content views):
|
||||||
|
-- 1. posts i18n:activity.posts
|
||||||
|
-- 2. pages i18n:activity.pages
|
||||||
|
-- 3. media i18n:activity.media
|
||||||
|
-- 4. scripts i18n:activity.scripts
|
||||||
|
-- 5. templates i18n:activity.templates
|
||||||
|
-- 6. tags i18n:activity.tags
|
||||||
|
-- 7. chat i18n:activity.aiAssistant
|
||||||
|
-- 8. import i18n:activity.import
|
||||||
|
-- Bottom group (tools):
|
||||||
|
-- 9. git i18n:activity.sourceControl (badge: pending pull count)
|
||||||
|
-- 10. settings i18n:common.settings
|
||||||
|
|
||||||
|
-- Each activity ID maps 1:1 to a SidebarView of the same name.
|
||||||
|
|
||||||
|
-- ─── Activity click behaviour ─────────────────────────────────
|
||||||
|
|
||||||
|
-- All activities share the same toggle-sidebar strategy.
|
||||||
|
-- The sidebar shows the view that matches the clicked activity.
|
||||||
|
|
||||||
|
rule ActivityClick {
|
||||||
|
when: ActivityClicked(activity_id)
|
||||||
|
let target_view = activity_id
|
||||||
|
if active_view = target_view:
|
||||||
|
ensures: ToggleSidebarRequested()
|
||||||
|
-- If already on this view, toggle sidebar open/closed
|
||||||
|
else:
|
||||||
|
ensures: active_view = target_view
|
||||||
|
if not sidebar_visible:
|
||||||
|
ensures: ToggleSidebarRequested()
|
||||||
|
-- Switch view; open sidebar if hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant ActivityActiveHighlight {
|
||||||
|
-- An activity button shows active state iff its view is the
|
||||||
|
-- current active_view AND the sidebar is visible
|
||||||
|
for btn in ActivityBar.all_buttons:
|
||||||
|
btn.active = (active_view = btn.id and sidebar_visible)
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── Git badge ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Only the git activity button carries a badge.
|
||||||
|
-- Badge shows remote "behind" count, polled every 30 seconds.
|
||||||
|
|
||||||
|
config {
|
||||||
|
git_badge_poll_interval: Integer = 30
|
||||||
|
-- seconds between badge refresh polls
|
||||||
|
}
|
||||||
|
|
||||||
|
rule RefreshGitBadge {
|
||||||
|
when: GitBadgePollTick(badge)
|
||||||
|
requires: online and active_project != null
|
||||||
|
let repo_state = git.getRepoState()
|
||||||
|
requires: repo_state.is_repo and repo_state.has_remote
|
||||||
|
ensures: git.fetch()
|
||||||
|
let remote_state = git.getRemoteState()
|
||||||
|
ensures: badge.count = max(0, remote_state.behind)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule ClearGitBadge {
|
||||||
|
when: ClearGitBadgeTick(badge)
|
||||||
|
requires: not online or active_project = null or not is_repo or not has_remote
|
||||||
|
ensures: badge.count = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── Bottom panel ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
value Panel {
|
||||||
|
visible: Boolean
|
||||||
|
active_tab: String -- tasks | output | post_links | git_log
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Panel tab availability depends on active editor tab
|
||||||
|
invariant PanelTabAvailability {
|
||||||
|
-- tasks: always available
|
||||||
|
-- output: always available
|
||||||
|
-- post_links: only when active editor tab is a post
|
||||||
|
-- git_log: only when active editor tab is a post or media
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant PanelTabFallback {
|
||||||
|
-- If active panel tab becomes unavailable, fall back to tasks
|
||||||
|
-- post_links unavailable when no post tab is active
|
||||||
|
-- git_log unavailable when neither post nor media tab is active
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Tasks tab: last 10 tasks, newest first, with progress/cancel.
|
||||||
|
-- Tasks with shared group_id are collapsible groups showing aggregate progress.
|
||||||
|
-- Output tab: log entries with copy-all button.
|
||||||
|
-- Post Links tab: backlinks (posts linking here) + outlinks (posts linked from here).
|
||||||
|
-- Each entry clickable, opens linked post as pinned tab.
|
||||||
|
-- Git Log tab: file-level git history for active post/media (up to 50 entries).
|
||||||
|
-- For posts: path = posts/YYYY/MM/{slug}.md
|
||||||
|
-- For media: path relative to project root
|
||||||
|
|
||||||
|
-- ─── Status bar ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
value StatusBar {
|
||||||
|
left: StatusBarLeft
|
||||||
|
right: StatusBarRight
|
||||||
|
}
|
||||||
|
|
||||||
|
value StatusBarLeft {
|
||||||
|
-- Project selector dropdown to switch active project
|
||||||
|
running_task_message: String? -- spinner + message when tasks running
|
||||||
|
running_task_overflow: Integer? -- "+N more" count when multiple running
|
||||||
|
}
|
||||||
|
|
||||||
|
value StatusBarRight {
|
||||||
|
-- In display order (left to right):
|
||||||
|
post_status: String? -- draft|published|archived dot, when post tab active
|
||||||
|
post_count: String -- "{count} posts"
|
||||||
|
media_count: String -- "{count} media"
|
||||||
|
token_usage: TokenUsage? -- shown only when active tab is chat
|
||||||
|
theme_badge: String -- pico theme name
|
||||||
|
offline_mode: Boolean -- airplane icon toggle, keyboard accessible
|
||||||
|
ui_language: String -- dropdown: en, de, fr, it, es
|
||||||
|
brand: String -- "bDS"
|
||||||
|
}
|
||||||
|
|
||||||
|
value TokenUsage {
|
||||||
|
input_tokens: Integer
|
||||||
|
output_tokens: Integer
|
||||||
|
cache_read_tokens: Integer
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── Keyboard shortcuts (global) ──────────────────────────────
|
||||||
|
|
||||||
|
-- Ctrl/Cmd+B: toggle sidebar
|
||||||
|
-- Ctrl/Cmd+W: close active tab (see tabs.allium)
|
||||||
352
specs/sidebar_views.allium
Normal file
352
specs/sidebar_views.allium
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
-- allium: 1
|
||||||
|
-- bDS Sidebar Views
|
||||||
|
-- Scope: UI content (all waves + extensions)
|
||||||
|
-- Distilled from: Sidebar.tsx, PostsList.tsx, MediaList.tsx,
|
||||||
|
-- SidebarEntityList.tsx, SettingsNav.tsx, TagsNav.tsx,
|
||||||
|
-- ChatList.tsx, ImportList.tsx, ScriptsList.tsx, TemplatesList.tsx,
|
||||||
|
-- GitSidebar.tsx, sidebarDateFormatting.ts
|
||||||
|
|
||||||
|
-- Describes the content and behaviour of each of the 10 sidebar views.
|
||||||
|
-- The sidebar shell (visibility, resize, view switching) is in layout.allium.
|
||||||
|
-- Tab opening behaviour is in tabs.allium.
|
||||||
|
|
||||||
|
use "./layout.allium" as layout
|
||||||
|
use "./tabs.allium" as tabs
|
||||||
|
use "./post.allium" as post
|
||||||
|
use "./media.allium" as media
|
||||||
|
use "./tag.allium" as tag
|
||||||
|
use "./i18n.allium" as i18n
|
||||||
|
|
||||||
|
-- ─── Sidebar view registry ───────────────────────────────────
|
||||||
|
|
||||||
|
-- 10 views: posts, pages, media, scripts, templates, settings, tags, chat, import, git
|
||||||
|
-- Default view: posts
|
||||||
|
-- The sidebar renders exactly one view at a time, selected by active_view.
|
||||||
|
-- Each ActivityId maps 1:1 to a SidebarView of the same name.
|
||||||
|
|
||||||
|
config {
|
||||||
|
default_sidebar_view: String = "posts"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── Shared patterns ─────────────────────────────────────────
|
||||||
|
|
||||||
|
-- SidebarEntityList: reusable pattern for scripts, templates, chat, import.
|
||||||
|
-- Shows: header with title + create button, scrollable item list,
|
||||||
|
-- empty state with message + action CTA.
|
||||||
|
|
||||||
|
-- Date formatting for sidebar items:
|
||||||
|
-- Today -> time only (e.g. "2:30 PM")
|
||||||
|
-- Yesterday -> i18n "Yesterday"
|
||||||
|
-- <7 days -> short weekday (e.g. "Mon")
|
||||||
|
-- Older -> "Mon DD" format
|
||||||
|
-- Locale map: en->en-US, de->de-DE, fr->fr-FR, it->it-IT, es->es-ES
|
||||||
|
|
||||||
|
-- ─── 1. Posts view ────────────────────────────────────────────
|
||||||
|
|
||||||
|
value PostsView {
|
||||||
|
mode: String -- "posts" or "pages"
|
||||||
|
search_query: String? -- FTS via posts.search(query)
|
||||||
|
filter_panel_visible: Boolean -- collapsible, toggled by icon button
|
||||||
|
calendar_filter: CalendarFilter? -- year/month archive tree
|
||||||
|
tag_filter: List<String> -- selected tags (multi-select chips with colours)
|
||||||
|
category_filter: List<String> -- selected categories (multi-select chips)
|
||||||
|
draft_section: List<PostListItem>
|
||||||
|
published_section: List<PostListItem>
|
||||||
|
archived_section: List<PostListItem>
|
||||||
|
has_more: Boolean -- pagination, 500 per batch
|
||||||
|
}
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
-- Filters auto-refresh when any post's status changes.
|
||||||
|
|
||||||
|
value PostListItem {
|
||||||
|
post_id: String
|
||||||
|
type_icon: String -- derived from categories (below)
|
||||||
|
title: String -- fallback "Untitled"
|
||||||
|
language_count: Integer? -- badge shown when availableLanguages.count > 1
|
||||||
|
date: String -- drafts/archived: updatedAt; published: publishedAt ?? updatedAt
|
||||||
|
active: Boolean -- highlighted when activeTabId = post.id
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Post type icon derivation from categories (first match wins, case-insensitive):
|
||||||
|
-- picture/photo/image -> camera icon
|
||||||
|
-- aside/note/quick -> notepad icon
|
||||||
|
-- link/bookmark -> link icon
|
||||||
|
-- video -> film icon
|
||||||
|
-- quote -> speech bubble icon
|
||||||
|
-- default -> document icon
|
||||||
|
|
||||||
|
rule PostListClick {
|
||||||
|
when: PostListItemClicked(post_id, click_type)
|
||||||
|
if click_type = single:
|
||||||
|
ensures: OpenTabRequested(type: post, id: post_id, intent: preview)
|
||||||
|
if click_type = double:
|
||||||
|
ensures: OpenTabRequested(type: post, id: post_id, intent: pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── 2. Pages view ───────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Identical to PostsView but:
|
||||||
|
-- mode = "pages"
|
||||||
|
-- Filters to posts with "page" category
|
||||||
|
-- Create button auto-adds "page" category
|
||||||
|
-- Category filter excludes "page" from chips but auto-merges into backend calls
|
||||||
|
|
||||||
|
-- ─── 3. Media view ────────────────────────────────────────────
|
||||||
|
|
||||||
|
value MediaView {
|
||||||
|
search_query: String? -- FTS via media.search(query)
|
||||||
|
filter_panel_visible: Boolean
|
||||||
|
calendar_filter: CalendarFilter?
|
||||||
|
tag_filter: List<String> -- tags only, no categories for media
|
||||||
|
grid: List<MediaGridItem> -- grid layout (not list)
|
||||||
|
}
|
||||||
|
|
||||||
|
value MediaGridItem {
|
||||||
|
media_id: String
|
||||||
|
has_thumbnail: Boolean -- image: custom protocol bds-thumb://{id}; non-image: file icon
|
||||||
|
name: String -- title truncated 60 chars, fallback originalName
|
||||||
|
file_size: String -- formatted (B / KB / MB)
|
||||||
|
tooltip: String -- caption ?? originalName
|
||||||
|
active: Boolean -- highlighted when activeTabId = media.id
|
||||||
|
}
|
||||||
|
|
||||||
|
rule MediaListClick {
|
||||||
|
when: MediaGridItemClicked(media_id, click_type)
|
||||||
|
if click_type = single:
|
||||||
|
ensures: OpenTabRequested(type: media, id: media_id, intent: preview)
|
||||||
|
if click_type = double:
|
||||||
|
ensures: OpenTabRequested(type: media, id: media_id, intent: pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Import button: opens native file import dialog
|
||||||
|
|
||||||
|
-- ─── 4. Scripts view ──────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Uses SidebarEntityList pattern.
|
||||||
|
|
||||||
|
value ScriptsView {
|
||||||
|
items: List<ScriptListItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
value ScriptListItem {
|
||||||
|
script_id: String
|
||||||
|
title: String
|
||||||
|
date: String -- relative date format
|
||||||
|
active: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
rule ScriptListClick {
|
||||||
|
when: ScriptListItemClicked(script_id, click_type)
|
||||||
|
if click_type = single:
|
||||||
|
ensures: OpenTabRequested(type: scripts, id: script_id, intent: preview)
|
||||||
|
if click_type = double:
|
||||||
|
ensures: OpenTabRequested(type: scripts, id: script_id, intent: pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Keyboard: Enter = pin, Space = preview.
|
||||||
|
-- Delete button: deletes script and closes its tab.
|
||||||
|
-- Create defaults: kind=utility, content='print("new script")', entrypoint='render', enabled=true.
|
||||||
|
-- Refreshes on scripts-changed event.
|
||||||
|
|
||||||
|
-- ─── 5. Templates view ───────────────────────────────────────
|
||||||
|
|
||||||
|
-- Uses SidebarEntityList pattern. Same as scripts with:
|
||||||
|
|
||||||
|
value TemplatesView {
|
||||||
|
items: List<TemplateListItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
value TemplateListItem {
|
||||||
|
template_id: String
|
||||||
|
title: String
|
||||||
|
date: String
|
||||||
|
active: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
rule TemplateListClick {
|
||||||
|
when: TemplateListItemClicked(template_id, click_type)
|
||||||
|
if click_type = single:
|
||||||
|
ensures: OpenTabRequested(type: templates, id: template_id, intent: preview)
|
||||||
|
if click_type = double:
|
||||||
|
ensures: OpenTabRequested(type: templates, id: template_id, intent: pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Delete: if referenced by posts or tags, shows confirmation with counts before force-delete.
|
||||||
|
-- Create defaults: kind=post, content='', enabled=true.
|
||||||
|
-- Refreshes on templates-changed event.
|
||||||
|
|
||||||
|
-- ─── 6. Settings view ────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Navigation list that controls sections within the settings editor tab.
|
||||||
|
-- 9 sections in order:
|
||||||
|
-- 1. project (folder icon)
|
||||||
|
-- 2. editor (notepad icon)
|
||||||
|
-- 3. content (clipboard icon)
|
||||||
|
-- 4. ai (robot icon)
|
||||||
|
-- 5. technology (gear icon)
|
||||||
|
-- 6. publishing (rocket icon)
|
||||||
|
-- 7. data (database icon)
|
||||||
|
-- 8. mcp (plug icon)
|
||||||
|
-- 9. style (palette icon) -- special: opens style tab, not settings section
|
||||||
|
|
||||||
|
value SettingsNav {
|
||||||
|
active_section: String? -- persisted across sidebar switches
|
||||||
|
}
|
||||||
|
|
||||||
|
rule SettingsNavClick {
|
||||||
|
when: SettingsNavEntryClicked(section)
|
||||||
|
if section = style:
|
||||||
|
ensures: OpenTabRequested(type: style, id: style, intent: pin)
|
||||||
|
else:
|
||||||
|
ensures: OpenTabRequested(type: settings, id: settings, intent: pin)
|
||||||
|
ensures: ScrollToSection(section)
|
||||||
|
ensures: active_section = section
|
||||||
|
-- Active section is persisted across sidebar switches
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── 7. Tags view ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Navigation list that controls sections within the tags editor tab.
|
||||||
|
-- 3 sections:
|
||||||
|
-- 1. cloud (cloud icon) -- tag cloud visualization
|
||||||
|
-- 2. manage (pencil icon) -- create/edit tags
|
||||||
|
-- 3. merge (merge icon) -- merge duplicate tags
|
||||||
|
|
||||||
|
value TagsNav {
|
||||||
|
active_section: String? -- persisted
|
||||||
|
}
|
||||||
|
|
||||||
|
rule TagsNavClick {
|
||||||
|
when: TagsNavEntryClicked(section)
|
||||||
|
ensures: OpenTabRequested(type: tags, id: tags, intent: pin)
|
||||||
|
ensures: ScrollToSection(section)
|
||||||
|
ensures: active_section = section
|
||||||
|
-- Active section is persisted across sidebar switches
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── 8. Chat view ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Uses SidebarEntityList pattern.
|
||||||
|
|
||||||
|
value ChatView {
|
||||||
|
api_ready: Boolean -- shows API key prompt if false
|
||||||
|
items: List<ChatListItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
value ChatListItem {
|
||||||
|
conversation_id: String
|
||||||
|
title: String -- live-updated via onTitleUpdated events
|
||||||
|
date: String -- relative date format
|
||||||
|
}
|
||||||
|
|
||||||
|
rule ChatListClick {
|
||||||
|
when: ChatListItemClicked(conversation_id)
|
||||||
|
ensures: OpenTabRequested(type: chat, id: conversation_id, intent: pin)
|
||||||
|
-- Chat tabs are always pinned
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Delete button: deletes conversation and closes its tab.
|
||||||
|
|
||||||
|
-- ─── 9. Import view ──────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Uses SidebarEntityList pattern.
|
||||||
|
|
||||||
|
value ImportView {
|
||||||
|
items: List<ImportListItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
value ImportListItem {
|
||||||
|
definition_id: String
|
||||||
|
name: String -- live-updated via onNameUpdated events
|
||||||
|
date: String
|
||||||
|
}
|
||||||
|
|
||||||
|
rule ImportListClick {
|
||||||
|
when: ImportListItemClicked(definition_id)
|
||||||
|
ensures: OpenTabRequested(type: import, id: definition_id, intent: pin)
|
||||||
|
-- Import tabs are always pinned
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── 10. Git view ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Full git interface, not the SidebarEntityList pattern.
|
||||||
|
-- Three possible states: loading, not_a_repo, active_repo.
|
||||||
|
|
||||||
|
-- State: not_a_repo
|
||||||
|
-- Remote URL text input + "Initialize Git" button.
|
||||||
|
-- Init progress with phase/percentage/detail, collapsible transcript.
|
||||||
|
|
||||||
|
-- State: active_repo
|
||||||
|
value GitActiveView {
|
||||||
|
branch: String -- current branch name
|
||||||
|
upstream: String? -- tracking info (local -> upstream)
|
||||||
|
ahead: Integer
|
||||||
|
behind: Integer
|
||||||
|
status_files: List<GitStatusFile>
|
||||||
|
history_entries: List<GitHistoryEntry>
|
||||||
|
has_more_history: Boolean -- paginated, 20 per page
|
||||||
|
}
|
||||||
|
|
||||||
|
value GitStatusFile {
|
||||||
|
path: String
|
||||||
|
status: String -- modified, added, deleted, renamed, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
value GitHistoryEntry {
|
||||||
|
short_hash: String
|
||||||
|
subject: String
|
||||||
|
author: String
|
||||||
|
date: String -- locale-formatted
|
||||||
|
sync_status: String -- synced, local_only, remote_only
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Action buttons: fetch, pull, push, prune_lfs. All disabled while any action is loading.
|
||||||
|
-- Changes section: file count, commit message input, commit button, file list.
|
||||||
|
-- Changes list polls every 2 seconds in background.
|
||||||
|
-- Remote state refreshes every 30 seconds with auto-fetch.
|
||||||
|
|
||||||
|
rule GitFileClick {
|
||||||
|
when: GitStatusFileClicked(file_path, click_type)
|
||||||
|
if click_type = single:
|
||||||
|
ensures: OpenTabRequested(type: git_diff, id: "git-diff:" + file_path, intent: preview)
|
||||||
|
if click_type = double:
|
||||||
|
ensures: OpenTabRequested(type: git_diff, id: "git-diff:" + file_path, intent: pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule GitHistoryClick {
|
||||||
|
when: GitHistoryEntryClicked(commit_hash, click_type)
|
||||||
|
if click_type = single:
|
||||||
|
ensures: OpenTabRequested(type: git_diff, id: "git-diff:commit:" + commit_hash, intent: preview)
|
||||||
|
if click_type = double:
|
||||||
|
ensures: OpenTabRequested(type: git_diff, id: "git-diff:commit:" + commit_hash, intent: pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule GitCommit {
|
||||||
|
when: GitCommitRequested(message)
|
||||||
|
ensures: git.commitAll(message)
|
||||||
|
-- Also: all git_diff tabs are closed and git state is reloaded
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── Calendar archive (shared widget) ─────────────────────────
|
||||||
|
|
||||||
|
-- Collapsible year/month tree.
|
||||||
|
-- Selecting a year loads all posts/media for that year.
|
||||||
|
-- Selecting a month narrows to that month.
|
||||||
|
|
||||||
|
value CalendarFilter {
|
||||||
|
selected_year: Integer?
|
||||||
|
selected_month: Integer? -- 1-12
|
||||||
|
}
|
||||||
|
|
||||||
|
value CalendarYear {
|
||||||
|
year: Integer
|
||||||
|
months: List<CalendarMonth>
|
||||||
|
}
|
||||||
|
|
||||||
|
value CalendarMonth {
|
||||||
|
month: Integer -- 1-12
|
||||||
|
count: Integer -- number of items in this month
|
||||||
|
}
|
||||||
198
specs/tabs.allium
Normal file
198
specs/tabs.allium
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
-- 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
|
||||||
|
|
||||||
|
-- ─── 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── 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).
|
||||||
|
|
||||||
|
value TabBarItem {
|
||||||
|
title: String
|
||||||
|
is_active: Boolean
|
||||||
|
is_transient: Boolean -- italic title when true
|
||||||
|
is_dirty: Boolean -- dot indicator, only for post tabs
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Tab title resolution:
|
||||||
|
-- post: post.title from DB (listens post-updated events)
|
||||||
|
-- media: media.originalName
|
||||||
|
-- scripts: script.title from DB (listens scripts-changed)
|
||||||
|
-- templates: template.title from DB (listens templates-changed)
|
||||||
|
-- chat: conversation.title, truncated to 18 chars
|
||||||
|
-- import: definition.name (listens name-updated)
|
||||||
|
-- git_diff file: filename from path (last path segment)
|
||||||
|
-- git_diff commit: "{shortHash} {subject}" from git history
|
||||||
|
-- singletons: i18n key lookup (common.settings, tabBar.style, etc.)
|
||||||
|
-- fallback: i18n:tabBar.unknown
|
||||||
|
|
||||||
|
-- ─── 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.
|
||||||
Reference in New Issue
Block a user