@navecss/tokens 0.1.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.
Files changed (121) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +122 -0
  3. package/TOKENS.md +180 -0
  4. package/dist/breakpoints.d.ts +25 -0
  5. package/dist/breakpoints.js +44 -0
  6. package/dist/build-record.json +77 -0
  7. package/dist/core-contract.json +30 -0
  8. package/dist/lib/bin.d.ts +2 -0
  9. package/dist/lib/bin.js +113 -0
  10. package/dist/lib/builder.d.ts +21 -0
  11. package/dist/lib/builder.js +41 -0
  12. package/dist/lib/collision.d.ts +12 -0
  13. package/dist/lib/collision.js +46 -0
  14. package/dist/lib/composite-value.d.ts +3 -0
  15. package/dist/lib/composite-value.js +152 -0
  16. package/dist/lib/core-version.d.ts +12 -0
  17. package/dist/lib/core-version.js +40 -0
  18. package/dist/lib/dtcg-descriptions.d.ts +2 -0
  19. package/dist/lib/dtcg-descriptions.js +23 -0
  20. package/dist/lib/dtcg-malformed.d.ts +10 -0
  21. package/dist/lib/dtcg-malformed.js +56 -0
  22. package/dist/lib/dtcg-outputs.d.ts +2 -0
  23. package/dist/lib/dtcg-outputs.js +32 -0
  24. package/dist/lib/dtcg-shape.d.ts +13 -0
  25. package/dist/lib/dtcg-shape.js +113 -0
  26. package/dist/lib/errors.d.ts +23 -0
  27. package/dist/lib/errors.js +34 -0
  28. package/dist/lib/facade.d.ts +26 -0
  29. package/dist/lib/facade.js +97 -0
  30. package/dist/lib/formats.d.ts +6 -0
  31. package/dist/lib/formats.js +236 -0
  32. package/dist/lib/json-pointer.d.ts +2 -0
  33. package/dist/lib/json-pointer.js +28 -0
  34. package/dist/lib/overrides.d.ts +2 -0
  35. package/dist/lib/overrides.js +59 -0
  36. package/dist/lib/package-root.d.ts +2 -0
  37. package/dist/lib/package-root.js +31 -0
  38. package/dist/lib/reader.d.ts +8 -0
  39. package/dist/lib/reader.js +136 -0
  40. package/dist/lib/theming/adjacency-source.d.ts +20 -0
  41. package/dist/lib/theming/adjacency-source.js +103 -0
  42. package/dist/lib/theming/adjacency.d.ts +17 -0
  43. package/dist/lib/theming/adjacency.js +133 -0
  44. package/dist/lib/theming/build-record.d.ts +45 -0
  45. package/dist/lib/theming/build-record.js +87 -0
  46. package/dist/lib/theming/build-step.d.ts +14 -0
  47. package/dist/lib/theming/build-step.js +91 -0
  48. package/dist/lib/theming/color-math.d.ts +33 -0
  49. package/dist/lib/theming/color-math.js +125 -0
  50. package/dist/lib/theming/comment-open-tracker.d.ts +1 -0
  51. package/dist/lib/theming/comment-open-tracker.js +49 -0
  52. package/dist/lib/theming/consumer-build.d.ts +18 -0
  53. package/dist/lib/theming/consumer-build.js +29 -0
  54. package/dist/lib/theming/contact-sheet.d.ts +22 -0
  55. package/dist/lib/theming/contact-sheet.js +123 -0
  56. package/dist/lib/theming/contrast.d.ts +31 -0
  57. package/dist/lib/theming/contrast.js +133 -0
  58. package/dist/lib/theming/copy-lint.d.ts +13 -0
  59. package/dist/lib/theming/copy-lint.js +98 -0
  60. package/dist/lib/theming/core-contract.d.ts +29 -0
  61. package/dist/lib/theming/core-contract.js +71 -0
  62. package/dist/lib/theming/core-source.d.ts +2 -0
  63. package/dist/lib/theming/core-source.js +18 -0
  64. package/dist/lib/theming/css-color-4.d.ts +19 -0
  65. package/dist/lib/theming/css-color-4.js +66 -0
  66. package/dist/lib/theming/css-named-colours.d.ts +1 -0
  67. package/dist/lib/theming/css-named-colours.js +151 -0
  68. package/dist/lib/theming/descriptions.d.ts +1 -0
  69. package/dist/lib/theming/descriptions.js +21 -0
  70. package/dist/lib/theming/emit.d.ts +10 -0
  71. package/dist/lib/theming/emit.js +62 -0
  72. package/dist/lib/theming/feedback-signal.d.ts +11 -0
  73. package/dist/lib/theming/feedback-signal.js +23 -0
  74. package/dist/lib/theming/guard-message-probes.d.ts +6 -0
  75. package/dist/lib/theming/guard-message-probes.js +96 -0
  76. package/dist/lib/theming/ladder.d.ts +15 -0
  77. package/dist/lib/theming/ladder.js +81 -0
  78. package/dist/lib/theming/neutral.d.ts +12 -0
  79. package/dist/lib/theming/neutral.js +76 -0
  80. package/dist/lib/theming/on-star.d.ts +11 -0
  81. package/dist/lib/theming/on-star.js +87 -0
  82. package/dist/lib/theming/palette-record.d.ts +2 -0
  83. package/dist/lib/theming/palette-record.js +15 -0
  84. package/dist/lib/theming/pipeline.d.ts +43 -0
  85. package/dist/lib/theming/pipeline.js +104 -0
  86. package/dist/lib/theming/ramp.d.ts +34 -0
  87. package/dist/lib/theming/ramp.js +92 -0
  88. package/dist/lib/theming/seed-channel-split.d.ts +8 -0
  89. package/dist/lib/theming/seed-channel-split.js +99 -0
  90. package/dist/lib/theming/seed-form-parsers-css-color-4.d.ts +8 -0
  91. package/dist/lib/theming/seed-form-parsers-css-color-4.js +85 -0
  92. package/dist/lib/theming/seed-form-parsers.d.ts +6 -0
  93. package/dist/lib/theming/seed-form-parsers.js +155 -0
  94. package/dist/lib/theming/seed-ingest.d.ts +15 -0
  95. package/dist/lib/theming/seed-ingest.js +112 -0
  96. package/dist/lib/theming/seed-input.d.ts +22 -0
  97. package/dist/lib/theming/seed-input.js +1 -0
  98. package/dist/lib/theming/seed-refusal.d.ts +21 -0
  99. package/dist/lib/theming/seed-refusal.js +77 -0
  100. package/dist/lib/theming/semantics.d.ts +19 -0
  101. package/dist/lib/theming/semantics.js +71 -0
  102. package/dist/lib/theming/shipped-seeds.d.ts +2 -0
  103. package/dist/lib/theming/shipped-seeds.js +5 -0
  104. package/dist/lib/theming/step-table.d.ts +5 -0
  105. package/dist/lib/theming/step-table.js +26 -0
  106. package/dist/lib/theming/third-party-provenance.d.ts +9 -0
  107. package/dist/lib/theming/third-party-provenance.js +35 -0
  108. package/dist/lib/theming/tokens-source.d.ts +3 -0
  109. package/dist/lib/theming/tokens-source.js +61 -0
  110. package/dist/lib/token-name.d.ts +2 -0
  111. package/dist/lib/token-name.js +28 -0
  112. package/dist/lib/validate-report.d.ts +19 -0
  113. package/dist/lib/validate-report.js +93 -0
  114. package/dist/lib/validate.d.ts +16 -0
  115. package/dist/lib/validate.js +65 -0
  116. package/dist/palette-record.json +293 -0
  117. package/dist/tokens.css +433 -0
  118. package/dist/tokens.d.ts +90 -0
  119. package/dist/tokens.js +88 -0
  120. package/package.json +85 -0
  121. package/tokens.json +938 -0
@@ -0,0 +1,19 @@
1
+ import type { Oklch } from './color-math.ts';
2
+ export interface CieLab {
3
+ l: number;
4
+ a: number;
5
+ b: number;
6
+ }
7
+ export interface CieLch {
8
+ l: number;
9
+ c: number;
10
+ h: number;
11
+ }
12
+ export interface DisplayP3 {
13
+ r: number;
14
+ g: number;
15
+ b: number;
16
+ }
17
+ export declare function labToOklch(lab: CieLab): Oklch;
18
+ export declare function lchToOklch(lch: CieLch): Oklch;
19
+ export declare function displayP3ToOklch(p3: DisplayP3): Oklch;
@@ -0,0 +1,66 @@
1
+ import { oklabToOklch, srgbToLinearRgb } from "./color-math.js";
2
+ const D50_WHITE = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
3
+ const KAPPA = 24_389 / 27;
4
+ const EPSILON = 216 / 24_389;
5
+ const D50_TO_D65 = [
6
+ [0.955473421488075, -0.02309845494876471, 0.06325924320057072],
7
+ [-0.0283697093338637, 1.0099953980813041, 0.021041441191917323],
8
+ [0.012314014864481998, -0.020507649298898964, 1.330365926242124],
9
+ ];
10
+ const LIN_P3_TO_XYZ = [
11
+ [608_311 / 1_250_200, 189_793 / 714_400, 198_249 / 1_000_160],
12
+ [35_783 / 156_275, 247_089 / 357_200, 198_249 / 2_500_400],
13
+ [0, 32_229 / 714_400, 5_220_557 / 5_000_800],
14
+ ];
15
+ const XYZ_TO_LMS = [
16
+ [0.819022437996703, 0.3619062600528904, -0.1288737815209879],
17
+ [0.0329836539323885, 0.9292868615863434, 0.0361446663506424],
18
+ [0.0481771893596242, 0.2642395317527308, 0.6335478284694309],
19
+ ];
20
+ const LMS_TO_OKLAB = [
21
+ [0.210454268309314, 0.7936177747023054, -0.0040720430116193],
22
+ [1.9779985324311684, -2.4285922420485799, 0.450593709617411],
23
+ [0.0259040424655478, 0.7827717124575296, -0.8086757549230774],
24
+ ];
25
+ function multiply(m, v) {
26
+ return [
27
+ m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
28
+ m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
29
+ m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
30
+ ];
31
+ }
32
+ function labToXyzD50(lab) {
33
+ const fy = (lab.l + 16) / 116;
34
+ const fx = lab.a / 500 + fy;
35
+ const fz = fy - lab.b / 200;
36
+ const x = fx ** 3 > EPSILON ? fx ** 3 : (116 * fx - 16) / KAPPA;
37
+ const y = lab.l > KAPPA * EPSILON ? ((lab.l + 16) / 116) ** 3 : lab.l / KAPPA;
38
+ const z = fz ** 3 > EPSILON ? fz ** 3 : (116 * fz - 16) / KAPPA;
39
+ return [x * D50_WHITE[0], y * D50_WHITE[1], z * D50_WHITE[2]];
40
+ }
41
+ function xyzD65ToOklch(xyz) {
42
+ const lms = multiply(XYZ_TO_LMS, xyz);
43
+ const [l, a, b] = multiply(LMS_TO_OKLAB, [
44
+ Math.cbrt(lms[0]),
45
+ Math.cbrt(lms[1]),
46
+ Math.cbrt(lms[2]),
47
+ ]);
48
+ return oklabToOklch({ a, b, l });
49
+ }
50
+ export function labToOklch(lab) {
51
+ const result = xyzD65ToOklch(multiply(D50_TO_D65, labToXyzD50(lab)));
52
+ if (lab.a === 0 && lab.b === 0)
53
+ return { c: 0, h: 0, l: result.l };
54
+ return result;
55
+ }
56
+ export function lchToOklch(lch) {
57
+ const hRad = (lch.h * Math.PI) / 180;
58
+ return labToOklch({ a: lch.c * Math.cos(hRad), b: lch.c * Math.sin(hRad), l: lch.l });
59
+ }
60
+ export function displayP3ToOklch(p3) {
61
+ const linear = srgbToLinearRgb(p3);
62
+ const result = xyzD65ToOklch(multiply(LIN_P3_TO_XYZ, [linear.r, linear.g, linear.b]));
63
+ if (p3.r === p3.g && p3.g === p3.b)
64
+ return { c: 0, h: 0, l: result.l };
65
+ return result;
66
+ }
@@ -0,0 +1 @@
1
+ export declare const CSS_NAMED_COLOURS: Set<string>;
@@ -0,0 +1,151 @@
1
+ export const CSS_NAMED_COLOURS = new Set([
2
+ 'aliceblue',
3
+ 'antiquewhite',
4
+ 'aqua',
5
+ 'aquamarine',
6
+ 'azure',
7
+ 'beige',
8
+ 'bisque',
9
+ 'black',
10
+ 'blanchedalmond',
11
+ 'blue',
12
+ 'blueviolet',
13
+ 'brown',
14
+ 'burlywood',
15
+ 'cadetblue',
16
+ 'chartreuse',
17
+ 'chocolate',
18
+ 'coral',
19
+ 'cornflowerblue',
20
+ 'cornsilk',
21
+ 'crimson',
22
+ 'cyan',
23
+ 'darkblue',
24
+ 'darkcyan',
25
+ 'darkgoldenrod',
26
+ 'darkgray',
27
+ 'darkgreen',
28
+ 'darkgrey',
29
+ 'darkkhaki',
30
+ 'darkmagenta',
31
+ 'darkolivegreen',
32
+ 'darkorange',
33
+ 'darkorchid',
34
+ 'darkred',
35
+ 'darksalmon',
36
+ 'darkseagreen',
37
+ 'darkslateblue',
38
+ 'darkslategray',
39
+ 'darkslategrey',
40
+ 'darkturquoise',
41
+ 'darkviolet',
42
+ 'deeppink',
43
+ 'deepskyblue',
44
+ 'dimgray',
45
+ 'dimgrey',
46
+ 'dodgerblue',
47
+ 'firebrick',
48
+ 'floralwhite',
49
+ 'forestgreen',
50
+ 'fuchsia',
51
+ 'gainsboro',
52
+ 'ghostwhite',
53
+ 'gold',
54
+ 'goldenrod',
55
+ 'gray',
56
+ 'green',
57
+ 'greenyellow',
58
+ 'grey',
59
+ 'honeydew',
60
+ 'hotpink',
61
+ 'indianred',
62
+ 'indigo',
63
+ 'ivory',
64
+ 'khaki',
65
+ 'lavender',
66
+ 'lavenderblush',
67
+ 'lawngreen',
68
+ 'lemonchiffon',
69
+ 'lightblue',
70
+ 'lightcoral',
71
+ 'lightcyan',
72
+ 'lightgoldenrodyellow',
73
+ 'lightgray',
74
+ 'lightgreen',
75
+ 'lightgrey',
76
+ 'lightpink',
77
+ 'lightsalmon',
78
+ 'lightseagreen',
79
+ 'lightskyblue',
80
+ 'lightslategray',
81
+ 'lightslategrey',
82
+ 'lightsteelblue',
83
+ 'lightyellow',
84
+ 'lime',
85
+ 'limegreen',
86
+ 'linen',
87
+ 'magenta',
88
+ 'maroon',
89
+ 'mediumaquamarine',
90
+ 'mediumblue',
91
+ 'mediumorchid',
92
+ 'mediumpurple',
93
+ 'mediumseagreen',
94
+ 'mediumslateblue',
95
+ 'mediumspringgreen',
96
+ 'mediumturquoise',
97
+ 'mediumvioletred',
98
+ 'midnightblue',
99
+ 'mintcream',
100
+ 'mistyrose',
101
+ 'moccasin',
102
+ 'navajowhite',
103
+ 'navy',
104
+ 'oldlace',
105
+ 'olive',
106
+ 'olivedrab',
107
+ 'orange',
108
+ 'orangered',
109
+ 'orchid',
110
+ 'palegoldenrod',
111
+ 'palegreen',
112
+ 'paleturquoise',
113
+ 'palevioletred',
114
+ 'papayawhip',
115
+ 'peachpuff',
116
+ 'peru',
117
+ 'pink',
118
+ 'plum',
119
+ 'powderblue',
120
+ 'purple',
121
+ 'rebeccapurple',
122
+ 'red',
123
+ 'rosybrown',
124
+ 'royalblue',
125
+ 'saddlebrown',
126
+ 'salmon',
127
+ 'sandybrown',
128
+ 'seagreen',
129
+ 'seashell',
130
+ 'sienna',
131
+ 'silver',
132
+ 'skyblue',
133
+ 'slateblue',
134
+ 'slategray',
135
+ 'slategrey',
136
+ 'snow',
137
+ 'springgreen',
138
+ 'steelblue',
139
+ 'tan',
140
+ 'teal',
141
+ 'thistle',
142
+ 'tomato',
143
+ 'transparent',
144
+ 'turquoise',
145
+ 'violet',
146
+ 'wheat',
147
+ 'white',
148
+ 'whitesmoke',
149
+ 'yellow',
150
+ 'yellowgreen',
151
+ ]);
@@ -0,0 +1 @@
1
+ export declare const SLOT_DESCRIPTIONS: ReadonlyMap<string, string>;
@@ -0,0 +1,21 @@
1
+ export const SLOT_DESCRIPTIONS = new Map([
2
+ [
3
+ 'border.control',
4
+ 'Identifying boundary for a control (inputs, form boundaries, and the secondary action control).',
5
+ ],
6
+ ['border.default', 'Decorative dividers and card edges.'],
7
+ ['content.disabled', 'Text inside a disabled interactive component.'],
8
+ ['content.inverse', 'Declared foreground for surface.inverse.'],
9
+ [
10
+ 'content.link',
11
+ "Do not distinguish a link by colour alone. Nave's own examples keep a non-colour distinction wherever this token is shown.",
12
+ ],
13
+ ['content.tertiary', 'Placeholder and hint text only.'],
14
+ ['feedback.danger', 'Solid-background role for a destructive control or error state.'],
15
+ ['feedback.danger.foreground', 'Foreground role for danger text and icons.'],
16
+ ['feedback.info', 'Alias of feedback.warning (shared tinted-neutral family).'],
17
+ ['feedback.success', 'Alias of feedback.warning (shared tinted-neutral family).'],
18
+ ['feedback.warning', 'Solid-background role for a warning state (shared tinted-neutral family).'],
19
+ ['feedback.warning.foreground', 'Foreground role for warning text and icons.'],
20
+ ['on-feedback.danger', 'Declared foreground for the solid feedback.danger background.'],
21
+ ]);
@@ -0,0 +1,10 @@
1
+ import type { PipelineResult } from './pipeline.ts';
2
+ export declare const TINT_PROPERTY = "--nave-color-tint";
3
+ export declare const TINT_SEED_COMMENT_STEM = "/* Tint seed: ";
4
+ export declare const FEEDBACK_TOKENS_COMMENT_STEM = "/* Feedback tokens: ";
5
+ export declare function shippedThemingPropertyNames(): ReadonlySet<string>;
6
+ export interface EmittedCss {
7
+ customProperties: ReadonlyMap<string, string>;
8
+ css: string;
9
+ }
10
+ export declare function emitCss(result: PipelineResult, layer?: string): EmittedCss;
@@ -0,0 +1,62 @@
1
+ import { formatOklch } from "./color-math.js";
2
+ import { FEEDBACK_SHARED_IDENTITY_NOTICE, RETHEMING_NOTICE } from "./copy-lint.js";
3
+ import { ACTION_SECONDARY_BORDER, ACTION_SECONDARY_FOREGROUND, SEMANTIC_SLOTS, } from "./semantics.js";
4
+ const PREFIX = '--nave-color-';
5
+ export const TINT_PROPERTY = '--nave-color-tint';
6
+ export const TINT_SEED_COMMENT_STEM = '/* Tint seed: ';
7
+ export const FEEDBACK_TOKENS_COMMENT_STEM = '/* Feedback tokens: ';
8
+ function slotCssName(slot) {
9
+ return PREFIX + slot.replaceAll('.', '-');
10
+ }
11
+ export function shippedThemingPropertyNames() {
12
+ return new Set([TINT_PROPERTY, ...SEMANTIC_SLOTS.map((slot) => slotCssName(slot))]);
13
+ }
14
+ function lightDarkDeclaration(result, slot) {
15
+ const light = result.slots.find((s) => s.slot === slot && s.branch === 'light');
16
+ const dark = result.slots.find((s) => s.slot === slot && s.branch === 'dark');
17
+ if (!light || !dark)
18
+ throw new Error(`Missing branch for slot ${slot}: no slot is allowed to lack a dark value. Open an issue.`);
19
+ return { name: slotCssName(slot), value: `light-dark(${light.css}, ${dark.css})` };
20
+ }
21
+ export function emitCss(result, layer = 'tokens.defaults') {
22
+ const props = new Map();
23
+ for (const slot of SEMANTIC_SLOTS) {
24
+ const { name, value } = lightDarkDeclaration(result, slot);
25
+ props.set(name, value);
26
+ }
27
+ void ACTION_SECONDARY_FOREGROUND;
28
+ void ACTION_SECONDARY_BORDER;
29
+ const tintDefault = formatOklch(result.records.achromaticBranch
30
+ ? { l: 0.7, c: 0.05, h: result.records.tintHue }
31
+ : {
32
+ l: result.records.primary.usedSeed.l,
33
+ c: result.records.primary.usedSeed.c,
34
+ h: result.records.primary.usedSeed.h,
35
+ });
36
+ const lines = [
37
+ '/* Nave theming — generated, do not edit. See this package\'s README ("Theming"). */',
38
+ '',
39
+ `@property ${TINT_PROPERTY} {`,
40
+ ` syntax: '<color>';`,
41
+ ` inherits: true;`,
42
+ ` initial-value: ${tintDefault};`,
43
+ `}`,
44
+ '',
45
+ `@layer ${layer} {`,
46
+ ' :root {',
47
+ ` /* color-scheme lives here, not in @navecss/core's reset, so`,
48
+ ` * light-dark() resolves even for a tokens-only consumer. */`,
49
+ ` color-scheme: light dark;`,
50
+ ` ${TINT_SEED_COMMENT_STEM}${RETHEMING_NOTICE} */`,
51
+ ` ${TINT_PROPERTY}: ${tintDefault};`,
52
+ ];
53
+ const feedbackWarningName = slotCssName('feedback.warning');
54
+ for (const [name, value] of props) {
55
+ if (name === feedbackWarningName) {
56
+ lines.push(` ${FEEDBACK_TOKENS_COMMENT_STEM}${FEEDBACK_SHARED_IDENTITY_NOTICE} */`);
57
+ }
58
+ lines.push(` ${name}: ${value};`);
59
+ }
60
+ lines.push(' }', '}', '');
61
+ return { customProperties: props, css: lines.join('\n') };
62
+ }
@@ -0,0 +1,11 @@
1
+ export interface FeedbackArtifact {
2
+ file: string;
3
+ usesFeedbackToken: boolean;
4
+ hasStateLabel: boolean;
5
+ nonColourForm: string | undefined;
6
+ }
7
+ export interface FeedbackViolation {
8
+ file: string;
9
+ reason: string;
10
+ }
11
+ export declare function checkFeedbackSignal(artifacts: readonly FeedbackArtifact[]): FeedbackViolation[];
@@ -0,0 +1,23 @@
1
+ export function checkFeedbackSignal(artifacts) {
2
+ const violations = [];
3
+ const forms = new Map();
4
+ for (const artifact of artifacts) {
5
+ if (!artifact.usesFeedbackToken)
6
+ continue;
7
+ if (!artifact.nonColourForm) {
8
+ violations.push({ file: artifact.file, reason: 'conveys a feedback state by colour alone' });
9
+ continue;
10
+ }
11
+ if (!artifact.hasStateLabel) {
12
+ violations.push({
13
+ file: artifact.file,
14
+ reason: 'has a non-colour element but no text label naming the state',
15
+ });
16
+ continue;
17
+ }
18
+ const seen = forms.get(artifact.nonColourForm) ?? new Set();
19
+ seen.add(artifact.file);
20
+ forms.set(artifact.nonColourForm, seen);
21
+ }
22
+ return violations;
23
+ }
@@ -0,0 +1,6 @@
1
+ export interface GuardMessageProbe {
2
+ label: string;
3
+ capture: () => string;
4
+ }
5
+ export declare const ACCESSIBILITY_GUARD_MESSAGE_PROBES: readonly GuardMessageProbe[];
6
+ export declare const STRUCTURALLY_UNPROBEABLE_GUARD_MESSAGES: readonly GuardMessageProbe[];
@@ -0,0 +1,96 @@
1
+ import { ADJACENCY, assertNoOrphanedSemanticSlot } from "./adjacency.js";
2
+ import { assertContrastFloors, assertFloorProvenance, assertNoSameStepViolations, checkSameStepLint, runContrastHarness, } from "./contrast.js";
3
+ import { assertNoticeIsEmitted, RETHEMING_NOTICE } from "./copy-lint.js";
4
+ import { TINT_SEED_COMMENT_STEM } from "./emit.js";
5
+ import { assertNeutralChromaCeilingWithinMargin } from "./neutral.js";
6
+ function capturedFailureMessage(fn) {
7
+ try {
8
+ fn();
9
+ }
10
+ catch (error) {
11
+ return error.message;
12
+ }
13
+ throw new Error('capturedFailureMessage: the given function did not throw as expected.');
14
+ }
15
+ function pinnedSlot(slot, step) {
16
+ return {
17
+ slot,
18
+ branch: 'light',
19
+ resolved: { scale: 'neutral', step },
20
+ literal: { l: 0.18, c: 0, h: 0 },
21
+ css: 'oklch(0.18 0 0)',
22
+ };
23
+ }
24
+ function pinnedPair(slot, light, dark) {
25
+ return { dark: { ...pinnedSlot(slot, dark), branch: 'dark' }, light: pinnedSlot(slot, light) };
26
+ }
27
+ const NOTICE_PROBE_LABEL = 'Retheming notice';
28
+ export const ACCESSIBILITY_GUARD_MESSAGE_PROBES = [
29
+ {
30
+ label: "the contrast harness's fail-closed output",
31
+ capture: () => capturedFailureMessage(() => runContrastHarness({}, [])),
32
+ },
33
+ {
34
+ label: 'the contrast-floor threshold-failure output',
35
+ capture: () => capturedFailureMessage(() => assertContrastFloors([
36
+ {
37
+ pair: { subject: 'content.primary', against: 'surface.base', class: 'text' },
38
+ scheme: 'light',
39
+ ratio: 1,
40
+ floor: 4.5,
41
+ threshold: 4.545,
42
+ pass: false,
43
+ },
44
+ ])),
45
+ },
46
+ {
47
+ label: 'the same-step lint fail-closed output',
48
+ capture: () => capturedFailureMessage(() => checkSameStepLint({}, 'nave', [])),
49
+ },
50
+ {
51
+ label: 'the floor-provenance mismatch output',
52
+ capture: () => capturedFailureMessage(() => assertFloorProvenance({ textFloor: 0, nonTextFloor: 0, margin: 0 })),
53
+ },
54
+ {
55
+ label: 'the neutral chroma-ceiling margin output',
56
+ capture: () => capturedFailureMessage(() => assertNeutralChromaCeilingWithinMargin(1)),
57
+ },
58
+ {
59
+ label: 'the orphaned-semantic-slot output',
60
+ capture: () => capturedFailureMessage(() => assertNoOrphanedSemanticSlot(['content.brandNew'])),
61
+ },
62
+ {
63
+ label: "the same-step lint's real violation output (build-step.ts's runResultGuards)",
64
+ capture: () => {
65
+ const pinned = pinnedSlot('content.primary', 900);
66
+ const result = {
67
+ slots: [pinned, { ...pinned, branch: 'dark' }],
68
+ actionSecondaryForeground: pinnedPair('action.secondary.foreground', 100, 200),
69
+ actionSecondaryBorder: pinnedPair('action.secondary.border', 500, 400),
70
+ };
71
+ const sameStep = checkSameStepLint(result, 'nave', ADJACENCY);
72
+ return capturedFailureMessage(() => assertNoSameStepViolations(sameStep));
73
+ },
74
+ },
75
+ {
76
+ label: "the emitted-notice guard's absent-notice refusal",
77
+ capture: () => capturedFailureMessage(() => assertNoticeIsEmitted(':root {\n}', RETHEMING_NOTICE, NOTICE_PROBE_LABEL)),
78
+ },
79
+ {
80
+ label: "the emitted-notice guard's multi-line-notice refusal",
81
+ capture: () => {
82
+ const split = `${RETHEMING_NOTICE.slice(0, 10)}\n${RETHEMING_NOTICE.slice(10)}`;
83
+ return capturedFailureMessage(() => assertNoticeIsEmitted(`:root {\n ${TINT_SEED_COMMENT_STEM}${split} */\n}`, split, NOTICE_PROBE_LABEL));
84
+ },
85
+ },
86
+ {
87
+ label: "the emitted-notice guard's not-a-self-contained-comment refusal",
88
+ capture: () => capturedFailureMessage(() => assertNoticeIsEmitted(`:root {\n /* wrapped, so\n * ${RETHEMING_NOTICE} */\n}`, RETHEMING_NOTICE, NOTICE_PROBE_LABEL)),
89
+ },
90
+ ];
91
+ export const STRUCTURALLY_UNPROBEABLE_GUARD_MESSAGES = [
92
+ {
93
+ label: "the emitted-notice guard's composed-comment-line refusal",
94
+ capture: () => capturedFailureMessage(() => assertNoticeIsEmitted(`:root {\n /* meets ${TINT_SEED_COMMENT_STEM.slice(3)}${RETHEMING_NOTICE} */\n}`, RETHEMING_NOTICE, NOTICE_PROBE_LABEL)),
95
+ },
96
+ ];
@@ -0,0 +1,15 @@
1
+ type UpgradePosture = 'fine' | 'unbounded-0x-bounded-1.0';
2
+ export interface LadderRung {
3
+ rung: string;
4
+ situation: string;
5
+ whatYouWrite: string;
6
+ whereItLives: string;
7
+ cost: string;
8
+ preserves: string;
9
+ voids: string;
10
+ upgrade: UpgradePosture;
11
+ reserved?: true;
12
+ }
13
+ export declare const LADDER: readonly LadderRung[];
14
+ export declare function assertLadderOrder(): void;
15
+ export {};
@@ -0,0 +1,81 @@
1
+ export const LADDER = [
2
+ {
3
+ rung: '0',
4
+ situation: 'Just give me something that looks good.',
5
+ whatYouWrite: 'nothing',
6
+ whereItLives: 'a preset stylesheet import',
7
+ cost: 'one import',
8
+ preserves: 'everything, generated ahead of time from a seed Nave chose',
9
+ voids: 'nothing',
10
+ upgrade: 'fine',
11
+ reserved: true,
12
+ },
13
+ {
14
+ rung: '1a',
15
+ situation: 'Warm our greys up.',
16
+ whatYouWrite: 're-point the tint',
17
+ whereItLives: 'a CSS block in your app',
18
+ cost: 'one custom property',
19
+ preserves: 'everything; the neutral surface re-hues live, the accent does not move',
20
+ voids: 'nothing',
21
+ upgrade: 'fine',
22
+ },
23
+ {
24
+ rung: '1b',
25
+ situation: 'Our brand colour is this.',
26
+ whatYouWrite: 're-seed the accent',
27
+ whereItLives: 'one build command, output committed',
28
+ cost: 'one build command',
29
+ preserves: 'everything, generated from your seed',
30
+ voids: 'nothing',
31
+ upgrade: 'fine',
32
+ },
33
+ {
34
+ rung: '2',
35
+ situation: 'Our danger red is the corporate red.',
36
+ whatYouWrite: 'override a semantic slot',
37
+ whereItLives: 'a CSS block in your app',
38
+ cost: "generation, minus one role's link",
39
+ preserves: 'the rest of the generated mapping',
40
+ voids: 'the overridden slot, and anything that assumed the shipped value',
41
+ upgrade: 'fine',
42
+ },
43
+ {
44
+ rung: '3',
45
+ situation: 'Step 500 is muddy in our brand.',
46
+ whatYouWrite: 'override a step',
47
+ whereItLives: 'a JSON file in your repo',
48
+ cost: 'generation, minus one rung of one ramp',
49
+ preserves: 'every other step',
50
+ voids: 'contrast guarantees resting on the overridden step',
51
+ upgrade: 'fine',
52
+ },
53
+ {
54
+ rung: '4',
55
+ situation: 'Our lightness ramp is different.',
56
+ whatYouWrite: 'retheme through the build',
57
+ whereItLives: 'your build config',
58
+ cost: 'the semantic contract, not the ramp',
59
+ preserves: 'the required core contract',
60
+ voids: 'the shipped ramp shape',
61
+ upgrade: 'unbounded-0x-bounded-1.0',
62
+ },
63
+ {
64
+ rung: '5',
65
+ situation: 'We have our own token source.',
66
+ whatYouWrite: 'bring your own DTCG 2025.10',
67
+ whereItLives: 'your repo',
68
+ cost: 'nearly everything but the required core contract',
69
+ preserves: 'the required core contract only',
70
+ voids: 'every Nave-generated guarantee outside that contract',
71
+ upgrade: 'unbounded-0x-bounded-1.0',
72
+ },
73
+ ];
74
+ export function assertLadderOrder() {
75
+ const order = LADDER.map((r) => r.rung);
76
+ const expected = ['0', '1a', '1b', '2', '3', '4', '5'];
77
+ if (JSON.stringify(order) !== JSON.stringify(expected)) {
78
+ throw new Error(`The theming-obligation ladder order is ${order.join(', ')}, expected ` +
79
+ `${expected.join(', ')}. Open an issue.`);
80
+ }
81
+ }
@@ -0,0 +1,12 @@
1
+ import { type Oklch } from './color-math.ts';
2
+ export declare function computeNeutralChromaCeiling(): number;
3
+ export declare function computeNeutralHueBandSpread(ceiling?: number): number;
4
+ export declare function assertNeutralChromaCeilingWithinMargin(ceiling?: number): void;
5
+ export interface NeutralStep {
6
+ step: number;
7
+ l: number;
8
+ c: number;
9
+ }
10
+ export declare function generateNeutralRampLiterals(ceiling?: number): readonly NeutralStep[];
11
+ export declare function resolveNeutralStep(step: number, hue: number, literals?: readonly NeutralStep[]): Oklch;
12
+ export declare function neutralRcsFormula(step: number, literals: readonly NeutralStep[]): string;
@@ -0,0 +1,76 @@
1
+ import { contrastRatio, maxChroma } from "./color-math.js";
2
+ import { STEP_TABLE } from "./step-table.js";
3
+ const HUE_SWEEP_STEP_DEG = 5;
4
+ const SAFETY_FACTOR = 0.95;
5
+ export function computeNeutralChromaCeiling() {
6
+ let min = Infinity;
7
+ for (const { l } of STEP_TABLE) {
8
+ if (l <= 0 || l >= 1)
9
+ continue;
10
+ for (let h = 0; h < 360; h += HUE_SWEEP_STEP_DEG) {
11
+ const cap = maxChroma(l, h);
12
+ if (cap < min)
13
+ min = cap;
14
+ }
15
+ }
16
+ return min * SAFETY_FACTOR;
17
+ }
18
+ const HUE_BAND_MARGIN = 0.01;
19
+ const ACHROMATIC_WHITE = { l: 1, c: 0, h: 0 };
20
+ const ACHROMATIC_BLACK = { l: 0, c: 0, h: 0 };
21
+ function hueSweepSpreadAt(l, ceiling, anchor) {
22
+ let min = Infinity;
23
+ let max = -Infinity;
24
+ for (let h = 0; h < 360; h += HUE_SWEEP_STEP_DEG) {
25
+ const ratio = contrastRatio({ l, c: ceiling, h }, anchor);
26
+ if (ratio < min)
27
+ min = ratio;
28
+ if (ratio > max)
29
+ max = ratio;
30
+ }
31
+ return min > 0 ? max / min - 1 : 0;
32
+ }
33
+ export function computeNeutralHueBandSpread(ceiling = computeNeutralChromaCeiling()) {
34
+ let worst = 0;
35
+ for (const { l } of STEP_TABLE) {
36
+ if (l <= 0 || l >= 1)
37
+ continue;
38
+ for (const anchor of [ACHROMATIC_WHITE, ACHROMATIC_BLACK]) {
39
+ const spread = hueSweepSpreadAt(l, ceiling, anchor);
40
+ if (spread > worst)
41
+ worst = spread;
42
+ }
43
+ }
44
+ return worst;
45
+ }
46
+ const NEUTRAL_CHROMA_CEILING_MARGIN_REASON = 'at or past the 1 percent margin this build holds. That margin is a fixed constraint ' +
47
+ "of the palette's design and is not this build's to move: a pair clearing its floor " +
48
+ 'only inside that margin is telling you about one tint, not about the token. ' +
49
+ 'Re-measure the tinted-neutral arithmetic before shipping a ceiling change. If you ' +
50
+ 'believe the margin itself is wrong, open an issue rather than widening it here.';
51
+ export function assertNeutralChromaCeilingWithinMargin(ceiling = computeNeutralChromaCeiling()) {
52
+ const spread = computeNeutralHueBandSpread(ceiling);
53
+ if (spread >= HUE_BAND_MARGIN) {
54
+ throw new Error(`Neutral chroma ceiling: the ceiling ${ceiling} produces a worst-case hue-band spread ` +
55
+ `of ${(spread * 100).toFixed(3)} percent in the contrast values derived from it, ${NEUTRAL_CHROMA_CEILING_MARGIN_REASON}`);
56
+ }
57
+ }
58
+ export function generateNeutralRampLiterals(ceiling = computeNeutralChromaCeiling()) {
59
+ return STEP_TABLE.map(({ step, l }) => ({ step, l, c: l <= 0 || l >= 1 ? 0 : ceiling }));
60
+ }
61
+ export function resolveNeutralStep(step, hue, literals = generateNeutralRampLiterals()) {
62
+ const found = literals.find((s) => s.step === step);
63
+ if (!found)
64
+ throw new Error(`Neutral step ${step} is not in the shared step table.`);
65
+ return { l: found.l, c: found.c, h: hue };
66
+ }
67
+ export function neutralRcsFormula(step, literals) {
68
+ const found = literals.find((s) => s.step === step);
69
+ if (!found)
70
+ throw new Error(`Neutral step ${step} is not in the shared step table.`);
71
+ return `oklch(from var(--nave-color-tint) ${roundTo(found.l, 4)} ${roundTo(found.c, 4)} h)`;
72
+ }
73
+ function roundTo(n, d) {
74
+ const f = 10 ** d;
75
+ return Math.round(n * f) / f;
76
+ }