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
@@ -5,6 +5,8 @@
5
5
  import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { colors } from "../utils/themes.js";
7
7
 
8
+ import { toREM } from "../utils/units.js";
9
+
8
10
  export const Icon = (props) => {
9
11
  const {
10
12
  icon,
@@ -20,7 +22,7 @@ export const Icon = (props) => {
20
22
  widgetName: "Icon",
21
23
  className: "material-icons",
22
24
  text: iconName, // ✅ CHANGED: text instead of textContent
23
- fontSize: typeof size === "number" ? `${size}px` : size,
25
+ fontSize: toREM(size),
24
26
  color: color,
25
27
  display: "inline-flex",
26
28
  alignItems: "center",
@@ -1,20 +1,119 @@
1
1
  /**
2
2
  * @file Image.js
3
3
  * @description Thin `<img>` wrapper that forwards all props to the factory.
4
+ * Three props are handled here because they are not CSS the engine can apply:
5
+ * `circular` picks a shape, and `downloadable` decides what a click does.
4
6
  */
5
7
  import { WidgetFactory } from "../widget-factory/index.js";
8
+ import {
9
+ isReactive,
10
+ reactiveKeyOf,
11
+ resolveReactive,
12
+ } from "../widget-factory/reactiveValue.js";
13
+ import { composeUpdate } from "../utils/composeUpdate.js";
14
+ import { registerWidget } from "../tools/useState.js";
15
+
16
+ /**
17
+ * The name the browser offers when saving an image with no `downloadFileName`:
18
+ * the last path segment of the URL, with the query string and fragment removed.
19
+ * A `data:` URL has no path — its base64 tail is not a file name — so it gets a
20
+ * neutral one.
21
+ *
22
+ * @param {string} src - Value of the `src` attribute.
23
+ * @returns {string} A file name.
24
+ */
25
+ const nameFromSrc = (src) => {
26
+ const raw = String(src || "");
27
+ if (!raw || raw.startsWith("data:")) return "image";
28
+ const tail = raw.split("?")[0].split("#")[0].split("/").pop();
29
+ return tail || "image";
30
+ };
31
+
32
+ /**
33
+ * Saves the image under `name`.
34
+ *
35
+ * The `download` attribute is only honoured for a same-origin (or CORS-served)
36
+ * image; for anything else the click would turn into a navigation, which is why
37
+ * the anchor opens in a tab instead of replacing the app.
38
+ *
39
+ * @param {HTMLImageElement} img - The rendered image.
40
+ * @param {string} name - File name to suggest.
41
+ */
42
+ const saveImage = (img, name) => {
43
+ const link = document.createElement("a");
44
+ link.href = img.src;
45
+ link.download = name;
46
+ link.target = "_blank";
47
+ link.rel = "noopener";
48
+ link.click();
49
+ };
6
50
 
7
51
  export const Image = (props) => {
52
+ const inputProps = props || {};
8
53
  // `fit` is now a proper style prop in translateProps (→ object-fit), so it
9
54
  // flows through the factory like any other CSS property. Only `circular`
10
55
  // needs special handling — it maps to borderRadius 50%.
11
- const { circular, ...rest } = props || {};
12
- return WidgetFactory({
56
+ const { circular, downloadable, downloadFileName, ...rest } = inputProps;
57
+
58
+ // Both download props are `special`: nothing routes them, so the factory
59
+ // never sees them and cannot re-apply or re-subscribe them. They are read
60
+ // here instead — resolved once (a live `useState` value is a *function*,
61
+ // which is truthy, so an image bound to `false` would download without this),
62
+ // kept in `boundKeys`, and re-applied through `composeUpdate` when a later
63
+ // patch or store change arrives.
64
+ const boundKeys = new Map();
65
+ let canDownload = Boolean(resolveReactive(downloadable));
66
+ let fileName = resolveReactive(downloadFileName);
67
+ for (const key of ["downloadable", "downloadFileName"]) {
68
+ const value = inputProps[key];
69
+ if (!isReactive(value)) continue;
70
+ const stateKey = reactiveKeyOf(value);
71
+ if (stateKey) boundKeys.set(key, stateKey);
72
+ }
73
+
74
+ const img = WidgetFactory({
13
75
  tag: "img",
14
76
  widgetName: "Image",
15
77
  ...(circular ? { borderRadius: "50%" } : null),
16
78
  ...rest,
17
79
  });
80
+
81
+ // A caller who set `cursor` themselves keeps their own value; this widget
82
+ // only borrows the property while it is offering a save.
83
+ const paintsCursor = rest.cursor == null;
84
+
85
+ const offerSave = () => {
86
+ saveImage(img, fileName ? String(fileName) : nameFromSrc(img.src));
87
+ };
88
+
89
+ let attached = false;
90
+ const applyDownloadProps = () => {
91
+ if (canDownload && !attached) {
92
+ img.addEventListener("click", offerSave);
93
+ attached = true;
94
+ } else if (!canDownload && attached) {
95
+ img.removeEventListener("click", offerSave);
96
+ attached = false;
97
+ }
98
+ if (paintsCursor) img.style.cursor = canDownload ? "pointer" : "";
99
+ };
100
+
101
+ applyDownloadProps();
102
+
103
+ composeUpdate(img, ["downloadable", "downloadFileName"], (next = {}) => {
104
+ if ("downloadable" in next) canDownload = Boolean(resolveReactive(next.downloadable));
105
+ if ("downloadFileName" in next) fileName = resolveReactive(next.downloadFileName);
106
+ applyDownloadProps();
107
+ });
108
+
109
+ // After composing: `registerWidget` pushes the current store value through
110
+ // `update`, which must be the composed one that routes these keys here. It
111
+ // releases the subscription again on unmount.
112
+ for (const [propName, stateKey] of boundKeys) {
113
+ registerWidget(stateKey, img, propName);
114
+ }
115
+
116
+ return img;
18
117
  };
19
118
 
20
- export default Image;
119
+ export default Image;
@@ -4,6 +4,8 @@
4
4
  */
5
5
  import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { colors } from "../utils/themes.js";
7
+ import { toREM } from "../utils/units.js";
8
+ import { TAP_MINIMUM } from "../tools/index.js";
7
9
  import { Icon } from "./Icon.js";
8
10
  import { TextInputValidator } from "../utils/TextInputValidator.js";
9
11
  import { composeUpdate } from "../utils/composeUpdate.js";
@@ -76,7 +78,7 @@ export const Input = (props) => {
76
78
  size = "medium",
77
79
  variant = "outlined",
78
80
  fullWidth = false,
79
- borderRadius = 24,
81
+ borderRadius = 16,
80
82
 
81
83
  iconLeft,
82
84
  iconRight,
@@ -131,11 +133,17 @@ export const Input = (props) => {
131
133
  const isFocused = () => document.activeElement === inputElement;
132
134
 
133
135
  // ========== SIZES ==========
134
- // Maps the `size` prop to concrete CSS values so the rest of the code stays size-agnostic.
136
+ /**
137
+ * Framework units, not CSS strings: this widget builds a real `<input>` and
138
+ * writes `style.*` by hand, so every length here goes through `toREM` at the
139
+ * point of use. Written as `"10px 14px"` the field froze at 16px base and the
140
+ * reader who raises their font got small text inside a control that should have
141
+ * grown with it. `minHeight` is the Fitts floor — a text field is a target.
142
+ */
135
143
  const sizes = {
136
- small: { padding: "6px 12px", fontSize: "12px", iconSize: 16 },
137
- medium: { padding: "10px 14px", fontSize: "14px", iconSize: 20 },
138
- large: { padding: "14px 18px", fontSize: "16px", iconSize: 24 },
144
+ small: { padding: [4, 12], fontSize: 12, iconSize: 16, minHeight: 32 },
145
+ medium: { padding: [8, 12], fontSize: 14, iconSize: 20, minHeight: TAP_MINIMUM },
146
+ large: { padding: [12, 16], fontSize: 16, iconSize: 24, minHeight: 48 },
139
147
  };
140
148
  const sz = sizes[size] || sizes.medium;
141
149
 
@@ -350,7 +358,7 @@ export const Input = (props) => {
350
358
  fontSize: 12,
351
359
  fontWeight: 500,
352
360
  color: error ? colors.danger : colors.textSecondary,
353
- marginBottom: 2,
361
+ marginBottom: 4,
354
362
  cursor: "text",
355
363
  });
356
364
  // Clicking the label focuses the native input — mirrors native <label for> behavior
@@ -387,8 +395,7 @@ export const Input = (props) => {
387
395
 
388
396
  if (variant === "filled") {
389
397
  wrapperStyles.backgroundColor = colors.gray100;
390
- wrapperStyles.borderRadius =
391
- typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius;
398
+ wrapperStyles.borderRadius = toREM(borderRadius);
392
399
  wrapperStyles.border = "none";
393
400
  } else if (variant === "underlined") {
394
401
  wrapperStyles.backgroundColor = "transparent";
@@ -397,8 +404,7 @@ export const Input = (props) => {
397
404
  } else {
398
405
  // Default: outlined
399
406
  wrapperStyles.backgroundColor = "transparent";
400
- wrapperStyles.borderRadius =
401
- typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius;
407
+ wrapperStyles.borderRadius = toREM(borderRadius);
402
408
  wrapperStyles.border = `1px solid ${error ? colors.danger : colors.border}`;
403
409
  }
404
410
 
@@ -414,7 +420,7 @@ export const Input = (props) => {
414
420
  name: iconLeft,
415
421
  size: iconSize || sz.iconSize,
416
422
  color: iconColor,
417
- marginLeft: "12px",
423
+ marginLeft: 12,
418
424
  flexShrink: 0,
419
425
  pointerEvents: "auto",
420
426
  });
@@ -436,8 +442,9 @@ export const Input = (props) => {
436
442
  inputElement.value = currentValue;
437
443
 
438
444
  inputElement.style.flex = "1";
439
- inputElement.style.padding = sz.padding;
440
- inputElement.style.fontSize = sz.fontSize;
445
+ inputElement.style.padding = `${toREM(sz.padding[0])} ${toREM(sz.padding[1])}`;
446
+ inputElement.style.fontSize = toREM(sz.fontSize);
447
+ inputElement.style.minHeight = toREM(sz.minHeight);
441
448
  inputElement.style.border = "none";
442
449
  inputElement.style.outline = "none";
443
450
  inputElement.style.backgroundColor = "transparent";
@@ -505,15 +512,15 @@ export const Input = (props) => {
505
512
  btn.type = "button";
506
513
  btn.textContent = glyph;
507
514
  btn.setAttribute("aria-label", label);
508
- btn.style.width = "28px";
509
- btn.style.height = "28px";
515
+ btn.style.width = toREM(32);
516
+ btn.style.height = toREM(32);
510
517
  btn.style.flexShrink = "0";
511
- btn.style.marginRight = "6px";
518
+ btn.style.marginRight = toREM(4);
512
519
  btn.style.padding = "0";
513
520
  btn.style.border = "none";
514
521
  btn.style.background = "transparent";
515
522
  btn.style.color = colors.textSecondary;
516
- btn.style.fontSize = "15px";
523
+ btn.style.fontSize = toREM(16);
517
524
  btn.style.lineHeight = "1";
518
525
  btn.style.cursor = disabled ? "not-allowed" : "pointer";
519
526
  btn.style.display = "flex";
@@ -564,12 +571,12 @@ export const Input = (props) => {
564
571
  if (showValidationIcon && validation !== "none") {
565
572
  validationIcon = document.createElement("span");
566
573
  validationIcon.textContent = "";
567
- validationIcon.style.width = "24px";
568
- validationIcon.style.fontSize = "16px";
574
+ validationIcon.style.width = toREM(24);
575
+ validationIcon.style.fontSize = toREM(16);
569
576
  validationIcon.style.display = "none";
570
577
  validationIcon.style.alignItems = "center";
571
578
  validationIcon.style.justifyContent = "center";
572
- validationIcon.style.marginRight = "8px";
579
+ validationIcon.style.marginRight = toREM(8);
573
580
  validationIcon.style.flexShrink = "0";
574
581
  inputWrapper.appendChild(validationIcon);
575
582
  }
@@ -580,7 +587,7 @@ export const Input = (props) => {
580
587
  name: iconRight,
581
588
  size: iconSize || sz.iconSize,
582
589
  color: iconColor,
583
- marginRight: "12px",
590
+ marginRight: 12,
584
591
  flexShrink: 0,
585
592
  pointerEvents: "auto",
586
593
  });
@@ -593,9 +600,9 @@ export const Input = (props) => {
593
600
  // --- Error message element rendered below the input ---
594
601
  errorMessageElement = document.createElement("span");
595
602
  errorMessageElement.textContent = "";
596
- errorMessageElement.style.fontSize = "11px";
603
+ errorMessageElement.style.fontSize = toREM(12);
597
604
  errorMessageElement.style.color = colors.danger;
598
- errorMessageElement.style.marginTop = "2px";
605
+ errorMessageElement.style.marginTop = toREM(4);
599
606
  errorMessageElement.style.display = "none";
600
607
  container.appendChild(errorMessageElement);
601
608
 
@@ -1,5 +1,7 @@
1
1
  // utils/Inspector.js
2
2
  import { getWidgetProps } from "../utils/getWidgetProps.js";
3
+ import { WidgetFactory } from "../widget-factory/index.js";
4
+ import { colors } from "../utils/themes.js";
3
5
 
4
6
  /**
5
7
  * Converts a JavaScript value to its source-code string representation.
@@ -138,6 +140,44 @@ const collectChildren = (widget) => {
138
140
  return children;
139
141
  };
140
142
 
143
+ /**
144
+ * The widget form: a monospace block holding the reconstructed call.
145
+ *
146
+ * `Inspector` itself is a string builder, which is what the console helpers
147
+ * need. A screen that wants to *show* a tree asks for this one, by passing the
148
+ * props object the documentation describes.
149
+ *
150
+ * @param {{ widget?: HTMLElement, indent?: number }} props
151
+ * @returns {HTMLElement}
152
+ */
153
+ const InspectorView = ({ widget, indent = 0, ...rest }) => {
154
+ const dump = widget
155
+ ? Inspector(widget, indent)
156
+ : 'Inspector({ widget }) — nothing to inspect.';
157
+
158
+ const container = WidgetFactory({
159
+ widgetName: "Inspector",
160
+ tag: "pre",
161
+ text: dump,
162
+ fontFamily:
163
+ 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Courier New", monospace',
164
+ fontSize: 12,
165
+ color: colors.text,
166
+ backgroundColor: colors.card,
167
+ padding: 16,
168
+ borderRadius: 8,
169
+ overflowX: "auto",
170
+ width: "100%",
171
+ ...rest,
172
+ });
173
+
174
+ // A <pre> that wraps is a dump nobody can read; the horizontal scroll above
175
+ // is the deal.
176
+ container.style.whiteSpace = "pre";
177
+
178
+ return container;
179
+ };
180
+
141
181
  /**
142
182
  * Converts a widget element back into its equivalent source-code representation,
143
183
  * recursively reconstructing its full prop tree and children.
@@ -149,7 +189,20 @@ const collectChildren = (widget) => {
149
189
  * @param {number} [indent=0] - Starting indentation depth for pretty-printing.
150
190
  * @returns {string} Source code string such as `Row({ gap: 8, children: [...] })`.
151
191
  */
152
- export const Inspector = (widget, indent = 0) => {
192
+ export const Inspector = (target, indent = 0) => {
193
+ // Two calls, one function. `Inspector({ widget, indent })` is the documented
194
+ // widget: it paints the dump so a settings screen can show a tree. Called
195
+ // with a node it stays the string builder `printWidgetCode` uses.
196
+ if (
197
+ target &&
198
+ typeof target === "object" &&
199
+ target.nodeType !== 1 &&
200
+ "widget" in target
201
+ ) {
202
+ return InspectorView(target);
203
+ }
204
+
205
+ const widget = target;
153
206
  if (!widget || widget.nodeType !== 1) {
154
207
  return String(widget);
155
208
  }
@@ -10,9 +10,11 @@ export const InstallButton = (props) => {
10
10
  text = "📲 Install",
11
11
  variant = "filled",
12
12
  size = "medium",
13
- borderRadius = 28,
14
- padding = "12px 24px",
15
- bottom = 20,
13
+ borderRadius = 16,
14
+ // A number is the framework's own form: Button doubles it horizontally
15
+ // (12 vertical, 24 horizontal) and the factory emits it as `rem`.
16
+ padding = 12,
17
+ bottom = 24,
16
18
  backgroundColor,
17
19
  color,
18
20
  onInstalled,
@@ -64,7 +66,7 @@ export const InstallButton = (props) => {
64
66
  borderRadius: borderRadius,
65
67
  padding: padding,
66
68
  position: "fixed",
67
- bottom: `${bottom}px`,
69
+ bottom,
68
70
  left: "50%",
69
71
  transform: "translateX(-50%)",
70
72
  zIndex: 10000,
@@ -7,6 +7,7 @@ import { Row } from "./Row.js";
7
7
  import { Column } from "./Column.js";
8
8
  import { Text } from "./Text.js";
9
9
  import { colors, subscribeTheme } from "../utils/themes.js";
10
+ import { toREM } from "../utils/units.js";
10
11
 
11
12
  /**
12
13
  * @typedef {Object} ListTileProps
@@ -132,7 +133,7 @@ export const ListTile = (props) => {
132
133
  const contentRow = Row({
133
134
  alignItems: "center",
134
135
  gap: gap,
135
- padding: `${paddingVertical}px ${paddingHorizontal}px`,
136
+ padding: `${toREM(paddingVertical)} ${toREM(paddingHorizontal)}`,
136
137
  width: "100%",
137
138
  boxSizing: "border-box",
138
139
  children: rowChildren,
@@ -199,8 +200,8 @@ export const ListTile = (props) => {
199
200
  const dividerLine = WidgetFactory({
200
201
  height: 1,
201
202
  backgroundColor: colors.border,
202
- marginLeft: `${paddingHorizontal}px`,
203
- width: `calc(100% - ${paddingHorizontal}px)`,
203
+ marginLeft: toREM(paddingHorizontal),
204
+ width: `calc(100% - ${toREM(paddingHorizontal)})`,
204
205
  });
205
206
 
206
207
  const container = WidgetFactory({
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { injectKeyframes } from "../utils/styleInjector.js";
7
+ import { toREM, getBaseFontSize } from "../utils/units.js";
7
8
 
8
9
  /**
9
10
  * @typedef {Object} ListViewProps
@@ -11,8 +12,8 @@ import { injectKeyframes } from "../utils/styleInjector.js";
11
12
  * @property {Function} renderItem - Called with `(item, index)` and must return an `HTMLElement`.
12
13
  * @property {number|string} [height=400] - Height of the scroll container. Overridden when `expand` is true.
13
14
  * @property {number|string} [width="100%"] - Width of the widget.
14
- * @property {number} [itemSize=60] - Fixed height of each row in pixels (required for virtualization math).
15
- * @property {number} [gap=0] - Vertical gap between rows in pixels.
15
+ * @property {number} [itemSize=60] - Fixed height of each row in framework units, i.e. it grows with the base font (required for virtualization math).
16
+ * @property {number} [gap=0] - Vertical gap between rows in framework units.
16
17
  * @property {number} [bufferSize=5] - Number of extra rows to render above and below the visible viewport.
17
18
  * Larger values reduce the chance of blank flashes during fast scrolling at the cost of more DOM nodes.
18
19
  * @property {boolean} [showsScrollIndicator=true] - Whether to show the native scrollbar.
@@ -140,6 +141,23 @@ export const ListView = (props) => {
140
141
 
141
142
  // ---- Helper functions ----
142
143
 
144
+ /**
145
+ * Rendered pixels per framework unit, read from the live root font size.
146
+ *
147
+ * `itemSize` and `gap` are framework units, so the row they reserve grows with
148
+ * the reader's font like every other box. The virtualisation math compares
149
+ * against `scrollTop` and `clientHeight`, which are always CSS pixels, so the
150
+ * two spaces have to be joined here: a 60-unit row is 90px of scroll space for
151
+ * a reader on a 24px base, and rows written straight from the unit overlap.
152
+ */
153
+ const unitPx = () => {
154
+ if (typeof document === "undefined") return 1;
155
+ const root = parseFloat(getComputedStyle(document.documentElement).fontSize);
156
+ return root > 0 ? root / getBaseFontSize() : 1;
157
+ };
158
+ const slotHeight = () => (itemSize + gap) * unitPx();
159
+ const gapHeight = () => gap * unitPx();
160
+
143
161
  /** @returns {number} Total number of rows (accounting for grid columns). */
144
162
  const getTotalRows = () => Math.ceil(_data.length / cols);
145
163
 
@@ -159,7 +177,7 @@ export const ListView = (props) => {
159
177
  */
160
178
  const getItemTop = (index) => {
161
179
  const row = Math.floor(index / cols);
162
- return row * (itemSize + gap);
180
+ return row * slotHeight();
163
181
  };
164
182
 
165
183
  /**
@@ -171,7 +189,7 @@ export const ListView = (props) => {
171
189
  const getTotalHeight = () => {
172
190
  const rows = getTotalRows();
173
191
  if (rows === 0) return 0;
174
- return Math.max(0, rows * (itemSize + gap) - gap);
192
+ return Math.max(0, rows * slotHeight() - gapHeight());
175
193
  };
176
194
 
177
195
  /** Applies the correct logical height to the inner container. */
@@ -190,7 +208,7 @@ export const ListView = (props) => {
190
208
  if (!scrollContainer) return;
191
209
 
192
210
  const viewportH = scrollContainer.clientHeight;
193
- const itemTotalH = itemSize + gap;
211
+ const itemTotalH = slotHeight();
194
212
  // Convert the scroll position into ITEM space: rows start below the header, so
195
213
  // the first rows are still on screen while `scrollTop` is inside the header.
196
214
  const itemsTop = Math.max(0, scrollContainer.scrollTop - getHeaderHeight());
@@ -228,7 +246,7 @@ export const ListView = (props) => {
228
246
  console.error("Error in renderItem:", err);
229
247
  itemEl = document.createElement("div");
230
248
  itemEl.textContent = "Error";
231
- itemEl.style.padding = "8px";
249
+ itemEl.style.padding = toREM(8);
232
250
  }
233
251
  }
234
252
  if (!itemEl) continue;
@@ -240,12 +258,12 @@ export const ListView = (props) => {
240
258
  const col = i % cols;
241
259
  wrapper.style.left = `${(col / cols) * 100}%`;
242
260
  wrapper.style.width = `${100 / cols}%`;
243
- wrapper.style.padding = `0 ${gap / 2}px`;
261
+ wrapper.style.padding = `0 ${toREM(gap / 2)}`;
244
262
  wrapper.style.boxSizing = "border-box";
245
263
  } else {
246
264
  wrapper.style.left = "0";
247
265
  wrapper.style.right = "0";
248
- wrapper.style.padding = `0 ${gap}px`;
266
+ wrapper.style.padding = `0 ${toREM(gap)}`;
249
267
  }
250
268
  wrapper.appendChild(itemEl);
251
269
  fragment.appendChild(wrapper);
@@ -367,8 +385,8 @@ export const ListView = (props) => {
367
385
  refreshIndicator.style.transition = "height 0.2s";
368
386
 
369
387
  const spinner = document.createElement("div");
370
- spinner.style.width = "24px";
371
- spinner.style.height = "24px";
388
+ spinner.style.width = toREM(24);
389
+ spinner.style.height = toREM(24);
372
390
  spinner.style.border = "2px solid #e0e0e0";
373
391
  spinner.style.borderTop = "2px solid #007aff";
374
392
  spinner.style.borderRadius = "50%";
@@ -401,7 +419,7 @@ export const ListView = (props) => {
401
419
  if (h >= 60) {
402
420
  // Threshold met — trigger the refresh
403
421
  _refreshing = true;
404
- refreshIndicator.style.height = "60px";
422
+ refreshIndicator.style.height = toREM(60);
405
423
  onRefresh(() => {
406
424
  _refreshing = false;
407
425
  refreshIndicator.style.height = "0";
@@ -498,7 +516,7 @@ export const ListView = (props) => {
498
516
  set: (val) => {
499
517
  _refreshing = val;
500
518
  if (refreshIndicator)
501
- refreshIndicator.style.height = _refreshing ? "60px" : "0";
519
+ refreshIndicator.style.height = _refreshing ? toREM(60) : "0";
502
520
  },
503
521
  enumerable: true,
504
522
  });