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 {
|
||||
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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { tick } from 'svelte';
|
||||
import { t } from './i18n.svelte.ts';
|
||||
import {
|
||||
toggleSettings, toggleSearch, setShowWizard,
|
||||
} from './ui-store.svelte.ts';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
|
@ -17,7 +20,7 @@
|
|||
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) {
|
||||
window.dispatchEvent(new CustomEvent('palette-command', { detail: name }));
|
||||
}
|
||||
|
|
@ -33,9 +36,9 @@
|
|||
|
||||
const COMMAND_DEFS: CommandDef[] = [
|
||||
{ id: 'new-terminal', labelKey: 'palette.newTerminal', shortcut: 'Ctrl+`', action: () => dispatch('new-terminal') },
|
||||
{ id: 'settings', labelKey: 'palette.openSettings', shortcut: 'Ctrl+,', action: () => dispatch('settings') },
|
||||
{ id: 'search', labelKey: 'palette.searchMessages', shortcut: 'Ctrl+Shift+F', action: () => dispatch('search') },
|
||||
{ id: 'new-project', labelKey: 'palette.addProject', descKey: 'palette.addProjectDesc', action: () => dispatch('new-project') },
|
||||
{ id: 'settings', labelKey: 'palette.openSettings', shortcut: 'Ctrl+,', action: () => toggleSettings() },
|
||||
{ id: 'search', labelKey: 'palette.searchMessages', shortcut: 'Ctrl+Shift+F', action: () => toggleSearch() },
|
||||
{ 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: 'copy-cost', labelKey: 'palette.copyCost', action: () => dispatch('copy-cost') },
|
||||
{ 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: 'toggle-terminal', labelKey: 'palette.toggleTerminal', shortcut: 'Ctrl+J', action: () => dispatch('toggle-terminal') },
|
||||
{ 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-out', labelKey: 'palette.zoomOut', shortcut: 'Ctrl+-', action: () => dispatch('zoom-out') },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@
|
|||
loadLastSession,
|
||||
type AgentStatus, type AgentMessage,
|
||||
} from './agent-store.svelte.ts';
|
||||
|
||||
type ProjectTab = 'model' | 'docs' | 'context' | 'files' | 'ssh' | 'memory' | 'comms' | 'tasks';
|
||||
import {
|
||||
getActiveTab, setActiveTab, isTabActivated,
|
||||
ALL_TABS, type ProjectTab,
|
||||
} from './project-tabs-store.svelte.ts';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
|
|
@ -117,11 +119,8 @@
|
|||
showCloneDialog = false;
|
||||
}
|
||||
|
||||
let activeTab = $state<ProjectTab>('model');
|
||||
// Track which project tabs have been activated (PERSISTED-LAZY pattern)
|
||||
let activatedTabs = $state<Set<ProjectTab>>(new Set(['model']));
|
||||
|
||||
const ALL_TABS: ProjectTab[] = ['model', 'docs', 'context', 'files', 'ssh', 'memory', 'comms', 'tasks'];
|
||||
// Derived from project-tabs-store for reactive reads
|
||||
let activeTab = $derived(getActiveTab(id));
|
||||
|
||||
// ── Load last session on mount ──────────────────────────────────────
|
||||
$effect(() => {
|
||||
|
|
@ -129,8 +128,7 @@
|
|||
});
|
||||
|
||||
function setTab(tab: ProjectTab) {
|
||||
activeTab = tab;
|
||||
activatedTabs = new Set([...activatedTabs, tab]);
|
||||
setActiveTab(id, tab);
|
||||
}
|
||||
|
||||
function handleSend(text: string) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
import RemoteMachinesSettings from './settings/RemoteMachinesSettings.svelte';
|
||||
import DiagnosticsTab from './settings/DiagnosticsTab.svelte';
|
||||
import { t } from './i18n.svelte.ts';
|
||||
import {
|
||||
getSettingsCategory, setSettingsCategory,
|
||||
type SettingsCategory,
|
||||
} from './ui-store.svelte.ts';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
|
@ -18,7 +22,7 @@
|
|||
|
||||
let { open, onClose }: Props = $props();
|
||||
|
||||
type CategoryId = 'appearance' | 'agents' | 'security' | 'projects' | 'orchestration' | 'machines' | 'advanced' | 'marketplace' | 'keyboard' | 'diagnostics';
|
||||
type CategoryId = SettingsCategory;
|
||||
|
||||
interface Category {
|
||||
id: CategoryId;
|
||||
|
|
@ -43,7 +47,7 @@
|
|||
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) {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
|
|
@ -83,7 +87,7 @@
|
|||
<button
|
||||
class="cat-btn"
|
||||
class:active={activeCategory === cat.id}
|
||||
onclick={() => activeCategory = cat.id}
|
||||
onclick={() => setSettingsCategory(cat.id)}
|
||||
aria-current={activeCategory === cat.id ? 'page' : undefined}
|
||||
>
|
||||
<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