import { RAW, STATUS } from '../../../packages/ui/src/theme-raw' import { D3RO_NATIVE_RAW, D3RO_NATIVE_STATUS, getD3roNativePalette, } from '@d3ro/ui-native' type Rgb = readonly [number, number, number] function rgb(hex: string): Rgb { const normalized = hex.replace('#', '') if (!/^[0-9a-f]{6}$/i.test(normalized)) throw new Error(`Expected 6-digit hex, got ${hex}`) return [0, 2, 4].map((offset) => Number.parseInt(normalized.slice(offset, offset + 2), 16)) as unknown as Rgb } function luminance(hex: string): number { const channels = rgb(hex).map((value) => { const channel = value / 255 return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4 }) return (0.2126 * channels[0]!) + (0.7152 * channels[1]!) + (0.0722 * channels[2]!) } function contrast(foreground: string, background: string): number { const [lighter, darker] = [luminance(foreground), luminance(background)].sort((a, b) => b - a) return (lighter! + 0.05) / (darker! + 0.05) } describe('mobile v4 theme contract', () => { test.each(['dark', 'light'] as const)('%s raw tokens stay in desktop SSOT parity', (mode) => { for (const group of ['bg', 'text', 'border', 'accent'] as const) { for (const [key, value] of Object.entries(D3RO_NATIVE_RAW[mode][group])) { expect(RAW[mode][group][key as keyof typeof RAW[typeof mode][typeof group]]).toBe(value) } } expect(D3RO_NATIVE_STATUS[mode]).toEqual(STATUS[mode]) }) test.each(['dark', 'light'] as const)('%s readable roles meet 4.5:1 contrast', (mode) => { const palette = getD3roNativePalette(mode) for (const background of [palette.bg.app, palette.bg.card, palette.bg.elevated, palette.bg.sidebar]) { for (const foreground of [palette.text.primary, palette.text.secondary, palette.text.tertiary]) { expect(contrast(foreground, background)).toBeGreaterThanOrEqual(4.5) } } expect(contrast(palette.accent.ink, palette.accent.solid)).toBeGreaterThanOrEqual(4.5) }) })