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/Markdown.js
CHANGED
|
@@ -16,6 +16,8 @@ import { generateHighlightedHtml } from "../utils/syntaxHighlight.js";
|
|
|
16
16
|
* @param {string} text - Raw text to escape.
|
|
17
17
|
* @returns {string} Text with `&`, `<`, `>`, `"` and `'` replaced by entities.
|
|
18
18
|
*/
|
|
19
|
+
import { toREM } from "../utils/units.js";
|
|
20
|
+
|
|
19
21
|
const escapeCode = (text) =>
|
|
20
22
|
String(text)
|
|
21
23
|
.replace(/&/g, "&")
|
|
@@ -633,25 +635,31 @@ const parseMarkdown = (text) => {
|
|
|
633
635
|
`<a href="${escapeCode(href)}" target="_blank" rel="noopener">${label}</a>`,
|
|
634
636
|
);
|
|
635
637
|
|
|
636
|
-
// Unordered lists —
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
|
|
641
|
-
//
|
|
642
|
-
//
|
|
638
|
+
// Unordered lists — tag each "- item" line as an unordered <li>, then group
|
|
639
|
+
// the consecutive runs. `*` and `+` are CommonMark markers for the same list,
|
|
640
|
+
// so they are accepted too: `* uno` used to stay a literal paragraph. Emphasis
|
|
641
|
+
// has already run by now, which is why `**bold**` and `*italic*` at the start
|
|
642
|
+
// of a line are no longer reachable here. The tag is what keeps the ordered
|
|
643
|
+
// pass below from touching these items; without it, a `- a\n- b` run came out
|
|
644
|
+
// as `<ul><ol><li>a</li></ol><li>b</li></ul>`, so the first bullet rendered as
|
|
645
|
+
// the number "1." one indent level deeper than the second.
|
|
646
|
+
html = html.replace(/^[ \t]*[-*+][ \t]+(.*$)/gm, '<li data-list="ul">$1</li>');
|
|
643
647
|
html = html.replace(
|
|
644
|
-
/(<li>(?:(?!<\/
|
|
645
|
-
(match) =>
|
|
648
|
+
/(<li data-list="ul">(?:(?!<\/li>).)*?<\/li>(\n|$))+/g,
|
|
649
|
+
(match) =>
|
|
650
|
+
`<ul>${match
|
|
651
|
+
.replace(/\n$/, "")
|
|
652
|
+
.replace(/<li data-list="(?:ul|ol)">/g, "<li>")}</ul>\n`,
|
|
646
653
|
);
|
|
647
654
|
|
|
648
|
-
// Ordered lists
|
|
649
|
-
|
|
650
|
-
html = html.replace(/^[ \t]*\d+\.[ \t]+(.*$)/gm, "<li>$1</li>");
|
|
651
|
-
// Step 2: group consecutive ordered <li> runs that are NOT already inside a <ul>.
|
|
655
|
+
// Ordered lists — the same two steps with their own tag.
|
|
656
|
+
html = html.replace(/^[ \t]*\d+\.[ \t]+(.*$)/gm, '<li data-list="ol">$1</li>');
|
|
652
657
|
html = html.replace(
|
|
653
|
-
/(<li>(?:(?!<\/
|
|
654
|
-
(match) =>
|
|
658
|
+
/(<li data-list="ol">(?:(?!<\/li>).)*?<\/li>(\n|$))+/g,
|
|
659
|
+
(match) =>
|
|
660
|
+
`<ol>${match
|
|
661
|
+
.replace(/\n$/, "")
|
|
662
|
+
.replace(/<li data-list="(?:ul|ol)">/g, "<li>")}</ol>\n`,
|
|
655
663
|
);
|
|
656
664
|
|
|
657
665
|
// Blockquotes
|
|
@@ -842,7 +850,10 @@ export const Markdown = (props) => {
|
|
|
842
850
|
|
|
843
851
|
// List styling
|
|
844
852
|
listMargin = "0em 0",
|
|
845
|
-
|
|
853
|
+
// An outside marker hangs 1.5px left of the li text at 14px font, so a 1em
|
|
854
|
+
// indent leaves the disc straddling the root's left edge and `overflow: auto`
|
|
855
|
+
// clips half of it. The extra 0.25em keeps the whole marker inside.
|
|
856
|
+
listPadding = "0 0 0 1.25em",
|
|
846
857
|
listItemMargin = "0.0em 0",
|
|
847
858
|
|
|
848
859
|
// Image styling
|
|
@@ -877,15 +888,15 @@ export const Markdown = (props) => {
|
|
|
877
888
|
tag: "div",
|
|
878
889
|
widgetName: "Markdown",
|
|
879
890
|
fontFamily: fontFamily,
|
|
880
|
-
fontSize:
|
|
891
|
+
fontSize: toREM(fontSize),
|
|
881
892
|
lineHeight: lineHeight,
|
|
882
893
|
color: color,
|
|
883
|
-
padding:
|
|
884
|
-
maxHeight:
|
|
894
|
+
padding: toREM(padding),
|
|
895
|
+
maxHeight: toREM(maxHeight),
|
|
885
896
|
overflow: overflow,
|
|
886
897
|
backgroundColor: backgroundColor,
|
|
887
898
|
borderRadius:
|
|
888
|
-
|
|
899
|
+
toREM(borderRadius),
|
|
889
900
|
...rest,
|
|
890
901
|
});
|
|
891
902
|
|
|
@@ -1026,9 +1037,9 @@ export const Markdown = (props) => {
|
|
|
1026
1037
|
background-color: ${codeBgColor};
|
|
1027
1038
|
color: ${codeColor};
|
|
1028
1039
|
padding: ${codePadding};
|
|
1029
|
-
border-radius: ${
|
|
1040
|
+
border-radius: ${toREM(codeBorderRadius)};
|
|
1030
1041
|
font-family: ${codeFontFamily};
|
|
1031
|
-
font-size: ${
|
|
1042
|
+
font-size: ${toREM(codeFontSize)};
|
|
1032
1043
|
}
|
|
1033
1044
|
|
|
1034
1045
|
/* Bare <pre> elements (raw HTML injected via allowDangerousHtml or future extensions).
|
|
@@ -1036,7 +1047,7 @@ export const Markdown = (props) => {
|
|
|
1036
1047
|
.markdown-content pre:not(.md-code-block pre) {
|
|
1037
1048
|
background-color: ${preBgColor};
|
|
1038
1049
|
padding: ${prePadding};
|
|
1039
|
-
border-radius: ${
|
|
1050
|
+
border-radius: ${toREM(preBorderRadius)};
|
|
1040
1051
|
overflow: auto;
|
|
1041
1052
|
overflow-x: auto;
|
|
1042
1053
|
margin: ${preMargin};
|
|
@@ -1076,7 +1087,7 @@ export const Markdown = (props) => {
|
|
|
1076
1087
|
.markdown-content img {
|
|
1077
1088
|
max-width: ${imageMaxWidth};
|
|
1078
1089
|
height: auto;
|
|
1079
|
-
border-radius: ${
|
|
1090
|
+
border-radius: ${toREM(imageBorderRadius)};
|
|
1080
1091
|
}
|
|
1081
1092
|
|
|
1082
1093
|
.markdown-content hr {
|
|
@@ -1096,7 +1107,7 @@ export const Markdown = (props) => {
|
|
|
1096
1107
|
max-width: 100%;
|
|
1097
1108
|
min-width: 0;
|
|
1098
1109
|
box-sizing: border-box;
|
|
1099
|
-
border-radius: ${
|
|
1110
|
+
border-radius: ${toREM(preBorderRadius)};
|
|
1100
1111
|
overflow: hidden;
|
|
1101
1112
|
margin-top: 1em;
|
|
1102
1113
|
margin-bottom: 1em;
|
|
@@ -1172,7 +1183,7 @@ export const Markdown = (props) => {
|
|
|
1172
1183
|
/* Padding on <code> (not <pre>) keeps the scrollbar aligned. */
|
|
1173
1184
|
padding: ${prePadding};
|
|
1174
1185
|
font-family: ${codeFontFamily};
|
|
1175
|
-
font-size: ${
|
|
1186
|
+
font-size: ${toREM(codeFontSize)};
|
|
1176
1187
|
/* 1.5 matches VS Code / GitHub's code block density.
|
|
1177
1188
|
Setting it here AND on md-code-line stops any ancestor
|
|
1178
1189
|
line-height (the widget's default 1.6) from bleeding in. */
|
package/src/widgets/Modal.js
CHANGED
|
@@ -14,6 +14,8 @@ import { Icon } from "./Icon.js";
|
|
|
14
14
|
|
|
15
15
|
// ========== INTERNAL COMPONENTS ==========
|
|
16
16
|
|
|
17
|
+
import { toREM } from "../utils/units.js";
|
|
18
|
+
|
|
17
19
|
const ModalOverlay = ({
|
|
18
20
|
closeOnOverlayClick,
|
|
19
21
|
onClose,
|
|
@@ -42,10 +44,11 @@ const ModalOverlay = ({
|
|
|
42
44
|
timing: "ease",
|
|
43
45
|
delay: 0.1,
|
|
44
46
|
}),
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
onclick: (e) => {
|
|
48
|
+
// The panel is the overlay's only child, so a click whose target is the
|
|
49
|
+
// overlay itself landed on the backdrop and nowhere else.
|
|
50
|
+
if (e.target === e.currentTarget && closeOnOverlayClick) onClose();
|
|
51
|
+
},
|
|
49
52
|
});
|
|
50
53
|
};
|
|
51
54
|
|
|
@@ -224,10 +227,10 @@ const ModalContainer = ({
|
|
|
224
227
|
backgroundColor: backgroundColor || colors.surface,
|
|
225
228
|
borderRadius:
|
|
226
229
|
typeof borderRadius === "number"
|
|
227
|
-
?
|
|
230
|
+
? toREM(borderRadius)
|
|
228
231
|
: borderRadius || "16px",
|
|
229
|
-
width:
|
|
230
|
-
minWidth:
|
|
232
|
+
width: toREM(width),
|
|
233
|
+
minWidth: toREM(minWidth),
|
|
231
234
|
maxWidth: maxWidth || "90%",
|
|
232
235
|
maxHeight: maxHeight || "80vh",
|
|
233
236
|
overflow: "hidden",
|
|
@@ -273,7 +276,7 @@ export const Modal = (props = {}) => {
|
|
|
273
276
|
|
|
274
277
|
// Main container styles
|
|
275
278
|
backgroundColor = colors.surface,
|
|
276
|
-
borderRadius =
|
|
279
|
+
borderRadius = 16,
|
|
277
280
|
border = null,
|
|
278
281
|
borderColor = colors.border,
|
|
279
282
|
borderWidth = 1,
|
|
@@ -281,7 +284,7 @@ export const Modal = (props = {}) => {
|
|
|
281
284
|
elevation = 3, // ← New: shadow by elevation
|
|
282
285
|
|
|
283
286
|
// Content padding
|
|
284
|
-
padding =
|
|
287
|
+
padding = 24,
|
|
285
288
|
contentBgColor = colors.surface,
|
|
286
289
|
contentElevation = 0, // ← Optional inner shadow
|
|
287
290
|
|
|
@@ -374,7 +377,7 @@ export const Modal = (props = {}) => {
|
|
|
374
377
|
|
|
375
378
|
const contentElement = ModalContent({
|
|
376
379
|
content,
|
|
377
|
-
padding:
|
|
380
|
+
padding: toREM(padding),
|
|
378
381
|
contentBgColor,
|
|
379
382
|
contentElevation,
|
|
380
383
|
});
|
|
@@ -468,7 +471,7 @@ export const Modal = (props = {}) => {
|
|
|
468
471
|
updatePadding: (newPadding) => {
|
|
469
472
|
if (contentElement) {
|
|
470
473
|
contentElement.style.padding =
|
|
471
|
-
|
|
474
|
+
toREM(newPadding);
|
|
472
475
|
}
|
|
473
476
|
},
|
|
474
477
|
|
|
@@ -8,6 +8,8 @@ import { Button } from "./Button.js";
|
|
|
8
8
|
import { Text } from "./Text.js";
|
|
9
9
|
import { Icon } from "./Icon.js";
|
|
10
10
|
import { colors } from "../utils/themes.js";
|
|
11
|
+
import { toREM } from "../utils/units.js";
|
|
12
|
+
import { TAP_MINIMUM } from "../tools/scale.js";
|
|
11
13
|
|
|
12
14
|
export const Pagination = (props) => {
|
|
13
15
|
const {
|
|
@@ -56,26 +58,29 @@ export const Pagination = (props) => {
|
|
|
56
58
|
};
|
|
57
59
|
|
|
58
60
|
const pageNumbers = getPageNumbers();
|
|
61
|
+
// Every size is at least TAP_MINIMUM units tall: a page number is a thumb
|
|
62
|
+
// target, and the strip may only get smaller by type and gap, never below the
|
|
63
|
+
// 40-unit floor the rest of the framework holds itself to.
|
|
59
64
|
const sizes = {
|
|
60
|
-
small: { buttonSize:
|
|
61
|
-
medium: { buttonSize:
|
|
62
|
-
large: { buttonSize:
|
|
65
|
+
small: { buttonSize: TAP_MINIMUM, fontSize: 12, iconSize: 16, gap: 4 },
|
|
66
|
+
medium: { buttonSize: TAP_MINIMUM, fontSize: 14, iconSize: 20, gap: 6 },
|
|
67
|
+
large: { buttonSize: 48, fontSize: 16, iconSize: 24, gap: 8 },
|
|
63
68
|
};
|
|
64
69
|
const sz = sizes[size] || sizes.medium;
|
|
65
70
|
|
|
66
71
|
const buttonBase = {
|
|
67
|
-
minWidth:
|
|
68
|
-
height:
|
|
72
|
+
minWidth: toREM(sz.buttonSize),
|
|
73
|
+
height: toREM(sz.buttonSize),
|
|
69
74
|
padding: "0",
|
|
70
|
-
fontSize:
|
|
75
|
+
fontSize: toREM(sz.fontSize),
|
|
71
76
|
};
|
|
72
77
|
|
|
73
78
|
const createPageButton = (page, isActive = false) => {
|
|
74
79
|
const buttonVariant = isActive ? "filled" : variant;
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
const buttonColor = isActive ?
|
|
80
|
+
// The active page is a primary plate, so its number is the theme's ink. It
|
|
81
|
+
// is disabled only to say "you are already here" — disabling it made Button
|
|
82
|
+
// repaint the plate grey and the number grey, erasing the accent.
|
|
83
|
+
const buttonColor = isActive ? colors.onPrimary : colors.textSecondary;
|
|
79
84
|
|
|
80
85
|
return Button({
|
|
81
86
|
text: String(page),
|
|
@@ -85,7 +90,7 @@ export const Pagination = (props) => {
|
|
|
85
90
|
size: size,
|
|
86
91
|
...buttonBase,
|
|
87
92
|
onPress: () => handlePageChange(page),
|
|
88
|
-
disabled: disabled
|
|
93
|
+
disabled: disabled,
|
|
89
94
|
});
|
|
90
95
|
};
|
|
91
96
|
|
|
@@ -6,6 +6,8 @@ import { WidgetFactory } from "../widget-factory/index.js";
|
|
|
6
6
|
import { colors } from "../utils/themes.js";
|
|
7
7
|
import { injectKeyframes } from "../utils/styleInjector.js";
|
|
8
8
|
|
|
9
|
+
import { toREM } from "../utils/units.js";
|
|
10
|
+
|
|
9
11
|
export const ProgressBar = (props) => {
|
|
10
12
|
const {
|
|
11
13
|
value = 0,
|
|
@@ -37,7 +39,7 @@ export const ProgressBar = (props) => {
|
|
|
37
39
|
backgroundColor: color,
|
|
38
40
|
borderRadius:
|
|
39
41
|
typeof finalBorderRadius === "number"
|
|
40
|
-
?
|
|
42
|
+
? toREM(finalBorderRadius)
|
|
41
43
|
: finalBorderRadius,
|
|
42
44
|
transition: indeterminate ? "none" : "width 0.3s ease",
|
|
43
45
|
position: "relative",
|
|
@@ -73,11 +75,11 @@ export const ProgressBar = (props) => {
|
|
|
73
75
|
// Track background
|
|
74
76
|
const track = WidgetFactory({
|
|
75
77
|
flex: 1,
|
|
76
|
-
height:
|
|
78
|
+
height: toREM(height),
|
|
77
79
|
backgroundColor: backgroundColor,
|
|
78
80
|
borderRadius:
|
|
79
81
|
typeof finalBorderRadius === "number"
|
|
80
|
-
?
|
|
82
|
+
? toREM(finalBorderRadius)
|
|
81
83
|
: finalBorderRadius,
|
|
82
84
|
overflow: "hidden",
|
|
83
85
|
position: "relative",
|
|
@@ -148,7 +150,7 @@ export const ProgressBar = (props) => {
|
|
|
148
150
|
flexDirection: isColumn ? "column" : "row",
|
|
149
151
|
alignItems: "center",
|
|
150
152
|
gap: "8px",
|
|
151
|
-
width:
|
|
153
|
+
width: toREM(width),
|
|
152
154
|
children: children,
|
|
153
155
|
...rest,
|
|
154
156
|
});
|
package/src/widgets/Radio.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* @description Radio input with controlled/uncontrolled selection state.
|
|
4
4
|
*/
|
|
5
5
|
import { WidgetFactory } from "../widget-factory/index.js";
|
|
6
|
+
import { Text } from "./Text.js";
|
|
6
7
|
import { colors } from "../utils/themes.js";
|
|
8
|
+
import { TAP_MINIMUM, LABEL_TEXT } from "../tools/index.js";
|
|
7
9
|
|
|
8
10
|
export const Radio = (props) => {
|
|
9
11
|
const {
|
|
@@ -14,6 +16,7 @@ export const Radio = (props) => {
|
|
|
14
16
|
size = 20,
|
|
15
17
|
name,
|
|
16
18
|
activeColor = colors.primary,
|
|
19
|
+
label = null,
|
|
17
20
|
...rest
|
|
18
21
|
} = props;
|
|
19
22
|
|
|
@@ -25,10 +28,16 @@ export const Radio = (props) => {
|
|
|
25
28
|
let radioRef = null;
|
|
26
29
|
let dotRef = null;
|
|
27
30
|
|
|
31
|
+
// The ring's "off" rim is a control outline, not a hairline: `colors.border`
|
|
32
|
+
// measured 1.2:1 on the dark card and 1.3:1 on white, so an unselected radio
|
|
33
|
+
// was invisible in both themes. `gray500` is the same #71717A in both
|
|
34
|
+
// palettes and clears the 3:1 non-text minimum on each surface.
|
|
35
|
+
const offColor = disabled ? colors.border : colors.gray500;
|
|
36
|
+
|
|
28
37
|
const updateUI = (newSelected) => {
|
|
29
38
|
if (radioRef) {
|
|
30
39
|
radioRef.style.backgroundColor = newSelected ? activeColor : "transparent";
|
|
31
|
-
radioRef.style.border = `2px solid ${newSelected ? activeColor :
|
|
40
|
+
radioRef.style.border = `2px solid ${newSelected ? activeColor : offColor}`;
|
|
32
41
|
}
|
|
33
42
|
if (dotRef) {
|
|
34
43
|
dotRef.style.display = newSelected ? "block" : "none";
|
|
@@ -37,32 +46,81 @@ export const Radio = (props) => {
|
|
|
37
46
|
|
|
38
47
|
// Createste dot
|
|
39
48
|
const dot = WidgetFactory({
|
|
40
|
-
width: size
|
|
41
|
-
height: size
|
|
42
|
-
backgroundColor:
|
|
49
|
+
width: Math.max(8, Math.round(size * 0.4)),
|
|
50
|
+
height: Math.max(8, Math.round(size * 0.4)),
|
|
51
|
+
backgroundColor: colors.onPrimary,
|
|
43
52
|
borderRadius: "50%",
|
|
44
53
|
display: internalSelected ? "block" : "none",
|
|
45
54
|
});
|
|
46
55
|
dotRef = dot;
|
|
47
56
|
|
|
48
|
-
//
|
|
49
|
-
const
|
|
50
|
-
widgetName: "Radio",
|
|
57
|
+
// The visual circle: a 2-unit ring and the fill, nothing else.
|
|
58
|
+
const circle = WidgetFactory({
|
|
51
59
|
width: size,
|
|
52
60
|
height: size,
|
|
53
|
-
border: `2px solid ${disabled ? colors.border : internalSelected ? activeColor :
|
|
61
|
+
border: `2px solid ${disabled ? colors.border : internalSelected ? activeColor : offColor}`,
|
|
54
62
|
borderRadius: "50%",
|
|
55
63
|
backgroundColor: internalSelected ? activeColor : "transparent",
|
|
56
64
|
display: "flex",
|
|
57
65
|
alignItems: "center",
|
|
58
66
|
justifyContent: "center",
|
|
59
67
|
transition: "all 0.2s ease",
|
|
60
|
-
|
|
61
|
-
opacity: disabled ? 0.5 : 1,
|
|
68
|
+
boxSizing: "border-box",
|
|
62
69
|
child: dot,
|
|
63
|
-
...rest,
|
|
64
70
|
});
|
|
65
|
-
radioRef =
|
|
71
|
+
radioRef = circle;
|
|
72
|
+
|
|
73
|
+
// Createste main radio container — the finger's box, larger than the ring it
|
|
74
|
+
// centres, because radios are chosen with a thumb and a 20-unit circle is not
|
|
75
|
+
// something a thumb hits reliably.
|
|
76
|
+
//
|
|
77
|
+
// The overhang is given back with a negative margin. A 40-unit box in the
|
|
78
|
+
// layout next to a label stretched the row to 40 and left the text at the top
|
|
79
|
+
// of it, so the ring sat ~10 units below the words and the requested `gap` was
|
|
80
|
+
// silently doubled. The press still lands on the full 40-unit box, which now
|
|
81
|
+
// overflows its layout footprint instead of pushing the row around it.
|
|
82
|
+
const overhang = Math.max(0, (TAP_MINIMUM - size) / 2);
|
|
83
|
+
|
|
84
|
+
// A `label` is part of the control, not a caption beside it: the whole row is
|
|
85
|
+
// pressable. It also cannot reach the factory as a flat prop, which would set
|
|
86
|
+
// the root's `textContent` and render the words *before* the ring it describes,
|
|
87
|
+
// outside the hit box, while leaving the requested `gap` unused.
|
|
88
|
+
const isLabelled = label !== null && label !== undefined && label !== "";
|
|
89
|
+
|
|
90
|
+
const rootProps = {
|
|
91
|
+
widgetName: "Radio",
|
|
92
|
+
display: "inline-flex",
|
|
93
|
+
alignItems: "center",
|
|
94
|
+
justifyContent: isLabelled ? "flex-start" : "center",
|
|
95
|
+
gap: isLabelled ? 12 : undefined,
|
|
96
|
+
minWidth: isLabelled ? undefined : TAP_MINIMUM,
|
|
97
|
+
minHeight: TAP_MINIMUM,
|
|
98
|
+
margin: isLabelled ? 0 : -overhang,
|
|
99
|
+
flexShrink: 0,
|
|
100
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
101
|
+
opacity: disabled ? 0.5 : 1,
|
|
102
|
+
child: isLabelled
|
|
103
|
+
? [
|
|
104
|
+
circle,
|
|
105
|
+
Text({
|
|
106
|
+
text: label,
|
|
107
|
+
size: LABEL_TEXT,
|
|
108
|
+
fontWeight: 500,
|
|
109
|
+
color: disabled ? colors.textDisabled : colors.text,
|
|
110
|
+
}),
|
|
111
|
+
]
|
|
112
|
+
: circle,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Pressing the words must not scale the words. The ring on its own keeps the
|
|
116
|
+
// default press feedback, because there the root *is* the control.
|
|
117
|
+
if (isLabelled) rootProps.disableTransform = true;
|
|
118
|
+
|
|
119
|
+
const radio = WidgetFactory({ ...rootProps, ...rest });
|
|
120
|
+
|
|
121
|
+
// Inline usage (a radio dropped mid-paragraph) would otherwise baseline-align
|
|
122
|
+
// the box's bottom edge and lift the ring above the text.
|
|
123
|
+
radio.style.verticalAlign = "middle";
|
|
66
124
|
|
|
67
125
|
// Handle click
|
|
68
126
|
if (!disabled) {
|
|
@@ -92,16 +150,16 @@ export const Radio = (props) => {
|
|
|
92
150
|
// direct assignment died on the first `update()` or reactive setter.
|
|
93
151
|
radio.update({ onPress: handleClick });
|
|
94
152
|
|
|
95
|
-
// Hover effect
|
|
153
|
+
// Hover effect — on the drawn ring, not the hit box around it.
|
|
96
154
|
radio.addEventListener("mouseenter", () => {
|
|
97
155
|
if (!disabled && !internalSelected) {
|
|
98
|
-
|
|
156
|
+
circle.style.backgroundColor = `color-mix(in srgb, ${activeColor} 12%, transparent)`;
|
|
99
157
|
}
|
|
100
158
|
});
|
|
101
159
|
|
|
102
160
|
radio.addEventListener("mouseleave", () => {
|
|
103
161
|
if (!disabled && !internalSelected) {
|
|
104
|
-
|
|
162
|
+
circle.style.backgroundColor = "transparent";
|
|
105
163
|
}
|
|
106
164
|
});
|
|
107
165
|
}
|
package/src/widgets/Rating.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Row } from "./Row.js";
|
|
|
7
7
|
import { Text } from "./Text.js";
|
|
8
8
|
import { Icon } from "./Icon.js";
|
|
9
9
|
import { colors, subscribeTheme } from "../utils/themes.js";
|
|
10
|
+
import { TAP_MINIMUM } from "../tools/index.js";
|
|
10
11
|
|
|
11
12
|
export const Rating = (props) => {
|
|
12
13
|
const {
|
|
@@ -21,7 +22,7 @@ export const Rating = (props) => {
|
|
|
21
22
|
iconActive = "star",
|
|
22
23
|
iconInactive = "star_border",
|
|
23
24
|
iconHalf = "star_half",
|
|
24
|
-
gap =
|
|
25
|
+
gap = 4,
|
|
25
26
|
allowHalf = false,
|
|
26
27
|
showValue = false,
|
|
27
28
|
valueColor = colors.textSecondary,
|
|
@@ -52,12 +53,22 @@ export const Rating = (props) => {
|
|
|
52
53
|
let iconName = iconInactive;
|
|
53
54
|
let color = inactiveColor;
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
// A score like 4.4 must still show a half star. Matching `starNumber - 0.5
|
|
57
|
+
// === currentValue` only covered exact halves, so every fractional score
|
|
58
|
+
// fell through and the last star stayed empty.
|
|
59
|
+
const whole = Math.floor(currentValue);
|
|
60
|
+
const fraction = currentValue - whole;
|
|
61
|
+
if (starNumber <= whole) {
|
|
56
62
|
iconName = iconActive;
|
|
57
63
|
color = activeColor;
|
|
58
|
-
} else if (allowHalf && starNumber
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
} else if (allowHalf && starNumber === whole + 1) {
|
|
65
|
+
if (fraction >= 0.75) {
|
|
66
|
+
iconName = iconActive;
|
|
67
|
+
color = activeColor;
|
|
68
|
+
} else if (fraction >= 0.25) {
|
|
69
|
+
iconName = iconHalf;
|
|
70
|
+
color = activeColor;
|
|
71
|
+
}
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
span.textContent = iconName;
|
|
@@ -108,10 +119,17 @@ export const Rating = (props) => {
|
|
|
108
119
|
});
|
|
109
120
|
const wrapper = WidgetFactory({
|
|
110
121
|
display: "inline-flex",
|
|
122
|
+
alignItems: "center",
|
|
123
|
+
justifyContent: "center",
|
|
111
124
|
cursor: readOnly ? "default" : "pointer",
|
|
112
125
|
transition: "transform 0.1s ease",
|
|
113
126
|
child: icon,
|
|
114
|
-
|
|
127
|
+
// A star is picked with a thumb, so the cell — not the glyph — is the
|
|
128
|
+
// target: 20 units of star inside a 40-unit box. A read-only rating shows a
|
|
129
|
+
// score instead of a control, so it keeps its compact 4-unit gutter.
|
|
130
|
+
...(readOnly
|
|
131
|
+
? { padding: 4 }
|
|
132
|
+
: { minWidth: TAP_MINIMUM, minHeight: TAP_MINIMUM }),
|
|
115
133
|
});
|
|
116
134
|
|
|
117
135
|
starIcons.push(icon);
|