@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,11 @@
1
+ import { type Adjacency } from './adjacency-source.ts';
2
+ import { type SlotMapping } from './semantics.ts';
3
+ export declare function deriveOnStarAnchors(adjacency: readonly Adjacency[]): Record<string, string>;
4
+ export declare const ON_STAR_ANCHORS: Record<string, string>;
5
+ export interface OnStarSource {
6
+ mapping: Record<string, SlotMapping>;
7
+ branchMapping: Record<string, SlotMapping>;
8
+ anchors: Record<string, string>;
9
+ adjacency?: readonly Adjacency[];
10
+ }
11
+ export declare function assertOnStarShape(source?: OnStarSource): void;
@@ -0,0 +1,87 @@
1
+ import { ADJACENCY } from "./adjacency-source.js";
2
+ import { ACHROMATIC_MAPPING, CHROMATIC_MAPPING } from "./semantics.js";
3
+ const STATE_OR_FOREGROUND_SUFFIX_RE = /\.(hover|active|foreground)$/;
4
+ function familyAnchorFor(subject) {
5
+ return subject.slice('on-'.length);
6
+ }
7
+ export function deriveOnStarAnchors(adjacency) {
8
+ const anchors = {};
9
+ for (const { subject, against } of adjacency) {
10
+ if (!subject.startsWith('on-'))
11
+ continue;
12
+ if (STATE_OR_FOREGROUND_SUFFIX_RE.test(against))
13
+ continue;
14
+ if (against !== familyAnchorFor(subject))
15
+ continue;
16
+ anchors[subject] = against;
17
+ }
18
+ return anchors;
19
+ }
20
+ export const ON_STAR_ANCHORS = deriveOnStarAnchors(ADJACENCY);
21
+ const SHIPPED_ON_STAR_SOURCE = () => ({
22
+ mapping: CHROMATIC_MAPPING,
23
+ branchMapping: ACHROMATIC_MAPPING,
24
+ anchors: ON_STAR_ANCHORS,
25
+ adjacency: ADJACENCY,
26
+ });
27
+ function restingPartnersBySubject(adjacency) {
28
+ const resting = new Map();
29
+ for (const { subject, against } of adjacency) {
30
+ if (!subject.startsWith('on-'))
31
+ continue;
32
+ if (STATE_OR_FOREGROUND_SUFFIX_RE.test(against))
33
+ continue;
34
+ const candidates = resting.get(subject) ?? [];
35
+ if (!candidates.includes(against))
36
+ candidates.push(against);
37
+ resting.set(subject, candidates);
38
+ }
39
+ return resting;
40
+ }
41
+ function assertOnStarAnchorsResolve(adjacency, anchors) {
42
+ for (const [subject, candidates] of restingPartnersBySubject(adjacency)) {
43
+ if (Object.hasOwn(anchors, subject))
44
+ continue;
45
+ const listed = candidates.map((name) => `"${name}"`).join(' and ');
46
+ throw new Error(`on-* anchor check: "${subject}" declares the resting partner(s) ${listed}, none of ` +
47
+ `which is "${familyAnchorFor(subject)}" — its own family's solid-background role, the ` +
48
+ 'only anchor an on-* foreground can take. Declare that pair, or rename the ' +
49
+ 'subject to the family it is the foreground of.');
50
+ }
51
+ }
52
+ function assertOnStarSlot(slot, source) {
53
+ const anchor = source.anchors[slot];
54
+ if (anchor === undefined) {
55
+ throw new Error(`on-* anchor check: ${slot} names no anchor in ON_STAR_ANCHORS.`);
56
+ }
57
+ if (STATE_OR_FOREGROUND_SUFFIX_RE.test(anchor)) {
58
+ throw new Error(`on-* anchor check: ${slot}'s anchor "${anchor}" is a state or a foreground role, ` +
59
+ 'not the resting solid-background it must be.');
60
+ }
61
+ if (!Object.hasOwn(source.mapping, anchor)) {
62
+ throw new Error(`on-* anchor check: ${slot}'s anchor "${anchor}" is not a name the build emits.`);
63
+ }
64
+ for (const mapping of [source.mapping[slot], source.branchMapping[slot]]) {
65
+ if (mapping && ('alias' in mapping.light || 'alias' in mapping.dark)) {
66
+ throw new Error(`on-* anchor check: ${slot}'s value is composed or aliased, not a whole declaration.`);
67
+ }
68
+ }
69
+ }
70
+ function assertNoAliasingOfOnStarSlots(mapping) {
71
+ for (const [name, slotMapping] of Object.entries(mapping)) {
72
+ for (const branch of ['light', 'dark']) {
73
+ const ref = slotMapping[branch];
74
+ if ('alias' in ref && ref.alias.startsWith('on-')) {
75
+ throw new Error(`on-* anchor check: ${name} aliases the on-* slot "${ref.alias}".`);
76
+ }
77
+ }
78
+ }
79
+ }
80
+ export function assertOnStarShape(source = SHIPPED_ON_STAR_SOURCE()) {
81
+ if (source.adjacency)
82
+ assertOnStarAnchorsResolve(source.adjacency, source.anchors);
83
+ const onStarSlots = Object.keys(source.mapping).filter((name) => name.startsWith('on-'));
84
+ for (const slot of onStarSlots)
85
+ assertOnStarSlot(slot, source);
86
+ assertNoAliasingOfOnStarSlots(source.mapping);
87
+ }
@@ -0,0 +1,2 @@
1
+ import type { PipelineResult } from './pipeline.ts';
2
+ export declare function composePaletteRecord(result: PipelineResult): Record<string, unknown>;
@@ -0,0 +1,15 @@
1
+ import { SLOT_DESCRIPTIONS } from "./descriptions.js";
2
+ const PALETTE_RECORD_NOTE = 'This file is a resolved palette record: what this build produced for every semantic colour slot, in both schemes. It is not a DTCG 2025.10 document, and it is not meant to be read by a DTCG 2025.10 tool, because its keys are flat dotted names and most of its values are relative-colour-syntax strings, neither of which that format expresses. The token document is tokens.json, shipped beside it.';
3
+ const PALETTE_RECORD_NOTE_KEY = 'aboutThisFile';
4
+ export function composePaletteRecord(result) {
5
+ const record = { [PALETTE_RECORD_NOTE_KEY]: PALETTE_RECORD_NOTE };
6
+ for (const slot of result.slots) {
7
+ const description = SLOT_DESCRIPTIONS.get(slot.slot);
8
+ record[`color.${slot.slot}.${slot.branch}`] = {
9
+ $type: 'color',
10
+ $value: slot.css,
11
+ ...(description !== undefined && { $description: description }),
12
+ };
13
+ }
14
+ return record;
15
+ }
@@ -0,0 +1,43 @@
1
+ import type { Oklch } from './color-math.ts';
2
+ import { type NeutralStep } from './neutral.ts';
3
+ import { type Ramp, type SeedRecord } from './ramp.ts';
4
+ export interface Seeds {
5
+ primary: Oklch;
6
+ danger: Oklch;
7
+ declaredTintHue: number;
8
+ }
9
+ export declare const OVERRIDE_SCALE_NAMES: readonly ["danger", "neutral", "primary"];
10
+ export type PerStepOverrides = Partial<Record<(typeof OVERRIDE_SCALE_NAMES)[number], Record<number, number>>>;
11
+ interface PipelineRecords {
12
+ primary: SeedRecord;
13
+ danger: SeedRecord;
14
+ achromaticBranch: boolean;
15
+ tintHue: number;
16
+ }
17
+ export interface ResolvedSlot {
18
+ slot: string;
19
+ branch: 'dark' | 'light';
20
+ resolved: {
21
+ scale: 'danger' | 'neutral' | 'primary';
22
+ step: number;
23
+ };
24
+ literal: Oklch;
25
+ css: string;
26
+ }
27
+ export interface PipelineResult {
28
+ records: PipelineRecords;
29
+ primaryRamp: Ramp;
30
+ dangerRamp: Ramp;
31
+ neutralLiterals: readonly NeutralStep[];
32
+ slots: readonly ResolvedSlot[];
33
+ actionSecondaryForeground: {
34
+ dark: ResolvedSlot;
35
+ light: ResolvedSlot;
36
+ };
37
+ actionSecondaryBorder: {
38
+ dark: ResolvedSlot;
39
+ light: ResolvedSlot;
40
+ };
41
+ }
42
+ export declare function runPipeline(seeds: Seeds, overrides?: PerStepOverrides, env?: ReadonlyMap<number, number>): PipelineResult;
43
+ export {};
@@ -0,0 +1,104 @@
1
+ import { formatOklch } from "./color-math.js";
2
+ import { computeNeutralChromaCeiling, generateNeutralRampLiterals, neutralRcsFormula, resolveNeutralStep, } from "./neutral.js";
3
+ import { DEFAULT_ENV, generateRamp, rampStep } from "./ramp.js";
4
+ import { ACTION_SECONDARY_BORDER, ACTION_SECONDARY_FOREGROUND, resolveSlotMapping, SEMANTIC_SLOTS, } from "./semantics.js";
5
+ export const OVERRIDE_SCALE_NAMES = ['danger', 'neutral', 'primary'];
6
+ function resolveStepRef(ref, branch, isAchromaticBranch) {
7
+ let current = ref;
8
+ for (let depth = 0; depth < 10; depth++) {
9
+ if (!('alias' in current))
10
+ return current;
11
+ const target = resolveSlotMapping(current.alias, isAchromaticBranch);
12
+ current = target[branch];
13
+ }
14
+ throw new Error('Alias chain too deep — likely a cycle in the semantic mapping.');
15
+ }
16
+ function cssForResolved(resolved, ctx) {
17
+ if (resolved.scale === 'neutral') {
18
+ const colour = resolveNeutralStep(resolved.step, ctx.tintHue, ctx.neutralLiterals);
19
+ return { css: neutralRcsFormula(resolved.step, ctx.neutralLiterals), literal: colour };
20
+ }
21
+ const ramp = resolved.scale === 'primary' ? ctx.primaryRamp : ctx.dangerRamp;
22
+ const colour = rampStep(ramp, resolved.step);
23
+ return { css: formatOklch(colour), literal: colour };
24
+ }
25
+ function resolveOne(slot, ref, branch, ctx) {
26
+ const resolved = resolveStepRef(ref, branch, ctx.isAchromaticBranch);
27
+ const { css, literal } = cssForResolved(resolved, ctx);
28
+ return { slot, branch, resolved, css, literal };
29
+ }
30
+ function applyOverrides(steps, overrides) {
31
+ if (!overrides)
32
+ return steps;
33
+ return steps.map((s) => {
34
+ const override = overrides[s.step];
35
+ return override === undefined ? s : { ...s, c: override };
36
+ });
37
+ }
38
+ function generateRamps(seeds, overrides, env) {
39
+ const primaryGenerated = generateRamp(seeds.primary, env);
40
+ const dangerGenerated = generateRamp(seeds.danger, env);
41
+ const finalPrimaryRamp = {
42
+ ...primaryGenerated,
43
+ steps: applyOverrides(primaryGenerated.steps, overrides.primary),
44
+ };
45
+ const finalDangerRamp = {
46
+ ...dangerGenerated,
47
+ steps: applyOverrides(dangerGenerated.steps, overrides.danger),
48
+ };
49
+ const neutralLiterals = applyOverrides(generateNeutralRampLiterals(computeNeutralChromaCeiling()), overrides.neutral);
50
+ const isAchromaticBranch = finalPrimaryRamp.seedRecord.achromatic;
51
+ const tintHue = isAchromaticBranch
52
+ ? seeds.declaredTintHue
53
+ : finalPrimaryRamp.seedRecord.usedSeed.h;
54
+ return {
55
+ primaryRamp: finalPrimaryRamp,
56
+ dangerRamp: finalDangerRamp,
57
+ neutralLiterals,
58
+ tintHue,
59
+ isAchromaticBranch,
60
+ };
61
+ }
62
+ function resolveAllSlots(ctx) {
63
+ const slots = [];
64
+ for (const slotName of SEMANTIC_SLOTS) {
65
+ const mapping = resolveSlotMapping(slotName, ctx.isAchromaticBranch);
66
+ for (const branch of ['light', 'dark']) {
67
+ slots.push(resolveOne(slotName, mapping[branch], branch, ctx));
68
+ }
69
+ }
70
+ return slots;
71
+ }
72
+ function resolveActionSecondaryCompanions(ctx) {
73
+ const fgRef = { alias: ACTION_SECONDARY_FOREGROUND };
74
+ const borderRef = { alias: ACTION_SECONDARY_BORDER };
75
+ return {
76
+ actionSecondaryForeground: {
77
+ light: resolveOne('action.secondary.foreground', fgRef, 'light', ctx),
78
+ dark: resolveOne('action.secondary.foreground', fgRef, 'dark', ctx),
79
+ },
80
+ actionSecondaryBorder: {
81
+ light: resolveOne('action.secondary.border', borderRef, 'light', ctx),
82
+ dark: resolveOne('action.secondary.border', borderRef, 'dark', ctx),
83
+ },
84
+ };
85
+ }
86
+ export function runPipeline(seeds, overrides = {}, env = DEFAULT_ENV) {
87
+ const ctx = generateRamps(seeds, overrides, env);
88
+ const records = {
89
+ primary: ctx.primaryRamp.seedRecord,
90
+ danger: ctx.dangerRamp.seedRecord,
91
+ achromaticBranch: ctx.isAchromaticBranch,
92
+ tintHue: ctx.tintHue,
93
+ };
94
+ const slots = resolveAllSlots(ctx);
95
+ const companions = resolveActionSecondaryCompanions(ctx);
96
+ return {
97
+ records,
98
+ primaryRamp: ctx.primaryRamp,
99
+ dangerRamp: ctx.dangerRamp,
100
+ neutralLiterals: ctx.neutralLiterals,
101
+ slots,
102
+ ...companions,
103
+ };
104
+ }
@@ -0,0 +1,34 @@
1
+ import { type Oklch, type SeedNormalization } from './color-math.ts';
2
+ export declare const DEFAULT_ENV: ReadonlyMap<number, number>;
3
+ export declare const ENV_CEILING = 0.95;
4
+ export declare const ADVERSARIAL_SEEDS: readonly {
5
+ name: string;
6
+ seed: Oklch;
7
+ }[];
8
+ export declare const S_BAND: {
9
+ readonly lo: 0.2;
10
+ readonly hi: 0.9;
11
+ };
12
+ export interface SeedRecord {
13
+ normalized: boolean;
14
+ normalization: SeedNormalization;
15
+ bandClamped: boolean;
16
+ usedSeed: Oklch;
17
+ sLightness: number;
18
+ s: number;
19
+ achromatic: boolean;
20
+ }
21
+ export declare function decomposeSeed(seed: Oklch): SeedRecord;
22
+ interface RampStep {
23
+ step: number;
24
+ l: number;
25
+ c: number;
26
+ h: number;
27
+ }
28
+ export interface Ramp {
29
+ seedRecord: SeedRecord;
30
+ steps: readonly RampStep[];
31
+ }
32
+ export declare function generateRamp(seed: Oklch, env?: ReadonlyMap<number, number>): Ramp;
33
+ export declare function rampStep(ramp: Ramp, step: number): Oklch;
34
+ export {};
@@ -0,0 +1,92 @@
1
+ import { isOklchInGamut, maxChroma, normalizeSeed, } from "./color-math.js";
2
+ import { STEP_TABLE, stepLightness } from "./step-table.js";
3
+ export const DEFAULT_ENV = new Map([
4
+ [0, 0],
5
+ [50, 0.35],
6
+ [100, 0.55],
7
+ [150, 0.68],
8
+ [200, 0.78],
9
+ [300, 0.88],
10
+ [400, 0.93],
11
+ [500, 0.95],
12
+ [600, 0.95],
13
+ [700, 0.95],
14
+ [800, 0.88],
15
+ [850, 0.78],
16
+ [900, 0.62],
17
+ [950, 0.4],
18
+ [1000, 0],
19
+ ]);
20
+ export const ENV_CEILING = 0.95;
21
+ export const ADVERSARIAL_SEEDS = [
22
+ { name: 'red', seed: { l: 0.6, c: 0.15, h: 25 } },
23
+ { name: 'blue', seed: { l: 0.55, c: 0.15, h: 260 } },
24
+ { name: 'violet', seed: { l: 0.55, c: 0.15, h: 300 } },
25
+ { name: 'yellow-green', seed: { l: 0.7, c: 0.15, h: 110 } },
26
+ ];
27
+ export const S_BAND = { lo: 0.2, hi: 0.9 };
28
+ export function decomposeSeed(seed) {
29
+ const { seed: used, normalization, normalized } = normalizeSeed(seed);
30
+ const isAchromatic = used.c === 0;
31
+ if (isAchromatic) {
32
+ return {
33
+ normalized,
34
+ normalization,
35
+ bandClamped: false,
36
+ usedSeed: used,
37
+ sLightness: used.l,
38
+ s: 0,
39
+ achromatic: true,
40
+ };
41
+ }
42
+ const isBandClamped = used.l < S_BAND.lo || used.l > S_BAND.hi;
43
+ const sLightness = isBandClamped ? Math.min(Math.max(used.l, S_BAND.lo), S_BAND.hi) : used.l;
44
+ const denom = maxChroma(sLightness, used.h);
45
+ const s = denom === 0 ? 0 : Math.min(Math.max(used.c / denom, 0), 1);
46
+ return {
47
+ normalized,
48
+ normalization,
49
+ bandClamped: isBandClamped,
50
+ usedSeed: used,
51
+ sLightness,
52
+ s,
53
+ achromatic: false,
54
+ };
55
+ }
56
+ export function generateRamp(seed, env = DEFAULT_ENV) {
57
+ const seedRecord = decomposeSeed(seed);
58
+ const hue = seedRecord.achromatic ? 0 : seedRecord.usedSeed.h;
59
+ const steps = STEP_TABLE.map(({ step, l }) => {
60
+ const envValue = env.get(step);
61
+ if (envValue === undefined) {
62
+ throw new Error(`The environment curve has no value for step ${step}. Open an issue.`);
63
+ }
64
+ if (envValue > ENV_CEILING) {
65
+ throw new Error(`env(${step}) = ${envValue} exceeds the hard invariant ceiling of ${ENV_CEILING}. ` +
66
+ 'Raising the 0.95 ceiling is a gamut change, not a curve edit: open an issue.');
67
+ }
68
+ if (seedRecord.achromatic) {
69
+ return { step, l, c: 0, h: 0 };
70
+ }
71
+ const cap = maxChroma(l, hue);
72
+ const c = seedRecord.s * envValue * cap;
73
+ const permitted = ENV_CEILING * cap;
74
+ if (c > permitted + 1e-9) {
75
+ throw new Error(`The scale at hue ${hue} step ${step} requested chroma ${c}, which exceeds the ` +
76
+ `permitted ceiling ${permitted} (0.95 * maxChroma(${l}, ${hue})). Open an issue ` +
77
+ 'rather than raising the ceiling here.');
78
+ }
79
+ if (!isOklchInGamut({ l, c, h: hue })) {
80
+ throw new Error(`Step ${step} of the scale at hue ${hue} is out of sRGB gamut. Open an issue.`);
81
+ }
82
+ return { step, l, c, h: hue };
83
+ });
84
+ return { seedRecord, steps };
85
+ }
86
+ export function rampStep(ramp, step) {
87
+ stepLightness(step);
88
+ const found = ramp.steps.find((s) => s.step === step);
89
+ if (!found)
90
+ throw new Error(`Step ${step} missing from generated ramp.`);
91
+ return { l: found.l, c: found.c, h: found.h };
92
+ }
@@ -0,0 +1,8 @@
1
+ export declare function readNumericToken(text: string): number;
2
+ export declare function isNoneKeyword(text: string): boolean;
3
+ export declare function splitChannelArgs(args: string): {
4
+ alpha: number | undefined;
5
+ isLegacySyntax: boolean;
6
+ parts: string[];
7
+ };
8
+ export declare function parseHueDeg(token: string): number;
@@ -0,0 +1,99 @@
1
+ import { clamp01 } from "./seed-refusal.js";
2
+ const CSS_WHITESPACE_EDGES = /^[ \t\n\r\f]+|[ \t\n\r\f]+$/g;
3
+ function cssTrim(text) {
4
+ return text.replaceAll(CSS_WHITESPACE_EDGES, '');
5
+ }
6
+ const CSS_NUMBER = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?$/;
7
+ export function readNumericToken(text) {
8
+ if (text.trim() === '')
9
+ return NaN;
10
+ if (/\s/u.test(text))
11
+ return NaN;
12
+ if (!CSS_NUMBER.test(text))
13
+ return NaN;
14
+ return Number(text);
15
+ }
16
+ export function isNoneKeyword(text) {
17
+ return /^none$/i.test(text);
18
+ }
19
+ function parseAlphaToken(token) {
20
+ const t = cssTrim(token);
21
+ if (t.endsWith('%'))
22
+ return clamp01(readNumericToken(t.slice(0, -1)) / 100);
23
+ return clamp01(readNumericToken(t));
24
+ }
25
+ const CSS_WHITESPACE_CHARS = ' \t\n\r\f';
26
+ function splitTopLevel(text, isSeparator) {
27
+ const tokens = [];
28
+ let current = '';
29
+ let depth = 0;
30
+ for (const ch of text) {
31
+ if (ch === '(')
32
+ depth += 1;
33
+ else if (ch === ')')
34
+ depth = Math.max(0, depth - 1);
35
+ if (depth === 0 && isSeparator(ch)) {
36
+ if (current !== '')
37
+ tokens.push(current);
38
+ current = '';
39
+ continue;
40
+ }
41
+ current += ch;
42
+ }
43
+ if (current !== '')
44
+ tokens.push(current);
45
+ return tokens;
46
+ }
47
+ function lastTopLevelSlashIndex(text) {
48
+ let depth = 0;
49
+ let lastIndex = -1;
50
+ for (const [i, ch] of text.split('').entries()) {
51
+ if (ch === '(')
52
+ depth += 1;
53
+ else if (ch === ')')
54
+ depth = Math.max(0, depth - 1);
55
+ else if (ch === '/' && depth === 0)
56
+ lastIndex = i;
57
+ }
58
+ return lastIndex;
59
+ }
60
+ export function splitChannelArgs(args) {
61
+ const trimmed = cssTrim(args);
62
+ const slashIdx = lastTopLevelSlashIndex(trimmed);
63
+ let main = trimmed;
64
+ let alpha;
65
+ if (slashIdx !== -1) {
66
+ main = cssTrim(trimmed.slice(0, slashIdx));
67
+ alpha = parseAlphaToken(trimmed.slice(slashIdx + 1));
68
+ }
69
+ const commaParts = splitTopLevel(main, (ch) => ch === ',');
70
+ const isLegacySyntax = commaParts.length > 1;
71
+ let parts;
72
+ if (isLegacySyntax) {
73
+ parts = commaParts.map((p) => cssTrim(p)).filter(Boolean);
74
+ if (alpha === undefined && parts.length === 4) {
75
+ alpha = parseAlphaToken(parts.pop());
76
+ }
77
+ }
78
+ else {
79
+ parts = splitTopLevel(main, (ch) => CSS_WHITESPACE_CHARS.includes(ch));
80
+ }
81
+ return { alpha, isLegacySyntax, parts };
82
+ }
83
+ const ANGLE_UNITS = [
84
+ { toDeg: 0.9, unit: 'grad' },
85
+ { toDeg: 360, unit: 'turn' },
86
+ { toDeg: 180 / Math.PI, unit: 'rad' },
87
+ { toDeg: 1, unit: 'deg' },
88
+ ];
89
+ export function parseHueDeg(token) {
90
+ if (isNoneKeyword(token))
91
+ return 0;
92
+ const lower = token.toLowerCase();
93
+ for (const { toDeg, unit } of ANGLE_UNITS) {
94
+ if (lower.endsWith(unit)) {
95
+ return readNumericToken(token.slice(0, token.length - unit.length)) * toDeg;
96
+ }
97
+ }
98
+ return readNumericToken(token);
99
+ }
@@ -0,0 +1,8 @@
1
+ import type { Oklch } from './color-math.ts';
2
+ import type { ColorFunctionSpace } from './seed-input.ts';
3
+ export declare function ingestLab(input: string, args: string): Oklch;
4
+ export declare function ingestLch(input: string, args: string): Oklch;
5
+ export declare function ingestColor(input: string, args: string): {
6
+ space: ColorFunctionSpace;
7
+ value: Oklch;
8
+ };
@@ -0,0 +1,85 @@
1
+ import { srgbToOklch } from "./color-math.js";
2
+ import { displayP3ToOklch, labToOklch, lchToOklch } from "./css-color-4.js";
3
+ import { isNoneKeyword, parseHueDeg, readNumericToken, splitChannelArgs, } from "./seed-channel-split.js";
4
+ import { badChannelValue, checkOpaqueAlpha, unsupportedColourSpace } from "./seed-refusal.js";
5
+ const COLOR_FUNCTION_SPACES = new Set([
6
+ 'display-p3',
7
+ 'srgb',
8
+ ]);
9
+ function parseLabLightness(token) {
10
+ if (isNoneKeyword(token))
11
+ return 0;
12
+ if (token.endsWith('%'))
13
+ return readNumericToken(token.slice(0, -1));
14
+ return readNumericToken(token);
15
+ }
16
+ function parseScaledChannel(token, percentReference) {
17
+ if (isNoneKeyword(token))
18
+ return 0;
19
+ if (token.endsWith('%'))
20
+ return (readNumericToken(token.slice(0, -1)) / 100) * percentReference;
21
+ return readNumericToken(token);
22
+ }
23
+ function parseColorChannel(token) {
24
+ if (isNoneKeyword(token))
25
+ return 0;
26
+ if (token.endsWith('%'))
27
+ return readNumericToken(token.slice(0, -1)) / 100;
28
+ return readNumericToken(token);
29
+ }
30
+ function clampCieLightness(l) {
31
+ return Math.min(100, Math.max(0, l));
32
+ }
33
+ function readThreeChannels(input, form, parts) {
34
+ if (parts.length !== 3) {
35
+ throw badChannelValue(input, form, `it needs three channels and this has ${parts.length}.`);
36
+ }
37
+ return [parts[0], parts[1], parts[2]];
38
+ }
39
+ function checkFiniteChannels(input, form, channels) {
40
+ if (channels.some((n) => !Number.isFinite(n))) {
41
+ throw badChannelValue(input, form, 'one or more of its channel values could not be read as a number.');
42
+ }
43
+ }
44
+ export function ingestLab(input, args) {
45
+ const { alpha, parts } = splitChannelArgs(args);
46
+ const [lToken, aToken, bToken] = readThreeChannels(input, 'lab()', parts);
47
+ const l = parseLabLightness(lToken);
48
+ const a = parseScaledChannel(aToken, 125);
49
+ const b = parseScaledChannel(bToken, 125);
50
+ checkFiniteChannels(input, 'lab()', [l, a, b]);
51
+ checkOpaqueAlpha(input, alpha, 'lab()');
52
+ return labToOklch({ a, b, l: clampCieLightness(l) });
53
+ }
54
+ export function ingestLch(input, args) {
55
+ const { alpha, parts } = splitChannelArgs(args);
56
+ const [lToken, cToken, hToken] = readThreeChannels(input, 'lch()', parts);
57
+ const l = parseLabLightness(lToken);
58
+ const c = parseScaledChannel(cToken, 150);
59
+ const h = parseHueDeg(hToken);
60
+ checkFiniteChannels(input, 'lch()', [l, c, h]);
61
+ checkOpaqueAlpha(input, alpha, 'lch()');
62
+ return lchToOklch({ c: Math.max(0, c), h, l: clampCieLightness(l) });
63
+ }
64
+ export function ingestColor(input, args) {
65
+ const { alpha, parts } = splitChannelArgs(args);
66
+ const spaceToken = parts[0];
67
+ if (spaceToken === undefined) {
68
+ throw badChannelValue(input, 'color()', 'it needs a colour space and three channels and this has neither.');
69
+ }
70
+ const space = spaceToken.toLowerCase();
71
+ if (!COLOR_FUNCTION_SPACES.has(space))
72
+ throw unsupportedColourSpace(input, spaceToken);
73
+ const [rToken, gToken, bToken] = readThreeChannels(input, 'color()', parts.slice(1));
74
+ const rgb = {
75
+ b: parseColorChannel(bToken),
76
+ g: parseColorChannel(gToken),
77
+ r: parseColorChannel(rToken),
78
+ };
79
+ checkFiniteChannels(input, 'color()', [rgb.r, rgb.g, rgb.b]);
80
+ checkOpaqueAlpha(input, alpha, 'color()');
81
+ return {
82
+ space: space,
83
+ value: space === 'srgb' ? srgbToOklch(rgb) : displayP3ToOklch(rgb),
84
+ };
85
+ }
@@ -0,0 +1,6 @@
1
+ import type { Oklch } from './color-math.ts';
2
+ export declare const RECOGNISED_ALIASED_FUNCTIONS: Record<string, 'hsl' | 'oklch' | 'rgb'>;
3
+ export declare function ingestHex(input: string, digits: string): Oklch;
4
+ export declare function ingestRgb(input: string, args: string): Oklch;
5
+ export declare function ingestHsl(input: string, args: string): Oklch;
6
+ export declare function ingestOklch(input: string, args: string): Oklch;