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
@@ -36,6 +36,23 @@ export const indexHtml = (appName = "FletBox App") => `<!DOCTYPE html>
36
36
  <meta property="og:title" content="${appName}">
37
37
  <meta property="og:description" content="${appName} built with Flet-Box.">
38
38
  <meta property="og:type" content="website">
39
+ <meta property="og:url" content="./">
40
+ <meta property="og:image" content="./src/assets/icon-512.png">
41
+ <meta property="og:image:alt" content="${appName}">
42
+
43
+ <meta name="twitter:card" content="summary">
44
+ <meta name="twitter:title" content="${appName}">
45
+ <meta name="twitter:description" content="${appName} built with Flet-Box.">
46
+ <meta name="twitter:url" content="./">
47
+ <meta name="twitter:image" content="./src/assets/icon-512.png">
48
+ <meta name="twitter:image:alt" content="${appName}">
49
+
50
+ <!-- The share tags stay relative on purpose: this one file runs on :8000,
51
+ through a tunnel and on Pages. 'flet-box build' rewrites og:image,
52
+ og:url, twitter:image and twitter:url to absolute URLs at the site
53
+ origin in the copy it publishes, because a scraper does not resolve
54
+ a relative path the way a browser does. Replace the icon with a real
55
+ cover if you have one: a square icon is what a 'summary' card expects. -->
39
56
 
40
57
  <meta name="theme-color" content="${SHELL_BRAND}">
41
58
 
@@ -344,6 +361,7 @@ import {
344
361
  Slider,
345
362
  Divider,
346
363
  Tooltip,
364
+ ThemePicker,
347
365
  Icon,
348
366
  SnackBar,
349
367
  AlertDialog,
@@ -353,7 +371,8 @@ import {
353
371
  setTheme,
354
372
  getTheme,
355
373
  setBaseFontSize,
356
- getBaseFontSize
374
+ getBaseFontSize,
375
+ listAccents
357
376
  } from 'flet-box';
358
377
 
359
378
  // Key-based RAM keeps a value across re-renders and hot reloads; localStorage is
@@ -451,6 +470,28 @@ export const AboutScreen = () => {
451
470
  activeColor: colors.primary,
452
471
  onPress: (value) => setTheme(value ? 'dark' : 'light')
453
472
  }),
473
+ // The accent is the reader's answer, not the build's. This app opens on
474
+ // neutral (themes.js applies it at boot) and this row is how they leave it:
475
+ // one square per family, split diagonally into what the hue wears on a light
476
+ // page and what it wears on a dark one, with the tick already on the family
477
+ // painted. A tap repaints every widget on screen and stores the choice, so
478
+ // this does not wait for the Save button below — that keeps the profile.
479
+ // Five squares is a decision; \`listAccents()\` has twelve, so the filter is
480
+ // the brand's answer to what this app is willing to look like.
481
+ Column({
482
+ gap: 8,
483
+ children: [
484
+ Text({ text: 'Accent color', size: 16, weight: 'bold', color: colors.text }),
485
+ ThemePicker({
486
+ label: 'Accent color',
487
+ accents: listAccents().filter((family) =>
488
+ ['neutral', 'blue', 'teal', 'rose', 'orange'].includes(family.id)
489
+ ),
490
+ onChange: (id) =>
491
+ SnackBar({ message: 'Accent: ' + id, type: 'success' }).show()
492
+ })
493
+ ]
494
+ }),
454
495
  Column({
455
496
  gap: 8,
456
497
  children: [
@@ -591,12 +632,15 @@ export const themesJs = () => `// database/themes.js
591
632
  // framework's own \`color().contrastRatio()\` against the surface it is written on:
592
633
  // the number sits next to the pair that needs it, because a token that cannot
593
634
  // carry AA text is decoration, not a color.
594
- import { registerPalette } from 'flet-box';
635
+ import { applyAccentPreference, registerPalette } from 'flet-box';
595
636
 
596
637
  export const lightColors = {
597
- // #6366f1 put white on 4.47:1 — a hair under AA. Two units darker measures
598
- // 4.55:1, the same indigo, and every brand button passes.
599
- primary: '#6165ef',
638
+ // The brand a new app shows is neutral: no hue, the page's own ink. White on
639
+ // this fill measures 17.85:1 and the fill as words on this page measures the
640
+ // same, so a button, an active tab and a link all pass in both directions
641
+ // before anyone has chosen a color. The reader changes it on the Preferences
642
+ // screen (\`ThemePicker\`), which paints a hue and remembers it.
643
+ primary: '#0f172a',
600
644
  secondary: '#8b5cf6',
601
645
  success: '#10b981',
602
646
  warning: '#f59e0b',
@@ -629,10 +673,14 @@ export const lightColors = {
629
673
  border: '#e2e8f0',
630
674
  overlay: 'rgba(0, 0, 0, 0.5)',
631
675
  shadow: 'rgba(0, 0, 0, 0.1)',
632
- // What to write on a brand fill, and the soft tints that replace
633
- // \`colors.primary + '15'\`.
676
+ // What to write on a brand fill, and the soft plate that replaces
677
+ // \`colors.primary + '15'\`. The plate is a lifted gray rather than a tint of
678
+ // a hue, because the brand has no hue: the page ink sits on it at 14.56:1, and
679
+ // since the neutral brand *is* that ink a chip written in \`colors.primary\`
680
+ // measures the same. One limit: \`textSecondary\` below (#64748b) on this plate
681
+ // is 3.88:1, so a caption inside a \`primaryBg\` area uses \`colors.text\`.
634
682
  onPrimary: '#ffffff',
635
- primaryBg: '#eef2ff',
683
+ primaryBg: '#e7e8ea',
636
684
  // The translucent surface for a card with \`backdropFilter\`. A token, so the
637
685
  // app never hand-writes an \`rgba()\` the clean-UI gate would reject.
638
686
  glass: 'rgba(255, 255, 255, 0.66)',
@@ -647,7 +695,10 @@ export const lightColors = {
647
695
  };
648
696
 
649
697
  export const darkColors = {
650
- primary: '#818cf8',
698
+ // The other half of the same neutral: here the brand is the page's near-white
699
+ // ink. It reads 12.02:1 on this background and 9.85:1 on a card, so it is
700
+ // legal as words and as a glyph, and it carries its own label below.
701
+ primary: '#cbd5e1',
651
702
  secondary: '#a78bfa',
652
703
  success: '#34d399',
653
704
  warning: '#fbbf24',
@@ -677,9 +728,14 @@ export const darkColors = {
677
728
  border: '#334155',
678
729
  overlay: 'rgba(0, 0, 0, 0.6)',
679
730
  shadow: 'rgba(0, 0, 0, 0.3)',
680
- // #818cf8 is a light fill: white on it is 2.98:1, the page's own ink is 5.98:1.
681
- onPrimary: '#0f172a',
682
- primaryBg: '#1e1b4b',
731
+ // #cbd5e1 is a light fill: white on it is 1.48:1, so its words are the page's
732
+ // own near-black and measure 13.40:1. The plate sits one step off the page
733
+ // (1.18:1) and one very quiet step off a card (1.03:1), which is what a brand
734
+ // with no hue costs: a tinted banner works on the background, and on a card
735
+ // the mark is \`colors.border\` or the ink itself. Ink on the plate: 13.81:1,
736
+ // secondary ink 5.90:1, the brand 10.19:1.
737
+ onPrimary: '#09090b',
738
+ primaryBg: '#252629',
683
739
  glass: 'rgba(30, 41, 59, 0.66)',
684
740
  glassBorder: 'rgba(226, 232, 240, 0.22)',
685
741
  error: '#f87171',
@@ -697,6 +753,21 @@ export const darkColors = {
697
753
  // the reader's answer (\`setTheme\`, \`toggleTheme\`), not the designer's.
698
754
  registerPalette('light', lightColors);
699
755
  registerPalette('dark', darkColors);
756
+
757
+ // The palette above answers what a theme *is*; the accent layer is what the reader
758
+ // picks, and it owns exactly three tokens: \`primary\`, \`onPrimary\` and
759
+ // \`primaryBg\`. A new app opens on \`neutral\` — the same three values the palettes
760
+ // carry, so the two layers agree and nothing repaints a frame after boot.
761
+ //
762
+ // \`applyAccentPreference\` is the app's verb, not the reader's: it paints only when
763
+ // nobody has chosen yet and writes no preference down, so the Preferences screen's
764
+ // \`ThemePicker\` opens with the neutral square marked as the answer, and a hue
765
+ // picked on a previous visit still wins over this line. \`setAccent\` would answer
766
+ // for the reader and overwrite their choice on every load.
767
+ //
768
+ // An app with a brand of its own keeps the three tokens above and changes the id
769
+ // here to a family it registers with \`registerAccent\`.
770
+ applyAccentPreference('neutral');
700
771
  `;
701
772
 
702
773
  export const runSh = () => `#!/bin/bash
@@ -858,10 +929,13 @@ export const packageJson = (name) => `{
858
929
  "version": "1.0.0",
859
930
  "type": "module",
860
931
  "scripts": {
932
+ "predev": "flet-box check-api --emit-api && flet-box unpack --all --md",
861
933
  "dev": "bash run.sh",
934
+ "verify": "flet-box verify",
862
935
  "check": "node scripts/check-app.mjs",
863
936
  "ui": "node scripts/check-ui.mjs",
864
937
  "fix": "flet-box audit-fix",
938
+ "metrics": "flet-box metrics",
865
939
  "api": "flet-box unpack --all --md",
866
940
  "build": "flet-box createBundle www",
867
941
  "start": "npm run dev",
@@ -907,6 +981,22 @@ export const capacitorConfig = (appName, appId) => `${JSON.stringify(
907
981
  // The user replaces TU-USUARIO/TU-REPO with their real public GitHub names.
908
982
  export const OTA_PLACEHOLDER_URL = "https://TU-USUARIO.github.io/TU-REPO/";
909
983
 
984
+ // The pictures a created app keeps in `assets/`. `flet-box create` makes that
985
+ // folder and writes `assets/logo.png` into it, so it is where an app's own
986
+ // images live — but `createBundle` copies only `src/assets` (icons, fonts) by
987
+ // itself. Undeclared, every bundled build served those screens with broken
988
+ // images while `npm run dev`, which serves the project root, looked perfect.
989
+ // One glob per extension: the asset matcher understands `*`, `**` and `?`,
990
+ // not braces. Images only — a source document parked in `assets/` (the PDF a
991
+ // catalogue was written from) must stay out of the bundle.
992
+ const PROJECT_ASSET_PATTERNS = [
993
+ "assets/**/*.png",
994
+ "assets/**/*.jpg",
995
+ "assets/**/*.jpeg",
996
+ "assets/**/*.svg",
997
+ "assets/**/*.webp",
998
+ ];
999
+
910
1000
  // OTA manifest written to every generated project. The loader (www/loader.js)
911
1001
  // reads `update.url` via www/config.json to know where to fetch updates.
912
1002
  export const fletboxConfig = (url = OTA_PLACEHOLDER_URL) => `${JSON.stringify(
@@ -918,6 +1008,7 @@ export const fletboxConfig = (url = OTA_PLACEHOLDER_URL) => `${JSON.stringify(
918
1008
  mandatory: false,
919
1009
  minNativeVersion: "0.0.0",
920
1010
  notes: "",
1011
+ assets: PROJECT_ASSET_PATTERNS,
921
1012
  },
922
1013
  },
923
1014
  null,
@@ -1628,6 +1719,7 @@ android/capacitor-cordova-android-plugins/
1628
1719
  package-lock.json
1629
1720
  .DS_Store
1630
1721
  *.log
1722
+ .fletbox/reports/
1631
1723
  `;
1632
1724
 
1633
1725
  export const readme = (name, _template, appId) => `# ${name}
@@ -1672,6 +1764,8 @@ framework from scratch:
1672
1764
  Claude Code and Cursor.
1673
1765
  - \`.fletbox/api/cheatsheet.md\` — every widget and its real props, generated from the
1674
1766
  installed package. This is the lookup an agent reads instead of the framework.
1767
+ A prop marked \`(deprecated → x)\` still works, but it is the name being retired;
1768
+ write \`x\`. The old spelling will not fail — nothing else will tell you.
1675
1769
  - \`.fletbox/api/api.json\` — the same surface as data: per prop its engine target,
1676
1770
  default, aliases and a \`sample\` value the types prove works. Write calls from this.
1677
1771
  - \`.fletbox/memory/\` — notes the agent keeps between sessions, so it does not
@@ -1687,7 +1781,8 @@ npm run ui # clean-UI gate: tokens, scale, tap targets,
1687
1781
  # contrast — each finding cites the rule it broke
1688
1782
  npm run ui -- --tokens # measured contrast of every token, on both surfaces
1689
1783
  npm run ui -- --strict # warnings (a one-off tint, an image with no alt) fail too
1690
- npm run fix # rewrite typos it can prove (imports, prop keys)
1784
+ npm run fix # rewrite what it can prove: imports, prop typos,
1785
+ # renamed props
1691
1786
  npm run api # regenerate .fletbox/api/api.json and api.md
1692
1787
  flet-box unpack Button # each prop, where it really goes, and what is dead
1693
1788
  flet-box unpack Button --example # a paste-ready call using only verified props
@@ -2078,10 +2173,12 @@ export const AppBarComponent = ({ title, isMobile = false, showBack = false, onB
2078
2173
  size: 24,
2079
2174
  height: 40,
2080
2175
  color: colors.text,
2176
+ 'aria-label': 'Open the menu',
2081
2177
  onclick: () => openDrawer()
2082
2178
  })
2083
2179
  }) : null,
2084
- // An icon-only control still needs a name: the Tooltip is what says so.
2180
+ // An icon-only control still needs a name, and a Tooltip is a description
2181
+ // (aria-describedby), not a name: the aria-label is what a screen reader says.
2085
2182
  actions: [
2086
2183
  Tooltip({
2087
2184
  text: isDark ? 'Switch to light' : 'Switch to dark',
@@ -2091,6 +2188,7 @@ export const AppBarComponent = ({ title, isMobile = false, showBack = false, onB
2091
2188
  size: 24,
2092
2189
  height: 40,
2093
2190
  color: colors.text,
2191
+ 'aria-label': isDark ? 'Switch to light' : 'Switch to dark',
2094
2192
  onclick: () => toggleTheme()
2095
2193
  })
2096
2194
  })
@@ -2433,6 +2531,13 @@ export const agentsMd = (appName, template, version) =>
2433
2531
  'fixes every size and color you may use, and the checker in section 5',
2434
2532
  'proves you right or wrong in seconds.',
2435
2533
  '',
2534
+ 'If you are an agent **writing code** in this app rather than reading it,',
2535
+ '[AGENTS_FOR_CODE_GEN.md](AGENTS_FOR_CODE_GEN.md) is the shorter contract:',
2536
+ 'the golden rule (a prop that is not in the cheatsheet does not exist), what',
2537
+ 'may never be written, the ten design rules that survive a review, what gets',
2538
+ 'rejected and why, and the verification protocol. Read that one first; this',
2539
+ 'file is its long form.',
2540
+ '',
2436
2541
  '## 0. Start here — the skills folder',
2437
2542
  '',
2438
2543
  'Everything this framework knows about building a professional screen is grouped',
@@ -2488,7 +2593,7 @@ export const agentsMd = (appName, template, version) =>
2488
2593
  '// Correct',
2489
2594
  "Text({ text: 'Hello', size: 18, color: colors.primary })",
2490
2595
  "Button({ text: 'Save', onPress: save, bgColor: colors.primary })",
2491
- 'Box({ child: Text({ text: count }), padding: 12 })',
2596
+ 'Container({ child: Text({ text: count }), padding: 12 })',
2492
2597
  '```',
2493
2598
  '',
2494
2599
  '```js',
@@ -2506,7 +2611,7 @@ export const agentsMd = (appName, template, version) =>
2506
2611
  ' at runtime and nothing fetches a CDN.',
2507
2612
  '- No `.jsx`, `.tsx`, `.vue` or `.svelte` files. Plain `.js`, ESM imports with a',
2508
2613
  ' explicit `.js` extension.',
2509
- '- Styling is props (`bgColor`, `radius`, `padding`), not class strings or',
2614
+ '- Styling is props (`bgColor`, `borderRadius`, `padding`), not class strings or',
2510
2615
  ' `styled-components`.',
2511
2616
  '- `child` takes one widget; `children` takes an array or string. Swapping them',
2512
2617
  ' is the most common silent bug.',
@@ -2611,6 +2716,11 @@ export const agentsMd = (appName, template, version) =>
2611
2716
  '',
2612
2717
  '- A screen uses **one** brand color. Two accent colors competing on one view',
2613
2718
  ' means one of them is wrong.',
2719
+ '- The brand is an **accent**, not a value on the screen: this app opens on the',
2720
+ ' \`neutral\` family (\`applyAccentPreference\` at the bottom of',
2721
+ ' \`src/database/themes.js\`) and the reader can move it on the Preferences',
2722
+ ' screen, where \`ThemePicker\` lives. To ship a hue, change that id or register',
2723
+ ' a family with \`registerAccent\` — never write the hex into a screen.',
2614
2724
  '- Never type a hex or `rgb()` literal. If a token is missing, add it to a',
2615
2725
  ' `theme.js` in this app that re-exports `colors` — do not scatter the value.',
2616
2726
  '- A frosted surface is `colors.glass` with `colors.glassBorder` plus a',
@@ -2628,7 +2738,7 @@ export const agentsMd = (appName, template, version) =>
2628
2738
  '',
2629
2739
  '**Space and type: an 8-based scale.** Use these numbers and nothing between',
2630
2740
  'them. They are plain props (`padding`, `gap`, `margin`, `width`, `height`,',
2631
- '`size`, `radius`) — the factory takes the number, no unit and no `px` string.',
2741
+ '`size`, `borderRadius`) — the factory takes the number, no unit and no `px` string.',
2632
2742
  '',
2633
2743
  '```',
2634
2744
  'spacing 4 · hairline, icon inset',
@@ -2656,29 +2766,76 @@ export const agentsMd = (appName, template, version) =>
2656
2766
  '- `useState` bindings, `flex` and `gap` do the layout. Reach for an absolute',
2657
2767
  ' offset only for overlays, and use `stackPosition` rather than raw coordinates.',
2658
2768
  '',
2769
+ '**The phone range is the test device — strict.** Every phone is `xs` in the',
2770
+ 'breakpoint table, so nothing in the shell changes between 320 and 430; the',
2771
+ 'layout has to. Compose at `360×640` and prove the screen at `320×568` (the',
2772
+ 'floor), `375×667`, `390×844`, `393×852`, `412×915` and `430×932` (the',
2773
+ 'ceiling), in portrait. At 320 nothing overflows or clips: no horizontal',
2774
+ 'scroll, no label cut because its row ran out of room. A heading whose text',
2775
+ 'the app does not control — a lesson name, a chapter, a user title — is `24`',
2776
+ 'on a phone and `32` only from `atLeast("md")`: at 360 a long name at `32` is',
2777
+ 'four lines and pushes the controls under it off the first screen. Fixed',
2778
+ 'chrome is inside the 320 budget: an app bar title plus its actions, a tab',
2779
+ 'strip, a labeled bottom bar. If one needs more than 320, shorten what it',
2780
+ 'says — never let it scroll sideways.',
2781
+ '',
2659
2782
  '## 5. Verification loop (required)',
2660
2783
  '',
2661
2784
  'After every edit, before you say a task is done:',
2662
2785
  '',
2663
2786
  '```bash',
2787
+ 'npm run verify # parses first, then every gate, then your test scripts',
2788
+ ' # — one report, one exit code; this is the command to',
2789
+ ' # run after every edit',
2790
+ 'npm run verify -- --fix # the same, after the mechanical repair: it rewrites',
2791
+ ' # a mistyped prop or a wrong relative path first, so a',
2792
+ ' # typo never reaches you as work to do',
2664
2793
  'npm run check # static gate: invented widgets/props, positional args,',
2665
2794
  ' # foreign imports, and broken imports between your own',
2666
2795
  ' # modules (a name a file never exports)',
2667
2796
  'npm run ui # clean-UI gate: the tokens and the scale above, 40px',
2668
2797
  ' # targets, labels, contrast — every finding cites the',
2669
2798
  ' # rule number it broke',
2799
+ 'npm run metrics # the north metric: how much code passed the gate with',
2800
+ ' # no correction, and which failures keep coming back',
2801
+ 'flet-box report # lost? every file of this project, its verdict, and',
2802
+ ' # the one next step — read-only, writes no metrics row',
2803
+ 'flet-box tools # the map: which command answers which question',
2670
2804
  'npm run dev # serve on :8000 — check the UI actually renders',
2671
2805
  '```',
2672
2806
  '',
2673
- 'Both gates read the installed package, so they are always in sync with what',
2807
+ '`npm run verify` parses every module first (`node --check`, the parser the',
2808
+ 'runtime uses: a missing `)` comes back as `file:line:column` with a caret',
2809
+ 'under the character and a hint), then runs those two gates, the type gate',
2810
+ '(your own `jsconfig.json` through TypeScript, the same service the editor',
2811
+ 'uses: a widget declared as a bare `HTMLElement` makes `widget.update()` an',
2812
+ 'error here and there), the structural gate (a cycle between two of your own',
2813
+ 'modules, a module outside the project folders, markup written by hand) and',
2814
+ 'the prop contract in parallel, and',
2815
+ 'prints one report: the gate that failed, the file at `line:column`, and what',
2816
+ 'to do about it. A project that declares its own test scripts gets those run',
2817
+ 'too; a created app declares none.',
2818
+ 'All of them read the installed package, so they are always in sync with what',
2674
2819
  'runs. They exit non-zero on a real error: fix the code, not the checker. A',
2675
2820
  'warning is allowed to stay only when you can justify it in one sentence.',
2676
2821
  '',
2822
+ 'Fix what the report names, and nothing else. Every finding carries a file and',
2823
+ 'a line: change that line. Do not rewrite the file to make it pass, and do not',
2824
+ 'reformat or rename a line the report did not name. If the same error survives',
2825
+ 'two attempts, the diagnosis was wrong: revert and hand the report back.',
2826
+ '',
2677
2827
  '`npm run ui` proves what is *written*; the rules it cites are the numbered ones',
2678
2828
  'in [skills/fletbox-design/SKILL.md](skills/fletbox-design/SKILL.md),',
2679
2829
  'and that file is the part a checker cannot do — hierarchy, copy, whether the',
2680
2830
  'screen solves the problem. Read it before writing a screen, not after.',
2681
2831
  '',
2832
+ 'Every `npm run verify` also appends a row to',
2833
+ '`.fletbox/metrics/generations.md` — green or not — and `npm run metrics`',
2834
+ 'reads those rows: the share of code that passed the gate with no correction',
2835
+ 'at all, plus the failures that recur most often. When the same code comes',
2836
+ 'back a third time with no rule behind it, the command fails: that is the',
2837
+ 'signal to write the rule, not to fix the same line again.',
2838
+ '',
2682
2839
  'When the error is a mistyped name, `npm run fix` rewrites it for you — a',
2683
2840
  'relative path that does not resolve, a name the target module never exports,',
2684
2841
  'an identifier used without its `import`, and a prop key the API does not know',
@@ -2686,6 +2843,13 @@ export const agentsMd = (appName, template, version) =>
2686
2843
  'one real name is close enough; anything ambiguous is printed untouched for you',
2687
2844
  'to decide. Run `npm run check` again after it, and read the diff.',
2688
2845
  '',
2846
+ 'When you are lost, do not guess which gate to run: `flet-box report` names',
2847
+ 'every file of this project (clean ones included), its verdict, its widgets,',
2848
+ 'and a ranked next step; `flet-box tools` is the catalogue that maps a',
2849
+ 'question to the command. Both are read-only — no repair, no metrics row —',
2850
+ 'and the only file they can write is the report page, when `--html` asks for',
2851
+ 'it, into `.fletbox/reports/` (generated output, not committed).',
2852
+ '',
2689
2853
  '## 6. Memory — do not relearn the same thing',
2690
2854
  '',
2691
2855
  'The full format lives in [skills/fletbox-memory/SKILL.md](skills/fletbox-memory/SKILL.md).',
@@ -2708,8 +2872,14 @@ export const agentsMd = (appName, template, version) =>
2708
2872
  '| Command | What it does |',
2709
2873
  '| --- | --- |',
2710
2874
  '| `npm run dev` | static dev server on port 8000 |',
2875
+ '| `npm run verify` | parses every module, then every gate, then your test scripts: one report, one exit code |',
2711
2876
  '| `npm run check` | anti-hallucination gate over `src/` |',
2712
2877
  '| `npm run fix` | rewrite mistyped imports and prop names it can prove |',
2878
+ '| `npm run metrics` | the north metric: how much code passed the gate with no correction |',
2879
+ '| `flet-box report` | every file with its verdict, the widget ledger, and a ranked next step (read-only) |',
2880
+ '| `flet-box report --html` | the same report as one self-contained page in `.fletbox/reports/` |',
2881
+ '| `flet-box tools` | the catalogue: which command answers which question (`--emit` writes `.fletbox/api/tools.json`) |',
2882
+ '| `flet-box mcp` | the same read-only tools over MCP stdio, for an assistant with no shell |',
2713
2883
  '| `npm run check -- --props Button` | print the real props of a widget |',
2714
2884
  '| `flet-box unpack Button` | everything Button accepts **and that actually works** |',
2715
2885
  '| `flet-box unpack Button --example` | a paste-ready call using only live props |',
@@ -2828,6 +2998,43 @@ export const memoryStatusMd = (appName, template) => {
2828
2998
  ].join('\n');
2829
2999
  };
2830
3000
 
3001
+ export const metricsLogMd = () =>
3002
+ [
3003
+ '# Generations',
3004
+ '',
3005
+ 'The log of every `npm run verify` run in this app, newest last. One row per',
3006
+ 'run: when it happened, whether the gates were green, which gates failed and',
3007
+ 'which codes they reported.',
3008
+ '',
3009
+ 'A **generation** is a run of consecutive failing rows that ends at the first',
3010
+ 'green one — the rows before it are the corrections that were needed. A green',
3011
+ 'row that follows a green row is a generation that needed none.',
3012
+ '',
3013
+ '**The north metric** is the share of generations whose *first* run was green:',
3014
+ 'the code that passed the gate with no correction at all. It is measured by',
3015
+ '`npm run metrics`, which also prints the three failures that recurred most',
3016
+ 'often and the rule each one should belong to. That list is the loop: a code',
3017
+ 'that keeps coming back and has no rule behind it is a rule that has not been',
3018
+ 'written yet.',
3019
+ '',
3020
+ '## Gate defects',
3021
+ '',
3022
+ 'A code listed here was reported by the gate, not by the code it checked. The',
3023
+ 'window is the first and the last day it accounts for, so the runs inside it',
3024
+ 'count as passed and never feed the rule loop: that is the framework\'s debt,',
3025
+ 'not a correction this app needed. `Fixed` empty (`—`) means the defect is',
3026
+ 'still open. A run after `Fixed` counts again — a defect that comes back is',
3027
+ 'this app\'s finding until the window is reopened. Remove a row to hand its',
3028
+ 'findings back.',
3029
+ '',
3030
+ '| Code | Seen | Fixed | Where |',
3031
+ '| --- | --- | --- | --- |',
3032
+ '',
3033
+ '| When | Project | Result | Gates | Codes |',
3034
+ '| --- | --- | --- | --- | --- |',
3035
+ '',
3036
+ ].join('\n');
3037
+
2831
3038
  export const memoryIndexMd = (appName) =>
2832
3039
  [
2833
3040
  '# Memory index',
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @file bin/utils/toolkit.js
3
+ * @description The diagnosis toolkit, as data — one entry per question an agent
4
+ * may ask, and the command that answers it.
5
+ *
6
+ * The problem this file solves is not "there are no checkers". There are ten,
7
+ * and each one is right about its own slice. The problem is that an agent does
8
+ * not know which one to run for the question in front of it, so it runs the one
9
+ * it remembers and reports a number that answers something else.
10
+ *
11
+ * So the tools are listed here with the question each one answers, in the
12
+ * language a person uses when they are stuck, and every entry names the exact
13
+ * command. The CLI prints this list (`flet-box tools`), the report links every
14
+ * finding back to the tool that proves it, and the MCP server (`flet-box mcp`)
15
+ * exposes the same entries to an assistant that has no shell. One source, three
16
+ * mouths.
17
+ *
18
+ * The rule for membership: a tool in this catalogue **reads**. No entry may
19
+ * repair, rename, delete, build or write — those are decisions, and the assistant
20
+ * asks a human before taking one. `writes` is therefore empty on every entry;
21
+ * the field exists so a future write tool cannot be added silently.
22
+ *
23
+ * Pure data: this module imports nothing. Renderers live in
24
+ * `bin/commands/tools.js` and `bin/mcp.js`.
25
+ */
26
+
27
+ /** Bumped only when an entry's shape changes, so a cached copy can be noticed. */
28
+ export const CATALOG_VERSION = 1;
29
+
30
+ /**
31
+ * Every read-only tool, ordered from "where do I start" to "one narrow check".
32
+ *
33
+ * `params` is the JSON-Schema source used by the MCP server:
34
+ * `{ name, type: "string"|"boolean", required?, description }`.
35
+ */
36
+ export const TOOLS = [
37
+ {
38
+ id: "report",
39
+ title: "Project report",
40
+ question: "Where is the work? Which file is wrong, how wrong, and what is missing?",
41
+ cli: "flet-box report",
42
+ usage: "flet-box report [--json | --md | --html] [--tests]",
43
+ output: ["text", "json", "md", "html"],
44
+ writes: ["--html only: .fletbox/reports/report.html (or --out <file>)"],
45
+ params: [
46
+ { name: "tests", type: "boolean", description: "Also run the project's own test scripts (slow)." },
47
+ { name: "root", type: "string", description: "Project folder; defaults to the current directory." },
48
+ ],
49
+ when: [
50
+ "after any edit, before saying the work is done",
51
+ "when a screen renders but is wrong and the file is not obvious",
52
+ "when the user asks what is missing or what to do next",
53
+ ],
54
+ },
55
+ {
56
+ id: "widget_props",
57
+ title: "Widget props",
58
+ question: "Which props does this widget really accept, with types and deprecations?",
59
+ cli: "flet-box check-api --props <Widget>",
60
+ usage: "flet-box check-api --props Text",
61
+ output: ["text"],
62
+ writes: [],
63
+ params: [
64
+ { name: "widget", type: "string", required: true, description: "Widget name, e.g. Text or Container." },
65
+ { name: "root", type: "string", description: "Project folder; defaults to the current directory." },
66
+ ],
67
+ when: ["before writing a call", "when a prop silently does nothing", "when the cheatsheet looks wrong"],
68
+ },
69
+ {
70
+ id: "widget_contract",
71
+ title: "Widget contract",
72
+ question: "Do the declaration, the engine and the docs agree about this widget — and which props are dead?",
73
+ cli: "flet-box unpack <Widget> --json",
74
+ usage: "flet-box unpack Text --json",
75
+ output: ["text", "json"],
76
+ writes: [],
77
+ params: [
78
+ { name: "widget", type: "string", required: true, description: "Widget name, e.g. Text or Container." },
79
+ { name: "root", type: "string", description: "Project folder; defaults to the current directory." },
80
+ ],
81
+ when: ["when a prop is typed but does nothing", "when reading the framework's own gaps"],
82
+ },
83
+ {
84
+ id: "list_widgets",
85
+ title: "List widgets",
86
+ question: "What exists in this version of the framework?",
87
+ cli: "flet-box check-api --list",
88
+ usage: "flet-box check-api --list",
89
+ output: ["text"],
90
+ writes: [],
91
+ params: [{ name: "root", type: "string", description: "Project folder; defaults to the current directory." }],
92
+ when: ["before inventing a name", "when unsure whether a helper is real"],
93
+ },
94
+ {
95
+ id: "check",
96
+ title: "API check",
97
+ question: "Is any widget, prop or import invented?",
98
+ cli: "flet-box check-api",
99
+ usage: "flet-box check-api [--json] [--full]",
100
+ output: ["text", "json"],
101
+ writes: [],
102
+ params: [
103
+ { name: "full", type: "boolean", description: "List every warning site instead of collapsing repeated ones." },
104
+ { name: "root", type: "string", description: "Project folder; defaults to the current directory." },
105
+ ],
106
+ when: ["when a screen is blank or a widget renders nothing", "after copying code from another project"],
107
+ },
108
+ {
109
+ id: "design",
110
+ title: "Design check",
111
+ question: "Are the tokens, the scale, the tap targets, the labels and the contrast legal?",
112
+ cli: "flet-box check-ui",
113
+ usage: "flet-box check-ui [--json] [--strict] [--tokens]",
114
+ output: ["text", "json"],
115
+ writes: [],
116
+ params: [
117
+ { name: "tokens", type: "boolean", description: "Print the measured contrast of every colour token and exit." },
118
+ { name: "strict", type: "boolean", description: "Fail on contrast warnings too." },
119
+ { name: "root", type: "string", description: "Project folder; defaults to the current directory." },
120
+ ],
121
+ when: ["after building or editing a screen", "when the user says it looks off"],
122
+ },
123
+ {
124
+ id: "syntax",
125
+ title: "Parse check",
126
+ question: "Does every module parse?",
127
+ cli: "flet-box verify --only syntax --no-tests",
128
+ usage: "flet-box verify --only syntax --no-tests [--json]",
129
+ output: ["text", "json"],
130
+ writes: [],
131
+ params: [{ name: "root", type: "string", description: "Project folder; defaults to the current directory." }],
132
+ when: ["when the app is blank and the console says nothing", "before blaming the framework"],
133
+ },
134
+ {
135
+ id: "types",
136
+ title: "Type check",
137
+ question: "What does the editor complain about?",
138
+ cli: "flet-box verify --only types --no-tests",
139
+ usage: "flet-box verify --only types --no-tests [--json]",
140
+ output: ["text", "json"],
141
+ writes: [],
142
+ params: [{ name: "root", type: "string", description: "Project folder; defaults to the current directory." }],
143
+ when: ["when a widget method or prop is flagged in the editor", "after upgrading the package"],
144
+ },
145
+ {
146
+ id: "metrics",
147
+ title: "Recurring failures",
148
+ question: "Which mistakes keep coming back, and does a rule own each one?",
149
+ cli: "flet-box metrics",
150
+ usage: "flet-box metrics [--log <file>]",
151
+ output: ["text"],
152
+ writes: [],
153
+ params: [{ name: "root", type: "string", description: "Project folder; defaults to the current directory." }],
154
+ when: ["when the same finding keeps returning", "when deciding which rule to write"],
155
+ },
156
+ ];
157
+
158
+ /**
159
+ * One entry by id.
160
+ *
161
+ * @param {string} id
162
+ * @returns {object|null}
163
+ */
164
+ export const toolById = (id) => TOOLS.find((tool) => tool.id === id) || null;
165
+
166
+ /**
167
+ * The catalogue as the JSON a tool reads: stable, relative to nothing, and with
168
+ * the invariant stated in the payload so a consumer can assert it.
169
+ *
170
+ * @returns {object}
171
+ */
172
+ export const catalogJson = () => ({
173
+ tool: "flet-box tools",
174
+ version: CATALOG_VERSION,
175
+ // No entry edits a source, deletes, builds or publishes. The only file any of
176
+ // them can produce is the report's own HTML, and only when `--html` asks.
177
+ readOnly: true,
178
+ tools: TOOLS.map((tool) => ({
179
+ id: tool.id,
180
+ title: tool.title,
181
+ question: tool.question,
182
+ cli: tool.cli,
183
+ usage: tool.usage,
184
+ output: tool.output,
185
+ writes: tool.writes,
186
+ // The arguments are part of the map: a reader that cannot ask the MCP
187
+ // server still has to know that `widget_props` needs a widget name.
188
+ params: tool.params,
189
+ when: tool.when,
190
+ })),
191
+ });