flet-box 1.1.0 → 1.1.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/README.md +16 -12
- package/bin/commands/checkUi.js +276 -45
- package/bin/commands/create.js +8 -1
- package/bin/commands/createBundle.js +18 -6
- package/bin/commands/createScreen.js +1 -1
- package/bin/commands/runBundle.js +1 -1
- package/bin/utils/bundleConfig.js +47 -3
- package/bin/utils/colors.js +0 -8
- package/bin/utils/helpers.js +0 -5
- package/bin/utils/images/jpeg.js +2 -3
- package/bin/utils/templates.js +101 -85
- package/docs/README.md +24 -1
- package/docs/cli/README.md +51 -10
- package/docs/guides/README.md +5 -2
- package/docs/guides/accessibility.md +282 -0
- package/docs/guides/clean-ui.md +53 -8
- package/docs/guides/first-app.md +6 -7
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/mobile-and-platforms.md +51 -0
- package/docs/guides/online-updates.md +18 -6
- package/docs/guides/utilities.md +116 -4
- package/docs/guides/widget-structure.md +30 -0
- package/docs/tools/README.md +1 -1
- package/docs/tools/animation.md +134 -11
- package/docs/tools/device.md +75 -3
- package/docs/tools/text-and-time.md +116 -6
- package/docs/widget/Accordion.md +2 -0
- package/docs/widget/AnimatedBox.md +46 -20
- package/docs/widget/AnimatedText.md +13 -7
- package/docs/widget/Badge.md +1 -0
- package/docs/widget/COMMON_PROPS.md +26 -3
- package/docs/widget/Chart.md +1 -0
- package/docs/widget/Checkbox.md +7 -1
- package/docs/widget/CircularChart.md +2 -0
- package/docs/widget/Drawer.md +1 -0
- package/docs/widget/GridView.md +4 -1
- package/docs/widget/Input.md +2 -0
- package/docs/widget/InstallButton.md +1 -0
- package/docs/widget/Markdown.md +4 -0
- package/docs/widget/Rating.md +2 -0
- package/docs/widget/Slider.md +2 -0
- package/docs/widget/Stack.md +6 -5
- package/docs/widget/Stepper.md +2 -0
- package/docs/widget/Tooltip.md +3 -1
- package/package.json +3 -2
- package/skills/fletbox-design/SKILL.md +390 -14
- package/skills/fletbox-motion/SKILL.md +145 -47
- package/skills/fletbox-rules/SKILL.md +140 -1
- package/src/animations/AnimatedBox.js +321 -226
- package/src/animations/MatrixRain.js +7 -1
- package/src/animations/ParallaxBox.js +13 -10
- package/src/animations/animate.js +269 -106
- package/src/animations/color.js +91 -0
- package/src/animations/easings.js +275 -0
- package/src/animations/index.js +21 -0
- package/src/animations/motion.js +204 -0
- package/src/animations/transitions.js +327 -0
- package/src/core/runApp.js +0 -6
- package/src/helpers/boxEdges.js +69 -0
- package/src/helpers/html.js +39 -0
- package/src/index.d.ts +377 -25
- package/src/index.js +77 -0
- package/src/navigations/AdaptiveScaffold.js +6 -2
- package/src/navigations/AppBar.js +16 -7
- package/src/navigations/BottomNavigation.js +24 -1
- package/src/navigations/CollapsibleSideBar.js +0 -1
- package/src/navigations/Drawer.js +32 -1
- package/src/navigations/DrawerItem.js +1 -1
- package/src/navigations/Router.js +0 -1
- package/src/navigations/Scaffold.js +9 -3
- package/src/navigations/Tabs.js +104 -31
- package/src/services/RamStore.js +1 -2
- package/src/tools/breakpoints.js +113 -0
- package/src/tools/createList.js +0 -2
- package/src/tools/fonts.js +99 -0
- package/src/tools/index.js +74 -1
- package/src/tools/margin.js +5 -63
- package/src/tools/padding.js +5 -63
- package/src/tools/print.js +0 -8
- package/src/tools/scale.js +9 -2
- package/src/tools/time.js +56 -15
- package/src/tools/useState.js +1 -54
- package/src/utils/fieldFont.js +77 -0
- package/src/utils/focusRing.js +73 -0
- package/src/utils/focusScope.js +113 -0
- package/src/utils/markdownParser.js +7 -9
- package/src/utils/readableInk.js +7 -1
- package/src/utils/stackLayout.js +55 -0
- package/src/utils/styleInjector.js +25 -0
- package/src/utils/syntaxHighlight.js +1 -17
- package/src/utils/themes.js +15 -0
- package/src/utils/visualEffects.js +49 -47
- package/src/widget-factory/createWidget.js +16 -3
- package/src/widget-factory/effects.js +76 -3
- package/src/widget-factory/reactiveValue.js +0 -7
- package/src/widget-factory/tools.js +26 -143
- package/src/widget-factory/translateProps.js +5 -0
- package/src/widget-factory/widgetFactory.js +1 -1
- package/src/widgets/Accordion.js +70 -12
- package/src/widgets/AdMob.js +1 -1
- package/src/widgets/AdSense.js +1 -1
- package/src/widgets/AlertDialog.js +11 -9
- package/src/widgets/Avatar.js +10 -1
- package/src/widgets/Badge.js +7 -1
- package/src/widgets/BottomSheet.js +39 -5
- package/src/widgets/Chart.js +41 -9
- package/src/widgets/Checkbox.js +59 -9
- package/src/widgets/Chip.js +13 -1
- package/src/widgets/CircularBar.js +17 -5
- package/src/widgets/CircularChart.js +24 -2
- package/src/widgets/CodeViewer.js +5 -3
- package/src/widgets/Dropdown.js +223 -20
- package/src/widgets/GridView.js +17 -1
- package/src/widgets/Icon.js +21 -0
- package/src/widgets/Input.js +75 -44
- package/src/widgets/InstallButton.js +8 -2
- package/src/widgets/ListTile.js +6 -5
- package/src/widgets/ListView.js +21 -7
- package/src/widgets/Markdown.js +25 -44
- package/src/widgets/Modal.js +43 -6
- package/src/widgets/Pagination.js +35 -3
- package/src/widgets/ProgressBar.js +18 -0
- package/src/widgets/Radio.js +7 -0
- package/src/widgets/Rating.js +104 -16
- package/src/widgets/Skeleton.js +9 -5
- package/src/widgets/Slider.js +87 -7
- package/src/widgets/SnackBar.js +30 -1
- package/src/widgets/Stack.js +25 -13
- package/src/widgets/Stepper.js +112 -64
- package/src/widgets/Switch.js +10 -1
- package/src/widgets/Tooltip.js +93 -10
- package/src/widgets/TreeView.js +5 -5
- package/src/create-index.sh +0 -237
- package/src/widget-factory/pixTool.js +0 -40
- package/src/widget-factory/stringTool.js +0 -26
|
@@ -12,25 +12,18 @@
|
|
|
12
12
|
* - boxShadow (offsetX, offsetY, blur, spread, color)
|
|
13
13
|
* - background (gradients: linear, radial, conic)
|
|
14
14
|
*
|
|
15
|
+
* Each entry is `{ effect, from, to }` plus the optional `duration`, `delay`,
|
|
16
|
+
* `loop` and `reverse`, and each one is honoured: a step's own duration is how
|
|
17
|
+
* long *that* step takes, not the duration of whichever step came first.
|
|
18
|
+
*
|
|
15
19
|
* ⭐ Numbers in length properties (width, height, translate, etc.)
|
|
16
20
|
* are automatically converted to rem (consistent with widget-factory behavior)
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
};
|
|
23
|
+
import { toREM } from "../utils/units.js";
|
|
24
|
+
import { injectKeyframes, animationName } from "../utils/styleInjector.js";
|
|
25
|
+
import { mixColor } from "./color.js";
|
|
26
|
+
import { prefersReducedMotion, markMotionLoop } from "./motion.js";
|
|
34
27
|
|
|
35
28
|
/**
|
|
36
29
|
* Effect names whose values represent CSS lengths and must be expressed in rem.
|
|
@@ -41,6 +34,7 @@ const lengthEffects = new Set([
|
|
|
41
34
|
"height",
|
|
42
35
|
"translateX",
|
|
43
36
|
"translateY",
|
|
37
|
+
"translateZ",
|
|
44
38
|
"marginTop",
|
|
45
39
|
"marginRight",
|
|
46
40
|
"marginBottom",
|
|
@@ -59,15 +53,45 @@ const lengthEffects = new Set([
|
|
|
59
53
|
*/
|
|
60
54
|
const noUnitEffects = new Set([
|
|
61
55
|
"scale",
|
|
56
|
+
"scaleX",
|
|
57
|
+
"scaleY",
|
|
62
58
|
"rotate",
|
|
63
59
|
"rotateX",
|
|
64
60
|
"rotateY",
|
|
61
|
+
"rotateZ",
|
|
62
|
+
"skewX",
|
|
63
|
+
"skewY",
|
|
65
64
|
"opacity",
|
|
66
65
|
]);
|
|
67
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The effects that land inside the single `transform` declaration.
|
|
69
|
+
*
|
|
70
|
+
* Two CSS animations that both write `transform` do not compose: the one listed
|
|
71
|
+
* last wins and the other's movement disappears. So these are always generated
|
|
72
|
+
* into one rule, and the rule is the reason a group exists at all.
|
|
73
|
+
* @type {Set<string>}
|
|
74
|
+
*/
|
|
75
|
+
const transformEffects = new Set([
|
|
76
|
+
"scale",
|
|
77
|
+
"scaleX",
|
|
78
|
+
"scaleY",
|
|
79
|
+
"rotate",
|
|
80
|
+
"rotateX",
|
|
81
|
+
"rotateY",
|
|
82
|
+
"rotateZ",
|
|
83
|
+
"skewX",
|
|
84
|
+
"skewY",
|
|
85
|
+
"translateX",
|
|
86
|
+
"translateY",
|
|
87
|
+
"translateZ",
|
|
88
|
+
]);
|
|
89
|
+
|
|
68
90
|
/**
|
|
69
91
|
* Maps shorthand effect keys (used in the animations API) to their canonical
|
|
70
|
-
* CSS property names used during keyframe generation.
|
|
92
|
+
* CSS property names used during keyframe generation. The right-hand names are
|
|
93
|
+
* what the property is written as; the left-hand ones are the spellings the
|
|
94
|
+
* examples and the snippet library use.
|
|
71
95
|
* @type {Object.<string, string>}
|
|
72
96
|
*/
|
|
73
97
|
const effectMap = {
|
|
@@ -77,11 +101,17 @@ const effectMap = {
|
|
|
77
101
|
rounded: "borderRadius",
|
|
78
102
|
// Transform
|
|
79
103
|
scale: "scale",
|
|
104
|
+
scaleX: "scaleX",
|
|
105
|
+
scaleY: "scaleY",
|
|
80
106
|
rotate: "rotate",
|
|
81
107
|
rotateY: "rotateY",
|
|
82
108
|
rotateX: "rotateX",
|
|
109
|
+
rotateZ: "rotateZ",
|
|
110
|
+
skewX: "skewX",
|
|
111
|
+
skewY: "skewY",
|
|
83
112
|
translateX: "translateX",
|
|
84
113
|
translateY: "translateY",
|
|
114
|
+
translateZ: "translateZ",
|
|
85
115
|
opacity: "opacity",
|
|
86
116
|
// Dimensions and spacing
|
|
87
117
|
width: "width",
|
|
@@ -105,80 +135,24 @@ const effectMap = {
|
|
|
105
135
|
backgroundGradient: "background",
|
|
106
136
|
};
|
|
107
137
|
|
|
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
138
|
/**
|
|
150
139
|
* Interpolates between two numeric values that may carry CSS unit suffixes.
|
|
151
|
-
*
|
|
140
|
+
* A length that needs rem is converted by its own branch in
|
|
141
|
+
* {@link generateRule}, through the framework's shared `toREM`.
|
|
152
142
|
*
|
|
153
143
|
* @param {number|string} from - Starting value (number or string with optional unit).
|
|
154
144
|
* @param {number|string} to - Ending value (number or string with optional unit).
|
|
155
145
|
* @param {number} t - Interpolation factor in [0, 1].
|
|
156
|
-
* @
|
|
157
|
-
* @returns {string|number} Interpolated value, with unit appended when applicable.
|
|
146
|
+
* @returns {string} Interpolated value with the starting value's unit re-attached.
|
|
158
147
|
*/
|
|
159
|
-
const interpolateNumber = (from, to, t
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
148
|
+
const interpolateNumber = (from, to, t) => {
|
|
149
|
+
const fromNum = parseFloat(from);
|
|
150
|
+
const toNum = parseFloat(to);
|
|
151
|
+
const fromUnit = from.toString().replace(fromNum.toString(), "");
|
|
152
|
+
const toUnit = to.toString().replace(toNum.toString(), "");
|
|
174
153
|
const value = fromNum + (toNum - fromNum) * t;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return `${value / 16}rem`;
|
|
178
|
-
} else {
|
|
179
|
-
const unit = fromUnit || toUnit || "px";
|
|
180
|
-
return `${value}${unit}`;
|
|
181
|
-
}
|
|
154
|
+
const unit = fromUnit || toUnit || "px";
|
|
155
|
+
return `${value}${unit}`;
|
|
182
156
|
};
|
|
183
157
|
|
|
184
158
|
/**
|
|
@@ -208,13 +182,12 @@ const interpolateBoxShadow = (shadow1, shadow2, t) => {
|
|
|
208
182
|
const s1 = parseShadow(shadow1);
|
|
209
183
|
const s2 = parseShadow(shadow2);
|
|
210
184
|
// box-shadow lengths keep their original units (not converted to rem)
|
|
211
|
-
const interpolateLength = (len1, len2, t) =>
|
|
212
|
-
interpolateNumber(len1, len2, t, false);
|
|
185
|
+
const interpolateLength = (len1, len2, t) => interpolateNumber(len1, len2, t);
|
|
213
186
|
const newOffsetX = interpolateLength(s1.offsetX, s2.offsetX, t);
|
|
214
187
|
const newOffsetY = interpolateLength(s1.offsetY, s2.offsetY, t);
|
|
215
188
|
const newBlur = interpolateLength(s1.blur, s2.blur, t);
|
|
216
189
|
const newSpread = interpolateLength(s1.spread, s2.spread, t);
|
|
217
|
-
const newColor =
|
|
190
|
+
const newColor = mixColor(s1.color, s2.color, t);
|
|
218
191
|
return `${newOffsetX} ${newOffsetY} ${newBlur} ${newSpread} ${newColor}`;
|
|
219
192
|
};
|
|
220
193
|
|
|
@@ -299,7 +272,7 @@ const interpolateGradient = (gradient1, gradient2, t) => {
|
|
|
299
272
|
for (let i = 0; i < maxStops; i++) {
|
|
300
273
|
const stop1 = p1.stops[i] || p1.stops[p1.stops.length - 1];
|
|
301
274
|
const stop2 = p2.stops[i] || p2.stops[p2.stops.length - 1];
|
|
302
|
-
const color =
|
|
275
|
+
const color = mixColor(stop1.color, stop2.color, t);
|
|
303
276
|
let position = "";
|
|
304
277
|
if (stop1.position && stop2.position) {
|
|
305
278
|
const pos1 = parseFloat(stop1.position);
|
|
@@ -325,75 +298,164 @@ const interpolateGradient = (gradient1, gradient2, t) => {
|
|
|
325
298
|
return gradient1;
|
|
326
299
|
};
|
|
327
300
|
|
|
301
|
+
/** The duration a step that does not name one gets. */
|
|
302
|
+
const DEFAULT_DURATION = 500;
|
|
303
|
+
|
|
328
304
|
/**
|
|
329
|
-
*
|
|
330
|
-
* Skips injection if a rule with the same name has already been added
|
|
331
|
-
* (tracked via the module-level `injectedKeyframes` Set).
|
|
305
|
+
* Reads a step's time as milliseconds.
|
|
332
306
|
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
307
|
+
* A bare number is milliseconds and a unit string is parsed, because that is what
|
|
308
|
+
* the docs promise (`delay` is "in milliseconds") and what `AnimatedText` writes
|
|
309
|
+
* (`"0.1s"`). Handing the `animation` shorthand an unqualified number — `delay:
|
|
310
|
+
* 200` — makes the whole declaration invalid, and the element does not animate at
|
|
311
|
+
* all, which reads as the widget being broken rather than as a missing unit.
|
|
312
|
+
*
|
|
313
|
+
* @param {number|string|undefined|null} value
|
|
314
|
+
* @param {number} fallback - Milliseconds to use when the value is absent or nonsense.
|
|
315
|
+
* @returns {number} A finite, non-negative number of milliseconds.
|
|
336
316
|
*/
|
|
317
|
+
const cssMs = (value, fallback) => {
|
|
318
|
+
const num = (text) => {
|
|
319
|
+
const parsed = parseFloat(text);
|
|
320
|
+
if (!Number.isFinite(parsed) || parsed < 0) return null;
|
|
321
|
+
// "0.5s" is seconds, "500ms" and a bare "500" are milliseconds. The unit is
|
|
322
|
+
// read from the END of the string: a `\bs\b` test never matches "0.1s", because
|
|
323
|
+
// the digit and the `s` are both word characters, and the delay would be
|
|
324
|
+
// one thousandth of what the caller wrote.
|
|
325
|
+
if (/(?:^|[^a-z])ms\s*$/i.test(text)) return parsed;
|
|
326
|
+
if (/(?:^|[^a-z])s\s*$/i.test(text)) return parsed * 1000;
|
|
327
|
+
return parsed;
|
|
328
|
+
};
|
|
329
|
+
if (typeof value === "number")
|
|
330
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
331
|
+
if (typeof value !== "string" || value.trim() === "") return fallback;
|
|
332
|
+
return num(value.trim()) ?? fallback;
|
|
333
|
+
};
|
|
334
|
+
|
|
337
335
|
/**
|
|
338
|
-
*
|
|
339
|
-
* identical keyframes are injected only once.
|
|
336
|
+
* Formats one transform function's value the way the keyframes do.
|
|
340
337
|
*
|
|
341
|
-
* @param {string}
|
|
342
|
-
* @
|
|
338
|
+
* @param {string} effect - A canonical transform name.
|
|
339
|
+
* @param {number|string} value - A bare number is a framework unit, not a pixel.
|
|
340
|
+
* @returns {string} The value with the unit that function needs.
|
|
343
341
|
*/
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
h1 = Math.imul(h1 ^ c, 0x01000193);
|
|
350
|
-
h2 = Math.imul(h2 + c, 0x85ebca6b) ^ (h2 >>> 13);
|
|
351
|
-
}
|
|
352
|
-
return (h1 >>> 0).toString(36) + (h2 >>> 0).toString(36);
|
|
342
|
+
const transformValue = (effect, value) => {
|
|
343
|
+
if (typeof value === "string") return value;
|
|
344
|
+
if (effect.startsWith("rotate") || effect.startsWith("skew")) return `${value}deg`;
|
|
345
|
+
if (effect.startsWith("translate")) return toREM(value);
|
|
346
|
+
return String(value);
|
|
353
347
|
};
|
|
354
348
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
349
|
+
/**
|
|
350
|
+
* The order the parts are written in, so the start state and the keyframes agree
|
|
351
|
+
* with each other and with `animate()` — CSS applies transforms left to right, and
|
|
352
|
+
* translating first is what keeps a rotation about the element's own centre.
|
|
353
|
+
* @type {string[]}
|
|
354
|
+
*/
|
|
355
|
+
const TRANSFORM_ORDER = [
|
|
356
|
+
"translateX",
|
|
357
|
+
"translateY",
|
|
358
|
+
"translateZ",
|
|
359
|
+
"rotate",
|
|
360
|
+
"rotateX",
|
|
361
|
+
"rotateY",
|
|
362
|
+
"rotateZ",
|
|
363
|
+
"skewX",
|
|
364
|
+
"skewY",
|
|
365
|
+
"scale",
|
|
366
|
+
"scaleX",
|
|
367
|
+
"scaleY",
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Writes each step's `from` on the element before its rule starts playing.
|
|
372
|
+
*
|
|
373
|
+
* @param {HTMLElement} child - The element about to be animated.
|
|
374
|
+
* @param {Array<object>} steps - The resolved descriptors.
|
|
375
|
+
* @returns {void}
|
|
376
|
+
*/
|
|
377
|
+
const writeStartValues = (child, steps) => {
|
|
378
|
+
const transforms = new Map();
|
|
379
|
+
steps.forEach((anim) => {
|
|
380
|
+
const effect = effectMap[anim.effect] || anim.effect;
|
|
381
|
+
const { from } = anim;
|
|
382
|
+
if (transformEffects.has(effect)) {
|
|
383
|
+
transforms.set(effect, from);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (effect === "backgroundColor") {
|
|
387
|
+
child.style.backgroundColor = from;
|
|
388
|
+
} else if (effect === "color") {
|
|
389
|
+
child.style.color = from;
|
|
390
|
+
} else if (effect === "borderColor") {
|
|
391
|
+
child.style.borderColor = from;
|
|
392
|
+
} else if (effect === "boxShadow") {
|
|
393
|
+
child.style.boxShadow = from;
|
|
394
|
+
} else if (effect === "background") {
|
|
395
|
+
child.style.background = from;
|
|
396
|
+
} else if (effect === "opacity") {
|
|
397
|
+
child.style.opacity = from;
|
|
398
|
+
} else if (typeof from === "number" && lengthEffects.has(effect)) {
|
|
399
|
+
// A length written as a plain number is a framework unit
|
|
400
|
+
child.style[effect] = toREM(from);
|
|
401
|
+
} else {
|
|
402
|
+
child.style[effect] = from;
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
if (transforms.size === 0) return;
|
|
406
|
+
child.style.transform = TRANSFORM_ORDER.filter((name) => transforms.has(name))
|
|
407
|
+
.map((name) => `${name}(${transformValue(name, transforms.get(name))})`)
|
|
408
|
+
.join(" ");
|
|
361
409
|
};
|
|
362
410
|
|
|
363
411
|
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
412
|
+
* True when a timing group writes the `transform` declaration.
|
|
413
|
+
*
|
|
414
|
+
* @param {{members: Array<object>}} group
|
|
415
|
+
* @returns {boolean}
|
|
416
|
+
*/
|
|
417
|
+
const groupHasTransform = (group) =>
|
|
418
|
+
group.members.some((anim) =>
|
|
419
|
+
transformEffects.has(effectMap[anim.effect] || anim.effect),
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Generates a CSS `@keyframes` body for one timing group.
|
|
424
|
+
*
|
|
425
|
+
* Every step of a group is written into the same rule — that is what lets a
|
|
426
|
+
* `translateY` and a `scale` share one `transform` declaration instead of
|
|
427
|
+
* overwriting each other — but each step runs on its own clock: it waits its
|
|
428
|
+
* `offset`, takes its own `duration`, and holds `to` for the rest of the rule.
|
|
429
|
+
* A group spans the longest of its steps, so a 400 ms fade inside a 600 ms
|
|
430
|
+
* entrance is finished and holding when the last frame is written.
|
|
368
431
|
*
|
|
369
|
-
* @param {Array<{effect: string, from: *, to: *, reverse?: boolean}>}
|
|
370
|
-
*
|
|
432
|
+
* @param {Array<{effect: string, from: *, to: *, reverse?: boolean, offset?: number, duration?: number}>} entries
|
|
433
|
+
* The steps of the group, already resolved to millisecond `offset`/`duration`.
|
|
434
|
+
* @param {number} span - The group's total length in ms; the rule's duration.
|
|
371
435
|
* @returns {string} CSS keyframe body string (100 steps, 0%–100%).
|
|
372
436
|
*/
|
|
373
|
-
const
|
|
437
|
+
const generateRule = (entries, span) => {
|
|
374
438
|
const steps = 100;
|
|
375
439
|
let frames = [];
|
|
376
440
|
|
|
377
441
|
for (let i = 0; i <= steps; i++) {
|
|
378
|
-
|
|
442
|
+
// Where the group's clock is, in ms, at this frame.
|
|
443
|
+
const clock = (i / steps) * span;
|
|
379
444
|
let transformParts = [];
|
|
380
445
|
let styleParts = [];
|
|
381
446
|
|
|
382
|
-
|
|
447
|
+
entries.forEach((anim) => {
|
|
383
448
|
const effect = effectMap[anim.effect] || anim.effect;
|
|
384
449
|
// Straight `from → to` is the default: a one-shot entry animation must hold
|
|
385
450
|
// `to`. Cycling back is opt-in, or every documented `opacity 0 → 1` faded
|
|
386
451
|
// to invisible again at 100%.
|
|
387
|
-
const { from, to, reverse = false } = anim;
|
|
388
|
-
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
);
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
452
|
+
const { from, to, reverse = false, offset = 0, duration = span } = anim;
|
|
453
|
+
|
|
454
|
+
// The step's own progress through its own duration.
|
|
455
|
+
const t =
|
|
456
|
+
duration <= 0
|
|
457
|
+
? 1
|
|
458
|
+
: Math.min(1, Math.max(0, (clock - offset) / duration));
|
|
397
459
|
|
|
398
460
|
let value;
|
|
399
461
|
let color;
|
|
@@ -411,7 +473,7 @@ const generateKeyframes = (animations) => {
|
|
|
411
473
|
effect === "color" ||
|
|
412
474
|
effect === "borderColor"
|
|
413
475
|
) {
|
|
414
|
-
color =
|
|
476
|
+
color = mixColor(from, to, cycleColor);
|
|
415
477
|
} else if (effect === "boxShadow") {
|
|
416
478
|
value = interpolateBoxShadow(from, to, cycleColor);
|
|
417
479
|
} else if (effect === "background") {
|
|
@@ -440,7 +502,7 @@ const generateKeyframes = (animations) => {
|
|
|
440
502
|
effect === "color" ||
|
|
441
503
|
effect === "borderColor"
|
|
442
504
|
) {
|
|
443
|
-
color =
|
|
505
|
+
color = mixColor(from, to, t);
|
|
444
506
|
} else if (effect === "boxShadow") {
|
|
445
507
|
value = interpolateBoxShadow(from, to, t);
|
|
446
508
|
} else if (effect === "background") {
|
|
@@ -457,21 +519,30 @@ const generateKeyframes = (animations) => {
|
|
|
457
519
|
const rawValue = fromNum + (toNum - fromNum) * t;
|
|
458
520
|
value = toREM(rawValue);
|
|
459
521
|
} else {
|
|
460
|
-
value = interpolateNumber(from, to, t
|
|
522
|
+
value = interpolateNumber(from, to, t);
|
|
461
523
|
}
|
|
462
524
|
}
|
|
463
525
|
}
|
|
464
526
|
|
|
465
|
-
// Build transform() calls
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
transformParts.push(
|
|
470
|
-
} else if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
527
|
+
// Build transform() calls. They all land in one `transform` declaration, in
|
|
528
|
+
// the CSS order — travel, then turn, then size — so a rotation spins about
|
|
529
|
+
// the element's own centre instead of the page's.
|
|
530
|
+
if (effect === "scale" || effect === "scaleX" || effect === "scaleY") {
|
|
531
|
+
transformParts.push(`${effect}(${value})`);
|
|
532
|
+
} else if (
|
|
533
|
+
effect === "rotate" ||
|
|
534
|
+
effect === "rotateX" ||
|
|
535
|
+
effect === "rotateY" ||
|
|
536
|
+
effect === "rotateZ" ||
|
|
537
|
+
effect === "skewX" ||
|
|
538
|
+
effect === "skewY"
|
|
539
|
+
) {
|
|
540
|
+
transformParts.push(`${effect}(${value}deg)`);
|
|
541
|
+
} else if (
|
|
542
|
+
effect === "translateX" ||
|
|
543
|
+
effect === "translateY" ||
|
|
544
|
+
effect === "translateZ"
|
|
545
|
+
) {
|
|
475
546
|
// Value is already in rem when it originated from a plain number
|
|
476
547
|
transformParts.push(`${effect}(${value})`);
|
|
477
548
|
}
|
|
@@ -517,16 +588,18 @@ const generateKeyframes = (animations) => {
|
|
|
517
588
|
/**
|
|
518
589
|
* Wraps an existing DOM element with a generated CSS keyframe animation.
|
|
519
590
|
*
|
|
520
|
-
* Accepts an array of animation descriptors, generates
|
|
521
|
-
*
|
|
522
|
-
* `element.style.animation`. Positional overrides (`top`, `right`,
|
|
523
|
-
* `left`) are forwarded to the child's `dataset` for layout use.
|
|
591
|
+
* Accepts an array of animation descriptors, generates one `@keyframes` rule per
|
|
592
|
+
* timing group, injects it into the document, then applies the rules to the child
|
|
593
|
+
* element through `element.style.animation`. Positional overrides (`top`, `right`,
|
|
594
|
+
* `bottom`, `left`) are forwarded to the child's `dataset` for layout use.
|
|
524
595
|
*
|
|
525
596
|
* @param {object} props
|
|
526
597
|
* @param {Array<object>} [props.animations] - Array of animation descriptors. Each entry
|
|
527
|
-
* has at minimum `{ effect, from, to }` and may
|
|
598
|
+
* has at minimum `{ effect, from, to }` and may carry its own `duration` (ms), `delay`
|
|
599
|
+
* (ms or a CSS time), `loop` and `reverse` — all four are read off that entry.
|
|
528
600
|
* @param {string} [props.timing="ease"] - CSS `animation-timing-function`.
|
|
529
|
-
* @param {string}
|
|
601
|
+
* @param {number|string} [props.delay="0s"] - Default delay for the steps that
|
|
602
|
+
* do not name one: a number is milliseconds, a string is handed to CSS.
|
|
530
603
|
* @param {string} [props.fillMode="forwards"] - CSS `animation-fill-mode`.
|
|
531
604
|
* @param {HTMLElement|null} [props.child] - The DOM element to animate.
|
|
532
605
|
* @param {number|string} [props.top] - CSS top offset forwarded to child dataset.
|
|
@@ -549,91 +622,113 @@ export const AnimatedBox = ({
|
|
|
549
622
|
}) => {
|
|
550
623
|
if (!child) return null;
|
|
551
624
|
|
|
552
|
-
// Forward position values to child dataset for use by layout systems
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (
|
|
557
|
-
|
|
558
|
-
if (
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
625
|
+
// Forward position values to child dataset for use by layout systems. A bare
|
|
626
|
+
// number is a framework unit, so it is written the way every other length in
|
|
627
|
+
// the framework is written: as rem.
|
|
628
|
+
const offset = (value) => (typeof value === "number" ? toREM(value) : value);
|
|
629
|
+
if (top) child.dataset.top = offset(top);
|
|
630
|
+
if (right) child.dataset.right = offset(right);
|
|
631
|
+
if (bottom) child.dataset.bottom = offset(bottom);
|
|
632
|
+
if (left) child.dataset.left = offset(left);
|
|
633
|
+
|
|
634
|
+
if (!animations || animations.length === 0) return child;
|
|
635
|
+
|
|
636
|
+
// Each step's own `duration` and `delay`, read off that step. Taking whichever
|
|
637
|
+
// descriptor came first is what made the documented
|
|
638
|
+
// `{ scale, duration: 600 } + { opacity, duration: 400 }` entrance run both
|
|
639
|
+
// steps for 600 ms, and what made `AnimatedText`'s per-letter delay the only
|
|
640
|
+
// delay anyone could set.
|
|
641
|
+
const defaultDelay = cssMs(delay, 0);
|
|
642
|
+
const steps = [];
|
|
643
|
+
for (const anim of animations) {
|
|
644
|
+
if (!anim || anim.from === undefined || anim.to === undefined) {
|
|
645
|
+
// Malformed descriptor: skip it with a warning — one, not one per keyframe
|
|
646
|
+
// step — rather than throwing a TypeError out of the caller's whole render.
|
|
647
|
+
console.warn(
|
|
648
|
+
`⚠️ [AnimatedBox] Skipping animation "${anim?.effect}": "from" and "to" are required.`,
|
|
649
|
+
);
|
|
650
|
+
continue;
|
|
576
651
|
}
|
|
652
|
+
steps.push({
|
|
653
|
+
...anim,
|
|
654
|
+
duration: cssMs(anim.duration, DEFAULT_DURATION),
|
|
655
|
+
delayMs: cssMs(anim.delay, defaultDelay),
|
|
656
|
+
loop: Boolean(anim.loop),
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
if (steps.length === 0) return child;
|
|
660
|
+
|
|
661
|
+
const gradient = steps.find((anim) => anim.effect === "textGradient");
|
|
662
|
+
if (gradient) {
|
|
663
|
+
// Apply gradient-clip styles to the child (typically a Text widget). The
|
|
664
|
+
// starting gradient arrives below, with every other step's `from`.
|
|
665
|
+
child.style.color = "transparent";
|
|
666
|
+
child.style.backgroundClip = "text";
|
|
667
|
+
child.style.webkitBackgroundClip = "text";
|
|
668
|
+
child.style.backgroundColor = "transparent";
|
|
669
|
+
}
|
|
577
670
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
child.style.backgroundColor = firstAnim.from;
|
|
598
|
-
} else if (effect === "borderRadius") {
|
|
599
|
-
setInitialStyle("borderRadius", firstAnim.from);
|
|
600
|
-
} else if (effect === "color") {
|
|
601
|
-
child.style.color = firstAnim.from;
|
|
602
|
-
} else if (effect === "width") {
|
|
603
|
-
setInitialStyle("width", firstAnim.from);
|
|
604
|
-
} else if (effect === "height") {
|
|
605
|
-
setInitialStyle("height", firstAnim.from);
|
|
606
|
-
} else if (effect.startsWith("margin")) {
|
|
607
|
-
const prop = effect.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
608
|
-
setInitialStyle(prop, firstAnim.from);
|
|
609
|
-
} else if (effect.startsWith("padding")) {
|
|
610
|
-
const prop = effect.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
611
|
-
setInitialStyle(prop, firstAnim.from);
|
|
612
|
-
} else if (effect === "borderWidth") {
|
|
613
|
-
setInitialStyle("borderWidth", firstAnim.from);
|
|
614
|
-
} else if (effect === "borderColor") {
|
|
615
|
-
child.style.borderColor = firstAnim.from;
|
|
616
|
-
} else if (effect === "boxShadow") {
|
|
617
|
-
child.style.boxShadow = firstAnim.from;
|
|
618
|
-
} else if (effect === "background") {
|
|
619
|
-
child.style.background = firstAnim.from;
|
|
620
|
-
}
|
|
671
|
+
// Every step starts where it is going to leave from, written inline, so the
|
|
672
|
+
// frame before the animation begins is the frame it begins with. Doing this
|
|
673
|
+
// only for the first step left a delayed or shorter step showing its ending
|
|
674
|
+
// state — a fade that flashed visible before it faded in.
|
|
675
|
+
writeStartValues(child, steps);
|
|
676
|
+
|
|
677
|
+
// A looping step and a one-shot step cannot live in the same rule: repeating
|
|
678
|
+
// the rule would repeat the one-shot too, so an entrance meant to play once
|
|
679
|
+
// would flicker forever. Each timing group generates its own `@keyframes`.
|
|
680
|
+
const groups = [];
|
|
681
|
+
for (const loop of [false, true]) {
|
|
682
|
+
const members = steps.filter((anim) => anim.loop === loop);
|
|
683
|
+
if (members.length) groups.push({ loop, members });
|
|
684
|
+
}
|
|
685
|
+
if (groups.length > 1 && groups.every(groupHasTransform)) {
|
|
686
|
+
console.warn(
|
|
687
|
+
'⚠️ [AnimatedBox] transform steps split between a loop and a one-shot: two rules cannot write one `transform`, so only the last rule\'s transform is applied. Keep the transform steps in one group.',
|
|
688
|
+
);
|
|
689
|
+
}
|
|
621
690
|
|
|
622
|
-
|
|
691
|
+
const rules = groups.map(({ loop, members }) => {
|
|
692
|
+
// The group starts when its earliest step does; a later step waits inside
|
|
693
|
+
// the rule instead of buying a second `animation-delay`.
|
|
694
|
+
const startsAt = Math.min(...members.map((anim) => anim.delayMs));
|
|
695
|
+
const entries = members.map((anim) => ({
|
|
696
|
+
...anim,
|
|
697
|
+
offset: anim.delayMs - startsAt,
|
|
698
|
+
}));
|
|
699
|
+
const span = entries.reduce(
|
|
700
|
+
(longest, anim) => Math.max(longest, anim.offset + anim.duration),
|
|
701
|
+
0,
|
|
702
|
+
);
|
|
703
|
+
const keyframes = generateRule(entries, span);
|
|
623
704
|
// The name is derived from the keyframe body so identical animations reuse a
|
|
624
705
|
// single <style>. A per-call unique name (`Date.now()`) made the dedupe check
|
|
625
706
|
// in `injectKeyframes` dead code: every call appended a permanent <style> to
|
|
626
707
|
// document.head, and AnimatedText calls this once per letter.
|
|
627
|
-
const name =
|
|
708
|
+
const name = animationName("anim", keyframes);
|
|
628
709
|
injectKeyframes(name, keyframes);
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
710
|
+
return { name, span, startsAt, loop };
|
|
711
|
+
});
|
|
712
|
+
// The hook the OS preference rule matches. The JS values below cover the app's
|
|
713
|
+
// own mode; this is what stops a loop already running when the system setting
|
|
714
|
+
// flips, with nothing to rebuild and no listener to leak.
|
|
715
|
+
markMotionLoop(child);
|
|
716
|
+
|
|
717
|
+
// Reduced motion is the accessibility recipe, not a switch-off: a near-zero
|
|
718
|
+
// duration lands the element on its `to` values through `fillMode: forwards`,
|
|
719
|
+
// so the state the animation was carrying still arrives — only the movement
|
|
720
|
+
// disappears. A loop becomes a single frame, and the delay goes with it:
|
|
721
|
+
// content held invisible for two seconds is worse than content that moved.
|
|
722
|
+
const reduced = prefersReducedMotion();
|
|
723
|
+
child.style.animation = rules
|
|
724
|
+
.map((rule) =>
|
|
725
|
+
reduced
|
|
726
|
+
? `${rule.name} 0.01ms ${timing} 0s 1 normal ${fillMode}`
|
|
727
|
+
: `${rule.name} ${rule.span}ms ${timing} ${rule.startsAt}ms ${
|
|
728
|
+
rule.loop ? "infinite" : "1"
|
|
729
|
+
} normal ${fillMode}`,
|
|
730
|
+
)
|
|
731
|
+
.join(", ");
|
|
637
732
|
|
|
638
733
|
return child;
|
|
639
734
|
};
|