@pantoia/ui 0.12.1 → 0.14.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.
@@ -0,0 +1,3 @@
1
+ .group > * {
2
+ box-shadow: 0 0 0 2px var(--avatar-group-ring, var(--color-bg));
3
+ }
@@ -0,0 +1,52 @@
1
+ 'use client'
2
+
3
+ import { Children, cloneElement, isValidElement, type ComponentProps, type ReactElement, type ReactNode } from 'react'
4
+
5
+ import { avatar, type AvatarProps } from '../avatar'
6
+ import { tv } from '../tv'
7
+
8
+ import s from './index.module.css'
9
+
10
+ type Size = NonNullable<AvatarProps['size']>
11
+
12
+ export const avatarGroup = tv({
13
+ base: 'inline-flex items-center',
14
+
15
+ variants: {
16
+ size: {
17
+ xs: '-space-x-1',
18
+ sm: '-space-x-1',
19
+ md: '-space-x-1.5',
20
+ lg: '-space-x-2',
21
+ xl: '-space-x-3',
22
+ },
23
+ },
24
+
25
+ defaultVariants: { size: 'md' },
26
+ })
27
+
28
+ export type AvatarGroupProps = Omit<ComponentProps<'div'>, 'className' | 'children'> & {
29
+ max?: number
30
+ size?: Size
31
+ renderOverflow?: (count: number) => ReactNode
32
+ className?: string
33
+ children?: ReactNode
34
+ }
35
+
36
+ export function AvatarGroup({ max, size = 'md', renderOverflow, className, children, ...rest }: AvatarGroupProps) {
37
+ const people = Children.toArray(children).filter(isValidElement) as ReactElement<AvatarProps>[]
38
+ const shown = max !== undefined && people.length > max ? people.slice(0, max) : people
39
+ const hidden = people.length - shown.length
40
+
41
+ return (
42
+ <div role="group" {...rest} className={avatarGroup({ size, className: [s.group, className] })}>
43
+ {shown.map(person => cloneElement(person, { size }))}
44
+
45
+ {hidden > 0 && (
46
+ <span role="img" aria-label={`외 ${hidden}명`} className={avatar({ color: 'neutral', size })}>
47
+ {renderOverflow ? renderOverflow(hidden) : `+${hidden}`}
48
+ </span>
49
+ )}
50
+ </div>
51
+ )
52
+ }
@@ -3,6 +3,7 @@
3
3
  import { Drawer as DrawerPrimitive } from '@base-ui/react/drawer'
4
4
  import type { ComponentProps, ReactNode } from 'react'
5
5
 
6
+ import { Button } from '../button'
6
7
  import { tv } from '../tv'
7
8
 
8
9
  import s from './index.module.css'
@@ -62,11 +63,23 @@ export const bottomSheetHeader = tv({
62
63
  export interface BottomSheetHeaderProps {
63
64
  lead?: ReactNode
64
65
  trail?: ReactNode
66
+ closable?: boolean
67
+ closeLabel?: string
65
68
  className?: string
66
69
  children?: ReactNode
67
70
  }
68
71
 
69
- export function BottomSheetHeader({ lead, trail, className, children }: BottomSheetHeaderProps) {
72
+ function HeaderClose({ label }: { label: string }) {
73
+ return (
74
+ <DrawerPrimitive.Close render={<Button color="neutral" emphasis="ghost" shape="square" aria-label={label} />}>
75
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
76
+ <path d="M18 6 6 18M6 6l12 12" />
77
+ </svg>
78
+ </DrawerPrimitive.Close>
79
+ )
80
+ }
81
+
82
+ export function BottomSheetHeader({ lead, trail, closable = false, closeLabel = '닫기', className, children }: BottomSheetHeaderProps) {
70
83
  const ui = bottomSheetHeader()
71
84
 
72
85
  return (
@@ -74,6 +87,7 @@ export function BottomSheetHeader({ lead, trail, className, children }: BottomSh
74
87
  {lead && <div className={ui.lead()}>{lead}</div>}
75
88
  <div className={ui.body()}>{children}</div>
76
89
  {trail && <div className={ui.trail()}>{trail}</div>}
90
+ {closable && <div className={ui.trail()}><HeaderClose label={closeLabel} /></div>}
77
91
  </div>
78
92
  )
79
93
  }
@@ -135,7 +135,9 @@ export function ComboboxCancel({ label = '지우기', keepMounted = true, classN
135
135
  )
136
136
  }
137
137
 
138
- export type ComboboxContentProps = Omit<ComponentProps<typeof ComboboxPrimitive.Popup>, 'className' | 'children'> & {
138
+ type ComboboxPlacement = Pick<ComponentProps<typeof ComboboxPrimitive.Positioner>, 'sideOffset' | 'alignOffset' | 'collisionAvoidance'>
139
+
140
+ export type ComboboxContentProps = Omit<ComponentProps<typeof ComboboxPrimitive.Popup>, 'className' | 'children'> & ComboboxPlacement & {
139
141
  className?: string
140
142
  children?: ComboboxPrimitive.List.Props['children'] | ComboboxPrimitive.List.Props['children'][]
141
143
  }
@@ -148,7 +150,7 @@ const comboboxContent = tv({
148
150
 
149
151
  const isEmpty = (child: unknown) => isValidElement(child) && child.type === ComboboxEmpty
150
152
 
151
- export function ComboboxContent({ 'aria-label': label, className, children, ...rest }: ComboboxContentProps) {
153
+ export function ComboboxContent({ 'aria-label': label, sideOffset = 4, alignOffset, collisionAvoidance, className, children, ...rest }: ComboboxContentProps) {
152
154
  const nodes = Array.isArray(children) ? children : [children]
153
155
  const empty = nodes.filter(isEmpty) as ReactNode[]
154
156
  const list = nodes.filter(node => !isEmpty(node))
@@ -157,7 +159,9 @@ export function ComboboxContent({ 'aria-label': label, className, children, ...r
157
159
  <ComboboxPrimitive.Portal>
158
160
  <ComboboxPrimitive.Positioner
159
161
  align="start"
160
- sideOffset={4}
162
+ sideOffset={sideOffset}
163
+ alignOffset={alignOffset}
164
+ collisionAvoidance={collisionAvoidance}
161
165
  className="z-overlay min-w-max will-change-transform"
162
166
  >
163
167
  <ComboboxPrimitive.Popup {...rest} className={comboboxContent({ className })}>
@@ -1,12 +1,13 @@
1
1
  'use client'
2
2
 
3
3
  import { ContextMenu as ContextMenuPrimitive } from '@base-ui/react/context-menu'
4
- import type { ComponentProps } from 'react'
4
+ import type { ComponentProps, ReactNode } from 'react'
5
5
 
6
6
  import {
7
7
  DropdownMenuCheckboxItem,
8
8
  DropdownMenuGroup,
9
9
  DropdownMenuItem,
10
+ DropdownMenuItemDescription,
10
11
  DropdownMenuRadioGroup,
11
12
  DropdownMenuRadioItem,
12
13
  DropdownMenuSeparator,
@@ -37,14 +38,21 @@ export function ContextMenuTrigger(props: ContextMenuTriggerProps) {
37
38
  return <ContextMenuPrimitive.Trigger {...props} />
38
39
  }
39
40
 
40
- export type ContextMenuContentProps = Omit<ComponentProps<typeof ContextMenuPrimitive.Popup>, 'className'> & {
41
+ type ContextMenuPlacement = Pick<ComponentProps<typeof ContextMenuPrimitive.Positioner>, 'sideOffset' | 'alignOffset' | 'collisionAvoidance'>
42
+
43
+ export type ContextMenuContentProps = Omit<ComponentProps<typeof ContextMenuPrimitive.Popup>, 'className'> & ContextMenuPlacement & {
41
44
  className?: string
42
45
  }
43
46
 
44
- export function ContextMenuContent({ className, ...rest }: ContextMenuContentProps) {
47
+ export function ContextMenuContent({ sideOffset, alignOffset, collisionAvoidance, className, ...rest }: ContextMenuContentProps) {
45
48
  return (
46
49
  <ContextMenuPrimitive.Portal>
47
- <ContextMenuPrimitive.Positioner className="z-overlay min-w-max will-change-transform">
50
+ <ContextMenuPrimitive.Positioner
51
+ sideOffset={sideOffset}
52
+ alignOffset={alignOffset}
53
+ collisionAvoidance={collisionAvoidance}
54
+ className="z-overlay min-w-max will-change-transform"
55
+ >
48
56
  <ContextMenuPrimitive.Popup {...rest} className={menuPanel({ className })} />
49
57
  </ContextMenuPrimitive.Positioner>
50
58
  </ContextMenuPrimitive.Portal>
@@ -63,6 +71,10 @@ export function ContextMenuItem(props: ContextMenuItemProps) {
63
71
  return <DropdownMenuItem {...props} />
64
72
  }
65
73
 
74
+ export function ContextMenuItemDescription(props: { className?: string, children?: ReactNode }) {
75
+ return <DropdownMenuItemDescription {...props} />
76
+ }
77
+
66
78
  export type ContextMenuCheckboxItemProps = DropdownMenuCheckboxItemProps
67
79
 
68
80
  export function ContextMenuCheckboxItem(props: ContextMenuCheckboxItemProps) {
package/dialog/index.tsx CHANGED
@@ -3,6 +3,7 @@
3
3
  import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
4
4
  import type { ComponentProps, ReactNode } from 'react'
5
5
 
6
+ import { Button } from '../button'
6
7
  import { tv } from '../tv'
7
8
 
8
9
  export const dialog = tv({
@@ -92,11 +93,23 @@ export const dialogHeader = tv({
92
93
  export interface DialogHeaderProps {
93
94
  lead?: ReactNode
94
95
  trail?: ReactNode
96
+ closable?: boolean
97
+ closeLabel?: string
95
98
  className?: string
96
99
  children?: ReactNode
97
100
  }
98
101
 
99
- export function DialogHeader({ lead, trail, className, children }: DialogHeaderProps) {
102
+ function HeaderClose({ label }: { label: string }) {
103
+ return (
104
+ <DialogPrimitive.Close render={<Button color="neutral" emphasis="ghost" shape="square" aria-label={label} />}>
105
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
106
+ <path d="M18 6 6 18M6 6l12 12" />
107
+ </svg>
108
+ </DialogPrimitive.Close>
109
+ )
110
+ }
111
+
112
+ export function DialogHeader({ lead, trail, closable = false, closeLabel = '닫기', className, children }: DialogHeaderProps) {
100
113
  const ui = dialogHeader()
101
114
 
102
115
  return (
@@ -104,6 +117,7 @@ export function DialogHeader({ lead, trail, className, children }: DialogHeaderP
104
117
  {lead && <div className={ui.lead()}>{lead}</div>}
105
118
  <div className={ui.body()}>{children}</div>
106
119
  {trail && <div className={ui.trail()}>{trail}</div>}
120
+ {closable && <div className={ui.trail()}><HeaderClose label={closeLabel} /></div>}
107
121
  </div>
108
122
  )
109
123
  }
package/drawer/index.tsx CHANGED
@@ -3,6 +3,7 @@
3
3
  import { Drawer as DrawerPrimitive } from '@base-ui/react/drawer'
4
4
  import { createContext, use, type ComponentProps, type ReactNode } from 'react'
5
5
 
6
+ import { Button } from '../button'
6
7
  import { tv } from '../tv'
7
8
 
8
9
  import s from './index.module.css'
@@ -85,11 +86,23 @@ export const drawerHeader = tv({
85
86
  export interface DrawerHeaderProps {
86
87
  lead?: ReactNode
87
88
  trail?: ReactNode
89
+ closable?: boolean
90
+ closeLabel?: string
88
91
  className?: string
89
92
  children?: ReactNode
90
93
  }
91
94
 
92
- export function DrawerHeader({ lead, trail, className, children }: DrawerHeaderProps) {
95
+ function HeaderClose({ label }: { label: string }) {
96
+ return (
97
+ <DrawerPrimitive.Close render={<Button color="neutral" emphasis="ghost" shape="square" aria-label={label} />}>
98
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
99
+ <path d="M18 6 6 18M6 6l12 12" />
100
+ </svg>
101
+ </DrawerPrimitive.Close>
102
+ )
103
+ }
104
+
105
+ export function DrawerHeader({ lead, trail, closable = false, closeLabel = '닫기', className, children }: DrawerHeaderProps) {
93
106
  const ui = drawerHeader()
94
107
 
95
108
  return (
@@ -97,6 +110,7 @@ export function DrawerHeader({ lead, trail, className, children }: DrawerHeaderP
97
110
  {lead && <div className={ui.lead()}>{lead}</div>}
98
111
  <div className={ui.body()}>{children}</div>
99
112
  {trail && <div className={ui.trail()}>{trail}</div>}
113
+ {closable && <div className={ui.trail()}><HeaderClose label={closeLabel} /></div>}
100
114
  </div>
101
115
  )
102
116
  }
@@ -1,13 +1,14 @@
1
1
  'use client'
2
2
 
3
3
  import { Menu as MenuPrimitive } from '@base-ui/react/menu'
4
- import type { ComponentProps, ReactNode } from 'react'
4
+ import { createContext, use, useEffect, useId, useState, type ComponentProps, type ReactNode } from 'react'
5
5
 
6
6
  import { tv } from '../tv'
7
7
 
8
8
  export const menuItem = tv({
9
- base: 'flex h-9 cursor-default items-center gap-2 rounded-r8 px-2 menu-gutter:ps-8'
9
+ base: 'flex min-h-9 cursor-default items-center gap-2 rounded-r8 px-2 py-1.5 menu-gutter:ps-8'
10
10
  + ' [&_svg]:size-4 [&_svg]:shrink-0'
11
+ + ' described:items-start described:[&>.menu-lead]:mt-0.5 described:[&>svg]:mt-0.5'
11
12
  + ' text-label transition-state'
12
13
  + ' data-highlighted:outline-none data-disabled:pointer-events-none',
13
14
 
@@ -21,6 +22,20 @@ export const menuItem = tv({
21
22
  defaultVariants: { color: 'neutral' },
22
23
  })
23
24
 
25
+ interface DescriptionContext {
26
+ id: string
27
+ setDescribed: (described: boolean) => void
28
+ }
29
+
30
+ const ItemDescription = createContext<DescriptionContext | null>(null)
31
+
32
+ function useDescription() {
33
+ const id = useId()
34
+ const [described, setDescribed] = useState(false)
35
+
36
+ return { context: { id, setDescribed }, describedBy: described ? id : undefined }
37
+ }
38
+
24
39
  const PANEL = 'menu float text-fg rounded-r12 z-overlay'
25
40
  + ' flex min-w-44 flex-col gap-px p-1'
26
41
  + ' open:animate-enter closed:animate-exit'
@@ -38,7 +53,9 @@ export function DropdownMenuTrigger(props: DropdownMenuTriggerProps) {
38
53
  return <MenuPrimitive.Trigger {...props} />
39
54
  }
40
55
 
41
- export type DropdownMenuContentProps = Omit<ComponentProps<typeof MenuPrimitive.Popup>, 'className'> & {
56
+ type MenuPlacement = Pick<ComponentProps<typeof MenuPrimitive.Positioner>, 'anchor' | 'sideOffset' | 'alignOffset' | 'collisionAvoidance'>
57
+
58
+ export type DropdownMenuContentProps = Omit<ComponentProps<typeof MenuPrimitive.Popup>, 'className'> & MenuPlacement & {
42
59
  side?: 'top' | 'right' | 'bottom' | 'left'
43
60
  align?: 'start' | 'center' | 'end'
44
61
  arrow?: boolean
@@ -59,6 +76,10 @@ export function DropdownMenuContent({
59
76
  side = 'bottom',
60
77
  align = 'start',
61
78
  arrow = true,
79
+ anchor,
80
+ sideOffset,
81
+ alignOffset,
82
+ collisionAvoidance,
62
83
  className,
63
84
  children,
64
85
  ...rest
@@ -66,9 +87,12 @@ export function DropdownMenuContent({
66
87
  return (
67
88
  <MenuPrimitive.Portal>
68
89
  <MenuPrimitive.Positioner
90
+ anchor={anchor}
69
91
  side={side}
70
92
  align={align}
71
- sideOffset={arrow ? 14 : 8}
93
+ sideOffset={sideOffset ?? (arrow ? 14 : 8)}
94
+ alignOffset={alignOffset}
95
+ collisionAvoidance={collisionAvoidance}
72
96
  arrowPadding={16}
73
97
  className="z-overlay min-w-max will-change-transform"
74
98
  >
@@ -116,17 +140,21 @@ export type DropdownMenuItemProps = Omit<ComponentProps<typeof MenuPrimitive.Ite
116
140
  }
117
141
 
118
142
  export function DropdownMenuItem({ color, lead, trail, className, children, ...rest }: DropdownMenuItemProps) {
119
- return (
120
- <MenuPrimitive.Item {...rest} className={menuItem({ color, className })}>
121
- {lead && (
122
- <span className="menu-lead grid size-4 shrink-0 place-items-center">
123
- {lead}
124
- </span>
125
- )}
143
+ const { context, describedBy } = useDescription()
126
144
 
127
- <span className="min-w-0 grow">{children}</span>
128
- {trail}
129
- </MenuPrimitive.Item>
145
+ return (
146
+ <ItemDescription value={context}>
147
+ <MenuPrimitive.Item aria-describedby={describedBy} {...rest} className={menuItem({ color, className })}>
148
+ {lead && (
149
+ <span className="menu-lead grid size-4 shrink-0 place-items-center">
150
+ {lead}
151
+ </span>
152
+ )}
153
+
154
+ <span className="min-w-0 grow">{children}</span>
155
+ {trail}
156
+ </MenuPrimitive.Item>
157
+ </ItemDescription>
130
158
  )
131
159
  }
132
160
 
@@ -135,18 +163,22 @@ export type DropdownMenuCheckboxItemProps = Omit<ComponentProps<typeof MenuPrimi
135
163
  }
136
164
 
137
165
  export function DropdownMenuCheckboxItem({ className, children, ...rest }: DropdownMenuCheckboxItemProps) {
166
+ const { context, describedBy } = useDescription()
167
+
138
168
  return (
139
- <MenuPrimitive.CheckboxItem {...rest} className={menuItem({ className })}>
140
- <span className="menu-lead grid size-4 shrink-0 place-items-center">
141
- <MenuPrimitive.CheckboxItemIndicator>
142
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
143
- <path d="M20 6 9 17l-5-5" />
144
- </svg>
145
- </MenuPrimitive.CheckboxItemIndicator>
146
- </span>
147
-
148
- <span className="min-w-0 grow">{children}</span>
149
- </MenuPrimitive.CheckboxItem>
169
+ <ItemDescription value={context}>
170
+ <MenuPrimitive.CheckboxItem aria-describedby={describedBy} {...rest} className={menuItem({ className })}>
171
+ <span className="menu-lead grid size-4 shrink-0 place-items-center">
172
+ <MenuPrimitive.CheckboxItemIndicator>
173
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
174
+ <path d="M20 6 9 17l-5-5" />
175
+ </svg>
176
+ </MenuPrimitive.CheckboxItemIndicator>
177
+ </span>
178
+
179
+ <span className="min-w-0 grow">{children}</span>
180
+ </MenuPrimitive.CheckboxItem>
181
+ </ItemDescription>
150
182
  )
151
183
  }
152
184
 
@@ -161,16 +193,39 @@ export type DropdownMenuRadioItemProps = Omit<ComponentProps<typeof MenuPrimitiv
161
193
  }
162
194
 
163
195
  export function DropdownMenuRadioItem({ className, children, ...rest }: DropdownMenuRadioItemProps) {
196
+ const { context, describedBy } = useDescription()
197
+
164
198
  return (
165
- <MenuPrimitive.RadioItem {...rest} className={menuItem({ className })}>
166
- <span className="menu-lead grid size-4 shrink-0 place-items-center">
167
- <MenuPrimitive.RadioItemIndicator>
168
- <span className="bg-bg-neutral-solid in-disabled:bg-bg-neutral-solid-checked-disabled size-2 rounded-full" />
169
- </MenuPrimitive.RadioItemIndicator>
170
- </span>
171
-
172
- <span className="min-w-0 grow">{children}</span>
173
- </MenuPrimitive.RadioItem>
199
+ <ItemDescription value={context}>
200
+ <MenuPrimitive.RadioItem aria-describedby={describedBy} {...rest} className={menuItem({ className })}>
201
+ <span className="menu-lead grid size-4 shrink-0 place-items-center">
202
+ <MenuPrimitive.RadioItemIndicator>
203
+ <span className="bg-bg-neutral-solid in-disabled:bg-bg-neutral-solid-checked-disabled block size-2 rounded-full" />
204
+ </MenuPrimitive.RadioItemIndicator>
205
+ </span>
206
+
207
+ <span className="min-w-0 grow">{children}</span>
208
+ </MenuPrimitive.RadioItem>
209
+ </ItemDescription>
210
+ )
211
+ }
212
+
213
+ const menuItemDescription = tv({ base: 'text-caption text-fg-subtle block' })
214
+
215
+ export function DropdownMenuItemDescription({ className, children }: { className?: string, children?: ReactNode }) {
216
+ const ctx = use(ItemDescription)
217
+ const setDescribed = ctx?.setDescribed
218
+
219
+ useEffect(() => {
220
+ if (!setDescribed) return
221
+ setDescribed(true)
222
+ return () => setDescribed(false)
223
+ }, [setDescribed])
224
+
225
+ return (
226
+ <span id={ctx?.id} data-description aria-hidden="true" className={menuItemDescription({ className })}>
227
+ {children}
228
+ </span>
174
229
  )
175
230
  }
176
231
 
@@ -196,20 +251,24 @@ export type DropdownMenuSubTriggerProps = Omit<ComponentProps<typeof MenuPrimiti
196
251
  }
197
252
 
198
253
  export function DropdownMenuSubTrigger({ lead, className, children, ...rest }: DropdownMenuSubTriggerProps) {
199
- return (
200
- <MenuPrimitive.SubmenuTrigger {...rest} className={menuItem({ className })}>
201
- {lead && (
202
- <span className="menu-lead grid size-4 shrink-0 place-items-center">
203
- {lead}
204
- </span>
205
- )}
254
+ const { context, describedBy } = useDescription()
206
255
 
207
- <span className="min-w-0 grow">{children}</span>
208
-
209
- <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">
210
- <path d="m9 18 6-6-6-6" />
211
- </svg>
212
- </MenuPrimitive.SubmenuTrigger>
256
+ return (
257
+ <ItemDescription value={context}>
258
+ <MenuPrimitive.SubmenuTrigger aria-describedby={describedBy} {...rest} className={menuItem({ className })}>
259
+ {lead && (
260
+ <span className="menu-lead grid size-4 shrink-0 place-items-center">
261
+ {lead}
262
+ </span>
263
+ )}
264
+
265
+ <span className="min-w-0 grow">{children}</span>
266
+
267
+ <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">
268
+ <path d="m9 18 6-6-6-6" />
269
+ </svg>
270
+ </MenuPrimitive.SubmenuTrigger>
271
+ </ItemDescription>
213
272
  )
214
273
  }
215
274
 
package/modal/index.tsx CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  BottomSheetClose,
10
10
  BottomSheetContent,
11
11
  BottomSheetDescription,
12
+ BottomSheetHeader,
12
13
  BottomSheetTitle,
13
14
  BottomSheetTrigger,
14
15
  } from '../bottom-sheet'
@@ -38,7 +39,11 @@ export const modalQuery: Record<ModalBreakpoint, string> = {
38
39
  md: '(min-width: 48rem)',
39
40
  }
40
41
 
41
- const ModalShape = createContext<Shape>('sheet')
42
+ const ModalShape = createContext<Shape | null>(null)
43
+
44
+ export function useModalShape(): Shape | null {
45
+ return use(ModalShape)
46
+ }
42
47
 
43
48
  const media = (query: string) => (typeof window.matchMedia === 'function' ? window.matchMedia(query) : null)
44
49
 
@@ -109,7 +114,7 @@ export function ModalContent({ size, ...rest }: ModalContentProps) {
109
114
  export type ModalHeaderProps = DialogHeaderProps
110
115
 
111
116
  export function ModalHeader(props: ModalHeaderProps) {
112
- return <DialogHeader {...props} />
117
+ return use(ModalShape) === 'dialog' ? <DialogHeader {...props} /> : <BottomSheetHeader {...props} />
113
118
  }
114
119
 
115
120
  export interface ModalTitleProps {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pantoia/ui",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "description": "Pantoia Design System",
5
5
  "homepage": "https://design.pantoia.com",
6
6
  "repository": {
package/popover/index.tsx CHANGED
@@ -48,7 +48,9 @@ export function PopoverClose(props: PopoverCloseProps) {
48
48
  return <PopoverPrimitive.Close {...props} />
49
49
  }
50
50
 
51
- export type PopoverContentProps = Omit<ComponentProps<typeof PopoverPrimitive.Popup>, 'className'> & {
51
+ type PopoverPlacement = Pick<ComponentProps<typeof PopoverPrimitive.Positioner>, 'anchor' | 'sideOffset' | 'alignOffset' | 'collisionAvoidance'>
52
+
53
+ export type PopoverContentProps = Omit<ComponentProps<typeof PopoverPrimitive.Popup>, 'className'> & PopoverPlacement & {
52
54
  side?: 'top' | 'right' | 'bottom' | 'left'
53
55
  align?: 'start' | 'center' | 'end'
54
56
  arrow?: boolean
@@ -73,19 +75,25 @@ export function PopoverContent({
73
75
  side = 'bottom',
74
76
  align = 'center',
75
77
  arrow = true,
78
+ anchor,
79
+ sideOffset,
80
+ alignOffset,
81
+ collisionAvoidance,
76
82
  className,
77
83
  children,
78
84
  ...rest
79
85
  }: PopoverContentProps) {
80
- const [anchor] = useContext(AnchorContext)
86
+ const [anchorPart] = useContext(AnchorContext)
81
87
 
82
88
  return (
83
89
  <PopoverPrimitive.Portal>
84
90
  <PopoverPrimitive.Positioner
85
- anchor={anchor ?? undefined}
91
+ anchor={anchor ?? anchorPart ?? undefined}
86
92
  side={side}
87
93
  align={align}
88
- sideOffset={arrow ? 14 : 8}
94
+ sideOffset={sideOffset ?? (arrow ? 14 : 8)}
95
+ alignOffset={alignOffset}
96
+ collisionAvoidance={collisionAvoidance}
89
97
  arrowPadding={16}
90
98
  className="z-overlay min-w-max will-change-transform"
91
99
  >