-- 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.
