beez-ui 2.6.1 → 2.7.0
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/CHANGELOG.md +29 -0
- package/README.md +48 -28
- package/dist/components/alert-dialog.d.ts +2 -2
- package/dist/components/alert-dialog.js +3 -3
- package/dist/components/dialog-surface.js +1 -1
- package/dist/components/dialog.d.ts +5 -4
- package/dist/components/dialog.js +7 -6
- package/dist/components/sidebar.js +4 -9
- package/dist/components/themed-toaster.d.ts +3 -4
- package/dist/components/themed-toaster.js +42 -56
- package/dist/components/toaster.d.ts +12 -0
- package/dist/components/toaster.js +62 -0
- package/dist/motion/arc-tokens.d.ts +5 -1
- package/dist/motion/arc-tokens.js +19 -3
- package/dist/motion/glide-indicator.d.ts +4 -6
- package/dist/motion/glide-indicator.js +61 -23
- package/dist/motion/motion-slot.js +30 -61
- package/dist/motion/surface-keyframes.d.ts +12 -1
- package/dist/motion/surface-keyframes.js +48 -83
- package/dist/motion/tokens.d.ts +97 -61
- package/dist/motion/tokens.js +95 -99
- package/dist/sonner.d.ts +6 -1
- package/dist/sonner.js +6 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -47,42 +47,81 @@ export const GLIDE_PRESETS = {
|
|
|
47
47
|
export const GLIDE_TARGET_ATTRIBUTE = "data-glide-target";
|
|
48
48
|
/** Marks items whose transitions, including pseudo-element ones, are suspended during a glide. */
|
|
49
49
|
export const GLIDE_FREEZE_ATTRIBUTE = "data-glide-freeze";
|
|
50
|
-
/**
|
|
50
|
+
/** Laid-out size of the border box, unaffected by any transform on the element or its ancestors. */
|
|
51
|
+
function layoutSize(element) {
|
|
52
|
+
const style = getComputedStyle(element);
|
|
53
|
+
const width = Number.parseFloat(style.width);
|
|
54
|
+
const height = Number.parseFloat(style.height);
|
|
55
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) {
|
|
56
|
+
return {
|
|
57
|
+
width: element.offsetWidth,
|
|
58
|
+
height: element.offsetHeight
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (style.boxSizing === "border-box") return {
|
|
62
|
+
width,
|
|
63
|
+
height
|
|
64
|
+
};
|
|
65
|
+
const sum = (...values) => values.reduce((total, value) => total + (Number.parseFloat(value) || 0), 0);
|
|
66
|
+
return {
|
|
67
|
+
width: width + sum(style.paddingLeft, style.paddingRight, style.borderLeftWidth, style.borderRightWidth),
|
|
68
|
+
height: height + sum(style.paddingTop, style.paddingBottom, style.borderTopWidth, style.borderBottomWidth)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Converts a viewport rectangle into the container's own scrolling coordinate space. A container
|
|
73
|
+
* still scaling in (a menu or listbox entrance) paints everything inside it smaller, so painted
|
|
74
|
+
* distances are divided by that scale to land where absolute positioning places the copy.
|
|
75
|
+
*/
|
|
51
76
|
function toContainerBox(container, rect) {
|
|
52
77
|
const origin = container.getBoundingClientRect();
|
|
78
|
+
const layout = layoutSize(container);
|
|
79
|
+
const scaleX = layout.width > 0 && origin.width > 0 ? origin.width / layout.width : 1;
|
|
80
|
+
const scaleY = layout.height > 0 && origin.height > 0 ? origin.height / layout.height : 1;
|
|
53
81
|
return {
|
|
54
|
-
left: rect.left - origin.left - container.clientLeft + container.scrollLeft,
|
|
55
|
-
top: rect.top - origin.top - container.clientTop + container.scrollTop,
|
|
56
|
-
width: rect.width,
|
|
57
|
-
height: rect.height
|
|
82
|
+
left: (rect.left - origin.left) / scaleX - container.clientLeft + container.scrollLeft,
|
|
83
|
+
top: (rect.top - origin.top) / scaleY - container.clientTop + container.scrollTop,
|
|
84
|
+
width: rect.width / scaleX,
|
|
85
|
+
height: rect.height / scaleY
|
|
58
86
|
};
|
|
59
87
|
}
|
|
60
|
-
/**
|
|
61
|
-
function
|
|
62
|
-
const
|
|
88
|
+
/** Scale and translation an item's own running entrance adds, read from its computed `matrix()`. */
|
|
89
|
+
function inFlightTransform(item) {
|
|
90
|
+
const style = getComputedStyle(item);
|
|
91
|
+
const values = /^matrix\(([^)]+)\)$/.exec(style.transform);
|
|
92
|
+
const [originX = 0, originY = 0] = style.transformOrigin.split(" ").map(Number.parseFloat);
|
|
63
93
|
if (!values) return {
|
|
94
|
+
scaleX: 1,
|
|
95
|
+
scaleY: 1,
|
|
64
96
|
x: 0,
|
|
65
|
-
y: 0
|
|
97
|
+
y: 0,
|
|
98
|
+
originX,
|
|
99
|
+
originY
|
|
66
100
|
};
|
|
67
101
|
const parts = values[1].split(",").map(Number.parseFloat);
|
|
68
102
|
return {
|
|
103
|
+
scaleX: parts[0] || 1,
|
|
104
|
+
scaleY: parts[3] || 1,
|
|
69
105
|
x: parts[4] ?? 0,
|
|
70
|
-
y: parts[5] ?? 0
|
|
106
|
+
y: parts[5] ?? 0,
|
|
107
|
+
originX,
|
|
108
|
+
originY
|
|
71
109
|
};
|
|
72
110
|
}
|
|
73
111
|
/**
|
|
74
|
-
* Measures the highlight: the item box, or its positioned `::after`
|
|
75
|
-
* An item still
|
|
76
|
-
*
|
|
112
|
+
* Measures the highlight in the container's coordinates: the item box, or its positioned `::after`
|
|
113
|
+
* bar for line variants. An item still entering (Select options settle from a slight scale) is
|
|
114
|
+
* measured where it will rest, so the highlight neither lands undersized nor jumps when it ends.
|
|
77
115
|
*/
|
|
78
|
-
function highlightBox(item, surface) {
|
|
79
|
-
const
|
|
80
|
-
const
|
|
116
|
+
function highlightBox(container, item, surface) {
|
|
117
|
+
const painted = toContainerBox(container, item.getBoundingClientRect());
|
|
118
|
+
const motion = inFlightTransform(item);
|
|
119
|
+
// The entrance scales around the transform origin, so the resting corner sits that far back.
|
|
81
120
|
const rect = {
|
|
82
|
-
left:
|
|
83
|
-
top:
|
|
84
|
-
width:
|
|
85
|
-
height:
|
|
121
|
+
left: painted.left - motion.x - motion.originX * (1 - motion.scaleX),
|
|
122
|
+
top: painted.top - motion.y - motion.originY * (1 - motion.scaleY),
|
|
123
|
+
width: painted.width / motion.scaleX,
|
|
124
|
+
height: painted.height / motion.scaleY
|
|
86
125
|
};
|
|
87
126
|
if (surface === "background") return rect;
|
|
88
127
|
const pseudo = getComputedStyle(item, "::after");
|
|
@@ -201,13 +240,13 @@ export function attachGlideIndicator(container, preset) {
|
|
|
201
240
|
/** Starts, or redirects, the glide from the previous highlight to the new active item. */
|
|
202
241
|
function glide(from, to) {
|
|
203
242
|
const surface = preset.surface(container);
|
|
204
|
-
const
|
|
243
|
+
const start = overlay ? toContainerBox(container, overlay.getBoundingClientRect()) : highlightBox(container, from, surface);
|
|
205
244
|
settle();
|
|
206
245
|
// The previous item drops its highlight instantly: the copy is what leaves it.
|
|
207
246
|
freezeTransitions(from)();
|
|
208
247
|
// Read the target's settled paint before any transition has a chance to start.
|
|
209
248
|
const thawTarget = freezeTransitions(to);
|
|
210
|
-
const destination =
|
|
249
|
+
const destination = highlightBox(container, to, surface);
|
|
211
250
|
const paint = highlightPaint(to, surface);
|
|
212
251
|
if (destination.width === 0 || destination.height === 0) {
|
|
213
252
|
thawTarget();
|
|
@@ -227,7 +266,6 @@ export function attachGlideIndicator(container, preset) {
|
|
|
227
266
|
zIndex: "-1",
|
|
228
267
|
margin: "0"
|
|
229
268
|
});
|
|
230
|
-
const start = toContainerBox(container, origin);
|
|
231
269
|
container.prepend(overlay);
|
|
232
270
|
releaseTarget = hideTarget(to, surface, thawTarget);
|
|
233
271
|
animation = animate(overlay, {
|
|
@@ -6,8 +6,8 @@ import { Slot } from "radix-ui";
|
|
|
6
6
|
import { animate, hover, press, usePresence, frame } from "motion/react";
|
|
7
7
|
import { usePrefersReducedMotion } from "../hooks/use-prefers-reduced-motion.js";
|
|
8
8
|
import { HOVER_CAPABLE_QUERY, MILLISECONDS_PER_SECOND, MOTION_ENTERING_ATTRIBUTE } from "../constants/motion.js";
|
|
9
|
-
import { MOTION_EASE, MOTION_TIMING, MOTION_PRESS_SCALE, MOTION_SUBTLE_PRESS_SCALE, MOTION_TOGGLE_PRESS_SCALE, MOTION_HOVER_SCALE, MOTION_THUMB_SQUISH_SCALE, MOTION_CONTENT_DISTANCE,
|
|
10
|
-
import { surfaceEnter, surfaceExit } from "./surface-keyframes.js";
|
|
9
|
+
import { MOTION_EASE, MOTION_TIMING, MOTION_PRESS_SCALE, MOTION_SUBTLE_PRESS_SCALE, MOTION_TOGGLE_PRESS_SCALE, MOTION_HOVER_SCALE, MOTION_THUMB_SQUISH_SCALE, MOTION_CONTENT_DISTANCE, MOTION_ITEM_CHECK_SCALE, MOTION_ITEM_ENTER_SCALE, MOTION_ICON_SWAP_SCALE, MOTION_CLOSE_BUTTON_SCALE, MOTION_EASE_IN_OUT, MOTION_REVEAL_BLUR_PX, MOTION_REVEAL_SCALE, MOTION_SHIMMER_TRANSLATE, MOTION_CARD_SHADOW_LIFT, MOTION_CARD_SHADOW_REST, SPRING_CHEVRON, SPRING_PANEL, SPRING_POP, SPRING_PRESS, SPRING_THUMB } from "./tokens.js";
|
|
10
|
+
import { surfaceEnter, surfaceExit, withRestingStyle } from "./surface-keyframes.js";
|
|
11
11
|
import { useAttachedElement } from "./use-attached-element.js";
|
|
12
12
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
13
|
const EASE_OUT = [...MOTION_EASE];
|
|
@@ -33,8 +33,6 @@ const ANIMATED_PROPERTIES = [
|
|
|
33
33
|
"filter",
|
|
34
34
|
"clipPath"
|
|
35
35
|
];
|
|
36
|
-
/** Input groups draw the field frame; their controls are borderless. */
|
|
37
|
-
const INPUT_GROUP_SLOT = "input-group";
|
|
38
36
|
/** SVG attributes Motion writes to draw a stroke with `pathLength`. */
|
|
39
37
|
const STROKE_DRAW_ATTRIBUTES = [
|
|
40
38
|
"pathLength",
|
|
@@ -44,7 +42,6 @@ const STROKE_DRAW_ATTRIBUTES = [
|
|
|
44
42
|
/** Attributes whose changes re-announce a field's state with a subtle settle. */
|
|
45
43
|
const FIELD_STATE_ATTRIBUTES = [
|
|
46
44
|
"data-state",
|
|
47
|
-
"aria-invalid",
|
|
48
45
|
"aria-selected",
|
|
49
46
|
"aria-checked",
|
|
50
47
|
"data-loading",
|
|
@@ -87,14 +84,6 @@ function rotationOf(transform) {
|
|
|
87
84
|
const { a, b } = matrixOf(transform);
|
|
88
85
|
return Math.atan2(b, a) * 180 / Math.PI;
|
|
89
86
|
}
|
|
90
|
-
/** Position of an item among its siblings of the same slot inside the closest list or menu. */
|
|
91
|
-
function listIndexOf(element) {
|
|
92
|
-
const slot = element.getAttribute("data-slot");
|
|
93
|
-
const list = element.closest("[role=\"listbox\"],[role=\"menu\"],[role=\"list\"]");
|
|
94
|
-
if (!slot || !list) return 0;
|
|
95
|
-
const items = Array.from(list.querySelectorAll(`[data-slot="${slot}"]`));
|
|
96
|
-
return Math.max(items.indexOf(element), 0);
|
|
97
|
-
}
|
|
98
87
|
/** Checked and indeterminate indicators are visible; only unchecked ones are hidden. */
|
|
99
88
|
function isChecked(element) {
|
|
100
89
|
const state = element.getAttribute("data-state");
|
|
@@ -192,13 +181,18 @@ export function MotionSlot(t0) {
|
|
|
192
181
|
snapshot[property_1] = value;
|
|
193
182
|
}
|
|
194
183
|
});
|
|
195
|
-
interruptedRef.current =
|
|
184
|
+
interruptedRef.current = {
|
|
185
|
+
element,
|
|
186
|
+
values: snapshot
|
|
187
|
+
};
|
|
196
188
|
}
|
|
197
189
|
cleanups.forEach(_temp);
|
|
198
190
|
cancel();
|
|
199
191
|
restore();
|
|
200
192
|
element.removeAttribute(MOTION_ENTERING_ATTRIBUTE);
|
|
201
193
|
};
|
|
194
|
+
const interrupted = interruptedRef.current;
|
|
195
|
+
interruptedRef.current = null;
|
|
202
196
|
if (reduceMotion || !isPresent && element.hasAttribute("data-beez-theme-menu")) {
|
|
203
197
|
if (!isPresent) {
|
|
204
198
|
queueMicrotask(() => {
|
|
@@ -253,13 +247,15 @@ export function MotionSlot(t0) {
|
|
|
253
247
|
});
|
|
254
248
|
};
|
|
255
249
|
const tween = _temp2;
|
|
256
|
-
const interrupted = interruptedRef.current;
|
|
257
|
-
interruptedRef.current = null;
|
|
258
250
|
if (SURFACE_KINDS.includes(kind)) {
|
|
259
251
|
const step = isPresent ? surfaceEnter(element, kind) : surfaceExit(element, kind);
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
252
|
+
const computed_0 = getComputedStyle(element);
|
|
253
|
+
const keyframes_0 = withRestingStyle(step.keyframes, {
|
|
254
|
+
transform: computed_0.transform,
|
|
255
|
+
opacity: Number.parseFloat(computed_0.opacity)
|
|
256
|
+
});
|
|
257
|
+
if (interrupted?.element === element) {
|
|
258
|
+
Object.entries(interrupted.values).forEach((t10) => {
|
|
263
259
|
const [property_8, value_0] = t10;
|
|
264
260
|
const frames_0 = keyframes_0[property_8];
|
|
265
261
|
if (!Array.isArray(frames_0) || !value_0) {
|
|
@@ -298,11 +294,10 @@ export function MotionSlot(t0) {
|
|
|
298
294
|
if (kind === "icon-swap") {
|
|
299
295
|
play({
|
|
300
296
|
opacity: [0, 1],
|
|
301
|
-
transform: [`scale(${MOTION_ICON_SWAP_SCALE})`, "scale(1)"]
|
|
302
|
-
filter: ["blur(8px)", "blur(0px)"]
|
|
297
|
+
transform: [`scale(${MOTION_ICON_SWAP_SCALE})`, "scale(1)"]
|
|
303
298
|
}, {
|
|
304
|
-
|
|
305
|
-
|
|
299
|
+
...SPRING_POP,
|
|
300
|
+
opacity: tween(MOTION_TIMING.iconSwap)
|
|
306
301
|
});
|
|
307
302
|
} else {
|
|
308
303
|
if (kind === "item-check") {
|
|
@@ -315,16 +310,10 @@ export function MotionSlot(t0) {
|
|
|
315
310
|
});
|
|
316
311
|
} else {
|
|
317
312
|
if (kind === "list-item") {
|
|
318
|
-
|
|
319
|
-
const position = Math.min(index, MOTION_LIST_STAGGER_LIMIT);
|
|
320
|
-
play(index < MOTION_LIST_STAGGER_LIMIT ? {
|
|
313
|
+
play({
|
|
321
314
|
opacity: [0, 1],
|
|
322
|
-
transform: [`
|
|
323
|
-
|
|
324
|
-
} : { opacity: [0, 1] }, {
|
|
325
|
-
...tween(MOTION_TIMING.listItem),
|
|
326
|
-
delay: MOTION_TIMING.listDelay + position * MOTION_TIMING.listStagger
|
|
327
|
-
});
|
|
315
|
+
transform: [`scale(${MOTION_ITEM_ENTER_SCALE})`, "scale(1)"]
|
|
316
|
+
}, tween(MOTION_TIMING.listItem));
|
|
328
317
|
} else {
|
|
329
318
|
if (kind === "delayed-pop" && typeof element.animate === "function") {
|
|
330
319
|
const pop = element.animate({
|
|
@@ -356,8 +345,7 @@ export function MotionSlot(t0) {
|
|
|
356
345
|
if (kind === "message") {
|
|
357
346
|
play({
|
|
358
347
|
opacity: [0, 1],
|
|
359
|
-
transform: [`
|
|
360
|
-
filter: ["blur(4px)", "blur(0px)"]
|
|
348
|
+
transform: [`scale(${MOTION_ITEM_ENTER_SCALE})`, "scale(1)"]
|
|
361
349
|
}, tween(MOTION_TIMING.message));
|
|
362
350
|
} else {
|
|
363
351
|
if (kind === "fade" || kind === "card" || kind === "row") {
|
|
@@ -450,26 +438,10 @@ export function MotionSlot(t0) {
|
|
|
450
438
|
};
|
|
451
439
|
element.addEventListener("focusin", settleField);
|
|
452
440
|
cleanups.push(() => element.removeEventListener("focusin", settleField));
|
|
453
|
-
const
|
|
454
|
-
const isGroupControl = !isGroup && element.parentElement?.closest(`[data-slot="${INPUT_GROUP_SLOT}"]`) != null;
|
|
455
|
-
const readInvalid = () => isGroup ? element.querySelector("[data-slot][aria-invalid=\"true\"]") !== null : element.getAttribute("aria-invalid") === "true";
|
|
456
|
-
let wasInvalid = readInvalid();
|
|
457
|
-
const observer = new MutationObserver((records) => {
|
|
458
|
-
const isInvalid = readInvalid();
|
|
459
|
-
const becameInvalid = isInvalid && !wasInvalid;
|
|
460
|
-
wasInvalid = isInvalid;
|
|
461
|
-
if (becameInvalid && !isGroupControl && !element.hasAttribute("hidden")) {
|
|
462
|
-
play({ transform: MOTION_INVALID_SHAKE_PX.map(_temp3) }, { duration: MOTION_TIMING.shake });
|
|
463
|
-
} else {
|
|
464
|
-
if (records.some((record) => record.target === element && record.attributeName !== "aria-invalid")) {
|
|
465
|
-
settleField();
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
});
|
|
441
|
+
const observer = new MutationObserver(settleField);
|
|
469
442
|
observer.observe(element, {
|
|
470
443
|
attributes: true,
|
|
471
|
-
attributeFilter: FIELD_STATE_ATTRIBUTES
|
|
472
|
-
subtree: isGroup
|
|
444
|
+
attributeFilter: FIELD_STATE_ATTRIBUTES
|
|
473
445
|
});
|
|
474
446
|
cleanups.push(() => observer.disconnect());
|
|
475
447
|
}
|
|
@@ -477,9 +449,9 @@ export function MotionSlot(t0) {
|
|
|
477
449
|
let wasChecked = isChecked(element);
|
|
478
450
|
const strokeDraws = new Set();
|
|
479
451
|
const settleStrokes = () => {
|
|
480
|
-
strokeDraws.forEach(
|
|
452
|
+
strokeDraws.forEach(_temp3);
|
|
481
453
|
strokeDraws.clear();
|
|
482
|
-
element.querySelectorAll("path").forEach(
|
|
454
|
+
element.querySelectorAll("path").forEach(_temp4);
|
|
483
455
|
};
|
|
484
456
|
cleanups.push(settleStrokes);
|
|
485
457
|
const observer_0 = new MutationObserver(() => {
|
|
@@ -644,7 +616,7 @@ export function MotionSlot(t0) {
|
|
|
644
616
|
release.cancel();
|
|
645
617
|
squish = null;
|
|
646
618
|
}
|
|
647
|
-
}).catch(
|
|
619
|
+
}).catch(_temp5);
|
|
648
620
|
};
|
|
649
621
|
}));
|
|
650
622
|
}
|
|
@@ -697,16 +669,13 @@ export function MotionSlot(t0) {
|
|
|
697
669
|
}
|
|
698
670
|
return t6;
|
|
699
671
|
}
|
|
700
|
-
function
|
|
701
|
-
function
|
|
672
|
+
function _temp5() {}
|
|
673
|
+
function _temp4(path) {
|
|
702
674
|
STROKE_DRAW_ATTRIBUTES.forEach((attribute) => path.removeAttribute(attribute));
|
|
703
675
|
}
|
|
704
|
-
function
|
|
676
|
+
function _temp3(draw) {
|
|
705
677
|
return draw.cancel();
|
|
706
678
|
}
|
|
707
|
-
function _temp3(offset) {
|
|
708
|
-
return `translateX(${offset}px)`;
|
|
709
|
-
}
|
|
710
679
|
function _temp2(duration) {
|
|
711
680
|
return {
|
|
712
681
|
duration,
|
|
@@ -7,7 +7,18 @@ export interface MotionStep {
|
|
|
7
7
|
keyframes: DOMKeyframesDefinition;
|
|
8
8
|
transition: AnimationOptions;
|
|
9
9
|
}
|
|
10
|
-
/** Entrance playback
|
|
10
|
+
/** Entrance playback for each surface kind, on Arc's motion presets. */
|
|
11
11
|
export declare function surfaceEnter(element: Element, kind: SurfaceKind): MotionStep;
|
|
12
|
+
/** Transform and opacity a surface rests at, which its caller may set through a class or style. */
|
|
13
|
+
export interface RestingStyle {
|
|
14
|
+
transform: string;
|
|
15
|
+
opacity: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Composes surface playback with the surface's resting transform and opacity, so a caller-owned
|
|
19
|
+
* tilt, offset or translucency holds through the whole entrance and exit instead of being
|
|
20
|
+
* replaced by identity and full opacity, then snapping back once playback restores it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function withRestingStyle(keyframes: DOMKeyframesDefinition, resting: RestingStyle): DOMKeyframesDefinition;
|
|
12
23
|
/** Exit playback: brief and eased so dismissal never blocks the next interaction. */
|
|
13
24
|
export declare function surfaceExit(element: Element, kind: SurfaceKind): MotionStep;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MOTION_DIALOG_DISTANCE, MOTION_DIALOG_EXIT_DISTANCE, MOTION_EASE, MOTION_EASE_DRAWER, MOTION_EASE_STANDARD, MOTION_EXIT_SCALE, MOTION_SURFACE_CLIP_BLEED_PX, MOTION_SURFACE_CLIP_PERCENT, MOTION_SURFACE_SCALE, MOTION_TIMING, MOTION_TOOLTIP_DISTANCE, MOTION_TOOLTIP_SCALE, SPRING_PANEL } from "./tokens.js";
|
|
2
2
|
const EASE_OUT = [...MOTION_EASE];
|
|
3
3
|
const EASE_DRAWER = [...MOTION_EASE_DRAWER];
|
|
4
|
-
const
|
|
4
|
+
const EASE_STANDARD = [...MOTION_EASE_STANDARD];
|
|
5
5
|
const SIDES = [
|
|
6
6
|
"top",
|
|
7
7
|
"right",
|
|
@@ -70,45 +70,14 @@ function collapsedClip(side, align) {
|
|
|
70
70
|
const [top, bottom] = crossAxis(align);
|
|
71
71
|
return side === "right" ? `inset(${top} ${hidden} ${bottom} ${OPEN_EDGE})` : `inset(${top} ${OPEN_EDGE} ${bottom} ${hidden})`;
|
|
72
72
|
}
|
|
73
|
-
/** Computed radius of each corner, clockwise from the top left, as border-radius serializes it. */
|
|
74
|
-
const CORNER_RADIUS_PROPERTIES = [
|
|
75
|
-
"borderTopLeftRadius",
|
|
76
|
-
"borderTopRightRadius",
|
|
77
|
-
"borderBottomRightRadius",
|
|
78
|
-
"borderBottomLeftRadius"
|
|
79
|
-
];
|
|
80
|
-
/**
|
|
81
|
-
* Builds the `round` part of an inset clip that repeats the surface's own corners, each grown by
|
|
82
|
-
* the bleed so the clip edge stays parallel to the border. A corner serializes as one radius or
|
|
83
|
-
* as a horizontal and vertical pair (elliptical), in pixels or percentages of the box.
|
|
84
|
-
*/
|
|
85
|
-
function bleedingCornerRadii(element) {
|
|
86
|
-
const computed = getComputedStyle(element);
|
|
87
|
-
const grow = (radius) => `calc(${radius} + ${MOTION_SURFACE_CLIP_BLEED_PX}px)`;
|
|
88
|
-
const corners = CORNER_RADIUS_PROPERTIES.map((property) => {
|
|
89
|
-
const [horizontal = "0px", vertical = horizontal] = computed[property].trim().split(/\s+/);
|
|
90
|
-
return {
|
|
91
|
-
horizontal: grow(horizontal),
|
|
92
|
-
vertical: grow(vertical)
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
const horizontals = corners.map((corner) => corner.horizontal).join(" ");
|
|
96
|
-
const verticals = corners.map((corner) => corner.vertical).join(" ");
|
|
97
|
-
return `round ${horizontals} / ${verticals}`;
|
|
98
|
-
}
|
|
99
73
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* rounding corners.
|
|
74
|
+
* Arc's dialog leave: a short tween on the standard curve, so it never lingers and a reopen
|
|
75
|
+
* retargets from wherever the panel is.
|
|
103
76
|
*/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
folded: `inset(${folded} ${folded} ${folded} ${folded} ${round})`,
|
|
109
|
-
unfolded: `inset(${OPEN_EDGE} ${OPEN_EDGE} ${OPEN_EDGE} ${OPEN_EDGE} ${round})`
|
|
110
|
-
};
|
|
111
|
-
}
|
|
77
|
+
const DIALOG_LEAVE = {
|
|
78
|
+
duration: MOTION_TIMING.fast,
|
|
79
|
+
ease: EASE_STANDARD
|
|
80
|
+
};
|
|
112
81
|
/** Edge-panel travel: fully off-screen on the side the sheet is attached to. */
|
|
113
82
|
function sheetOffset(element) {
|
|
114
83
|
const side = readPlacement(element).side ?? "right";
|
|
@@ -129,7 +98,7 @@ function sheetOffset(element) {
|
|
|
129
98
|
y: "0%"
|
|
130
99
|
};
|
|
131
100
|
}
|
|
132
|
-
/** Entrance playback
|
|
101
|
+
/** Entrance playback for each surface kind, on Arc's motion presets. */
|
|
133
102
|
export function surfaceEnter(element, kind) {
|
|
134
103
|
const { side, align } = readPlacement(element);
|
|
135
104
|
if (kind === "overlay") {
|
|
@@ -142,42 +111,33 @@ export function surfaceEnter(element, kind) {
|
|
|
142
111
|
};
|
|
143
112
|
}
|
|
144
113
|
if (kind === "tooltip") {
|
|
114
|
+
// Arc's tooltip: a short rise from its trigger and a fade, without blur.
|
|
145
115
|
const offset = offsetAwayFromTrigger(side, MOTION_TOOLTIP_DISTANCE);
|
|
146
116
|
return {
|
|
147
117
|
keyframes: {
|
|
148
118
|
opacity: [0, 1],
|
|
149
|
-
transform: [transform(offset.x, offset.y,
|
|
150
|
-
filter: ["blur(5px)", "blur(0px)"]
|
|
119
|
+
transform: [transform(offset.x, offset.y, MOTION_TOOLTIP_SCALE), transform(0, 0)]
|
|
151
120
|
},
|
|
152
121
|
transition: {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
duration: MOTION_TIMING.fast,
|
|
156
|
-
ease: EASE_OUT
|
|
157
|
-
},
|
|
158
|
-
filter: {
|
|
159
|
-
duration: MOTION_TIMING.enter,
|
|
160
|
-
ease: EASE_OUT
|
|
161
|
-
}
|
|
122
|
+
duration: MOTION_TIMING.fast,
|
|
123
|
+
ease: EASE_OUT
|
|
162
124
|
}
|
|
163
125
|
};
|
|
164
126
|
}
|
|
165
|
-
|
|
127
|
+
// The deprecated center morph now plays Arc's dialog transition, like the library's own dialogs.
|
|
128
|
+
if (kind === "dialog" || kind === "center-morph") {
|
|
129
|
+
// Arc's dialog: the panel rises and scales in on the smooth spring while it fades in quickly.
|
|
166
130
|
return {
|
|
167
131
|
keyframes: {
|
|
168
132
|
opacity: [0, 1],
|
|
169
|
-
transform: [transform(0, MOTION_DIALOG_DISTANCE,
|
|
133
|
+
transform: [transform(0, MOTION_DIALOG_DISTANCE, MOTION_SURFACE_SCALE), transform(0, 0)]
|
|
170
134
|
},
|
|
171
|
-
transition: SPRING_PANEL
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
if (kind === "center-morph") {
|
|
175
|
-
const { folded, unfolded } = centerMorphClips(element);
|
|
176
|
-
return {
|
|
177
|
-
keyframes: { clipPath: [folded, unfolded] },
|
|
178
135
|
transition: {
|
|
179
|
-
|
|
180
|
-
|
|
136
|
+
...SPRING_PANEL,
|
|
137
|
+
opacity: {
|
|
138
|
+
duration: MOTION_TIMING.fast,
|
|
139
|
+
ease: EASE_OUT
|
|
140
|
+
}
|
|
181
141
|
}
|
|
182
142
|
};
|
|
183
143
|
}
|
|
@@ -211,6 +171,22 @@ export function surfaceEnter(element, kind) {
|
|
|
211
171
|
}
|
|
212
172
|
};
|
|
213
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Composes surface playback with the surface's resting transform and opacity, so a caller-owned
|
|
176
|
+
* tilt, offset or translucency holds through the whole entrance and exit instead of being
|
|
177
|
+
* replaced by identity and full opacity, then snapping back once playback restores it.
|
|
178
|
+
*/
|
|
179
|
+
export function withRestingStyle(keyframes, resting) {
|
|
180
|
+
const composed = { ...keyframes };
|
|
181
|
+
const { transform, opacity } = composed;
|
|
182
|
+
if (Array.isArray(transform) && resting.transform && resting.transform !== "none") {
|
|
183
|
+
composed.transform = transform.map((value) => `${String(value)} ${resting.transform}`);
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(opacity) && Number.isFinite(resting.opacity) && resting.opacity !== 1) {
|
|
186
|
+
composed.opacity = opacity.map((value) => Number(value) * resting.opacity);
|
|
187
|
+
}
|
|
188
|
+
return composed;
|
|
189
|
+
}
|
|
214
190
|
/** Exit playback: brief and eased so dismissal never blocks the next interaction. */
|
|
215
191
|
export function surfaceExit(element, kind) {
|
|
216
192
|
const exitTween = {
|
|
@@ -220,40 +196,29 @@ export function surfaceExit(element, kind) {
|
|
|
220
196
|
if (kind === "overlay") {
|
|
221
197
|
return {
|
|
222
198
|
keyframes: { opacity: [1, 0] },
|
|
223
|
-
transition:
|
|
199
|
+
transition: DIALOG_LEAVE
|
|
224
200
|
};
|
|
225
201
|
}
|
|
226
202
|
if (kind === "tooltip") {
|
|
227
|
-
|
|
203
|
+
// Arc's tooltip leaves in place, shrinking slightly while it fades, on the instant timing.
|
|
228
204
|
return {
|
|
229
205
|
keyframes: {
|
|
230
206
|
opacity: [1, 0],
|
|
231
|
-
transform: [transform(0, 0), transform(
|
|
232
|
-
filter: ["blur(0px)", "blur(3px)"]
|
|
207
|
+
transform: [transform(0, 0), transform(0, 0, MOTION_EXIT_SCALE)]
|
|
233
208
|
},
|
|
234
|
-
transition:
|
|
209
|
+
transition: {
|
|
210
|
+
duration: MOTION_TIMING.instant,
|
|
211
|
+
ease: EASE_STANDARD
|
|
212
|
+
}
|
|
235
213
|
};
|
|
236
214
|
}
|
|
237
|
-
if (kind === "dialog") {
|
|
215
|
+
if (kind === "dialog" || kind === "center-morph") {
|
|
238
216
|
return {
|
|
239
217
|
keyframes: {
|
|
240
218
|
opacity: [1, 0],
|
|
241
|
-
transform: [transform(0, 0), transform(0,
|
|
219
|
+
transform: [transform(0, 0), transform(0, MOTION_DIALOG_EXIT_DISTANCE, MOTION_EXIT_SCALE)]
|
|
242
220
|
},
|
|
243
|
-
transition:
|
|
244
|
-
duration: MOTION_TIMING.enter,
|
|
245
|
-
ease: EASE_OUT
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
if (kind === "center-morph") {
|
|
250
|
-
const { folded, unfolded } = centerMorphClips(element);
|
|
251
|
-
return {
|
|
252
|
-
keyframes: { clipPath: [unfolded, folded] },
|
|
253
|
-
transition: {
|
|
254
|
-
duration: MOTION_TIMING.centerMorphFold,
|
|
255
|
-
ease: EASE_UNFOLD
|
|
256
|
-
}
|
|
221
|
+
transition: DIALOG_LEAVE
|
|
257
222
|
};
|
|
258
223
|
}
|
|
259
224
|
if (kind === "sheet") {
|