atagen

malice

public
Star0
Watch0
06339d2de37a2adbd7c9697702da5360fad61c36
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/appearance.ts

c122e088beceatagen2026-10-25 02:37 UTC1
import { get, writable, type Writable } from 'svelte/store';
c122e088beceatagen2026-10-25 02:37 UTC2
import { THEME_COLOURS, isCustomTheme, type CustomTheme, type ThemeDocument } from './custom-themes';
df3c0f2b3da4atagen2026-10-22 13:07 UTC3
df3c0f2b3da4atagen2026-10-22 13:07 UTC4
export const MESSAGE_SPACING_MIN = 0;
df3c0f2b3da4atagen2026-10-22 13:07 UTC5
export const MESSAGE_SPACING_MAX = 16;
df3c0f2b3da4atagen2026-10-22 13:07 UTC6
export const MESSAGE_SPACING_DEFAULT = 3;
df3c0f2b3da4atagen2026-10-22 13:07 UTC7
df3c0f2b3da4atagen2026-10-22 13:07 UTC8
function persisted<T>(key: string, fallback: T, valid: (v: unknown) => v is T): Writable<T> {
df3c0f2b3da4atagen2026-10-22 13:07 UTC9
  let initial = fallback;
df3c0f2b3da4atagen2026-10-22 13:07 UTC10
  try {
df3c0f2b3da4atagen2026-10-22 13:07 UTC11
    const raw = localStorage.getItem(key);
df3c0f2b3da4atagen2026-10-22 13:07 UTC12
    if (raw !== null) {
df3c0f2b3da4atagen2026-10-22 13:07 UTC13
      const parsed: unknown = JSON.parse(raw);
df3c0f2b3da4atagen2026-10-22 13:07 UTC14
      if (valid(parsed)) initial = parsed;
df3c0f2b3da4atagen2026-10-22 13:07 UTC15
    }
df3c0f2b3da4atagen2026-10-22 13:07 UTC16
  } catch {}
df3c0f2b3da4atagen2026-10-22 13:07 UTC17
  const store = writable(initial);
df3c0f2b3da4atagen2026-10-22 13:07 UTC18
  store.subscribe((value) => {
df3c0f2b3da4atagen2026-10-22 13:07 UTC19
    try {
df3c0f2b3da4atagen2026-10-22 13:07 UTC20
      localStorage.setItem(key, JSON.stringify(value));
df3c0f2b3da4atagen2026-10-22 13:07 UTC21
    } catch {}
df3c0f2b3da4atagen2026-10-22 13:07 UTC22
  });
df3c0f2b3da4atagen2026-10-22 13:07 UTC23
  return store;
df3c0f2b3da4atagen2026-10-22 13:07 UTC24
}
df3c0f2b3da4atagen2026-10-22 13:07 UTC25
df3c0f2b3da4atagen2026-10-22 13:07 UTC26
const isBool = (v: unknown): v is boolean => typeof v === 'boolean';
df3c0f2b3da4atagen2026-10-22 13:07 UTC27
const isSpacing = (v: unknown): v is number =>
df3c0f2b3da4atagen2026-10-22 13:07 UTC28
  typeof v === 'number' && Number.isFinite(v) && v >= MESSAGE_SPACING_MIN && v <= MESSAGE_SPACING_MAX;
df3c0f2b3da4atagen2026-10-22 13:07 UTC29
df3c0f2b3da4atagen2026-10-22 13:07 UTC30
export const navFolded = persisted('malice.navFolded', false, isBool);
e15025643a20atagen2026-10-24 06:51 UTC31
export const railFolded = persisted('malice.railFolded', false, isBool);
05a48e9e7942atagen2026-10-24 09:32 UTC32
05a48e9e7942atagen2026-10-24 09:32 UTC33
export const SIDE_WIDTH = { min: 200, max: 480, default: 260 };
05a48e9e7942atagen2026-10-24 09:32 UTC34
export const MEMBERS_WIDTH = { min: 200, max: 420, default: 240 };
7ebc2472a9e3atagen2026-10-25 09:53 UTC35
export const THREAD_WIDTH = { min: 300, max: 720, default: 400 };
05a48e9e7942atagen2026-10-24 09:32 UTC36
const widthIn = (range: { min: number; max: number }) => (v: unknown): v is number =>
05a48e9e7942atagen2026-10-24 09:32 UTC37
  typeof v === 'number' && Number.isFinite(v) && v >= range.min && v <= range.max;
05a48e9e7942atagen2026-10-24 09:32 UTC38
export const sideWidth = persisted('malice.sideWidth', SIDE_WIDTH.default, widthIn(SIDE_WIDTH));
05a48e9e7942atagen2026-10-24 09:32 UTC39
export const membersWidth = persisted('malice.membersWidth', MEMBERS_WIDTH.default, widthIn(MEMBERS_WIDTH));
7ebc2472a9e3atagen2026-10-25 09:53 UTC40
export const threadWidth = persisted('malice.threadWidth', THREAD_WIDTH.default, widthIn(THREAD_WIDTH));
df3c0f2b3da4atagen2026-10-22 13:07 UTC41
export const messageSpacing = persisted('malice.messageSpacing', MESSAGE_SPACING_DEFAULT, isSpacing);
df3c0f2b3da4atagen2026-10-22 13:07 UTC42
df3c0f2b3da4atagen2026-10-22 13:07 UTC43
export const LINE_HEIGHT_MIN = 1.1;
df3c0f2b3da4atagen2026-10-22 13:07 UTC44
export const LINE_HEIGHT_MAX = 2;
df3c0f2b3da4atagen2026-10-22 13:07 UTC45
export const LINE_HEIGHT_DEFAULT = 1.5;
df3c0f2b3da4atagen2026-10-22 13:07 UTC46
df3c0f2b3da4atagen2026-10-22 13:07 UTC47
const isLineHeight = (v: unknown): v is number =>
df3c0f2b3da4atagen2026-10-22 13:07 UTC48
  typeof v === 'number' && Number.isFinite(v) && v >= LINE_HEIGHT_MIN && v <= LINE_HEIGHT_MAX;
df3c0f2b3da4atagen2026-10-22 13:07 UTC49
df3c0f2b3da4atagen2026-10-22 13:07 UTC50
export const lineHeight = persisted('malice.lineHeight', LINE_HEIGHT_DEFAULT, isLineHeight);
df3c0f2b3da4atagen2026-10-22 13:07 UTC51
705b1e1062c2atagen2026-10-23 09:05 UTC52
const isStringRecord = (v: unknown): v is Record<string, string> =>
705b1e1062c2atagen2026-10-23 09:05 UTC53
  typeof v === 'object' && v !== null && !Array.isArray(v) && Object.values(v).every((x) => typeof x === 'string');
705b1e1062c2atagen2026-10-23 09:05 UTC54
705b1e1062c2atagen2026-10-23 09:05 UTC55
export const lastSpaceRooms = persisted<Record<string, string>>('malice.lastSpaceRooms', {}, isStringRecord);
705b1e1062c2atagen2026-10-23 09:05 UTC56
df3c0f2b3da4atagen2026-10-22 13:07 UTC57
export const THEMES = [
df3c0f2b3da4atagen2026-10-22 13:07 UTC58
  { id: 'system', label: 'Match system' },
df3c0f2b3da4atagen2026-10-22 13:07 UTC59
  { id: 'dark', label: 'Dark' },
df3c0f2b3da4atagen2026-10-22 13:07 UTC60
  { id: 'light', label: 'Light' },
df3c0f2b3da4atagen2026-10-22 13:07 UTC61
  { id: 'black', label: 'Black' },
c122e088beceatagen2026-10-25 02:37 UTC62
  { id: 'nord', label: 'Nord' },
c122e088beceatagen2026-10-25 02:37 UTC63
  { id: 'malicious', label: 'Malicious' }
df3c0f2b3da4atagen2026-10-22 13:07 UTC64
] as const;
df3c0f2b3da4atagen2026-10-22 13:07 UTC65
c122e088beceatagen2026-10-25 02:37 UTC66
export type ThemeId = (typeof THEMES)[number]['id'] | CustomTheme['id'];
df3c0f2b3da4atagen2026-10-22 13:07 UTC67
c122e088beceatagen2026-10-25 02:37 UTC68
const isCustomThemeList = (v: unknown): v is CustomTheme[] => Array.isArray(v) && v.every(isCustomTheme);
c122e088beceatagen2026-10-25 02:37 UTC69
export const customThemes = persisted<CustomTheme[]>('malice.customThemes', [], isCustomThemeList);
c122e088beceatagen2026-10-25 02:37 UTC70
export const themePreview = writable<ThemeDocument | null>(null);
c122e088beceatagen2026-10-25 02:37 UTC71
c122e088beceatagen2026-10-25 02:37 UTC72
const isTheme = (v: unknown): v is ThemeId => THEMES.some((t) => t.id === v) || get(customThemes).some((t) => t.id === v);
df3c0f2b3da4atagen2026-10-22 13:07 UTC73
const isAccent = (v: unknown): v is string | null => v === null || (typeof v === 'string' && /^#[0-9a-f]{6}$/i.test(v));
df3c0f2b3da4atagen2026-10-22 13:07 UTC74
df3c0f2b3da4atagen2026-10-22 13:07 UTC75
export const theme = persisted<ThemeId>('malice.theme', 'dark', isTheme);
df3c0f2b3da4atagen2026-10-22 13:07 UTC76
export const accent = persisted<string | null>('malice.accent', null, isAccent);
df3c0f2b3da4atagen2026-10-22 13:07 UTC77
df3c0f2b3da4atagen2026-10-22 13:07 UTC78
if (typeof document !== 'undefined') {
df3c0f2b3da4atagen2026-10-22 13:07 UTC79
  const root = document.documentElement;
df3c0f2b3da4atagen2026-10-22 13:07 UTC80
  const prefersLight = window.matchMedia('(prefers-color-scheme: light)');
d8772d4eb408atagen2026-10-26 04:43 UTC81
  const themeColor = document.querySelector('meta[name="theme-color"]');
df3c0f2b3da4atagen2026-10-22 13:07 UTC82
  const apply = () => {
c122e088beceatagen2026-10-25 02:37 UTC83
    const current = get(theme);
c122e088beceatagen2026-10-25 02:37 UTC84
    const custom = get(themePreview) ?? get(customThemes).find((t) => t.id === current);
c122e088beceatagen2026-10-25 02:37 UTC85
    const base = current === 'system' ? (prefersLight.matches ? 'light' : 'dark') : current;
c122e088beceatagen2026-10-25 02:37 UTC86
    root.dataset.theme = custom?.scheme ?? (current.startsWith('custom:') ? 'dark' : base);
c122e088beceatagen2026-10-25 02:37 UTC87
    for (const [key] of THEME_COLOURS) {
c122e088beceatagen2026-10-25 02:37 UTC88
      if (custom) root.style.setProperty(`--${key}`, custom.colours[key]);
c122e088beceatagen2026-10-25 02:37 UTC89
      else root.style.removeProperty(`--${key}`);
c122e088beceatagen2026-10-25 02:37 UTC90
    }
c122e088beceatagen2026-10-25 02:37 UTC91
    if (custom) {
c122e088beceatagen2026-10-25 02:37 UTC92
      root.style.setProperty('--name-lightness', `${custom.nameLightness}%`);
c122e088beceatagen2026-10-25 02:37 UTC93
      root.style.colorScheme = custom.scheme;
c122e088beceatagen2026-10-25 02:37 UTC94
    } else {
c122e088beceatagen2026-10-25 02:37 UTC95
      root.style.removeProperty('--name-lightness');
c122e088beceatagen2026-10-25 02:37 UTC96
      root.style.removeProperty('color-scheme');
c122e088beceatagen2026-10-25 02:37 UTC97
    }
c122e088beceatagen2026-10-25 02:37 UTC98
    const override = get(accent);
c122e088beceatagen2026-10-25 02:37 UTC99
    if (override && !get(themePreview)) root.style.setProperty('--accent', override);
d8772d4eb408atagen2026-10-26 04:43 UTC100
    const bar = getComputedStyle(root).getPropertyValue('--rail-bg').trim();
d8772d4eb408atagen2026-10-26 04:43 UTC101
    if (bar) themeColor?.setAttribute('content', bar);
df3c0f2b3da4atagen2026-10-22 13:07 UTC102
  };
c122e088beceatagen2026-10-25 02:37 UTC103
  theme.subscribe(apply);
c122e088beceatagen2026-10-25 02:37 UTC104
  customThemes.subscribe(apply);
c122e088beceatagen2026-10-25 02:37 UTC105
  themePreview.subscribe(apply);
df3c0f2b3da4atagen2026-10-22 13:07 UTC106
  prefersLight.addEventListener('change', apply);
c122e088beceatagen2026-10-25 02:37 UTC107
  accent.subscribe(apply);
df3c0f2b3da4atagen2026-10-22 13:07 UTC108
}