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
@@ -3,16 +3,75 @@
3
3
  * @description A user avatar widget that renders an image, an icon, or
4
4
  * initials derived from a name, depending on what props are provided.
5
5
  * Falls back gracefully: image → icon → initials → generic person icon.
6
- * Supports circular, rounded-square, and square shapes, and exposes a method
7
- * to update its content without rebuilding the DOM.
6
+ * Supports circular, rounded-square, and square shapes, shows an optional
7
+ * presence dot, and applies every one of its own props again on update.
8
8
  */
9
9
 
10
10
  import { WidgetFactory } from "../widget-factory/index.js";
11
11
  import { colors } from "../utils/themes.js";
12
+ import { composeUpdate } from "../utils/composeUpdate.js";
13
+ import {
14
+ isReactive,
15
+ reactiveKeyOf,
16
+ resolveReactive,
17
+ } from "../widget-factory/reactiveValue.js";
18
+ import { registerWidget } from "../tools/useState.js";
12
19
  import { Image } from "./Image.js";
13
20
  import { Icon } from "./Icon.js";
14
21
  import { Text } from "./Text.js";
15
22
 
23
+ /**
24
+ * Props the avatar applies itself: content (image, icon, initials), the box it
25
+ * sits in, and the presence dot. They must never reach the factory — and the
26
+ * factory must not keep them in `_originalProps`, where a later update would
27
+ * re-apply the construction-time value over the widget's own.
28
+ *
29
+ * `bgColor` and `fontSize` are deliberately absent: the engine already routes
30
+ * them (`backgroundColor`, `font-size`), so it can re-apply and re-subscribe
31
+ * them better than this widget can.
32
+ */
33
+ import { toREM } from "../utils/units.js";
34
+
35
+ const AVATAR_PROPS = [
36
+ "src",
37
+ "name",
38
+ "icon",
39
+ "size",
40
+ "shape",
41
+ "textColor",
42
+ "online",
43
+ "badgeColor",
44
+ ];
45
+
46
+ /** Diameter of the presence dot, as a share of the avatar. */
47
+ const DOT_SIZE_RATIO = 0.3;
48
+
49
+ /**
50
+ * Gap between the dot and the edge of the avatar. The dot stays inside because
51
+ * the avatar clips its content (`overflow: hidden`) to its own shape: a dot
52
+ * hung on the corner would be cut in half by a circular avatar, and a corner
53
+ * inset of `s·0.12` with a `s·0.3` dot is the largest one that still fits
54
+ * inside a circle of diameter `s`.
55
+ */
56
+ const DOT_INSET_RATIO = 0.12;
57
+
58
+ /** Framework units → a CSS length that scales with the reader's base font. */
59
+
60
+ /**
61
+ * Extracts up to two initials from a full name.
62
+ * Single-word names return just the first letter; multi-word names use
63
+ * the first letter of the first and last words.
64
+ *
65
+ * @param {string} fullName - The user's full name.
66
+ * @returns {string} 1–2 uppercase initials, or "?" if the name is empty.
67
+ */
68
+ const getInitials = (fullName) => {
69
+ if (!fullName) return "?";
70
+ const parts = fullName.trim().split(/\s+/);
71
+ if (parts.length === 1) return parts[0].charAt(0).toUpperCase();
72
+ return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
73
+ };
74
+
16
75
  /**
17
76
  * Creates an Avatar widget.
18
77
  *
@@ -26,15 +85,32 @@ import { Text } from "./Text.js";
26
85
  * @param {string} [props.src] - URL of a profile image.
27
86
  * @param {string} [props.name] - Full name from which initials are derived (e.g. "Jane Doe" → "JD").
28
87
  * @param {number} [props.size=40] - Diameter of the avatar in pixels.
29
- * @param {number} [props.fontSize=16] - Font size of initials text in pixels (overridden by the auto-computed size when `size` changes).
88
+ * @param {number} [props.fontSize=16] - Font size of the avatar box in pixels; initials are sized from `size`.
30
89
  * @param {'circle'|'rounded'|'square'} [props.shape='circle'] - Shape of the avatar container.
31
90
  * @param {string} [props.bgColor=colors.primary] - Background color shown behind icons or initials.
32
- * @param {string} [props.textColor=colors.text] - Color of icon or initials.
91
+ * @param {string} [props.textColor=colors.onPrimary] - Color of icon or initials.
33
92
  * @param {string} [props.icon] - Material icon name to display instead of an image or initials.
93
+ * @param {boolean} [props.online=false] - Shows a presence dot in the bottom-right corner.
94
+ * @param {string} [props.badgeColor=colors.success] - Color of the presence dot.
34
95
  * @param {Function} [props.onPress] - Click handler. When provided, a hover scale effect is also applied.
35
- * @returns {HTMLElement} The avatar container element, augmented with an `updateContent` method.
96
+ * @returns {HTMLElement} The avatar element, augmented with an `updateContent` method.
36
97
  */
37
- export const Avatar = (props) => {
98
+ export const Avatar = (inputProps = {}) => {
99
+ // The factory never sees these keys, so it can neither resolve a `useState`
100
+ // value for them (a live reactive is a *function*, and the content branches
101
+ // below only test them for presence) nor subscribe this widget to the key.
102
+ // Both happen here instead: read once, keep the keys, bind after
103
+ // `composeUpdate` has routed a later patch into `applyProps`.
104
+ const boundKeys = new Map();
105
+ const props = { ...inputProps };
106
+ for (const key of AVATAR_PROPS) {
107
+ const value = inputProps[key];
108
+ if (!isReactive(value)) continue;
109
+ props[key] = resolveReactive(value);
110
+ const stateKey = reactiveKeyOf(value);
111
+ if (stateKey) boundKeys.set(key, stateKey);
112
+ }
113
+
38
114
  let {
39
115
  src,
40
116
  name,
@@ -42,167 +118,194 @@ export const Avatar = (props) => {
42
118
  fontSize = 16,
43
119
  shape = "circle",
44
120
  bgColor = colors.primary,
45
- 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,
46
124
  icon,
125
+ online = false,
126
+ badgeColor = colors.success,
47
127
  onPress,
48
128
  ...rest
49
129
  } = props;
50
130
 
51
- // Map shape to a CSS border-radius value
52
- let borderRadius = "50%";
53
- if (shape === "rounded") borderRadius = `${size * 0.2}px`;
54
- if (shape === "square") borderRadius = "0";
55
-
56
131
  /**
57
- * Extracts up to two initials from a full name.
58
- * Single-word names return just the first letter; multi-word names use
59
- * the first letter of the first and last words.
60
- *
61
- * @param {string} fullName - The user's full name.
62
- * @returns {string} 1–2 uppercase initials, or "?" if the name is empty.
132
+ * Border radius the current `shape` gives the current `size`. A square has to
133
+ * say so — it is not merely "no radius applied yet", or the radius from the
134
+ * previous shape would stay on the element.
63
135
  */
64
- const getInitials = (fullName) => {
65
- if (!fullName) return "?";
66
- const parts = fullName.trim().split(/\s+/);
67
- if (parts.length === 1) return parts[0].charAt(0).toUpperCase();
68
- return (
69
- parts[0].charAt(0) + parts[parts.length - 1].charAt(0)
70
- ).toUpperCase();
136
+ const radiusFor = () => {
137
+ if (shape === "rounded") return toREM(size * 0.2);
138
+ if (shape === "square") return "0";
139
+ return "50%";
71
140
  };
72
141
 
73
- // Choose the appropriate child element based on prop priority
74
- let content = null;
75
- if (src) {
76
- content = Image({
77
- src: src,
78
- width: "100%",
79
- height: "100%",
80
- fit: "cover",
81
- });
82
- } else if (icon) {
83
- content = Icon({
84
- name: icon,
85
- size: size * 0.5,
86
- color: textColor,
87
- });
88
- } else if (name) {
89
- const initials = getInitials(name);
90
- content = Text({
91
- text: initials,
92
- color: textColor,
93
- size: size * 0.4,
94
- weight: "bold",
95
- });
96
- } else {
142
+ /** Chooses the child element for the props currently held. */
143
+ const buildContent = () => {
144
+ if (src) {
145
+ return Image({ src, width: "100%", height: "100%", fit: "cover" });
146
+ }
147
+ if (icon) return Icon({ name: icon, size: size * 0.5, color: textColor });
148
+ if (name) {
149
+ return Text({
150
+ text: getInitials(name),
151
+ color: textColor,
152
+ size: size * 0.4,
153
+ weight: "bold",
154
+ });
155
+ }
97
156
  // Generic fallback when no content is supplied
98
- content = Icon({
99
- name: "person",
100
- size: size * 0.5,
101
- color: textColor,
102
- });
103
- }
157
+ return Icon({ name: "person", size: size * 0.5, color: textColor });
158
+ };
104
159
 
105
160
  const avatar = WidgetFactory({
106
161
  widgetName: "Avatar",
162
+ position: "relative",
107
163
  display: "inline-flex",
108
164
  alignItems: "center",
109
165
  justifyContent: "center",
110
- width: size,
111
- height: size,
112
- borderRadius: borderRadius,
113
- backgroundColor: bgColor,
114
- color: textColor,
115
- overflow: "hidden",
166
+ bgColor: bgColor,
167
+ textColor: textColor,
116
168
  fontSize: fontSize,
169
+ overflow: "hidden",
117
170
  fontWeight: "500",
118
171
  userSelect: "none",
119
172
  cursor: onPress ? "pointer" : "default",
120
173
  transition: "transform 0.2s ease",
121
- child: content,
122
174
  onclick: onPress,
123
175
  ...rest,
124
176
  });
125
177
 
126
- // Add a subtle scale-up on hover when the avatar is interactive
127
- if (onPress) {
128
- avatar.addEventListener("mouseenter", () => {
129
- avatar.style.transform = "scale(1.05)";
130
- });
131
- avatar.addEventListener("mouseleave", () => {
132
- avatar.style.transform = "scale(1)";
133
- });
134
- }
178
+ /**
179
+ * Paints the box. Geometry is set here rather than passed to the factory as
180
+ * `width`/`height`/`borderRadius` because the factory keeps those in
181
+ * `_originalProps` and re-applies them on every later update — which would
182
+ * snap the avatar back to its construction-time size. An explicit `width`,
183
+ * `height` or `borderRadius` still wins, as it always has.
184
+ */
185
+ const paintGeometry = () => {
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());
192
+ };
193
+
194
+ paintGeometry();
195
+
196
+ // Both children are appended, never passed as `child`: the factory re-creates
197
+ // whatever it built for `child` on any update that re-passes it, which would
198
+ // drop the presence dot and revert the content to the construction-time node.
199
+ let content = buildContent();
200
+ const dot = WidgetFactory({
201
+ tag: "div",
202
+ position: "absolute",
203
+ borderRadius: "50%",
204
+ pointerEvents: "none",
205
+ });
206
+ avatar.appendChild(content);
207
+ avatar.appendChild(dot);
208
+
209
+ const paintDot = () => {
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));
214
+ dot.style.backgroundColor = badgeColor;
215
+ dot.style.display = online ? "block" : "none";
216
+ };
135
217
 
136
218
  /**
137
- * Updates the avatar's content and/or dimensions without recreating the element.
138
- * Only the properties present in `newProps` are applied; others are left as-is.
219
+ * Applies the props this widget owns. Only the keys present in `next` are
220
+ * touched; anything absent keeps its current value, so an update that only
221
+ * flips `online` never reloads the image.
139
222
  *
140
- * @param {Object} newProps - Partial props to apply.
141
- * @param {string} [newProps.src] - New image URL.
142
- * @param {string} [newProps.name] - New name (re-derives initials).
143
- * @param {string} [newProps.icon] - New icon name.
144
- * @param {string} [newProps.bgColor] - New background color.
145
- * @param {number|string} [newProps.size] - New avatar diameter.
223
+ * @param {Object} next - Partial props to apply.
146
224
  */
147
- avatar.updateContent = (newProps) => {
148
- // Sync the mutable local variables with any supplied overrides
149
- if (newProps.src !== undefined) src = newProps.src;
150
- if (newProps.name !== undefined) name = newProps.name;
151
- if (newProps.icon !== undefined) icon = newProps.icon;
152
- if (newProps.bgColor !== undefined)
153
- avatar.style.backgroundColor = newProps.bgColor;
154
- if (newProps.size !== undefined) {
155
- const newSize = newProps.size;
156
- avatar.style.width =
157
- typeof newSize === "number" ? `${newSize}px` : newSize;
158
- avatar.style.height =
159
- typeof newSize === "number" ? `${newSize}px` : newSize;
160
- avatar.style.fontSize =
161
- typeof newSize === "number" ? `${newSize * 0.4}px` : "16px";
225
+ const applyProps = (next = {}) => {
226
+ let rebuild = false;
227
+ let resize = false;
228
+
229
+ if ("src" in next) {
230
+ src = next.src;
231
+ rebuild = true;
232
+ }
233
+ if ("icon" in next) {
234
+ icon = next.icon;
235
+ rebuild = true;
236
+ }
237
+ if ("name" in next) {
238
+ name = next.name;
239
+ rebuild = true;
240
+ }
241
+ if ("textColor" in next) {
242
+ textColor = next.textColor;
243
+ avatar.style.color = textColor;
244
+ rebuild = true;
162
245
  }
246
+ if ("size" in next) {
247
+ size = next.size;
248
+ rebuild = true;
249
+ resize = true;
250
+ }
251
+ if ("shape" in next) {
252
+ shape = next.shape;
253
+ resize = true;
254
+ }
255
+ if ("online" in next) online = next.online;
256
+ if ("badgeColor" in next) badgeColor = next.badgeColor;
163
257
 
164
- // Rebuild the inner content using the updated props
165
- let newContent = null;
166
- if (newProps.src !== undefined || src !== undefined) {
167
- const finalSrc = newProps.src !== undefined ? newProps.src : src;
168
- newContent = Image({
169
- src: finalSrc,
170
- width: "100%",
171
- height: "100%",
172
- fit: "cover",
173
- });
174
- } else if (newProps.icon !== undefined || icon !== undefined) {
175
- const finalIcon = newProps.icon !== undefined ? newProps.icon : icon;
176
- const finalSize = newProps.size || size;
177
- newContent = Icon({
178
- name: finalIcon,
179
- size: finalSize * 0.5,
180
- color: textColor,
181
- });
182
- } else if (newProps.name !== undefined || name !== undefined) {
183
- const finalName = newProps.name !== undefined ? newProps.name : name;
184
- const initials = getInitials(finalName);
185
- const finalSize = newProps.size || size;
186
- newContent = Text({
187
- text: initials,
188
- color: textColor,
189
- size: finalSize * 0.4,
190
- weight: "bold",
191
- });
192
- } else {
193
- const finalSize = newProps.size || size;
194
- newContent = Icon({
195
- name: "person",
196
- size: finalSize * 0.5,
197
- color: textColor,
198
- });
258
+ if (resize) paintGeometry();
259
+ if (rebuild) {
260
+ const nextContent = buildContent();
261
+ avatar.replaceChild(nextContent, content);
262
+ content = nextContent;
199
263
  }
264
+ paintDot();
265
+ };
200
266
 
201
- // Replace the existing child with the new content
202
- while (avatar.firstChild) avatar.removeChild(avatar.firstChild);
203
- avatar.appendChild(newContent);
204
- avatar._child = newContent;
267
+ paintDot();
268
+
269
+ // Add a subtle scale-up on hover when the avatar is interactive
270
+ const scaleUp = () => {
271
+ avatar.style.transform = "scale(1.05)";
205
272
  };
273
+ const scaleDown = () => {
274
+ avatar.style.transform = "scale(1)";
275
+ };
276
+ if (onPress) {
277
+ avatar.addEventListener("mouseenter", scaleUp);
278
+ avatar.addEventListener("mouseleave", scaleDown);
279
+ }
280
+
281
+ let disposed = false;
282
+ const dispose = () => {
283
+ if (disposed) return;
284
+ disposed = true;
285
+ avatar.removeEventListener("mouseenter", scaleUp);
286
+ avatar.removeEventListener("mouseleave", scaleDown);
287
+ };
288
+ avatar.onUnmount(dispose);
289
+
290
+ composeUpdate(avatar, AVATAR_PROPS, applyProps);
291
+
292
+ /**
293
+ * Updates the avatar without recreating the element. This is `update()`: the
294
+ * keys the avatar owns are applied here, everything else — `bgColor`,
295
+ * `fontSize`, padding — goes through the factory, so the two public paths can
296
+ * never disagree about what a prop does.
297
+ *
298
+ * @param {Object} newProps - Partial props to apply: `src`, `name`, `icon`,
299
+ * `size`, `shape`, `textColor`, `online`, `badgeColor`, plus any style prop.
300
+ */
301
+ avatar.updateContent = (newProps = {}) => avatar.update(newProps);
302
+
303
+ // Bind after composing: `registerWidget` pushes the current store value
304
+ // through `update`, which must be the composed one that routes owned keys to
305
+ // `applyProps`. It also releases the subscription itself on unmount.
306
+ for (const [propName, stateKey] of boundKeys) {
307
+ registerWidget(stateKey, avatar, propName);
308
+ }
206
309
 
207
310
  return avatar;
208
311
  };
@@ -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`,