flet-box 1.0.7 → 1.0.9
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 +3 -0
- 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 +1398 -443
- package/docs/README.md +4 -0
- package/docs/cli/README.md +63 -2
- 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/mobile-and-platforms.md +31 -0
- 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 +42 -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/README.md +2 -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 +265 -58
- 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
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",
|
package/src/widgets/Checkbox.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* @description Checkbox input with controlled/uncontrolled 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 Checkbox = (props) => {
|
|
9
11
|
const {
|
|
@@ -13,6 +15,7 @@ export const Checkbox = (props) => {
|
|
|
13
15
|
disabled = false,
|
|
14
16
|
size = 20,
|
|
15
17
|
activeColor = colors.primary,
|
|
18
|
+
label = null,
|
|
16
19
|
...rest
|
|
17
20
|
} = props;
|
|
18
21
|
|
|
@@ -24,45 +27,100 @@ export const Checkbox = (props) => {
|
|
|
24
27
|
let checkboxRef = null;
|
|
25
28
|
let checkMarkRef = null;
|
|
26
29
|
|
|
30
|
+
// The box's "off" rim is a control outline, not a hairline: `colors.border`
|
|
31
|
+
// measured 1.2:1 on the dark card and 1.3:1 on white, so an unchecked box was
|
|
32
|
+
// invisible in both themes. `gray500` is the same #71717A in both palettes and
|
|
33
|
+
// clears the 3:1 non-text minimum on each surface.
|
|
34
|
+
const offColor = disabled ? colors.border : colors.gray500;
|
|
35
|
+
|
|
27
36
|
const updateUI = (newChecked) => {
|
|
28
37
|
if (checkboxRef) {
|
|
29
|
-
checkboxRef.style.
|
|
30
|
-
checkboxRef.style.border = `2px solid ${newChecked ? activeColor : colors.border}`;
|
|
38
|
+
checkboxRef.style.border = `2px solid ${newChecked ? activeColor : offColor}`;
|
|
31
39
|
}
|
|
32
40
|
if (checkMarkRef) {
|
|
33
41
|
checkMarkRef.style.display = newChecked ? "inline-block" : "none";
|
|
34
42
|
}
|
|
35
43
|
};
|
|
36
44
|
|
|
37
|
-
//
|
|
45
|
+
// The tick is drawn inside the frame, in the active colour: a filled plate
|
|
46
|
+
// would hide the border the box is supposed to show, and the theme ink it used
|
|
47
|
+
// to carry (white in light, near-black in dark) only read on that plate.
|
|
38
48
|
const checkMark = WidgetFactory({
|
|
39
49
|
tag: "span",
|
|
40
50
|
textContent: "✓",
|
|
41
|
-
color:
|
|
42
|
-
fontSize: size * 0.
|
|
51
|
+
color: disabled ? colors.textDisabled : activeColor,
|
|
52
|
+
fontSize: Math.max(12, Math.round(size * 0.65)),
|
|
43
53
|
fontWeight: "bold",
|
|
54
|
+
lineHeight: 1,
|
|
44
55
|
display: internalChecked ? "inline-block" : "none",
|
|
45
56
|
});
|
|
46
57
|
checkMarkRef = checkMark;
|
|
47
58
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
// The visual square. Everything drawn lives here, so the box below can be the
|
|
60
|
+
// finger's target without becoming a 40-unit checkbox.
|
|
61
|
+
const box = WidgetFactory({
|
|
51
62
|
width: size,
|
|
52
63
|
height: size,
|
|
53
|
-
border: `2px solid ${disabled ? colors.border : internalChecked ? activeColor :
|
|
54
|
-
borderRadius:
|
|
55
|
-
backgroundColor:
|
|
64
|
+
border: `2px solid ${disabled ? colors.border : internalChecked ? activeColor : offColor}`,
|
|
65
|
+
borderRadius: 0,
|
|
66
|
+
backgroundColor: "transparent",
|
|
56
67
|
display: "flex",
|
|
57
68
|
alignItems: "center",
|
|
58
69
|
justifyContent: "center",
|
|
59
70
|
transition: "all 0.2s ease",
|
|
60
|
-
|
|
61
|
-
opacity: disabled ? 0.5 : 1,
|
|
71
|
+
boxSizing: "border-box",
|
|
62
72
|
child: checkMark,
|
|
63
|
-
...rest,
|
|
64
73
|
});
|
|
65
|
-
checkboxRef =
|
|
74
|
+
checkboxRef = box;
|
|
75
|
+
|
|
76
|
+
// Createste main checkbox container
|
|
77
|
+
//
|
|
78
|
+
// The overhang is given back with a negative margin. A 40-unit box in the
|
|
79
|
+
// layout next to a label stretched the row to 40 and left the text at the top
|
|
80
|
+
// of it, so the box sat ~10 units below the words and the requested `gap` was
|
|
81
|
+
// silently doubled. The press still lands on the full 40-unit box, which now
|
|
82
|
+
// overflows its layout footprint instead of pushing the row around it.
|
|
83
|
+
const overhang = Math.max(0, (TAP_MINIMUM - size) / 2);
|
|
84
|
+
|
|
85
|
+
// A `label` is part of the control, not a caption beside it: the whole row is
|
|
86
|
+
// pressable. It also cannot reach the factory as a flat prop, which would set
|
|
87
|
+
// the root's `textContent` and wipe the box it is supposed to describe.
|
|
88
|
+
const isLabelled = label !== null && label !== undefined && label !== "";
|
|
89
|
+
|
|
90
|
+
const rootProps = {
|
|
91
|
+
widgetName: "Checkbox",
|
|
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
|
+
box,
|
|
105
|
+
Text({
|
|
106
|
+
text: label,
|
|
107
|
+
size: LABEL_TEXT,
|
|
108
|
+
fontWeight: 500,
|
|
109
|
+
color: disabled ? colors.textDisabled : colors.text,
|
|
110
|
+
}),
|
|
111
|
+
]
|
|
112
|
+
: box,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Pressing the words must not scale the words. The box 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 checkbox = WidgetFactory({ ...rootProps, ...rest });
|
|
120
|
+
|
|
121
|
+
// Inline usage (a checkbox dropped mid-paragraph) would otherwise
|
|
122
|
+
// baseline-align the box's bottom edge and lift the square above the text.
|
|
123
|
+
checkbox.style.verticalAlign = "middle";
|
|
66
124
|
|
|
67
125
|
// Handle click
|
|
68
126
|
if (!disabled) {
|
|
@@ -80,19 +138,19 @@ export const Checkbox = (props) => {
|
|
|
80
138
|
// padding…) calls update — leaving the control unable to toggle ever again.
|
|
81
139
|
checkbox.update({ onPress: handleClick });
|
|
82
140
|
|
|
83
|
-
// Hover effect
|
|
141
|
+
// Hover effect — on the drawn square, not the whole labelled row.
|
|
84
142
|
checkbox.addEventListener("mouseenter", () => {
|
|
85
143
|
if (!disabled && !internalChecked) {
|
|
86
144
|
// `color-mix` rather than `activeColor + "20"`: that string concat only
|
|
87
145
|
// produced a valid color for `#rrggbb` hexes and silently broke on
|
|
88
146
|
// `rgba()`, named colors and 3-digit hex `activeColor` values.
|
|
89
|
-
|
|
147
|
+
box.style.backgroundColor = `color-mix(in srgb, ${activeColor} 12%, transparent)`;
|
|
90
148
|
}
|
|
91
149
|
});
|
|
92
150
|
|
|
93
151
|
checkbox.addEventListener("mouseleave", () => {
|
|
94
152
|
if (!disabled && !internalChecked) {
|
|
95
|
-
|
|
153
|
+
box.style.backgroundColor = "transparent";
|
|
96
154
|
}
|
|
97
155
|
});
|
|
98
156
|
}
|
package/src/widgets/Chip.js
CHANGED
|
@@ -10,6 +10,7 @@ import { Row } from "./Row.js";
|
|
|
10
10
|
import { Text } from "./Text.js";
|
|
11
11
|
import { Icon } from "./Icon.js";
|
|
12
12
|
import { colors } from "../utils/themes.js";
|
|
13
|
+
import { PILL, TAP_MINIMUM } from "../tools/index.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Creates a Chip widget — a small interactive label commonly used for tags,
|
|
@@ -22,10 +23,10 @@ import { colors } from "../utils/themes.js";
|
|
|
22
23
|
* @param {Function} [props.onDelete] - When provided, renders a close ("×") icon on the right side of the chip. Called when that icon is clicked.
|
|
23
24
|
* @param {'filled'|'outlined'} [props.variant='filled'] - Visual style. 'filled' uses a solid background; 'outlined' uses a transparent background with a border.
|
|
24
25
|
* @param {string} [props.color=colors.primary] - Chip accent color: used as the background (filled) or border/text (outlined).
|
|
25
|
-
* @param {string} [props.textColor] - Explicit text color override. Defaults to
|
|
26
|
+
* @param {string} [props.textColor] - Explicit text color override. Defaults to `colors.onPrimary` (filled) or `color` (outlined).
|
|
26
27
|
* @param {string} [props.borderColor] - Explicit border color override (outlined variant only). Defaults to `color`.
|
|
27
|
-
* @param {number} [props.borderRadius=
|
|
28
|
-
* @param {string} [props.padding
|
|
28
|
+
* @param {number} [props.borderRadius=999] - Corner radius in framework units (converted to `rem`). The default clamps to a pill.
|
|
29
|
+
* @param {number|string} [props.padding] - Internal padding. A number is a uniform gutter (doubled horizontally); omitted it is 4 vertical x 12 horizontal, in framework units.
|
|
29
30
|
* @param {number} [props.gap=4] - Gap in pixels between icon, label, and delete icon.
|
|
30
31
|
* @param {number} [props.size=12] - Font size in pixels. Also scales the icons proportionally.
|
|
31
32
|
* @param {number} [props.elevation=0] - Box-shadow depth. 0 means no shadow.
|
|
@@ -41,14 +42,26 @@ export const Chip = (props) => {
|
|
|
41
42
|
color = colors.primary,
|
|
42
43
|
textColor,
|
|
43
44
|
borderColor,
|
|
44
|
-
borderRadius =
|
|
45
|
-
padding
|
|
45
|
+
borderRadius = PILL,
|
|
46
|
+
padding,
|
|
46
47
|
gap = 4,
|
|
47
48
|
size = 12,
|
|
48
49
|
elevation = 0,
|
|
49
50
|
...rest
|
|
50
51
|
} = props;
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* `4px 12px` as a literal string reaches the factory as raw CSS and freezes
|
|
55
|
+
* there: the chip's type grew with the reader's font and its gutters did not.
|
|
56
|
+
* Numbers go through `toREM` instead, and the pair is on the spacing scale.
|
|
57
|
+
*/
|
|
58
|
+
const chipPadding =
|
|
59
|
+
padding === undefined
|
|
60
|
+
? { paddingVertical: 4, paddingHorizontal: 12 }
|
|
61
|
+
: typeof padding === "number"
|
|
62
|
+
? { paddingVertical: padding, paddingHorizontal: padding * 2 }
|
|
63
|
+
: { padding };
|
|
64
|
+
|
|
52
65
|
// Derive background, text, and border colors from the variant
|
|
53
66
|
let bgColor, txtColor, brdColor;
|
|
54
67
|
|
|
@@ -58,7 +71,10 @@ export const Chip = (props) => {
|
|
|
58
71
|
brdColor = borderColor || color;
|
|
59
72
|
} else {
|
|
60
73
|
bgColor = color;
|
|
61
|
-
|
|
74
|
+
// The label sits on the primary plate, so its ink is the theme's, not a
|
|
75
|
+
// literal white: in dark mode a primary chip is near-white and white words
|
|
76
|
+
// would vanish.
|
|
77
|
+
txtColor = textColor || colors.onPrimary;
|
|
62
78
|
brdColor = "transparent";
|
|
63
79
|
}
|
|
64
80
|
|
|
@@ -88,7 +104,7 @@ export const Chip = (props) => {
|
|
|
88
104
|
if (onDelete) {
|
|
89
105
|
const closeIcon = Icon({
|
|
90
106
|
name: "close",
|
|
91
|
-
size: size - 2,
|
|
107
|
+
size: Math.max(12, size - 2),
|
|
92
108
|
color: txtColor,
|
|
93
109
|
cursor: "pointer",
|
|
94
110
|
onclick: (e) => {
|
|
@@ -106,7 +122,7 @@ export const Chip = (props) => {
|
|
|
106
122
|
backgroundColor: bgColor,
|
|
107
123
|
border: variant === "outlined" ? `1px solid ${brdColor}` : "none",
|
|
108
124
|
borderRadius: borderRadius,
|
|
109
|
-
|
|
125
|
+
...chipPadding,
|
|
110
126
|
width: "fit-content",
|
|
111
127
|
minWidth: "auto",
|
|
112
128
|
boxShadow:
|
|
@@ -116,10 +132,15 @@ export const Chip = (props) => {
|
|
|
116
132
|
children: children,
|
|
117
133
|
});
|
|
118
134
|
|
|
119
|
-
// Outer wrapper element enables the onclick without styling conflicts
|
|
135
|
+
// Outer wrapper element enables the onclick without styling conflicts.
|
|
136
|
+
// A chip you can press or dismiss is a target, so the wrapper carries the
|
|
137
|
+
// Fitts floor while the pill inside keeps its compact 23-unit height.
|
|
120
138
|
const chip = WidgetFactory({
|
|
121
139
|
widgetName: "Chip",
|
|
122
|
-
display: "inline-
|
|
140
|
+
display: "inline-flex",
|
|
141
|
+
alignItems: "center",
|
|
142
|
+
justifyContent: "center",
|
|
143
|
+
...(onPress || onDelete ? { minHeight: TAP_MINIMUM } : {}),
|
|
123
144
|
cursor: onPress ? "pointer" : "default",
|
|
124
145
|
transition: "all 0.2s ease",
|
|
125
146
|
child: chipContent,
|
|
@@ -85,13 +85,13 @@ export const CircularBar = (props) => {
|
|
|
85
85
|
|
|
86
86
|
const updateCanvasSize = () => {
|
|
87
87
|
if (!canvas) return;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
// The box is routed through rem, so a fixed `size: 120` paints 180px wide
|
|
89
|
+
// when the reader's base font is 24. The drawing buffer has to follow that
|
|
90
|
+
// rendered box, not the number the developer wrote, or the ring grows and
|
|
91
|
+
// turns soft. The number is only the fallback for a node with no layout yet.
|
|
92
|
+
const measured = container.getBoundingClientRect().width;
|
|
93
|
+
const width =
|
|
94
|
+
measured > 0 ? measured : isFixedSize ? fixedSize : 200;
|
|
95
95
|
canvas.width = width;
|
|
96
96
|
canvas.height = width;
|
|
97
97
|
currentWidth = width;
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { WidgetFactory } from "../widget-factory/index.js";
|
|
6
6
|
import { colors } from "../utils/themes.js";
|
|
7
|
+
import { readableInk } from "../utils/readableInk.js";
|
|
8
|
+
|
|
9
|
+
import { toREM } from "../utils/units.js";
|
|
7
10
|
|
|
8
11
|
export const CircularChart = (props) => {
|
|
9
12
|
let {
|
|
@@ -21,7 +24,10 @@ export const CircularChart = (props) => {
|
|
|
21
24
|
centerContent = null,
|
|
22
25
|
showLabels = false,
|
|
23
26
|
labelSize = 12,
|
|
24
|
-
|
|
27
|
+
// Unset by default: every slice has its own fill, and the theme's ink is
|
|
28
|
+
// the same colour as the primary slice in both palettes, so the label has
|
|
29
|
+
// to be chosen against the slice it sits on (see `labelInk`).
|
|
30
|
+
labelColor = null,
|
|
25
31
|
onClick = null,
|
|
26
32
|
onHover = null,
|
|
27
33
|
defaultColors = [
|
|
@@ -42,7 +48,7 @@ export const CircularChart = (props) => {
|
|
|
42
48
|
borderColor = null,
|
|
43
49
|
borderWidth = 0,
|
|
44
50
|
sliceBorderWidth = 0,
|
|
45
|
-
sliceBorderColor =
|
|
51
|
+
sliceBorderColor = colors.card,
|
|
46
52
|
...rest
|
|
47
53
|
} = props;
|
|
48
54
|
|
|
@@ -117,10 +123,15 @@ export const CircularChart = (props) => {
|
|
|
117
123
|
const updateCanvasSize = () => {
|
|
118
124
|
if (!canvas) return;
|
|
119
125
|
if (isFixedSize) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
// The box is routed through rem, so `size: 160` is 240px wide for a reader
|
|
127
|
+
// on a 24px base font. The buffer has to follow the rendered box or the
|
|
128
|
+
// chart grows and turns soft; the numbers are the fallback for a node
|
|
129
|
+
// that has no layout yet.
|
|
130
|
+
const { width, height } = container.getBoundingClientRect();
|
|
131
|
+
canvas.width = width > 0 ? width : canvasWidth;
|
|
132
|
+
canvas.height = height > 0 ? height : canvasHeight;
|
|
133
|
+
currentWidth = canvas.width;
|
|
134
|
+
currentHeight = canvas.height;
|
|
124
135
|
} else {
|
|
125
136
|
const rect = container.getBoundingClientRect();
|
|
126
137
|
let w = rect.width;
|
|
@@ -192,6 +203,13 @@ export const CircularChart = (props) => {
|
|
|
192
203
|
return defaultColors[index % defaultColors.length];
|
|
193
204
|
};
|
|
194
205
|
|
|
206
|
+
const labelInk = (fill) => {
|
|
207
|
+
if (labelColor) return labelColor;
|
|
208
|
+
if (fill === colors.primary) return colors.onPrimary;
|
|
209
|
+
if (fill === colors.secondary) return colors.onSecondary;
|
|
210
|
+
return readableInk(fill, colors.text);
|
|
211
|
+
};
|
|
212
|
+
|
|
195
213
|
// Draw each segment with rounded ends (each one with its own color)
|
|
196
214
|
const drawRoundedSegment = (slice, index) => {
|
|
197
215
|
const r = getRadiusVal() / 2;
|
|
@@ -269,7 +287,7 @@ export const CircularChart = (props) => {
|
|
|
269
287
|
const x = cx + labelRadius * Math.cos(midAngle);
|
|
270
288
|
const y = cy + labelRadius * Math.sin(midAngle);
|
|
271
289
|
ctx.font = `${labelSize}px system-ui`;
|
|
272
|
-
ctx.fillStyle =
|
|
290
|
+
ctx.fillStyle = labelInk(color);
|
|
273
291
|
ctx.shadowBlur = 0;
|
|
274
292
|
const percent = Math.round(
|
|
275
293
|
(slice.value / (slice.end - slice.start)) * 100,
|
|
@@ -463,7 +481,7 @@ export const CircularChart = (props) => {
|
|
|
463
481
|
position: "relative",
|
|
464
482
|
overflow: "hidden",
|
|
465
483
|
borderRadius:
|
|
466
|
-
|
|
484
|
+
toREM(borderRadius),
|
|
467
485
|
border:
|
|
468
486
|
borderWidth > 0 && borderColor
|
|
469
487
|
? `${borderWidth}px solid ${borderColor}`
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
import { WidgetFactory } from "../widget-factory/index.js";
|
|
6
6
|
import { colors } from "../utils/themes.js";
|
|
7
7
|
import { composeUpdate } from "../utils/composeUpdate.js";
|
|
8
|
-
import { generateHighlightedHtml } from "../utils/syntaxHighlight.js";
|
|
8
|
+
import { generateHighlightedHtml, syntaxPaletteFor } from "../utils/syntaxHighlight.js";
|
|
9
|
+
import { clipboard } from "../tools/clipboard.js";
|
|
10
|
+
|
|
11
|
+
import { toREM } from "../utils/units.js";
|
|
9
12
|
|
|
10
13
|
export const CodeViewer = (props) => {
|
|
11
14
|
const {
|
|
@@ -20,22 +23,32 @@ code = "",
|
|
|
20
23
|
showLineNumbers = false,
|
|
21
24
|
startingLineNumber = 1,
|
|
22
25
|
lineNumberWidth = 40,
|
|
23
|
-
lineNumberColor
|
|
26
|
+
lineNumberColor,
|
|
24
27
|
...rest
|
|
25
28
|
} = props;
|
|
26
29
|
|
|
30
|
+
// The token map follows the block's own surface: the light theme's #F4F4F5
|
|
31
|
+
// pane needs the light map, and a caller that paints a dark pane on the light
|
|
32
|
+
// theme still gets spans that read.
|
|
33
|
+
const syntaxPalette = syntaxPaletteFor(backgroundColor);
|
|
34
|
+
// The gutter is not a token: it takes the muted ink of the pane's palette, so
|
|
35
|
+
// a custom pane does not get the page theme's grey on top of it.
|
|
36
|
+
const gutterInk = lineNumberColor || syntaxPalette.muted;
|
|
37
|
+
|
|
27
38
|
let currentCode =
|
|
28
39
|
typeof code === "string" ? code : JSON.stringify(code, null, 2);
|
|
29
40
|
let currentTitle = title;
|
|
30
41
|
let headerRef = null;
|
|
42
|
+
let headerTitleRef = null;
|
|
31
43
|
let scrollWrapperRef = null;
|
|
32
44
|
let lineNumbersColRef = null;
|
|
33
45
|
let codeColRef = null;
|
|
34
46
|
let codeWrapperRef = null;
|
|
47
|
+
let copyResetTimer = null;
|
|
35
48
|
|
|
36
49
|
const refreshContent = () => {
|
|
37
50
|
const lines = currentCode.split("\n");
|
|
38
|
-
const highlightedLines = lines.map((line) => generateHighlightedHtml(line));
|
|
51
|
+
const highlightedLines = lines.map((line) => generateHighlightedHtml(line, syntaxPalette));
|
|
39
52
|
|
|
40
53
|
if (showLineNumbers) {
|
|
41
54
|
// Update line numbers
|
|
@@ -44,7 +57,7 @@ code = "",
|
|
|
44
57
|
for (let i = 0; i < lines.length; i++) {
|
|
45
58
|
const numberDiv = WidgetFactory({
|
|
46
59
|
tag: "div",
|
|
47
|
-
padding: `0 ${padding / 2}
|
|
60
|
+
padding: `0 ${toREM(padding / 2)} 0 ${toREM(padding)}`,
|
|
48
61
|
whiteSpace: "pre",
|
|
49
62
|
textContent: String(startingLineNumber + i),
|
|
50
63
|
});
|
|
@@ -79,28 +92,86 @@ code = "",
|
|
|
79
92
|
width: "100%",
|
|
80
93
|
display: "flex",
|
|
81
94
|
flexDirection: "column",
|
|
95
|
+
position: "relative",
|
|
82
96
|
...rest,
|
|
83
97
|
});
|
|
84
98
|
|
|
99
|
+
// The copy button carries its own solid plate, so it reads on the primary
|
|
100
|
+
// header and on any caller-painted pane, and it never relies on the page ink.
|
|
101
|
+
const copyButtonRef = WidgetFactory({
|
|
102
|
+
tag: "button",
|
|
103
|
+
type: "button",
|
|
104
|
+
textContent: "Copy",
|
|
105
|
+
backgroundColor: colors.card,
|
|
106
|
+
color: colors.text,
|
|
107
|
+
border: `1px solid ${colors.border}`,
|
|
108
|
+
borderRadius: 6,
|
|
109
|
+
padding: "2px 8px",
|
|
110
|
+
fontFamily: "monospace",
|
|
111
|
+
fontSize: 11,
|
|
112
|
+
lineHeight: "1.6",
|
|
113
|
+
flexShrink: 0,
|
|
114
|
+
cursor: "pointer",
|
|
115
|
+
onclick: () => {
|
|
116
|
+
clipboard.copy(currentCode);
|
|
117
|
+
copyButtonRef.textContent = "Copied";
|
|
118
|
+
clearTimeout(copyResetTimer);
|
|
119
|
+
copyResetTimer = setTimeout(() => {
|
|
120
|
+
copyButtonRef.textContent = "Copy";
|
|
121
|
+
}, 1500);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Inside the header when there is one; floating over the block otherwise.
|
|
126
|
+
const attachCopyButton = () => {
|
|
127
|
+
if (headerRef) {
|
|
128
|
+
copyButtonRef.style.position = "";
|
|
129
|
+
copyButtonRef.style.top = "";
|
|
130
|
+
copyButtonRef.style.right = "";
|
|
131
|
+
copyButtonRef.style.zIndex = "";
|
|
132
|
+
headerRef.appendChild(copyButtonRef);
|
|
133
|
+
} else {
|
|
134
|
+
copyButtonRef.style.position = "absolute";
|
|
135
|
+
copyButtonRef.style.top = toREM(8);
|
|
136
|
+
copyButtonRef.style.right = toREM(8);
|
|
137
|
+
copyButtonRef.style.zIndex = "3";
|
|
138
|
+
container.appendChild(copyButtonRef);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
85
142
|
// Header. Built through a helper so a later `title` update can create one on a
|
|
86
143
|
// widget that was constructed without it.
|
|
87
144
|
const ensureHeader = (text) => {
|
|
88
145
|
if (!showHeader || !text) return null;
|
|
89
146
|
if (headerRef) {
|
|
90
|
-
|
|
147
|
+
headerTitleRef.textContent = text;
|
|
91
148
|
return headerRef;
|
|
92
149
|
}
|
|
93
150
|
headerRef = WidgetFactory({
|
|
94
151
|
tag: "div",
|
|
152
|
+
display: "flex",
|
|
153
|
+
alignItems: "center",
|
|
154
|
+
justifyContent: "space-between",
|
|
155
|
+
gap: 8,
|
|
95
156
|
padding: "8px 12px",
|
|
96
157
|
backgroundColor: colors.primary,
|
|
97
|
-
borderRadius: `${borderRadius}
|
|
98
|
-
|
|
158
|
+
borderRadius: `${toREM(borderRadius)} ${toREM(borderRadius)} 0 0`,
|
|
159
|
+
// The header is a primary plate; in dark mode that plate is near-white,
|
|
160
|
+
// so a literal white title disappeared into it.
|
|
161
|
+
color: colors.onPrimary,
|
|
99
162
|
fontSize: "14px",
|
|
100
163
|
fontWeight: "bold",
|
|
164
|
+
});
|
|
165
|
+
headerTitleRef = WidgetFactory({
|
|
166
|
+
tag: "span",
|
|
101
167
|
textContent: text,
|
|
168
|
+
overflow: "hidden",
|
|
169
|
+
textOverflow: "ellipsis",
|
|
170
|
+
whiteSpace: "nowrap",
|
|
102
171
|
});
|
|
172
|
+
headerRef.appendChild(headerTitleRef);
|
|
103
173
|
container.insertBefore(headerRef, container.firstChild);
|
|
174
|
+
attachCopyButton();
|
|
104
175
|
return headerRef;
|
|
105
176
|
};
|
|
106
177
|
ensureHeader(currentTitle);
|
|
@@ -110,10 +181,10 @@ code = "",
|
|
|
110
181
|
tag: "div",
|
|
111
182
|
backgroundColor,
|
|
112
183
|
borderRadius: currentTitle
|
|
113
|
-
? `0 0 ${borderRadius}
|
|
114
|
-
:
|
|
184
|
+
? `0 0 ${toREM(borderRadius)} ${toREM(borderRadius)}`
|
|
185
|
+
: toREM(borderRadius),
|
|
115
186
|
overflow: "auto",
|
|
116
|
-
maxHeight:
|
|
187
|
+
maxHeight: toREM(maxHeight),
|
|
117
188
|
overflowX: "auto",
|
|
118
189
|
overflowY: "auto",
|
|
119
190
|
});
|
|
@@ -132,22 +203,22 @@ code = "",
|
|
|
132
203
|
tag: "div",
|
|
133
204
|
backgroundColor,
|
|
134
205
|
borderRight: `1px solid ${colors.border}`,
|
|
135
|
-
padding: `${padding}
|
|
206
|
+
padding: `${toREM(padding)} 0`,
|
|
136
207
|
fontFamily: "monospace",
|
|
137
|
-
fontSize:
|
|
208
|
+
fontSize: toREM(fontSize),
|
|
138
209
|
lineHeight: "1.5",
|
|
139
210
|
textAlign: "right",
|
|
140
|
-
color:
|
|
211
|
+
color: gutterInk,
|
|
141
212
|
userSelect: "none",
|
|
142
|
-
width:
|
|
213
|
+
width: toREM(lineNumberWidth),
|
|
143
214
|
flexShrink: 0,
|
|
144
215
|
});
|
|
145
216
|
|
|
146
217
|
codeColRef = WidgetFactory({
|
|
147
218
|
tag: "div",
|
|
148
|
-
padding:
|
|
219
|
+
padding: toREM(padding),
|
|
149
220
|
fontFamily: "monospace",
|
|
150
|
-
fontSize:
|
|
221
|
+
fontSize: toREM(fontSize),
|
|
151
222
|
lineHeight: "1.5",
|
|
152
223
|
whiteSpace: "pre",
|
|
153
224
|
flex: 1,
|
|
@@ -170,9 +241,9 @@ code = "",
|
|
|
170
241
|
} else {
|
|
171
242
|
codeWrapperRef = WidgetFactory({
|
|
172
243
|
tag: "div",
|
|
173
|
-
padding:
|
|
244
|
+
padding: toREM(padding),
|
|
174
245
|
fontFamily: "monospace",
|
|
175
|
-
fontSize:
|
|
246
|
+
fontSize: toREM(fontSize),
|
|
176
247
|
whiteSpace: "pre",
|
|
177
248
|
lineHeight: "1.5",
|
|
178
249
|
});
|
|
@@ -180,16 +251,18 @@ code = "",
|
|
|
180
251
|
}
|
|
181
252
|
|
|
182
253
|
container.appendChild(scrollWrapperRef);
|
|
254
|
+
if (!headerRef) attachCopyButton();
|
|
255
|
+
container.onUnmount(() => clearTimeout(copyResetTimer));
|
|
183
256
|
|
|
184
257
|
// Fill initial content
|
|
185
258
|
const lines = currentCode.split("\n");
|
|
186
|
-
const highlightedLines = lines.map((line) => generateHighlightedHtml(line));
|
|
259
|
+
const highlightedLines = lines.map((line) => generateHighlightedHtml(line, syntaxPalette));
|
|
187
260
|
|
|
188
261
|
if (showLineNumbers) {
|
|
189
262
|
for (let i = 0; i < lines.length; i++) {
|
|
190
263
|
const numberDiv = WidgetFactory({
|
|
191
264
|
tag: "div",
|
|
192
|
-
padding: `0 ${padding / 2}
|
|
265
|
+
padding: `0 ${toREM(padding / 2)} 0 ${toREM(padding)}`,
|
|
193
266
|
whiteSpace: "pre",
|
|
194
267
|
textContent: String(startingLineNumber + i),
|
|
195
268
|
});
|