New stores: - ui-store.svelte.ts: settingsOpen, paletteOpen, searchOpen, notifDrawerOpen, showWizard, settingsCategory, projectToDelete, showAddGroup, newGroupName - project-tabs-store.svelte.ts: per-project activeTab + activatedTabs via Map Wired: - App.svelte: 8 inline $state removed, reads/writes via ui-store - ProjectCard: activeTab/activatedTabs from project-tabs-store - SettingsDrawer: activeCategory from ui-store - CommandPalette: 4 commands call ui-store directly (no CustomEvent dispatch) Components are now pure view layers reading from stores.
261 lines
7.3 KiB
Svelte
261 lines
7.3 KiB
Svelte
<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: 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>
|