@ultimat3/ui 19.4.0 → 20.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ // The palette's accessibility contract as DATA: every foreground/background pairing a shipped
2
+ // component actually renders, with the WCAG 2.2 AA floor it has to clear.
3
+ //
4
+ // In source rather than in `contrast.test.ts`, where it lived until 2026-09, because two things
5
+ // read it and only one of them is a test. `defineTheme()` measures a brand override against this
6
+ // exact list, so an app cannot ship a palette that fails the bar the framework's own palette is
7
+ // held to — that is the difference between an accessible design system and a design system with an
8
+ // accessible default.
9
+ //
10
+ // WCAG 2.2 AA, deliberately, and not APCA: APCA is not a standard, and AA is the operative legal
11
+ // benchmark (EN 301 549, ADA Title II, the EAA). 4.5:1 for body text (1.4.3), 3:1 for large text
12
+ // and non-text UI (1.4.11).
13
+
14
+ import { AA_LARGE, AA_TEXT } from './contrast';
15
+ import type { ColorRole } from './tokens';
16
+
17
+ /** What a page, a card and a raised panel are made of. */
18
+ const SURFACES: readonly ColorRole[] = ['bg', 'bg-soft', 'surface', 'surface-raised'];
19
+
20
+ const STATUS = ['success', 'warning', 'danger', 'info'] as const;
21
+
22
+ /**
23
+ * The framework's own floor for a 1px edge, not a WCAG level: below it a border is a rumour, and
24
+ * `line` on `surface-raised` in dark measured 1.16 — an input with no visible outline.
25
+ */
26
+ export const VISIBLE_EDGE = 1.4;
27
+
28
+ export interface ContrastPair {
29
+ readonly fg: ColorRole;
30
+ readonly bg: ColorRole;
31
+ readonly minimum: number;
32
+ /** What the pairing IS on screen. A refusal that names only two role names explains nothing. */
33
+ readonly what: string;
34
+ }
35
+
36
+ const pair = (fg: ColorRole, bg: ColorRole, minimum: number, what: string): ContrastPair => ({
37
+ fg,
38
+ bg,
39
+ minimum,
40
+ what,
41
+ });
42
+
43
+ const onSurfaces = (fg: ColorRole, minimum: number, what: string): ContrastPair[] =>
44
+ SURFACES.map((bg) => pair(fg, bg, minimum, what));
45
+
46
+ /**
47
+ * Every pairing, both directions of the palette. Order is stable so a refusal names the same pair
48
+ * on every run and a diff of this file is a diff of the contract.
49
+ */
50
+ export const CONTRAST_PAIRS: readonly ContrastPair[] = [
51
+ ...(['fg', 'fg-strong', 'fg-muted'] as const).flatMap((fg) =>
52
+ onSurfaces(fg, AA_TEXT, 'body, heading and caption text on a surface'),
53
+ ),
54
+ ...(['accent', 'accent-strong'] as const).flatMap((fg) =>
55
+ onSurfaces(fg, AA_TEXT, 'link and accent text on a surface'),
56
+ ),
57
+ pair('accent-fg', 'accent', AA_TEXT, 'the label on a primary Button'),
58
+ pair('accent-fg', 'accent-strong', AA_TEXT, 'the label on a hovered primary Button'),
59
+ ...STATUS.map((status) =>
60
+ pair(`${status}-fg`, status, AA_TEXT, `the label on a solid ${status} Badge`),
61
+ ),
62
+ ...STATUS.map((status) =>
63
+ pair(status, `${status}-soft`, AA_TEXT, `${status} text on its own soft tint`),
64
+ ),
65
+ ...STATUS.map((status) =>
66
+ pair('fg-muted', `${status}-soft`, AA_TEXT, `Alert body copy on the ${status} tint`),
67
+ ),
68
+ ...STATUS.flatMap((status) => onSurfaces(status, AA_TEXT, 'status text on a surface')),
69
+ ...onSurfaces('accent', AA_LARGE, 'the focus ring against a surface'),
70
+ ...onSurfaces('line', VISIBLE_EDGE, 'a border or divider against a surface'),
71
+ ];