flet-box 1.0.7 → 1.0.9

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 (139) hide show
  1. package/README.md +3 -0
  2. package/bin/cli.js +13 -0
  3. package/bin/commands/checkApi.js +13 -1
  4. package/bin/commands/checkUi.js +1369 -0
  5. package/bin/commands/create.js +49 -5
  6. package/bin/commands/createBundle.js +21 -0
  7. package/bin/commands/runServer.js +11 -2
  8. package/bin/utils/pwaShell.js +137 -0
  9. package/bin/utils/templates.js +1398 -443
  10. package/docs/README.md +4 -0
  11. package/docs/cli/README.md +63 -2
  12. package/docs/guides/README.md +2 -0
  13. package/docs/guides/clean-ui.md +211 -0
  14. package/docs/guides/minification-and-protection.md +1 -1
  15. package/docs/guides/mobile-and-platforms.md +31 -0
  16. package/docs/guides/online-updates.md +48 -0
  17. package/docs/tools/README.md +1 -1
  18. package/docs/tools/animation.md +1 -1
  19. package/docs/tools/markdown.md +3 -3
  20. package/docs/tools/theme.md +42 -1
  21. package/docs/widget/Accordion.md +3 -0
  22. package/docs/widget/AdMob.md +4 -1
  23. package/docs/widget/AdSense.md +3 -0
  24. package/docs/widget/AnimatedBox.md +6 -3
  25. package/docs/widget/AnimatedText.md +3 -0
  26. package/docs/widget/Audio.md +3 -0
  27. package/docs/widget/Avatar.md +3 -0
  28. package/docs/widget/Badge.md +3 -0
  29. package/docs/widget/Button.md +3 -0
  30. package/docs/widget/COMMON_PROPS.md +52 -0
  31. package/docs/widget/Card.md +3 -0
  32. package/docs/widget/Carousel.md +3 -0
  33. package/docs/widget/Chart.md +3 -0
  34. package/docs/widget/Checkbox.md +3 -0
  35. package/docs/widget/Chip.md +3 -0
  36. package/docs/widget/CircularBar.md +3 -0
  37. package/docs/widget/CircularChart.md +3 -0
  38. package/docs/widget/CodeViewer.md +3 -0
  39. package/docs/widget/Column.md +3 -0
  40. package/docs/widget/Container.md +3 -0
  41. package/docs/widget/DataTable.md +3 -0
  42. package/docs/widget/Divider.md +3 -0
  43. package/docs/widget/DraggBox.md +3 -0
  44. package/docs/widget/Dropdown.md +3 -0
  45. package/docs/widget/DroppBox.md +3 -0
  46. package/docs/widget/FloatingActionButton.md +3 -0
  47. package/docs/widget/GridView.md +3 -0
  48. package/docs/widget/Icon.md +3 -0
  49. package/docs/widget/Image.md +3 -0
  50. package/docs/widget/Input.md +3 -0
  51. package/docs/widget/Inspector.md +2 -2
  52. package/docs/widget/InstallButton.md +3 -0
  53. package/docs/widget/ListTile.md +3 -0
  54. package/docs/widget/ListView.md +3 -0
  55. package/docs/widget/Maps.md +3 -0
  56. package/docs/widget/Markdown.md +3 -0
  57. package/docs/widget/MatrixRain.md +3 -0
  58. package/docs/widget/Pagination.md +3 -0
  59. package/docs/widget/ParallaxBox.md +3 -0
  60. package/docs/widget/ProgressBar.md +3 -0
  61. package/docs/widget/QRCode.md +3 -0
  62. package/docs/widget/README.md +2 -0
  63. package/docs/widget/Radio.md +4 -1
  64. package/docs/widget/Rating.md +3 -0
  65. package/docs/widget/Row.md +3 -0
  66. package/docs/widget/Skeleton.md +3 -0
  67. package/docs/widget/Slider.md +3 -0
  68. package/docs/widget/Stack.md +3 -0
  69. package/docs/widget/Stepper.md +3 -0
  70. package/docs/widget/Switch.md +3 -0
  71. package/docs/widget/Tabs.md +5 -5
  72. package/docs/widget/Text.md +3 -0
  73. package/docs/widget/Tooltip.md +3 -0
  74. package/docs/widget/TreeView.md +3 -0
  75. package/docs/widget/Video.md +3 -0
  76. package/package.json +2 -2
  77. package/src/animations/AnimatedBox.js +4 -1
  78. package/src/animations/MatrixRain.js +3 -3
  79. package/src/index.d.ts +64 -8
  80. package/src/index.js +1 -0
  81. package/src/navigations/AppBar.js +7 -12
  82. package/src/navigations/BottomNavigation.js +8 -13
  83. package/src/navigations/CollapsibleSideBar.js +30 -8
  84. package/src/navigations/Drawer.js +16 -14
  85. package/src/navigations/DrawerItem.js +3 -1
  86. package/src/navigations/Scaffold.js +26 -12
  87. package/src/navigations/Tabs.js +104 -59
  88. package/src/tools/color.js +54 -0
  89. package/src/tools/index.js +9 -1
  90. package/src/tools/scale.js +41 -0
  91. package/src/tools/shadow.js +20 -0
  92. package/src/utils/index.js +1 -0
  93. package/src/utils/readableInk.js +19 -0
  94. package/src/utils/syntaxHighlight.js +78 -10
  95. package/src/utils/themes.js +265 -58
  96. package/src/utils/units.js +33 -11
  97. package/src/widget-factory/effects.js +56 -5
  98. package/src/widget-factory/processProps.js +10 -1
  99. package/src/widget-factory/remTool.js +15 -29
  100. package/src/widget-factory/stackPosition.js +18 -3
  101. package/src/widget-factory/translateProps.js +28 -0
  102. package/src/widgets/Accordion.js +20 -23
  103. package/src/widgets/AdMob.js +7 -2
  104. package/src/widgets/Avatar.js +18 -12
  105. package/src/widgets/Badge.js +3 -2
  106. package/src/widgets/BottomSheet.js +14 -13
  107. package/src/widgets/Button.js +69 -30
  108. package/src/widgets/Card.js +6 -2
  109. package/src/widgets/Carousel.js +12 -10
  110. package/src/widgets/Chart.js +28 -5
  111. package/src/widgets/Checkbox.js +76 -18
  112. package/src/widgets/Chip.js +31 -10
  113. package/src/widgets/CircularBar.js +7 -7
  114. package/src/widgets/CircularChart.js +26 -8
  115. package/src/widgets/CodeViewer.js +93 -20
  116. package/src/widgets/DataTable.js +17 -11
  117. package/src/widgets/DraggBox.js +2 -1
  118. package/src/widgets/Dropdown.js +26 -13
  119. package/src/widgets/DroppBox.js +4 -2
  120. package/src/widgets/FloatingActionButton.js +13 -7
  121. package/src/widgets/Icon.js +3 -1
  122. package/src/widgets/Input.js +30 -23
  123. package/src/widgets/Inspector.js +54 -1
  124. package/src/widgets/InstallButton.js +6 -4
  125. package/src/widgets/ListTile.js +4 -3
  126. package/src/widgets/ListView.js +30 -12
  127. package/src/widgets/Markdown.js +37 -26
  128. package/src/widgets/Modal.js +14 -11
  129. package/src/widgets/Pagination.js +16 -11
  130. package/src/widgets/ProgressBar.js +6 -4
  131. package/src/widgets/Radio.js +73 -15
  132. package/src/widgets/Rating.js +24 -6
  133. package/src/widgets/Skeleton.js +31 -31
  134. package/src/widgets/Slider.js +29 -18
  135. package/src/widgets/SnackBar.js +35 -39
  136. package/src/widgets/Stepper.js +13 -9
  137. package/src/widgets/Switch.js +59 -18
  138. package/src/widgets/Tooltip.js +12 -10
  139. package/src/widgets/TreeView.js +27 -15
@@ -108,6 +108,21 @@ const askRepo = () =>
108
108
  );
109
109
  });
110
110
 
111
+ /**
112
+ * The GitHub Pages address a repo value implies, or null when the value is not
113
+ * a GitHub repo. Kept apart from `resolveOtaUrl` because the workflow header
114
+ * names the *Pages* site: a custom-domain OTA URL is not an address this file
115
+ * can publish to, so it must not be printed as one.
116
+ *
117
+ * @param {string|null} input - "user/repo" or a github.com URL.
118
+ * @returns {string|null} Base URL ending in "/", or null.
119
+ */
120
+ const pagesSiteUrl = (input) => {
121
+ if (!input) return null;
122
+ const gh = parseGithubRepo(input.trim());
123
+ return gh ? `https://${gh.user}.github.io/${gh.repo}/` : null;
124
+ };
125
+
111
126
  /**
112
127
  * Normalizes the user's OTA target into a base URL ending in "/".
113
128
  * - "user/repo" (or any GitHub URL shape) → GitHub Pages URL.
@@ -123,8 +138,7 @@ const resolveOtaUrl = (input) => {
123
138
  if (/^https?:\/\//i.test(value)) {
124
139
  return value.endsWith("/") ? value : `${value}/`;
125
140
  }
126
- const gh = parseGithubRepo(value);
127
- return gh ? `https://${gh.user}.github.io/${gh.repo}/` : null;
141
+ return pagesSiteUrl(value);
128
142
  };
129
143
 
130
144
  /**
@@ -167,6 +181,10 @@ export const createProject = async (projectName, rawArgs = []) => {
167
181
  repoInput = await askRepo();
168
182
  }
169
183
  const otaUrl = resolveOtaUrl(repoInput);
184
+ // The Pages address the workflow file prints as its own header. Derived from
185
+ // the repo alone: `flet-box create myapp --repo me/my-website` says so in
186
+ // build.yml, and a missing or unparsable repo leaves the placeholder to edit.
187
+ const siteUrl = pagesSiteUrl(repoInput);
170
188
  if (repoInput && !otaUrl) {
171
189
  console.warn(
172
190
  c(
@@ -198,6 +216,8 @@ export const createProject = async (projectName, rawArgs = []) => {
198
216
  ".fletbox/api",
199
217
  ".fletbox/memory/notes",
200
218
  ".claude",
219
+ ".claude/skills/fletbox-clean-ui",
220
+ ".qoder/skills/fletbox-clean-ui",
201
221
  ".cursor/rules",
202
222
  ];
203
223
  const extraDirs =
@@ -228,7 +248,7 @@ export const createProject = async (projectName, rawArgs = []) => {
228
248
  createFile(path.join(projectPath, ".gitignore"), templates.gitignore());
229
249
  createFile(
230
250
  path.join(projectPath, ".github/workflows/build.yml"),
231
- templates.githubWorkflow(finalName),
251
+ templates.githubWorkflow(finalName, siteUrl),
232
252
  );
233
253
  createFile(
234
254
  path.join(projectPath, ".github/workflows/build-native.yml"),
@@ -416,6 +436,29 @@ export const createProject = async (projectName, rawArgs = []) => {
416
436
  path.join(projectPath, ".cursor/rules/fletbox.mdc"),
417
437
  templates.cursorRules(),
418
438
  );
439
+
440
+ // The clean-UI skill. Claude Code loads `.claude/skills/<name>/SKILL.md` by
441
+ // description, so the agent picks it up whenever it touches a screen without
442
+ // being told. `.qoder/skills/` holds a byte-identical copy because Qoder reads
443
+ // the same shape from a different root — one text, two loaders, never two edits.
444
+ const cleanUiSkill = templates.cleanUiSkill();
445
+ createFile(
446
+ path.join(projectPath, ".claude/skills/fletbox-clean-ui/SKILL.md"),
447
+ cleanUiSkill,
448
+ );
449
+ createFile(
450
+ path.join(projectPath, ".qoder/skills/fletbox-clean-ui/SKILL.md"),
451
+ cleanUiSkill,
452
+ );
453
+ createFile(
454
+ path.join(projectPath, ".cursor/rules/fletbox-clean-ui.mdc"),
455
+ templates.cleanUiCursorRule(),
456
+ );
457
+ createFile(
458
+ path.join(projectPath, "scripts/check-ui.mjs"),
459
+ templates.checkUiScript(finalName),
460
+ );
461
+
419
462
  createFile(
420
463
  path.join(projectPath, ".fletbox/memory/MEMORY.md"),
421
464
  templates.memoryIndexMd(finalName),
@@ -494,8 +537,9 @@ export const createProject = async (projectName, rawArgs = []) => {
494
537
  c(
495
538
  "gray",
496
539
  "CI: .github/workflows/build.yml publishes the web to GitHub Pages on every push.\n" +
497
- " Enable it once: Settings → Pages → Source: GitHub Actions. The job prints the URL.\n" +
498
- " Android and iOS are manual: .github/workflows/build-native.yml, Run workflow.\n",
540
+ ` It goes live at: ${siteUrl || `https://TU-USUARIO.github.io/${finalName}/`}\n` +
541
+ " Enable it once: Settings → Pages → Source: GitHub Actions. The job prints the URL.\n" +
542
+ " Android and iOS are manual: .github/workflows/build-native.yml, Run workflow.\n",
499
543
  ),
500
544
  );
501
545
  console.log(
@@ -18,6 +18,7 @@ import path from "path";
18
18
  import { fileURLToPath } from "url";
19
19
  import { c, banner, gradient, section } from "../utils/colors.js";
20
20
  import { buildBundleConfig, collectAssets, getGitRemoteUrl, resolveAssetPatterns, absoluteShareTags } from "../utils/bundleConfig.js";
21
+ import { normaliseManifest, writeSpaFallback } from "../utils/pwaShell.js";
21
22
  import { INITIAL_VERSION, incrementVersion, isValidVersion } from "../utils/version.js";
22
23
 
23
24
  const __filename = fileURLToPath(import.meta.url);
@@ -57,6 +58,7 @@ const copyFolderSync = (src, dest) => {
57
58
  * ```
58
59
  * <target>/
59
60
  * index.html
61
+ * 404.html ← copy of the shell, so a refresh on a deep link serves the app
60
62
  * manifest.json
61
63
  * service-worker.js
62
64
  * run.sh
@@ -408,6 +410,25 @@ export const createBundle = async (targetArg) => {
408
410
  }
409
411
  }
410
412
 
413
+ // ---- Installable, deep-linkable output ----------------------------------
414
+ // Both corrections belong to the deployable folder, not to the project: the
415
+ // app's own manifest.json stays host-agnostic and this bundle is what a static
416
+ // host serves. See bin/utils/pwaShell.js for the two failure modes.
417
+ const manifestFixes = normaliseManifest(targetDir);
418
+ if (manifestFixes.length) {
419
+ console.log(c("green", `✅ manifest.json → ${manifestFixes.length} correction(s)`));
420
+ for (const fix of manifestFixes) console.log(c("gray", ` ${fix}`));
421
+ }
422
+
423
+ if (writeSpaFallback(targetDir)) {
424
+ console.log(
425
+ c(
426
+ "green",
427
+ "✅ 404.html → copy of the shell (hosts without SPA rewrites now serve the app on a refresh)",
428
+ ),
429
+ );
430
+ }
431
+
411
432
  // Preserve the app source tree so the in-app "View code" feature can read it.
412
433
  for (const dir of ["modules", "screens", "database", "components"]) {
413
434
  const src = path.join(srcDir, dir);
@@ -154,7 +154,11 @@ export const runDevServer = async (options = {}) => {
154
154
  url = req.url;
155
155
  }
156
156
 
157
- const safePath = path.normalize(url).replace(/^(\.\.[\/\\])+/, "");
157
+ // The query is part of the request, not of the file name: `?theme=dark`
158
+ // and the CSS hot-swap's `?t=` cache-buster were being resolved on disk
159
+ // and answered with a 404.
160
+ const pathname = url.split("?")[0].split("#")[0];
161
+ const safePath = path.normalize(pathname).replace(/^(\.\.[\/\\])+/, "");
158
162
  let filePath = path.join(
159
163
  projectRoot,
160
164
  safePath === "/" ? "index.html" : safePath,
@@ -186,7 +190,12 @@ export const runDevServer = async (options = {}) => {
186
190
  // still resolves `./loader.js` against `/demo/forms/`, so the page came
187
191
  // up blank on every shared link. Anchoring the page at the served root
188
192
  // keeps the app's relative URLs working whatever the route is.
189
- if (isFallback && ext === ".html") {
193
+ //
194
+ // The root document needs the same anchor: a client router pushes
195
+ // `/widget/Button` without a reload, and from then on the browser
196
+ // re-resolves a relative `<link rel="icon">` against that path. The
197
+ // favicon then 404s on every in-app navigation.
198
+ if (spaMode && ext === ".html" && (isFallback || safePath === "/")) {
190
199
  let html = content.toString("utf-8");
191
200
  if (!/<base [^>]*href=/i.test(html)) {
192
201
  html = html.replace(/<head(\s[^>]*)?>/i, (m) => `${m}\n<base href="/">`);
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @file bin/utils/pwaShell.js
3
+ * @description Two build-time corrections that make a bundle deployable as a PWA
4
+ * on a static host, which is where every generated app actually ends up
5
+ * (GitHub Pages, S3, any bucket under `/repo/`).
6
+ *
7
+ * Neither can be left to the app author to remember, because both fail silently
8
+ * on the developer's own machine and only show up once deployed:
9
+ *
10
+ * 1. `writeSpaFallback` — a single-page app has routes the host does not know.
11
+ * GitHub Pages has no rewrite rule, so a refresh on `/repo/menu` is answered
12
+ * with Pages' own "Page not found": the app looks broken exactly on the
13
+ * entry point a shared link or a reload produces. `404.html` is the portable
14
+ * remedy: every static host serves that file for a missing path, so copying
15
+ * the built shell over it hands the deep link to the router instead.
16
+ *
17
+ * 2. `normaliseManifest` — a generated `manifest.json` says `"start_url": "/"`
18
+ * and `"scope": "/"`. Resolved against the manifest's own URL that is the
19
+ * host root, i.e. `https://user.github.io/`, which is outside the app's
20
+ * directory and outside its service worker's scope. Chrome then refuses to
21
+ * offer the install at all: the site renders, works offline, and never shows
22
+ * "Add to Home Screen". Icons have the quieter version of the same bug — a
23
+ * copied logo declared as `192x192` that is really 512 px wide.
24
+ *
25
+ * Both only touch the bundle output. The project's own `manifest.json` and
26
+ * `index.html` stay host-agnostic so the same folder still runs on `:8000`.
27
+ */
28
+
29
+ import fs from "fs";
30
+ import path from "path";
31
+
32
+ /**
33
+ * Read the intrinsic size of a PNG from its IHDR chunk.
34
+ *
35
+ * @param {string} filePath - Absolute path to the candidate icon.
36
+ * @returns {{width: number, height: number}|null} Nothing for a non-PNG file.
37
+ */
38
+ export const pngDimensions = (filePath) => {
39
+ const header = Buffer.alloc(24);
40
+ let handle;
41
+ try {
42
+ handle = fs.openSync(filePath, "r");
43
+ if (fs.readSync(handle, header, 0, 24, 0) !== 24) return null;
44
+ } catch {
45
+ return null;
46
+ } finally {
47
+ if (handle !== undefined) fs.closeSync(handle);
48
+ }
49
+
50
+ const isPng =
51
+ header[0] === 0x89 &&
52
+ header.subarray(12, 16).toString("ascii") === "IHDR";
53
+ if (!isPng) return null;
54
+ return { width: header.readUInt32BE(16), height: header.readUInt32BE(20) };
55
+ };
56
+
57
+ /**
58
+ * Make the bundled `manifest.json` launch and stay inside the app it ships with.
59
+ *
60
+ * `start_url` and `scope` are resolved against the manifest URL, so `"/"` means
61
+ * the host root. Rewriting only that value (and an empty string) to `"./"` keeps
62
+ * a root-hosted app identical to what it was while pulling a sub-path app back
63
+ * inside its own directory; any other value is a deliberate base path and is left
64
+ * alone. Each icon's declared `sizes` is corrected to the pixels the file really
65
+ * has, which is also what makes the 192 px requirement provable to the browser.
66
+ *
67
+ * @param {string} targetDir - Absolute path of the bundle directory.
68
+ * @returns {string[]} Human-readable description of each correction made.
69
+ */
70
+ export const normaliseManifest = (targetDir) => {
71
+ const manifestPath = path.join(targetDir, "manifest.json");
72
+ if (!fs.existsSync(manifestPath)) return [];
73
+
74
+ let raw;
75
+ try {
76
+ raw = fs.readFileSync(manifestPath, "utf8");
77
+ } catch {
78
+ return [];
79
+ }
80
+
81
+ let manifest;
82
+ try {
83
+ manifest = JSON.parse(raw);
84
+ } catch {
85
+ return [];
86
+ }
87
+
88
+ const changes = [];
89
+
90
+ // "/" escapes the app on a sub-path host; "" is the same mistake written
91
+ // differently. Anything else was typed on purpose.
92
+ for (const field of ["start_url", "scope"]) {
93
+ if (manifest[field] === "/" || manifest[field] === "") {
94
+ manifest[field] = "./";
95
+ changes.push(`${field}: "/" → "./"`);
96
+ }
97
+ }
98
+
99
+ if (Array.isArray(manifest.icons)) {
100
+ for (const icon of manifest.icons) {
101
+ if (!icon || typeof icon.src !== "string") continue;
102
+ // Strip any query/hash and the leading "./" before hitting the disk; the
103
+ // bundle keeps icons relative to its own directory.
104
+ const clean = icon.src.split(/[?#]/)[0].replace(/^\.\//, "");
105
+ const size = pngDimensions(path.join(targetDir, clean));
106
+ if (!size) continue;
107
+ const actual = `${size.width}x${size.height}`;
108
+ if (icon.sizes !== actual) {
109
+ const declared = icon.sizes;
110
+ icon.sizes = actual;
111
+ changes.push(`${clean}: sizes now ${actual} (was ${declared})`);
112
+ }
113
+ }
114
+ }
115
+
116
+ if (changes.length) {
117
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
118
+ }
119
+ return changes;
120
+ };
121
+
122
+ /**
123
+ * Copy the finished shell to `404.html` so unknown paths serve the app.
124
+ *
125
+ * Called last, after the OTA loader is wired and the share tags are absolute, so
126
+ * the fallback document is byte-identical to `index.html` and boots the same
127
+ * bundle. Idempotent: rebuilding the project always rewrites it.
128
+ *
129
+ * @param {string} targetDir - Absolute path of the bundle directory.
130
+ * @returns {boolean} False when there is no `index.html` to copy.
131
+ */
132
+ export const writeSpaFallback = (targetDir) => {
133
+ const index = path.join(targetDir, "index.html");
134
+ if (!fs.existsSync(index)) return false;
135
+ fs.copyFileSync(index, path.join(targetDir, "404.html"));
136
+ return true;
137
+ };