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.
- package/bin/commands/checkUi.js +276 -45
- package/bin/commands/create.js +4 -0
- package/bin/commands/createScreen.js +1 -1
- package/bin/commands/runBundle.js +1 -1
- package/bin/utils/colors.js +0 -8
- package/bin/utils/helpers.js +0 -5
- package/bin/utils/images/jpeg.js +2 -3
- package/bin/utils/templates.js +78 -73
- package/docs/README.md +2 -0
- package/docs/cli/README.md +18 -1
- package/docs/guides/README.md +5 -2
- package/docs/guides/accessibility.md +282 -0
- package/docs/guides/clean-ui.md +53 -8
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/mobile-and-platforms.md +51 -0
- package/docs/guides/utilities.md +116 -4
- package/docs/guides/widget-structure.md +30 -0
- package/docs/tools/README.md +1 -1
- package/docs/tools/animation.md +134 -11
- package/docs/tools/device.md +75 -3
- package/docs/tools/text-and-time.md +116 -6
- package/docs/widget/Accordion.md +2 -0
- package/docs/widget/AnimatedBox.md +46 -20
- package/docs/widget/AnimatedText.md +13 -7
- package/docs/widget/Badge.md +1 -0
- package/docs/widget/COMMON_PROPS.md +26 -3
- package/docs/widget/Chart.md +1 -0
- package/docs/widget/Checkbox.md +7 -1
- package/docs/widget/CircularChart.md +2 -0
- package/docs/widget/Drawer.md +1 -0
- package/docs/widget/GridView.md +4 -1
- package/docs/widget/Input.md +2 -0
- package/docs/widget/InstallButton.md +1 -0
- package/docs/widget/Markdown.md +4 -0
- package/docs/widget/Rating.md +2 -0
- package/docs/widget/Slider.md +2 -0
- package/docs/widget/Stack.md +6 -5
- package/docs/widget/Stepper.md +2 -0
- package/docs/widget/Tooltip.md +3 -1
- package/package.json +3 -2
- package/skills/fletbox-design/SKILL.md +390 -14
- package/skills/fletbox-motion/SKILL.md +145 -47
- package/skills/fletbox-rules/SKILL.md +140 -1
- package/src/animations/AnimatedBox.js +321 -226
- package/src/animations/MatrixRain.js +7 -1
- package/src/animations/ParallaxBox.js +13 -10
- package/src/animations/animate.js +269 -106
- package/src/animations/color.js +91 -0
- package/src/animations/easings.js +275 -0
- package/src/animations/index.js +21 -0
- package/src/animations/motion.js +204 -0
- package/src/animations/transitions.js +327 -0
- package/src/core/runApp.js +0 -6
- package/src/helpers/boxEdges.js +69 -0
- package/src/helpers/html.js +39 -0
- package/src/index.d.ts +377 -25
- package/src/index.js +77 -0
- package/src/navigations/AdaptiveScaffold.js +6 -2
- package/src/navigations/AppBar.js +16 -7
- package/src/navigations/BottomNavigation.js +24 -1
- package/src/navigations/CollapsibleSideBar.js +0 -1
- package/src/navigations/Drawer.js +32 -1
- package/src/navigations/DrawerItem.js +1 -1
- package/src/navigations/Router.js +0 -1
- package/src/navigations/Scaffold.js +9 -3
- package/src/navigations/Tabs.js +104 -31
- package/src/services/RamStore.js +1 -2
- package/src/tools/breakpoints.js +113 -0
- package/src/tools/createList.js +0 -2
- package/src/tools/fonts.js +99 -0
- package/src/tools/index.js +74 -1
- package/src/tools/margin.js +5 -63
- package/src/tools/padding.js +5 -63
- package/src/tools/print.js +0 -8
- package/src/tools/scale.js +9 -2
- package/src/tools/time.js +56 -15
- package/src/tools/useState.js +1 -54
- package/src/utils/fieldFont.js +77 -0
- package/src/utils/focusRing.js +73 -0
- package/src/utils/focusScope.js +113 -0
- package/src/utils/markdownParser.js +7 -9
- package/src/utils/readableInk.js +7 -1
- package/src/utils/stackLayout.js +55 -0
- package/src/utils/styleInjector.js +25 -0
- package/src/utils/syntaxHighlight.js +1 -17
- package/src/utils/themes.js +15 -0
- package/src/utils/visualEffects.js +49 -47
- package/src/widget-factory/createWidget.js +16 -3
- package/src/widget-factory/effects.js +76 -3
- package/src/widget-factory/reactiveValue.js +0 -7
- package/src/widget-factory/tools.js +26 -143
- package/src/widget-factory/translateProps.js +5 -0
- package/src/widget-factory/widgetFactory.js +1 -1
- package/src/widgets/Accordion.js +70 -12
- package/src/widgets/AdMob.js +1 -1
- package/src/widgets/AdSense.js +1 -1
- package/src/widgets/AlertDialog.js +11 -9
- package/src/widgets/Avatar.js +10 -1
- package/src/widgets/Badge.js +7 -1
- package/src/widgets/BottomSheet.js +39 -5
- package/src/widgets/Chart.js +41 -9
- package/src/widgets/Checkbox.js +59 -9
- package/src/widgets/Chip.js +13 -1
- package/src/widgets/CircularBar.js +17 -5
- package/src/widgets/CircularChart.js +24 -2
- package/src/widgets/CodeViewer.js +5 -3
- package/src/widgets/Dropdown.js +223 -20
- package/src/widgets/GridView.js +17 -1
- package/src/widgets/Icon.js +21 -0
- package/src/widgets/Input.js +75 -44
- package/src/widgets/InstallButton.js +8 -2
- package/src/widgets/ListTile.js +6 -5
- package/src/widgets/ListView.js +21 -7
- package/src/widgets/Markdown.js +25 -44
- package/src/widgets/Modal.js +43 -6
- package/src/widgets/Pagination.js +35 -3
- package/src/widgets/ProgressBar.js +18 -0
- package/src/widgets/Radio.js +7 -0
- package/src/widgets/Rating.js +104 -16
- package/src/widgets/Skeleton.js +9 -5
- package/src/widgets/Slider.js +87 -7
- package/src/widgets/SnackBar.js +30 -1
- package/src/widgets/Stack.js +25 -13
- package/src/widgets/Stepper.js +112 -64
- package/src/widgets/Switch.js +10 -1
- package/src/widgets/Tooltip.js +93 -10
- package/src/widgets/TreeView.js +5 -5
- package/src/create-index.sh +0 -237
- package/src/widget-factory/pixTool.js +0 -40
- package/src/widget-factory/stringTool.js +0 -26
package/bin/utils/templates.js
CHANGED
|
@@ -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,
|
|
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
|
|
2268
|
-
// re-renders; the
|
|
2269
|
-
|
|
2270
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
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 [
|
|
2311
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
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 [
|
|
2387
|
-
|
|
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
|
|
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 =
|
|
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
|
|
2551
|
-
'seeded from `
|
|
2552
|
-
'module scope —
|
|
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
|
-
'-
|
|
2570
|
-
'
|
|
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
|
```
|
package/docs/cli/README.md
CHANGED
|
@@ -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
|
-
|
|
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
|
package/docs/guides/README.md
CHANGED
|
@@ -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
|
|
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**.
|