flet-box 1.1.0 → 1.1.2
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/README.md +16 -12
- package/bin/commands/checkUi.js +276 -45
- package/bin/commands/create.js +8 -1
- package/bin/commands/createBundle.js +18 -6
- package/bin/commands/createScreen.js +1 -1
- package/bin/commands/runBundle.js +1 -1
- package/bin/utils/bundleConfig.js +47 -3
- 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 +101 -85
- package/docs/README.md +24 -1
- package/docs/cli/README.md +51 -10
- 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/first-app.md +6 -7
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/mobile-and-platforms.md +51 -0
- package/docs/guides/online-updates.md +18 -6
- 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/README.md
CHANGED
|
@@ -38,21 +38,24 @@ Real DOM · No Virtual DOM · No build config to learn.
|
|
|
38
38
|
|
|
39
39
|
## Installation
|
|
40
40
|
|
|
41
|
-
FletBox
|
|
41
|
+
FletBox is on npm, and it drags nothing in with it — `"dependencies": {}`:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
git clone https://github.com/kuko53348/flet-box.git
|
|
47
|
-
cd flet-box
|
|
48
|
-
npm link # link the flet-box CLI globally
|
|
44
|
+
npm install -g flet-box # the CLI, once (Node >= 20.9)
|
|
49
45
|
|
|
50
46
|
flet-box create appName # scaffold your first app
|
|
51
47
|
cd appName
|
|
52
|
-
npm
|
|
53
|
-
flet-box run
|
|
48
|
+
npm install # pulls flet-box itself from the registry
|
|
49
|
+
flet-box run # dev server with hot reload
|
|
54
50
|
```
|
|
55
51
|
|
|
52
|
+
Nothing to install at all: `npx flet-box create appName` runs the same scaffolder
|
|
53
|
+
straight from the registry, and the project it writes declares `flet-box` as a
|
|
54
|
+
normal dependency.
|
|
55
|
+
|
|
56
|
+
Cloning the repository and `npm link`ing it is still how you *develop the framework
|
|
57
|
+
itself* — see [CONTRIBUTING](docs/CONTRIBUTING.md).
|
|
58
|
+
|
|
56
59
|
## Quick start
|
|
57
60
|
|
|
58
61
|
```javascript
|
|
@@ -80,7 +83,7 @@ runApp(App);
|
|
|
80
83
|
## Features
|
|
81
84
|
|
|
82
85
|
- Declarative, widget-based UI · reactive state with `useState` · router for SPAs
|
|
83
|
-
- Zero runtime dependencies; ESM only, Node >=
|
|
86
|
+
- Zero runtime dependencies; ESM only, Node >= 20.9 · real DOM, no Virtual DOM
|
|
84
87
|
- Local storage, HTTP services, theme, PWA, and production builds
|
|
85
88
|
- Android & iOS from the CLI (APK, simulator `.app`, `.xcarchive`)
|
|
86
89
|
- Utility API for layout, text, color, animation, and more
|
|
@@ -137,12 +140,11 @@ invent against.
|
|
|
137
140
|
|
|
138
141
|
## Android and iOS
|
|
139
142
|
|
|
140
|
-
`flet-box create` ships the Capacitor config, so the same SPA builds for both platforms.
|
|
143
|
+
`flet-box create` ships the Capacitor config, so the same SPA builds for both platforms. The generated project already declares `flet-box` as a dependency, so one install covers everything:
|
|
141
144
|
|
|
142
145
|
```bash
|
|
143
146
|
flet-box create my-app && cd my-app
|
|
144
|
-
npm
|
|
145
|
-
npm install # Capacitor CLI, sharp, platform package
|
|
147
|
+
npm install # flet-box from the registry, plus the Capacitor toolchain
|
|
146
148
|
```
|
|
147
149
|
|
|
148
150
|
- **Android** — Node >= 20.9, a JDK, and the Android SDK. `flet-box build android` → debug APK; `flet-box build android --release [--aab]` → unsigned release artifact to sign yourself (`apksigner` / Play App Signing).
|
|
@@ -160,6 +162,8 @@ They are **one continuous book** — the cover maps every page: [The FletBox Boo
|
|
|
160
162
|
|
|
161
163
|
Rather than read, click: **[the live widget gallery](https://kuko53348.github.io/flet-box-widgets-exemples/)** gives every widget its own page, renders each example in the browser it was written for, and prints the exact call underneath it. The theme toggle in the corner shows the same screen in light and dark, which is the honest way to judge a default color.
|
|
162
164
|
|
|
165
|
+
And the book itself is an app: **[`flet-box-docs`](https://www.npmjs.com/package/flet-box-docs)** (`npm install flet-box-docs`) renders these same pages with the framework's own `Markdown` widget, with an index, a theme toggle, an offline cache and Android/iOS wrappers.
|
|
166
|
+
|
|
163
167
|
- [Your first app](docs/guides/first-app.md) — build a working todo app in 20 minutes
|
|
164
168
|
- [The widget book](docs/widget/README.md) — every widget with property tables and examples
|
|
165
169
|
- [State, Router & Services](docs/guides/state.md) — shared state, routing, storage, and HTTP
|
package/bin/commands/checkUi.js
CHANGED
|
@@ -120,8 +120,9 @@ export const INFO_CODES = new Set([
|
|
|
120
120
|
* Which design rule each code proves, and the number the finding prints.
|
|
121
121
|
*
|
|
122
122
|
* The gate is the machine half of a contract whose human half is
|
|
123
|
-
* `skills/fletbox-design/SKILL.md`:
|
|
124
|
-
*
|
|
123
|
+
* `skills/fletbox-design/SKILL.md`: 38 numbered rules across two parts, of which
|
|
124
|
+
* this table cites the enforceable 00-24 (25-37 are the visual atlas, judgement
|
|
125
|
+
* no static pass can prove). A finding that says `OFF_SCALE_SPACING` tells the reader the
|
|
125
126
|
* value is wrong; a finding that says `OFF_SCALE_SPACING — rule 07` tells them to
|
|
126
127
|
* open rule 07 and learn *why*, which is the only way the next screen is right
|
|
127
128
|
* without being told twice. `scripts/check-skills.mjs` fails if a code is missing
|
|
@@ -390,7 +391,9 @@ export const entriesOf = (code, open, close) => {
|
|
|
390
391
|
};
|
|
391
392
|
|
|
392
393
|
/**
|
|
393
|
-
* Numeric literals written in a value, skipping the ones inside strings.
|
|
394
|
+
* Numeric literals written in a value, skipping the ones inside strings. This is
|
|
395
|
+
* a lexical scan, not an evaluation: use `measuredNumbers` before judging a
|
|
396
|
+
* value against a scale, or `base * 1.5` is read as a 1.5 size.
|
|
394
397
|
*
|
|
395
398
|
* @param {string} value
|
|
396
399
|
* @returns {Array<{number: number, offset: number}>}
|
|
@@ -415,6 +418,143 @@ export const numbersIn = (value) => {
|
|
|
415
418
|
return found;
|
|
416
419
|
};
|
|
417
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Tokenise an arithmetic expression made only of number literals operators and
|
|
423
|
+
* parentheses. Anything else — an identifier, a member access, a call, a
|
|
424
|
+
* ternary, an object or array — returns null, because its value is decided at
|
|
425
|
+
* runtime and a static pass that guessed it would report a number the screen
|
|
426
|
+
* never paints.
|
|
427
|
+
*
|
|
428
|
+
* @param {string} text
|
|
429
|
+
* @returns {Array<{type: string, value?: number}> | null}
|
|
430
|
+
*/
|
|
431
|
+
const arithmeticTokens = (text) => {
|
|
432
|
+
const tokens = [];
|
|
433
|
+
let i = 0;
|
|
434
|
+
while (i < text.length) {
|
|
435
|
+
const ch = text[i];
|
|
436
|
+
if (/\s/.test(ch)) {
|
|
437
|
+
i++;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (/[0-9]/.test(ch) || (ch === "." && /[0-9]/.test(text[i + 1] ?? ""))) {
|
|
441
|
+
const match = /^\d*\.?\d+(?:[eE][+-]?\d+)?/.exec(text.slice(i));
|
|
442
|
+
tokens.push({ type: "number", value: Number(match[0]) });
|
|
443
|
+
i += match[0].length;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (/[A-Za-z_$]/.test(ch)) return null;
|
|
447
|
+
if ("+-*/%()".includes(ch)) {
|
|
448
|
+
tokens.push({ type: ch });
|
|
449
|
+
i++;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
return tokens;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Evaluate a token list by recursive descent. Null means "not provable", which
|
|
459
|
+
* is never the same answer as "off the scale".
|
|
460
|
+
*
|
|
461
|
+
* @param {Array<{type: string, value?: number}>} tokens
|
|
462
|
+
* @returns {number | null}
|
|
463
|
+
*/
|
|
464
|
+
const foldArithmetic = (tokens) => {
|
|
465
|
+
let i = 0;
|
|
466
|
+
const bad = () => {
|
|
467
|
+
throw new Error("unmeasurable");
|
|
468
|
+
};
|
|
469
|
+
const primary = () => {
|
|
470
|
+
const token = tokens[i++];
|
|
471
|
+
if (!token) bad();
|
|
472
|
+
if (token.type === "number") return token.value;
|
|
473
|
+
if (token.type === "(") {
|
|
474
|
+
const value = expression();
|
|
475
|
+
if (tokens[i++]?.type !== ")") bad();
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
return bad();
|
|
479
|
+
};
|
|
480
|
+
const unary = () => {
|
|
481
|
+
if (tokens[i]?.type === "-") {
|
|
482
|
+
i++;
|
|
483
|
+
return -unary();
|
|
484
|
+
}
|
|
485
|
+
if (tokens[i]?.type === "+") {
|
|
486
|
+
i++;
|
|
487
|
+
return unary();
|
|
488
|
+
}
|
|
489
|
+
return primary();
|
|
490
|
+
};
|
|
491
|
+
const term = () => {
|
|
492
|
+
let value = unary();
|
|
493
|
+
while (["*", "/", "%"].includes(tokens[i]?.type)) {
|
|
494
|
+
const op = tokens[i++].type;
|
|
495
|
+
const right = unary();
|
|
496
|
+
if (op === "*") value *= right;
|
|
497
|
+
else if (right === 0) bad();
|
|
498
|
+
else value = op === "/" ? value / right : value % right;
|
|
499
|
+
}
|
|
500
|
+
return value;
|
|
501
|
+
};
|
|
502
|
+
const expression = () => {
|
|
503
|
+
let value = term();
|
|
504
|
+
while (["+", "-"].includes(tokens[i]?.type)) {
|
|
505
|
+
const op = tokens[i++].type;
|
|
506
|
+
value = op === "+" ? value + term() : value - term();
|
|
507
|
+
}
|
|
508
|
+
return value;
|
|
509
|
+
};
|
|
510
|
+
try {
|
|
511
|
+
if (!tokens.length) return null;
|
|
512
|
+
const value = expression();
|
|
513
|
+
if (i !== tokens.length || !Number.isFinite(value)) return null;
|
|
514
|
+
return value;
|
|
515
|
+
} catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* The numbers a prop value provably paints, and whether the pass can say at all.
|
|
522
|
+
*
|
|
523
|
+
* A value is measured when it is a literal (`24`), arithmetic over literals
|
|
524
|
+
* (`8 * 3`), a quoted pixel shorthand (`"12px 20px"`), or any other literal
|
|
525
|
+
* string (a preset name has no numbers and is therefore on no scale to break).
|
|
526
|
+
* It is unmeasured when its value depends on an identifier, a ternary, a call or
|
|
527
|
+
* a member access — `size: base * 1.5`, `sz.h - 8`, `padding: size > 20 ? … : 0`.
|
|
528
|
+
* The distinction is the point: a lexical scan reads the `1.5` inside
|
|
529
|
+
* `base * 1.5` as the size and reports a value the screen never renders.
|
|
530
|
+
*
|
|
531
|
+
* @param {string} value raw prop value
|
|
532
|
+
* @returns {{measured: boolean, numbers: number[]}}
|
|
533
|
+
*/
|
|
534
|
+
export const measuredNumbers = (value) => {
|
|
535
|
+
const text = value.trim();
|
|
536
|
+
if (!text) return { measured: true, numbers: [] };
|
|
537
|
+
const quote = text[0];
|
|
538
|
+
if (isQuote(quote)) {
|
|
539
|
+
const end = skipString(text, 0);
|
|
540
|
+
if (end !== text.length) return { measured: false, numbers: [] };
|
|
541
|
+
const body = text.slice(1, end - 1);
|
|
542
|
+
if (/\\|\$\{/.test(body)) return { measured: false, numbers: [] };
|
|
543
|
+
if (/\b(?:var|calc|clamp|min|max|rgb|rgba|hsl|hsla)\s*\(|%/.test(body)) {
|
|
544
|
+
return { measured: false, numbers: [] };
|
|
545
|
+
}
|
|
546
|
+
const numbers = [...body.matchAll(/-?\d+(?:\.\d+)?px|\b0\b/g)].map((match) =>
|
|
547
|
+
match[0].endsWith("px") ? Number(match[0].slice(0, -2)) : 0,
|
|
548
|
+
);
|
|
549
|
+
return { measured: true, numbers };
|
|
550
|
+
}
|
|
551
|
+
const tokens = arithmeticTokens(text);
|
|
552
|
+
if (!tokens) return { measured: false, numbers: [] };
|
|
553
|
+
const folded = foldArithmetic(tokens);
|
|
554
|
+
if (folded === null) return { measured: false, numbers: [] };
|
|
555
|
+
return { measured: true, numbers: [folded] };
|
|
556
|
+
};
|
|
557
|
+
|
|
418
558
|
/**
|
|
419
559
|
* `#rgb`/`#rrggbb`/`#rrggbbaa` or a functional color typed as a literal.
|
|
420
560
|
*
|
|
@@ -695,6 +835,68 @@ const finding = (ctx, line, code, message, detail) => ({
|
|
|
695
835
|
* @param {object} ctx resolved package data
|
|
696
836
|
* @returns {object[]} findings
|
|
697
837
|
*/
|
|
838
|
+
/**
|
|
839
|
+
* Is the `{` at `brace` a function body? Looks back over the parameter list and
|
|
840
|
+
* asks what closes it: `function`, an arrow, or a method name. Control blocks
|
|
841
|
+
* (`if`, `for`, `switch`, `catch`, `try`, `else`) and object literals — including
|
|
842
|
+
* a widget's own `Widget({ … })` — are not function bodies.
|
|
843
|
+
*
|
|
844
|
+
* @param {string} code
|
|
845
|
+
* @param {number} brace index of `{`
|
|
846
|
+
* @returns {boolean}
|
|
847
|
+
*/
|
|
848
|
+
const opensFunction = (code, brace) => {
|
|
849
|
+
let i = brace - 1;
|
|
850
|
+
let depth = 0;
|
|
851
|
+
while (i >= 0) {
|
|
852
|
+
const ch = code[i];
|
|
853
|
+
if (ch === ")" || ch === "]") depth++;
|
|
854
|
+
else if (ch === "(" || ch === "[") {
|
|
855
|
+
if (depth === 0) break;
|
|
856
|
+
depth--;
|
|
857
|
+
} else if (depth === 0 && (ch === ";" || ch === "{" || ch === "}" || ch === "\n")) {
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
i--;
|
|
861
|
+
}
|
|
862
|
+
const head = code.slice(i + 1, brace).trim();
|
|
863
|
+
if (/^(?:async\s+)?function\b/.test(head)) return true;
|
|
864
|
+
if (/(?:^|[\s;}])(?:async\s+)?(?:\([^]*?\)|[A-Za-z_$][\w$]*)\s*=>$/.test(head)) return true;
|
|
865
|
+
const method = /^([A-Za-z_$][\w$]*)\s*\([^]*\)$/.exec(head);
|
|
866
|
+
return Boolean(
|
|
867
|
+
method &&
|
|
868
|
+
!["if", "for", "while", "switch", "catch", "with", "do", "else", "try"].includes(
|
|
869
|
+
method[1],
|
|
870
|
+
),
|
|
871
|
+
);
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* The brace range of every function body in a file.
|
|
876
|
+
*
|
|
877
|
+
* @param {string} code
|
|
878
|
+
* @returns {Array<[number, number]>} `[open, close]` pairs
|
|
879
|
+
*/
|
|
880
|
+
export const functionBodies = (code) => {
|
|
881
|
+
const bodies = [];
|
|
882
|
+
const open = [];
|
|
883
|
+
for (let i = 0; i < code.length; i++) {
|
|
884
|
+
const ch = code[i];
|
|
885
|
+
if (isQuote(ch)) {
|
|
886
|
+
i = skipString(code, i) - 1;
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
if (ch === "{") {
|
|
890
|
+
open.push(i);
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
if (ch !== "}") continue;
|
|
894
|
+
const start = open.pop();
|
|
895
|
+
if (start !== undefined && opensFunction(code, start)) bodies.push([start, i]);
|
|
896
|
+
}
|
|
897
|
+
return bodies;
|
|
898
|
+
};
|
|
899
|
+
|
|
698
900
|
const checkFile = (file, ctx) => {
|
|
699
901
|
const findings = [];
|
|
700
902
|
const src = fs.readFileSync(file, "utf8");
|
|
@@ -716,6 +918,7 @@ const checkFile = (file, ctx) => {
|
|
|
716
918
|
}
|
|
717
919
|
return { ...call, props };
|
|
718
920
|
});
|
|
921
|
+
const bodies = functionBodies(code);
|
|
719
922
|
|
|
720
923
|
/**
|
|
721
924
|
* The nearest enclosing call that says it paints a plate. Its props are handed
|
|
@@ -771,23 +974,27 @@ const checkFile = (file, ctx) => {
|
|
|
771
974
|
);
|
|
772
975
|
};
|
|
773
976
|
|
|
774
|
-
const namedByTooltip = (call) =>
|
|
775
|
-
enclosing(call).some((other) => other.name === "Tooltip" && other.props.has("text"));
|
|
776
|
-
|
|
777
977
|
/**
|
|
778
978
|
* A glyph control that carries its own accessible name is named, even when this
|
|
779
979
|
* pass cannot see the wrapper: an app that builds the control inside a helper
|
|
780
|
-
* and
|
|
781
|
-
* call site, and a lexical scan stops at the helper's own braces. `aria-*` is
|
|
782
|
-
* routed passthrough (translateProps.js:948), so the name really does reach the
|
|
980
|
+
* and names the *result* at the call site has a `child: iconButton("share")` at
|
|
981
|
+
* the call site, and a lexical scan stops at the helper's own braces. `aria-*` is
|
|
982
|
+
* a routed passthrough (translateProps.js:948), so the name really does reach the
|
|
783
983
|
* DOM — which is the sentence the finding asks for.
|
|
984
|
+
*
|
|
985
|
+
* A `Tooltip` is deliberately not on this list. It points `aria-describedby` at
|
|
986
|
+
* the bubble, and a description is not a name: a screen reader still announces
|
|
987
|
+
* "button" with no word for what it does.
|
|
784
988
|
*/
|
|
785
989
|
const namedByAria = (call) =>
|
|
786
990
|
[...call.props.keys()].some((key) => /^aria-(label|labelledby)$/i.test(key));
|
|
787
991
|
|
|
788
|
-
const isFilled = (call) =>
|
|
789
|
-
call.name
|
|
790
|
-
|
|
992
|
+
const isFilled = (call) => {
|
|
993
|
+
if (call.name !== "Button") return false;
|
|
994
|
+
const variant = call.props.get("variant")?.value.trim();
|
|
995
|
+
if (variant === undefined) return true;
|
|
996
|
+
return /^(['"`])filled\1$/.test(variant);
|
|
997
|
+
};
|
|
791
998
|
|
|
792
999
|
/** A width that caps the block: a number or a percentage below full bleed. */
|
|
793
1000
|
const capsWidth = (call) => {
|
|
@@ -811,7 +1018,8 @@ const checkFile = (file, ctx) => {
|
|
|
811
1018
|
if (SKIP_VALUE_KEYS.has(key)) continue;
|
|
812
1019
|
const kind = ctx.propKinds.get(key);
|
|
813
1020
|
const valueLine = lineOf(offsets, entry.valueStart);
|
|
814
|
-
const
|
|
1021
|
+
const measured = measuredNumbers(entry.value);
|
|
1022
|
+
const numbers = measured.numbers;
|
|
815
1023
|
|
|
816
1024
|
if (kind === "color" || COLOR_FALLBACK_KEYS.has(key)) {
|
|
817
1025
|
const literal = colorLiteralIn(entry.value);
|
|
@@ -834,45 +1042,35 @@ const checkFile = (file, ctx) => {
|
|
|
834
1042
|
|
|
835
1043
|
if (kind === "spacing") {
|
|
836
1044
|
for (const n of numbers) {
|
|
837
|
-
if (SPACING_SCALE.has(Math.abs(n
|
|
1045
|
+
if (SPACING_SCALE.has(Math.abs(n))) continue;
|
|
838
1046
|
push(
|
|
839
1047
|
valueLine,
|
|
840
1048
|
"OFF_SCALE_SPACING",
|
|
841
|
-
`${call.name}.${key} = ${n
|
|
1049
|
+
`${call.name}.${key} = ${n} is not on the spacing scale.`,
|
|
842
1050
|
"Use 4 · 8 · 12 · 16 · 18 · 24 · 32 · 40 · 48 · 64 · 96. The scale is what makes two screens look like one app.",
|
|
843
1051
|
);
|
|
844
1052
|
}
|
|
845
1053
|
// A CSS shorthand still has to obey the scale: `padding: '12px 20px'` is
|
|
846
|
-
// the same mistake with a unit glued on
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const value = Number(match[0].slice(0, -2));
|
|
850
|
-
if (SPACING_SCALE.has(value)) continue;
|
|
851
|
-
push(
|
|
852
|
-
valueLine,
|
|
853
|
-
"OFF_SCALE_SPACING",
|
|
854
|
-
`${call.name}.${key} = '${match[0]}' is not on the spacing scale.`,
|
|
855
|
-
"Use 4 · 8 · 12 · 16 · 18 · 24 · 32 · 40 · 48 · 64 · 96, or the plain number the factory prefers.",
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
1054
|
+
// the same mistake with a unit glued on, and `measuredNumbers` already
|
|
1055
|
+
// read it — an unmeasurable one (`calc`, `var`, a percentage) reports no
|
|
1056
|
+
// numbers instead of the literals it happens to contain.
|
|
859
1057
|
} else if (kind === "radius") {
|
|
860
1058
|
for (const n of numbers) {
|
|
861
|
-
if (RADIUS_SCALE.has(Math.abs(n
|
|
1059
|
+
if (RADIUS_SCALE.has(Math.abs(n))) continue;
|
|
862
1060
|
push(
|
|
863
1061
|
valueLine,
|
|
864
1062
|
"OFF_SCALE_RADIUS",
|
|
865
|
-
`${call.name}.${key} = ${n
|
|
866
|
-
"Use 4 · 8 · 12 · 16 · 999 (a pill). A radius between them is a shape no other card in this app has.",
|
|
1063
|
+
`${call.name}.${key} = ${n} is not a scale radius.`,
|
|
1064
|
+
"Use 4 · 8 · 12 · 16 · 20 · 24 · 999 (a pill). A radius between them is a shape no other card in this app has.",
|
|
867
1065
|
);
|
|
868
1066
|
}
|
|
869
1067
|
} else if (kind === "type" && (key === "fontSize" || call.name === "Text")) {
|
|
870
1068
|
for (const n of numbers) {
|
|
871
|
-
if (TYPE_SCALE.has(Math.abs(n
|
|
1069
|
+
if (TYPE_SCALE.has(Math.abs(n))) continue;
|
|
872
1070
|
push(
|
|
873
1071
|
valueLine,
|
|
874
1072
|
"OFF_SCALE_TYPE",
|
|
875
|
-
`${call.name}.${key} = ${n
|
|
1073
|
+
`${call.name}.${key} = ${n} is not a scale type size.`,
|
|
876
1074
|
"12 caption · 14 body-small · 16 body · 18 subtitle · 24 title · 32 headline · 40/48 display. One size per element, one weight per role.",
|
|
877
1075
|
);
|
|
878
1076
|
}
|
|
@@ -880,23 +1078,36 @@ const checkFile = (file, ctx) => {
|
|
|
880
1078
|
|
|
881
1079
|
if (tappable && (key === "height" || key === "minHeight")) {
|
|
882
1080
|
for (const n of numbers) {
|
|
883
|
-
if (n
|
|
1081
|
+
if (n <= 0 || n >= TAP_MINIMUM) continue;
|
|
884
1082
|
push(
|
|
885
1083
|
valueLine,
|
|
886
1084
|
"SMALL_TAP_TARGET",
|
|
887
|
-
`${call.name} is pressable but only ${n
|
|
1085
|
+
`${call.name} is pressable but only ${n}px tall.`,
|
|
888
1086
|
`Fitts: a finger needs ${TAP_MINIMUM}x${TAP_MINIMUM}. Get there with \`height\`, not with padding.`,
|
|
889
1087
|
);
|
|
890
1088
|
}
|
|
891
1089
|
}
|
|
892
1090
|
|
|
893
1091
|
if (tappable && ICON_WIDGETS.includes(call.name) && key === "size") {
|
|
1092
|
+
// A glyph's target is its box, not its font size: `size: 16, padding: 12`
|
|
1093
|
+
// is 40x40 and legal, while `size: 16` alone is the glyph and nothing else.
|
|
1094
|
+
// A padding this pass cannot resolve makes the target unmeasured.
|
|
1095
|
+
const padded = props.has("paddingVertical")
|
|
1096
|
+
? measuredNumbers(props.get("paddingVertical").value)
|
|
1097
|
+
: props.has("padding")
|
|
1098
|
+
? measuredNumbers(props.get("padding").value)
|
|
1099
|
+
: { measured: true, numbers: [0] };
|
|
1100
|
+
const vertical = padded.measured ? Math.abs(padded.numbers[0] ?? 0) : null;
|
|
894
1101
|
for (const n of numbers) {
|
|
895
|
-
if (
|
|
1102
|
+
if (vertical === null) break;
|
|
1103
|
+
const target = n + vertical * 2;
|
|
1104
|
+
if (target >= TAP_MINIMUM || props.has("height") || props.has("minHeight")) break;
|
|
896
1105
|
push(
|
|
897
1106
|
valueLine,
|
|
898
1107
|
"SMALL_TAP_TARGET",
|
|
899
|
-
`${call.name} is pressable at size ${n
|
|
1108
|
+
`${call.name} is pressable at size ${n}${
|
|
1109
|
+
vertical ? ` with ${vertical} of padding` : ""
|
|
1110
|
+
}, so its whole target is ${target}px.`,
|
|
900
1111
|
`Wrap it in the ${TAP_MINIMUM}px box it needs, or use \`Button({ icon, onPress })\` and keep the glyph at 20.`,
|
|
901
1112
|
);
|
|
902
1113
|
}
|
|
@@ -956,14 +1167,13 @@ const checkFile = (file, ctx) => {
|
|
|
956
1167
|
ICON_WIDGETS.includes(call.name) &&
|
|
957
1168
|
tappable &&
|
|
958
1169
|
!labeledBySibling(call) &&
|
|
959
|
-
!namedByTooltip(call) &&
|
|
960
1170
|
!namedByAria(call)
|
|
961
1171
|
) {
|
|
962
1172
|
push(
|
|
963
1173
|
line,
|
|
964
1174
|
"ICON_ONLY_CONTROL",
|
|
965
1175
|
`${call.name} is pressable and carries no name.`,
|
|
966
|
-
|
|
1176
|
+
'A glyph with no words is a guess, and a screen reader says nothing at all. A `Tooltip` is a description (`aria-describedby`), not a name: put a `Text` beside the control or name it with `"aria-label": "Share"`.',
|
|
967
1177
|
);
|
|
968
1178
|
}
|
|
969
1179
|
if (
|
|
@@ -971,14 +1181,13 @@ const checkFile = (file, ctx) => {
|
|
|
971
1181
|
["icon", "iconLeft", "iconRight"].some((key) => props.has(key)) &&
|
|
972
1182
|
!props.has("text") &&
|
|
973
1183
|
!labeledBySibling(call) &&
|
|
974
|
-
!namedByTooltip(call) &&
|
|
975
1184
|
!namedByAria(call)
|
|
976
1185
|
) {
|
|
977
1186
|
push(
|
|
978
1187
|
line,
|
|
979
1188
|
"ICON_ONLY_CONTROL",
|
|
980
1189
|
"Button shows an icon and no label.",
|
|
981
|
-
|
|
1190
|
+
'Give it `text`, or name it with `"aria-label": "Share"`. A tooltip explains the control, it does not name it.',
|
|
982
1191
|
);
|
|
983
1192
|
}
|
|
984
1193
|
|
|
@@ -1076,13 +1285,35 @@ const checkFile = (file, ctx) => {
|
|
|
1076
1285
|
}
|
|
1077
1286
|
}
|
|
1078
1287
|
|
|
1079
|
-
// Rule 04 — one
|
|
1288
|
+
// Rule 04 — one view, one primary action. Emphasis is a budget, not a style.
|
|
1289
|
+
// The view is the function body a screen renders, not the file: a components
|
|
1290
|
+
// module exports several screens, and two buttons in two components are never
|
|
1291
|
+
// on the same screen. A `variant` this pass cannot resolve counts as neither,
|
|
1292
|
+
// since the button may be outlined at runtime.
|
|
1080
1293
|
const primaries = calls.filter(isFilled);
|
|
1081
|
-
|
|
1294
|
+
const scopeOf = (call) => {
|
|
1295
|
+
let scope = null;
|
|
1296
|
+
for (const [start, end] of bodies) {
|
|
1297
|
+
if (call.open > start && call.close < end && (scope === null || start > scope)) {
|
|
1298
|
+
scope = start;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
if (scope !== null) return `fn:${scope}`;
|
|
1302
|
+
const root = enclosing(call).at(-1);
|
|
1303
|
+
return root ? `root:${root.open}` : "file";
|
|
1304
|
+
};
|
|
1305
|
+
const views = new Map();
|
|
1306
|
+
for (const call of primaries) {
|
|
1307
|
+
const scope = scopeOf(call);
|
|
1308
|
+
if (!views.has(scope)) views.set(scope, []);
|
|
1309
|
+
views.get(scope).push(call);
|
|
1310
|
+
}
|
|
1311
|
+
for (const group of views.values()) {
|
|
1312
|
+
if (group.length < 2) continue;
|
|
1082
1313
|
push(
|
|
1083
|
-
|
|
1314
|
+
lineOf(offsets, group[0].open),
|
|
1084
1315
|
"MULTIPLE_PRIMARY_ACTIONS",
|
|
1085
|
-
`${
|
|
1316
|
+
`${group.length} filled buttons in this view (lines ${group
|
|
1086
1317
|
.map((call) => lineOf(offsets, call.open))
|
|
1087
1318
|
.join(", ")}).`,
|
|
1088
1319
|
"Everything filled is competing with everything else. Keep one filled, make the next `variant: \"outlined\"`, and drop the rest to `variant: \"text\"`.",
|
package/bin/commands/create.js
CHANGED
|
@@ -249,6 +249,10 @@ export const createProject = async (projectName, rawArgs = []) => {
|
|
|
249
249
|
path.join(projectPath, "package.json"),
|
|
250
250
|
templates.packageJson(finalName),
|
|
251
251
|
);
|
|
252
|
+
createFile(
|
|
253
|
+
path.join(projectPath, "jsconfig.json"),
|
|
254
|
+
templates.jsconfig(),
|
|
255
|
+
);
|
|
252
256
|
createFile(
|
|
253
257
|
path.join(projectPath, "fletbox.config.json"),
|
|
254
258
|
templates.fletboxConfig(otaUrl || undefined),
|
|
@@ -542,7 +546,10 @@ export const createProject = async (projectName, rawArgs = []) => {
|
|
|
542
546
|
console.log(
|
|
543
547
|
c(
|
|
544
548
|
"gray",
|
|
545
|
-
`⬆️ OTA:
|
|
549
|
+
`⬆️ OTA: ${templates.OTA_PLACEHOLDER_URL} is a placeholder, and the build knows it —\n` +
|
|
550
|
+
` it derives the real Pages address from the project's \`repository\`/git remote.\n` +
|
|
551
|
+
` So: push this folder to a public GitHub repo and build, or name the repo now\n` +
|
|
552
|
+
` with ${c("brightCyan", "flet-box create <name> --repo user/repo")} / edit fletbox.config.json.\n`,
|
|
546
553
|
),
|
|
547
554
|
);
|
|
548
555
|
}
|
|
@@ -17,7 +17,7 @@ import fs from "fs";
|
|
|
17
17
|
import path from "path";
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
19
19
|
import { c, banner, gradient, section } from "../utils/colors.js";
|
|
20
|
-
import { buildBundleConfig, collectAssets, getGitRemoteUrl, resolveAssetPatterns, absoluteShareTags } from "../utils/bundleConfig.js";
|
|
20
|
+
import { buildBundleConfig, collectAssets, getGitRemoteUrl, resolveAssetPatterns, absoluteShareTags, declaredUpdateUrl, isPlaceholderUpdateUrl } from "../utils/bundleConfig.js";
|
|
21
21
|
import { normaliseManifest, writeSpaFallback } from "../utils/pwaShell.js";
|
|
22
22
|
import { INITIAL_VERSION, incrementVersion, isValidVersion } from "../utils/version.js";
|
|
23
23
|
|
|
@@ -342,12 +342,24 @@ export const createBundle = async (targetArg) => {
|
|
|
342
342
|
c("gray", " (or set `update.url` in fletbox.config.json / `repository` in package.json)") +
|
|
343
343
|
"\n",
|
|
344
344
|
);
|
|
345
|
-
} else if (
|
|
345
|
+
} else if (isPlaceholderUpdateUrl(declaredUpdateUrl(projectRoot))) {
|
|
346
|
+
// The file still says TU-USUARIO/TU-REPO, so this address was derived from
|
|
347
|
+
// the project's own repository — correct for a GitHub-hosted app, and worth
|
|
348
|
+
// saying out loud because the file the developer will read disagrees with it.
|
|
346
349
|
console.log(
|
|
347
|
-
c(
|
|
348
|
-
"
|
|
349
|
-
"
|
|
350
|
-
|
|
350
|
+
c("green", "⬆️ OTA source: ") +
|
|
351
|
+
c("gray", bundleConfig.update.url) +
|
|
352
|
+
"\n" +
|
|
353
|
+
c(
|
|
354
|
+
"yellow",
|
|
355
|
+
" derived from the project's `repository`/git remote: `fletbox.config.json` still holds the placeholder.",
|
|
356
|
+
) +
|
|
357
|
+
"\n" +
|
|
358
|
+
c(
|
|
359
|
+
"gray",
|
|
360
|
+
" Correct for a GitHub Pages site. Set `update.url` by hand if this app will live on another host.",
|
|
361
|
+
) +
|
|
362
|
+
"\n",
|
|
351
363
|
);
|
|
352
364
|
} else {
|
|
353
365
|
console.log(
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import fs from "fs";
|
|
12
12
|
import path from "path";
|
|
13
|
-
import { c,
|
|
13
|
+
import { c, gradient, rainbow, section } from "../utils/colors.js";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Returns the source code for a new FletBox screen module.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { createBundle } from "./createBundle.js";
|
|
12
12
|
import { runDevServer } from "./runServer.js";
|
|
13
|
-
import { c, banner,
|
|
13
|
+
import { c, banner, dot } from "../utils/colors.js";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Builds the project bundle and serves it with a static dev server.
|