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 name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Svelte App</title>
<script type="module" crossorigin src="/assets/index-B4yMIAeH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D6jrqWO6.css">
<script type="module" crossorigin src="/assets/index-U683DxRe.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-RZPm-TN9.css">
</head>
<body>
<div id="app"></div>

View file

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