fix(electrobun): rewrite terminal collapse — blur fix, tab bar as bottom divider

This commit is contained in:
Hibryda 2026-03-20 02:41:07 +01:00
parent 9edece0dc7
commit e8132b7dc6
5 changed files with 108 additions and 160 deletions

View file

@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Svelte App</title> <title>Svelte App</title>
<script type="module" crossorigin src="/assets/index-B4yMIAeH.js"></script> <script type="module" crossorigin src="/assets/index-U683DxRe.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D6jrqWO6.css"> <link rel="stylesheet" crossorigin href="/assets/index-RZPm-TN9.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View file

@ -13,21 +13,25 @@
title: string; title: string;
} }
// Capture stable initial value — projectId is a mount-time constant, not reactive
// svelte-ignore state_referenced_locally // svelte-ignore state_referenced_locally
const initialId = projectId; const initialId = projectId;
const firstTabId = `${initialId}-t1`; const firstTabId = `${initialId}-t1`;
let tabs = $state<TermTab[]>([{ id: firstTabId, title: 'shell 1' }]); let tabs = $state<TermTab[]>([{ id: firstTabId, title: 'shell 1' }]);
let activeTabId = $state<string>(firstTabId); let activeTabId = $state<string>(firstTabId);
let collapsed = $state(false); let expanded = $state(true);
let counter = $state(2); let counter = $state(2);
// Track which tabs have been mounted at least once (for lazy init)
let mounted = $state<Set<string>>(new Set([firstTabId])); let mounted = $state<Set<string>>(new Set([firstTabId]));
function addTab(e?: MouseEvent) { function blurTerminal() {
// Blur terminal canvas so future clicks on tab bar work // Force-blur xterm canvas so UI buttons become clickable
if (e) (e.target as HTMLElement)?.focus(); if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}
function addTab() {
blurTerminal();
const id = `${projectId}-t${counter}`; const id = `${projectId}-t${counter}`;
tabs = [...tabs, { id, title: `shell ${counter}` }]; tabs = [...tabs, { id, title: `shell ${counter}` }];
counter++; counter++;
@ -37,6 +41,7 @@
function closeTab(id: string, e: MouseEvent) { function closeTab(id: string, e: MouseEvent) {
e.stopPropagation(); e.stopPropagation();
blurTerminal();
const idx = tabs.findIndex(t => t.id === id); const idx = tabs.findIndex(t => t.id === id);
tabs = tabs.filter(t => t.id !== id); tabs = tabs.filter(t => t.id !== id);
if (activeTabId === id) { if (activeTabId === id) {
@ -49,43 +54,42 @@
} }
function activateTab(id: string) { function activateTab(id: string) {
blurTerminal();
activeTabId = id; activeTabId = id;
if (!mounted.has(id)) { if (!mounted.has(id)) mounted = new Set([...mounted, id]);
mounted = new Set([...mounted, id]); if (!expanded) expanded = true;
}
} }
function toggleCollapse() { function toggleExpand() {
collapsed = !collapsed; blurTerminal();
expanded = !expanded;
} }
</script> </script>
<div class="terminal-section" class:collapsed style="--accent: {accent}"> <!-- Wrapper: uses flex to push tab bar to bottom when terminal is collapsed -->
<!-- Tab bar always visible — between agent pane and terminal content --> <div class="term-wrapper" style="--accent: {accent}">
<div class="term-section-header"> <!-- Tab bar: always visible, acts as divider -->
<div class="term-bar" onmousedown={blurTerminal}>
<button <button
class="collapse-btn" class="expand-btn"
onclick={toggleCollapse} onclick={toggleExpand}
aria-label={collapsed ? 'Expand terminals' : 'Collapse terminals'} title={expanded ? 'Collapse terminal' : 'Expand terminal'}
aria-expanded={!collapsed}
title={collapsed ? 'Expand' : 'Collapse'}
> >
<svg <svg
class="chevron" class="chevron"
class:rotated={collapsed} class:open={expanded}
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
aria-hidden="true"
> >
<polyline points="6 9 12 15 18 9"/> <polyline points="9 6 15 12 9 18"/>
</svg> </svg>
</button> </button>
<div class="term-tabs" role="tablist" aria-label="Terminal tabs"> <div class="term-tabs" role="tablist">
{#each tabs as tab (tab.id)} {#each tabs as tab (tab.id)}
<button <button
class="term-tab" class="term-tab"
@ -93,42 +97,23 @@
role="tab" role="tab"
aria-selected={activeTabId === tab.id} aria-selected={activeTabId === tab.id}
onclick={() => activateTab(tab.id)} onclick={() => activateTab(tab.id)}
title={tab.title}
> >
<span class="term-tab-title">{tab.title}</span> <span class="tab-label">{tab.title}</span>
{#if tabs.length > 1} {#if tabs.length > 1}
<span <span class="tab-close" onclick={(e) => closeTab(tab.id, e)}>×</span>
class="term-tab-close"
role="button"
tabindex="0"
aria-label="Close {tab.title}"
onclick={(e) => closeTab(tab.id, e)}
onkeydown={(e) => e.key === 'Enter' && closeTab(tab.id, e as unknown as MouseEvent)}
>×</span>
{/if} {/if}
</button> </button>
{/each} {/each}
<button class="tab-add" onclick={() => addTab()}>+</button>
<button
class="term-tab-add"
onclick={addTab}
aria-label="Add terminal tab"
title="New terminal"
>+</button>
</div> </div>
</div> </div>
<!-- Terminal panes — hidden when collapsed, agent pane fills freed space --> <!-- Terminal panes: only rendered when expanded -->
{#if !collapsed} {#if expanded}
<div class="term-panes"> <div class="term-panes">
{#each tabs as tab (tab.id)} {#each tabs as tab (tab.id)}
{#if mounted.has(tab.id)} {#if mounted.has(tab.id)}
<div <div class="term-pane" style:display={activeTabId === tab.id ? 'flex' : 'none'}>
class="term-pane"
style:display={activeTabId === tab.id ? 'flex' : 'none'}
role="tabpanel"
aria-label={tab.title}
>
<Terminal /> <Terminal />
</div> </div>
{/if} {/if}
@ -138,33 +123,25 @@
</div> </div>
<style> <style>
.terminal-section { .term-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-top: 1px solid var(--ctp-surface0);
flex-shrink: 0; flex-shrink: 0;
background: var(--ctp-crust);
} }
/* When collapsed: no terminal height, just the tab bar */ /* Tab bar */
.terminal-section.collapsed { .term-bar {
flex-shrink: 1;
}
/* Section header: collapse + tabs in one row */
.term-section-header {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
height: 1.875rem; height: 1.75rem;
background: var(--ctp-mantle); background: var(--ctp-mantle);
border-bottom: 1px solid var(--ctp-surface0); border-top: 1px solid var(--ctp-surface0);
flex-shrink: 0; flex-shrink: 0;
position: relative; z-index: 5;
z-index: 10;
} }
.collapse-btn { .expand-btn {
width: 2rem; width: 1.75rem;
flex-shrink: 0; flex-shrink: 0;
background: transparent; background: transparent;
border: none; border: none;
@ -174,23 +151,13 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: color 0.12s;
padding: 0; padding: 0;
} }
.collapse-btn:hover { color: var(--ctp-text); } .expand-btn:hover { color: var(--ctp-text); }
.expand-btn svg { width: 0.75rem; height: 0.75rem; transition: transform 0.15s; }
.chevron.open { transform: rotate(90deg); }
.collapse-btn svg {
width: 0.875rem;
height: 0.875rem;
transition: transform 0.15s;
}
.chevron.rotated {
transform: rotate(90deg);
}
/* Tab pills */
.term-tabs { .term-tabs {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
@ -200,7 +167,6 @@
gap: 0.125rem; gap: 0.125rem;
padding: 0 0.25rem; padding: 0 0.25rem;
} }
.term-tabs::-webkit-scrollbar { display: none; } .term-tabs::-webkit-scrollbar { display: none; }
.term-tab { .term-tab {
@ -212,68 +178,50 @@
border: none; border: none;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
color: var(--ctp-subtext0); color: var(--ctp-subtext0);
font-family: var(--ui-font-family); font-size: 0.6875rem;
font-size: 0.75rem;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
transition: color 0.1s, border-color 0.1s;
margin-bottom: -1px; margin-bottom: -1px;
font-family: inherit;
} }
.term-tab:hover { color: var(--ctp-text); } .term-tab:hover { color: var(--ctp-text); }
.term-tab.active { color: var(--ctp-text); border-bottom-color: var(--accent); }
.tab-label { pointer-events: none; }
.term-tab.active { .tab-close {
color: var(--ctp-text); width: 0.875rem;
border-bottom-color: var(--accent, var(--ctp-mauve)); height: 0.875rem;
} border-radius: 0.2rem;
font-size: 0.6875rem;
.term-tab-title { pointer-events: none; } color: var(--ctp-overlay0);
cursor: pointer;
.term-tab-close {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 1rem;
height: 1rem;
border-radius: 0.2rem;
font-size: 0.75rem;
color: var(--ctp-overlay0);
transition: background 0.1s, color 0.1s;
cursor: pointer;
} }
.tab-close:hover { background: var(--ctp-surface1); color: var(--ctp-red); }
.term-tab-close:hover { .tab-add {
background: var(--ctp-surface1);
color: var(--ctp-red);
}
.term-tab-add {
align-self: center; align-self: center;
width: 1.375rem; width: 1.25rem;
height: 1.375rem; height: 1.25rem;
flex-shrink: 0; flex-shrink: 0;
background: transparent; background: transparent;
border: 1px solid var(--ctp-surface1); border: 1px solid var(--ctp-surface1);
border-radius: 0.25rem; border-radius: 0.2rem;
color: var(--ctp-overlay1); color: var(--ctp-overlay1);
font-size: 0.875rem; font-size: 0.75rem;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: background 0.1s, color 0.1s;
margin-left: 0.125rem; margin-left: 0.125rem;
} }
.tab-add:hover { background: var(--ctp-surface0); color: var(--ctp-text); }
.term-tab-add:hover { /* Terminal panes */
background: var(--ctp-surface0);
color: var(--ctp-text);
}
/* Terminal pane container */
.term-panes { .term-panes {
height: 12rem; height: 12rem;
min-height: 8rem;
position: relative; position: relative;
} }