@pantoia/ui 0.3.0 → 0.5.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 (63) hide show
  1. package/README.md +46 -0
  2. package/accordion/index.module.css +4 -4
  3. package/accordion/index.tsx +21 -51
  4. package/alert-dialog/index.tsx +36 -42
  5. package/badge/index.tsx +1 -1
  6. package/button/index.tsx +31 -30
  7. package/callout/index.tsx +4 -4
  8. package/checkbox/index.tsx +20 -85
  9. package/combobox/index.tsx +122 -316
  10. package/command-palette/index.tsx +51 -32
  11. package/dialog/index.tsx +28 -24
  12. package/dist/index.js +5 -5
  13. package/drawer/index.module.css +2 -2
  14. package/drawer/index.tsx +52 -130
  15. package/dropdown-menu/index.tsx +87 -95
  16. package/field/index.tsx +27 -86
  17. package/input/index.tsx +7 -3
  18. package/kbd/index.tsx +2 -2
  19. package/label/index.tsx +6 -8
  20. package/number-field/index.tsx +122 -173
  21. package/package.json +19 -11
  22. package/popover/index.tsx +61 -32
  23. package/radio-group/index.tsx +35 -38
  24. package/registry.json +64 -154
  25. package/select/index.tsx +141 -70
  26. package/switch/index.tsx +13 -6
  27. package/table/index.tsx +1 -2
  28. package/tabs/index.module.css +3 -3
  29. package/tabs/index.tsx +22 -76
  30. package/textarea/index.tsx +19 -7
  31. package/toast/index.module.css +187 -0
  32. package/toast/index.tsx +141 -46
  33. package/toggle/index.tsx +4 -3
  34. package/tokens.css +7 -1
  35. package/tooltip/index.tsx +61 -30
  36. package/utilities.css +11 -6
  37. package/accordion/AGENTS.md +0 -60
  38. package/alert-dialog/AGENTS.md +0 -78
  39. package/badge/AGENTS.md +0 -35
  40. package/button/AGENTS.md +0 -39
  41. package/callout/AGENTS.md +0 -37
  42. package/checkbox/AGENTS.md +0 -54
  43. package/combobox/AGENTS.md +0 -150
  44. package/command-palette/AGENTS.md +0 -41
  45. package/dialog/AGENTS.md +0 -81
  46. package/drawer/AGENTS.md +0 -66
  47. package/dropdown-menu/AGENTS.md +0 -133
  48. package/field/AGENTS.md +0 -92
  49. package/input/AGENTS.md +0 -35
  50. package/kbd/AGENTS.md +0 -17
  51. package/label/AGENTS.md +0 -21
  52. package/number-field/AGENTS.md +0 -49
  53. package/popover/AGENTS.md +0 -64
  54. package/radio-group/AGENTS.md +0 -81
  55. package/select/AGENTS.md +0 -90
  56. package/switch/AGENTS.md +0 -37
  57. package/table/AGENTS.md +0 -82
  58. package/tabs/AGENTS.md +0 -61
  59. package/test-utils.ts +0 -10
  60. package/textarea/AGENTS.md +0 -35
  61. package/toast/AGENTS.md +0 -34
  62. package/toggle/AGENTS.md +0 -37
  63. package/tooltip/AGENTS.md +0 -50
@@ -1,154 +1,24 @@
1
1
  'use client'
2
2
 
3
- import { Popover as PopoverPrimitive } from 'radix-ui'
4
- import {
5
- createContext,
6
- use,
7
- useEffect,
8
- useId,
9
- useRef,
10
- useState,
11
- type ComponentProps,
12
- type InputHTMLAttributes,
13
- type KeyboardEvent as ReactKeyboardEvent,
14
- type ReactNode,
15
- } from 'react'
3
+ import { Combobox as ComboboxPrimitive, type ComboboxFilter, type ComboboxFilterOptions } from '@base-ui/react/combobox'
4
+ import { isValidElement, type ComponentProps, type ReactNode } from 'react'
16
5
 
17
6
  import { tv } from '../tv'
18
7
 
19
- interface ComboboxContext {
20
- id: string
21
- value: string | undefined
22
- open: boolean
23
- active: string | undefined
24
- search: string
25
- setSearch: (search: string) => void
26
- setNotify: (notify: ((search: string) => void) | undefined) => void
27
- setOpen: (open: boolean) => void
28
- pick: (value: string) => void
29
- move: (step: number) => void
30
- setActive: (value: string | undefined) => void
31
- itemId: (value: string) => string
32
- count: number
33
- ignoreFilter: boolean
34
- see: (visible: boolean) => () => void
35
- }
36
-
37
- const USABLE = '[role="option"]:not([data-disabled])'
38
-
39
- const Context = createContext<ComboboxContext | null>(null)
40
-
41
- const useCombobox = () => {
42
- const ctx = use(Context)
43
- if (!ctx) throw new Error('Combobox 부품은 Combobox 안에서만 선다')
44
- return ctx
45
- }
46
-
47
- export interface ComboboxProps {
48
- value?: string
49
- defaultValue?: string
50
- onValueChange?: (value: string) => void
51
- open?: boolean
52
- defaultOpen?: boolean
53
- onOpenChange?: (open: boolean) => void
54
- ignoreFilter?: boolean
8
+ export type ComboboxProps<Value = string, Item = Value> = Omit<ComboboxPrimitive.Root.Props<Value, false, Item>, 'className'> & {
55
9
  className?: string
56
- children?: ReactNode
57
10
  }
58
11
 
59
- export function Combobox({
60
- value,
61
- defaultValue,
62
- onValueChange,
63
- open,
64
- defaultOpen = false,
65
- onOpenChange,
66
- ignoreFilter = true,
12
+ export function Combobox<Value = string, Item = Value>({
13
+ openOnInputClick = false,
67
14
  className,
68
15
  children,
69
- }: ComboboxProps) {
70
- const id = useId()
71
- const [ownValue, setOwnValue] = useState(defaultValue)
72
- const [ownOpen, setOwnOpen] = useState(defaultOpen)
73
- const [active, setActive] = useState<string | undefined>(undefined)
74
- const [search, setSearch] = useState('')
75
- const [count, setCount] = useState(0)
76
- const [seen, setSeen] = useState(false)
77
- const notifyRef = useRef<((search: string) => void) | undefined>(undefined)
78
-
79
- const isOpen = open ?? ownOpen
80
- const current = value ?? ownValue
81
-
82
- if (seen !== isOpen) {
83
- setSeen(isOpen)
84
- if (!isOpen) setActive(undefined)
85
- }
86
-
87
- const setOpen = (next: boolean) => {
88
- if (open === undefined) setOwnOpen(next)
89
- onOpenChange?.(next)
90
- }
91
-
92
- const setNotify = (next: ((search: string) => void) | undefined) => {
93
- notifyRef.current = next
94
- }
95
-
96
- const changeSearch = (next: string) => {
97
- setSearch(next)
98
- notifyRef.current?.(next)
99
- }
100
-
101
- const pick = (next: string) => {
102
- if (value === undefined) setOwnValue(next)
103
- onValueChange?.(next)
104
- changeSearch(next)
105
- setOpen(false)
106
- }
107
-
108
- const see = (visible: boolean) => {
109
- if (!visible) return () => {}
110
- setCount(n => n + 1)
111
- return () => setCount(n => n - 1)
112
- }
113
-
114
- const move = (step: number) => {
115
- const list = document.getElementById(`${id}-list`)
116
- if (!list) return
117
-
118
- const usable = [...list.querySelectorAll<HTMLElement>(USABLE)].map(el => el.dataset.value!)
119
- if (usable.length === 0) return
120
-
121
- const at = active === undefined ? -1 : usable.indexOf(active)
122
- const next = at === -1
123
- ? (step > 0 ? 0 : usable.length - 1)
124
- : (at + step + usable.length) % usable.length
125
-
126
- setActive(usable[next])
127
- }
128
-
16
+ ...rest
17
+ }: ComboboxProps<Value, Item>) {
129
18
  return (
130
- <Context value={{
131
- id,
132
- value: current,
133
- open: isOpen,
134
- active,
135
- search,
136
- setSearch: changeSearch,
137
- setNotify,
138
- setOpen,
139
- pick,
140
- see,
141
- move,
142
- setActive,
143
- itemId: (item: string) => `${id}-${item}`,
144
- count,
145
- ignoreFilter,
146
- }}
147
- >
148
- <PopoverPrimitive.Root open={isOpen} onOpenChange={setOpen}>
149
- <div className={className}>{children}</div>
150
- </PopoverPrimitive.Root>
151
- </Context>
19
+ <ComboboxPrimitive.Root<Value, false, Item> {...rest} openOnInputClick={openOnInputClick}>
20
+ <div className={className}>{children}</div>
21
+ </ComboboxPrimitive.Root>
152
22
  )
153
23
  }
154
24
 
@@ -171,24 +41,18 @@ export const comboboxAnchor = tv({
171
41
  defaultVariants: { surface: 'bare', size: 'md' },
172
42
  })
173
43
 
174
- export interface ComboboxAnchorProps {
44
+ export type ComboboxAnchorProps = Omit<ComponentProps<typeof ComboboxPrimitive.InputGroup>, 'className'> & {
175
45
  surface?: 'bare' | 'well'
176
46
  size?: 'sm' | 'md' | 'lg'
177
47
  className?: string
178
- children?: ReactNode
179
48
  }
180
49
 
181
- export function ComboboxAnchor({ surface, size, className, children }: ComboboxAnchorProps) {
182
- return (
183
- <PopoverPrimitive.Anchor className={comboboxAnchor({ surface, size, className })}>
184
- {children}
185
- </PopoverPrimitive.Anchor>
186
- )
50
+ export function ComboboxAnchor({ surface, size, className, ...rest }: ComboboxAnchorProps) {
51
+ return <ComboboxPrimitive.InputGroup {...rest} className={comboboxAnchor({ surface, size, className })} />
187
52
  }
188
53
 
189
- export interface ComboboxInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
190
- value?: string
191
- onValueChange?: (value: string) => void
54
+ export type ComboboxInputProps = Omit<ComponentProps<typeof ComboboxPrimitive.Input>, 'className'> & {
55
+ className?: string
192
56
  }
193
57
 
194
58
  const comboboxInput = tv({
@@ -197,55 +61,8 @@ const comboboxInput = tv({
197
61
  + ' disabled:text-fg-disabled disabled:cursor-not-allowed',
198
62
  })
199
63
 
200
- export function ComboboxInput({ value, onValueChange, className, ...rest }: ComboboxInputProps) {
201
- const ctx = useCombobox()
202
- const text = value ?? ctx.search
203
- const { setNotify } = ctx
204
-
205
- useEffect(() => {
206
- setNotify(onValueChange)
207
- return () => setNotify(undefined)
208
- })
209
-
210
- const change = (next: string) => {
211
- ctx.setSearch(next)
212
- if (!ctx.open) ctx.setOpen(true)
213
- }
214
-
215
- const onKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {
216
- if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
217
- e.preventDefault()
218
- if (!ctx.open) ctx.setOpen(true)
219
- else ctx.move(e.key === 'ArrowDown' ? 1 : -1)
220
- return
221
- }
222
-
223
- if (e.key === 'Enter' && ctx.open && ctx.active !== undefined) {
224
- e.preventDefault()
225
- ctx.pick(ctx.active)
226
- return
227
- }
228
-
229
- if (e.key === 'Escape' && ctx.open) {
230
- e.preventDefault()
231
- ctx.setOpen(false)
232
- }
233
- }
234
-
235
- return (
236
- <input
237
- {...rest}
238
- role="combobox"
239
- aria-expanded={ctx.open}
240
- aria-controls={ctx.open ? `${ctx.id}-list` : undefined}
241
- aria-autocomplete="list"
242
- aria-activedescendant={ctx.active === undefined ? undefined : ctx.itemId(ctx.active)}
243
- value={text}
244
- onChange={e => change(e.target.value)}
245
- onKeyDown={onKeyDown}
246
- className={comboboxInput({ className })}
247
- />
248
- )
64
+ export function ComboboxInput({ className, ...rest }: ComboboxInputProps) {
65
+ return <ComboboxPrimitive.Input {...rest} className={comboboxInput({ className })} />
249
66
  }
250
67
 
251
68
  const comboboxTrigger = tv({
@@ -255,27 +72,20 @@ const comboboxTrigger = tv({
255
72
  + ' focus-visible:outline-line-focus-ring',
256
73
  })
257
74
 
258
- export function ComboboxTrigger({ label = '목록 열기', disabled, className }: {
75
+ export type ComboboxTriggerProps = Omit<ComponentProps<typeof ComboboxPrimitive.Trigger>, 'className'> & {
259
76
  label?: string
260
- disabled?: boolean
261
77
  className?: string
262
- }) {
263
- const ctx = useCombobox()
78
+ }
264
79
 
80
+ export function ComboboxTrigger({ label = '목록 열기', className, children, ...rest }: ComboboxTriggerProps) {
265
81
  return (
266
- <button
267
- type="button"
268
- tabIndex={-1}
269
- aria-label={label}
270
- disabled={disabled}
271
- data-state={ctx.open ? 'open' : 'closed'}
272
- onClick={() => ctx.setOpen(!ctx.open)}
273
- className={comboboxTrigger({ className })}
274
- >
275
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4 transition-transform open:rotate-180 motion-reduce:transition-none">
276
- <path d="m6 9 6 6 6-6" />
277
- </svg>
278
- </button>
82
+ <ComboboxPrimitive.Trigger aria-label={label} {...rest} className={comboboxTrigger({ className })}>
83
+ {children ?? (
84
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4 transition-transform in-data-popup-open:rotate-180 motion-reduce:transition-none">
85
+ <path d="m6 9 6 6 6-6" />
86
+ </svg>
87
+ )}
88
+ </ComboboxPrimitive.Trigger>
279
89
  )
280
90
  }
281
91
 
@@ -286,81 +96,78 @@ const comboboxCancel = tv({
286
96
  + ' focus-visible:outline-line-focus-ring',
287
97
  })
288
98
 
289
- export function ComboboxCancel({ label = '지우기', className }: { label?: string, className?: string }) {
290
- const ctx = useCombobox()
99
+ export type ComboboxCancelProps = Omit<ComponentProps<typeof ComboboxPrimitive.Clear>, 'className'> & {
100
+ label?: string
101
+ className?: string
102
+ }
291
103
 
104
+ export function ComboboxCancel({ label = '지우기', keepMounted = true, className, children, ...rest }: ComboboxCancelProps) {
292
105
  return (
293
- <button
294
- type="button"
295
- tabIndex={-1}
296
- aria-label={label}
297
- onClick={() => ctx.setSearch('')}
298
- className={comboboxCancel({ className })}
299
- >
300
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
301
- <path d="M18 6 6 18" />
302
- <path d="m6 6 12 12" />
303
- </svg>
304
- </button>
106
+ <ComboboxPrimitive.Clear aria-label={label} keepMounted={keepMounted} {...rest} className={comboboxCancel({ className })}>
107
+ {children ?? (
108
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
109
+ <path d="M18 6 6 18" />
110
+ <path d="m6 6 12 12" />
111
+ </svg>
112
+ )}
113
+ </ComboboxPrimitive.Clear>
305
114
  )
306
115
  }
307
116
 
308
- export type ComboboxContentProps =
309
- Omit<ComponentProps<typeof PopoverPrimitive.Content>, 'align' | 'sideOffset' | 'onOpenAutoFocus'>
117
+ export type ComboboxContentProps = Omit<ComponentProps<typeof ComboboxPrimitive.Popup>, 'className' | 'children'> & {
118
+ className?: string
119
+ children?: ComboboxPrimitive.List.Props['children'] | ComboboxPrimitive.List.Props['children'][]
120
+ }
310
121
 
311
122
  const comboboxContent = tv({
312
123
  base: 'float text-fg rounded-r12 z-overlay'
313
- + ' w-(--radix-popover-trigger-width)'
124
+ + ' w-(--anchor-width)'
314
125
  + ' open:animate-enter closed:animate-exit',
315
126
  })
316
127
 
317
- export function ComboboxContent({ className, children, ...rest }: ComboboxContentProps) {
318
- const label = rest['aria-label']
128
+ const isEmpty = (child: unknown) => isValidElement(child) && child.type === ComboboxEmpty
129
+
130
+ export function ComboboxContent({ 'aria-label': label, className, children, ...rest }: ComboboxContentProps) {
131
+ const nodes = Array.isArray(children) ? children : [children]
132
+ const empty = nodes.filter(isEmpty) as ReactNode[]
133
+ const list = nodes.filter(node => !isEmpty(node))
319
134
 
320
135
  return (
321
- <PopoverPrimitive.Portal>
322
- <PopoverPrimitive.Content
323
- {...rest}
136
+ <ComboboxPrimitive.Portal>
137
+ <ComboboxPrimitive.Positioner
324
138
  align="start"
325
139
  sideOffset={4}
326
- onOpenAutoFocus={e => e.preventDefault()}
327
- className={comboboxContent({ className })}
140
+ className="z-overlay min-w-max will-change-transform"
328
141
  >
329
- <ComboboxViewport label={label} className="max-h-72">
330
- {children}
331
- </ComboboxViewport>
332
- </PopoverPrimitive.Content>
333
- </PopoverPrimitive.Portal>
142
+ <ComboboxPrimitive.Popup {...rest} className={comboboxContent({ className })}>
143
+ {empty}
144
+ <ComboboxViewport aria-label={label} className="max-h-72">
145
+ {list.length === 1 ? list[0] : list as ReactNode[]}
146
+ </ComboboxViewport>
147
+ </ComboboxPrimitive.Popup>
148
+ </ComboboxPrimitive.Positioner>
149
+ </ComboboxPrimitive.Portal>
334
150
  )
335
151
  }
336
152
 
337
- const comboboxViewport = tv({ base: 'flex flex-col gap-px overflow-y-auto p-1' })
153
+ const comboboxViewport = tv({ base: 'flex flex-col gap-px overflow-y-auto p-1 data-empty:p-0' })
338
154
 
339
- export function ComboboxViewport({ label, className, children }: {
340
- label?: string
155
+ export type ComboboxViewportProps = Omit<ComponentProps<typeof ComboboxPrimitive.List>, 'className'> & {
341
156
  className?: string
342
- children?: ReactNode
343
- }) {
344
- const ctx = useCombobox()
157
+ }
345
158
 
346
- return (
347
- <div
348
- id={`${ctx.id}-list`}
349
- role={ctx.count > 0 ? 'listbox' : undefined}
350
- aria-label={ctx.count > 0 ? label : undefined}
351
- className={comboboxViewport({ className })}
352
- >
353
- {children}
354
- </div>
355
- )
159
+ export function ComboboxViewport({ className, ...rest }: ComboboxViewportProps) {
160
+ return <ComboboxPrimitive.List {...rest} className={comboboxViewport({ className })} />
356
161
  }
357
162
 
358
- export interface ComboboxItemProps {
359
- value: string
360
- textValue?: string
361
- disabled?: boolean
163
+ export type ComboboxCollectionProps = ComboboxPrimitive.Collection.Props
164
+
165
+ export function ComboboxCollection(props: ComboboxCollectionProps) {
166
+ return <ComboboxPrimitive.Collection {...props} />
167
+ }
168
+
169
+ export type ComboboxItemProps = Omit<ComponentProps<typeof ComboboxPrimitive.Item>, 'className'> & {
362
170
  className?: string
363
- children?: ReactNode
364
171
  }
365
172
 
366
173
  const comboboxItem = tv({
@@ -370,44 +177,30 @@ const comboboxItem = tv({
370
177
  + ' data-disabled:text-fg-disabled data-disabled:pointer-events-none',
371
178
  })
372
179
 
373
- export function ComboboxItem({ value, textValue, disabled = false, className, children }: ComboboxItemProps) {
374
- const ctx = useCombobox()
375
- const shown = ctx.ignoreFilter || contains(textValue ?? value, ctx.search)
376
-
377
- useEffect(() => ctx.see(shown), [ctx, shown])
378
-
379
- if (!shown) return null
380
-
180
+ export function ComboboxItem({ className, children, ...rest }: ComboboxItemProps) {
381
181
  return (
382
- <div
383
- id={ctx.itemId(value)}
384
- role="option"
385
- data-value={value}
386
- aria-selected={ctx.value === value}
387
- data-highlighted={ctx.active === value ? '' : undefined}
388
- data-disabled={disabled ? '' : undefined}
389
- onPointerMove={() => !disabled && ctx.setActive(value)}
390
- onClick={() => !disabled && ctx.pick(value)}
391
- className={comboboxItem({ className })}
392
- >
182
+ <ComboboxPrimitive.Item {...rest} className={comboboxItem({ className })}>
393
183
  <span className="flex min-w-0 flex-col gap-0.5">
394
184
  {children}
395
185
  </span>
396
186
 
397
- {ctx.value === value && (
398
- <span className="text-fg-muted flex shrink-0">
399
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
400
- <path d="M20 6 9 17l-5-5" />
401
- </svg>
402
- </span>
403
- )}
404
- </div>
187
+ <ComboboxPrimitive.ItemIndicator className="text-fg-muted flex shrink-0">
188
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
189
+ <path d="M20 6 9 17l-5-5" />
190
+ </svg>
191
+ </ComboboxPrimitive.ItemIndicator>
192
+ </ComboboxPrimitive.Item>
405
193
  )
406
194
  }
407
195
 
196
+ export interface ComboboxItemTextProps {
197
+ className?: string
198
+ children?: ReactNode
199
+ }
200
+
408
201
  const comboboxItemText = tv({ base: 'text-label text-fg' })
409
202
 
410
- export function ComboboxItemText({ className, children }: { className?: string, children?: ReactNode }) {
203
+ export function ComboboxItemText({ className, children }: ComboboxItemTextProps) {
411
204
  return (
412
205
  <span className={comboboxItemText({ className })}>
413
206
  {children}
@@ -415,9 +208,14 @@ export function ComboboxItemText({ className, children }: { className?: string,
415
208
  )
416
209
  }
417
210
 
211
+ export interface ComboboxItemDescriptionProps {
212
+ className?: string
213
+ children?: ReactNode
214
+ }
215
+
418
216
  const comboboxItemDescription = tv({ base: 'text-caption text-fg-subtle' })
419
217
 
420
- export function ComboboxItemDescription({ className, children }: { className?: string, children?: ReactNode }) {
218
+ export function ComboboxItemDescription({ className, children }: ComboboxItemDescriptionProps) {
421
219
  return (
422
220
  <span className={comboboxItemDescription({ className })}>
423
221
  {children}
@@ -425,44 +223,52 @@ export function ComboboxItemDescription({ className, children }: { className?: s
425
223
  )
426
224
  }
427
225
 
428
- const comboboxGroup = tv({ base: 'flex flex-col gap-px' })
429
-
430
- export function ComboboxGroup({ label, className, children }: {
226
+ export type ComboboxGroupProps = Omit<ComponentProps<typeof ComboboxPrimitive.Group>, 'className'> & {
431
227
  label?: string
432
228
  className?: string
433
- children?: ReactNode
434
- }) {
229
+ }
230
+
231
+ const comboboxGroup = tv({ base: 'flex flex-col gap-px' })
232
+
233
+ export function ComboboxGroup({ label, className, children, ...rest }: ComboboxGroupProps) {
435
234
  return (
436
- <div role="group" className={comboboxGroup({ className })}>
235
+ <ComboboxPrimitive.Group {...rest} className={comboboxGroup({ className })}>
437
236
  {label && (
438
- <span className="text-caption text-fg-subtle px-2 pb-1 pt-2">
237
+ <ComboboxPrimitive.GroupLabel className="text-caption text-fg-subtle px-2 pb-1 pt-2">
439
238
  {label}
440
- </span>
239
+ </ComboboxPrimitive.GroupLabel>
441
240
  )}
442
241
  {children}
443
- </div>
242
+ </ComboboxPrimitive.Group>
444
243
  )
445
244
  }
446
245
 
447
- const comboboxEmpty = tv({ base: 'text-caption text-fg-subtle px-2 py-3 text-center' })
246
+ export type ComboboxEmptyProps = Omit<ComponentProps<typeof ComboboxPrimitive.Empty>, 'className'> & {
247
+ className?: string
248
+ }
448
249
 
449
- export function ComboboxEmpty({ className, children }: { className?: string, children?: ReactNode }) {
450
- const ctx = useCombobox()
451
- if (ctx.count > 0) return null
250
+ const comboboxEmpty = tv({ base: 'text-caption text-fg-subtle m-1 px-2 py-3 text-center' })
452
251
 
252
+ export function ComboboxEmpty({ className, children, ...rest }: ComboboxEmptyProps) {
453
253
  return (
454
- <div role="presentation" className={comboboxEmpty({ className })}>
455
- {children}
456
- </div>
254
+ <ComboboxPrimitive.Empty {...rest}>
255
+ <div className={comboboxEmpty({ className })}>
256
+ {children}
257
+ </div>
258
+ </ComboboxPrimitive.Empty>
457
259
  )
458
260
  }
459
261
 
262
+ export type ComboboxSeparatorProps = Omit<ComponentProps<typeof ComboboxPrimitive.Separator>, 'className'> & {
263
+ className?: string
264
+ }
265
+
460
266
  const comboboxSeparator = tv({ base: 'bg-line -mx-1 my-1 h-px' })
461
267
 
462
- export function ComboboxSeparator({ className }: { className?: string }) {
463
- return <div className={comboboxSeparator({ className })} />
268
+ export function ComboboxSeparator({ className, ...rest }: ComboboxSeparatorProps) {
269
+ return <ComboboxPrimitive.Separator {...rest} className={comboboxSeparator({ className })} />
464
270
  }
465
271
 
466
- export function contains(text: string, search: string) {
467
- return text.normalize('NFC').toLocaleLowerCase().includes(search.normalize('NFC').toLocaleLowerCase())
272
+ export function useComboboxFilter(options?: ComboboxFilterOptions): ComboboxFilter {
273
+ return ComboboxPrimitive.useFilter(options)
468
274
  }