Build the ui/ component library and persistent app shell
Button, Input, Select, Badge, SeverityBadge, Table, Card, Modal, Tooltip, Tabs, Skeleton, EmptyState -- a shared library so pages stop hand-rolling markup per page (web/src/lib/components/ui, barrel export in index.ts). Modal and CommandPalette are built on native <dialog> for a real focus trap, Escape-to-close, and top-layer stacking instead of hand-rolling those. Tabs uses roving tabindex with arrow-key nav. NavSidebar replaces the old top-nav with a persistent sidebar (Search/Dashboards/Alerts/Data Sources/Settings), a live tenant indicator (api.ts's new getCurrentSession(), a client for the already-existing POST /internal/authorize -- zero new backend surface), theme/density quick-toggles, and a command-palette hint. Collapses to an off-canvas drawer under 860px. CommandPalette (Cmd/Ctrl+K) indexes the five static destinations plus live-fetched dashboards/alert rules. Data Sources is a new, honestly-scoped placeholder page (one data source per tenant today, no UI needed yet). Settings and Select-tenant are re-tokened onto the new component library. +layout.svelte's content wrapper is a plain <div>, not a second <main> -- every page already renders its own top-level <main>.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getAuthFeatures, enterpriseAuthBase, type AuthFeatures } from '$lib/api';
|
||||
import { getTheme, setTheme, type Theme } from '$lib/theme.svelte';
|
||||
import { getDensity, setDensity, type Density } from '$lib/density.svelte';
|
||||
|
||||
let loading = $state(true);
|
||||
let features = $state<AuthFeatures>({ sso_configured: false, oidc_enabled: false, saml_enabled: false });
|
||||
@@ -10,18 +12,60 @@
|
||||
loading = false;
|
||||
}
|
||||
load();
|
||||
|
||||
const themeOptions: { value: Theme; label: string; hint: string }[] = [
|
||||
{ value: 'dark', label: 'Dark', hint: 'Default' },
|
||||
{ value: 'light', label: 'Light', hint: '' },
|
||||
{ value: 'system', label: 'System', hint: 'Follow OS setting' }
|
||||
];
|
||||
const densityOptions: { value: Density; label: string; hint: string }[] = [
|
||||
{ value: 'comfortable', label: 'Comfortable', hint: 'Dashboards, forms' },
|
||||
{ value: 'compact', label: 'Compact', hint: 'Log tables, results' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<h1>Settings</h1>
|
||||
|
||||
<section>
|
||||
<h2>Appearance</h2>
|
||||
<div class="option-group" role="radiogroup" aria-label="Theme">
|
||||
{#each themeOptions as opt (opt.value)}
|
||||
<button
|
||||
type="button"
|
||||
class="option"
|
||||
class:selected={getTheme() === opt.value}
|
||||
aria-pressed={getTheme() === opt.value}
|
||||
onclick={() => setTheme(opt.value)}
|
||||
>
|
||||
<span class="option-label">{opt.label}</span>
|
||||
{#if opt.hint}<span class="option-hint">{opt.hint}</span>{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="option-group" role="radiogroup" aria-label="Row density">
|
||||
{#each densityOptions as opt (opt.value)}
|
||||
<button
|
||||
type="button"
|
||||
class="option"
|
||||
class:selected={getDensity() === opt.value}
|
||||
aria-pressed={getDensity() === opt.value}
|
||||
onclick={() => setDensity(opt.value)}
|
||||
>
|
||||
<span class="option-label">{opt.label}</span>
|
||||
<span class="option-hint">{opt.hint}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Core</h2>
|
||||
<p>Single-tenant deployment settings live here. Nothing configurable yet.</p>
|
||||
</section>
|
||||
|
||||
{#if loading}
|
||||
<p>Loading…</p>
|
||||
<p class="muted">Loading…</p>
|
||||
{:else if features.sso_configured}
|
||||
<section>
|
||||
<h2>Single sign-on</h2>
|
||||
@@ -48,19 +92,66 @@
|
||||
|
||||
<style>
|
||||
main {
|
||||
font-family: system-ui, sans-serif;
|
||||
max-width: 960px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
max-width: 40rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: var(--text-xl);
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
section {
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.1rem;
|
||||
font-size: var(--text-md);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
.muted {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.note {
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.option-group {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
.option {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.option:hover {
|
||||
border-color: var(--color-border-strong);
|
||||
}
|
||||
.option.selected {
|
||||
border-color: var(--color-accent);
|
||||
background: color-mix(in srgb, var(--color-accent) 8%, var(--color-surface));
|
||||
}
|
||||
.option-label {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.option-hint {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
/* The accent-tinted .selected background is *lighter* than plain
|
||||
--color-surface, which quietly drops --color-text-muted below
|
||||
4.5:1 (axe-core's color-contrast check caught this at 4.4:1) --
|
||||
fixed by using the full-strength --color-text on that lighter
|
||||
background specifically, not by changing the shared muted token
|
||||
everywhere else it still passes correctly. */
|
||||
.option.selected .option-hint {
|
||||
color: var(--color-text);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user