feat(electrobun): hierarchical state tree (Rule 58)
New files: - project-state.types.ts: all per-project state interfaces - project-state.svelte.ts: unified per-project state with version counter - app-state.svelte.ts: root facade re-exporting all stores as appState.* Rewired components (no more local $state): - ProjectCard: reads via appState.agent.* and appState.project.tab.* - TerminalTabs: state in appState.project.terminals.* - FileBrowser: state in appState.project.files.* - CommsTab: state in appState.project.comms.* - TaskBoardTab: state in appState.project.tasks.* All follow Rule 57 (no $derived with new objects) and Rule 58 (state tree architecture, components are pure renderers).
This commit is contained in:
parent
ae4c07c160
commit
162b5417e4
9 changed files with 870 additions and 400 deletions
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import Terminal from './Terminal.svelte';
|
||||
import { appState } from './app-state.svelte.ts';
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
|
|
@ -10,23 +11,10 @@
|
|||
|
||||
let { projectId, accent = 'var(--ctp-mauve)', cwd }: Props = $props();
|
||||
|
||||
interface TermTab {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const initialId = projectId;
|
||||
const firstTabId = `${initialId}-t1`;
|
||||
|
||||
let tabs = $state<TermTab[]>([{ id: firstTabId, title: 'shell 1' }]);
|
||||
let activeTabId = $state<string>(firstTabId);
|
||||
let expanded = $state(true);
|
||||
let counter = $state(2);
|
||||
let mounted = $state<Set<string>>(new Set([firstTabId]));
|
||||
// Read terminal state from project state tree
|
||||
function getTerminals() { return appState.project.getState(projectId).terminals; }
|
||||
|
||||
function blurTerminal() {
|
||||
// Force-blur xterm canvas so UI buttons become clickable
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
|
@ -34,37 +22,23 @@
|
|||
|
||||
function addTab() {
|
||||
blurTerminal();
|
||||
const id = `${projectId}-t${counter}`;
|
||||
tabs = [...tabs, { id, title: `shell ${counter}` }];
|
||||
counter++;
|
||||
activeTabId = id;
|
||||
mounted = new Set([...mounted, id]);
|
||||
appState.project.terminals.addTab(projectId);
|
||||
}
|
||||
|
||||
function closeTab(id: string, e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
blurTerminal();
|
||||
const idx = tabs.findIndex(t => t.id === id);
|
||||
tabs = tabs.filter(t => t.id !== id);
|
||||
if (activeTabId === id) {
|
||||
const next = tabs[Math.min(idx, tabs.length - 1)];
|
||||
activeTabId = next?.id ?? '';
|
||||
}
|
||||
const m = new Set(mounted);
|
||||
m.delete(id);
|
||||
mounted = m;
|
||||
appState.project.terminals.closeTab(projectId, id);
|
||||
}
|
||||
|
||||
function activateTab(id: string) {
|
||||
blurTerminal();
|
||||
activeTabId = id;
|
||||
if (!mounted.has(id)) mounted = new Set([...mounted, id]);
|
||||
if (!expanded) expanded = true;
|
||||
appState.project.terminals.activateTab(projectId, id);
|
||||
}
|
||||
|
||||
function toggleExpand() {
|
||||
blurTerminal();
|
||||
expanded = !expanded;
|
||||
appState.project.terminals.toggleExpanded(projectId);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -75,11 +49,11 @@
|
|||
<button
|
||||
class="expand-btn"
|
||||
onclick={toggleExpand}
|
||||
title={expanded ? 'Collapse terminal' : 'Expand terminal'}
|
||||
title={getTerminals().expanded ? 'Collapse terminal' : 'Expand terminal'}
|
||||
>
|
||||
<svg
|
||||
class="chevron"
|
||||
class:open={expanded}
|
||||
class:open={getTerminals().expanded}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
|
|
@ -92,19 +66,19 @@
|
|||
</button>
|
||||
|
||||
<div class="term-tabs" role="tablist">
|
||||
{#each tabs as tab (tab.id)}
|
||||
{#each getTerminals().tabs as tab (tab.id)}
|
||||
<!-- div+role="tab" allows a nested <button> for the close action -->
|
||||
<div
|
||||
class="term-tab"
|
||||
class:active={activeTabId === tab.id}
|
||||
class:active={getTerminals().activeTabId === tab.id}
|
||||
role="tab"
|
||||
tabindex={activeTabId === tab.id ? 0 : -1}
|
||||
aria-selected={activeTabId === tab.id}
|
||||
tabindex={getTerminals().activeTabId === tab.id ? 0 : -1}
|
||||
aria-selected={getTerminals().activeTabId === tab.id}
|
||||
onclick={() => activateTab(tab.id)}
|
||||
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') activateTab(tab.id); }}
|
||||
>
|
||||
<span class="tab-label">{tab.title}</span>
|
||||
{#if tabs.length > 1}
|
||||
{#if getTerminals().tabs.length > 1}
|
||||
<button
|
||||
class="tab-close"
|
||||
aria-label="Close {tab.title}"
|
||||
|
|
@ -119,10 +93,10 @@
|
|||
|
||||
<!-- Terminal panes: always in DOM, display toggled.
|
||||
WebKitGTK corrupts hit-test tree on DOM add/remove during click. -->
|
||||
<div class="term-panes" style:display={expanded ? 'block' : 'none'}>
|
||||
{#each tabs as tab (tab.id)}
|
||||
{#if mounted.has(tab.id)}
|
||||
<div class="term-pane" style:display={activeTabId === tab.id ? 'flex' : 'none'}>
|
||||
<div class="term-panes" style:display={getTerminals().expanded ? 'block' : 'none'}>
|
||||
{#each getTerminals().tabs as tab (tab.id)}
|
||||
{#if getTerminals().mounted.has(tab.id)}
|
||||
<div class="term-pane" style:display={getTerminals().activeTabId === tab.id ? 'flex' : 'none'}>
|
||||
<Terminal sessionId={tab.id} {cwd} />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue