flet-box 0.0.0-stage → 1.0.2
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/LICENSE +21 -0
- package/README.md +190 -2
- package/bin/cli.js +394 -0
- package/bin/commands/buildAndroid.js +200 -0
- package/bin/commands/buildIOS.js +237 -0
- package/bin/commands/create.js +406 -0
- package/bin/commands/createBundle.js +375 -0
- package/bin/commands/createComponent.js +115 -0
- package/bin/commands/createScreen.js +161 -0
- package/bin/commands/killServer.js +107 -0
- package/bin/commands/package.js +131 -0
- package/bin/commands/release.js +189 -0
- package/bin/commands/runBundle.js +62 -0
- package/bin/commands/runCloudflare.js +89 -0
- package/bin/commands/runNative.js +109 -0
- package/bin/commands/runServer.js +390 -0
- package/bin/ota/loader.js +361 -0
- package/bin/utils/bundleConfig.js +178 -0
- package/bin/utils/capacitor.js +137 -0
- package/bin/utils/colors.js +215 -0
- package/bin/utils/helpers.js +80 -0
- package/bin/utils/spinner.js +96 -0
- package/bin/utils/templates.js +1824 -0
- package/bin/utils/version.js +108 -0
- package/package.json +58 -4
- package/src/animations/AnimatedBox.js +607 -0
- package/src/animations/AnimatedText.js +105 -0
- package/src/animations/MatrixRain.js +220 -0
- package/src/animations/ParallaxBox.js +369 -0
- package/src/animations/animate.js +332 -0
- package/src/animations/index.js +25 -0
- package/src/app.js +41 -0
- package/src/assets/icon-1024-maskable.png +0 -0
- package/src/assets/icon-192-maskable.png +0 -0
- package/src/assets/icon-192.png +0 -0
- package/src/assets/icon-512-maskable.png +0 -0
- package/src/assets/icon-512.png +0 -0
- package/src/assets/logo.png +0 -0
- package/src/core/App.js +44 -0
- package/src/core/hmr-client.js +142 -0
- package/src/core/index.js +34 -0
- package/src/core/pwa.js +100 -0
- package/src/core/runApp.js +430 -0
- package/src/create-index.sh +237 -0
- package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
- package/src/fonts/icons.css +23 -0
- package/src/index.d.ts +1930 -0
- package/src/index.js +288 -0
- package/src/navigations/AdaptiveScaffold.js +149 -0
- package/src/navigations/AppBar.js +331 -0
- package/src/navigations/BottomNavigation.js +298 -0
- package/src/navigations/CollapsibleSideBar.js +139 -0
- package/src/navigations/Drawer.js +333 -0
- package/src/navigations/DrawerItem.js +260 -0
- package/src/navigations/Router.js +475 -0
- package/src/navigations/Scaffold.js +537 -0
- package/src/navigations/Tabs.js +384 -0
- package/src/navigations/index.js +57 -0
- package/src/services/RamStore.js +213 -0
- package/src/services/Session.js +310 -0
- package/src/services/Storage.js +309 -0
- package/src/services/http.js +350 -0
- package/src/services/index.js +57 -0
- package/src/tools/animation.js +42 -0
- package/src/tools/array.js +95 -0
- package/src/tools/border.js +28 -0
- package/src/tools/clipboard.js +104 -0
- package/src/tools/color.js +329 -0
- package/src/tools/createList.js +215 -0
- package/src/tools/delay.js +55 -0
- package/src/tools/device.js +138 -0
- package/src/tools/dict.js +297 -0
- package/src/tools/dimensions.js +118 -0
- package/src/tools/filter.js +41 -0
- package/src/tools/flex.js +26 -0
- package/src/tools/gradient.js +34 -0
- package/src/tools/grid.js +57 -0
- package/src/tools/index.js +38 -0
- package/src/tools/mapList.js +107 -0
- package/src/tools/margin.js +93 -0
- package/src/tools/memo.js +60 -0
- package/src/tools/os.js +104 -0
- package/src/tools/padding.js +93 -0
- package/src/tools/print.js +69 -0
- package/src/tools/random.js +299 -0
- package/src/tools/ref.js +70 -0
- package/src/tools/rgba.js +4 -0
- package/src/tools/shadow.js +23 -0
- package/src/tools/string.js +85 -0
- package/src/tools/time.js +674 -0
- package/src/tools/transform.js +68 -0
- package/src/tools/transition.js +48 -0
- package/src/tools/useState.js +268 -0
- package/src/tools/uuid.js +42 -0
- package/src/utils/TextInputValidator.js +256 -0
- package/src/utils/composeUpdate.js +65 -0
- package/src/utils/getWidgetProps.js +35 -0
- package/src/utils/index.js +76 -0
- package/src/utils/markdownParser.js +368 -0
- package/src/utils/mediaTime.js +79 -0
- package/src/utils/navigation.js +263 -0
- package/src/utils/scriptInjector.js +73 -0
- package/src/utils/stackPosition.js +31 -0
- package/src/utils/stopWebRefresh.js +77 -0
- package/src/utils/styleInjector.js +38 -0
- package/src/utils/syntaxHighlight.js +339 -0
- package/src/utils/themes.js +347 -0
- package/src/utils/units.js +87 -0
- package/src/utils/visualEffects.js +169 -0
- package/src/widget-factory/addChildren.js +114 -0
- package/src/widget-factory/assignProps.js +256 -0
- package/src/widget-factory/createWidget.js +28 -0
- package/src/widget-factory/effects.js +132 -0
- package/src/widget-factory/index.js +7 -0
- package/src/widget-factory/lifecycle.js +210 -0
- package/src/widget-factory/makeParentable.js +89 -0
- package/src/widget-factory/pixTool.js +40 -0
- package/src/widget-factory/processProps.js +122 -0
- package/src/widget-factory/reactivity.js +98 -0
- package/src/widget-factory/remTool.js +34 -0
- package/src/widget-factory/stackPosition.js +87 -0
- package/src/widget-factory/stringTool.js +26 -0
- package/src/widget-factory/tools.js +249 -0
- package/src/widget-factory/translateProps.js +1049 -0
- package/src/widget-factory/warnings.js +284 -0
- package/src/widget-factory/widgetFactory.js +200 -0
- package/src/widgets/Accordion.js +562 -0
- package/src/widgets/AdMob.js +315 -0
- package/src/widgets/AdSense.js +268 -0
- package/src/widgets/AlertDialog.js +162 -0
- package/src/widgets/Audio.js +311 -0
- package/src/widgets/Avatar.js +210 -0
- package/src/widgets/Badge.js +189 -0
- package/src/widgets/BottomSheet.js +287 -0
- package/src/widgets/Button.js +247 -0
- package/src/widgets/Card.js +33 -0
- package/src/widgets/Carousel.js +301 -0
- package/src/widgets/Chart.js +632 -0
- package/src/widgets/Checkbox.js +106 -0
- package/src/widgets/Chip.js +153 -0
- package/src/widgets/CircularBar.js +352 -0
- package/src/widgets/CircularChart.js +549 -0
- package/src/widgets/CodeViewer.js +236 -0
- package/src/widgets/Column.js +29 -0
- package/src/widgets/Container.js +31 -0
- package/src/widgets/DataTable.js +257 -0
- package/src/widgets/Divider.js +93 -0
- package/src/widgets/DraggBox.js +115 -0
- package/src/widgets/Dropdown.js +505 -0
- package/src/widgets/DroppBox.js +180 -0
- package/src/widgets/FloatingActionButton.js +91 -0
- package/src/widgets/GridView.js +20 -0
- package/src/widgets/Icon.js +33 -0
- package/src/widgets/Image.js +15 -0
- package/src/widgets/Input.js +565 -0
- package/src/widgets/Inspector.js +254 -0
- package/src/widgets/InstallButton.js +110 -0
- package/src/widgets/ListTile.js +224 -0
- package/src/widgets/ListView.js +503 -0
- package/src/widgets/Markdown.js +1236 -0
- package/src/widgets/Modal.js +496 -0
- package/src/widgets/Pagination.js +195 -0
- package/src/widgets/ProgressBar.js +178 -0
- package/src/widgets/QRCode.js +467 -0
- package/src/widgets/Radio.js +132 -0
- package/src/widgets/Rating.js +261 -0
- package/src/widgets/Row.js +29 -0
- package/src/widgets/Skeleton.js +243 -0
- package/src/widgets/Slider.js +527 -0
- package/src/widgets/SnackBar.js +227 -0
- package/src/widgets/Stack.js +29 -0
- package/src/widgets/Stepper.js +296 -0
- package/src/widgets/Switch.js +102 -0
- package/src/widgets/Text.js +98 -0
- package/src/widgets/Tooltip.js +267 -0
- package/src/widgets/TreeView.js +370 -0
- package/src/widgets/Video.js +92 -0
- package/src/widgets/index.js +52 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Checkbox.js
|
|
3
|
+
* @description Checkbox input with controlled/uncontrolled state.
|
|
4
|
+
*/
|
|
5
|
+
import { WidgetFactory } from "../widget-factory/index.js";
|
|
6
|
+
import { colors } from "../utils/themes.js";
|
|
7
|
+
|
|
8
|
+
export const Checkbox = (props) => {
|
|
9
|
+
const {
|
|
10
|
+
checked = false,
|
|
11
|
+
onCheck,
|
|
12
|
+
onPress: onPressProp,
|
|
13
|
+
disabled = false,
|
|
14
|
+
size = 20,
|
|
15
|
+
...rest
|
|
16
|
+
} = props;
|
|
17
|
+
|
|
18
|
+
/** @deprecated Use onPress instead */
|
|
19
|
+
// onCheck kept as alias for backward compatibility
|
|
20
|
+
const onPress = onPressProp || onCheck;
|
|
21
|
+
|
|
22
|
+
let internalChecked = checked;
|
|
23
|
+
let checkboxRef = null;
|
|
24
|
+
let checkMarkRef = null;
|
|
25
|
+
|
|
26
|
+
const updateUI = (newChecked) => {
|
|
27
|
+
if (checkboxRef) {
|
|
28
|
+
checkboxRef.style.backgroundColor = newChecked
|
|
29
|
+
? colors.primary
|
|
30
|
+
: "transparent";
|
|
31
|
+
checkboxRef.style.border = `2px solid ${newChecked ? colors.primary : colors.border}`;
|
|
32
|
+
}
|
|
33
|
+
if (checkMarkRef) {
|
|
34
|
+
checkMarkRef.style.display = newChecked ? "inline-block" : "none";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Createste checkmark
|
|
39
|
+
const checkMark = WidgetFactory({
|
|
40
|
+
tag: "span",
|
|
41
|
+
textContent: "✓",
|
|
42
|
+
color: "#fff",
|
|
43
|
+
fontSize: size * 0.7,
|
|
44
|
+
fontWeight: "bold",
|
|
45
|
+
display: internalChecked ? "inline-block" : "none",
|
|
46
|
+
});
|
|
47
|
+
checkMarkRef = checkMark;
|
|
48
|
+
|
|
49
|
+
// Createste main checkbox container
|
|
50
|
+
const checkbox = WidgetFactory({
|
|
51
|
+
widgetName: "Checkbox",
|
|
52
|
+
width: size,
|
|
53
|
+
height: size,
|
|
54
|
+
border: `2px solid ${disabled ? colors.border : internalChecked ? colors.primary : colors.border}`,
|
|
55
|
+
borderRadius: 6,
|
|
56
|
+
backgroundColor: internalChecked ? colors.primary : "transparent",
|
|
57
|
+
display: "flex",
|
|
58
|
+
alignItems: "center",
|
|
59
|
+
justifyContent: "center",
|
|
60
|
+
transition: "all 0.2s ease",
|
|
61
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
62
|
+
opacity: disabled ? 0.5 : 1,
|
|
63
|
+
child: checkMark,
|
|
64
|
+
...rest,
|
|
65
|
+
});
|
|
66
|
+
checkboxRef = checkbox;
|
|
67
|
+
|
|
68
|
+
// Handle click
|
|
69
|
+
if (!disabled) {
|
|
70
|
+
checkbox.onclick = (e) => {
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
const newChecked = !internalChecked;
|
|
73
|
+
internalChecked = newChecked;
|
|
74
|
+
updateUI(newChecked);
|
|
75
|
+
if (onPress) onPress(newChecked);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Hover effect
|
|
79
|
+
checkbox.addEventListener("mouseenter", () => {
|
|
80
|
+
if (!disabled && !internalChecked) {
|
|
81
|
+
checkbox.style.backgroundColor = colors.primary + "20";
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
checkbox.addEventListener("mouseleave", () => {
|
|
86
|
+
if (!disabled && !internalChecked) {
|
|
87
|
+
checkbox.style.backgroundColor = "transparent";
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Public methods
|
|
93
|
+
checkbox.setChecked = (newChecked, triggerCallback = true) => {
|
|
94
|
+
if (internalChecked !== newChecked) {
|
|
95
|
+
internalChecked = newChecked;
|
|
96
|
+
updateUI(newChecked);
|
|
97
|
+
if (triggerCallback && onPress) onPress(newChecked);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
checkbox.getChecked = () => internalChecked;
|
|
102
|
+
|
|
103
|
+
return checkbox;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default Checkbox;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Chip.js
|
|
3
|
+
* @description A compact label/tag widget, optionally adorned with a leading
|
|
4
|
+
* icon and a delete button. Supports "filled" and "outlined" variants with
|
|
5
|
+
* configurable colors, sizing, and elevation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { WidgetFactory } from "../widget-factory/index.js";
|
|
9
|
+
import { Row } from "./Row.js";
|
|
10
|
+
import { Text } from "./Text.js";
|
|
11
|
+
import { Icon } from "./Icon.js";
|
|
12
|
+
import { colors } from "../utils/themes.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a Chip widget — a small interactive label commonly used for tags,
|
|
16
|
+
* filters, or multi-select values.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} props - Configuration for the chip.
|
|
19
|
+
* @param {string} props.label - The text displayed inside the chip.
|
|
20
|
+
* @param {string} [props.icon] - Optional Material icon name rendered before the label.
|
|
21
|
+
* @param {Function} [props.onPress] - Called when the chip is clicked. When provided, the cursor becomes a pointer and a hover opacity effect is added.
|
|
22
|
+
* @param {Function} [props.onDelete] - When provided, renders a close ("×") icon on the right side of the chip. Called when that icon is clicked.
|
|
23
|
+
* @param {'filled'|'outlined'} [props.variant='filled'] - Visual style. 'filled' uses a solid background; 'outlined' uses a transparent background with a border.
|
|
24
|
+
* @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 '#fff' (filled) or `color` (outlined).
|
|
26
|
+
* @param {string} [props.borderColor] - Explicit border color override (outlined variant only). Defaults to `color`.
|
|
27
|
+
* @param {number} [props.borderRadius=32] - Corner radius in pixels. High values produce a pill shape.
|
|
28
|
+
* @param {string} [props.padding='4px 12px'] - Internal padding of the chip row.
|
|
29
|
+
* @param {number} [props.gap=4] - Gap in pixels between icon, label, and delete icon.
|
|
30
|
+
* @param {number} [props.size=12] - Font size in pixels. Also scales the icons proportionally.
|
|
31
|
+
* @param {number} [props.elevation=0] - Box-shadow depth. 0 means no shadow.
|
|
32
|
+
* @returns {HTMLElement} The chip container element.
|
|
33
|
+
*/
|
|
34
|
+
export const Chip = (props) => {
|
|
35
|
+
const {
|
|
36
|
+
label,
|
|
37
|
+
icon,
|
|
38
|
+
onPress,
|
|
39
|
+
onDelete,
|
|
40
|
+
variant = "filled",
|
|
41
|
+
color = colors.primary,
|
|
42
|
+
textColor,
|
|
43
|
+
borderColor,
|
|
44
|
+
borderRadius = 32,
|
|
45
|
+
padding = "4px 12px",
|
|
46
|
+
gap = 4,
|
|
47
|
+
size = 12,
|
|
48
|
+
elevation = 0,
|
|
49
|
+
...rest
|
|
50
|
+
} = props;
|
|
51
|
+
|
|
52
|
+
// Derive background, text, and border colors from the variant
|
|
53
|
+
let bgColor, txtColor, brdColor;
|
|
54
|
+
|
|
55
|
+
if (variant === "outlined") {
|
|
56
|
+
bgColor = "transparent";
|
|
57
|
+
txtColor = textColor || color;
|
|
58
|
+
brdColor = borderColor || color;
|
|
59
|
+
} else {
|
|
60
|
+
bgColor = color;
|
|
61
|
+
txtColor = textColor || "#fff";
|
|
62
|
+
brdColor = "transparent";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const children = [];
|
|
66
|
+
|
|
67
|
+
// Optional leading icon — inherits the chip's text color for visual consistency
|
|
68
|
+
if (icon) {
|
|
69
|
+
children.push(
|
|
70
|
+
Icon({
|
|
71
|
+
name: icon,
|
|
72
|
+
size: size,
|
|
73
|
+
color: txtColor,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
children.push(
|
|
79
|
+
Text({
|
|
80
|
+
text: label,
|
|
81
|
+
size: size,
|
|
82
|
+
color: txtColor,
|
|
83
|
+
weight: "500",
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Optional trailing delete button — stopPropagation prevents triggering onPress
|
|
88
|
+
if (onDelete) {
|
|
89
|
+
const closeIcon = Icon({
|
|
90
|
+
name: "close",
|
|
91
|
+
size: size - 2,
|
|
92
|
+
color: txtColor,
|
|
93
|
+
cursor: "pointer",
|
|
94
|
+
onclick: (e) => {
|
|
95
|
+
e.stopPropagation();
|
|
96
|
+
onDelete();
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
children.push(closeIcon);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Inner Row lays out icon + label + delete icon horizontally
|
|
103
|
+
const chipContent = Row({
|
|
104
|
+
alignItems: "center",
|
|
105
|
+
gap: gap,
|
|
106
|
+
backgroundColor: bgColor,
|
|
107
|
+
border: variant === "outlined" ? `1px solid ${brdColor}` : "none",
|
|
108
|
+
borderRadius: borderRadius,
|
|
109
|
+
padding: padding,
|
|
110
|
+
width: "fit-content",
|
|
111
|
+
minWidth: "auto",
|
|
112
|
+
boxShadow:
|
|
113
|
+
elevation > 0
|
|
114
|
+
? `0 ${elevation}px ${elevation * 2}px rgba(0,0,0,0.1)`
|
|
115
|
+
: "none",
|
|
116
|
+
children: children,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Outer wrapper element enables the onclick without styling conflicts
|
|
120
|
+
const chip = WidgetFactory({
|
|
121
|
+
widgetName: "Chip",
|
|
122
|
+
display: "inline-block",
|
|
123
|
+
cursor: onPress ? "pointer" : "default",
|
|
124
|
+
transition: "all 0.2s ease",
|
|
125
|
+
child: chipContent,
|
|
126
|
+
onclick: onPress,
|
|
127
|
+
...rest,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Hover effects that respect the chip's visual variant
|
|
131
|
+
if (onPress) {
|
|
132
|
+
chip.addEventListener("mouseenter", () => {
|
|
133
|
+
if (variant === "filled") {
|
|
134
|
+
// Slightly dim the chip to signal interactivity
|
|
135
|
+
chip.style.opacity = "0.85";
|
|
136
|
+
} else if (chipContent) {
|
|
137
|
+
// Tint the outlined chip background subtly on hover
|
|
138
|
+
chipContent.style.backgroundColor = `${color}10`;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
chip.addEventListener("mouseleave", () => {
|
|
142
|
+
if (variant === "filled") {
|
|
143
|
+
chip.style.opacity = "1";
|
|
144
|
+
} else if (chipContent) {
|
|
145
|
+
chipContent.style.backgroundColor = "transparent";
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return chip;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export default Chip;
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file CircularBar.js
|
|
3
|
+
* @description Circular/radial progress bar drawn on canvas.
|
|
4
|
+
*/
|
|
5
|
+
import { WidgetFactory } from "../widget-factory/index.js";
|
|
6
|
+
import { colors } from "../utils/themes.js";
|
|
7
|
+
|
|
8
|
+
export const CircularBar = (props) => {
|
|
9
|
+
let {
|
|
10
|
+
// === Core (existing) ===
|
|
11
|
+
value = 0,
|
|
12
|
+
max = 100,
|
|
13
|
+
size = null,
|
|
14
|
+
strokeWidth = 12,
|
|
15
|
+
color = colors.primary,
|
|
16
|
+
backgroundColor = colors.gray200,
|
|
17
|
+
showValue = true,
|
|
18
|
+
valueColor = colors.text,
|
|
19
|
+
valueSize = 24,
|
|
20
|
+
label,
|
|
21
|
+
labelColor = colors.textSecondary,
|
|
22
|
+
labelSize = 12,
|
|
23
|
+
lineCap = "round",
|
|
24
|
+
animate = true,
|
|
25
|
+
animationDuration = 1000,
|
|
26
|
+
onComplete,
|
|
27
|
+
ref,
|
|
28
|
+
...rest
|
|
29
|
+
} = props;
|
|
30
|
+
|
|
31
|
+
// === NEW PROPS (optional, non-breaking) ===
|
|
32
|
+
const {
|
|
33
|
+
// Value format
|
|
34
|
+
valueFormat = "percent", // 'percent', 'value', 'custom'
|
|
35
|
+
valueSuffix = "",
|
|
36
|
+
valuePrefix = "",
|
|
37
|
+
valueDecimals = 0,
|
|
38
|
+
customValueFormatter = null, // (currentValue, max) => string
|
|
39
|
+
|
|
40
|
+
// Gradient
|
|
41
|
+
gradient = null, // array of colores ['red', 'yellow', 'green'] or string with gradiente CSS
|
|
42
|
+
gradientAngle = 135,
|
|
43
|
+
|
|
44
|
+
// Shadow
|
|
45
|
+
shadowBlur = 0,
|
|
46
|
+
shadowColor = "rgba(0,0,0,0.3)",
|
|
47
|
+
|
|
48
|
+
// Additional visual effects
|
|
49
|
+
glow = false,
|
|
50
|
+
glowColor = null, // if not specified, uses the stroke color
|
|
51
|
+
|
|
52
|
+
// Markers (points on the perimeter)
|
|
53
|
+
markers = [], // [{ value, color, size, label }]
|
|
54
|
+
|
|
55
|
+
// Extra inner ring (for a two-layer donut)
|
|
56
|
+
innerStrokeWidth = 0,
|
|
57
|
+
innerColor = null,
|
|
58
|
+
|
|
59
|
+
// Events
|
|
60
|
+
onClick = null,
|
|
61
|
+
onHover = null,
|
|
62
|
+
|
|
63
|
+
// Additional text
|
|
64
|
+
subtitle = null,
|
|
65
|
+
subtitleColor = colors.textSecondary,
|
|
66
|
+
subtitleSize = 10,
|
|
67
|
+
|
|
68
|
+
// Tooltip on mouse hover
|
|
69
|
+
tooltip = null,
|
|
70
|
+
} = props;
|
|
71
|
+
|
|
72
|
+
// Internal variables (no changes to the core logic)
|
|
73
|
+
let canvas = null;
|
|
74
|
+
let ctx = null;
|
|
75
|
+
let animationId = null;
|
|
76
|
+
let currentValue = 0;
|
|
77
|
+
let startTime = null;
|
|
78
|
+
let resizeObserver = null;
|
|
79
|
+
let container = null;
|
|
80
|
+
let currentWidth = 0;
|
|
81
|
+
let isHovering = false;
|
|
82
|
+
|
|
83
|
+
const isFixedSize = size !== null && size !== undefined;
|
|
84
|
+
const fixedSize = isFixedSize ? size : 200;
|
|
85
|
+
|
|
86
|
+
const updateCanvasSize = () => {
|
|
87
|
+
if (!canvas) return;
|
|
88
|
+
let width;
|
|
89
|
+
if (isFixedSize) width = fixedSize;
|
|
90
|
+
else {
|
|
91
|
+
const rect = container.getBoundingClientRect();
|
|
92
|
+
width = rect.width;
|
|
93
|
+
if (width <= 0) width = 200;
|
|
94
|
+
}
|
|
95
|
+
canvas.width = width;
|
|
96
|
+
canvas.height = width;
|
|
97
|
+
currentWidth = width;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const getCenter = () => currentWidth / 2;
|
|
101
|
+
const getRadius = () => (currentWidth - strokeWidth) / 2;
|
|
102
|
+
|
|
103
|
+
const getGradient = () => {
|
|
104
|
+
if (!gradient) return color;
|
|
105
|
+
if (typeof gradient === "string") return gradient;
|
|
106
|
+
if (Array.isArray(gradient)) {
|
|
107
|
+
const grad = ctx.createLinearGradient(0, 0, currentWidth, currentWidth);
|
|
108
|
+
gradient.forEach((c, i) => {
|
|
109
|
+
grad.addColorStop(i / (gradient.length - 1), c);
|
|
110
|
+
});
|
|
111
|
+
return grad;
|
|
112
|
+
}
|
|
113
|
+
return color;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const getFormattedValue = () => {
|
|
117
|
+
if (customValueFormatter) return customValueFormatter(currentValue, max);
|
|
118
|
+
if (valueFormat === "percent") {
|
|
119
|
+
const percent =
|
|
120
|
+
Math.round((currentValue / max) * 100 * Math.pow(10, valueDecimals)) /
|
|
121
|
+
Math.pow(10, valueDecimals);
|
|
122
|
+
return `${valuePrefix}${percent}${valueSuffix}`;
|
|
123
|
+
}
|
|
124
|
+
if (valueFormat === "value") {
|
|
125
|
+
const val =
|
|
126
|
+
Math.round(currentValue * Math.pow(10, valueDecimals)) /
|
|
127
|
+
Math.pow(10, valueDecimals);
|
|
128
|
+
return `${valuePrefix}${val}${valueSuffix}`;
|
|
129
|
+
}
|
|
130
|
+
return `${currentValue}/${max}`;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const drawBackground = () => {
|
|
134
|
+
ctx.beginPath();
|
|
135
|
+
ctx.arc(getCenter(), getCenter(), getRadius(), 0, Math.PI * 2);
|
|
136
|
+
ctx.strokeStyle = backgroundColor;
|
|
137
|
+
ctx.lineWidth = strokeWidth;
|
|
138
|
+
ctx.lineCap = lineCap;
|
|
139
|
+
ctx.stroke();
|
|
140
|
+
|
|
141
|
+
// Inner ring (optional)
|
|
142
|
+
if (innerStrokeWidth > 0 && innerColor) {
|
|
143
|
+
ctx.beginPath();
|
|
144
|
+
ctx.arc(
|
|
145
|
+
getCenter(),
|
|
146
|
+
getCenter(),
|
|
147
|
+
getRadius() - innerStrokeWidth / 2,
|
|
148
|
+
0,
|
|
149
|
+
Math.PI * 2,
|
|
150
|
+
);
|
|
151
|
+
ctx.strokeStyle = innerColor;
|
|
152
|
+
ctx.lineWidth = innerStrokeWidth;
|
|
153
|
+
ctx.stroke();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const drawProgress = (angleRad) => {
|
|
158
|
+
ctx.beginPath();
|
|
159
|
+
ctx.arc(
|
|
160
|
+
getCenter(),
|
|
161
|
+
getCenter(),
|
|
162
|
+
getRadius(),
|
|
163
|
+
-Math.PI / 2,
|
|
164
|
+
-Math.PI / 2 + angleRad,
|
|
165
|
+
);
|
|
166
|
+
ctx.strokeStyle = getGradient();
|
|
167
|
+
ctx.lineWidth = strokeWidth;
|
|
168
|
+
ctx.lineCap = lineCap;
|
|
169
|
+
if (glow) {
|
|
170
|
+
ctx.shadowBlur = 12;
|
|
171
|
+
ctx.shadowColor = glowColor || color;
|
|
172
|
+
} else {
|
|
173
|
+
ctx.shadowBlur = shadowBlur;
|
|
174
|
+
ctx.shadowColor = shadowColor;
|
|
175
|
+
}
|
|
176
|
+
ctx.stroke();
|
|
177
|
+
ctx.shadowBlur = 0;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const drawMarkers = () => {
|
|
181
|
+
if (!markers.length) return;
|
|
182
|
+
// Inner circle radius (where the stroke goes)
|
|
183
|
+
const r = getRadius();
|
|
184
|
+
const center = getCenter();
|
|
185
|
+
markers.forEach((m) => {
|
|
186
|
+
const angle = (m.value / max) * Math.PI * 2 - Math.PI / 2;
|
|
187
|
+
const x = center + r * Math.cos(angle);
|
|
188
|
+
const y = center + r * Math.sin(angle);
|
|
189
|
+
ctx.beginPath();
|
|
190
|
+
ctx.arc(x, y, m.size || 4, 0, Math.PI * 2);
|
|
191
|
+
ctx.fillStyle = m.color || colors.textSecondary;
|
|
192
|
+
ctx.fill();
|
|
193
|
+
if (m.label) {
|
|
194
|
+
ctx.font = `10px system-ui`;
|
|
195
|
+
ctx.fillStyle = colors.textSecondary;
|
|
196
|
+
ctx.fillText(m.label, x + 6, y - 4);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const drawText = () => {
|
|
202
|
+
if (showValue) {
|
|
203
|
+
ctx.font = `${valueSize}px system-ui, sans-serif`;
|
|
204
|
+
ctx.fillStyle = valueColor;
|
|
205
|
+
ctx.textAlign = "center";
|
|
206
|
+
ctx.textBaseline = "middle";
|
|
207
|
+
ctx.fillText(
|
|
208
|
+
getFormattedValue(),
|
|
209
|
+
getCenter(),
|
|
210
|
+
getCenter() - (subtitle ? 8 : 0),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (label) {
|
|
214
|
+
ctx.font = `${labelSize}px system-ui, sans-serif`;
|
|
215
|
+
ctx.fillStyle = labelColor;
|
|
216
|
+
ctx.fillText(
|
|
217
|
+
label,
|
|
218
|
+
getCenter(),
|
|
219
|
+
getCenter() + valueSize / 1.5 + (subtitle ? 4 : 0),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (subtitle) {
|
|
223
|
+
ctx.font = `${subtitleSize}px system-ui, sans-serif`;
|
|
224
|
+
ctx.fillStyle = subtitleColor;
|
|
225
|
+
ctx.fillText(
|
|
226
|
+
subtitle,
|
|
227
|
+
getCenter(),
|
|
228
|
+
getCenter() + valueSize / 1.5 + (label ? 14 : 4),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const drawStatic = () => {
|
|
234
|
+
if (!ctx || !canvas) return;
|
|
235
|
+
currentValue = value;
|
|
236
|
+
const angle = (value / max) * Math.PI * 2;
|
|
237
|
+
ctx.clearRect(0, 0, currentWidth, currentWidth);
|
|
238
|
+
drawBackground();
|
|
239
|
+
drawProgress(angle);
|
|
240
|
+
drawMarkers();
|
|
241
|
+
drawText();
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const drawFrame = (timestamp) => {
|
|
245
|
+
if (!ctx || !canvas) return;
|
|
246
|
+
if (!startTime) {
|
|
247
|
+
startTime = timestamp;
|
|
248
|
+
requestAnimationFrame(drawFrame);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const elapsed = timestamp - startTime;
|
|
252
|
+
const progress = Math.min(1, elapsed / animationDuration);
|
|
253
|
+
currentValue = progress * value;
|
|
254
|
+
const angle = (currentValue / max) * Math.PI * 2;
|
|
255
|
+
ctx.clearRect(0, 0, currentWidth, currentWidth);
|
|
256
|
+
drawBackground();
|
|
257
|
+
drawProgress(angle);
|
|
258
|
+
drawMarkers();
|
|
259
|
+
drawText();
|
|
260
|
+
if (progress < 1 && animate) {
|
|
261
|
+
animationId = requestAnimationFrame(drawFrame);
|
|
262
|
+
} else {
|
|
263
|
+
currentValue = value;
|
|
264
|
+
if (onComplete) onComplete();
|
|
265
|
+
animationId = null;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const startAnimation = () => {
|
|
270
|
+
if (animationId) cancelAnimationFrame(animationId);
|
|
271
|
+
if (!ctx || !canvas) return;
|
|
272
|
+
if (animate) {
|
|
273
|
+
startTime = null;
|
|
274
|
+
currentValue = 0;
|
|
275
|
+
animationId = requestAnimationFrame(drawFrame);
|
|
276
|
+
} else drawStatic();
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const resizeCanvas = () => {
|
|
280
|
+
if (!canvas) return;
|
|
281
|
+
updateCanvasSize();
|
|
282
|
+
if (animationId) cancelAnimationFrame(animationId);
|
|
283
|
+
startAnimation();
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const setupEvents = () => {
|
|
287
|
+
if (!canvas) return;
|
|
288
|
+
if (onClick)
|
|
289
|
+
canvas.addEventListener("click", (e) =>
|
|
290
|
+
onClick({
|
|
291
|
+
value: currentValue,
|
|
292
|
+
max,
|
|
293
|
+
percent: (currentValue / max) * 100,
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
if (onHover) {
|
|
297
|
+
canvas.addEventListener("mouseenter", () => {
|
|
298
|
+
isHovering = true;
|
|
299
|
+
onHover(true, { value: currentValue, max });
|
|
300
|
+
});
|
|
301
|
+
canvas.addEventListener("mouseleave", () => {
|
|
302
|
+
isHovering = false;
|
|
303
|
+
onHover(false);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (tooltip) canvas.title = tooltip;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const initWidget = () => {
|
|
310
|
+
container = WidgetFactory({
|
|
311
|
+
tag: "div",
|
|
312
|
+
widgetName: "CircularBar",
|
|
313
|
+
display: isFixedSize ? "inline-block" : "block",
|
|
314
|
+
width: isFixedSize ? `${fixedSize}px` : "100%",
|
|
315
|
+
height: isFixedSize ? `${fixedSize}px` : "auto",
|
|
316
|
+
...rest,
|
|
317
|
+
});
|
|
318
|
+
canvas = document.createElement("canvas");
|
|
319
|
+
canvas.style.display = "block";
|
|
320
|
+
canvas.style.width = "100%";
|
|
321
|
+
canvas.style.height = "auto";
|
|
322
|
+
canvas.style.aspectRatio = "1 / 1";
|
|
323
|
+
container.appendChild(canvas);
|
|
324
|
+
ctx = canvas.getContext("2d");
|
|
325
|
+
updateCanvasSize();
|
|
326
|
+
if (!isFixedSize && typeof ResizeObserver !== "undefined") {
|
|
327
|
+
resizeObserver = new ResizeObserver(() => resizeCanvas());
|
|
328
|
+
resizeObserver.observe(container);
|
|
329
|
+
} else if (!isFixedSize) window.addEventListener("resize", resizeCanvas);
|
|
330
|
+
startAnimation();
|
|
331
|
+
setupEvents();
|
|
332
|
+
if (ref && typeof ref === "function") ref(canvas);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
initWidget();
|
|
336
|
+
|
|
337
|
+
container.onUnmount(() => {
|
|
338
|
+
if (animationId) cancelAnimationFrame(animationId);
|
|
339
|
+
if (resizeObserver) resizeObserver?.disconnect();
|
|
340
|
+
else window.removeEventListener("resize", resizeCanvas);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
container.updateValue = (newValue, newMax = null) => {
|
|
344
|
+
if (newMax !== null) max = newMax;
|
|
345
|
+
value = Math.min(newValue, max);
|
|
346
|
+
startAnimation();
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
return container;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
export default CircularBar;
|