@pantoia/ui 0.2.0 → 0.4.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 (66) hide show
  1. package/accordion/index.module.css +4 -4
  2. package/accordion/index.tsx +30 -58
  3. package/alert-dialog/index.tsx +38 -44
  4. package/badge/index.tsx +2 -2
  5. package/button/index.tsx +31 -30
  6. package/callout/index.tsx +29 -8
  7. package/checkbox/index.tsx +24 -88
  8. package/combobox/index.tsx +133 -323
  9. package/command-palette/index.tsx +55 -34
  10. package/dialog/index.tsx +30 -26
  11. package/dist/index.js +155 -27
  12. package/drawer/index.module.css +2 -2
  13. package/drawer/index.tsx +54 -132
  14. package/dropdown-menu/index.tsx +95 -100
  15. package/field/index.tsx +43 -94
  16. package/input/index.tsx +13 -9
  17. package/kbd/index.tsx +2 -2
  18. package/label/index.tsx +7 -9
  19. package/number-field/index.tsx +129 -176
  20. package/package.json +12 -15
  21. package/palette.css +114 -0
  22. package/popover/index.tsx +62 -33
  23. package/radio-group/index.tsx +36 -39
  24. package/registry.json +65 -169
  25. package/select/index.tsx +154 -76
  26. package/switch/index.tsx +13 -6
  27. package/table/index.tsx +5 -6
  28. package/tabs/index.module.css +5 -3
  29. package/tabs/index.tsx +26 -77
  30. package/tailwind.css +3 -0
  31. package/textarea/index.tsx +25 -13
  32. package/toast/index.module.css +187 -0
  33. package/toast/index.tsx +141 -46
  34. package/toggle/index.tsx +8 -7
  35. package/{styles.css → tokens.css} +82 -303
  36. package/tooltip/index.tsx +62 -31
  37. package/tv.ts +0 -1
  38. package/utilities.css +124 -0
  39. package/accordion/AGENTS.md +0 -60
  40. package/alert-dialog/AGENTS.md +0 -78
  41. package/badge/AGENTS.md +0 -35
  42. package/button/AGENTS.md +0 -39
  43. package/callout/AGENTS.md +0 -41
  44. package/checkbox/AGENTS.md +0 -54
  45. package/combobox/AGENTS.md +0 -150
  46. package/command-palette/AGENTS.md +0 -45
  47. package/dialog/AGENTS.md +0 -81
  48. package/drawer/AGENTS.md +0 -66
  49. package/dropdown-menu/AGENTS.md +0 -133
  50. package/field/AGENTS.md +0 -96
  51. package/input/AGENTS.md +0 -35
  52. package/kbd/AGENTS.md +0 -17
  53. package/label/AGENTS.md +0 -21
  54. package/number-field/AGENTS.md +0 -53
  55. package/popover/AGENTS.md +0 -64
  56. package/press.css +0 -37
  57. package/radio-group/AGENTS.md +0 -81
  58. package/select/AGENTS.md +0 -90
  59. package/switch/AGENTS.md +0 -37
  60. package/table/AGENTS.md +0 -82
  61. package/tabs/AGENTS.md +0 -60
  62. package/test-utils.ts +0 -10
  63. package/textarea/AGENTS.md +0 -35
  64. package/toast/AGENTS.md +0 -34
  65. package/toggle/AGENTS.md +0 -37
  66. package/tooltip/AGENTS.md +0 -50
package/drawer/index.tsx CHANGED
@@ -1,16 +1,7 @@
1
1
  'use client'
2
2
 
3
- import { Dialog } from 'radix-ui'
4
- import {
5
- createContext,
6
- use,
7
- useRef,
8
- useState,
9
- type ComponentProps,
10
- type PointerEvent as ReactPointerEvent,
11
- type ReactNode,
12
- type RefObject,
13
- } from 'react'
3
+ import { Drawer as DrawerPrimitive } from '@base-ui/react/drawer'
4
+ import { createContext, use, type ComponentProps, type ReactNode } from 'react'
14
5
 
15
6
  import { tv } from '../tv'
16
7
 
@@ -18,106 +9,34 @@ import s from './index.module.css'
18
9
 
19
10
  type Side = 'left' | 'right'
20
11
 
21
- const DrawerContext = createContext<{ side: Side, close: () => void }>({
22
- side: 'left',
23
- close: () => {},
24
- })
25
-
26
- const SWIPE_THRESHOLD = 40
27
- const SWIPE_VELOCITY = 0.3
28
- const MIN_DRAG = 1
29
- const IGNORE = 'button,a,input,select,textarea,label,[role="button"]'
30
-
31
- const MOVEMENT = '--drawer-swipe-movement-x'
32
-
33
- type Drag = { id: number, from: number, at: number, dx: number, v: number, on: boolean }
34
-
35
- function useSwipeDismiss(el: RefObject<HTMLDivElement | null>, side: Side, close: () => void) {
36
- const drag = useRef<Drag | null>(null)
37
-
38
- const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
39
- if (e.button !== 0) return
40
- if ((e.target as Element).closest(IGNORE)) return
41
-
42
- drag.current = { id: e.pointerId, from: e.clientX, at: performance.now(), dx: 0, v: 0, on: false }
43
- }
44
-
45
- const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
46
- const d = drag.current
47
- if (!d || d.id !== e.pointerId || !el.current) return
48
-
49
- const raw = e.clientX - d.from
50
- const dx = side === 'left' ? Math.min(0, raw) : Math.max(0, raw)
51
- const now = performance.now()
52
- const span = now - d.at
53
-
54
- if (span > 0) d.v = (dx - d.dx) / span
55
- d.dx = dx
56
- d.at = now
57
-
58
- if (!d.on && Math.abs(dx) >= MIN_DRAG) {
59
- d.on = true
60
- el.current.setAttribute('data-swiping', '')
61
- el.current.setPointerCapture(e.pointerId)
62
- }
63
-
64
- if (d.on) el.current.style.setProperty(MOVEMENT, `${dx}px`)
65
- }
66
-
67
- const onPointerUp = (e: ReactPointerEvent<HTMLDivElement>) => {
68
- const d = drag.current
69
- if (!d || d.id !== e.pointerId) return
70
- drag.current = null
71
-
72
- if (!d.on) return
73
- el.current?.removeAttribute('data-swiping')
74
-
75
- const gone = side === 'left' ? -d.dx : d.dx
76
- const speed = side === 'left' ? -d.v : d.v
77
-
78
- if (gone >= SWIPE_THRESHOLD || speed > SWIPE_VELOCITY) close()
79
- else el.current?.style.removeProperty(MOVEMENT)
80
- }
12
+ const DrawerSide = createContext<Side>('left')
81
13
 
82
- return { onPointerDown, onPointerMove, onPointerUp, onPointerCancel: onPointerUp }
83
- }
84
-
85
- export interface DrawerProps {
14
+ export type DrawerProps = Omit<DrawerPrimitive.Root.Props, 'swipeDirection'> & {
86
15
  side?: Side
87
- open?: boolean
88
- defaultOpen?: boolean
89
- onOpenChange?: (open: boolean) => void
90
- children: ReactNode
91
16
  }
92
17
 
93
- export function Drawer({ side = 'left', open, defaultOpen = false, onOpenChange, children }: DrawerProps) {
94
- const [own, setOwn] = useState(defaultOpen)
95
- const isOpen = open ?? own
96
-
97
- const set = (next: boolean) => {
98
- if (open === undefined) setOwn(next)
99
- onOpenChange?.(next)
100
- }
101
-
18
+ export function Drawer({ side = 'left', ...rest }: DrawerProps) {
102
19
  return (
103
- <DrawerContext value={{ side, close: () => set(false) }}>
104
- <Dialog.Root open={isOpen} onOpenChange={set} modal>
105
- {children}
106
- </Dialog.Root>
107
- </DrawerContext>
20
+ <DrawerSide value={side}>
21
+ <DrawerPrimitive.Root {...rest} swipeDirection={side} />
22
+ </DrawerSide>
108
23
  )
109
24
  }
110
25
 
111
- export function DrawerTrigger({ children }: { children: ReactNode }) {
112
- return <Dialog.Trigger asChild>{children}</Dialog.Trigger>
26
+ export type DrawerTriggerProps = ComponentProps<typeof DrawerPrimitive.Trigger>
27
+
28
+ export function DrawerTrigger(props: DrawerTriggerProps) {
29
+ return <DrawerPrimitive.Trigger {...props} />
113
30
  }
114
31
 
115
- export function DrawerClose({ children }: { children: ReactNode }) {
116
- return <Dialog.Close asChild>{children}</Dialog.Close>
32
+ export type DrawerCloseProps = ComponentProps<typeof DrawerPrimitive.Close>
33
+
34
+ export function DrawerClose(props: DrawerCloseProps) {
35
+ return <DrawerPrimitive.Close {...props} />
117
36
  }
118
37
 
119
38
  export const drawerContent = tv({
120
- base: 'sheet text-foreground z-overlay fixed inset-y-0'
39
+ base: 'sheet text-fg z-overlay fixed inset-y-0'
121
40
  + ' flex w-full flex-col gap-stack overflow-y-auto p-4 sm:w-120'
122
41
  + ' focus-visible:outline-none',
123
42
  variants: {
@@ -125,37 +44,32 @@ export const drawerContent = tv({
125
44
  },
126
45
  })
127
46
 
128
- export function DrawerContent({
129
- className,
130
- style,
131
- children,
132
- ...rest
133
- }: ComponentProps<typeof Dialog.Content>) {
134
- const { side, close } = use(DrawerContext)
135
- const el = useRef<HTMLDivElement>(null)
136
- const swipe = useSwipeDismiss(el, side, close)
47
+ export type DrawerContentProps = Omit<ComponentProps<typeof DrawerPrimitive.Popup>, 'className'> & {
48
+ className?: string
49
+ }
50
+
51
+ export function DrawerContent({ className, style, ...rest }: DrawerContentProps) {
52
+ const side = use(DrawerSide)
137
53
 
138
54
  return (
139
- <Dialog.Portal>
140
- <Dialog.Overlay className="bg-background-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
141
-
142
- <Dialog.Content
143
- {...rest}
144
- ref={el}
145
- style={{ touchAction: 'pan-y', ...style }}
146
- onPointerDown={swipe.onPointerDown}
147
- onPointerMove={swipe.onPointerMove}
148
- onPointerUp={swipe.onPointerUp}
149
- onPointerCancel={swipe.onPointerCancel}
150
- className={[
151
- s.panel,
152
- side === 'right' ? s['panel-right'] : '',
153
- drawerContent({ side, className }),
154
- ].filter(Boolean).join(' ')}
155
- >
156
- {children}
157
- </Dialog.Content>
158
- </Dialog.Portal>
55
+ <DrawerPrimitive.Portal>
56
+ <DrawerPrimitive.Backdrop className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
57
+
58
+ <DrawerPrimitive.Viewport>
59
+ <DrawerPrimitive.Popup
60
+ {...rest}
61
+ style={state => ({
62
+ touchAction: 'pan-y',
63
+ ...(typeof style === 'function' ? style(state) : style),
64
+ })}
65
+ className={[
66
+ s.panel,
67
+ side === 'right' ? s['panel-right'] : '',
68
+ drawerContent({ side, className }),
69
+ ].filter(Boolean).join(' ')}
70
+ />
71
+ </DrawerPrimitive.Viewport>
72
+ </DrawerPrimitive.Portal>
159
73
  )
160
74
  }
161
75
 
@@ -189,14 +103,22 @@ export function DrawerHeader({ lead, trail, className, children }: DrawerHeaderP
189
103
 
190
104
  const drawerTitle = tv({ base: 'text-section min-w-0' })
191
105
 
192
- export function DrawerTitle({ className, ...rest }: ComponentProps<typeof Dialog.Title>) {
193
- return <Dialog.Title {...rest} className={drawerTitle({ className })} />
106
+ export type DrawerTitleProps = Omit<ComponentProps<typeof DrawerPrimitive.Title>, 'className'> & {
107
+ className?: string
108
+ }
109
+
110
+ export function DrawerTitle({ className, ...rest }: DrawerTitleProps) {
111
+ return <DrawerPrimitive.Title {...rest} className={drawerTitle({ className })} />
194
112
  }
195
113
 
196
- const drawerDescription = tv({ base: 'text-prose text-foreground-muted' })
114
+ const drawerDescription = tv({ base: 'text-prose text-fg-muted' })
115
+
116
+ export type DrawerDescriptionProps = Omit<ComponentProps<typeof DrawerPrimitive.Description>, 'className'> & {
117
+ className?: string
118
+ }
197
119
 
198
- export function DrawerDescription({ className, ...rest }: ComponentProps<typeof Dialog.Description>) {
120
+ export function DrawerDescription({ className, ...rest }: DrawerDescriptionProps) {
199
121
  return (
200
- <Dialog.Description {...rest} className={drawerDescription({ className })} />
122
+ <DrawerPrimitive.Description {...rest} className={drawerDescription({ className })} />
201
123
  )
202
124
  }
@@ -1,7 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { Check, ChevronRight } from 'lucide-react'
4
- import { DropdownMenu as MenuPrimitive } from 'radix-ui'
3
+ import { Menu as MenuPrimitive } from '@base-ui/react/menu'
5
4
  import type { ComponentProps, ReactNode } from 'react'
6
5
 
7
6
  import { tv } from '../tv'
@@ -14,7 +13,7 @@ export const menuItem = tv({
14
13
 
15
14
  variants: {
16
15
  color: {
17
- neutral: 'text-foreground data-highlighted:bg-bg-neutral-hover data-disabled:text-foreground-disabled',
16
+ neutral: 'text-fg data-highlighted:bg-bg-neutral-hover data-disabled:text-fg-disabled',
18
17
  danger: 'text-fg-danger data-highlighted:bg-bg-danger-weak data-disabled:text-fg-danger/40',
19
18
  },
20
19
  },
@@ -22,37 +21,39 @@ export const menuItem = tv({
22
21
  defaultVariants: { color: 'neutral' },
23
22
  })
24
23
 
25
- const PANEL = 'menu float text-foreground rounded-float z-overlay'
24
+ const PANEL = 'menu float text-fg rounded-r12 z-overlay'
26
25
  + ' flex min-w-44 flex-col gap-px p-1'
27
26
  + ' open:animate-enter closed:animate-exit'
28
27
  + ' focus-visible:outline-none'
29
28
 
30
- export interface DropdownMenuProps {
31
- open?: boolean
32
- defaultOpen?: boolean
33
- onOpenChange?: (open: boolean) => void
34
- children?: ReactNode
35
- }
29
+ export type DropdownMenuProps = ComponentProps<typeof MenuPrimitive.Root>
36
30
 
37
- export function DropdownMenu({ open, defaultOpen, onOpenChange, children }: DropdownMenuProps) {
38
- return (
39
- <MenuPrimitive.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
40
- {children}
41
- </MenuPrimitive.Root>
42
- )
31
+ export function DropdownMenu(props: DropdownMenuProps) {
32
+ return <MenuPrimitive.Root {...props} />
43
33
  }
44
34
 
45
- export function DropdownMenuTrigger({ children }: { children: ReactNode }) {
46
- return <MenuPrimitive.Trigger asChild>{children}</MenuPrimitive.Trigger>
35
+ export type DropdownMenuTriggerProps = ComponentProps<typeof MenuPrimitive.Trigger>
36
+
37
+ export function DropdownMenuTrigger(props: DropdownMenuTriggerProps) {
38
+ return <MenuPrimitive.Trigger {...props} />
47
39
  }
48
40
 
49
- export interface DropdownMenuContentProps extends Omit<ComponentProps<typeof MenuPrimitive.Content>, 'align'> {
41
+ export type DropdownMenuContentProps = Omit<ComponentProps<typeof MenuPrimitive.Popup>, 'className'> & {
50
42
  align?: 'start' | 'center' | 'end'
51
43
  arrow?: boolean
44
+ className?: string
52
45
  }
53
46
 
54
47
  const menuPanel = tv({ base: PANEL })
55
48
 
49
+ const menuArrow = tv({
50
+ base: 'data-uncentered:invisible'
51
+ + ' side-top:top-full'
52
+ + ' side-bottom:bottom-full side-bottom:rotate-180'
53
+ + ' side-right:left-0 side-right:origin-top-left side-right:rotate-90 side-right:-translate-y-1/2'
54
+ + ' side-left:right-0 side-left:origin-top-right side-left:-rotate-90 side-left:-translate-y-1/2',
55
+ })
56
+
56
57
  export function DropdownMenuContent({
57
58
  align = 'start',
58
59
  arrow = true,
@@ -62,50 +63,53 @@ export function DropdownMenuContent({
62
63
  }: DropdownMenuContentProps) {
63
64
  return (
64
65
  <MenuPrimitive.Portal>
65
- <MenuPrimitive.Content
66
- {...rest}
66
+ <MenuPrimitive.Positioner
67
67
  align={align}
68
- sideOffset={8}
68
+ sideOffset={arrow ? 14 : 8}
69
69
  arrowPadding={16}
70
- className={menuPanel({ className })}
70
+ className="z-overlay min-w-max will-change-transform"
71
71
  >
72
- {children}
73
-
74
- {arrow && (
75
- <MenuPrimitive.Arrow asChild width={12} height={6}>
76
- <svg viewBox="0 0 12 6" preserveAspectRatio="none" className="fill-background-panel -translate-y-px">
77
- <path d="M0 0L4.58579 4.58579C5.36683 5.36683 6.63316 5.36684 7.41421 4.58579L12 0" className="stroke-line stroke-1" />
78
- </svg>
79
- </MenuPrimitive.Arrow>
80
- )}
81
- </MenuPrimitive.Content>
72
+ <MenuPrimitive.Popup {...rest} className={menuPanel({ className })}>
73
+ {children}
74
+
75
+ {arrow && (
76
+ <MenuPrimitive.Arrow className={menuArrow()}>
77
+ <svg width={12} height={6} viewBox="0 0 12 6" preserveAspectRatio="none" className="block fill-bg-panel -translate-y-px">
78
+ <path d="M0 0L4.58579 4.58579C5.36683 5.36683 6.63316 5.36684 7.41421 4.58579L12 0" className="stroke-line stroke-1" />
79
+ </svg>
80
+ </MenuPrimitive.Arrow>
81
+ )}
82
+ </MenuPrimitive.Popup>
83
+ </MenuPrimitive.Positioner>
82
84
  </MenuPrimitive.Portal>
83
85
  )
84
86
  }
85
87
 
86
88
  const menuGroup = tv({ base: 'flex flex-col gap-px' })
87
89
 
88
- export function DropdownMenuGroup({ label, className, children }: {
90
+ export type DropdownMenuGroupProps = Omit<ComponentProps<typeof MenuPrimitive.Group>, 'className'> & {
89
91
  label?: string
90
92
  className?: string
91
- children?: ReactNode
92
- }) {
93
+ }
94
+
95
+ export function DropdownMenuGroup({ label, className, children, ...rest }: DropdownMenuGroupProps) {
93
96
  return (
94
- <MenuPrimitive.Group className={menuGroup({ className })}>
97
+ <MenuPrimitive.Group {...rest} className={menuGroup({ className })}>
95
98
  {label && (
96
- <MenuPrimitive.Label className="text-caption text-foreground-subtle px-2 pb-1 pt-2">
99
+ <MenuPrimitive.GroupLabel className="text-caption text-fg-subtle px-2 pb-1 pt-2">
97
100
  {label}
98
- </MenuPrimitive.Label>
101
+ </MenuPrimitive.GroupLabel>
99
102
  )}
100
103
  {children}
101
104
  </MenuPrimitive.Group>
102
105
  )
103
106
  }
104
107
 
105
- export interface DropdownMenuItemProps extends Omit<ComponentProps<typeof MenuPrimitive.Item>, 'color'> {
108
+ export type DropdownMenuItemProps = Omit<ComponentProps<typeof MenuPrimitive.Item>, 'className' | 'color'> & {
106
109
  color?: 'neutral' | 'danger'
107
110
  lead?: ReactNode
108
111
  trail?: ReactNode
112
+ className?: string
109
113
  }
110
114
 
111
115
  export function DropdownMenuItem({ color, lead, trail, className, children, ...rest }: DropdownMenuItemProps) {
@@ -123,25 +127,19 @@ export function DropdownMenuItem({ color, lead, trail, className, children, ...r
123
127
  )
124
128
  }
125
129
 
126
- export function DropdownMenuCheckboxItem({ checked, onCheckedChange, disabled, className, children }: {
127
- checked?: boolean
128
- onCheckedChange?: (checked: boolean) => void
129
- disabled?: boolean
130
+ export type DropdownMenuCheckboxItemProps = Omit<ComponentProps<typeof MenuPrimitive.CheckboxItem>, 'className'> & {
130
131
  className?: string
131
- children?: ReactNode
132
- }) {
132
+ }
133
+
134
+ export function DropdownMenuCheckboxItem({ className, children, ...rest }: DropdownMenuCheckboxItemProps) {
133
135
  return (
134
- <MenuPrimitive.CheckboxItem
135
- checked={checked}
136
- onCheckedChange={onCheckedChange}
137
- disabled={disabled}
138
- className={menuItem({ className })}
139
- onSelect={e => e.preventDefault()}
140
- >
136
+ <MenuPrimitive.CheckboxItem {...rest} className={menuItem({ className })}>
141
137
  <span className="menu-lead grid size-4 shrink-0 place-items-center">
142
- <MenuPrimitive.ItemIndicator>
143
- <Check className="size-4" />
144
- </MenuPrimitive.ItemIndicator>
138
+ <MenuPrimitive.CheckboxItemIndicator>
139
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
140
+ <path d="M20 6 9 17l-5-5" />
141
+ </svg>
142
+ </MenuPrimitive.CheckboxItemIndicator>
145
143
  </span>
146
144
 
147
145
  <span className="min-w-0 grow">{children}</span>
@@ -149,30 +147,23 @@ export function DropdownMenuCheckboxItem({ checked, onCheckedChange, disabled, c
149
147
  )
150
148
  }
151
149
 
152
- export function DropdownMenuRadioGroup({ value, onValueChange, children }: {
153
- value?: string
154
- onValueChange?: (value: string) => void
155
- children?: ReactNode
156
- }) {
157
- return (
158
- <MenuPrimitive.RadioGroup value={value} onValueChange={onValueChange}>
159
- {children}
160
- </MenuPrimitive.RadioGroup>
161
- )
150
+ export type DropdownMenuRadioGroupProps = ComponentProps<typeof MenuPrimitive.RadioGroup>
151
+
152
+ export function DropdownMenuRadioGroup(props: DropdownMenuRadioGroupProps) {
153
+ return <MenuPrimitive.RadioGroup {...props} />
162
154
  }
163
155
 
164
- export function DropdownMenuRadioItem({ value, disabled, className, children }: {
165
- value: string
166
- disabled?: boolean
156
+ export type DropdownMenuRadioItemProps = Omit<ComponentProps<typeof MenuPrimitive.RadioItem>, 'className'> & {
167
157
  className?: string
168
- children?: ReactNode
169
- }) {
158
+ }
159
+
160
+ export function DropdownMenuRadioItem({ className, children, ...rest }: DropdownMenuRadioItemProps) {
170
161
  return (
171
- <MenuPrimitive.RadioItem value={value} disabled={disabled} className={menuItem({ className })}>
162
+ <MenuPrimitive.RadioItem {...rest} className={menuItem({ className })}>
172
163
  <span className="menu-lead grid size-4 shrink-0 place-items-center">
173
- <MenuPrimitive.ItemIndicator>
164
+ <MenuPrimitive.RadioItemIndicator>
174
165
  <span className="bg-bg-neutral-solid in-disabled:bg-bg-neutral-solid-checked-disabled size-2 rounded-full" />
175
- </MenuPrimitive.ItemIndicator>
166
+ </MenuPrimitive.RadioItemIndicator>
176
167
  </span>
177
168
 
178
169
  <span className="min-w-0 grow">{children}</span>
@@ -182,31 +173,28 @@ export function DropdownMenuRadioItem({ value, disabled, className, children }:
182
173
 
183
174
  const menuSeparator = tv({ base: 'bg-line -mx-1 my-1 h-px' })
184
175
 
185
- export function DropdownMenuSeparator({ className }: { className?: string }) {
186
- return <MenuPrimitive.Separator className={menuSeparator({ className })} />
176
+ export type DropdownMenuSeparatorProps = Omit<ComponentProps<typeof MenuPrimitive.Separator>, 'className'> & {
177
+ className?: string
187
178
  }
188
179
 
189
- export function DropdownMenuSub({ open, defaultOpen, onOpenChange, children }: {
190
- open?: boolean
191
- defaultOpen?: boolean
192
- onOpenChange?: (open: boolean) => void
193
- children?: ReactNode
194
- }) {
195
- return (
196
- <MenuPrimitive.Sub open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
197
- {children}
198
- </MenuPrimitive.Sub>
199
- )
180
+ export function DropdownMenuSeparator({ className, ...rest }: DropdownMenuSeparatorProps) {
181
+ return <MenuPrimitive.Separator {...rest} className={menuSeparator({ className })} />
200
182
  }
201
183
 
202
- export function DropdownMenuSubTrigger({ disabled, lead, className, children }: {
203
- disabled?: boolean
184
+ export type DropdownMenuSubProps = ComponentProps<typeof MenuPrimitive.SubmenuRoot>
185
+
186
+ export function DropdownMenuSub(props: DropdownMenuSubProps) {
187
+ return <MenuPrimitive.SubmenuRoot {...props} />
188
+ }
189
+
190
+ export type DropdownMenuSubTriggerProps = Omit<ComponentProps<typeof MenuPrimitive.SubmenuTrigger>, 'className'> & {
204
191
  lead?: ReactNode
205
192
  className?: string
206
- children?: ReactNode
207
- }) {
193
+ }
194
+
195
+ export function DropdownMenuSubTrigger({ lead, className, children, ...rest }: DropdownMenuSubTriggerProps) {
208
196
  return (
209
- <MenuPrimitive.SubTrigger disabled={disabled} className={menuItem({ className })}>
197
+ <MenuPrimitive.SubmenuTrigger {...rest} className={menuItem({ className })}>
210
198
  {lead && (
211
199
  <span className="menu-lead grid size-4 shrink-0 place-items-center">
212
200
  {lead}
@@ -215,21 +203,28 @@ export function DropdownMenuSubTrigger({ disabled, lead, className, children }:
215
203
 
216
204
  <span className="min-w-0 grow">{children}</span>
217
205
 
218
- <ChevronRight className="text-foreground-subtle size-4 shrink-0" />
219
- </MenuPrimitive.SubTrigger>
206
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="text-fg-subtle size-4 shrink-0">
207
+ <path d="m9 18 6-6-6-6" />
208
+ </svg>
209
+ </MenuPrimitive.SubmenuTrigger>
220
210
  )
221
211
  }
222
212
 
223
- export function DropdownMenuSubContent({ className, children, ...rest }: ComponentProps<typeof MenuPrimitive.SubContent>) {
213
+ export type DropdownMenuSubContentProps = Omit<ComponentProps<typeof MenuPrimitive.Popup>, 'className'> & {
214
+ className?: string
215
+ }
216
+
217
+ export function DropdownMenuSubContent({ className, children, ...rest }: DropdownMenuSubContentProps) {
224
218
  return (
225
219
  <MenuPrimitive.Portal>
226
- <MenuPrimitive.SubContent
227
- {...rest}
220
+ <MenuPrimitive.Positioner
228
221
  sideOffset={4}
229
- className={menuPanel({ className })}
222
+ className="z-overlay min-w-max will-change-transform"
230
223
  >
231
- {children}
232
- </MenuPrimitive.SubContent>
224
+ <MenuPrimitive.Popup {...rest} className={menuPanel({ className })}>
225
+ {children}
226
+ </MenuPrimitive.Popup>
227
+ </MenuPrimitive.Positioner>
233
228
  </MenuPrimitive.Portal>
234
229
  )
235
230
  }