flet-box 1.0.7 → 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/bin/cli.js +13 -0
- package/bin/commands/checkApi.js +13 -1
- package/bin/commands/checkUi.js +1369 -0
- package/bin/commands/create.js +49 -5
- package/bin/commands/createBundle.js +21 -0
- package/bin/commands/runServer.js +11 -2
- package/bin/utils/pwaShell.js +137 -0
- package/bin/utils/templates.js +1373 -434
- package/docs/README.md +2 -0
- package/docs/cli/README.md +62 -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 +48 -0
- 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 +3 -0
- package/docs/widget/AdMob.md +4 -1
- package/docs/widget/AdSense.md +3 -0
- package/docs/widget/AnimatedBox.md +6 -3
- package/docs/widget/AnimatedText.md +3 -0
- package/docs/widget/Audio.md +3 -0
- package/docs/widget/Avatar.md +3 -0
- package/docs/widget/Badge.md +3 -0
- package/docs/widget/Button.md +3 -0
- package/docs/widget/COMMON_PROPS.md +52 -0
- package/docs/widget/Card.md +3 -0
- package/docs/widget/Carousel.md +3 -0
- package/docs/widget/Chart.md +3 -0
- package/docs/widget/Checkbox.md +3 -0
- package/docs/widget/Chip.md +3 -0
- package/docs/widget/CircularBar.md +3 -0
- package/docs/widget/CircularChart.md +3 -0
- package/docs/widget/CodeViewer.md +3 -0
- package/docs/widget/Column.md +3 -0
- package/docs/widget/Container.md +3 -0
- package/docs/widget/DataTable.md +3 -0
- package/docs/widget/Divider.md +3 -0
- package/docs/widget/DraggBox.md +3 -0
- package/docs/widget/Dropdown.md +3 -0
- package/docs/widget/DroppBox.md +3 -0
- package/docs/widget/FloatingActionButton.md +3 -0
- package/docs/widget/GridView.md +3 -0
- package/docs/widget/Icon.md +3 -0
- package/docs/widget/Image.md +3 -0
- package/docs/widget/Input.md +3 -0
- package/docs/widget/Inspector.md +2 -2
- package/docs/widget/InstallButton.md +3 -0
- package/docs/widget/ListTile.md +3 -0
- package/docs/widget/ListView.md +3 -0
- package/docs/widget/Maps.md +3 -0
- package/docs/widget/Markdown.md +3 -0
- package/docs/widget/MatrixRain.md +3 -0
- package/docs/widget/Pagination.md +3 -0
- package/docs/widget/ParallaxBox.md +3 -0
- package/docs/widget/ProgressBar.md +3 -0
- package/docs/widget/QRCode.md +3 -0
- package/docs/widget/Radio.md +4 -1
- package/docs/widget/Rating.md +3 -0
- package/docs/widget/Row.md +3 -0
- package/docs/widget/Skeleton.md +3 -0
- package/docs/widget/Slider.md +3 -0
- package/docs/widget/Stack.md +3 -0
- package/docs/widget/Stepper.md +3 -0
- package/docs/widget/Switch.md +3 -0
- package/docs/widget/Tabs.md +5 -5
- package/docs/widget/Text.md +3 -0
- package/docs/widget/Tooltip.md +3 -0
- package/docs/widget/TreeView.md +3 -0
- package/docs/widget/Video.md +3 -0
- package/package.json +2 -2
- package/src/animations/AnimatedBox.js +4 -1
- package/src/animations/MatrixRain.js +3 -3
- package/src/index.d.ts +64 -8
- package/src/index.js +1 -0
- package/src/navigations/AppBar.js +7 -12
- package/src/navigations/BottomNavigation.js +8 -13
- package/src/navigations/CollapsibleSideBar.js +30 -8
- package/src/navigations/Drawer.js +16 -14
- 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 +28 -0
- package/src/widgets/Accordion.js +20 -23
- package/src/widgets/AdMob.js +7 -2
- package/src/widgets/Avatar.js +18 -12
- 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/DataTable.js +17 -11
- package/src/widgets/DraggBox.js +2 -1
- package/src/widgets/Dropdown.js +26 -13
- package/src/widgets/DroppBox.js +4 -2
- package/src/widgets/FloatingActionButton.js +13 -7
- package/src/widgets/Icon.js +3 -1
- 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/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 +12 -10
- package/src/widgets/TreeView.js +27 -15
|
@@ -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
|
// ============================================================
|
package/src/widgets/Accordion.js
CHANGED
|
@@ -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
|
|
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='
|
|
51
|
-
* @param {string} [props.contentPadding='
|
|
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 =
|
|
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
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
titleElement.style.fontSize =
|
|
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 =
|
|
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 =
|
|
207
|
+
container.style.borderRadius = toREM(currentBorderRadius);
|
|
206
208
|
container.style.overflow = "hidden";
|
|
207
209
|
|
|
208
|
-
//
|
|
210
|
+
// The framework's depth scale, not a local copy of it (src/tools/shadow.js).
|
|
209
211
|
if (currentElevation > 0) {
|
|
210
|
-
|
|
211
|
-
|
|
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;
|
package/src/widgets/AdMob.js
CHANGED
|
@@ -205,7 +205,7 @@ export const AdMob = (props) => {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
const showBanner = () => {
|
|
208
|
-
if (disposed ||
|
|
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
|
-
|
|
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
|
|
package/src/widgets/Avatar.js
CHANGED
|
@@ -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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
184
|
-
avatar.style.height =
|
|
185
|
-
|
|
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 =
|
|
205
|
-
dot.style.height =
|
|
206
|
-
dot.style.right =
|
|
207
|
-
dot.style.bottom =
|
|
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
|
};
|
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`,
|
package/src/widgets/Button.js
CHANGED
|
@@ -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: [
|
|
15
|
-
medium: { padding: [
|
|
16
|
-
large: { padding: [
|
|
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,
|
|
48
|
+
function computeTextColor(variant, disabled, color, background, inkFromFill) {
|
|
46
49
|
if (disabled) return colors.textDisabled;
|
|
47
|
-
if (variant === "filled")
|
|
48
|
-
|
|
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
|
|
55
|
-
return `2px solid ${
|
|
66
|
+
const tint = color || bgColor || colors.primary;
|
|
67
|
+
return `2px solid ${tint}`;
|
|
56
68
|
}
|
|
57
69
|
if (variant === "text") return "none";
|
|
58
|
-
|
|
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 =
|
|
169
|
-
elevation =
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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;
|
package/src/widgets/Card.js
CHANGED
|
@@ -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
|
|
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
|
});
|
package/src/widgets/Carousel.js
CHANGED
|
@@ -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
|
-
?
|
|
64
|
+
? toREM(dotActiveSize)
|
|
63
65
|
: dotActiveSize;
|
|
64
66
|
} else {
|
|
65
67
|
dot.style.backgroundColor = dotColor;
|
|
66
68
|
dot.style.width =
|
|
67
|
-
|
|
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:
|
|
158
|
-
height:
|
|
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:
|
|
177
|
-
height:
|
|
178
|
+
width: toREM(width),
|
|
179
|
+
height: toREM(height),
|
|
178
180
|
overflow: "hidden",
|
|
179
181
|
borderRadius:
|
|
180
|
-
|
|
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
|
-
?
|
|
260
|
+
? toREM(dotActiveSize)
|
|
259
261
|
: dotActiveSize
|
|
260
262
|
: typeof dotSize === "number"
|
|
261
|
-
?
|
|
263
|
+
? toREM(dotSize)
|
|
262
264
|
: dotSize,
|
|
263
|
-
height:
|
|
265
|
+
height: toREM(dotSize),
|
|
264
266
|
borderRadius: "4px",
|
|
265
267
|
backgroundColor: index === currentIndex ? dotActiveColor : dotColor,
|
|
266
268
|
cursor: "pointer",
|
package/src/widgets/Chart.js
CHANGED
|
@@ -42,6 +42,8 @@ import { colors } from "../utils/themes.js";
|
|
|
42
42
|
* @param {number} [props.borderRadius=8] - Corner radius of the chart container in pixels.
|
|
43
43
|
* @returns {HTMLElement} The chart container element, augmented with `updateData` and `redraw` methods.
|
|
44
44
|
*/
|
|
45
|
+
import { toREM } from "../utils/units.js";
|
|
46
|
+
|
|
45
47
|
export const Chart = (props) => {
|
|
46
48
|
const {
|
|
47
49
|
type = "candle",
|
|
@@ -130,6 +132,19 @@ export const Chart = (props) => {
|
|
|
130
132
|
maxVal = Math.max(...data);
|
|
131
133
|
minVal = Math.min(...data);
|
|
132
134
|
}
|
|
135
|
+
// Bars are measured from zero: if the scale started at the smallest
|
|
136
|
+
// value instead, `getYPosition(0)` fell below the axis and every bar
|
|
137
|
+
// ran past the baseline to the canvas edge, so the bottom label (6)
|
|
138
|
+
// did not describe where the bars started.
|
|
139
|
+
if (type === "bar") {
|
|
140
|
+
maxVal = Math.max(0, maxVal);
|
|
141
|
+
minVal = Math.min(0, minVal);
|
|
142
|
+
const barRange = maxVal - minVal || 1;
|
|
143
|
+
return {
|
|
144
|
+
maxValue: maxVal + (maxVal > 0 ? barRange * 0.1 : 0),
|
|
145
|
+
minValue: minVal - (minVal < 0 ? barRange * 0.1 : 0),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
133
148
|
const range = maxVal - minVal;
|
|
134
149
|
const paddingRange = range * 0.1;
|
|
135
150
|
return {
|
|
@@ -170,8 +185,11 @@ export const Chart = (props) => {
|
|
|
170
185
|
*/
|
|
171
186
|
const drawBar = (ctx, canvasWidth, chartHeight, maxValue, minValue) => {
|
|
172
187
|
const availableWidth = canvasWidth - pad.left - pad.right;
|
|
173
|
-
const
|
|
174
|
-
|
|
188
|
+
const bandWidth = availableWidth / data.length;
|
|
189
|
+
// 70/30 left a gap 43% as wide as the bar; on a wide chart the columns
|
|
190
|
+
// read as unrelated blocks instead of one series.
|
|
191
|
+
const barWidth = bandWidth * 0.8;
|
|
192
|
+
const barSpacing = bandWidth - barWidth;
|
|
175
193
|
|
|
176
194
|
data.forEach((value, index) => {
|
|
177
195
|
const x = pad.left + index * (barWidth + barSpacing) + barSpacing / 2;
|
|
@@ -186,6 +204,11 @@ export const Chart = (props) => {
|
|
|
186
204
|
ctx.fillStyle = textColor;
|
|
187
205
|
ctx.font = "8px sans-serif";
|
|
188
206
|
ctx.fillText(value, x + barWidth / 2 - 5, y - 2);
|
|
207
|
+
}
|
|
208
|
+
// Bars are data points too: `showLabels` used to be ignored here
|
|
209
|
+
// unless `showValues` was on, so the documented default drew no
|
|
210
|
+
// x-axis labels at all.
|
|
211
|
+
if (showLabels && labels[index]) {
|
|
189
212
|
ctx.save();
|
|
190
213
|
ctx.translate(x + barWidth / 2, chartHeight - pad.bottom + 8);
|
|
191
214
|
ctx.rotate(-0.3);
|
|
@@ -525,11 +548,11 @@ export const Chart = (props) => {
|
|
|
525
548
|
|
|
526
549
|
const container = WidgetFactory({
|
|
527
550
|
widgetName: "Chart",
|
|
528
|
-
width:
|
|
529
|
-
height:
|
|
551
|
+
width: toREM(width),
|
|
552
|
+
height: toREM(height),
|
|
530
553
|
backgroundColor: bgColor,
|
|
531
554
|
borderRadius:
|
|
532
|
-
|
|
555
|
+
toREM(borderRadius),
|
|
533
556
|
overflow: "hidden",
|
|
534
557
|
display: "flex",
|
|
535
558
|
flexDirection: "column",
|