atagen

malice

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

Blame: web/src/lib/custom-themes.ts

c122e088beceatagen2026-10-25 02:37 UTC1
export const THEME_COLOURS = [
c122e088beceatagen2026-10-25 02:37 UTC2
  ['bg', 'Background'], ['bg-soft', 'Soft background'], ['panel', 'Panels'],
c122e088beceatagen2026-10-25 02:37 UTC3
  ['panel-2', 'Raised panels'], ['rail-bg', 'Spaces rail'], ['border', 'Borders'],
c122e088beceatagen2026-10-25 02:37 UTC4
  ['text', 'Text'], ['muted', 'Muted text'], ['accent', 'Accent'],
c122e088beceatagen2026-10-25 02:37 UTC5
  ['accent-2', 'Secondary accent'], ['on-accent', 'Text on accent'], ['glow', 'Glow'],
c122e088beceatagen2026-10-25 02:37 UTC6
  ['hover', 'Hover'], ['hover-faint', 'Subtle hover'],
c122e088beceatagen2026-10-25 02:37 UTC7
  ['selection-bg', 'Selection background'], ['selection-text', 'Selected text'],
c122e088beceatagen2026-10-25 02:37 UTC8
  ['ok', 'Success'], ['ok-text', 'Success text'],
c122e088beceatagen2026-10-25 02:37 UTC9
  ['danger', 'Error'], ['danger-bg', 'Error background'],
c122e088beceatagen2026-10-25 02:37 UTC10
  ['danger-border', 'Error border'], ['danger-text', 'Error text'],
c122e088beceatagen2026-10-25 02:37 UTC11
  ['warn', 'Warning'], ['warn-bg', 'Warning background'],
c122e088beceatagen2026-10-25 02:37 UTC12
  ['warn-border', 'Warning border'], ['warn-text', 'Warning text']
c122e088beceatagen2026-10-25 02:37 UTC13
] as const;
c122e088beceatagen2026-10-25 02:37 UTC14
c122e088beceatagen2026-10-25 02:37 UTC15
export type ThemeColour = (typeof THEME_COLOURS)[number][0];
c122e088beceatagen2026-10-25 02:37 UTC16
export type ThemeColours = Record<ThemeColour, string>;
c122e088beceatagen2026-10-25 02:37 UTC17
export interface ThemeDocument {
c122e088beceatagen2026-10-25 02:37 UTC18
  format: 'malice-theme';
c122e088beceatagen2026-10-25 02:37 UTC19
  version: 1;
c122e088beceatagen2026-10-25 02:37 UTC20
  name: string;
c122e088beceatagen2026-10-25 02:37 UTC21
  scheme: 'dark' | 'light';
c122e088beceatagen2026-10-25 02:37 UTC22
  colours: ThemeColours;
c122e088beceatagen2026-10-25 02:37 UTC23
  palette: string[];
c122e088beceatagen2026-10-25 02:37 UTC24
  nameLightness: number;
c122e088beceatagen2026-10-25 02:37 UTC25
}
c122e088beceatagen2026-10-25 02:37 UTC26
export interface CustomTheme extends ThemeDocument {
c122e088beceatagen2026-10-25 02:37 UTC27
  id: `custom:${string}`;
c122e088beceatagen2026-10-25 02:37 UTC28
}
c122e088beceatagen2026-10-25 02:37 UTC29
c122e088beceatagen2026-10-25 02:37 UTC30
export const MALICIOUS_PALETTE = [
c122e088beceatagen2026-10-25 02:37 UTC31
  '#1b2021', '#cecbca', '#272a2a', '#202e2f', '#c43325', '#c46056',
c122e088beceatagen2026-10-25 02:37 UTC32
  '#8cc992', '#c2dab0', '#ffb852', '#ffab5b', '#5299ff', '#92beff',
c122e088beceatagen2026-10-25 02:37 UTC33
  '#645ac9', '#928cc9', '#5abfc9', '#8cc4c9', '#b0c2da', '#caccce'
c122e088beceatagen2026-10-25 02:37 UTC34
];
c122e088beceatagen2026-10-25 02:37 UTC35
c122e088beceatagen2026-10-25 02:37 UTC36
export function isThemeColour(value: unknown): value is `#${string}` {
c122e088beceatagen2026-10-25 02:37 UTC37
  return typeof value === 'string' && /^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/i.test(value);
c122e088beceatagen2026-10-25 02:37 UTC38
}
c122e088beceatagen2026-10-25 02:37 UTC39
c122e088beceatagen2026-10-25 02:37 UTC40
export function isThemeDocument(value: unknown): value is ThemeDocument {
c122e088beceatagen2026-10-25 02:37 UTC41
  if (typeof value !== 'object' || value === null) return false;
c122e088beceatagen2026-10-25 02:37 UTC42
  const doc = value as Record<string, unknown>;
c122e088beceatagen2026-10-25 02:37 UTC43
  return doc.format === 'malice-theme' && doc.version === 1 &&
c122e088beceatagen2026-10-25 02:37 UTC44
    typeof doc.name === 'string' && doc.name.trim().length > 0 && doc.name.length <= 80 &&
c122e088beceatagen2026-10-25 02:37 UTC45
    (doc.scheme === 'dark' || doc.scheme === 'light') &&
c122e088beceatagen2026-10-25 02:37 UTC46
    typeof doc.nameLightness === 'number' && Number.isFinite(doc.nameLightness) &&
c122e088beceatagen2026-10-25 02:37 UTC47
    doc.nameLightness >= 0 && doc.nameLightness <= 100 &&
c122e088beceatagen2026-10-25 02:37 UTC48
    Array.isArray(doc.palette) && doc.palette.length <= 64 && doc.palette.every(isThemeColour) &&
c122e088beceatagen2026-10-25 02:37 UTC49
    typeof doc.colours === 'object' && doc.colours !== null &&
c122e088beceatagen2026-10-25 02:37 UTC50
    THEME_COLOURS.every(([key]) => isThemeColour((doc.colours as Record<string, unknown>)[key]));
c122e088beceatagen2026-10-25 02:37 UTC51
}
c122e088beceatagen2026-10-25 02:37 UTC52
c122e088beceatagen2026-10-25 02:37 UTC53
export function isCustomTheme(value: unknown): value is CustomTheme {
c122e088beceatagen2026-10-25 02:37 UTC54
  return isThemeDocument(value) && 'id' in value &&
c122e088beceatagen2026-10-25 02:37 UTC55
    typeof value.id === 'string' && /^custom:[\w-]{1,80}$/.test(value.id);
c122e088beceatagen2026-10-25 02:37 UTC56
}
c122e088beceatagen2026-10-25 02:37 UTC57
c122e088beceatagen2026-10-25 02:37 UTC58
export function themeDocument(value: ThemeDocument): ThemeDocument {
c122e088beceatagen2026-10-25 02:37 UTC59
  return {
c122e088beceatagen2026-10-25 02:37 UTC60
    format: 'malice-theme', version: 1, name: value.name.trim(), scheme: value.scheme,
c122e088beceatagen2026-10-25 02:37 UTC61
    colours: Object.fromEntries(THEME_COLOURS.map(([key]) => [key, value.colours[key]])) as ThemeColours,
c122e088beceatagen2026-10-25 02:37 UTC62
    palette: [...value.palette], nameLightness: value.nameLightness
c122e088beceatagen2026-10-25 02:37 UTC63
  };
c122e088beceatagen2026-10-25 02:37 UTC64
}
c122e088beceatagen2026-10-25 02:37 UTC65
c122e088beceatagen2026-10-25 02:37 UTC66
export function readThemeFile(contents: string): ThemeDocument {
c122e088beceatagen2026-10-25 02:37 UTC67
  let value: unknown;
c122e088beceatagen2026-10-25 02:37 UTC68
  try {
c122e088beceatagen2026-10-25 02:37 UTC69
    value = JSON.parse(contents);
c122e088beceatagen2026-10-25 02:37 UTC70
  } catch {
c122e088beceatagen2026-10-25 02:37 UTC71
    throw new Error('This file does not contain valid JSON.');
c122e088beceatagen2026-10-25 02:37 UTC72
  }
c122e088beceatagen2026-10-25 02:37 UTC73
  if (!isThemeDocument(value)) throw new Error('Choose a valid Malice theme file with version 1.');
c122e088beceatagen2026-10-25 02:37 UTC74
  return themeDocument(value);
c122e088beceatagen2026-10-25 02:37 UTC75
}
c122e088beceatagen2026-10-25 02:37 UTC76
c122e088beceatagen2026-10-25 02:37 UTC77
function colourHex(value: string): string {
c122e088beceatagen2026-10-25 02:37 UTC78
  if (/^#[0-9a-f]{3,4}$/i.test(value)) return '#' + [...value.slice(1)].map((char) => char + char).join('');
c122e088beceatagen2026-10-25 02:37 UTC79
  if (isThemeColour(value)) return value;
c122e088beceatagen2026-10-25 02:37 UTC80
  const channels = value.match(/[\d.]+/g)?.map(Number);
c122e088beceatagen2026-10-25 02:37 UTC81
  if (!channels || channels.length < 3) throw new Error('Could not read the current theme colours.');
c122e088beceatagen2026-10-25 02:37 UTC82
  const bytes = channels.slice(0, 3);
c122e088beceatagen2026-10-25 02:37 UTC83
  if (channels.length === 4) bytes.push(Math.round(channels[3] * 255));
c122e088beceatagen2026-10-25 02:37 UTC84
  return '#' + bytes.map((byte) => Math.round(byte).toString(16).padStart(2, '0')).join('');
c122e088beceatagen2026-10-25 02:37 UTC85
}
c122e088beceatagen2026-10-25 02:37 UTC86
c122e088beceatagen2026-10-25 02:37 UTC87
export function captureTheme(name: string, palette?: string[]): ThemeDocument {
c122e088beceatagen2026-10-25 02:37 UTC88
  const style = getComputedStyle(document.documentElement);
c122e088beceatagen2026-10-25 02:37 UTC89
  const colours = Object.fromEntries(THEME_COLOURS.map(([key]) => [key, colourHex(style.getPropertyValue(`--${key}`).trim())])) as ThemeColours;
c122e088beceatagen2026-10-25 02:37 UTC90
  return {
c122e088beceatagen2026-10-25 02:37 UTC91
    format: 'malice-theme', version: 1, name,
c122e088beceatagen2026-10-25 02:37 UTC92
    scheme: style.colorScheme === 'light' ? 'light' : 'dark', colours,
c122e088beceatagen2026-10-25 02:37 UTC93
    palette: palette ? [...palette] : [...new Set(Object.values(colours))].slice(0, 64),
c122e088beceatagen2026-10-25 02:37 UTC94
    nameLightness: parseFloat(style.getPropertyValue('--name-lightness'))
c122e088beceatagen2026-10-25 02:37 UTC95
  };
c122e088beceatagen2026-10-25 02:37 UTC96
}
c122e088beceatagen2026-10-25 02:37 UTC97
c122e088beceatagen2026-10-25 02:37 UTC98
export function themeStyle(doc: ThemeDocument): string {
c122e088beceatagen2026-10-25 02:37 UTC99
  return THEME_COLOURS.map(([key]) => `--${key}:${doc.colours[key]}`).join(';') +
c122e088beceatagen2026-10-25 02:37 UTC100
    `;--name-lightness:${doc.nameLightness}%;color-scheme:${doc.scheme}`;
c122e088beceatagen2026-10-25 02:37 UTC101
}