beez-ui 0.6.1 → 0.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +134 -82
  2. package/README.md +25 -11
  3. package/dist/components/alert.js +1 -1
  4. package/dist/components/avatar.d.ts +2 -1
  5. package/dist/components/avatar.js +4 -3
  6. package/dist/components/calendar.js +102 -0
  7. package/dist/components/carousel.js +61 -48
  8. package/dist/components/checkbox.js +1 -1
  9. package/dist/components/data-table.js +34 -4
  10. package/dist/components/dropdown-menu.js +5 -2
  11. package/dist/components/filter-query-bar.js +11 -6
  12. package/dist/components/form.d.ts +5 -0
  13. package/dist/components/form.js +11 -9
  14. package/dist/components/input-group.js +1 -1
  15. package/dist/components/input.js +1 -1
  16. package/dist/components/pagination.d.ts +1 -0
  17. package/dist/components/pagination.js +9 -4
  18. package/dist/components/radio-group.js +1 -1
  19. package/dist/components/select.js +1 -1
  20. package/dist/components/sheet.js +12 -9
  21. package/dist/components/sidebar.js +9 -5
  22. package/dist/components/skeleton.d.ts +6 -0
  23. package/dist/components/skeleton.js +7 -1
  24. package/dist/components/switch.js +1 -1
  25. package/dist/components/textarea.js +1 -1
  26. package/dist/components/typing-animation.js +42 -15
  27. package/dist/lib/column-filter-value.d.ts +12 -0
  28. package/dist/lib/column-filter-value.js +52 -0
  29. package/dist/motion/glide-indicator.d.ts +37 -3
  30. package/dist/motion/glide-indicator.js +37 -3
  31. package/dist/motion/motion-slot.js +47 -18
  32. package/dist/motion/surface-keyframes.js +18 -6
  33. package/dist/motion/tokens.d.ts +24 -1
  34. package/dist/motion/tokens.js +23 -0
  35. package/dist/providers/beez-ui-provider.js +49 -17
  36. package/dist/styles.css +1 -1
  37. package/package.json +2 -2
@@ -3,6 +3,7 @@ import { c as _c } from "react/compiler-runtime";
3
3
  import { useEffect, useRef, useState } from "react";
4
4
  import { motion } from "motion/react";
5
5
  import { usePrefersReducedMotion } from "../hooks/use-prefers-reduced-motion.js";
6
+ import { MOTION_TIMING } from "../motion/tokens.js";
6
7
  import { cn } from "../lib/utils.js";
7
8
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
9
  const motionElements = {
@@ -19,8 +20,23 @@ const motionElements = {
19
20
  section: motion.section,
20
21
  span: motion.span
21
22
  };
23
+ /** Idle cursors fade out and back in, like a text editor's caret. */
24
+ const CURSOR_BLINK_OPACITY = [
25
+ 1,
26
+ 1,
27
+ 0,
28
+ 0,
29
+ 1
30
+ ];
31
+ const CURSOR_BLINK_TIMES = [
32
+ 0,
33
+ .4,
34
+ .5,
35
+ .9,
36
+ 1
37
+ ];
22
38
  export function TypingAnimation(t0) {
23
- const $ = _c(67);
39
+ const $ = _c(68);
24
40
  let children;
25
41
  let className;
26
42
  let deleteSpeed;
@@ -293,6 +309,7 @@ export function TypingAnimation(t0) {
293
309
  const currentWordGraphemes = t19;
294
310
  const isComplete = !loop && currentWordIndex === wordsToAnimate.length - 1 && currentCharIndex >= currentWordGraphemes.length && phase !== "deleting";
295
311
  const shouldShowCursor = showCursor && !shouldReduceMotion && !isComplete && (hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length);
312
+ const isCursorIdle = !shouldStart || phase === "pause" || displayedText === "";
296
313
  let t20;
297
314
  if ($[51] !== cursorStyle) {
298
315
  t20 = () => {
@@ -328,20 +345,30 @@ export function TypingAnimation(t0) {
328
345
  t23 = $[56];
329
346
  }
330
347
  let t24;
331
- if ($[57] !== blinkCursor || $[58] !== getCursorChar || $[59] !== shouldShowCursor) {
332
- t24 = shouldShowCursor && /* @__PURE__ */ _jsx("span", {
333
- className: cn("inline-block", blinkCursor && "animate-blink-cursor"),
348
+ if ($[57] !== blinkCursor || $[58] !== getCursorChar || $[59] !== isCursorIdle || $[60] !== shouldShowCursor) {
349
+ t24 = shouldShowCursor && /* @__PURE__ */ _jsx(motion.span, {
350
+ "aria-hidden": "true",
351
+ "data-slot": "typing-animation-cursor",
352
+ className: "inline-block",
353
+ animate: { opacity: blinkCursor && isCursorIdle ? CURSOR_BLINK_OPACITY : 1 },
354
+ transition: blinkCursor && isCursorIdle ? {
355
+ duration: MOTION_TIMING.cursorBlink,
356
+ times: CURSOR_BLINK_TIMES,
357
+ repeat: Infinity,
358
+ ease: "easeInOut"
359
+ } : { duration: MOTION_TIMING.fast },
334
360
  children: getCursorChar()
335
361
  });
336
362
  $[57] = blinkCursor;
337
363
  $[58] = getCursorChar;
338
- $[59] = shouldShowCursor;
339
- $[60] = t24;
364
+ $[59] = isCursorIdle;
365
+ $[60] = shouldShowCursor;
366
+ $[61] = t24;
340
367
  } else {
341
- t24 = $[60];
368
+ t24 = $[61];
342
369
  }
343
370
  let t25;
344
- if ($[61] !== MotionComponent || $[62] !== displayedText || $[63] !== props || $[64] !== t23 || $[65] !== t24) {
371
+ if ($[62] !== MotionComponent || $[63] !== displayedText || $[64] !== props || $[65] !== t23 || $[66] !== t24) {
345
372
  t25 = /* @__PURE__ */ _jsxs(MotionComponent, {
346
373
  ref: elementRef,
347
374
  "data-slot": "typing-animation",
@@ -349,14 +376,14 @@ export function TypingAnimation(t0) {
349
376
  ...props,
350
377
  children: [displayedText, t24]
351
378
  });
352
- $[61] = MotionComponent;
353
- $[62] = displayedText;
354
- $[63] = props;
355
- $[64] = t23;
356
- $[65] = t24;
357
- $[66] = t25;
379
+ $[62] = MotionComponent;
380
+ $[63] = displayedText;
381
+ $[64] = props;
382
+ $[65] = t23;
383
+ $[66] = t24;
384
+ $[67] = t25;
358
385
  } else {
359
- t25 = $[66];
386
+ t25 = $[67];
360
387
  }
361
388
  return t25;
362
389
  }
@@ -0,0 +1,12 @@
1
+ /** Normalizes text for accent- and case-insensitive comparisons. */
2
+ export declare function normalizeFilterToken(value: string): string;
3
+ /**
4
+ * Evalúa el valor de una celda contra un filtro de columna de la barra de filtros.
5
+ * Entiende los valores estructurados (`numberRange`, `enum`, `presence`, `yearMonthRange`)
6
+ * y, para texto plano, busca coincidencias parciales sin distinguir acentos ni mayúsculas.
7
+ * Una celda sin valor comparable no cumple los rangos; un filtro vacío deja pasar la fila.
8
+ * @param cellValue - Valor de la celda tal como lo devuelve el accessor de la columna.
9
+ * @param filterValue - Valor activo del filtro de esa columna.
10
+ * @returns `true` cuando la fila debe mostrarse.
11
+ */
12
+ export declare function matchesColumnFilterValue(cellValue: unknown, filterValue: unknown): boolean;
@@ -0,0 +1,52 @@
1
+ import { matchesAdvancedYearMonthRangeFilter } from "./year-month-filter.js";
2
+ const DIACRITICS_PATTERN = /[̀-ͯ]/g;
3
+ /** Normalizes text for accent- and case-insensitive comparisons. */
4
+ export function normalizeFilterToken(value) {
5
+ return value.normalize("NFD").replace(DIACRITICS_PATTERN, "").toLocaleLowerCase().trim();
6
+ }
7
+ /** Distinguishes the grammar's structured values from plain text written by the search input. */
8
+ function isColumnFilterValue(value) {
9
+ return typeof value === "object" && value !== null && typeof value.kind === "string";
10
+ }
11
+ /** Reads numbers and numeric strings; anything else has no comparable value. */
12
+ function toFiniteNumber(value) {
13
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
14
+ if (typeof value === "string" && value.trim() !== "") {
15
+ const parsed = Number(value);
16
+ return Number.isFinite(parsed) ? parsed : null;
17
+ }
18
+ return null;
19
+ }
20
+ /** Treats `null`, `undefined`, blank strings and empty lists as a missing value. */
21
+ function isMissingValue(value) {
22
+ if (value == null) return true;
23
+ if (typeof value === "string") return value.trim() === "";
24
+ if (Array.isArray(value)) return value.length === 0;
25
+ return false;
26
+ }
27
+ /**
28
+ * Evalúa el valor de una celda contra un filtro de columna de la barra de filtros.
29
+ * Entiende los valores estructurados (`numberRange`, `enum`, `presence`, `yearMonthRange`)
30
+ * y, para texto plano, busca coincidencias parciales sin distinguir acentos ni mayúsculas.
31
+ * Una celda sin valor comparable no cumple los rangos; un filtro vacío deja pasar la fila.
32
+ * @param cellValue - Valor de la celda tal como lo devuelve el accessor de la columna.
33
+ * @param filterValue - Valor activo del filtro de esa columna.
34
+ * @returns `true` cuando la fila debe mostrarse.
35
+ */
36
+ export function matchesColumnFilterValue(cellValue, filterValue) {
37
+ if (!isColumnFilterValue(filterValue)) {
38
+ const query = normalizeFilterToken(String(filterValue ?? ""));
39
+ return query === "" || normalizeFilterToken(String(cellValue ?? "")).includes(query);
40
+ }
41
+ switch (filterValue.kind) {
42
+ case "numberRange": {
43
+ const value = toFiniteNumber(cellValue);
44
+ if (value == null) return false;
45
+ if (filterValue.min != null && value < filterValue.min) return false;
46
+ return filterValue.max == null || value <= filterValue.max;
47
+ }
48
+ case "enum": return Array.isArray(cellValue) ? cellValue.some((item) => String(item) === filterValue.value) : String(cellValue ?? "") === filterValue.value;
49
+ case "presence": return filterValue.value === "hasValue" ? !isMissingValue(cellValue) : isMissingValue(cellValue);
50
+ case "yearMonthRange": return matchesAdvancedYearMonthRangeFilter(filterValue, toFiniteNumber(cellValue));
51
+ }
52
+ }
@@ -1,4 +1,4 @@
1
- import { SPRING_LAYOUT, SPRING_TABS } from "./tokens.js";
1
+ import { SPRING_HIGHLIGHT, SPRING_LAYOUT, SPRING_TABS } from "./tokens.js";
2
2
  /** Which part of the active item carries the highlight that must travel. */
3
3
  type GlideSurface = "background" | "after";
4
4
  /** Describes the items of one container and how their active state is exposed. */
@@ -13,7 +13,13 @@ export interface GlidePreset {
13
13
  attributes: string[];
14
14
  isActive: (item: Element) => boolean;
15
15
  surface: (container: HTMLElement) => GlideSurface;
16
- spring: typeof SPRING_LAYOUT | typeof SPRING_TABS;
16
+ spring: typeof SPRING_LAYOUT | typeof SPRING_TABS | typeof SPRING_HIGHLIGHT;
17
+ /**
18
+ * Highlights that follow the pointer (menus, listboxes) jump straight to the hovered item:
19
+ * the pointer already marks the position, so a travelling copy would only trail behind it.
20
+ * The glide is kept for keyboard navigation.
21
+ */
22
+ instantOnPointer?: boolean;
17
23
  }
18
24
  export declare const GLIDE_PRESETS: {
19
25
  tabs: {
@@ -42,7 +48,7 @@ export declare const GLIDE_PRESETS: {
42
48
  readonly mass: 0.6;
43
49
  };
44
50
  };
45
- menu: {
51
+ pagination: {
46
52
  itemSelector: string;
47
53
  containerSelector: string;
48
54
  attributes: string[];
@@ -55,6 +61,34 @@ export declare const GLIDE_PRESETS: {
55
61
  readonly mass: 0.6;
56
62
  };
57
63
  };
64
+ suggestions: {
65
+ itemSelector: string;
66
+ containerSelector: string;
67
+ attributes: string[];
68
+ isActive: (item: Element) => boolean;
69
+ surface: () => "background";
70
+ spring: {
71
+ readonly type: "spring";
72
+ readonly stiffness: 700;
73
+ readonly damping: 45;
74
+ readonly mass: 0.5;
75
+ };
76
+ instantOnPointer: true;
77
+ };
78
+ menu: {
79
+ itemSelector: string;
80
+ containerSelector: string;
81
+ attributes: string[];
82
+ isActive: (item: Element) => boolean;
83
+ surface: () => "background";
84
+ spring: {
85
+ readonly type: "spring";
86
+ readonly stiffness: 700;
87
+ readonly damping: 45;
88
+ readonly mass: 0.5;
89
+ };
90
+ instantOnPointer: true;
91
+ };
58
92
  };
59
93
  export type GlidePresetName = keyof typeof GLIDE_PRESETS;
60
94
  /** Marks the item whose own highlight is hidden while the travelling copy stands in for it. */
@@ -4,7 +4,7 @@
4
4
  * previous item and is removed once it lands, leaving the item's own styles in charge again.
5
5
  */
6
6
  import { animate } from "motion/react";
7
- import { GLIDE_CONTINUITY_MS, SPRING_LAYOUT, SPRING_TABS } from "./tokens.js";
7
+ import { GLIDE_CONTINUITY_MS, SPRING_HIGHLIGHT, SPRING_LAYOUT, SPRING_TABS } from "./tokens.js";
8
8
  export const GLIDE_PRESETS = {
9
9
  tabs: {
10
10
  itemSelector: "[data-slot=\"tabs-trigger\"]",
@@ -22,13 +22,31 @@ export const GLIDE_PRESETS = {
22
22
  surface: () => "background",
23
23
  spring: SPRING_LAYOUT
24
24
  },
25
+ pagination: {
26
+ itemSelector: "[data-slot=\"pagination-link\"]",
27
+ containerSelector: "[data-slot=\"pagination-content\"]",
28
+ attributes: ["aria-current"],
29
+ isActive: (item) => item.getAttribute("aria-current") === "page",
30
+ surface: () => "background",
31
+ spring: SPRING_LAYOUT
32
+ },
33
+ suggestions: {
34
+ itemSelector: "[role=\"option\"]",
35
+ containerSelector: "[role=\"listbox\"]",
36
+ attributes: ["aria-selected"],
37
+ isActive: (item) => item.getAttribute("aria-selected") === "true",
38
+ surface: () => "background",
39
+ spring: SPRING_HIGHLIGHT,
40
+ instantOnPointer: true
41
+ },
25
42
  menu: {
26
43
  itemSelector: "[role=\"menuitem\"],[role=\"menuitemcheckbox\"],[role=\"menuitemradio\"],[role=\"option\"]",
27
44
  containerSelector: "[role=\"menu\"],[role=\"listbox\"]",
28
45
  attributes: ["data-highlighted"],
29
46
  isActive: (item) => item.hasAttribute("data-highlighted"),
30
47
  surface: () => "background",
31
- spring: SPRING_LAYOUT
48
+ spring: SPRING_HIGHLIGHT,
49
+ instantOnPointer: true
32
50
  }
33
51
  };
34
52
  /** Marks the item whose own highlight is hidden while the travelling copy stands in for it. */
@@ -136,6 +154,20 @@ export function attachGlideIndicator(container, preset) {
136
154
  position: container.style.position,
137
155
  isolation: container.style.isolation
138
156
  };
157
+ // The last input decides whether a highlight change was driven by the pointer or the keyboard.
158
+ // Keyboard focus may live outside the container (a combobox input), so both are read from the
159
+ // document; the pointer only counts while it moves over this container.
160
+ let isPointerDriven = false;
161
+ const onPointerMove = () => {
162
+ isPointerDriven = true;
163
+ };
164
+ const onKeyDown = () => {
165
+ isPointerDriven = false;
166
+ };
167
+ if (preset.instantOnPointer) {
168
+ container.addEventListener("pointermove", onPointerMove);
169
+ container.ownerDocument.addEventListener("keydown", onKeyDown, true);
170
+ }
139
171
  /** Puts the item's own highlight back and removes the travelling copy. */
140
172
  function settle() {
141
173
  animation?.cancel();
@@ -236,7 +268,7 @@ export function attachGlideIndicator(container, preset) {
236
268
  const isContinuous = previous !== null && previous !== active && previous.isConnected && !preset.isActive(previous) && (lastActiveSeenAt === Number.NEGATIVE_INFINITY || now - lastActiveSeenAt <= GLIDE_CONTINUITY_MS);
237
269
  lastActive = active;
238
270
  lastActiveSeenAt = Number.NEGATIVE_INFINITY;
239
- if (isContinuous) glide(previous, active);
271
+ if (isContinuous && !(preset.instantOnPointer && isPointerDriven)) glide(previous, active);
240
272
  else settle();
241
273
  });
242
274
  observer.observe(container, {
@@ -247,6 +279,8 @@ export function attachGlideIndicator(container, preset) {
247
279
  });
248
280
  return () => {
249
281
  observer.disconnect();
282
+ container.removeEventListener("pointermove", onPointerMove);
283
+ container.ownerDocument.removeEventListener("keydown", onKeyDown, true);
250
284
  settle();
251
285
  };
252
286
  }
@@ -6,7 +6,7 @@ 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 } 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, SPRING_CHEVRON, SPRING_PANEL, SPRING_PRESS, SPRING_THUMB } from "./tokens.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
10
  import { surfaceEnter, surfaceExit } from "./surface-keyframes.js";
11
11
  import { useAttachedElement } from "./use-attached-element.js";
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
@@ -277,16 +277,14 @@ export function MotionSlot(t0) {
277
277
  });
278
278
  return dispose;
279
279
  }
280
- if (kind === "skeleton") {
281
- animations.set(animate(element, { opacity: [
282
- 1,
283
- .6,
284
- 1
285
- ] }, {
286
- duration: MOTION_TIMING.skeleton,
287
- repeat: Infinity,
288
- ease: "easeInOut"
289
- }), ["opacity"]);
280
+ if (kind === "skeleton" && typeof element.animate === "function") {
281
+ const shimmer = element.animate({ translate: [...MOTION_SHIMMER_TRANSLATE] }, {
282
+ duration: MOTION_TIMING.skeleton * MILLISECONDS_PER_SECOND,
283
+ easing: `cubic-bezier(${MOTION_EASE_IN_OUT.join(", ")})`,
284
+ iterations: Infinity,
285
+ pseudoElement: "::after"
286
+ });
287
+ cleanups.push(() => shimmer.cancel());
290
288
  } else {
291
289
  if (kind === "icon-swap") {
292
290
  play({
@@ -398,6 +396,13 @@ export function MotionSlot(t0) {
398
396
  }));
399
397
  }
400
398
  }
399
+ let restingShadow = "none";
400
+ const withRestingShadow = (shadow) => {
401
+ if (!element.style.boxShadow) {
402
+ restingShadow = getComputedStyle(element).boxShadow;
403
+ }
404
+ return restingShadow === "none" ? shadow : `${restingShadow}, ${shadow}`;
405
+ };
401
406
  if (kind === "card" || kind === "link" || kind === "icon") {
402
407
  cleanups.push(hover(element, () => {
403
408
  if (!isEnabled(element) || !canHover() || kind === "card" && !element.querySelector("a,button")) {
@@ -405,7 +410,9 @@ export function MotionSlot(t0) {
405
410
  }
406
411
  const keep = { restoreOnComplete: false };
407
412
  if (kind === "card") {
408
- play({ boxShadow: "0 4px 16px rgba(0,0,0,0.08)" }, tween(MOTION_TIMING.enter), keep);
413
+ const rest = withRestingShadow(MOTION_CARD_SHADOW_REST);
414
+ const from = element.style.boxShadow ? getComputedStyle(element).boxShadow : rest;
415
+ play({ boxShadow: [from, withRestingShadow(MOTION_CARD_SHADOW_LIFT)] }, tween(MOTION_TIMING.enter), keep);
409
416
  } else {
410
417
  if (kind === "icon") {
411
418
  play({ transform: ["rotate(0deg) scale(1)", "rotate(-3deg) scale(1.025)"] }, SPRING_PRESS, keep);
@@ -415,7 +422,7 @@ export function MotionSlot(t0) {
415
422
  }
416
423
  return () => {
417
424
  if (kind === "card") {
418
- play({ boxShadow: original.boxShadow || "0 0px 0px rgba(0,0,0,0)" }, tween(MOTION_TIMING.fast));
425
+ play({ boxShadow: [getComputedStyle(element).boxShadow, withRestingShadow(MOTION_CARD_SHADOW_REST)] }, tween(MOTION_TIMING.fast));
419
426
  } else {
420
427
  if (kind === "icon") {
421
428
  play({ transform: [currentTransform(element), "rotate(0deg) scale(1)"] }, SPRING_PRESS);
@@ -544,6 +551,28 @@ export function MotionSlot(t0) {
544
551
  }
545
552
  cleanups.push(() => observer_1.disconnect());
546
553
  }
554
+ if (kind === "reveal") {
555
+ const isLoading = () => element.hasAttribute("data-loading");
556
+ let wasLoading = isLoading();
557
+ const observer_2 = new MutationObserver(() => {
558
+ const loading = isLoading();
559
+ const loaded = wasLoading && !loading && !element.hasAttribute("data-error");
560
+ wasLoading = loading;
561
+ if (!loaded) {
562
+ return;
563
+ }
564
+ play({
565
+ opacity: [0, 1],
566
+ transform: [`scale(${MOTION_REVEAL_SCALE})`, "scale(1)"],
567
+ filter: [`blur(${MOTION_REVEAL_BLUR_PX}px)`, "blur(0px)"]
568
+ }, tween(MOTION_TIMING.reveal));
569
+ });
570
+ observer_2.observe(element, {
571
+ attributes: true,
572
+ attributeFilter: ["data-loading", "data-error"]
573
+ });
574
+ cleanups.push(() => observer_2.disconnect());
575
+ }
547
576
  if (kind === "thumb") {
548
577
  const restingOffset = () => {
549
578
  if (!element.getClientRects().length) {
@@ -565,7 +594,7 @@ export function MotionSlot(t0) {
565
594
  element.parentElement?.removeEventListener("pointerenter", remeasure);
566
595
  element.parentElement?.removeEventListener("focusin", remeasure);
567
596
  });
568
- const observer_2 = new MutationObserver(() => {
597
+ const observer_3 = new MutationObserver(() => {
569
598
  const glideOffset = translateXOf(getComputedStyle(element).transform);
570
599
  cancel(["transform"]);
571
600
  restore(["transform"]);
@@ -576,11 +605,11 @@ export function MotionSlot(t0) {
576
605
  play({ transform: [`translateX(${delta_0}px)`, "translateX(0px)"] }, SPRING_THUMB);
577
606
  }
578
607
  });
579
- observer_2.observe(element, {
608
+ observer_3.observe(element, {
580
609
  attributes: true,
581
610
  attributeFilter: ["data-state"]
582
611
  });
583
- cleanups.push(() => observer_2.disconnect());
612
+ cleanups.push(() => observer_3.disconnect());
584
613
  const root = element.parentElement;
585
614
  if (root && typeof element.animate === "function") {
586
615
  const squishTiming = {
@@ -597,9 +626,9 @@ export function MotionSlot(t0) {
597
626
  squish?.cancel();
598
627
  squish = element.animate({ scale: ["1", String(MOTION_THUMB_SQUISH_SCALE)] }, squishTiming);
599
628
  return () => {
600
- const from = getComputedStyle(element).scale;
629
+ const from_0 = getComputedStyle(element).scale;
601
630
  squish?.cancel();
602
- const release = element.animate({ scale: [from === "none" ? "1" : from, "1"] }, squishTiming);
631
+ const release = element.animate({ scale: [from_0 === "none" ? "1" : from_0, "1"] }, squishTiming);
603
632
  squish = release;
604
633
  release.finished.then(() => {
605
634
  if (squish === release) {
@@ -1,5 +1,6 @@
1
- import { MOTION_DIALOG_DISTANCE, MOTION_EASE, 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_EASE, MOTION_EASE_DRAWER, MOTION_SURFACE_CLIP_BLEED_PX, MOTION_SURFACE_CLIP_PERCENT, MOTION_SURFACE_SCALE, MOTION_TIMING, MOTION_TOOLTIP_DISTANCE, SPRING_PANEL, SPRING_TOOLTIP } from "./tokens.js";
2
2
  const EASE_OUT = [...MOTION_EASE];
3
+ const EASE_DRAWER = [...MOTION_EASE_DRAWER];
3
4
  const SIDES = [
4
5
  "top",
5
6
  "right",
@@ -48,18 +49,25 @@ function offsetAwayFromTrigger(side, distance) {
48
49
  y: 0
49
50
  };
50
51
  }
52
+ /**
53
+ * Open edges extend the clip past the box, so the ring and drop shadow drawn outside it
54
+ * (Tailwind rings are box-shadows) are never cut while the surface reveals or settles.
55
+ */
56
+ const OPEN_EDGE = `-${MOTION_SURFACE_CLIP_BLEED_PX}px`;
57
+ /** Fully revealed clip: nothing of the surface, its outline or its shadow is hidden. */
58
+ const REVEALED_CLIP = `inset(${OPEN_EDGE} ${OPEN_EDGE} ${OPEN_EDGE} ${OPEN_EDGE})`;
51
59
  /** Collapsed clip anchored at the corner or edge closest to the trigger. */
52
60
  function collapsedClip(side, align) {
53
61
  const hidden = `${MOTION_SURFACE_CLIP_PERCENT}%`;
54
62
  const half = `${MOTION_SURFACE_CLIP_PERCENT / 2}%`;
55
63
  /** Maps an alignment to the inset kept on the leading and trailing edges of the cross axis. */
56
- const crossAxis = (value) => value === "start" ? ["0%", hidden] : value === "end" ? [hidden, "0%"] : [half, half];
64
+ const crossAxis = (value) => value === "start" ? [OPEN_EDGE, hidden] : value === "end" ? [hidden, OPEN_EDGE] : [half, half];
57
65
  if (side === "bottom" || side === "top") {
58
66
  const [left, right] = crossAxis(align);
59
- return side === "bottom" ? `inset(0% ${right} ${hidden} ${left})` : `inset(${hidden} ${right} 0% ${left})`;
67
+ return side === "bottom" ? `inset(${OPEN_EDGE} ${right} ${hidden} ${left})` : `inset(${hidden} ${right} ${OPEN_EDGE} ${left})`;
60
68
  }
61
69
  const [top, bottom] = crossAxis(align);
62
- return side === "right" ? `inset(${top} ${hidden} ${bottom} 0%)` : `inset(${top} 0% ${bottom} ${hidden})`;
70
+ return side === "right" ? `inset(${top} ${hidden} ${bottom} ${OPEN_EDGE})` : `inset(${top} ${OPEN_EDGE} ${bottom} ${hidden})`;
63
71
  }
64
72
  /** Edge-panel travel: fully off-screen on the side the sheet is attached to. */
65
73
  function sheetOffset(element) {
@@ -136,7 +144,7 @@ export function surfaceEnter(element, kind) {
136
144
  };
137
145
  // Primitives aligned over their trigger (Select item-aligned) have no side to reveal from.
138
146
  if (side) {
139
- keyframes.clipPath = [collapsedClip(side, align), "inset(0% 0% 0% 0%)"];
147
+ keyframes.clipPath = [collapsedClip(side, align), REVEALED_CLIP];
140
148
  }
141
149
  return {
142
150
  keyframes,
@@ -190,9 +198,13 @@ export function surfaceExit(element, kind) {
190
198
  }
191
199
  if (kind === "sheet") {
192
200
  const offset = sheetOffset(element);
201
+ // A spring would linger at the edge; the drawer curve accelerates the panel away.
193
202
  return {
194
203
  keyframes: { transform: [transform("0%", "0%"), transform(offset.x, offset.y)] },
195
- transition: SPRING_PANEL
204
+ transition: {
205
+ duration: MOTION_TIMING.sheetExit,
206
+ ease: EASE_DRAWER
207
+ }
196
208
  };
197
209
  }
198
210
  return {
@@ -16,6 +16,10 @@ export declare const MOTION_TIMING: {
16
16
  readonly listStagger: 0.035;
17
17
  readonly closeEnter: 0.2;
18
18
  readonly closeDelay: 0.16;
19
+ readonly sheetExit: 0.24;
20
+ readonly reveal: 0.4;
21
+ readonly monthSwap: 0.24;
22
+ readonly cursorBlink: 1;
19
23
  readonly skeleton: 1.8;
20
24
  };
21
25
  /** Items beyond this position share the last stagger delay, so long lists never lag behind. */
@@ -47,6 +51,13 @@ export declare const SPRING_LAYOUT: {
47
51
  readonly damping: 32;
48
52
  readonly mass: 0.6;
49
53
  };
54
+ /** Keyboard highlight in menus and lists: quick enough to keep up with a held arrow key. */
55
+ export declare const SPRING_HIGHLIGHT: {
56
+ readonly type: "spring";
57
+ readonly stiffness: 700;
58
+ readonly damping: 45;
59
+ readonly mass: 0.5;
60
+ };
50
61
  /** Tab indicator glide, tuned to settle without overshooting the list edges. */
51
62
  export declare const SPRING_TABS: {
52
63
  readonly type: "spring";
@@ -84,13 +95,25 @@ export declare const MOTION_ITEM_CHECK_SCALE = 0.75;
84
95
  export declare const MOTION_ICON_SWAP_SCALE = 0.25;
85
96
  export declare const MOTION_CLOSE_BUTTON_SCALE = 0.8;
86
97
  export declare const MOTION_LIST_ITEM_DISTANCE = 6;
98
+ /** Loaded images settle from a slight zoom, so they develop in place instead of popping in. */
99
+ export declare const MOTION_REVEAL_SCALE = 1.06;
100
+ export declare const MOTION_REVEAL_BLUR_PX = 6;
101
+ /** Calendar months slide just enough to show the direction of navigation. */
102
+ export declare const MOTION_MONTH_DISTANCE = 12;
103
+ /** Skeleton shine travels from fully before the box to fully past it, as a `translate` value. */
104
+ export declare const MOTION_SHIMMER_TRANSLATE: readonly ["-100% 0", "100% 0"];
105
+ /** Hovered interactive cards gain a soft drop shadow; the rest value has the same shape, invisible. */
106
+ export declare const MOTION_CARD_SHADOW_LIFT = "0px 4px 16px 0px rgba(0, 0, 0, 0.08)";
107
+ export declare const MOTION_CARD_SHADOW_REST = "0px 0px 0px 0px rgba(0, 0, 0, 0)";
87
108
  export declare const MOTION_SURFACE_SCALE = 0.96;
88
109
  /** Fraction of the surface kept hidden by the clip at the start of its reveal, in percent. */
89
110
  export declare const MOTION_SURFACE_CLIP_PERCENT = 92;
111
+ /** Room left outside a revealing surface for its ring and drop shadow (`shadow-lg` spans ~22px). */
112
+ export declare const MOTION_SURFACE_CLIP_BLEED_PX = 24;
90
113
  export declare const MOTION_TOOLTIP_DISTANCE = 8;
91
114
  export declare const MOTION_DIALOG_DISTANCE = 20;
92
115
  export declare const MOTION_CONTENT_DISTANCE = 4;
93
116
  export declare const MOTION_INVALID_SHAKE_PX: readonly [0, -6, 6, -4, 4, -2, 0];
94
117
  /** Time after an item loses its active state during which a new active item still glides from it. */
95
118
  export declare const GLIDE_CONTINUITY_MS = 300;
96
- export type MotionKind = "press" | "subtle-press" | "toggle" | "fade" | "content" | "message" | "card" | "row" | "field" | "thumb" | "check" | "selection" | "item-check" | "rotate" | "list-item" | "icon-swap" | "delayed-pop" | "skeleton" | "link" | "icon" | "tooltip" | "surface" | "dialog" | "sheet" | "overlay";
119
+ export type MotionKind = "press" | "subtle-press" | "toggle" | "fade" | "content" | "message" | "card" | "row" | "field" | "thumb" | "check" | "selection" | "item-check" | "rotate" | "list-item" | "icon-swap" | "delayed-pop" | "skeleton" | "reveal" | "link" | "icon" | "tooltip" | "surface" | "dialog" | "sheet" | "overlay";
@@ -16,6 +16,10 @@ export const MOTION_TIMING = {
16
16
  listStagger: .035,
17
17
  closeEnter: .2,
18
18
  closeDelay: .16,
19
+ sheetExit: .24,
20
+ reveal: .4,
21
+ monthSwap: .24,
22
+ cursorBlink: 1,
19
23
  skeleton: 1.8
20
24
  };
21
25
  /** Items beyond this position share the last stagger delay, so long lists never lag behind. */
@@ -62,6 +66,13 @@ export const SPRING_LAYOUT = {
62
66
  damping: 32,
63
67
  mass: .6
64
68
  };
69
+ /** Keyboard highlight in menus and lists: quick enough to keep up with a held arrow key. */
70
+ export const SPRING_HIGHLIGHT = {
71
+ type: "spring",
72
+ stiffness: 700,
73
+ damping: 45,
74
+ mass: .5
75
+ };
65
76
  /** Tab indicator glide, tuned to settle without overshooting the list edges. */
66
77
  export const SPRING_TABS = {
67
78
  type: "spring",
@@ -99,9 +110,21 @@ export const MOTION_ITEM_CHECK_SCALE = .75;
99
110
  export const MOTION_ICON_SWAP_SCALE = .25;
100
111
  export const MOTION_CLOSE_BUTTON_SCALE = .8;
101
112
  export const MOTION_LIST_ITEM_DISTANCE = 6;
113
+ /** Loaded images settle from a slight zoom, so they develop in place instead of popping in. */
114
+ export const MOTION_REVEAL_SCALE = 1.06;
115
+ export const MOTION_REVEAL_BLUR_PX = 6;
116
+ /** Calendar months slide just enough to show the direction of navigation. */
117
+ export const MOTION_MONTH_DISTANCE = 12;
118
+ /** Skeleton shine travels from fully before the box to fully past it, as a `translate` value. */
119
+ export const MOTION_SHIMMER_TRANSLATE = ["-100% 0", "100% 0"];
120
+ /** Hovered interactive cards gain a soft drop shadow; the rest value has the same shape, invisible. */
121
+ export const MOTION_CARD_SHADOW_LIFT = "0px 4px 16px 0px rgba(0, 0, 0, 0.08)";
122
+ export const MOTION_CARD_SHADOW_REST = "0px 0px 0px 0px rgba(0, 0, 0, 0)";
102
123
  export const MOTION_SURFACE_SCALE = .96;
103
124
  /** Fraction of the surface kept hidden by the clip at the start of its reveal, in percent. */
104
125
  export const MOTION_SURFACE_CLIP_PERCENT = 92;
126
+ /** Room left outside a revealing surface for its ring and drop shadow (`shadow-lg` spans ~22px). */
127
+ export const MOTION_SURFACE_CLIP_BLEED_PX = 24;
105
128
  export const MOTION_TOOLTIP_DISTANCE = 8;
106
129
  export const MOTION_DIALOG_DISTANCE = 20;
107
130
  export const MOTION_CONTENT_DISTANCE = 4;