@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.
- package/LICENSE +11 -2
- package/dist/css/base.override.css +2351 -0
- package/dist/css/base.tokens.css +2354 -0
- package/dist/css/erv.override.css +2351 -0
- package/dist/css/erv.tokens.css +2351 -0
- package/dist/css/tcs.override.css +2351 -0
- package/dist/css/tcs.tokens.css +2351 -0
- package/dist/docs/base.tokens.json +80732 -0
- package/dist/js/base.tokens.js +1992 -0
- package/dist/out-tsc/config.base.js +210 -0
- package/dist/out-tsc/config.brand.js +92 -0
- package/dist/out-tsc/css-naming.js +26 -0
- package/dist/out-tsc/css-preview.js +8 -0
- package/dist/out-tsc/css-value.js +301 -0
- package/dist/out-tsc/formatter.js +517 -0
- package/dist/out-tsc/index.js +46 -0
- package/dist/out-tsc/transformers.js +196 -0
- package/dist/sass/base.tokens.scss +17 -0
- package/dist/web/base.tokens.json +1990 -0
- package/package.json +31 -9
- package/sbom.cdx.json +76462 -0
- package/README.md +0 -37
- package/dist/deprecated/tokens.css +0 -119
- package/dist/deprecated/tokens.css.scss +0 -119
- package/dist/deprecated/tokens.docs.json +0 -2764
- package/dist/deprecated/tokens.json +0 -114
- package/dist/deprecated/tokens.less +0 -116
- package/dist/deprecated/tokens.scss +0 -116
- package/dist/figma/color.json +0 -606
- package/dist/figma/size.json +0 -232
- package/dist/tokens.css +0 -270
- package/dist/tokens.css.scss +0 -270
- package/dist/tokens.docs.json +0 -5853
- package/dist/tokens.esm.js +0 -268
- package/dist/tokens.js +0 -270
- package/dist/tokens.json +0 -265
- package/dist/tokens.less +0 -267
- package/dist/tokens.scss +0 -267
- 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
|
+
};
|