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
@@ -26,9 +26,16 @@ export const stackPosition = (widget, props) => {
26
26
  // Absolute positioning
27
27
  widget.style.position = "absolute";
28
28
 
29
+ // `centerX` + `centerY` must compose: writing each transform as it is found
30
+ // would let the second one erase the first.
31
+ const transforms = [];
32
+
29
33
  // Process each position prop
30
34
  for (const [key, value] of Object.entries(props)) {
31
35
  if (key === "position") continue;
36
+ // The placement shorthands are booleans: a present `false` means "do not
37
+ // apply", not "the key exists".
38
+ if (value === false) continue;
32
39
 
33
40
  switch (key) {
34
41
  case "top":
@@ -51,16 +58,16 @@ export const stackPosition = (widget, props) => {
51
58
  break;
52
59
  case "centerX":
53
60
  widget.style.left = "50%";
54
- widget.style.transform = "translateX(-50%)";
61
+ transforms.push("translateX(-50%)");
55
62
  break;
56
63
  case "centerY":
57
64
  widget.style.top = "50%";
58
- widget.style.transform = "translateY(-50%)";
65
+ transforms.push("translateY(-50%)");
59
66
  break;
60
67
  case "center":
61
68
  widget.style.top = "50%";
62
69
  widget.style.left = "50%";
63
- widget.style.transform = "translate(-50%, -50%)";
70
+ transforms.push("translate(-50%, -50%)");
64
71
  break;
65
72
  case "fullWidth":
66
73
  widget.style.width = "100%";
@@ -81,6 +88,14 @@ export const stackPosition = (widget, props) => {
81
88
  }
82
89
  }
83
90
 
91
+ if (transforms.length) {
92
+ // `center` already covers both axes; mixing it with a single-axis shorthand
93
+ // must not shift the widget twice.
94
+ widget.style.transform = transforms.includes("translate(-50%, -50%)")
95
+ ? "translate(-50%, -50%)"
96
+ : transforms.join(" ");
97
+ }
98
+
84
99
  return widget;
85
100
  };
86
101
 
@@ -183,6 +183,34 @@ export const ALL_PROPS = [
183
183
  { prop: "boxShadow", domProp: "boxShadow", type: "style", unit: "none", reactive: true },
184
184
  { prop: "elevation", domProp: "boxShadow", type: "style", unit: "none", reactive: true },
185
185
 
186
+ // ============================================================
187
+ // GLASS & FILTERS
188
+ // ============================================================
189
+ // A translucent card is only glass when what sits behind it blurs, and there
190
+ // is no `style` object in this API — so if `backdrop-filter` were not routed,
191
+ // the effect would be unreachable rather than merely undocumented.
192
+ //
193
+ // Both names are written because WebKit only lifted the `-webkit-` requirement
194
+ // for `backdrop-filter` in Safari 18, and this framework ships iOS apps.
195
+ {
196
+ prop: "backdropFilter",
197
+ domProp: "backdropFilter",
198
+ type: "style",
199
+ unit: "none",
200
+ reactive: true,
201
+ targets: ["backdropFilter", "WebkitBackdropFilter"],
202
+ },
203
+ // Composes with the object builder in `src/tools/filter.js`.
204
+ { prop: "filter", domProp: "filter", type: "style", unit: "none", reactive: true },
205
+ {
206
+ prop: "maskImage",
207
+ domProp: "maskImage",
208
+ type: "style",
209
+ unit: "none",
210
+ reactive: true,
211
+ targets: ["maskImage", "WebkitMaskImage"],
212
+ },
213
+
186
214
  // ============================================================
187
215
  // DISPLAY & LAYOUT
188
216
  // ============================================================
@@ -8,11 +8,13 @@
8
8
 
9
9
  import { WidgetFactory } from "../widget-factory/index.js";
10
10
  import { colors } from "../utils/themes.js";
11
+ import { toREM } from "../utils/units.js";
11
12
  import { composeUpdate } from "../utils/composeUpdate.js";
12
13
  import { Container } from "./Container.js";
13
14
  import { Row } from "./Row.js";
14
15
  import { Text } from "./Text.js";
15
16
  import { Icon } from "./Icon.js";
17
+ import { SHADOW_LEVELS } from "../tools/shadow.js";
16
18
 
17
19
  /**
18
20
  * Props owned by the Accordion's own closure state. They are routed to the
@@ -41,14 +43,14 @@ const ACCORDION_PROP_KEYS = [
41
43
  * @param {number} [props.borderRadius=8] - Corner radius in pixels.
42
44
  * @param {string} [props.bgColor=colors.surface] - Background color of the container (used in 'contained' variant).
43
45
  * @param {string} [props.titleColor=colors.text] - Text color of the title when collapsed.
44
- * @param {string} [props.expandedColor=colors.primary] - Accent color applied to the title and background when expanded.
46
+ * @param {string} [props.expandedColor=colors.primary] - Accent color applied to the title-bar background when expanded.
45
47
  * @param {string|null} [props.border=null] - Explicit CSS border shorthand. Overrides variant-based border logic.
46
48
  * @param {string} [props.borderColor=colors.border] - Border color used in 'outlined' variant.
47
49
  * @param {number} [props.borderWidth=1] - Border width in pixels (outlined variant).
48
50
  * @param {number} [props.titleSize=14] - Font size of the title text in pixels.
49
51
  * @param {string} [props.titleWeight='500'] - Font weight of the title text.
50
- * @param {string} [props.titlePadding='12px 16px'] - CSS padding for the title bar.
51
- * @param {string} [props.contentPadding='16px'] - CSS padding applied to the inner content area.
52
+ * @param {string} [props.titlePadding='0.75rem 1rem'] - CSS padding for the title bar.
53
+ * @param {string} [props.contentPadding='1rem'] - CSS padding applied to the inner content area.
52
54
  * @param {string} [props.iconCollapsed='chevron_right'] - Material icon name shown when collapsed.
53
55
  * @param {string} [props.iconExpanded='expand_more'] - Material icon name shown when expanded.
54
56
  * @param {string} [props.iconColor=colors.textSecondary] - Color of the expand/collapse icon.
@@ -83,7 +85,7 @@ export const Accordion = (props) => {
83
85
  titleWeight = "500",
84
86
  titlePadding = "12px 16px",
85
87
  // Content
86
- contentPadding = "16px",
88
+ contentPadding = 16,
87
89
  // Icon
88
90
  iconCollapsed = "chevron_right",
89
91
  iconExpanded = "expand_more",
@@ -149,11 +151,11 @@ export const Accordion = (props) => {
149
151
  titleElement.textContent = currentTitle;
150
152
  }
151
153
 
152
- // Title color shifts to the accent color when expanded
153
- titleElement.style.color = isExpanded
154
- ? currentExpandedColor
155
- : currentTitleColor;
156
- titleElement.style.fontSize = `${currentTitleSize}px`;
154
+ // The accent marks the open state through the title-bar tint; the title
155
+ // itself keeps `titleColor`. Painting it with `primary` put #00A5FF words
156
+ // on a 6% primary wash (2.4:1) in the light theme.
157
+ titleElement.style.color = currentTitleColor;
158
+ titleElement.style.fontSize = toREM(currentTitleSize);
157
159
  titleElement.style.fontWeight = currentTitleWeight;
158
160
 
159
161
  // Swap icon based on current expanded state. `Icon` renders its glyph as
@@ -163,12 +165,12 @@ export const Accordion = (props) => {
163
165
  ? currentIconExpanded
164
166
  : currentIconCollapsed;
165
167
  iconElement.style.color = currentIconColor;
166
- iconElement.style.fontSize = `${currentIconSize}px`;
168
+ iconElement.style.fontSize = toREM(currentIconSize);
167
169
 
168
170
  // Title bar cursor and opacity reflect the disabled state
169
171
  titleBar.style.cursor = currentDisabled ? "not-allowed" : "pointer";
170
172
  titleBar.style.opacity = currentDisabled ? "0.5" : "1";
171
- titleBar.style.padding = currentTitlePadding;
173
+ titleBar.style.padding = toREM(currentTitlePadding);
172
174
 
173
175
  // Tint the title bar background slightly when expanded (contained only)
174
176
  if (currentVariant === "contained") {
@@ -202,23 +204,18 @@ export const Accordion = (props) => {
202
204
  container.style.backgroundColor = "transparent";
203
205
  }
204
206
 
205
- container.style.borderRadius = `${currentBorderRadius}px`;
207
+ container.style.borderRadius = toREM(currentBorderRadius);
206
208
  container.style.overflow = "hidden";
207
209
 
208
- // Predefined shadow presets keyed by elevation level
210
+ // The framework's depth scale, not a local copy of it (src/tools/shadow.js).
209
211
  if (currentElevation > 0) {
210
- const shadows = {
211
- 1: "0 1px 3px rgba(0,0,0,0.12)",
212
- 2: "0 3px 6px rgba(0,0,0,0.16)",
213
- 3: "0 6px 12px rgba(0,0,0,0.2)",
214
- 4: "0 10px 20px rgba(0,0,0,0.25)",
215
- };
216
- container.style.boxShadow = shadows[currentElevation] || shadows[2];
212
+ container.style.boxShadow =
213
+ SHADOW_LEVELS[currentElevation] || SHADOW_LEVELS[2];
217
214
  } else {
218
215
  container.style.boxShadow = "none";
219
216
  }
220
217
 
221
- contentInner.style.padding = currentContentPadding;
218
+ contentInner.style.padding = toREM(currentContentPadding);
222
219
  };
223
220
 
224
221
  // ========== HEIGHT ANIMATION ==========
@@ -238,7 +235,7 @@ export const Accordion = (props) => {
238
235
  clone.style.visibility = "hidden";
239
236
  clone.style.display = "block";
240
237
  clone.style.height = "auto";
241
- clone.style.padding = currentContentPadding;
238
+ clone.style.padding = toREM(currentContentPadding);
242
239
  document.body.appendChild(clone);
243
240
  const height = clone.offsetHeight;
244
241
  document.body.removeChild(clone);
@@ -508,7 +505,7 @@ export const Accordion = (props) => {
508
505
  currentTitlePadding = newProps.titlePadding;
509
506
  if (newProps.contentPadding !== undefined) {
510
507
  currentContentPadding = newProps.contentPadding;
511
- contentInner.style.padding = currentContentPadding;
508
+ contentInner.style.padding = toREM(currentContentPadding);
512
509
  }
513
510
  if (newProps.iconCollapsed !== undefined)
514
511
  currentIconCollapsed = newProps.iconCollapsed;
@@ -205,7 +205,7 @@ export const AdMob = (props) => {
205
205
  }
206
206
 
207
207
  const showBanner = () => {
208
- if (disposed || !autoShow || bannerVisible) return Promise.resolve(null);
208
+ if (disposed || bannerVisible) return Promise.resolve(null);
209
209
  return withPlugin((plugin) =>
210
210
  Promise.resolve(
211
211
  plugin.showBanner({
@@ -305,7 +305,12 @@ export const AdMob = (props) => {
305
305
  };
306
306
  container.onUnmount(dispose);
307
307
 
308
- container.onMount(showBanner);
308
+ // `autoShow` is the mount switch, nothing more. An app that holds the banner
309
+ // back for a paywall moment still has to be able to ask for it later with
310
+ // `container.show()`, so the flag must not gate that method.
311
+ container.onMount(() => {
312
+ if (autoShow) showBanner();
313
+ });
309
314
 
310
315
  refreshPlaceholder();
311
316
 
@@ -30,6 +30,8 @@ import { Text } from "./Text.js";
30
30
  * them (`backgroundColor`, `font-size`), so it can re-apply and re-subscribe
31
31
  * them better than this widget can.
32
32
  */
33
+ import { toREM } from "../utils/units.js";
34
+
33
35
  const AVATAR_PROPS = [
34
36
  "src",
35
37
  "name",
@@ -53,8 +55,7 @@ const DOT_SIZE_RATIO = 0.3;
53
55
  */
54
56
  const DOT_INSET_RATIO = 0.12;
55
57
 
56
- /** CSS length for a number of pixels; any other value is passed through. */
57
- const px = (value) => (typeof value === "number" ? `${value}px` : value);
58
+ /** Framework units → a CSS length that scales with the reader's base font. */
58
59
 
59
60
  /**
60
61
  * Extracts up to two initials from a full name.
@@ -87,7 +88,7 @@ const getInitials = (fullName) => {
87
88
  * @param {number} [props.fontSize=16] - Font size of the avatar box in pixels; initials are sized from `size`.
88
89
  * @param {'circle'|'rounded'|'square'} [props.shape='circle'] - Shape of the avatar container.
89
90
  * @param {string} [props.bgColor=colors.primary] - Background color shown behind icons or initials.
90
- * @param {string} [props.textColor=colors.text] - Color of icon or initials.
91
+ * @param {string} [props.textColor=colors.onPrimary] - Color of icon or initials.
91
92
  * @param {string} [props.icon] - Material icon name to display instead of an image or initials.
92
93
  * @param {boolean} [props.online=false] - Shows a presence dot in the bottom-right corner.
93
94
  * @param {string} [props.badgeColor=colors.success] - Color of the presence dot.
@@ -117,7 +118,9 @@ export const Avatar = (inputProps = {}) => {
117
118
  fontSize = 16,
118
119
  shape = "circle",
119
120
  bgColor = colors.primary,
120
- textColor = colors.text,
121
+ // The default plate is `primary`, so the initials are its ink. Reading
122
+ // `colors.text` here put near-white letters on a near-white avatar in dark.
123
+ textColor = colors.onPrimary,
121
124
  icon,
122
125
  online = false,
123
126
  badgeColor = colors.success,
@@ -131,7 +134,7 @@ export const Avatar = (inputProps = {}) => {
131
134
  * previous shape would stay on the element.
132
135
  */
133
136
  const radiusFor = () => {
134
- if (shape === "rounded") return `${size * 0.2}px`;
137
+ if (shape === "rounded") return toREM(size * 0.2);
135
138
  if (shape === "square") return "0";
136
139
  return "50%";
137
140
  };
@@ -180,9 +183,12 @@ export const Avatar = (inputProps = {}) => {
180
183
  * `height` or `borderRadius` still wins, as it always has.
181
184
  */
182
185
  const paintGeometry = () => {
183
- avatar.style.width = px(rest.width ?? size);
184
- avatar.style.height = px(rest.height ?? size);
185
- avatar.style.borderRadius = rest.borderRadius ?? radiusFor();
186
+ avatar.style.width = toREM(rest.width ?? size);
187
+ avatar.style.height = toREM(rest.height ?? size);
188
+ // `radiusFor()` is already a CSS value; a caller's `borderRadius: 8` is a
189
+ // framework number, and written straight to `style` it is an invalid value
190
+ // the browser drops. toREM tells the two apart and scales the first.
191
+ avatar.style.borderRadius = toREM(rest.borderRadius ?? radiusFor());
186
192
  };
187
193
 
188
194
  paintGeometry();
@@ -201,10 +207,10 @@ export const Avatar = (inputProps = {}) => {
201
207
  avatar.appendChild(dot);
202
208
 
203
209
  const paintDot = () => {
204
- dot.style.width = `${Math.round(size * DOT_SIZE_RATIO)}px`;
205
- dot.style.height = `${Math.round(size * DOT_SIZE_RATIO)}px`;
206
- dot.style.right = `${Math.round(size * DOT_INSET_RATIO)}px`;
207
- dot.style.bottom = `${Math.round(size * DOT_INSET_RATIO)}px`;
210
+ dot.style.width = toREM(Math.round(size * DOT_SIZE_RATIO));
211
+ dot.style.height = toREM(Math.round(size * DOT_SIZE_RATIO));
212
+ dot.style.right = toREM(Math.round(size * DOT_INSET_RATIO));
213
+ dot.style.bottom = toREM(Math.round(size * DOT_INSET_RATIO));
208
214
  dot.style.backgroundColor = badgeColor;
209
215
  dot.style.display = online ? "block" : "none";
210
216
  };
@@ -13,6 +13,7 @@ import { WidgetFactory } from "../widget-factory/index.js";
13
13
  import { Stack } from "./Stack.js";
14
14
  import { Text } from "./Text.js";
15
15
  import { colors, subscribeTheme } from "../utils/themes.js";
16
+ import { toREM } from "../utils/units.js";
16
17
 
17
18
  /**
18
19
  * Creates a Badge widget that overlays a small count or status indicator on
@@ -40,7 +41,7 @@ export const Badge = (props) => {
40
41
  value,
41
42
  child,
42
43
  bgColor = colors.secondary,
43
- color = colors.text,
44
+ color = colors.onSecondary,
44
45
  size = 20,
45
46
  position = "top-right",
46
47
  borderWidth = 2,
@@ -113,7 +114,7 @@ export const Badge = (props) => {
113
114
  minWidth: size,
114
115
  height: size,
115
116
  // Extra horizontal padding for two-digit numbers so they don't clip
116
- padding: size > 20 ? `0 ${size / 3}px` : 0,
117
+ padding: size > 20 ? `0 ${toREM(size / 3)}` : 0,
117
118
  display: "flex",
118
119
  alignItems: "center",
119
120
  justifyContent: "center",
@@ -9,6 +9,7 @@ import { Column } from "./Column.js";
9
9
  import { Text } from "./Text.js";
10
10
  import { Icon } from "./Icon.js";
11
11
  import { Container } from "./Container.js";
12
+ import { toREM } from "../utils/units.js";
12
13
 
13
14
  export const BottomSheet = (props) => {
14
15
  const {
@@ -31,14 +32,16 @@ export const BottomSheet = (props) => {
31
32
  actionBorderColor = colors.border,
32
33
 
33
34
  // Borders and shadows
34
- borderRadius = 24,
35
+ borderRadius = 16,
35
36
  shadow = "0 -4px 12px rgba(0,0,0,0.1)",
36
37
 
37
- // Spacing
38
- headerPadding = "0 16px 8px 16px",
39
- contentPadding = "0 16px",
40
- actionPadding = "12px 16px",
41
- dragHandlePadding = "14px 0 8px 0",
38
+ // Spacing. These are CSS shorthands the widget writes by hand, so each
39
+ // number goes through `toREM` — a literal "16px" here would leave the sheet
40
+ // frozen at one text size while its own type grew.
41
+ headerPadding = `0 ${toREM(16)} ${toREM(8)} ${toREM(16)}`,
42
+ contentPadding = `0 ${toREM(16)}`,
43
+ actionPadding = `${toREM(12)} ${toREM(16)}`,
44
+ dragHandlePadding = `${toREM(16)} 0 ${toREM(8)} 0`,
42
45
  dragHandleWidth = 40,
43
46
  dragHandleHeight = 4,
44
47
 
@@ -76,12 +79,10 @@ export const BottomSheet = (props) => {
76
79
  opacity: 0,
77
80
  visibility: "hidden",
78
81
  transition: `opacity ${animationDuration}ms ease, visibility ${animationDuration}ms ease`,
79
- // onclick: (e) => {
80
- // // ✅ Only close if the click was directly on the overlay (not on the sheet)
81
- // if (e.target === overlay && closeOnOverlayClick) {
82
- // close();
83
- // }
84
- // },
82
+ onclick: (e) => {
83
+ // Only close if the click was directly on the overlay, not on the sheet.
84
+ if (e.target === e.currentTarget && closeOnOverlayClick) close();
85
+ },
85
86
  });
86
87
 
87
88
  // ✅ Drag handle with stopPropagation
@@ -166,7 +167,7 @@ onclick: (e) => e.stopPropagation(), // ✅ Prevents the click from reaching the
166
167
  right: 0,
167
168
  paddingBottom: 12,
168
169
  backgroundColor: backgroundColor,
169
- borderRadius: `${borderRadius}px ${borderRadius}px 0 0`,
170
+ borderRadius: `${toREM(borderRadius)} ${toREM(borderRadius)} 0 0`,
170
171
  boxShadow: shadow,
171
172
  transform: "translateY(100%)",
172
173
  transition: `transform ${animationDuration}ms ease`,
@@ -3,6 +3,8 @@
3
3
  * @description Button with size presets, variants, icons and gradient support.
4
4
  */
5
5
  import { WidgetFactory } from "../widget-factory/index.js";import { colors } from "../utils/themes.js";
6
+ import { SHADOW_LEVELS, TAP_MINIMUM } from "../tools/index.js";
7
+ import { readableInk } from "../utils/readableInk.js";
6
8
  import { Text } from "./Text.js";
7
9
  import { Icon } from "./Icon.js";
8
10
 
@@ -10,19 +12,20 @@ import { Icon } from "./Icon.js";
10
12
  // CONFIGURATION
11
13
  // ============================================================
12
14
 
15
+ /**
16
+ * Every number here is a framework unit — the factory converts lengths to `rem`,
17
+ * so the whole preset grows with the reader's font size instead of freezing at
18
+ * the pixel it was written in.
19
+ *
20
+ * `minHeight` is the Fitts floor: a pressable box the finger can hit, set on the
21
+ * box rather than reached through padding, so `small` stays a small *looking*
22
+ * control that is still a real target. `medium` and `large` land on 40 and 48;
23
+ * `small` is 32 and is a desktop-density escape hatch, not a touch target.
24
+ */
13
25
  const SIZE_PRESETS = {
14
- small: { padding: [6, 12], fontSize: 12, gap: 6, iconSize: 16 },
15
- medium: { padding: [10, 20], fontSize: 14, gap: 8, iconSize: 20 },
16
- large: { padding: [14, 28], fontSize: 16, gap: 10, iconSize: 24 },
17
- };
18
-
19
- const SHADOW_LEVELS = {
20
- 0: "none",
21
- 1: "0 1px 3px rgba(0,0,0,0.12)",
22
- 2: "0 3px 6px rgba(0,0,0,0.16)",
23
- 3: "0 6px 12px rgba(0,0,0,0.2)",
24
- 4: "0 10px 20px rgba(0,0,0,0.25)",
25
- 5: "0 15px 30px rgba(0,0,0,0.3)",
26
+ small: { padding: [4, 12], fontSize: 12, gap: 4, iconSize: 16, minHeight: 32 },
27
+ medium: { padding: [8, 16], fontSize: 14, gap: 8, iconSize: 20, minHeight: TAP_MINIMUM },
28
+ large: { padding: [12, 24], fontSize: 16, gap: 12, iconSize: 24, minHeight: 48 },
26
29
  };
27
30
 
28
31
  // ============================================================
@@ -42,20 +45,35 @@ function computeBackground(variant, disabled, bgColor, gradient) {
42
45
  return "transparent";
43
46
  }
44
47
 
45
- function computeTextColor(variant, disabled, color, bgColor) {
48
+ function computeTextColor(variant, disabled, color, background, inkFromFill) {
46
49
  if (disabled) return colors.textDisabled;
47
- if (variant === "filled") return color || "#fff";
48
- return color || bgColor || colors.primary;
50
+ if (variant === "filled") {
51
+ // The default `color` is the theme's ink, which is the right answer for a
52
+ // filled button sitting on the default surface. Once the caller names the
53
+ // fill, that ink can be the fill itself (dark on the light theme's primary,
54
+ // near-white on the dark theme's), so the label has to follow the plate.
55
+ if (!inkFromFill) return color;
56
+ if (background === colors.primary) return colors.onPrimary;
57
+ if (background === colors.secondary) return colors.onSecondary;
58
+ return readableInk(background, colors.text);
59
+ }
60
+ return color || background || colors.primary;
49
61
  }
50
62
 
51
- function computeBorder(variant, disabled, color, bgColor) {
63
+ function computeBorder(variant, disabled, color, bgColor, borderColor, namesItsOwnFill) {
52
64
  if (disabled) return `1px solid ${colors.gray300}`;
53
65
  if (variant === "outlined") {
54
- const borderColor = color || bgColor || colors.primary;
55
- return `2px solid ${borderColor}`;
66
+ const tint = color || bgColor || colors.primary;
67
+ return `2px solid ${tint}`;
56
68
  }
57
69
  if (variant === "text") return "none";
58
- return `1px solid ${colors.border}`;
70
+ // A filled button whose caller named the fill is a colour decision; a neutral
71
+ // grey ring around it is an edge that belongs to no token and reads as a
72
+ // rendering bug on the deep variants. A button that names no fill lands on
73
+ // `colors.surface`, and there the hairline is what keeps the control findable
74
+ // on a card of the same colour. An explicit `borderColor` always wins.
75
+ if (borderColor) return `1px solid ${borderColor}`;
76
+ return namesItsOwnFill ? "none" : `1px solid ${colors.border}`;
59
77
  }
60
78
 
61
79
  function createIcon(name, size, color) {
@@ -120,11 +138,8 @@ function buildButtonChildren(text, textColor, fontSize, gap, iconElements) {
120
138
  return children;
121
139
  }
122
140
 
123
- function applySpacingToProps(buttonProps, padding, margin) {
124
- const normalizedPadding = normalizeSpacing(
125
- padding,
126
- SIZE_PRESETS.medium.padding,
127
- );
141
+ function applySpacingToProps(buttonProps, padding, margin, fallbackPadding) {
142
+ const normalizedPadding = normalizeSpacing(padding, fallbackPadding);
128
143
  const normalizedMargin = normalizeSpacing(margin, undefined);
129
144
 
130
145
  if (Array.isArray(normalizedPadding)) {
@@ -165,8 +180,8 @@ export const Button = (props) => {
165
180
  iconPosition = "left",
166
181
  variant = "filled",
167
182
  size = "medium",
168
- borderRadius = 24,
169
- elevation = 2,
183
+ borderRadius = 16,
184
+ elevation = null,
170
185
  disabled = false,
171
186
  fullWidth = false,
172
187
  bgColor = colors.surface,
@@ -175,11 +190,34 @@ export const Button = (props) => {
175
190
  } = props;
176
191
 
177
192
  const sizePreset = SIZE_PRESETS[size] || SIZE_PRESETS.medium;
178
- const shadow = SHADOW_LEVELS[elevation] || SHADOW_LEVELS[0];
193
+ // Depth follows the variant: a text or outlined button is not a surface lifted
194
+ // off the page, and a shadow drawn around a transparent box paints a grey blob
195
+ // where the label should sit alone. An explicit `elevation` still wins.
196
+ const lifted = elevation === null ? (variant === "filled" ? 2 : 0) : elevation;
197
+ const shadow = SHADOW_LEVELS[lifted] || SHADOW_LEVELS[0];
179
198
 
180
199
  const background = computeBackground(variant, disabled, bgColor, gradient);
181
- const textColor = computeTextColor(variant, disabled, color, bgColor);
182
- const border = computeBorder(variant, disabled, color, bgColor);
200
+ // `bgColor` above always has a value (the default is the surface), so whether
201
+ // the caller chose the fill has to be read off the props, not the local.
202
+ const namesItsOwnFill =
203
+ props.bgColor !== undefined ||
204
+ props.backgroundColor !== undefined ||
205
+ !!gradient;
206
+ const textColor = computeTextColor(
207
+ variant,
208
+ disabled,
209
+ color,
210
+ background,
211
+ props.color === undefined && namesItsOwnFill,
212
+ );
213
+ const border = computeBorder(
214
+ variant,
215
+ disabled,
216
+ color,
217
+ bgColor,
218
+ rest.borderColor,
219
+ namesItsOwnFill,
220
+ );
183
221
 
184
222
  const iconNames = resolveIconNames({
185
223
  icon,
@@ -216,6 +254,7 @@ export const Button = (props) => {
216
254
  justifyContent: "center",
217
255
  gap: sizePreset.gap,
218
256
  borderRadius,
257
+ minHeight: sizePreset.minHeight,
219
258
  width: fullWidth ? "100%" : "auto",
220
259
  backgroundColor: background,
221
260
  border,
@@ -231,7 +270,7 @@ export const Button = (props) => {
231
270
  ...rest,
232
271
  };
233
272
 
234
- applySpacingToProps(buttonProps, padding, margin);
273
+ applySpacingToProps(buttonProps, padding, margin, sizePreset.padding);
235
274
 
236
275
  if (gradient) {
237
276
  buttonProps.background = gradient;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { colors } from "../utils/themes.js";
7
- import { border } from "../tools/index.js";
7
+ import { border, SHADOW_LEVELS } from "../tools/index.js";
8
8
 
9
9
  export const Card = (props = {}) => {
10
10
  const {
@@ -24,7 +24,11 @@ export const Card = (props = {}) => {
24
24
  backgroundColor: bgColor,
25
25
  borderRadius: borderRadius,
26
26
  padding: padding,
27
- elevation: elevation,
27
+ // The engine routes `elevation` straight into `style.boxShadow`, and CSS drops
28
+ // a bare number: forwarding it left every card flat, whatever depth was asked
29
+ // for. Depth is a scale, so the level is read here into a real shadow — and an
30
+ // explicit `shadow`/`boxShadow` in `rest` still wins over the scale.
31
+ boxShadow: SHADOW_LEVELS[elevation] ?? SHADOW_LEVELS[2],
28
32
  border: showBorder ? border(borderWidth, borderStyle, borderColor) : "none",
29
33
  ...rest,
30
34
  });
@@ -6,6 +6,8 @@ import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { Icon } from "./Icon.js";
7
7
  import { colors } from "../utils/themes.js";
8
8
 
9
+ import { toREM } from "../utils/units.js";
10
+
9
11
  export const Carousel = (props) => {
10
12
  const {
11
13
  items = [],
@@ -59,12 +61,12 @@ export const Carousel = (props) => {
59
61
  dot.style.backgroundColor = dotActiveColor;
60
62
  dot.style.width =
61
63
  typeof dotActiveSize === "number"
62
- ? `${dotActiveSize}px`
64
+ ? toREM(dotActiveSize)
63
65
  : dotActiveSize;
64
66
  } else {
65
67
  dot.style.backgroundColor = dotColor;
66
68
  dot.style.width =
67
- typeof dotSize === "number" ? `${dotSize}px` : dotSize;
69
+ toREM(dotSize);
68
70
  }
69
71
  }
70
72
  };
@@ -154,8 +156,8 @@ export const Carousel = (props) => {
154
156
  cursor: "pointer",
155
157
  backgroundColor: buttonBgColor,
156
158
  borderRadius: "50%",
157
- width: typeof buttonSize === "number" ? `${buttonSize}px` : buttonSize,
158
- height: typeof buttonSize === "number" ? `${buttonSize}px` : buttonSize,
159
+ width: toREM(buttonSize),
160
+ height: toREM(buttonSize),
159
161
  display: "flex",
160
162
  alignItems: "center",
161
163
  justifyContent: "center",
@@ -173,11 +175,11 @@ export const Carousel = (props) => {
173
175
  const container = WidgetFactory({
174
176
  widgetName: "Carousel",
175
177
  position: "relative",
176
- width: typeof width === "number" ? `${width}px` : width,
177
- height: typeof height === "number" ? `${height}px` : height,
178
+ width: toREM(width),
179
+ height: toREM(height),
178
180
  overflow: "hidden",
179
181
  borderRadius:
180
- typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
182
+ toREM(borderRadius),
181
183
  ...rest,
182
184
  });
183
185
 
@@ -255,12 +257,12 @@ export const Carousel = (props) => {
255
257
  width:
256
258
  index === currentIndex
257
259
  ? typeof dotActiveSize === "number"
258
- ? `${dotActiveSize}px`
260
+ ? toREM(dotActiveSize)
259
261
  : dotActiveSize
260
262
  : typeof dotSize === "number"
261
- ? `${dotSize}px`
263
+ ? toREM(dotSize)
262
264
  : dotSize,
263
- height: typeof dotSize === "number" ? `${dotSize}px` : dotSize,
265
+ height: toREM(dotSize),
264
266
  borderRadius: "4px",
265
267
  backgroundColor: index === currentIndex ? dotActiveColor : dotColor,
266
268
  cursor: "pointer",