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