@ttoss/fsl-theme 1.1.13 → 1.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Types-BiBa17RL.d.cts +1427 -0
- package/dist/Types-BiBa17RL.d.mts +1427 -0
- package/dist/baseBundle-DxvXyhGa.mjs +17 -0
- package/dist/baseBundle-iEFf5nqT.cjs +22 -0
- package/dist/{esm/chunk-SE5Z52RE.js → createTheme-BLNYztZU.mjs} +76 -172
- package/dist/createTheme-Cv6RP9D6.cjs +1825 -0
- package/dist/css.cjs +48 -0
- package/dist/{css.d.ts → css.d.cts} +67 -63
- package/dist/css.d.mts +168 -0
- package/dist/css.mjs +42 -0
- package/dist/dataviz/index.cjs +45 -0
- package/dist/dataviz/{index.d.ts → index.d.cts} +9 -5
- package/dist/dataviz/index.d.mts +66 -0
- package/dist/dataviz/index.mjs +39 -0
- package/dist/dtcg.cjs +115 -0
- package/dist/{dtcg.d.ts → dtcg.d.cts} +9 -7
- package/dist/dtcg.d.mts +51 -0
- package/dist/dtcg.mjs +112 -0
- package/dist/helpers-4p4-QVt_.cjs +258 -0
- package/dist/helpers-CaswNJMy.mjs +211 -0
- package/dist/{index.d.ts → index-CsIjfw86.d.cts} +42 -34
- package/dist/index-nJrjI0BA.d.mts +94 -0
- package/dist/index.cjs +16 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +7 -0
- package/dist/{react.d.ts → react-CGa6FlNL.d.cts} +130 -106
- package/dist/react-DnKxR2gK.d.mts +370 -0
- package/dist/react-EUwpdvY7.cjs +481 -0
- package/dist/react.cjs +12 -0
- package/dist/react.d.cts +4 -0
- package/dist/react.d.mts +4 -0
- package/dist/react.mjs +412 -0
- package/dist/runtime-entry.cjs +9 -0
- package/dist/runtime-entry.d.cts +3 -0
- package/dist/runtime-entry.d.mts +3 -0
- package/dist/runtime-entry.mjs +3 -0
- package/dist/{runtime-entry.d.ts → ssrScript-BVysxDws.d.cts} +26 -23
- package/dist/ssrScript-BVysxDws.d.mts +98 -0
- package/dist/ssrScript-CRfrN8Pm.cjs +202 -0
- package/dist/ssrScript-D3kGPQpi.mjs +179 -0
- package/dist/themes/bruttal.cjs +75 -0
- package/dist/themes/bruttal.d.cts +3 -0
- package/dist/themes/bruttal.d.mts +3 -0
- package/dist/themes/bruttal.mjs +72 -0
- package/dist/themes/corporate.cjs +34 -0
- package/dist/themes/corporate.d.cts +3 -0
- package/dist/themes/corporate.d.mts +3 -0
- package/dist/{esm/chunk-TPMN75JM.js → themes/corporate.mjs} +7 -5
- package/dist/themes/oca.cjs +34 -0
- package/dist/themes/oca.d.cts +3 -0
- package/dist/themes/oca.d.mts +3 -0
- package/dist/{esm/chunk-DU4QDQUC.js → themes/oca.mjs} +7 -5
- package/dist/themes/ventures.cjs +34 -0
- package/dist/themes/ventures.d.cts +3 -0
- package/dist/themes/ventures.d.mts +3 -0
- package/dist/{esm/chunk-BXKVVQEP.js → themes/ventures.mjs} +7 -5
- package/dist/toCssVars-CYZCe-on.mjs +286 -0
- package/dist/toCssVars-DudHKvt2.cjs +297 -0
- package/dist/{esm/chunk-4Q4P3JBB.js → tokenRegistry-DjgSN3oU.mjs} +23 -20
- package/dist/tokenRegistry-OhaJ9sPJ.cjs +199 -0
- package/dist/vars.cjs +127 -0
- package/dist/{vars.d.ts → vars.d.cts} +8 -7
- package/dist/vars.d.mts +128 -0
- package/dist/vars.mjs +123 -0
- package/dist/withDataviz-B4pVsOwV.cjs +192 -0
- package/dist/{esm/chunk-FBVUI2PK.js → withDataviz-DY5s7R51.mjs} +40 -12
- package/package.json +6 -6
- package/dist/Types-6tR0_2Ss.d.ts +0 -1452
- package/dist/esm/chunk-5PWPAQMC.js +0 -9
- package/dist/esm/chunk-HRNXVRS3.js +0 -54
- package/dist/esm/chunk-IJGA42O6.js +0 -141
- package/dist/esm/chunk-PQPQNZ73.js +0 -262
- package/dist/esm/chunk-UMRQ4OTX.js +0 -11
- package/dist/esm/chunk-VL6EGE6Z.js +0 -222
- package/dist/esm/chunk-WVQSTQD5.js +0 -192
- package/dist/esm/css.js +0 -6
- package/dist/esm/dataviz/index.js +0 -19
- package/dist/esm/dtcg.js +0 -65
- package/dist/esm/index.js +0 -10
- package/dist/esm/react.js +0 -8
- package/dist/esm/runtime-entry.js +0 -4
- package/dist/esm/themes/bruttal.js +0 -6
- package/dist/esm/themes/corporate.js +0 -6
- package/dist/esm/themes/oca.js +0 -6
- package/dist/esm/themes/ventures.js +0 -6
- package/dist/esm/vars.js +0 -28
- package/dist/themes/bruttal.d.ts +0 -5
- package/dist/themes/corporate.d.ts +0 -5
- package/dist/themes/oca.d.ts +0 -5
- package/dist/themes/ventures.d.ts +0 -5
package/dist/dtcg.cjs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, {
|
|
3
|
+
value: 'Module'
|
|
4
|
+
});
|
|
5
|
+
const require_helpers = require('./helpers-4p4-QVt_.cjs');
|
|
6
|
+
const require_tokenRegistry = require('./tokenRegistry-OhaJ9sPJ.cjs');
|
|
7
|
+
|
|
8
|
+
//#region src/roots/toDTCG.ts
|
|
9
|
+
/**
|
|
10
|
+
* Suffix-level overrides applied after prefix lookup.
|
|
11
|
+
*
|
|
12
|
+
* The token registry uses a single prefix per semantic subtree (e.g.
|
|
13
|
+
* `semantic.motion.`, `semantic.text.`) which cannot express per-property
|
|
14
|
+
* DTCG types for composite token shapes (TextStyle, SemanticMotionEntry).
|
|
15
|
+
* These overrides catch every case where the leaf property name alone
|
|
16
|
+
* carries the required type — regardless of the token family it belongs to.
|
|
17
|
+
*
|
|
18
|
+
* Rule: a suffix override takes precedence over the prefix-based $type.
|
|
19
|
+
*
|
|
20
|
+
* Typography leaves (TextStyle) that need non-string $types:
|
|
21
|
+
* - `.fontFamily` → 'fontFamily' (DTCG named type for font stacks)
|
|
22
|
+
* - `.fontSize` → 'dimension' (clamp() / px value)
|
|
23
|
+
* - `.fontWeight` → 'fontWeight' (numeric weight: 400, 500, 700 …)
|
|
24
|
+
* - `.lineHeight` → 'number' (unitless multiplier: 1.15, 1.5 …)
|
|
25
|
+
* - `.letterSpacing` → 'dimension' (em value: '-0.02em', '0.04em' …)
|
|
26
|
+
*
|
|
27
|
+
* Motion leaves (SemanticMotionEntry) overridden separately:
|
|
28
|
+
* - `.duration` → 'duration'
|
|
29
|
+
*/
|
|
30
|
+
const SUFFIX_TYPE_OVERRIDES = [[".duration", "duration"], [".fontFamily", "fontFamily"], [".fontSize", "dimension"], [".fontWeight", "fontWeight"], [".lineHeight", "number"], [".letterSpacing", "dimension"], [".ring.color", "color"]];
|
|
31
|
+
const inferType = path => {
|
|
32
|
+
for (const [suffix, type] of SUFFIX_TYPE_OVERRIDES) if (path.endsWith(suffix)) return type;
|
|
33
|
+
for (const [prefix, type] of require_tokenRegistry.DTCG_TYPE_PREFIXES) if (path.startsWith(prefix)) return type;
|
|
34
|
+
return "string";
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Set a value at a dot-path in a nested object, creating intermediate
|
|
38
|
+
* objects as needed.
|
|
39
|
+
*/
|
|
40
|
+
const setNestedValue = (root, path, value) => {
|
|
41
|
+
const segments = path.split(".");
|
|
42
|
+
let current = root;
|
|
43
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
44
|
+
const seg = segments[i];
|
|
45
|
+
if (!(seg in current) || typeof current[seg] !== "object") current[seg] = {};
|
|
46
|
+
current = current[seg];
|
|
47
|
+
}
|
|
48
|
+
current[segments[segments.length - 1]] = value;
|
|
49
|
+
};
|
|
50
|
+
/** Semantic hit token prefix that carries coarse-pointer overrides. */
|
|
51
|
+
const SEMANTIC_HIT_PREFIX = "semantic.sizing.hit.";
|
|
52
|
+
/**
|
|
53
|
+
* Build the `$extensions` metadata for a semantic hit token.
|
|
54
|
+
*
|
|
55
|
+
* Each `semantic.sizing.hit.{step}` token defaults to the fine-pointer value.
|
|
56
|
+
* The extension declares the corresponding coarse-pointer raw value so that
|
|
57
|
+
* non-CSS consumers (React Native, design tool pipelines) can locate and
|
|
58
|
+
* apply touch-target overrides without reading `toCssVars` source code.
|
|
59
|
+
*/
|
|
60
|
+
const buildHitExtension = (step, theme) => {
|
|
61
|
+
const coarseValue = theme.core.sizing.hit.coarse[step];
|
|
62
|
+
if (coarseValue === void 0) return void 0;
|
|
63
|
+
return {
|
|
64
|
+
"com.ttoss.pointer-override": {
|
|
65
|
+
condition: "any-pointer: coarse",
|
|
66
|
+
value: coarseValue
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Root 3 — W3C Design Tokens (DTCG JSON).
|
|
72
|
+
*
|
|
73
|
+
* Convert a `ThemeTokens` into a structured token tree following the
|
|
74
|
+
* W3C Design Tokens Community Group format. Every leaf node has `$value`
|
|
75
|
+
* (fully resolved) and `$type` (inferred from the token path).
|
|
76
|
+
*
|
|
77
|
+
* Semantic hit tokens (`semantic.sizing.hit.*`) include a `$extensions`
|
|
78
|
+
* field declaring their coarse-pointer override value, so non-CSS consumers
|
|
79
|
+
* can apply touch-target ergonomics without reading the CSS emitter source.
|
|
80
|
+
*
|
|
81
|
+
* This is the interchange format for design tools (Tokens Studio, Figma,
|
|
82
|
+
* Style Dictionary, Specify, Supernova) and CI/CD token pipelines.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* import { toDTCG } from '@ttoss/fsl-theme/dtcg';
|
|
87
|
+
* import { createTheme } from '@ttoss/fsl-theme';
|
|
88
|
+
*
|
|
89
|
+
* const myBundle = createTheme();
|
|
90
|
+
* const tokens = toDTCG(myBundle.base);
|
|
91
|
+
* // tokens.core.colors.brand['500'] === { $value: '#0469E3', $type: 'color' }
|
|
92
|
+
*
|
|
93
|
+
* // Write to file (build script / token pipeline)
|
|
94
|
+
* await fs.writeFile('tokens.json', JSON.stringify(tokens, null, 2));
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
const toDTCG = theme => {
|
|
98
|
+
const flat = require_helpers.toFlatTokens(theme);
|
|
99
|
+
const tree = {};
|
|
100
|
+
for (const [path, value] of Object.entries(flat)) {
|
|
101
|
+
const token = {
|
|
102
|
+
$value: value,
|
|
103
|
+
$type: inferType(path)
|
|
104
|
+
};
|
|
105
|
+
if (path.startsWith(SEMANTIC_HIT_PREFIX)) {
|
|
106
|
+
const ext = buildHitExtension(path.slice(20), theme);
|
|
107
|
+
if (ext) token.$extensions = ext;
|
|
108
|
+
}
|
|
109
|
+
setNestedValue(tree, path, token);
|
|
110
|
+
}
|
|
111
|
+
return tree;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
exports.toDTCG = toDTCG;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { T as ThemeTokens } from './Types-6tR0_2Ss.js';
|
|
2
1
|
|
|
2
|
+
import { i as ThemeTokens } from "./Types-BiBa17RL.cjs";
|
|
3
|
+
|
|
4
|
+
//#region src/roots/toDTCG.d.ts
|
|
3
5
|
/**
|
|
4
6
|
* A single DTCG token node with `$value` and `$type`.
|
|
5
7
|
*
|
|
@@ -7,15 +9,15 @@ import { T as ThemeTokens } from './Types-6tR0_2Ss.js';
|
|
|
7
9
|
* https://design-tokens.github.io/community-group/format/
|
|
8
10
|
*/
|
|
9
11
|
interface DTCGToken {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
$value: string | number;
|
|
13
|
+
$type: string;
|
|
14
|
+
$extensions?: Record<string, unknown>;
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* A recursive tree where every leaf is a `DTCGToken`.
|
|
16
18
|
*/
|
|
17
19
|
type DTCGTokenTree = DTCGToken | {
|
|
18
|
-
|
|
20
|
+
[key: string]: DTCGTokenTree;
|
|
19
21
|
};
|
|
20
22
|
/**
|
|
21
23
|
* Root 3 — W3C Design Tokens (DTCG JSON).
|
|
@@ -45,5 +47,5 @@ type DTCGTokenTree = DTCGToken | {
|
|
|
45
47
|
* ```
|
|
46
48
|
*/
|
|
47
49
|
declare const toDTCG: (theme: ThemeTokens) => DTCGTokenTree;
|
|
48
|
-
|
|
49
|
-
export { type DTCGToken, type DTCGTokenTree, toDTCG };
|
|
50
|
+
//#endregion
|
|
51
|
+
export { type DTCGToken, type DTCGTokenTree, toDTCG };
|
package/dist/dtcg.d.mts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
import { i as ThemeTokens } from "./Types-BiBa17RL.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/roots/toDTCG.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* A single DTCG token node with `$value` and `$type`.
|
|
7
|
+
*
|
|
8
|
+
* Follows the W3C Design Tokens Community Group format:
|
|
9
|
+
* https://design-tokens.github.io/community-group/format/
|
|
10
|
+
*/
|
|
11
|
+
interface DTCGToken {
|
|
12
|
+
$value: string | number;
|
|
13
|
+
$type: string;
|
|
14
|
+
$extensions?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A recursive tree where every leaf is a `DTCGToken`.
|
|
18
|
+
*/
|
|
19
|
+
type DTCGTokenTree = DTCGToken | {
|
|
20
|
+
[key: string]: DTCGTokenTree;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Root 3 — W3C Design Tokens (DTCG JSON).
|
|
24
|
+
*
|
|
25
|
+
* Convert a `ThemeTokens` into a structured token tree following the
|
|
26
|
+
* W3C Design Tokens Community Group format. Every leaf node has `$value`
|
|
27
|
+
* (fully resolved) and `$type` (inferred from the token path).
|
|
28
|
+
*
|
|
29
|
+
* Semantic hit tokens (`semantic.sizing.hit.*`) include a `$extensions`
|
|
30
|
+
* field declaring their coarse-pointer override value, so non-CSS consumers
|
|
31
|
+
* can apply touch-target ergonomics without reading the CSS emitter source.
|
|
32
|
+
*
|
|
33
|
+
* This is the interchange format for design tools (Tokens Studio, Figma,
|
|
34
|
+
* Style Dictionary, Specify, Supernova) and CI/CD token pipelines.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { toDTCG } from '@ttoss/fsl-theme/dtcg';
|
|
39
|
+
* import { createTheme } from '@ttoss/fsl-theme';
|
|
40
|
+
*
|
|
41
|
+
* const myBundle = createTheme();
|
|
42
|
+
* const tokens = toDTCG(myBundle.base);
|
|
43
|
+
* // tokens.core.colors.brand['500'] === { $value: '#0469E3', $type: 'color' }
|
|
44
|
+
*
|
|
45
|
+
* // Write to file (build script / token pipeline)
|
|
46
|
+
* await fs.writeFile('tokens.json', JSON.stringify(tokens, null, 2));
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
declare const toDTCG: (theme: ThemeTokens) => DTCGTokenTree;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { type DTCGToken, type DTCGTokenTree, toDTCG };
|
package/dist/dtcg.mjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
|
+
import { s as toFlatTokens } from "./helpers-CaswNJMy.mjs";
|
|
3
|
+
import { n as DTCG_TYPE_PREFIXES } from "./tokenRegistry-DjgSN3oU.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/roots/toDTCG.ts
|
|
6
|
+
/**
|
|
7
|
+
* Suffix-level overrides applied after prefix lookup.
|
|
8
|
+
*
|
|
9
|
+
* The token registry uses a single prefix per semantic subtree (e.g.
|
|
10
|
+
* `semantic.motion.`, `semantic.text.`) which cannot express per-property
|
|
11
|
+
* DTCG types for composite token shapes (TextStyle, SemanticMotionEntry).
|
|
12
|
+
* These overrides catch every case where the leaf property name alone
|
|
13
|
+
* carries the required type — regardless of the token family it belongs to.
|
|
14
|
+
*
|
|
15
|
+
* Rule: a suffix override takes precedence over the prefix-based $type.
|
|
16
|
+
*
|
|
17
|
+
* Typography leaves (TextStyle) that need non-string $types:
|
|
18
|
+
* - `.fontFamily` → 'fontFamily' (DTCG named type for font stacks)
|
|
19
|
+
* - `.fontSize` → 'dimension' (clamp() / px value)
|
|
20
|
+
* - `.fontWeight` → 'fontWeight' (numeric weight: 400, 500, 700 …)
|
|
21
|
+
* - `.lineHeight` → 'number' (unitless multiplier: 1.15, 1.5 …)
|
|
22
|
+
* - `.letterSpacing` → 'dimension' (em value: '-0.02em', '0.04em' …)
|
|
23
|
+
*
|
|
24
|
+
* Motion leaves (SemanticMotionEntry) overridden separately:
|
|
25
|
+
* - `.duration` → 'duration'
|
|
26
|
+
*/
|
|
27
|
+
const SUFFIX_TYPE_OVERRIDES = [[".duration", "duration"], [".fontFamily", "fontFamily"], [".fontSize", "dimension"], [".fontWeight", "fontWeight"], [".lineHeight", "number"], [".letterSpacing", "dimension"], [".ring.color", "color"]];
|
|
28
|
+
const inferType = path => {
|
|
29
|
+
for (const [suffix, type] of SUFFIX_TYPE_OVERRIDES) if (path.endsWith(suffix)) return type;
|
|
30
|
+
for (const [prefix, type] of DTCG_TYPE_PREFIXES) if (path.startsWith(prefix)) return type;
|
|
31
|
+
return "string";
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Set a value at a dot-path in a nested object, creating intermediate
|
|
35
|
+
* objects as needed.
|
|
36
|
+
*/
|
|
37
|
+
const setNestedValue = (root, path, value) => {
|
|
38
|
+
const segments = path.split(".");
|
|
39
|
+
let current = root;
|
|
40
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
41
|
+
const seg = segments[i];
|
|
42
|
+
if (!(seg in current) || typeof current[seg] !== "object") current[seg] = {};
|
|
43
|
+
current = current[seg];
|
|
44
|
+
}
|
|
45
|
+
current[segments[segments.length - 1]] = value;
|
|
46
|
+
};
|
|
47
|
+
/** Semantic hit token prefix that carries coarse-pointer overrides. */
|
|
48
|
+
const SEMANTIC_HIT_PREFIX = "semantic.sizing.hit.";
|
|
49
|
+
/**
|
|
50
|
+
* Build the `$extensions` metadata for a semantic hit token.
|
|
51
|
+
*
|
|
52
|
+
* Each `semantic.sizing.hit.{step}` token defaults to the fine-pointer value.
|
|
53
|
+
* The extension declares the corresponding coarse-pointer raw value so that
|
|
54
|
+
* non-CSS consumers (React Native, design tool pipelines) can locate and
|
|
55
|
+
* apply touch-target overrides without reading `toCssVars` source code.
|
|
56
|
+
*/
|
|
57
|
+
const buildHitExtension = (step, theme) => {
|
|
58
|
+
const coarseValue = theme.core.sizing.hit.coarse[step];
|
|
59
|
+
if (coarseValue === void 0) return void 0;
|
|
60
|
+
return {
|
|
61
|
+
"com.ttoss.pointer-override": {
|
|
62
|
+
condition: "any-pointer: coarse",
|
|
63
|
+
value: coarseValue
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Root 3 — W3C Design Tokens (DTCG JSON).
|
|
69
|
+
*
|
|
70
|
+
* Convert a `ThemeTokens` into a structured token tree following the
|
|
71
|
+
* W3C Design Tokens Community Group format. Every leaf node has `$value`
|
|
72
|
+
* (fully resolved) and `$type` (inferred from the token path).
|
|
73
|
+
*
|
|
74
|
+
* Semantic hit tokens (`semantic.sizing.hit.*`) include a `$extensions`
|
|
75
|
+
* field declaring their coarse-pointer override value, so non-CSS consumers
|
|
76
|
+
* can apply touch-target ergonomics without reading the CSS emitter source.
|
|
77
|
+
*
|
|
78
|
+
* This is the interchange format for design tools (Tokens Studio, Figma,
|
|
79
|
+
* Style Dictionary, Specify, Supernova) and CI/CD token pipelines.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* import { toDTCG } from '@ttoss/fsl-theme/dtcg';
|
|
84
|
+
* import { createTheme } from '@ttoss/fsl-theme';
|
|
85
|
+
*
|
|
86
|
+
* const myBundle = createTheme();
|
|
87
|
+
* const tokens = toDTCG(myBundle.base);
|
|
88
|
+
* // tokens.core.colors.brand['500'] === { $value: '#0469E3', $type: 'color' }
|
|
89
|
+
*
|
|
90
|
+
* // Write to file (build script / token pipeline)
|
|
91
|
+
* await fs.writeFile('tokens.json', JSON.stringify(tokens, null, 2));
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
const toDTCG = theme => {
|
|
95
|
+
const flat = toFlatTokens(theme);
|
|
96
|
+
const tree = {};
|
|
97
|
+
for (const [path, value] of Object.entries(flat)) {
|
|
98
|
+
const token = {
|
|
99
|
+
$value: value,
|
|
100
|
+
$type: inferType(path)
|
|
101
|
+
};
|
|
102
|
+
if (path.startsWith(SEMANTIC_HIT_PREFIX)) {
|
|
103
|
+
const ext = buildHitExtension(path.slice(20), theme);
|
|
104
|
+
if (ext) token.$extensions = ext;
|
|
105
|
+
}
|
|
106
|
+
setNestedValue(tree, path, token);
|
|
107
|
+
}
|
|
108
|
+
return tree;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
export { toDTCG };
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
|
+
//#region src/roots/helpers.ts
|
|
3
|
+
/** Check if a value is a token reference like `{core.colors.brand.500}` */
|
|
4
|
+
const isTokenRef = value => {
|
|
5
|
+
return typeof value === "string" && value.length > 2 && value.startsWith("{") && value.endsWith("}");
|
|
6
|
+
};
|
|
7
|
+
/** Extract the inner path from a token reference: `{core.colors.brand.500}` → `core.colors.brand.500` */
|
|
8
|
+
const extractRefPath = ref => {
|
|
9
|
+
return ref.slice(1, -1);
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Allowed characters for a theme identifier: alphanumeric, hyphens, underscores.
|
|
13
|
+
* Shared between toCssVars (CSS selector injection guard) and ssrScript (SSR inline script).
|
|
14
|
+
* A single definition prevents the two validation paths from silently diverging.
|
|
15
|
+
*/
|
|
16
|
+
const SAFE_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
17
|
+
/**
|
|
18
|
+
* Matches every `{token.path}` reference embedded in a string value.
|
|
19
|
+
* Shared between helpers.ts (toFlatTokens) and toCssVars.ts (inlineRefsToVars)
|
|
20
|
+
* so a single definition governs the `{…}` syntax in both resolution paths.
|
|
21
|
+
*/
|
|
22
|
+
const COMPOUND_REF_RE = /\{([^}]+)\}/g;
|
|
23
|
+
const isPlainObject = value => {
|
|
24
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Recursively merges `overrides` into `base`.
|
|
28
|
+
* - Plain objects are merged recursively.
|
|
29
|
+
* - All other values (primitives, arrays) are replaced.
|
|
30
|
+
*/
|
|
31
|
+
const deepMerge = (base, overrides) => {
|
|
32
|
+
if (!isPlainObject(base) || !isPlainObject(overrides)) return overrides ?? base;
|
|
33
|
+
const result = {
|
|
34
|
+
...base
|
|
35
|
+
};
|
|
36
|
+
for (const key of Object.keys(overrides)) {
|
|
37
|
+
const baseVal = result[key];
|
|
38
|
+
const overVal = overrides[key];
|
|
39
|
+
if (overVal === void 0) continue;
|
|
40
|
+
if (isPlainObject(baseVal) && isPlainObject(overVal)) result[key] = deepMerge(baseVal, overVal);else result[key] = overVal;
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Flatten a nested object into a flat record with dot-separated keys.
|
|
46
|
+
*
|
|
47
|
+
* `{ brand: { 500: '#0469E3' } }` → `{ 'brand.500': '#0469E3' }`
|
|
48
|
+
*/
|
|
49
|
+
const flattenObject = (obj, prefix = "") => {
|
|
50
|
+
const result = {};
|
|
51
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
52
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
53
|
+
if (isPlainObject(value)) Object.assign(result, flattenObject(value, fullKey));else if (typeof value === "string" || typeof value === "number") result[fullKey] = value;
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Flatten a `ThemeTokens` into separate `{ core, semantic }` flat records
|
|
59
|
+
* with dot-separated keys. Centralizes the unsafe casts needed to traverse
|
|
60
|
+
* the opaque token trees.
|
|
61
|
+
*
|
|
62
|
+
* Used by both `toFlatTokens` (resolution) and `buildCssVars` (CSS emission)
|
|
63
|
+
* so the casts live in exactly one place.
|
|
64
|
+
*/
|
|
65
|
+
const flattenTheme = theme => {
|
|
66
|
+
return {
|
|
67
|
+
core: flattenObject(theme.core, "core"),
|
|
68
|
+
semantic: flattenObject(theme.semantic, "semantic")
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Compute the Levenshtein edit distance between two strings.
|
|
73
|
+
* Used exclusively by `validateRefs` to power "did you mean?" suggestions.
|
|
74
|
+
*/
|
|
75
|
+
const levenshtein = (a, b) => {
|
|
76
|
+
const m = a.length;
|
|
77
|
+
const n = b.length;
|
|
78
|
+
const dp = Array.from({
|
|
79
|
+
length: m + 1
|
|
80
|
+
}, () => {
|
|
81
|
+
return Array.from({
|
|
82
|
+
length: n + 1
|
|
83
|
+
}, () => {
|
|
84
|
+
return 0;
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
88
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
89
|
+
for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
90
|
+
return dp[m][n];
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Validate that every `{ref}` in the merged theme points to an existing path.
|
|
94
|
+
* Emits `console.warn` for each broken reference with a "did you mean?" suggestion.
|
|
95
|
+
*
|
|
96
|
+
* **DEV-only** — callers gate this behind `process.env.NODE_ENV !== 'production'`
|
|
97
|
+
* so bundlers tree-shake the entire call in production builds.
|
|
98
|
+
*/
|
|
99
|
+
const validateRefs = theme => {
|
|
100
|
+
const {
|
|
101
|
+
core,
|
|
102
|
+
semantic
|
|
103
|
+
} = flattenTheme(theme);
|
|
104
|
+
const all = {
|
|
105
|
+
...core,
|
|
106
|
+
...semantic
|
|
107
|
+
};
|
|
108
|
+
const allKeys = Object.keys(all);
|
|
109
|
+
const findSuggestion = (refPath, candidates) => {
|
|
110
|
+
if (candidates.length === 0) return "";
|
|
111
|
+
let bestDist = Infinity;
|
|
112
|
+
let bestKey = "";
|
|
113
|
+
for (const candidate of candidates) {
|
|
114
|
+
const dist = levenshtein(refPath, candidate);
|
|
115
|
+
if (dist < bestDist) {
|
|
116
|
+
bestDist = dist;
|
|
117
|
+
bestKey = candidate;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (bestDist <= Math.ceil(refPath.length * .4)) return `\n Did you mean '{${bestKey}}'?`;
|
|
121
|
+
return "";
|
|
122
|
+
};
|
|
123
|
+
for (const [ownerKey, value] of Object.entries(all)) {
|
|
124
|
+
if (typeof value !== "string" || !value.includes("{")) continue;
|
|
125
|
+
let match;
|
|
126
|
+
const re = new RegExp(COMPOUND_REF_RE.source, COMPOUND_REF_RE.flags);
|
|
127
|
+
while ((match = re.exec(value)) !== null) {
|
|
128
|
+
const refPath = match[1];
|
|
129
|
+
if (all[refPath] !== void 0) continue;
|
|
130
|
+
const prefix = refPath.split(".")[0];
|
|
131
|
+
const suggestion = findSuggestion(refPath, allKeys.filter(k => {
|
|
132
|
+
return k.startsWith(prefix + ".");
|
|
133
|
+
}));
|
|
134
|
+
console.warn(`[fsl-theme] Invalid token reference '{${refPath}}' at path '${ownerKey}'.${suggestion}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Flatten a `ThemeTokens` into a `Record<string, string | number>` with
|
|
140
|
+
* every `{ref}` recursively resolved to its final raw value where possible.
|
|
141
|
+
*
|
|
142
|
+
* By default, unresolvable references (missing target or circular dependency)
|
|
143
|
+
* are preserved as-is in the output. Pass `{ strict: true }` to instead throw
|
|
144
|
+
* on any unresolved reference — useful in tests and build steps that must
|
|
145
|
+
* fail loudly on palette drift.
|
|
146
|
+
*
|
|
147
|
+
* This is the universal primitive — every root is derived from this.
|
|
148
|
+
*/
|
|
149
|
+
const toFlatTokens = (theme, options = {}) => {
|
|
150
|
+
const {
|
|
151
|
+
strict = false
|
|
152
|
+
} = options;
|
|
153
|
+
const {
|
|
154
|
+
core: coreFlat,
|
|
155
|
+
semantic: semanticFlat
|
|
156
|
+
} = flattenTheme(theme);
|
|
157
|
+
const all = {
|
|
158
|
+
...coreFlat,
|
|
159
|
+
...semanticFlat
|
|
160
|
+
};
|
|
161
|
+
const unresolved = [];
|
|
162
|
+
const reportUnresolved = (key, path, reason) => {
|
|
163
|
+
if (strict) unresolved.push(`${key} → {${path}} (${reason})`);
|
|
164
|
+
};
|
|
165
|
+
/** Resolve a single pure `{path}` reference to its raw value. */
|
|
166
|
+
const resolveRef = (value, seen, ownerKey) => {
|
|
167
|
+
if (typeof value !== "string" || !isTokenRef(value)) return value;
|
|
168
|
+
const path = extractRefPath(value);
|
|
169
|
+
if (seen.has(path)) {
|
|
170
|
+
reportUnresolved(ownerKey, path, "circular reference");
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
const target = all[path];
|
|
174
|
+
if (target === void 0) {
|
|
175
|
+
reportUnresolved(ownerKey, path, "missing target");
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
seen.add(path);
|
|
179
|
+
return resolveRef(target, seen, ownerKey);
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Resolve all embedded `{path}` refs in a raw string expression.
|
|
183
|
+
*
|
|
184
|
+
* Handles both pure refs (`{core.space.4}`) and compound expressions
|
|
185
|
+
* (`clamp({core.space.4}, {core.space.6}, {core.space.12})`).
|
|
186
|
+
*/
|
|
187
|
+
const resolveInline = (value, seen, ownerKey) => {
|
|
188
|
+
return value.replace(COMPOUND_REF_RE, (_match, path) => {
|
|
189
|
+
const target = all[path];
|
|
190
|
+
if (target === void 0) {
|
|
191
|
+
reportUnresolved(ownerKey, path, "missing target");
|
|
192
|
+
return `{${path}}`;
|
|
193
|
+
}
|
|
194
|
+
if (seen.has(path)) {
|
|
195
|
+
reportUnresolved(ownerKey, path, "circular reference");
|
|
196
|
+
return `{${path}}`;
|
|
197
|
+
}
|
|
198
|
+
const resolved = resolveRef(target, new Set(seen).add(path), ownerKey);
|
|
199
|
+
return String(resolved);
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
const resolved = {};
|
|
203
|
+
for (const [key, value] of Object.entries(all)) if (typeof value === "string") {
|
|
204
|
+
if (isTokenRef(value)) resolved[key] = resolveRef(value, /* @__PURE__ */new Set(), key);else if (value.includes("{")) resolved[key] = resolveInline(value, /* @__PURE__ */new Set(), key);else resolved[key] = value;
|
|
205
|
+
} else resolved[key] = value;
|
|
206
|
+
if (strict && unresolved.length > 0) throw new Error(`toFlatTokens: ${unresolved.length} unresolved reference(s):\n ${unresolved.join("\n ")}`);
|
|
207
|
+
return resolved;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
//#endregion
|
|
211
|
+
Object.defineProperty(exports, 'COMPOUND_REF_RE', {
|
|
212
|
+
enumerable: true,
|
|
213
|
+
get: function () {
|
|
214
|
+
return COMPOUND_REF_RE;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
Object.defineProperty(exports, 'SAFE_ID_RE', {
|
|
218
|
+
enumerable: true,
|
|
219
|
+
get: function () {
|
|
220
|
+
return SAFE_ID_RE;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
Object.defineProperty(exports, 'deepMerge', {
|
|
224
|
+
enumerable: true,
|
|
225
|
+
get: function () {
|
|
226
|
+
return deepMerge;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
Object.defineProperty(exports, 'flattenObject', {
|
|
230
|
+
enumerable: true,
|
|
231
|
+
get: function () {
|
|
232
|
+
return flattenObject;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
Object.defineProperty(exports, 'flattenTheme', {
|
|
236
|
+
enumerable: true,
|
|
237
|
+
get: function () {
|
|
238
|
+
return flattenTheme;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
Object.defineProperty(exports, 'isPlainObject', {
|
|
242
|
+
enumerable: true,
|
|
243
|
+
get: function () {
|
|
244
|
+
return isPlainObject;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
Object.defineProperty(exports, 'toFlatTokens', {
|
|
248
|
+
enumerable: true,
|
|
249
|
+
get: function () {
|
|
250
|
+
return toFlatTokens;
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
Object.defineProperty(exports, 'validateRefs', {
|
|
254
|
+
enumerable: true,
|
|
255
|
+
get: function () {
|
|
256
|
+
return validateRefs;
|
|
257
|
+
}
|
|
258
|
+
});
|