Site-agnostic parts of the X app, behind a small site adapter: - crates/waybetter-host: webview-label security policy and tab rect clamp, account profiles with native-owned accounts (AccountRules), plugin store with bundled-plugin upgrades (PluginHost), signed data-only hotfix channel, update scheduling and crash handshake, tab payloads/link routing/tab-chrome validation, and (feature "tauri") webview helpers and the Windows system menu. - packages/runtime: DOM hub, request hub, plugin API core, OKLab theme color model, split-view rail hiding, link routing and account reporting, each configured by a SiteAdapter. - packages/shell: address bar, pane, link-drop, profile, palette and update-chip models, and window controls with the Tauri API injected. Tests moved/adapted from the app: 45 Rust (headless), 40 TypeScript. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
52 lines
2.9 KiB
TypeScript
52 lines
2.9 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { axisPosition, buildColorModel, deriveColors, oklabToRgb, projectColor, rgbToOklab, rolePositions, BASE_STEPS, type SourceAxis } from '../src/themeTokens.ts';
|
|
|
|
// A made-up dark site palette: every role lists its colors as `r,g,b`, the first is canonical.
|
|
const SOURCE: SourceAxis = {
|
|
bg: ['10,10,12'],
|
|
surface: ['20,21,24'],
|
|
elevated: ['32,33,38'],
|
|
border: ['50,52,58'],
|
|
muted: ['140,144,150'],
|
|
text: ['235,236,238'],
|
|
accent: ['83,252,24'],
|
|
};
|
|
const THEME = { bg: '#fdf6e3', surface: '#f5eedb', elevated: '#eee8d5', border: '#d9d2bf', muted: '#657b83', text: '#073642', accent: '#268bd2' };
|
|
const rgb = (key: string) => key.split(',').map(Number) as [number, number, number];
|
|
const hex = (value: number[]) => `#${value.map(v => v.toString(16).padStart(2, '0')).join('')}`;
|
|
|
|
test('OKLab round trip is exact to one step', () => {
|
|
for (const color of [[0, 0, 0], [255, 255, 255], [29, 155, 240], [22, 24, 28], [83, 100, 113]] as [number, number, number][]) {
|
|
const back = oklabToRgb(rgbToOklab(color));
|
|
color.forEach((v, i) => assert.ok(Math.abs(v - back[i]!) <= 1, `${color} -> ${back}`));
|
|
}
|
|
});
|
|
|
|
test('listed site colors become exactly the theme role, per usage', () => {
|
|
const background = buildColorModel(SOURCE, THEME, 'background')!;
|
|
assert.equal(hex(projectColor(background, rgb(SOURCE.bg[0]!))!), THEME.bg);
|
|
assert.equal(hex(projectColor(background, rgb(SOURCE.surface[0]!))!), THEME.surface);
|
|
const text = buildColorModel(SOURCE, THEME, 'text')!;
|
|
assert.equal(hex(projectColor(text, rgb(SOURCE.text[0]!))!), THEME.text);
|
|
assert.equal(hex(projectColor(text, rgb(SOURCE.accent[0]!))!), THEME.accent, 'accent text is exactly the theme accent');
|
|
});
|
|
|
|
test('unlisted neutrals land on the theme curve in order; chromatic non-accent colors stay', () => {
|
|
const model = buildColorModel(SOURCE, THEME, 'background')!;
|
|
const steps = [15, 60, 120, 200].map(v => projectColor(model, [v, v, v + 2])!);
|
|
const lightness = steps.map(step => rgbToOklab(step)[0]);
|
|
for (let i = 1; i < lightness.length; i++) assert.ok(lightness[i]! < lightness[i - 1]!, 'light theme: darker as the site gets lighter');
|
|
assert.equal(projectColor(model, [249, 24, 128]), null, 'a pink like-heart is left alone');
|
|
assert.equal(axisPosition(model, [249, 24, 128]), null);
|
|
assert.equal(buildColorModel(SOURCE, { accent: '#ffffff' }), null, 'a theme needs a background and text');
|
|
});
|
|
|
|
test('derived roles follow the base structure', () => {
|
|
const dark = deriveColors({ bg: '#15202b', text: '#e7e9ea', accent: '#1d9bf0' });
|
|
const positions = rolePositions(dark);
|
|
for (const role of ['surface', 'elevated', 'border', 'muted'] as const) {
|
|
assert.ok(Math.abs(positions[role] - BASE_STEPS.dark[role]) < 0.03, `${role}: ${positions[role]}`);
|
|
}
|
|
assert.equal(deriveColors({ bg: '#ffffff', text: '#000000', accent: '#000000', border: '#123456' }).border, '#123456');
|
|
});
|