feat(electrobun): full UI — terminal tabs, agent pane, settings, palette

Extracted into 6 components:
- ProjectCard.svelte: header with badges, tab bar, content area
- AgentPane.svelte: collapsible tool calls, status strip, prompt input
- TerminalTabs.svelte: add/close shell tabs, active highlighting
- SettingsDrawer.svelte: theme, fonts, providers
- CommandPalette.svelte: Ctrl+K search overlay
- Terminal.svelte: xterm.js with Canvas + Image addons

Status bar: running/idle/stalled counts, attention queue, session
duration, notification bell, Ctrl+K hint. All ARIA labeled.
This commit is contained in:
Hibryda 2026-03-20 01:55:24 +01:00
parent 931bc1b94c
commit b11a856b72
11 changed files with 2001 additions and 220 deletions

View file

@ -0,0 +1,329 @@
<script lang="ts">
interface Props {
open: boolean;
onClose: () => void;
}
let { open, onClose }: Props = $props();
// Local settings state (demo — no persistence in prototype)
let theme = $state('Catppuccin Mocha');
let uiFontSize = $state(14);
let termFontSize = $state(13);
interface Provider {
id: string;
label: string;
enabled: boolean;
}
let providers = $state<Provider[]>([
{ id: 'claude', label: 'Claude (Anthropic)', enabled: true },
{ id: 'codex', label: 'Codex (OpenAI)', enabled: false },
{ id: 'ollama', label: 'Ollama (local)', enabled: false },
]);
function toggleProvider(id: string) {
providers = providers.map(p => p.id === id ? { ...p, enabled: !p.enabled } : p);
}
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) onClose();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') onClose();
}
</script>
<!-- Backdrop -->
{#if open}
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="drawer-backdrop"
role="dialog"
aria-modal="true"
aria-label="Settings"
tabindex="-1"
onclick={handleBackdropClick}
onkeydown={handleKeydown}
>
<aside class="drawer-panel">
<header class="drawer-header">
<h2 class="drawer-title">Settings</h2>
<button class="drawer-close" onclick={onClose} aria-label="Close settings">×</button>
</header>
<div class="drawer-body">
<!-- Appearance -->
<section class="settings-section">
<h3 class="section-heading">Appearance</h3>
<div class="setting-row">
<label class="setting-label" for="theme-select">Theme</label>
<div class="setting-value theme-pill">{theme}</div>
</div>
<div class="setting-row">
<span class="setting-label" id="ui-font-label">UI font size</span>
<div class="font-stepper" role="group" aria-labelledby="ui-font-label">
<button
class="stepper-btn"
onclick={() => uiFontSize = Math.max(10, uiFontSize - 1)}
aria-label="Decrease UI font size"
></button>
<span class="stepper-value">{uiFontSize}px</span>
<button
class="stepper-btn"
onclick={() => uiFontSize = Math.min(24, uiFontSize + 1)}
aria-label="Increase UI font size"
>+</button>
</div>
</div>
<div class="setting-row">
<span class="setting-label" id="term-font-label">Terminal font size</span>
<div class="font-stepper" role="group" aria-labelledby="term-font-label">
<button
class="stepper-btn"
onclick={() => termFontSize = Math.max(8, termFontSize - 1)}
aria-label="Decrease terminal font size"
></button>
<span class="stepper-value">{termFontSize}px</span>
<button
class="stepper-btn"
onclick={() => termFontSize = Math.min(24, termFontSize + 1)}
aria-label="Increase terminal font size"
>+</button>
</div>
</div>
</section>
<!-- Providers -->
<section class="settings-section">
<h3 class="section-heading">Providers</h3>
{#each providers as prov (prov.id)}
<div class="setting-row">
<label class="setting-label" for="prov-{prov.id}">{prov.label}</label>
<button
id="prov-{prov.id}"
class="toggle-btn"
class:enabled={prov.enabled}
onclick={() => toggleProvider(prov.id)}
role="switch"
aria-checked={prov.enabled}
aria-label="{prov.label} {prov.enabled ? 'enabled' : 'disabled'}"
>
<span class="toggle-track">
<span class="toggle-thumb"></span>
</span>
<span class="toggle-label">{prov.enabled ? 'on' : 'off'}</span>
</button>
</div>
{/each}
</section>
</div>
</aside>
</div>
{/if}
<style>
.drawer-backdrop {
position: fixed;
inset: 0;
z-index: 200;
background: color-mix(in srgb, var(--ctp-crust) 60%, transparent);
display: flex;
align-items: stretch;
}
.drawer-panel {
width: 18rem;
max-width: 90vw;
background: var(--ctp-mantle);
border-right: 1px solid var(--ctp-surface0);
display: flex;
flex-direction: column;
overflow: hidden;
animation: slide-in 0.18s ease-out;
}
@keyframes slide-in {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.drawer-header {
height: 3rem;
display: flex;
align-items: center;
padding: 0 0.875rem;
border-bottom: 1px solid var(--ctp-surface0);
flex-shrink: 0;
}
.drawer-title {
flex: 1;
margin: 0;
font-size: 0.9375rem;
font-weight: 600;
color: var(--ctp-text);
}
.drawer-close {
width: 1.75rem;
height: 1.75rem;
background: transparent;
border: none;
border-radius: 0.3rem;
color: var(--ctp-overlay1);
font-size: 1.125rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.12s, color 0.12s;
}
.drawer-close:hover {
background: var(--ctp-surface0);
color: var(--ctp-text);
}
.drawer-body {
flex: 1;
overflow-y: auto;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.drawer-body::-webkit-scrollbar { width: 0.375rem; }
.drawer-body::-webkit-scrollbar-track { background: transparent; }
.drawer-body::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
/* Sections */
.settings-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.section-heading {
margin: 0 0 0.25rem;
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ctp-overlay0);
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.25rem 0;
}
.setting-label {
font-size: 0.8125rem;
color: var(--ctp-subtext1);
}
.setting-value {
font-size: 0.8125rem;
color: var(--ctp-text);
}
.theme-pill {
background: var(--ctp-surface0);
padding: 0.2rem 0.5rem;
border-radius: 0.3rem;
font-size: 0.75rem;
color: var(--ctp-mauve);
}
/* Font stepper */
.font-stepper {
display: flex;
align-items: center;
gap: 0.375rem;
}
.stepper-btn {
width: 1.375rem;
height: 1.375rem;
background: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-radius: 0.25rem;
color: var(--ctp-text);
font-size: 0.875rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.1s;
}
.stepper-btn:hover { background: var(--ctp-surface1); }
.stepper-value {
font-size: 0.8125rem;
color: var(--ctp-text);
min-width: 2.5rem;
text-align: center;
}
/* Toggle switch */
.toggle-btn {
display: flex;
align-items: center;
gap: 0.375rem;
background: transparent;
border: none;
cursor: pointer;
padding: 0;
}
.toggle-track {
width: 2rem;
height: 1.125rem;
background: var(--ctp-surface1);
border-radius: 0.5625rem;
position: relative;
transition: background 0.15s;
display: block;
}
.toggle-btn.enabled .toggle-track {
background: var(--ctp-mauve);
}
.toggle-thumb {
position: absolute;
top: 0.1875rem;
left: 0.1875rem;
width: 0.75rem;
height: 0.75rem;
background: var(--ctp-base);
border-radius: 50%;
transition: transform 0.15s;
display: block;
}
.toggle-btn.enabled .toggle-thumb {
transform: translateX(0.875rem);
}
.toggle-label {
font-size: 0.75rem;
color: var(--ctp-subtext0);
min-width: 1.5rem;
}
.toggle-btn.enabled .toggle-label {
color: var(--ctp-mauve);
}
</style>