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,607 @@
1
+ /**
2
+ * ANIMATED BOX - Widget with CSS animations
3
+ * @module animations/AnimatedBox
4
+ *
5
+ * COMPLETE LIST OF ANIMATABLE PROPERTIES:
6
+ * - backgroundColor, color, borderRadius, opacity
7
+ * - transform: scale, rotate, rotateX, rotateY, translateX, translateY
8
+ * - width, height
9
+ * - marginTop/Right/Bottom/Left
10
+ * - paddingTop/Right/Bottom/Left
11
+ * - borderWidth, borderColor
12
+ * - boxShadow (offsetX, offsetY, blur, spread, color)
13
+ * - background (gradients: linear, radial, conic)
14
+ *
15
+ * ⭐ Numbers in length properties (width, height, translate, etc.)
16
+ * are automatically converted to rem (consistent with widget-factory behavior)
17
+ */
18
+
19
+ let animationCounter = 0;
20
+ const injectedKeyframes = new Set();
21
+
22
+ /**
23
+ * Converts a numeric pixel value to a rem string.
24
+ * Passes through non-numeric values unchanged.
25
+ *
26
+ * @param {number|string|undefined|null} value - The value to convert.
27
+ * @returns {string|undefined} The value expressed in rem, or undefined if input was nullish.
28
+ */
29
+ const toREM = (value) => {
30
+ if (value === undefined || value === null) return undefined;
31
+ if (typeof value === "number") return `${value / 16}rem`;
32
+ return value;
33
+ };
34
+
35
+ /**
36
+ * Effect names whose values represent CSS lengths and must be expressed in rem.
37
+ * @type {Set<string>}
38
+ */
39
+ const lengthEffects = new Set([
40
+ "width",
41
+ "height",
42
+ "translateX",
43
+ "translateY",
44
+ "marginTop",
45
+ "marginRight",
46
+ "marginBottom",
47
+ "marginLeft",
48
+ "paddingTop",
49
+ "paddingRight",
50
+ "paddingBottom",
51
+ "paddingLeft",
52
+ "borderWidth",
53
+ "borderRadius",
54
+ ]);
55
+
56
+ /**
57
+ * Effect names that are dimensionless scalars (no CSS unit appended).
58
+ * @type {Set<string>}
59
+ */
60
+ const noUnitEffects = new Set([
61
+ "scale",
62
+ "rotate",
63
+ "rotateX",
64
+ "rotateY",
65
+ "opacity",
66
+ ]);
67
+
68
+ /**
69
+ * Maps shorthand effect keys (used in the animations API) to their canonical
70
+ * CSS property names used during keyframe generation.
71
+ * @type {Object.<string, string>}
72
+ */
73
+ const effectMap = {
74
+ // Color / visual
75
+ bgColor: "backgroundColor",
76
+ textColor: "color",
77
+ rounded: "borderRadius",
78
+ // Transform
79
+ scale: "scale",
80
+ rotate: "rotate",
81
+ rotateY: "rotateY",
82
+ rotateX: "rotateX",
83
+ translateX: "translateX",
84
+ translateY: "translateY",
85
+ opacity: "opacity",
86
+ // Dimensions and spacing
87
+ width: "width",
88
+ height: "height",
89
+ marginTop: "marginTop",
90
+ marginRight: "marginRight",
91
+ marginBottom: "marginBottom",
92
+ marginLeft: "marginLeft",
93
+ paddingTop: "paddingTop",
94
+ paddingRight: "paddingRight",
95
+ paddingBottom: "paddingBottom",
96
+ paddingLeft: "paddingLeft",
97
+ borderWidth: "borderWidth",
98
+ borderColor: "borderColor",
99
+ // Shadow
100
+ boxShadow: "boxShadow",
101
+ elevation: "boxShadow",
102
+ // Gradient
103
+ gradient: "background",
104
+ textGradient: "background",
105
+ backgroundGradient: "background",
106
+ };
107
+
108
+ /**
109
+ * Linearly interpolates between two CSS color values (hex, rgb, or rgba).
110
+ *
111
+ * @param {string} color1 - Starting color (hex, rgb, or rgba string).
112
+ * @param {string} color2 - Ending color (hex, rgb, or rgba string).
113
+ * @param {number} t - Interpolation factor in [0, 1].
114
+ * @returns {string} Interpolated color as an rgba() string.
115
+ */
116
+ const interpolateColor = (color1, color2, t) => {
117
+ if (color1 === color2) return color1;
118
+
119
+ const parseRgb = (color) => {
120
+ if (color.startsWith("#")) {
121
+ const r = parseInt(color.slice(1, 3), 16);
122
+ const g = parseInt(color.slice(3, 5), 16);
123
+ const b = parseInt(color.slice(5, 7), 16);
124
+ return [r, g, b, 1];
125
+ }
126
+ const match = color.match(
127
+ /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/,
128
+ );
129
+ if (match) {
130
+ const a = match[4] !== undefined ? parseFloat(match[4]) : 1;
131
+ return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), a];
132
+ }
133
+ return [0, 0, 0, 1];
134
+ };
135
+
136
+ const rgbToRgba = (r, g, b, a = 1) => `rgba(${r}, ${g}, ${b}, ${a})`;
137
+
138
+ const [r1, g1, b1, a1] = parseRgb(color1);
139
+ const [r2, g2, b2, a2] = parseRgb(color2);
140
+
141
+ const r = Math.round(r1 + (r2 - r1) * t);
142
+ const g = Math.round(g1 + (g2 - g1) * t);
143
+ const b = Math.round(b1 + (b2 - b1) * t);
144
+ const a = a1 + (a2 - a1) * t;
145
+
146
+ return rgbToRgba(r, g, b, a);
147
+ };
148
+
149
+ /**
150
+ * Interpolates between two numeric values that may carry CSS unit suffixes.
151
+ * When `useREM` is true the raw interpolated number is converted to rem.
152
+ *
153
+ * @param {number|string} from - Starting value (number or string with optional unit).
154
+ * @param {number|string} to - Ending value (number or string with optional unit).
155
+ * @param {number} t - Interpolation factor in [0, 1].
156
+ * @param {boolean} [useREM=false] - When true, result is expressed in rem (÷16).
157
+ * @returns {string|number} Interpolated value, with unit appended when applicable.
158
+ */
159
+ const interpolateNumber = (from, to, t, useREM = false) => {
160
+ let fromNum, toNum, fromUnit, toUnit;
161
+
162
+ if (typeof from === "number" && typeof to === "number") {
163
+ fromNum = from;
164
+ toNum = to;
165
+ fromUnit = "";
166
+ toUnit = "";
167
+ } else {
168
+ fromNum = parseFloat(from);
169
+ toNum = parseFloat(to);
170
+ fromUnit = from.toString().replace(fromNum.toString(), "");
171
+ toUnit = to.toString().replace(toNum.toString(), "");
172
+ }
173
+
174
+ const value = fromNum + (toNum - fromNum) * t;
175
+ if (useREM) {
176
+ // Express the interpolated pixel value in rem
177
+ return `${value / 16}rem`;
178
+ } else {
179
+ const unit = fromUnit || toUnit || "px";
180
+ return `${value}${unit}`;
181
+ }
182
+ };
183
+
184
+ /**
185
+ * Interpolates between two CSS box-shadow values, blending lengths and color.
186
+ *
187
+ * @param {string|null} shadow1 - Starting box-shadow string.
188
+ * @param {string|null} shadow2 - Ending box-shadow string.
189
+ * @param {number} t - Interpolation factor in [0, 1].
190
+ * @returns {string|null} Interpolated box-shadow string, or the non-null input if one is missing.
191
+ */
192
+ const interpolateBoxShadow = (shadow1, shadow2, t) => {
193
+ if (!shadow1 || !shadow2) return shadow1 || shadow2;
194
+ const parseShadow = (shadow) => {
195
+ const parts = shadow.trim().split(/\s+/);
196
+ let offsetX, offsetY, blur, spread, color;
197
+ let colorIndex = parts.findIndex(
198
+ (p) => isNaN(parseFloat(p)) && !p.match(/^\d+(\.\d+)?(px|%|em|rem)$/),
199
+ );
200
+ if (colorIndex === -1) colorIndex = parts.length;
201
+ offsetX = parts[0];
202
+ offsetY = parts[1];
203
+ blur = parts[2] || "0px";
204
+ spread = parts[3] || "0px";
205
+ color = parts.slice(colorIndex).join(" ");
206
+ return { offsetX, offsetY, blur, spread, color };
207
+ };
208
+ const s1 = parseShadow(shadow1);
209
+ const s2 = parseShadow(shadow2);
210
+ // box-shadow lengths keep their original units (not converted to rem)
211
+ const interpolateLength = (len1, len2, t) =>
212
+ interpolateNumber(len1, len2, t, false);
213
+ const newOffsetX = interpolateLength(s1.offsetX, s2.offsetX, t);
214
+ const newOffsetY = interpolateLength(s1.offsetY, s2.offsetY, t);
215
+ const newBlur = interpolateLength(s1.blur, s2.blur, t);
216
+ const newSpread = interpolateLength(s1.spread, s2.spread, t);
217
+ const newColor = interpolateColor(s1.color, s2.color, t);
218
+ return `${newOffsetX} ${newOffsetY} ${newBlur} ${newSpread} ${newColor}`;
219
+ };
220
+
221
+ /**
222
+ * Interpolates between two CSS gradient values (linear, radial, or conic).
223
+ * Blends stop colors and, for linear gradients, the angle.
224
+ *
225
+ * @param {string|null} gradient1 - Starting gradient CSS string.
226
+ * @param {string|null} gradient2 - Ending gradient CSS string.
227
+ * @param {number} t - Interpolation factor in [0, 1].
228
+ * @returns {string|null} Interpolated gradient string, or the non-null input if one is missing.
229
+ */
230
+ const interpolateGradient = (gradient1, gradient2, t) => {
231
+ if (!gradient1 || !gradient2) return gradient1 || gradient2;
232
+
233
+ const linearMatch = (grad) => grad.match(/linear-gradient\((.*?)\)/i);
234
+ const radialMatch = (grad) => grad.match(/radial-gradient\((.*?)\)/i);
235
+ const conicMatch = (grad) => grad.match(/conic-gradient\((.*?)\)/i);
236
+
237
+ let type = "linear";
238
+ let params1 = "",
239
+ params2 = "";
240
+
241
+ if (linearMatch(gradient1)) {
242
+ type = "linear";
243
+ params1 = linearMatch(gradient1)[1];
244
+ params2 = linearMatch(gradient2)[1];
245
+ } else if (radialMatch(gradient1)) {
246
+ type = "radial";
247
+ params1 = radialMatch(gradient1)[1];
248
+ params2 = radialMatch(gradient2)[1];
249
+ } else if (conicMatch(gradient1)) {
250
+ type = "conic";
251
+ params1 = conicMatch(gradient1)[1];
252
+ params2 = conicMatch(gradient2)[1];
253
+ } else {
254
+ return gradient1;
255
+ }
256
+
257
+ const parseStops = (paramStr) => {
258
+ let angleOrPosition = "";
259
+ let stopsPart = paramStr;
260
+
261
+ const angleMatch = paramStr.match(/^([\d.]+deg)\s*,/);
262
+ const positionMatch = paramStr.match(/^(circle|ellipse|at\s+[^,]+)\s*,/i);
263
+ if (angleMatch) {
264
+ angleOrPosition = angleMatch[1];
265
+ stopsPart = paramStr.substring(angleMatch[0].length);
266
+ } else if (positionMatch) {
267
+ angleOrPosition = positionMatch[0].replace(/,$/, "");
268
+ stopsPart = paramStr.substring(positionMatch[0].length);
269
+ }
270
+
271
+ const stopRegex = /([^,]+?)(\s+[\d.]+%?)?(?=,|$)/g;
272
+ let stops = [];
273
+ let match;
274
+ while ((match = stopRegex.exec(stopsPart)) !== null) {
275
+ let color = match[1].trim();
276
+ let position = match[2] ? match[2].trim() : null;
277
+ stops.push({ color, position });
278
+ }
279
+ return { angleOrPosition, stops };
280
+ };
281
+
282
+ const p1 = parseStops(params1);
283
+ const p2 = parseStops(params2);
284
+
285
+ let newAngleOrPosition = p1.angleOrPosition;
286
+ if (p1.angleOrPosition && p2.angleOrPosition) {
287
+ if (p1.angleOrPosition.includes("deg")) {
288
+ const deg1 = parseFloat(p1.angleOrPosition);
289
+ const deg2 = parseFloat(p2.angleOrPosition);
290
+ const newDeg = deg1 + (deg2 - deg1) * t;
291
+ newAngleOrPosition = `${newDeg}deg`;
292
+ } else {
293
+ newAngleOrPosition = p1.angleOrPosition;
294
+ }
295
+ }
296
+
297
+ const maxStops = Math.max(p1.stops.length, p2.stops.length);
298
+ const stops = [];
299
+ for (let i = 0; i < maxStops; i++) {
300
+ const stop1 = p1.stops[i] || p1.stops[p1.stops.length - 1];
301
+ const stop2 = p2.stops[i] || p2.stops[p2.stops.length - 1];
302
+ const color = interpolateColor(stop1.color, stop2.color, t);
303
+ let position = "";
304
+ if (stop1.position && stop2.position) {
305
+ const pos1 = parseFloat(stop1.position);
306
+ const pos2 = parseFloat(stop2.position);
307
+ const newPos = pos1 + (pos2 - pos1) * t;
308
+ position = ` ${newPos}%`;
309
+ } else if (stop1.position) {
310
+ position = ` ${stop1.position}`;
311
+ } else if (stop2.position) {
312
+ position = ` ${stop2.position}`;
313
+ }
314
+ stops.push(`${color}${position}`);
315
+ }
316
+
317
+ const stopsString = stops.join(", ");
318
+ if (type === "linear") {
319
+ return `linear-gradient(${newAngleOrPosition ? newAngleOrPosition + ", " : ""}${stopsString})`;
320
+ } else if (type === "radial") {
321
+ return `radial-gradient(${newAngleOrPosition ? newAngleOrPosition + ", " : ""}${stopsString})`;
322
+ } else if (type === "conic") {
323
+ return `conic-gradient(${newAngleOrPosition ? newAngleOrPosition + ", " : ""}${stopsString})`;
324
+ }
325
+ return gradient1;
326
+ };
327
+
328
+ /**
329
+ * Injects a named `@keyframes` rule into the document `<head>`.
330
+ * Skips injection if a rule with the same name has already been added
331
+ * (tracked via the module-level `injectedKeyframes` Set).
332
+ *
333
+ * @param {string} name - The keyframe animation name.
334
+ * @param {string} keyframes - The keyframe body (everything inside `@keyframes name { … }`).
335
+ * @returns {void}
336
+ */
337
+ const injectKeyframes = (name, keyframes) => {
338
+ if (injectedKeyframes.has(name)) return;
339
+ const style = document.createElement("style");
340
+ style.textContent = `@keyframes ${name} { ${keyframes} }`;
341
+ document.head.appendChild(style);
342
+ injectedKeyframes.add(name);
343
+ };
344
+
345
+ /**
346
+ * Generates a CSS `@keyframes` body string from an array of animation descriptors.
347
+ * Each animation describes an effect, a `from` value, a `to` value, and an optional
348
+ * `reverse` flag that controls whether the animation cycles back (sine wave) or plays
349
+ * straight through (linear interpolation).
350
+ *
351
+ * @param {Array<{effect: string, from: *, to: *, reverse?: boolean}>} animations
352
+ * Array of animation descriptors to combine into a single keyframe sequence.
353
+ * @returns {string} CSS keyframe body string (100 steps, 0%–100%).
354
+ */
355
+ const generateKeyframes = (animations) => {
356
+ const steps = 100;
357
+ let frames = [];
358
+
359
+ for (let i = 0; i <= steps; i++) {
360
+ const t = i / steps;
361
+ let transformParts = [];
362
+ let styleParts = [];
363
+
364
+ animations.forEach((anim) => {
365
+ const effect = effectMap[anim.effect] || anim.effect;
366
+ const { from, to, reverse = true } = anim;
367
+
368
+ let value;
369
+ let color;
370
+
371
+ if (reverse) {
372
+ // Sine-wave cycling: value oscillates between `from` and `to`
373
+ const angle = t * Math.PI * 2;
374
+ const mid = (from + to) / 2;
375
+ const amp = (to - from) / 2;
376
+ value = mid + amp * Math.sin(angle - Math.PI / 2);
377
+ const cycleColor = (Math.sin(angle - Math.PI / 2) + 1) / 2;
378
+
379
+ if (
380
+ effect === "backgroundColor" ||
381
+ effect === "color" ||
382
+ effect === "borderColor"
383
+ ) {
384
+ color = interpolateColor(from, to, cycleColor);
385
+ } else if (effect === "boxShadow") {
386
+ value = interpolateBoxShadow(from, to, cycleColor);
387
+ } else if (effect === "background") {
388
+ value = interpolateGradient(from, to, cycleColor);
389
+ } else {
390
+ const fromNum = parseFloat(from);
391
+ const toNum = parseFloat(to);
392
+ let cycledNum = fromNum + (toNum - fromNum) * cycleColor;
393
+
394
+ if (noUnitEffects.has(effect)) {
395
+ value = cycledNum;
396
+ } else if (lengthEffects.has(effect)) {
397
+ // Convert interpolated pixel value to rem
398
+ value = toREM(cycledNum);
399
+ } else {
400
+ const fromUnit = from.toString().replace(fromNum.toString(), "");
401
+ const toUnit = to.toString().replace(toNum.toString(), "");
402
+ const unit = fromUnit || toUnit || "px";
403
+ value = `${cycledNum}${unit}`;
404
+ }
405
+ }
406
+ } else {
407
+ // Linear forward pass: value progresses straight from `from` to `to`
408
+ if (
409
+ effect === "backgroundColor" ||
410
+ effect === "color" ||
411
+ effect === "borderColor"
412
+ ) {
413
+ color = interpolateColor(from, to, t);
414
+ } else if (effect === "boxShadow") {
415
+ value = interpolateBoxShadow(from, to, t);
416
+ } else if (effect === "background") {
417
+ value = interpolateGradient(from, to, t);
418
+ } else {
419
+ if (noUnitEffects.has(effect)) {
420
+ const fromNum = parseFloat(from);
421
+ const toNum = parseFloat(to);
422
+ value = fromNum + (toNum - fromNum) * t;
423
+ } else if (lengthEffects.has(effect)) {
424
+ // Interpolate numerically then convert to rem
425
+ const fromNum = parseFloat(from);
426
+ const toNum = parseFloat(to);
427
+ const rawValue = fromNum + (toNum - fromNum) * t;
428
+ value = toREM(rawValue);
429
+ } else {
430
+ value = interpolateNumber(from, to, t, false);
431
+ }
432
+ }
433
+ }
434
+
435
+ // Build transform() calls
436
+ if (effect === "scale") {
437
+ transformParts.push(`scale(${value})`);
438
+ } else if (effect === "rotate") {
439
+ transformParts.push(`rotate(${value}deg)`);
440
+ } else if (effect === "rotateY") {
441
+ transformParts.push(`rotateY(${value}deg)`);
442
+ } else if (effect === "rotateX") {
443
+ transformParts.push(`rotateX(${value}deg)`);
444
+ } else if (effect === "translateX" || effect === "translateY") {
445
+ // Value is already in rem when it originated from a plain number
446
+ transformParts.push(`${effect}(${value})`);
447
+ }
448
+ // Build CSS property declarations
449
+ else if (effect === "opacity") {
450
+ styleParts.push(`opacity: ${value};`);
451
+ } else if (effect === "backgroundColor") {
452
+ styleParts.push(`background-color: ${color || value};`);
453
+ } else if (effect === "color") {
454
+ styleParts.push(`color: ${color || value};`);
455
+ } else if (effect === "borderRadius") {
456
+ styleParts.push(`border-radius: ${value};`);
457
+ } else if (effect === "width") {
458
+ styleParts.push(`width: ${value};`);
459
+ } else if (effect === "height") {
460
+ styleParts.push(`height: ${value};`);
461
+ } else if (effect === "borderWidth") {
462
+ styleParts.push(`border-width: ${value};`);
463
+ } else if (effect === "borderColor") {
464
+ styleParts.push(`border-color: ${color || value};`);
465
+ } else if (effect.startsWith("margin")) {
466
+ const cssProp = effect.replace(/([A-Z])/g, "-$1").toLowerCase();
467
+ styleParts.push(`${cssProp}: ${value};`);
468
+ } else if (effect.startsWith("padding")) {
469
+ const cssProp = effect.replace(/([A-Z])/g, "-$1").toLowerCase();
470
+ styleParts.push(`${cssProp}: ${value};`);
471
+ } else if (effect === "boxShadow") {
472
+ styleParts.push(`box-shadow: ${value};`);
473
+ } else if (effect === "background") {
474
+ styleParts.push(`background: ${value};`);
475
+ }
476
+ });
477
+
478
+ const transformStyle = transformParts.length
479
+ ? `transform: ${transformParts.join(" ")};`
480
+ : "";
481
+ frames[i] = `${i}% { ${transformStyle} ${styleParts.join(" ")} }`;
482
+ }
483
+
484
+ return frames.join("\n");
485
+ };
486
+
487
+ /**
488
+ * Wraps an existing DOM element with a generated CSS keyframe animation.
489
+ *
490
+ * Accepts an array of animation descriptors, generates a unique `@keyframes`
491
+ * rule, injects it into the document, then applies it to the child element via
492
+ * `element.style.animation`. Positional overrides (`top`, `right`, `bottom`,
493
+ * `left`) are forwarded to the child's `dataset` for layout use.
494
+ *
495
+ * @param {object} props
496
+ * @param {Array<object>} [props.animations] - Array of animation descriptors. Each entry
497
+ * has at minimum `{ effect, from, to }` and may include `duration`, `delay`, `loop`, `reverse`.
498
+ * @param {string} [props.timing="ease"] - CSS `animation-timing-function`.
499
+ * @param {string} [props.delay="0s"] - Default animation delay (CSS time string).
500
+ * @param {string} [props.fillMode="forwards"] - CSS `animation-fill-mode`.
501
+ * @param {HTMLElement|null} [props.child] - The DOM element to animate.
502
+ * @param {number|string} [props.top] - CSS top offset forwarded to child dataset.
503
+ * @param {number|string} [props.right] - CSS right offset forwarded to child dataset.
504
+ * @param {number|string} [props.bottom] - CSS bottom offset forwarded to child dataset.
505
+ * @param {number|string} [props.left] - CSS left offset forwarded to child dataset.
506
+ * @returns {HTMLElement|null} The mutated `child` element (with animation applied), or `null`
507
+ * if no `child` was provided.
508
+ */
509
+ export const AnimatedBox = ({
510
+ animations,
511
+ timing = "ease",
512
+ delay = "0s",
513
+ fillMode = "forwards",
514
+ child,
515
+ top,
516
+ right,
517
+ bottom,
518
+ left,
519
+ }) => {
520
+ if (!child) return null;
521
+
522
+ // Forward position values to child dataset for use by layout systems
523
+ if (top) child.dataset.top = typeof top === "number" ? `${top}px` : top;
524
+ if (right)
525
+ child.dataset.right = typeof right === "number" ? `${right}px` : right;
526
+ if (bottom)
527
+ child.dataset.bottom = typeof bottom === "number" ? `${bottom}px` : bottom;
528
+ if (left) child.dataset.left = typeof left === "number" ? `${left}px` : left;
529
+
530
+ if (animations && animations.length > 0) {
531
+ const firstAnim = animations[0];
532
+ const effect = effectMap[firstAnim.effect] || firstAnim.effect;
533
+
534
+ if (firstAnim.effect === "textGradient") {
535
+ // Apply gradient-clip styles to the child (typically a Text widget)
536
+ child.style.color = "transparent";
537
+ child.style.backgroundClip = "text";
538
+ child.style.webkitBackgroundClip = "text";
539
+ child.style.backgroundColor = "transparent";
540
+
541
+ // Apply the starting gradient immediately so there is no flash on first render
542
+ const gradientValue = firstAnim.from;
543
+ if (gradientValue) {
544
+ child.style.background = gradientValue;
545
+ }
546
+ }
547
+
548
+ /**
549
+ * Applies an initial inline style to the child, converting numeric length
550
+ * values to rem before assignment.
551
+ *
552
+ * @param {string} prop - camelCase CSS property name.
553
+ * @param {number|string} value - Value to set (numbers are rem-converted for length effects).
554
+ * @returns {void}
555
+ */
556
+ const setInitialStyle = (prop, value) => {
557
+ if (value === undefined) return;
558
+ let finalValue = value;
559
+ if (typeof value === "number" && lengthEffects.has(effect)) {
560
+ finalValue = toREM(value);
561
+ }
562
+ child.style[prop] = finalValue;
563
+ };
564
+
565
+ // Set the starting (from) value on the child so there is no layout jump
566
+ if (effect === "backgroundColor") {
567
+ child.style.backgroundColor = firstAnim.from;
568
+ } else if (effect === "borderRadius") {
569
+ setInitialStyle("borderRadius", firstAnim.from);
570
+ } else if (effect === "color") {
571
+ child.style.color = firstAnim.from;
572
+ } else if (effect === "width") {
573
+ setInitialStyle("width", firstAnim.from);
574
+ } else if (effect === "height") {
575
+ setInitialStyle("height", firstAnim.from);
576
+ } else if (effect.startsWith("margin")) {
577
+ const prop = effect.replace(/([A-Z])/g, "-$1").toLowerCase();
578
+ setInitialStyle(prop, firstAnim.from);
579
+ } else if (effect.startsWith("padding")) {
580
+ const prop = effect.replace(/([A-Z])/g, "-$1").toLowerCase();
581
+ setInitialStyle(prop, firstAnim.from);
582
+ } else if (effect === "borderWidth") {
583
+ setInitialStyle("borderWidth", firstAnim.from);
584
+ } else if (effect === "borderColor") {
585
+ child.style.borderColor = firstAnim.from;
586
+ } else if (effect === "boxShadow") {
587
+ child.style.boxShadow = firstAnim.from;
588
+ } else if (effect === "background") {
589
+ child.style.background = firstAnim.from;
590
+ }
591
+
592
+ const name = `anim-${Date.now()}-${animationCounter++}`;
593
+ const keyframes = generateKeyframes(animations);
594
+ injectKeyframes(name, keyframes);
595
+
596
+ const duration = animations[0]?.duration || 500;
597
+ const iteration = animations[0]?.loop ? "infinite" : "1";
598
+ // Per-animation delay takes precedence over the prop-level default
599
+ const animationDelay = animations[0]?.delay || delay;
600
+
601
+ child.style.animation = `${name} ${duration}ms ${timing} ${animationDelay} ${iteration} normal ${fillMode}`;
602
+ }
603
+
604
+ return child;
605
+ };
606
+
607
+ export default AnimatedBox;