flet-box 1.0.6 โ†’ 1.0.8

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 (162) hide show
  1. package/README.md +28 -1
  2. package/bin/cli.js +90 -11
  3. package/bin/commands/auditFix.js +682 -0
  4. package/bin/commands/checkApi.js +20 -6
  5. package/bin/commands/checkUi.js +1369 -0
  6. package/bin/commands/create.js +86 -12
  7. package/bin/commands/createBundle.js +75 -1
  8. package/bin/commands/images.js +349 -0
  9. package/bin/commands/pruneAssets.js +170 -0
  10. package/bin/commands/release.js +34 -1
  11. package/bin/commands/runCloudflare.js +47 -9
  12. package/bin/commands/runServer.js +91 -25
  13. package/bin/commands/toPng.js +346 -0
  14. package/bin/commands/unpack.js +1173 -0
  15. package/bin/ota/loader.js +90 -0
  16. package/bin/utils/appSources.js +101 -0
  17. package/bin/utils/bundleConfig.js +166 -0
  18. package/bin/utils/dtsProps.js +147 -0
  19. package/bin/utils/imageTools.js +271 -0
  20. package/bin/utils/images/bmp.js +99 -0
  21. package/bin/utils/images/gif.js +244 -0
  22. package/bin/utils/images/index.js +163 -0
  23. package/bin/utils/images/jpeg.js +643 -0
  24. package/bin/utils/images/png.js +486 -0
  25. package/bin/utils/images/unsupported.js +29 -0
  26. package/bin/utils/neutralise.js +109 -6
  27. package/bin/utils/pwaShell.js +137 -0
  28. package/bin/utils/templates.js +1653 -471
  29. package/docs/README.md +2 -0
  30. package/docs/cli/README.md +144 -1
  31. package/docs/guides/README.md +2 -0
  32. package/docs/guides/clean-ui.md +211 -0
  33. package/docs/guides/minification-and-protection.md +1 -1
  34. package/docs/guides/online-updates.md +105 -9
  35. package/docs/tools/README.md +1 -1
  36. package/docs/tools/animation.md +1 -1
  37. package/docs/tools/markdown.md +3 -3
  38. package/docs/tools/theme.md +41 -1
  39. package/docs/widget/Accordion.md +4 -0
  40. package/docs/widget/AdMob.md +5 -1
  41. package/docs/widget/AdSense.md +4 -0
  42. package/docs/widget/AnimatedBox.md +7 -3
  43. package/docs/widget/AnimatedText.md +4 -0
  44. package/docs/widget/Audio.md +5 -1
  45. package/docs/widget/Avatar.md +15 -3
  46. package/docs/widget/Badge.md +4 -0
  47. package/docs/widget/Button.md +4 -0
  48. package/docs/widget/COMMON_PROPS.md +52 -0
  49. package/docs/widget/Card.md +4 -0
  50. package/docs/widget/Carousel.md +4 -0
  51. package/docs/widget/Chart.md +4 -0
  52. package/docs/widget/Checkbox.md +4 -0
  53. package/docs/widget/Chip.md +4 -0
  54. package/docs/widget/CircularBar.md +4 -0
  55. package/docs/widget/CircularChart.md +4 -0
  56. package/docs/widget/CodeViewer.md +4 -0
  57. package/docs/widget/Column.md +4 -0
  58. package/docs/widget/Container.md +4 -0
  59. package/docs/widget/DataTable.md +4 -0
  60. package/docs/widget/Divider.md +4 -0
  61. package/docs/widget/DraggBox.md +6 -2
  62. package/docs/widget/Dropdown.md +6 -0
  63. package/docs/widget/DroppBox.md +5 -1
  64. package/docs/widget/FloatingActionButton.md +4 -0
  65. package/docs/widget/GridView.md +4 -0
  66. package/docs/widget/Icon.md +4 -0
  67. package/docs/widget/Image.md +14 -3
  68. package/docs/widget/Input.md +4 -0
  69. package/docs/widget/Inspector.md +2 -2
  70. package/docs/widget/InstallButton.md +4 -0
  71. package/docs/widget/ListTile.md +4 -0
  72. package/docs/widget/ListView.md +4 -0
  73. package/docs/widget/Maps.md +191 -0
  74. package/docs/widget/Markdown.md +4 -0
  75. package/docs/widget/MatrixRain.md +4 -0
  76. package/docs/widget/Pagination.md +4 -0
  77. package/docs/widget/ParallaxBox.md +4 -0
  78. package/docs/widget/ProgressBar.md +4 -0
  79. package/docs/widget/QRCode.md +4 -0
  80. package/docs/widget/README.md +12 -10
  81. package/docs/widget/Radio.md +5 -1
  82. package/docs/widget/Rating.md +4 -0
  83. package/docs/widget/Row.md +4 -0
  84. package/docs/widget/Skeleton.md +4 -0
  85. package/docs/widget/Slider.md +4 -0
  86. package/docs/widget/Stack.md +4 -0
  87. package/docs/widget/Stepper.md +4 -0
  88. package/docs/widget/Switch.md +4 -0
  89. package/docs/widget/Tabs.md +5 -5
  90. package/docs/widget/Text.md +4 -0
  91. package/docs/widget/Tooltip.md +14 -13
  92. package/docs/widget/TreeView.md +4 -0
  93. package/docs/widget/Video.md +6 -2
  94. package/package.json +2 -2
  95. package/src/animations/AnimatedBox.js +4 -1
  96. package/src/animations/MatrixRain.js +3 -3
  97. package/src/core/hmr-client.js +129 -34
  98. package/src/index.d.ts +195 -7
  99. package/src/index.js +2 -0
  100. package/src/navigations/AppBar.js +22 -16
  101. package/src/navigations/BottomNavigation.js +8 -13
  102. package/src/navigations/CollapsibleSideBar.js +30 -8
  103. package/src/navigations/Drawer.js +25 -15
  104. package/src/navigations/DrawerItem.js +3 -1
  105. package/src/navigations/Scaffold.js +26 -12
  106. package/src/navigations/Tabs.js +104 -59
  107. package/src/tools/color.js +54 -0
  108. package/src/tools/index.js +9 -1
  109. package/src/tools/scale.js +41 -0
  110. package/src/tools/shadow.js +20 -0
  111. package/src/utils/index.js +1 -0
  112. package/src/utils/readableInk.js +19 -0
  113. package/src/utils/syntaxHighlight.js +78 -10
  114. package/src/utils/themes.js +217 -54
  115. package/src/utils/units.js +33 -11
  116. package/src/widget-factory/effects.js +56 -5
  117. package/src/widget-factory/processProps.js +10 -1
  118. package/src/widget-factory/remTool.js +15 -29
  119. package/src/widget-factory/stackPosition.js +18 -3
  120. package/src/widget-factory/translateProps.js +60 -1
  121. package/src/widgets/Accordion.js +20 -23
  122. package/src/widgets/AdMob.js +7 -2
  123. package/src/widgets/Avatar.js +236 -133
  124. package/src/widgets/Badge.js +3 -2
  125. package/src/widgets/BottomSheet.js +14 -13
  126. package/src/widgets/Button.js +69 -30
  127. package/src/widgets/Card.js +6 -2
  128. package/src/widgets/Carousel.js +12 -10
  129. package/src/widgets/Chart.js +28 -5
  130. package/src/widgets/Checkbox.js +76 -18
  131. package/src/widgets/Chip.js +31 -10
  132. package/src/widgets/CircularBar.js +7 -7
  133. package/src/widgets/CircularChart.js +26 -8
  134. package/src/widgets/CodeViewer.js +93 -20
  135. package/src/widgets/Container.js +13 -0
  136. package/src/widgets/DataTable.js +17 -11
  137. package/src/widgets/DraggBox.js +2 -1
  138. package/src/widgets/Dropdown.js +59 -14
  139. package/src/widgets/DroppBox.js +4 -2
  140. package/src/widgets/FloatingActionButton.js +13 -7
  141. package/src/widgets/Icon.js +3 -1
  142. package/src/widgets/Image.js +102 -3
  143. package/src/widgets/Input.js +30 -23
  144. package/src/widgets/Inspector.js +54 -1
  145. package/src/widgets/InstallButton.js +6 -4
  146. package/src/widgets/ListTile.js +4 -3
  147. package/src/widgets/ListView.js +30 -12
  148. package/src/widgets/Maps.js +1495 -0
  149. package/src/widgets/Markdown.js +37 -26
  150. package/src/widgets/Modal.js +14 -11
  151. package/src/widgets/Pagination.js +16 -11
  152. package/src/widgets/ProgressBar.js +6 -4
  153. package/src/widgets/Radio.js +73 -15
  154. package/src/widgets/Rating.js +24 -6
  155. package/src/widgets/Skeleton.js +31 -31
  156. package/src/widgets/Slider.js +29 -18
  157. package/src/widgets/SnackBar.js +35 -39
  158. package/src/widgets/Stepper.js +13 -9
  159. package/src/widgets/Switch.js +59 -18
  160. package/src/widgets/Tooltip.js +24 -12
  161. package/src/widgets/TreeView.js +27 -15
  162. package/src/widgets/index.js +1 -0
@@ -0,0 +1,1173 @@
1
+ /**
2
+ * `flet-box unpack` โ€” take a widget apart and show what is provably true about it.
3
+ *
4
+ * `check-api --props` answers "does this prop exist?". This answers the other
5
+ * questions, the ones that cost a dev an afternoon: where the prop is handled,
6
+ * what it is an alias of, whether the widget really reads it, what its default
7
+ * is, and where the three API surfaces disagree.
8
+ *
9
+ * Every line is derived from the installed package at run time โ€” the barrel,
10
+ * `translateProps.js`, `src/index.d.ts` and the widget's own source file โ€” so
11
+ * the output cannot drift from the code the way a hand-written reference can.
12
+ *
13
+ * flet-box unpack Button every prop it can receive, and which of
14
+ * them actually do something
15
+ * flet-box unpack Button --diff declared vs documented vs implemented,
16
+ * the disagreements only
17
+ * flet-box unpack Button --example a paste-ready call built from props the
18
+ * widget provably reads
19
+ * flet-box unpack Button --json the same contract as data
20
+ * flet-box unpack --all every widget โ†’ .fletbox/api/api.json
21
+ * flet-box unpack --all --diff which widgets disagree with themselves
22
+ * flet-box unpack --all --md and the readable form โ†’ api.md
23
+ * flet-box unpack --json whole tree on stdout, for the app's AI agent
24
+ */
25
+ import fs from "node:fs";
26
+ import path from "node:path";
27
+ import { c, banner, section } from "../utils/colors.js";
28
+ import { neutralise } from "../utils/neutralise.js";
29
+ import { membersOf, propsInterfaceFor, resolveInterfaceProps } from "../utils/dtsProps.js";
30
+ import { STRUCTURAL_PROPS, loadApi, nearest } from "./checkApi.js";
31
+
32
+ /**
33
+ * Index of the character closing the bracket opened at `open`, or -1.
34
+ *
35
+ * @param {string} text Neutralised source, so every bracket here is syntax.
36
+ * @param {number} open
37
+ * @returns {number}
38
+ */
39
+ const matchPair = (text, open) => {
40
+ const close = { "{": "}", "[": "]", "(": ")" }[text[open]];
41
+ if (!close) return -1;
42
+ let depth = 0;
43
+ for (let i = open; i < text.length; i++) {
44
+ if (text[i] === text[open]) depth++;
45
+ else if (text[i] === close) {
46
+ depth--;
47
+ if (depth === 0) return i;
48
+ }
49
+ }
50
+ return -1;
51
+ };
52
+
53
+ /**
54
+ * Every name a file defines, mapped to its file.
55
+ *
56
+ * A widget is usually `src/widgets/Name.js`, but navigations and tools live
57
+ * elsewhere, so the barrel's own layout is read instead of guessed.
58
+ *
59
+ * @param {string} packageDir Installed package root.
60
+ * @returns {Map<string, string>} name โ†’ absolute file
61
+ */
62
+ const definitionIndex = (packageDir) => {
63
+ const index = new Map();
64
+ const reexports = [];
65
+ const src = path.join(packageDir, "src");
66
+ if (!fs.existsSync(src)) return index;
67
+ const files = [];
68
+ const walk = (dir) => {
69
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
70
+ const full = path.join(dir, entry.name);
71
+ if (entry.isDirectory()) walk(full);
72
+ else if (/\.m?js$/.test(entry.name)) files.push(full);
73
+ }
74
+ };
75
+ walk(src);
76
+ for (const file of files) {
77
+ const code = neutralise(fs.readFileSync(file, "utf8"), { keepStrings: true });
78
+ for (const m of code.matchAll(
79
+ /\bexport\s+(?:async\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/g,
80
+ )) {
81
+ if (!index.has(m[1])) index.set(m[1], file);
82
+ }
83
+ // `export { Real as Alias }` is only a pointer: it must never win over the
84
+ // file that actually defines the name, or every widget maps to the barrel.
85
+ for (const m of code.matchAll(/\bexport\s*\{([^}]*)\}/g)) {
86
+ for (const token of m[1].split(",")) {
87
+ const aliased = /^\s*([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)\s*$/.exec(token);
88
+ const plain = /^\s*([A-Za-z_$][\w$]*)\s*$/.exec(token);
89
+ const name = aliased ? aliased[2] : plain ? plain[1] : null;
90
+ if (name) reexports.push([name, file]);
91
+ }
92
+ }
93
+ }
94
+ for (const [name, file] of reexports) if (!index.has(name)) index.set(name, file);
95
+ return index;
96
+ };
97
+
98
+ /**
99
+ * Which props a widget source actually reads.
100
+ *
101
+ * Two forms matter: a destructuring pattern whose initialiser is `props` (with
102
+ * its default values) and plain `props.name` access. Keys reaching the factory
103
+ * through a `...rest` spread are reported separately, because then every engine
104
+ * prop is live without being named.
105
+ *
106
+ * @param {string} file
107
+ * @returns {{ read: Map<string, string|null>, spread: boolean, child: boolean, children: boolean }}
108
+ */
109
+ const readProps = (file) => {
110
+ const raw = fs.readFileSync(file, "utf8");
111
+ const code = neutralise(raw);
112
+ const read = new Map();
113
+ let spread = false;
114
+
115
+ /** Top-level member spans of a pattern, using the blanked text for structure. */
116
+ const memberSpans = (from, to) => {
117
+ const spans = [];
118
+ let depth = 0;
119
+ let start = from;
120
+ for (let i = from; i < to; i++) {
121
+ const ch = code[i];
122
+ if (ch === "{" || ch === "[" || ch === "(") depth++;
123
+ else if (ch === "}" || ch === "]" || ch === ")") depth--;
124
+ else if (ch === "," && depth === 0) {
125
+ spans.push([start, i]);
126
+ start = i + 1;
127
+ }
128
+ }
129
+ if (to > start) spans.push([start, to]);
130
+ return spans;
131
+ };
132
+
133
+ const trim = (s, e) => {
134
+ while (s < e && /\s/.test(code[s])) s++;
135
+ while (e > s && /\s/.test(code[e - 1])) e--;
136
+ return [s, e];
137
+ };
138
+
139
+ /** Index of the first `=` at depth 0, or -1. */
140
+ const assignment = (s, e) => {
141
+ let depth = 0;
142
+ for (let i = s; i < e; i++) {
143
+ const ch = code[i];
144
+ if (ch === "{" || ch === "[" || ch === "(") depth++;
145
+ else if (ch === "}" || ch === "]" || ch === ")") depth--;
146
+ else if (ch === "=" && depth === 0) return i;
147
+ }
148
+ return -1;
149
+ };
150
+
151
+ const collect = (from, to) => {
152
+ for (const [a, b] of memberSpans(from, to)) {
153
+ const [s, e] = trim(a, b);
154
+ if (s >= e) continue;
155
+ if (code.startsWith("...", s)) {
156
+ spread = true;
157
+ continue;
158
+ }
159
+ const eq = assignment(s, e);
160
+ const nameEnd = eq < 0 ? e : eq;
161
+ // A rename (`marginTop: propMarginTop`) or a nested pattern (`margin: { top }`)
162
+ // keys on the left of the colon; the identifier itself comes from the raw
163
+ // file, because only there are the string defaults still readable.
164
+ const keyEnd = (() => {
165
+ let depth = 0;
166
+ for (let i = s; i < nameEnd; i++) {
167
+ const ch = code[i];
168
+ if (ch === "{" || ch === "[" || ch === "(") depth++;
169
+ else if (ch === "}" || ch === "]" || ch === ")") depth--;
170
+ else if (ch === ":" && depth === 0) return i;
171
+ }
172
+ return -1;
173
+ })();
174
+ const key = raw.slice(s, keyEnd < 0 ? nameEnd : keyEnd).trim();
175
+ if (/^[A-Za-z_$][\w$]*$/.test(key)) {
176
+ const def = eq < 0 ? null : raw.slice(...trim(eq + 1, e)).replace(/\s+/g, " ");
177
+ // A helper in the same file may destructure the prop without its
178
+ // default; keep the occurrence that actually carries one.
179
+ if (!read.has(key) || (read.get(key) === null && def !== null)) read.set(key, def);
180
+ }
181
+ if (keyEnd >= 0) {
182
+ const inner = trim(keyEnd + 1, nameEnd)[0];
183
+ if (code[inner] === "{" || code[inner] === "[") {
184
+ const close = matchPair(code, inner);
185
+ if (close > 0) collect(inner + 1, close);
186
+ }
187
+ }
188
+ }
189
+ };
190
+
191
+ let i = 0;
192
+ while (i < code.length) {
193
+ const open = code.indexOf("{", i);
194
+ if (open < 0) break;
195
+ const close = matchPair(code, open);
196
+ if (close < 0) break;
197
+ if (/^\s*=\s*props\b/.test(code.slice(close + 1, close + 24))) {
198
+ // Offsets are preserved, so every slice above can mix the blanked text
199
+ // (structure) with the raw one (values).
200
+ collect(open + 1, close);
201
+ i = close + 1;
202
+ continue;
203
+ }
204
+ // Not a destructuring of `props`: step inside it rather than over it, so a
205
+ // function body does not hide the patterns declared within it.
206
+ i = open + 1;
207
+ }
208
+ for (const m of code.matchAll(/\bprops\.([A-Za-z_$][\w$]*)/g)) {
209
+ if (!read.has(m[1])) read.set(m[1], null);
210
+ }
211
+ return {
212
+ read,
213
+ spread,
214
+ child: /\bchild\b/.test(code),
215
+ children: /\bchildren\b/.test(code),
216
+ };
217
+ };
218
+
219
+ /**
220
+ * Props that compile to the same DOM target, so they are aliases by definition.
221
+ *
222
+ * @param {object} api
223
+ * @returns {Map<string, string[]>} prop โ†’ the other names for the same thing
224
+ */
225
+ const aliasGroups = (api) => {
226
+ const byTarget = new Map();
227
+ for (const def of api.ALL_PROPS) {
228
+ const key = `${def.type}:${def.domProp}`;
229
+ if (!byTarget.has(key)) byTarget.set(key, []);
230
+ byTarget.get(key).push(def.prop);
231
+ }
232
+ const groups = new Map();
233
+ for (const list of byTarget.values()) {
234
+ for (const name of list) {
235
+ groups.set(name, list.filter((other) => other !== name));
236
+ }
237
+ }
238
+ return groups;
239
+ };
240
+
241
+ /**
242
+ * Prop names the widget's own doc page promises (`## Props` table only).
243
+ *
244
+ * `### Common props` is left out: those arrive through `CommonProps`, which the
245
+ * interface chain already resolves, so counting them twice would only invent
246
+ * differences.
247
+ *
248
+ * @param {string|null} mdFile
249
+ * @returns {string[]}
250
+ */
251
+ const docPropNames = (mdFile) => {
252
+ if (!mdFile || !fs.existsSync(mdFile)) return [];
253
+ const lines = fs.readFileSync(mdFile, "utf8").split("\n");
254
+ const names = [];
255
+ let inside = false;
256
+ for (const line of lines) {
257
+ if (/^##\s+props\s*$/i.test(line.trim())) {
258
+ inside = true;
259
+ continue;
260
+ }
261
+ if (inside && /^#{1,2}\s/.test(line)) break;
262
+ if (!inside || !line.trim().startsWith("|")) continue;
263
+ const first = line.split("|")[1];
264
+ const name = /^\s*`([A-Za-z_$][\w$]*)`/.exec(first || "");
265
+ if (name) names.push(name[1]);
266
+ }
267
+ return names;
268
+ };
269
+
270
+ /**
271
+ * How this export is called and what it hands back, read from its signature.
272
+ *
273
+ * Most widgets take a props object and return an element to mount. Overlays
274
+ * return a controller you drive with methods, so `const view = Modal({ โ€ฆ })` on
275
+ * its own shows nothing. A few take positional arguments at all, and a snippet
276
+ * that wrapped them in `{ โ€ฆ }` would silently do nothing.
277
+ *
278
+ * @param {object} api
279
+ * @param {string} name
280
+ * @returns {{ params: object[], positional: boolean, type: string|null, element: boolean|null, members: string[] }}
281
+ */
282
+ const describeCall = (api, name) => {
283
+ const sig = api.signatures?.get(name);
284
+ if (!sig) return { params: [], positional: false, type: null, element: null, members: [] };
285
+ const type = sig.returns;
286
+ let members = membersOf(type);
287
+ // `DrawerInstance` is declared elsewhere: read its interface instead.
288
+ if (!members.length && /^[A-Za-z_$][\w$]*$/.test(type)) {
289
+ const iface = api.interfaces.get(type);
290
+ if (iface)
291
+ members = [...iface.props].map(([key, info]) =>
292
+ /=>/.test(info.type) || /^\(/.test(info.type) ? `${key}()` : key,
293
+ );
294
+ }
295
+ const first = sig.params[0];
296
+ // Only a scalar first argument means the call really is positional. SnackBar
297
+ // takes `options: SnackBarOptions`, which is still one object literal.
298
+ const takesProps = Boolean(first) && (/(?:Props|Options|Config|Settings)\b/.test(first.type) || first.type.startsWith("{"));
299
+ return {
300
+ params: sig.params,
301
+ positional: sig.params.length > 0 && !takesProps,
302
+ type,
303
+ element: /^Widget\b|^HTMLElement\b/.test(type),
304
+ members,
305
+ };
306
+ };
307
+
308
+ const INLINE = "(inline)";
309
+
310
+ /**
311
+ * Which declaration types this export's props, if any.
312
+ *
313
+ * Most widgets use `<Name>Props`, a few reuse `<Name>Options`, and `App` and
314
+ * `ThemeProvider` are typed by an object written straight into the signature.
315
+ * Anything else is a singleton API (Storage, Session) or a helper, not a widget.
316
+ *
317
+ * @param {object} api
318
+ * @param {string} name
319
+ * @returns {string|null} interface name, `INLINE`, or null
320
+ */
321
+ const propsSource = (api, name) => {
322
+ const iface = propsInterfaceFor(api.interfaces, name);
323
+ if (iface) return iface;
324
+ const first = api.signatures?.get(name)?.params?.[0];
325
+ return first && first.type.startsWith("{") ? INLINE : null;
326
+ };
327
+
328
+ /**
329
+ * Build the full contract of one widget.
330
+ *
331
+ * @param {object} api
332
+ * @param {string} name
333
+ * @param {Map<string, string>} definitions
334
+ * @returns {object|null} null when the package has no such export
335
+ */
336
+ const unpackWidget = (api, name, definitions) => {
337
+ const typedBy = propsSource(api, name);
338
+ const ifaceName = typedBy && typedBy !== INLINE ? typedBy : `${name}Props`;
339
+ const file = definitions.get(name);
340
+ if (!api.exports.includes(name)) return null;
341
+ const inline =
342
+ typedBy === INLINE ? objectFields(api.signatures.get(name).params[0].type.slice(1, -1)) : null;
343
+ const declared =
344
+ typedBy === INLINE
345
+ ? new Map(inline.map((f) => [f.prop, { type: f.type, optional: f.optional, declaredBy: INLINE }]))
346
+ : api.interfaces.has(ifaceName)
347
+ ? resolveInterfaceProps(api.interfaces, ifaceName)
348
+ : new Map();
349
+ const own =
350
+ typedBy === INLINE
351
+ ? new Map(inline.map((f) => [f.prop, f]))
352
+ : api.interfaces.has(ifaceName)
353
+ ? api.interfaces.get(ifaceName).props
354
+ : new Map();
355
+ const aliases = aliasGroups(api);
356
+ const source = file ? readProps(file) : { read: new Map(), spread: false, child: false, children: false };
357
+
358
+ const props = [];
359
+ const names = new Set([...declared.keys(), ...source.read.keys()]);
360
+ for (const prop of [...names].sort()) {
361
+ const info = declared.get(prop);
362
+ const def = api.PROP_MAP[prop];
363
+ const entry = {
364
+ prop,
365
+ declared: Boolean(info),
366
+ own: own.has(prop),
367
+ type: info ? info.type : null,
368
+ optional: info ? info.optional : null,
369
+ declaredBy: info ? info.declaredBy : null,
370
+ engine: def
371
+ ? { domProp: def.domProp, kind: def.type, unit: def.unit || null, reactive: Boolean(def.reactive) }
372
+ : null,
373
+ aliasOf: aliases.get(prop) || [],
374
+ read: source.read.has(prop),
375
+ default: source.read.get(prop) ?? null,
376
+ structural: STRUCTURAL_PROPS.has(prop),
377
+ };
378
+ // Every JSON consumer gets a value it can paste, not just the prop name.
379
+ entry.sample = exampleValue(entry, api);
380
+ props.push(entry);
381
+ }
382
+
383
+ const dead = props.filter(
384
+ (c_) => c_.declared && !c_.engine && !c_.read && !c_.structural,
385
+ );
386
+ const invented = props.filter(
387
+ (c_) => !c_.declared && !c_.engine && c_.read && !c_.structural,
388
+ );
389
+ const hidden = props.filter((c_) => c_.engine && !c_.declared && !c_.structural);
390
+
391
+ const docFile = fs.existsSync(path.join(api.dir, "docs", "widget", `${name}.md`))
392
+ ? path.join(api.dir, "docs", "widget", `${name}.md`)
393
+ : null;
394
+ const documented = docPropNames(docFile);
395
+ const docOnly = documented.filter(
396
+ (prop) => !api.PROP_MAP[prop] && !declared.has(prop) && !source.read.has(prop) && !STRUCTURAL_PROPS.has(prop),
397
+ );
398
+ // Only this widget's own interface: inherited props are documented once, in
399
+ // the Common props table, and `read || own` keeps utilities out of the list.
400
+ const codeOnly = props
401
+ .filter((p) => !p.structural && (p.read || p.own) && !documented.includes(p.prop))
402
+ .map((p) => p.prop);
403
+
404
+ return {
405
+ name,
406
+ exported: true,
407
+ interface: typedBy === INLINE ? INLINE : api.interfaces.has(ifaceName) ? ifaceName : null,
408
+ call: describeCall(api, name),
409
+ sourceFile: file || null,
410
+ docFile,
411
+ documented,
412
+ readsCount: [...source.read.keys()].filter((key) => !STRUCTURAL_PROPS.has(key)).length,
413
+ props,
414
+ events: props.filter((c_) => c_.engine && c_.engine.kind === "event"),
415
+ spreadsRest: source.spread,
416
+ accepts: {
417
+ child: source.read.has("child") || declared.has("child") || source.child,
418
+ children: source.read.has("children") || declared.has("children") || source.children,
419
+ },
420
+ gaps: { dead, invented, hidden, docOnly, codeOnly },
421
+ };
422
+ };
423
+
424
+ /* ------------------------------------------------------------------ print */
425
+
426
+ const cell = (text, color, width_) => c(color, String(text).padEnd(width_));
427
+
428
+ /**
429
+ * One line about how the widget is called.
430
+ *
431
+ * @param {object} data
432
+ * @returns {string}
433
+ */
434
+ const describeCallLine = (data) => {
435
+ const r = data.call || {};
436
+ if (!r.params?.length) return c("gray", "not declared in index.d.ts");
437
+ const shape = r.params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ");
438
+ if (!r.positional)
439
+ return `${c("brightGreen", "a props object")} ${c("gray", `({ โ€ฆ }) โ€” typed by ${r.params[0].type}`)}`;
440
+ return `${c("red", "positional arguments, NOT a props object")} ${c("gray", `(${shape})`)}`;
441
+ };
442
+
443
+ /**
444
+ * One line about what the widget hands back.
445
+ *
446
+ * @param {object} data
447
+ * @returns {string}
448
+ */
449
+ const describeReturnLine = (data) => {
450
+ const r = data.call || {};
451
+ if (!r.type) return c("gray", "not declared in index.d.ts");
452
+ // What you can call beats the raw type: an inline return object is hundreds of
453
+ // characters, its member list is the part that matters.
454
+ const detail = r.members.length
455
+ ? c("brightMagenta", `ยท ${r.members.slice(0, 9).join(" ยท ")}${r.members.length > 9 ? ` +${r.members.length - 9}` : ""}`)
456
+ : c("gray", `(${r.type})`);
457
+ if (r.element) return `${c("brightGreen", "an element you mount")} ${detail}`;
458
+ return `${c("yellow", "a controller โ€” nothing shows until you call it")} ${detail}`;
459
+ };
460
+
461
+ /**
462
+ * @param {object} data unpackWidget() output
463
+ * @param {object} api
464
+ * @returns {string[]}
465
+ */
466
+ const renderWidget = (data, api) => {
467
+ const out = [];
468
+ const label = (text) => c("gray", text.padEnd(7));
469
+ const styleProps = data.props.filter((p) => !p.engine || p.engine.kind !== "event");
470
+ out.push(
471
+ section("๐Ÿ“ฆ", `${data.name} โ€” ${data.props.length} props known to this widget`),
472
+ );
473
+ out.push(` ${label("source")} ${data.sourceFile ? path.relative(api.dir, data.sourceFile) : c("yellow", "not found in the package")}`);
474
+ out.push(` ${label("types")} ${data.interface ? (data.interface === INLINE ? c("gray", "props typed inline in its declaration") : data.interface) : c("yellow", "no <Name>Props in index.d.ts")}`);
475
+ out.push(` ${label("docs")} ${data.docFile ? path.relative(api.dir, data.docFile) : c("yellow", "no docs/widget page")}`);
476
+ out.push(` ${label("call")} ${describeCallLine(data)}`);
477
+ out.push(` ${label("returns")} ${describeReturnLine(data)}`);
478
+ out.push(
479
+ ` ${label("shape")} ${data.accepts.child ? c("brightGreen", "child โœ“") : c("gray", "child โœ—")} ` +
480
+ `${data.accepts.children ? c("brightGreen", "children โœ“") : c("gray", "children โœ—")} ` +
481
+ c("gray", `ยท reads ${data.readsCount} props by name`),
482
+ );
483
+ out.push(
484
+ ` ${label("reaches")} ${data.spreadsRest
485
+ ? c("gray", `the engine: every prop not named here is still passed through the ${c("brightWhite", "...rest")} spread`)
486
+ : c("brightYellow", `the engine: only the props named here โ€” an unlisted prop is dropped`)}`,
487
+ );
488
+
489
+ out.push(section("๐Ÿงฑ", `Props (${styleProps.length})`, "#a855f7"));
490
+ out.push(
491
+ ` ${c("gray", "prop".padEnd(18))}${c("gray", "declared type".padEnd(36))}${c("gray", "engine target".padEnd(20))}aliases ยท defaults ยท provenance`,
492
+ );
493
+ for (const p of styleProps) {
494
+ const flags = [];
495
+ if (p.read) flags.push(c("brightGreen", "read"));
496
+ if (p.engine?.reactive) flags.push(c("brightCyan", "reactive"));
497
+ if (p.aliasOf.length)
498
+ flags.push(
499
+ c("brightYellow", `โ‰ก ${p.aliasOf.slice(0, 3).join(", ")}${p.aliasOf.length > 3 ? ` +${p.aliasOf.length - 3}` : ""}`),
500
+ );
501
+ if (p.default !== null) flags.push(c("brightGreen", `= ${p.default}`));
502
+ if (!p.declared) flags.push(c("red", "not in index.d.ts"));
503
+ else if (!p.own) flags.push(c("gray", "inherited"));
504
+ if (!p.engine && p.read && !p.structural) flags.push(c("magenta", "read by the widget"));
505
+ const route = p.structural
506
+ ? c("gray", "structural")
507
+ : p.engine
508
+ ? c("green", `${p.engine.domProp}${p.engine.unit && p.engine.unit !== "none" ? `:${p.engine.unit}` : ""}`)
509
+ : c("gray", "โ€”");
510
+ const type = (p.type || "any").length > 34 ? `${p.type.slice(0, 31)}โ€ฆ` : p.type || c("red", "no declared type");
511
+ out.push(
512
+ ` ${cell(p.prop + (p.declared && p.optional ? "?" : ""), "brightWhite", 18)}${cell(type, "cyan", 36)}${cell(route, "", 20)}${flags.join(" ")}`,
513
+ );
514
+ }
515
+
516
+ if (data.events.length) {
517
+ out.push(section("๐ŸŽฏ", `Events (${data.events.length})`, "#f59e0b"));
518
+ for (const p of data.events) {
519
+ const alias = p.aliasOf.filter((a) => a !== p.prop).slice(0, 2).join(", ");
520
+ out.push(
521
+ ` ${cell(p.prop, "brightWhite", 17)}` +
522
+ `${cell(p.engine.domProp, "green", 18)}` +
523
+ (p.declared ? c("cyan", (p.type || "").slice(0, 40)) : c("red", "not declared")),
524
+ );
525
+ if (alias) out.push(` ${" ".repeat(17)}${c("brightYellow", `โ‰ก ${alias}`)}`);
526
+ }
527
+ }
528
+
529
+ const { dead, invented, hidden, docOnly, codeOnly } = data.gaps;
530
+ if (dead.length || invented.length || hidden.length || docOnly.length || codeOnly.length) {
531
+ out.push(section("โš ๏ธ", "Surface gaps", "#ef4444"));
532
+ for (const p of dead)
533
+ out.push(
534
+ ` ${c("red", "DEAD")} ${p.prop}: declared in ${p.declaredBy || "index.d.ts"}, routed by nothing and read by nothing โ€” it has no effect`,
535
+ );
536
+ for (const p of invented)
537
+ out.push(
538
+ ` ${c("brightYellow", "INVENTED")} ${p.prop}: the widget reads it, but no interface declares it and no engine entry routes it`,
539
+ );
540
+ for (const p of hidden)
541
+ out.push(
542
+ ` ${c("yellow", "UNDOCUMENTED")} ${p.prop}: the engine handles it for this widget but ${data.interface || "the interface"} does not declare it`,
543
+ );
544
+ for (const prop of docOnly)
545
+ out.push(
546
+ ` ${c("brightRed", "DOC-ONLY")} ${prop}: documented in ${data.docFile ? path.basename(data.docFile) : "the doc page"} and in nothing else โ€” writing it does nothing`,
547
+ );
548
+ for (const prop of codeOnly)
549
+ out.push(
550
+ ` ${c("brightMagenta", "CODE-ONLY")} ${prop}: the widget handles it, but its doc page never mentions it`,
551
+ );
552
+ } else {
553
+ out.push(`\n ${c("brightGreen", "โœ“")} ${c("gray", `types, engine, docs and implementation agree โ€” nothing dead or invented`)}`);
554
+ }
555
+ out.push(
556
+ `\n ${c("gray", `props table: flet-box check-api --props ${data.name} ยท offline gate: flet-box check-api`)}`,
557
+ );
558
+ return out;
559
+ };
560
+
561
+ /**
562
+ * The doc-vs-code disagreements alone, for a quick "is this page lying?".
563
+ *
564
+ * @param {object} data
565
+ * @returns {string[]}
566
+ */
567
+ const renderDiff = (data) => {
568
+ const { dead, invented, hidden, docOnly, codeOnly } = data.gaps;
569
+ const rows = [
570
+ ["DEAD", dead.map((p) => p.prop), "in index.d.ts, handled by nothing"],
571
+ ["INVENTED", invented.map((p) => p.prop), "read by the widget, absent from every type"],
572
+ ["UNDOCUMENTED", hidden.map((p) => p.prop), "the engine routes it, the interface hides it"],
573
+ ["DOC-ONLY", docOnly, "the doc page promises it, the code has never heard of it"],
574
+ ["CODE-ONLY", codeOnly, "the code accepts it, the doc page omits it"],
575
+ ];
576
+ const out = [
577
+ section("๐Ÿ”ฌ", `${data.name} โ€” declared vs real`, "#f59e0b"),
578
+ ` ${c("gray", "types")} ${data.interface || "โ€”"} ${c("gray", "code")} ${data.sourceFile ? path.relative(".", data.sourceFile) : "โ€”"} ${c("gray", "docs")} ${data.docFile ? path.relative(".", data.docFile) : "โ€”"}`,
579
+ ];
580
+ for (const [name, list, meaning] of rows) {
581
+ if (!list.length) continue;
582
+ out.push(` ${c("brightRed", name.padEnd(14))} ${list.join(", ")}`);
583
+ out.push(` ${" ".repeat(14)} ${c("gray", meaning)}`);
584
+ }
585
+ if (out.length === 2)
586
+ out.push(` ${c("brightGreen", "โœ“")} ${c("gray", "every surface agrees: nothing to reconcile")}`);
587
+ return out;
588
+ };
589
+
590
+ /**
591
+ * Split the body of an inline object type (`icon: string; label?: string`) into
592
+ * `{ prop, type }` pairs. Semicolons inside `<โ€ฆ>` or `{โ€ฆ}` do not end a field.
593
+ *
594
+ * @param {string} body
595
+ * @returns {Array<{ prop: string, type: string }>}
596
+ */
597
+ const objectFields = (body) => {
598
+ const parts = [];
599
+ let depth = 0;
600
+ let current = "";
601
+ for (const ch of body) {
602
+ if (ch === "{" || ch === "[" || ch === "(" || ch === "<") depth++;
603
+ else if (ch === "}" || ch === "]" || ch === ")" || ch === ">") depth--;
604
+ if (ch === ";" && depth === 0) {
605
+ parts.push(current);
606
+ current = "";
607
+ continue;
608
+ }
609
+ current += ch;
610
+ }
611
+ parts.push(current);
612
+ return parts
613
+ .map((part) => /^\s*([A-Za-z_$][\w$]*)\s*(\??)\s*:\s*([\s\S]+)$/.exec(part.trim()))
614
+ .filter(Boolean)
615
+ .map((m) => ({ prop: m[1], optional: m[2] === "?", type: m[3].trim() }));
616
+ };
617
+
618
+ /**
619
+ * Split at a separator that sits outside every bracket: `number[] | Array<{ a:
620
+ * number }>` is two members, `Record<string, any>` keeps its comma whole.
621
+ *
622
+ * @param {string} raw
623
+ * @param {string} separator
624
+ * @returns {string[]}
625
+ */
626
+ const splitTop = (raw, separator) => {
627
+ const parts = [];
628
+ let depth = 0;
629
+ let current = "";
630
+ for (const ch of raw) {
631
+ if (ch === "{" || ch === "[" || ch === "(" || ch === "<") depth++;
632
+ else if (ch === "}" || ch === "]" || ch === ")" || ch === ">") depth--;
633
+ if (ch === separator && depth === 0) {
634
+ parts.push(current);
635
+ current = "";
636
+ continue;
637
+ }
638
+ current += ch;
639
+ }
640
+ parts.push(current);
641
+ return parts.map((part) => part.trim()).filter(Boolean);
642
+ };
643
+
644
+ /**
645
+ * @param {string} raw
646
+ * @returns {string[]}
647
+ */
648
+ const unionMembers = (raw) => splitTop(raw, "|");
649
+
650
+ /**
651
+ * The element type of a list, or null when the type is not a list.
652
+ *
653
+ * Arrays are written three different ways in `index.d.ts` (`T[]`, `Array<T>`, and
654
+ * unions of both), and the richest member is the one worth printing: a candle
655
+ * series explains `Chart` better than `[1, 2, 3]` does.
656
+ *
657
+ * @param {string} raw
658
+ * @param {object} api
659
+ * @returns {string|null}
660
+ */
661
+ const arrayElement = (raw, api) => {
662
+ const elements = [];
663
+ for (const member of unionMembers(raw)) {
664
+ const tuple = /^\[([\s\S]*)\]$/.exec(member);
665
+ if (tuple) {
666
+ elements.push(...splitTop(tuple[1], ","));
667
+ continue;
668
+ }
669
+ const angled = /^\s*Array\s*<([\s\S]*)>\s*$/i.exec(member);
670
+ if (angled) {
671
+ elements.push(angled[1].trim());
672
+ continue;
673
+ }
674
+ const boxed = /^([\s\S]*?)\[\]\s*$/.exec(member);
675
+ if (boxed && boxed[1].trim()) elements.push(boxed[1].trim());
676
+ }
677
+ if (!elements.length) return null;
678
+ return (
679
+ elements.find((e) => /\{/.test(e)) ||
680
+ elements.find((e) => /^[A-Za-z_$][\w$]*$/.test(e) && api?.interfaces.has(e)) ||
681
+ elements[0]
682
+ );
683
+ };
684
+
685
+ /**
686
+ * One object literal built from the fields that make this shape work.
687
+ *
688
+ * @param {Array<{ prop: string, type: string, optional?: boolean }>} fields
689
+ * @param {object} api
690
+ * @returns {string|null}
691
+ */
692
+ const sampleObject = (fields, api) => {
693
+ const picked = fields
694
+ .filter((f) => !/=>/.test(f.type))
695
+ .sort((a, b) => Number(b.optional === false) - Number(a.optional === false))
696
+ .slice(0, 4);
697
+ if (!picked.length) return null;
698
+ return `{ ${picked.map((f) => `${f.prop}: ${exampleValue(f, api)}`).join(", ")} }`;
699
+ };
700
+
701
+ /**
702
+ * The type a declaration really means, after the two shorthands `index.d.ts`
703
+ * uses: `ContainerProps['alignItems']` is that field's own type, and
704
+ * `Partial<TextProps>` is TextProps with every field optional.
705
+ *
706
+ * @param {string} raw
707
+ * @param {object} api
708
+ * @param {number} [depth]
709
+ * @returns {string}
710
+ */
711
+ const resolveType = (raw, api, depth = 0) => {
712
+ const trimmed = (raw || "").trim();
713
+ if (!trimmed || depth > 4) return trimmed;
714
+ const indexed = /^([A-Za-z_$][\w$]*)\s*\[\s*['"]([^'"]+)['"]\s*\]$/.exec(trimmed);
715
+ if (indexed) {
716
+ const field = api?.interfaces.get(indexed[1])?.props.get(indexed[2]);
717
+ return field ? resolveType(field.type, api, depth + 1) : trimmed;
718
+ }
719
+ const wrapped = /^(?:Partial|Required|Omit|Pick)\s*<\s*([A-Za-z_$][\w$]*)/i.exec(trimmed);
720
+ if (wrapped) return resolveType(wrapped[1], api, depth + 1);
721
+ return trimmed;
722
+ };
723
+
724
+ /**
725
+ * An object literal that satisfies this type, or null when it describes a
726
+ * primitive. Reads the four ways `index.d.ts` writes a shape โ€” inline `{ a: 1 }`,
727
+ * a named interface, `Record<key, value>` and a union of those โ€” and keeps the
728
+ * richest member it can build, so `(string | DataTableColumn)[]` prints a column
729
+ * instead of a string.
730
+ *
731
+ * @param {string} type
732
+ * @param {object} api
733
+ * @returns {string|null}
734
+ */
735
+ const objectSampleFor = (type, api) => {
736
+ const trimmed = resolveType(type, api);
737
+ const members = unionMembers(/^\(([\s\S]*)\)$/.test(trimmed) ? trimmed.slice(1, -1) : trimmed);
738
+ for (const member of members) {
739
+ const braces = /\{([\s\S]*)\}/.exec(member);
740
+ if (braces) {
741
+ const built = sampleObject(objectFields(braces[1]), api);
742
+ if (built) return built;
743
+ continue;
744
+ }
745
+ const record = /^Record\s*<([\s\S]*)>$/i.exec(member);
746
+ if (record) {
747
+ const value = splitTop(record[1], ",").at(-1);
748
+ const primitive = { string: '"value"', number: "1", boolean: "true" }[value];
749
+ return `{ id: ${primitive || objectSampleFor(value, api) || "1"} }`;
750
+ }
751
+ const named = api?.interfaces.get(member);
752
+ if (named) {
753
+ const built = sampleObject(
754
+ [...named.props].map(([key, info]) => ({ prop: key, ...info })),
755
+ api,
756
+ );
757
+ if (built) return built;
758
+ }
759
+ }
760
+ return null;
761
+ };
762
+
763
+ /**
764
+ * A value that actually type-checks for this prop, taken from its declared type.
765
+ *
766
+ * The type decides the kind of value, the prop name decides which sample of that
767
+ * kind to show: `value` is `true` on Switch and `"Hello"` on Input because the
768
+ * interface says so, not because the example guessed.
769
+ *
770
+ * @param {object} p
771
+ * @param {object} api
772
+ * @returns {string}
773
+ */
774
+ const exampleValue = (p, api) => {
775
+ const raw = resolveType(p.type || "", api);
776
+ const type = raw.toLowerCase();
777
+ if (p.structural)
778
+ return p.prop === "children" ? `[Text({ text: "one" }), Text({ text: "two" })]` : `Text({ text: "hello" })`;
779
+ // Lists are tested before callbacks, because an array of objects can contain
780
+ // `=>` inside it: `Array<{ onPress?: () => void }>` is a list, not a handler.
781
+ const element = arrayElement(raw, api);
782
+ if (element !== null) {
783
+ if (/widget/i.test(element)) return `[Text({ text: "a" }), Text({ text: "b" })]`;
784
+ const one = objectSampleFor(element, api);
785
+ if (one) return `[${one}]`;
786
+ const first = unionMembers(element)[0] || element;
787
+ if (/^string$/i.test(first) || /^'/.test(first)) return '["one", "two"]';
788
+ if (/^number$/i.test(first)) return "[1, 2, 3]";
789
+ if (/^boolean$/i.test(first)) return "[true, false]";
790
+ return "[{ id: 1 }, { id: 2 }]";
791
+ }
792
+ // A renderer prop returns a Widget, so the example must return one too.
793
+ if (/=>\s*(Widget|JSX|ReactNode)/.test(raw)) return '() => Text({ text: "โ€ฆ" })';
794
+ if (p.engine?.kind === "event" || /=>/.test(type)) return "() => {}";
795
+ if (/boolean/.test(type)) return "true";
796
+ const literal = /'([^']+)'/.exec(raw);
797
+ if (literal) return `"${literal[1]}"`;
798
+ if (/color/i.test(raw)) return "colors.primary";
799
+ if (/widget/i.test(raw)) return 'Text({ text: "a" })';
800
+ // A shape the interface already describes is worth spelling out: `options:
801
+ // SnackBarOptions` and `headers: Record<string, string>` are objects, not
802
+ // strings.
803
+ const shape = objectSampleFor(raw, api);
804
+ if (shape) return shape;
805
+ if (/^(Padding|Margin|Size|Spacing)$/i.test(raw) || /number|rem/.test(type)) return "16";
806
+ const samples = {
807
+ text: '"Hello"',
808
+ label: '"Hello"',
809
+ title: '"Hello"',
810
+ caption: '"Hello"',
811
+ value: '"Hello"',
812
+ placeholder: '"Type hereโ€ฆ"',
813
+ name: '"name"',
814
+ icon: '"home"',
815
+ effect: '"opacity"',
816
+ from: "1",
817
+ to: "0",
818
+ src: '"/assets/image.png"',
819
+ href: '"/"',
820
+ url: '"/"',
821
+ alt: '"a short description"',
822
+ };
823
+ if (samples[p.prop] !== undefined) return samples[p.prop];
824
+ // `options: SnackBarOptions` is an object the interface already describes.
825
+ if (/^[A-Za-z_$][\w$]*$/.test(raw)) {
826
+ const iface = api?.interfaces.get(raw);
827
+ if (iface) {
828
+ const one = sampleObject(
829
+ [...iface.props].map(([key, info]) => ({ prop: key, ...info })),
830
+ api,
831
+ );
832
+ if (one) return one;
833
+ }
834
+ }
835
+ if (/^any$|^unknown$/i.test(raw)) return "1";
836
+ if (/string|symbol/i.test(raw)) return `"${p.prop}"`;
837
+ return `"${p.prop}"`;
838
+ };
839
+
840
+ const RANK = ["text", "label", "title", "placeholder", "value", "data", "items", "options", "rows", "src", "name"];
841
+ /** Boilerplate a pasted example never needs to demonstrate. */
842
+ const SKIP = new Set(["ref", "key", "id", "className", "style"]);
843
+
844
+ /**
845
+ * The order an example should demonstrate props in: what it says, what it does,
846
+ * how it looks. Props outside these groups come last.
847
+ */
848
+ const GROUPS = [RANK, ["width", "height", "padding", "margin", "gap", "bgColor", "color", "borderRadius"]];
849
+
850
+ /** Within the callback group a renderer says more than a hook, a hook more than an option. */
851
+ const callbackRank = (name) => (name.startsWith("render") ? 0 : name.startsWith("on") ? 1 : 2);
852
+
853
+ /**
854
+ * Build a paste-ready call using only props this widget provably handles.
855
+ *
856
+ * @param {object} data
857
+ * @param {object} api
858
+ * @returns {string[]}
859
+ */
860
+ const buildExample = (data, api) => {
861
+ const dead = new Set(data.gaps.dead.map((p) => p.prop));
862
+ const required = (p) => p.optional === false;
863
+ // A required prop belongs in the example even when nothing reads it: the types
864
+ // demand it, so a snippet without it would not compile. The gap is called out
865
+ // underneath instead of being hidden.
866
+ const forced = data.props.filter((p) => required(p) && !p.structural && (dead.has(p.prop) || (!p.read && !p.engine)));
867
+ const usable = data.props.filter(
868
+ (p) =>
869
+ !p.structural &&
870
+ (required(p) || ((p.read || p.engine) && !dead.has(p.prop))) &&
871
+ !SKIP.has(p.prop),
872
+ );
873
+ const groupOf = (p) => {
874
+ if (RANK.includes(p.prop)) return 0;
875
+ if (p.engine?.kind === "event" || /=>/.test((p.type || "").toLowerCase())) return 1;
876
+ if (GROUPS[1].includes(p.prop)) return 2;
877
+ return 3;
878
+ };
879
+ // Inside a group, the order the array declares is the order of interest:
880
+ // `data` before `name`, `width` before `borderRadius`, a renderer before a hook.
881
+ const within = (p) => {
882
+ const g = groupOf(p);
883
+ if (g === 0) return RANK.indexOf(p.prop);
884
+ if (g === 1) return callbackRank(p.prop);
885
+ if (g === 2) return GROUPS[1].indexOf(p.prop);
886
+ return 9;
887
+ };
888
+ const score = (p) =>
889
+ // A required prop is never optional in the example either: an interface
890
+ // without it does not build, so it leads no matter which group it is in.
891
+ (p.optional === false ? -100000 : 0) +
892
+ groupOf(p) * 1000 +
893
+ within(p) * 10 +
894
+ (p.own ? 0 : 5);
895
+ // One prop per engine target: `text`, `label`, `title` and `caption` all write
896
+ // textContent, so an example carrying two of them is noise, not a demo.
897
+ const seen = new Set();
898
+ const samples = new Set();
899
+ const picked = [];
900
+ let callbacks = 0;
901
+ for (const p of [...usable].sort(
902
+ (a, b) => score(a) - score(b) || a.prop.localeCompare(b.prop),
903
+ )) {
904
+ const key = p.engine ? `${p.engine.kind}:${p.engine.domProp}` : `own:${p.prop}`;
905
+ const required = p.optional === false;
906
+ // Aliases share one engine target, so only the first of them is shown โ€” but a
907
+ // required prop is never dropped, even when a sibling writes the same target.
908
+ if (seen.has(key) && !required) continue;
909
+ // Two callbacks already tell the story; the rest of the example is layout.
910
+ if (!required && groupOf(p) === 1 && callbacks >= 2) continue;
911
+ // `text` and `value` on Text are both real props, but a snippet that writes
912
+ // the same word twice teaches nothing, so only the first one is shown.
913
+ const value = exampleValue(p, api);
914
+ if (!required && value.startsWith('"') && samples.has(value)) continue;
915
+ if (groupOf(p) === 1) callbacks++;
916
+ seen.add(key);
917
+ samples.add(value);
918
+ picked.push(p);
919
+ if (!required && picked.length >= 5) break;
920
+ }
921
+ // `child` and `children` are structural, so they never reach the loop above โ€”
922
+ // but a widget whose interface marks them required is broken without them.
923
+ for (const p of data.props) {
924
+ if (p.structural && p.optional === false && !seen.has(`own:${p.prop}`)) {
925
+ seen.add(`own:${p.prop}`);
926
+ picked.push(p);
927
+ }
928
+ }
929
+ // A widget that already received its content prop (`text`, `data`, `label`)
930
+ // does not also want a `child`: the example would contradict itself.
931
+ const wantsChild =
932
+ data.accepts.child &&
933
+ !picked.some((p) => RANK.includes(p.prop) || p.engine?.kind === "text" || p.prop === "child");
934
+ if (wantsChild) picked.push({ prop: "child", structural: true });
935
+
936
+ const usesColors = picked.some((p) => exampleValue(p, api).includes("colors."));
937
+ const usesText = picked.some((p) => exampleValue(p, api).includes("Text(") || wantsChild);
938
+ const imports = [...new Set([data.name, ...(usesColors ? ["colors"] : []), ...(usesText ? ["Text"] : [])])];
939
+ const out = [section("๐Ÿ“‹", `Paste-ready โ€” ${data.name}`, "#22d3ee")];
940
+ out.push(` ${c("magenta", "import")} { ${imports.join(", ")} } ${c("magenta", "from")} ${c("green", "'flet-box'")};`);
941
+ out.push("");
942
+
943
+ if (data.call?.positional) {
944
+ // Wrapping these in `{ โ€ฆ }` is the quietest bug an app can have: the call
945
+ // builds, the widget reads `undefined`, nothing renders. So the example
946
+ // passes arguments the way the declaration says.
947
+ const args = data.call.params
948
+ .filter((p) => !p.optional)
949
+ .map((p) => exampleValue({ prop: p.name, type: p.type }, api));
950
+ out.push(` ${c("magenta", "const")} view = ${c("brightCyan", data.name)}(${c("green", args.join(", "))});`);
951
+ out.push("");
952
+ out.push(
953
+ ` ${c("red", `โš  ${data.name} takes positional arguments (${data.call.params.map((p) => p.name).join(", ")}) โ€” a props object here is silently ignored`)}`,
954
+ );
955
+ out.push(
956
+ ` ${c("gray", `see \`flet-box unpack ${data.name}\` for the props its interface declares`)}`,
957
+ );
958
+ return out;
959
+ }
960
+
961
+ out.push(` ${c("magenta", "const")} view = ${c("brightCyan", `${data.name}`)}({`);
962
+ const width = Math.min(28, Math.max(...picked.map((p) => p.prop.length)) + 1);
963
+ for (const p of picked) {
964
+ const value = exampleValue(p, api);
965
+ out.push(` ${cell(p.prop, "brightWhite", width)}: ${/^\(\) =>/.test(value) ? c("brightBlue", value) : c("green", value)},`);
966
+ }
967
+ out.push(` });`);
968
+ out.push("");
969
+ if (forced.length)
970
+ out.push(
971
+ ` ${c("red", `โš  ${forced.map((p) => p.prop).join(", ")} โ€” required by index.d.ts, read by nothing: the widget ignores them`)}`,
972
+ );
973
+ if (data.call?.members.length)
974
+ out.push(
975
+ ` ${c("yellow", data.call.element ? "view also answers:" : "view is a controller, nothing shows until:")} ${c(
976
+ "brightMagenta",
977
+ ` ${data.call.members.slice(0, 9).join(" ยท ")}`,
978
+ )}`,
979
+ );
980
+ out.push(
981
+ ` ${c("gray", `${picked.length} of ${data.props.length} accepted props ยท each one reaches the DOM or is read by the widget ยท see \`flet-box unpack ${data.name}\` for the rest`)}`,
982
+ );
983
+ return out;
984
+ };
985
+
986
+ /**
987
+ * Human-readable form of a whole API surface dump.
988
+ *
989
+ * @param {object[]} all
990
+ * @param {object} api
991
+ * @returns {string}
992
+ */
993
+ const renderMarkdown = (all, api) => {
994
+ const out = [
995
+ "# FletBox API โ€” unpacked",
996
+ "",
997
+ `Generated by \`flet-box unpack --all --md\` from \`flet-box@${api.pkg.version}\`. Do not edit.`,
998
+ "",
999
+ `Widgets: ${all.length}`,
1000
+ "",
1001
+ ];
1002
+ for (const data of all) {
1003
+ out.push(`## ${data.name}`, "");
1004
+ if (data.docFile) out.push(`Docs: \`docs/widget/${data.name}.md\` ยท Source: \`src/${path.relative(api.dir, data.sourceFile || "").replace(/\\/g, "/")}\``);
1005
+ out.push("", "| prop | type | engine target | aliases | notes |", "| --- | --- | --- | --- | --- |");
1006
+ for (const p of data.props) {
1007
+ const notes = [
1008
+ p.declared ? "" : "not declared",
1009
+ p.default !== null ? `default \`${p.default}\`` : "",
1010
+ p.structural ? "structural" : "",
1011
+ data.gaps.dead.includes(p) ? "DEAD" : "",
1012
+ ]
1013
+ .filter(Boolean)
1014
+ .join(", ");
1015
+ out.push(
1016
+ `| \`${p.prop}\` | \`${(p.type || "any").replace(/\|/g, "\\|")}\` | \`${p.engine ? p.engine.domProp : "โ€”"}\` | ${p.aliasOf.map((a) => `\`${a}\``).join(" ")} | ${notes} |`,
1017
+ );
1018
+ }
1019
+ out.push("");
1020
+ }
1021
+ return out.join("\n");
1022
+ };
1023
+
1024
+ /* ------------------------------------------------------------------ entry */
1025
+
1026
+ /**
1027
+ * @param {string[]} argv
1028
+ * @param {{ print?: Function }} [io]
1029
+ * @returns {Promise<{ exitCode: number }>}
1030
+ */
1031
+ export const unpack = async (argv = [], io = {}) => {
1032
+ const print = io.print || ((line) => console.log(line));
1033
+ const root = (() => {
1034
+ const i = argv.indexOf("--root");
1035
+ return i >= 0 && argv[i + 1] ? path.resolve(argv[i + 1]) : process.cwd();
1036
+ })();
1037
+
1038
+ let api;
1039
+ try {
1040
+ api = await loadApi(root);
1041
+ } catch (error) {
1042
+ print(c("red", `โœ— ${error.message}`));
1043
+ return { exitCode: 1 };
1044
+ }
1045
+ const definitions = definitionIndex(api.dir);
1046
+ const widgets = api.exports
1047
+ .filter((name) => /^[A-Z]/.test(name) && propsSource(api, name))
1048
+ .sort();
1049
+ const utilities = api.exports.filter((name) => !/^[A-Z]/.test(name)).sort();
1050
+
1051
+ // A generated app commits this file, so nothing in it may be this machine's
1052
+ // absolute path, and `events` is dropped: it is `props` filtered by
1053
+ // `engine.kind === "event"`, and repeating every event prop doubled the size
1054
+ // of the dump for nothing.
1055
+ const portable = (data) => {
1056
+ const rel = (p) => (p ? path.relative(api.dir, p).split(path.sep).join("/") : null);
1057
+ const { events, sourceFile, docFile, ...rest } = data;
1058
+ return {
1059
+ ...rest,
1060
+ sourceFile: rel(sourceFile),
1061
+ docFile: rel(docFile),
1062
+ documented: [...new Set(data.documented)],
1063
+ };
1064
+ };
1065
+
1066
+ const surface = () => ({
1067
+ package: "flet-box",
1068
+ version: api.pkg.version,
1069
+ generatedBy: "flet-box unpack",
1070
+ engineProps: api.ALL_PROPS.length,
1071
+ widgets: Object.fromEntries(
1072
+ widgets.map((name) => [name, portable(unpackWidget(api, name, definitions))]),
1073
+ ),
1074
+ utilities,
1075
+ });
1076
+
1077
+ const name = argv.find((a) => !a.startsWith("--"));
1078
+
1079
+ if (argv.includes("--all")) {
1080
+ const all = widgets.map((n) => unpackWidget(api, n, definitions)).filter(Boolean);
1081
+ if (argv.includes("--json")) {
1082
+ print(JSON.stringify(surface(), null, 2));
1083
+ return { exitCode: 0 };
1084
+ }
1085
+ const dir = path.join(root, ".fletbox", "api");
1086
+ fs.mkdirSync(dir, { recursive: true });
1087
+ const target = path.join(dir, "api.json");
1088
+ fs.writeFileSync(target, JSON.stringify(surface(), null, 2), "utf8");
1089
+ print(banner("๐Ÿ“ฆ FletBox unpack", `${all.length} widgets ยท flet-box@${api.pkg.version}`));
1090
+ print(
1091
+ ` ${c("brightGreen", target)} ${c("gray", "โ€” the whole verified surface, for the AI agent inside this app")}`,
1092
+ );
1093
+ if (argv.includes("--md")) {
1094
+ const md = path.join(dir, "api.md");
1095
+ fs.writeFileSync(md, renderMarkdown(all, api), "utf8");
1096
+ print(` ${c("brightGreen", md)} ${c("gray", "โ€” the same data, readable")}`);
1097
+ }
1098
+ if (argv.includes("--diff")) {
1099
+ const withGaps = all.filter(
1100
+ (d) =>
1101
+ d.gaps.dead.length + d.gaps.invented.length + d.gaps.hidden.length +
1102
+ d.gaps.docOnly.length + d.gaps.codeOnly.length >
1103
+ 0,
1104
+ );
1105
+ print(section("๐Ÿ”ฌ", `${withGaps.length}/${all.length} widgets disagree with themselves`, "#f59e0b"));
1106
+ for (const d of withGaps) {
1107
+ const parts = [
1108
+ d.gaps.dead.length && c("red", `dead: ${d.gaps.dead.map((p) => p.prop).join(" ")}`),
1109
+ d.gaps.docOnly.length && c("brightRed", `doc-only: ${d.gaps.docOnly.join(" ")}`),
1110
+ d.gaps.codeOnly.length && c("brightMagenta", `code-only: ${d.gaps.codeOnly.join(" ")}`),
1111
+ d.gaps.hidden.length && c("yellow", `not in .d.ts: ${d.gaps.hidden.map((p) => p.prop).join(" ")}`),
1112
+ d.gaps.invented.length && c("brightYellow", `invented: ${d.gaps.invented.map((p) => p.prop).join(" ")}`),
1113
+ ].filter(Boolean);
1114
+ print(` ${cell(d.name, "brightWhite", 18)}${parts.join(c("gray", " ยท "))}`);
1115
+ }
1116
+ } else {
1117
+ const gaps = all.reduce(
1118
+ (n, d) =>
1119
+ n + d.props.filter((p) => p.declared && !p.engine && !p.read && !p.structural).length,
1120
+ 0,
1121
+ );
1122
+ print(
1123
+ ` ${c("gray", `${all.reduce((n, d) => n + d.props.length, 0)} prop contracts ยท ${gaps} declared-but-dead ยท use \`flet-box unpack --all --diff\` to list them`)}`,
1124
+ );
1125
+ }
1126
+ return { exitCode: 0 };
1127
+ }
1128
+
1129
+ if (!name) {
1130
+ if (argv.includes("--json")) {
1131
+ print(JSON.stringify(surface(), null, 2));
1132
+ return { exitCode: 0 };
1133
+ }
1134
+ print(c("red", "Usage: flet-box unpack <Widget> [--diff | --example | --json] | --all [--diff | --md]"));
1135
+ print(c("gray", ` ${widgets.length} widgets available โ€” flet-box check-api --list`));
1136
+ return { exitCode: 1 };
1137
+ }
1138
+
1139
+ const exact = widgets.includes(name) ? name : null;
1140
+ const target = exact || widgets.find((w) => w.toLowerCase() === name.toLowerCase());
1141
+ if (!target) {
1142
+ const guess = nearest(name, widgets);
1143
+ print(banner("๐Ÿ“ฆ FletBox unpack", `flet-box@${api.pkg.version}`));
1144
+ print(` ${c("red", `โœ— "${name}" is not a FletBox widget`)}`);
1145
+ if (api.exports.includes(name))
1146
+ print(` ${c("gray", `it is exported, but index.d.ts never types a props object for it โ€” a singleton API, not a widget`)}`);
1147
+ print(
1148
+ guess
1149
+ ? ` ${c("gray", "did you mean")} ${c("brightCyan", guess)}?`
1150
+ : ` ${c("gray", "list them all with: flet-box check-api --list")}`,
1151
+ );
1152
+ return { exitCode: 1 };
1153
+ }
1154
+
1155
+ const data = unpackWidget(api, target, definitions);
1156
+ if (argv.includes("--json")) {
1157
+ print(JSON.stringify(portable(data), null, 2));
1158
+ return { exitCode: 0 };
1159
+ }
1160
+ print(banner("๐Ÿ“ฆ FletBox unpack", `${target} ยท flet-box@${api.pkg.version}`));
1161
+ if (argv.includes("--diff")) {
1162
+ for (const line of renderDiff(data)) print(line);
1163
+ return { exitCode: 0 };
1164
+ }
1165
+ if (argv.includes("--example")) {
1166
+ for (const line of buildExample(data, api)) print(line);
1167
+ return { exitCode: 0 };
1168
+ }
1169
+ for (const line of renderWidget(data, api)) print(line);
1170
+ return { exitCode: 0 };
1171
+ };
1172
+
1173
+ export default unpack;