flet-box 1.1.2 → 1.1.3

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 (154) hide show
  1. package/AGENTS_FOR_CODE_GEN.md +284 -0
  2. package/CONTRIBUTING.md +238 -0
  3. package/README.md +2 -2
  4. package/bin/cli.js +88 -1
  5. package/bin/commands/auditFix.js +181 -27
  6. package/bin/commands/checkApi.js +14 -3
  7. package/bin/commands/checkStructure.js +368 -0
  8. package/bin/commands/checkSyntax.js +293 -0
  9. package/bin/commands/checkUi.js +666 -14
  10. package/bin/commands/create.js +14 -0
  11. package/bin/commands/createBundle.js +2 -2
  12. package/bin/commands/metrics.js +448 -0
  13. package/bin/commands/release.js +18 -0
  14. package/bin/commands/report.js +849 -0
  15. package/bin/commands/tools.js +76 -0
  16. package/bin/commands/unpack.js +39 -11
  17. package/bin/commands/verify.js +879 -0
  18. package/bin/mcp.js +280 -0
  19. package/bin/utils/contract.js +27 -0
  20. package/bin/utils/dtsProps.js +5 -1
  21. package/bin/utils/migrationGuide.js +146 -0
  22. package/bin/utils/moduleExports.js +9 -5
  23. package/bin/utils/templates.js +225 -18
  24. package/bin/utils/toolkit.js +191 -0
  25. package/docs/README.md +5 -1
  26. package/docs/arquitectura.md +1 -1
  27. package/docs/cli/README.md +168 -2
  28. package/docs/guides/README.md +6 -0
  29. package/docs/guides/accessibility.md +2 -1
  30. package/docs/guides/ai-toolkit.md +166 -0
  31. package/docs/guides/compatibility.md +1 -1
  32. package/docs/guides/migration.md +92 -0
  33. package/docs/guides/minification-and-protection.md +2 -1
  34. package/docs/guides/mobile-and-platforms.md +1 -1
  35. package/docs/guides/online-updates.md +2 -0
  36. package/docs/server/deployment.md +1 -1
  37. package/docs/tools/styling.md +2 -2
  38. package/docs/tools/theme.md +139 -4
  39. package/docs/widget/Accordion.md +74 -8
  40. package/docs/widget/AdMob.md +68 -3
  41. package/docs/widget/AdSense.md +67 -2
  42. package/docs/widget/AlertDialog.md +1 -1
  43. package/docs/widget/AnimatedBox.md +80 -39
  44. package/docs/widget/AnimatedText.md +74 -18
  45. package/docs/widget/Audio.md +73 -14
  46. package/docs/widget/Avatar.md +69 -4
  47. package/docs/widget/Badge.md +68 -3
  48. package/docs/widget/BottomSheet.md +2 -2
  49. package/docs/widget/Button.md +69 -3
  50. package/docs/widget/Card.md +69 -4
  51. package/docs/widget/Carousel.md +67 -2
  52. package/docs/widget/Chart.md +67 -2
  53. package/docs/widget/Checkbox.md +74 -8
  54. package/docs/widget/Chip.md +68 -3
  55. package/docs/widget/CircularBar.md +74 -15
  56. package/docs/widget/CircularChart.md +71 -10
  57. package/docs/widget/CodeViewer.md +67 -2
  58. package/docs/widget/Column.md +68 -3
  59. package/docs/widget/Container.md +68 -3
  60. package/docs/widget/DataTable.md +67 -2
  61. package/docs/widget/Divider.md +67 -2
  62. package/docs/widget/DraggBox.md +67 -2
  63. package/docs/widget/Dropdown.md +76 -10
  64. package/docs/widget/DroppBox.md +67 -2
  65. package/docs/widget/FloatingActionButton.md +68 -3
  66. package/docs/widget/GridView.md +67 -2
  67. package/docs/widget/Icon.md +71 -5
  68. package/docs/widget/Image.md +68 -3
  69. package/docs/widget/Input.md +68 -3
  70. package/docs/widget/Inspector.md +2 -2
  71. package/docs/widget/InstallButton.md +73 -13
  72. package/docs/widget/ListTile.md +69 -3
  73. package/docs/widget/ListView.md +67 -2
  74. package/docs/widget/Maps.md +68 -3
  75. package/docs/widget/Markdown.md +74 -15
  76. package/docs/widget/MatrixRain.md +72 -11
  77. package/docs/widget/Menu.md +209 -0
  78. package/docs/widget/Modal.md +3 -3
  79. package/docs/widget/Pagination.md +68 -3
  80. package/docs/widget/ParallaxBox.md +72 -12
  81. package/docs/widget/ProgressBar.md +69 -4
  82. package/docs/widget/QRCode.md +67 -2
  83. package/docs/widget/README.md +54 -50
  84. package/docs/widget/Radio.md +76 -10
  85. package/docs/widget/Rating.md +70 -6
  86. package/docs/widget/Row.md +68 -3
  87. package/docs/widget/Skeleton.md +68 -3
  88. package/docs/widget/Slider.md +75 -9
  89. package/docs/widget/SnackBar.md +2 -2
  90. package/docs/widget/Stack.md +68 -3
  91. package/docs/widget/Stepper.md +68 -3
  92. package/docs/widget/Switch.md +74 -8
  93. package/docs/widget/Text.md +71 -4
  94. package/docs/widget/ThemePicker.md +197 -0
  95. package/docs/widget/Tooltip.md +72 -7
  96. package/docs/widget/TreeView.md +70 -10
  97. package/docs/widget/Video.md +71 -10
  98. package/package.json +10 -3
  99. package/skills/fletbox-design/SKILL.md +89 -5
  100. package/skills/fletbox-motion/SKILL.md +1 -1
  101. package/skills/fletbox-rules/SKILL.md +56 -4
  102. package/src/core/runApp.js +12 -0
  103. package/src/index.d.ts +374 -101
  104. package/src/index.js +9 -0
  105. package/src/navigations/AdaptiveScaffold.js +2 -2
  106. package/src/navigations/BottomNavigation.js +1 -1
  107. package/src/navigations/Scaffold.js +18 -6
  108. package/src/tools/useState.js +11 -4
  109. package/src/utils/colors.js +275 -0
  110. package/src/utils/errorBoundary.js +288 -0
  111. package/src/utils/index.js +10 -2
  112. package/src/utils/stackPosition.js +39 -24
  113. package/src/utils/syntaxHighlight.js +4 -1
  114. package/src/utils/themes.js +279 -3
  115. package/src/widget-factory/addChildren.js +17 -10
  116. package/src/widget-factory/assignProps.js +18 -6
  117. package/src/widget-factory/createWidget.js +7 -0
  118. package/src/widget-factory/deprecations.js +77 -0
  119. package/src/widget-factory/translateProps.js +11 -1
  120. package/src/widget-factory/warnings.js +1 -27
  121. package/src/widget-factory/widgetFactory.js +59 -17
  122. package/src/widgets/Accordion.js +21 -7
  123. package/src/widgets/Badge.js +11 -2
  124. package/src/widgets/Button.js +15 -3
  125. package/src/widgets/Carousel.js +12 -4
  126. package/src/widgets/Chart.js +7 -2
  127. package/src/widgets/Checkbox.js +1 -2
  128. package/src/widgets/Chip.js +24 -10
  129. package/src/widgets/CircularBar.js +9 -3
  130. package/src/widgets/CircularChart.js +5 -3
  131. package/src/widgets/Container.js +6 -0
  132. package/src/widgets/Divider.js +3 -1
  133. package/src/widgets/DraggBox.js +9 -0
  134. package/src/widgets/Dropdown.js +3 -3
  135. package/src/widgets/DroppBox.js +5 -0
  136. package/src/widgets/FloatingActionButton.js +1 -1
  137. package/src/widgets/Icon.js +5 -1
  138. package/src/widgets/InstallButton.js +4 -2
  139. package/src/widgets/Markdown.js +171 -103
  140. package/src/widgets/Menu.js +340 -0
  141. package/src/widgets/Modal.js +7 -1
  142. package/src/widgets/Pagination.js +10 -5
  143. package/src/widgets/ProgressBar.js +5 -4
  144. package/src/widgets/QRCode.js +4 -0
  145. package/src/widgets/Radio.js +1 -2
  146. package/src/widgets/Rating.js +9 -0
  147. package/src/widgets/Slider.js +4 -1
  148. package/src/widgets/SnackBar.js +2 -2
  149. package/src/widgets/Switch.js +1 -2
  150. package/src/widgets/Text.js +9 -5
  151. package/src/widgets/ThemePicker.js +234 -0
  152. package/src/widgets/TreeView.js +2 -1
  153. package/src/widgets/index.js +2 -0
  154. package/docs/CONTRIBUTING.md +0 -156
@@ -1,5 +1,6 @@
1
1
  // src/components/flet-box/utils/themes.js
2
2
  import { readableInk } from "./readableInk.js";
3
+ import { ACCENT_PRESETS, accentSurfaces, accentTokens, accentStop } from "./colors.js";
3
4
 
4
5
  // ========== DEFAULT COLORS (static, safe) ==========
5
6
  const DEFAULT_COLORS = {
@@ -325,9 +326,30 @@ const syncBrowserChrome = () => {
325
326
  const applyPalette = (themeName) => {
326
327
  const palette = palettes[themeName];
327
328
  if (palette) {
328
- Object.keys(palette).forEach((key) => {
329
+ // The accent sits on top of the neutral scale and owns three tokens by
330
+ // default — `primary`, the ink that reads on it and its tint — so switching
331
+ // theme keeps every surface and moves only the brand. Worn as a *theme*
332
+ // (`setAccent(id, { surfaces: true })`) it also writes the page, the panels,
333
+ // the cards and the hairlines off the same five stops, which is the difference
334
+ // between a colored button and a colored app. Merging here, rather than
335
+ // writing the tokens straight onto `colors`, is what makes the choice survive a
336
+ // `setTheme`, an OS flip and a re-register: the layer is re-applied on every
337
+ // repaint instead of being overwritten by the next palette.
338
+ //
339
+ // It therefore also wins over a palette's own `primary`, including one an app
340
+ // wrote with `registerPalette` — the reader's choice of color is the later
341
+ // word. An app that wants its brand back calls `setAccent(null)`.
342
+ const accent = activeAccent ? accents[activeAccent] : null;
343
+ const layer = accent
344
+ ? {
345
+ ...accent[themeName],
346
+ ...(accentSurfacesOn && accent[`${themeName}Surfaces`]),
347
+ }
348
+ : null;
349
+ const merged = layer ? { ...palette, ...layer } : palette;
350
+ Object.keys(merged).forEach((key) => {
329
351
  if (colors.hasOwnProperty(key)) {
330
- colors[key] = palette[key];
352
+ colors[key] = merged[key];
331
353
  }
332
354
  });
333
355
  }
@@ -402,6 +424,243 @@ export const registerPalette = (themeName, palette) => {
402
424
  return palettes[themeName];
403
425
  };
404
426
 
427
+ // ========== ACCENTS ==========
428
+ /**
429
+ * The selectable brand colors, keyed by id.
430
+ *
431
+ * An accent is not a fourth palette: it is the three tokens a palette cannot
432
+ * agree on between its own two themes. `palettes.light` and `palettes.dark`
433
+ * still own every surface, border and ink; this map owns only `primary`,
434
+ * `onPrimary` and `primaryBg`, and `applyPalette` lays it over whichever palette
435
+ * is live. That is why one tap changes the buttons, icons and selection of the
436
+ * screen without changing whether the screen is dark.
437
+ *
438
+ * @type {Object<string, {id: string, label: string, light: Object, dark: Object}>}
439
+ */
440
+ const accents = {};
441
+
442
+ /** What the stored value puts between a family's id and "it also paints the page". */
443
+ const SURFACES_SUFFIX = "+surfaces";
444
+
445
+ /** The accent currently laid over the palette, or `null` for the palette's own. */
446
+ let activeAccent = null;
447
+
448
+ /** Whether that accent also paints the page and its panels, or only the brand. */
449
+ let accentSurfacesOn = false;
450
+
451
+ /**
452
+ * The three tokens one end of an accent writes, from either shape an accent can
453
+ * arrive in: a family's ramp, whose wash is a designed stop, or a bare hex, whose
454
+ * wash has to be derived.
455
+ *
456
+ * @param {string|string[]} spec - The accent for one theme.
457
+ * @param {"light"|"dark"} theme
458
+ */
459
+ const accentLayer = (spec, theme) =>
460
+ accentTokens(
461
+ Array.isArray(spec) ? accentStop(spec, theme) : spec,
462
+ theme,
463
+ Array.isArray(spec) ? spec : undefined
464
+ );
465
+
466
+ /**
467
+ * The page and panel tokens the same end writes when the accent is worn as a
468
+ * theme. Only a ramp can produce them: five stops are a scale, and a bare brand
469
+ * hex is one color with no lightness to walk.
470
+ *
471
+ * @param {string|string[]} spec
472
+ * @param {"light"|"dark"} theme
473
+ * @returns {Object|null}
474
+ */
475
+ const accentSurfaceLayer = (spec, theme) =>
476
+ Array.isArray(spec) ? accentSurfaces(spec, theme) : null;
477
+
478
+ const putAccent = (preset) => {
479
+ const { id, label, light, dark } = preset || {};
480
+ if (!id || !light || !dark) return null;
481
+ accents[id] = {
482
+ id,
483
+ label: label || id,
484
+ light: accentLayer(light, "light"),
485
+ dark: accentLayer(dark, "dark"),
486
+ lightSurfaces: accentSurfaceLayer(light, "light"),
487
+ darkSurfaces: accentSurfaceLayer(dark, "dark"),
488
+ // Kept only when the accent arrived as a ramp; a picker paints the family,
489
+ // not just its accent stop, and a pair of hexes has no family to show.
490
+ ramp: Array.isArray(light) || Array.isArray(dark) ? { light, dark } : null,
491
+ };
492
+ return accents[id];
493
+ };
494
+
495
+ ACCENT_PRESETS.forEach(putAccent);
496
+
497
+ /**
498
+ * Writes the reader's accent choice down, or removes it. `window` is the test, not
499
+ * a try/catch around the call, for the same reason `setTheme` uses it: Node 22+
500
+ * has a global `localStorage` that warns and throws.
501
+ *
502
+ * Surfaces ride on the end of the same value (`"teal+surfaces"`) so the stored
503
+ * form stays a string a person can read in devtools, and a bare id — what every
504
+ * app wrote before surfaces existed — still parses.
505
+ * @param {string|null} id
506
+ * @param {boolean} surfaces
507
+ */
508
+ const persistAccent = (id, surfaces) => {
509
+ if (typeof window === "undefined") return;
510
+ try {
511
+ if (id) {
512
+ localStorage.setItem(
513
+ "fletbox-accent",
514
+ surfaces ? `${id}${SURFACES_SUFFIX}` : id,
515
+ );
516
+ } else localStorage.removeItem("fletbox-accent");
517
+ } catch (e) {
518
+ // A full or private store is not a reason to stop painting the page.
519
+ }
520
+ };
521
+
522
+ /**
523
+ * The stored accent, when it is one the engine knows. A stale id — a family an
524
+ * older build shipped and this one dropped — reads as "no choice" rather than
525
+ * painting a color nothing derives from.
526
+ * @returns {{id: string, surfaces: boolean}|null}
527
+ */
528
+ const readStoredAccent = () => {
529
+ try {
530
+ const raw = localStorage.getItem("fletbox-accent");
531
+ if (!raw) return null;
532
+ const surfaces = raw.endsWith(SURFACES_SUFFIX);
533
+ const id = surfaces ? raw.slice(0, -SURFACES_SUFFIX.length) : raw;
534
+ return id && accents[id] ? { id, surfaces } : null;
535
+ } catch (e) {
536
+ return null;
537
+ }
538
+ };
539
+
540
+ /**
541
+ * Adds (or replaces) one selectable accent.
542
+ *
543
+ * The pair is what a picker shows and what the engine paints: `light` is the deep
544
+ * end of the hue, dark enough to carry white words on a light page, and `dark` is
545
+ * the lifted end, bright enough to carry a dark page's own ink. The ink and the
546
+ * soft tint are derived from each stop, so an app cannot register a family whose
547
+ * label is unreadable — the same derivation the shipped presets pass.
548
+ *
549
+ * Each end may be a single hex or a five-stop ramp (dark-to-light in `light`,
550
+ * bright-to-dark in `dark`), in which case the accent and its wash are read off
551
+ * the stops the shipped families use.
552
+ *
553
+ * Repaints only when the id being replaced is the one on screen.
554
+ *
555
+ * @param {Object} preset
556
+ * @param {string} preset.id - Stable key, e.g. `"teal"`.
557
+ * @param {string} [preset.label] - Accessible name shown to the reader; defaults to `id`.
558
+ * @param {string|string[]} preset.light - The accent, or its ramp, for the light theme.
559
+ * @param {string|string[]} preset.dark - The accent, or its ramp, for the dark theme.
560
+ * @returns {Object|null} The stored accent, or `null` when the preset is incomplete.
561
+ */
562
+ export const registerAccent = (preset) => {
563
+ const accent = putAccent(preset);
564
+ if (!accent) return null;
565
+ if (activeAccent === accent.id) applyPalette(getTheme());
566
+ return accent;
567
+ };
568
+
569
+ /**
570
+ * Lays an accent over the live palette and persists the choice.
571
+ *
572
+ * Pass `null` (or an unknown id) to take it back off: the screen returns to
573
+ * whatever `primary` the palette itself ships, and the stored preference is
574
+ * cleared so the next boot does it too.
575
+ *
576
+ * With `{ surfaces: true }` the family is worn as a whole theme: the same five
577
+ * stops also write `background`, `surface`, `card`, `border` and `divider`, so the
578
+ * page, the panels and the hairlines take the hue and not only the buttons. The
579
+ * ink scale, the focus ring and the semantic signals stay the palette's — a tinted
580
+ * theme still has to be able to say "this failed". An accent registered as a bare
581
+ * pair of hexes has no scale to walk, so it keeps painting brand only.
582
+ *
583
+ * @param {string|null} id - A registered accent, or `null` for the palette's own.
584
+ * @param {Object} [options]
585
+ * @param {boolean} [options.surfaces] - Also paint the page and its panels.
586
+ * @returns {Object} The updated `colors` object.
587
+ */
588
+ export const setAccent = (id, options) => {
589
+ if (id && !accents[id]) return colors;
590
+ activeAccent = id || null;
591
+ accentSurfacesOn = Boolean(
592
+ activeAccent && options?.surfaces && accents[activeAccent].lightSurfaces,
593
+ );
594
+ persistAccent(activeAccent, accentSurfacesOn);
595
+ applyPalette(getTheme());
596
+ return colors;
597
+ };
598
+
599
+ /**
600
+ * Applies an accent only when the reader has never picked one.
601
+ *
602
+ * This is the one an app calls at boot with its own brand: it sets the default
603
+ * without writing the reader's preference down, so a picker still shows nothing
604
+ * selected on the first visit and a later choice still wins over it.
605
+ *
606
+ * @param {string} id - A registered accent.
607
+ * @param {Object} [options]
608
+ * @param {boolean} [options.surfaces] - Wear it as a theme, not only as a brand.
609
+ * @returns {string|null} The accent now active.
610
+ */
611
+ export const applyAccentPreference = (id, options) => {
612
+ if (readStoredAccent() || !accents[id]) return activeAccent;
613
+ activeAccent = id;
614
+ accentSurfacesOn = Boolean(
615
+ options?.surfaces && accents[id].lightSurfaces,
616
+ );
617
+ applyPalette(getTheme());
618
+ return activeAccent;
619
+ };
620
+
621
+ /**
622
+ * The accent currently laid over the palette.
623
+ * @returns {string|null} Its id, or `null` when the palette's own `primary` is showing.
624
+ */
625
+ export const getAccent = () => activeAccent;
626
+
627
+ /**
628
+ * Whether the active accent also paints the page and its panels, or only the brand.
629
+ *
630
+ * A picker needs this to decide whether a tap on the cell already ticked is a
631
+ * no-op: the same family means two different things, and re-tapping it to move
632
+ * between them must repaint.
633
+ *
634
+ * @returns {boolean}
635
+ */
636
+ export const getAccentSurfaces = () => accentSurfacesOn;
637
+
638
+ /**
639
+ * Every selectable accent, in registration order, as the two stops a picker paints.
640
+ * @returns {Array<{id: string, label: string, light: string, dark: string}>}
641
+ */
642
+ export const listAccents = () =>
643
+ Object.values(accents).map(({ id, label, light, dark }) => ({
644
+ id,
645
+ label,
646
+ light: light.primary,
647
+ dark: dark.primary,
648
+ }));
649
+
650
+ /**
651
+ * The same list with each family's five stops beside it, for a picker that shows
652
+ * the ramp rather than one square: `light` runs dark-to-light and `dark`
653
+ * bright-to-dark, so the accent stop of each theme is the one `listAccents` names.
654
+ * @returns {Array<{id: string, label: string, light: string[], dark: string[]}>}
655
+ */
656
+ export const listAccentRamps = () =>
657
+ Object.values(accents).filter(({ ramp }) => ramp).map(({ id, label, ramp }) => ({
658
+ id,
659
+ label,
660
+ light: ramp.light,
661
+ dark: ramp.dark,
662
+ }));
663
+
405
664
  /**
406
665
  * Returns the currently active theme name.
407
666
  * Reads from `localStorage` first; falls back to the OS
@@ -528,7 +787,10 @@ export const ThemeProvider = ({ children, theme = null }) => {
528
787
  * getTheme: function,
529
788
  * toggleTheme: function,
530
789
  * getColor: function,
531
- * applySystemTheme: function
790
+ * applySystemTheme: function,
791
+ * setAccent: function,
792
+ * getAccent: function,
793
+ * listAccents: function
532
794
  * }}
533
795
  */
534
796
  export const useTheme = () => {
@@ -539,12 +801,21 @@ export const useTheme = () => {
539
801
  toggleTheme,
540
802
  getColor,
541
803
  applySystemTheme,
804
+ setAccent,
805
+ getAccent,
806
+ listAccents,
542
807
  };
543
808
  };
544
809
 
545
810
  // ========== INITIALIZATION (safe) ==========
546
811
  const initTheme = () => {
547
812
  try {
813
+ // Restored before the first paint, not after: laying the accent on afterwards
814
+ // would repaint the whole page a frame into boot and flash the brand color
815
+ // over the reader's own choice.
816
+ const stored = readStoredAccent();
817
+ activeAccent = stored?.id ?? null;
818
+ accentSurfacesOn = Boolean(stored?.surfaces);
548
819
  const savedTheme = localStorage.getItem("fletbox-theme");
549
820
  if (savedTheme && (savedTheme === "light" || savedTheme === "dark")) {
550
821
  applyPalette(savedTheme);
@@ -580,6 +851,11 @@ export default {
580
851
  subscribeTheme,
581
852
  applySystemTheme,
582
853
  watchSystemTheme,
854
+ registerAccent,
855
+ setAccent,
856
+ applyAccentPreference,
857
+ getAccent,
858
+ listAccents,
583
859
  ThemeProvider,
584
860
  useTheme,
585
861
  };
@@ -54,8 +54,13 @@ export const addChildren = (parent, children, options = {}) => {
54
54
  };
55
55
 
56
56
  /**
57
- * Removes all children that were previously appended via props (i.e. tracked in
58
- * `PROP_CHILDREN`).
57
+ * Removes the children that were previously appended via props (i.e. tracked in
58
+ * `PROP_CHILDREN`) and **returns them**, so a caller can put them back.
59
+ *
60
+ * The return value is the point: an update that swaps the content takes the old
61
+ * content out first, so if attaching the new content throws, the widget would
62
+ * be left empty — a hole where the content used to be. Keeping the nodes lets
63
+ * the caller restore them.
59
64
  *
60
65
  * Handles two cases:
61
66
  * 1. **Normal path** – the node is still a direct child of `parent`. The
@@ -69,14 +74,14 @@ export const addChildren = (parent, children, options = {}) => {
69
74
  *
70
75
  * @param {HTMLElement} parent - The parent widget whose prop-children should be
71
76
  * removed.
72
- * @returns {HTMLElement} The parent widget (for chaining).
77
+ * @returns {Array<Node>} The nodes that were taken out.
73
78
  */
74
- export const removePropChildren = (parent) => {
75
- const toRemove = Array.from(parent.childNodes).filter((child) =>
76
- PROP_CHILDREN.has(child)
77
- );
79
+ export const takePropChildren = (parent) => {
80
+ const taken = [];
81
+
82
+ Array.from(parent.childNodes).forEach((child) => {
83
+ if (!PROP_CHILDREN.has(child)) return;
78
84
 
79
- toRemove.forEach((child) => {
80
85
  if (child.parentNode === parent) {
81
86
  // Normal path: removeChild override also cleans up _children.
82
87
  parent.removeChild(child);
@@ -87,11 +92,13 @@ export const removePropChildren = (parent) => {
87
92
  if (child._isWidget) child.parent = null;
88
93
  }
89
94
  PROP_CHILDREN.delete(child);
95
+ taken.push(child);
90
96
  });
91
97
 
92
- return parent;
98
+ return taken;
93
99
  };
94
100
 
101
+
95
102
  /**
96
103
  * Removes ALL children from a parent widget, regardless of whether they were
97
104
  * appended via props or programmatically.
@@ -111,4 +118,4 @@ export const clearChildren = (parent) => {
111
118
  return parent;
112
119
  };
113
120
 
114
- export default { addChildren, removePropChildren, clearChildren };
121
+ export default { addChildren, takePropChildren, clearChildren };
@@ -1,6 +1,7 @@
1
1
  // core/assignProps.js
2
2
  import { setStyles } from "./tools.js";
3
3
  import { resolveReactive } from "./reactiveValue.js";
4
+ import { guardHandler } from "../utils/errorBoundary.js";
4
5
 
5
6
  /**
6
7
  * Extracts the real value from a prop that may be a reactive accessor or a
@@ -154,11 +155,18 @@ export const assignProps = (widget, props) => {
154
155
  if (props.events && Object.keys(props.events).length > 0) {
155
156
  Object.entries(props.events).forEach(([event, handler]) => {
156
157
  if (typeof handler !== "function") return;
158
+ // The handler is wrapped where it is attached, so a throw inside it is
159
+ // reported with the name of the widget that owns it instead of
160
+ // arriving as an anonymous stack.
161
+ const guarded = guardHandler(handler, {
162
+ widgetName: widget._widgetName,
163
+ event,
164
+ });
157
165
  if (event === "click") {
158
- widget.onclick = handler;
166
+ widget.onclick = guarded;
159
167
  } else {
160
- widget.addEventListener(event, handler);
161
- widget._events.push({ event, handler });
168
+ widget.addEventListener(event, guarded);
169
+ widget._events.push({ event, handler: guarded });
162
170
  }
163
171
  });
164
172
  }
@@ -219,11 +227,15 @@ export const assignProps = (widget, props) => {
219
227
 
220
228
  if (key.startsWith("on") && typeof value === "function") {
221
229
  const eventName = key.slice(2).toLowerCase();
230
+ const guarded = guardHandler(value, {
231
+ widgetName: widget._widgetName,
232
+ event: eventName,
233
+ });
222
234
  if (eventName === "click") {
223
- widget.onclick = value;
235
+ widget.onclick = guarded;
224
236
  } else {
225
- widget.addEventListener(eventName, value);
226
- widget._events.push({ event: eventName, handler: value });
237
+ widget.addEventListener(eventName, guarded);
238
+ widget._events.push({ event: eventName, handler: guarded });
227
239
  }
228
240
  } else if (
229
241
  [
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { ensureFocusRing } from "../utils/focusRing.js";
4
4
  import { ensureFieldFont } from "../utils/fieldFont.js";
5
+ import { installErrorBoundary } from "../utils/errorBoundary.js";
5
6
 
6
7
  /**
7
8
  * Creates a bare DOM element for use as a widget.
@@ -19,6 +20,11 @@ import { ensureFieldFont } from "../utils/fieldFont.js";
19
20
  * rule about text fields, and this is the only step that runs exactly once per
20
21
  * widget without being owned by any of them.
21
22
  *
23
+ * So does the error boundary: the global handler that catches the errors no
24
+ * widget owns (a rejected promise, a timer that exploded) belongs to the
25
+ * document, not to any single widget, and this is the one step every widget
26
+ * passes through.
27
+ *
22
28
  * @param {string} tag - HTML tag name for the widget.
23
29
  * Defaults to `'div'` when the value is not a string.
24
30
  * @returns {HTMLElement} The newly created DOM element with `_events` initialised.
@@ -26,6 +32,7 @@ import { ensureFieldFont } from "../utils/fieldFont.js";
26
32
  export const createWidget = (tag) => {
27
33
  ensureFocusRing();
28
34
  ensureFieldFont();
35
+ installErrorBoundary();
29
36
 
30
37
  // Validateste that tag is a string; fall back to 'div' when it is not.
31
38
  let finalTag = tag;
@@ -0,0 +1,77 @@
1
+ // The names FletBox still honours but no longer recommends.
2
+ //
3
+ // Every row is a promise to an app written against the old spelling: the prop
4
+ // keeps working, and the migration guide says what to write instead. This
5
+ // table is the single source — the guide (`docs/guides/migration.md`), the
6
+ // codemod in `audit-fix` and the contract test (`scripts/check-migrations.mjs`)
7
+ // all read it, so a rename cannot be documented in one place and forgotten in
8
+ // another.
9
+ //
10
+ // A row names a widget, because the same spelling means different things in
11
+ // different widgets: `onToggle` is the old name of `onPress` on `Switch` and
12
+ // on `Accordion`, and the *current* name on `TreeView`, so a flat list of
13
+ // names would send a `TreeView` to rename a prop that is already right.
14
+
15
+ /**
16
+ * @typedef {object} Deprecation
17
+ * @property {string} widget The widget whose prop is the old spelling.
18
+ * @property {string} from The old prop name.
19
+ * @property {string|null} to The prop to write instead, or `null` when the old
20
+ * prop is read by nobody and the key is simply removed.
21
+ * @property {string} since The release that renamed it.
22
+ * @property {{ prop: string, value: string, to: string }} [alternate]
23
+ * The other target, when the call itself says which one applies.
24
+ * @property {string[]} [guard]
25
+ * Props whose presence means the old prop is already inert, so the codemod
26
+ * leaves the call alone instead of changing what the screen shows.
27
+ */
28
+
29
+ /** @type {Deprecation[]} */
30
+ export const DEPRECATED_PROPS = [
31
+ { widget: "Accordion", from: "onToggle", to: "onPress", since: "1.2.0" },
32
+ { widget: "Checkbox", from: "onCheck", to: "onPress", since: "1.2.0" },
33
+ { widget: "Dropdown", from: "onChange", to: "onPress", since: "1.2.0" },
34
+ { widget: "Radio", from: "onSelect", to: "onPress", since: "1.2.0" },
35
+ { widget: "Slider", from: "onChanged", to: "onPress", since: "1.2.0" },
36
+ { widget: "Switch", from: "onToggle", to: "onPress", since: "1.2.0" },
37
+ { widget: "Tooltip", from: "tooltipDelay", to: "delay", since: "1.2.0" },
38
+ {
39
+ widget: "Scaffold",
40
+ from: "navSideBar",
41
+ to: "leftNavBar",
42
+ since: "1.2.0",
43
+ // The legacy prop could sit on either side, and the legacy position prop
44
+ // is what said which. The codemod follows it instead of guessing a side.
45
+ alternate: { prop: "navSideBarPosition", value: "right", to: "rightNavBar" },
46
+ // `Scaffold` ignores the legacy sidebar once a new one is present, so a
47
+ // call that already has one must be left exactly as it is.
48
+ guard: ["leftNavBar", "rightNavBar"],
49
+ },
50
+ {
51
+ widget: "Scaffold",
52
+ from: "navSideBarWidth",
53
+ to: "leftNavBarWidth",
54
+ since: "1.2.0",
55
+ alternate: { prop: "navSideBarPosition", value: "right", to: "rightNavBarWidth" },
56
+ guard: ["leftNavBarWidth", "rightNavBarWidth"],
57
+ },
58
+ {
59
+ widget: "Scaffold",
60
+ from: "navSideBarPosition",
61
+ to: null,
62
+ since: "1.2.0",
63
+ // The position only ever said which side the *legacy* sidebar was on. Once
64
+ // the sidebar itself carries the side in its name, this prop is read by
65
+ // nobody and the key goes with it.
66
+ },
67
+ ];
68
+
69
+ /**
70
+ * The deprecation of one prop on one widget, or null.
71
+ *
72
+ * @param {string} widget
73
+ * @param {string} prop
74
+ * @returns {Deprecation|null}
75
+ */
76
+ export const deprecationFor = (widget, prop) =>
77
+ DEPRECATED_PROPS.find((entry) => entry.widget === widget && entry.from === prop) || null;
@@ -128,6 +128,11 @@ export const ALL_PROPS = [
128
128
  { prop: "wordBreak", domProp: "wordBreak", type: "style", unit: "none", reactive: true },
129
129
  { prop: "fontFamily", domProp: "fontFamily", type: "style", unit: "none", reactive: true },
130
130
  { prop: "textOverflow", domProp: "textOverflow", type: "style", unit: "none", reactive: true },
131
+ // Contorno de glifo: `textStroke: "4px #fff"` pinta un aro alrededor de las letras
132
+ // (-webkit-text-stroke) and `paintOrder: "stroke fill"` put it behind the fill,
133
+ // so the center keeps its own color instead of having the stroke absorb it.
134
+ { prop: "textStroke", domProp: "webkitTextStroke", type: "style", unit: "none", reactive: true },
135
+ { prop: "paintOrder", domProp: "paintOrder", type: "style", unit: "none", reactive: true },
131
136
 
132
137
  // ============================================================
133
138
  // FLEXBOX
@@ -167,7 +172,7 @@ export const ALL_PROPS = [
167
172
  { prop: "roundedLeft", domProp: "borderTopLeftRadius", type: "style", unit: "rem", reactive: true, targets: ["borderTopLeftRadius", "borderBottomLeftRadius"] },
168
173
  { prop: "roundedRight", domProp: "borderTopRightRadius", type: "style", unit: "rem", reactive: true, targets: ["borderTopRightRadius", "borderBottomRightRadius"] },
169
174
 
170
- // Aliases simétricos (usados en only_controls: Image({ borderRadiusTop: 20 }))
175
+ // Symmetric aliases (used in only_controls: Image({ borderRadiusTop: 20 }))
171
176
  { prop: "borderRadiusTop", domProp: "borderTopLeftRadius", type: "style", unit: "rem", reactive: true, targets: ["borderTopLeftRadius", "borderTopRightRadius"] },
172
177
  { prop: "borderRadiusBottom", domProp: "borderBottomLeftRadius", type: "style", unit: "rem", reactive: true, targets: ["borderBottomLeftRadius", "borderBottomRightRadius"] },
173
178
  { prop: "borderRadiusLeft", domProp: "borderTopLeftRadius", type: "style", unit: "rem", reactive: true, targets: ["borderTopLeftRadius", "borderBottomLeftRadius"] },
@@ -502,6 +507,11 @@ export const ALL_PROPS = [
502
507
  { prop: "unselectedColor", domProp: "unselectedColor", type: "special" },
503
508
  { prop: "showLabels", domProp: "showLabels", type: "special" },
504
509
 
510
+ // ---- ThemePicker ----
511
+ // The families a picker offers. `listAccents()` is the default and the widget
512
+ // reads the list to build one swatch per entry, so it must never reach the DOM.
513
+ { prop: "accents", domProp: "accents", type: "special" },
514
+
505
515
  // ---- DrawerItem / CollapsibleSideBar ----
506
516
  { prop: "route", domProp: "route", type: "special" },
507
517
  { prop: "trailingIcon", domProp: "trailingIcon", type: "special" },
@@ -1,7 +1,7 @@
1
1
  // core/warnings.js
2
2
  // FletBox Warning System – Elegant, minimal, helpful.
3
3
 
4
- // 🚀 Gate of entorno: desactiva warnings in producción
4
+ // 🚀 Environment gate: disable warnings in production
5
5
  const IS_DEV = typeof FLETBOX_DEV !== 'undefined' ? FLETBOX_DEV : true;
6
6
 
7
7
  const suggestionCache = new Map();
@@ -13,7 +13,6 @@ let propNames = [];
13
13
  // the cost of a known prop. Because `update()` re-applies every original prop, one
14
14
  // unknown prop slowed that widget for its whole lifetime.
15
15
  const warnedProps = new Set();
16
- const warnedDeprecated = new Set();
17
16
 
18
17
  export function setPropNames(names) {
19
18
  propNames = names;
@@ -240,30 +239,6 @@ export function warnUnknownProp(propName, context = {}) {
240
239
  console.groupEnd();
241
240
  }
242
241
 
243
- export function warnDeprecatedProp(propName, replacement = null, context = {}) {
244
- if (!IS_DEV) return;
245
- // Same reasoning as `warnUnknownProp`: one report per prop/replacement pair.
246
- const deprecatedKey = `${propName}\u0000${replacement || ""}`;
247
- if (warnedDeprecated.has(deprecatedKey)) return;
248
- warnedDeprecated.add(deprecatedKey);
249
-
250
- const location = getCallLocation();
251
-
252
- printHeader("DEPRECATED PROP");
253
- printProp(propName, "is deprecated");
254
- if (replacement) {
255
- console.log(
256
- ` %c🔄 Use: %c"${replacement}"%c instead`,
257
- "color: #94a3b8; font-size: 13px;",
258
- "color: #34d399; font-weight: 700; font-size: 16px;",
259
- "color: #94a3b8; font-size: 13px;",
260
- );
261
- }
262
- printLocation(location);
263
- printWidget(context.widgetName);
264
- console.groupEnd();
265
- }
266
-
267
242
  export function warnInvalidPropType(propName, expected, actual, context = {}) {
268
243
  if (!IS_DEV) return;
269
244
 
@@ -292,7 +267,6 @@ export function clearSuggestionCache() {
292
267
 
293
268
  export default {
294
269
  warnUnknownProp,
295
- warnDeprecatedProp,
296
270
  warnInvalidPropType,
297
271
  clearSuggestionCache,
298
272
  setPropNames,