@massimo-cassandro/minimo 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/design-tokens/badge.minimo.tokens.jsonc +2 -2
  2. package/design-tokens/basic.minimo.tokens.jsonc +1 -1
  3. package/design-tokens/btn-primary.minimo.tokens.jsonc +2 -2
  4. package/design-tokens/color-accent.minimo.tokens.jsonc +2 -2
  5. package/design-tokens/color-neutral.minimo.tokens.jsonc +2 -2
  6. package/design-tokens/color-primary.minimo.tokens.jsonc +6 -2
  7. package/design-tokens/color-secondary.minimo.tokens.jsonc +2 -2
  8. package/design-tokens/components/recaptcha.minimo.tokens.jsonc +1 -1
  9. package/design-tokens/components/spinner.minimo.tokens.jsonc +1 -1
  10. package/design-tokens/form.minimo.tokens.jsonc +23 -1
  11. package/design-tokens/recaptcha.minimo.tokens.jsonc +1 -1
  12. package/design-tokens/spinner.minimo.tokens.jsonc +1 -1
  13. package/design-tokens/status.minimo.tokens.jsonc +4 -4
  14. package/design-tokens-src/badge.minimo.tokens.jsonc +2 -2
  15. package/design-tokens-src/basic.minimo.tokens.jsonc +1 -1
  16. package/design-tokens-src/btn-primary.minimo.tokens.jsonc +2 -2
  17. package/design-tokens-src/{btn-secondary.minimo.tokens.jsonc → btn-secondary.minimo.tokens.mjs} +7 -6
  18. package/design-tokens-src/color-accent.minimo.tokens.mjs +13 -0
  19. package/design-tokens-src/color-neutral.minimo.tokens.mjs +12 -0
  20. package/design-tokens-src/color-primary.minimo.tokens.mjs +13 -0
  21. package/design-tokens-src/color-secondary.minimo.tokens.mjs +12 -0
  22. package/design-tokens-src/components/recaptcha.minimo.tokens.jsonc +1 -1
  23. package/design-tokens-src/components/sf-macro.minimo.tokens.jsonc +1 -1
  24. package/design-tokens-src/components/spinner.minimo.tokens.jsonc +1 -1
  25. package/design-tokens-src/form.minimo.tokens.mjs +22 -0
  26. package/design-tokens-src/status.minimo.tokens.jsonc +4 -4
  27. package/design-tokens-src/web-components/simple-datatable.minimo.tokens.jsonc +2 -2
  28. package/design-tokens-utilities/build-tokens-src/config.mjs +8 -0
  29. package/design-tokens-utilities/build-tokens-src/formats/css.mjs +10 -2
  30. package/design-tokens-utilities/build-tokens-src/formats/penpot.mjs +20 -9
  31. package/design-tokens-utilities/build-tokens-src/lib/color-scale.mjs +81 -0
  32. package/design-tokens-utilities/build-tokens-src/platforms.mjs +4 -1
  33. package/design-tokens-utilities/build-tokens.mjs +3 -1
  34. package/design-tokens-utilities/check-unresolved-custom-props.mjs +21 -14
  35. package/design-tokens-utilities/tokens-config-sample.mjs +6 -0
  36. package/package.json +1 -1
  37. package/src/components/modal-alert/modal-alert.css +5 -5
  38. package/src/components/popup-page/popup-page.css +1 -1
  39. package/src/components/recaptcha/recaptcha.css +1 -1
  40. package/src/components/sf-macro/readme.md +55 -0
  41. package/src/components/sf-macro/sf-macro.css +13 -0
  42. package/src/css/alerts.css +4 -4
  43. package/src/css/buttons/status-buttons.css +4 -4
  44. package/src/css/forms/form-disabled-readonly.css +41 -0
  45. package/src/css/forms/forms.css +3 -19
  46. package/src/custom-properties-default.css +28 -23
  47. package/src/index-template.css +2 -4
  48. package/design-tokens-src/color-accent.minimo.tokens.jsonc +0 -13
  49. package/design-tokens-src/color-neutral.minimo.tokens.jsonc +0 -12
  50. package/design-tokens-src/color-primary.minimo.tokens.jsonc +0 -12
  51. package/design-tokens-src/color-secondary.minimo.tokens.jsonc +0 -12
@@ -9,12 +9,12 @@
9
9
  "background": {
10
10
  "color": {
11
11
  "$type": "color",
12
- "$value": "{primary.base}"
12
+ "$value": "{primary.100}"
13
13
  }
14
14
  },
15
15
  "color": {
16
16
  "$type": "color",
17
- "$value": "{primary.on-base}"
17
+ "$value": "{primary.100-fg}"
18
18
  },
19
19
  "font": {
20
20
  "size": {
@@ -28,7 +28,7 @@
28
28
  "text": {
29
29
  "muted": {
30
30
  "$type": "color",
31
- "$value": "{neutral.base}"
31
+ "$value": "{neutral.100}"
32
32
  },
33
33
  "dim": {
34
34
  "$type": "color",
@@ -10,12 +10,12 @@
10
10
  "background": {
11
11
  "color": {
12
12
  "$type": "color",
13
- "$value": "{primary.base}"
13
+ "$value": "{primary.100}"
14
14
  }
15
15
  },
16
16
  "color": {
17
17
  "$type": "color",
18
- "$value": "{primary.on-base}"
18
+ "$value": "{primary.100-fg}"
19
19
  },
20
20
  "border": {
21
21
  "color": {
@@ -6,11 +6,11 @@
6
6
  // -----------------------------------------------------------------------
7
7
  {
8
8
  "accent": {
9
- "base": {
9
+ "100": {
10
10
  "$type": "color",
11
11
  "$value": "#567d0d"
12
12
  },
13
- "on-base": {
13
+ "100-fg": {
14
14
  "$type": "color",
15
15
  "$value": "#fff"
16
16
  }
@@ -6,11 +6,11 @@
6
6
  // -----------------------------------------------------------------------
7
7
  {
8
8
  "neutral": {
9
- "base": {
9
+ "100": {
10
10
  "$type": "color",
11
11
  "$value": "#666"
12
12
  },
13
- "on-base": {
13
+ "100-fg": {
14
14
  "$type": "color",
15
15
  "$value": "#fff"
16
16
  }
@@ -6,11 +6,15 @@
6
6
  // -----------------------------------------------------------------------
7
7
  {
8
8
  "primary": {
9
- "base": {
9
+ "100": {
10
10
  "$type": "color",
11
11
  "$value": "#083865"
12
12
  },
13
- "on-base": {
13
+ "080": {
14
+ "$type": "color",
15
+ "$value": "#0957A0"
16
+ },
17
+ "100-fg": {
14
18
  "$type": "color",
15
19
  "$value": "#fff"
16
20
  }
@@ -6,11 +6,11 @@
6
6
  // -----------------------------------------------------------------------
7
7
  {
8
8
  "secondary": {
9
- "base": {
9
+ "100": {
10
10
  "$type": "color",
11
11
  "$value": "#650853"
12
12
  },
13
- "on-base": {
13
+ "100-fg": {
14
14
  "$type": "color",
15
15
  "$value": "#fff"
16
16
  }
@@ -19,7 +19,7 @@
19
19
  "$value": "#000"
20
20
  }
21
21
  },
22
- "foreground": {
22
+ "fg": {
23
23
  "$type": "color",
24
24
  "$value": "#fff"
25
25
  }
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "color": {
14
14
  "$type": "color",
15
- "$value": "{primary.base}"
15
+ "$value": "{primary.100}"
16
16
  },
17
17
  "stroke": {
18
18
  "width": {
@@ -136,7 +136,29 @@
136
136
  },
137
137
  "opacity": {
138
138
  "$type": "number",
139
- "$value": ".3"
139
+ "$value": 0.7
140
+ },
141
+ "font": {
142
+ "style": {
143
+ "$type": "string",
144
+ "$value": "italic"
145
+ }
146
+ }
147
+ },
148
+ "readonly": {
149
+ "background": {
150
+ "color": {
151
+ "$type": "color",
152
+ "$value": "{form.control.background.color}"
153
+ }
154
+ },
155
+ "color": {
156
+ "$type": "color",
157
+ "$value": "#666"
158
+ },
159
+ "opacity": {
160
+ "$type": "number",
161
+ "$value": 1
140
162
  },
141
163
  "font": {
142
164
  "style": {
@@ -19,7 +19,7 @@
19
19
  "$value": "#000"
20
20
  }
21
21
  },
22
- "foreground": {
22
+ "fg": {
23
23
  "$type": "color",
24
24
  "$value": "#fff"
25
25
  }
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "color": {
14
14
  "$type": "color",
15
- "$value": "{primary.base}"
15
+ "$value": "{primary.100}"
16
16
  },
17
17
  "stroke": {
18
18
  "width": {
@@ -17,7 +17,7 @@
17
17
  "$value": "{status.danger.color}"
18
18
  }
19
19
  },
20
- "foreground": {
20
+ "fg": {
21
21
  "$type": "color",
22
22
  "$value": "#fff"
23
23
  },
@@ -39,7 +39,7 @@
39
39
  "$value": "{status.success.color}"
40
40
  }
41
41
  },
42
- "foreground": {
42
+ "fg": {
43
43
  "$type": "color",
44
44
  "$value": "#fff"
45
45
  },
@@ -61,7 +61,7 @@
61
61
  "$value": "{status.info.color}"
62
62
  }
63
63
  },
64
- "foreground": {
64
+ "fg": {
65
65
  "$type": "color",
66
66
  "$value": "#fff"
67
67
  },
@@ -83,7 +83,7 @@
83
83
  "$value": "{status.warning.color}"
84
84
  }
85
85
  },
86
- "foreground": {
86
+ "fg": {
87
87
  "$type": "color",
88
88
  "$value": "#4f2900"
89
89
  },
@@ -2,12 +2,12 @@
2
2
  "badge": {
3
3
  "background": {
4
4
  "color": {
5
- "$value": "{primary.base}",
5
+ "$value": "{primary.100}",
6
6
  "$type": "color"
7
7
  }
8
8
  },
9
9
  "color": {
10
- "$value": "{primary.on-base}",
10
+ "$value": "{primary.100-fg}",
11
11
  "$type": "color"
12
12
  },
13
13
  "font": {
@@ -22,7 +22,7 @@
22
22
  "text": {
23
23
  "muted": {
24
24
  "$type": "color",
25
- "$value": "{neutral.base}"
25
+ "$value": "{neutral.100}"
26
26
  },
27
27
  "dim": {
28
28
  "$type": "color",
@@ -3,12 +3,12 @@
3
3
  "primary": {
4
4
  "background": {
5
5
  "color": {
6
- "$value": "{primary.base}",
6
+ "$value": "{primary.100}",
7
7
  "$type": "color"
8
8
  }
9
9
  },
10
10
  "color": {
11
- "$value": "{primary.on-base}",
11
+ "$value": "{primary.100-fg}",
12
12
  "$type": "color"
13
13
  },
14
14
 
@@ -1,13 +1,14 @@
1
- {
2
- "btn": {
3
- "secondary": {
1
+ export default {
2
+ btn: {
4
3
 
5
- "hollow": {
4
+ secondary: {
5
+
6
+ hollow: {
6
7
 
7
- } // end hollow
8
+ }
8
9
  } // end secondary
9
10
  } // end btn
10
- }
11
+ };
11
12
  // --btn-secondary-bg-color: var(--_secondary);
12
13
  // --btn-secondary-fg-color: var(--_secondary-fg);
13
14
  // --btn-secondary-border-color: var(--_secondary);
@@ -0,0 +1,13 @@
1
+ export default {
2
+ accent: {
3
+ 100: {
4
+ $value: '#567d0d',
5
+ $type: 'color'
6
+ },
7
+ '100-fg': {
8
+ $value: '#fff',
9
+ $type: 'color'
10
+ }
11
+ }
12
+
13
+ };
@@ -0,0 +1,12 @@
1
+ export default {
2
+ neutral: {
3
+ 100: {
4
+ $value: '#666',
5
+ $type: 'color'
6
+ },
7
+ '100-fg': {
8
+ $value: '#fff',
9
+ $type: 'color'
10
+ }
11
+ }
12
+ };
@@ -0,0 +1,13 @@
1
+ export default {
2
+ primary: {
3
+
4
+ 100: {
5
+ $value: '#083865',
6
+ $type: 'color'
7
+ },
8
+ '100-fg': {
9
+ $value: '#fff',
10
+ $type: 'color'
11
+ }
12
+ }
13
+ };
@@ -0,0 +1,12 @@
1
+ export default {
2
+ secondary: {
3
+ 100: {
4
+ $value: '#650853',
5
+ $type: 'color'
6
+ },
7
+ '100-fg': {
8
+ $value: '#fff',
9
+ $type: 'color'
10
+ }
11
+ }
12
+ };
@@ -15,7 +15,7 @@
15
15
  "$value": "#000"
16
16
  }
17
17
  },
18
- "foreground": {
18
+ "fg": {
19
19
  "$type": "color",
20
20
  "$value": "#fff"
21
21
  },
@@ -36,7 +36,7 @@
36
36
  "border": {
37
37
  "$type": "border",
38
38
  "$value": {
39
- "color": "{neutral.base}",
39
+ "color": "{neutral.100}",
40
40
  "width": "1px",
41
41
  "style": "solid"
42
42
  }
@@ -7,7 +7,7 @@
7
7
  },
8
8
 
9
9
  "color": {
10
- "$value": "{primary.base}",
10
+ "$value": "{primary.100}",
11
11
  "$type": "color"
12
12
  },
13
13
 
@@ -148,6 +148,28 @@ export default {
148
148
  }
149
149
  }
150
150
  },
151
+ readonly: {
152
+ background: {
153
+ color: {
154
+ $value: '{form.control.background.color}',
155
+ $type: 'color'
156
+ }
157
+ },
158
+ color: {
159
+ $value: '#666',
160
+ $type: 'color'
161
+ },
162
+ opacity: {
163
+ $value: 1,
164
+ $type: 'number'
165
+ },
166
+ font: {
167
+ style: {
168
+ $value: 'italic',
169
+ $type: 'string'
170
+ }
171
+ }
172
+ },
151
173
  required: {
152
174
  'flag-color': {
153
175
  $value: '#c00',
@@ -12,7 +12,7 @@
12
12
  "$type": "color"
13
13
  }
14
14
  },
15
- "foreground": {
15
+ "fg": {
16
16
  "$value": "#fff",
17
17
  "$type": "color"
18
18
  },
@@ -34,7 +34,7 @@
34
34
  "$type": "color"
35
35
  }
36
36
  },
37
- "foreground": {
37
+ "fg": {
38
38
  "$value": "#fff",
39
39
  "$type": "color"
40
40
  },
@@ -56,7 +56,7 @@
56
56
  "$type": "color"
57
57
  }
58
58
  },
59
- "foreground": {
59
+ "fg": {
60
60
  "$value": "#fff",
61
61
  "$type": "color"
62
62
  },
@@ -78,7 +78,7 @@
78
78
  "$type": "color"
79
79
  }
80
80
  },
81
- "foreground": {
81
+ "fg": {
82
82
  "$value": "#4f2900",
83
83
  "$type": "color"
84
84
  },
@@ -5,7 +5,7 @@
5
5
  "hover": {
6
6
  "background": {
7
7
  "color": {
8
- "$value": "color-mix(in srgb, {table.thead.background.color} 90%, {accent.base})",
8
+ "$value": "color-mix(in srgb, {table.thead.background.color} 90%, {accent.100})",
9
9
  "$type": "color"
10
10
  }
11
11
  }
@@ -16,7 +16,7 @@
16
16
  "border": {
17
17
  "$type": "border",
18
18
  "$value": {
19
- "color": "{neutral.base}",
19
+ "color": "{neutral.100}",
20
20
  "width": "{btn.border.width}",
21
21
  // "style": "solid"
22
22
  }
@@ -68,6 +68,14 @@ export const penpotFormat = buildConfig.penpotFormat === 'jsonc' ? 'jsonc' : 'js
68
68
  // 'keep' (default) — write the expression as-is
69
69
  // 'calc' — wrap in CSS calc()
70
70
  // 'resolve' — evaluate numerically; unit inherited from the first referenced token
71
+ // colorScalePrefixes: list of color token prefixes for which the numeric scale
72
+ // segment is zero-padded to 3 digits (e.g. --neutral-80 -> --neutral-080).
73
+ // Applied to both CSS custom property names and Penpot DTCG token keys.
74
+ // Set to an empty array or omit to disable.
75
+ export const colorScalePrefixes = Array.isArray(buildConfig.colorScalePrefixes)
76
+ ? buildConfig.colorScalePrefixes
77
+ : [];
78
+
71
79
  const VALID_EXPR_MODES = ['keep', 'calc', 'resolve'];
72
80
  export const penpotExpressions = VALID_EXPR_MODES.includes(buildConfig.penpotExpressions)
73
81
  ? buildConfig.penpotExpressions
@@ -6,6 +6,7 @@
6
6
  // - individual properties for letterSpacing, textTransform, textDecoration
7
7
 
8
8
  import StyleDictionary from 'style-dictionary';
9
+ import { padColorName } from '../lib/color-scale.mjs';
9
10
 
10
11
  const BASE_FONT_SIZE = 16;
11
12
 
@@ -147,6 +148,8 @@ StyleDictionary.registerFormat({
147
148
 
148
149
  const resolveRefs = makeResolveRefs(tokenByPath);
149
150
 
151
+ const colorScalePrefixes = options.colorScalePrefixes ?? [];
152
+
150
153
  const lines = dictionary.allTokens
151
154
  .slice()
152
155
  .sort((a, b) => a.name.localeCompare(b.name))
@@ -154,8 +157,13 @@ StyleDictionary.registerFormat({
154
157
  const type = token.$type ?? token.type;
155
158
  const orig = token.original?.$value ?? token.original?.value;
156
159
 
160
+ // Apply zero-padding to color scale names (e.g. --neutral-80 -> --neutral-080)
161
+ const tokenName = (type === 'color')
162
+ ? padColorName(token.name, colorScalePrefixes)
163
+ : token.name;
164
+
157
165
  if (type === 'typography') {
158
- return [buildTypography(token.name, orig ?? {}, resolveRefs)];
166
+ return [buildTypography(tokenName, orig ?? {}, resolveRefs)];
159
167
  }
160
168
 
161
169
  let value;
@@ -192,7 +200,7 @@ StyleDictionary.registerFormat({
192
200
  value = String(token.$value ?? token.value);
193
201
  }
194
202
 
195
- return [` --${token.name}: ${value};`];
203
+ return [` --${tokenName}: ${value};`];
196
204
  });
197
205
 
198
206
  customPropsCount = lines.length;
@@ -33,6 +33,7 @@
33
33
 
34
34
  import StyleDictionary from 'style-dictionary';
35
35
  import path from 'path';
36
+ import { padTokenPath } from '../lib/color-scale.mjs';
36
37
 
37
38
  // Disclaimer prepended to every generated file when penpotFormat is 'jsonc'
38
39
  const DISCLAIMER = [
@@ -126,20 +127,28 @@ StyleDictionary.registerFormat({
126
127
  name: 'json/penpot',
127
128
  format: ({ dictionary, options }) => {
128
129
  const root = {};
129
- const expressionMode = options.penpotExpressions ?? 'keep'; // 'keep' | 'calc' | 'resolve'
130
+ const expressionMode = options.penpotExpressions ?? 'keep';
131
+ const colorScalePrefixes = options.colorScalePrefixes ?? [];
130
132
  const tokenMap = (expressionMode === 'resolve') ? makeTokenMap(dictionary) : null;
131
133
 
132
134
  for (const token of dictionary.allTokens) {
133
- // Rebuild the nested tree from the token path
135
+ const type = token.$type ?? token.type;
136
+
137
+ // Apply zero-padding to color scale path segments
138
+ // e.g. ['neutral', '80'] -> ['neutral', '080']
139
+ const tokenPath = (type === 'color')
140
+ ? padTokenPath(token.path, colorScalePrefixes)
141
+ : token.path;
142
+
143
+ // Rebuild the nested tree from the (possibly padded) token path
134
144
  let node = root;
135
- for (let i = 0; i < token.path.length - 1; i++) {
136
- const segment = token.path[i];
145
+ for (let i = 0; i < tokenPath.length - 1; i++) {
146
+ const segment = tokenPath[i];
137
147
  node[segment] = node[segment] ?? {};
138
148
  node = node[segment];
139
149
  }
140
150
 
141
- const leafKey = token.path[token.path.length - 1];
142
- const type = token.$type ?? token.type;
151
+ const leafKey = tokenPath[tokenPath.length - 1];
143
152
  const orig = token.original?.$value ?? token.original?.value;
144
153
 
145
154
  // ── Value resolution ────────────────────────────────────────────────────
@@ -223,13 +232,15 @@ export const collectConcreteFilePaths = async (sd) => {
223
232
  // @param {string|null} penpotDestFile Aggregated file base name, or null
224
233
  // @param {'json'|'jsonc'} penpotFormat Output format
225
234
  // @param {'keep'|'calc'|'resolve'} penpotExpressions Expression handling mode
235
+ // @param {string[]} colorScalePrefixes Color token prefixes for numeric scale zero-padding
226
236
  // @returns {object[]} File descriptors for the Style Dictionary platform
227
237
 
228
238
  export const buildPenpotFiles = (
229
239
  concreteFilePaths,
230
240
  penpotDestFile,
231
241
  penpotFormat = 'json',
232
- penpotExpressions = 'keep'
242
+ penpotExpressions = 'keep',
243
+ colorScalePrefixes = []
233
244
  ) => {
234
245
  const ext = penpotFormat === 'jsonc' ? '.jsonc' : '.json';
235
246
  const jsonc = penpotFormat === 'jsonc';
@@ -238,7 +249,7 @@ export const buildPenpotFiles = (
238
249
  return [{
239
250
  destination: penpotDestFile + ext,
240
251
  format: 'json/penpot',
241
- options: { jsonc, penpotExpressions },
252
+ options: { jsonc, penpotExpressions, colorScalePrefixes },
242
253
  }];
243
254
  }
244
255
 
@@ -252,7 +263,7 @@ export const buildPenpotFiles = (
252
263
  return {
253
264
  destination: destName,
254
265
  format: 'json/penpot',
255
- options: { jsonc, penpotExpressions },
266
+ options: { jsonc, penpotExpressions, colorScalePrefixes },
256
267
  filter: (token) => {
257
268
  const fp = token.filePath ? path.resolve(token.filePath) : '';
258
269
  return fp === absPath;
@@ -0,0 +1,81 @@
1
+ // build-tokens-src/lib/color-scale.mjs
2
+ // Shared helper for color scale zero-padding.
3
+ //
4
+ // When colorScalePrefixes is configured, color tokens whose name matches
5
+ // --<prefix>-<number> (e.g. --primary-100, --neutral-80)
6
+ // have the numeric segment zero-padded to 3 digits:
7
+ // --primary-100 -> --primary-100 (unchanged, already 3 digits)
8
+ // --neutral-80 -> --neutral-080
9
+ // --secondary-5 -> --secondary-005
10
+ //
11
+ // The same transformation is applied to CSS custom property names and to
12
+ // Penpot DTCG token keys, so both outputs stay in sync.
13
+ //
14
+ // Configuration (in the project config file):
15
+ // colorScalePrefixes: ['primary', 'secondary', 'neutral']
16
+ //
17
+ // Set to an empty array or omit to disable the feature entirely.
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // padColorName
21
+ //
22
+ // Given a full token name (kebab-case, no leading --) and the list of
23
+ // configured prefixes, returns the name with the numeric segment zero-padded
24
+ // if applicable, or the original name unchanged otherwise.
25
+ //
26
+ // Examples:
27
+ // padColorName('primary-100', ['primary']) -> 'primary-100'
28
+ // padColorName('neutral-80', ['neutral']) -> 'neutral-080'
29
+ // padColorName('neutral-80', []) -> 'neutral-80'
30
+ // padColorName('size-base', ['primary']) -> 'size-base'
31
+ // ---------------------------------------------------------------------------
32
+ export const padColorName = (name, prefixes) => {
33
+ if (!prefixes || prefixes.length === 0) return name;
34
+
35
+ // Match: <prefix>-<integer> optionally followed by more segments
36
+ // The number must be a pure integer (no decimals, no units).
37
+ const pattern = new RegExp(
38
+ `^(${prefixes.map(p => escapeRegex(p)).join('|')})-([0-9]+)(-.+)?$`
39
+ );
40
+
41
+ const match = name.match(pattern);
42
+ if (!match) return name;
43
+
44
+ const [, prefix, num, rest = ''] = match;
45
+ const padded = num.padStart(3, '0');
46
+
47
+ return `${prefix}-${padded}${rest}`;
48
+ };
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // padTokenPath
52
+ //
53
+ // Applies the same zero-padding logic to a DTCG token path array.
54
+ // Used by the Penpot format to rename leaf keys and intermediate path segments.
55
+ //
56
+ // The segment to pad is the one immediately following the matching prefix.
57
+ // If the prefix occupies multiple path segments this function is intentionally
58
+ // conservative: it only pads when the prefix is a single path segment.
59
+ //
60
+ // Examples (path arrays):
61
+ // ['primary', '100'] -> ['primary', '100'] (unchanged)
62
+ // ['neutral', '80'] -> ['neutral', '080']
63
+ // ['color', 'neutral', '80'] with prefix 'neutral' -> ['color', 'neutral', '080']
64
+ // ---------------------------------------------------------------------------
65
+ export const padTokenPath = (pathArr, prefixes) => {
66
+ if (!prefixes || prefixes.length === 0) return pathArr;
67
+
68
+ return pathArr.map((segment, i) => {
69
+ // Is this segment a pure integer and is the previous segment a prefix?
70
+ if (i === 0) return segment;
71
+ const prev = pathArr[i - 1];
72
+ if (!prefixes.includes(prev)) return segment;
73
+ if (!/^[0-9]+$/.test(segment)) return segment;
74
+ return segment.padStart(3, '0');
75
+ });
76
+ };
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Internal: escape special regex characters in a prefix string
80
+ // ---------------------------------------------------------------------------
81
+ const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');