df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 1 | import { writable, type Writable } from 'svelte/store'; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 2 | |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 3 | export const MESSAGE_SPACING_MIN = 0; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 4 | export const MESSAGE_SPACING_MAX = 16; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 5 | export const MESSAGE_SPACING_DEFAULT = 3; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 6 | |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 7 | function persisted<T>(key: string, fallback: T, valid: (v: unknown) => v is T): Writable<T> { |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 8 | let initial = fallback; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 9 | try { |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 10 | const raw = localStorage.getItem(key); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 11 | if (raw !== null) { |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 12 | const parsed: unknown = JSON.parse(raw); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 13 | if (valid(parsed)) initial = parsed; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 14 | } |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 15 | } catch {} |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 16 | const store = writable(initial); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 17 | store.subscribe((value) => { |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 18 | try { |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 19 | localStorage.setItem(key, JSON.stringify(value)); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 20 | } catch {} |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 21 | }); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 22 | return store; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 23 | } |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 24 | |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 25 | const isBool = (v: unknown): v is boolean => typeof v === 'boolean'; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 26 | const isSpacing = (v: unknown): v is number => |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 27 | typeof v === 'number' && Number.isFinite(v) && v >= MESSAGE_SPACING_MIN && v <= MESSAGE_SPACING_MAX; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 28 | |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 29 | export const navFolded = persisted('malice.navFolded', false, isBool); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 30 | export const messageSpacing = persisted('malice.messageSpacing', MESSAGE_SPACING_DEFAULT, isSpacing); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 31 | |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 32 | export const LINE_HEIGHT_MIN = 1.1; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 33 | export const LINE_HEIGHT_MAX = 2; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 34 | export const LINE_HEIGHT_DEFAULT = 1.5; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 35 | |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 36 | const isLineHeight = (v: unknown): v is number => |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 37 | typeof v === 'number' && Number.isFinite(v) && v >= LINE_HEIGHT_MIN && v <= LINE_HEIGHT_MAX; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 38 | |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 39 | export const lineHeight = persisted('malice.lineHeight', LINE_HEIGHT_DEFAULT, isLineHeight); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 40 | |
705b1e1062c2 | atagen | 2026-10-23 09:05 UTC | 41 | const isStringRecord = (v: unknown): v is Record<string, string> => |
705b1e1062c2 | atagen | 2026-10-23 09:05 UTC | 42 | typeof v === 'object' && v !== null && !Array.isArray(v) && Object.values(v).every((x) => typeof x === 'string'); |
705b1e1062c2 | atagen | 2026-10-23 09:05 UTC | 43 | |
705b1e1062c2 | atagen | 2026-10-23 09:05 UTC | 44 | export const lastSpaceRooms = persisted<Record<string, string>>('malice.lastSpaceRooms', {}, isStringRecord); |
705b1e1062c2 | atagen | 2026-10-23 09:05 UTC | 45 | |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 46 | export const THEMES = [ |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 47 | { id: 'system', label: 'Match system' }, |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 48 | { id: 'dark', label: 'Dark' }, |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 49 | { id: 'light', label: 'Light' }, |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 50 | { id: 'black', label: 'Black' }, |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 51 | { id: 'nord', label: 'Nord' } |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 52 | ] as const; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 53 | |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 54 | export type ThemeId = (typeof THEMES)[number]['id']; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 55 | |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 56 | const isTheme = (v: unknown): v is ThemeId => THEMES.some((t) => t.id === v); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 57 | const isAccent = (v: unknown): v is string | null => v === null || (typeof v === 'string' && /^#[0-9a-f]{6}$/i.test(v)); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 58 | |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 59 | export const theme = persisted<ThemeId>('malice.theme', 'dark', isTheme); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 60 | export const accent = persisted<string | null>('malice.accent', null, isAccent); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 61 | |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 62 | if (typeof document !== 'undefined') { |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 63 | const root = document.documentElement; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 64 | const prefersLight = window.matchMedia('(prefers-color-scheme: light)'); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 65 | let current: ThemeId = 'system'; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 66 | const apply = () => { |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 67 | root.dataset.theme = current === 'system' ? (prefersLight.matches ? 'light' : 'dark') : current; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 68 | }; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 69 | theme.subscribe((t) => { |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 70 | current = t; |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 71 | apply(); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 72 | }); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 73 | prefersLight.addEventListener('change', apply); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 74 | accent.subscribe((a) => { |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 75 | if (a) root.style.setProperty('--accent', a); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 76 | else root.style.removeProperty('--accent'); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 77 | }); |
df3c0f2b3da4 | atagen | 2026-10-22 13:07 UTC | 78 | } |