fix(electrobun): rewrite terminal collapse — blur fix, tab bar as bottom divider
This commit is contained in:
parent
9edece0dc7
commit
e8132b7dc6
5 changed files with 108 additions and 160 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue