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
@@ -417,6 +417,35 @@ waiting, remembering (p.099).
417
417
  - Break a convention only when the measurement says the convention is wrong here,
418
418
  and write that measurement down.
419
419
 
420
+ ### The phone range is the test device — strict
421
+
422
+ "Fine on my phone" is not a measurement. Every phone is `xs` in the breakpoint
423
+ table, so nothing in the shell changes between 320 and 430; the layout has to.
424
+ Compose at 360 and prove the screen at each width below, in portrait:
425
+
426
+ | Width × height | Device | What that width judges |
427
+ | --- | --- | --- |
428
+ | 320 × 568 | iPhone SE 1 / small Android | **the floor** — nothing overflows or clips |
429
+ | 360 × 640 | Android baseline | **compose here first**, the cramped common case |
430
+ | 375 × 667 | iPhone SE 2/3 | narrow and tall |
431
+ | 390 × 844 | iPhone 12/13/14 | the modern default |
432
+ | 393 × 852 | iPhone 14 Pro / 15 / 16 | notch and home indicator |
433
+ | 412 × 915 | Pixel 7 | large Android |
434
+ | 430 × 932 | iPhone 14/15 Pro Max | **the ceiling** — no measure so wide it loses the eye |
435
+
436
+ - At 320 nothing overflows or clips: `scrollWidth <= clientWidth` on the page
437
+ and on every bar, and no label ellipsized because its row ran out of room.
438
+ - A heading whose text the app does not control — a lesson name, a chapter, a
439
+ user's title — is `24` on a phone and `32` only where the layout is wide
440
+ (`atLeast("md")`). At 360 a 55-character name at `32` is four lines, and the
441
+ tab strip under it leaves the first screen. The size steps down; the copy is
442
+ never cut to fit it.
443
+ - Fixed chrome is inside the 320 budget, not outside it: an app bar title plus
444
+ its actions, a tab strip, a labeled bottom bar. If a bar needs more than 320,
445
+ shorten what it says — never let it scroll sideways.
446
+ - Verify by resizing the viewport at each width and reading the layout that
447
+ results, not by scaling a 1440 composition down.
448
+
420
449
  ## 17 Composition starts as a sentence
421
450
 
422
451
  Before the first widget, write four lines: what this screen decides for the
@@ -465,9 +494,9 @@ broken" report, and none of them is something `npm run ui` can see:
465
494
  (long-press, a native back gesture), not a layout one. A screen built from two
466
495
  hard-coded layouts is two screens to maintain; a screen with one per-step value
467
496
  where the shape truly changes is one.
468
- - Overlays go in a `Stack` with `stackPosition`, not in absolute coordinates on
469
- `Container({ pos: "absolute", top, left })`. The first survives a content
470
- change; the second is a pixel bomb in any language longer than English.
497
+ - Overlays go in a `Stack` with `stackPosition`, never in absolute coordinates
498
+ (`pos: "absolute"`, `top`, `left`) on a plain `Container`. The first survives a
499
+ content change; the second is a pixel bomb in any language longer than English.
471
500
 
472
501
  ## 19 The dated tells
473
502
 
@@ -588,7 +617,7 @@ Both are worth more read than copied. The parts that do not survive measurement:
588
617
  ```bash
589
618
  npm run check
590
619
  npm run ui
591
- npm run dev # look at the screen, at 375px and at 1440px
620
+ npm run dev # look at the screen at 320, 430 and 1440px
592
621
  ```
593
622
 
594
623
  Fix the code, not the checker. Then make the rule reusable: if the fix was right
@@ -701,6 +730,28 @@ that made it findable.
701
730
  bloom over a light page is indistinguishable from a smudge:
702
731
  `CircularBar({ glow: true, glowColor: colors.primary })`, or a tinted `shadow`
703
732
  string on the CTA. One glowing element per screen.
733
+ - A reader's choice does not raise the budget. `ThemePicker` lets them pick the
734
+ hue; the three placements stay the only places it is spent. The picker marks
735
+ the chosen square with the page's ink and a tick, never with the accent — a
736
+ swatch selected in its own color says nothing the square does not already say,
737
+ and rule 28 forbids spending the accent on the control that hands it out.
738
+ - A hue may also be worn as a **theme**, which is a different thing from spending
739
+ it: `ThemePicker({ surfaces: true })` and `setAccent("teal", { surfaces: true })`
740
+ derive the page, the panels, the cards and the hairlines from the same ramp, so
741
+ the screen is tinted teal while the accent itself stays on its three placements.
742
+ Only those five surface tokens move — the ink scale, `primaryBg` and every
743
+ semantic signal keep the palette's values, which is what lets a tinted theme
744
+ still say "this failed" in red — and `getAccentSurfaces()` answers which of the
745
+ two the reader is wearing. Offer it when 27's paper archetype is the app's look;
746
+ offer brand-only when the page is meant to stay neutral.
747
+ - An app that ships a brand color sets it at boot with
748
+ `applyAccentPreference("teal")`, never with `setAccent`: the first paints a
749
+ default without writing a preference down, so a reader who has already chosen
750
+ still opens on their own hue; the second answers for them and stores it, which
751
+ is the app overwriting a person on every load. A created app ships `neutral` —
752
+ no hue, the page's own ink — so the picker opens with that square ticked and
753
+ the reader leaves it, rather than choosing between twelve colors on a first
754
+ visit.
704
755
 
705
756
  ## 29 The number is the hero
706
757
 
@@ -741,6 +792,14 @@ Column({ gap: 4, children: [
741
792
 
742
793
  - Controls are pills: `borderRadius` = half the height. Cards are 16-24. Inner
743
794
  thumbnails and tiles are 12-16, one step below their card.
795
+ - **A pill is a one-line shape.** `borderRadius: 999` reads as a control only
796
+ while its height is about one line of its label: a label that wraps turns the
797
+ fill into an egg and the capsule's rounded ends cut into the words. `Button`
798
+ holds its label on one line and ellipsizes what does not fit, so a CTA that
799
+ needs three lines is a copy bug wearing a pill. Keep the label a verb and its
800
+ - **Follow Mission 1** — and leave the lesson title to the heading
801
+ (rule 04). If the action genuinely needs a sentence, the control is a card or
802
+ a list tile, not a button (rule 32).
744
803
  - **The nesting rule**, as arithmetic: `outer = inner + padding`. A 16-radius
745
804
  tile inside 8 of padding needs a 24-radius card. Equal radii at both levels is
746
805
  the failure — the inner corner and the outer corner curve on the same radius,
@@ -807,6 +866,7 @@ Stack({ alignment: "bottom-left", height: 420, children: [
807
866
  | switch 2 views of one object | segmented pill with a sliding plate | `Tabs({ variant: "pills", tabs, activeIndex, activeTextColor, buttonColor })` |
808
867
  | choose 1 of N with a preview | selection **tile**, not a radio list | `ListTile({ selected, selectedBgColor })`, or a 2-up `Row` of `Card`s |
809
868
  | pick a color/size | dot row, selected gets a ring | `Avatar({ shape: "circle", size: 28 })` row, ring via `borderColor` + `borderWidth` |
869
+ | pick the app's accent | two-tone square per hue family | `ThemePicker({ size: 48, showLabels: true })` — it repaints the screen and remembers the choice; hand-built swatches do neither |
810
870
  | count then buy | stepper merged into the CTA | one `Row` of `Button`s inside the add-to-cart bar, not two actions |
811
871
  | navigate the app | bottom bar, active = accent + label | `BottomNavigation({ items, currentIndex, selectedColor, unselectedColor, showLabels: true })` |
812
872
  | a badge on an icon | dot, never a word | `Badge({ value, size, offset, borderColor: colors.background })` |
@@ -878,7 +938,31 @@ ink white or near-white, and its opacity tiers
878
938
  - **Ramp, not swatch.** A family is 5-7 stops (base → tint → accent → shadow).
879
939
  Build it as a ramp so a card, a tile and a plate can be three steps of one hue
880
940
  instead of three hues — which is the only way 27's surface discipline is
881
- possible without breaking 25's one-accent law.
941
+ possible without breaking 25's one-accent law. The framework already does this
942
+ arithmetic for a reader who asks for a colored app: `setAccent(id,
943
+ { surfaces: true })` takes the five stops it ships and writes `background`,
944
+ `surface`, `card`, `border` and `divider` off them (palest → wash → white card →
945
+ wash darkened, light; the deep stop scaled toward black in four steps, dark), so
946
+ a tinted theme is a call, not a hand-mixed set of hexes.
947
+ - **A preset beats a mid-tone.** An app that has no ramp of its own does not pick
948
+ a blue and hope: it takes one of the twelve families the framework ships
949
+ (`listAccents()`, `listAccentRamps()` to see the stops) and paints a **stop** of
950
+ that ramp, so `colors.primary` and `colors.primaryBg` are two steps of one
951
+ gradient and never a hue plus a guess. Each pair is bought with seven measured
952
+ numbers per theme: the accent carries its own label at 4.5:1, reads as text on
953
+ the page at 4.5:1 and as a glyph on a card at 3:1, its wash holds page ink,
954
+ secondary ink and the accent at 4.5:1, and the wash lifts off the background by
955
+ 1.04-1.5. That is why `#3b82f6` is not a brand color — white words on it
956
+ measure 3.15:1 — and why a hue the list lacks is registered instead of
957
+ invented: `registerAccent` takes an `id`, a `label` and a `light`/`dark` pair —
958
+ one hex each, or a five-stop ramp, whose stop 0 (light) and stop 1 (dark) are
959
+ the ones painted and whose wash is a stop further along the same direction.
960
+ Run a ramp the other way and the plate lands *under* the accent instead of just
961
+ off the page. It belongs in `src/database/themes.js` beside the palette it
962
+ feeds (rule 03), and a registered pair answers to the same veto as a hand-built
963
+ one. What `flet-box create` boots is the `neutral` family — the accent is the
964
+ page's own ink, so an unfinished app reads as paper and type rather than as
965
+ somebody else's brand — and the first change a designer makes is that id.
882
966
  - **The light/dark pair rule**: for the same hue in both palettes, lift the
883
967
  lightness and drop the saturation going dark. That is why this framework's dark
884
968
  `dangerStrong` is `#fca5a5` where the light one is `#b91c1c` — the readable red
@@ -232,7 +232,7 @@ const chooseMotion = (on) => {
232
232
  };
233
233
 
234
234
  // A settings screen hands the decision to the reader, and the engine applies it.
235
- Switch({ label: 'Reduce motion', value: getMotionMode() === 'reduce', onToggle: chooseMotion });
235
+ Switch({ label: 'Reduce motion', value: getMotionMode() === 'reduce', onPress: chooseMotion });
236
236
  ```
237
237
 
238
238
  Build the screen so it reads with zero animation anyway. Motion is a decision you
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fletbox-rules
3
- description: The working contract for a FletBox app - FletBox syntax only, never a widget or prop from React/Vue/Svelte/Flutter, look an API up before using it, the rem unit contract, prop aliases, key-based state, resource cleanup, one home for every shared helper, and the two gates that prove the work. Read first, before any other skill, and before writing or editing a single file in src/.
3
+ description: The working contract for a FletBox app - FletBox syntax only, never a widget or prop from React/Vue/Svelte/Flutter, look an API up before using it, the rem unit contract, prop aliases, key-based state, resource cleanup, one home for every shared helper, and the gates that prove the work. Read first, before any other skill, and before writing or editing a single file in src/.
4
4
  ---
5
5
 
6
6
  # Rules — how to work inside this framework
@@ -101,18 +101,47 @@ reactivity). It is the only renderer. A custom component is still a function cal
101
101
  returning widgets; never add a parallel way to paint, and never monkey-patch
102
102
  `container.update` — compose the update.
103
103
 
104
- ## R8 The two gates, after every edit
104
+ That includes the DOM itself. App code never writes `innerHTML`: a hand-written
105
+ element skips the theme, the scale and the accessibility every widget already
106
+ has, and it is how a screen ends up speaking two visual languages at once.
107
+ Building an element with `document.createElement` is the same mistake one layer
108
+ down — the gate reports it as a warning, and it is usually a sign that a widget
109
+ is missing.
110
+
111
+ ## R8 The gates, after every edit
105
112
 
106
113
  ```bash
114
+ npm run verify # every gate at once — one report, one exit code. This is the
115
+ # command to run after every edit
116
+ npm run verify -- --fix # the same, after the mechanical repair: a typo is the
117
+ # tool's job, not yours
107
118
  npm run check # does this API exist? exits 1 on an invented widget, prop,
108
119
  # positional call, foreign import or broken module import
109
120
  npm run ui # is this screen clean? tokens, the scale, tap targets, labels,
110
121
  # contrast — and it cites the rule it broke
111
122
  npm run fix # rewrites what the checker can prove: a wrong path, a default
112
123
  # import of a named export, a missing import line, a typo in a
113
- # prop key. Anything ambiguous is printed, never touched
124
+ # prop key, a prop the framework renamed. Anything ambiguous is
125
+ # printed, never touched
126
+ npm run metrics # the north metric: how much code passed the gate with no
127
+ # correction at all, and which failures keep coming back
114
128
  ```
115
129
 
130
+ `npm run verify` parses every module first — `node --check`, the parser the
131
+ runtime uses, so a missing `)` comes back as `file:line:column` with the
132
+ offending line, a caret under the character and a hint — then runs the API check,
133
+ the type check (the same `jsconfig.json` the editor reads, through the same
134
+ TypeScript service: a widget typed as a bare `HTMLElement` makes
135
+ `widget.update()` an error here and there), the design check, the structural
136
+ check (two of your modules importing each other
137
+ in a circle, a module outside the project folders, markup written by hand) and
138
+ the prop contract, and then any test scripts the project declares. It prints one
139
+ report: the gate that failed, the file at `line:column`, and what to do about it.
140
+
141
+ Fix the line it names. Do not rewrite the file to make it pass, and do not
142
+ reformat a line the report did not name — a diff you cannot justify is a diff
143
+ you should not make.
144
+
116
145
  Then look at the screen at 375px and at 1440px, in both themes: **the checker has no
117
146
  eyes.** A clean `npm run ui` says the values are legal, not that the screen is
118
147
  understood.
@@ -121,14 +150,37 @@ A finding you keep needs one sentence of justification, written where the next
121
150
  session will read it (R10). "The design wants it" is not a justification — it is a
122
151
  rule that has not been written down yet.
123
152
 
153
+ Every `npm run verify` writes a row into `.fletbox/metrics/generations.md`, green
154
+ or not, so `npm run metrics` can say how much code passed the gate with no
155
+ correction at all. The same command prints the failures that recur most often
156
+ and the rule each one should belong to — and it **fails** when a code comes back
157
+ three times with no rule behind it. That is the loop: the same mistake is a rule
158
+ you did not write, and you are the one who writes it. A code that comes back
159
+ *with* a rule behind it is a different problem, and the command says so: the
160
+ rule is not landing.
161
+
162
+ But a recurring code can also be the gate's own defect. When you prove that,
163
+ write it into the log's **Gate defects** table with the window of runs it
164
+ explains and where the fix lives — do not rename exports in the app to dodge
165
+ it, and do not hard-code an exception inside the checker: the window is scoped,
166
+ so the same code counts against the project again after the fix day.
167
+
124
168
  ## R9 Do not trust your memory of this framework
125
169
 
126
170
  FletBox has no `.d.ts` you have read in full, the prop set grows between versions,
127
- and the installed version is the truth for this app. Three habits follow:
171
+ and the installed version is the truth for this app. Four habits follow:
128
172
 
129
173
  - Look up before using, even for `Text`.
130
174
  - After a dependency bump, re-run `npm run check` across the app — it also reports
131
175
  names the package stopped exporting.
176
+ - **A renamed prop never breaks the call, so nothing will tell you.** The old
177
+ spelling keeps working, marked `@deprecated`, until the next major; the gate
178
+ stays green either way, and an example you copied from three versions ago still
179
+ runs. The cheatsheet marks it — `onToggle?: (value: boolean) => void
180
+ (deprecated → onPress)` — and the prop after the arrow is the one to write.
181
+ `npm run fix` renames the calls it can prove (imports, prop typos and
182
+ deprecations in one pass) and prints the ones it cannot, so run it after every
183
+ upgrade and read the diff — a codemod that guesses is worse than one that stops.
132
184
  - What is deployed is built from the **published** package, not from your working
133
185
  tree. A fix you made in a local checkout does not exist for an installed app
134
186
  until it is released.
@@ -9,6 +9,7 @@ import {
9
9
  import { Container } from "../widgets/Container.js";
10
10
  import { setGlobalRender } from "../tools/useState.js";
11
11
  import { beginRenderPass, endRenderPass } from "../widget-factory/reactiveValue.js";
12
+ import { renderFailure } from "../utils/errorBoundary.js";
12
13
 
13
14
  /**
14
15
  * Mounts a FletBox application into the DOM and manages its full lifecycle.
@@ -184,12 +185,23 @@ export const runApp = (
184
185
  // state value surgical.
185
186
  beginRenderPass();
186
187
  let appContent;
188
+ let failure = null;
187
189
  try {
188
190
  appContent = typeof App === "function" ? App() : App;
191
+ } catch (error) {
192
+ failure = error;
189
193
  } finally {
190
194
  endRenderPass();
191
195
  }
192
196
 
197
+ // A throw in the component body — before any widget exists — has no widget
198
+ // to catch it. Without this the root stayed empty and the screen was
199
+ // blank; now it says what went wrong and where.
200
+ if (failure) {
201
+ renderFailure(root, failure, { widgetName: "App", phase: "app" });
202
+ return;
203
+ }
204
+
193
205
  const mainContainer = Container({
194
206
  margin: 0,
195
207
  padding: 0,