@navecss/tokens 0.1.1 → 0.2.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/README.md CHANGED
@@ -46,7 +46,7 @@ Every custom property the stylesheet declares is listed in [TOKENS.md](./TOKENS.
46
46
  | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
47
47
  | `@navecss/tokens` | JS entry point — the typed constants described under `@navecss/tokens/js` below. Not a CSS entry: `@import url('@navecss/tokens/css')` is the stylesheet |
48
48
  | `@navecss/tokens/css` | The generated stylesheet — `@import url('@navecss/tokens/css')` |
49
- | `@navecss/tokens/js` | Typed ESM token constants — the DTCG 2025.10 token set; every `--nave-color-*` custom property ships in the stylesheet above and not through this export, the `--nave-color-tint` seed included |
49
+ | `@navecss/tokens/js` | Typed ESM token constants — the DTCG 2025.10 token set; every `--nave-color-*` custom property (the `--nave-color-tint` seed included) ships as a value in the stylesheet above and not through this export, but its NAME is typed here as `ColorPropertyName`, exported alongside `TokenName`/`TokenValue` and disjoint from them |
50
50
  | `@navecss/tokens/breakpoints` | Breakpoint values, em conversions, media strings |
51
51
  | `@navecss/tokens/core-contract` | The shipped core-contract manifest, as JSON, for a token layer you generate yourself and load through `@navecss/core/no-tokens`: the `--nave-*` custom-property names `@navecss/core` reads, generated from that package's own source rather than listed by hand, and not the colour namespace alone. Import it with `with { type: 'json' }`. A clean check against it means those names are present rather than that your token set is complete; for what else it does and does not establish, read the header comment in `@navecss/core/no-tokens`. A validator reading the manifest directly should refuse a `formatVersion` higher than it understands, rather than mis-reading it. |
52
52
  | `@navecss/tokens/build` | The token build as a JavaScript API: `build` and `validate`, the two operations the `navecss-tokens` binary wraps |
@@ -2,7 +2,7 @@
2
2
  "formatVersion": 1,
3
3
  "producer": {
4
4
  "name": "@navecss/core",
5
- "version": "0.1.1"
5
+ "version": "0.2.0"
6
6
  },
7
7
  "tokens": [
8
8
  "--nave-border-width-focus",
package/dist/lib/bin.js CHANGED
@@ -20,11 +20,21 @@ function normalizationQualifier(seedNormalization) {
20
20
  const USAGE = 'Usage:\n' +
21
21
  ' navecss-tokens build --seed=<colour> --out=<dir> [--source=<file>] [--overrides=<file>]\n' +
22
22
  ' navecss-tokens validate --source=<file>';
23
+ function assertNoDuplicateFlags(tokens) {
24
+ const seen = new Set();
25
+ for (const token of tokens) {
26
+ if (token.kind !== 'option' || token.name === undefined)
27
+ continue;
28
+ if (seen.has(token.name)) {
29
+ throw new UsageError(`--${token.name} was given more than once; each flag accepts exactly one value.\n${USAGE}`);
30
+ }
31
+ seen.add(token.name);
32
+ }
33
+ }
23
34
  function parseBuildArgs(args) {
24
35
  let values;
25
36
  try {
26
- ;
27
- ({ values } = parseArgs({
37
+ const parsed = parseArgs({
28
38
  args,
29
39
  options: {
30
40
  out: { type: 'string' },
@@ -33,9 +43,14 @@ function parseBuildArgs(args) {
33
43
  source: { type: 'string' },
34
44
  },
35
45
  strict: true,
36
- }));
46
+ tokens: true,
47
+ });
48
+ assertNoDuplicateFlags(parsed.tokens);
49
+ values = parsed.values;
37
50
  }
38
51
  catch (error) {
52
+ if (error instanceof UsageError)
53
+ throw error;
39
54
  throw new UsageError(error.message);
40
55
  }
41
56
  if (!values.seed)
@@ -52,10 +67,18 @@ function parseBuildArgs(args) {
52
67
  function parseValidateArgs(args) {
53
68
  let values;
54
69
  try {
55
- ;
56
- ({ values } = parseArgs({ args, options: { source: { type: 'string' } }, strict: true }));
70
+ const parsed = parseArgs({
71
+ args,
72
+ options: { source: { type: 'string' } },
73
+ strict: true,
74
+ tokens: true,
75
+ });
76
+ assertNoDuplicateFlags(parsed.tokens);
77
+ values = parsed.values;
57
78
  }
58
79
  catch (error) {
80
+ if (error instanceof UsageError)
81
+ throw error;
59
82
  throw new UsageError(error.message);
60
83
  }
61
84
  if (!values.source)
@@ -6,8 +6,10 @@ import { detectCollidingNames, refuseOnCollision } from "./collision.js";
6
6
  import { detectVersionSkew } from "./core-version.js";
7
7
  import { composeDtcgOutputs } from "./dtcg-outputs.js";
8
8
  import { MissingContractTokensError, UsageError } from "./errors.js";
9
+ import { isOutPathConflict } from "./out-path-conflict.js";
9
10
  import { readOverrides } from "./overrides.js";
10
11
  import { findPackageRoot } from "./package-root.js";
12
+ import { readManifest } from "./read-manifest.js";
11
13
  import { formatOklch } from "./theming/color-math.js";
12
14
  import { composeConsumerBuild } from "./theming/consumer-build.js";
13
15
  import { validateAgainstManifest } from "./theming/core-contract.js";
@@ -20,10 +22,6 @@ export { DuplicateTokenNameRefusal, MissingContractTokensError, TokenCollisionRe
20
22
  export { SeedIngestRefusal } from "./theming/seed-ingest.js";
21
23
  export { formatVersionSkewFact } from "./validate-report.js";
22
24
  const PACKAGE_ROOT = findPackageRoot(import.meta.url);
23
- async function readManifest() {
24
- const raw = await readFile(path.join(PACKAGE_ROOT, 'dist', 'core-contract.json'), 'utf8');
25
- return JSON.parse(raw);
26
- }
27
25
  export async function build(options) {
28
26
  const sourcePath = options.source ?? path.join(PACKAGE_ROOT, 'tokens.json');
29
27
  if (!existsSync(sourcePath)) {
@@ -57,7 +55,15 @@ export async function build(options) {
57
55
  ? { ...file, content: `${file.content}\n${theming.css}` }
58
56
  : file);
59
57
  const allFiles = [...merged, ...theming.files];
60
- await writeOutputs(allFiles);
58
+ try {
59
+ await writeOutputs(allFiles);
60
+ }
61
+ catch (error) {
62
+ if (isOutPathConflict(error)) {
63
+ throw new UsageError(`could not write to --out="${options.outDir}": ${error.message}`);
64
+ }
65
+ throw error;
66
+ }
61
67
  return {
62
68
  resolvedSeed: formatOklch(theming.primaryRecord.usedSeed),
63
69
  seedNormalization: theming.primaryRecord.normalization,
@@ -1,3 +1,4 @@
1
+ import { shippedThemingPropertyNames } from "./theming/emit.js";
1
2
  const HEADER = '/* Nave Design System — generated, do not edit */';
2
3
  const LAYER_ORDER_STATEMENT = '@layer tokens.defaults, tokens.presets, reset, atomic, components.nave, components.consumer, overrides;';
3
4
  const PREFIX = '--nave-';
@@ -15,8 +16,10 @@ const isPublic = (token) => !token.path.includes('_primitive') && token.path[0]
15
16
  const isReducibleDuration = (token) => token.type === 'duration' &&
16
17
  token.path[0] === 'motion' &&
17
18
  token.name !== 'motion-duration-instant';
19
+ const isZIndexShaped = (token) => token.type === 'number' && token.path[0] === 'layer';
20
+ const atPropertySyntaxFor = (token) => isZIndexShaped(token) ? '<integer>' : AT_PROPERTY_SYNTAX[token.type];
18
21
  function isRegistrable(token) {
19
- const syntax = AT_PROPERTY_SYNTAX[token.type];
22
+ const syntax = atPropertySyntaxFor(token);
20
23
  if (syntax === undefined)
21
24
  return false;
22
25
  if (syntax !== '<length>')
@@ -26,7 +29,7 @@ function isRegistrable(token) {
26
29
  function buildAtProperty(token) {
27
30
  return [
28
31
  ` @property ${PREFIX}${token.name} {`,
29
- ` syntax: '${AT_PROPERTY_SYNTAX[token.type]}';`,
32
+ ` syntax: '${atPropertySyntaxFor(token)}';`,
30
33
  ` inherits: true;`,
31
34
  ` initial-value: ${token.value};`,
32
35
  ` }`,
@@ -93,8 +96,11 @@ export function formatJsTokens(tokens) {
93
96
  '',
94
97
  '/** The DTCG 2025.10 token set as a typed const object (e.g. `--nave-font-size-md`).',
95
98
  ' * Keys are CSS custom property names. The semantic colour layer',
96
- ' * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css`',
97
- ' * and is not part of this export.',
99
+ ' * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css` and is',
100
+ ' * not part of this object; its property NAMES are typed separately as',
101
+ ' * `ColorPropertyName`, and its VALUES are reachable only through CSS (e.g.',
102
+ ' * `var(--nave-color-action-primary)`): the browser resolves each one for the',
103
+ ' * active colour scheme, and the neutral-derived ones also follow the tint.',
98
104
  ' */',
99
105
  'export const tokens = {',
100
106
  entries,
@@ -107,13 +113,20 @@ export function formatTsDeclarations(tokens) {
107
113
  .filter((t) => isPublic(t))
108
114
  .map((t) => ` '${PREFIX}${t.name}': ${typeof t.value === 'number' ? 'number' : 'string'}`)
109
115
  .join('\n');
116
+ const colorPropertyNameMembers = [...shippedThemingPropertyNames()]
117
+ .toSorted((a, b) => a.localeCompare(b))
118
+ .map((name) => ` | '${name}'`)
119
+ .join('\n');
110
120
  return [
111
121
  HEADER,
112
122
  '',
113
123
  '/** The DTCG 2025.10 token set as a typed const object (e.g. `--nave-font-size-md`).',
114
124
  ' * Keys are CSS custom property names. The semantic colour layer',
115
- ' * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css`',
116
- ' * and is not part of this export.',
125
+ ' * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css` and is',
126
+ ' * not part of this object; its property NAMES are typed separately as',
127
+ ' * `ColorPropertyName`, and its VALUES are reachable only through CSS (e.g.',
128
+ ' * `var(--nave-color-action-primary)`): the browser resolves each one for the',
129
+ ' * active colour scheme, and the neutral-derived ones also follow the tint.',
117
130
  ' */',
118
131
  'export declare const tokens: {',
119
132
  keys,
@@ -122,6 +135,14 @@ export function formatTsDeclarations(tokens) {
122
135
  'export declare type TokenName = keyof typeof tokens',
123
136
  'export declare type TokenValue = typeof tokens[TokenName]',
124
137
  '',
138
+ '/** Every `--nave-color-*` custom property the semantic colour layer emits: names',
139
+ ' * only, no value (see the doc comment above `tokens` for where the values live).',
140
+ ' * Disjoint from `TokenName` — a consumer who wants both writes',
141
+ ' * `TokenName | ColorPropertyName`.',
142
+ ' */',
143
+ 'export declare type ColorPropertyName =',
144
+ colorPropertyNameMembers,
145
+ '',
125
146
  ].join('\n');
126
147
  }
127
148
  function buildBreakpointMap(tokens) {
@@ -0,0 +1,3 @@
1
+ export declare function isOutPathConflict(error: unknown): error is Error & {
2
+ code: string;
3
+ };
@@ -0,0 +1,7 @@
1
+ const OUT_PATH_CONFLICT_CODES = new Set(['EEXIST', 'EISDIR', 'ENOTDIR']);
2
+ export function isOutPathConflict(error) {
3
+ return (error instanceof Error &&
4
+ 'code' in error &&
5
+ typeof error.code === 'string' &&
6
+ OUT_PATH_CONFLICT_CODES.has(error.code));
7
+ }
@@ -0,0 +1,2 @@
1
+ import type { CoreContractManifest } from './theming/core-contract.ts';
2
+ export declare function readManifest(): Promise<CoreContractManifest>;
@@ -0,0 +1,17 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { UsageError } from "./errors.js";
4
+ import { findPackageRoot } from "./package-root.js";
5
+ const PACKAGE_ROOT = findPackageRoot(import.meta.url);
6
+ export async function readManifest() {
7
+ let raw;
8
+ try {
9
+ raw = await readFile(path.join(PACKAGE_ROOT, 'dist', 'core-contract.json'), 'utf8');
10
+ }
11
+ catch (error) {
12
+ throw new UsageError(`could not read this package's own dist/core-contract.json: ${error.message}. ` +
13
+ 'This is a broken install of @navecss/tokens, not something your build/validate ' +
14
+ 'arguments can fix — reinstall the package.');
15
+ }
16
+ return JSON.parse(raw);
17
+ }
@@ -5,4 +5,10 @@ export interface FlatToken {
5
5
  readonly type: string;
6
6
  readonly value: string | number;
7
7
  }
8
+ export interface RawToken {
9
+ path: string[];
10
+ ref?: string;
11
+ type: string;
12
+ rawValue: unknown;
13
+ }
8
14
  export declare function readTokens(source: unknown, sourceName?: string): FlatToken[];
@@ -1,14 +1,35 @@
1
- import { renderTokenValue } from "./composite-value.js";
2
1
  import { classifyMalformed, formatUnreadableNodesRefusal } from "./dtcg-malformed.js";
3
2
  import { classifyDraftShape, refuseTypeReason } from "./dtcg-shape.js";
4
3
  import { DtcgShapeRefusal } from "./errors.js";
5
- import { refuseBothReferenceForms, resolveTokenPointer } from "./json-pointer.js";
4
+ import { refuseBothReferenceForms } from "./json-pointer.js";
5
+ import { refuseUnsupportedReservedKey } from "./reserved-keys.js";
6
+ import { resolveValue } from "./resolve-value.js";
6
7
  import { nameFromPath } from "./token-name.js";
7
8
  export { kebabName } from "./token-name.js";
8
- const REF_RE = /^\{([^{}]+)\}$/;
9
+ const RESERVED_METADATA_KEYS = new Set([
10
+ '$deprecated',
11
+ '$description',
12
+ '$extensions',
13
+ '$ref',
14
+ '$type',
15
+ '$value',
16
+ ]);
9
17
  function isPlainObject(value) {
10
18
  return typeof value === 'object' && value !== null && !Array.isArray(value);
11
19
  }
20
+ function assertRecognisedDollarKey(key, at) {
21
+ if (RESERVED_METADATA_KEYS.has(key))
22
+ return;
23
+ refuseUnsupportedReservedKey(key, at);
24
+ throw new TypeError(`DTCG 2025.10 reader: "${at}" starts with "$", which DTCG 2025.10 ` +
25
+ 'reserves for metadata — it is not a valid token or group name');
26
+ }
27
+ function assertTokenDollarKeys(node, path) {
28
+ for (const key of Object.keys(node)) {
29
+ if (key.startsWith('$'))
30
+ assertRecognisedDollarKey(key, [...path, key].join('.'));
31
+ }
32
+ }
12
33
  function readTokenNode(node, path, inheritedType) {
13
34
  const hasValue = '$value' in node;
14
35
  const hasRef = typeof node.$ref === 'string';
@@ -17,6 +38,13 @@ function readTokenNode(node, path, inheritedType) {
17
38
  const at = path.join('.');
18
39
  if (hasValue && hasRef)
19
40
  refuseBothReferenceForms(at);
41
+ assertTokenDollarKeys(node, path);
42
+ const childKeys = Object.keys(node).filter((key) => !key.startsWith('$'));
43
+ if (childKeys.length > 0) {
44
+ throw new TypeError(`DTCG 2025.10 reader: "${at}" carries both a $value and child token(s) (${childKeys.join(', ')}) ` +
45
+ '— a node is either a token (a $value/$ref leaf) or a group (children only), never both; ' +
46
+ 'give the children their own group, separate from this token');
47
+ }
20
48
  const type = typeof node.$type === 'string' ? node.$type : inheritedType;
21
49
  if (type === undefined) {
22
50
  throw new TypeError(`DTCG 2025.10 reader: no resolvable $type for token "${at}"`);
@@ -41,8 +69,16 @@ function collectRawTokens(node, path, inheritedType, out) {
41
69
  }
42
70
  const groupType = typeof node.$type === 'string' ? node.$type : inheritedType;
43
71
  for (const [key, child] of Object.entries(node)) {
44
- if (key.startsWith('$'))
72
+ if (key.startsWith('$')) {
73
+ assertRecognisedDollarKey(key, [...path, key].join('.'));
45
74
  continue;
75
+ }
76
+ if (key.includes('.')) {
77
+ throw new TypeError(`DTCG 2025.10 reader: "${[...path, key].join('.')}" contains a literal "." in "${key}" ` +
78
+ "— dotted keys are ambiguous against this reader's own dotted alias paths " +
79
+ '(a group "a" -> "b" -> "c" and a group "a" with a key "b.c" both name the path ' +
80
+ '"a.b.c"), and the format has no other separator to disambiguate them');
81
+ }
46
82
  collectRawTokens(child, [...path, key], groupType, out);
47
83
  }
48
84
  }
@@ -81,43 +117,6 @@ function refuseUnreadableNodes(raw, sourceName) {
81
117
  throw new DtcgShapeRefusal(formatUnreadableNodesRefusal(drafts, malformed, sourceName), drafts);
82
118
  }
83
119
  }
84
- function resolveReference(key, targetPath, context) {
85
- if (context.visiting.has(key)) {
86
- throw new TypeError(`DTCG 2025.10 reader: reference cycle detected at "${key}" -> "${targetPath}"`);
87
- }
88
- const target = context.byPath.get(targetPath);
89
- if (!target) {
90
- throw new TypeError(`DTCG 2025.10 reader: "${key}" references unresolved target "${targetPath}"`);
91
- }
92
- context.visiting.add(key);
93
- const value = resolveValue(target, context);
94
- context.visiting.delete(key);
95
- return value;
96
- }
97
- function resolveValue(token, context) {
98
- const key = token.path.join('.');
99
- const cached = context.resolved.get(key);
100
- if (cached !== undefined)
101
- return cached;
102
- let value;
103
- if (token.ref !== undefined) {
104
- const targetPath = resolveTokenPointer(token.ref, key, (path) => context.byPath.has(path));
105
- value = resolveReference(key, targetPath, context);
106
- }
107
- else if (typeof token.rawValue === 'string' && REF_RE.test(token.rawValue)) {
108
- value = resolveReference(key, REF_RE.exec(token.rawValue)[1], context);
109
- }
110
- else if (typeof token.rawValue === 'string' &&
111
- (token.rawValue.includes('{') || token.rawValue.includes('}'))) {
112
- throw new TypeError(`DTCG 2025.10 reader: "${key}" embeds a reference inside a larger string ("${token.rawValue}") — ` +
113
- 'only a whole-value alias (curly braces around one dotted path, nothing else) is supported');
114
- }
115
- else {
116
- value = renderTokenValue(token.type, token.rawValue, key);
117
- }
118
- context.resolved.set(key, value);
119
- return value;
120
- }
121
120
  export function readTokens(source, sourceName) {
122
121
  const raw = [];
123
122
  collectRawTokens(source, [], undefined, raw);
@@ -126,6 +125,7 @@ export function readTokens(source, sourceName) {
126
125
  byPath: new Map(raw.map((t) => [t.path.join('.'), t])),
127
126
  resolved: new Map(),
128
127
  visiting: new Set(),
128
+ depth: 0,
129
129
  };
130
130
  return raw.map((token) => ({
131
131
  path: token.path,
@@ -0,0 +1 @@
1
+ export declare function refuseUnsupportedReservedKey(key: string, at: string): void;
@@ -0,0 +1,18 @@
1
+ const UNSUPPORTED_RESERVED_KEYS = [
2
+ {
3
+ key: '$root',
4
+ reason: 'DTCG 2025.10 §6.2 "Root Tokens in Groups". Move its $value to a sibling token with an ' +
5
+ 'ordinary name (for example "base") and alias that instead: {group.base}.',
6
+ },
7
+ {
8
+ key: '$extends',
9
+ reason: 'DTCG 2025.10 §6.4 "Extending Groups". Copy the extended group\'s tokens into this group, ' +
10
+ 'or alias them individually with {group.token}.',
11
+ },
12
+ ];
13
+ export function refuseUnsupportedReservedKey(key, at) {
14
+ const found = UNSUPPORTED_RESERVED_KEYS.find((entry) => entry.key === key);
15
+ if (found === undefined)
16
+ return;
17
+ throw new TypeError(`DTCG 2025.10 reader: "${at}" uses "${key}", which this reader does not support yet (${found.reason})`);
18
+ }
@@ -0,0 +1,8 @@
1
+ import type { RawToken } from './reader.ts';
2
+ export interface ResolveContext {
3
+ byPath: ReadonlyMap<string, RawToken>;
4
+ resolved: Map<string, string | number>;
5
+ visiting: Set<string>;
6
+ depth: number;
7
+ }
8
+ export declare function resolveValue(token: RawToken, context: ResolveContext): string | number;
@@ -0,0 +1,56 @@
1
+ import { renderTokenValue } from "./composite-value.js";
2
+ import { resolveTokenPointer } from "./json-pointer.js";
3
+ const REF_RE = /^\{([^{}]+)\}$/;
4
+ const MAX_ALIAS_CHAIN_DEPTH = 1000;
5
+ function resolveReference(key, targetPath, context) {
6
+ if (context.visiting.has(key)) {
7
+ throw new TypeError(`DTCG 2025.10 reader: reference cycle detected at "${key}" -> "${targetPath}"`);
8
+ }
9
+ const target = context.byPath.get(targetPath);
10
+ if (!target) {
11
+ throw new TypeError(`DTCG 2025.10 reader: "${key}" references unresolved target "${targetPath}"`);
12
+ }
13
+ if (context.depth >= MAX_ALIAS_CHAIN_DEPTH) {
14
+ throw new TypeError(`DTCG 2025.10 reader: "${key}" -> "${targetPath}" exceeds the maximum alias chain depth ` +
15
+ `(${MAX_ALIAS_CHAIN_DEPTH}); this reads as a generated or malformed source rather than an ` +
16
+ 'authored one');
17
+ }
18
+ context.visiting.add(key);
19
+ context.depth += 1;
20
+ const value = resolveValue(target, context);
21
+ context.depth -= 1;
22
+ context.visiting.delete(key);
23
+ return value;
24
+ }
25
+ function refuseUnsafeStringValue(key, rawValue) {
26
+ if (rawValue.includes(';') || rawValue.includes('/*')) {
27
+ throw new TypeError(`DTCG 2025.10 reader: "${key}" carries a ";" or a comment opener ("/*") in its value ` +
28
+ `("${rawValue}") — a raw string value is spliced directly into generated CSS, and ` +
29
+ 'either would splice a second declaration in beside it');
30
+ }
31
+ if (rawValue.includes('{') || rawValue.includes('}')) {
32
+ throw new TypeError(`DTCG 2025.10 reader: "${key}" embeds a reference inside a larger string ("${rawValue}") — ` +
33
+ 'only a whole-value alias (curly braces around one dotted path, nothing else) is supported');
34
+ }
35
+ }
36
+ export function resolveValue(token, context) {
37
+ const key = token.path.join('.');
38
+ const cached = context.resolved.get(key);
39
+ if (cached !== undefined)
40
+ return cached;
41
+ let value;
42
+ if (token.ref !== undefined) {
43
+ const targetPath = resolveTokenPointer(token.ref, key, (path) => context.byPath.has(path));
44
+ value = resolveReference(key, targetPath, context);
45
+ }
46
+ else if (typeof token.rawValue === 'string' && REF_RE.test(token.rawValue)) {
47
+ value = resolveReference(key, REF_RE.exec(token.rawValue)[1], context);
48
+ }
49
+ else {
50
+ if (typeof token.rawValue === 'string')
51
+ refuseUnsafeStringValue(key, token.rawValue);
52
+ value = renderTokenValue(token.type, token.rawValue, key);
53
+ }
54
+ context.resolved.set(key, value);
55
+ return value;
56
+ }
@@ -13,6 +13,8 @@ export function linesThatBeginInsideAnUnclosedComment(lines) {
13
13
  isInsideAnOpenComment = isLineStillInsideAnOpenComment(line, isInsideAnOpenComment, reading);
14
14
  }
15
15
  }
16
+ markLinesFlaggedByTheUrlOpaqueReading(lines, beginningsInsideAnUnclosedComment);
17
+ markLinesFlaggedByTheEscapedLineBreakReading(lines, beginningsInsideAnUnclosedComment);
16
18
  return beginningsInsideAnUnclosedComment;
17
19
  }
18
20
  const AS_THE_TOKENIZER_READS_IT = {
@@ -79,3 +81,190 @@ function indexAfterAStringLiteral(line, quoteIndex, quote) {
79
81
  }
80
82
  return -1;
81
83
  }
84
+ function markLinesFlaggedByTheUrlOpaqueReading(lines, into) {
85
+ let state = { isInsideAnOpenComment: false, urlCarry: 'none' };
86
+ for (const [index, line] of lines.entries()) {
87
+ if (state.isInsideAnOpenComment)
88
+ into.add(index);
89
+ state = scanLineWithOpaqueUrlContent(line, state);
90
+ }
91
+ }
92
+ function resumeUrlCarry(line, urlCarry) {
93
+ if (urlCarry === 'none')
94
+ return 0;
95
+ return urlCarry === 'pending' ? resolveUrlContent(line, 0) : indexAfterUnquotedUrlContent(line, 0);
96
+ }
97
+ function scanLineWithOpaqueUrlContent(line, state) {
98
+ const cursor = resumeUrlCarry(line, state.urlCarry);
99
+ if (typeof cursor === 'string')
100
+ return { isInsideAnOpenComment: false, urlCarry: cursor };
101
+ return scanLineForOpaqueUrlContentAndComments(line, state.isInsideAnOpenComment, cursor);
102
+ }
103
+ function scanLineForOpaqueUrlContentAndComments(line, wasInsideAnOpenComment, cursor) {
104
+ let isInsideAnOpenComment = wasInsideAnOpenComment;
105
+ let index = cursor;
106
+ while (index < line.length) {
107
+ if (isInsideAnOpenComment) {
108
+ const closerIndex = line.indexOf('*/', index);
109
+ if (closerIndex === -1)
110
+ return { isInsideAnOpenComment: true, urlCarry: 'none' };
111
+ isInsideAnOpenComment = false;
112
+ index = closerIndex + 2;
113
+ continue;
114
+ }
115
+ const urlLookup = tryToSkipAnUnquotedUrlToken(line, index);
116
+ if (typeof urlLookup === 'string')
117
+ return { isInsideAnOpenComment: false, urlCarry: urlLookup };
118
+ if (urlLookup !== undefined) {
119
+ index = urlLookup;
120
+ continue;
121
+ }
122
+ const character = line[index];
123
+ if (isAQuoteCharacter(character)) {
124
+ const afterTheString = indexAfterAStringLiteral(line, index, character);
125
+ index = afterTheString === -1 ? line.length : afterTheString;
126
+ continue;
127
+ }
128
+ if (line.startsWith('/*', index)) {
129
+ isInsideAnOpenComment = true;
130
+ index += 2;
131
+ continue;
132
+ }
133
+ index += 1;
134
+ }
135
+ return { isInsideAnOpenComment, urlCarry: 'none' };
136
+ }
137
+ function isAnIdentifierCharacter(character) {
138
+ return (character !== undefined && (/[A-Za-z0-9_-]/.test(character) || character.codePointAt(0) > 0x7f));
139
+ }
140
+ function tryToSkipAnUnquotedUrlToken(line, cursor) {
141
+ if (isAnIdentifierCharacter(line[cursor - 1]) || /[#@]/.test(line[cursor - 1]))
142
+ return undefined;
143
+ if (!/^url\(/i.test(line.slice(cursor, cursor + 4)))
144
+ return undefined;
145
+ return resolveUrlContent(line, cursor + 4);
146
+ }
147
+ function resolveUrlContent(line, cursor) {
148
+ let index = cursor;
149
+ while (index < line.length && /\s/.test(line[index]))
150
+ index += 1;
151
+ if (index === line.length)
152
+ return 'pending';
153
+ if (isAQuoteCharacter(line[index]))
154
+ return index;
155
+ return indexAfterUnquotedUrlContent(line, index);
156
+ }
157
+ function indexAfterUnquotedUrlContent(line, contentStart) {
158
+ let index = contentStart;
159
+ while (index < line.length) {
160
+ if (line[index] === '\\' && index + 1 < line.length) {
161
+ index += 2;
162
+ continue;
163
+ }
164
+ if (line[index] === ')')
165
+ return index + 1;
166
+ index += 1;
167
+ }
168
+ return 'contentOpen';
169
+ }
170
+ function markLinesFlaggedByTheEscapedLineBreakReading(lines, into) {
171
+ let state = NOTHING_IS_OPEN;
172
+ for (const [index, line] of lines.entries()) {
173
+ if (state.isInsideAnOpenComment)
174
+ into.add(index);
175
+ state = scanLineAcrossEscapedLineBreaksAndUrls(line, state);
176
+ }
177
+ }
178
+ const NOTHING_IS_OPEN = {
179
+ isInsideAnOpenComment: false,
180
+ urlCarry: 'none',
181
+ openStringQuote: undefined,
182
+ };
183
+ const STILL_INSIDE_AN_OPEN_COMMENT = {
184
+ ...NOTHING_IS_OPEN,
185
+ isInsideAnOpenComment: true,
186
+ };
187
+ function scanLineAcrossEscapedLineBreaksAndUrls(line, state) {
188
+ if (state.openStringQuote !== undefined) {
189
+ return resumeAnOpenStringCarriedIntoThisLine(line, state.isInsideAnOpenComment, state.openStringQuote);
190
+ }
191
+ const cursor = resumeUrlCarry(line, state.urlCarry);
192
+ if (typeof cursor === 'string')
193
+ return { ...NOTHING_IS_OPEN, urlCarry: cursor };
194
+ return scanFromStartOfLine(line, state.isInsideAnOpenComment, cursor);
195
+ }
196
+ function resumeAnOpenStringCarriedIntoThisLine(line, isInsideAnOpenComment, openStringQuote) {
197
+ const outcome = scanAPossiblyContinuedStringLiteral(line, -1, openStringQuote);
198
+ if (outcome.kind === 'continuesAcrossAnEscapedLineBreak') {
199
+ return { ...NOTHING_IS_OPEN, isInsideAnOpenComment, openStringQuote };
200
+ }
201
+ if (outcome.kind === 'doesNotClose') {
202
+ return { ...NOTHING_IS_OPEN, isInsideAnOpenComment };
203
+ }
204
+ return scanFromStartOfLine(line, isInsideAnOpenComment, outcome.indexAfter);
205
+ }
206
+ function stringStep(outcome, endOfLine) {
207
+ if (outcome.kind === 'closes')
208
+ return outcome.indexAfter;
209
+ if (outcome.kind === 'continuesAcrossAnEscapedLineBreak')
210
+ return undefined;
211
+ return endOfLine;
212
+ }
213
+ function scanFromStartOfLine(line, wasInsideAnOpenComment, cursor) {
214
+ let isInsideAnOpenComment = wasInsideAnOpenComment;
215
+ let index = cursor;
216
+ while (index < line.length) {
217
+ if (isInsideAnOpenComment) {
218
+ const closerIndex = line.indexOf('*/', index);
219
+ if (closerIndex === -1)
220
+ return STILL_INSIDE_AN_OPEN_COMMENT;
221
+ isInsideAnOpenComment = false;
222
+ index = closerIndex + 2;
223
+ continue;
224
+ }
225
+ const urlLookup = tryToSkipAnUnquotedUrlToken(line, index);
226
+ if (urlLookup !== undefined) {
227
+ if (typeof urlLookup === 'string')
228
+ return { ...NOTHING_IS_OPEN, urlCarry: urlLookup };
229
+ index = urlLookup;
230
+ continue;
231
+ }
232
+ const character = line[index];
233
+ if (isAQuoteCharacter(character)) {
234
+ const outcome = scanAPossiblyContinuedStringLiteral(line, index, character);
235
+ const target = stringStep(outcome, line.length);
236
+ if (target === undefined)
237
+ return { ...NOTHING_IS_OPEN, openStringQuote: character };
238
+ index = target;
239
+ continue;
240
+ }
241
+ if (line.startsWith('/*', index)) {
242
+ isInsideAnOpenComment = true;
243
+ index += 2;
244
+ continue;
245
+ }
246
+ index += 1;
247
+ }
248
+ return { ...NOTHING_IS_OPEN, isInsideAnOpenComment };
249
+ }
250
+ function scanAPossiblyContinuedStringLiteral(line, quoteIndex, quote) {
251
+ let cursor = quoteIndex + 1;
252
+ while (cursor < line.length) {
253
+ if (line[cursor] === '\\') {
254
+ if (isAnEscapedLineBreakAtEndOfLine(line, cursor)) {
255
+ return { kind: 'continuesAcrossAnEscapedLineBreak' };
256
+ }
257
+ cursor += 2;
258
+ continue;
259
+ }
260
+ if (line[cursor] === quote)
261
+ return { kind: 'closes', indexAfter: cursor + 1 };
262
+ cursor += 1;
263
+ }
264
+ return { kind: 'doesNotClose' };
265
+ }
266
+ function isAnEscapedLineBreakAtEndOfLine(line, cursor) {
267
+ if (cursor === line.length - 1)
268
+ return true;
269
+ return cursor === line.length - 2 && line[cursor + 1] === '\r';
270
+ }
@@ -68,7 +68,7 @@ export function neutralRcsFormula(step, literals) {
68
68
  const found = literals.find((s) => s.step === step);
69
69
  if (!found)
70
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)`;
71
+ return `oklch(from var(--nave-color-tint) ${roundTo(found.l, 4)} ${roundTo(found.c, 4)} h / 1)`;
72
72
  }
73
73
  function roundTo(n, d) {
74
74
  const f = 10 ** d;
@@ -1,7 +1,13 @@
1
1
  import { clamp01 } from "./seed-refusal.js";
2
- const CSS_WHITESPACE_EDGES = /^[ \t\n\r\f]+|[ \t\n\r\f]+$/g;
2
+ const CSS_WHITESPACE_CHARS = ' \t\n\r\f';
3
3
  function cssTrim(text) {
4
- return text.replaceAll(CSS_WHITESPACE_EDGES, '');
4
+ let start = 0;
5
+ let end = text.length;
6
+ while (start < end && CSS_WHITESPACE_CHARS.includes(text[start]))
7
+ start += 1;
8
+ while (end > start && CSS_WHITESPACE_CHARS.includes(text[end - 1]))
9
+ end -= 1;
10
+ return text.slice(start, end);
5
11
  }
6
12
  const CSS_NUMBER = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?$/;
7
13
  export function readNumericToken(text) {
@@ -22,7 +28,6 @@ function parseAlphaToken(token) {
22
28
  return clamp01(readNumericToken(t.slice(0, -1)) / 100);
23
29
  return clamp01(readNumericToken(t));
24
30
  }
25
- const CSS_WHITESPACE_CHARS = ' \t\n\r\f';
26
31
  function splitTopLevel(text, isSeparator) {
27
32
  const tokens = [];
28
33
  let current = '';
@@ -10,11 +10,25 @@ export function detectSourceKind(filePath) {
10
10
  return 'json';
11
11
  return undefined;
12
12
  }
13
- const DECLARED_CUSTOM_PROPERTY_RE = /(--[a-zA-Z0-9-]+)\s*:/g;
14
- const CSS_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
13
+ const DECLARED_CUSTOM_PROPERTY_RE = /(?<![\w\u{80}-\u{10FFFF}-])(--[a-zA-Z0-9-]+)\s*:/gu;
14
+ function stripCssComments(css) {
15
+ let kept = '';
16
+ let from = 0;
17
+ for (;;) {
18
+ const open = css.indexOf('/*', from);
19
+ if (open === -1)
20
+ break;
21
+ const close = css.indexOf('*/', open + 2);
22
+ if (close === -1)
23
+ break;
24
+ kept += css.slice(from, open);
25
+ from = close + 2;
26
+ }
27
+ return kept + css.slice(from);
28
+ }
15
29
  export function scanDeclaredCustomProperties(css) {
16
30
  const found = new Set();
17
- const declarations = css.replaceAll(CSS_COMMENT_RE, '');
31
+ const declarations = stripCssComments(css);
18
32
  for (const match of declarations.matchAll(DECLARED_CUSTOM_PROPERTY_RE))
19
33
  found.add(match[1]);
20
34
  return found;
@@ -2,88 +2,88 @@
2
2
  "aboutThisFile": "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
3
  "color.surface.base.light": {
4
4
  "$type": "color",
5
- "$value": "oklch(from var(--nave-color-tint) 1 0 h)"
5
+ "$value": "oklch(from var(--nave-color-tint) 1 0 h / 1)"
6
6
  },
7
7
  "color.surface.base.dark": {
8
8
  "$type": "color",
9
- "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h)"
9
+ "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1)"
10
10
  },
11
11
  "color.surface.raised.light": {
12
12
  "$type": "color",
13
- "$value": "oklch(from var(--nave-color-tint) 1 0 h)"
13
+ "$value": "oklch(from var(--nave-color-tint) 1 0 h / 1)"
14
14
  },
15
15
  "color.surface.raised.dark": {
16
16
  "$type": "color",
17
- "$value": "oklch(from var(--nave-color-tint) 0.225 0.0067 h)"
17
+ "$value": "oklch(from var(--nave-color-tint) 0.225 0.0067 h / 1)"
18
18
  },
19
19
  "color.surface.overlay.light": {
20
20
  "$type": "color",
21
- "$value": "oklch(from var(--nave-color-tint) 1 0 h)"
21
+ "$value": "oklch(from var(--nave-color-tint) 1 0 h / 1)"
22
22
  },
23
23
  "color.surface.overlay.dark": {
24
24
  "$type": "color",
25
- "$value": "oklch(from var(--nave-color-tint) 0.27 0.0067 h)"
25
+ "$value": "oklch(from var(--nave-color-tint) 0.27 0.0067 h / 1)"
26
26
  },
27
27
  "color.surface.sunken.light": {
28
28
  "$type": "color",
29
- "$value": "oklch(from var(--nave-color-tint) 0.985 0.0067 h)"
29
+ "$value": "oklch(from var(--nave-color-tint) 0.985 0.0067 h / 1)"
30
30
  },
31
31
  "color.surface.sunken.dark": {
32
32
  "$type": "color",
33
- "$value": "oklch(from var(--nave-color-tint) 0.12 0.0067 h)"
33
+ "$value": "oklch(from var(--nave-color-tint) 0.12 0.0067 h / 1)"
34
34
  },
35
35
  "color.surface.inverse.light": {
36
36
  "$type": "color",
37
- "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h)"
37
+ "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1)"
38
38
  },
39
39
  "color.surface.inverse.dark": {
40
40
  "$type": "color",
41
- "$value": "oklch(from var(--nave-color-tint) 0.965 0.0067 h)"
41
+ "$value": "oklch(from var(--nave-color-tint) 0.965 0.0067 h / 1)"
42
42
  },
43
43
  "color.content.primary.light": {
44
44
  "$type": "color",
45
- "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h)"
45
+ "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1)"
46
46
  },
47
47
  "color.content.primary.dark": {
48
48
  "$type": "color",
49
- "$value": "oklch(from var(--nave-color-tint) 0.965 0.0067 h)"
49
+ "$value": "oklch(from var(--nave-color-tint) 0.965 0.0067 h / 1)"
50
50
  },
51
51
  "color.content.secondary.light": {
52
52
  "$type": "color",
53
- "$value": "oklch(from var(--nave-color-tint) 0.37 0.0067 h)"
53
+ "$value": "oklch(from var(--nave-color-tint) 0.37 0.0067 h / 1)"
54
54
  },
55
55
  "color.content.secondary.dark": {
56
56
  "$type": "color",
57
- "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h)"
57
+ "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1)"
58
58
  },
59
59
  "color.content.tertiary.light": {
60
60
  "$type": "color",
61
- "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h)",
61
+ "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1)",
62
62
  "$description": "Placeholder and hint text only."
63
63
  },
64
64
  "color.content.tertiary.dark": {
65
65
  "$type": "color",
66
- "$value": "oklch(from var(--nave-color-tint) 0.72 0.0067 h)",
66
+ "$value": "oklch(from var(--nave-color-tint) 0.72 0.0067 h / 1)",
67
67
  "$description": "Placeholder and hint text only."
68
68
  },
69
69
  "color.content.disabled.light": {
70
70
  "$type": "color",
71
- "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h)",
71
+ "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1)",
72
72
  "$description": "Text inside a disabled interactive component."
73
73
  },
74
74
  "color.content.disabled.dark": {
75
75
  "$type": "color",
76
- "$value": "oklch(from var(--nave-color-tint) 0.37 0.0067 h)",
76
+ "$value": "oklch(from var(--nave-color-tint) 0.37 0.0067 h / 1)",
77
77
  "$description": "Text inside a disabled interactive component."
78
78
  },
79
79
  "color.content.inverse.light": {
80
80
  "$type": "color",
81
- "$value": "oklch(from var(--nave-color-tint) 0.965 0.0067 h)",
81
+ "$value": "oklch(from var(--nave-color-tint) 0.965 0.0067 h / 1)",
82
82
  "$description": "Declared foreground for surface.inverse."
83
83
  },
84
84
  "color.content.inverse.dark": {
85
85
  "$type": "color",
86
- "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h)",
86
+ "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1)",
87
87
  "$description": "Declared foreground for surface.inverse."
88
88
  },
89
89
  "color.content.link.light": {
@@ -98,39 +98,39 @@
98
98
  },
99
99
  "color.border.default.light": {
100
100
  "$type": "color",
101
- "$value": "oklch(from var(--nave-color-tint) 0.905 0.0067 h)",
101
+ "$value": "oklch(from var(--nave-color-tint) 0.905 0.0067 h / 1)",
102
102
  "$description": "Decorative dividers and card edges."
103
103
  },
104
104
  "color.border.default.dark": {
105
105
  "$type": "color",
106
- "$value": "oklch(from var(--nave-color-tint) 0.37 0.0067 h)",
106
+ "$value": "oklch(from var(--nave-color-tint) 0.37 0.0067 h / 1)",
107
107
  "$description": "Decorative dividers and card edges."
108
108
  },
109
109
  "color.border.strong.light": {
110
110
  "$type": "color",
111
- "$value": "oklch(from var(--nave-color-tint) 0.72 0.0067 h)"
111
+ "$value": "oklch(from var(--nave-color-tint) 0.72 0.0067 h / 1)"
112
112
  },
113
113
  "color.border.strong.dark": {
114
114
  "$type": "color",
115
- "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h)"
115
+ "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1)"
116
116
  },
117
117
  "color.border.control.light": {
118
118
  "$type": "color",
119
- "$value": "oklch(from var(--nave-color-tint) 0.59 0.0067 h)",
119
+ "$value": "oklch(from var(--nave-color-tint) 0.59 0.0067 h / 1)",
120
120
  "$description": "Identifying boundary for a control (inputs, form boundaries, and the secondary action control)."
121
121
  },
122
122
  "color.border.control.dark": {
123
123
  "$type": "color",
124
- "$value": "oklch(from var(--nave-color-tint) 0.59 0.0067 h)",
124
+ "$value": "oklch(from var(--nave-color-tint) 0.59 0.0067 h / 1)",
125
125
  "$description": "Identifying boundary for a control (inputs, form boundaries, and the secondary action control)."
126
126
  },
127
127
  "color.border.disabled.light": {
128
128
  "$type": "color",
129
- "$value": "oklch(from var(--nave-color-tint) 0.94 0.0067 h)"
129
+ "$value": "oklch(from var(--nave-color-tint) 0.94 0.0067 h / 1)"
130
130
  },
131
131
  "color.border.disabled.dark": {
132
132
  "$type": "color",
133
- "$value": "oklch(from var(--nave-color-tint) 0.27 0.0067 h)"
133
+ "$value": "oklch(from var(--nave-color-tint) 0.27 0.0067 h / 1)"
134
134
  },
135
135
  "color.border.focus.light": {
136
136
  "$type": "color",
@@ -166,19 +166,19 @@
166
166
  },
167
167
  "color.on-action.primary.light": {
168
168
  "$type": "color",
169
- "$value": "oklch(from var(--nave-color-tint) 1 0 h)"
169
+ "$value": "oklch(from var(--nave-color-tint) 1 0 h / 1)"
170
170
  },
171
171
  "color.on-action.primary.dark": {
172
172
  "$type": "color",
173
- "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h)"
173
+ "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1)"
174
174
  },
175
175
  "color.action.secondary.light": {
176
176
  "$type": "color",
177
- "$value": "oklch(from var(--nave-color-tint) 0.965 0.0067 h)"
177
+ "$value": "oklch(from var(--nave-color-tint) 0.965 0.0067 h / 1)"
178
178
  },
179
179
  "color.action.secondary.dark": {
180
180
  "$type": "color",
181
- "$value": "oklch(from var(--nave-color-tint) 0.27 0.0067 h)"
181
+ "$value": "oklch(from var(--nave-color-tint) 0.27 0.0067 h / 1)"
182
182
  },
183
183
  "color.feedback.danger.light": {
184
184
  "$type": "color",
@@ -202,92 +202,92 @@
202
202
  },
203
203
  "color.on-feedback.danger.light": {
204
204
  "$type": "color",
205
- "$value": "oklch(from var(--nave-color-tint) 1 0 h)",
205
+ "$value": "oklch(from var(--nave-color-tint) 1 0 h / 1)",
206
206
  "$description": "Declared foreground for the solid feedback.danger background."
207
207
  },
208
208
  "color.on-feedback.danger.dark": {
209
209
  "$type": "color",
210
- "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h)",
210
+ "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1)",
211
211
  "$description": "Declared foreground for the solid feedback.danger background."
212
212
  },
213
213
  "color.feedback.warning.light": {
214
214
  "$type": "color",
215
- "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h)",
215
+ "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1)",
216
216
  "$description": "Solid-background role for a warning state (shared tinted-neutral family)."
217
217
  },
218
218
  "color.feedback.warning.dark": {
219
219
  "$type": "color",
220
- "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h)",
220
+ "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1)",
221
221
  "$description": "Solid-background role for a warning state (shared tinted-neutral family)."
222
222
  },
223
223
  "color.feedback.warning.foreground.light": {
224
224
  "$type": "color",
225
- "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h)",
225
+ "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1)",
226
226
  "$description": "Foreground role for warning text and icons."
227
227
  },
228
228
  "color.feedback.warning.foreground.dark": {
229
229
  "$type": "color",
230
- "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h)",
230
+ "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1)",
231
231
  "$description": "Foreground role for warning text and icons."
232
232
  },
233
233
  "color.on-feedback.warning.light": {
234
234
  "$type": "color",
235
- "$value": "oklch(from var(--nave-color-tint) 1 0 h)"
235
+ "$value": "oklch(from var(--nave-color-tint) 1 0 h / 1)"
236
236
  },
237
237
  "color.on-feedback.warning.dark": {
238
238
  "$type": "color",
239
- "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h)"
239
+ "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1)"
240
240
  },
241
241
  "color.feedback.success.light": {
242
242
  "$type": "color",
243
- "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h)",
243
+ "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1)",
244
244
  "$description": "Alias of feedback.warning (shared tinted-neutral family)."
245
245
  },
246
246
  "color.feedback.success.dark": {
247
247
  "$type": "color",
248
- "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h)",
248
+ "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1)",
249
249
  "$description": "Alias of feedback.warning (shared tinted-neutral family)."
250
250
  },
251
251
  "color.feedback.success.foreground.light": {
252
252
  "$type": "color",
253
- "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h)"
253
+ "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1)"
254
254
  },
255
255
  "color.feedback.success.foreground.dark": {
256
256
  "$type": "color",
257
- "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h)"
257
+ "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1)"
258
258
  },
259
259
  "color.on-feedback.success.light": {
260
260
  "$type": "color",
261
- "$value": "oklch(from var(--nave-color-tint) 1 0 h)"
261
+ "$value": "oklch(from var(--nave-color-tint) 1 0 h / 1)"
262
262
  },
263
263
  "color.on-feedback.success.dark": {
264
264
  "$type": "color",
265
- "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h)"
265
+ "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1)"
266
266
  },
267
267
  "color.feedback.info.light": {
268
268
  "$type": "color",
269
- "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h)",
269
+ "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1)",
270
270
  "$description": "Alias of feedback.warning (shared tinted-neutral family)."
271
271
  },
272
272
  "color.feedback.info.dark": {
273
273
  "$type": "color",
274
- "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h)",
274
+ "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1)",
275
275
  "$description": "Alias of feedback.warning (shared tinted-neutral family)."
276
276
  },
277
277
  "color.feedback.info.foreground.light": {
278
278
  "$type": "color",
279
- "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h)"
279
+ "$value": "oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1)"
280
280
  },
281
281
  "color.feedback.info.foreground.dark": {
282
282
  "$type": "color",
283
- "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h)"
283
+ "$value": "oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1)"
284
284
  },
285
285
  "color.on-feedback.info.light": {
286
286
  "$type": "color",
287
- "$value": "oklch(from var(--nave-color-tint) 1 0 h)"
287
+ "$value": "oklch(from var(--nave-color-tint) 1 0 h / 1)"
288
288
  },
289
289
  "color.on-feedback.info.dark": {
290
290
  "$type": "color",
291
- "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h)"
291
+ "$value": "oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1)"
292
292
  }
293
293
  }
package/dist/tokens.css CHANGED
@@ -216,43 +216,43 @@
216
216
  }
217
217
 
218
218
  @property --nave-layer-base {
219
- syntax: '<number>';
219
+ syntax: '<integer>';
220
220
  inherits: true;
221
221
  initial-value: 0;
222
222
  }
223
223
 
224
224
  @property --nave-layer-dropdown {
225
- syntax: '<number>';
225
+ syntax: '<integer>';
226
226
  inherits: true;
227
227
  initial-value: 100;
228
228
  }
229
229
 
230
230
  @property --nave-layer-sticky {
231
- syntax: '<number>';
231
+ syntax: '<integer>';
232
232
  inherits: true;
233
233
  initial-value: 200;
234
234
  }
235
235
 
236
236
  @property --nave-layer-overlay {
237
- syntax: '<number>';
237
+ syntax: '<integer>';
238
238
  inherits: true;
239
239
  initial-value: 300;
240
240
  }
241
241
 
242
242
  @property --nave-layer-modal {
243
- syntax: '<number>';
243
+ syntax: '<integer>';
244
244
  inherits: true;
245
245
  initial-value: 400;
246
246
  }
247
247
 
248
248
  @property --nave-layer-toast {
249
- syntax: '<number>';
249
+ syntax: '<integer>';
250
250
  inherits: true;
251
251
  initial-value: 500;
252
252
  }
253
253
 
254
254
  @property --nave-layer-tooltip {
255
- syntax: '<number>';
255
+ syntax: '<integer>';
256
256
  inherits: true;
257
257
  initial-value: 600;
258
258
  }
@@ -395,39 +395,39 @@
395
395
  color-scheme: light dark;
396
396
  /* Tint seed: Changing this changes everything Nave derives from it. The contrast of the resulting palette follows from what you set, and checking it is yours. */
397
397
  --nave-color-tint: oklch(0.7859 0.1316 186.17);
398
- --nave-color-surface-base: light-dark(oklch(from var(--nave-color-tint) 1 0 h), oklch(from var(--nave-color-tint) 0.18 0.0067 h));
399
- --nave-color-surface-raised: light-dark(oklch(from var(--nave-color-tint) 1 0 h), oklch(from var(--nave-color-tint) 0.225 0.0067 h));
400
- --nave-color-surface-overlay: light-dark(oklch(from var(--nave-color-tint) 1 0 h), oklch(from var(--nave-color-tint) 0.27 0.0067 h));
401
- --nave-color-surface-sunken: light-dark(oklch(from var(--nave-color-tint) 0.985 0.0067 h), oklch(from var(--nave-color-tint) 0.12 0.0067 h));
402
- --nave-color-surface-inverse: light-dark(oklch(from var(--nave-color-tint) 0.18 0.0067 h), oklch(from var(--nave-color-tint) 0.965 0.0067 h));
403
- --nave-color-content-primary: light-dark(oklch(from var(--nave-color-tint) 0.18 0.0067 h), oklch(from var(--nave-color-tint) 0.965 0.0067 h));
404
- --nave-color-content-secondary: light-dark(oklch(from var(--nave-color-tint) 0.37 0.0067 h), oklch(from var(--nave-color-tint) 0.835 0.0067 h));
405
- --nave-color-content-tertiary: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h), oklch(from var(--nave-color-tint) 0.72 0.0067 h));
406
- --nave-color-content-disabled: light-dark(oklch(from var(--nave-color-tint) 0.835 0.0067 h), oklch(from var(--nave-color-tint) 0.37 0.0067 h));
407
- --nave-color-content-inverse: light-dark(oklch(from var(--nave-color-tint) 0.965 0.0067 h), oklch(from var(--nave-color-tint) 0.18 0.0067 h));
398
+ --nave-color-surface-base: light-dark(oklch(from var(--nave-color-tint) 1 0 h / 1), oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1));
399
+ --nave-color-surface-raised: light-dark(oklch(from var(--nave-color-tint) 1 0 h / 1), oklch(from var(--nave-color-tint) 0.225 0.0067 h / 1));
400
+ --nave-color-surface-overlay: light-dark(oklch(from var(--nave-color-tint) 1 0 h / 1), oklch(from var(--nave-color-tint) 0.27 0.0067 h / 1));
401
+ --nave-color-surface-sunken: light-dark(oklch(from var(--nave-color-tint) 0.985 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.12 0.0067 h / 1));
402
+ --nave-color-surface-inverse: light-dark(oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.965 0.0067 h / 1));
403
+ --nave-color-content-primary: light-dark(oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.965 0.0067 h / 1));
404
+ --nave-color-content-secondary: light-dark(oklch(from var(--nave-color-tint) 0.37 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1));
405
+ --nave-color-content-tertiary: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.72 0.0067 h / 1));
406
+ --nave-color-content-disabled: light-dark(oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.37 0.0067 h / 1));
407
+ --nave-color-content-inverse: light-dark(oklch(from var(--nave-color-tint) 0.965 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1));
408
408
  --nave-color-content-link: light-dark(oklch(0.47 0.0748 186.17), oklch(0.835 0.123 186.17));
409
- --nave-color-border-default: light-dark(oklch(from var(--nave-color-tint) 0.905 0.0067 h), oklch(from var(--nave-color-tint) 0.37 0.0067 h));
410
- --nave-color-border-strong: light-dark(oklch(from var(--nave-color-tint) 0.72 0.0067 h), oklch(from var(--nave-color-tint) 0.47 0.0067 h));
411
- --nave-color-border-control: light-dark(oklch(from var(--nave-color-tint) 0.59 0.0067 h), oklch(from var(--nave-color-tint) 0.59 0.0067 h));
412
- --nave-color-border-disabled: light-dark(oklch(from var(--nave-color-tint) 0.94 0.0067 h), oklch(from var(--nave-color-tint) 0.27 0.0067 h));
409
+ --nave-color-border-default: light-dark(oklch(from var(--nave-color-tint) 0.905 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.37 0.0067 h / 1));
410
+ --nave-color-border-strong: light-dark(oklch(from var(--nave-color-tint) 0.72 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1));
411
+ --nave-color-border-control: light-dark(oklch(from var(--nave-color-tint) 0.59 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.59 0.0067 h / 1));
412
+ --nave-color-border-disabled: light-dark(oklch(from var(--nave-color-tint) 0.94 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.27 0.0067 h / 1));
413
413
  --nave-color-border-focus: light-dark(oklch(0.59 0.0939 186.17), oklch(0.59 0.0939 186.17));
414
414
  --nave-color-action-primary: light-dark(oklch(0.47 0.0748 186.17), oklch(0.835 0.123 186.17));
415
415
  --nave-color-action-primary-hover: light-dark(oklch(0.37 0.0589 186.17), oklch(0.905 0.1106 186.17));
416
416
  --nave-color-action-primary-active: light-dark(oklch(0.27 0.0398 186.17), oklch(0.94 0.0574 186.17));
417
- --nave-color-on-action-primary: light-dark(oklch(from var(--nave-color-tint) 1 0 h), oklch(from var(--nave-color-tint) 0.18 0.0067 h));
418
- --nave-color-action-secondary: light-dark(oklch(from var(--nave-color-tint) 0.965 0.0067 h), oklch(from var(--nave-color-tint) 0.27 0.0067 h));
417
+ --nave-color-on-action-primary: light-dark(oklch(from var(--nave-color-tint) 1 0 h / 1), oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1));
418
+ --nave-color-action-secondary: light-dark(oklch(from var(--nave-color-tint) 0.965 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.27 0.0067 h / 1));
419
419
  --nave-color-feedback-danger: light-dark(oklch(0.47 0.1455 15.02), oklch(0.835 0.0661 15.02));
420
420
  --nave-color-feedback-danger-foreground: light-dark(oklch(0.47 0.1455 15.02), oklch(0.835 0.0661 15.02));
421
- --nave-color-on-feedback-danger: light-dark(oklch(from var(--nave-color-tint) 1 0 h), oklch(from var(--nave-color-tint) 0.18 0.0067 h));
421
+ --nave-color-on-feedback-danger: light-dark(oklch(from var(--nave-color-tint) 1 0 h / 1), oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1));
422
422
  /* Feedback tokens: warning, success and info, and their -foreground variants, resolve to one shared value here; only danger differs. Colour alone therefore cannot tell those three states apart, so wherever a feedback token carries meaning, give it a text label naming the state and a distinct icon or shape that differs per state. */
423
- --nave-color-feedback-warning: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h), oklch(from var(--nave-color-tint) 0.835 0.0067 h));
424
- --nave-color-feedback-warning-foreground: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h), oklch(from var(--nave-color-tint) 0.835 0.0067 h));
425
- --nave-color-on-feedback-warning: light-dark(oklch(from var(--nave-color-tint) 1 0 h), oklch(from var(--nave-color-tint) 0.18 0.0067 h));
426
- --nave-color-feedback-success: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h), oklch(from var(--nave-color-tint) 0.835 0.0067 h));
427
- --nave-color-feedback-success-foreground: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h), oklch(from var(--nave-color-tint) 0.835 0.0067 h));
428
- --nave-color-on-feedback-success: light-dark(oklch(from var(--nave-color-tint) 1 0 h), oklch(from var(--nave-color-tint) 0.18 0.0067 h));
429
- --nave-color-feedback-info: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h), oklch(from var(--nave-color-tint) 0.835 0.0067 h));
430
- --nave-color-feedback-info-foreground: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h), oklch(from var(--nave-color-tint) 0.835 0.0067 h));
431
- --nave-color-on-feedback-info: light-dark(oklch(from var(--nave-color-tint) 1 0 h), oklch(from var(--nave-color-tint) 0.18 0.0067 h));
423
+ --nave-color-feedback-warning: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1));
424
+ --nave-color-feedback-warning-foreground: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1));
425
+ --nave-color-on-feedback-warning: light-dark(oklch(from var(--nave-color-tint) 1 0 h / 1), oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1));
426
+ --nave-color-feedback-success: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1));
427
+ --nave-color-feedback-success-foreground: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1));
428
+ --nave-color-on-feedback-success: light-dark(oklch(from var(--nave-color-tint) 1 0 h / 1), oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1));
429
+ --nave-color-feedback-info: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1));
430
+ --nave-color-feedback-info-foreground: light-dark(oklch(from var(--nave-color-tint) 0.47 0.0067 h / 1), oklch(from var(--nave-color-tint) 0.835 0.0067 h / 1));
431
+ --nave-color-on-feedback-info: light-dark(oklch(from var(--nave-color-tint) 1 0 h / 1), oklch(from var(--nave-color-tint) 0.18 0.0067 h / 1));
432
432
  }
433
433
  }
package/dist/tokens.d.ts CHANGED
@@ -2,8 +2,11 @@
2
2
 
3
3
  /** The DTCG 2025.10 token set as a typed const object (e.g. `--nave-font-size-md`).
4
4
  * Keys are CSS custom property names. The semantic colour layer
5
- * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css`
6
- * and is not part of this export.
5
+ * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css` and is
6
+ * not part of this object; its property NAMES are typed separately as
7
+ * `ColorPropertyName`, and its VALUES are reachable only through CSS (e.g.
8
+ * `var(--nave-color-action-primary)`): the browser resolves each one for the
9
+ * active colour scheme, and the neutral-derived ones also follow the tint.
7
10
  */
8
11
  export declare const tokens: {
9
12
  '--nave-font-family-base': string
@@ -88,3 +91,44 @@ export declare const tokens: {
88
91
 
89
92
  export declare type TokenName = keyof typeof tokens
90
93
  export declare type TokenValue = typeof tokens[TokenName]
94
+
95
+ /** Every `--nave-color-*` custom property the semantic colour layer emits: names
96
+ * only, no value (see the doc comment above `tokens` for where the values live).
97
+ * Disjoint from `TokenName` — a consumer who wants both writes
98
+ * `TokenName | ColorPropertyName`.
99
+ */
100
+ export declare type ColorPropertyName =
101
+ | '--nave-color-action-primary'
102
+ | '--nave-color-action-primary-active'
103
+ | '--nave-color-action-primary-hover'
104
+ | '--nave-color-action-secondary'
105
+ | '--nave-color-border-control'
106
+ | '--nave-color-border-default'
107
+ | '--nave-color-border-disabled'
108
+ | '--nave-color-border-focus'
109
+ | '--nave-color-border-strong'
110
+ | '--nave-color-content-disabled'
111
+ | '--nave-color-content-inverse'
112
+ | '--nave-color-content-link'
113
+ | '--nave-color-content-primary'
114
+ | '--nave-color-content-secondary'
115
+ | '--nave-color-content-tertiary'
116
+ | '--nave-color-feedback-danger'
117
+ | '--nave-color-feedback-danger-foreground'
118
+ | '--nave-color-feedback-info'
119
+ | '--nave-color-feedback-info-foreground'
120
+ | '--nave-color-feedback-success'
121
+ | '--nave-color-feedback-success-foreground'
122
+ | '--nave-color-feedback-warning'
123
+ | '--nave-color-feedback-warning-foreground'
124
+ | '--nave-color-on-action-primary'
125
+ | '--nave-color-on-feedback-danger'
126
+ | '--nave-color-on-feedback-info'
127
+ | '--nave-color-on-feedback-success'
128
+ | '--nave-color-on-feedback-warning'
129
+ | '--nave-color-surface-base'
130
+ | '--nave-color-surface-inverse'
131
+ | '--nave-color-surface-overlay'
132
+ | '--nave-color-surface-raised'
133
+ | '--nave-color-surface-sunken'
134
+ | '--nave-color-tint'
package/dist/tokens.js CHANGED
@@ -3,8 +3,11 @@
3
3
 
4
4
  /** The DTCG 2025.10 token set as a typed const object (e.g. `--nave-font-size-md`).
5
5
  * Keys are CSS custom property names. The semantic colour layer
6
- * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css`
7
- * and is not part of this export.
6
+ * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css` and is
7
+ * not part of this object; its property NAMES are typed separately as
8
+ * `ColorPropertyName`, and its VALUES are reachable only through CSS (e.g.
9
+ * `var(--nave-color-action-primary)`): the browser resolves each one for the
10
+ * active colour scheme, and the neutral-derived ones also follow the tint.
8
11
  */
9
12
  export const tokens = {
10
13
  '--nave-font-family-base': "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navecss/tokens",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "DTCG 2025.10 design tokens for Nave: seed-derived semantic colours, typography, spacing, and the rest of the token layer, built by a first-party DTCG 2025.10 reader.",
5
5
  "keywords": [
6
6
  "css",
@@ -53,7 +53,7 @@
53
53
  "devDependencies": {
54
54
  "@types/node": "^26.6.2",
55
55
  "@vitest/coverage-v8": "5.0.1",
56
- "prettier": "3.9.8",
56
+ "prettier": "3.9.9",
57
57
  "typescript": "6.0.3",
58
58
  "vitest": "5.0.1"
59
59
  },