agent-orchestrator/ui-electrobun/src/mainview/SettingsDrawer.svelte
Hibryda 5032021915 feat(electrobun): wire EVERYTHING — all settings persist, theme editor, marketplace
All settings wired to SQLite persistence:
- AgentSettings: shell, CWD, permissions, providers (JSON blob)
- SecuritySettings: branch policies (JSON array)
- ProjectSettings: per-project via setProject RPC
- OrchestrationSettings: wake, anchors, notifications
- AdvancedSettings: logging, OTLP, plugins, import/export JSON

Theme Editor:
- 26 color pickers (14 Accents + 12 Neutrals)
- Live CSS var preview as you pick colors
- Save custom theme to SQLite, cancel reverts
- Import/export theme as JSON
- Custom themes in dropdown with delete button

Extensions Marketplace:
- 8-plugin demo catalog (Browse/Installed tabs)
- Search/filter by name or tag
- Install/uninstall with SQLite persistence
- Plugin cards with emoji icons, tags, version

Terminal font hot-swap:
- fontStore.onTermFontChange() → xterm.js options update + fitAddon.fit()
- Resize notification to PTY daemon after font change

All 7 settings categories functional. Every control persists and takes effect.
2026-03-20 05:45:10 +01:00

240 lines
6.4 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';
interface Props {
open: boolean;
onClose: () => void;
}
let { open, onClose }: Props = $props();
type CategoryId = 'appearance' | 'agents' | 'security' | 'projects' | 'orchestration' | 'advanced' | 'marketplace';
interface Category {
id: CategoryId;
label: string;
icon: string;
}
const CATEGORIES: Category[] = [
{ id: 'appearance', label: 'Appearance', icon: '🎨' },
{ id: 'agents', label: 'Agents', icon: '🤖' },
{ id: 'security', label: 'Security', icon: '🔒' },
{ id: 'projects', label: 'Projects', icon: '📁' },
{ id: 'orchestration', label: 'Orchestration', icon: '⚙' },
{ id: 'advanced', label: 'Advanced', icon: '🔧' },
{ id: 'marketplace', label: 'Marketplace', icon: '🛒' },
];
let activeCategory = $state<CategoryId>('appearance');
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) onClose();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') onClose();
}
</script>
{#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}
>
<!-- 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">Settings</h2>
<button class="drawer-close" onclick={onClose} aria-label="Close settings">×</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={() => activeCategory = 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 === 'advanced'}
<AdvancedSettings />
{:else if activeCategory === 'marketplace'}
<MarketplaceTab />
{/if}
</div>
</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: 30rem;
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>