agent-orchestrator/ui-electrobun/src/mainview/SettingsDrawer.svelte
Hibryda 1de6c93e01 feat(electrobun): settings overhaul — fonts, shells, providers, retention, chords
- Settings drawer: responsive width clamp(24rem, 45vw, 50rem)
- System font detection: fc-list for UI fonts (preferred sans-serif starred)
  and mono fonts (Nerd Fonts starred), fallback to hardcoded lists
- Scrollback: default 5000, min 1000, step 500
- Shell detection: system.shells RPC, pre-selects $SHELL login shell
- Provider enablement: provider.scan gates toggle, unavailable shown as N/A
- Session retention: count 0-100 (0=Keep all), age 0-365 (0=Forever)
- Chord keybindings: Ctrl+K → Ctrl+S style multi-key sequences,
  1s prefix wait, arrow separator display, 26 tests passing
2026-03-25 01:42:34 +01:00

261 lines
7.3 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import AppearanceSettings from './settings/AppearanceSettings.svelte';
import AgentSettings from './settings/AgentSettings.svelte';
import SecuritySettings from './settings/SecuritySettings.svelte';
import ProjectSettings from './settings/ProjectSettings.svelte';
import OrchestrationSettings from './settings/OrchestrationSettings.svelte';
import AdvancedSettings from './settings/AdvancedSettings.svelte';
import MarketplaceTab from './settings/MarketplaceTab.svelte';
import KeyboardSettings from './settings/KeyboardSettings.svelte';
import RemoteMachinesSettings from './settings/RemoteMachinesSettings.svelte';
import DiagnosticsTab from './settings/DiagnosticsTab.svelte';
import { t } from './i18n.svelte.ts';
import {
getSettingsCategory, setSettingsCategory,
type SettingsCategory,
} from './ui-store.svelte.ts';
interface Props {
open: boolean;
onClose: () => void;
}
let { open, onClose }: Props = $props();
type CategoryId = SettingsCategory;
interface Category {
id: CategoryId;
label: string;
icon: string;
}
const CATEGORY_DEFS: Array<{ id: CategoryId; icon: string; key: string }> = [
{ id: 'appearance', icon: '🎨', key: 'settings.appearance' },
{ id: 'agents', icon: '🤖', key: 'settings.agents' },
{ id: 'security', icon: '🔒', key: 'settings.security' },
{ id: 'projects', icon: '📁', key: 'settings.projects' },
{ id: 'orchestration', icon: '⚙', key: 'settings.orchestration' },
{ id: 'machines', icon: '🖥', key: 'settings.machines' },
{ id: 'keyboard', icon: '⌨', key: 'settings.keyboard' },
{ id: 'advanced', icon: '🔧', key: 'settings.advanced' },
{ id: 'marketplace', icon: '🛒', key: 'settings.marketplace' },
{ id: 'diagnostics', icon: '📊', key: 'settings.diagnostics' },
];
let CATEGORIES = $derived<Category[]>(
CATEGORY_DEFS.map(d => ({ id: d.id, label: t(d.key as any), icon: d.icon }))
);
let activeCategory = $derived(getSettingsCategory());
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) onClose();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') onClose();
}
</script>
<!-- Fix #11: display toggle instead of {#if} -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="drawer-backdrop"
style:display={open ? 'flex' : 'none'}
role="dialog"
aria-modal="true"
aria-label="Settings"
tabindex="-1"
onclick={handleBackdropClick}
onkeydown={handleKeydown}
>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<aside class="drawer-panel" onclick={e => e.stopPropagation()} onkeydown={e => e.stopPropagation()}>
<!-- Header -->
<header class="drawer-header">
<h2 class="drawer-title">{t('settings.title')}</h2>
<button class="drawer-close" onclick={onClose} aria-label={t('settings.close')}>×</button>
</header>
<!-- Body: sidebar + content -->
<div class="drawer-body">
<!-- Category nav -->
<nav class="cat-nav" aria-label="Settings categories">
{#each CATEGORIES as cat}
<button
class="cat-btn"
class:active={activeCategory === cat.id}
onclick={() => setSettingsCategory(cat.id)}
aria-current={activeCategory === cat.id ? 'page' : undefined}
>
<span class="cat-icon" aria-hidden="true">{cat.icon}</span>
<span class="cat-label">{cat.label}</span>
</button>
{/each}
</nav>
<!-- Category content -->
<div class="cat-content">
{#if activeCategory === 'appearance'}
<AppearanceSettings />
{:else if activeCategory === 'agents'}
<AgentSettings />
{:else if activeCategory === 'security'}
<SecuritySettings />
{:else if activeCategory === 'projects'}
<ProjectSettings />
{:else if activeCategory === 'orchestration'}
<OrchestrationSettings />
{:else if activeCategory === 'machines'}
<RemoteMachinesSettings />
{:else if activeCategory === 'advanced'}
<AdvancedSettings />
{:else if activeCategory === 'keyboard'}
<KeyboardSettings />
{:else if activeCategory === 'marketplace'}
<MarketplaceTab />
{:else if activeCategory === 'diagnostics'}
<DiagnosticsTab />
{/if}
</div>
</div>
</aside>
</div>
<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: clamp(24rem, 45vw, 50rem);
max-width: 95vw;
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;
min-height: 0;
display: flex;
overflow: hidden;
}
.cat-nav {
width: 8.5rem;
flex-shrink: 0;
background: var(--ctp-crust);
border-right: 1px solid var(--ctp-surface0);
display: flex;
flex-direction: column;
padding: 0.375rem 0;
gap: 0.0625rem;
overflow-y: auto;
}
.cat-nav::-webkit-scrollbar { display: none; }
.cat-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: transparent;
border: none;
color: var(--ctp-subtext0);
font-family: var(--ui-font-family);
font-size: 0.8125rem;
cursor: pointer;
text-align: left;
border-radius: 0;
transition: background 0.1s, color 0.1s;
}
.cat-btn:hover {
background: var(--ctp-surface0);
color: var(--ctp-text);
}
.cat-btn.active {
background: var(--ctp-surface0);
color: var(--ctp-text);
border-left: 2px solid var(--ctp-mauve);
padding-left: calc(0.75rem - 2px);
}
.cat-icon {
font-size: 0.875rem;
flex-shrink: 0;
}
.cat-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cat-content {
flex: 1;
min-width: 0;
overflow-y: auto;
padding: 0.875rem;
}
.cat-content::-webkit-scrollbar { width: 0.375rem; }
.cat-content::-webkit-scrollbar-track { background: transparent; }
.cat-content::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
</style>