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
@@ -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 DataTable = (props) => {
9
11
  const {
10
12
  columns,
@@ -22,7 +24,7 @@ export const DataTable = (props) => {
22
24
 
23
25
  rowBgColor = "transparent",
24
26
  rowTextColor = colors.text,
25
- rowFontSize = 13,
27
+ rowFontSize = 14,
26
28
 
27
29
  stripedRowBgColor = colors.gray50,
28
30
 
@@ -31,8 +33,12 @@ export const DataTable = (props) => {
31
33
  borderColor = colors.border,
32
34
  borderWidth = 1,
33
35
 
34
- cellPadding = "10px 12px",
35
- headerCellPadding = "12px",
36
+ // A padding the caller can hand as `8`, `"8px 12px"` or `"1rem"`: every
37
+ // write below goes through toREM, which scales the first two and leaves the
38
+ // third alone. Written straight to `style`, a bare number is an invalid CSS
39
+ // value the browser drops, and a string is a frozen pixel.
40
+ cellPadding = "8px 12px",
41
+ headerCellPadding = 12,
36
42
 
37
43
  // ========== OTHER PROPS ==========
38
44
  ...rest
@@ -61,7 +67,7 @@ export const DataTable = (props) => {
61
67
  for (const col of columns) {
62
68
  const th = document.createElement("th");
63
69
  th.textContent = typeof col === "string" ? col : col.label;
64
- th.style.padding = headerCellPadding;
70
+ th.style.padding = toREM(headerCellPadding);
65
71
  th.style.textAlign = "left";
66
72
  th.style.borderBottom = `2px solid ${borderColor}`;
67
73
  th.style.backgroundColor = headerBgColor;
@@ -69,7 +75,7 @@ export const DataTable = (props) => {
69
75
  th.style.fontWeight = headerFontWeight;
70
76
  th.style.fontSize =
71
77
  typeof headerFontSize === "number"
72
- ? `${headerFontSize}px`
78
+ ? toREM(headerFontSize)
73
79
  : headerFontSize;
74
80
 
75
81
  // Custom alignment per column
@@ -122,11 +128,11 @@ export const DataTable = (props) => {
122
128
  const value = row[key];
123
129
 
124
130
  td.textContent = value !== undefined ? String(value) : "";
125
- td.style.padding = cellPadding;
131
+ td.style.padding = toREM(cellPadding);
126
132
  td.style.borderBottom = `${borderWidth}px solid ${borderColor}`;
127
133
  td.style.color = rowTextColor;
128
134
  td.style.fontSize =
129
- typeof rowFontSize === "number" ? `${rowFontSize}px` : rowFontSize;
135
+ toREM(rowFontSize);
130
136
 
131
137
  // Custom alignment per column
132
138
  if (typeof col !== "string" && col.align) {
@@ -195,11 +201,11 @@ export const DataTable = (props) => {
195
201
  const value = row[key];
196
202
 
197
203
  td.textContent = value !== undefined ? String(value) : "";
198
- td.style.padding = cellPadding;
204
+ td.style.padding = toREM(cellPadding);
199
205
  td.style.borderBottom = `${borderWidth}px solid ${borderColor}`;
200
206
  td.style.color = rowTextColor;
201
207
  td.style.fontSize =
202
- typeof rowFontSize === "number" ? `${rowFontSize}px` : rowFontSize;
208
+ toREM(rowFontSize);
203
209
 
204
210
  if (typeof col !== "string" && col.align) {
205
211
  td.style.textAlign = col.align;
@@ -236,7 +242,7 @@ export const DataTable = (props) => {
236
242
  for (const col of newColumns) {
237
243
  const th = document.createElement("th");
238
244
  th.textContent = typeof col === "string" ? col : col.label;
239
- th.style.padding = headerCellPadding;
245
+ th.style.padding = toREM(headerCellPadding);
240
246
  th.style.textAlign = "left";
241
247
  th.style.borderBottom = `2px solid ${borderColor}`;
242
248
  th.style.backgroundColor = headerBgColor;
@@ -244,7 +250,7 @@ export const DataTable = (props) => {
244
250
  th.style.fontWeight = headerFontWeight;
245
251
  th.style.fontSize =
246
252
  typeof headerFontSize === "number"
247
- ? `${headerFontSize}px`
253
+ ? toREM(headerFontSize)
248
254
  : headerFontSize;
249
255
 
250
256
  if (typeof col !== "string" && col.align) {
@@ -4,6 +4,7 @@
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";
7
8
 
8
9
  export const DraggBox = (props) => {
9
10
  const {
@@ -53,7 +54,7 @@ export const DraggBox = (props) => {
53
54
  clone.style.zIndex = "99999";
54
55
  clone.style.backgroundColor = dragOverlayColor;
55
56
  clone.style.border = `2px dashed ${dragBorderColor}`;
56
- clone.style.borderRadius = "8px";
57
+ clone.style.borderRadius = toREM(8);
57
58
  document.body.appendChild(clone);
58
59
  return clone;
59
60
  };
@@ -12,6 +12,8 @@ import { Row } from "./Row.js";
12
12
  import { Column } from "./Column.js";
13
13
  import { Text } from "./Text.js";
14
14
  import { Icon } from "./Icon.js";
15
+ import { toREM } from "../utils/units.js";
16
+ import { TAP_MINIMUM } from "../tools/index.js";
15
17
 
16
18
  /**
17
19
  * Props owned by the Dropdown's closure state. Routed to the widget's `update`
@@ -97,10 +99,12 @@ export const Dropdown = (inputProps = {}) => {
97
99
  let resizeListener = null;
98
100
  let scrollListener = null;
99
101
 
102
+ /** [vertical, horizontal] in framework units — see `Input`: a `"8px 14px"`
103
+ * string is raw CSS and stops the field growing with the reader's font. */
100
104
  const sizes = {
101
- small: { padding: "6px 12px", fontSize: 12 },
102
- medium: { padding: "8px 14px", fontSize: 14 },
103
- large: { padding: "12px 16px", fontSize: 16 },
105
+ small: { padding: [4, 12], fontSize: 12, minHeight: 32 },
106
+ medium: { padding: [8, 12], fontSize: 14, minHeight: TAP_MINIMUM },
107
+ large: { padding: [12, 16], fontSize: 16, minHeight: 48 },
104
108
  };
105
109
  let currentSize = sizes[size] || sizes.medium;
106
110
 
@@ -197,14 +201,16 @@ export const Dropdown = (inputProps = {}) => {
197
201
  alignItems: "center",
198
202
  justifyContent: "space-between",
199
203
  width: "100%",
200
- padding: currentSize.padding,
204
+ paddingVertical: currentSize.padding[0],
205
+ paddingHorizontal: currentSize.padding[1],
206
+ minHeight: currentSize.minHeight,
201
207
  backgroundColor:
202
208
  currentVariant === "filled" ? `${currentBgColor}CC` : "transparent",
203
209
  border:
204
210
  currentVariant === "outlined"
205
211
  ? `1px solid ${currentError ? colors.danger : colors.border}`
206
212
  : "none",
207
- borderRadius: `${currentBorderRadius}px`,
213
+ borderRadius: currentBorderRadius,
208
214
  cursor: currentDisabled ? "not-allowed" : "pointer",
209
215
  opacity: currentDisabled ? 0.5 : 1,
210
216
  boxSizing: "border-box",
@@ -236,11 +242,15 @@ export const Dropdown = (inputProps = {}) => {
236
242
  ],
237
243
  });
238
244
 
239
- // Main container
245
+ // The root must not paint a surface: only the selector does, and an
246
+ // `outlined` selector is deliberately transparent. Container's default
247
+ // background otherwise showed through behind the label as a full-width
248
+ // block, and painted the outlined field as if it were filled.
240
249
  const container = Container({
241
250
  widgetName: "Dropdown",
242
251
  position: "relative",
243
252
  width: width,
253
+ bgColor: "transparent",
244
254
  ...rest,
245
255
  });
246
256
 
@@ -248,9 +258,9 @@ export const Dropdown = (inputProps = {}) => {
248
258
  const labelEl = WidgetFactory({
249
259
  tag: "label",
250
260
  textContent: currentLabel,
251
- fontSize: "12px",
261
+ fontSize: 12,
252
262
  color: currentError ? colors.danger : colors.textSecondary,
253
- marginBottom: "4px",
263
+ marginBottom: 4,
254
264
  display: "block",
255
265
  });
256
266
  container.appendChild(labelEl);
@@ -318,7 +328,7 @@ export const Dropdown = (inputProps = {}) => {
318
328
  zIndex: 10000,
319
329
  border: `1px solid ${colors.border}`,
320
330
  backgroundColor: currentBgColor,
321
- borderRadius: `${currentBorderRadius}px`, // same borderRadius
331
+ borderRadius: currentBorderRadius, // same borderRadius
322
332
  boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
323
333
  maxHeight: "250px",
324
334
  overflowY: "auto",
@@ -348,12 +358,14 @@ export const Dropdown = (inputProps = {}) => {
348
358
  );
349
359
 
350
360
  const optionItem = Row({
351
- padding: "10px 16px",
361
+ paddingVertical: 8,
362
+ paddingHorizontal: 12,
363
+ minHeight: TAP_MINIMUM,
352
364
  cursor: "pointer",
353
365
  backgroundColor: isSelected ? currentColor + "15" : "transparent",
354
366
  alignItems: "center",
355
367
  justifyContent: "space-between",
356
- gap: "8px",
368
+ gap: 8,
357
369
  children: [
358
370
  Row({
359
371
  alignItems: "center",
@@ -496,8 +508,9 @@ export const Dropdown = (inputProps = {}) => {
496
508
  }
497
509
  if (needsDisplayUpdate) {
498
510
  updateDisplay();
499
- selector.style.padding = currentSize.padding;
500
- selector.style.borderRadius = `${currentBorderRadius}px`;
511
+ selector.style.padding = `${toREM(currentSize.padding[0])} ${toREM(currentSize.padding[1])}`;
512
+ selector.style.minHeight = toREM(currentSize.minHeight);
513
+ selector.style.borderRadius = toREM(currentBorderRadius);
501
514
  if (currentVariant === "outlined")
502
515
  selector.style.border = `1px solid ${currentError ? colors.danger : colors.border}`;
503
516
  else selector.style.border = "none";
@@ -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 DroppBox = (props) => {
9
11
  const {
10
12
  child,
@@ -58,12 +60,12 @@ export const DroppBox = (props) => {
58
60
  transition: `all ${transitionDuration} ${transitionTiming}`,
59
61
  backgroundColor: bgColor,
60
62
  borderRadius:
61
- typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
63
+ toREM(borderRadius),
62
64
  borderWidth: `${borderWidth}px`,
63
65
  borderStyle: borderStyle,
64
66
  borderColor: borderColor,
65
67
  boxShadow: shadow,
66
- padding: typeof padding === "number" ? `${padding}px` : padding,
68
+ padding: toREM(padding),
67
69
  ...rest,
68
70
  });
69
71
 
@@ -7,6 +7,8 @@ import { Row } from "./Row.js";
7
7
  import { Text } from "./Text.js";
8
8
  import { Icon } from "./Icon.js";
9
9
  import { colors } from "../utils/themes.js";
10
+ import { toREM } from "../utils/units.js";
11
+ import { SHADOW_LEVELS, PILL } from "../tools/index.js";
10
12
 
11
13
  export const FloatingActionButton = (props) => {
12
14
  const {
@@ -14,9 +16,11 @@ export const FloatingActionButton = (props) => {
14
16
  label,
15
17
  onPress,
16
18
  backgroundColor = colors.primary,
17
- foregroundColor = "#ffffff",
18
- elevation = 6,
19
- borderRadius = 48,
19
+ foregroundColor = colors.onPrimary,
20
+ // Depth is the 0-5 scale, like every other widget; 6 was a number the scale
21
+ // does not have, which only worked because this widget built its own string.
22
+ elevation = 4,
23
+ borderRadius = PILL,
20
24
  padding = 12,
21
25
  mini = false,
22
26
  extended = false,
@@ -24,7 +28,9 @@ export const FloatingActionButton = (props) => {
24
28
  ...rest
25
29
  } = props;
26
30
 
27
- const size = mini ? 40 : 56;
31
+ // 48 and 40 are both scale steps, and both clear the Fitts floor: the mini FAB
32
+ // sits exactly on it, which is as small as a floating target may get.
33
+ const size = mini ? 40 : 48;
28
34
  const iconSize = mini ? 20 : 24;
29
35
 
30
36
  const children = [
@@ -58,14 +64,14 @@ export const FloatingActionButton = (props) => {
58
64
  gap: 8,
59
65
  width: extended ? "auto" : size,
60
66
  height: size,
61
- borderRadius: extended ? "24px" : "50%",
67
+ borderRadius: extended ? borderRadius : "50%",
62
68
  backgroundColor: backgroundColor,
63
69
  color: foregroundColor,
64
70
  cursor: disabled ? "not-allowed" : "pointer",
65
71
  opacity: disabled ? 0.6 : 1,
66
- boxShadow: `0 ${elevation / 2}px ${elevation}px rgba(0,0,0,0.3)`,
72
+ boxShadow: SHADOW_LEVELS[elevation] ?? SHADOW_LEVELS[4],
67
73
  transition: "all 0.2s ease",
68
- padding: extended ? "0 20px" : 0,
74
+ padding: extended ? `0 ${toREM(16)}` : 0,
69
75
  child: extended
70
76
  ? Row({ alignItems: "center", gap: 8, children })
71
77
  : children[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",
@@ -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({