refactor(electrobun): centralize all shared state into global stores
New stores: - ui-store.svelte.ts: settingsOpen, paletteOpen, searchOpen, notifDrawerOpen, showWizard, settingsCategory, projectToDelete, showAddGroup, newGroupName - project-tabs-store.svelte.ts: per-project activeTab + activatedTabs via Map Wired: - App.svelte: 8 inline $state removed, reads/writes via ui-store - ProjectCard: activeTab/activatedTabs from project-tabs-store - SettingsDrawer: activeCategory from ui-store - CommandPalette: 4 commands call ui-store directly (no CustomEvent dispatch) Components are now pure view layers reading from stores.
This commit is contained in:
parent
c88577a34a
commit
2b1194c809
6 changed files with 230 additions and 60 deletions
|
|
@ -27,17 +27,20 @@
|
||||||
import {
|
import {
|
||||||
getNotifications, clearAll as clearNotifications, getNotifCount,
|
getNotifications, clearAll as clearNotifications, getNotifCount,
|
||||||
} from './notifications-store.svelte.ts';
|
} from './notifications-store.svelte.ts';
|
||||||
|
import {
|
||||||
|
getSettingsOpen, setSettingsOpen, toggleSettings,
|
||||||
|
getPaletteOpen, setPaletteOpen, togglePalette,
|
||||||
|
getSearchOpen, toggleSearch,
|
||||||
|
getNotifDrawerOpen, setNotifDrawerOpen, toggleNotifDrawer,
|
||||||
|
getShowWizard, setShowWizard,
|
||||||
|
getProjectToDelete, setProjectToDelete,
|
||||||
|
getShowAddGroup, setShowAddGroup, toggleAddGroup,
|
||||||
|
getNewGroupName, setNewGroupName, resetAddGroupForm,
|
||||||
|
toggleWizard,
|
||||||
|
} from './ui-store.svelte.ts';
|
||||||
|
|
||||||
// ── Local UI state (view-only, not domain state) ────────────
|
// ── Local view-only state (not shared across components) ────
|
||||||
let appReady = $state(false);
|
let appReady = $state(false);
|
||||||
let settingsOpen = $state(false);
|
|
||||||
let paletteOpen = $state(false);
|
|
||||||
let drawerOpen = $state(false);
|
|
||||||
let searchOpen = $state(false);
|
|
||||||
let showWizard = $state(false);
|
|
||||||
let projectToDelete = $state<string | null>(null);
|
|
||||||
let showAddGroup = $state(false);
|
|
||||||
let newGroupName = $state('');
|
|
||||||
let sessionStart = $state(Date.now());
|
let sessionStart = $state(Date.now());
|
||||||
|
|
||||||
// ── Blink timer ─────────────────────────────────────────────
|
// ── Blink timer ─────────────────────────────────────────────
|
||||||
|
|
@ -81,28 +84,28 @@
|
||||||
// ── Wizard handler ──────────────────────────────────────────
|
// ── Wizard handler ──────────────────────────────────────────
|
||||||
function handleWizardCreated(result: WizardResult) {
|
function handleWizardCreated(result: WizardResult) {
|
||||||
addProjectFromWizard(result);
|
addProjectFromWizard(result);
|
||||||
showWizard = false;
|
setShowWizard(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmDeleteProject() {
|
async function confirmDeleteProject() {
|
||||||
if (!projectToDelete) return;
|
const toDelete = getProjectToDelete();
|
||||||
await deleteProject(projectToDelete);
|
if (!toDelete) return;
|
||||||
projectToDelete = null;
|
await deleteProject(toDelete);
|
||||||
|
setProjectToDelete(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Group add (local UI + store) ────────────────────────────
|
// ── Group add (local UI + store) ────────────────────────────
|
||||||
async function handleAddGroup() {
|
async function handleAddGroup() {
|
||||||
const name = newGroupName.trim();
|
const name = getNewGroupName().trim();
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
await wsAddGroup(name);
|
await wsAddGroup(name);
|
||||||
showAddGroup = false;
|
resetAddGroupForm();
|
||||||
newGroupName = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Notification drawer ─────────────────────────────────────
|
// ── Notification drawer ─────────────────────────────────────
|
||||||
function handleClearNotifications() {
|
function handleClearNotifications() {
|
||||||
clearNotifications();
|
clearNotifications();
|
||||||
drawerOpen = false;
|
setNotifDrawerOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Toast ref for agent notifications ───────────────────────
|
// ── Toast ref for agent notifications ───────────────────────
|
||||||
|
|
@ -183,8 +186,8 @@
|
||||||
trackAllProjects();
|
trackAllProjects();
|
||||||
});
|
});
|
||||||
|
|
||||||
keybindingStore.on('palette', () => { paletteOpen = !paletteOpen; });
|
keybindingStore.on('palette', () => { togglePalette(); });
|
||||||
keybindingStore.on('settings', () => { settingsOpen = !settingsOpen; });
|
keybindingStore.on('settings', () => { toggleSettings(); });
|
||||||
keybindingStore.on('group1', () => setActiveGroup(getGroups()[0]?.id));
|
keybindingStore.on('group1', () => setActiveGroup(getGroups()[0]?.id));
|
||||||
keybindingStore.on('group2', () => setActiveGroup(getGroups()[1]?.id));
|
keybindingStore.on('group2', () => setActiveGroup(getGroups()[1]?.id));
|
||||||
keybindingStore.on('group3', () => setActiveGroup(getGroups()[2]?.id));
|
keybindingStore.on('group3', () => setActiveGroup(getGroups()[2]?.id));
|
||||||
|
|
@ -194,7 +197,7 @@
|
||||||
function handleSearchShortcut(e: KeyboardEvent) {
|
function handleSearchShortcut(e: KeyboardEvent) {
|
||||||
if (e.ctrlKey && e.shiftKey && e.key === 'F') {
|
if (e.ctrlKey && e.shiftKey && e.key === 'F') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
searchOpen = !searchOpen;
|
toggleSearch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener('keydown', handleSearchShortcut);
|
document.addEventListener('keydown', handleSearchShortcut);
|
||||||
|
|
@ -202,10 +205,10 @@
|
||||||
function handlePaletteCommand(e: Event) {
|
function handlePaletteCommand(e: Event) {
|
||||||
const detail = (e as CustomEvent).detail;
|
const detail = (e as CustomEvent).detail;
|
||||||
switch (detail) {
|
switch (detail) {
|
||||||
case 'settings': settingsOpen = !settingsOpen; break;
|
case 'settings': toggleSettings(); break;
|
||||||
case 'search': searchOpen = !searchOpen; break;
|
case 'search': toggleSearch(); break;
|
||||||
case 'new-project': showWizard = true; break;
|
case 'new-project': setShowWizard(true); break;
|
||||||
case 'toggle-sidebar': settingsOpen = !settingsOpen; break;
|
case 'toggle-sidebar': toggleSettings(); break;
|
||||||
default: console.log(`[palette] unhandled command: ${detail}`);
|
default: console.log(`[palette] unhandled command: ${detail}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -221,15 +224,15 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SplashScreen ready={appReady} />
|
<SplashScreen ready={appReady} />
|
||||||
<SettingsDrawer open={settingsOpen} onClose={() => settingsOpen = false} />
|
<SettingsDrawer open={getSettingsOpen()} onClose={() => setSettingsOpen(false)} />
|
||||||
<CommandPalette open={paletteOpen} onClose={() => paletteOpen = false} />
|
<CommandPalette open={getPaletteOpen()} onClose={() => setPaletteOpen(false)} />
|
||||||
<SearchOverlay open={searchOpen} onClose={() => searchOpen = false} />
|
<SearchOverlay open={getSearchOpen()} onClose={() => setSearchOpen(false)} />
|
||||||
<ToastContainer bind:this={toastRef} />
|
<ToastContainer bind:this={toastRef} />
|
||||||
<NotifDrawer
|
<NotifDrawer
|
||||||
open={drawerOpen}
|
open={getNotifDrawerOpen()}
|
||||||
notifications={getNotifications()}
|
notifications={getNotifications()}
|
||||||
onClear={handleClearNotifications}
|
onClear={handleClearNotifications}
|
||||||
onClose={() => drawerOpen = false}
|
onClose={() => setNotifDrawerOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -262,7 +265,7 @@
|
||||||
<!-- Add group button -->
|
<!-- Add group button -->
|
||||||
<button
|
<button
|
||||||
class="group-btn add-group-btn"
|
class="group-btn add-group-btn"
|
||||||
onclick={() => showAddGroup = !showAddGroup}
|
onclick={() => toggleAddGroup()}
|
||||||
aria-label="Add group"
|
aria-label="Add group"
|
||||||
title="Add group"
|
title="Add group"
|
||||||
>
|
>
|
||||||
|
|
@ -270,14 +273,15 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showAddGroup}
|
{#if getShowAddGroup()}
|
||||||
<div class="add-group-form">
|
<div class="add-group-form">
|
||||||
<input
|
<input
|
||||||
class="add-group-input"
|
class="add-group-input"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Group name"
|
placeholder="Group name"
|
||||||
bind:value={newGroupName}
|
value={getNewGroupName()}
|
||||||
onkeydown={(e) => { if (e.key === 'Enter') handleAddGroup(); if (e.key === 'Escape') showAddGroup = false; }}
|
oninput={(e) => setNewGroupName((e.target as HTMLInputElement).value)}
|
||||||
|
onkeydown={(e) => { if (e.key === 'Enter') handleAddGroup(); if (e.key === 'Escape') setShowAddGroup(false); }}
|
||||||
autofocus
|
autofocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -286,7 +290,7 @@
|
||||||
<!-- Add project button -->
|
<!-- Add project button -->
|
||||||
<button
|
<button
|
||||||
class="sidebar-icon"
|
class="sidebar-icon"
|
||||||
onclick={() => showWizard = !showWizard}
|
onclick={() => toggleWizard()}
|
||||||
aria-label="Add project"
|
aria-label="Add project"
|
||||||
title="Add project"
|
title="Add project"
|
||||||
>
|
>
|
||||||
|
|
@ -300,8 +304,8 @@
|
||||||
<!-- Settings gear -->
|
<!-- Settings gear -->
|
||||||
<button
|
<button
|
||||||
class="sidebar-icon"
|
class="sidebar-icon"
|
||||||
class:active={settingsOpen}
|
class:active={getSettingsOpen()}
|
||||||
onclick={() => settingsOpen = !settingsOpen}
|
onclick={() => toggleSettings()}
|
||||||
aria-label="Settings (Ctrl+,)"
|
aria-label="Settings (Ctrl+,)"
|
||||||
title="Settings (Ctrl+,)"
|
title="Settings (Ctrl+,)"
|
||||||
data-testid="settings-btn"
|
data-testid="settings-btn"
|
||||||
|
|
@ -347,9 +351,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Project wizard overlay (display toggle) -->
|
<!-- Project wizard overlay (display toggle) -->
|
||||||
<div style:display={showWizard ? 'contents' : 'none'}>
|
<div style:display={getShowWizard() ? 'contents' : 'none'}>
|
||||||
<ProjectWizard
|
<ProjectWizard
|
||||||
onClose={() => showWizard = false}
|
onClose={() => setShowWizard(false)}
|
||||||
onCreated={handleWizardCreated}
|
onCreated={handleWizardCreated}
|
||||||
groupId={getActiveGroupId()}
|
groupId={getActiveGroupId()}
|
||||||
groups={getGroups().map(g => ({ id: g.id, name: g.name }))}
|
groups={getGroups().map(g => ({ id: g.id, name: g.name }))}
|
||||||
|
|
@ -358,11 +362,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete project confirmation -->
|
<!-- Delete project confirmation -->
|
||||||
{#if projectToDelete}
|
{#if getProjectToDelete()}
|
||||||
<div class="delete-overlay" role="listitem">
|
<div class="delete-overlay" role="listitem">
|
||||||
<p class="delete-text">Delete project "{getProjects().find(p => p.id === projectToDelete)?.name}"?</p>
|
<p class="delete-text">Delete project "{getProjects().find(p => p.id === getProjectToDelete())?.name}"?</p>
|
||||||
<div class="add-card-actions">
|
<div class="add-card-actions">
|
||||||
<button class="add-cancel" onclick={() => projectToDelete = null}>Cancel</button>
|
<button class="add-cancel" onclick={() => setProjectToDelete(null)}>Cancel</button>
|
||||||
<button class="delete-confirm" onclick={confirmDeleteProject}>Delete</button>
|
<button class="delete-confirm" onclick={confirmDeleteProject}>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -382,8 +386,8 @@
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="right-icon notif-btn"
|
class="right-icon notif-btn"
|
||||||
class:active={drawerOpen}
|
class:active={getNotifDrawerOpen()}
|
||||||
onclick={() => drawerOpen = !drawerOpen}
|
onclick={() => toggleNotifDrawer()}
|
||||||
aria-label="{getNotifCount() > 0 ? `${getNotifCount()} notifications` : 'Notifications'}"
|
aria-label="{getNotifCount() > 0 ? `${getNotifCount()} notifications` : 'Notifications'}"
|
||||||
title="Notifications"
|
title="Notifications"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { t } from './i18n.svelte.ts';
|
import { t } from './i18n.svelte.ts';
|
||||||
|
import {
|
||||||
|
toggleSettings, toggleSearch, setShowWizard,
|
||||||
|
} from './ui-store.svelte.ts';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -17,7 +20,7 @@
|
||||||
action: () => void;
|
action: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build commands — actions dispatch via CustomEvent so App.svelte can handle
|
// Dispatch for commands not yet handled by stores
|
||||||
function dispatch(name: string) {
|
function dispatch(name: string) {
|
||||||
window.dispatchEvent(new CustomEvent('palette-command', { detail: name }));
|
window.dispatchEvent(new CustomEvent('palette-command', { detail: name }));
|
||||||
}
|
}
|
||||||
|
|
@ -33,9 +36,9 @@
|
||||||
|
|
||||||
const COMMAND_DEFS: CommandDef[] = [
|
const COMMAND_DEFS: CommandDef[] = [
|
||||||
{ id: 'new-terminal', labelKey: 'palette.newTerminal', shortcut: 'Ctrl+`', action: () => dispatch('new-terminal') },
|
{ id: 'new-terminal', labelKey: 'palette.newTerminal', shortcut: 'Ctrl+`', action: () => dispatch('new-terminal') },
|
||||||
{ id: 'settings', labelKey: 'palette.openSettings', shortcut: 'Ctrl+,', action: () => dispatch('settings') },
|
{ id: 'settings', labelKey: 'palette.openSettings', shortcut: 'Ctrl+,', action: () => toggleSettings() },
|
||||||
{ id: 'search', labelKey: 'palette.searchMessages', shortcut: 'Ctrl+Shift+F', action: () => dispatch('search') },
|
{ id: 'search', labelKey: 'palette.searchMessages', shortcut: 'Ctrl+Shift+F', action: () => toggleSearch() },
|
||||||
{ id: 'new-project', labelKey: 'palette.addProject', descKey: 'palette.addProjectDesc', action: () => dispatch('new-project') },
|
{ id: 'new-project', labelKey: 'palette.addProject', descKey: 'palette.addProjectDesc', action: () => setShowWizard(true) },
|
||||||
{ id: 'clear-agent', labelKey: 'palette.clearAgent', descKey: 'palette.clearAgentDesc', action: () => dispatch('clear-agent') },
|
{ id: 'clear-agent', labelKey: 'palette.clearAgent', descKey: 'palette.clearAgentDesc', action: () => dispatch('clear-agent') },
|
||||||
{ id: 'copy-cost', labelKey: 'palette.copyCost', action: () => dispatch('copy-cost') },
|
{ id: 'copy-cost', labelKey: 'palette.copyCost', action: () => dispatch('copy-cost') },
|
||||||
{ id: 'docs', labelKey: 'palette.openDocs', shortcut: 'F1', action: () => dispatch('docs') },
|
{ id: 'docs', labelKey: 'palette.openDocs', shortcut: 'F1', action: () => dispatch('docs') },
|
||||||
|
|
@ -47,7 +50,7 @@
|
||||||
{ id: 'close-tab', labelKey: 'palette.closeTab', shortcut: 'Ctrl+W', action: () => dispatch('close-tab') },
|
{ id: 'close-tab', labelKey: 'palette.closeTab', shortcut: 'Ctrl+W', action: () => dispatch('close-tab') },
|
||||||
{ id: 'toggle-terminal', labelKey: 'palette.toggleTerminal', shortcut: 'Ctrl+J', action: () => dispatch('toggle-terminal') },
|
{ id: 'toggle-terminal', labelKey: 'palette.toggleTerminal', shortcut: 'Ctrl+J', action: () => dispatch('toggle-terminal') },
|
||||||
{ id: 'reload-plugins', labelKey: 'palette.reloadPlugins', action: () => dispatch('reload-plugins') },
|
{ id: 'reload-plugins', labelKey: 'palette.reloadPlugins', action: () => dispatch('reload-plugins') },
|
||||||
{ id: 'toggle-sidebar', labelKey: 'palette.toggleSidebar', shortcut: 'Ctrl+B', action: () => dispatch('toggle-sidebar') },
|
{ id: 'toggle-sidebar', labelKey: 'palette.toggleSidebar', shortcut: 'Ctrl+B', action: () => toggleSettings() },
|
||||||
{ id: 'zoom-in', labelKey: 'palette.zoomIn', shortcut: 'Ctrl+=', action: () => dispatch('zoom-in') },
|
{ id: 'zoom-in', labelKey: 'palette.zoomIn', shortcut: 'Ctrl+=', action: () => dispatch('zoom-in') },
|
||||||
{ id: 'zoom-out', labelKey: 'palette.zoomOut', shortcut: 'Ctrl+-', action: () => dispatch('zoom-out') },
|
{ id: 'zoom-out', labelKey: 'palette.zoomOut', shortcut: 'Ctrl+-', action: () => dispatch('zoom-out') },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,10 @@
|
||||||
loadLastSession,
|
loadLastSession,
|
||||||
type AgentStatus, type AgentMessage,
|
type AgentStatus, type AgentMessage,
|
||||||
} from './agent-store.svelte.ts';
|
} from './agent-store.svelte.ts';
|
||||||
|
import {
|
||||||
type ProjectTab = 'model' | 'docs' | 'context' | 'files' | 'ssh' | 'memory' | 'comms' | 'tasks';
|
getActiveTab, setActiveTab, isTabActivated,
|
||||||
|
ALL_TABS, type ProjectTab,
|
||||||
|
} from './project-tabs-store.svelte.ts';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -117,11 +119,8 @@
|
||||||
showCloneDialog = false;
|
showCloneDialog = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeTab = $state<ProjectTab>('model');
|
// Derived from project-tabs-store for reactive reads
|
||||||
// Track which project tabs have been activated (PERSISTED-LAZY pattern)
|
let activeTab = $derived(getActiveTab(id));
|
||||||
let activatedTabs = $state<Set<ProjectTab>>(new Set(['model']));
|
|
||||||
|
|
||||||
const ALL_TABS: ProjectTab[] = ['model', 'docs', 'context', 'files', 'ssh', 'memory', 'comms', 'tasks'];
|
|
||||||
|
|
||||||
// ── Load last session on mount ──────────────────────────────────────
|
// ── Load last session on mount ──────────────────────────────────────
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|
@ -129,8 +128,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
function setTab(tab: ProjectTab) {
|
function setTab(tab: ProjectTab) {
|
||||||
activeTab = tab;
|
setActiveTab(id, tab);
|
||||||
activatedTabs = new Set([...activatedTabs, tab]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSend(text: string) {
|
function handleSend(text: string) {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@
|
||||||
import RemoteMachinesSettings from './settings/RemoteMachinesSettings.svelte';
|
import RemoteMachinesSettings from './settings/RemoteMachinesSettings.svelte';
|
||||||
import DiagnosticsTab from './settings/DiagnosticsTab.svelte';
|
import DiagnosticsTab from './settings/DiagnosticsTab.svelte';
|
||||||
import { t } from './i18n.svelte.ts';
|
import { t } from './i18n.svelte.ts';
|
||||||
|
import {
|
||||||
|
getSettingsCategory, setSettingsCategory,
|
||||||
|
type SettingsCategory,
|
||||||
|
} from './ui-store.svelte.ts';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -18,7 +22,7 @@
|
||||||
|
|
||||||
let { open, onClose }: Props = $props();
|
let { open, onClose }: Props = $props();
|
||||||
|
|
||||||
type CategoryId = 'appearance' | 'agents' | 'security' | 'projects' | 'orchestration' | 'machines' | 'advanced' | 'marketplace' | 'keyboard' | 'diagnostics';
|
type CategoryId = SettingsCategory;
|
||||||
|
|
||||||
interface Category {
|
interface Category {
|
||||||
id: CategoryId;
|
id: CategoryId;
|
||||||
|
|
@ -43,7 +47,7 @@
|
||||||
CATEGORY_DEFS.map(d => ({ id: d.id, label: t(d.key as any), icon: d.icon }))
|
CATEGORY_DEFS.map(d => ({ id: d.id, label: t(d.key as any), icon: d.icon }))
|
||||||
);
|
);
|
||||||
|
|
||||||
let activeCategory = $state<CategoryId>('appearance');
|
let activeCategory = $derived(getSettingsCategory());
|
||||||
|
|
||||||
function handleBackdropClick(e: MouseEvent) {
|
function handleBackdropClick(e: MouseEvent) {
|
||||||
if (e.target === e.currentTarget) onClose();
|
if (e.target === e.currentTarget) onClose();
|
||||||
|
|
@ -83,7 +87,7 @@
|
||||||
<button
|
<button
|
||||||
class="cat-btn"
|
class="cat-btn"
|
||||||
class:active={activeCategory === cat.id}
|
class:active={activeCategory === cat.id}
|
||||||
onclick={() => activeCategory = cat.id}
|
onclick={() => setSettingsCategory(cat.id)}
|
||||||
aria-current={activeCategory === cat.id ? 'page' : undefined}
|
aria-current={activeCategory === cat.id ? 'page' : undefined}
|
||||||
>
|
>
|
||||||
<span class="cat-icon" aria-hidden="true">{cat.icon}</span>
|
<span class="cat-icon" aria-hidden="true">{cat.icon}</span>
|
||||||
|
|
|
||||||
67
ui-electrobun/src/mainview/project-tabs-store.svelte.ts
Normal file
67
ui-electrobun/src/mainview/project-tabs-store.svelte.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* Project tabs store — per-project tab state.
|
||||||
|
*
|
||||||
|
* Tracks which tab is active and which tabs have been activated (PERSISTED-LAZY
|
||||||
|
* pattern) for each project card. This allows cross-component access — e.g.,
|
||||||
|
* StatusBar can show which tab is active, palette commands can switch tabs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Types ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type ProjectTab =
|
||||||
|
| 'model' | 'docs' | 'context' | 'files'
|
||||||
|
| 'ssh' | 'memory' | 'comms' | 'tasks';
|
||||||
|
|
||||||
|
export const ALL_TABS: ProjectTab[] = [
|
||||||
|
'model', 'docs', 'context', 'files', 'ssh', 'memory', 'comms', 'tasks',
|
||||||
|
];
|
||||||
|
|
||||||
|
interface TabState {
|
||||||
|
activeTab: ProjectTab;
|
||||||
|
activatedTabs: Set<ProjectTab>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── State ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let _tabs = $state<Map<string, TabState>>(new Map());
|
||||||
|
|
||||||
|
// ── Internal helper ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function ensureEntry(projectId: string): TabState {
|
||||||
|
let entry = _tabs.get(projectId);
|
||||||
|
if (!entry) {
|
||||||
|
entry = { activeTab: 'model', activatedTabs: new Set(['model']) };
|
||||||
|
_tabs.set(projectId, entry);
|
||||||
|
// Trigger reactivity by reassigning the map
|
||||||
|
_tabs = new Map(_tabs);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Getters ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function getActiveTab(projectId: string): ProjectTab {
|
||||||
|
return _tabs.get(projectId)?.activeTab ?? 'model';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTabActivated(projectId: string, tab: ProjectTab): boolean {
|
||||||
|
return _tabs.get(projectId)?.activatedTabs.has(tab) ?? (tab === 'model');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Actions ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function setActiveTab(projectId: string, tab: ProjectTab): void {
|
||||||
|
const entry = ensureEntry(projectId);
|
||||||
|
entry.activeTab = tab;
|
||||||
|
entry.activatedTabs = new Set([...entry.activatedTabs, tab]);
|
||||||
|
// Trigger reactivity
|
||||||
|
_tabs = new Map(_tabs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove tab state when a project is deleted. */
|
||||||
|
export function removeProject(projectId: string): void {
|
||||||
|
if (_tabs.has(projectId)) {
|
||||||
|
_tabs.delete(projectId);
|
||||||
|
_tabs = new Map(_tabs);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
ui-electrobun/src/mainview/ui-store.svelte.ts
Normal file
94
ui-electrobun/src/mainview/ui-store.svelte.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
/**
|
||||||
|
* UI store — global UI overlay/drawer/panel state.
|
||||||
|
*
|
||||||
|
* Single source of truth for ephemeral UI state that multiple components
|
||||||
|
* need to read or write (e.g., palette commands opening settings from anywhere).
|
||||||
|
*
|
||||||
|
* Components are pure view layers: they read from this store and call
|
||||||
|
* its methods to mutate state. No component should own $state that
|
||||||
|
* other components need.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Settings drawer ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type SettingsCategory =
|
||||||
|
| 'appearance' | 'agents' | 'security' | 'projects'
|
||||||
|
| 'orchestration' | 'machines' | 'advanced' | 'marketplace'
|
||||||
|
| 'keyboard' | 'diagnostics';
|
||||||
|
|
||||||
|
let _settingsOpen = $state(false);
|
||||||
|
let _settingsCategory = $state<SettingsCategory>('appearance');
|
||||||
|
|
||||||
|
export function getSettingsOpen(): boolean { return _settingsOpen; }
|
||||||
|
export function setSettingsOpen(v: boolean): void { _settingsOpen = v; }
|
||||||
|
export function toggleSettings(): void { _settingsOpen = !_settingsOpen; }
|
||||||
|
|
||||||
|
export function getSettingsCategory(): SettingsCategory { return _settingsCategory; }
|
||||||
|
export function setSettingsCategory(c: SettingsCategory): void { _settingsCategory = c; }
|
||||||
|
|
||||||
|
/** Open settings drawer directly to a specific category. */
|
||||||
|
export function openSettingsCategory(category: SettingsCategory): void {
|
||||||
|
_settingsCategory = category;
|
||||||
|
_settingsOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Command palette ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let _paletteOpen = $state(false);
|
||||||
|
|
||||||
|
export function getPaletteOpen(): boolean { return _paletteOpen; }
|
||||||
|
export function setPaletteOpen(v: boolean): void { _paletteOpen = v; }
|
||||||
|
export function togglePalette(): void { _paletteOpen = !_paletteOpen; }
|
||||||
|
|
||||||
|
// ── Search overlay ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let _searchOpen = $state(false);
|
||||||
|
|
||||||
|
export function getSearchOpen(): boolean { return _searchOpen; }
|
||||||
|
export function setSearchOpen(v: boolean): void { _searchOpen = v; }
|
||||||
|
export function toggleSearch(): void { _searchOpen = !_searchOpen; }
|
||||||
|
|
||||||
|
// ── Notification drawer ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let _notifDrawerOpen = $state(false);
|
||||||
|
|
||||||
|
export function getNotifDrawerOpen(): boolean { return _notifDrawerOpen; }
|
||||||
|
export function setNotifDrawerOpen(v: boolean): void { _notifDrawerOpen = v; }
|
||||||
|
export function toggleNotifDrawer(): void { _notifDrawerOpen = !_notifDrawerOpen; }
|
||||||
|
|
||||||
|
// ── Project wizard ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let _showWizard = $state(false);
|
||||||
|
|
||||||
|
export function getShowWizard(): boolean { return _showWizard; }
|
||||||
|
export function setShowWizard(v: boolean): void { _showWizard = v; }
|
||||||
|
export function toggleWizard(): void { _showWizard = !_showWizard; }
|
||||||
|
|
||||||
|
// ── Delete project confirmation ──────────────────────────────────────────
|
||||||
|
|
||||||
|
let _projectToDelete = $state<string | null>(null);
|
||||||
|
|
||||||
|
export function getProjectToDelete(): string | null { return _projectToDelete; }
|
||||||
|
export function setProjectToDelete(id: string | null): void { _projectToDelete = id; }
|
||||||
|
|
||||||
|
// ── Add group form ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let _showAddGroup = $state(false);
|
||||||
|
let _newGroupName = $state('');
|
||||||
|
|
||||||
|
export function getShowAddGroup(): boolean { return _showAddGroup; }
|
||||||
|
export function setShowAddGroup(v: boolean): void { _showAddGroup = v; }
|
||||||
|
export function toggleAddGroup(): void { _showAddGroup = !_showAddGroup; }
|
||||||
|
|
||||||
|
export function getNewGroupName(): string { return _newGroupName; }
|
||||||
|
export function setNewGroupName(v: string): void { _newGroupName = v; }
|
||||||
|
|
||||||
|
/** Reset add-group form after submission. */
|
||||||
|
export function resetAddGroupForm(): void {
|
||||||
|
_showAddGroup = false;
|
||||||
|
_newGroupName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Type re-export ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type { SettingsCategory };
|
||||||
Loading…
Add table
Add a link
Reference in a new issue