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:
Hibryda 2026-03-23 20:26:07 +01:00
parent c88577a34a
commit 2b1194c809
6 changed files with 230 additions and 60 deletions

View file

@ -27,17 +27,20 @@
import {
getNotifications, clearAll as clearNotifications, getNotifCount,
} 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 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());
// ── Blink timer ─────────────────────────────────────────────
@ -81,28 +84,28 @@
// ── Wizard handler ──────────────────────────────────────────
function handleWizardCreated(result: WizardResult) {
addProjectFromWizard(result);
showWizard = false;
setShowWizard(false);
}
async function confirmDeleteProject() {
if (!projectToDelete) return;
await deleteProject(projectToDelete);
projectToDelete = null;
const toDelete = getProjectToDelete();
if (!toDelete) return;
await deleteProject(toDelete);
setProjectToDelete(null);
}
// ── Group add (local UI + store) ────────────────────────────
async function handleAddGroup() {
const name = newGroupName.trim();
const name = getNewGroupName().trim();
if (!name) return;
await wsAddGroup(name);
showAddGroup = false;
newGroupName = '';
resetAddGroupForm();
}
// ── Notification drawer ─────────────────────────────────────
function handleClearNotifications() {
clearNotifications();
drawerOpen = false;
setNotifDrawerOpen(false);
}
// ── Toast ref for agent notifications ───────────────────────
@ -183,8 +186,8 @@
trackAllProjects();
});
keybindingStore.on('palette', () => { paletteOpen = !paletteOpen; });
keybindingStore.on('settings', () => { settingsOpen = !settingsOpen; });
keybindingStore.on('palette', () => { togglePalette(); });
keybindingStore.on('settings', () => { toggleSettings(); });
keybindingStore.on('group1', () => setActiveGroup(getGroups()[0]?.id));
keybindingStore.on('group2', () => setActiveGroup(getGroups()[1]?.id));
keybindingStore.on('group3', () => setActiveGroup(getGroups()[2]?.id));
@ -194,7 +197,7 @@
function handleSearchShortcut(e: KeyboardEvent) {
if (e.ctrlKey && e.shiftKey && e.key === 'F') {
e.preventDefault();
searchOpen = !searchOpen;
toggleSearch();
}
}
document.addEventListener('keydown', handleSearchShortcut);
@ -202,10 +205,10 @@
function handlePaletteCommand(e: Event) {
const detail = (e as CustomEvent).detail;
switch (detail) {
case 'settings': settingsOpen = !settingsOpen; break;
case 'search': searchOpen = !searchOpen; break;
case 'new-project': showWizard = true; break;
case 'toggle-sidebar': settingsOpen = !settingsOpen; break;
case 'settings': toggleSettings(); break;
case 'search': toggleSearch(); break;
case 'new-project': setShowWizard(true); break;
case 'toggle-sidebar': toggleSettings(); break;
default: console.log(`[palette] unhandled command: ${detail}`);
}
}
@ -221,15 +224,15 @@
</script>
<SplashScreen ready={appReady} />
<SettingsDrawer open={settingsOpen} onClose={() => settingsOpen = false} />
<CommandPalette open={paletteOpen} onClose={() => paletteOpen = false} />
<SearchOverlay open={searchOpen} onClose={() => searchOpen = false} />
<SettingsDrawer open={getSettingsOpen()} onClose={() => setSettingsOpen(false)} />
<CommandPalette open={getPaletteOpen()} onClose={() => setPaletteOpen(false)} />
<SearchOverlay open={getSearchOpen()} onClose={() => setSearchOpen(false)} />
<ToastContainer bind:this={toastRef} />
<NotifDrawer
open={drawerOpen}
open={getNotifDrawerOpen()}
notifications={getNotifications()}
onClear={handleClearNotifications}
onClose={() => drawerOpen = false}
onClose={() => setNotifDrawerOpen(false)}
/>
<div
@ -262,7 +265,7 @@
<!-- Add group button -->
<button
class="group-btn add-group-btn"
onclick={() => showAddGroup = !showAddGroup}
onclick={() => toggleAddGroup()}
aria-label="Add group"
title="Add group"
>
@ -270,14 +273,15 @@
</button>
</div>
{#if showAddGroup}
{#if getShowAddGroup()}
<div class="add-group-form">
<input
class="add-group-input"
type="text"
placeholder="Group name"
bind:value={newGroupName}
onkeydown={(e) => { if (e.key === 'Enter') handleAddGroup(); if (e.key === 'Escape') showAddGroup = false; }}
value={getNewGroupName()}
oninput={(e) => setNewGroupName((e.target as HTMLInputElement).value)}
onkeydown={(e) => { if (e.key === 'Enter') handleAddGroup(); if (e.key === 'Escape') setShowAddGroup(false); }}
autofocus
/>
</div>
@ -286,7 +290,7 @@
<!-- Add project button -->
<button
class="sidebar-icon"
onclick={() => showWizard = !showWizard}
onclick={() => toggleWizard()}
aria-label="Add project"
title="Add project"
>
@ -300,8 +304,8 @@
<!-- Settings gear -->
<button
class="sidebar-icon"
class:active={settingsOpen}
onclick={() => settingsOpen = !settingsOpen}
class:active={getSettingsOpen()}
onclick={() => toggleSettings()}
aria-label="Settings (Ctrl+,)"
title="Settings (Ctrl+,)"
data-testid="settings-btn"
@ -347,9 +351,9 @@
</div>
<!-- Project wizard overlay (display toggle) -->
<div style:display={showWizard ? 'contents' : 'none'}>
<div style:display={getShowWizard() ? 'contents' : 'none'}>
<ProjectWizard
onClose={() => showWizard = false}
onClose={() => setShowWizard(false)}
onCreated={handleWizardCreated}
groupId={getActiveGroupId()}
groups={getGroups().map(g => ({ id: g.id, name: g.name }))}
@ -358,11 +362,11 @@
</div>
<!-- Delete project confirmation -->
{#if projectToDelete}
{#if getProjectToDelete()}
<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">
<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>
</div>
</div>
@ -382,8 +386,8 @@
<button
class="right-icon notif-btn"
class:active={drawerOpen}
onclick={() => drawerOpen = !drawerOpen}
class:active={getNotifDrawerOpen()}
onclick={() => toggleNotifDrawer()}
aria-label="{getNotifCount() > 0 ? `${getNotifCount()} notifications` : 'Notifications'}"
title="Notifications"
>