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
|
@@ -40,6 +40,8 @@ import { initRouter, getCurrentRoute, subscribe } from "./Router.js";
|
|
|
40
40
|
* - `updateLeftNavBar(cfg)` / `updateRightNavBar(cfg)` — replace sidebar content.
|
|
41
41
|
* - `setLeftNavBarWidth(w)` / `setRightNavBarWidth(w)` — resize a sidebar at runtime.
|
|
42
42
|
*/
|
|
43
|
+
import { toREM } from "../utils/units.js";
|
|
44
|
+
|
|
43
45
|
export const Scaffold = (props) => {
|
|
44
46
|
const {
|
|
45
47
|
appBar: originalAppBar,
|
|
@@ -106,11 +108,15 @@ export const Scaffold = (props) => {
|
|
|
106
108
|
* Normalises a prop value to an HTMLElement.
|
|
107
109
|
* Accepts an HTMLElement directly, a factory function, or null/undefined.
|
|
108
110
|
*
|
|
109
|
-
*
|
|
111
|
+
* A boolean is the documented "show / hide this slot" value, never a widget,
|
|
112
|
+
* so it resolves to an empty slot instead of reaching appendChild() and
|
|
113
|
+
* throwing on a non-Node.
|
|
114
|
+
*
|
|
115
|
+
* @param {HTMLElement|Function|boolean|null} item
|
|
110
116
|
* @returns {HTMLElement|null}
|
|
111
117
|
*/
|
|
112
118
|
const toElement = (item) => {
|
|
113
|
-
if (!item) return null;
|
|
119
|
+
if (!item || typeof item === "boolean") return null;
|
|
114
120
|
if (item instanceof HTMLElement) return item;
|
|
115
121
|
if (typeof item === "function") return item();
|
|
116
122
|
return item;
|
|
@@ -283,7 +289,7 @@ export const Scaffold = (props) => {
|
|
|
283
289
|
if (widget) {
|
|
284
290
|
leftNavBarContainer.appendChild(widget);
|
|
285
291
|
if (mainContentContainer)
|
|
286
|
-
mainContentContainer.style.width = `calc(100% - ${leftNavBarWidth}
|
|
292
|
+
mainContentContainer.style.width = `calc(100% - ${toREM(leftNavBarWidth)})`;
|
|
287
293
|
}
|
|
288
294
|
};
|
|
289
295
|
|
|
@@ -359,9 +365,17 @@ export const Scaffold = (props) => {
|
|
|
359
365
|
if (closeDrawerOnNavigate && drawerInstance && drawerInstance.close) {
|
|
360
366
|
drawerInstance.close();
|
|
361
367
|
}
|
|
362
|
-
} else
|
|
363
|
-
|
|
364
|
-
|
|
368
|
+
} else {
|
|
369
|
+
if (body) {
|
|
370
|
+
const bodyWidget = makeFullSize(toElement(body));
|
|
371
|
+
if (bodyWidget) mainContentContainer.appendChild(bodyWidget);
|
|
372
|
+
}
|
|
373
|
+
// Static mode mounts the shell zones too. They used to be router-only, so
|
|
374
|
+
// the documented `Scaffold({ appBar, body })` example rendered the body
|
|
375
|
+
// with no top bar, bottom bar or FAB at all.
|
|
376
|
+
updateAppBar(originalAppBar !== false);
|
|
377
|
+
updateBottomBar(originalBottomBar !== false);
|
|
378
|
+
updateFab(originalFab !== false);
|
|
365
379
|
}
|
|
366
380
|
};
|
|
367
381
|
|
|
@@ -389,7 +403,7 @@ export const Scaffold = (props) => {
|
|
|
389
403
|
|
|
390
404
|
if (finalLeftNavBar) {
|
|
391
405
|
leftNavBarContainer = document.createElement("div");
|
|
392
|
-
leftNavBarContainer.style.width =
|
|
406
|
+
leftNavBarContainer.style.width = toREM(leftNavBarWidth);
|
|
393
407
|
leftNavBarContainer.style.flexShrink = "0";
|
|
394
408
|
leftNavBarContainer.style.overflow = "auto";
|
|
395
409
|
leftNavBarContainer.style.height = "100%";
|
|
@@ -409,7 +423,7 @@ export const Scaffold = (props) => {
|
|
|
409
423
|
|
|
410
424
|
if (finalRightNavBar) {
|
|
411
425
|
rightNavBarContainer = document.createElement("div");
|
|
412
|
-
rightNavBarContainer.style.width =
|
|
426
|
+
rightNavBarContainer.style.width = toREM(rightNavBarWidth);
|
|
413
427
|
rightNavBarContainer.style.flexShrink = "0";
|
|
414
428
|
rightNavBarContainer.style.overflow = "auto";
|
|
415
429
|
rightNavBarContainer.style.height = "100%";
|
|
@@ -425,8 +439,8 @@ export const Scaffold = (props) => {
|
|
|
425
439
|
// FAB slot — absolutely positioned so it floats above content.
|
|
426
440
|
fabContainer = document.createElement("div");
|
|
427
441
|
fabContainer.style.position = "absolute";
|
|
428
|
-
fabContainer.style.bottom =
|
|
429
|
-
fabContainer.style.right =
|
|
442
|
+
fabContainer.style.bottom = toREM(16);
|
|
443
|
+
fabContainer.style.right = toREM(16);
|
|
430
444
|
fabContainer.style.zIndex = "10";
|
|
431
445
|
container.appendChild(fabContainer);
|
|
432
446
|
|
|
@@ -528,7 +542,7 @@ export const Scaffold = (props) => {
|
|
|
528
542
|
*/
|
|
529
543
|
container.setLeftNavBarWidth = (w) => {
|
|
530
544
|
if (leftNavBarContainer)
|
|
531
|
-
leftNavBarContainer.style.width =
|
|
545
|
+
leftNavBarContainer.style.width = toREM(w);
|
|
532
546
|
};
|
|
533
547
|
|
|
534
548
|
/**
|
|
@@ -538,7 +552,7 @@ export const Scaffold = (props) => {
|
|
|
538
552
|
*/
|
|
539
553
|
container.setRightNavBarWidth = (w) => {
|
|
540
554
|
if (rightNavBarContainer)
|
|
541
|
-
rightNavBarContainer.style.width =
|
|
555
|
+
rightNavBarContainer.style.width = toREM(w);
|
|
542
556
|
};
|
|
543
557
|
|
|
544
558
|
return container;
|
package/src/navigations/Tabs.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import { WidgetFactory } from "../widget-factory/index.js";
|
|
3
3
|
import { colors } from "../utils/themes.js";
|
|
4
4
|
import { Container } from "../widgets/Container.js";
|
|
5
|
+
import { Column } from "../widgets/Column.js";
|
|
5
6
|
import { Row } from "../widgets/Row.js";
|
|
6
7
|
import { Text } from "../widgets/Text.js";
|
|
7
8
|
import { Icon } from "../widgets/Icon.js";
|
|
9
|
+
import { toREM } from "../utils/units.js";
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* @typedef {Object} TabDefinition
|
|
@@ -66,14 +68,16 @@ export const Tabs = (props) => {
|
|
|
66
68
|
variant = "underline",
|
|
67
69
|
size = "medium",
|
|
68
70
|
color = colors.primary,
|
|
69
|
-
textColor = colors.
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
activeTextColor = colors.background,
|
|
71
|
+
textColor = colors.textSecondary,
|
|
72
|
+
// `background` made the active label the page color, so it vanished on a
|
|
73
|
+
// transparent tab bar (2.4:1 in light, 1.3:1 in dark). Resolved per variant
|
|
74
|
+
// below: the slider sits on a secondary thumb and needs the ink color.
|
|
75
|
+
activeTextColor,
|
|
75
76
|
bgColor = colors.gray100,
|
|
76
|
-
|
|
77
|
+
// The thumb carries the accent, and its label is the ink color in both
|
|
78
|
+
// themes. `secondary` missed AA on the light thumb (4.23:1); primary is
|
|
79
|
+
// 6.65:1 with ink, 6.68:1 in dark.
|
|
80
|
+
buttonColor = color,
|
|
77
81
|
alignment = "left",
|
|
78
82
|
fullWidth = true,
|
|
79
83
|
showDivider = true,
|
|
@@ -86,7 +90,7 @@ export const Tabs = (props) => {
|
|
|
86
90
|
} = props;
|
|
87
91
|
|
|
88
92
|
let currentIndex = activeIndex;
|
|
89
|
-
let
|
|
93
|
+
let activeIndicator = null;
|
|
90
94
|
let tabsWrapperRef = null;
|
|
91
95
|
let tabButtonsRef = [];
|
|
92
96
|
|
|
@@ -99,6 +103,16 @@ export const Tabs = (props) => {
|
|
|
99
103
|
};
|
|
100
104
|
const sz = sizes[size] || sizes.medium;
|
|
101
105
|
|
|
106
|
+
const resolvedActiveTextColor =
|
|
107
|
+
activeTextColor ?? (variant === "slider" ? colors.onPrimary : colors.text);
|
|
108
|
+
|
|
109
|
+
const tabBackground = (isActive) => {
|
|
110
|
+
if (!isActive) return "transparent";
|
|
111
|
+
if (variant === "pills") return `${color}1A`;
|
|
112
|
+
if (variant === "filled") return colors.card;
|
|
113
|
+
return "transparent";
|
|
114
|
+
};
|
|
115
|
+
|
|
102
116
|
/** Extract display label from a tab definition or plain string. */
|
|
103
117
|
const getLabel = (t) =>
|
|
104
118
|
typeof t === "object" ? t.label || t.title : String(t);
|
|
@@ -157,43 +171,64 @@ export const Tabs = (props) => {
|
|
|
157
171
|
const badge = getBadge(idx);
|
|
158
172
|
const isActive = currentIndex === idx;
|
|
159
173
|
|
|
160
|
-
// Build the
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
Text({
|
|
174
|
+
// Build the content inside each tab button. Every documented value of
|
|
175
|
+
// `iconPosition` keeps the icon: "left" puts it before the label, "right"
|
|
176
|
+
// after it, and "top"/"bottom" stack it above/below the label row.
|
|
177
|
+
const iconWidget =
|
|
178
|
+
showIcon && icon
|
|
179
|
+
? Icon({
|
|
180
|
+
name: icon,
|
|
181
|
+
size: iconSize || sz.i,
|
|
182
|
+
color: isActive ? resolvedActiveTextColor : textColor,
|
|
183
|
+
})
|
|
184
|
+
: null;
|
|
185
|
+
const labelWidget = label
|
|
186
|
+
? Text({
|
|
174
187
|
text: label,
|
|
175
188
|
size: sz.f,
|
|
176
|
-
color: isActive ?
|
|
189
|
+
color: isActive ? resolvedActiveTextColor : textColor,
|
|
177
190
|
weight: isActive ? "500" : "400",
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
content.push(
|
|
183
|
-
Container({
|
|
191
|
+
})
|
|
192
|
+
: null;
|
|
193
|
+
const badgeWidget = badge
|
|
194
|
+
? Container({
|
|
184
195
|
backgroundColor: colors.danger,
|
|
185
196
|
borderRadius: 10,
|
|
186
197
|
padding: "2px 6px",
|
|
187
198
|
marginLeft: 4,
|
|
188
199
|
child: Text({ text: String(badge), size: 10, color: "#fff" }),
|
|
189
|
-
})
|
|
190
|
-
|
|
200
|
+
})
|
|
201
|
+
: null;
|
|
202
|
+
|
|
203
|
+
const line = [labelWidget, badgeWidget].filter(Boolean);
|
|
204
|
+
if (iconWidget && iconPosition !== "top" && iconPosition !== "bottom") {
|
|
205
|
+
if (iconPosition === "left") line.unshift(iconWidget);
|
|
206
|
+
else line.push(iconWidget);
|
|
191
207
|
}
|
|
208
|
+
const labelRow = Row({
|
|
209
|
+
alignItems: "center",
|
|
210
|
+
justifyContent: "center",
|
|
211
|
+
gap: 4,
|
|
212
|
+
children: line,
|
|
213
|
+
});
|
|
214
|
+
const stacked =
|
|
215
|
+
iconWidget && (iconPosition === "top" || iconPosition === "bottom");
|
|
216
|
+
const content = stacked
|
|
217
|
+
? Column({
|
|
218
|
+
alignItems: "center",
|
|
219
|
+
justifyContent: "center",
|
|
220
|
+
gap: 4,
|
|
221
|
+
children:
|
|
222
|
+
iconPosition === "top"
|
|
223
|
+
? [iconWidget, labelRow]
|
|
224
|
+
: [labelRow, iconWidget],
|
|
225
|
+
})
|
|
226
|
+
: labelRow;
|
|
192
227
|
|
|
193
228
|
const btn = Container({
|
|
194
229
|
borderRadius: variant === "slider" ? 20 : variant === "pills" ? 20 : 0,
|
|
195
|
-
backgroundColor:
|
|
196
|
-
color: isActive ?
|
|
230
|
+
backgroundColor: tabBackground(isActive),
|
|
231
|
+
color: isActive ? resolvedActiveTextColor : textColor,
|
|
197
232
|
display: "flex",
|
|
198
233
|
alignItems: "center",
|
|
199
234
|
justifyContent: "center",
|
|
@@ -220,12 +255,7 @@ export const Tabs = (props) => {
|
|
|
220
255
|
}
|
|
221
256
|
}
|
|
222
257
|
},
|
|
223
|
-
child:
|
|
224
|
-
alignItems: "center",
|
|
225
|
-
justifyContent: "center",
|
|
226
|
-
gap: 4,
|
|
227
|
-
children: content,
|
|
228
|
-
}),
|
|
258
|
+
child: content,
|
|
229
259
|
});
|
|
230
260
|
|
|
231
261
|
btn.onclick = () => {
|
|
@@ -242,20 +272,34 @@ export const Tabs = (props) => {
|
|
|
242
272
|
tabButtonsRef = tabButtons;
|
|
243
273
|
|
|
244
274
|
// The slider variant uses an absolutely positioned "thumb" div that
|
|
245
|
-
// animates to the active tab's position via CSS transform.
|
|
246
|
-
|
|
247
|
-
|
|
275
|
+
// animates to the active tab's position via CSS transform. The default
|
|
276
|
+
// `underline` variant needs the same moving element: without it the active
|
|
277
|
+
// tab had no cue besides its label color.
|
|
278
|
+
const usesIndicator = variant === "slider" || variant === "underline";
|
|
279
|
+
if (usesIndicator) {
|
|
280
|
+
activeIndicator = WidgetFactory({
|
|
248
281
|
tag: "div",
|
|
249
282
|
position: "absolute",
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
283
|
+
...(variant === "slider"
|
|
284
|
+
? {
|
|
285
|
+
top: "4px",
|
|
286
|
+
left: "4px",
|
|
287
|
+
height: "calc(100% - 8px)",
|
|
288
|
+
backgroundColor: buttonColor,
|
|
289
|
+
borderRadius: 20,
|
|
290
|
+
}
|
|
291
|
+
: {
|
|
292
|
+
bottom: "0",
|
|
293
|
+
left: "0",
|
|
294
|
+
height: "2px",
|
|
295
|
+
backgroundColor: color,
|
|
296
|
+
borderRadius: "2px 2px 0 0",
|
|
297
|
+
}),
|
|
255
298
|
transition: "transform 0.3s ease, width 0.3s ease",
|
|
299
|
+
pointerEvents: "none",
|
|
256
300
|
zIndex: 1,
|
|
257
301
|
});
|
|
258
|
-
tabsWrapper.appendChild(
|
|
302
|
+
tabsWrapper.appendChild(activeIndicator);
|
|
259
303
|
}
|
|
260
304
|
|
|
261
305
|
tabBar.appendChild(tabsWrapper);
|
|
@@ -263,7 +307,7 @@ export const Tabs = (props) => {
|
|
|
263
307
|
|
|
264
308
|
const contentContainer = Container({
|
|
265
309
|
display: "flex",
|
|
266
|
-
padding: `${sz.cp}
|
|
310
|
+
padding: `${toREM(sz.cp)} 0`,
|
|
267
311
|
width: "100%",
|
|
268
312
|
overflow: "auto",
|
|
269
313
|
});
|
|
@@ -284,13 +328,13 @@ export const Tabs = (props) => {
|
|
|
284
328
|
* Reads live DOM rectangles so it stays accurate after layout changes.
|
|
285
329
|
*/
|
|
286
330
|
const updateSliderPosition = () => {
|
|
287
|
-
if (!
|
|
331
|
+
if (!activeIndicator || !tabButtonsRef[currentIndex]) return;
|
|
288
332
|
const btn = tabButtonsRef[currentIndex];
|
|
289
333
|
const wrapperRect = tabsWrapperRef.getBoundingClientRect();
|
|
290
334
|
const btnRect = btn.getBoundingClientRect();
|
|
291
335
|
const left = btnRect.left - wrapperRect.left;
|
|
292
|
-
|
|
293
|
-
|
|
336
|
+
activeIndicator.style.width = `${btnRect.width}px`;
|
|
337
|
+
activeIndicator.style.transform = `translateX(${left}px)`;
|
|
294
338
|
};
|
|
295
339
|
|
|
296
340
|
/**
|
|
@@ -308,14 +352,15 @@ export const Tabs = (props) => {
|
|
|
308
352
|
|
|
309
353
|
const textSpan = btn.querySelector("span:not(.material-icons)");
|
|
310
354
|
if (textSpan) {
|
|
311
|
-
textSpan.style.color = isActive ?
|
|
355
|
+
textSpan.style.color = isActive ? resolvedActiveTextColor : textColor;
|
|
312
356
|
textSpan.style.fontWeight = isActive ? "500" : "400";
|
|
313
357
|
}
|
|
314
358
|
const iconEl = btn.querySelector(".material-icons");
|
|
315
359
|
if (iconEl) {
|
|
316
|
-
iconEl.style.color = isActive ?
|
|
360
|
+
iconEl.style.color = isActive ? resolvedActiveTextColor : textColor;
|
|
317
361
|
}
|
|
318
|
-
btn.style.color = isActive ?
|
|
362
|
+
btn.style.color = isActive ? resolvedActiveTextColor : textColor;
|
|
363
|
+
btn.style.backgroundColor = tabBackground(isActive);
|
|
319
364
|
});
|
|
320
365
|
|
|
321
366
|
updateSliderPosition();
|
|
@@ -341,17 +386,17 @@ export const Tabs = (props) => {
|
|
|
341
386
|
const handleResize = () => {
|
|
342
387
|
clearTimeout(resizeTimeout);
|
|
343
388
|
resizeTimeout = setTimeout(() => {
|
|
344
|
-
if (
|
|
389
|
+
if (activeIndicator && tabButtonsRef[currentIndex]) {
|
|
345
390
|
updateSliderPosition();
|
|
346
391
|
}
|
|
347
392
|
}, 100);
|
|
348
393
|
};
|
|
349
394
|
window.addEventListener("resize", handleResize);
|
|
350
395
|
|
|
351
|
-
// Also observe the tab bar element itself so the
|
|
396
|
+
// Also observe the tab bar element itself so the indicator repositions when
|
|
352
397
|
// the container resizes independently of the window (e.g. sidebar toggle).
|
|
353
|
-
// Only
|
|
354
|
-
if (
|
|
398
|
+
// Only slider and underline render an indicator, so skip it for the others.
|
|
399
|
+
if (usesIndicator && typeof ResizeObserver !== "undefined") {
|
|
355
400
|
resizeObserver = new ResizeObserver(() => updateSliderPosition());
|
|
356
401
|
if (tabsWrapperRef) resizeObserver.observe(tabsWrapperRef);
|
|
357
402
|
}
|
package/src/tools/color.js
CHANGED
|
@@ -291,6 +291,60 @@ class Color {
|
|
|
291
291
|
return this.isDark() ? "#ffffff" : "#000000";
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Relative luminance of the color, as WCAG 2.1 defines it.
|
|
296
|
+
*
|
|
297
|
+
* Each channel is sRGB-decoded (the linear branch below 0.04045 is the one
|
|
298
|
+
* that matters for dark colors, where `isDark()`'s BT.601 luma is a poor
|
|
299
|
+
* proxy) and weighted 0.2126 / 0.7152 / 0.0722. Alpha is ignored: this is the
|
|
300
|
+
* luminance of the color itself, so a translucent color must be composited
|
|
301
|
+
* over its background before being measured.
|
|
302
|
+
*
|
|
303
|
+
* @returns {number} 0 for black, 1 for white
|
|
304
|
+
*/
|
|
305
|
+
luminance() {
|
|
306
|
+
const channel = (value) => {
|
|
307
|
+
const c = value / 255;
|
|
308
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
309
|
+
};
|
|
310
|
+
return (
|
|
311
|
+
0.2126 * channel(this.r) + 0.7152 * channel(this.g) + 0.0722 * channel(this.b)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Contrast ratio between this color and another, in WCAG terms.
|
|
317
|
+
*
|
|
318
|
+
* Returns the ratio of the lighter luminance to the darker one, from 1
|
|
319
|
+
* (identical) to 21 (black on white). This is what the readability thresholds
|
|
320
|
+
* are written against: 4.5 for body text, 3 for large text (>= 24px, or 19px
|
|
321
|
+
* bold) and for a control's own outline against the surface it sits on.
|
|
322
|
+
*
|
|
323
|
+
* @param {string|Color} other - The second color (any form `color()` parses).
|
|
324
|
+
* @returns {number} Contrast ratio, >= 1
|
|
325
|
+
* @example
|
|
326
|
+
* color('#64748b').contrastRatio('#ffffff'); // 4.76 — the textSecondary token, passes
|
|
327
|
+
* color('#94a3b8').contrastRatio('#ffffff'); // 2.56 — the textDisabled token, fails
|
|
328
|
+
*/
|
|
329
|
+
contrastRatio(other) {
|
|
330
|
+
const a = this.luminance();
|
|
331
|
+
const b = other instanceof Color ? other.luminance() : color(other).luminance();
|
|
332
|
+
const light = Math.max(a, b);
|
|
333
|
+
const dark = Math.min(a, b);
|
|
334
|
+
return (light + 0.05) / (dark + 0.05);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Whether this color and another clear a WCAG contrast threshold.
|
|
339
|
+
*
|
|
340
|
+
* @param {string|Color} other - The color it is read against.
|
|
341
|
+
* @param {number} [minimum=4.5] - The ratio required: 4.5 body text, 3 large text.
|
|
342
|
+
* @returns {boolean} `true` when the pair is at or above `minimum`
|
|
343
|
+
*/
|
|
344
|
+
meetsContrast(other, minimum = 4.5) {
|
|
345
|
+
return this.contrastRatio(other) >= minimum;
|
|
346
|
+
}
|
|
347
|
+
|
|
294
348
|
/**
|
|
295
349
|
* Invert the color by complementing each RGB channel.
|
|
296
350
|
*
|
package/src/tools/index.js
CHANGED
|
@@ -20,7 +20,15 @@ export { default as padding } from "./padding.js";
|
|
|
20
20
|
export { print } from "./print.js";
|
|
21
21
|
export { random } from "./random.js";
|
|
22
22
|
export { default as rgba } from "./rgba.js";
|
|
23
|
-
export {
|
|
23
|
+
export {
|
|
24
|
+
SPACING_SCALE,
|
|
25
|
+
RADIUS_SCALE,
|
|
26
|
+
TYPE_SCALE,
|
|
27
|
+
TAP_MINIMUM,
|
|
28
|
+
PILL,
|
|
29
|
+
LABEL_TEXT,
|
|
30
|
+
} from "./scale.js";
|
|
31
|
+
export { default as shadow, SHADOW_LEVELS } from "./shadow.js";
|
|
24
32
|
export {
|
|
25
33
|
capitalize,
|
|
26
34
|
capitalizeWords,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// scale.js - The framework's design scales: the numbers every widget default is drawn from.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The clean-UI contract, expressed as data instead of prose.
|
|
5
|
+
*
|
|
6
|
+
* These are the same scales `flet-box check-ui` enforces against an app's own
|
|
7
|
+
* source. They live here because a gate that only reads the app cannot make the
|
|
8
|
+
* app clean: it can reject a `padding: 21`, but the widget defaults the app never
|
|
9
|
+
* typed already decide the rhythm, the radius and the tap size of every screen.
|
|
10
|
+
* A default below the Fitts floor is a rule the framework breaks on the user's
|
|
11
|
+
* behalf, and no amount of app-side linting can undo it.
|
|
12
|
+
*
|
|
13
|
+
* Units are framework units, not CSS pixels. A numeric length prop goes through
|
|
14
|
+
* the factory's `toREM` (`src/utils/units.js`), so `40` is emitted as `2.5rem`:
|
|
15
|
+
* 40px at the 16px base, and *bigger* when the reader raises their browser font.
|
|
16
|
+
* That is the point — a tap target written in rem obeys the accessibility
|
|
17
|
+
* setting, while the 44px the guidelines quote is a fixed-pixel floor that
|
|
18
|
+
* shrinks in relative terms the moment the type grows.
|
|
19
|
+
*
|
|
20
|
+
* Depth is the exception: `SHADOW_LEVELS` ships raw `px` inside the box-shadow
|
|
21
|
+
* string on purpose, because a shadow that scales with the reader's font size
|
|
22
|
+
* stops matching the surface it lifts.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** A radius this large clamps to half the box: a pill, whatever the widget's size. */
|
|
26
|
+
export const PILL = 999;
|
|
27
|
+
|
|
28
|
+
/** The 8-based rhythm, with the two steps a real layout needs (18 for field text, 96 for hero gutters). */
|
|
29
|
+
export const SPACING_SCALE = new Set([0, 4, 8, 12, 16, 18, 24, 32, 40, 48, 64, 96]);
|
|
30
|
+
|
|
31
|
+
/** Corner radii. `999` is a pill, `0` a flush edge — both are decisions, not omissions. */
|
|
32
|
+
export const RADIUS_SCALE = new Set([0, 4, 8, 12, 16, PILL]);
|
|
33
|
+
|
|
34
|
+
/** Type sizes. Two steps per jump: a 15 next to a 16 reads as a mistake, not as hierarchy. */
|
|
35
|
+
export const TYPE_SCALE = new Set([12, 14, 16, 18, 24, 32, 40, 48]);
|
|
36
|
+
|
|
37
|
+
/** Fitts: the smallest pressable box a finger reliably hits. */
|
|
38
|
+
export const TAP_MINIMUM = 40;
|
|
39
|
+
|
|
40
|
+
/** The reading size a control's own label may not fall under. */
|
|
41
|
+
export const LABEL_TEXT = 14;
|
package/src/tools/shadow.js
CHANGED
|
@@ -20,4 +20,24 @@
|
|
|
20
20
|
const shadow = (x, y, blur, spread, color) =>
|
|
21
21
|
`${x}px ${y}px ${blur}px ${spread}px ${color}`;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The framework's depth scale, shared by every widget that lifts off the page.
|
|
25
|
+
*
|
|
26
|
+
* The engine routes `elevation` straight into `style.boxShadow`, so a bare
|
|
27
|
+
* number is an invalid declaration the browser drops without a word: a widget
|
|
28
|
+
* that forwards `elevation` instead of one of these strings looks configured and
|
|
29
|
+
* paints flat. A widget that wants a numeric depth reads the level here and
|
|
30
|
+
* passes the resulting string.
|
|
31
|
+
*
|
|
32
|
+
* `0` is deliberately `none` — a flush surface is a decision, not a missing one.
|
|
33
|
+
*/
|
|
34
|
+
export const SHADOW_LEVELS = {
|
|
35
|
+
0: "none",
|
|
36
|
+
1: "0 1px 3px rgba(0,0,0,0.12)",
|
|
37
|
+
2: "0 3px 6px rgba(0,0,0,0.16)",
|
|
38
|
+
3: "0 6px 12px rgba(0,0,0,0.2)",
|
|
39
|
+
4: "0 10px 20px rgba(0,0,0,0.25)",
|
|
40
|
+
5: "0 15px 30px rgba(0,0,0,0.3)",
|
|
41
|
+
};
|
|
42
|
+
|
|
23
43
|
export default shadow;
|
package/src/utils/index.js
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { color } from "../tools/color.js";
|
|
2
|
+
|
|
3
|
+
const SOLID_COLOR =
|
|
4
|
+
/^(#[0-9a-f]{3,8}|rgba?\(\s*\d|hsla?\(\s*\d|black|white|red|green|blue|yellow|cyan|magenta|gray|darkgray|lightgray)$/i;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Black or white, whichever reads better on `background` by WCAG contrast.
|
|
8
|
+
* Falls back when the fill is not a solid color this code can measure (a
|
|
9
|
+
* gradient, a CSS variable) — guessing there is worse than the caller's choice.
|
|
10
|
+
*/
|
|
11
|
+
export const readableInk = (background, fallback) => {
|
|
12
|
+
if (typeof background !== "string" || !SOLID_COLOR.test(background)) {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
const bg = color(background);
|
|
16
|
+
return bg.contrastRatio("#ffffff") >= bg.contrastRatio("#000000")
|
|
17
|
+
? "#ffffff"
|
|
18
|
+
: "#000000";
|
|
19
|
+
};
|