-- allium: 1 -- bDS UI Data Flow Model -- Scope: cross-cutting (all waves) -- Distilled from: appStore.ts, PostEditor.tsx, MediaEditor.tsx, Editor.tsx, -- Sidebar.tsx, NotificationWatcher.ts -- Describes the reactive data flows that connect sidebar, editor, tab bar, -- and backend engine operations. Covers: sidebar->editor, editor->sidebar, -- cross-tab coordination, and backend->UI event propagation. use "./layout.allium" as layout use "./tabs.allium" as tabs use "./sidebar_views.allium" as sidebar -- ─── External surfaces ────────────────────────────────────── -- User-initiated navigation and entity management actions. -- These triggers originate from sidebar clicks, tab operations, -- dashboard interactions, and save/delete actions in editors. surface UserNavigation { provides: SidebarItemClicked(entity_type, entity_id, click_type) provides: SidebarCreateRequested(entity_type) provides: SidebarDeleteRequested(entity_type, entity_id) provides: DashboardPostClicked(post_id, click_type) provides: PostSaved(post_id, updated_post) provides: PostStatusTransitioned(post_id, old_status, new_status) provides: PostDeletedFromEditor(post_id) provides: MediaSavedFromEditor(media_id, updated_media) provides: MediaDeletedFromEditor(media_id) provides: SettingsRebuildCompleted(entity_type, new_data) provides: TransientTabBeingReplaced(old_tab, new_tab) provides: TabClosed(tab) } -- ─── 1. Sidebar -> Editor flows ────────────────────────────── -- All UI coordination flows through shared application state. -- There are no direct calls between sidebar and editor. -- Both read from and write to the same state model; changes propagate -- reactively. rule SidebarEntityClick { when: SidebarItemClicked(entity_type, entity_id, click_type) -- Single click: open as transient/preview tab -- Double click: open as pinned tab -- See sidebar_views.allium for per-view click rules -- Effect: Editor mounts the matching view keyed by entity_id -- Effect: Sidebar item highlights based on activeTabId match -- If a prior transient tab of same type exists, it is replaced, -- and the old editor unmounts (triggering auto-save if dirty) } rule SidebarCreateEntity { when: SidebarCreateRequested(entity_type) -- Posts/Pages: backend creates post, emits post:created, -- store adds to posts list, sidebar re-renders with new item. -- Does NOT open a tab automatically. User must click to open. -- Sets selectedPostId for highlighting, optionally ensures sidebar visible. -- Scripts/Templates: backend creates, emits event, opens tab immediately. -- Chat: creates conversation, opens as pinned tab. -- Import: creates definition, opens as pinned tab. } rule SidebarDeleteEntity { when: SidebarDeleteRequested(entity_type, entity_id) -- Scripts/Templates/Chat/Import: backend deletes entity, -- then closeTab(entity_id) removes its tab, -- activates adjacent tab or shows dashboard. -- Posts/Media: deletion is triggered from the EDITOR (not sidebar). -- See editor_post.allium PostDeleteAction / editor_media.allium MediaDeleteAction. } invariant SidebarFilterIsolation { -- Sidebar search/filter state is local to the sidebar component. -- Filtering never affects: active tab, editor content, selectedPostId. -- Only the visible list of items changes. } -- ─── 2. Editor -> Sidebar flows ────────────────────────────── rule PostTitleChanged { when: PostSaved(post_id, updated_post) -- Auto-save fires after 3s idle, or on Ctrl+S, or on unmount/tab switch -- Store's posts array is updated with new title/metadata -- Sidebar PostsList re-renders reactively showing new title -- TabBar re-renders showing new title ensures: dirtyPosts.remove(post_id) } rule PostStatusChanged { when: PostStatusTransitioned(post_id, old_status, new_status) -- Store's posts array is updated with new status -- Sidebar detects status change (compares prev/current status maps) -- and re-runs search/filter if active -- Post moves between draft/published/archived sections in sidebar } rule PostEditorDelete { when: PostDeletedFromEditor(post_id) ensures: store.removePost(post_id) -- Removes from posts array, clears selectedPostId if matching, -- removes from dirtyPosts ensures: closeTab(post_id) -- Sidebar re-renders without the post } rule MediaEditorSave { when: MediaSavedFromEditor(media_id, updated_media) ensures: store.updateMedia(media_id, updated_media) -- Sidebar MediaList re-renders with updated metadata } rule MediaEditorDelete { when: MediaDeletedFromEditor(media_id) ensures: store.removeMedia(media_id) -- Editor.tsx safety net: detects active media tab references -- non-existent item, calls closeTab(activeTab.id) -- Sidebar re-renders without the media item } rule SettingsRebuild { when: SettingsRebuildCompleted(entity_type, new_data) -- Wholesale replacement of posts or media array in store ensures: store.setPosts(new_data) or store.setMedia(new_data) -- Sidebar re-renders entirely with fresh data } -- ─── 3. Cross-tab coordination ────────────────────────────── invariant TabSwitchDoesNotChangeSidebarView { -- Switching tabs does NOT change activeView in the sidebar. -- The sidebar view is controlled exclusively by the Activity Bar. -- Exception: Dashboard post click explicitly sets activeView = "posts". } invariant SidebarHighlightFollowsActiveTab { -- Sidebar item highlight is based on activeTabId === entity.id, -- NOT on selectedPostId/selectedMediaId (which are separate concepts -- used only for post creation flow). } rule TransientTabReplacement { when: TransientTabBeingReplaced(old_tab, new_tab) -- Old editor unmounts -> cleanup auto-save fires if dirty -- New editor mounts with new entity -- Sidebar highlight shifts to newly active entity } rule TabCloseCleanup { when: TabClosed(tab) -- If post tab: PostEditor unmounts, auto-save fires if dirty -- Store activates adjacent tab (prefer right, then left, then null) -- Sidebar highlight updates to new active tab -- If no tabs remain: dashboard shown } rule DashboardPostClick { when: DashboardPostClicked(post_id, click_type) -- This is the ONLY place where opening a tab also switches sidebar view ensures: setActiveView("posts") ensures: setSelectedPost(post_id) 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) } -- ─── 4. Backend -> UI event propagation ───────────────────── -- Backend engines emit events via IPC. The renderer listens and updates -- the shared store. Both sidebar and editor re-render reactively. -- Events: post:created, post:updated, post:deleted, -- media:imported, media:updated, media:deleted, -- template:created/updated/deleted, script:created/updated/deleted, -- entity:changed (from CLI/MCP mutations via NotificationWatcher) -- TabBar also listens directly for: -- post-updated (title refresh) -- bds:scripts-changed (script title refresh) -- BDS_EVENT_TEMPLATES_CHANGED (template title refresh) -- chat.onTitleUpdated (chat conversation title refresh) -- importDefinitions.onNameUpdated (import name refresh) -- Editor.tsx has safety-net useEffect guards that: -- Close media tabs when referenced media no longer exists in store -- Clear selectedPostId/selectedMediaId when entity is gone -- ─── 5. Keyboard shortcut map ───────────────────────────── -- All shortcuts use Cmd on macOS, Ctrl on other platforms. -- Global: -- Ctrl/Cmd+B: toggle sidebar visibility -- Ctrl/Cmd+W: close active tab -- Post editor: -- Ctrl/Cmd+S: save post immediately (resets auto-save timer) -- Ctrl/Cmd+K: open InsertModal (insert post link) -- Sidebar lists: -- Enter: open selected item as pinned tab -- Space: open selected item as preview/transient tab