@gentleduck/registry-ui 0.2.1

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 (175) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/index.css +3 -0
  3. package/package.json +59 -0
  4. package/src/_old/_table/index.ts +5 -0
  5. package/src/_old/_table/table-advanced.constants.tsx +24 -0
  6. package/src/_old/_table/table-advanced.tsx +311 -0
  7. package/src/_old/_table/table-advanced.types.ts +272 -0
  8. package/src/_old/_table/table.constants.ts +2 -0
  9. package/src/_old/_table/table.hook.tsx +115 -0
  10. package/src/_old/_table/table.lib.ts +85 -0
  11. package/src/_old/_table/table.tsx +916 -0
  12. package/src/_old/_table/table.types.ts +118 -0
  13. package/src/_old/_table/todo.md +11 -0
  14. package/src/_old/_upload/index.ts +9 -0
  15. package/src/_old/_upload/todo.md +38 -0
  16. package/src/_old/_upload/upload-advanced-chunks.tsx +1624 -0
  17. package/src/_old/_upload/upload-advanced.tsx +507 -0
  18. package/src/_old/_upload/upload-sonner.tsx +58 -0
  19. package/src/_old/_upload/upload.assets.tsx +239 -0
  20. package/src/_old/_upload/upload.constants.tsx +75 -0
  21. package/src/_old/_upload/upload.dto.ts +19 -0
  22. package/src/_old/_upload/upload.lib.tsx +630 -0
  23. package/src/_old/_upload/upload.tsx +491 -0
  24. package/src/_old/_upload/upload.types.ts +436 -0
  25. package/src/accordion/accordion.tsx +247 -0
  26. package/src/accordion/index.ts +1 -0
  27. package/src/alert/alert.constants.ts +17 -0
  28. package/src/alert/alert.tsx +52 -0
  29. package/src/alert/index.ts +2 -0
  30. package/src/alert-dialog/alert-dialog.tsx +107 -0
  31. package/src/alert-dialog/index.ts +1 -0
  32. package/src/aspect-ratio/aspect-ratio.tsx +33 -0
  33. package/src/aspect-ratio/index.ts +1 -0
  34. package/src/audio/audio-record.tsx +776 -0
  35. package/src/audio/audio-visualizer.tsx +377 -0
  36. package/src/audio/audio.libs.ts +5 -0
  37. package/src/audio/audio.types.ts +50 -0
  38. package/src/audio/index.ts +2 -0
  39. package/src/avatar/avatar.tsx +78 -0
  40. package/src/avatar/index.ts +1 -0
  41. package/src/badge/badge.constants.ts +38 -0
  42. package/src/badge/badge.tsx +19 -0
  43. package/src/badge/index.ts +2 -0
  44. package/src/breadcrumb/breadcrumb.tsx +119 -0
  45. package/src/breadcrumb/index.ts +1 -0
  46. package/src/button/button.constants.ts +44 -0
  47. package/src/button/button.tsx +79 -0
  48. package/src/button/button.types.ts +38 -0
  49. package/src/button/index.ts +3 -0
  50. package/src/button-group/button-group.constants.ts +26 -0
  51. package/src/button-group/button-group.tsx +65 -0
  52. package/src/button-group/index.ts +2 -0
  53. package/src/calendar/calendar.tsx +191 -0
  54. package/src/calendar/index.ts +1 -0
  55. package/src/card/card.tsx +81 -0
  56. package/src/card/index.ts +1 -0
  57. package/src/carousel/carousel.tsx +211 -0
  58. package/src/carousel/carousel.types.ts +23 -0
  59. package/src/carousel/index.ts +2 -0
  60. package/src/chart/chart.libs.ts +27 -0
  61. package/src/chart/chart.tsx +260 -0
  62. package/src/chart/chart.types.ts +38 -0
  63. package/src/chart/index.ts +3 -0
  64. package/src/checkbox/checkbox.tsx +144 -0
  65. package/src/checkbox/checkbox.types.ts +24 -0
  66. package/src/checkbox/index.ts +2 -0
  67. package/src/collapsible/collapsible.tsx +151 -0
  68. package/src/collapsible/index.ts +1 -0
  69. package/src/combobox/combobox.tsx +132 -0
  70. package/src/combobox/index.ts +1 -0
  71. package/src/command/command.tsx +192 -0
  72. package/src/command/command.types.ts +11 -0
  73. package/src/command/index.ts +2 -0
  74. package/src/context-menu/context-menu.tsx +178 -0
  75. package/src/context-menu/index.ts +1 -0
  76. package/src/dialog/dialog-responsive.tsx +137 -0
  77. package/src/dialog/dialog.tsx +97 -0
  78. package/src/dialog/index.ts +2 -0
  79. package/src/direction/direction.tsx +13 -0
  80. package/src/direction/index.ts +1 -0
  81. package/src/drawer/drawer.tsx +185 -0
  82. package/src/drawer/index.ts +1 -0
  83. package/src/dropdown-menu/dropdown-menu.tsx +181 -0
  84. package/src/dropdown-menu/index.ts +1 -0
  85. package/src/empty/empty.constants.ts +15 -0
  86. package/src/empty/empty.tsx +73 -0
  87. package/src/empty/index.ts +2 -0
  88. package/src/field/field.constants.ts +22 -0
  89. package/src/field/field.tsx +203 -0
  90. package/src/field/index.ts +2 -0
  91. package/src/hover-card/hover-card.tsx +79 -0
  92. package/src/hover-card/index.ts +1 -0
  93. package/src/input/index.ts +1 -0
  94. package/src/input/input.tsx +45 -0
  95. package/src/input-group/index.ts +1 -0
  96. package/src/input-group/input-group.tsx +170 -0
  97. package/src/input-otp/index.ts +1 -0
  98. package/src/input-otp/input-otp.tsx +66 -0
  99. package/src/item/index.ts +2 -0
  100. package/src/item/item.constants.ts +22 -0
  101. package/src/item/item.tsx +185 -0
  102. package/src/json-editor/index.ts +4 -0
  103. package/src/json-editor/json-editor.hooks.ts +21 -0
  104. package/src/json-editor/json-editor.libs.ts +34 -0
  105. package/src/json-editor/json-editor.tsx +425 -0
  106. package/src/json-editor/json-editor.types.ts +80 -0
  107. package/src/json-editor/json-editor.view.tsx +110 -0
  108. package/src/json-editor/json-text-area.tsx +7 -0
  109. package/src/kbd/index.ts +1 -0
  110. package/src/kbd/kbd.tsx +39 -0
  111. package/src/label/index.ts +1 -0
  112. package/src/label/label.tsx +28 -0
  113. package/src/menubar/index.ts +1 -0
  114. package/src/menubar/menubar.tsx +213 -0
  115. package/src/navigation-menu/index.ts +1 -0
  116. package/src/navigation-menu/navigation-menu.tsx +152 -0
  117. package/src/pagination/index.ts +2 -0
  118. package/src/pagination/pagination.tsx +191 -0
  119. package/src/pagination/pagination.types.ts +17 -0
  120. package/src/popover/index.ts +1 -0
  121. package/src/popover/popover.tsx +35 -0
  122. package/src/preview-panel/index.ts +3 -0
  123. package/src/preview-panel/preview-panel-dialog.tsx +99 -0
  124. package/src/preview-panel/preview-panel.tsx +389 -0
  125. package/src/preview-panel/preview-panel.types.ts +49 -0
  126. package/src/progress/index.ts +1 -0
  127. package/src/progress/progress.tsx +32 -0
  128. package/src/radio-group/index.ts +1 -0
  129. package/src/radio-group/radio-group.tsx +92 -0
  130. package/src/resizable/index.ts +1 -0
  131. package/src/resizable/resizable.tsx +52 -0
  132. package/src/scroll-area/index.ts +1 -0
  133. package/src/scroll-area/scroll-area.tsx +30 -0
  134. package/src/select/index.ts +1 -0
  135. package/src/select/select.tsx +138 -0
  136. package/src/separator/index.ts +1 -0
  137. package/src/separator/separator.tsx +28 -0
  138. package/src/sheet/index.ts +2 -0
  139. package/src/sheet/sheet.constants.tsx +20 -0
  140. package/src/sheet/sheet.tsx +92 -0
  141. package/src/sidebar/index.ts +4 -0
  142. package/src/sidebar/sidebar.constants.ts +30 -0
  143. package/src/sidebar/sidebar.hooks.ts +13 -0
  144. package/src/sidebar/sidebar.tsx +676 -0
  145. package/src/sidebar/sidebar.types.ts +28 -0
  146. package/src/skeleton/index.ts +1 -0
  147. package/src/skeleton/skeleton.tsx +22 -0
  148. package/src/slider/index.ts +1 -0
  149. package/src/slider/slider.tsx +57 -0
  150. package/src/sonner/index.ts +4 -0
  151. package/src/sonner/sonner.chunks.tsx +80 -0
  152. package/src/sonner/sonner.libs.ts +13 -0
  153. package/src/sonner/sonner.tsx +31 -0
  154. package/src/sonner/sonner.types.ts +9 -0
  155. package/src/switch/index.ts +1 -0
  156. package/src/switch/switch.tsx +63 -0
  157. package/src/table/index.ts +1 -0
  158. package/src/table/table.tsx +95 -0
  159. package/src/tabs/index.ts +1 -0
  160. package/src/tabs/tabs.tsx +151 -0
  161. package/src/textarea/index.ts +1 -0
  162. package/src/textarea/textarea.tsx +24 -0
  163. package/src/toggle/index.ts +2 -0
  164. package/src/toggle/toggle.constants.ts +22 -0
  165. package/src/toggle/toggle.tsx +24 -0
  166. package/src/toggle-group/index.ts +1 -0
  167. package/src/toggle-group/toggle-group.tsx +69 -0
  168. package/src/tooltip/index.ts +1 -0
  169. package/src/tooltip/tooltip.tsx +32 -0
  170. package/src/upload/index.ts +1 -0
  171. package/src/upload/upload.constants.tsx +19 -0
  172. package/src/upload/upload.libs.ts +97 -0
  173. package/src/upload/upload.tsx +340 -0
  174. package/src/upload/upload.types.ts +44 -0
  175. package/tsconfig.json +25 -0
@@ -0,0 +1,144 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@gentleduck/libs/cn'
4
+ import { checkersStylePattern } from '@gentleduck/motion/anim'
5
+ import { useSvgIndicator } from '@gentleduck/primitives/checkers'
6
+ import { type Direction, useDirection } from '@gentleduck/primitives/direction'
7
+ import * as React from 'react'
8
+ import { Label } from '../label'
9
+ import type { CheckboxGroupProps, CheckboxProps, CheckboxWithLabelProps, CheckedState } from './checkbox.types'
10
+
11
+ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
12
+ (
13
+ {
14
+ className,
15
+ indicator,
16
+ checkedIndicator,
17
+ style,
18
+ checked: controlledChecked,
19
+ defaultChecked = false,
20
+ onCheckedChange,
21
+ dir,
22
+ ...props
23
+ },
24
+ ref,
25
+ ) => {
26
+ const direction = useDirection(dir as Direction)
27
+ const { indicatorReady, checkedIndicatorReady, inputStyle, SvgIndicator } = useSvgIndicator({
28
+ checkedIndicator,
29
+ indicator,
30
+ })
31
+ const inputRef = React.useRef<HTMLInputElement>(null)
32
+
33
+ const isControlled = controlledChecked !== undefined
34
+ const checked = isControlled ? controlledChecked : defaultChecked
35
+
36
+ const handleChange = (next: CheckedState) => {
37
+ onCheckedChange?.(next)
38
+ }
39
+
40
+ React.useEffect(() => {
41
+ if (ref && typeof ref !== 'function' && checked === 'indeterminate' && ref.current) {
42
+ ref.current.indeterminate = true
43
+ changeCheckedState(checked, ref.current)
44
+ }
45
+ changeCheckedState(checked, inputRef.current as HTMLInputElement)
46
+ }, [checked, ref])
47
+
48
+ function changeCheckedState(state: CheckedState, input: HTMLInputElement) {
49
+ if (state === 'indeterminate') {
50
+ input.indeterminate = true
51
+ input.checked = false
52
+ } else {
53
+ input.indeterminate = false
54
+ input.checked = state as boolean
55
+ }
56
+ input.setAttribute('data-checked', `${state}`)
57
+ }
58
+
59
+ return (
60
+ <>
61
+ <input
62
+ className={cn(
63
+ checkersStylePattern({
64
+ indicatorState:
65
+ indicatorReady && checkedIndicatorReady
66
+ ? 'both'
67
+ : indicatorReady
68
+ ? 'indicatorReady'
69
+ : checkedIndicatorReady
70
+ ? 'checkedIndicatorReady'
71
+ : 'default',
72
+ type: 'checkbox',
73
+ }),
74
+ 'transition-all transition-discrete duration-[200ms,150ms] ease-(--duck-motion-ease)',
75
+ '[&:before,&:after]:transition-gpu [&:before,&:after]:duration-[inherit] [&:before,&:after]:ease-[inherit] [&:before,&:after]:will-change-[inherit]',
76
+ (indicatorReady && checkedIndicatorReady) || indicatorReady
77
+ ? ''
78
+ : 'after:mb-0.5 after:h-2.25 after:w-1 after:rotate-45 after:border-[1.5px] after:border-t-0 after:border-l-0 after:bg-transparent',
79
+ 'data-[checked="indeterminate"]:border-border data-[checked="indeterminate"]:bg-transparent data-[checked="indeterminate"]:text-foreground',
80
+ 'rounded-sm bg-transparent',
81
+ className,
82
+ )}
83
+ data-slot="checkbox"
84
+ onChange={(e) => {
85
+ const nextChecked = e.target.checked ? true : e.target.indeterminate ? 'indeterminate' : false
86
+ e.target.indeterminate = false
87
+ changeCheckedState(nextChecked, e.target)
88
+ handleChange(nextChecked)
89
+ }}
90
+ ref={ref ?? inputRef}
91
+ dir={direction}
92
+ style={{ ...style, ...inputStyle }}
93
+ type="checkbox"
94
+ {...props}
95
+ />
96
+ <SvgIndicator className="sr-only" />
97
+ </>
98
+ )
99
+ },
100
+ )
101
+ Checkbox.displayName = 'Checkbox'
102
+
103
+ const CheckboxWithLabel = React.forwardRef<HTMLDivElement, Omit<CheckboxWithLabelProps, 'ref'>>(
104
+ ({ id, _checkbox, _label, className, ...props }, ref) => {
105
+ const { className: labelClassName, ...labelProps } = _label
106
+ return (
107
+ <div
108
+ className={cn('flex items-center justify-start gap-2', className)}
109
+ ref={ref}
110
+ {...props}
111
+ data-slot="checkbox-with-label">
112
+ <Checkbox id={id} {..._checkbox} />
113
+ <Label className={cn('cursor-pointer', labelClassName)} htmlFor={id} {...labelProps} />
114
+ </div>
115
+ )
116
+ },
117
+ )
118
+ CheckboxWithLabel.displayName = 'CheckboxWithLabel'
119
+
120
+ const CheckboxGroup = React.forwardRef<HTMLDivElement, Omit<CheckboxGroupProps, 'ref'>>(
121
+ ({ subtasks, subtasks_default_values, ...props }, ref) => {
122
+ const { _checkbox, _label } = subtasks_default_values || {}
123
+ return (
124
+ <div className={cn('mb-3 flex flex-col gap-2')} {...props} data-slot="checkbox-group" ref={ref}>
125
+ {subtasks.map(({ id, title, checked }) => (
126
+ <CheckboxWithLabel
127
+ _checkbox={{
128
+ ..._checkbox,
129
+ checked,
130
+ className: 'w-4 h-4 rounded-full border-muted-foreground/80',
131
+ }}
132
+ _label={{ ..._label, children: title }}
133
+ data-slot="checkbox-with-label"
134
+ id={id}
135
+ key={id}
136
+ />
137
+ ))}
138
+ </div>
139
+ )
140
+ },
141
+ )
142
+ CheckboxGroup.displayName = 'CheckboxGroup'
143
+
144
+ export { Checkbox, CheckboxGroup, CheckboxWithLabel }
@@ -0,0 +1,24 @@
1
+ import type { Label } from '../label'
2
+ import type { Checkbox } from './checkbox'
3
+
4
+ export type CheckedState = boolean | 'indeterminate'
5
+
6
+ export interface CheckboxProps
7
+ extends Omit<React.HTMLProps<HTMLInputElement>, 'checked' | 'onChange' | 'defaultChecked'> {
8
+ indicator?: React.ReactElement
9
+ checkedIndicator?: React.ReactElement
10
+ checked?: CheckedState
11
+ defaultChecked?: CheckedState
12
+ onCheckedChange?: (checked: CheckedState) => void
13
+ }
14
+
15
+ export interface CheckboxWithLabelProps extends React.HTMLProps<HTMLDivElement> {
16
+ _checkbox: React.ComponentPropsWithoutRef<typeof Checkbox>
17
+ _label: React.ComponentPropsWithoutRef<typeof Label>
18
+ }
19
+
20
+ export type CheckboxGroupSubtasks = { id: string; title: string; checked?: CheckedState }
21
+ export type CheckboxGroupProps = React.HTMLProps<HTMLDivElement> & {
22
+ subtasks: CheckboxGroupSubtasks[]
23
+ subtasks_default_values?: CheckboxWithLabelProps
24
+ }
@@ -0,0 +1,2 @@
1
+ export * from './checkbox'
2
+ export * from './checkbox.types'
@@ -0,0 +1,151 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@gentleduck/libs/cn'
4
+ import { type Direction, useDirection } from '@gentleduck/primitives/direction'
5
+ import { MountMinimal } from '@gentleduck/primitives/mount'
6
+ import * as React from 'react'
7
+ import { Button } from '../button'
8
+
9
+ const CollapsibleContext = React.createContext<{
10
+ open: boolean
11
+ onOpenChange: (open: boolean) => void
12
+ wrapperRef: React.RefObject<HTMLDivElement | null>
13
+ triggerRef: React.RefObject<HTMLButtonElement | null>
14
+ contentRef: React.RefObject<HTMLDivElement | null>
15
+ contentId: string
16
+ } | null>(null)
17
+
18
+ export function useCollapsible() {
19
+ const context = React.useContext(CollapsibleContext)
20
+ if (!context) {
21
+ throw new Error('useCollapsible must be used within a Collapsible')
22
+ }
23
+ return context
24
+ }
25
+
26
+ const Collapsible = React.forwardRef<
27
+ HTMLDivElement,
28
+ React.HTMLAttributes<HTMLDivElement> & {
29
+ open?: boolean
30
+ onOpenChange?: (open: boolean) => void
31
+ defaultOpen?: boolean
32
+ }
33
+ >(({ children, className, open: openProp, onOpenChange, defaultOpen, dir, ...props }, ref) => {
34
+ const direction = useDirection(dir as Direction)
35
+ const wrapperRef = React.useRef<HTMLDivElement>(null)
36
+ const triggerRef = React.useRef<HTMLButtonElement>(null)
37
+ const contentRef = React.useRef<HTMLDivElement>(null)
38
+ const [open, setOpen] = React.useState(openProp ?? defaultOpen ?? false)
39
+
40
+ const contentId = React.useId()
41
+
42
+ function handleOpenChange(state: boolean) {
43
+ setOpen(state)
44
+ onOpenChange?.(state)
45
+ }
46
+
47
+ React.useEffect(() => {
48
+ if (open) {
49
+ handleOpenChange(open)
50
+ }
51
+
52
+ function handleClick() {
53
+ const open = triggerRef.current?.getAttribute('data-open') === 'true'
54
+ onOpenChange?.(open)
55
+ }
56
+
57
+ triggerRef.current?.addEventListener('click', handleClick)
58
+ return () => triggerRef.current?.removeEventListener('click', handleClick)
59
+ }, [open])
60
+
61
+ return (
62
+ <CollapsibleContext.Provider
63
+ value={{ contentId, contentRef, onOpenChange: handleOpenChange, open, triggerRef, wrapperRef }}>
64
+ <div
65
+ className={cn('flex flex-col gap-2', className)}
66
+ dir={direction}
67
+ data-slot="collapsible"
68
+ ref={(node) => {
69
+ ;(wrapperRef as React.RefObject<HTMLDivElement | null>).current = node
70
+ if (typeof ref === 'function') {
71
+ ref(node)
72
+ } else if (ref) {
73
+ ref.current = node
74
+ }
75
+ }}
76
+ {...props}
77
+ data-open={open}>
78
+ {children}
79
+ </div>
80
+ </CollapsibleContext.Provider>
81
+ )
82
+ })
83
+ Collapsible.displayName = 'Collapsible'
84
+
85
+ const CollapsibleTrigger = React.forwardRef<HTMLButtonElement, React.ComponentPropsWithoutRef<typeof Button>>(
86
+ ({ children, onClick, ...props }, ref) => {
87
+ const { open, onOpenChange, triggerRef, contentId } = useCollapsible()
88
+
89
+ return (
90
+ <Button
91
+ aria-controls={contentId}
92
+ aria-expanded={open}
93
+ data-open={open}
94
+ data-slot="collapsible-trigger"
95
+ onClick={(e) => {
96
+ onOpenChange?.(!open)
97
+ onClick?.(e)
98
+ }}
99
+ ref={(node) => {
100
+ ;(triggerRef as React.RefObject<HTMLButtonElement | null>).current = node
101
+ if (typeof ref === 'function') {
102
+ ref(node)
103
+ } else if (ref) {
104
+ ref.current = node
105
+ }
106
+ }}
107
+ variant="ghost"
108
+ {...props}>
109
+ {children}
110
+ </Button>
111
+ )
112
+ },
113
+ )
114
+ CollapsibleTrigger.displayName = 'CollapsibleTrigger'
115
+
116
+ const CollapsibleContent = React.forwardRef<
117
+ HTMLElement,
118
+ React.HTMLAttributes<HTMLDivElement> & { forceMount?: boolean }
119
+ >(({ children, forceMount = false, className, ...props }, ref) => {
120
+ const { open, contentRef, contentId } = useCollapsible()
121
+
122
+ return (
123
+ <section
124
+ aria-hidden={!open}
125
+ className={cn(
126
+ 'h-0 overflow-hidden transition-all data-[open=true]:h-auto',
127
+ 'transition-all transition-discrete duration-[200ms,150ms] ease-(--duck-motion-ease)',
128
+ className,
129
+ )}
130
+ data-open={open}
131
+ inert={!open || undefined}
132
+ data-slot="collapsible-content"
133
+ id={contentId}
134
+ ref={(node) => {
135
+ ;(contentRef as React.RefObject<HTMLDivElement | null>).current = node as HTMLDivElement
136
+ if (typeof ref === 'function') {
137
+ ref(node)
138
+ } else if (ref) {
139
+ ref.current = node
140
+ }
141
+ }}
142
+ {...props}>
143
+ <MountMinimal forceMount={forceMount} open={open} ref={contentRef as never}>
144
+ {children}
145
+ </MountMinimal>
146
+ </section>
147
+ )
148
+ })
149
+ CollapsibleContent.displayName = 'CollapsibleContent'
150
+
151
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
@@ -0,0 +1 @@
1
+ export * from './collapsible'
@@ -0,0 +1,132 @@
1
+ import { cn } from '@gentleduck/libs/cn'
2
+ import { type Direction, useDirection } from '@gentleduck/primitives/direction'
3
+ import React from 'react'
4
+ import { Badge } from '../badge'
5
+ import { Button } from '../button'
6
+ import { Checkbox } from '../checkbox'
7
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../command'
8
+ import { Popover, PopoverContent, PopoverTrigger } from '../popover'
9
+ import { Separator } from '../separator'
10
+
11
+ export type ComboboxItemType = {
12
+ label: string
13
+ value: string
14
+ }
15
+
16
+ export type ComboboxProps<TData extends readonly ComboboxItemType[], TType extends 'single' | 'multiple' = 'single'> = {
17
+ items: TData
18
+ onValueChange?: TType extends 'single'
19
+ ? (value: TData[number]['value']) => void
20
+ : (value: TData[number]['value'][]) => void
21
+ withSearch?: boolean
22
+ showSelected?: boolean
23
+ defaultValue?: TType extends 'single' ? TData[number]['value'] : TData[number]['value'][]
24
+ value?: TType extends 'single' ? TData[number]['value'] : TData[number]['value'][]
25
+ popover?: React.ComponentPropsWithoutRef<typeof Popover>
26
+ popoverTrigger?: React.ComponentPropsWithoutRef<typeof Button>
27
+ popoverContent?: React.ComponentPropsWithoutRef<typeof PopoverContent>
28
+ command?: React.ComponentPropsWithoutRef<typeof Command>
29
+ commandInput?: React.ComponentPropsWithoutRef<typeof CommandInput>
30
+ commandTriggerPlaceholder?: string
31
+ commandEmpty?: string
32
+ children: (item: TData) => React.ReactNode
33
+ }
34
+
35
+ export function Combobox<TData extends readonly ComboboxItemType[], TType extends 'single' | 'multiple' = 'single'>({
36
+ value,
37
+ defaultValue,
38
+ items,
39
+ command,
40
+ commandInput,
41
+ commandEmpty = 'Nothing found.',
42
+ commandTriggerPlaceholder = 'Select item...',
43
+ popover,
44
+ popoverTrigger,
45
+ popoverContent,
46
+ withSearch = true,
47
+ showSelected = true,
48
+ children,
49
+ }: ComboboxProps<TData, TType>) {
50
+ const { dir, ...popoverProps } = popover ?? {}
51
+ const direction = useDirection(dir as Direction)
52
+ const MAX_SELECTION = 2
53
+ const _value = value ?? defaultValue
54
+
55
+ return (
56
+ <Popover {...popoverProps} dir={direction}>
57
+ <PopoverTrigger asChild>
58
+ <Button {...popoverTrigger} variant={popoverTrigger?.variant ?? 'dashed'}>
59
+ {popoverTrigger?.children}
60
+ {showSelected &&
61
+ (_value ? (
62
+ _value instanceof Array && _value.length ? (
63
+ <>
64
+ <Separator orientation="vertical" />
65
+ <div className="flex gap-1">
66
+ {_value.length > MAX_SELECTION ? (
67
+ <Badge className="px-2 py-0.75 rounded-sm font-normal" variant={'secondary'}>
68
+ +{_value.length} Selected
69
+ </Badge>
70
+ ) : (
71
+ _value.map((item) => (
72
+ <Badge className="px-2 py-0.5 rounded-[3px] capitalize" key={item} variant={'secondary'}>
73
+ {item}
74
+ </Badge>
75
+ ))
76
+ )}
77
+ </div>
78
+ </>
79
+ ) : (
80
+ _value
81
+ )
82
+ ) : (
83
+ commandTriggerPlaceholder
84
+ ))}
85
+ </Button>
86
+ </PopoverTrigger>
87
+ <PopoverContent
88
+ {...popoverContent}
89
+ dir={direction}
90
+ className={cn('w-(--gentleduck-popover-trigger-width) p-0', popoverContent?.className)}>
91
+ <Command {...command}>
92
+ {withSearch && <CommandInput {...commandInput} className={cn('h-8 [&_svg]:size-4.5 px-2', commandInput)} />}
93
+ <CommandList>
94
+ {commandEmpty && <CommandEmpty>{commandEmpty}</CommandEmpty>}
95
+ {children(items)}
96
+ </CommandList>
97
+ </Command>
98
+ </PopoverContent>
99
+ </Popover>
100
+ )
101
+ }
102
+
103
+ export function ComboxGroup({ children, ...props }: React.ComponentPropsWithoutRef<typeof CommandGroup>) {
104
+ return <CommandGroup {...props}>{children}</CommandGroup>
105
+ }
106
+
107
+ type ComboboxItemProps<T extends ComboboxItemType> = Omit<
108
+ React.ComponentPropsWithoutRef<typeof CommandItem>,
109
+ 'onSelect'
110
+ > & {
111
+ item: T
112
+ onSelect?: (value: T['value']) => void
113
+ checked?: React.ComponentPropsWithoutRef<typeof Checkbox>['checked']
114
+ }
115
+
116
+ export function ComboboxItem<T extends ComboboxItemType>({ item, onSelect, checked, ...props }: ComboboxItemProps<T>) {
117
+ return (
118
+ <CommandItem
119
+ onSelect={() => {
120
+ onSelect?.(item.value)
121
+ }}
122
+ {...props}>
123
+ <Checkbox
124
+ aria-hidden="true"
125
+ checked={checked}
126
+ className="border-foreground/50 pointer-events-none"
127
+ tabIndex={-1}
128
+ />
129
+ {item?.label}
130
+ </CommandItem>
131
+ )
132
+ }
@@ -0,0 +1 @@
1
+ export * from './combobox'
@@ -0,0 +1,192 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@gentleduck/libs/cn'
4
+ import * as CommandPrimitive from '@gentleduck/primitives/command'
5
+ import { useKeyCommands } from '@gentleduck/vim/react'
6
+ import { Search } from 'lucide-react'
7
+ import * as React from 'react'
8
+ import { Dialog, DialogContent, DialogDescription, DialogTitle } from '../dialog'
9
+ import { ScrollArea } from '../scroll-area'
10
+ import type { CommandBadgeProps } from './command.types'
11
+
12
+ const Command = React.forwardRef<
13
+ React.ComponentRef<typeof CommandPrimitive.Root>,
14
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Root>
15
+ >(({ className, ...props }, ref) => (
16
+ <CommandPrimitive.Root
17
+ ref={ref}
18
+ data-slot="command"
19
+ className={cn(
20
+ 'flex h-full w-full max-w-162.5 flex-col overflow-hidden rounded-md bg-popover p-2 text-popover-foreground shadow-sm',
21
+ className,
22
+ )}
23
+ {...props}
24
+ />
25
+ ))
26
+ Command.displayName = CommandPrimitive.Root.displayName
27
+
28
+ const CommandInput = React.forwardRef<
29
+ React.ComponentRef<typeof CommandPrimitive.Input>,
30
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> & {
31
+ wrapperClassName?: string
32
+ }
33
+ >(({ className, wrapperClassName, placeholder = 'Search...', autoFocus = false, ...props }, ref) => (
34
+ <div className={cn('mb-2 flex items-center gap-2 border-b px-1', wrapperClassName)} data-slot="command-input">
35
+ <Search aria-hidden="true" className="size-5 shrink-0 opacity-50" />
36
+ <CommandPrimitive.Input
37
+ ref={ref}
38
+ autoFocus={autoFocus}
39
+ placeholder={placeholder}
40
+ className={cn(
41
+ 'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
42
+ className,
43
+ )}
44
+ // tabIndex={0}
45
+ {...props}
46
+ />
47
+ </div>
48
+ ))
49
+ CommandInput.displayName = CommandPrimitive.Input.displayName
50
+
51
+ const CommandList = React.forwardRef<
52
+ React.ComponentRef<typeof CommandPrimitive.List>,
53
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> & {
54
+ scrollRef?: React.Ref<HTMLDivElement>
55
+ }
56
+ >(({ className, scrollRef, ...props }, ref) => (
57
+ <ScrollArea
58
+ className="overflow-y-auto overflow-x-hidden"
59
+ data-slot="command-list"
60
+ viewportClassName="overflow-x-hidden"
61
+ viewportRef={scrollRef}>
62
+ <CommandPrimitive.List ref={ref} className={cn('max-h-75 focus:outline-none', className)} {...props} />
63
+ </ScrollArea>
64
+ ))
65
+ CommandList.displayName = CommandPrimitive.List.displayName
66
+
67
+ const CommandEmpty = React.forwardRef<
68
+ React.ComponentRef<typeof CommandPrimitive.Empty>,
69
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
70
+ >(({ className, ...props }, ref) => (
71
+ <CommandPrimitive.Empty
72
+ ref={ref}
73
+ data-slot="command-empty"
74
+ className={cn('py-6 text-center text-sm', className)}
75
+ {...props}
76
+ />
77
+ ))
78
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName
79
+
80
+ const CommandGroup = React.forwardRef<
81
+ React.ComponentRef<typeof CommandPrimitive.Group>,
82
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
83
+ >(({ className, ...props }, ref) => (
84
+ <CommandPrimitive.Group
85
+ ref={ref}
86
+ data-slot="command-group"
87
+ className={cn(
88
+ 'overflow-hidden text-foreground *:data-[slot="command-group-heading"]:px-2 *:data-[slot="command-group-heading"]:py-1.5 *:data-[slot="command-group-heading"]:font-medium *:data-[slot="command-group-heading"]:text-muted-foreground *:data-[slot="command-group-heading"]:text-sm',
89
+ className,
90
+ )}
91
+ {...props}
92
+ />
93
+ ))
94
+ CommandGroup.displayName = CommandPrimitive.Group.displayName
95
+
96
+ const CommandItem = React.forwardRef<
97
+ React.ComponentRef<typeof CommandPrimitive.Item>,
98
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
99
+ >(({ className, ...props }, ref) => (
100
+ <CommandPrimitive.Item
101
+ ref={ref}
102
+ data-slot="command-item"
103
+ className={cn(
104
+ 'relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden transition-color duration-300 hover:bg-muted hover:text-accent-foreground data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-50 [&_svg]:size-4',
105
+ className,
106
+ )}
107
+ {...props}
108
+ />
109
+ ))
110
+ CommandItem.displayName = CommandPrimitive.Item.displayName
111
+
112
+ const CommandSeparator = React.forwardRef<
113
+ React.ComponentRef<typeof CommandPrimitive.Separator>,
114
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
115
+ >(({ className, ...props }, ref) => (
116
+ <CommandPrimitive.Separator
117
+ ref={ref}
118
+ data-slot="command-separator"
119
+ className={cn('-mx-1 my-2 h-px bg-secondary', className)}
120
+ {...props}
121
+ />
122
+ ))
123
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName
124
+
125
+ function CommandShortcut({
126
+ className,
127
+ keys,
128
+ onKeysPressed,
129
+ variant = 'default',
130
+ ref,
131
+ ...props
132
+ }: CommandBadgeProps): React.JSX.Element {
133
+ const commands = React.useMemo(
134
+ () =>
135
+ keys && onKeysPressed
136
+ ? {
137
+ [keys]: {
138
+ description: keys,
139
+ execute: () => {
140
+ onKeysPressed()
141
+ },
142
+ name: keys,
143
+ },
144
+ }
145
+ : {},
146
+ [keys, onKeysPressed],
147
+ )
148
+
149
+ useKeyCommands(commands, { preventDefault: true })
150
+
151
+ return (
152
+ <kbd
153
+ className={cn(
154
+ 'focus:offset-2 pointer-events-none ms-auto inline-flex cursor-none select-none items-center gap-0.5 rounded-sm px-2 py-[.12rem] font-sans text-secondary-foreground text-sm tracking-widest transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring [&_svg]:size-3',
155
+ variant === 'secondary' && 'bg-secondary',
156
+ className,
157
+ )}
158
+ data-slot="command-badge"
159
+ ref={ref}
160
+ {...props}
161
+ />
162
+ )
163
+ }
164
+
165
+ function CommandDialog({ children, ...props }: React.ComponentPropsWithRef<typeof Dialog>): React.JSX.Element {
166
+ return (
167
+ <Dialog {...props}>
168
+ <DialogContent className="h-125 max-w-full p-0 lg:w-[700px]">
169
+ <DialogTitle className="sr-only">Command palette</DialogTitle>
170
+ <DialogDescription className="sr-only">Search for commands and navigation items</DialogDescription>
171
+ <Command className="max-w-full">{children}</Command>
172
+ </DialogContent>
173
+ </Dialog>
174
+ )
175
+ }
176
+
177
+ function useCommandListContext(__scopeCommand: Parameters<typeof CommandPrimitive.useCommandContext>[1]) {
178
+ return CommandPrimitive.useCommandContext('Command', __scopeCommand)
179
+ }
180
+
181
+ export {
182
+ Command,
183
+ CommandInput,
184
+ CommandList,
185
+ CommandGroup,
186
+ CommandItem,
187
+ CommandEmpty,
188
+ CommandShortcut,
189
+ CommandSeparator,
190
+ CommandDialog,
191
+ useCommandListContext,
192
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Props for the CommandBadge component (also used as CommandShortcut).
3
+ * This component displays a badge that indicates the keyboard shortcut for a command.
4
+ */
5
+ export interface CommandBadgeProps extends React.HTMLProps<HTMLElement> {
6
+ variant?: 'default' | 'secondary'
7
+ /** The keyboard shortcut keys (e.g., "ctrl+K"). */
8
+ keys?: string
9
+ /** Callback function that is invoked when the shortcut keys are pressed. */
10
+ onKeysPressed?: () => void
11
+ }
@@ -0,0 +1,2 @@
1
+ export * from './command'
2
+ export * from './command.types'