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.
@@ -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
- /** Converts a viewport rectangle into the container's scrolling coordinate space. */
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
- /** Translation an item's own running entrance adds, read from its computed `matrix()`. */
61
- function inFlightTranslation(item) {
62
- const values = /^matrix\(([^)]+)\)$/.exec(getComputedStyle(item).transform);
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` bar for line variants.
75
- * An item still sliding into place (staggered Select options) is measured where it will
76
- * settle, so the highlight does not land offset and jump when the entrance ends.
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 box = item.getBoundingClientRect();
80
- const offset = inFlightTranslation(item);
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: box.left - offset.x,
83
- top: box.top - offset.y,
84
- width: box.width,
85
- height: box.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 origin = overlay ? overlay.getBoundingClientRect() : highlightBox(from, surface);
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 = toContainerBox(container, highlightBox(to, surface));
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, MOTION_INVALID_SHAKE_PX, MOTION_ITEM_CHECK_SCALE, MOTION_ICON_SWAP_SCALE, MOTION_CLOSE_BUTTON_SCALE, MOTION_LIST_ITEM_DISTANCE, MOTION_LIST_STAGGER_LIMIT, 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_PRESS, SPRING_THUMB } from "./tokens.js";
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 = snapshot;
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 keyframes_0 = { ...step.keyframes };
261
- if (!isPresent && interrupted) {
262
- Object.entries(interrupted).forEach((t10) => {
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
- duration: MOTION_TIMING.iconSwap,
305
- ease: [...MOTION_EASE_IN_OUT]
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
- const index = listIndexOf(element);
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: [`translateY(${-MOTION_LIST_ITEM_DISTANCE}px)`, "translateY(0px)"],
323
- filter: ["blur(3px)", "blur(0px)"]
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: [`translateY(${-MOTION_CONTENT_DISTANCE}px)`, "translateY(0px)"],
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 isGroup = element.getAttribute("data-slot") === INPUT_GROUP_SLOT;
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(_temp4);
452
+ strokeDraws.forEach(_temp3);
481
453
  strokeDraws.clear();
482
- element.querySelectorAll("path").forEach(_temp5);
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(_temp6);
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 _temp6() {}
701
- function _temp5(path) {
672
+ function _temp5() {}
673
+ function _temp4(path) {
702
674
  STROKE_DRAW_ATTRIBUTES.forEach((attribute) => path.removeAttribute(attribute));
703
675
  }
704
- function _temp4(draw) {
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 matching the beui surface family for each kind. */
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 { MOTION_CENTER_MORPH_FOLDED_PERCENT, MOTION_DIALOG_DISTANCE, MOTION_EASE, MOTION_EASE_DRAWER, MOTION_EASE_UNFOLD, MOTION_SURFACE_CLIP_BLEED_PX, MOTION_SURFACE_CLIP_PERCENT, MOTION_SURFACE_SCALE, MOTION_TIMING, MOTION_TOOLTIP_DISTANCE, SPRING_PANEL, SPRING_TOOLTIP } from "./tokens.js";
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 EASE_UNFOLD = [...MOTION_EASE_UNFOLD];
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
- * Folded and unfolded clips of a center-morph surface. Both share the surface's own corners,
101
- * so the shape matches the panel once unfolded and the clip never spends its last frames
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
- function centerMorphClips(element) {
105
- const round = bleedingCornerRadii(element);
106
- const folded = `${MOTION_CENTER_MORPH_FOLDED_PERCENT}%`;
107
- return {
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 matching the beui surface family for each kind. */
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, .9), transform(0, 0)],
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
- ...SPRING_TOOLTIP,
154
- opacity: {
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
- if (kind === "dialog") {
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, .97), transform(0, 0)]
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
- duration: MOTION_TIMING.centerMorph,
180
- ease: EASE_UNFOLD
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: exitTween
199
+ transition: DIALOG_LEAVE
224
200
  };
225
201
  }
226
202
  if (kind === "tooltip") {
227
- const offset = offsetAwayFromTrigger(readPlacement(element).side, MOTION_TOOLTIP_DISTANCE * .6);
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(offset.x, offset.y, .94)],
232
- filter: ["blur(0px)", "blur(3px)"]
207
+ transform: [transform(0, 0), transform(0, 0, MOTION_EXIT_SCALE)]
233
208
  },
234
- transition: exitTween
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, MOTION_DIALOG_DISTANCE, .98)]
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") {