bestax-migrate 2.2.2 → 2.3.1

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 (54) hide show
  1. package/README.md +12 -10
  2. package/dist/sources/_shared/imports.d.ts +28 -0
  3. package/dist/sources/_shared/imports.d.ts.map +1 -1
  4. package/dist/sources/_shared/imports.js +62 -0
  5. package/dist/sources/_shared/jsx-utils.d.ts +17 -0
  6. package/dist/sources/_shared/jsx-utils.d.ts.map +1 -1
  7. package/dist/sources/_shared/jsx-utils.js +49 -1
  8. package/dist/sources/_shared/props.d.ts +17 -0
  9. package/dist/sources/_shared/props.d.ts.map +1 -1
  10. package/dist/sources/_shared/props.js +72 -3
  11. package/dist/sources/_shared/specials-utils.d.ts +49 -3
  12. package/dist/sources/_shared/specials-utils.d.ts.map +1 -1
  13. package/dist/sources/_shared/specials-utils.js +165 -5
  14. package/dist/sources/_shared/viewports.d.ts +17 -0
  15. package/dist/sources/_shared/viewports.d.ts.map +1 -0
  16. package/dist/sources/_shared/viewports.js +32 -0
  17. package/dist/sources/bloomer/deps.d.ts +17 -0
  18. package/dist/sources/bloomer/deps.d.ts.map +1 -0
  19. package/dist/sources/bloomer/deps.js +119 -0
  20. package/dist/sources/bloomer/index.d.ts +3 -0
  21. package/dist/sources/bloomer/index.d.ts.map +1 -0
  22. package/dist/sources/bloomer/index.js +10 -0
  23. package/dist/sources/bloomer/mapping.d.ts +84 -0
  24. package/dist/sources/bloomer/mapping.d.ts.map +1 -0
  25. package/dist/sources/bloomer/mapping.js +988 -0
  26. package/dist/sources/bloomer/responsive.d.ts +39 -0
  27. package/dist/sources/bloomer/responsive.d.ts.map +1 -0
  28. package/dist/sources/bloomer/responsive.js +259 -0
  29. package/dist/sources/bloomer/specials.d.ts +19 -0
  30. package/dist/sources/bloomer/specials.d.ts.map +1 -0
  31. package/dist/sources/bloomer/specials.js +774 -0
  32. package/dist/sources/bloomer/styles.d.ts +13 -0
  33. package/dist/sources/bloomer/styles.d.ts.map +1 -0
  34. package/dist/sources/bloomer/styles.js +17 -0
  35. package/dist/sources/bloomer/transform.d.ts +27 -0
  36. package/dist/sources/bloomer/transform.d.ts.map +1 -0
  37. package/dist/sources/bloomer/transform.js +826 -0
  38. package/dist/sources/rbx/responsive.d.ts.map +1 -1
  39. package/dist/sources/rbx/responsive.js +1 -31
  40. package/dist/sources/rbx/specials.d.ts.map +1 -1
  41. package/dist/sources/rbx/specials.js +14 -91
  42. package/dist/sources/rbx/transform.d.ts.map +1 -1
  43. package/dist/sources/rbx/transform.js +168 -78
  44. package/dist/sources/react-bulma-components/responsive.d.ts.map +1 -1
  45. package/dist/sources/react-bulma-components/responsive.js +2 -14
  46. package/dist/sources/react-bulma-components/specials.d.ts.map +1 -1
  47. package/dist/sources/react-bulma-components/specials.js +33 -17
  48. package/dist/sources/react-bulma-components/transform.d.ts.map +1 -1
  49. package/dist/sources/react-bulma-components/transform.js +168 -78
  50. package/dist/sources/registry.d.ts.map +1 -1
  51. package/dist/sources/registry.js +2 -0
  52. package/dist/types.d.ts +11 -0
  53. package/dist/types.d.ts.map +1 -1
  54. package/package.json +6 -5
@@ -0,0 +1,988 @@
1
+ /**
2
+ * bloomer (0.6) → @allxsmith/bestax-bulma mapping tables.
3
+ *
4
+ * Data only — no AST work. `MAPPING` is the single source of truth for what
5
+ * the transform does with each bloomer export; `BLOOMER_EXPORTS` vendors
6
+ * bloomer's public surface so `mapping-coverage.test.ts` can close it in both
7
+ * directions.
8
+ *
9
+ * bloomer's shape is the reverse of rbx's: every export is a FLAT name
10
+ * (`CardHeaderTitle`, `NavbarItem`), while bestax groups the same components
11
+ * into dotted compounds (`Card.Header.Title`, `Navbar.Item`). So most rows
12
+ * here rename a flat identifier onto a dotted target — a shape the rename and
13
+ * value-reference passes already build, since the first segment of a dotted
14
+ * target is what gets imported.
15
+ *
16
+ * Its modifier vocabulary is already bestax's for the booleans (`isActive`,
17
+ * `isLoading`, `isOutlined`, …) — those are `{}` entries, present so the
18
+ * universal pass never sees them. What differs is the value-carrying props:
19
+ * `isColor` → `color`, `isSize` → `size` (claimed per component, because its
20
+ * type differs per component: numeric on Title/Subtitle, sizes-or-fractions on
21
+ * Column, `'128x128'` on Image, `'large'` elsewhere), `isAlign` → whatever
22
+ * the target calls it, and `tag` → `as` where bestax declares one.
23
+ */
24
+ const DOCS = 'https://bestax.io/docs';
25
+ /**
26
+ * The two Bulma 0.6 shades bestax's `validColors` does not carry. Every other
27
+ * colour and shade name of that era exists in bestax verbatim.
28
+ */
29
+ const SHADE_TODO = {
30
+ 'white-ter': `\`white-ter\` is not a bestax colour; use \`white\` or a custom class (${DOCS}/api/helpers/usebulmaclasses)`,
31
+ 'white-bis': `\`white-bis\` is not a bestax colour; use \`white\` or a custom class (${DOCS}/api/helpers/usebulmaclasses)`,
32
+ };
33
+ /**
34
+ * `tag` is on nearly every bloomer component (`Bulma.Tag`, rendered through
35
+ * `React.createElement(tag, …)`), but bestax declares `as` on only some
36
+ * components, several of them constrained to a literal union (`Footer` is
37
+ * `'footer' | 'div'`). So the default is a TODO, and the components whose
38
+ * bestax counterpart really does accept `as` opt in with `TAG_AS` in their
39
+ * own prop map, which runs first and wins. A literal outside the target's
40
+ * union still surfaces — as a type error the user can see, not a rewrite.
41
+ */
42
+ const TAG_TODO = {
43
+ todo: `bloomer's \`tag\` renders a different HTML element; bestax declares \`as\` on only some components, and this is not one of them — render the tag directly or restructure (${DOCS}/api)`,
44
+ };
45
+ const TAG_AS = { rename: 'as' };
46
+ /**
47
+ * `render` (`Bulma.Render`) let a component hand its computed props to a
48
+ * custom renderer. bestax has no render-prop escape hatch, and there is no
49
+ * way to rewrite a function value into markup, so it is always a TODO.
50
+ */
51
+ export const RENDER_TODO = {
52
+ todo: `bloomer's \`render\` prop injected the computed props into your own renderer; bestax has no render-prop escape hatch — render the markup directly (\`useBulmaClasses\` yields the helper classes) or wrap the component (${DOCS}/api/helpers/usebulmaclasses)`,
53
+ };
54
+ /**
55
+ * `isFullWidth` is universal in bloomer (via `withHelpersModifiers`), but
56
+ * Bulma's `is-fullwidth` only means something on a handful of elements, and
57
+ * bestax declares `isFullWidth` on exactly those: Button, Select, Table and
58
+ * Tabs opt in with `FULLWIDTH_OK`.
59
+ */
60
+ const FULLWIDTH_TODO = {
61
+ todo: 'Bulma defines `is-fullwidth` only on buttons, files, images, inputs, textareas, selects, tables and tabs, and this is none of them — drop it, or add className="is-fullwidth" if your own CSS styled the class',
62
+ };
63
+ /** Bulma styles `is-fullwidth` here, but bestax declares no prop for it. */
64
+ const FULLWIDTH_CLASS = { toClass: 'is-fullwidth' };
65
+ const FULLWIDTH_OK = {};
66
+ /**
67
+ * `textColor` is not part of `BulmaClassesProps` — each component declares it
68
+ * by hand, and many do not. So it is claimed per component, like `isSize`;
69
+ * everywhere else bloomer's `hasTextColor` becomes the Bulma class it stood
70
+ * for, which every element takes.
71
+ */
72
+ const TEXT_COLOR_OK = {
73
+ rename: 'textColor',
74
+ valueTodo: SHADE_TODO,
75
+ };
76
+ const size = { rename: 'size' };
77
+ const color = { rename: 'color' };
78
+ const active = { rename: 'active' };
79
+ const NAV_TODO = (name) => ({
80
+ status: 'todo',
81
+ todo: `\`${name}\` is Bulma 0.4's \`.nav\`, which Bulma removed in 0.5 (bloomer kept the component); rebuild it with bestax's \`Navbar\` — \`Nav\` → \`Navbar\`, \`NavLeft\`/\`NavRight\` → \`Navbar.Start\`/\`Navbar.End\`, \`NavItem\` → \`Navbar.Item\`, \`NavToggle\` → \`Navbar.Burger\` (${DOCS}/api/components/navbar)`,
82
+ });
83
+ /**
84
+ * bestax's `Message.color` is the six semantic colours; bloomer's `isColor`
85
+ * is any string. The Bulma 0.6 colours outside that union are named so they
86
+ * become a TODO instead of a silent type error.
87
+ */
88
+ const MESSAGE_COLOR_TODO = Object.fromEntries(['white', 'black', 'light', 'dark'].map(c => [
89
+ c,
90
+ `bestax \`Message\` takes only the semantic colours (primary … danger); use \`bgColor="${c}"\` or a custom class`,
91
+ ]));
92
+ /**
93
+ * Helper props `withHelpersModifiers` mixes into EVERY bloomer component.
94
+ * Applied after each component's own prop map, to whatever attributes are
95
+ * left.
96
+ *
97
+ * `isDisplay` and `isHidden` are deliberately absent: they are consumed
98
+ * structurally by responsive.ts before this pass runs (their values are
99
+ * strings, arrays or objects that flatten to several bestax props), and
100
+ * bestax has no prop of either name, so a leftover would be an
101
+ * excess-property type error rather than a TODO.
102
+ */
103
+ export const UNIVERSAL_PROPS = {
104
+ // --- typography ----------------------------------------------------------
105
+ hasTextAlign: { rename: 'textAlign' }, // left | right | centered — same union
106
+ // Per-component maps override this wherever the target declares `textColor`.
107
+ hasTextColor: { toClassPrefix: 'has-text-' },
108
+ // --- float / overlay -----------------------------------------------------
109
+ isPulled: { rename: 'float' }, // left | right — same union
110
+ isClearfix: { booleanToProp: { name: 'clearfix' } },
111
+ isOverlay: { booleanToProp: { name: 'overlay' } },
112
+ isUnselectable: {
113
+ booleanToProp: { name: 'interaction', value: 'unselectable' },
114
+ },
115
+ // --- spacing: bestax expresses the `is-*less` helpers as `m`/`p` --------
116
+ isMarginless: { booleanToProp: { name: 'm', value: '0' } },
117
+ isPaddingless: { booleanToProp: { name: 'p', value: '0' } },
118
+ // Per-component maps override these where bestax has the prop.
119
+ isFullWidth: FULLWIDTH_TODO,
120
+ tag: TAG_TODO,
121
+ render: RENDER_TODO,
122
+ };
123
+ /**
124
+ * The helper props responsive.ts consumes. Named here so the plain-element
125
+ * path can strip them from an element that is becoming intrinsic HTML: the
126
+ * structural pass runs before the rename step, which a `replaced: true`
127
+ * special skips.
128
+ */
129
+ export const RESPONSIVE_PROPS = {
130
+ isDisplay: null,
131
+ isHidden: null,
132
+ };
133
+ /**
134
+ * bestax targets that extend only React's HTML attributes — no
135
+ * `BulmaClassesProps` — so bloomer's universal helpers have no prop to land
136
+ * on. The transform turns each into a TODO naming the Bulma class instead of
137
+ * emitting an excess-property type error.
138
+ */
139
+ /**
140
+ * Targets whose `className` cannot be used: `Navbar.Divider` and
141
+ * `Pagination.Ellipsis` spread `{...props}` AFTER their own className (so one
142
+ * written here replaces the class that makes them work), and
143
+ * `Dropdown.Divider` is a bare `React.FC` that declares no props at all and
144
+ * renders its own `<hr>` regardless. A helper on one of these is named, never
145
+ * converted.
146
+ */
147
+ export const NO_CLASSNAME_TARGETS = new Set([
148
+ 'Navbar.Divider',
149
+ 'Pagination.Ellipsis',
150
+ 'Dropdown.Divider',
151
+ ]);
152
+ export const HELPERLESS_TARGETS = new Set([
153
+ 'Pagination.Previous',
154
+ 'Pagination.Next',
155
+ 'Navbar.Dropdown',
156
+ 'Navbar.DropdownMenu',
157
+ 'Dropdown.Divider',
158
+ 'Panel.Heading',
159
+ 'Panel.Tabs',
160
+ 'Panel.Block',
161
+ 'Tabs.List',
162
+ 'Tabs.Item',
163
+ 'Message.Header',
164
+ 'Message.Body',
165
+ 'Modal.Background',
166
+ 'Modal.Content',
167
+ 'Modal.Card',
168
+ 'Modal.Card.Head',
169
+ 'Modal.Card.Title',
170
+ 'Modal.Card.Body',
171
+ 'Modal.Card.Foot',
172
+ 'Modal.Close',
173
+ ]);
174
+ /** The Bulma class a helper prop stood for, for the hint on those targets. */
175
+ export function helperClassHint(name, value) {
176
+ const v = typeof value === 'string' ? value : undefined;
177
+ switch (name) {
178
+ case 'hasTextAlign':
179
+ return `has-text-${v ?? '<alignment>'}`;
180
+ case 'hasTextColor':
181
+ return `has-text-${v ?? '<colour>'}`;
182
+ case 'isPulled':
183
+ return `is-pulled-${v ?? '<side>'}`;
184
+ case 'isClearfix':
185
+ return 'is-clearfix';
186
+ case 'isOverlay':
187
+ return 'is-overlay';
188
+ case 'isUnselectable':
189
+ return 'is-unselectable';
190
+ case 'isMarginless':
191
+ return 'm-0';
192
+ case 'isPaddingless':
193
+ return 'p-0';
194
+ case 'isDisplay':
195
+ return `is-${v ?? '<display>[-<viewport>]'}`;
196
+ case 'isHidden':
197
+ return v ? `is-hidden-${v}` : 'is-hidden';
198
+ default:
199
+ return null;
200
+ }
201
+ }
202
+ /** Bloomer's fraction and width spellings → bestax's `BulmaColumnSize` names. */
203
+ export const COLUMN_SIZE_MAP = {
204
+ '1/2': 'half',
205
+ '1/3': 'one-third',
206
+ '1/4': 'one-quarter',
207
+ '2/3': 'two-thirds',
208
+ '3/4': 'three-quarters',
209
+ full: 'full',
210
+ };
211
+ export const MAPPING = {
212
+ // ---- grid ---------------------------------------------------------------
213
+ Columns: {
214
+ status: 'mapped',
215
+ target: 'Columns',
216
+ props: {
217
+ hasTextColor: TEXT_COLOR_OK,
218
+ isMobile: {},
219
+ isDesktop: {},
220
+ isGapless: {},
221
+ isMultiline: {},
222
+ isVCentered: {},
223
+ isCentered: {},
224
+ isGrid: {
225
+ todo: 'Bulma removed `columns.is-grid` in 0.5; bestax `Columns` has no equivalent — use `isMultiline` with sized Columns, or the Grid component',
226
+ },
227
+ },
228
+ },
229
+ // `isSize` / `isOffset` (numbers, fractions, or a per-breakpoint object)
230
+ // are flattened by responsive.ts, which the `column` special opts into.
231
+ Column: {
232
+ status: 'mapped',
233
+ target: 'Column',
234
+ special: 'column',
235
+ props: { hasTextColor: TEXT_COLOR_OK },
236
+ },
237
+ Tile: {
238
+ status: 'todo',
239
+ todo: `Bulma v1 replaced tiles with the Grid/Cell components — see ${DOCS}/api/grid and the migration guide ${DOCS}/guides/getting-started/migration/bulma-0-9-to-1`,
240
+ },
241
+ // ---- elements -----------------------------------------------------------
242
+ Box: {
243
+ status: 'mapped',
244
+ target: 'Box',
245
+ props: { hasTextColor: TEXT_COLOR_OK },
246
+ },
247
+ Button: {
248
+ status: 'mapped',
249
+ target: 'Button',
250
+ // `isLink` and `href` are handled by the special: the first is a colour
251
+ // that may collide with `isColor`, the second selects the anchor form.
252
+ special: 'button',
253
+ props: {
254
+ isColor: color,
255
+ isSize: size,
256
+ isOutlined: {},
257
+ isInverted: {},
258
+ isStatic: {},
259
+ isActive: {},
260
+ isHovered: {},
261
+ isFocused: {},
262
+ isLoading: {},
263
+ isFullWidth: FULLWIDTH_OK,
264
+ tag: TAG_AS,
265
+ hasTextColor: TEXT_COLOR_OK,
266
+ },
267
+ },
268
+ Content: {
269
+ status: 'mapped',
270
+ target: 'Content',
271
+ props: { hasTextColor: TEXT_COLOR_OK, isSize: size },
272
+ },
273
+ Delete: {
274
+ status: 'mapped',
275
+ target: 'Delete',
276
+ props: {
277
+ hasTextColor: TEXT_COLOR_OK,
278
+ isSize: size,
279
+ href: {
280
+ todo: 'bestax `Delete` renders a <button> and has no anchor form; wrap it in an <a>, or handle the navigation in `onClick`',
281
+ },
282
+ },
283
+ },
284
+ Icon: {
285
+ status: 'partial',
286
+ target: 'Icon',
287
+ special: 'icon',
288
+ props: { isSize: size },
289
+ },
290
+ Image: {
291
+ status: 'mapped',
292
+ target: 'Image',
293
+ special: 'image',
294
+ props: {
295
+ tag: TAG_AS,
296
+ hasTextColor: TEXT_COLOR_OK,
297
+ isFullWidth: FULLWIDTH_CLASS,
298
+ },
299
+ },
300
+ Notification: {
301
+ status: 'mapped',
302
+ target: 'Notification',
303
+ props: { hasTextColor: TEXT_COLOR_OK, isColor: color },
304
+ },
305
+ Progress: {
306
+ status: 'mapped',
307
+ target: 'Progress',
308
+ props: { isColor: color, isSize: size },
309
+ },
310
+ Table: {
311
+ status: 'mapped',
312
+ target: 'Table',
313
+ props: {
314
+ isBordered: {},
315
+ isStriped: {},
316
+ isNarrow: {},
317
+ isFullWidth: FULLWIDTH_OK,
318
+ },
319
+ },
320
+ Tag: {
321
+ status: 'mapped',
322
+ target: 'Tag',
323
+ props: { isColor: color, isSize: size },
324
+ },
325
+ // bestax's `Title.size` accepts the numbers 1–6 as well as their string
326
+ // forms, so bloomer's numeric `isSize` carries over untouched.
327
+ Title: {
328
+ status: 'mapped',
329
+ target: 'Title',
330
+ props: {
331
+ hasTextColor: TEXT_COLOR_OK,
332
+ isSize: size,
333
+ isSpaced: {},
334
+ tag: TAG_AS,
335
+ },
336
+ },
337
+ Subtitle: {
338
+ status: 'mapped',
339
+ target: 'SubTitle',
340
+ special: 'subtitle',
341
+ props: {
342
+ hasTextColor: TEXT_COLOR_OK,
343
+ isSize: size,
344
+ isSpaced: { toClass: 'is-spaced' },
345
+ tag: TAG_AS,
346
+ },
347
+ },
348
+ // bloomer's Heading is Bulma's `.heading` label, whose styles Bulma v1 no
349
+ // longer ships — bestax has no component for it.
350
+ Heading: { status: 'partial', special: 'heading' },
351
+ // ---- form ---------------------------------------------------------------
352
+ Checkbox: {
353
+ status: 'mapped',
354
+ target: 'Checkbox',
355
+ props: { hasTextColor: TEXT_COLOR_OK },
356
+ },
357
+ Radio: {
358
+ status: 'mapped',
359
+ target: 'Radio',
360
+ props: { hasTextColor: TEXT_COLOR_OK },
361
+ },
362
+ Control: {
363
+ status: 'mapped',
364
+ target: 'Control',
365
+ special: 'control-icons',
366
+ props: {
367
+ hasTextColor: TEXT_COLOR_OK,
368
+ isExpanded: {},
369
+ isLoading: {},
370
+ tag: TAG_AS,
371
+ },
372
+ },
373
+ Help: { status: 'mapped', special: 'help' },
374
+ // bloomer's form elements were bare <input>/<select>/<textarea>s; bestax's
375
+ // Input/Select/TextArea wrap themselves in Field and Control when not
376
+ // already inside one, which a bare element in a table cell or beside an
377
+ // icon did not do. The *Base exports are the bare elements.
378
+ Input: {
379
+ status: 'mapped',
380
+ target: 'InputBase',
381
+ props: {
382
+ isFullWidth: FULLWIDTH_CLASS,
383
+ isColor: color,
384
+ isSize: size,
385
+ isHovered: {},
386
+ isFocused: {},
387
+ isActive: { toClass: 'is-active' },
388
+ },
389
+ },
390
+ Label: { status: 'mapped', special: 'label' },
391
+ Select: {
392
+ status: 'mapped',
393
+ target: 'SelectBase',
394
+ props: {
395
+ isColor: color,
396
+ isSize: size,
397
+ isLoading: {},
398
+ isFullWidth: FULLWIDTH_OK,
399
+ },
400
+ },
401
+ TextArea: {
402
+ status: 'mapped',
403
+ target: 'TextAreaBase',
404
+ props: {
405
+ isFullWidth: FULLWIDTH_CLASS,
406
+ isSize: size,
407
+ isActive: {},
408
+ isHovered: {},
409
+ isFocused: {},
410
+ },
411
+ },
412
+ Field: {
413
+ status: 'mapped',
414
+ target: 'Field',
415
+ props: {
416
+ hasTextColor: TEXT_COLOR_OK,
417
+ // `boolean | 'right' | 'centered'` on both sides.
418
+ isGrouped: { rename: 'grouped' },
419
+ hasAddons: {
420
+ valueTodo: {
421
+ fullwidth: 'bestax `Field.hasAddons` takes `true`, "centered" or "right"; Bulma v1 keeps `has-addons-fullwidth`, so use `hasAddons` plus className="has-addons-fullwidth"',
422
+ },
423
+ },
424
+ isHorizontal: { rename: 'horizontal' },
425
+ },
426
+ },
427
+ FieldBody: {
428
+ status: 'mapped',
429
+ target: 'Field.Body',
430
+ props: { hasTextColor: TEXT_COLOR_OK },
431
+ },
432
+ FieldLabel: {
433
+ status: 'mapped',
434
+ target: 'Field.Label',
435
+ props: {
436
+ hasTextColor: TEXT_COLOR_OK,
437
+ isSize: size,
438
+ isNormal: { booleanToProp: { name: 'size', value: 'normal' } },
439
+ },
440
+ },
441
+ // ---- breadcrumb ---------------------------------------------------------
442
+ // bestax's Breadcrumb renders its own <ul>; bloomer's was the bare <nav>
443
+ // and its docs wrote the <ul>, which the special folds away.
444
+ Breadcrumb: {
445
+ status: 'mapped',
446
+ target: 'Breadcrumb',
447
+ special: 'breadcrumb',
448
+ props: {
449
+ hasSeparator: { rename: 'separator' },
450
+ isAlign: { rename: 'alignment' }, // centered | right — same union
451
+ isSize: size,
452
+ },
453
+ },
454
+ // bestax's Breadcrumb renders the <ul> and takes plain <li> children.
455
+ BreadcrumbItem: { status: 'mapped', special: 'breadcrumb-item' },
456
+ // ---- card ---------------------------------------------------------------
457
+ Card: {
458
+ status: 'mapped',
459
+ target: 'Card',
460
+ props: { hasTextColor: TEXT_COLOR_OK },
461
+ },
462
+ CardImage: {
463
+ status: 'mapped',
464
+ target: 'Card.Image',
465
+ props: { hasTextColor: TEXT_COLOR_OK },
466
+ },
467
+ CardContent: {
468
+ status: 'mapped',
469
+ target: 'Card.Content',
470
+ props: { hasTextColor: TEXT_COLOR_OK },
471
+ },
472
+ CardHeader: {
473
+ status: 'mapped',
474
+ target: 'Card.Header',
475
+ props: { hasTextColor: TEXT_COLOR_OK },
476
+ },
477
+ CardHeaderTitle: {
478
+ status: 'mapped',
479
+ target: 'Card.Header.Title',
480
+ props: { hasTextColor: TEXT_COLOR_OK },
481
+ },
482
+ CardHeaderIcon: {
483
+ status: 'mapped',
484
+ target: 'Card.Header.Icon',
485
+ props: {
486
+ hasTextColor: TEXT_COLOR_OK,
487
+ href: {
488
+ todo: 'bestax `Card.Header.Icon` renders a <button>; put an <a> inside it, or handle the navigation in `onClick`',
489
+ },
490
+ },
491
+ },
492
+ CardFooter: {
493
+ status: 'mapped',
494
+ target: 'Card.Footer',
495
+ props: { hasTextColor: TEXT_COLOR_OK },
496
+ },
497
+ CardFooterItem: {
498
+ status: 'mapped',
499
+ target: 'Card.FooterItem',
500
+ props: {
501
+ hasTextColor: TEXT_COLOR_OK,
502
+ href: {
503
+ todo: 'bestax `Card.FooterItem` renders a <span> with no anchor form; put an <a> inside it',
504
+ },
505
+ },
506
+ },
507
+ // ---- dropdown -----------------------------------------------------------
508
+ // bestax's Dropdown takes a `label` and renders its own trigger and menu, so
509
+ // bloomer's trigger/menu/content wrappers have nothing to become.
510
+ Dropdown: {
511
+ status: 'partial',
512
+ target: 'Dropdown',
513
+ special: 'dropdown',
514
+ props: {
515
+ isActive: active,
516
+ isHoverable: { rename: 'hoverable' },
517
+ isAlign: { valueMap: { right: 'right' }, valueToProp: true },
518
+ },
519
+ },
520
+ DropdownTrigger: {
521
+ status: 'todo',
522
+ todo: 'bestax `Dropdown` renders its own trigger; move this content into its `label` prop and drop the wrapper',
523
+ },
524
+ DropdownMenu: {
525
+ status: 'todo',
526
+ todo: 'bestax `Dropdown` renders its own menu; drop this wrapper and keep the `<Dropdown.Item>`s as direct children',
527
+ },
528
+ DropdownContent: {
529
+ status: 'todo',
530
+ todo: 'bestax `Dropdown` renders its own menu content; drop this wrapper and keep the `<Dropdown.Item>`s as direct children',
531
+ },
532
+ DropdownItem: {
533
+ status: 'mapped',
534
+ target: 'Dropdown.Item',
535
+ special: 'dropdown-item',
536
+ props: {
537
+ isActive: active,
538
+ tag: TAG_AS,
539
+ href: {
540
+ todo: 'bestax `Dropdown.Item` declares no `href`; navigate in `onClick`, or put an <a> inside the item',
541
+ },
542
+ },
543
+ },
544
+ DropdownDivider: { status: 'mapped', target: 'Dropdown.Divider' },
545
+ // ---- level / media / menu / message ------------------------------------
546
+ Level: {
547
+ status: 'mapped',
548
+ target: 'Level',
549
+ props: { hasTextColor: TEXT_COLOR_OK, isMobile: {} },
550
+ },
551
+ LevelItem: {
552
+ status: 'mapped',
553
+ target: 'Level.Item',
554
+ special: 'level-item',
555
+ props: {
556
+ hasTextColor: TEXT_COLOR_OK,
557
+ isFlexible: { toClass: 'is-flexible' },
558
+ tag: TAG_AS,
559
+ },
560
+ },
561
+ LevelLeft: {
562
+ status: 'mapped',
563
+ target: 'Level.Left',
564
+ props: { hasTextColor: TEXT_COLOR_OK },
565
+ },
566
+ LevelRight: {
567
+ status: 'mapped',
568
+ target: 'Level.Right',
569
+ props: { hasTextColor: TEXT_COLOR_OK },
570
+ },
571
+ Media: {
572
+ status: 'mapped',
573
+ target: 'Media',
574
+ props: {
575
+ hasTextColor: TEXT_COLOR_OK,
576
+ isSize: { toClassPrefix: 'is-' },
577
+ tag: TAG_AS,
578
+ },
579
+ },
580
+ MediaContent: {
581
+ status: 'mapped',
582
+ target: 'Media.Content',
583
+ props: { hasTextColor: TEXT_COLOR_OK },
584
+ },
585
+ MediaLeft: {
586
+ status: 'mapped',
587
+ target: 'Media.Left',
588
+ props: { hasTextColor: TEXT_COLOR_OK, tag: TAG_AS },
589
+ },
590
+ MediaRight: {
591
+ status: 'mapped',
592
+ target: 'Media.Right',
593
+ props: { hasTextColor: TEXT_COLOR_OK },
594
+ },
595
+ Menu: { status: 'mapped', target: 'Menu' },
596
+ MenuLabel: { status: 'mapped', target: 'Menu.Label' },
597
+ MenuList: { status: 'mapped', target: 'Menu.List' },
598
+ // bestax's Menu.Item renders its own <li><a>; bloomer's MenuLink was the
599
+ // <a>, wrapped in a literal <li> by convention (its docs do), which the
600
+ // special folds away.
601
+ MenuLink: {
602
+ status: 'mapped',
603
+ target: 'Menu.Item',
604
+ special: 'menu-link',
605
+ props: { isActive: active, tag: TAG_AS },
606
+ },
607
+ Message: {
608
+ status: 'mapped',
609
+ target: 'Message',
610
+ props: {
611
+ hasTextColor: TEXT_COLOR_OK,
612
+ isColor: { rename: 'color', valueTodo: MESSAGE_COLOR_TODO },
613
+ },
614
+ },
615
+ MessageHeader: { status: 'mapped', target: 'Message.Header' },
616
+ MessageBody: { status: 'mapped', target: 'Message.Body' },
617
+ // ---- modal --------------------------------------------------------------
618
+ Modal: {
619
+ status: 'partial',
620
+ target: 'Modal',
621
+ special: 'modal',
622
+ props: { hasTextColor: TEXT_COLOR_OK, isActive: active },
623
+ },
624
+ ModalBackground: { status: 'mapped', target: 'Modal.Background' },
625
+ ModalContent: { status: 'mapped', target: 'Modal.Content' },
626
+ // bestax's Modal.Close defaults to `variant="delete"` (the small X for a
627
+ // card header) and honours `size` only on the floating variant; bloomer's
628
+ // ModalClose was always Bulma's `.modal-close` overlay button.
629
+ ModalClose: {
630
+ status: 'mapped',
631
+ target: 'Modal.Close',
632
+ special: 'modal-close',
633
+ props: { isSize: size },
634
+ },
635
+ ModalCard: { status: 'mapped', target: 'Modal.Card' },
636
+ ModalCardHeader: { status: 'mapped', target: 'Modal.Card.Head' },
637
+ ModalCardTitle: { status: 'mapped', target: 'Modal.Card.Title' },
638
+ ModalCardBody: { status: 'mapped', target: 'Modal.Card.Body' },
639
+ ModalCardFooter: { status: 'mapped', target: 'Modal.Card.Foot' },
640
+ // ---- nav (Bulma 0.4) ----------------------------------------------------
641
+ Nav: NAV_TODO('Nav'),
642
+ NavLeft: NAV_TODO('NavLeft'),
643
+ NavCenter: NAV_TODO('NavCenter'),
644
+ NavRight: NAV_TODO('NavRight'),
645
+ NavToggle: NAV_TODO('NavToggle'),
646
+ NavItem: NAV_TODO('NavItem'),
647
+ // ---- navbar -------------------------------------------------------------
648
+ Navbar: {
649
+ status: 'mapped',
650
+ target: 'Navbar',
651
+ props: {
652
+ hasTextColor: TEXT_COLOR_OK,
653
+ isTransparent: { rename: 'transparent' },
654
+ },
655
+ },
656
+ NavbarBrand: {
657
+ status: 'mapped',
658
+ target: 'Navbar.Brand',
659
+ props: { hasTextColor: TEXT_COLOR_OK },
660
+ },
661
+ NavbarBurger: {
662
+ status: 'mapped',
663
+ target: 'Navbar.Burger',
664
+ props: { hasTextColor: TEXT_COLOR_OK, isActive: active },
665
+ },
666
+ NavbarMenu: {
667
+ status: 'mapped',
668
+ target: 'Navbar.Menu',
669
+ props: { hasTextColor: TEXT_COLOR_OK, isActive: active },
670
+ },
671
+ NavbarStart: {
672
+ status: 'mapped',
673
+ target: 'Navbar.Start',
674
+ props: { hasTextColor: TEXT_COLOR_OK },
675
+ },
676
+ NavbarEnd: {
677
+ status: 'mapped',
678
+ target: 'Navbar.End',
679
+ props: { hasTextColor: TEXT_COLOR_OK },
680
+ },
681
+ // `hasDropdown` picks between Navbar.Item and the Navbar.Dropdown container.
682
+ NavbarItem: {
683
+ status: 'mapped',
684
+ special: 'navbar-item',
685
+ props: { isActive: active, tag: TAG_AS },
686
+ },
687
+ NavbarLink: {
688
+ status: 'mapped',
689
+ target: 'Navbar.Link',
690
+ props: {
691
+ hasTextColor: TEXT_COLOR_OK,
692
+ isActive: { toClass: 'is-active' },
693
+ tag: TAG_AS,
694
+ },
695
+ },
696
+ NavbarDropdown: { status: 'mapped', special: 'navbar-dropdown' },
697
+ NavbarDivider: {
698
+ status: 'mapped',
699
+ target: 'Navbar.Divider',
700
+ props: {
701
+ isBoxed: {
702
+ todo: 'Bulma v1 scopes navbar `is-boxed` to `.navbar-dropdown`, so there is no `.navbar-divider.is-boxed` to keep — drop it',
703
+ },
704
+ },
705
+ },
706
+ // ---- pagination ---------------------------------------------------------
707
+ Pagination: {
708
+ status: 'mapped',
709
+ target: 'Pagination',
710
+ special: 'pagination',
711
+ props: { hasTextColor: TEXT_COLOR_OK, isSize: size },
712
+ },
713
+ PageControl: {
714
+ status: 'mapped',
715
+ special: 'page-control',
716
+ props: {
717
+ isActive: { toClass: 'is-active' },
718
+ isFocused: { toClass: 'is-focused' },
719
+ },
720
+ },
721
+ PageEllipsis: {
722
+ status: 'mapped',
723
+ target: 'Pagination.Ellipsis',
724
+ props: {
725
+ isActive: {
726
+ todo: 'bestax `Pagination.Ellipsis` spreads its props after its own className, so a class written here would replace `pagination-ellipsis`; wrap it or restyle by hand if you need `is-active`',
727
+ },
728
+ isFocused: {
729
+ todo: 'bestax `Pagination.Ellipsis` spreads its props after its own className, so a class written here would replace `pagination-ellipsis`; wrap it or restyle by hand if you need `is-focused`',
730
+ },
731
+ },
732
+ },
733
+ // bestax's Pagination.Link and Pagination.Ellipsis render their own <li>.
734
+ Page: { status: 'mapped', special: 'page' },
735
+ PageList: {
736
+ status: 'mapped',
737
+ target: 'Pagination.List',
738
+ props: { hasTextColor: TEXT_COLOR_OK },
739
+ },
740
+ PageLink: {
741
+ status: 'mapped',
742
+ target: 'Pagination.Link',
743
+ props: {
744
+ hasTextColor: TEXT_COLOR_OK,
745
+ isCurrent: active, // bestax's `active` renders Bulma's `is-current`
746
+ isActive: { toClass: 'is-active' },
747
+ isFocused: { toClass: 'is-focused' },
748
+ },
749
+ },
750
+ // ---- panel --------------------------------------------------------------
751
+ Panel: { status: 'mapped', target: 'Panel' },
752
+ PanelHeading: { status: 'mapped', target: 'Panel.Heading' },
753
+ PanelTabs: { status: 'mapped', target: 'Panel.Tabs' },
754
+ PanelTab: { status: 'mapped', special: 'panel-tab' },
755
+ PanelBlock: {
756
+ status: 'mapped',
757
+ target: 'Panel.Block',
758
+ special: 'panel-block',
759
+ props: {
760
+ isActive: active,
761
+ isWrapped: { toClass: 'is-wrapped' },
762
+ },
763
+ },
764
+ PanelIcon: {
765
+ status: 'partial',
766
+ target: 'Panel.Icon',
767
+ special: 'panel-icon',
768
+ },
769
+ // ---- tabs ---------------------------------------------------------------
770
+ Tabs: {
771
+ status: 'mapped',
772
+ target: 'Tabs',
773
+ props: {
774
+ isAlign: { rename: 'align' }, // left | centered | right — same union
775
+ isSize: size,
776
+ isBoxed: { rename: 'boxed' },
777
+ isToggle: { rename: 'toggle' },
778
+ isFullWidth: FULLWIDTH_OK,
779
+ },
780
+ },
781
+ TabList: {
782
+ status: 'mapped',
783
+ target: 'Tabs.List',
784
+ props: {
785
+ isAlign: {
786
+ todo: "Bulma aligns tabs on the `.tabs` container, not the list (bloomer's `is-center` on the <ul> did nothing); set `align` on the `<Tabs>` instead",
787
+ },
788
+ },
789
+ },
790
+ // bestax's `Tabs.Tab` is the controlled API and requires an `index`;
791
+ // `Tabs.Item` is the plain <li> that bloomer's Tab actually is.
792
+ Tab: { status: 'mapped', target: 'Tabs.Item', props: { isActive: active } },
793
+ TabLink: { status: 'mapped', special: 'tab-link' },
794
+ // ---- layout -------------------------------------------------------------
795
+ Container: {
796
+ status: 'mapped',
797
+ target: 'Container',
798
+ props: { hasTextColor: TEXT_COLOR_OK, isFluid: { rename: 'fluid' } },
799
+ },
800
+ Footer: {
801
+ status: 'mapped',
802
+ target: 'Footer',
803
+ props: { hasTextColor: TEXT_COLOR_OK, tag: TAG_AS },
804
+ },
805
+ Section: {
806
+ status: 'mapped',
807
+ target: 'Section',
808
+ props: { hasTextColor: TEXT_COLOR_OK, isSize: size },
809
+ },
810
+ Hero: {
811
+ status: 'mapped',
812
+ target: 'Hero',
813
+ props: {
814
+ isColor: color,
815
+ isSize: size,
816
+ isFullHeight: { booleanToProp: { name: 'size', value: 'fullheight' } },
817
+ isHalfHeight: { toClass: 'is-halfheight' },
818
+ isBold: { toClass: 'is-bold' },
819
+ },
820
+ },
821
+ HeroHeader: {
822
+ status: 'mapped',
823
+ target: 'Hero.Head',
824
+ props: { hasTextColor: TEXT_COLOR_OK },
825
+ },
826
+ HeroBody: {
827
+ status: 'mapped',
828
+ target: 'Hero.Body',
829
+ props: { hasTextColor: TEXT_COLOR_OK },
830
+ },
831
+ HeroVideo: { status: 'mapped', special: 'hero-video' },
832
+ HeroFooter: {
833
+ status: 'mapped',
834
+ target: 'Hero.Foot',
835
+ props: { hasTextColor: TEXT_COLOR_OK },
836
+ },
837
+ // ---- the helper HOC -----------------------------------------------------
838
+ withHelpersModifiers: {
839
+ status: 'todo',
840
+ todo: `\`withHelpersModifiers\` wrapped a component so it accepted bloomer's helper props; bestax components take the same helpers natively (bar the parts that extend only React's HTML attributes, which take className), and a custom component can call \`useBulmaClasses\` itself (${DOCS}/api/helpers/usebulmaclasses)`,
841
+ },
842
+ };
843
+ /**
844
+ * bloomer's public export surface, vendored from `src/index.ts` at the SHA
845
+ * `scripts/validate-corpus-bloomer.mjs` pins, in that file's order. Every
846
+ * export is flat, so no entry carries sub-paths; the shape matches the other
847
+ * sources' tables so the coverage and doc tests port unchanged.
848
+ *
849
+ * `mapping-coverage.test.ts` walks this against `MAPPING` in both directions,
850
+ * so bloomer coverage cannot silently regress and `MAPPING` cannot grow an
851
+ * entry for something bloomer never exported.
852
+ */
853
+ export const BLOOMER_EXPORTS = Object.fromEntries([
854
+ // grid
855
+ 'Columns',
856
+ 'Column',
857
+ 'Tile',
858
+ // elements
859
+ 'Box',
860
+ 'Button',
861
+ 'Content',
862
+ 'Delete',
863
+ 'Icon',
864
+ 'Image',
865
+ 'Notification',
866
+ 'Progress',
867
+ 'Table',
868
+ 'Tag',
869
+ 'Title',
870
+ 'Subtitle',
871
+ 'Heading',
872
+ // form
873
+ 'Checkbox',
874
+ 'Control',
875
+ 'Help',
876
+ 'Input',
877
+ 'Label',
878
+ 'Radio',
879
+ 'Select',
880
+ 'TextArea',
881
+ 'Field',
882
+ 'FieldBody',
883
+ 'FieldLabel',
884
+ // breadcrumb
885
+ 'Breadcrumb',
886
+ 'BreadcrumbItem',
887
+ // card
888
+ 'Card',
889
+ 'CardImage',
890
+ 'CardContent',
891
+ 'CardHeader',
892
+ 'CardHeaderTitle',
893
+ 'CardHeaderIcon',
894
+ 'CardFooter',
895
+ 'CardFooterItem',
896
+ // dropdown
897
+ 'Dropdown',
898
+ 'DropdownContent',
899
+ 'DropdownDivider',
900
+ 'DropdownItem',
901
+ 'DropdownMenu',
902
+ 'DropdownTrigger',
903
+ // level
904
+ 'Level',
905
+ 'LevelItem',
906
+ 'LevelLeft',
907
+ 'LevelRight',
908
+ // media
909
+ 'Media',
910
+ 'MediaContent',
911
+ 'MediaLeft',
912
+ 'MediaRight',
913
+ // menu
914
+ 'Menu',
915
+ 'MenuLabel',
916
+ 'MenuList',
917
+ 'MenuLink',
918
+ // message
919
+ 'Message',
920
+ 'MessageHeader',
921
+ 'MessageBody',
922
+ // modal
923
+ 'Modal',
924
+ 'ModalBackground',
925
+ 'ModalContent',
926
+ 'ModalClose',
927
+ 'ModalCard',
928
+ 'ModalCardHeader',
929
+ 'ModalCardTitle',
930
+ 'ModalCardBody',
931
+ 'ModalCardFooter',
932
+ // nav
933
+ 'Nav',
934
+ 'NavLeft',
935
+ 'NavCenter',
936
+ 'NavRight',
937
+ 'NavToggle',
938
+ 'NavItem',
939
+ // navbar
940
+ 'Navbar',
941
+ 'NavbarBrand',
942
+ 'NavbarBurger',
943
+ 'NavbarMenu',
944
+ 'NavbarStart',
945
+ 'NavbarEnd',
946
+ 'NavbarItem',
947
+ 'NavbarLink',
948
+ 'NavbarDropdown',
949
+ 'NavbarDivider',
950
+ // pagination
951
+ 'Pagination',
952
+ 'PageControl',
953
+ 'PageEllipsis',
954
+ 'Page',
955
+ 'PageList',
956
+ 'PageLink',
957
+ // panel
958
+ 'Panel',
959
+ 'PanelHeading',
960
+ 'PanelTabs',
961
+ 'PanelTab',
962
+ 'PanelBlock',
963
+ 'PanelIcon',
964
+ // tabs
965
+ 'Tabs',
966
+ 'Tab',
967
+ 'TabList',
968
+ 'TabLink',
969
+ // layout
970
+ 'Container',
971
+ 'Footer',
972
+ 'Section',
973
+ 'Hero',
974
+ 'HeroHeader',
975
+ 'HeroBody',
976
+ 'HeroVideo',
977
+ 'HeroFooter',
978
+ // the HOC
979
+ 'withHelpersModifiers',
980
+ ].map(name => [name, []]));
981
+ /** Walk a dotted component path through `MAPPING`. */
982
+ export function resolveMapping(path) {
983
+ let current = MAPPING[path[0]];
984
+ for (const segment of path.slice(1)) {
985
+ current = current?.subs?.[segment];
986
+ }
987
+ return current;
988
+ }