@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 +43 -0
- package/src/archetypes.js +162 -0
- package/src/descriptors.js +468 -0
- package/src/index.js +447 -0
- package/src/ramp.js +55 -0
- package/src/severity-grid.js +87 -0
- package/src/surface-coverage.js +164 -0
- package/surface-inventory.json +21474 -0
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
|
+
}
|