<script lang="ts">
import { tick } from "svelte";
import { ui } from "../../stores/ui.svelte.js";
import AppIcon from "../layout/AppIcon.svelte";
import { onMount } from "svelte";
let inputRef: HTMLInputElement | undefined = $state(undefined);
let query = $state("");
let selectedIndex = $state(0);
interface ThemeOption {
label: string;
description: string;
value: string;
}
let options = $derived.by(() => {
const items: ThemeOption[] = [
{ label: "Auto (system)", description: "Follow system preference", value: "auto" },
...ui.allThemes.map(t => ({
label: t.label,
description: t.ui_theme === "vs" ? "Light" : t.ui_theme === "vs-dark" ? "Dark" : "High Contrast",
value: t.label,
})),
];
if (!query) return items;
const q = query.toLowerCase();
return items.filter(
o => o.label.toLowerCase().includes(q) || o.description.toLowerCase().includes(q),
);
});
let currentValue = $derived(ui.themeMode);
$effect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
options;
selectedIndex = 0;
});
$effect(() => {
if (inputRef) inputRef.focus();
});
$effect(() => {
const _idx = selectedIndex;
tick().then(() => {
document
.querySelector(".theme-picker-results .theme-item.selected")
?.scrollIntoView({ block: "nearest" });
});
});
function close() {
ui.setPreviewTheme(null);
ui.activeModal = null;
}
function select(option: ThemeOption) {
ui.setThemeMode(option.value);
close();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === "ArrowDown") {
e.preventDefault();
const nextIndex = Math.min(selectedIndex + 1, options.length - 1);
selectedIndex = nextIndex;
const opt = options[nextIndex];
if (opt) ui.setPreviewTheme(opt.value);
} else if (e.key === "ArrowUp") {
e.preventDefault();
const nextIndex = Math.max(selectedIndex - 1, 0);
selectedIndex = nextIndex;
const opt = options[nextIndex];
if (opt) ui.setPreviewTheme(opt.value);
} else if (e.key === "Enter") {
e.preventDefault();
const opt = options[selectedIndex];
if (opt) select(opt);
} else if (e.key === "Escape") {
e.preventDefault();
close();
}
}
function handleOverlayClick(e: MouseEvent) {
if ((e.target as HTMLElement).classList.contains("theme-overlay")) close();
}
onMount(() => {
// Pre-select the current theme in the list
const idx = options.findIndex(o => o.value === currentValue);
if (idx >= 0) selectedIndex = idx;
return () => ui.setPreviewTheme(null);
});
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="theme-overlay" onclick={handleOverlayClick} onkeydown={handleKeydown}>
<div class="theme-palette">
<div class="theme-input-wrap">
<span class="theme-prefix">
<AppIcon name="palette" size={14} />
</span>
<input
bind:this={inputRef}
class="theme-input"
type="text"
placeholder="Select Color Theme..."
bind:value={query}
/>
<kbd class="esc-hint">Esc</kbd>
</div>
<div class="theme-picker-results">
{#if options.length === 0}
<div class="theme-empty">No themes found</div>
{:else}
{#each options as option, i (option.value)}
<button
class="theme-item"
class:selected={i === selectedIndex}
class:current={option.value === currentValue}
onclick={() => select(option)}
onmouseenter={() => {
selectedIndex = i;
ui.setPreviewTheme(option.value);
}}
>
<span class="theme-label">{option.label}</span>
<span class="theme-desc">{option.description}</span>
{#if option.value === currentValue}
<span class="theme-check">
<AppIcon name="check" size={14} />
</span>
{/if}
</button>
{/each}
{/if}
</div>
</div>
</div>
<style>
.theme-overlay {
position: fixed;
inset: 0;
background: var(--vscode-widget-shadow);
display: flex;
justify-content: center;
padding-top: 20vh;
z-index: 100;
}
.theme-palette {
width: 480px;
max-height: 360px;
background: var(--vscode-sideBar-background);
border: 1px solid var(--vscode-panel-border);
border-radius: var(--wsb-radius-lg);
box-shadow: var(--wb-shadow-md);
display: flex;
flex-direction: column;
overflow: hidden;
}
.theme-input-wrap {
display: flex;
align-items: center;
gap: var(--wb-spacing-8);
padding: var(--wb-spacing-10) var(--wb-spacing-14);
border-bottom: 1px solid var(--vscode-panel-border);
}
.theme-prefix {
color: var(--vscode-focusBorder);
flex-shrink: 0;
display: flex;
}
.theme-input {
flex: 1;
background: none;
border: none;
font-size: var(--wb-font-size-14);
color: var(--vscode-editor-foreground);
outline: none;
}
.theme-input::placeholder {
color: var(--vscode-disabledForeground);
}
.esc-hint {
font-size: var(--wb-font-size-10);
padding: var(--wb-spacing-1) var(--wb-spacing-5);
border: 1px solid var(--vscode-panel-border);
border-radius: var(--wsb-radius-sm);
color: var(--vscode-disabledForeground);
background: var(--vscode-input-background);
font-family: var(--wsb-font-sans);
flex-shrink: 0;
}
.theme-picker-results {
overflow-y: auto;
flex: 1;
padding: var(--wb-spacing-4) 0;
}
.theme-item {
display: flex;
align-items: center;
gap: var(--wb-spacing-10);
width: 100%;
padding: var(--wb-spacing-7) var(--wb-spacing-14);
text-align: left;
font-size: var(--wb-font-size-13);
color: var(--vscode-editor-foreground);
}
.theme-item:hover,
.theme-item.selected {
background: var(--vscode-list-hoverBackground);
}
.theme-item.current {
color: var(--vscode-focusBorder);
}
.theme-label {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.theme-desc {
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
white-space: nowrap;
flex-shrink: 0;
}
.theme-check {
flex-shrink: 0;
display: flex;
color: var(--vscode-focusBorder);
}
.theme-empty {
padding: var(--wb-spacing-16);
text-align: center;
color: var(--vscode-disabledForeground);
font-size: var(--wb-font-size-13);
}
</style>