@transtyle/exporter-primeng 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@transtyle/exporter-primeng",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Transtyle exporter for PrimeNG design-token presets (definePreset over Aura). Not yet registered in the CLI — see docs/plan/component-tier.md C6.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "dependencies": {
11
+ "@transtyle/ir": "0.1.0-alpha.0"
12
+ },
13
+ "transtyle": {
14
+ "kind": "exporter",
15
+ "name": "primeng",
16
+ "irSpec": "v0-draft",
17
+ "pluginApi": "0",
18
+ "targets": {
19
+ "primeng": [
20
+ "*"
21
+ ]
22
+ },
23
+ "modes": [
24
+ "color-scheme"
25
+ ],
26
+ "capabilities": [
27
+ "build"
28
+ ]
29
+ },
30
+ "files": [
31
+ "src",
32
+ "surface-inventory.json"
33
+ ],
34
+ "publishConfig": { "access": "public" },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/transtyle/transtyle.git",
38
+ "directory": "packages/exporter-primeng"
39
+ },
40
+ "homepage": "https://github.com/transtyle/transtyle#readme",
41
+ "bugs": "https://github.com/transtyle/transtyle/issues",
42
+ "license": "MIT"
43
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Exporter-private archetype helpers (docs/proposals/0002-component-theming-primeng.md
3
+ * §2.3, §2.8, §4; docs/plan/component-tier.md C5). Per C1's cross-ecosystem
4
+ * study (docs/findings/component-tier-study.md), `field`/`list`/`navigation`/
5
+ * `overlay` are PrimeNG-specific groupings, not convergent vocabulary — they
6
+ * live here, permanently, reading straight from existing catalog cells.
7
+ * Nothing here is a catalog addition; every value below is `get(map, ...)`
8
+ * against a slot that already exists in `semantic.*`.
9
+ *
10
+ * Each helper returns `{ structural, colorScheme }` — verified against real
11
+ * PrimeNG source (`aura/base/index.ts`, fetched 2026-07-21), which itself
12
+ * splits every one of these groups the same way: a mode-invariant object
13
+ * (padding/gap/radius/focusRing-shape) at the top level, and a SEPARATE
14
+ * `colorScheme.{light,dark}.<group>` object for anything color-ish. This
15
+ * isn't a stylistic choice on our end — PrimeNG's own DesignTokens TypeScript
16
+ * types reject color fields on the top-level object and reject dimensional
17
+ * fields on the colorScheme-scoped one, so index.js calls each helper once
18
+ * per mode and assembles both halves into the shape PrimeNG's types require.
19
+ */
20
+
21
+ const get = (map, path) => map.get(`semantic.${path}`)?.value;
22
+ /** Component-tier read (AL2) — the promoted shared interactive-control slots. */
23
+ const comp = (map, path) => map.get(`component.${path}`)?.value;
24
+
25
+ export function field(map) {
26
+ return {
27
+ structural: {
28
+ // AL2: the shared control geometry is catalog vocabulary now, so an
29
+ // authored `component.control.*` reaches PrimeNG's formField (and, via
30
+ // buildButton, its Button) exactly as it reaches Bootstrap's
31
+ // $input-btn-*. Unauthored, these resolve to the same space.4/space.2/
32
+ // radius.control this helper read directly before.
33
+ paddingX: comp(map, 'control.padding-x'),
34
+ paddingY: comp(map, 'control.padding-y'),
35
+ borderRadius: comp(map, 'control.radius'),
36
+ transitionDuration: get(map, 'duration.fast'),
37
+ sm: { paddingX: get(map, 'space.3'), paddingY: get(map, 'space.1') },
38
+ lg: { paddingX: get(map, 'space.5'), paddingY: get(map, 'space.3') },
39
+ },
40
+ colorScheme: {
41
+ background: get(map, 'color.elevation.0.surface'),
42
+ disabledBackground: get(map, 'color.neutral.tint'),
43
+ filledBackground: get(map, 'color.elevation.1.surface'),
44
+ borderColor: get(map, 'color.neutral.outline'),
45
+ hoverBorderColor: get(map, 'color.neutral.outline-hover'),
46
+ focusBorderColor: get(map, 'color.primary.solid'),
47
+ invalidBorderColor: get(map, 'color.danger.solid'),
48
+ color: get(map, 'color.text.base'),
49
+ disabledColor: get(map, 'color.text.disabled'),
50
+ placeholderColor: get(map, 'color.text.muted'),
51
+ shadow: 'none', // no direct grid cell for a form-field elevation shadow; PrimeNG's own Aura default is flat too
52
+ },
53
+ };
54
+ }
55
+
56
+ export function list(map) {
57
+ return {
58
+ structural: {
59
+ padding: get(map, 'space.1'),
60
+ gap: get(map, 'space.0'),
61
+ header: { padding: get(map, 'space.2') },
62
+ option: { padding: get(map, 'space.2'), borderRadius: get(map, 'radius.control') },
63
+ optionGroup: {
64
+ padding: get(map, 'space.2'),
65
+ fontWeight: String(get(map, 'type.weight.semibold')),
66
+ },
67
+ },
68
+ colorScheme: {
69
+ option: {
70
+ focusBackground: get(map, 'color.primary.tint'),
71
+ selectedBackground: get(map, 'color.primary.tint-active'),
72
+ selectedFocusBackground: get(map, 'color.primary.tint-active'),
73
+ color: get(map, 'color.text.base'),
74
+ focusColor: get(map, 'color.text.base'),
75
+ selectedColor: get(map, 'color.primary.on-tint'),
76
+ selectedFocusColor: get(map, 'color.primary.on-tint'),
77
+ },
78
+ optionGroup: { background: 'transparent', color: get(map, 'color.text.muted') },
79
+ },
80
+ };
81
+ }
82
+
83
+ export function navigation(map) {
84
+ return {
85
+ structural: {
86
+ list: { padding: get(map, 'space.1'), gap: get(map, 'space.0') },
87
+ item: {
88
+ padding: get(map, 'space.2'),
89
+ borderRadius: get(map, 'radius.control'),
90
+ gap: get(map, 'space.2'),
91
+ },
92
+ submenuLabel: {
93
+ padding: get(map, 'space.2'),
94
+ fontWeight: String(get(map, 'type.weight.semibold')),
95
+ },
96
+ },
97
+ colorScheme: {
98
+ item: {
99
+ focusBackground: get(map, 'color.primary.tint'),
100
+ activeBackground: get(map, 'color.primary.tint-active'),
101
+ color: get(map, 'color.text.base'),
102
+ focusColor: get(map, 'color.text.base'),
103
+ activeColor: get(map, 'color.text.base'),
104
+ icon: { color: get(map, 'color.text.muted'), focusColor: get(map, 'color.text.base') },
105
+ },
106
+ submenuLabel: { background: 'transparent', color: get(map, 'color.text.muted') },
107
+ },
108
+ };
109
+ }
110
+
111
+ /** kind: 'popover' | 'modal' | 'navigation' | 'select' — each picks an elevation level. */
112
+ const OVERLAY_LEVEL = { select: 2, popover: 2, modal: 3, navigation: 2 };
113
+
114
+ export function overlay(map, kind, ctx) {
115
+ const n = OVERLAY_LEVEL[kind] ?? 2;
116
+ const shadow = map.get(`semantic.color.elevation.${n}.shadow`)?.value;
117
+ const shadowStr = shadow
118
+ ? `${shadow.offsetX} ${shadow.offsetY} ${shadow.blur} ${shadow.spread} ${ctx.formatColor(shadow.color)}`
119
+ : undefined;
120
+ return {
121
+ // borderRadius/shadow are mode-invariant at this position in PrimeNG's own
122
+ // type (verified: aura/base's top-level `overlay.*` carries no background/color at
123
+ // all) — `shadow` is computed from whichever map is passed; callers pass the light
124
+ // map once for this half, matching PrimeNG's own single (non-mode-varying) shadow.
125
+ // `padding` is NOT part of this object — verified it belongs on each component's
126
+ // own `content.padding` (Popover/Dialog), not on the shared semantic.overlay.* shape.
127
+ structural: {
128
+ ...(kind !== 'navigation' && {
129
+ borderRadius: get(map, kind === 'modal' ? 'radius.container' : 'radius.field'),
130
+ }),
131
+ shadow: shadowStr,
132
+ },
133
+ padding:
134
+ kind === 'navigation' || kind === 'select'
135
+ ? undefined
136
+ : get(map, kind === 'modal' ? 'space.6' : 'space.3'),
137
+ // navigation has no colorScheme-scoped entry in real PrimeNG (Menu's root reads
138
+ // `content.*` for its own background/color instead) — callers skip this for 'navigation'.
139
+ colorScheme:
140
+ kind === 'navigation'
141
+ ? undefined
142
+ : {
143
+ background: get(map, `color.elevation.${n}.surface`),
144
+ borderColor: get(map, 'color.neutral.outline'),
145
+ color: get(map, 'color.text.base'),
146
+ },
147
+ };
148
+ }
149
+
150
+ /** Card/Panel-style containers (§4: "content needs no helper" — inline, kept as one function for call-site symmetry). */
151
+ export function content(map) {
152
+ return {
153
+ structural: { borderRadius: get(map, 'radius.field') },
154
+ colorScheme: {
155
+ background: get(map, 'color.elevation.1.surface'),
156
+ hoverBackground: get(map, 'color.elevation.1.surface'),
157
+ borderColor: get(map, 'color.neutral.outline'),
158
+ color: get(map, 'color.text.base'),
159
+ hoverColor: get(map, 'color.text.base'),
160
+ },
161
+ };
162
+ }
@@ -0,0 +1,468 @@
1
+ /**
2
+ * Per-component builders (docs/plan/component-tier.md C4). Each reads either
3
+ * `mapSeverityGrid` (severity-colored components) or a plain semantic alias
4
+ * (primary-anchored components) or an archetype helper (field/list/navigation/
5
+ * overlay consumers) — zero per-component mapping *logic*, only which cells
6
+ * a component's real shape (verified against source, fetched 2026-07-21) asks
7
+ * for. See docs/worklog/2026-07-21-component-tier-c3-c5.md for what was
8
+ * verified and what this deliberately does not cover yet.
9
+ *
10
+ * Every builder takes `(light, dark, ctx)` — two per-mode token maps — and
11
+ * returns `{ tokens, coverage }` with `tokens.colorScheme = { light, dark }`
12
+ * wherever the real PrimeNG type requires that split (verified: it's not
13
+ * optional — PrimeNG's own DesignTokens types reject an unwrapped colorScheme
14
+ * object, confirmed by compiling the emitted preset against them directly).
15
+ *
16
+ * A real, corrective finding from verifying source rather than assuming
17
+ * proposal 0002's ~25-35 estimate (§5.2): most components proposal 0002
18
+ * expected to be severity-colored are NOT. Only Button, Tag, Badge, Message,
19
+ * and InlineMessage actually carry the `variant x severity x part` (or flat
20
+ * `severity x part`) shape. Checkbox, RadioButton, ToggleSwitch, SelectButton,
21
+ * ToggleButton, and SplitButton are `field`-shaped (single primary + neutral
22
+ * surfaces, no severity axis). ProgressBar, Slider, Knob, and Rating are
23
+ * primary-anchored only — and, verified against source, carry NO colorScheme
24
+ * block at all; their real Aura default literally writes token-reference
25
+ * strings like `'{primary.color}'`/`'{text.muted.color}'` rather than
26
+ * resolving a color, and PrimeNG's own alias engine resolves those per the
27
+ * active mode — so this file reuses that exact convention for them (a
28
+ * resolved literal would be mode-wrong: there's no per-mode slot to put it
29
+ * in). Chip has no brand color at all (plain neutral surface, not covered
30
+ * in this pass).
31
+ */
32
+
33
+ import { mapSeverityGrid } from './severity-grid.js';
34
+ import { field, list, navigation, overlay, content } from './archetypes.js';
35
+
36
+ const get = (map, path) => map.get(`semantic.${path}`)?.value;
37
+
38
+ // ---------- severity-colored components (the generic mapper, called once per mode) ----------
39
+
40
+ function buildButtonColorScheme(map) {
41
+ const g = mapSeverityGrid(map, {
42
+ variants: [
43
+ {
44
+ name: 'root',
45
+ parts: {
46
+ background: 'solid',
47
+ hoverBackground: 'solid-hover',
48
+ activeBackground: 'solid-active',
49
+ borderColor: 'solid',
50
+ hoverBorderColor: 'solid-hover',
51
+ activeBorderColor: 'solid-active',
52
+ color: 'on-solid',
53
+ hoverColor: 'on-solid',
54
+ activeColor: 'on-solid',
55
+ focusRingColor: 'solid',
56
+ },
57
+ },
58
+ {
59
+ name: 'outlined',
60
+ parts: {
61
+ hoverBackground: 'tint',
62
+ activeBackground: 'tint-active',
63
+ borderColor: 'outline',
64
+ color: 'text',
65
+ },
66
+ },
67
+ {
68
+ name: 'text',
69
+ parts: { hoverBackground: 'tint', activeBackground: 'tint-active', color: 'text' },
70
+ },
71
+ ],
72
+ });
73
+ const scheme = { root: {}, outlined: {}, text: {} };
74
+ for (const severity of [
75
+ 'primary',
76
+ 'secondary',
77
+ 'success',
78
+ 'info',
79
+ 'warn',
80
+ 'danger',
81
+ 'contrast',
82
+ ]) {
83
+ scheme.root[severity] = {
84
+ background: g.get('root', severity, 'background'),
85
+ hoverBackground: g.get('root', severity, 'hoverBackground'),
86
+ activeBackground: g.get('root', severity, 'activeBackground'),
87
+ borderColor: g.get('root', severity, 'borderColor'),
88
+ hoverBorderColor: g.get('root', severity, 'hoverBorderColor'),
89
+ activeBorderColor: g.get('root', severity, 'activeBorderColor'),
90
+ color: g.get('root', severity, 'color'),
91
+ hoverColor: g.get('root', severity, 'hoverColor'),
92
+ activeColor: g.get('root', severity, 'activeColor'),
93
+ focusRing: { color: g.get('root', severity, 'focusRingColor'), shadow: 'none' },
94
+ };
95
+ scheme.outlined[severity] = {
96
+ hoverBackground: g.get('outlined', severity, 'hoverBackground'),
97
+ activeBackground: g.get('outlined', severity, 'activeBackground'),
98
+ borderColor: g.get('outlined', severity, 'borderColor'),
99
+ color: g.get('outlined', severity, 'color'),
100
+ };
101
+ scheme.text[severity] = {
102
+ hoverBackground: g.get('text', severity, 'hoverBackground'),
103
+ activeBackground: g.get('text', severity, 'activeBackground'),
104
+ color: g.get('text', severity, 'color'),
105
+ };
106
+ }
107
+ return {
108
+ scheme,
109
+ coverage: g.coverage.map((c) => ({ ...c, variable: `button.colorScheme.${c.variable}` })),
110
+ };
111
+ }
112
+
113
+ export function buildButton(light, dark, ctx) {
114
+ const l = buildButtonColorScheme(light);
115
+ const d = buildButtonColorScheme(dark);
116
+ const f = field(light).structural;
117
+ // AL2 parity fix: Button reads the BUTTON layer (which defaults from
118
+ // control), not the shared field object directly. Before this, an authored
119
+ // `component.button.radius` moved Bootstrap but not PrimeNG — the two
120
+ // targets disagreed about what authoring a button token meant.
121
+ const btn = (t) => light.get(`component.button.${t}`)?.value;
122
+ return {
123
+ tokens: {
124
+ root: {
125
+ borderRadius: btn('radius'),
126
+ paddingX: btn('padding-x'),
127
+ paddingY: btn('padding-y'),
128
+ gap: get(light, 'space.2'),
129
+ transitionDuration: f.transitionDuration,
130
+ },
131
+ colorScheme: { light: l.scheme, dark: d.scheme },
132
+ ...(ctx.roleArchetypeExtend ? { extend: ctx.roleArchetypeExtend } : {}),
133
+ },
134
+ coverage: l.coverage,
135
+ };
136
+ }
137
+
138
+ function buildFlatSeverity(map, { variants, severities, path }) {
139
+ const g = mapSeverityGrid(map, { variants, severities });
140
+ const scheme = {};
141
+ for (const severity of severities) {
142
+ scheme[severity] = {};
143
+ for (const field of Object.keys(variants[0].parts))
144
+ scheme[severity][field] = g.get(variants[0].name, severity, field);
145
+ }
146
+ return {
147
+ scheme,
148
+ coverage: g.coverage.map((c) => ({ ...c, variable: `${path}.colorScheme.${c.variable}` })),
149
+ };
150
+ }
151
+
152
+ export function buildTag(light, dark) {
153
+ const opts = {
154
+ variants: [{ name: 'root', parts: { background: 'tint', color: 'text' } }],
155
+ severities: ['primary', 'secondary', 'success', 'info', 'warn', 'danger', 'contrast'],
156
+ path: 'tag',
157
+ };
158
+ const l = buildFlatSeverity(light, opts);
159
+ const d = buildFlatSeverity(dark, opts);
160
+ return { tokens: { colorScheme: { light: l.scheme, dark: d.scheme } }, coverage: l.coverage };
161
+ }
162
+
163
+ export function buildBadge(light, dark) {
164
+ const opts = {
165
+ variants: [{ name: 'root', parts: { background: 'solid', color: 'on-solid' } }],
166
+ severities: ['primary', 'secondary', 'success', 'info', 'warn', 'danger', 'contrast'],
167
+ path: 'badge',
168
+ };
169
+ const l = buildFlatSeverity(light, opts);
170
+ const d = buildFlatSeverity(dark, opts);
171
+ return { tokens: { colorScheme: { light: l.scheme, dark: d.scheme } }, coverage: l.coverage };
172
+ }
173
+
174
+ /** Message nests severity-major (colorScheme.<mode>.<severity>.{filled fields, outlined:{}, simple:{}}) — the opposite variant/severity order from Button. Verified against source, not assumed. No `primary` severity (info/success/warn/danger/secondary/contrast only) — a real PrimeNG inconsistency with Button/Tag/Badge, not ours. */
175
+ function buildMessageColorScheme(map) {
176
+ const g = mapSeverityGrid(map, {
177
+ severities: ['info', 'success', 'warn', 'danger', 'secondary', 'contrast'],
178
+ variants: [
179
+ { name: 'filled', parts: { background: 'tint', borderColor: 'outline', color: 'text' } },
180
+ { name: 'outlined', parts: { color: 'text', borderColor: 'text' } },
181
+ { name: 'simple', parts: { color: 'text' } },
182
+ ],
183
+ });
184
+ const scheme = {};
185
+ for (const severity of ['info', 'success', 'warn', 'danger', 'secondary', 'contrast']) {
186
+ const key = severity === 'danger' ? 'error' : severity; // real PrimeNG naming inconsistency (verified against source), not ours
187
+ scheme[key] = {
188
+ background: g.get('filled', severity, 'background'),
189
+ borderColor: g.get('filled', severity, 'borderColor'),
190
+ color: g.get('filled', severity, 'color'),
191
+ outlined: {
192
+ color: g.get('outlined', severity, 'color'),
193
+ borderColor: g.get('outlined', severity, 'borderColor'),
194
+ },
195
+ simple: { color: g.get('simple', severity, 'color') },
196
+ };
197
+ }
198
+ return {
199
+ scheme,
200
+ coverage: g.coverage.map((c) => ({ ...c, variable: `message.colorScheme.${c.variable}` })),
201
+ };
202
+ }
203
+
204
+ export function buildMessage(light, dark) {
205
+ const l = buildMessageColorScheme(light);
206
+ const d = buildMessageColorScheme(dark);
207
+ return { tokens: { colorScheme: { light: l.scheme, dark: d.scheme } }, coverage: l.coverage };
208
+ }
209
+
210
+ /** Flat like Tag/Badge but uses `danger`-as-`error` naming (real PrimeNG inconsistency) plus a `shadow` field, approximated from the role's solid color at fixed low alpha (PrimeNG's own convention: `color-mix(..., transparent 96%)`). */
211
+ function buildInlineMessageColorScheme(map, ctx) {
212
+ const g = mapSeverityGrid(map, {
213
+ severities: ['info', 'success', 'warn', 'danger', 'secondary', 'contrast'],
214
+ variants: [
215
+ { name: 'root', parts: { background: 'tint', borderColor: 'outline', color: 'text' } },
216
+ ],
217
+ });
218
+ const scheme = {};
219
+ const coverage = [...g.coverage];
220
+ const roleFor = {
221
+ info: 'info',
222
+ success: 'success',
223
+ warn: 'warning',
224
+ danger: 'danger',
225
+ secondary: 'secondary',
226
+ };
227
+ for (const severity of ['info', 'success', 'warn', 'danger', 'secondary', 'contrast']) {
228
+ const solid =
229
+ severity === 'contrast'
230
+ ? map.get('semantic.color.neutral.text-strong')?.value
231
+ : map.get(`semantic.color.${roleFor[severity]}.solid`)?.value;
232
+ const key = severity === 'danger' ? 'error' : severity;
233
+ scheme[key] = {
234
+ background: g.get('root', severity, 'background'),
235
+ borderColor: g.get('root', severity, 'borderColor'),
236
+ color: g.get('root', severity, 'color'),
237
+ shadow: solid ? `0px 4px 8px 0px ${ctx.formatColor({ ...solid, alpha: 0.04 })}` : undefined,
238
+ };
239
+ if (solid)
240
+ coverage.push({
241
+ variable: `inlinemessage.colorScheme.${severity}.shadow`,
242
+ slot: '—',
243
+ class: 'approximated',
244
+ note: "fixed low-alpha tint of the role solid color, matching PrimeNG's own color-mix(..., transparent 96%) convention — not a colorimetric shadow derivation",
245
+ });
246
+ }
247
+ return {
248
+ scheme,
249
+ coverage: coverage.map((c) =>
250
+ c.variable?.startsWith('inlinemessage')
251
+ ? c
252
+ : { ...c, variable: `inlinemessage.colorScheme.${c.variable}` },
253
+ ),
254
+ };
255
+ }
256
+
257
+ export function buildInlineMessage(light, dark, ctx) {
258
+ const l = buildInlineMessageColorScheme(light, ctx);
259
+ const d = buildInlineMessageColorScheme(dark, ctx);
260
+ return { tokens: { colorScheme: { light: l.scheme, dark: d.scheme } }, coverage: l.coverage };
261
+ }
262
+
263
+ // ---------- primary-anchored components: verified to have NO colorScheme block in real
264
+ // PrimeNG at all — their own Aura default writes a bare token-reference string, which
265
+ // PrimeNG's own alias engine resolves per the active mode. We reuse those exact strings
266
+ // rather than resolving a literal (a literal would be mode-wrong: there is no per-mode
267
+ // slot to put a second value in). Not mode-dependent, so a single build suffices. ----------
268
+
269
+ export function buildProgressBar() {
270
+ return {
271
+ tokens: {
272
+ value: { background: '{primary.color}' },
273
+ label: { color: '{primary.contrastColor}' },
274
+ },
275
+ coverage: [
276
+ {
277
+ variable: 'progressbar.value.background',
278
+ slot: 'PrimeNG preset {primary.color} (runtime alias, not an IR path)',
279
+ class: 'native',
280
+ },
281
+ ],
282
+ };
283
+ }
284
+
285
+ export function buildRating() {
286
+ return {
287
+ tokens: {
288
+ icon: {
289
+ color: '{text.mutedColor}',
290
+ hoverColor: '{primary.color}',
291
+ activeColor: '{primary.color}',
292
+ },
293
+ },
294
+ coverage: [
295
+ {
296
+ variable: 'rating.icon.hoverColor',
297
+ slot: 'PrimeNG preset {primary.color} (runtime alias, not an IR path)',
298
+ class: 'native',
299
+ },
300
+ ],
301
+ };
302
+ }
303
+
304
+ // ---------- archetype-consumer components (§4/§5, C5) ----------
305
+
306
+ export function buildListbox(light, dark) {
307
+ const fl = field(light),
308
+ fd = field(dark);
309
+ const ll = list(light),
310
+ ld = list(dark);
311
+ return {
312
+ tokens: {
313
+ root: {
314
+ borderRadius: fl.structural.borderRadius,
315
+ transitionDuration: fl.structural.transitionDuration,
316
+ },
317
+ colorScheme: {
318
+ light: {
319
+ root: {
320
+ background: fl.colorScheme.background,
321
+ disabledBackground: fl.colorScheme.disabledBackground,
322
+ borderColor: fl.colorScheme.borderColor,
323
+ invalidBorderColor: fl.colorScheme.invalidBorderColor,
324
+ color: fl.colorScheme.color,
325
+ disabledColor: fl.colorScheme.disabledColor,
326
+ },
327
+ option: ll.colorScheme.option,
328
+ },
329
+ dark: {
330
+ root: {
331
+ background: fd.colorScheme.background,
332
+ disabledBackground: fd.colorScheme.disabledBackground,
333
+ borderColor: fd.colorScheme.borderColor,
334
+ invalidBorderColor: fd.colorScheme.invalidBorderColor,
335
+ color: fd.colorScheme.color,
336
+ disabledColor: fd.colorScheme.disabledColor,
337
+ },
338
+ option: ld.colorScheme.option,
339
+ },
340
+ },
341
+ list: {
342
+ padding: ll.structural.padding,
343
+ gap: ll.structural.gap,
344
+ header: ll.structural.header,
345
+ },
346
+ option: ll.structural.option,
347
+ optionGroup: ll.structural.optionGroup,
348
+ },
349
+ coverage: [
350
+ { variable: 'listbox.root.*', slot: 'exporter-private: field()', class: 'derived' },
351
+ { variable: 'listbox.option.*', slot: 'exporter-private: list()', class: 'derived' },
352
+ ],
353
+ };
354
+ }
355
+
356
+ export function buildMenu(light, dark, ctx) {
357
+ const nl = navigation(light),
358
+ nd = navigation(dark);
359
+ const cl = content(light),
360
+ cd = content(dark);
361
+ const shadow = light.get('semantic.color.elevation.2.shadow')?.value;
362
+ return {
363
+ tokens: {
364
+ root: {
365
+ borderRadius: cl.structural.borderRadius,
366
+ shadow: shadow
367
+ ? `${shadow.offsetX} ${shadow.offsetY} ${shadow.blur} ${shadow.spread} ${ctx.formatColor(shadow.color)}`
368
+ : undefined,
369
+ },
370
+ colorScheme: {
371
+ // Menu's own component-level `item` type is narrower than the shared
372
+ // semantic.navigation.item group (verified against source: no active*
373
+ // fields at the component level — only focus, since Menu has no active state).
374
+ light: {
375
+ root: {
376
+ background: cl.colorScheme.background,
377
+ borderColor: cl.colorScheme.borderColor,
378
+ color: cl.colorScheme.color,
379
+ },
380
+ item: {
381
+ focusBackground: nl.colorScheme.item.focusBackground,
382
+ color: nl.colorScheme.item.color,
383
+ focusColor: nl.colorScheme.item.focusColor,
384
+ icon: nl.colorScheme.item.icon,
385
+ },
386
+ submenuLabel: nl.colorScheme.submenuLabel,
387
+ },
388
+ dark: {
389
+ root: {
390
+ background: cd.colorScheme.background,
391
+ borderColor: cd.colorScheme.borderColor,
392
+ color: cd.colorScheme.color,
393
+ },
394
+ item: {
395
+ focusBackground: nd.colorScheme.item.focusBackground,
396
+ color: nd.colorScheme.item.color,
397
+ focusColor: nd.colorScheme.item.focusColor,
398
+ icon: nd.colorScheme.item.icon,
399
+ },
400
+ submenuLabel: nd.colorScheme.submenuLabel,
401
+ },
402
+ },
403
+ list: nl.structural.list,
404
+ item: nl.structural.item,
405
+ submenuLabel: nl.structural.submenuLabel,
406
+ },
407
+ coverage: [
408
+ { variable: 'menu.item.*', slot: 'exporter-private: navigation()', class: 'derived' },
409
+ ],
410
+ };
411
+ }
412
+
413
+ export function buildPopover(light, dark, ctx) {
414
+ const ol = overlay(light, 'popover', ctx),
415
+ od = overlay(dark, 'popover', ctx);
416
+ return {
417
+ tokens: {
418
+ root: { ...ol.structural },
419
+ colorScheme: { light: { root: ol.colorScheme }, dark: { root: od.colorScheme } },
420
+ content: { padding: ol.padding },
421
+ },
422
+ coverage: [
423
+ { variable: 'popover.root.*', slot: 'exporter-private: overlay(popover)', class: 'derived' },
424
+ ],
425
+ };
426
+ }
427
+
428
+ export function buildDialog(light, dark, ctx) {
429
+ const ol = overlay(light, 'modal', ctx),
430
+ od = overlay(dark, 'modal', ctx);
431
+ return {
432
+ tokens: {
433
+ root: { ...ol.structural },
434
+ colorScheme: { light: { root: ol.colorScheme }, dark: { root: od.colorScheme } },
435
+ header: { padding: ol.padding },
436
+ content: { padding: ol.padding },
437
+ footer: { padding: ol.padding },
438
+ },
439
+ coverage: [
440
+ { variable: 'dialog.root.*', slot: 'exporter-private: overlay(modal)', class: 'derived' },
441
+ ],
442
+ };
443
+ }
444
+
445
+ /**
446
+ * Structural residue (§5.2 / C5): components with no severity-colored surface
447
+ * and not yet given a real builder above. Honest `unsupported` coverage,
448
+ * matching every other exporter's convention — not silently dropped.
449
+ */
450
+ export const STRUCTURAL_RESIDUE = [
451
+ 'datatable',
452
+ 'galleria',
453
+ 'tree',
454
+ 'treetable',
455
+ 'splitter',
456
+ 'timeline',
457
+ 'organizationchart',
458
+ 'carousel',
459
+ 'dataview',
460
+ 'orderlist',
461
+ 'picklist',
462
+ 'paginator',
463
+ 'stepper',
464
+ 'steps',
465
+ 'tabs',
466
+ 'tabview',
467
+ 'accordion',
468
+ ];