@handamade/psi-tokens 0.26.0 → 0.27.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.
package/dist/acme.css CHANGED
@@ -40,6 +40,7 @@
40
40
  --psi-border-faint: oklch(from var(--psi-fg-primary) l c h / 0.08);
41
41
  --psi-border-neutral: oklch(from var(--psi-fg-primary) l c h / 0.15);
42
42
  --psi-border-strong: oklch(from var(--psi-fg-primary) l c h / 0.3);
43
+ --psi-border-control: oklch(from var(--psi-fg-primary) l c h / 0.55);
43
44
  --psi-border-focus: var(--psi-fg-accent);
44
45
  --psi-scrim-soft: oklch(from var(--psi-bg-primary) l c h / 0.25);
45
46
  --psi-scrim-medium: oklch(from var(--psi-bg-primary) l c h / 0.35);
@@ -2,9 +2,11 @@
2
2
  export const checkboxVars = {
3
3
  fg: "var(--psi-fg-primary)",
4
4
  "box-bg": "var(--psi-bg-primary)",
5
- "box-border": "var(--psi-border-neutral)",
5
+ "box-border": "var(--psi-border-control)",
6
6
  "box-bg-checked": "var(--psi-fill-accent)",
7
7
  "box-border-checked": "var(--psi-fill-accent)",
8
+ // D108: an unchecked, aria-invalid box, in the error colour Input uses.
9
+ "box-border-error": "var(--psi-fg-danger)",
8
10
  // D56: tracks a sharper theme down, never rounder than radius-4 — the box
9
11
  // is ~16px, so the 8px control default would read as a circle.
10
12
  "box-radius": "min(var(--psi-control-radius), var(--psi-radius-4))",
@@ -2,9 +2,10 @@
2
2
  :where(:root, [data-psi-theme]) {
3
3
  --psi-checkbox-fg: var(--psi-fg-primary);
4
4
  --psi-checkbox-box-bg: var(--psi-bg-primary);
5
- --psi-checkbox-box-border: var(--psi-border-neutral);
5
+ --psi-checkbox-box-border: var(--psi-border-control);
6
6
  --psi-checkbox-box-bg-checked: var(--psi-fill-accent);
7
7
  --psi-checkbox-box-border-checked: var(--psi-fill-accent);
8
+ --psi-checkbox-box-border-error: var(--psi-fg-danger);
8
9
  --psi-checkbox-box-radius: min(var(--psi-control-radius), var(--psi-radius-4));
9
10
  --psi-checkbox-check-fg: var(--psi-fg-on-accent);
10
11
  --psi-checkbox-focus-ring: var(--psi-border-focus);
@@ -5,9 +5,11 @@
5
5
  export const radioGroupVars = {
6
6
  fg: "var(--psi-fg-primary)",
7
7
  "circle-bg": "var(--psi-bg-primary)",
8
- "circle-border": "var(--psi-border-neutral)",
8
+ "circle-border": "var(--psi-border-control)",
9
9
  "circle-bg-checked": "var(--psi-fill-accent)",
10
10
  "circle-border-checked": "var(--psi-fill-accent)",
11
+ // D108: an unchecked, aria-invalid circle, in the error colour Input uses.
12
+ "circle-border-error": "var(--psi-fg-danger)",
11
13
  "dot-fg": "var(--psi-fg-on-accent)",
12
14
  "focus-ring": "var(--psi-border-focus)",
13
15
  };
@@ -2,9 +2,10 @@
2
2
  :where(:root, [data-psi-theme]) {
3
3
  --psi-radio-group-fg: var(--psi-fg-primary);
4
4
  --psi-radio-group-circle-bg: var(--psi-bg-primary);
5
- --psi-radio-group-circle-border: var(--psi-border-neutral);
5
+ --psi-radio-group-circle-border: var(--psi-border-control);
6
6
  --psi-radio-group-circle-bg-checked: var(--psi-fill-accent);
7
7
  --psi-radio-group-circle-border-checked: var(--psi-fill-accent);
8
+ --psi-radio-group-circle-border-error: var(--psi-fg-danger);
8
9
  --psi-radio-group-dot-fg: var(--psi-fg-on-accent);
9
10
  --psi-radio-group-focus-ring: var(--psi-border-focus);
10
11
  }
@@ -15,6 +15,7 @@ import { switchVars } from "./switch.js";
15
15
  import { tableVars } from "./table.js";
16
16
  import { tagVars } from "./tag.js";
17
17
  import { toolbarVars } from "./toolbar.js";
18
+ import { textVars } from "./text.js";
18
19
  import { tooltipVars } from "./tooltip.js";
19
20
  import { cardVars } from "./card.js";
20
21
  import { controlVars } from "./control.js";
@@ -57,6 +58,7 @@ export const componentVars = {
57
58
  tag: tagVars,
58
59
  textarea: textareaVars,
59
60
  toolbar: toolbarVars,
61
+ text: textVars,
60
62
  toast: toastVars,
61
63
  tooltip: tooltipVars,
62
64
  };
@@ -0,0 +1,6 @@
1
+ /** Text component tokens (--psi-text-fg*) — D109. Pure indirection onto the
2
+ * semantic layer. fgPrimary and fgSecondary are gated at 4.5 on every
3
+ * surface in wcagAAPairs, so no new contrast pair. The prefix shares
4
+ * `--psi-text-` with the type styles (`--psi-text-14-20-regular`); no type
5
+ * style is named `fg`, so the two cannot collide. */
6
+ export declare const textVars: Record<string, string>;
@@ -0,0 +1,9 @@
1
+ /** Text component tokens (--psi-text-fg*) — D109. Pure indirection onto the
2
+ * semantic layer. fgPrimary and fgSecondary are gated at 4.5 on every
3
+ * surface in wcagAAPairs, so no new contrast pair. The prefix shares
4
+ * `--psi-text-` with the type styles (`--psi-text-14-20-regular`); no type
5
+ * style is named `fg`, so the two cannot collide. */
6
+ export const textVars = {
7
+ fg: "var(--psi-fg-primary)",
8
+ "fg-secondary": "var(--psi-fg-secondary)",
9
+ };
@@ -0,0 +1,6 @@
1
+ @layer psi.components {
2
+ :where(:root, [data-psi-theme]) {
3
+ --psi-text-fg: var(--psi-fg-primary);
4
+ --psi-text-fg-secondary: var(--psi-fg-secondary);
5
+ }
6
+ }
@@ -100,9 +100,10 @@
100
100
  :where(:root, [data-psi-theme]) {
101
101
  --psi-checkbox-fg: var(--psi-fg-primary);
102
102
  --psi-checkbox-box-bg: var(--psi-bg-primary);
103
- --psi-checkbox-box-border: var(--psi-border-neutral);
103
+ --psi-checkbox-box-border: var(--psi-border-control);
104
104
  --psi-checkbox-box-bg-checked: var(--psi-fill-accent);
105
105
  --psi-checkbox-box-border-checked: var(--psi-fill-accent);
106
+ --psi-checkbox-box-border-error: var(--psi-fg-danger);
106
107
  --psi-checkbox-box-radius: min(var(--psi-control-radius), var(--psi-radius-4));
107
108
  --psi-checkbox-check-fg: var(--psi-fg-on-accent);
108
109
  --psi-checkbox-focus-ring: var(--psi-border-focus);
@@ -261,9 +262,10 @@
261
262
  :where(:root, [data-psi-theme]) {
262
263
  --psi-radio-group-fg: var(--psi-fg-primary);
263
264
  --psi-radio-group-circle-bg: var(--psi-bg-primary);
264
- --psi-radio-group-circle-border: var(--psi-border-neutral);
265
+ --psi-radio-group-circle-border: var(--psi-border-control);
265
266
  --psi-radio-group-circle-bg-checked: var(--psi-fill-accent);
266
267
  --psi-radio-group-circle-border-checked: var(--psi-fill-accent);
268
+ --psi-radio-group-circle-border-error: var(--psi-fg-danger);
267
269
  --psi-radio-group-dot-fg: var(--psi-fg-on-accent);
268
270
  --psi-radio-group-focus-ring: var(--psi-border-focus);
269
271
  }
@@ -446,6 +448,13 @@
446
448
  }
447
449
  }
448
450
 
451
+ @layer psi.components {
452
+ :where(:root, [data-psi-theme]) {
453
+ --psi-text-fg: var(--psi-fg-primary);
454
+ --psi-text-fg-secondary: var(--psi-fg-secondary);
455
+ }
456
+ }
457
+
449
458
  @layer psi.components {
450
459
  :where(:root, [data-psi-theme]) {
451
460
  --psi-toast-bg: var(--psi-surface-bg);
@@ -32,3 +32,8 @@ export declare const wcagAAPairs: ContrastPair[];
32
32
  * button/tag label on its variant background ≥ 4.5"). Tint-variant labels are
33
33
  * already covered by the fg-on-tint pairs in wcagAAPairs. */
34
34
  export declare const componentLabelPairs: ContrastPair[];
35
+ /** WCAG 1.4.11 non-text contrast (D106): 3:1 for the visual information that
36
+ * identifies a control. An unchecked checkbox or radio is identified by its
37
+ * outline alone, against its own fill (bgPrimary) and the surface it sits on
38
+ * (bgPrimary on the canvas, bgSecondary on Panel, Dialog and Menu). */
39
+ export declare const nonTextPairs: ContrastPair[];
@@ -136,3 +136,11 @@ export const componentLabelPairs = [
136
136
  { fg: "fgStaticBlack", bg: "fillWarning", minRatio: 4.5 }, // Tag warning (D22)
137
137
  { fg: "fgOnInverted", bg: "bgInverted", minRatio: 4.5 }, // Tooltip label (D46 inversion pair)
138
138
  ];
139
+ /** WCAG 1.4.11 non-text contrast (D106): 3:1 for the visual information that
140
+ * identifies a control. An unchecked checkbox or radio is identified by its
141
+ * outline alone, against its own fill (bgPrimary) and the surface it sits on
142
+ * (bgPrimary on the canvas, bgSecondary on Panel, Dialog and Menu). */
143
+ export const nonTextPairs = [
144
+ { fg: "borderControl", bg: "bgPrimary", minRatio: 3 },
145
+ { fg: "borderControl", bg: "bgSecondary", minRatio: 3 },
146
+ ];
package/dist/dark.css CHANGED
@@ -40,6 +40,7 @@
40
40
  --psi-border-faint: oklch(from var(--psi-fg-primary) l c h / 0.08);
41
41
  --psi-border-neutral: oklch(from var(--psi-fg-primary) l c h / 0.15);
42
42
  --psi-border-strong: oklch(from var(--psi-fg-primary) l c h / 0.3);
43
+ --psi-border-control: oklch(from var(--psi-fg-primary) l c h / 0.4);
43
44
  --psi-border-focus: var(--psi-fg-accent);
44
45
  --psi-scrim-soft: oklch(from var(--psi-bg-primary) l c h / 0.25);
45
46
  --psi-scrim-medium: oklch(from var(--psi-bg-primary) l c h / 0.35);
@@ -420,6 +420,18 @@
420
420
  }
421
421
  }
422
422
  },
423
+ "control": {
424
+ "$type": "color",
425
+ "$value": "#3c28258c",
426
+ "$description": "ref(fgPrimary) → alpha(0.55)",
427
+ "$extensions": {
428
+ "psi": {
429
+ "scopes": [
430
+ "border"
431
+ ]
432
+ }
433
+ }
434
+ },
423
435
  "focus": {
424
436
  "$type": "color",
425
437
  "$value": "#b10b00",
@@ -420,6 +420,18 @@
420
420
  }
421
421
  }
422
422
  },
423
+ "control": {
424
+ "$type": "color",
425
+ "$value": "#e3e8ee66",
426
+ "$description": "ref(fgPrimary) → alpha(0.4)",
427
+ "$extensions": {
428
+ "psi": {
429
+ "scopes": [
430
+ "border"
431
+ ]
432
+ }
433
+ }
434
+ },
423
435
  "focus": {
424
436
  "$type": "color",
425
437
  "$value": "#7daeff",
@@ -420,6 +420,18 @@
420
420
  }
421
421
  }
422
422
  },
423
+ "control": {
424
+ "$type": "color",
425
+ "$value": "#f3ede666",
426
+ "$description": "ref(fgPrimary) → alpha(0.4)",
427
+ "$extensions": {
428
+ "psi": {
429
+ "scopes": [
430
+ "border"
431
+ ]
432
+ }
433
+ }
434
+ },
423
435
  "focus": {
424
436
  "$type": "color",
425
437
  "$value": "#ff885d",
@@ -420,6 +420,18 @@
420
420
  }
421
421
  }
422
422
  },
423
+ "control": {
424
+ "$type": "color",
425
+ "$value": "#222f3c8c",
426
+ "$description": "ref(fgPrimary) → alpha(0.55)",
427
+ "$extensions": {
428
+ "psi": {
429
+ "scopes": [
430
+ "border"
431
+ ]
432
+ }
433
+ }
434
+ },
423
435
  "focus": {
424
436
  "$type": "color",
425
437
  "$value": "#0053c9",
package/dist/ember.css CHANGED
@@ -40,6 +40,7 @@
40
40
  --psi-border-faint: oklch(from var(--psi-fg-primary) l c h / 0.08);
41
41
  --psi-border-neutral: oklch(from var(--psi-fg-primary) l c h / 0.15);
42
42
  --psi-border-strong: oklch(from var(--psi-fg-primary) l c h / 0.3);
43
+ --psi-border-control: oklch(from var(--psi-fg-primary) l c h / 0.4);
43
44
  --psi-border-focus: var(--psi-fg-accent);
44
45
  --psi-scrim-soft: oklch(from var(--psi-bg-primary) l c h / 0.25);
45
46
  --psi-scrim-medium: oklch(from var(--psi-bg-primary) l c h / 0.35);
@@ -1,4 +1,4 @@
1
- import { checkContrast, componentLabelPairs, wcagAAPairs } from "../contrast-matrix.js";
1
+ import { checkContrast, componentLabelPairs, nonTextPairs, wcagAAPairs } from "../contrast-matrix.js";
2
2
  import { resolve } from "../dsl/resolver.js";
3
3
  import { cap, set, slot, token } from "../dsl/builders.js";
4
4
  import { assembleCustomerTheme, } from "../themes/customers/index.js";
@@ -25,7 +25,10 @@ function withAlpha(hex, alpha) {
25
25
  }
26
26
  /**
27
27
  * The gate, and it must be the SAME set `scripts/build.ts` runs over every
28
- * committed theme: `[...wcagAAPairs, ...componentLabelPairs]`, 33 pairs.
28
+ * committed theme: `[...wcagAAPairs, ...componentLabelPairs, ...nonTextPairs]`,
29
+ * 38 pairs (this said 33 until D106; D62 and D86 had grown it). The two
30
+ * non-text pairs (D106) are not solved here: `borderControl` is a fixed alpha
31
+ * of `fgPrimary`, and the sweep test proves it holds.
29
32
  *
30
33
  * Checking only `wcagAAPairs` (28) is what let "a theme derived from any
31
34
  * prompt clears the AA matrix by construction" ship false a third time. The
@@ -40,7 +43,7 @@ function withAlpha(hex, alpha) {
40
43
  * `accentLightness` there for why the fix has to be upstream of this loop
41
44
  * rather than another escalating target inside it).
42
45
  */
43
- const AA_MATRIX = [...wcagAAPairs, ...componentLabelPairs];
46
+ const AA_MATRIX = [...wcagAAPairs, ...componentLabelPairs, ...nonTextPairs];
44
47
  const BASE_TARGET = 4.5;
45
48
  const TARGET_ESCALATION = 0.1;
46
49
  const MAX_OUTER_ATTEMPTS = 40;
@@ -26,7 +26,7 @@ const ACCENT_L = 0.55;
26
26
  * renders white-on-accent at **4.4979:1** — under AA, and the exact failure
27
27
  * the prompt "bold calm app" produced. It cleared CI only because `derive.ts`
28
28
  * checked the 28-pair `wcagAAPairs` while `scripts/build.ts` gates committed
29
- * themes on all 33.
29
+ * themes on the full matrix.
30
30
  *
31
31
  * So the accent is darkened — never lightened — to the lightest value that
32
32
  * still carries a white label at `LABEL_TARGET`, a deliberate margin above
@@ -45,7 +45,7 @@ const ACCENT_L = 0.55;
45
45
  * `fillDanger`, `fgStaticBlack` on `fillWarning`) sit on fixed hues and were
46
46
  * measured at 5.16 / 5.21 / 6.94:1 worst-case across the same sweep, so they
47
47
  * need no equivalent treatment — but the sweep in
48
- * `__tests__/generate-derive.test.ts` gates all 33 pairs, not just this one.
48
+ * `__tests__/generate-derive.test.ts` gates the full matrix, not just this one.
49
49
  */
50
50
  const LABEL_TARGET = 4.65;
51
51
  function accentLightness(c, h) {
package/dist/light.css CHANGED
@@ -40,6 +40,7 @@
40
40
  --psi-border-faint: oklch(from var(--psi-fg-primary) l c h / 0.08);
41
41
  --psi-border-neutral: oklch(from var(--psi-fg-primary) l c h / 0.15);
42
42
  --psi-border-strong: oklch(from var(--psi-fg-primary) l c h / 0.3);
43
+ --psi-border-control: oklch(from var(--psi-fg-primary) l c h / 0.55);
43
44
  --psi-border-focus: var(--psi-fg-accent);
44
45
  --psi-scrim-soft: oklch(from var(--psi-bg-primary) l c h / 0.25);
45
46
  --psi-scrim-medium: oklch(from var(--psi-bg-primary) l c h / 0.35);
@@ -431,6 +431,20 @@
431
431
  "border"
432
432
  ]
433
433
  },
434
+ "borderControl": {
435
+ "name": "borderControl",
436
+ "oklch": {
437
+ "l": 0.3,
438
+ "c": 0.03,
439
+ "h": 30,
440
+ "alpha": 0.55
441
+ },
442
+ "hex": "#3c2825",
443
+ "formula": "ref(fgPrimary) → alpha(0.55)",
444
+ "scopes": [
445
+ "border"
446
+ ]
447
+ },
434
448
  "borderFocus": {
435
449
  "name": "borderFocus",
436
450
  "oklch": {
@@ -431,6 +431,20 @@
431
431
  "border"
432
432
  ]
433
433
  },
434
+ "borderControl": {
435
+ "name": "borderControl",
436
+ "oklch": {
437
+ "l": 0.93,
438
+ "c": 0.01,
439
+ "h": 250,
440
+ "alpha": 0.4
441
+ },
442
+ "hex": "#e3e8ee",
443
+ "formula": "ref(fgPrimary) → alpha(0.4)",
444
+ "scopes": [
445
+ "border"
446
+ ]
447
+ },
434
448
  "borderFocus": {
435
449
  "name": "borderFocus",
436
450
  "oklch": {
@@ -431,6 +431,20 @@
431
431
  "border"
432
432
  ]
433
433
  },
434
+ "borderControl": {
435
+ "name": "borderControl",
436
+ "oklch": {
437
+ "l": 0.949,
438
+ "c": 0.011,
439
+ "h": 72,
440
+ "alpha": 0.4
441
+ },
442
+ "hex": "#f3ede6",
443
+ "formula": "ref(fgPrimary) → alpha(0.4)",
444
+ "scopes": [
445
+ "border"
446
+ ]
447
+ },
434
448
  "borderFocus": {
435
449
  "name": "borderFocus",
436
450
  "oklch": {
@@ -431,6 +431,20 @@
431
431
  "border"
432
432
  ]
433
433
  },
434
+ "borderControl": {
435
+ "name": "borderControl",
436
+ "oklch": {
437
+ "l": 0.3,
438
+ "c": 0.03,
439
+ "h": 250,
440
+ "alpha": 0.55
441
+ },
442
+ "hex": "#222f3c",
443
+ "formula": "ref(fgPrimary) → alpha(0.55)",
444
+ "scopes": [
445
+ "border"
446
+ ]
447
+ },
434
448
  "borderFocus": {
435
449
  "name": "borderFocus",
436
450
  "oklch": {
@@ -173,6 +173,9 @@
173
173
  "border-strong": [
174
174
  "border"
175
175
  ],
176
+ "border-control": [
177
+ "border"
178
+ ],
176
179
  "border-focus": [
177
180
  "border"
178
181
  ],
@@ -349,6 +352,9 @@
349
352
  "checkbox-box-border-checked": [
350
353
  "border"
351
354
  ],
355
+ "checkbox-box-border-error": [
356
+ "border"
357
+ ],
352
358
  "checkbox-check-fg": [
353
359
  "text"
354
360
  ],
@@ -499,6 +505,9 @@
499
505
  "radio-group-circle-border-checked": [
500
506
  "border"
501
507
  ],
508
+ "radio-group-circle-border-error": [
509
+ "border"
510
+ ],
502
511
  "radio-group-dot-fg": [
503
512
  "text"
504
513
  ],
@@ -691,6 +700,12 @@
691
700
  "textarea-counter-fg": [
692
701
  "text"
693
702
  ],
703
+ "text-fg": [
704
+ "text"
705
+ ],
706
+ "text-fg-secondary": [
707
+ "text"
708
+ ],
694
709
  "toast-bg": [
695
710
  "surface"
696
711
  ],
@@ -1,7 +1,7 @@
1
1
  import { validate } from "../dsl/validator.js";
2
2
  import { resolve } from "../dsl/resolver.js";
3
3
  import { gamutWarnings } from "../gamut.js";
4
- import { checkContrast, wcagAAPairs, componentLabelPairs } from "../contrast-matrix.js";
4
+ import { checkContrast, wcagAAPairs, componentLabelPairs, nonTextPairs } from "../contrast-matrix.js";
5
5
  import { checkScopes, checkOverrideScopes } from "../scope-gate.js";
6
6
  import { componentVars } from "../components/registry.js";
7
7
  /** Thrown when a theme fails the WCAG AA contrast gate or the D46 scope gate. */
@@ -32,7 +32,7 @@ export function gateTheme(name, theme, palette, slots, componentOverrides = {})
32
32
  validate(theme, slots);
33
33
  const resolved = resolve(theme, palette, slots);
34
34
  const warnings = gamutWarnings(resolved, theme, palette, slots);
35
- const contrastFailures = checkContrast(resolved, [...wcagAAPairs, ...componentLabelPairs]).filter((r) => !r.pass);
35
+ const contrastFailures = checkContrast(resolved, [...wcagAAPairs, ...componentLabelPairs, ...nonTextPairs]).filter((r) => !r.pass);
36
36
  const scopeViolations = [
37
37
  ...checkScopes(componentVars, theme),
38
38
  ...checkOverrideScopes(componentOverrides, componentVars, theme),
@@ -41,6 +41,10 @@ export const darkTheme = {
41
41
  borderFaint: token({ from: ref.fgPrimary, alpha: 0.08, scopes: ["border"] }),
42
42
  borderNeutral: token({ from: ref.fgPrimary, alpha: 0.15, scopes: ["border"] }),
43
43
  borderStrong: token({ from: ref.fgPrimary, alpha: 0.3, scopes: ["border"] }),
44
+ /** D106: the outline that alone identifies an unchecked checkbox or radio,
45
+ * so WCAG 1.4.11 asks 3:1 for it (`nonTextPairs`). borderNeutral measured
46
+ * 1.32-1.48. Alpha chosen by sweep over every derivable theme. */
47
+ borderControl: token({ from: ref.fgPrimary, alpha: 0.4, scopes: ["border"] }),
44
48
  borderFocus: token({ from: ref.fgAccent, scopes: ["border"] }),
45
49
  // Scrims (D32): page-canvas surfaces at alpha — never text pairs, so they
46
50
  // carry no AA gate entries.
@@ -44,6 +44,10 @@ export const lightTheme = {
44
44
  borderFaint: token({ from: ref.fgPrimary, alpha: 0.08, scopes: ["border"] }),
45
45
  borderNeutral: token({ from: ref.fgPrimary, alpha: 0.15, scopes: ["border"] }),
46
46
  borderStrong: token({ from: ref.fgPrimary, alpha: 0.3, scopes: ["border"] }),
47
+ /** D106: the outline that alone identifies an unchecked checkbox or radio,
48
+ * so WCAG 1.4.11 asks 3:1 for it (`nonTextPairs`). borderNeutral measured
49
+ * 1.32-1.48. Alpha chosen by sweep over every derivable theme. */
50
+ borderControl: token({ from: ref.fgPrimary, alpha: 0.55, scopes: ["border"] }),
47
51
  borderFocus: token({ from: ref.fgAccent, scopes: ["border"] }),
48
52
  // Scrims (D32): page-canvas surfaces at alpha — never text pairs, so they
49
53
  // carry no AA gate entries.
@@ -4,6 +4,7 @@ export type TokenName =
4
4
  | "bgInverted"
5
5
  | "bgPrimary"
6
6
  | "bgSecondary"
7
+ | "borderControl"
7
8
  | "borderFaint"
8
9
  | "borderFocus"
9
10
  | "borderNeutral"
package/llms.txt CHANGED
@@ -39,6 +39,7 @@ OKLCH formula-based design tokens. Code is the source of truth; all artifacts ge
39
39
  ## Scrims and borders
40
40
  - --psi-scrim-soft/medium/heavy: bgPrimary-derived translucent overlays (0.25/0.35/0.82 alpha) for text-over-media legibility.
41
41
  - --psi-border-faint: low-contrast hairline border (fgPrimary at 0.08 alpha), used by Card/NavBar and other bordered surfaces.
42
+ - --psi-border-control: the outline that alone identifies an unchecked Checkbox or Radio (fgPrimary at 0.55 alpha in light-based themes, 0.40 in dark-based), gated at 3:1 on bgPrimary and bgSecondary by `nonTextPairs` (WCAG 1.4.11, D106). Bind it, not --psi-border-neutral (1.32-1.48:1), for any control identified by its outline alone. A consumer theme built with buildThemeCss is held to the same 3:1.
42
43
 
43
44
  ## Media tint (--psi-media-tint, .psi-media-tint)
44
45
  - --psi-media-tint is a brand-defined filter chain (D35); apply the .psi-media-tint utility class to media elements to get a tinted-at-rest, true-color-on-hover/focus treatment, transitioning over --psi-duration-450 / --psi-ease-soft.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handamade/psi-tokens",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {