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.
Files changed (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -2
  3. package/bin/cli.js +394 -0
  4. package/bin/commands/buildAndroid.js +200 -0
  5. package/bin/commands/buildIOS.js +237 -0
  6. package/bin/commands/create.js +406 -0
  7. package/bin/commands/createBundle.js +375 -0
  8. package/bin/commands/createComponent.js +115 -0
  9. package/bin/commands/createScreen.js +161 -0
  10. package/bin/commands/killServer.js +107 -0
  11. package/bin/commands/package.js +131 -0
  12. package/bin/commands/release.js +189 -0
  13. package/bin/commands/runBundle.js +62 -0
  14. package/bin/commands/runCloudflare.js +89 -0
  15. package/bin/commands/runNative.js +109 -0
  16. package/bin/commands/runServer.js +390 -0
  17. package/bin/ota/loader.js +361 -0
  18. package/bin/utils/bundleConfig.js +178 -0
  19. package/bin/utils/capacitor.js +137 -0
  20. package/bin/utils/colors.js +215 -0
  21. package/bin/utils/helpers.js +80 -0
  22. package/bin/utils/spinner.js +96 -0
  23. package/bin/utils/templates.js +1824 -0
  24. package/bin/utils/version.js +108 -0
  25. package/package.json +58 -4
  26. package/src/animations/AnimatedBox.js +607 -0
  27. package/src/animations/AnimatedText.js +105 -0
  28. package/src/animations/MatrixRain.js +220 -0
  29. package/src/animations/ParallaxBox.js +369 -0
  30. package/src/animations/animate.js +332 -0
  31. package/src/animations/index.js +25 -0
  32. package/src/app.js +41 -0
  33. package/src/assets/icon-1024-maskable.png +0 -0
  34. package/src/assets/icon-192-maskable.png +0 -0
  35. package/src/assets/icon-192.png +0 -0
  36. package/src/assets/icon-512-maskable.png +0 -0
  37. package/src/assets/icon-512.png +0 -0
  38. package/src/assets/logo.png +0 -0
  39. package/src/core/App.js +44 -0
  40. package/src/core/hmr-client.js +142 -0
  41. package/src/core/index.js +34 -0
  42. package/src/core/pwa.js +100 -0
  43. package/src/core/runApp.js +430 -0
  44. package/src/create-index.sh +237 -0
  45. package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
  46. package/src/fonts/icons.css +23 -0
  47. package/src/index.d.ts +1930 -0
  48. package/src/index.js +288 -0
  49. package/src/navigations/AdaptiveScaffold.js +149 -0
  50. package/src/navigations/AppBar.js +331 -0
  51. package/src/navigations/BottomNavigation.js +298 -0
  52. package/src/navigations/CollapsibleSideBar.js +139 -0
  53. package/src/navigations/Drawer.js +333 -0
  54. package/src/navigations/DrawerItem.js +260 -0
  55. package/src/navigations/Router.js +475 -0
  56. package/src/navigations/Scaffold.js +537 -0
  57. package/src/navigations/Tabs.js +384 -0
  58. package/src/navigations/index.js +57 -0
  59. package/src/services/RamStore.js +213 -0
  60. package/src/services/Session.js +310 -0
  61. package/src/services/Storage.js +309 -0
  62. package/src/services/http.js +350 -0
  63. package/src/services/index.js +57 -0
  64. package/src/tools/animation.js +42 -0
  65. package/src/tools/array.js +95 -0
  66. package/src/tools/border.js +28 -0
  67. package/src/tools/clipboard.js +104 -0
  68. package/src/tools/color.js +329 -0
  69. package/src/tools/createList.js +215 -0
  70. package/src/tools/delay.js +55 -0
  71. package/src/tools/device.js +138 -0
  72. package/src/tools/dict.js +297 -0
  73. package/src/tools/dimensions.js +118 -0
  74. package/src/tools/filter.js +41 -0
  75. package/src/tools/flex.js +26 -0
  76. package/src/tools/gradient.js +34 -0
  77. package/src/tools/grid.js +57 -0
  78. package/src/tools/index.js +38 -0
  79. package/src/tools/mapList.js +107 -0
  80. package/src/tools/margin.js +93 -0
  81. package/src/tools/memo.js +60 -0
  82. package/src/tools/os.js +104 -0
  83. package/src/tools/padding.js +93 -0
  84. package/src/tools/print.js +69 -0
  85. package/src/tools/random.js +299 -0
  86. package/src/tools/ref.js +70 -0
  87. package/src/tools/rgba.js +4 -0
  88. package/src/tools/shadow.js +23 -0
  89. package/src/tools/string.js +85 -0
  90. package/src/tools/time.js +674 -0
  91. package/src/tools/transform.js +68 -0
  92. package/src/tools/transition.js +48 -0
  93. package/src/tools/useState.js +268 -0
  94. package/src/tools/uuid.js +42 -0
  95. package/src/utils/TextInputValidator.js +256 -0
  96. package/src/utils/composeUpdate.js +65 -0
  97. package/src/utils/getWidgetProps.js +35 -0
  98. package/src/utils/index.js +76 -0
  99. package/src/utils/markdownParser.js +368 -0
  100. package/src/utils/mediaTime.js +79 -0
  101. package/src/utils/navigation.js +263 -0
  102. package/src/utils/scriptInjector.js +73 -0
  103. package/src/utils/stackPosition.js +31 -0
  104. package/src/utils/stopWebRefresh.js +77 -0
  105. package/src/utils/styleInjector.js +38 -0
  106. package/src/utils/syntaxHighlight.js +339 -0
  107. package/src/utils/themes.js +347 -0
  108. package/src/utils/units.js +87 -0
  109. package/src/utils/visualEffects.js +169 -0
  110. package/src/widget-factory/addChildren.js +114 -0
  111. package/src/widget-factory/assignProps.js +256 -0
  112. package/src/widget-factory/createWidget.js +28 -0
  113. package/src/widget-factory/effects.js +132 -0
  114. package/src/widget-factory/index.js +7 -0
  115. package/src/widget-factory/lifecycle.js +210 -0
  116. package/src/widget-factory/makeParentable.js +89 -0
  117. package/src/widget-factory/pixTool.js +40 -0
  118. package/src/widget-factory/processProps.js +122 -0
  119. package/src/widget-factory/reactivity.js +98 -0
  120. package/src/widget-factory/remTool.js +34 -0
  121. package/src/widget-factory/stackPosition.js +87 -0
  122. package/src/widget-factory/stringTool.js +26 -0
  123. package/src/widget-factory/tools.js +249 -0
  124. package/src/widget-factory/translateProps.js +1049 -0
  125. package/src/widget-factory/warnings.js +284 -0
  126. package/src/widget-factory/widgetFactory.js +200 -0
  127. package/src/widgets/Accordion.js +562 -0
  128. package/src/widgets/AdMob.js +315 -0
  129. package/src/widgets/AdSense.js +268 -0
  130. package/src/widgets/AlertDialog.js +162 -0
  131. package/src/widgets/Audio.js +311 -0
  132. package/src/widgets/Avatar.js +210 -0
  133. package/src/widgets/Badge.js +189 -0
  134. package/src/widgets/BottomSheet.js +287 -0
  135. package/src/widgets/Button.js +247 -0
  136. package/src/widgets/Card.js +33 -0
  137. package/src/widgets/Carousel.js +301 -0
  138. package/src/widgets/Chart.js +632 -0
  139. package/src/widgets/Checkbox.js +106 -0
  140. package/src/widgets/Chip.js +153 -0
  141. package/src/widgets/CircularBar.js +352 -0
  142. package/src/widgets/CircularChart.js +549 -0
  143. package/src/widgets/CodeViewer.js +236 -0
  144. package/src/widgets/Column.js +29 -0
  145. package/src/widgets/Container.js +31 -0
  146. package/src/widgets/DataTable.js +257 -0
  147. package/src/widgets/Divider.js +93 -0
  148. package/src/widgets/DraggBox.js +115 -0
  149. package/src/widgets/Dropdown.js +505 -0
  150. package/src/widgets/DroppBox.js +180 -0
  151. package/src/widgets/FloatingActionButton.js +91 -0
  152. package/src/widgets/GridView.js +20 -0
  153. package/src/widgets/Icon.js +33 -0
  154. package/src/widgets/Image.js +15 -0
  155. package/src/widgets/Input.js +565 -0
  156. package/src/widgets/Inspector.js +254 -0
  157. package/src/widgets/InstallButton.js +110 -0
  158. package/src/widgets/ListTile.js +224 -0
  159. package/src/widgets/ListView.js +503 -0
  160. package/src/widgets/Markdown.js +1236 -0
  161. package/src/widgets/Modal.js +496 -0
  162. package/src/widgets/Pagination.js +195 -0
  163. package/src/widgets/ProgressBar.js +178 -0
  164. package/src/widgets/QRCode.js +467 -0
  165. package/src/widgets/Radio.js +132 -0
  166. package/src/widgets/Rating.js +261 -0
  167. package/src/widgets/Row.js +29 -0
  168. package/src/widgets/Skeleton.js +243 -0
  169. package/src/widgets/Slider.js +527 -0
  170. package/src/widgets/SnackBar.js +227 -0
  171. package/src/widgets/Stack.js +29 -0
  172. package/src/widgets/Stepper.js +296 -0
  173. package/src/widgets/Switch.js +102 -0
  174. package/src/widgets/Text.js +98 -0
  175. package/src/widgets/Tooltip.js +267 -0
  176. package/src/widgets/TreeView.js +370 -0
  177. package/src/widgets/Video.js +92 -0
  178. 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;