flet-box 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/bin/commands/checkUi.js +276 -45
  2. package/bin/commands/create.js +4 -0
  3. package/bin/commands/createScreen.js +1 -1
  4. package/bin/commands/runBundle.js +1 -1
  5. package/bin/utils/colors.js +0 -8
  6. package/bin/utils/helpers.js +0 -5
  7. package/bin/utils/images/jpeg.js +2 -3
  8. package/bin/utils/templates.js +78 -73
  9. package/docs/README.md +2 -0
  10. package/docs/cli/README.md +18 -1
  11. package/docs/guides/README.md +5 -2
  12. package/docs/guides/accessibility.md +282 -0
  13. package/docs/guides/clean-ui.md +53 -8
  14. package/docs/guides/minification-and-protection.md +1 -1
  15. package/docs/guides/mobile-and-platforms.md +51 -0
  16. package/docs/guides/utilities.md +116 -4
  17. package/docs/guides/widget-structure.md +30 -0
  18. package/docs/tools/README.md +1 -1
  19. package/docs/tools/animation.md +134 -11
  20. package/docs/tools/device.md +75 -3
  21. package/docs/tools/text-and-time.md +116 -6
  22. package/docs/widget/Accordion.md +2 -0
  23. package/docs/widget/AnimatedBox.md +46 -20
  24. package/docs/widget/AnimatedText.md +13 -7
  25. package/docs/widget/Badge.md +1 -0
  26. package/docs/widget/COMMON_PROPS.md +26 -3
  27. package/docs/widget/Chart.md +1 -0
  28. package/docs/widget/Checkbox.md +7 -1
  29. package/docs/widget/CircularChart.md +2 -0
  30. package/docs/widget/Drawer.md +1 -0
  31. package/docs/widget/GridView.md +4 -1
  32. package/docs/widget/Input.md +2 -0
  33. package/docs/widget/InstallButton.md +1 -0
  34. package/docs/widget/Markdown.md +4 -0
  35. package/docs/widget/Rating.md +2 -0
  36. package/docs/widget/Slider.md +2 -0
  37. package/docs/widget/Stack.md +6 -5
  38. package/docs/widget/Stepper.md +2 -0
  39. package/docs/widget/Tooltip.md +3 -1
  40. package/package.json +3 -2
  41. package/skills/fletbox-design/SKILL.md +390 -14
  42. package/skills/fletbox-motion/SKILL.md +145 -47
  43. package/skills/fletbox-rules/SKILL.md +140 -1
  44. package/src/animations/AnimatedBox.js +321 -226
  45. package/src/animations/MatrixRain.js +7 -1
  46. package/src/animations/ParallaxBox.js +13 -10
  47. package/src/animations/animate.js +269 -106
  48. package/src/animations/color.js +91 -0
  49. package/src/animations/easings.js +275 -0
  50. package/src/animations/index.js +21 -0
  51. package/src/animations/motion.js +204 -0
  52. package/src/animations/transitions.js +327 -0
  53. package/src/core/runApp.js +0 -6
  54. package/src/helpers/boxEdges.js +69 -0
  55. package/src/helpers/html.js +39 -0
  56. package/src/index.d.ts +377 -25
  57. package/src/index.js +77 -0
  58. package/src/navigations/AdaptiveScaffold.js +6 -2
  59. package/src/navigations/AppBar.js +16 -7
  60. package/src/navigations/BottomNavigation.js +24 -1
  61. package/src/navigations/CollapsibleSideBar.js +0 -1
  62. package/src/navigations/Drawer.js +32 -1
  63. package/src/navigations/DrawerItem.js +1 -1
  64. package/src/navigations/Router.js +0 -1
  65. package/src/navigations/Scaffold.js +9 -3
  66. package/src/navigations/Tabs.js +104 -31
  67. package/src/services/RamStore.js +1 -2
  68. package/src/tools/breakpoints.js +113 -0
  69. package/src/tools/createList.js +0 -2
  70. package/src/tools/fonts.js +99 -0
  71. package/src/tools/index.js +74 -1
  72. package/src/tools/margin.js +5 -63
  73. package/src/tools/padding.js +5 -63
  74. package/src/tools/print.js +0 -8
  75. package/src/tools/scale.js +9 -2
  76. package/src/tools/time.js +56 -15
  77. package/src/tools/useState.js +1 -54
  78. package/src/utils/fieldFont.js +77 -0
  79. package/src/utils/focusRing.js +73 -0
  80. package/src/utils/focusScope.js +113 -0
  81. package/src/utils/markdownParser.js +7 -9
  82. package/src/utils/readableInk.js +7 -1
  83. package/src/utils/stackLayout.js +55 -0
  84. package/src/utils/styleInjector.js +25 -0
  85. package/src/utils/syntaxHighlight.js +1 -17
  86. package/src/utils/themes.js +15 -0
  87. package/src/utils/visualEffects.js +49 -47
  88. package/src/widget-factory/createWidget.js +16 -3
  89. package/src/widget-factory/effects.js +76 -3
  90. package/src/widget-factory/reactiveValue.js +0 -7
  91. package/src/widget-factory/tools.js +26 -143
  92. package/src/widget-factory/translateProps.js +5 -0
  93. package/src/widget-factory/widgetFactory.js +1 -1
  94. package/src/widgets/Accordion.js +70 -12
  95. package/src/widgets/AdMob.js +1 -1
  96. package/src/widgets/AdSense.js +1 -1
  97. package/src/widgets/AlertDialog.js +11 -9
  98. package/src/widgets/Avatar.js +10 -1
  99. package/src/widgets/Badge.js +7 -1
  100. package/src/widgets/BottomSheet.js +39 -5
  101. package/src/widgets/Chart.js +41 -9
  102. package/src/widgets/Checkbox.js +59 -9
  103. package/src/widgets/Chip.js +13 -1
  104. package/src/widgets/CircularBar.js +17 -5
  105. package/src/widgets/CircularChart.js +24 -2
  106. package/src/widgets/CodeViewer.js +5 -3
  107. package/src/widgets/Dropdown.js +223 -20
  108. package/src/widgets/GridView.js +17 -1
  109. package/src/widgets/Icon.js +21 -0
  110. package/src/widgets/Input.js +75 -44
  111. package/src/widgets/InstallButton.js +8 -2
  112. package/src/widgets/ListTile.js +6 -5
  113. package/src/widgets/ListView.js +21 -7
  114. package/src/widgets/Markdown.js +25 -44
  115. package/src/widgets/Modal.js +43 -6
  116. package/src/widgets/Pagination.js +35 -3
  117. package/src/widgets/ProgressBar.js +18 -0
  118. package/src/widgets/Radio.js +7 -0
  119. package/src/widgets/Rating.js +104 -16
  120. package/src/widgets/Skeleton.js +9 -5
  121. package/src/widgets/Slider.js +87 -7
  122. package/src/widgets/SnackBar.js +30 -1
  123. package/src/widgets/Stack.js +25 -13
  124. package/src/widgets/Stepper.js +112 -64
  125. package/src/widgets/Switch.js +10 -1
  126. package/src/widgets/Tooltip.js +93 -10
  127. package/src/widgets/TreeView.js +5 -5
  128. package/src/create-index.sh +0 -237
  129. package/src/widget-factory/pixTool.js +0 -40
  130. package/src/widget-factory/stringTool.js +0 -26
@@ -709,54 +709,6 @@ echo "Press Ctrl+C to stop"
709
709
  python3 -m http.server $PORT --bind $BIND
710
710
  `;
711
711
 
712
- export const createBundleSh = (target = "www") => `#!/bin/bash
713
- # createBundle.sh — Bundle UNIFICADO a \${TARGET}/ (un solo app.js + PWA)
714
- # Uso: bash createBundle.sh [target]
715
- set -euo pipefail
716
- cd "\$(dirname "\$0")"
717
- TARGET="\${1:-${target}}"
718
-
719
- if ! npx --no-install esbuild --version >/dev/null 2>&1; then
720
- echo "📦 Instalando esbuild (devDependency)…"
721
- npm install --save-dev esbuild >/dev/null
722
- fi
723
-
724
- rm -rf "\$TARGET"
725
- mkdir -p "\$TARGET/src" "\$TARGET/assets"
726
-
727
- # Root static files (PWA + preview)
728
- for f in index.html manifest.json run.sh service-worker.js; do
729
- [ -f "\$f" ] && cp "\$f" "\$TARGET/"
730
- done
731
- [ -f "\$TARGET/run.sh" ] && chmod +x "\$TARGET/run.sh"
732
-
733
- # Assets: iconos PWA + fuentes de iconos
734
- if [ -d "src/assets" ]; then
735
- cp -R src/assets/. "\$TARGET/src/assets/"
736
- cp -R src/assets/. "\$TARGET/assets/"
737
- fi
738
-
739
- # CSS global (lo carga index.html)
740
- if [ -f "src/styles/global.css" ]; then
741
- mkdir -p "\$TARGET/src/styles"
742
- cp src/styles/global.css "\$TARGET/src/styles/"
743
- fi
744
-
745
- # BUNDLE UNIFICADO: flet-box + widgets en UN solo app.js
746
- echo "📦 Bundling (esbuild + tree-shaking)…"
747
- npx esbuild src/app.js \\
748
- --bundle --outfile="\$TARGET/src/app.js" --format=esm --minify \\
749
- --tree-shaking=true --target=es2020 --define:FLETBOX_DEV=false \\
750
- --external:*.css --external:*.woff2 --resolve-extensions=.js,.json
751
-
752
- # App source code (required by the "View code" button)
753
- for d in modules screens database components; do
754
- [ -d "src/\$d" ] && cp -R "src/\$d" "\$TARGET/src/"
755
- done
756
-
757
- echo "✅ Bundle listo: \$TARGET/ Prueba: cd \$TARGET && bash run.sh"
758
- `;
759
-
760
712
  export const serviceWorkerJs = () => `// service-worker.js - PWA offline support
761
713
  const CACHE_NAME = 'fletbox-v2';
762
714
  // Deployment prefix: a worker's own URL always sits at the app root, so this
@@ -879,6 +831,28 @@ self.addEventListener('fetch', event => {
879
831
  });
880
832
  `;
881
833
 
834
+ // Not TypeScript: an editor reads this file and runs its bundled JS language
835
+ // service over `flet-box`'s own .d.ts, so a prop autocompletes and a prop that
836
+ // does not exist is a squiggle. `bundler` is the resolution an unbundled-ESM
837
+ // app needs, and TypeScript 7 removed `node10` outright — an older editor that
838
+ // still only knows `node` ignores the whole jsconfig, which is no worse than
839
+ // not having one. Nothing here is compiled or installed.
840
+ export const jsconfig = () => `{
841
+ "compilerOptions": {
842
+ "target": "ES2020",
843
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
844
+ "module": "ESNext",
845
+ "moduleResolution": "bundler",
846
+ "allowJs": true,
847
+ "checkJs": true,
848
+ "strict": false,
849
+ "noEmit": true,
850
+ "skipLibCheck": true
851
+ },
852
+ "include": ["src/**/*.js"],
853
+ "exclude": ["node_modules", "www", "dist"]
854
+ }`;
855
+
882
856
  export const packageJson = (name) => `{
883
857
  "name": ${JSON.stringify(name)},
884
858
  "version": "1.0.0",
@@ -1663,6 +1637,15 @@ npm install
1663
1637
  npm run dev
1664
1638
  \`\`\`
1665
1639
 
1640
+ ## Autocomplete without TypeScript
1641
+
1642
+ \`jsconfig.json\` points the editor's own JavaScript language service at
1643
+ \`flet-box\`'s bundled \`.d.ts\` — nothing to install, nothing to compile. Every
1644
+ widget, prop, colour token and helper autocompletes in VS Code (or any editor
1645
+ that reads jsconfig), and a prop that does not exist is underlined as an error
1646
+ instead of failing silently at runtime. \`src/\` is the only folder checked;
1647
+ \`npm run check\` remains the gate that runs in CI.
1648
+
1666
1649
  ## Working with an AI assistant
1667
1650
 
1668
1651
  This project ships an in-project agent contract, so an AI editing the app uses the
@@ -2066,7 +2049,7 @@ import { AppBar, Icon, Tooltip, colors, openDrawer, toggleTheme, getTheme } from
2066
2049
  // It reads the theme instead of subscribing to it: setTheme() re-renders the app,
2067
2050
  // so a component that subscribed would add a listener on every render and never
2068
2051
  // remove it.
2069
- export const AppBarComponent = ({ title, isMobile = false, showBack = false, onBack }) => {
2052
+ export const AppBarComponent = ({ title, isMobile = false, showBack = false, onBack = () => {} }) => {
2070
2053
  const isDark = getTheme() === 'dark';
2071
2054
 
2072
2055
  return AppBar({
@@ -2244,7 +2227,7 @@ runApp(App, 'root');
2244
2227
  `;
2245
2228
 
2246
2229
  export const basicAppJs = () => `// app.js - Basic template (AppBar + Drawer)
2247
- import { runApp, Scaffold, colors, useState, dimensions } from 'flet-box';
2230
+ import { runApp, Scaffold, colors, useState, atLeast, onBreakpointChange } from 'flet-box';
2248
2231
  // Registers this app's design tokens. Without it every screen paints
2249
2232
  // with the framework's own palette, and the file that looks like the design
2250
2233
  // system is decoration.
@@ -2264,13 +2247,14 @@ const routes = {
2264
2247
  };
2265
2248
 
2266
2249
  // The drawer is only reachable through the AppBar's menu button, so the shell has
2267
- // to know whether it is on a phone. Key-based state keeps the width across
2268
- // re-renders; the resize listener is registered once, at module scope.
2269
- const [viewportWidth, setViewportWidth] = useState('viewportWidth', dimensions.getWidth());
2270
- dimensions.addListener(({ width }) => setViewportWidth(width));
2250
+ // to know whether it is on a phone. Key-based state keeps the answer across
2251
+ // re-renders; the subscription is registered once, at module scope, and only
2252
+ // fires when the viewport crosses a breakpoint, not on every pixel of a resize.
2253
+ const [isWide, setIsWide] = useState('isWide', atLeast('md'));
2254
+ onBreakpointChange(() => setIsWide(atLeast('md')));
2271
2255
 
2272
2256
  const MyApp = () => {
2273
- const isMobile = viewportWidth.valueOf() < 768;
2257
+ const isMobile = !isWide.valueOf();
2274
2258
  return Scaffold({
2275
2259
  routes,
2276
2260
  appBar: AppBarComponent({ title: 'FletBox', isMobile }),
@@ -2284,7 +2268,7 @@ runApp(MyApp, 'root');
2284
2268
 
2285
2269
  export const fullAppJs =
2286
2270
  () => `// app.js - Full template (AppBar + Drawer + BottomNav)
2287
- import { runApp, Scaffold, colors, useState, dimensions } from 'flet-box';
2271
+ import { runApp, Scaffold, colors, useState, atLeast, onBreakpointChange } from 'flet-box';
2288
2272
  // Registers this app's design tokens. Without it every screen paints
2289
2273
  // with the framework's own palette, and the file that looks like the design
2290
2274
  // system is decoration.
@@ -2304,15 +2288,15 @@ const routes = {
2304
2288
  '/about': AboutScreen
2305
2289
  };
2306
2290
 
2307
- // State is key-based, so the viewport width survives re-renders. One resize
2291
+ // State is key-based, so the layout answer survives re-renders. One breakpoint
2308
2292
  // subscription feeds it from module scope: registering it inside the component
2309
- // would add a listener on every render.
2310
- const [viewportWidth, setViewportWidth] = useState('viewportWidth', dimensions.getWidth());
2311
- dimensions.addListener(({ width }) => setViewportWidth(width));
2293
+ // would add a listener on every render, and it only fires when a step is crossed.
2294
+ const [isWide, setIsWide] = useState('isWide', atLeast('md'));
2295
+ onBreakpointChange(() => setIsWide(atLeast('md')));
2312
2296
 
2313
2297
  const MyApp = () => {
2314
2298
  // Structural read of the key, so crossing the breakpoint re-renders the shell.
2315
- const isMobile = viewportWidth.valueOf() < 768;
2299
+ const isMobile = !isWide.valueOf();
2316
2300
  return Scaffold({
2317
2301
  routes,
2318
2302
  appBar: AppBarComponent({ title: 'FletBox', isMobile }),
@@ -2357,7 +2341,7 @@ runApp(MyApp, 'root');
2357
2341
 
2358
2342
  export const adaptiveAppJs =
2359
2343
  () => `// app.js - Adaptive template (mobile + desktop)
2360
- import { runApp, Scaffold, colors, useState, dimensions } from 'flet-box';
2344
+ import { runApp, Scaffold, colors, useState, atLeast, onBreakpointChange } from 'flet-box';
2361
2345
  // Registers this app's design tokens. Without it every screen paints
2362
2346
  // with the framework's own palette, and the file that looks like the design
2363
2347
  // system is decoration.
@@ -2380,11 +2364,11 @@ const routes = {
2380
2364
  '/profile': ProfileScreen
2381
2365
  };
2382
2366
 
2383
- // State is key-based, so the viewport width survives re-renders. The resize
2367
+ // State is key-based, so the layout answer survives re-renders. The breakpoint
2384
2368
  // subscription is registered once at module scope; doing it inside the component
2385
2369
  // would add another listener on every render.
2386
- const [viewportWidth, setViewportWidth] = useState('viewportWidth', dimensions.getWidth());
2387
- dimensions.addListener(({ width }) => setViewportWidth(width));
2370
+ const [isWide, setIsWide] = useState('isWide', atLeast('md'));
2371
+ onBreakpointChange(() => setIsWide(atLeast('md')));
2388
2372
 
2389
2373
  // One menu, three surfaces: the drawer and the bottom bar on a phone, the
2390
2374
  // sidebar on a desktop. The labels stay short because the bottom bar has to fit
@@ -2397,9 +2381,9 @@ const menu = [
2397
2381
  ];
2398
2382
 
2399
2383
  const MyApp = () => {
2400
- // Structural read of the keyed viewport state, so crossing the breakpoint
2384
+ // Structural read of the keyed layout state, so crossing the breakpoint
2401
2385
  // re-renders the shell (drawer + bottom bar <-> sidebar).
2402
- const isMobile = viewportWidth.valueOf() < 768;
2386
+ const isMobile = !isWide.valueOf();
2403
2387
  return Scaffold({
2404
2388
  routes,
2405
2389
  appBar: AppBarComponent({ title: 'FletBox', isMobile }),
@@ -2547,9 +2531,13 @@ export const agentsMd = (appName, template, version) =>
2547
2531
  'Real `Scaffold` props: `appBar body bottomBar fab drawer leftNavBar',
2548
2532
  'leftNavBarWidth rightNavBar rightNavBarWidth routes basePath backgroundColor',
2549
2533
  'closeDrawerOnNavigate`. (`--props Scaffold` is authoritative.)',
2550
- 'For a responsive app, branch in JS on the keyed viewport state — `useState`',
2551
- 'seeded from `dimensions.getWidth()` plus one `dimensions.addListener()` at',
2552
- 'module scope — not on CSS alone. See `src/app.js` in this project.',
2534
+ 'For a responsive app, branch in JS on the breakpoint table, not on CSS alone:',
2535
+ '`useState` seeded from `atLeast("md")` plus one `onBreakpointChange()` at',
2536
+ 'module scope — it fires when the viewport crosses a step, not on every pixel',
2537
+ 'of a resize. `md` is 768, the same line `AdaptiveScaffold` crosses, so a',
2538
+ 'screen and its shell cannot disagree. A number that really wants to be',
2539
+ 'different at 400px and 1400px comes from `pick({ xs: 16, md: 24 })`.',
2540
+ 'See `src/app.js` in this project.',
2553
2541
  '',
2554
2542
  'Custom widgets follow the same shape every framework widget uses',
2555
2543
  '(`QRCode.js` is the reference):',
@@ -2566,8 +2554,25 @@ export const agentsMd = (appName, template, version) =>
2566
2554
  ' idempotent `onUnmount` disposer. Never monkey-patch `_cleanup`.',
2567
2555
  '- State is key-based: `useState("key", 0)` plus subscribers. There is no',
2568
2556
  ' component re-render tree to reason about.',
2569
- '- Reuse the framework utilities (`colors`, `margin`, `padding`, `gradient',
2570
- ' shadow`, `rgba`, `stackPosition`) instead of hand-writing CSS.',
2557
+ '- The package exports 123 helpers next to the widgets and they are the short',
2558
+ ' form of the code you would otherwise write: `mapList` (an array, a number or',
2559
+ " an object), `createList`, `range`, `repeat`, `sort`, `unique`, `chunk`,",
2560
+ ' `shadow`, `gradient`, `rgba`, `transform`, `transition`, `filter` (a CSS',
2561
+ ' filter, not a list filter), `color(hex).darken(12).hex()`, `random.*`,',
2562
+ ' `shortId`, `uuid`, `formatDate`, `formatTimeAmPm`, `relativeTime`, `timeAgo`,',
2563
+ ' `addDays`, `subtractDays`, `dateDiff`, `dayOfWeek`, `isWeekend`, `today`,',
2564
+ ' `seconds`, `days`, `parseDate`, `timer`, `every`, `everySecond`, `waitUntil`,',
2565
+ ' `split`, `join`, `trim`, `startsWith`, `endsWith`, `includes`,',
2566
+ ' `delay`, `withMinDelay`,',
2567
+ ' `retry`, `memo`, `dict`, `print`, `ref`, `atLeast`, `pick`,',
2568
+ ' `onBreakpointChange`, `loadFont`, `clipboard`. Hand-write the long version',
2569
+ ' only when it is genuinely shorter or clearer. `npm run check -- --list`',
2570
+ ' prints the set for the version actually installed.',
2571
+ ' The trap: a routed prop already IS the short form. `padding: 16` reaches the',
2572
+ ' rem contract; `padding(16)` returns a pixel string. Use the helpers for a',
2573
+ ' CSS value a prop cannot express, never instead of a number.',
2574
+ ' Second trap: `months(1)` is 30 days and `years(1)` is 365, and `addDays`',
2575
+ ' counts calendar days while `addHours` counts real elapsed ones.',
2571
2576
  '',
2572
2577
  'One rendering engine exists: `src/widget-factory/`. Never add a second one, and',
2573
2578
  'never style by reaching around the factory. **If you believe the engine is',
@@ -2625,7 +2630,7 @@ export const agentsMd = (appName, template, version) =>
2625
2630
  ' 40 · the smallest tap target, a roomy control',
2626
2631
  ' 48 · hero spacing',
2627
2632
  ' 64 / 96 · a full-bleed section, a landing hero',
2628
- 'radius 4 / 8 / 12 / 16 / 999 (pill)',
2633
+ 'radius 4 / 8 / 12 / 16 / 20 / 24 / 999 (pill)',
2629
2634
  'type 12 caption · 14 body-small · 16 body · 18 subtitle · 24 title',
2630
2635
  ' 32 headline · 40 / 48 display (a hero, a big number)',
2631
2636
  'weight 400 body · 500 label · 600 title · 700 headline',
package/docs/README.md CHANGED
@@ -17,6 +17,7 @@ This documentation is one continuous book. Start at **Chapter 0** and follow the
17
17
  | 10 | [Tools & Utilities](tools/README.md) | The helper functions that ship with the framework | Intermediate |
18
18
  | 11 | [The CLI](cli/README.md) | Create, run, and build projects with `flet-box` | Intermediate |
19
19
  | 12 | [Clean UI](guides/clean-ui.md) | The four skills every app ships with, the design system they hold, and the gate that enforces it | Intermediate |
20
+ | 13 | [Accessibility](guides/accessibility.md) | The focus ring, the keyboard pass over every press, the roles the widgets declare, and the overlays that hold the keyboard | Intermediate |
20
21
  | A | [Appendices](guides/minification-and-protection.md) | Minification, mobile, compatibility, embedding, online updates (OTA) | Advanced |
21
22
 
22
23
  > **Levels.** *Beginner:* start here and read in order. *Intermediate:* continue in order — you are comfortable with widgets. *Advanced:* for when you ship, skip on a first read.
@@ -54,6 +55,7 @@ Front cover (this page)
54
55
  → Utilities guide · guides/utilities.md
55
56
  → The CLI · cli/README.md
56
57
  → Clean UI · guides/clean-ui.md
58
+ → Accessibility · guides/accessibility.md
57
59
  → Appendices (minification → mobile → compatibility → embedding → online updates)
58
60
  → The end of the book
59
61
  ```
@@ -275,6 +275,7 @@ A typical generated project looks like this:
275
275
  ```text
276
276
  my-app/
277
277
  ├── index.html
278
+ ├── jsconfig.json
278
279
  ├── package.json
279
280
  ├── capacitor.config.json
280
281
  ├── manifest.json
@@ -301,7 +302,23 @@ my-app/
301
302
 
302
303
  The exact folders depend on the selected template.
303
304
 
304
- ## A complete beginner workflow
305
+ ### Autocomplete without TypeScript
306
+
307
+ `jsconfig.json` is not a build step: it tells the editor's own JavaScript
308
+ language service to read `flet-box`'s bundled `src/index.d.ts`. With `checkJs`
309
+ on, VS Code, Cursor and any editor that understands `jsconfig.json` autocomplete
310
+ every widget, prop, colour token and helper — and underline a prop that does not
311
+ exist, which is the same error `npm run check` reports from the command line.
312
+
313
+ Nothing is installed and nothing is compiled. The framework keeps the promise
314
+ honest with `scripts/check-app-types.mjs`, which creates all five templates and
315
+ requires zero diagnostics over their `src/` — a starter that produces red
316
+ squiggles fails `npm test`.
317
+
318
+ ```bash
319
+ npm run check # the gate: real API, real props, real imports
320
+ ```
321
+
305
322
 
306
323
  ```bash
307
324
  # 1. Create the project
@@ -11,8 +11,11 @@ This directory is the **conceptual index** of FletBox: how to build apps, set up
11
11
  - **[FletBox utilities](utilities.md)** — Chapter 10 · layout/theme/format helpers and validator tools.
12
12
  - **[The CLI](../cli/README.md)** — Chapter 11 · create, run, and build projects with `flet-box`.
13
13
  - **[Clean UI](clean-ui.md)** — Chapter 12 · the four skills `flet-box create`
14
- installs, the token scale, the 25 rules, and the `flet-box check-ui` gate that
15
- enforces them.
14
+ installs, the token scale, the numbered rules (00–24 enforced, 25–37 the
15
+ visual atlas), and the `flet-box check-ui` gate that enforces them.
16
+ - **[Accessibility](accessibility.md)** — Chapter 13 · the focus ring, the
17
+ keyboard pass over every press, the roles and states the widgets declare, and
18
+ the overlays that hold the keyboard.
16
19
  - **[Widget structure](widget-structure.md)** — the canonical, modular shape every widget follows (and how new ones are added).
17
20
 
18
21
  ## Appendices (Advanced — for shipping)
@@ -0,0 +1,282 @@
1
+ # Accessibility: the engine's half and your screen's half
2
+
3
+ A FletBox control is a `div` that the framework teaches to behave like a
4
+ `button`. That is what makes the syntax pleasant — `Container({ onPress })`
5
+ instead of a stylesheet and three event listeners — and it is also where
6
+ accessibility is won or lost. Three things have to be true for someone with no
7
+ pointer: the control must be **reachable** (Tab), **operable** (Enter and
8
+ Space), **announced** (a role and a name), and the person must be able to see
9
+ **where they are** (a focus ring). An overlay owes a fourth: hold the keyboard
10
+ while it is open, and give it back when it closes.
11
+
12
+ The engine already does all four. This chapter is about the half it cannot do
13
+ for you, and about the two ways an app destroys the half it was given.
14
+
15
+ ## 1. The focus ring is a stylesheet, not a paint
16
+
17
+ A focus ring is not a decoration — it is a *state*, and a state can only live in
18
+ a stylesheet. `src/utils/focusRing.js` appends one `<style id="fletbox-focus-ring">`
19
+ to `<head>` the first time any widget is created, and never appends a second:
20
+
21
+ ```css
22
+ :where(a[href], button, input, select, textarea, summary, [tabindex]:not([tabindex="-1"])):focus-visible {
23
+ outline: 2px solid var(--color-focusRing, #1d4ed8);
24
+ outline-offset: 2px;
25
+ }
26
+ ```
27
+
28
+ Three deliberate choices, each worth knowing when you want something different:
29
+
30
+ - **`:focus-visible`, not `:focus`.** A click with a mouse does not ring; the
31
+ keyboard does. That is the browser's own distinction, not the framework's.
32
+ - **`:where(…)` keeps specificity at zero.** One class of yours overrides it,
33
+ and no `!important` is needed anywhere.
34
+ - **The color is a token.** `--color-focusRing` is written by `applyPalette` for
35
+ every theme, so the ring moves with the theme instead of being a literal that
36
+ only suits one background. The light palette's `#1d4ed8` is 6.4:1 on the page,
37
+ the dark one's `#93c5fd` is 11:1, and the literal after the comma in the rule
38
+ is the boot value a page shows before the first `setTheme`.
39
+
40
+ The widths are pixels on purpose, next to `SHADOW_LEVELS` and the hairline
41
+ `borderWidth`: a 1px ring is invisible and a 6px one is a blob, whatever the
42
+ reader's font size is.
43
+
44
+ ### The one thing an app must never do
45
+
46
+ ```javascript
47
+ // Never. An inline declaration outranks every stylesheet rule, including the
48
+ // engine's, so this is what "the focus ring does not work" looks like in code.
49
+ element.style.outline = "none";
50
+
51
+ // Never, for the same reason, anywhere in a global stylesheet:
52
+ // *:focus { outline: none; }
53
+ ```
54
+
55
+ If you do not like the ring's colour, change the token — or override the rule
56
+ with one class of your own, which `:where()` makes possible:
57
+
58
+ ```javascript
59
+ import { registerPalette } from "flet-box";
60
+
61
+ // The token is part of the palette, so both themes move it and nothing is
62
+ // overridden behind the framework's back. A registered palette repaints only
63
+ // when it is the one on screen.
64
+ registerPalette("light", { focusRing: "#0f766e" });
65
+ registerPalette("dark", { focusRing: "#5eead4" });
66
+ ```
67
+
68
+ ```css
69
+ /* or, for one component only: a class beats :where(...) whatever the order */
70
+ .chart:focus-visible { outline-color: var(--color-primary); }
71
+ ```
72
+
73
+ ### A field rings as a whole control
74
+
75
+ `Input` is a wrapper with a border, an icon and sometimes a reveal button, and
76
+ the native `<input>` inside it is only part of the control. A ring around the
77
+ text box alone draws a box *inside* the thing it is supposed to mark. So the
78
+ rule has a second pair:
79
+
80
+ ```css
81
+ :where([data-fb-field]):has(> input:focus-visible) {
82
+ outline: 2px solid var(--color-focusRing, #1d4ed8);
83
+ outline-offset: 2px;
84
+ }
85
+ :where([data-fb-field]) > input:focus-visible {
86
+ outline: none;
87
+ }
88
+ ```
89
+
90
+ The `outline: none` there is a *rule*, at zero specificity, which any app can
91
+ still override — that is the whole difference between silencing the inner box
92
+ and killing the ring. The marker (`data-fb-field`) is written by `Input`; if you
93
+ build your own composite field (a text box with a button inside), put the same
94
+ marker on the wrapper and the ring lands where the control is.
95
+
96
+ And keep the two jobs apart: **the border is validation, the ring is focus.**
97
+ A field that paints focus by recolouring its rim, or by adding a `box-shadow`,
98
+ puts two indicators where one belongs and loses the validation state the moment
99
+ someone types.
100
+
101
+ ## 2. A press is a press, however it arrives
102
+
103
+ Give any widget a click handler as a prop and the engine runs a keyboard pass
104
+ over it — `src/widget-factory/effects.js`:
105
+
106
+ - it sets `role="button"` **if you have not set a role**;
107
+ - it sets `tabIndex = 0` **if you have not set one** (an explicit `-1` is your
108
+ decision to keep the element off the Tab path, and it is respected);
109
+ - Enter and Space fire the same handler a pointer fires, with Space's default
110
+ (scrolling the page) cancelled and a held key counted as one press;
111
+ - a nested control keeps its own keys — Enter on a `Button` inside a pressable
112
+ `Card` presses the button and the card, not twice the card;
113
+ - native `button`/`a[href]` are left alone, and disabled controls are not made
114
+ Tab stops.
115
+
116
+ ```javascript
117
+ Container({ onPress: openDetails, children: [Text({ text: "Details" })] });
118
+ // role="button", tabIndex=0, Enter and Space → openDetails
119
+ ```
120
+
121
+ The one way an app defeats this is to attach the handler to the element after
122
+ the widget was built. The pass runs during construction; a handler assigned
123
+ afterwards leaves a control with a click, no role, no Tab stop and no keyboard:
124
+
125
+ ```javascript
126
+ const card = Container({ children: [Text({ text: "Details" })] });
127
+ card.onclick = openDetails; // mouse-only: the engine already ran
128
+ ```
129
+
130
+ Express the control with a prop instead — `onPress` (or `onClick`) on any
131
+ widget — and the engine sees it.
132
+
133
+ ### An icon is decoration until it is a control
134
+
135
+ `Icon` has two lives, and it picks one by itself:
136
+
137
+ - `Icon({ name: "search" })` — no name for a reader, so it is marked
138
+ `aria-hidden="true"` and kept off the Tab path. Correct inside a `Button`
139
+ that already has text.
140
+ - `Icon({ name: "search", onPress: focusSearch, "aria-label": "Search" })` — a
141
+ named, pressable icon keeps `role="button"` and is read as "Search, button".
142
+
143
+ An icon-only control with no `aria-label` is read as "button", which is the
144
+ worst of both worlds: reachable, operable, and anonymous. The places where the
145
+ framework wires a glyph up for you already name it — the `AppBar` back arrow,
146
+ `Input`'s `onIconPress` and reveal button, and the menu icon a `Scaffold`
147
+ connects to its drawer.
148
+
149
+ ## 3. Names, roles and states the widgets already declare
150
+
151
+ You can pass `role` and `tabIndex` as ordinary props, and any `aria-*`
152
+ attribute reaches the element verbatim (`"aria-label": "…"`). Everything below
153
+ is already in place; the table exists so you know what not to duplicate.
154
+
155
+ | Widget | Role | States it keeps in sync |
156
+ | --- | --- | --- |
157
+ | `Button`, `Container` with `onPress` | `button` | — (engine pass) |
158
+ | `Switch` | `switch` | `aria-checked` |
159
+ | `Checkbox` | `checkbox` | `aria-checked` |
160
+ | `Radio` / `Rating` | `radio` / `radiogroup` | `aria-checked` |
161
+ | `Slider` | `slider` | `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-orientation` |
162
+ | `ProgressBar`, `CircularBar` | `progressbar` | `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-busy` while indeterminate |
163
+ | `Dropdown` | `combobox` | `aria-expanded`, `aria-controls`, `aria-activedescendant`, `aria-invalid`, `aria-labelledby` |
164
+ | `Dropdown` list | `listbox` / `option` | `aria-selected` |
165
+ | `Tabs` | `tablist` / `tab` / `tabpanel` | `aria-selected`, roving `tabIndex`, `aria-controls` |
166
+ | `Accordion` | `button` / `region` | `aria-expanded`, `aria-controls` |
167
+ | `Pagination` | `navigation` | `aria-current="page"` |
168
+ | `Stepper` | — | `aria-current="step"` |
169
+ | `BottomNavigation` | — | `aria-current="page"` |
170
+ | `Modal`, `BottomSheet`, `Drawer` | `dialog` | `aria-modal="true"` |
171
+ | `SnackBar` | live region | `aria-live="polite"`, `"assertive"` for error and warning |
172
+ | `Tooltip` | `tooltip` | `aria-describedby` on the trigger while shown |
173
+ | `Maps` | `application` | — |
174
+ | `CircularChart` | `img` | give it an `aria-label`; it is a picture of data |
175
+
176
+ Composite widgets answer the keyboard the way their ARIA pattern says: `Tabs`,
177
+ `Dropdown`, `Rating` and `Slider` move with the arrow keys, take `Home` and
178
+ `End`, and keep a single Tab stop per group. The Dropdown's options are
179
+ deliberately *not* Tab stops — the selector holds focus, the arrows move the
180
+ highlight and `aria-activedescendant` names the row.
181
+
182
+ What is left for you is **naming**: a visible `label` where the widget has one
183
+ (`Input`, `Dropdown`), `text` for a button (`"Save"` is a name; `"OK"` is not —
184
+ see the clean-UI rule about generic action labels), `alt` on images, and
185
+ `aria-label` for anything that has no visible words of its own.
186
+
187
+ ## 4. An invalid field says so in two places
188
+
189
+ A red rim is the visual half. The other half is the state and the reason, and
190
+ `Input` keeps them together: while a message is showing, the field carries
191
+ `aria-invalid="true"` and an `aria-describedby` pointing at the message, which
192
+ is a `polite` live region; when the value becomes valid, all three leave
193
+ together. A described-by pointing at a `display: none` node would be a control
194
+ claiming a description it does not have.
195
+
196
+ ```javascript
197
+ const email = Input({ label: "Email", validation: "email", required: true });
198
+
199
+ // submit
200
+ const { valid, message } = email.validate(); // paints, links and announces
201
+ ```
202
+
203
+ Use `validate()` (or `setValue()`, which validates) rather than colouring a rim
204
+ by hand: `showValidationMessage` decides whether the reason is on screen, and a
205
+ ring with no message tells a reader "wrong" and nothing else.
206
+
207
+ ## 5. Overlays hold the keyboard, then give it back
208
+
209
+ `Modal`, `BottomSheet` and `Drawer` share one scope (`src/utils/focusScope.js`),
210
+ and one implementation on purpose:
211
+
212
+ - focus moves into the panel when it opens — the first control, or the panel
213
+ itself (`tabIndex="-1"`) when it has none, so an alert with two lines of text
214
+ still takes the keyboard;
215
+ - Tab and Shift+Tab cycle **inside** the panel instead of walking out behind
216
+ the scrim into a page the person can no longer see;
217
+ - Escape closes it (each overlay already listens);
218
+ - closing hands focus back to whatever was focused before, and skips that if the
219
+ element has left the page (a route change replaced it).
220
+
221
+ The trap and the ring read the same list of reachable elements — `a[href]`,
222
+ `button`, `input`, `select`, `textarea`, `summary` and anything on the Tab path
223
+ — so a stop you can Tab to is always a stop that shows a ring.
224
+
225
+ For an app, the contract is simply: open and close these through their props and
226
+ refs, and do not build a full-screen scrim by hand with its own Escape listener.
227
+
228
+ ## 6. Verify it with the keyboard
229
+
230
+ Five minutes, no screen reader needed:
231
+
232
+ 1. Press Tab from the top of the screen. Every control gets a ring; nothing is
233
+ skipped and nothing rings twice.
234
+ 2. Press Enter and Space on each control. The same thing happens as with a click.
235
+ 3. Focus a field and type. The rim stays put until there is a verdict.
236
+ 4. Submit an invalid form. The message is under the field, the field says
237
+ `aria-invalid`, and the message disappears with the fix.
238
+ 5. Open a modal. Tab to the last control and press Tab once more: focus is back
239
+ at the first, not behind the scrim.
240
+ 6. Press Escape. Focus is back on whatever opened it.
241
+ 7. Repeat 1–6 with a screen reader (VoiceOver: `Cmd+F5`; TalkBack on Android) and
242
+ listen for names: "Search, button", "Email, invalid entry, Invalid email
243
+ address".
244
+
245
+ Then let the gates do what they do:
246
+
247
+ ```bash
248
+ npm run check # the API is real: widgets, props, imports
249
+ npm run ui # names, labels, tap targets, contrast:
250
+ # UNLABELED_FIELD, UNLABELED_TOGGLE, ICON_ONLY_CONTROL,
251
+ # IMAGE_NO_ALT, SMALL_TAP_TARGET, LOW_CONTRAST*, UNREADABLE
252
+ npm run dev # then Tab through it; the checker has no keyboard
253
+ ```
254
+
255
+ In the framework repository itself, `node scripts/check-a11y.mjs` runs inside
256
+ `npm test` and holds the invariants of this chapter: nothing writes an inline
257
+ `outline`; the ring rule is injected and carries the token; both palettes define
258
+ `focusRing`; the pressable path sets role, Tab stop and Enter/Space and releases
259
+ its listener; every overlay that reads Escape also traps focus; the trap and the
260
+ ring agree on what is reachable; and no handler is attached after the factory
261
+ returned. Every one of those rules has been proven to fail on the bug it
262
+ forbids. The runtime half lives in `/tests/a11y.html`.
263
+
264
+ ## See also
265
+
266
+ - [Clean UI](clean-ui.md) — the design rules this chapter's checklist comes from.
267
+ - [Themes](../tools/theme.md) — `registerPalette`, the call that moves the ring.
268
+ - [Input](../widget/Input.md) — the props used above.
269
+ - [Modal](../widget/Modal.md) · [BottomSheet](../widget/BottomSheet.md) ·
270
+ [Drawer](../widget/Drawer.md) — the three overlays and what they return.
271
+ - [Widget structure](widget-structure.md) — where a widget's defaults and
272
+ disposers live.
273
+
274
+ ---
275
+
276
+ ## Continue reading
277
+
278
+ - **Previous:** [Clean UI](clean-ui.md) — Chapter 12
279
+ - **Next:** [Minify and protect a FletBox build](minification-and-protection.md) — Appendix A
280
+ - **Index:** [Guides index](README.md) · [The FletBox Book](../README.md)
281
+
282
+ You are reading **Chapter 13 · Accessibility**.