@maggioli-design-system/magma-codemods 2.0.0-beta.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 (44) hide show
  1. package/README.md +193 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +77 -0
  4. package/dist/generate/diff-docs.d.ts +36 -0
  5. package/dist/generate/diff-docs.js +180 -0
  6. package/dist/index.d.ts +39 -0
  7. package/dist/index.js +166 -0
  8. package/dist/manifest/manifest.d.ts +3 -0
  9. package/dist/manifest/manifest.generated.d.ts +2 -0
  10. package/dist/manifest/manifest.generated.js +1901 -0
  11. package/dist/manifest/manifest.js +546 -0
  12. package/dist/manifest/registry.d.ts +32 -0
  13. package/dist/manifest/registry.js +102 -0
  14. package/dist/manifest/schema.d.ts +245 -0
  15. package/dist/manifest/schema.js +9 -0
  16. package/dist/report/diff.d.ts +2 -0
  17. package/dist/report/diff.js +95 -0
  18. package/dist/report/reporter.d.ts +27 -0
  19. package/dist/report/reporter.js +118 -0
  20. package/dist/report/types.d.ts +51 -0
  21. package/dist/report/types.js +2 -0
  22. package/dist/surfaces/angular.d.ts +3 -0
  23. package/dist/surfaces/angular.js +489 -0
  24. package/dist/surfaces/css.d.ts +5 -0
  25. package/dist/surfaces/css.js +287 -0
  26. package/dist/surfaces/html.d.ts +3 -0
  27. package/dist/surfaces/html.js +402 -0
  28. package/dist/surfaces/inline-templates.d.ts +3 -0
  29. package/dist/surfaces/inline-templates.js +98 -0
  30. package/dist/surfaces/react.d.ts +3 -0
  31. package/dist/surfaces/react.js +496 -0
  32. package/dist/surfaces/shared/attribute-ops.d.ts +56 -0
  33. package/dist/surfaces/shared/attribute-ops.js +62 -0
  34. package/dist/surfaces/shared/class-ops.d.ts +55 -0
  35. package/dist/surfaces/shared/class-ops.js +91 -0
  36. package/dist/surfaces/shared/edits.d.ts +19 -0
  37. package/dist/surfaces/shared/edits.js +30 -0
  38. package/dist/surfaces/shared/negate.d.ts +13 -0
  39. package/dist/surfaces/shared/negate.js +51 -0
  40. package/dist/surfaces/shared/transform.d.ts +17 -0
  41. package/dist/surfaces/shared/transform.js +8 -0
  42. package/dist/surfaces/shared/value-model.d.ts +49 -0
  43. package/dist/surfaces/shared/value-model.js +18 -0
  44. package/package.json +61 -0
@@ -0,0 +1,546 @@
1
+ /**
2
+ * Runtime v1 → v2 migration manifest.
3
+ *
4
+ * The bulk is machine-generated by diffing the v1/v2 `documentation.json`
5
+ * (`manifest.generated.ts`, produced by `scripts/build-candidate-manifest.ts`).
6
+ * This module layers the **curated corrections** on top — the things the docs
7
+ * diff cannot infer:
8
+ *
9
+ * - `tone` is migrated by the single global rule (with per-component
10
+ * validation), so the generated per-component `tone` remaps are dropped to
11
+ * avoid transforming the same attribute twice; the three text-capable
12
+ * components override `quiet → text`.
13
+ * - semantic prop renames the generator can only see as removals
14
+ * (`mds-label labelAction → label`) or missed entirely (`mds-tooltip arrow`).
15
+ * - slot → attribute lifts for `label` (`mds-button` preferred;
16
+ * `mds-breadcrumb-item` / `mds-tab-item` mandatory — v2 dropped their slot).
17
+ * - CSS custom properties the docs diff recorded as removals but that are
18
+ * renames in the shipped CSS.
19
+ * - utility-class migrations of the styles package (J): the shadow/ring,
20
+ * radius, border-width and named-gap token contracts that changed between
21
+ * v1 and v2 (invisible to the docs diff, which only sees components).
22
+ *
23
+ * Re-run the generator on each alpha/beta and review the diff; keep the
24
+ * corrections here.
25
+ */
26
+ import { generatedManifest } from './manifest.generated.js';
27
+ const curate = (base) => {
28
+ const m = structuredClone(base);
29
+ // Curated tone mapping. `weak` exists in every component's v2 tone set, so
30
+ // `quiet → weak` validates everywhere (including mds-banner / mds-label, which
31
+ // dropped `text`); `ghost → outline` applies where `outline` exists. The three
32
+ // components whose v2 tone set gained `text` follow the documented design
33
+ // intent (`quiet → text`) via per-tag overrides.
34
+ if (m.global.tone) {
35
+ m.global.tone.map = { ghost: 'outline', quiet: 'weak' };
36
+ const quietToText = { ghost: 'outline', quiet: 'text' };
37
+ m.global.tone.overrides = {
38
+ 'mds-button': { ...quietToText },
39
+ 'mds-radial-menu': { ...quietToText },
40
+ 'mds-radial-menu-item': { ...quietToText },
41
+ };
42
+ }
43
+ for (const component of Object.values(m.components)) {
44
+ // `tone` is handled by the global rule; drop the generated per-component
45
+ // tone remaps so the attribute is not transformed twice.
46
+ component.rules = component.rules.filter((r) => !(r.kind === 'enumRemap' && r.prop.prop === 'tone'));
47
+ }
48
+ // D — mds-label: `labelAction` was renamed to `label` (the diff only shows a removal).
49
+ const label = m.components['mds-label'];
50
+ if (label) {
51
+ label.rules = label.rules.filter((r) => !(r.kind === 'propRemove' && r.prop.prop === 'labelAction'));
52
+ label.rules.unshift({
53
+ kind: 'propRename',
54
+ from: { attr: 'label-action', prop: 'labelAction' },
55
+ to: { attr: 'label', prop: 'label' },
56
+ confidence: 'review',
57
+ note: 'v1 `labelAction` maps to v2 `label`.',
58
+ });
59
+ }
60
+ // B — boolean inversions the docs diff could not pair: either the v2 prop
61
+ // shares no stem with the v1 name (the diff only shows removals), the pair
62
+ // is missing from the diff entirely (mds-tooltip `arrow`), or the rename
63
+ // landed after the v2.0.0-beta docs snapshot (the mds-calendar set, #685:
64
+ // range mode, the two navigation buttons and the preselection area stay on
65
+ // by default, the v2 props opt out). `showPreselection` already defaulted
66
+ // to false, and the area kept showing up whenever the slot had content, so
67
+ // its inversion flips no default and needs no behaviour guard.
68
+ const curatedInversions = [
69
+ {
70
+ tag: 'mds-accordion',
71
+ react: 'MdsAccordion',
72
+ from: { attr: 'closable', prop: 'closable' },
73
+ to: { attr: 'disable-close', prop: 'disableClose' },
74
+ oldDefault: true,
75
+ },
76
+ {
77
+ tag: 'mds-notification',
78
+ react: 'MdsNotification',
79
+ from: { attr: 'visible', prop: 'visible' },
80
+ to: { attr: 'dismissed', prop: 'dismissed' },
81
+ oldDefault: true,
82
+ },
83
+ {
84
+ tag: 'mds-tooltip',
85
+ react: 'MdsTooltip',
86
+ from: { attr: 'arrow', prop: 'arrow' },
87
+ to: { attr: 'hide-arrow', prop: 'hideArrow' },
88
+ oldDefault: true,
89
+ },
90
+ {
91
+ tag: 'mds-calendar',
92
+ react: 'MdsCalendar',
93
+ from: { attr: 'range-picker', prop: 'rangePicker' },
94
+ to: { attr: 'single-picker', prop: 'singlePicker' },
95
+ oldDefault: true,
96
+ },
97
+ {
98
+ tag: 'mds-calendar',
99
+ react: 'MdsCalendar',
100
+ from: { attr: 'show-previous-button', prop: 'showPreviousButton' },
101
+ to: { attr: 'hide-previous-button', prop: 'hidePreviousButton' },
102
+ oldDefault: true,
103
+ },
104
+ {
105
+ tag: 'mds-calendar',
106
+ react: 'MdsCalendar',
107
+ from: { attr: 'show-next-button', prop: 'showNextButton' },
108
+ to: { attr: 'hide-next-button', prop: 'hideNextButton' },
109
+ oldDefault: true,
110
+ },
111
+ {
112
+ tag: 'mds-calendar',
113
+ react: 'MdsCalendar',
114
+ from: { attr: 'show-preselection', prop: 'showPreselection' },
115
+ to: { attr: 'hide-preselection', prop: 'hidePreselection' },
116
+ oldDefault: false,
117
+ },
118
+ ];
119
+ for (const { tag, react, from, to, oldDefault } of curatedInversions) {
120
+ const component = (m.components[tag] ??= { tag, react, rules: [] });
121
+ component.rules = component.rules.filter((r) => !(r.kind === 'propRemove' && r.prop.prop === from.prop));
122
+ component.rules.unshift({
123
+ kind: 'booleanInvert',
124
+ from: { ...from },
125
+ to: { ...to },
126
+ oldDefault,
127
+ newDefault: false,
128
+ confidence: 'review',
129
+ });
130
+ }
131
+ // F2 — lift slotted text into `label` (not derivable from the docs diff).
132
+ // mds-button keeps reading slotted text in v2, so the lift is the preferred
133
+ // form; mds-breadcrumb-item and mds-tab-item no longer render a slot at all,
134
+ // so there the lift is mandatory — v2 silently drops slotted content.
135
+ const slotLifts = [
136
+ { tag: 'mds-breadcrumb-item', react: 'MdsBreadcrumbItem' },
137
+ { tag: 'mds-button', react: 'MdsButton' },
138
+ { tag: 'mds-tab-item', react: 'MdsTabItem' },
139
+ ];
140
+ for (const { tag, react } of slotLifts) {
141
+ const component = (m.components[tag] ??= { tag, react, rules: [] });
142
+ if (component.rules.some((r) => r.kind === 'slotToAttr'))
143
+ continue;
144
+ component.rules.unshift({
145
+ kind: 'slotToAttr',
146
+ slot: 'default',
147
+ to: { attr: 'label', prop: 'label' },
148
+ confidence: 'review',
149
+ });
150
+ }
151
+ // G — CSS custom properties the docs diff saw as removals (or could not see
152
+ // at all) but that are really renames, verified against the shipped component
153
+ // CSS on both branches. Three flavours:
154
+ // - real renames (banner gap, header backdrop, filter `shodow → shadow`);
155
+ // - "doc-only v1 name" cases where the shipped v1 CSS already used the v2
156
+ // name, so overrides written against the documented name were silently
157
+ // inert — the rename activates them (explanatory note attached);
158
+ // - typo'd v1 names corrected in v2 (#566, plus the stepper `duaration`
159
+ // fixed while registering the properties from globals.css, #328): v1
160
+ // shipped the misspelled name, so the working v1 name is renamed to the
161
+ // corrected v2 one.
162
+ const docOnlyNote = (shipped) => `the v1 name was documented but never shipped (the CSS always read \`--${shipped}\`); this override was inert in v1 and becomes effective after the rename`;
163
+ const cssVarRenames = [
164
+ {
165
+ tag: 'mds-banner',
166
+ from: 'mds-banner-gap',
167
+ to: 'mds-banner-content-gap',
168
+ valueFormatChanged: true,
169
+ note: 'v1 accepted a "row column" gap shorthand; v2 takes a single gap value',
170
+ },
171
+ {
172
+ tag: 'mds-file',
173
+ from: 'mds-file-preview-icon-bacground',
174
+ to: 'mds-file-preview-icon-background',
175
+ },
176
+ {
177
+ tag: 'mds-filter',
178
+ from: 'mds-filter-wrapper-shodow-opacity',
179
+ to: 'mds-filter-wrapper-shadow-opacity',
180
+ },
181
+ {
182
+ tag: 'mds-filter-item',
183
+ from: '-mds-filter-item-count-background-selected',
184
+ to: 'mds-filter-item-count-background-selected',
185
+ note: docOnlyNote('mds-filter-item-count-background-selected'),
186
+ },
187
+ {
188
+ tag: 'mds-filter-item',
189
+ from: '-mds-filter-item-count-color-default',
190
+ to: 'mds-filter-item-count-color-default',
191
+ note: docOnlyNote('mds-filter-item-count-color-default'),
192
+ },
193
+ {
194
+ tag: 'mds-filter-item',
195
+ from: '-mds-filter-item-count-color-selected',
196
+ to: 'mds-filter-item-count-color-selected',
197
+ note: docOnlyNote('mds-filter-item-count-color-selected'),
198
+ },
199
+ {
200
+ tag: 'mds-header',
201
+ from: 'mds-header-backdrop-filter',
202
+ to: 'mds-header-backdrop-blur-strength',
203
+ valueFormatChanged: true,
204
+ note: 'v1 took a full backdrop-filter value (e.g. blur(10px)); v2 takes the blur length only',
205
+ },
206
+ {
207
+ tag: 'mds-tab',
208
+ from: 'mds-tab-item-transition-duration',
209
+ to: 'mds-tab-transition-duration',
210
+ note: docOnlyNote('mds-tab-transition-duration'),
211
+ },
212
+ {
213
+ tag: 'mds-tab',
214
+ from: 'mds-tab-item-transition-timing-function',
215
+ to: 'mds-tab-transition-timing-function',
216
+ note: docOnlyNote('mds-tab-transition-timing-function'),
217
+ },
218
+ {
219
+ tag: 'mds-stepper-bar-item',
220
+ from: 'mds-stepper-bar-item-duaration',
221
+ to: 'mds-stepper-bar-item-duration',
222
+ },
223
+ {
224
+ tag: 'mds-video-wall',
225
+ from: 'mds-video-wall-noise-fitler',
226
+ to: 'mds-video-wall-noise-filter',
227
+ },
228
+ ];
229
+ for (const { tag, from, to, valueFormatChanged, note } of cssVarRenames) {
230
+ const component = m.components[tag];
231
+ if (!component)
232
+ continue;
233
+ // Drop the stale generated removal for either spelling: the docs diff
234
+ // recorded `--mds-video-wall-noise-filter` (the rename target) as removed
235
+ // while v2 still shipped the typo.
236
+ component.rules = component.rules.filter((r) => !(r.kind === 'cssVarRemove' && (r.name === from || r.name === to)));
237
+ component.rules.push({
238
+ kind: 'cssVarRename',
239
+ from,
240
+ to,
241
+ ...(valueFormatChanged ? { valueFormatChanged } : {}),
242
+ ...(note ? { note } : {}),
243
+ });
244
+ }
245
+ // G3 / A2 - global (non-component) CSS custom-property migrations for the
246
+ // semantic color system.
247
+ // - Seed rename (A2): the bare `--tone-<family>` primitive became
248
+ // `--tone-<family>-seed`. A consumer that referenced the bare token keeps
249
+ // the pure-extreme value; a note points background users at a surface role.
250
+ // - Surface-candidate reports: a neutral tone used as a *background* (the bare
251
+ // token or any scale step) is a surface, but the exact role is contextual
252
+ // (and often the component's own default, C2 territory), so it is REPORTED
253
+ // for manual migration, never rewritten. In a background context the report
254
+ // wins over the seed rename, so a background is never seed-renamed.
255
+ const toneFamilies = ['porcelain', 'kaolin', 'neutral', 'fireclay', 'bisque'];
256
+ const neutralSteps = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10'];
257
+ m.global.cssVars = [
258
+ ...toneFamilies.map((family) => ({
259
+ kind: 'cssVarRename',
260
+ from: `tone-${family}`,
261
+ to: `tone-${family}-seed`,
262
+ note: 'A2: the bare tone primitive is now the off-scale `-seed` escape hatch (the pure extreme); if it was used as a surface/background, migrate it to a `--magma-surface-*` role instead',
263
+ })),
264
+ { kind: 'cssVarSurfaceReport', from: 'tone-neutral' },
265
+ ...neutralSteps.map((step) => ({
266
+ kind: 'cssVarSurfaceReport',
267
+ from: `tone-neutral-${step}`,
268
+ })),
269
+ ];
270
+ // J — utility classes of the styles package (the Tailwind design-token
271
+ // contract) that changed between v1.12 and v2, verified value-by-value
272
+ // against the two token sets (box-shadow.json, border-radius.json vs
273
+ // radius.json, border.json, gap.json vs spacing.json). Renames are
274
+ // value-exact — the v2 class paints the same pixels; classes with no exact
275
+ // v2 token are report-only, with the nearest candidates named. Numeric steps
276
+ // (`p-400`, `gap-200`, `border-50`, …) kept their values everywhere, and the
277
+ // generic Tailwind 3 → 4 migration is Tailwind's business, not ours.
278
+ const classRenames = [];
279
+ const classReports = [];
280
+ // Shadows. The size scale slid one name down at the bottom (v1 `sm` is v2
281
+ // `xs`; v2 `DEFAULT`/`sharp` alias the old values, so bare `shadow` and
282
+ // `shadow-sharp` are unchanged), `inner` became `inset-sm`, and the neutral
283
+ // ring family `outline-*` was republished as `ring-*` (#641): the suffix is
284
+ // now the width in px, weak/base/strong is the alpha (0.15 / 0.3 / 0.6).
285
+ const shadowRenames = [
286
+ ['shadow-sm', 'shadow-xs'],
287
+ ['shadow-sm-sharp', 'shadow-xs-sharp'],
288
+ ['shadow-outline', 'shadow-ring'], // 1px @ 30%
289
+ ['shadow-outline-50', 'shadow-ring-2'], // 2px @ 30%
290
+ ['shadow-outline-light', 'shadow-ring-weak'], // 1px @ 15%
291
+ ['shadow-outline-light-50', 'shadow-ring-weak-2'], // 2px @ 15%
292
+ ['shadow-outline-strong-50', 'shadow-ring-strong-2'], // 2px @ 60%
293
+ ['shadow-outline-strong-100', 'shadow-ring-strong-4'], // 4px @ 60%
294
+ ];
295
+ for (const [from, to] of shadowRenames)
296
+ classRenames.push({ kind: 'classRename', from, to });
297
+ classRenames.push({
298
+ kind: 'classRename',
299
+ from: 'shadow-inner',
300
+ to: 'shadow-inset-sm',
301
+ note: 'v2 `shadow-inset-sm` adds a 1%-alpha inset hairline to the v1 `shadow-inner` value — visually equivalent',
302
+ });
303
+ // v1 ring width×alpha combos that were not republished (the `-75` = 3px step
304
+ // is gone entirely). `shadow-outline-strong` is the dangerous one: v2 reuses
305
+ // that exact name for an unrelated black composite shadow, so leaving it
306
+ // unchanged silently restyles the element.
307
+ const ringReports = [
308
+ [
309
+ 'shadow-outline-75',
310
+ 'the v1 3px @ 30% ring has no v2 token; nearest `shadow-ring-2` (2px @ 30%)',
311
+ ],
312
+ [
313
+ 'shadow-outline-100',
314
+ 'the v1 4px @ 30% ring has no v2 token; nearest `shadow-ring-strong-4` (4px @ 60%)',
315
+ ],
316
+ [
317
+ 'shadow-outline-light-75',
318
+ 'the v1 3px @ 15% ring has no v2 token; nearest `shadow-ring-weak-2` (2px @ 15%)',
319
+ ],
320
+ [
321
+ 'shadow-outline-light-100',
322
+ 'the v1 4px @ 15% ring has no v2 token; nearest `shadow-ring-weak-2` (2px @ 15%)',
323
+ ],
324
+ [
325
+ 'shadow-outline-strong',
326
+ 'v2 reuses this name for a different shadow (a black composite, not the neutral ring), so leaving it is a silent restyle; the v1 1px @ 60% ring has no v2 token — nearest `shadow-ring` (1px @ 30%) or `shadow-ring-strong-2` (2px @ 60%)',
327
+ ],
328
+ [
329
+ 'shadow-outline-strong-75',
330
+ 'the v1 3px @ 60% ring has no v2 token; nearest `shadow-ring-strong-2` (2px) or `shadow-ring-strong-4` (4px)',
331
+ ],
332
+ ];
333
+ for (const [name, message] of ringReports)
334
+ classReports.push({ kind: 'classReport', name, message });
335
+ // Radius. The scale was retuned: every v1 step except `sm` keeps its value
336
+ // under a new (smaller-sounding) name — DEFAULT 4px → `3xs`, md 6px → `2xs`,
337
+ // lg 8px → `xs`, xl 12px → `md`, 2xl 16px → `lg`, 3xl 24px → `2xl`
338
+ // (`rounded-none` / `rounded-full` are unchanged). Corner and side variants
339
+ // share the theme scale, so the map is expanded over every prefix. The
340
+ // lookup is single-pass, so `rounded-xl → rounded-md` never cascades into
341
+ // the `rounded-md → rounded-2xs` rule.
342
+ const roundedPrefixes = [
343
+ 'rounded',
344
+ 'rounded-s',
345
+ 'rounded-e',
346
+ 'rounded-t',
347
+ 'rounded-r',
348
+ 'rounded-b',
349
+ 'rounded-l',
350
+ 'rounded-ss',
351
+ 'rounded-se',
352
+ 'rounded-ee',
353
+ 'rounded-es',
354
+ 'rounded-tl',
355
+ 'rounded-tr',
356
+ 'rounded-br',
357
+ 'rounded-bl',
358
+ ];
359
+ const radiusSteps = [
360
+ ['', '3xs'],
361
+ ['md', '2xs'],
362
+ ['lg', 'xs'],
363
+ ['xl', 'md'],
364
+ ['2xl', 'lg'],
365
+ ['3xl', '2xl'],
366
+ ];
367
+ for (const prefix of roundedPrefixes) {
368
+ for (const [from, to] of radiusSteps)
369
+ classRenames.push({
370
+ kind: 'classRename',
371
+ from: from === '' ? prefix : `${prefix}-${from}`,
372
+ to: `${prefix}-${to}`,
373
+ });
374
+ classReports.push({
375
+ kind: 'classReport',
376
+ name: `${prefix}-sm`,
377
+ message: `the v1 2px radius has no v2 step (the scale starts at 4px), and v2 reuses \`${prefix}-sm\` for 10px, so leaving it is a silent restyle; nearest \`${prefix}-3xs\` (4px), or keep 2px with the arbitrary \`${prefix}-[2px]\``,
378
+ });
379
+ }
380
+ // Border widths. The named steps were retuned (md 2px → 3px, lg 8px → 4px,
381
+ // xl 32px → 5px) but every v1 value survives: 2px under the new `sm` name,
382
+ // 8px and 32px under the numeric steps that kept their values.
383
+ const borderPrefixes = [
384
+ 'border',
385
+ 'border-x',
386
+ 'border-y',
387
+ 'border-s',
388
+ 'border-e',
389
+ 'border-t',
390
+ 'border-r',
391
+ 'border-b',
392
+ 'border-l',
393
+ ];
394
+ const borderSteps = [
395
+ ['md', 'sm'],
396
+ ['lg', '200'],
397
+ ['xl', '800'],
398
+ ];
399
+ for (const prefix of borderPrefixes)
400
+ for (const [from, to] of borderSteps)
401
+ classRenames.push({ kind: 'classRename', from: `${prefix}-${from}`, to: `${prefix}-${to}` });
402
+ // Gap. The named steps moved from the gap-only scale to the global spacing
403
+ // scale with two casualties: the bare DEFAULT spelling (24px; v2 has no bare
404
+ // `gap` class — `gap-lg` is the same 24px) and `3xl` (80px in v1, 64px in
405
+ // v2 — the numeric `gap-2000` keeps 80px). Every other named step (`xs` …
406
+ // `2xl`) kept its value.
407
+ for (const prefix of ['gap', 'gap-x', 'gap-y']) {
408
+ classRenames.push({
409
+ kind: 'classRename',
410
+ from: prefix,
411
+ to: `${prefix}-lg`,
412
+ note: `bare \`${prefix}\` is assumed to be the Tailwind DEFAULT step (24px); if it is a hand-written class of your own, skip this rule with --skip global/classRename/${prefix}`,
413
+ });
414
+ classRenames.push({ kind: 'classRename', from: `${prefix}-3xl`, to: `${prefix}-2000` });
415
+ }
416
+ m.global.classes = [...classRenames, ...classReports];
417
+ // Behavior guard: v2 mds-dropdown enables auto-placement by default (v1 was
418
+ // off). Add `disable-auto-placement` to dropdowns that set neither prop, to
419
+ // preserve the v1 behavior. (mds-tooltip's auto-placement default did not
420
+ // flip, so no guard there.)
421
+ const dropdown = m.components['mds-dropdown'];
422
+ if (dropdown && !dropdown.rules.some((r) => r.kind === 'ensureAttr')) {
423
+ dropdown.rules.push({
424
+ kind: 'ensureAttr',
425
+ attr: { attr: 'disable-auto-placement', prop: 'disableAutoPlacement' },
426
+ unless: [
427
+ { attr: 'auto-placement', prop: 'autoPlacement' },
428
+ { attr: 'disable-auto-placement', prop: 'disableAutoPlacement' },
429
+ ],
430
+ confidence: 'review',
431
+ reason: 'v2 enables auto-placement by default (v1 was off); added disable-auto-placement to preserve v1 behavior',
432
+ });
433
+ }
434
+ // Behavior guards for flipped *visual* defaults (same prop, new default —
435
+ // invisible to the docs diff): v1 `mds-banner` defaulted to variant="light"
436
+ // (v2: primary) and v1 `mds-label` did not truncate (v2: word). Both v1
437
+ // values still exist in v2, so pinning them preserves the v1 look.
438
+ const visualGuards = [
439
+ {
440
+ tag: 'mds-banner',
441
+ attr: { attr: 'variant', prop: 'variant' },
442
+ value: 'light',
443
+ reason: 'v2 defaults variant to "primary" (v1 was "light"); added variant="light" to preserve the v1 look',
444
+ },
445
+ {
446
+ tag: 'mds-label',
447
+ attr: { attr: 'truncate', prop: 'truncate' },
448
+ value: 'none',
449
+ reason: 'v2 defaults truncate to "word" (v1 did not truncate); added truncate="none" to preserve the v1 behavior',
450
+ },
451
+ ];
452
+ for (const { tag, attr, value, reason } of visualGuards) {
453
+ const component = m.components[tag];
454
+ if (!component ||
455
+ component.rules.some((r) => r.kind === 'ensureAttr' && r.attr.prop === attr.prop))
456
+ continue;
457
+ component.rules.push({
458
+ kind: 'ensureAttr',
459
+ attr: { ...attr },
460
+ value,
461
+ unless: [{ ...attr }],
462
+ confidence: 'review',
463
+ reason,
464
+ });
465
+ }
466
+ // Behavior guard: `mds-push-notification-item.deletable` defaults to true in
467
+ // v1 but false in v2 (same name, no rename for the diff to see). Add
468
+ // `deletable` to items that never set it, to preserve the v1 behavior.
469
+ const pushItem = (m.components['mds-push-notification-item'] ??= {
470
+ tag: 'mds-push-notification-item',
471
+ react: 'MdsPushNotificationItem',
472
+ rules: [],
473
+ });
474
+ if (!pushItem.rules.some((r) => r.kind === 'ensureAttr')) {
475
+ pushItem.rules.push({
476
+ kind: 'ensureAttr',
477
+ attr: { attr: 'deletable', prop: 'deletable' },
478
+ unless: [{ attr: 'deletable', prop: 'deletable' }],
479
+ confidence: 'review',
480
+ reason: 'v2 defaults deletable to false (v1 was true); added deletable to preserve v1 behavior',
481
+ });
482
+ }
483
+ // K — mode vs theme (#702). v1 called the light / dark / system control
484
+ // `mds-pref-theme`; v2 calls it `mds-pref-mode` and gives `mds-pref-theme` to
485
+ // the named-theme chooser, which v1 never had (the v2 betas shipped it as
486
+ // `mds-pref-theme-variant`). Every rename below is applied in one pass by
487
+ // name as written, so the name the two share is never renamed twice.
488
+ const modeControl = (m.components['mds-pref-theme'] ??= {
489
+ tag: 'mds-pref-theme',
490
+ react: 'MdsPrefTheme',
491
+ rules: [],
492
+ });
493
+ const overlayProps = ['fadeout-duration', 'show-duration', 'z-index'];
494
+ // The docs diff saw the overlay properties as removals: they moved with the tag.
495
+ modeControl.rules = modeControl.rules.filter((r) => !(r.kind === 'cssVarRemove' &&
496
+ overlayProps.includes(r.name.replace('mds-pref-theme-overlay-', ''))));
497
+ modeControl.rules.push({ kind: 'tagRename', to: 'mds-pref-mode', toReact: 'MdsPrefMode' }, ...overlayProps.map((prop) => ({
498
+ kind: 'cssVarRename',
499
+ from: `mds-pref-theme-overlay-${prop}`,
500
+ to: `mds-pref-mode-overlay-${prop}`,
501
+ })));
502
+ // v2 beta names, for a page written against a beta: harmless on v1 code.
503
+ m.components['mds-pref-theme-variant'] ??= {
504
+ tag: 'mds-pref-theme-variant',
505
+ react: 'MdsPrefThemeVariant',
506
+ rules: [{ kind: 'tagRename', to: 'mds-pref-theme', toReact: 'MdsPrefTheme' }],
507
+ };
508
+ m.components['mds-pref-theme-variant-item'] ??= {
509
+ tag: 'mds-pref-theme-variant-item',
510
+ react: 'MdsPrefThemeVariantItem',
511
+ rules: [
512
+ { kind: 'tagRename', to: 'mds-pref-theme-item', toReact: 'MdsPrefThemeItem' },
513
+ ...['background', 'status-error', 'status-success', 'status-warning', 'variant-primary'].map((color) => ({
514
+ kind: 'cssVarRename',
515
+ from: `mds-pref-theme-variant-item-color-${color}`,
516
+ to: `mds-pref-theme-item-color-${color}`,
517
+ })),
518
+ ],
519
+ };
520
+ m.global.cssVars = [
521
+ ...(m.global.cssVars ?? []),
522
+ { kind: 'cssVarRename', from: 'magma-pref-theme', to: 'magma-pref-mode' },
523
+ { kind: 'cssVarRename', from: 'magma-pref-theme-name', to: 'magma-pref-theme' },
524
+ ];
525
+ // The mode classes on <html>: in markup (a server-rendered first paint) and
526
+ // in the consumer stylesheets that select them.
527
+ m.global.classes = [
528
+ ...(m.global.classes ?? []),
529
+ ...['light', 'dark', 'system'].map((mode) => ({
530
+ kind: 'classRename',
531
+ from: `pref-theme-${mode}`,
532
+ to: `pref-mode-${mode}`,
533
+ selectors: true,
534
+ })),
535
+ // the transition overlay the mode control appends to <body>
536
+ {
537
+ kind: 'classRename',
538
+ from: 'mds-pref-theme-overlay',
539
+ to: 'mds-pref-mode-overlay',
540
+ selectors: true,
541
+ },
542
+ ];
543
+ return m;
544
+ };
545
+ export const manifest = curate(generatedManifest);
546
+ export default manifest;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Deterministic name helpers and manifest accessors shared by the generator
3
+ * (which builds the manifest from `documentation.json`) and the transformers
4
+ * (which consume it).
5
+ */
6
+ import { type ComponentManifest, type EnumRemapRule, type Manifest, type Rule, type TagRenameRule } from './schema.js';
7
+ /** camelCase prop → kebab-case attribute: `autoPlacement` → `auto-placement`. */
8
+ export declare const propToAttr: (prop: string) => string;
9
+ /** kebab-case attribute → camelCase prop: `auto-placement` → `autoPlacement`. */
10
+ export declare const attrToProp: (attr: string) => string;
11
+ /** Custom-element tag → React component name: `mds-dropdown` → `MdsDropdown`. */
12
+ export declare const tagToReactName: (tag: string) => string;
13
+ /** Stencil event name → React handler prop: `mdsChange` → `onMdsChange`. */
14
+ export declare const eventToReactProp: (event: string) => string;
15
+ /** A stable identifier for a rule, used by `--only`/`--skip` and the report. */
16
+ export declare const ruleId: (tag: string, rule: Rule) => string;
17
+ export declare const getByTag: (manifest: Manifest, tag: string) => ComponentManifest | undefined;
18
+ export declare const getByReactName: (manifest: Manifest, react: string) => ComponentManifest | undefined;
19
+ /**
20
+ * The effective rule list for a component: the global per-component rules
21
+ * (currently `tone`) resolved against this component, followed by its own
22
+ * rules. Global `tone` only materialises for components that declare the
23
+ * referenced v2 enum set.
24
+ *
25
+ * Note: `removeDefaultSlot` is intentionally *not* expanded here — `slot="default"`
26
+ * lives on the projected child (any tag), not on the `mds-*` element, so each
27
+ * surface applies it globally by reading `manifest.global.removeDefaultSlot`.
28
+ */
29
+ export declare const rulesForComponent: (manifest: Manifest, component: ComponentManifest) => Rule[];
30
+ /** Resolve the v2 enum set referenced by an `enumRemap` rule, if any. */
31
+ export declare const v2SetFor: (component: ComponentManifest, rule: EnumRemapRule) => readonly string[] | undefined;
32
+ export declare const tagRenamesOf: (manifest: Manifest) => Map<string, TagRenameRule>;