feat(settings): Sprint 1 — extract AppearanceSettings from monolith (222 lines)
This commit is contained in:
parent
48dd35000a
commit
b25d22e686
1 changed files with 222 additions and 0 deletions
222
src/lib/settings/categories/AppearanceSettings.svelte
Normal file
222
src/lib/settings/categories/AppearanceSettings.svelte
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getCurrentTheme, setTheme } from '../../stores/theme.svelte';
|
||||
import { THEME_LIST, getPalette, type ThemeId } from '../../styles/themes';
|
||||
import { getSetting, setSetting } from '../../adapters/settings-bridge';
|
||||
|
||||
const UI_FONTS = [
|
||||
{ value: '', label: 'System Default' },
|
||||
{ value: 'Inter', label: 'Inter' },
|
||||
{ value: 'IBM Plex Sans', label: 'IBM Plex Sans' },
|
||||
{ value: 'Noto Sans', label: 'Noto Sans' },
|
||||
{ value: 'Roboto', label: 'Roboto' },
|
||||
{ value: 'Source Sans 3', label: 'Source Sans 3' },
|
||||
{ value: 'Ubuntu', label: 'Ubuntu' },
|
||||
{ value: 'JetBrains Mono', label: 'JetBrains Mono' },
|
||||
{ value: 'Fira Code', label: 'Fira Code' },
|
||||
];
|
||||
const TERM_FONTS = [
|
||||
{ value: '', label: 'Default (JetBrains Mono)' },
|
||||
{ value: 'JetBrains Mono', label: 'JetBrains Mono' },
|
||||
{ value: 'Fira Code', label: 'Fira Code' },
|
||||
{ value: 'Cascadia Code', label: 'Cascadia Code' },
|
||||
{ value: 'Source Code Pro', label: 'Source Code Pro' },
|
||||
{ value: 'IBM Plex Mono', label: 'IBM Plex Mono' },
|
||||
{ value: 'Hack', label: 'Hack' },
|
||||
{ value: 'Inconsolata', label: 'Inconsolata' },
|
||||
{ value: 'Ubuntu Mono', label: 'Ubuntu Mono' },
|
||||
{ value: 'monospace', label: 'monospace' },
|
||||
];
|
||||
|
||||
let selectedTheme = $state<ThemeId>(getCurrentTheme());
|
||||
let uiFont = $state('');
|
||||
let uiFontSize = $state('14');
|
||||
let termFont = $state('');
|
||||
let termFontSize = $state('14');
|
||||
let cursorStyle = $state('block');
|
||||
let cursorBlink = $state(true);
|
||||
let scrollbackLines = $state('1000');
|
||||
let themeOpen = $state(false);
|
||||
let uiFontOpen = $state(false);
|
||||
let termFontOpen = $state(false);
|
||||
|
||||
let themeGroups = $derived(() => {
|
||||
const map = new Map<string, typeof THEME_LIST>();
|
||||
for (const t of THEME_LIST) {
|
||||
if (!map.has(t.group)) map.set(t.group, []);
|
||||
map.get(t.group)!.push(t);
|
||||
}
|
||||
return [...map.entries()];
|
||||
});
|
||||
let themeLabel = $derived(THEME_LIST.find(t => t.id === selectedTheme)?.label ?? selectedTheme);
|
||||
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 css(prop: string, val: string) { document.documentElement.style.setProperty(prop, val); }
|
||||
|
||||
onMount(async () => {
|
||||
const [font, size, tfont, tsize, cursor, blink, scroll] = await Promise.all([
|
||||
getSetting('ui_font_family'), getSetting('ui_font_size'),
|
||||
getSetting('term_font_family'), getSetting('term_font_size'),
|
||||
getSetting('term_cursor_style'), getSetting('term_cursor_blink'),
|
||||
getSetting('term_scrollback'),
|
||||
]);
|
||||
if (font) uiFont = font;
|
||||
if (size) uiFontSize = size;
|
||||
if (tfont) termFont = tfont;
|
||||
if (tsize) termFontSize = tsize;
|
||||
if (cursor) cursorStyle = cursor;
|
||||
if (blink !== null) cursorBlink = blink !== 'false';
|
||||
if (scroll) scrollbackLines = scroll;
|
||||
});
|
||||
|
||||
function pickTheme(id: ThemeId) { selectedTheme = id; themeOpen = false; setTheme(id); }
|
||||
function pickUiFont(val: string) {
|
||||
uiFont = val; uiFontOpen = false;
|
||||
css('--ui-font-family', val ? `'${val}', sans-serif` : 'system-ui, sans-serif');
|
||||
setSetting('ui_font_family', val);
|
||||
}
|
||||
function pickTermFont(val: string) {
|
||||
termFont = val; termFontOpen = false;
|
||||
css('--term-font-family', val ? `'${val}', monospace` : `'JetBrains Mono', monospace`);
|
||||
setSetting('term_font_family', val);
|
||||
}
|
||||
function stepUiSize(delta: number) {
|
||||
const n = parseInt(uiFontSize, 10) + delta;
|
||||
if (n < 8 || n > 24) return;
|
||||
uiFontSize = String(n); css('--ui-font-size', `${n}px`); setSetting('ui_font_size', String(n));
|
||||
}
|
||||
function stepTermSize(delta: number) {
|
||||
const n = parseInt(termFontSize, 10) + delta;
|
||||
if (n < 8 || n > 24) return;
|
||||
termFontSize = String(n); css('--term-font-size', `${n}px`); setSetting('term_font_size', String(n));
|
||||
}
|
||||
function setCursor(style: string) { cursorStyle = style; setSetting('term_cursor_style', style); }
|
||||
function toggleBlink() { cursorBlink = !cursorBlink; setSetting('term_cursor_blink', String(cursorBlink)); }
|
||||
function setScrollback(val: string) {
|
||||
const n = parseInt(val, 10);
|
||||
if (isNaN(n) || n < 100 || n > 100000) return;
|
||||
scrollbackLines = val; setSetting('term_scrollback', val);
|
||||
}
|
||||
|
||||
function closeDropdowns() { themeOpen = false; uiFontOpen = false; termFontOpen = false; }
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={(e) => { if (!(e.target as HTMLElement)?.closest('.dropdown')) closeDropdowns(); }}
|
||||
onkeydown={(e) => { if (e.key === 'Escape') closeDropdowns(); }} />
|
||||
|
||||
<div class="appearance">
|
||||
<h3>Theme</h3>
|
||||
<div class="field" id="setting-theme">
|
||||
<div class="dropdown">
|
||||
<button class="dropdown-btn" onclick={() => { closeDropdowns(); themeOpen = !themeOpen; }}>
|
||||
<span class="theme-dots">{@html (() => { const p = getPalette(selectedTheme); return [p.red, p.green, p.blue, p.yellow].map(c => `<span style="background:${c}" class="dot"></span>`).join(''); })()}</span>
|
||||
{themeLabel}
|
||||
</button>
|
||||
{#if themeOpen}
|
||||
<div class="dropdown-menu theme-menu">
|
||||
{#each themeGroups() as [group, themes]}
|
||||
<div class="group-label">{group}</div>
|
||||
{#each themes as t}
|
||||
<button class="dropdown-item" class:active={t.id === selectedTheme} onclick={() => pickTheme(t.id)}>
|
||||
<span class="theme-dots">{@html [t.palette.red, t.palette.green, t.palette.blue, t.palette.yellow].map(c => `<span style="background:${c}" class="dot"></span>`).join('')}</span>
|
||||
{t.label}
|
||||
</button>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>UI Font</h3>
|
||||
<div class="field row" id="setting-ui-font">
|
||||
<div class="dropdown flex1">
|
||||
<button class="dropdown-btn" onclick={() => { closeDropdowns(); uiFontOpen = !uiFontOpen; }}>{uiFontLabel}</button>
|
||||
{#if uiFontOpen}
|
||||
<div class="dropdown-menu">
|
||||
{#each UI_FONTS as f}
|
||||
<button class="dropdown-item" class:active={f.value === uiFont} onclick={() => pickUiFont(f.value)}>{f.label}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="stepper" id="setting-ui-font-size">
|
||||
<button onclick={() => stepUiSize(-1)} aria-label="Decrease UI font size">-</button>
|
||||
<span>{uiFontSize}px</span>
|
||||
<button onclick={() => stepUiSize(1)} aria-label="Increase UI font size">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Terminal Font</h3>
|
||||
<div class="field row" id="setting-term-font">
|
||||
<div class="dropdown flex1">
|
||||
<button class="dropdown-btn" onclick={() => { closeDropdowns(); termFontOpen = !termFontOpen; }}>{termFontLabel}</button>
|
||||
{#if termFontOpen}
|
||||
<div class="dropdown-menu">
|
||||
{#each TERM_FONTS as f}
|
||||
<button class="dropdown-item" class:active={f.value === termFont} onclick={() => pickTermFont(f.value)}>{f.label}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="stepper" id="setting-term-font-size">
|
||||
<button onclick={() => stepTermSize(-1)} aria-label="Decrease terminal font size">-</button>
|
||||
<span>{termFontSize}px</span>
|
||||
<button onclick={() => stepTermSize(1)} aria-label="Increase terminal font size">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Terminal Cursor</h3>
|
||||
<div class="field row" id="setting-cursor-style">
|
||||
<div class="segmented">
|
||||
{#each ['block', 'line', 'underline'] as s}
|
||||
<button class:active={cursorStyle === s} onclick={() => setCursor(s)}>{s[0].toUpperCase() + s.slice(1)}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<label class="toggle-label" id="setting-cursor-blink">
|
||||
<span>Blink</span>
|
||||
<button class="toggle" class:on={cursorBlink} onclick={toggleBlink} aria-label="Toggle cursor blink">{cursorBlink ? 'On' : 'Off'}</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h3>Scrollback</h3>
|
||||
<div class="field" id="setting-scrollback">
|
||||
<input type="number" min="100" max="100000" step="100" bind:value={scrollbackLines}
|
||||
onchange={() => setScrollback(scrollbackLines)} aria-label="Scrollback lines" />
|
||||
<span class="hint">lines (100–100,000)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.appearance { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
h3 { font-size: 0.75rem; font-weight: 600; color: var(--ctp-subtext0); text-transform: uppercase; letter-spacing: 0.05em; margin: 0.5rem 0 0.125rem; }
|
||||
.field { position: relative; }
|
||||
.row { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.flex1 { flex: 1; }
|
||||
.dropdown { position: relative; }
|
||||
.dropdown-btn { width: 100%; padding: 0.375rem 0.625rem; background: var(--ctp-mantle); border: 1px solid var(--ctp-surface0); border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8125rem; text-align: left; cursor: pointer; display: flex; align-items: center; gap: 0.375rem; }
|
||||
.dropdown-btn:hover { border-color: var(--ctp-surface1); }
|
||||
.dropdown-menu { position: absolute; top: 100%; left: 0; right: 0; z-index: 50; background: var(--ctp-mantle); border: 1px solid var(--ctp-surface0); border-radius: 0.25rem; max-height: 16rem; overflow-y: auto; margin-top: 0.125rem; }
|
||||
.theme-menu { min-width: 14rem; }
|
||||
.dropdown-item { display: flex; align-items: center; gap: 0.375rem; width: 100%; padding: 0.3125rem 0.625rem; background: none; border: none; color: var(--ctp-text); font-size: 0.8125rem; cursor: pointer; text-align: left; }
|
||||
.dropdown-item:hover { background: var(--ctp-surface0); }
|
||||
.dropdown-item.active { color: var(--ctp-blue); }
|
||||
.group-label { padding: 0.25rem 0.625rem; font-size: 0.6875rem; color: var(--ctp-overlay0); font-weight: 600; text-transform: uppercase; }
|
||||
.dot { display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 50%; }
|
||||
.theme-dots { display: flex; gap: 0.1875rem; }
|
||||
.stepper { display: flex; align-items: center; gap: 0.25rem; }
|
||||
.stepper button { width: 1.5rem; height: 1.5rem; background: var(--ctp-surface0); border: none; border-radius: 0.1875rem; color: var(--ctp-text); cursor: pointer; font-size: 0.875rem; display: flex; align-items: center; justify-content: center; }
|
||||
.stepper button:hover { background: var(--ctp-surface1); }
|
||||
.stepper span { font-size: 0.8125rem; min-width: 2.5rem; text-align: center; color: var(--ctp-subtext0); }
|
||||
.segmented { display: flex; gap: 1px; background: var(--ctp-surface0); border-radius: 0.25rem; overflow: hidden; }
|
||||
.segmented button { flex: 1; padding: 0.3125rem 0.5rem; background: var(--ctp-mantle); border: none; color: var(--ctp-subtext0); font-size: 0.75rem; cursor: pointer; }
|
||||
.segmented button:hover { background: var(--ctp-surface0); }
|
||||
.segmented button.active { background: var(--ctp-blue); color: var(--ctp-base); }
|
||||
.toggle-label { display: flex; align-items: center; gap: 0.375rem; font-size: 0.8125rem; color: var(--ctp-subtext0); }
|
||||
.toggle { padding: 0.1875rem 0.5rem; background: var(--ctp-surface0); border: none; border-radius: 0.1875rem; color: var(--ctp-subtext0); cursor: pointer; font-size: 0.75rem; }
|
||||
.toggle.on { background: var(--ctp-green); color: var(--ctp-base); }
|
||||
input[type="number"] { width: 5rem; padding: 0.375rem 0.5rem; background: var(--ctp-mantle); border: 1px solid var(--ctp-surface0); border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8125rem; }
|
||||
input[type="number"]:focus { border-color: var(--ctp-blue); outline: none; }
|
||||
.hint { font-size: 0.6875rem; color: var(--ctp-overlay0); margin-left: 0.5rem; }
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue