@baloise/ds-tokens 19.10.2 → 20.0.0-next.10

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 (39) hide show
  1. package/LICENSE +11 -2
  2. package/dist/css/base.override.css +2351 -0
  3. package/dist/css/base.tokens.css +2354 -0
  4. package/dist/css/erv.override.css +2351 -0
  5. package/dist/css/erv.tokens.css +2351 -0
  6. package/dist/css/tcs.override.css +2351 -0
  7. package/dist/css/tcs.tokens.css +2351 -0
  8. package/dist/docs/base.tokens.json +80732 -0
  9. package/dist/js/base.tokens.js +1992 -0
  10. package/dist/out-tsc/config.base.js +210 -0
  11. package/dist/out-tsc/config.brand.js +92 -0
  12. package/dist/out-tsc/css-naming.js +26 -0
  13. package/dist/out-tsc/css-preview.js +8 -0
  14. package/dist/out-tsc/css-value.js +301 -0
  15. package/dist/out-tsc/formatter.js +517 -0
  16. package/dist/out-tsc/index.js +46 -0
  17. package/dist/out-tsc/transformers.js +196 -0
  18. package/dist/sass/base.tokens.scss +17 -0
  19. package/dist/web/base.tokens.json +1990 -0
  20. package/package.json +31 -9
  21. package/sbom.cdx.json +76462 -0
  22. package/README.md +0 -37
  23. package/dist/deprecated/tokens.css +0 -119
  24. package/dist/deprecated/tokens.css.scss +0 -119
  25. package/dist/deprecated/tokens.docs.json +0 -2764
  26. package/dist/deprecated/tokens.json +0 -114
  27. package/dist/deprecated/tokens.less +0 -116
  28. package/dist/deprecated/tokens.scss +0 -116
  29. package/dist/figma/color.json +0 -606
  30. package/dist/figma/size.json +0 -232
  31. package/dist/tokens.css +0 -270
  32. package/dist/tokens.css.scss +0 -270
  33. package/dist/tokens.docs.json +0 -5853
  34. package/dist/tokens.esm.js +0 -268
  35. package/dist/tokens.js +0 -270
  36. package/dist/tokens.json +0 -265
  37. package/dist/tokens.less +0 -267
  38. package/dist/tokens.scss +0 -267
  39. package/dist/types/tokens.d.ts +0 -350
@@ -0,0 +1,210 @@
1
+ const basePxFontSize = 16;
2
+ const mode = 'Base';
3
+ // Shared with config.brand.ts — a brand's css platform must transform every token type the same
4
+ // way Base's own build does, since a brand's output is now a full merged set (see
5
+ // docs/adr/0030-full-merge-brand-token-css.md), not a small diff. Keeping one list instead of two
6
+ // prevents them silently drifting apart the way they had (config.brand.ts was missing 'ds/border'
7
+ // and used 'ds/font-family' where this list uses the stock 'fontFamily/css').
8
+ export const BASE_CSS_TRANSFORMS = [
9
+ 'attribute/cti',
10
+ 'name/kebab',
11
+ 'time/seconds',
12
+ 'html/icon',
13
+ 'size/rem',
14
+ 'color/css',
15
+ 'asset/url',
16
+ 'fontFamily/css',
17
+ 'cubicBezier/css',
18
+ 'strokeStyle/css/shorthand',
19
+ 'transition/css/shorthand',
20
+ 'ds/css/name',
21
+ 'ds/color/rgba',
22
+ 'ds/size/round',
23
+ 'ds/size/rem',
24
+ 'ds/font-weight',
25
+ 'ds/shadow',
26
+ 'ds/border',
27
+ 'ds/typography',
28
+ ];
29
+ const config = {
30
+ source: [`tokens/${mode}.tokens.json`],
31
+ platforms: {
32
+ css: {
33
+ // No transformGroup — the 'css' transformGroup's built-in 'shadow/css/shorthand' renders
34
+ // shadow colors as 'rgb(0% 0% 0% / 0.25)' (colorjs.io's default), not this codebase's
35
+ // '#hex'/'rgba(...)' convention (verified directly — see
36
+ // docs/plans/shadow-token-type-plan.md). Listed explicitly instead: every other 'css'
37
+ // transformGroup member is kept verbatim (so nothing else about this platform's output
38
+ // changes) — including 'fontFamily/css', which already produces correct output and is why
39
+ // there's no separate 'ds/font-family' transform anywhere — except 'shadow/css/shorthand',
40
+ // swapped for our own 'ds/shadow', and 'typography/css/shorthand', dropped entirely (not
41
+ // swapped) — it collapses a typography token's $value into a single `font` shorthand
42
+ // *string* before 'ds/typography' ever sees it, and there's no way to recover the original
43
+ // {fontFamily, fontSize, fontWeight, lineHeight} object from that string. This codebase wants
44
+ // 4 separate longhand custom properties per typography token, never a shorthand
45
+ // (docs/plans/typography-token-type-plan.md decision 5) — verified directly, same as
46
+ // shadow/border's own built-ins (the shorthand only became visible once a real
47
+ // $type: "typography" token existed to trigger its filter).
48
+ transforms: BASE_CSS_TRANSFORMS,
49
+ basePxFontSize,
50
+ buildPath: 'dist/',
51
+ prefix: 'ds',
52
+ files: [
53
+ {
54
+ format: 'ds/css/variables-responsive',
55
+ destination: `css/${mode.toLowerCase()}.tokens.css`,
56
+ },
57
+ {
58
+ format: 'ds/css/variables-brand',
59
+ destination: `css/${mode.toLowerCase()}.override.css`,
60
+ options: {
61
+ selector: `[data-theme="${mode.toLowerCase()}"], :host([data-theme="${mode.toLowerCase()}"])`,
62
+ outputReferences: true,
63
+ },
64
+ },
65
+ ],
66
+ options: {
67
+ outputReferences: true,
68
+ },
69
+ },
70
+ sass: {
71
+ // CSS custom properties can't appear inside an `@media` condition (`@media (min-width:
72
+ // var(--x))` is invalid per spec, not just unsupported) — so
73
+ // packages/styles/src/scss/mixins/breakpoint.mixin.scss needs real Sass variables, resolved at
74
+ // compile time, for its `@media` conditions and `- 1px` arithmetic. This platform exists
75
+ // solely to feed that: filtered to breakpoint tokens only (Global.Dimension.Breakpoint.* and
76
+ // Alias.Breakpoint.*, the latter referencing the former — both must ship together for the
77
+ // Sass `$var: $other-var` reference to resolve) — everything else in this design system is
78
+ // CSS-only. Transforms mirror the 'web' platform's (not 'css' platform's, which forces
79
+ // 'size/rem'): breakpoint tokens are defined with an explicit "px" unit, and 'ds/dimension'
80
+ // (unlike the stock 'size/rem') preserves that unit rather than converting it, which is what
81
+ // the mixin's px-based media queries need.
82
+ transforms: [
83
+ 'attribute/cti',
84
+ 'name/kebab',
85
+ 'color/css',
86
+ 'ds/css/name',
87
+ 'ds/color/hex',
88
+ 'ds/size/rem',
89
+ 'ds/font-weight',
90
+ 'ds/font-family',
91
+ 'size/rem',
92
+ 'ds/dimension',
93
+ 'ds/shadow',
94
+ 'ds/border',
95
+ 'ds/typography',
96
+ ],
97
+ prefix: 'ds',
98
+ buildPath: 'dist/',
99
+ files: [
100
+ {
101
+ format: 'ds/scss/variables',
102
+ destination: `sass/${mode.toLowerCase()}.tokens.scss`,
103
+ filter: token => token.path.some(segment => segment.includes('Breakpoint')),
104
+ },
105
+ ],
106
+ options: {
107
+ outputReferences: true,
108
+ },
109
+ },
110
+ web: {
111
+ // No transformGroup — the 'web' transformGroup's built-in 'size/px' unconditionally forces
112
+ // a "px" suffix without converting the number, which is wrong for a rem-unit dimension
113
+ // token (1.5rem would come out "1.5px": wrong unit label AND physically 24x smaller). Listed
114
+ // explicitly instead: 'attribute/cti'/'name/kebab'/'color/css' are 'web' transformGroup's
115
+ // other members, kept verbatim so nothing else about this platform's output changes;
116
+ // 'size/px' is dropped in favor of our own 'ds/dimension', which correctly preserves
117
+ // whichever unit the $value carries.
118
+ //
119
+ // The built-in 'size/rem' is added (harmlessly redundant with 'ds/dimension' for every
120
+ // standalone dimension token, since it already preserves an explicitly-provided unit the
121
+ // same way) because a border composite token's 'width' sub-value is a *reference* to a
122
+ // separate dimension token (see docs/plans/border-token-type-plan.md decision 4) — Style
123
+ // Dictionary only resolves that reference to its referenced token's already-transformed
124
+ // value when a transform *named* 'size/rem'/'color/css' is present, not an arbitrary
125
+ // custom-named dimension transform. Without it, 'ds/border' would see a null width for
126
+ // every Composite.* token on this platform.
127
+ transforms: [
128
+ 'attribute/cti',
129
+ 'name/kebab',
130
+ 'color/css',
131
+ 'ds/css/name',
132
+ 'ds/color/hex',
133
+ 'ds/size/rem',
134
+ 'ds/font-weight',
135
+ 'ds/font-family',
136
+ 'size/rem',
137
+ 'ds/dimension',
138
+ 'ds/shadow',
139
+ 'ds/border',
140
+ 'ds/typography',
141
+ ],
142
+ prefix: 'ds',
143
+ buildPath: 'dist/',
144
+ files: [
145
+ {
146
+ format: 'ds/json/flat',
147
+ destination: `web/${mode.toLowerCase()}.tokens.json`,
148
+ },
149
+ ],
150
+ options: {
151
+ outputReferences: true,
152
+ },
153
+ },
154
+ docs: {
155
+ // See the 'web' platform's comment above — same fix, same reasoning.
156
+ transforms: [
157
+ 'attribute/cti',
158
+ 'name/kebab',
159
+ 'color/css',
160
+ 'ds/css/name',
161
+ 'ds/color/hex',
162
+ 'ds/size/rem',
163
+ 'ds/font-weight',
164
+ 'ds/font-family',
165
+ 'size/rem',
166
+ 'ds/dimension',
167
+ 'ds/shadow',
168
+ 'ds/border',
169
+ 'ds/typography',
170
+ ],
171
+ prefix: 'ds',
172
+ buildPath: 'dist/',
173
+ files: [
174
+ {
175
+ format: 'json',
176
+ destination: `docs/${mode.toLowerCase()}.tokens.json`,
177
+ },
178
+ ],
179
+ options: {
180
+ outputReferences: true,
181
+ },
182
+ },
183
+ javascript: {
184
+ transformGroup: 'js',
185
+ transforms: [
186
+ 'ds/js/name',
187
+ 'ds/color/hex',
188
+ 'ds/size/round',
189
+ 'ds/dimension/js',
190
+ 'ds/font-weight',
191
+ 'ds/font-family',
192
+ 'ds/shadow',
193
+ 'ds/border',
194
+ 'ds/typography',
195
+ ],
196
+ prefix: 'ds',
197
+ buildPath: 'dist/',
198
+ files: [
199
+ {
200
+ format: 'ds/javascript/es6',
201
+ destination: `js/${mode.toLowerCase()}.tokens.js`,
202
+ },
203
+ ],
204
+ options: {
205
+ outputReferences: true,
206
+ },
207
+ },
208
+ },
209
+ };
210
+ export default config;
@@ -0,0 +1,92 @@
1
+ import { readFileSync, writeFileSync, unlinkSync } from 'fs';
2
+ import { BASE_CSS_TRANSFORMS } from './config.base.js';
3
+ const basePxFontSize = 16;
4
+ /**
5
+ * Recursively merges a brand's token tree onto Base's, returning the COMPLETE tree — every
6
+ * token Base defines, with the brand's own node substituted wherever the brand defines a
7
+ * `$value` leaf (keeping that leaf's own `$description`/`$extensions` as authored in the brand
8
+ * file). Unlike a diff, nothing is dropped: the result is self-sufficient and needs no `include`
9
+ * of Base to resolve references. See docs/adr/0030-full-merge-brand-token-css.md.
10
+ */
11
+ export function mergeTokenTree(base, brand) {
12
+ const result = { ...base };
13
+ for (const key of new Set([...Object.keys(base), ...Object.keys(brand)])) {
14
+ const baseVal = base[key];
15
+ const brandVal = brand[key];
16
+ if (brandVal === undefined) {
17
+ result[key] = baseVal;
18
+ continue;
19
+ }
20
+ if (typeof brandVal !== 'object' || brandVal === null) {
21
+ result[key] = brandVal;
22
+ continue;
23
+ }
24
+ if ('$value' in brandVal) {
25
+ // Token leaf — the brand's own node wins outright.
26
+ result[key] = brandVal;
27
+ }
28
+ else {
29
+ // Group node — recurse so every sibling token, changed or not, ends up in the result.
30
+ result[key] = mergeTokenTree((baseVal ?? {}), brandVal);
31
+ }
32
+ }
33
+ return result;
34
+ }
35
+ /**
36
+ * Creates a Style Dictionary config for a brand's token CSS build.
37
+ *
38
+ * Merges the brand token file onto Base (see mergeTokenTree) so the build's source is already
39
+ * the complete, self-sufficient token tree for that brand — no `include` of Base needed. Emits
40
+ * two files: `<brand>.tokens.css` (`:host, :root`, for apps that commit to one brand) and
41
+ * `<brand>.override.css` (`[data-theme]`/`:host([data-theme])`, for scoping a brand to one
42
+ * element — e.g. Storybook's theme switcher). See docs/adr/0030-full-merge-brand-token-css.md.
43
+ *
44
+ * Returns the config and a `cleanup` function that removes the temporary merged-tree file that
45
+ * was written to disk so Style Dictionary can read it as `source`.
46
+ */
47
+ export function createBrandConfig(mode) {
48
+ const brandLower = mode.toLowerCase();
49
+ const tmpFile = `tokens/.${brandLower}-full.tmp.json`;
50
+ const baseJson = JSON.parse(readFileSync(`tokens/Base.tokens.json`, 'utf8'));
51
+ const brandJson = JSON.parse(readFileSync(`tokens/${mode}.tokens.json`, 'utf8'));
52
+ const mergedTokens = mergeTokenTree(baseJson, brandJson);
53
+ writeFileSync(tmpFile, JSON.stringify(mergedTokens));
54
+ const config = {
55
+ source: [tmpFile],
56
+ platforms: {
57
+ css: {
58
+ transforms: BASE_CSS_TRANSFORMS,
59
+ basePxFontSize,
60
+ buildPath: 'dist/',
61
+ prefix: 'ds',
62
+ files: [
63
+ {
64
+ format: 'ds/css/variables-brand',
65
+ destination: `css/${brandLower}.tokens.css`,
66
+ options: {
67
+ selector: ':host, :root',
68
+ outputReferences: true,
69
+ },
70
+ },
71
+ {
72
+ format: 'ds/css/variables-brand',
73
+ destination: `css/${brandLower}.override.css`,
74
+ options: {
75
+ selector: `[data-theme="${brandLower}"], :host([data-theme="${brandLower}"])`,
76
+ outputReferences: true,
77
+ },
78
+ },
79
+ ],
80
+ },
81
+ },
82
+ };
83
+ const cleanup = () => {
84
+ try {
85
+ unlinkSync(tmpFile);
86
+ }
87
+ catch {
88
+ // ignore — file may already be gone
89
+ }
90
+ };
91
+ return { config, cleanup };
92
+ }
@@ -0,0 +1,26 @@
1
+ import { kebabCase } from 'change-case';
2
+ /**
3
+ * Turns a token's path into its final CSS custom property name, matching Style Dictionary's
4
+ * `css` transformGroup (`name/kebab`, which itself uses `change-case`'s `kebabCase`) followed by
5
+ * the `ds/css/name` transform (`transformers.ts`). Returns the bare name (no leading `--`), the
6
+ * same shape Style Dictionary expects from a `name`-type transform - the SD transform is a thin
7
+ * wrapper around this function, and `apps/toky`'s live preview imports it directly so both stay
8
+ * in sync (see ADR-0021).
9
+ */
10
+ export const tokenNameToCssVar = (pathSegments, prefix = 'ds') => {
11
+ let tokenName = kebabCase([prefix, ...pathSegments].join(' '));
12
+ const isComponent = pathSegments.includes('🧩 Component');
13
+ if (isComponent) {
14
+ tokenName = tokenName.replace('-component', '');
15
+ }
16
+ // we use t-shirt sizes and need to make sure that 2xl becomes 2xl and not 2-xl
17
+ // check if token names has number followed by xl or xs and replace it with numberxl without dash
18
+ if (/-?([0-9]+)-(xl|xs)/.test(tokenName)) {
19
+ tokenName = tokenName.replace(/-?([0-9]+)-(xl|xs)/, '-$1$2');
20
+ }
21
+ // check that no name has double dash and replace it with single dash
22
+ if (tokenName.includes('--')) {
23
+ tokenName = tokenName.replace(/--+/g, '-');
24
+ }
25
+ return tokenName;
26
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Public subpath (`@baloise/ds-tokens/css-preview`) for consumers outside the build pipeline —
3
+ * currently `apps/toky`'s live token preview — that need to turn a token path/resolved value into
4
+ * the same CSS the build actually produces, without duplicating the Style Dictionary transforms.
5
+ * See ADR-0021.
6
+ */
7
+ export { tokenNameToCssVar } from './css-naming.js';
8
+ export { resolvedValueToCss, typographyValueToCss, TYPOGRAPHY_CSS_SUFFIXES, responsiveDimensionValueToCss, RESPONSIVE_DIMENSION_CSS_SUFFIXES, } from './css-value.js';
@@ -0,0 +1,301 @@
1
+ import { tokenNameToCssVar } from './css-naming.js';
2
+ /**
3
+ * Mirrors the `ds/color/rgba` transform: DTCG color values already carry a `hex` field in this
4
+ * token set (see `tokens/Base.tokens.json`), so this only needs to add an `rgba()` wrapper when
5
+ * `alpha < 1`.
6
+ */
7
+ export const colorValueToCss = (value) => {
8
+ if (typeof value !== 'object' || value === null || !('hex' in value)) {
9
+ return null;
10
+ }
11
+ const { hex, alpha } = value;
12
+ if (typeof hex !== 'string') {
13
+ return null;
14
+ }
15
+ if (typeof alpha === 'number' && alpha < 1) {
16
+ const stripped = hex.replace('#', '');
17
+ const r = parseInt(stripped.substring(0, 2), 16);
18
+ const g = parseInt(stripped.substring(2, 4), 16);
19
+ const b = parseInt(stripped.substring(4, 6), 16);
20
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
21
+ }
22
+ return hex;
23
+ };
24
+ const ROUND_TOKEN_NAME_MARKERS = ['line-height', 'lineheight', 'opacity'];
25
+ /**
26
+ * Mirrors `ds/size/round`. Case-insensitive because callers pass different name casings
27
+ * depending on the platform's name transform - kebab-case for CSS/SCSS (`ds/css/name`), PascalCase
28
+ * for JS (`ds/js/name`).
29
+ */
30
+ export const roundNumberValue = (value, tokenName) => {
31
+ const lowerName = tokenName.toLowerCase();
32
+ if (ROUND_TOKEN_NAME_MARKERS.some(marker => lowerName.includes(marker))) {
33
+ return Math.round(value * 10) / 10;
34
+ }
35
+ return value;
36
+ };
37
+ // The only categories still $type: "number" — everything unit-bearing (Space, Radius, Border,
38
+ // FontSize, Breakpoint, Container, ...) migrated to $type: "dimension" (see
39
+ // docs/plans/dimension-token-type-plan.md). What's left is always unitless; LineHeight/Opacity
40
+ // round to 1 decimal, ZIndex/Interaction pass through as-is.
41
+ const NUMBER_ONLY_PATH_MARKERS = [
42
+ 'LineHeight',
43
+ 'Opacity',
44
+ '🌫️ Opacity',
45
+ 'ZIndex',
46
+ 'Z-Index',
47
+ '🗂️ ZIndex',
48
+ '🗂️ Z-Index',
49
+ 'Interaction',
50
+ '✨ Interaction',
51
+ ];
52
+ /** Mirrors `ds/size/round`: rounds a LineHeight/Opacity-path number to 1 decimal; passes everything else through unchanged. */
53
+ export const numberValueToCssSize = (value, path) => {
54
+ if (NUMBER_ONLY_PATH_MARKERS.some(marker => path.includes(marker))) {
55
+ return Math.round(value * 10) / 10;
56
+ }
57
+ return value;
58
+ };
59
+ const CSS_GENERIC_FONT_KEYWORDS = new Set([
60
+ 'inherit',
61
+ 'initial',
62
+ 'unset',
63
+ 'revert',
64
+ 'revert-layer',
65
+ 'serif',
66
+ 'sans-serif',
67
+ 'monospace',
68
+ 'cursive',
69
+ 'fantasy',
70
+ 'system-ui',
71
+ 'ui-serif',
72
+ 'ui-sans-serif',
73
+ 'ui-monospace',
74
+ 'ui-rounded',
75
+ 'math',
76
+ 'emoji',
77
+ 'fangsong',
78
+ ]);
79
+ /** Quote iff the name isn't a bare CSS identifier and isn't a generic keyword (e.g. sans-serif, inherit). */
80
+ const needsFontNameQuoting = (name) => !CSS_GENERIC_FONT_KEYWORDS.has(name.toLowerCase()) && /[^a-zA-Z0-9-]/.test(name);
81
+ /**
82
+ * Joins a fontFamily token's array (or single string, per DTCG) into a CSS-ready font-family
83
+ * declaration, quoting only entries that need it. Generic keywords and simple identifiers stay
84
+ * bare — quoting `sans-serif` or `inherit` would make CSS look for a font literally named that,
85
+ * instead of the real generic fallback/keyword behavior.
86
+ */
87
+ export const fontFamilyValueToCss = (value) => {
88
+ const names = Array.isArray(value) ? value : [value];
89
+ return names.map(name => (needsFontNameQuoting(String(name)) ? `"${name}"` : String(name))).join(', ');
90
+ };
91
+ /**
92
+ * Turns a dimension token's {value, unit} into a CSS-ready size, e.g. {value: 1.5, unit: 'rem'} ->
93
+ * '1.5rem'. A trivial passthrough — the source $value already holds the number in its own native
94
+ * unit (see docs/plans/dimension-token-type-plan.md), so there's no rem<->px conversion to do here;
95
+ * that only happens at the Figma push/pull boundary, where Figma's px-only reality lives.
96
+ */
97
+ export const dimensionValueToCss = (value) => {
98
+ if (typeof value !== 'object' || value === null || !('value' in value) || !('unit' in value)) {
99
+ return null;
100
+ }
101
+ const { value: num, unit } = value;
102
+ if (typeof num !== 'number' || (unit !== 'px' && unit !== 'rem')) {
103
+ return null;
104
+ }
105
+ return `${num}${unit}`;
106
+ };
107
+ /**
108
+ * Paths of `$type: "shadow"` tokens that feed the CSS `text-shadow` property rather than
109
+ * `box-shadow`. Unlike box-shadow, `text-shadow` syntax has no `spread` term — only
110
+ * `offsetX offsetY [blur] color` is legal — so a `spread` value (even `0`) makes the whole
111
+ * declaration invalid and the browser silently drops it. These are the only tokens in the set
112
+ * that back a `text-shadow`; every other `shadow` token backs `box-shadow` and keeps its spread.
113
+ */
114
+ const TEXT_SHADOW_PATHS = new Set([
115
+ '🌐 Global.🔤 Font.Shadow.0',
116
+ '🌐 Global.🔤 Font.Shadow.1',
117
+ '🔗 Alias.🌓 Shadow.Text',
118
+ '🔗 Alias.🔤 Text.Shadow.Elevated',
119
+ '🧩 Component.Heading.Shadow',
120
+ '🧩 Component.Text.Shadow',
121
+ ]);
122
+ const isTextShadowPath = (path) => path !== undefined && TEXT_SHADOW_PATHS.has(path.join('.'));
123
+ /**
124
+ * One shadow layer -> one CSS box-shadow value, e.g. 'inset 0px 4px 0.25rem 0px rgba(0, 0, 0, 0.15)'.
125
+ * Omits the `spread` term when `textShadow` is true, since `text-shadow` doesn't support it.
126
+ */
127
+ const shadowLayerToCss = (layer, textShadow) => {
128
+ const color = colorValueToCss(layer.color);
129
+ const offsetX = dimensionValueToCss(layer.offsetX);
130
+ const offsetY = dimensionValueToCss(layer.offsetY);
131
+ const blur = dimensionValueToCss(layer.blur);
132
+ const spread = dimensionValueToCss(layer.spread);
133
+ if (color === null || offsetX === null || offsetY === null || blur === null || spread === null) {
134
+ return null;
135
+ }
136
+ const spreadTerm = textShadow ? '' : `${spread} `;
137
+ return `${layer.inset ? 'inset ' : ''}${offsetX} ${offsetY} ${blur} ${spreadTerm}${color}`;
138
+ };
139
+ /**
140
+ * Turns a shadow token's value (a single shadow object, an array of them, or an empty array for
141
+ * "no shadow") into a CSS-ready box-shadow (or text-shadow, see TEXT_SHADOW_PATHS) value. Reuses
142
+ * colorValueToCss/dimensionValueToCss per sub-value rather than Style Dictionary's own built-in
143
+ * shadow transform, which renders color as `rgb(0% 0% 0% / 0.25)` (colorjs.io's default) instead
144
+ * of this codebase's `rgba(0, 0, 0, 0.25)`/hex convention — see docs/plans/shadow-token-type-plan.md.
145
+ */
146
+ export const shadowValueToCss = (value, path) => {
147
+ const textShadow = isTextShadowPath(path);
148
+ if (Array.isArray(value)) {
149
+ if (value.length === 0)
150
+ return 'none';
151
+ const layers = value.map(v => shadowLayerToCss(v, textShadow));
152
+ return layers.every((layer) => layer !== null) ? layers.join(', ') : null;
153
+ }
154
+ if (typeof value === 'object' && value !== null) {
155
+ return shadowLayerToCss(value, textShadow);
156
+ }
157
+ return null;
158
+ };
159
+ /**
160
+ * Turns a border token's {color, width, style} into a CSS-ready border shorthand value, e.g.
161
+ * '0.125rem solid #d8d8d8'. Reuses colorValueToCss/dimensionValueToCss per sub-value rather than
162
+ * Style Dictionary's own built-in `border/css/shorthand` transform, which renders each sub-value
163
+ * as a `var(--ds-*)` reference to its sibling custom property instead of a resolved literal —
164
+ * diverging from this codebase's convention (see shadowValueToCss, and
165
+ * docs/plans/border-token-type-plan.md decision 6).
166
+ *
167
+ * Unlike shadow's sub-values (always inline literals), border's `color`/`width` are references to
168
+ * separate real `color`/`dimension` tokens. By the time this transform runs in the actual build
169
+ * (after `color/css`/`size/rem` have processed those referenced tokens), Style Dictionary has
170
+ * already substituted their *post-transform* string value (e.g. '#d0d0d0', '0.125rem') into this
171
+ * composite's $value — so color/width may arrive pre-stringified rather than as raw DTCG objects.
172
+ * Toky's live preview (via resolvedValueToCss), which calls this directly on raw resolved values
173
+ * with no prior color/dimension transform, still needs the raw-object path.
174
+ */
175
+ export const borderValueToCss = (value) => {
176
+ if (typeof value !== 'object' || value === null)
177
+ return null;
178
+ const { color, width, style } = value;
179
+ const cssColor = typeof color === 'string' ? color : colorValueToCss(color);
180
+ const cssWidth = typeof width === 'string' ? width : dimensionValueToCss(width);
181
+ if (cssColor === null || cssWidth === null || typeof style !== 'string')
182
+ return null;
183
+ return `${cssWidth} ${style} ${cssColor}`;
184
+ };
185
+ // Suffix appended to a typography token's own CSS var name for each of its 4 longhand custom
186
+ // properties, e.g. `--ds-typography-heading-1-font-family` (see
187
+ // docs/plans/typography-token-type-plan.md decision 5 — 4 separate declarations, never a `font`
188
+ // shorthand). Exported so both the real build's format step and Toky's live preview build the
189
+ // exact same 4 names from one token path.
190
+ export const TYPOGRAPHY_CSS_SUFFIXES = {
191
+ fontFamily: 'font-family',
192
+ fontSize: 'font-size',
193
+ fontWeight: 'font-weight',
194
+ lineHeight: 'line-height',
195
+ };
196
+ /**
197
+ * Turns a typography token's {fontFamily, fontSize, fontWeight, lineHeight} into 4 CSS-ready
198
+ * strings, one per sub-property. Reuses fontFamilyValueToCss/dimensionValueToCss per sub-value,
199
+ * same reasoning as shadowValueToCss/borderValueToCss.
200
+ *
201
+ * fontFamily/fontWeight are always references in this codebase (docs/plans/typography-token-type-
202
+ * plan.md decision 4), so by the time this runs in the real build (after `ds/font-family`/
203
+ * `ds/font-weight` have processed the referenced tokens), Style Dictionary has already substituted
204
+ * their *post-transform* string value — same "may arrive pre-stringified" duality borderValueToCss
205
+ * documents. fontSize/lineHeight are free literal-or-reference, so either sub-value may arrive as a
206
+ * raw DTCG value (literal, untouched by any sibling transform) or an already-transformed string
207
+ * (referenced a real Font.Size or Font.LineHeight token). Toky's live preview (via
208
+ * computePreviewTokens) always calls this on raw resolved values, never pre-stringified ones.
209
+ */
210
+ export const typographyValueToCss = (value) => {
211
+ if (typeof value !== 'object' || value === null)
212
+ return null;
213
+ const { fontFamily, fontSize, fontWeight, lineHeight } = value;
214
+ const cssFontFamily = typeof fontFamily === 'string' ? fontFamily : Array.isArray(fontFamily) ? fontFamilyValueToCss(fontFamily) : null;
215
+ const cssFontSize = typeof fontSize === 'string' ? fontSize : dimensionValueToCss(fontSize);
216
+ const cssFontWeight = typeof fontWeight === 'string' ? fontWeight : typeof fontWeight === 'number' ? `${fontWeight}` : null;
217
+ const cssLineHeight = typeof lineHeight === 'string' ? lineHeight : typeof lineHeight === 'number' ? `${lineHeight}` : null;
218
+ if (cssFontFamily === null || cssFontSize === null || cssFontWeight === null || cssLineHeight === null)
219
+ return null;
220
+ return { fontFamily: cssFontFamily, fontSize: cssFontSize, fontWeight: cssFontWeight, lineHeight: cssLineHeight };
221
+ };
222
+ // The $extensions key a responsive dimension token's breakpoint values live under — see
223
+ // docs/plans/responsive-dimension-token-plan.md. Duplicated (not shared) from
224
+ // apps/toky/src/tokens/types.ts's identical constant, since the two packages have no common
225
+ // module to import it from — same "two independent implementations agree on a vocabulary" pattern
226
+ // as `com.figma.variableId` (see packages/tokens/CONTEXT.md's Figma Sync section).
227
+ export const RESPONSIVE_DIMENSION_EXTENSION_KEY = 'com.helvetia.responsive';
228
+ // Suffix appended to a responsive dimension token's own CSS var name for each of its 3 breakpoint
229
+ // declarations, e.g. `--ds-space-lg-mobile` — matches the -mobile/-tablet/-desktop convention the
230
+ // pre-existing sibling-token responsive pattern already uses (see formatter.ts's
231
+ // `ds/css/variables-responsive`), so that pattern's existing name-suffix-based -device media-query
232
+ // splitting needs zero changes to also handle these (see docs/plans/responsive-dimension-token-
233
+ // plan.md Phase 2's "key design win").
234
+ export const RESPONSIVE_DIMENSION_CSS_SUFFIXES = {
235
+ mobile: 'mobile',
236
+ tablet: 'tablet',
237
+ desktop: 'desktop',
238
+ };
239
+ /**
240
+ * Turns a responsive dimension token's already-reference-resolved {mobile, tablet, desktop}
241
+ * breakpoint map into 3 CSS-ready size strings. "Already-resolved" mirrors typographyValueToCss's
242
+ * own precondition — this doesn't walk $extensions or resolve `{reference}` strings itself;
243
+ * callers do that first (Toky's flatten.ts via `resolvedResponsive`; formatter.ts's own
244
+ * dictionary-based lookup for the real build, since Style Dictionary never resolves references
245
+ * living inside $extensions the way it automatically does for $value — see the plan's "Open
246
+ * technical question"). Returns null (not a partial object) if any breakpoint is unresolvable,
247
+ * same "don't half-render" discipline as typographyValueToCss.
248
+ */
249
+ export const responsiveDimensionValueToCss = (value) => {
250
+ if (typeof value !== 'object' || value === null)
251
+ return null;
252
+ const { mobile, tablet, desktop } = value;
253
+ const cssMobile = dimensionValueToCss(mobile);
254
+ const cssTablet = dimensionValueToCss(tablet);
255
+ const cssDesktop = dimensionValueToCss(desktop);
256
+ if (cssMobile === null || cssTablet === null || cssDesktop === null)
257
+ return null;
258
+ return { mobile: cssMobile, tablet: cssTablet, desktop: cssDesktop };
259
+ };
260
+ /**
261
+ * Turns a resolved DTCG token value into a CSS-ready string, for the live token preview
262
+ * (`apps/toky`) to send as a `--ds-*` custom property value. Mirrors the `ds/color/rgba` /
263
+ * `ds/size/round` / `ds/size/rem` Style Dictionary transforms via the functions above, so the
264
+ * preview never drifts from what the build actually produces (see ADR-0021). Returns `null` for
265
+ * value shapes it doesn't recognize, so the caller can skip that token rather than send garbage.
266
+ *
267
+ * No `'typography'` branch: unlike every other type here, one typography token maps to 4 CSS
268
+ * custom properties, not 1 (decision 5) — this function's single-`string`-or-`null` return shape
269
+ * can't carry that. Callers needing typography go straight to `typographyValueToCss` instead and
270
+ * fan the result out to 4 `--ds-*-{font-family,font-size,font-weight,line-height}` names
271
+ * themselves (see `apps/toky/src/tokens/css-preview.ts` and the `ds/css/variables-*` formatters).
272
+ */
273
+ export const resolvedValueToCss = (value, type, path) => {
274
+ if (type === 'color') {
275
+ return colorValueToCss(value);
276
+ }
277
+ if (type === 'number' && typeof value === 'number') {
278
+ const cssVarName = tokenNameToCssVar(path);
279
+ const rounded = roundNumberValue(value, cssVarName);
280
+ return `${numberValueToCssSize(rounded, path)}`;
281
+ }
282
+ if (type === 'fontWeight' && (typeof value === 'number' || typeof value === 'string')) {
283
+ return `${Number(value)}`;
284
+ }
285
+ if (type === 'fontFamily') {
286
+ return fontFamilyValueToCss(value);
287
+ }
288
+ if (type === 'dimension') {
289
+ return dimensionValueToCss(value);
290
+ }
291
+ if (type === 'shadow') {
292
+ return shadowValueToCss(value, path);
293
+ }
294
+ if (type === 'border') {
295
+ return borderValueToCss(value);
296
+ }
297
+ if (type === 'string' && typeof value === 'string') {
298
+ return value;
299
+ }
300
+ return null;
301
+ };