@passui/react 0.1.45 → 0.1.47

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 (141) hide show
  1. package/README.md +57 -46
  2. package/dist/types/src/components/accordion/accordion.d.ts +1 -1
  3. package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +1 -1
  4. package/dist/types/src/components/inline-disclosure/inline-disclosure.d.ts +1 -1
  5. package/dist/types/src/components/popover/anchored-surface.d.ts +5 -1
  6. package/dist/types/src/components/popover/index.d.ts +41 -0
  7. package/dist/types/src/components/popover/popover-content.d.ts +38 -0
  8. package/dist/types/src/components/popover/popover-root.d.ts +22 -0
  9. package/dist/types/src/components/popover/popover-trigger.d.ts +23 -0
  10. package/dist/types/src/components/stepper/rolling-digit.d.ts +1 -1
  11. package/dist/types/src/components/table/table.d.ts +1 -1
  12. package/dist/types/src/components/view-controller/view-controller.d.ts +2 -2
  13. package/dist/types/src/index.d.ts +1 -1
  14. package/dist/types/src/machines/popover.machine.d.ts +30 -0
  15. package/package.json +307 -307
  16. package/src/appearance/index.ts +2 -2
  17. package/src/appearance/platform.tsx +76 -60
  18. package/src/appearance/theme.tsx +409 -377
  19. package/src/components/accordion/accordion.tsx +58 -49
  20. package/src/components/ambient/ambient.tsx +25 -25
  21. package/src/components/ambient/index.ts +1 -1
  22. package/src/components/aspect-ratio/aspect-ratio.tsx +21 -16
  23. package/src/components/breadcrumb/breadcrumb.tsx +63 -57
  24. package/src/components/button/button-styles.ts +39 -32
  25. package/src/components/button/button.tsx +91 -84
  26. package/src/components/button/icon-button.tsx +27 -25
  27. package/src/components/button/pending-spinner.tsx +8 -8
  28. package/src/components/button/use-pending-indicator.ts +11 -11
  29. package/src/components/chip/chip.tsx +35 -30
  30. package/src/components/code-block/code-block.tsx +40 -39
  31. package/src/components/combo-box/combo-box.tsx +5 -5
  32. package/src/components/command/command.tsx +124 -94
  33. package/src/components/compact-date-picker/calendar-month.ts +75 -53
  34. package/src/components/compact-date-picker/compact-date-picker.tsx +308 -289
  35. package/src/components/compact-time-picker/compact-time-picker.tsx +288 -281
  36. package/src/components/compact-time-picker/time-value.ts +32 -29
  37. package/src/components/content-unavailable-view/content-unavailable-view.tsx +64 -48
  38. package/src/components/content-view/content-view.tsx +168 -159
  39. package/src/components/content-view/index.ts +1 -1
  40. package/src/components/context-menu/conceal-menu-source.ts +25 -25
  41. package/src/components/context-menu/context-menu-host.tsx +439 -402
  42. package/src/components/context-menu/context-menu-store.ts +125 -123
  43. package/src/components/context-menu/menu-navigation.ts +25 -18
  44. package/src/components/context-menu/menu-position.ts +33 -29
  45. package/src/components/context-menu/menu-reveal.ts +32 -32
  46. package/src/components/context-menu/use-context-menu.ts +63 -56
  47. package/src/components/conversation/conversation.tsx +265 -263
  48. package/src/components/conversation/index.ts +1 -1
  49. package/src/components/date-picker/date-picker.tsx +95 -94
  50. package/src/components/date-picker/date-value.ts +20 -10
  51. package/src/components/dialog/dialog-host.tsx +105 -103
  52. package/src/components/dialog/dialog-store.ts +37 -36
  53. package/src/components/dialog/dialog.tsx +192 -179
  54. package/src/components/dialog/index.ts +1 -1
  55. package/src/components/disclosure-group/disclosure-group.tsx +93 -86
  56. package/src/components/divider/divider.tsx +24 -22
  57. package/src/components/inline-disclosure/inline-disclosure.tsx +40 -36
  58. package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +23 -19
  59. package/src/components/labeled-content/labeled-content.tsx +68 -68
  60. package/src/components/locale-switcher/index.ts +1 -1
  61. package/src/components/locale-switcher/locale-switcher.tsx +278 -251
  62. package/src/components/menu/menu.tsx +75 -58
  63. package/src/components/morph/index.ts +2 -2
  64. package/src/components/morph/morph.tsx +227 -201
  65. package/src/components/morph/use-morph-disclosure.ts +19 -19
  66. package/src/components/one-time-code/one-time-code.tsx +216 -200
  67. package/src/components/picker/picker-trigger.ts +16 -13
  68. package/src/components/picker/picker.tsx +371 -345
  69. package/src/components/popover/anchored-surface.tsx +253 -229
  70. package/src/components/popover/index.ts +50 -0
  71. package/src/components/popover/popover-content.tsx +374 -0
  72. package/src/components/popover/popover-root.tsx +97 -0
  73. package/src/components/popover/popover-trigger.tsx +71 -0
  74. package/src/components/profile-image/profile-image.tsx +80 -64
  75. package/src/components/progress-view/progress-view.tsx +61 -59
  76. package/src/components/progress-view/use-liquid-progress.ts +38 -38
  77. package/src/components/radio-group/radio-group.tsx +71 -68
  78. package/src/components/search-field/expanding-search-field.tsx +191 -177
  79. package/src/components/search-field/search-field.tsx +59 -50
  80. package/src/components/search-list/search-list.tsx +246 -236
  81. package/src/components/section/section.tsx +58 -58
  82. package/src/components/secure-field/secure-field.tsx +3 -3
  83. package/src/components/segmented-control/segmented-control.tsx +136 -136
  84. package/src/components/sheet/modal-environment.ts +71 -64
  85. package/src/components/sheet/sheet-portal.tsx +58 -53
  86. package/src/components/sheet/sheet.tsx +146 -141
  87. package/src/components/sheet/use-sheet-focus.ts +97 -82
  88. package/src/components/sidebar/sidebar.tsx +78 -78
  89. package/src/components/skeleton/skeleton.tsx +30 -21
  90. package/src/components/slider/slider.tsx +113 -104
  91. package/src/components/slosh-slider/liquid-step.ts +32 -31
  92. package/src/components/slosh-slider/slosh-slider.tsx +265 -228
  93. package/src/components/stat-tile/index.ts +1 -1
  94. package/src/components/stat-tile/stat-tile.tsx +79 -68
  95. package/src/components/status-indicator/status-indicator.tsx +40 -37
  96. package/src/components/stepper/rolling-digit.tsx +82 -68
  97. package/src/components/stepper/stepper.tsx +122 -113
  98. package/src/components/swatch-picker/swatch-picker.tsx +120 -120
  99. package/src/components/tab-bar/tab-bar.tsx +149 -148
  100. package/src/components/tab-view/tab-view.tsx +140 -137
  101. package/src/components/table/table.tsx +117 -98
  102. package/src/components/text-editor/text-editor.tsx +34 -29
  103. package/src/components/text-field/text-field.tsx +182 -180
  104. package/src/components/toast/toast-store.ts +127 -115
  105. package/src/components/toast/toaster.tsx +361 -347
  106. package/src/components/toggle/toggle.tsx +297 -282
  107. package/src/components/tooltip/tooltip-host.tsx +157 -150
  108. package/src/components/view-controller/index.ts +1 -1
  109. package/src/components/view-controller/view-controller.tsx +64 -53
  110. package/src/components/wake-slider/wake-slider.tsx +220 -205
  111. package/src/hooks/scroll/minimize-decision.ts +11 -7
  112. package/src/hooks/use-anchored-disclosure.ts +19 -19
  113. package/src/hooks/use-anchored-placement.ts +29 -25
  114. package/src/hooks/use-dismiss.ts +33 -29
  115. package/src/hooks/use-floating-dismiss.ts +24 -22
  116. package/src/hooks/use-media-query.ts +67 -60
  117. package/src/hooks/use-minimize-on-scroll.ts +69 -60
  118. package/src/hooks/use-visual-viewport.ts +63 -63
  119. package/src/hooks/viewport/types.ts +16 -16
  120. package/src/hooks/viewport/viewport-metrics.ts +16 -10
  121. package/src/icons/icons.tsx +440 -328
  122. package/src/index.ts +91 -91
  123. package/src/lib/anchored-placement.ts +70 -65
  124. package/src/lib/class-names.ts +14 -13
  125. package/src/lib/clipboard.ts +8 -7
  126. package/src/lib/date.ts +61 -60
  127. package/src/lib/floating-layers.ts +39 -24
  128. package/src/lib/haptics.ts +10 -10
  129. package/src/lib/language.ts +9 -9
  130. package/src/lib/range-value.ts +27 -21
  131. package/src/lib/screen-edges.ts +33 -30
  132. package/src/lib/shapes.ts +38 -34
  133. package/src/machines/popover.machine.ts +51 -0
  134. package/src/motion/transitions.ts +46 -43
  135. package/src/styles/code-block.css +69 -43
  136. package/src/styles/motion-controls.css +393 -191
  137. package/src/tokens.ts +1 -1
  138. package/dist/types/src/components/popover/popover-fit.d.ts +0 -21
  139. package/dist/types/src/components/popover/popover.d.ts +0 -21
  140. package/src/components/popover/popover-fit.ts +0 -69
  141. package/src/components/popover/popover.tsx +0 -334
@@ -1,68 +1,85 @@
1
- import { forwardRef, type ReactNode, useRef, useSyncExternalStore } from "react"
2
- import { Button, type ButtonProps } from "../button/button"
3
1
  import {
4
- closeContextMenu,
5
- getContextMenu,
6
- openMenu,
7
- subscribeContextMenu,
8
- type ContextMenuEdge,
9
- type ContextMenuItem,
10
- } from "../context-menu/context-menu-store"
2
+ forwardRef,
3
+ type ReactNode,
4
+ useRef,
5
+ useSyncExternalStore,
6
+ } from "react";
7
+ import { Button, type ButtonProps } from "../button/button";
8
+ import {
9
+ closeContextMenu,
10
+ getContextMenu,
11
+ openMenu,
12
+ subscribeContextMenu,
13
+ type ContextMenuEdge,
14
+ type ContextMenuItem,
15
+ } from "../context-menu/context-menu-store";
11
16
 
12
17
  export interface MenuProps extends Omit<ButtonProps, "onClick" | "children"> {
13
- /** Accessible name for the menu trigger. */
14
- label: string
15
- /** Items are rendered by the app's existing ContextMenuHost. */
16
- items: readonly ContextMenuItem[]
17
- children: ReactNode
18
- /** Logical edge used to align the menu, including RTL. @default "leading" */
19
- edge?: ContextMenuEdge
20
- /** Expand upward from the trigger instead of downward. @default false */
21
- above?: boolean
18
+ /** Accessible name for the menu trigger. */
19
+ label: string;
20
+ /** Items are rendered by the app's existing ContextMenuHost. */
21
+ items: readonly ContextMenuItem[];
22
+ children: ReactNode;
23
+ /** Logical edge used to align the menu, including RTL. @default "leading" */
24
+ edge?: ContextMenuEdge;
25
+ /** Expand upward from the trigger instead of downward. @default false */
26
+ above?: boolean;
22
27
  }
23
28
 
24
29
  /** A declarative menu button using the same native host as context menus. */
25
30
  export const Menu = forwardRef<HTMLButtonElement, MenuProps>(function Menu(
26
- { label, items, children, edge = "leading", above = false, disabled, ...props },
27
- forwardedRef
31
+ {
32
+ label,
33
+ items,
34
+ children,
35
+ edge = "leading",
36
+ above = false,
37
+ disabled,
38
+ ...props
39
+ },
40
+ forwardedRef,
28
41
  ) {
29
- const trigger = useRef<HTMLButtonElement>(null)
30
- const current = useSyncExternalStore(subscribeContextMenu, getContextMenu, () => null)
31
- const expanded = Boolean(current && current.anchor === trigger.current)
42
+ const trigger = useRef<HTMLButtonElement>(null);
43
+ const current = useSyncExternalStore(
44
+ subscribeContextMenu,
45
+ getContextMenu,
46
+ () => null,
47
+ );
48
+ const expanded = Boolean(current && current.anchor === trigger.current);
32
49
 
33
- const toggle = () => {
34
- if (!trigger.current || disabled) return
35
- if (expanded) closeContextMenu(false)
36
- else openMenu(trigger.current, [...items], { edge, above })
37
- }
50
+ const toggle = () => {
51
+ if (!trigger.current || disabled) return;
52
+ if (expanded) closeContextMenu(false);
53
+ else openMenu(trigger.current, [...items], { edge, above });
54
+ };
38
55
 
39
- return (
40
- <Button
41
- {...props}
42
- ref={(node) => {
43
- trigger.current = node
44
- if (typeof forwardedRef === "function") forwardedRef(node)
45
- else if (forwardedRef) forwardedRef.current = node
46
- }}
47
- type="button"
48
- aria-label={label}
49
- aria-haspopup="menu"
50
- aria-expanded={expanded}
51
- disabled={disabled}
52
- onClick={toggle}
53
- onKeyDown={(event) => {
54
- props.onKeyDown?.(event)
55
- if (event.defaultPrevented) return
56
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
57
- event.preventDefault()
58
- if (!expanded) toggle()
59
- } else if (event.key === "Escape" && expanded) {
60
- event.preventDefault()
61
- closeContextMenu()
62
- }
63
- }}
64
- >
65
- {children}
66
- </Button>
67
- )
68
- })
56
+ return (
57
+ <Button
58
+ {...props}
59
+ ref={(node) => {
60
+ trigger.current = node;
61
+ if (typeof forwardedRef === "function") forwardedRef(node);
62
+ else if (forwardedRef) forwardedRef.current = node;
63
+ }}
64
+ type="button"
65
+ aria-label={label}
66
+ aria-haspopup="menu"
67
+ aria-expanded={expanded}
68
+ disabled={disabled}
69
+ onClick={toggle}
70
+ onKeyDown={(event) => {
71
+ props.onKeyDown?.(event);
72
+ if (event.defaultPrevented) return;
73
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
74
+ event.preventDefault();
75
+ if (!expanded) toggle();
76
+ } else if (event.key === "Escape" && expanded) {
77
+ event.preventDefault();
78
+ closeContextMenu();
79
+ }
80
+ }}
81
+ >
82
+ {children}
83
+ </Button>
84
+ );
85
+ });
@@ -1,2 +1,2 @@
1
- export * from "./morph"
2
- export * from "./use-morph-disclosure"
1
+ export * from "./morph";
2
+ export * from "./use-morph-disclosure";
@@ -1,42 +1,61 @@
1
- import { AnimatePresence, type HTMLMotionProps, motion, useReducedMotion } from "motion/react"
2
- import { type ReactNode, type RefObject, useLayoutEffect, useRef, useState } from "react"
3
- import { concealMenuSource } from "../context-menu/conceal-menu-source"
4
- import { menuSurfaceExit, menuSurfaceReveal, type MenuRect } from "../context-menu/menu-reveal"
5
- import { useSheetFocus } from "../sheet/use-sheet-focus"
6
- import { SheetPortal } from "../sheet/sheet-portal"
7
- import { cn } from "../../lib/class-names"
8
- import { contentCrossfade, fluidCollapse, fluidOvershoot } from "../../motion/transitions"
1
+ import {
2
+ AnimatePresence,
3
+ type HTMLMotionProps,
4
+ motion,
5
+ useReducedMotion,
6
+ } from "motion/react";
7
+ import {
8
+ type ReactNode,
9
+ type RefObject,
10
+ useLayoutEffect,
11
+ useRef,
12
+ useState,
13
+ } from "react";
14
+ import { concealMenuSource } from "../context-menu/conceal-menu-source";
15
+ import {
16
+ menuSurfaceExit,
17
+ menuSurfaceReveal,
18
+ type MenuRect,
19
+ } from "../context-menu/menu-reveal";
20
+ import { useSheetFocus } from "../sheet/use-sheet-focus";
21
+ import { SheetPortal } from "../sheet/sheet-portal";
22
+ import { cn } from "../../lib/class-names";
23
+ import {
24
+ contentCrossfade,
25
+ fluidCollapse,
26
+ fluidOvershoot,
27
+ } from "../../motion/transitions";
9
28
 
10
- export type MorphSize = "sm" | "md" | "lg"
29
+ export type MorphSize = "sm" | "md" | "lg";
11
30
 
12
31
  const outerPaddings: Record<MorphSize, string> = {
13
- sm: "p-4",
14
- md: "p-6",
15
- lg: "p-8",
16
- }
32
+ sm: "p-4",
33
+ md: "p-6",
34
+ lg: "p-8",
35
+ };
17
36
  const panelMaxWidths: Record<MorphSize, string> = {
18
- sm: "max-w-sm",
19
- md: "max-w-[720px]",
20
- lg: "max-w-3xl",
21
- }
37
+ sm: "max-w-sm",
38
+ md: "max-w-[720px]",
39
+ lg: "max-w-3xl",
40
+ };
22
41
 
23
42
  export interface MorphProps extends Omit<HTMLMotionProps<"div">, "children"> {
24
- /** Controls whether the overlay is presented. */
25
- open: boolean
26
- /** Called on Escape or outside press. */
27
- onClose: () => void
28
- /** The element whose rect the overlay grows from and collapses back to. */
29
- sourceRef: RefObject<HTMLElement | null>
30
- /** Accessible name (used when there is no labelledBy). */
31
- label: string
32
- /** ID of a visible heading inside the overlay. */
33
- labelledBy?: string
34
- /** Allow tapping the backdrop to close. Escape still calls onClose. @default true */
35
- dismissOnOutsidePress?: boolean
36
- /** Outer padding and panel max-width. @default "md" */
37
- size?: MorphSize
38
- className?: string
39
- children?: ReactNode
43
+ /** Controls whether the overlay is presented. */
44
+ open: boolean;
45
+ /** Called on Escape or outside press. */
46
+ onClose: () => void;
47
+ /** The element whose rect the overlay grows from and collapses back to. */
48
+ sourceRef: RefObject<HTMLElement | null>;
49
+ /** Accessible name (used when there is no labelledBy). */
50
+ label: string;
51
+ /** ID of a visible heading inside the overlay. */
52
+ labelledBy?: string;
53
+ /** Allow tapping the backdrop to close. Escape still calls onClose. @default true */
54
+ dismissOnOutsidePress?: boolean;
55
+ /** Outer padding and panel max-width. @default "md" */
56
+ size?: MorphSize;
57
+ className?: string;
58
+ children?: ReactNode;
40
59
  }
41
60
 
42
61
  /**
@@ -50,183 +69,190 @@ export interface MorphProps extends Omit<HTMLMotionProps<"div">, "children"> {
50
69
  * primitive that powers anchored `Popover` and `ContextMenu`.
51
70
  */
52
71
  export function Morph({ open, ...props }: MorphProps) {
53
- return <AnimatePresence>{open && <OpenMorph {...props} />}</AnimatePresence>
72
+ return <AnimatePresence>{open && <OpenMorph {...props} />}</AnimatePresence>;
54
73
  }
55
74
 
56
75
  function OpenMorph({
57
- onClose,
58
- sourceRef,
59
- label,
60
- labelledBy,
61
- dismissOnOutsidePress = true,
62
- size = "md",
63
- className,
64
- children,
65
- ...props
76
+ onClose,
77
+ sourceRef,
78
+ label,
79
+ labelledBy,
80
+ dismissOnOutsidePress = true,
81
+ size = "md",
82
+ className,
83
+ children,
84
+ ...props
66
85
  }: Omit<MorphProps, "open">) {
67
- const panel = useRef<HTMLDivElement>(null)
68
- const reducedMotion = useReducedMotion()
69
- const [settled, setSettled] = useState(false)
70
- const [target, setTarget] = useState<{ width: number; height: number; radius: number } | null>(
71
- null
72
- )
73
- const [reveal, setReveal] = useState<ReturnType<typeof menuSurfaceReveal> | null>(null)
74
- useSheetFocus(panel, onClose)
86
+ const panel = useRef<HTMLDivElement>(null);
87
+ const reducedMotion = useReducedMotion();
88
+ const [settled, setSettled] = useState(false);
89
+ const [target, setTarget] = useState<{
90
+ width: number;
91
+ height: number;
92
+ radius: number;
93
+ } | null>(null);
94
+ const [reveal, setReveal] = useState<ReturnType<
95
+ typeof menuSurfaceReveal
96
+ > | null>(null);
97
+ useSheetFocus(panel, onClose);
75
98
 
76
- // Measure the panel's resting rect and the source's rect, then compute
77
- // the reveal (initial material state) from menuSurfaceReveal. Re-measures
78
- // on resize/scroll so the morph stays aligned if either moves.
79
- useLayoutEffect(() => {
80
- const surface = panel.current
81
- const source = sourceRef.current
82
- if (!surface) return
83
- const measure = () => {
84
- const panelRect = surface.getBoundingClientRect()
85
- const radius = Number.parseFloat(getComputedStyle(surface).borderTopLeftRadius) || 0
86
- setTarget((current) =>
87
- current &&
88
- current.width === panelRect.width &&
89
- current.height === panelRect.height &&
90
- current.radius === radius
91
- ? current
92
- : { width: panelRect.width, height: panelRect.height, radius }
93
- )
94
- const sourceRect = source?.getBoundingClientRect()
95
- const sourceArg: MenuRect | undefined =
96
- sourceRect && sourceRect.width > 0 && sourceRect.height > 0
97
- ? {
98
- left: sourceRect.left,
99
- top: sourceRect.top,
100
- width: sourceRect.width,
101
- height: sourceRect.height,
102
- }
103
- : undefined
104
- setReveal(
105
- menuSurfaceReveal(sourceArg, {
106
- left: panelRect.left,
107
- top: panelRect.top,
108
- width: panelRect.width,
109
- height: panelRect.height,
110
- radius,
111
- })
112
- )
113
- }
114
- measure()
115
- let frame = 0
116
- const schedule = () => {
117
- cancelAnimationFrame(frame)
118
- frame = requestAnimationFrame(measure)
119
- }
120
- const observer = new ResizeObserver(schedule)
121
- observer.observe(surface)
122
- if (source) observer.observe(source)
123
- const viewport = window.visualViewport
124
- viewport?.addEventListener("resize", schedule)
125
- viewport?.addEventListener("scroll", schedule)
126
- window.addEventListener("resize", schedule)
127
- window.addEventListener("scroll", schedule, true)
128
- return () => {
129
- observer.disconnect()
130
- cancelAnimationFrame(frame)
131
- viewport?.removeEventListener("resize", schedule)
132
- viewport?.removeEventListener("scroll", schedule)
133
- window.removeEventListener("resize", schedule)
134
- window.removeEventListener("scroll", schedule, true)
135
- }
136
- }, [sourceRef])
99
+ // Measure the panel's resting rect and the source's rect, then compute
100
+ // the reveal (initial material state) from menuSurfaceReveal. Re-measures
101
+ // on resize/scroll so the morph stays aligned if either moves.
102
+ useLayoutEffect(() => {
103
+ const surface = panel.current;
104
+ const source = sourceRef.current;
105
+ if (!surface) return;
106
+ const measure = () => {
107
+ const panelRect = surface.getBoundingClientRect();
108
+ const radius =
109
+ Number.parseFloat(getComputedStyle(surface).borderTopLeftRadius) || 0;
110
+ setTarget((current) =>
111
+ current &&
112
+ current.width === panelRect.width &&
113
+ current.height === panelRect.height &&
114
+ current.radius === radius
115
+ ? current
116
+ : { width: panelRect.width, height: panelRect.height, radius },
117
+ );
118
+ const sourceRect = source?.getBoundingClientRect();
119
+ const sourceArg: MenuRect | undefined =
120
+ sourceRect && sourceRect.width > 0 && sourceRect.height > 0
121
+ ? {
122
+ left: sourceRect.left,
123
+ top: sourceRect.top,
124
+ width: sourceRect.width,
125
+ height: sourceRect.height,
126
+ }
127
+ : undefined;
128
+ setReveal(
129
+ menuSurfaceReveal(sourceArg, {
130
+ left: panelRect.left,
131
+ top: panelRect.top,
132
+ width: panelRect.width,
133
+ height: panelRect.height,
134
+ radius,
135
+ }),
136
+ );
137
+ };
138
+ measure();
139
+ let frame = 0;
140
+ const schedule = () => {
141
+ cancelAnimationFrame(frame);
142
+ frame = requestAnimationFrame(measure);
143
+ };
144
+ const observer = new ResizeObserver(schedule);
145
+ observer.observe(surface);
146
+ if (source) observer.observe(source);
147
+ const viewport = window.visualViewport;
148
+ viewport?.addEventListener("resize", schedule);
149
+ viewport?.addEventListener("scroll", schedule);
150
+ window.addEventListener("resize", schedule);
151
+ window.addEventListener("scroll", schedule, true);
152
+ return () => {
153
+ observer.disconnect();
154
+ cancelAnimationFrame(frame);
155
+ viewport?.removeEventListener("resize", schedule);
156
+ viewport?.removeEventListener("scroll", schedule);
157
+ window.removeEventListener("resize", schedule);
158
+ window.removeEventListener("scroll", schedule, true);
159
+ };
160
+ }, [sourceRef]);
137
161
 
138
- // Conceal the source while the morph is mounted; restore on unmount.
139
- useLayoutEffect(() => {
140
- const source = sourceRef.current
141
- if (!source) return
142
- const restore = concealMenuSource(source, false)
143
- return () => {
144
- restore()
145
- }
146
- }, [sourceRef])
162
+ // Conceal the source while the morph is mounted; restore on unmount.
163
+ useLayoutEffect(() => {
164
+ const source = sourceRef.current;
165
+ if (!source) return;
166
+ const restore = concealMenuSource(source, false);
167
+ return () => {
168
+ restore();
169
+ };
170
+ }, [sourceRef]);
147
171
 
148
- return (
149
- <SheetPortal>
150
- <div
151
- className={cn(
152
- "fixed inset-0 z-[75] flex items-center justify-center",
153
- outerPaddings[size]
154
- )}
155
- >
156
- <motion.div
157
- aria-hidden="true"
158
- className="absolute inset-0 bg-black/30 backdrop-blur-md"
159
- initial={{ opacity: 0 }}
160
- animate={{ opacity: 1 }}
161
- exit={{ opacity: 0 }}
162
- transition={reducedMotion ? { duration: 0 } : { duration: 0.18 }}
163
- onClick={dismissOnOutsidePress ? onClose : undefined}
164
- />
165
- <motion.div
166
- ref={panel}
167
- role="dialog"
168
- aria-modal="true"
169
- aria-label={labelledBy ? undefined : label}
170
- aria-labelledby={labelledBy}
171
- tabIndex={-1}
172
- className={cn(
173
- "relative isolate flex w-full flex-col rounded-5xl outline-none",
174
- "max-h-[92%]",
175
- panelMaxWidths[size],
176
- settled && "overflow-hidden",
177
- className
178
- )}
179
- {...props}
180
- >
181
- {/* The morphing material — mirrors Popover's data-popover-surface
172
+ return (
173
+ <SheetPortal>
174
+ <div
175
+ className={cn(
176
+ "fixed inset-0 z-[75] flex items-center justify-center",
177
+ outerPaddings[size],
178
+ )}
179
+ >
180
+ <motion.div
181
+ aria-hidden="true"
182
+ className="absolute inset-0 bg-black/30 backdrop-blur-md"
183
+ initial={{ opacity: 0 }}
184
+ animate={{ opacity: 1 }}
185
+ exit={{ opacity: 0 }}
186
+ transition={reducedMotion ? { duration: 0 } : { duration: 0.18 }}
187
+ onClick={dismissOnOutsidePress ? onClose : undefined}
188
+ />
189
+ <motion.div
190
+ ref={panel}
191
+ role="dialog"
192
+ aria-modal="true"
193
+ aria-label={labelledBy ? undefined : label}
194
+ aria-labelledby={labelledBy}
195
+ tabIndex={-1}
196
+ className={cn(
197
+ "rounded-5xl relative isolate flex w-full flex-col outline-none",
198
+ "max-h-[92%]",
199
+ panelMaxWidths[size],
200
+ settled && "overflow-hidden",
201
+ className,
202
+ )}
203
+ {...props}
204
+ >
205
+ {/* The morphing material — mirrors Popover's data-popover-surface
182
206
  pattern. Starts at the source's rect (offset relative to the
183
207
  panel) and grows to fill the panel, with the fluid overshoot
184
208
  bounce. Border radius interpolates from the source's
185
209
  "pillified" radius to the panel's authored radius. */}
186
- <motion.div
187
- key={reveal ? "surface" : "measure"}
188
- aria-hidden="true"
189
- data-morph-surface=""
190
- onAnimationComplete={() => {
191
- if (reveal) setSettled(true)
192
- }}
193
- className="glass-menu pointer-events-none absolute top-0 left-0 -z-10 rounded-5xl"
194
- style={{ width: "100%", height: "100%" }}
195
- initial={reducedMotion || !reveal ? { opacity: 0 } : reveal}
196
- animate={
197
- target
198
- ? {
199
- x: 0,
200
- y: 0,
201
- width: target.width,
202
- height: target.height,
203
- opacity: 1,
204
- borderRadius: target.radius,
205
- }
206
- : undefined
207
- }
208
- exit={
209
- reducedMotion || !reveal
210
- ? { opacity: 0 }
211
- : { ...menuSurfaceExit(reveal), transition: fluidCollapse }
212
- }
213
- transition={
214
- reducedMotion
215
- ? { duration: 0 }
216
- : { ...fluidOvershoot, opacity: { duration: 0.18 } }
217
- }
218
- />
219
- <motion.div
220
- className="flex min-h-0 flex-1 flex-col overflow-y-auto"
221
- initial={reducedMotion ? false : contentCrossfade.initial}
222
- animate={reducedMotion ? undefined : contentCrossfade.animate}
223
- exit={reducedMotion ? undefined : contentCrossfade.exit}
224
- transition={reducedMotion ? { duration: 0 } : contentCrossfade.transition}
225
- >
226
- {children}
227
- </motion.div>
228
- </motion.div>
229
- </div>
230
- </SheetPortal>
231
- )
210
+ <motion.div
211
+ key={reveal ? "surface" : "measure"}
212
+ aria-hidden="true"
213
+ data-morph-surface=""
214
+ onAnimationComplete={() => {
215
+ if (reveal) setSettled(true);
216
+ }}
217
+ className="glass-menu rounded-5xl pointer-events-none absolute top-0 left-0 -z-10"
218
+ style={{ width: "100%", height: "100%" }}
219
+ initial={reducedMotion || !reveal ? { opacity: 0 } : reveal}
220
+ animate={
221
+ target
222
+ ? {
223
+ x: 0,
224
+ y: 0,
225
+ width: target.width,
226
+ height: target.height,
227
+ opacity: 1,
228
+ borderRadius: target.radius,
229
+ }
230
+ : undefined
231
+ }
232
+ exit={
233
+ reducedMotion || !reveal
234
+ ? { opacity: 0 }
235
+ : { ...menuSurfaceExit(reveal), transition: fluidCollapse }
236
+ }
237
+ transition={
238
+ reducedMotion
239
+ ? { duration: 0 }
240
+ : { ...fluidOvershoot, opacity: { duration: 0.18 } }
241
+ }
242
+ />
243
+ <motion.div
244
+ className="flex min-h-0 flex-1 flex-col overflow-y-auto"
245
+ initial={reducedMotion ? false : contentCrossfade.initial}
246
+ animate={reducedMotion ? undefined : contentCrossfade.animate}
247
+ exit={reducedMotion ? undefined : contentCrossfade.exit}
248
+ transition={
249
+ reducedMotion ? { duration: 0 } : contentCrossfade.transition
250
+ }
251
+ >
252
+ {children}
253
+ </motion.div>
254
+ </motion.div>
255
+ </div>
256
+ </SheetPortal>
257
+ );
232
258
  }
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from "react"
1
+ import { useCallback, useEffect, useState } from "react";
2
2
 
3
3
  /**
4
4
  * Disclosure state that keeps the overlay mounted through its exit animation,
@@ -6,22 +6,22 @@ import { useCallback, useEffect, useState } from "react"
6
6
  * for the source.
7
7
  */
8
8
  export function useMorphDisclosure(disabled = false) {
9
- const [open, setOpen] = useState(false)
10
- const [mounted, setMounted] = useState(false)
11
- useEffect(() => {
12
- if (disabled) setOpen(false)
13
- }, [disabled])
14
- const show = useCallback(() => {
15
- setMounted(true)
16
- setOpen(true)
17
- }, [])
18
- const toggle = useCallback(() => {
19
- setMounted(true)
20
- setOpen((value) => !value)
21
- }, [])
22
- const close = useCallback(() => setOpen(false), [])
23
- const onExited = useCallback(() => {
24
- if (!open || disabled) setMounted(false)
25
- }, [open, disabled])
26
- return { open: open && !disabled, mounted, show, toggle, close, onExited }
9
+ const [open, setOpen] = useState(false);
10
+ const [mounted, setMounted] = useState(false);
11
+ useEffect(() => {
12
+ if (disabled) setOpen(false);
13
+ }, [disabled]);
14
+ const show = useCallback(() => {
15
+ setMounted(true);
16
+ setOpen(true);
17
+ }, []);
18
+ const toggle = useCallback(() => {
19
+ setMounted(true);
20
+ setOpen((value) => !value);
21
+ }, []);
22
+ const close = useCallback(() => setOpen(false), []);
23
+ const onExited = useCallback(() => {
24
+ if (!open || disabled) setMounted(false);
25
+ }, [open, disabled]);
26
+ return { open: open && !disabled, mounted, show, toggle, close, onExited };
27
27
  }