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.
- package/README.md +28 -1
- package/bin/cli.js +90 -11
- package/bin/commands/auditFix.js +682 -0
- package/bin/commands/checkApi.js +20 -6
- package/bin/commands/checkUi.js +1369 -0
- package/bin/commands/create.js +86 -12
- package/bin/commands/createBundle.js +75 -1
- package/bin/commands/images.js +349 -0
- package/bin/commands/pruneAssets.js +170 -0
- package/bin/commands/release.js +34 -1
- package/bin/commands/runCloudflare.js +47 -9
- package/bin/commands/runServer.js +91 -25
- package/bin/commands/toPng.js +346 -0
- package/bin/commands/unpack.js +1173 -0
- package/bin/ota/loader.js +90 -0
- package/bin/utils/appSources.js +101 -0
- package/bin/utils/bundleConfig.js +166 -0
- package/bin/utils/dtsProps.js +147 -0
- package/bin/utils/imageTools.js +271 -0
- package/bin/utils/images/bmp.js +99 -0
- package/bin/utils/images/gif.js +244 -0
- package/bin/utils/images/index.js +163 -0
- package/bin/utils/images/jpeg.js +643 -0
- package/bin/utils/images/png.js +486 -0
- package/bin/utils/images/unsupported.js +29 -0
- package/bin/utils/neutralise.js +109 -6
- package/bin/utils/pwaShell.js +137 -0
- package/bin/utils/templates.js +1653 -471
- package/docs/README.md +2 -0
- package/docs/cli/README.md +144 -1
- package/docs/guides/README.md +2 -0
- package/docs/guides/clean-ui.md +211 -0
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/online-updates.md +105 -9
- package/docs/tools/README.md +1 -1
- package/docs/tools/animation.md +1 -1
- package/docs/tools/markdown.md +3 -3
- package/docs/tools/theme.md +41 -1
- package/docs/widget/Accordion.md +4 -0
- package/docs/widget/AdMob.md +5 -1
- package/docs/widget/AdSense.md +4 -0
- package/docs/widget/AnimatedBox.md +7 -3
- package/docs/widget/AnimatedText.md +4 -0
- package/docs/widget/Audio.md +5 -1
- package/docs/widget/Avatar.md +15 -3
- package/docs/widget/Badge.md +4 -0
- package/docs/widget/Button.md +4 -0
- package/docs/widget/COMMON_PROPS.md +52 -0
- package/docs/widget/Card.md +4 -0
- package/docs/widget/Carousel.md +4 -0
- package/docs/widget/Chart.md +4 -0
- package/docs/widget/Checkbox.md +4 -0
- package/docs/widget/Chip.md +4 -0
- package/docs/widget/CircularBar.md +4 -0
- package/docs/widget/CircularChart.md +4 -0
- package/docs/widget/CodeViewer.md +4 -0
- package/docs/widget/Column.md +4 -0
- package/docs/widget/Container.md +4 -0
- package/docs/widget/DataTable.md +4 -0
- package/docs/widget/Divider.md +4 -0
- package/docs/widget/DraggBox.md +6 -2
- package/docs/widget/Dropdown.md +6 -0
- package/docs/widget/DroppBox.md +5 -1
- package/docs/widget/FloatingActionButton.md +4 -0
- package/docs/widget/GridView.md +4 -0
- package/docs/widget/Icon.md +4 -0
- package/docs/widget/Image.md +14 -3
- package/docs/widget/Input.md +4 -0
- package/docs/widget/Inspector.md +2 -2
- package/docs/widget/InstallButton.md +4 -0
- package/docs/widget/ListTile.md +4 -0
- package/docs/widget/ListView.md +4 -0
- package/docs/widget/Maps.md +191 -0
- package/docs/widget/Markdown.md +4 -0
- package/docs/widget/MatrixRain.md +4 -0
- package/docs/widget/Pagination.md +4 -0
- package/docs/widget/ParallaxBox.md +4 -0
- package/docs/widget/ProgressBar.md +4 -0
- package/docs/widget/QRCode.md +4 -0
- package/docs/widget/README.md +12 -10
- package/docs/widget/Radio.md +5 -1
- package/docs/widget/Rating.md +4 -0
- package/docs/widget/Row.md +4 -0
- package/docs/widget/Skeleton.md +4 -0
- package/docs/widget/Slider.md +4 -0
- package/docs/widget/Stack.md +4 -0
- package/docs/widget/Stepper.md +4 -0
- package/docs/widget/Switch.md +4 -0
- package/docs/widget/Tabs.md +5 -5
- package/docs/widget/Text.md +4 -0
- package/docs/widget/Tooltip.md +14 -13
- package/docs/widget/TreeView.md +4 -0
- package/docs/widget/Video.md +6 -2
- package/package.json +2 -2
- package/src/animations/AnimatedBox.js +4 -1
- package/src/animations/MatrixRain.js +3 -3
- package/src/core/hmr-client.js +129 -34
- package/src/index.d.ts +195 -7
- package/src/index.js +2 -0
- package/src/navigations/AppBar.js +22 -16
- package/src/navigations/BottomNavigation.js +8 -13
- package/src/navigations/CollapsibleSideBar.js +30 -8
- package/src/navigations/Drawer.js +25 -15
- package/src/navigations/DrawerItem.js +3 -1
- package/src/navigations/Scaffold.js +26 -12
- package/src/navigations/Tabs.js +104 -59
- package/src/tools/color.js +54 -0
- package/src/tools/index.js +9 -1
- package/src/tools/scale.js +41 -0
- package/src/tools/shadow.js +20 -0
- package/src/utils/index.js +1 -0
- package/src/utils/readableInk.js +19 -0
- package/src/utils/syntaxHighlight.js +78 -10
- package/src/utils/themes.js +217 -54
- package/src/utils/units.js +33 -11
- package/src/widget-factory/effects.js +56 -5
- package/src/widget-factory/processProps.js +10 -1
- package/src/widget-factory/remTool.js +15 -29
- package/src/widget-factory/stackPosition.js +18 -3
- package/src/widget-factory/translateProps.js +60 -1
- package/src/widgets/Accordion.js +20 -23
- package/src/widgets/AdMob.js +7 -2
- package/src/widgets/Avatar.js +236 -133
- package/src/widgets/Badge.js +3 -2
- package/src/widgets/BottomSheet.js +14 -13
- package/src/widgets/Button.js +69 -30
- package/src/widgets/Card.js +6 -2
- package/src/widgets/Carousel.js +12 -10
- package/src/widgets/Chart.js +28 -5
- package/src/widgets/Checkbox.js +76 -18
- package/src/widgets/Chip.js +31 -10
- package/src/widgets/CircularBar.js +7 -7
- package/src/widgets/CircularChart.js +26 -8
- package/src/widgets/CodeViewer.js +93 -20
- package/src/widgets/Container.js +13 -0
- package/src/widgets/DataTable.js +17 -11
- package/src/widgets/DraggBox.js +2 -1
- package/src/widgets/Dropdown.js +59 -14
- package/src/widgets/DroppBox.js +4 -2
- package/src/widgets/FloatingActionButton.js +13 -7
- package/src/widgets/Icon.js +3 -1
- package/src/widgets/Image.js +102 -3
- package/src/widgets/Input.js +30 -23
- package/src/widgets/Inspector.js +54 -1
- package/src/widgets/InstallButton.js +6 -4
- package/src/widgets/ListTile.js +4 -3
- package/src/widgets/ListView.js +30 -12
- package/src/widgets/Maps.js +1495 -0
- package/src/widgets/Markdown.js +37 -26
- package/src/widgets/Modal.js +14 -11
- package/src/widgets/Pagination.js +16 -11
- package/src/widgets/ProgressBar.js +6 -4
- package/src/widgets/Radio.js +73 -15
- package/src/widgets/Rating.js +24 -6
- package/src/widgets/Skeleton.js +31 -31
- package/src/widgets/Slider.js +29 -18
- package/src/widgets/SnackBar.js +35 -39
- package/src/widgets/Stepper.js +13 -9
- package/src/widgets/Switch.js +59 -18
- package/src/widgets/Tooltip.js +24 -12
- package/src/widgets/TreeView.js +27 -15
- package/src/widgets/index.js +1 -0
package/src/widgets/Avatar.js
CHANGED
|
@@ -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,
|
|
7
|
-
*
|
|
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
|
|
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.
|
|
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
|
|
96
|
+
* @returns {HTMLElement} The avatar element, augmented with an `updateContent` method.
|
|
36
97
|
*/
|
|
37
|
-
export const Avatar = (
|
|
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
|
-
|
|
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
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
*
|
|
138
|
-
*
|
|
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}
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
};
|
package/src/widgets/Badge.js
CHANGED
|
@@ -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.
|
|
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}
|
|
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 =
|
|
35
|
+
borderRadius = 16,
|
|
35
36
|
shadow = "0 -4px 12px rgba(0,0,0,0.1)",
|
|
36
37
|
|
|
37
|
-
// Spacing
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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}
|
|
170
|
+
borderRadius: `${toREM(borderRadius)} ${toREM(borderRadius)} 0 0`,
|
|
170
171
|
boxShadow: shadow,
|
|
171
172
|
transform: "translateY(100%)",
|
|
172
173
|
transition: `transform ${animationDuration}ms ease`,
|