feat(electrobun): wire persistence — SQLite, 17 themes, font system

Persistence:
- bun:sqlite at ~/.config/agor/settings.db (WAL mode, 500ms busy_timeout)
- 4 tables: schema_version, settings, projects, custom_themes
- 5 RPC handlers: settings.get/set/getAll, projects get/set

Theme system (LIVE switching):
- All 17 themes ported from Tauri (4 Catppuccin + 7 Editor + 6 Deep Dark)
- applyCssVars() sets 26 --ctp-* vars on document.documentElement
- Parallel xterm ITheme mapping per theme
- theme-store.svelte.ts: Svelte 5 rune store, persists to SQLite

Font system:
- font-store.svelte.ts: UI/terminal font family + size
- Live CSS var application (--ui-font-family/size, --term-font-family/size)
- onTermFontChange() callback registry for terminal instances
- Persists all 4 font settings to SQLite

AppearanceSettings wired: 17-theme grouped dropdown, font steppers
Init on startup: restores saved theme + fonts from SQLite
This commit is contained in:
Hibryda 2026-03-20 05:29:03 +01:00
parent 0b9e8b305a
commit 6002a379e4
13 changed files with 1043 additions and 53 deletions

View file

@ -1,10 +1,7 @@
<script lang="ts">
const THEMES = [
{ id: 'mocha', label: 'Catppuccin Mocha', group: 'Catppuccin' },
{ id: 'macchiato', label: 'Catppuccin Macchiato', group: 'Catppuccin' },
{ id: 'frappe', label: 'Catppuccin Frappé', group: 'Catppuccin' },
{ id: 'latte', label: 'Catppuccin Latte', group: 'Catppuccin' },
];
import { THEMES, THEME_GROUPS, type ThemeId } from '../themes.ts';
import { themeStore } from '../theme-store.svelte.ts';
import { fontStore } from '../font-store.svelte.ts';
const UI_FONTS = [
{ value: '', label: 'System Default' },
@ -25,30 +22,70 @@
{ value: 'monospace', label: 'monospace' },
];
let themeId = $state('mocha');
let uiFont = $state('');
let uiFontSize = $state(14);
let termFont = $state('');
let termFontSize = $state(13);
let cursorStyle = $state('block');
let cursorBlink = $state(true);
let scrollback = $state(1000);
// ── Local reactive state (mirrors store) ───────────────────────────────────
let themeId = $state<ThemeId>(themeStore.currentTheme);
let uiFont = $state(fontStore.uiFontFamily);
let uiFontSize = $state(fontStore.uiFontSize);
let termFont = $state(fontStore.termFontFamily);
let termFontSize = $state(fontStore.termFontSize);
let cursorStyle = $state('block');
let cursorBlink = $state(true);
let scrollback = $state(1000);
// Keep local state in sync when store changes (e.g. after initTheme)
$effect(() => { themeId = themeStore.currentTheme; });
$effect(() => { uiFont = fontStore.uiFontFamily; });
$effect(() => { uiFontSize = fontStore.uiFontSize; });
$effect(() => { termFont = fontStore.termFontFamily; });
$effect(() => { termFontSize = fontStore.termFontSize; });
// ── Dropdown open state ────────────────────────────────────────────────────
let themeOpen = $state(false);
let uiFontOpen = $state(false);
let termFontOpen = $state(false);
// ── Derived labels ─────────────────────────────────────────────────────────
let themeLabel = $derived(THEMES.find(t => t.id === themeId)?.label ?? 'Catppuccin Mocha');
let uiFontLabel = $derived(UI_FONTS.find(f => f.value === uiFont)?.label ?? 'System Default');
let termFontLabel = $derived(TERM_FONTS.find(f => f.value === termFont)?.label ?? 'Default (JetBrains Mono)');
function closeAll() { themeOpen = false; uiFontOpen = false; termFontOpen = false; }
// ── Actions ────────────────────────────────────────────────────────────────
function handleOutsideClick(e: MouseEvent) {
function selectTheme(id: ThemeId): void {
themeId = id;
themeOpen = false;
themeStore.setTheme(id);
}
function selectUIFont(value: string): void {
uiFont = value;
uiFontOpen = false;
fontStore.setUIFont(value, uiFontSize);
}
function selectTermFont(value: string): void {
termFont = value;
termFontOpen = false;
fontStore.setTermFont(value, termFontSize);
}
function adjustUISize(delta: number): void {
uiFontSize = Math.max(8, Math.min(24, uiFontSize + delta));
fontStore.setUIFont(uiFont, uiFontSize);
}
function adjustTermSize(delta: number): void {
termFontSize = Math.max(8, Math.min(24, termFontSize + delta));
fontStore.setTermFont(termFont, termFontSize);
}
function closeAll(): void { themeOpen = false; uiFontOpen = false; termFontOpen = false; }
function handleOutsideClick(e: MouseEvent): void {
if (!(e.target as HTMLElement).closest('.dd-wrap')) closeAll();
}
function handleKey(e: KeyboardEvent) {
function handleKey(e: KeyboardEvent): void {
if (e.key === 'Escape') closeAll();
}
</script>
@ -64,12 +101,16 @@
</button>
{#if themeOpen}
<ul class="dd-list" role="listbox">
{#each THEMES as t}
<li class="dd-item" class:sel={themeId === t.id} role="option" aria-selected={themeId === t.id}
tabindex="0"
onclick={() => { themeId = t.id; themeOpen = false; }}
onkeydown={e => (e.key === 'Enter' || e.key === ' ') && (themeId = t.id, themeOpen = false)}
>{t.label}</li>
{#each THEME_GROUPS as group}
<li class="dd-group-label" role="presentation">{group}</li>
{#each THEMES.filter(t => t.group === group) as t}
<li class="dd-item" class:sel={themeId === t.id}
role="option" aria-selected={themeId === t.id}
tabindex="0"
onclick={() => selectTheme(t.id)}
onkeydown={e => (e.key === 'Enter' || e.key === ' ') && selectTheme(t.id)}
>{t.label}</li>
{/each}
{/each}
</ul>
{/if}
@ -86,19 +127,20 @@
{#if uiFontOpen}
<ul class="dd-list" role="listbox">
{#each UI_FONTS as f}
<li class="dd-item" class:sel={uiFont === f.value} role="option" aria-selected={uiFont === f.value}
<li class="dd-item" class:sel={uiFont === f.value}
role="option" aria-selected={uiFont === f.value}
tabindex="0"
onclick={() => { uiFont = f.value; uiFontOpen = false; }}
onkeydown={e => (e.key === 'Enter' || e.key === ' ') && (uiFont = f.value, uiFontOpen = false)}
onclick={() => selectUIFont(f.value)}
onkeydown={e => (e.key === 'Enter' || e.key === ' ') && selectUIFont(f.value)}
>{f.label}</li>
{/each}
</ul>
{/if}
</div>
<div class="stepper">
<button onclick={() => uiFontSize = Math.max(8, uiFontSize - 1)} aria-label="Decrease UI font size"></button>
<button onclick={() => adjustUISize(-1)} aria-label="Decrease UI font size"></button>
<span>{uiFontSize}px</span>
<button onclick={() => uiFontSize = Math.min(24, uiFontSize + 1)} aria-label="Increase UI font size">+</button>
<button onclick={() => adjustUISize(1)} aria-label="Increase UI font size">+</button>
</div>
</div>
@ -112,19 +154,20 @@
{#if termFontOpen}
<ul class="dd-list" role="listbox">
{#each TERM_FONTS as f}
<li class="dd-item" class:sel={termFont === f.value} role="option" aria-selected={termFont === f.value}
<li class="dd-item" class:sel={termFont === f.value}
role="option" aria-selected={termFont === f.value}
tabindex="0"
onclick={() => { termFont = f.value; termFontOpen = false; }}
onkeydown={e => (e.key === 'Enter' || e.key === ' ') && (termFont = f.value, termFontOpen = false)}
onclick={() => selectTermFont(f.value)}
onkeydown={e => (e.key === 'Enter' || e.key === ' ') && selectTermFont(f.value)}
>{f.label}</li>
{/each}
</ul>
{/if}
</div>
<div class="stepper">
<button onclick={() => termFontSize = Math.max(8, termFontSize - 1)} aria-label="Decrease terminal font size"></button>
<button onclick={() => adjustTermSize(-1)} aria-label="Decrease terminal font size"></button>
<span>{termFontSize}px</span>
<button onclick={() => termFontSize = Math.min(24, termFontSize + 1)} aria-label="Increase terminal font size">+</button>
<button onclick={() => adjustTermSize(1)} aria-label="Increase terminal font size">+</button>
</div>
</div>
@ -169,9 +212,16 @@
position: absolute; top: calc(100% + 0.125rem); left: 0; right: 0; z-index: 50;
list-style: none; margin: 0; padding: 0.25rem;
background: var(--ctp-mantle); border: 1px solid var(--ctp-surface1); border-radius: 0.3rem;
max-height: 12rem; overflow-y: auto;
max-height: 14rem; overflow-y: auto;
box-shadow: 0 0.5rem 1rem color-mix(in srgb, var(--ctp-crust) 60%, transparent);
}
.dd-group-label {
padding: 0.25rem 0.5rem 0.125rem;
font-size: 0.625rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.06em; color: var(--ctp-overlay0);
border-top: 1px solid var(--ctp-surface0);
}
.dd-group-label:first-child { border-top: none; }
.dd-item {
padding: 0.3rem 0.5rem; border-radius: 0.2rem; font-size: 0.8125rem; color: var(--ctp-subtext1);
cursor: pointer; outline: none; list-style: none;