5dcadd1c20
Warm, friendly redesign replacing the generic cold-shadcn look. Built as a swappable token bundle so other presets can be added later; dark mode and the user-tunable accent hue are retained. Foundation - app.css: warm cream (light) + "dusk" (dark) token system; terracotta accent (default hue 16); pastel --room-* palette; vivid --status-* (dots/bars) plus AA-legible --status-*-ink (text); soft warm shadows; --radius 1rem; font tokens - Fonts: Fraunces (display) + Figtree (body), self-hosted in static/fonts (no Google CDN) so offline/LAN installs work; system-ui fallbacks kept - h1/h2/h3 render in Fraunces via base layer Chrome and surfaces - Sidebar, Header, home, AppCard/BoardCard, BoardHeader, sections, favorites - 29 widgets + integration renderers: cozy card shells, room-palette charts - Default background is a static warm "cozy" glow (mesh demoted, rAF gated on prefers-reduced-motion) System-wide - Status colors tokenized (no raw bg/text-*-500 or status hex); success/warning to status tokens, categorical to room palette, errors to destructive - Inputs rounded-xl; buttons rounded-xl; cards/dialogs rounded-[1.4rem]; soft-shadow vocabulary only; focus-visible:ring-primary/30 - Forms, admin tables (now cozy cards), dialogs, popovers, auth screens a11y: reduced-motion guards; darker status "ink" text for AA on cream. Known tradeoff: terracotta primary + white button text ~2.96:1 (signature color, user-tunable). Verified: svelte-check 0/0, build ok, 274 tests pass, eslint 0 errors. Design refs + system sheet in design-mockups/.
108 lines
2.4 KiB
Svelte
108 lines
2.4 KiB
Svelte
<script lang="ts">
|
|
interface Props {
|
|
id?: string;
|
|
name?: string;
|
|
value: string | undefined;
|
|
suggestions: string[];
|
|
placeholder?: string;
|
|
class?: string;
|
|
}
|
|
|
|
let {
|
|
id,
|
|
name,
|
|
value = $bindable(),
|
|
suggestions,
|
|
placeholder = '',
|
|
class: className = ''
|
|
}: Props = $props();
|
|
|
|
let open = $state(false);
|
|
let highlightIdx = $state(-1);
|
|
let inputEl: HTMLInputElement | undefined = $state();
|
|
let containerEl: HTMLDivElement | undefined = $state();
|
|
|
|
const filtered = $derived.by(() => {
|
|
const q = (value ?? '').trim().toLowerCase();
|
|
if (!q) return suggestions;
|
|
return suggestions.filter((s) => s.toLowerCase().includes(q));
|
|
});
|
|
|
|
function handleInput() {
|
|
open = true;
|
|
highlightIdx = -1;
|
|
}
|
|
|
|
function handleFocus() {
|
|
open = true;
|
|
}
|
|
|
|
function selectItem(item: string) {
|
|
value = item;
|
|
open = false;
|
|
inputEl?.focus();
|
|
}
|
|
|
|
function handleKeydown(e: KeyboardEvent) {
|
|
if (!open || filtered.length === 0) {
|
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
open = true;
|
|
e.preventDefault();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
highlightIdx = (highlightIdx + 1) % filtered.length;
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
highlightIdx = highlightIdx <= 0 ? filtered.length - 1 : highlightIdx - 1;
|
|
} else if (e.key === 'Enter' && highlightIdx >= 0) {
|
|
e.preventDefault();
|
|
selectItem(filtered[highlightIdx]);
|
|
} else if (e.key === 'Escape') {
|
|
open = false;
|
|
}
|
|
}
|
|
|
|
function handleClickOutside(e: MouseEvent) {
|
|
if (containerEl && !containerEl.contains(e.target as Node)) {
|
|
open = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:window onclick={handleClickOutside} />
|
|
|
|
<div class="relative" bind:this={containerEl}>
|
|
<input
|
|
{id}
|
|
{name}
|
|
type="text"
|
|
bind:this={inputEl}
|
|
bind:value
|
|
oninput={handleInput}
|
|
onfocus={handleFocus}
|
|
onkeydown={handleKeydown}
|
|
{placeholder}
|
|
class={className}
|
|
autocomplete="off"
|
|
/>
|
|
|
|
{#if open && filtered.length > 0}
|
|
<div class="absolute left-0 top-full z-50 mt-1 max-h-48 w-full overflow-y-auto rounded-xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
|
{#each filtered as item, i (item)}
|
|
<button
|
|
type="button"
|
|
onclick={() => selectItem(item)}
|
|
class="flex w-full items-center px-3 py-1.5 text-left text-sm text-foreground transition-colors
|
|
{i === highlightIdx ? 'bg-accent' : 'hover:bg-accent/50'}"
|
|
>
|
|
{item}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|