Build the Signal design system: tokens, fonts, theme, density
Self-hosted variable fonts (Overpass/Overpass Mono, OFL-licensed) -- no CDN dependency for the app to render correctly. Dark is the literal default in tokens.css (:root defines it directly; light is the override via both prefers-color-scheme and an explicit data-theme), not a retrofit. Severity tokens collapse OTel's seven severities to five visual tiers (severity.ts); their translucent -bg variants and light-mode warn's base color are already tuned for WCAG AA contrast against an opaque surface, not just the plain page background -- verified with real axe-core runs during the accessibility pass, see docs/design-system.md. theme.svelte.ts/density.svelte.ts persist to localStorage and expose getter/setter functions wrapping module-level $state (Svelte 5's shared-state-module pattern -- a directly exported $state doesn't preserve reactivity across modules). app.html's inline script applies both before first paint to avoid a flash of the wrong theme/density.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
// Theme is dark/light/"system" -- but unlike most apps, an unset
|
||||
// preference means dark, not system. That's the whole point of "real
|
||||
// dark mode as the default, not an afterthought": a first-time visitor
|
||||
// on a light-OS machine still lands in dark, matching the actual usage
|
||||
// pattern this product is designed for (long sessions, often at night).
|
||||
// "system" is available for anyone who wants their OS setting to win
|
||||
// instead, but it's a deliberate opt-in, not the fallback.
|
||||
//
|
||||
// The synchronous inline script in app.html applies the same stored
|
||||
// value before first paint -- this module is what UI controls read/write
|
||||
// after hydration; the two must stay in sync on the storage key and
|
||||
// values, not just in spirit.
|
||||
|
||||
export type Theme = 'dark' | 'light' | 'system';
|
||||
|
||||
const STORAGE_KEY = 'sentry.theme';
|
||||
|
||||
function readStored(): Theme {
|
||||
if (typeof localStorage === 'undefined') return 'dark';
|
||||
const v = localStorage.getItem(STORAGE_KEY);
|
||||
return v === 'light' || v === 'system' ? v : 'dark';
|
||||
}
|
||||
|
||||
function apply(t: Theme) {
|
||||
if (typeof document === 'undefined') return;
|
||||
if (t === 'system') {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
}
|
||||
}
|
||||
|
||||
const initial = readStored();
|
||||
let theme = $state<Theme>(initial);
|
||||
apply(initial);
|
||||
|
||||
export function getTheme(): Theme {
|
||||
return theme;
|
||||
}
|
||||
|
||||
export function setTheme(t: Theme) {
|
||||
theme = t;
|
||||
apply(t);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, t);
|
||||
} catch {
|
||||
// storage unavailable -- the choice just won't persist across reloads
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user