Repository Public
1// Helpers shared by the two anonymous pages. Nothing here knows about the signed-in application.2import { extOf, sanitizeName } from '@spd/shared';34export { BLOCKED_EXT } from '@spd/shared';5import { BLOCKED_EXT } from '@spd/shared';6/** Extension the server will see for a client file name. */7export const serverExt = (name: string): string => extOf(sanitizeName(name));89const lum = (hex: string): number => {10 const c = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255).map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));11 return 0.2126 * c[0]! + 0.7152 * c[1]! + 0.0722 * c[2]!;12};13const ratio = (a: number, b: number) => (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);14/** A valid #rrggbb, or the default accent. */15export const safeColor = (c: string | undefined | null): string => (c && /^#[0-9a-fA-F]{6}$/.test(c) ? c : '#2563eb');16/** Text colour (near-black or white) with the better WCAG contrast on a background colour. */17export function inkOn(bg: string): string {18 const l = lum(safeColor(bg));19 return ratio(l, 1) >= ratio(l, lum('#0f1419')) ? '#ffffff' : '#0f1419';20}21/** CSS variables that make `bg-accent text-accent-ink` use a portal's colour on one element (the theme tokens are resolved at the root, so both layers are set). */22export const accentVars = (color: string): React.CSSProperties => ({ '--accent': safeColor(color), '--accent-ink': inkOn(color), '--color-accent': safeColor(color), '--color-accent-ink': inkOn(color), '--grad-a': safeColor(color), '--grad-b': safeColor(color), '--grad-ink': inkOn(color) }) as React.CSSProperties;2324/** Storage key derived from a link token without containing it. */25export function tokenKey(prefix: string, token: string): string {26 let h1 = 0x811c9dc5, h2 = 0x01000193;27 for (let i = 0; i < token.length; i++) { h1 = Math.imul(h1 ^ token.charCodeAt(i), 0x01000193); h2 = Math.imul(h2 + token.charCodeAt(i), 0x85ebca6b) ^ (h2 >>> 13); }28 return `${prefix}.${(h1 >>> 0).toString(36)}${(h2 >>> 0).toString(36)}`;29}30export const store = {31 get<T>(key: string): T | null { try { const v = sessionStorage.getItem(key); return v ? (JSON.parse(v) as T) : null; } catch { return null; } },32 set(key: string, value: unknown): void { try { sessionStorage.setItem(key, JSON.stringify(value)); } catch { /* private mode: the page works without it */ } },33 del(key: string): void { try { sessionStorage.removeItem(key); } catch { /* idem */ } },34};35export const isTouch = (): boolean => window.matchMedia('(pointer: coarse)').matches;36