@passui/react 0.1.19 → 0.1.21

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 (84) hide show
  1. package/README.md +38 -0
  2. package/THIRD_PARTY_NOTICES.md +25 -0
  3. package/dist/types/src/components/accordion/accordion.d.ts +19 -0
  4. package/dist/types/src/components/aspect-ratio/aspect-ratio.d.ts +7 -0
  5. package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +16 -0
  6. package/dist/types/src/components/button/button.d.ts +2 -0
  7. package/dist/types/src/components/code-block/code-block.d.ts +10 -0
  8. package/dist/types/src/components/combo-box/combo-box.d.ts +9 -0
  9. package/dist/types/src/components/command/command.d.ts +18 -0
  10. package/dist/types/src/components/content-unavailable-view/content-unavailable-view.d.ts +9 -0
  11. package/dist/types/src/components/context-menu/context-menu-store.d.ts +2 -2
  12. package/dist/types/src/components/context-menu/menu-reveal.d.ts +3 -8
  13. package/dist/types/src/components/dialog/dialog-store.d.ts +3 -1
  14. package/dist/types/src/components/disclosure-group/disclosure-group.d.ts +3 -1
  15. package/dist/types/src/components/keyboard-shortcut/keyboard-shortcut.d.ts +5 -0
  16. package/dist/types/src/components/menu/menu.d.ts +1 -1
  17. package/dist/types/src/components/one-time-code/one-time-code.d.ts +23 -0
  18. package/dist/types/src/components/picker/picker.d.ts +7 -1
  19. package/dist/types/src/components/popover/anchored-surface.d.ts +13 -0
  20. package/dist/types/src/components/popover/popover.d.ts +5 -1
  21. package/dist/types/src/components/progress-view/progress-view.d.ts +1 -1
  22. package/dist/types/src/components/progress-view/use-liquid-progress.d.ts +3 -0
  23. package/dist/types/src/components/radio-group/radio-group.d.ts +17 -0
  24. package/dist/types/src/components/search-field/expanding-search-field.d.ts +5 -0
  25. package/dist/types/src/components/search-field/search-field.d.ts +5 -1
  26. package/dist/types/src/components/sheet/sheet.d.ts +2 -0
  27. package/dist/types/src/components/sidebar/sidebar.d.ts +3 -2
  28. package/dist/types/src/components/skeleton/skeleton.d.ts +6 -0
  29. package/dist/types/src/components/slosh-slider/liquid-step.d.ts +6 -0
  30. package/dist/types/src/components/slosh-slider/slosh-slider.d.ts +18 -0
  31. package/dist/types/src/components/stepper/rolling-digit.d.ts +11 -0
  32. package/dist/types/src/components/text-field/text-field.d.ts +3 -1
  33. package/dist/types/src/components/wake-slider/wake-slider.d.ts +12 -0
  34. package/dist/types/src/hooks/use-anchored-disclosure.d.ts +9 -0
  35. package/dist/types/src/hooks/use-anchored-placement.d.ts +1 -1
  36. package/dist/types/src/index.d.ts +13 -0
  37. package/dist/types/src/lib/anchored-placement.d.ts +2 -2
  38. package/package.json +60 -4
  39. package/src/components/accordion/accordion.tsx +48 -0
  40. package/src/components/aspect-ratio/aspect-ratio.tsx +22 -0
  41. package/src/components/breadcrumb/breadcrumb.tsx +53 -0
  42. package/src/components/button/button.tsx +5 -1
  43. package/src/components/code-block/code-block.tsx +41 -0
  44. package/src/components/combo-box/combo-box.tsx +13 -0
  45. package/src/components/command/command.tsx +119 -0
  46. package/src/components/compact-date-picker/compact-date-picker.tsx +40 -84
  47. package/src/components/compact-time-picker/compact-time-picker.tsx +44 -81
  48. package/src/components/content-unavailable-view/content-unavailable-view.tsx +37 -0
  49. package/src/components/context-menu/context-menu-host.tsx +28 -24
  50. package/src/components/context-menu/context-menu-store.ts +3 -4
  51. package/src/components/context-menu/menu-reveal.ts +5 -2
  52. package/src/components/dialog/dialog-host.tsx +13 -7
  53. package/src/components/dialog/dialog-store.ts +15 -4
  54. package/src/components/disclosure-group/disclosure-group.tsx +19 -10
  55. package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +17 -0
  56. package/src/components/menu/menu.tsx +1 -1
  57. package/src/components/one-time-code/one-time-code.tsx +198 -0
  58. package/src/components/picker/picker.tsx +121 -108
  59. package/src/components/popover/anchored-surface.tsx +223 -0
  60. package/src/components/popover/popover.tsx +149 -20
  61. package/src/components/progress-view/progress-view.tsx +6 -1
  62. package/src/components/progress-view/use-liquid-progress.ts +43 -0
  63. package/src/components/radio-group/radio-group.tsx +63 -0
  64. package/src/components/search-field/expanding-search-field.tsx +183 -0
  65. package/src/components/search-field/search-field.tsx +41 -25
  66. package/src/components/search-list/search-list.tsx +16 -7
  67. package/src/components/sheet/sheet.tsx +4 -1
  68. package/src/components/sidebar/sidebar.tsx +21 -12
  69. package/src/components/skeleton/skeleton.tsx +19 -0
  70. package/src/components/slosh-slider/liquid-step.ts +42 -0
  71. package/src/components/slosh-slider/slosh-slider.tsx +251 -0
  72. package/src/components/stepper/rolling-digit.tsx +77 -0
  73. package/src/components/stepper/stepper.tsx +50 -29
  74. package/src/components/text-field/text-field.tsx +45 -7
  75. package/src/components/toggle/toggle.tsx +147 -30
  76. package/src/components/wake-slider/wake-slider.tsx +237 -0
  77. package/src/hooks/use-anchored-disclosure.ts +23 -0
  78. package/src/hooks/use-anchored-placement.ts +7 -4
  79. package/src/icons/icons.tsx +150 -150
  80. package/src/index.ts +18 -0
  81. package/src/lib/anchored-placement.ts +7 -3
  82. package/src/styles/code-block.css +33 -0
  83. package/src/styles/motion-controls.css +189 -0
  84. package/src/styles/styles.css +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passui/react",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Passui: native React controls, design tokens, presentation and appearance.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,10 +17,15 @@
17
17
  "src",
18
18
  "README.md",
19
19
  "LICENSE",
20
- "dist/types"
20
+ "dist/types",
21
+ "THIRD_PARTY_NOTICES.md"
21
22
  ],
22
23
  "type": "module",
23
24
  "exports": {
25
+ "./accordion": {
26
+ "types": "./dist/types/src/components/accordion/accordion.d.ts",
27
+ "default": "./src/components/accordion/accordion.tsx"
28
+ },
24
29
  "./icons": {
25
30
  "types": "./dist/types/src/icons/icons.d.ts",
26
31
  "default": "./src/icons/icons.tsx"
@@ -209,6 +214,54 @@
209
214
  "./inline-disclosure": {
210
215
  "types": "./dist/types/src/components/inline-disclosure/inline-disclosure.d.ts",
211
216
  "default": "./src/components/inline-disclosure/inline-disclosure.tsx"
217
+ },
218
+ "./code-block": {
219
+ "types": "./dist/types/src/components/code-block/code-block.d.ts",
220
+ "default": "./src/components/code-block/code-block.tsx"
221
+ },
222
+ "./keyboard-shortcut": {
223
+ "types": "./dist/types/src/components/keyboard-shortcut/keyboard-shortcut.d.ts",
224
+ "default": "./src/components/keyboard-shortcut/keyboard-shortcut.tsx"
225
+ },
226
+ "./content-unavailable-view": {
227
+ "types": "./dist/types/src/components/content-unavailable-view/content-unavailable-view.d.ts",
228
+ "default": "./src/components/content-unavailable-view/content-unavailable-view.tsx"
229
+ },
230
+ "./combo-box": {
231
+ "types": "./dist/types/src/components/combo-box/combo-box.d.ts",
232
+ "default": "./src/components/combo-box/combo-box.tsx"
233
+ },
234
+ "./radio-group": {
235
+ "types": "./dist/types/src/components/radio-group/radio-group.d.ts",
236
+ "default": "./src/components/radio-group/radio-group.tsx"
237
+ },
238
+ "./breadcrumb": {
239
+ "types": "./dist/types/src/components/breadcrumb/breadcrumb.d.ts",
240
+ "default": "./src/components/breadcrumb/breadcrumb.tsx"
241
+ },
242
+ "./skeleton": {
243
+ "types": "./dist/types/src/components/skeleton/skeleton.d.ts",
244
+ "default": "./src/components/skeleton/skeleton.tsx"
245
+ },
246
+ "./command": {
247
+ "types": "./dist/types/src/components/command/command.d.ts",
248
+ "default": "./src/components/command/command.tsx"
249
+ },
250
+ "./slosh-slider": {
251
+ "types": "./dist/types/src/components/slosh-slider/slosh-slider.d.ts",
252
+ "default": "./src/components/slosh-slider/slosh-slider.tsx"
253
+ },
254
+ "./wake-slider": {
255
+ "types": "./dist/types/src/components/wake-slider/wake-slider.d.ts",
256
+ "default": "./src/components/wake-slider/wake-slider.tsx"
257
+ },
258
+ "./one-time-code": {
259
+ "types": "./dist/types/src/components/one-time-code/one-time-code.d.ts",
260
+ "default": "./src/components/one-time-code/one-time-code.tsx"
261
+ },
262
+ "./aspect-ratio": {
263
+ "types": "./dist/types/src/components/aspect-ratio/aspect-ratio.d.ts",
264
+ "default": "./src/components/aspect-ratio/aspect-ratio.tsx"
212
265
  }
213
266
  },
214
267
  "scripts": {
@@ -216,7 +269,7 @@
216
269
  "test": "vitest run",
217
270
  "typecheck": "tsc -p tsconfig.json",
218
271
  "publish:check": "bun pm pack --dry-run",
219
- "build": "bun run generate && tsc -p tsconfig.build.json",
272
+ "build": "bun run generate && bun ../../scripts/build-types.mjs",
220
273
  "prepack": "bun run build"
221
274
  },
222
275
  "packageManager": "bun@1.4.2",
@@ -245,5 +298,8 @@
245
298
  "tw-animate-css": "^1.4.0",
246
299
  "@passui/tokens": "^0.1.1"
247
300
  },
248
- "types": "./dist/types/src/index.d.ts"
301
+ "types": "./dist/types/src/index.d.ts",
302
+ "sideEffects": [
303
+ "**/*.css"
304
+ ]
249
305
  }
@@ -0,0 +1,48 @@
1
+ import type { ReactNode } from "react"
2
+ import { DisclosureGroup } from "../disclosure-group/disclosure-group"
3
+ import { cn } from "../../lib/class-names"
4
+
5
+ export interface AccordionItem {
6
+ /** Stable value used to identify the expanded item. */
7
+ value: string
8
+ title: string
9
+ content: ReactNode
10
+ description?: string
11
+ disabled?: boolean
12
+ }
13
+
14
+ export interface AccordionProps {
15
+ items: readonly AccordionItem[]
16
+ /** The expanded item, or `null` when all items are closed. */
17
+ value: string | null
18
+ /** Selecting the open item reports `null`, so every item can collapse. */
19
+ onValueChange: (value: string | null) => void
20
+ className?: string
21
+ }
22
+
23
+ /** A single-open, collapsible accordion with labelled animated content regions. */
24
+ export function Accordion({ items, value, onValueChange, className }: AccordionProps) {
25
+ return (
26
+ <div
27
+ className={cn(
28
+ "flex w-full min-w-0 flex-col overflow-hidden rounded-2xl border border-separator/60 bg-elevated",
29
+ className
30
+ )}
31
+ >
32
+ {items.map((item, index) => (
33
+ <DisclosureGroup
34
+ key={item.value}
35
+ label={item.title}
36
+ description={item.description}
37
+ open={value === item.value}
38
+ disabled={item.disabled}
39
+ triggerClassName="rounded-none"
40
+ onOpenChange={(open) => onValueChange(open ? item.value : null)}
41
+ className={cn(index > 0 && "border-t border-separator/60")}
42
+ >
43
+ {item.content}
44
+ </DisclosureGroup>
45
+ ))}
46
+ </div>
47
+ )
48
+ }
@@ -0,0 +1,22 @@
1
+ import type { HTMLAttributes } from "react"
2
+ import { cn } from "../../lib/class-names"
3
+
4
+ export interface AspectRatioProps extends HTMLAttributes<HTMLDivElement> {
5
+ /** Width divided by height. @default 1.3333333333333333 */
6
+ ratio?: number
7
+ }
8
+
9
+ /** A crop frame: size its geometry, while images cover without distortion. */
10
+ export function AspectRatio({ ratio = 4 / 3, className, style, ...props }: AspectRatioProps) {
11
+ const aspect = Number.isFinite(ratio) && ratio > 0 ? ratio : 4 / 3
12
+ return (
13
+ <div
14
+ {...props}
15
+ className={cn(
16
+ "relative w-full overflow-hidden [&>img]:size-full [&>img]:object-cover [&>video]:size-full [&>video]:object-cover",
17
+ className
18
+ )}
19
+ style={{ aspectRatio: aspect, ...style }}
20
+ />
21
+ )
22
+ }
@@ -0,0 +1,53 @@
1
+ import type { ReactNode } from "react"
2
+ import { cn } from "../../lib/class-names"
3
+
4
+ export interface BreadcrumbItem {
5
+ label: string
6
+ href?: string
7
+ }
8
+ export interface BreadcrumbProps {
9
+ label: string
10
+ items: readonly BreadcrumbItem[]
11
+ className?: string
12
+ /** Integrate a client router without nesting interactive elements. */
13
+ renderLink?: (item: BreadcrumbItem & { href: string }) => ReactNode
14
+ }
15
+ /** Ancestors stay links; the final item identifies the current page. */
16
+ export function Breadcrumb({ label, items, className, renderLink }: BreadcrumbProps) {
17
+ return (
18
+ <nav aria-label={label} className={cn("not-typeset", className)}>
19
+ <ol className="m-0 flex list-none flex-wrap items-center gap-x-2 gap-y-1 p-0 text-footnote text-label-secondary">
20
+ {items.map((item, index) => (
21
+ <li
22
+ key={`${index}-${item.label}`}
23
+ className="m-0 flex min-w-0 items-center gap-2 p-0"
24
+ >
25
+ {index > 0 && (
26
+ <span aria-hidden="true" className="text-label-tertiary">
27
+ /
28
+ </span>
29
+ )}
30
+ {index === items.length - 1 ? (
31
+ <span aria-current="page" className="font-medium text-label">
32
+ {item.label}
33
+ </span>
34
+ ) : item.href ? (
35
+ (renderLink?.({ ...item, href: item.href }) ?? (
36
+ <a
37
+ href={item.href}
38
+ className={cn(
39
+ "rounded-sm hover:text-label focus-visible:outline-2 focus-visible:outline-accent-text"
40
+ )}
41
+ >
42
+ {item.label}
43
+ </a>
44
+ ))
45
+ ) : (
46
+ <span>{item.label}</span>
47
+ )}
48
+ </li>
49
+ ))}
50
+ </ol>
51
+ </nav>
52
+ )
53
+ }
@@ -28,6 +28,8 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
28
28
  * @default false
29
29
  */
30
30
  isPending?: boolean
31
+ /** Layout classes for the content wrapper, such as a full-width disclosure row. */
32
+ contentClassName?: string
31
33
  }
32
34
 
33
35
  /** Shares native button appearance with semantic links without nesting a button. */
@@ -53,6 +55,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
53
55
  variant = "default",
54
56
  size = "default",
55
57
  isPending = false,
58
+ contentClassName,
56
59
  disabled,
57
60
  children,
58
61
  className,
@@ -85,7 +88,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
85
88
  <span
86
89
  className={cn(
87
90
  "inline-flex items-center justify-center gap-2 transition-opacity duration-200",
88
- showPendingIndicator && "opacity-0"
91
+ showPendingIndicator && "opacity-0",
92
+ contentClassName
89
93
  )}
90
94
  >
91
95
  {children}
@@ -0,0 +1,41 @@
1
+ import { Fragment, type ComponentProps, type ReactNode } from "react"
2
+ import { cn } from "../../lib/class-names"
3
+
4
+ export interface CodeBlockProps extends Omit<ComponentProps<"div">, "children"> {
5
+ code: string
6
+ /** Supply a highlighter's rendered pre/code element; source remains the fallback. */
7
+ children?: ReactNode
8
+ /** Show a nonselectable line gutter. @default true */
9
+ lineNumbers?: boolean
10
+ }
11
+
12
+ /** Code presentation without shipping a highlighter or formatter to consumers. */
13
+ export function CodeBlock({
14
+ code,
15
+ children,
16
+ lineNumbers = true,
17
+ className,
18
+ ...props
19
+ }: CodeBlockProps) {
20
+ const lines = code.split("\n")
21
+ return (
22
+ <div
23
+ {...props}
24
+ className={cn("passui-code-block", className)}
25
+ data-line-numbers={lineNumbers || undefined}
26
+ >
27
+ {children ?? (
28
+ <pre>
29
+ <code>
30
+ {lines.map((line, index) => (
31
+ <Fragment key={`${index}-${line}`}>
32
+ <span className="line">{line || " "}</span>
33
+ {index < lines.length - 1 ? "\n" : null}
34
+ </Fragment>
35
+ ))}
36
+ </code>
37
+ </pre>
38
+ )}
39
+ </div>
40
+ )
41
+ }
@@ -0,0 +1,13 @@
1
+ import { Picker, type PickerOption, type PickerProps } from "../picker/picker"
2
+
3
+ export interface ComboBoxOption<T extends string> extends PickerOption<T> {}
4
+ export interface ComboBoxProps<T extends string>
5
+ extends Omit<PickerProps<T>, "searchable" | "options"> {
6
+ options: readonly ComboBoxOption<T>[]
7
+ emptyLabel: string
8
+ }
9
+
10
+ /** A searchable Picker, with optional creation from the person's typed query. */
11
+ export function ComboBox<T extends string>(props: ComboBoxProps<T>) {
12
+ return <Picker {...props} searchable />
13
+ }
@@ -0,0 +1,119 @@
1
+ import { type ComponentProps, useEffect, useRef } from "react"
2
+ import { SearchIcon } from "../../icons/icons"
3
+ import { cn } from "../../lib/class-names"
4
+ import { KeyboardShortcut } from "../keyboard-shortcut/keyboard-shortcut"
5
+ import {
6
+ SearchList,
7
+ SearchListField,
8
+ SearchListResults,
9
+ SearchListEmpty,
10
+ SearchListGroup,
11
+ SearchListItem,
12
+ } from "../search-list/search-list"
13
+ import { Sheet, type SheetProps } from "../sheet/sheet"
14
+
15
+ export interface CommandProps extends ComponentProps<typeof SearchList> {}
16
+ export interface CommandDialogProps extends Omit<SheetProps, "placement"> {}
17
+
18
+ /** An inline command palette. SearchList owns filtering and keyboard selection. */
19
+ export function Command({ className, ...props }: CommandProps) {
20
+ return (
21
+ <SearchList
22
+ {...props}
23
+ className={cn(
24
+ "not-typeset glass-menu flex min-h-0 w-full flex-col overflow-hidden rounded-3xl",
25
+ className
26
+ )}
27
+ />
28
+ )
29
+ }
30
+
31
+ /** A modal command palette with Sheet's focus isolation and restoration. */
32
+ export function CommandDialog({ children, className, ...props }: CommandDialogProps) {
33
+ return (
34
+ <Sheet
35
+ {...props}
36
+ placement="center"
37
+ className={cn("max-w-lg rounded-3xl outline-none", className)}
38
+ >
39
+ <SearchList className="not-typeset flex min-h-0 flex-col">{children}</SearchList>
40
+ </Sheet>
41
+ )
42
+ }
43
+
44
+ export function CommandInput({
45
+ className,
46
+ autoFocus,
47
+ ref: forwardedRef,
48
+ ...props
49
+ }: ComponentProps<typeof SearchListField>) {
50
+ const input = useRef<HTMLInputElement>(null)
51
+ useEffect(() => {
52
+ if (autoFocus) input.current?.focus({ preventScroll: true })
53
+ }, [autoFocus])
54
+ return (
55
+ <div className="flex shrink-0 items-center gap-2 border-b border-separator/50 px-4">
56
+ <SearchIcon size={17} className="shrink-0 text-label-tertiary" />
57
+ <SearchListField
58
+ {...props}
59
+ ref={(node) => {
60
+ input.current = node
61
+ if (typeof forwardedRef === "function") forwardedRef(node)
62
+ else if (forwardedRef) forwardedRef.current = node
63
+ }}
64
+ className={cn(
65
+ "min-h-12 rounded-none border-0 bg-transparent px-0 focus-visible:ring-0",
66
+ className
67
+ )}
68
+ />
69
+ </div>
70
+ )
71
+ }
72
+
73
+ export function CommandList({ className, ...props }: ComponentProps<typeof SearchListResults>) {
74
+ return (
75
+ <SearchListResults {...props} className={cn("max-h-[min(60svh,24rem)] p-1.5", className)} />
76
+ )
77
+ }
78
+
79
+ export function CommandEmpty({ className, ...props }: ComponentProps<typeof SearchListEmpty>) {
80
+ return (
81
+ <SearchListEmpty
82
+ {...props}
83
+ className={cn("px-3 py-8 text-center text-footnote text-label-secondary", className)}
84
+ />
85
+ )
86
+ }
87
+
88
+ export function CommandShortcut({ className, ...props }: ComponentProps<typeof KeyboardShortcut>) {
89
+ return (
90
+ <KeyboardShortcut
91
+ {...props}
92
+ className={cn("ms-auto border-0 bg-transparent text-label-tertiary", className)}
93
+ />
94
+ )
95
+ }
96
+
97
+ export function CommandGroup({ className, ...props }: ComponentProps<typeof SearchListGroup>) {
98
+ return (
99
+ <SearchListGroup
100
+ {...props}
101
+ className={cn(
102
+ "border-separator/50 not-first:mt-1 not-first:border-t not-first:pt-1",
103
+ className
104
+ )}
105
+ />
106
+ )
107
+ }
108
+
109
+ export function CommandItem({ className, ...props }: ComponentProps<typeof SearchListItem>) {
110
+ return (
111
+ <SearchListItem
112
+ {...props}
113
+ className={cn(
114
+ "[&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-label-secondary",
115
+ className
116
+ )}
117
+ />
118
+ )
119
+ }
@@ -1,22 +1,10 @@
1
- import { AnimatePresence, motion, useReducedMotion } from "motion/react"
2
- import {
3
- type KeyboardEvent,
4
- useEffect,
5
- useId,
6
- useLayoutEffect,
7
- useMemo,
8
- useRef,
9
- useState,
10
- } from "react"
1
+ import { useAnchoredDisclosure } from "../../hooks/use-anchored-disclosure"
2
+ import { AnchoredSurface } from "../popover/anchored-surface"
3
+ import { type KeyboardEvent, useEffect, useId, useMemo, useRef, useState } from "react"
11
4
  import { createPortal } from "react-dom"
12
- import { FLOATING_LAYER } from "../../hooks/use-dismiss"
13
- import { useFloatingDismiss } from "../../hooks/use-floating-dismiss"
14
5
  import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "../../icons/icons"
15
6
  import { cn } from "../../lib/class-names"
16
- import { registerFloatingLayer } from "../../lib/floating-layers"
17
7
  import { haptic } from "../../lib/haptics"
18
- import { useAnchoredPlacement } from "../../hooks/use-anchored-placement"
19
- import { floatingPanelMotion } from "../../motion/transitions"
20
8
  import { IconButton } from "../button/icon-button"
21
9
  import { pickerTriggerStyle } from "../picker/picker-trigger"
22
10
  import {
@@ -82,7 +70,8 @@ export function CompactDatePicker({
82
70
  className,
83
71
  disabled = false,
84
72
  }: CompactDatePickerProps) {
85
- const [open, setOpen] = useState(false)
73
+ const panel = useAnchoredDisclosure(disabled)
74
+ const { open } = panel
86
75
  const trigger = useRef<HTMLButtonElement>(null)
87
76
  const calendarId = useId()
88
77
  const chosen = parseDay(value)
@@ -94,7 +83,7 @@ export function CompactDatePicker({
94
83
  : (placeholder ?? "")
95
84
 
96
85
  const close = (restoreFocus: boolean) => {
97
- setOpen(false)
86
+ panel.close()
98
87
  if (restoreFocus) trigger.current?.focus({ preventScroll: true })
99
88
  }
100
89
 
@@ -108,7 +97,7 @@ export function CompactDatePicker({
108
97
  aria-expanded={open && !disabled}
109
98
  aria-controls={open && !disabled ? calendarId : undefined}
110
99
  disabled={disabled}
111
- onClick={() => setOpen((current) => !current)}
100
+ onClick={() => panel.toggle()}
112
101
  className={pickerTriggerStyle(className)}
113
102
  >
114
103
  <span
@@ -123,26 +112,26 @@ export function CompactDatePicker({
123
112
  </button>
124
113
  {typeof document !== "undefined" &&
125
114
  createPortal(
126
- <AnimatePresence>
127
- {open && !disabled && trigger.current && (
128
- <MonthCalendar
129
- id={calendarId}
130
- anchor={trigger.current}
131
- label={label}
132
- locale={locale}
133
- value={value}
134
- min={min}
135
- max={max}
136
- labels={labels}
137
- onChoose={(next) => {
138
- haptic("selection")
139
- onChange(next)
140
- close(true)
141
- }}
142
- onClose={close}
143
- />
144
- )}
145
- </AnimatePresence>,
115
+ panel.mounted && trigger.current ? (
116
+ <MonthCalendar
117
+ open={open}
118
+ onExited={panel.onExited}
119
+ id={calendarId}
120
+ anchor={trigger.current}
121
+ label={label}
122
+ locale={locale}
123
+ value={value}
124
+ min={min}
125
+ max={max}
126
+ labels={labels}
127
+ onChoose={(next) => {
128
+ haptic("selection")
129
+ onChange(next)
130
+ close(true)
131
+ }}
132
+ onClose={close}
133
+ />
134
+ ) : null,
146
135
  document.body
147
136
  )}
148
137
  </>
@@ -150,6 +139,8 @@ export function CompactDatePicker({
150
139
  }
151
140
 
152
141
  function MonthCalendar({
142
+ open,
143
+ onExited,
153
144
  id,
154
145
  anchor,
155
146
  label,
@@ -161,6 +152,8 @@ function MonthCalendar({
161
152
  onChoose,
162
153
  onClose,
163
154
  }: {
155
+ open: boolean
156
+ onExited: () => void
164
157
  id: string
165
158
  anchor: HTMLElement
166
159
  label: string
@@ -173,8 +166,6 @@ function MonthCalendar({
173
166
  onClose: (restoreFocus: boolean) => void
174
167
  }) {
175
168
  const root = useRef<HTMLDivElement>(null)
176
- const reducedMotion = useReducedMotion()
177
- const placement = useAnchoredPlacement(anchor, WIDTH, 360)
178
169
  const today = todayDay()
179
170
  const [focused, setFocused] = useState<CalendarDay>(() => parseDay(value) ?? today)
180
171
  const firstDay = useMemo(() => firstWeekday(locale), [locale])
@@ -195,11 +186,6 @@ function MonthCalendar({
195
186
  timeZone: "UTC",
196
187
  })
197
188
 
198
- useLayoutEffect(() => {
199
- if (!root.current) return
200
- return registerFloatingLayer(root.current, anchor)
201
- }, [anchor])
202
-
203
189
  // The focused day takes keyboard focus as it moves.
204
190
  useEffect(() => {
205
191
  root.current
@@ -207,8 +193,6 @@ function MonthCalendar({
207
193
  ?.focus()
208
194
  }, [focused])
209
195
 
210
- useFloatingDismiss(root, anchor, onClose)
211
-
212
196
  const move = (next: CalendarDay) => setFocused(next)
213
197
 
214
198
  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
@@ -230,46 +214,18 @@ function MonthCalendar({
230
214
  }
231
215
 
232
216
  return (
233
- <motion.div
217
+ <AnchoredSurface
218
+ open={open}
219
+ onExited={onExited}
234
220
  ref={root}
221
+ anchor={anchor}
222
+ width={WIDTH}
223
+ needed={360}
224
+ onClose={onClose}
235
225
  id={id}
236
226
  role="dialog"
237
227
  aria-label={label}
238
- initial={
239
- reducedMotion
240
- ? { opacity: 0 }
241
- : {
242
- opacity: 0,
243
- scale: 1,
244
- y: placement.above ? 6 : -6,
245
- }
246
- }
247
- animate={{ opacity: 1, scale: 1, y: 0 }}
248
- exit={
249
- reducedMotion
250
- ? { opacity: 0 }
251
- : {
252
- opacity: 0,
253
- scale: 1,
254
- y: placement.above ? 4 : -4,
255
- }
256
- }
257
- transition={
258
- reducedMotion
259
- ? { duration: 0 }
260
- : floatingPanelMotion(placement.above ? "above" : "below").transition
261
- }
262
- style={{
263
- left: placement.left,
264
- width: placement.width,
265
- top: placement.top,
266
- bottom: placement.bottom,
267
- maxHeight: placement.maxHeight,
268
- overflowY: placement.maxHeight ? "auto" : undefined,
269
- transformOrigin: placement.above ? "50% 100%" : "50% 0%",
270
- }}
271
- className="glass-menu fixed z-[80] flex flex-col gap-2 rounded-3xl p-3"
272
- {...{ [FLOATING_LAYER]: "" }}
228
+ contentClassName="gap-2 p-3"
273
229
  onKeyDown={onKeyDown}
274
230
  >
275
231
  <div className="flex items-center justify-between gap-2 ps-2">
@@ -354,6 +310,6 @@ function MonthCalendar({
354
310
  {labels.clear}
355
311
  </button>
356
312
  )}
357
- </motion.div>
313
+ </AnchoredSurface>
358
314
  )
359
315
  }