@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/src/index.js ADDED
@@ -0,0 +1,447 @@
1
+ /**
2
+ * @transtyle/exporter-primeng — emits a PrimeNG `definePreset(Aura, overrides)`
3
+ * TypeScript module from the resolved IR. Spec: docs/proposals/0002-component-
4
+ * theming-primeng.md; plan: docs/plan/component-tier.md C3-C5.
5
+ *
6
+ * NOT registered in the CLI yet (docs/plan/component-tier.md process note 3;
7
+ * C6 does that). This is real, tested code — just not on the officially
8
+ * supported surface until the five-surface sync rule applies to it.
9
+ *
10
+ * Strategy (proposal 0002 §5): override Aura, don't author a preset from
11
+ * zero — anything not emitted here is filled by Aura's own default at
12
+ * runtime. `semantic.*` here holds the global tier + the four archetype
13
+ * groups (§2.3/§2.8/C1: exporter-private, not a catalog addition — see
14
+ * archetypes.js). `components.*` holds the per-component color/structural
15
+ * overrides — built by descriptors.js, which itself calls the one generic
16
+ * `mapSeverityGrid` (severity-grid.js) for every severity-colored component.
17
+ *
18
+ * Verified directly by compiling the emitted preset against PrimeNG's own
19
+ * DesignTokens TypeScript types (docs/worklog/2026-07-21-component-tier-c3-c5.md):
20
+ * every color-ish group in real PrimeNG splits into a mode-invariant top-level
21
+ * object (padding/gap/radius/shadow-shape) and a SEPARATE `colorScheme.{light,
22
+ * dark}.<group>` object — never a single flat object with both. This file
23
+ * builds both per-mode maps and assembles that split throughout.
24
+ */
25
+
26
+ import { droppedDimensions } from '@transtyle/ir';
27
+ import { projectRamp } from './ramp.js';
28
+ import { field, list, navigation, overlay, content } from './archetypes.js';
29
+ import { coverageRows, INVENTORY } from './surface-coverage.js';
30
+ import {
31
+ buildButton,
32
+ buildTag,
33
+ buildBadge,
34
+ buildMessage,
35
+ buildInlineMessage,
36
+ buildProgressBar,
37
+ buildRating,
38
+ buildListbox,
39
+ buildMenu,
40
+ buildPopover,
41
+ buildDialog,
42
+ } from './descriptors.js';
43
+
44
+ const get = (map, path) => map.get(`semantic.${path}`)?.value;
45
+ /** DTCG fontFamily is a list; render it as a CSS stack, quoting names that need it. */
46
+ const fontStack = (value) =>
47
+ Array.isArray(value)
48
+ ? value.map((f) => (/[^a-z-]/.test(f) ? `"${f}"` : f)).join(', ')
49
+ : (value ?? undefined);
50
+
51
+ export default {
52
+ name: 'primeng',
53
+
54
+ emit(normalized, ctx) {
55
+ const light = normalized.modes.light ?? normalized.modes[normalized.defaultMode];
56
+ const dark = normalized.modes.dark ?? light;
57
+ const coverage = [];
58
+
59
+ // A custom archetype role (T7) lands in `extend` per component, per
60
+ // PrimeNG's own documented escape hatch (proposal 0002 §2.7) — proven
61
+ // here on Button only (the one place the sketch specified). `extend` is
62
+ // unconstrained by PrimeNG's own types, so no mode-split is required here.
63
+ const archetypeRoles = [...normalized.roleArchetypes.keys()];
64
+ const roleArchetypeExtend = archetypeRoles.length
65
+ ? Object.fromEntries(
66
+ archetypeRoles.map((r) => [
67
+ r,
68
+ {
69
+ color: get(light, `color.${r}.solid`),
70
+ contrastColor: get(light, `color.${r}.on-solid`),
71
+ hoverColor: get(light, `color.${r}.solid-hover`),
72
+ },
73
+ ]),
74
+ )
75
+ : undefined;
76
+
77
+ // primary.{50..950}: PrimeNG's own type has this as a single, mode-invariant
78
+ // ramp (verified: appears once in aura/base, never under colorScheme) — we
79
+ // derive it from the light map only, an honest, documented simplification
80
+ // (our own per-mode grid legitimately shifts these values slightly by mode,
81
+ // but PrimeNG's architecture has no slot to express that at this position).
82
+ const primaryRamp = projectRamp(light, 'primary', ctx);
83
+ coverage.push(
84
+ ...primaryRamp.coverage.map((c) => ({
85
+ variable: `semantic.primary.${c.step}`,
86
+ slot: c.slot,
87
+ class: c.class,
88
+ })),
89
+ );
90
+
91
+ // surface.{0,50..950}: mode-scoped in real PrimeNG (verified: aura/base
92
+ // defines a DIFFERENT ramp — slate family light, zinc family dark — under
93
+ // colorScheme.light/dark.surface) — computed per mode, unlike primary above.
94
+ const surfaceRampLight = projectRamp(light, 'neutral', ctx, { includeZero: true });
95
+ const surfaceRampDark = projectRamp(dark, 'neutral', ctx, { includeZero: true });
96
+ coverage.push(
97
+ ...surfaceRampLight.coverage.map((c) => ({
98
+ variable: `semantic.colorScheme.light.surface.${c.step}`,
99
+ slot: c.slot,
100
+ class: c.class,
101
+ })),
102
+ );
103
+
104
+ const fLight = field(light),
105
+ fDark = field(dark);
106
+ const lLight = list(light),
107
+ lDark = list(dark);
108
+ const nLight = navigation(light),
109
+ nDark = navigation(dark);
110
+ const cLight = content(light),
111
+ cDark = content(dark);
112
+ const oSelectLight = overlay(light, 'select', ctx),
113
+ oSelectDark = overlay(dark, 'select', ctx);
114
+ const oPopoverLight = overlay(light, 'popover', ctx),
115
+ oPopoverDark = overlay(dark, 'popover', ctx);
116
+ const oModalLight = overlay(light, 'modal', ctx),
117
+ oModalDark = overlay(dark, 'modal', ctx);
118
+ const oNavLight = overlay(light, 'navigation', ctx);
119
+ coverage.push({
120
+ variable: 'semantic.formField.{paddingX,paddingY,borderRadius}',
121
+ slot: 'component.control.{padding-x,padding-y,radius}',
122
+ class: 'native',
123
+ note: 'AL2-promoted shared control geometry (proposal 0003)',
124
+ });
125
+ coverage.push({
126
+ variable: 'semantic.formField.* (rest)',
127
+ slot: 'exporter-private: field()',
128
+ class: 'derived',
129
+ });
130
+ coverage.push({
131
+ variable: 'semantic.disabledOpacity',
132
+ slot: 'semantic.opacity.disabled',
133
+ class: 'native',
134
+ note: 'AL2 promotion — was a hardcoded PrimeNG constant',
135
+ });
136
+ coverage.push({
137
+ variable: 'components.button.root.{borderRadius,paddingX,paddingY}',
138
+ slot: 'component.button.{radius,padding-x,padding-y}',
139
+ class: 'native',
140
+ note: 'AL2 parity: the button layer, which defaults from component.control.*',
141
+ });
142
+ coverage.push({
143
+ variable: 'semantic.list.*',
144
+ slot: 'exporter-private: list()',
145
+ class: 'derived',
146
+ });
147
+ coverage.push({
148
+ variable: 'semantic.navigation.*',
149
+ slot: 'exporter-private: navigation()',
150
+ class: 'derived',
151
+ });
152
+ coverage.push({
153
+ variable: 'semantic.overlay.*',
154
+ slot: 'semantic.color.elevation.N.{surface,shadow} + radius.*',
155
+ class: 'native',
156
+ });
157
+ coverage.push({
158
+ variable: 'semantic.content.*',
159
+ slot: 'semantic.color.elevation.1.surface + border + text.base',
160
+ class: 'native',
161
+ });
162
+ coverage.push({
163
+ variable: 'semantic.colorScheme.*.mask.background',
164
+ slot: 'semantic.color.scrim',
165
+ class: 'native',
166
+ note: 'scrim carries its own alpha — the veil strength needs no separate slot (proposal 0003, overlay pass)',
167
+ });
168
+ coverage.push({
169
+ variable: 'semantic.typography.{fontFamily,fontSize,fontWeight,lineHeight}',
170
+ slot: 'semantic.{font.sans, type.size.md, type.weight.regular, type.leading.normal}',
171
+ class: 'native',
172
+ note: "PrimeNG's semantic type base; 60 component slots reference it, so they follow the design system's typography instead of Aura's (AL3 follow-up)",
173
+ });
174
+ coverage.push({
175
+ variable: 'semantic.mask.transitionDuration',
176
+ slot: 'semantic.duration.normal',
177
+ class: 'approximated',
178
+ note: "PrimeNG's own convention is 0.3s; the nearest motion-scale rung is used so the veil fade is authorable (was hardcoded)",
179
+ });
180
+
181
+ const semantic = {
182
+ // AL3 follow-up: PrimeNG's semantic typography block was left on Aura's
183
+ // defaults, which the coverage bar exposed as a cascading gap — 60
184
+ // component slots reference {typography.font.size}/{typography.font.weight}
185
+ // and inherited Aura's values rather than the design system's. Mapped by
186
+ // meaning: PrimeNG's semantic base ← the IR's base body rungs.
187
+ typography: {
188
+ // No authored/derived font stack → leave Aura's `inherit` alone rather
189
+ // than inventing one; the rest of the block always resolves.
190
+ ...(get(light, 'font.sans') ? { fontFamily: fontStack(get(light, 'font.sans')) } : {}),
191
+ fontSize: get(light, 'type.size.md'),
192
+ fontWeight: String(get(light, 'type.weight.regular')),
193
+ lineHeight: String(get(light, 'type.leading.normal')),
194
+ },
195
+ transitionDuration: get(light, 'duration.fast'),
196
+ disabledOpacity: String(get(light, 'opacity.disabled')), // AL2: promoted to the catalog once Bootstrap independently needed it (was a hardcoded 0.6 here)
197
+ iconSize: '1rem',
198
+ // width/style/offset are PrimeNG conventions we deliberately don't have a composite
199
+ // for yet (proposal 0002 gap #8) — `color` reuses PrimeNG's own alias mechanism
200
+ // (`{primary.color}`) rather than a resolved value, exactly like Rating/ProgressBar.
201
+ focusRing: {
202
+ width: '1px',
203
+ style: 'solid',
204
+ color: '{primary.color}',
205
+ offset: '2px',
206
+ shadow: 'none',
207
+ },
208
+ primary: primaryRamp.ramp,
209
+ formField: fLight.structural,
210
+ list: lLight.structural,
211
+ navigation: nLight.structural,
212
+ content: cLight.structural,
213
+ overlay: {
214
+ select: oSelectLight.structural,
215
+ popover: oPopoverLight.structural,
216
+ modal: oModalLight.structural,
217
+ navigation: oNavLight.structural,
218
+ },
219
+ // Overlay pass (proposal 0003): was a hardcoded '0.3s'. The motion scale
220
+ // already expresses this — no promotion needed, just stop hardcoding.
221
+ // `mask.background` reads `color.scrim` (alpha included) below.
222
+ mask: { transitionDuration: get(light, 'duration.normal') },
223
+ colorScheme: {
224
+ light: {
225
+ surface: surfaceRampLight.ramp,
226
+ primary: {
227
+ color: get(light, 'color.primary.solid'),
228
+ contrastColor: get(light, 'color.primary.on-solid'),
229
+ hoverColor: get(light, 'color.primary.solid-hover'),
230
+ activeColor: get(light, 'color.primary.solid-active'),
231
+ },
232
+ highlight: {
233
+ background: get(light, 'color.primary.tint'),
234
+ focusBackground: get(light, 'color.primary.tint-hover'),
235
+ color: get(light, 'color.primary.on-tint'),
236
+ focusColor: get(light, 'color.primary.on-tint'),
237
+ },
238
+ mask: { background: get(light, 'color.scrim'), color: get(light, 'color.neutral.tint') },
239
+ formField: fLight.colorScheme,
240
+ text: {
241
+ color: get(light, 'color.text.base'),
242
+ hoverColor: get(light, 'color.text.base'),
243
+ mutedColor: get(light, 'color.text.muted'),
244
+ hoverMutedColor: get(light, 'color.text.muted'),
245
+ },
246
+ content: cLight.colorScheme,
247
+ overlay: {
248
+ select: oSelectLight.colorScheme,
249
+ popover: oPopoverLight.colorScheme,
250
+ modal: oModalLight.colorScheme,
251
+ },
252
+ list: { option: lLight.colorScheme.option, optionGroup: lLight.colorScheme.optionGroup },
253
+ navigation: nLight.colorScheme,
254
+ },
255
+ dark: {
256
+ surface: surfaceRampDark.ramp,
257
+ primary: {
258
+ color: get(dark, 'color.primary.solid'),
259
+ contrastColor: get(dark, 'color.primary.on-solid'),
260
+ hoverColor: get(dark, 'color.primary.solid-hover'),
261
+ activeColor: get(dark, 'color.primary.solid-active'),
262
+ },
263
+ highlight: {
264
+ background: get(dark, 'color.primary.tint'),
265
+ focusBackground: get(dark, 'color.primary.tint-hover'),
266
+ color: get(dark, 'color.primary.on-tint'),
267
+ focusColor: get(dark, 'color.primary.on-tint'),
268
+ },
269
+ mask: { background: get(dark, 'color.scrim'), color: get(dark, 'color.neutral.tint') },
270
+ formField: fDark.colorScheme,
271
+ text: {
272
+ color: get(dark, 'color.text.base'),
273
+ hoverColor: get(dark, 'color.text.base'),
274
+ mutedColor: get(dark, 'color.text.muted'),
275
+ hoverMutedColor: get(dark, 'color.text.muted'),
276
+ },
277
+ content: cDark.colorScheme,
278
+ overlay: {
279
+ select: oSelectDark.colorScheme,
280
+ popover: oPopoverDark.colorScheme,
281
+ modal: oModalDark.colorScheme,
282
+ },
283
+ list: { option: lDark.colorScheme.option, optionGroup: lDark.colorScheme.optionGroup },
284
+ navigation: nDark.colorScheme,
285
+ },
286
+ },
287
+ };
288
+
289
+ const button = buildButton(light, dark, { ...ctx, roleArchetypeExtend });
290
+ const tag = buildTag(light, dark);
291
+ const badge = buildBadge(light, dark);
292
+ const message = buildMessage(light, dark);
293
+ const inlinemessage = buildInlineMessage(light, dark, ctx);
294
+ const progressbar = buildProgressBar();
295
+ const rating = buildRating();
296
+ const listbox = buildListbox(light, dark);
297
+ const menu = buildMenu(light, dark, ctx);
298
+ const popover = buildPopover(light, dark, ctx);
299
+ const dialog = buildDialog(light, dark, ctx);
300
+ coverage.push(
301
+ ...button.coverage,
302
+ ...tag.coverage,
303
+ ...badge.coverage,
304
+ ...message.coverage,
305
+ ...inlinemessage.coverage,
306
+ ...progressbar.coverage,
307
+ ...rating.coverage,
308
+ ...listbox.coverage,
309
+ ...menu.coverage,
310
+ ...popover.coverage,
311
+ ...dialog.coverage,
312
+ );
313
+
314
+ coverage.push(...droppedDimensions(normalized.dimensionNames, ['color-scheme']));
315
+
316
+ const components = {
317
+ button: button.tokens,
318
+ tag: tag.tokens,
319
+ badge: badge.tokens,
320
+ message: message.tokens,
321
+ inlinemessage: inlinemessage.tokens,
322
+ progressbar: progressbar.tokens,
323
+ rating: rating.tokens,
324
+ listbox: listbox.tokens,
325
+ menu: menu.tokens,
326
+ popover: popover.tokens,
327
+ dialog: dialog.tokens,
328
+ };
329
+
330
+ // The overlay measure (proposal 0004). The catalog slot has no default, so
331
+ // this only appears when the design system actually authors it — unauthored,
332
+ // Aura's own 12.5rem stands, which is already the same 200px Bootstrap uses.
333
+ // Emitting a `maxWidth` we didn't derive would be inventing a value.
334
+ const tooltipMaxWidth = light.get('component.tooltip.max-width')?.value;
335
+ if (tooltipMaxWidth !== undefined) {
336
+ components.tooltip = { root: { maxWidth: String(tooltipMaxWidth) } };
337
+ coverage.push({
338
+ variable: 'components.tooltip.root.maxWidth',
339
+ slot: 'component.tooltip.max-width',
340
+ class: 'native',
341
+ note: 'proposal 0004: the one geometry concept both reference targets model identically (Bootstrap $tooltip-max-width 200px ≡ Aura 12.5rem)',
342
+ });
343
+ }
344
+
345
+ // AL3: measure this preset against PrimeNG's real theming surface
346
+ // (surface-inventory.json, extracted from the Aura preset). Replaces the
347
+ // hand-maintained STRUCTURAL_RESIDUE guess with per-family counts of what
348
+ // is driven, what follows our theme through PrimeNG's own token
349
+ // references, and what keeps Aura's default — every slot accounted for.
350
+ coverage.push(...coverageRows(INVENTORY, { semantic, components }));
351
+
352
+ const ts = renderPreset(ctx, semantic, components);
353
+ return {
354
+ files: [
355
+ { path: 'preset.transtyle.ts', contents: ts, kind: 'source' },
356
+ { path: 'usage.md', contents: renderUsage(ctx, coverage), kind: 'doc' },
357
+ ],
358
+ coverage,
359
+ };
360
+ },
361
+ };
362
+
363
+ // ---------- TS serialization ----------
364
+
365
+ function isColor(v) {
366
+ return (
367
+ v &&
368
+ typeof v === 'object' &&
369
+ typeof v.l === 'number' &&
370
+ typeof v.c === 'number' &&
371
+ typeof v.h === 'number'
372
+ );
373
+ }
374
+
375
+ function serialize(value, ctx, indent = 2) {
376
+ const pad = ' '.repeat(indent);
377
+ const padIn = ' '.repeat(indent + 4);
378
+ if (value === undefined) return undefined;
379
+ if (isColor(value)) return JSON.stringify(ctx.formatColor(value));
380
+ if (typeof value === 'string' || typeof value === 'number') return JSON.stringify(value);
381
+ if (Array.isArray(value)) return `[${value.map((v) => serialize(v, ctx, indent)).join(', ')}]`;
382
+ if (typeof value === 'object') {
383
+ const entries = Object.entries(value)
384
+ .map(([k, v]) => [k, serialize(v, ctx, indent + 4)])
385
+ .filter(([, v]) => v !== undefined);
386
+ if (!entries.length) return '{}';
387
+ const key = (k) => (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k) ? k : JSON.stringify(k));
388
+ return `{\n${entries.map(([k, v]) => `${padIn}${key(k)}: ${v}`).join(',\n')}\n${pad}}`;
389
+ }
390
+ return undefined;
391
+ }
392
+
393
+ function renderPreset(ctx, semantic, components) {
394
+ return `// GENERATED by transtyle — do not edit; source: ${ctx.projectName} token files
395
+ // Target: PrimeNG (definePreset over Aura) · rules standard@1
396
+ // See docs/proposals/0002-component-theming-primeng.md and usage.md (coverage) in this directory.
397
+ import { definePreset } from '@primeuix/themes';
398
+ import Aura from '@primeuix/themes/aura';
399
+
400
+ const ${camel(ctx.projectName)}Preset = definePreset(Aura, {
401
+ semantic: ${serialize(semantic, ctx)},
402
+ components: ${serialize(components, ctx)},
403
+ });
404
+
405
+ export default ${camel(ctx.projectName)}Preset;
406
+ `;
407
+ }
408
+
409
+ function camel(name) {
410
+ return (
411
+ String(name)
412
+ .replace(/[^a-zA-Z0-9]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
413
+ .replace(/^[A-Z]/, (c) => c.toLowerCase()) || 'transtyle'
414
+ );
415
+ }
416
+
417
+ function renderUsage(ctx, coverage) {
418
+ const counts = {};
419
+ for (const c of coverage) counts[c.class] = (counts[c.class] ?? 0) + 1;
420
+ const summary = Object.entries(counts)
421
+ .map(([k, v]) => `${v} ${k}`)
422
+ .join(' · ');
423
+ return `# Using this PrimeNG preset
424
+
425
+ Generated from the **${ctx.projectName}** design system by transtyle: a \`definePreset(Aura, { semantic, components })\` override — anything not listed here is inherited from PrimeNG's own Aura preset untouched. Coverage: ${summary}.
426
+
427
+ ## Setup
428
+
429
+ \`\`\`ts
430
+ // app.config.ts
431
+ import { providePrimeNG } from 'primeng/config';
432
+ import preset from './preset.transtyle';
433
+
434
+ providePrimeNG({ theme: { preset, options: { darkModeSelector: '.dark', cssLayer: false } } });
435
+ \`\`\`
436
+
437
+ ## What's covered in this pass
438
+
439
+ Full \`variant x severity x state\` color grid: **Button**. Flat \`severity x part\`: **Tag**, **Badge**, **Message**, **InlineMessage**. Primary-anchored only (no severity axis in real PrimeNG — verified against source, not assumed): **ProgressBar**, **Rating**. Archetype-helper consumers (\`formField\`/\`list\`/\`navigation\`/\`overlay\`, exporter-private per the C1 cross-ecosystem study — see docs/findings/component-tier-study.md): **Listbox**, **Menu**, **Popover**, **Dialog**.
440
+
441
+ Structural components with no severity-colored surface (DataTable, Galleria, Tree, Splitter, Timeline, ...) are left on Aura's own defaults for now — see \`report.json\` for the full list, each marked \`unsupported\`.
442
+
443
+ ## Regenerating
444
+
445
+ Never edit this file — change the design system tokens and run \`transtyle build primeng\`. See \`report.json\` for the full coverage/provenance breakdown.
446
+ `;
447
+ }
package/src/ramp.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * PrimeNG 11-step numeric ramp projector (docs/proposals/0002-component-theming-primeng.md
3
+ * §2.2, §2.6). Same technique `@transtyle/exporter-radix` already proved for
4
+ * Radix's 12 steps: pin named grid cells to numbered steps in lightness order,
5
+ * fresh-mix the one genuine gap. Applied here to `primary.{50..950}` and
6
+ * `surface.{0,50..950}` — PrimeNG's own two ramps (aura/base §2.2).
7
+ *
8
+ * 9 of 11 steps land on an existing grid cell (native); one step (800) has no
9
+ * direct cell and is a fresh `ctx.mix` between its neighbors (approximated) —
10
+ * a better native ratio than Radix's own 2-of-12 gap.
11
+ */
12
+
13
+ const STEPS = ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950'];
14
+
15
+ /** role: 'primary' | 'secondary' | ... any COLOR_ROLES member, or 'neutral' for the surface ramp. */
16
+ export function projectRamp(map, role, ctx, { includeZero = false } = {}) {
17
+ const get = (cell) => map.get(`semantic.color.${role}.${cell}`)?.value;
18
+ const cellFor = {
19
+ 50: get('tint'),
20
+ 100: get('tint-hover'),
21
+ 200: get('tint-active'),
22
+ 300: get('outline'),
23
+ 400: get('outline-hover'),
24
+ 500: get('solid'),
25
+ 600: get('solid-hover'),
26
+ 700: get('solid-active'),
27
+ 900: get('text'),
28
+ 950: get('text-strong'),
29
+ };
30
+ const solidActive = get('solid-active');
31
+ const textStrong = get('text-strong');
32
+ cellFor[800] = solidActive && textStrong ? ctx.mix(solidActive, textStrong, 0.5) : undefined;
33
+
34
+ const ramp = {};
35
+ const coverage = [];
36
+ if (includeZero) {
37
+ const zero = map.get('semantic.color.elevation.0.surface')?.value;
38
+ if (zero) {
39
+ ramp[0] = zero;
40
+ coverage.push({ step: 0, class: 'native', slot: 'semantic.color.elevation.0.surface' });
41
+ }
42
+ }
43
+ for (const step of STEPS) {
44
+ const n = Number(step);
45
+ const value = cellFor[n];
46
+ if (!value) continue;
47
+ ramp[n] = value;
48
+ coverage.push({
49
+ step: n,
50
+ class: n === 800 ? 'approximated' : 'native',
51
+ slot: n === 800 ? '—' : `semantic.color.${role}.*`,
52
+ });
53
+ }
54
+ return { ramp, coverage };
55
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The generic severity-grid mapper (docs/proposals/0002-component-theming-primeng.md
3
+ * §2.4, §5.2; docs/plan/component-tier.md C3/C4). One function, applied against
4
+ * a per-component *shape* descriptor — not a per-component value table.
5
+ *
6
+ * PrimeNG's `variant x severity x state x part` grid is isomorphic to the
7
+ * catalog's `prominence x role x state x cell` role grid (verified against
8
+ * Button's source, proposal 0002 §2.4). This function resolves that grid's
9
+ * values; it deliberately does NOT decide how a component nests them, because
10
+ * C4 verified (against real source, not assumption) that PrimeNG itself
11
+ * nests inconsistently: Button is `colorScheme.<variant>.<severity>.<part>`
12
+ * (variant-major), but Message is `colorScheme.<severity>.<variant>.<part>`
13
+ * (severity-major, e.g. `colorScheme.info.outlined.color`). Forcing one tree
14
+ * shape here would misrepresent real PrimeNG output — instead this returns a
15
+ * flat, addressable set of resolved values; each component's own builder
16
+ * (descriptors.js) reads from it and nests however that component really does.
17
+ *
18
+ * `contrast` is not a real Transtyle role (proposal 0002 gap #3) — it always
19
+ * reads the same two fixed cells regardless of which grid cell a part asks
20
+ * for: background/border-ish parts get `neutral.text-strong` (the near-black/
21
+ * near-white extreme), color-ish parts get `elevation.0.surface` (the page
22
+ * background) — both already correct per-mode with no special-casing needed
23
+ * beyond this one mapping.
24
+ */
25
+
26
+ const SEVERITY_ROLE = {
27
+ primary: 'primary',
28
+ secondary: 'secondary',
29
+ success: 'success',
30
+ info: 'info',
31
+ warn: 'warning',
32
+ danger: 'danger',
33
+ };
34
+ export const SEVERITIES = [...Object.keys(SEVERITY_ROLE), 'contrast'];
35
+ const BG_CELLS = new Set([
36
+ 'solid',
37
+ 'solid-hover',
38
+ 'solid-active',
39
+ 'tint',
40
+ 'tint-hover',
41
+ 'tint-active',
42
+ 'outline',
43
+ 'outline-hover',
44
+ ]);
45
+
46
+ function resolveCell(map, severity, cellSuffix) {
47
+ if (severity === 'contrast') {
48
+ const path = BG_CELLS.has(cellSuffix)
49
+ ? 'semantic.color.neutral.text-strong'
50
+ : 'semantic.color.elevation.0.surface';
51
+ return { value: map.get(path)?.value, slot: path };
52
+ }
53
+ const role = SEVERITY_ROLE[severity];
54
+ if (!role) return { value: undefined };
55
+ const slot = `semantic.color.${role}.${cellSuffix}`;
56
+ return { value: map.get(slot)?.value, slot };
57
+ }
58
+
59
+ /**
60
+ * @param map the normalized per-mode token map
61
+ * @param variants [{ name: string, parts: { fieldName: gridCellSuffix } }] — one
62
+ * entry per PrimeNG variant this component exposes (`root`, `outlined`, `text`,
63
+ * `filled`, `simple`, ... — whatever the real source names them).
64
+ * @param severities which PrimeNG severity keys to resolve (default: all 7)
65
+ * @returns { get(variantName, severity, field), coverage } — `get` returns the
66
+ * resolved value or `undefined`; the caller nests results into its own tree.
67
+ */
68
+ export function mapSeverityGrid(map, { variants, severities = SEVERITIES }) {
69
+ const coverage = [];
70
+ const resolved = new Map(); // "variant|severity|field" -> value
71
+
72
+ for (const variant of variants) {
73
+ for (const severity of severities) {
74
+ for (const [field, cellSuffix] of Object.entries(variant.parts)) {
75
+ const { value, slot } = resolveCell(map, severity, cellSuffix);
76
+ if (value === undefined) continue;
77
+ resolved.set(`${variant.name}|${severity}|${field}`, value);
78
+ coverage.push({ variable: `${variant.name}.${severity}.${field}`, slot, class: 'native' });
79
+ }
80
+ }
81
+ }
82
+
83
+ return {
84
+ get: (variantName, severity, field) => resolved.get(`${variantName}|${severity}|${field}`),
85
+ coverage,
86
+ };
87
+ }