@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.
- package/CHANGELOG.md +62 -0
- package/index.css +3 -0
- package/package.json +59 -0
- package/src/_old/_table/index.ts +5 -0
- package/src/_old/_table/table-advanced.constants.tsx +24 -0
- package/src/_old/_table/table-advanced.tsx +311 -0
- package/src/_old/_table/table-advanced.types.ts +272 -0
- package/src/_old/_table/table.constants.ts +2 -0
- package/src/_old/_table/table.hook.tsx +115 -0
- package/src/_old/_table/table.lib.ts +85 -0
- package/src/_old/_table/table.tsx +916 -0
- package/src/_old/_table/table.types.ts +118 -0
- package/src/_old/_table/todo.md +11 -0
- package/src/_old/_upload/index.ts +9 -0
- package/src/_old/_upload/todo.md +38 -0
- package/src/_old/_upload/upload-advanced-chunks.tsx +1624 -0
- package/src/_old/_upload/upload-advanced.tsx +507 -0
- package/src/_old/_upload/upload-sonner.tsx +58 -0
- package/src/_old/_upload/upload.assets.tsx +239 -0
- package/src/_old/_upload/upload.constants.tsx +75 -0
- package/src/_old/_upload/upload.dto.ts +19 -0
- package/src/_old/_upload/upload.lib.tsx +630 -0
- package/src/_old/_upload/upload.tsx +491 -0
- package/src/_old/_upload/upload.types.ts +436 -0
- package/src/accordion/accordion.tsx +247 -0
- package/src/accordion/index.ts +1 -0
- package/src/alert/alert.constants.ts +17 -0
- package/src/alert/alert.tsx +52 -0
- package/src/alert/index.ts +2 -0
- package/src/alert-dialog/alert-dialog.tsx +107 -0
- package/src/alert-dialog/index.ts +1 -0
- package/src/aspect-ratio/aspect-ratio.tsx +33 -0
- package/src/aspect-ratio/index.ts +1 -0
- package/src/audio/audio-record.tsx +776 -0
- package/src/audio/audio-visualizer.tsx +377 -0
- package/src/audio/audio.libs.ts +5 -0
- package/src/audio/audio.types.ts +50 -0
- package/src/audio/index.ts +2 -0
- package/src/avatar/avatar.tsx +78 -0
- package/src/avatar/index.ts +1 -0
- package/src/badge/badge.constants.ts +38 -0
- package/src/badge/badge.tsx +19 -0
- package/src/badge/index.ts +2 -0
- package/src/breadcrumb/breadcrumb.tsx +119 -0
- package/src/breadcrumb/index.ts +1 -0
- package/src/button/button.constants.ts +44 -0
- package/src/button/button.tsx +79 -0
- package/src/button/button.types.ts +38 -0
- package/src/button/index.ts +3 -0
- package/src/button-group/button-group.constants.ts +26 -0
- package/src/button-group/button-group.tsx +65 -0
- package/src/button-group/index.ts +2 -0
- package/src/calendar/calendar.tsx +191 -0
- package/src/calendar/index.ts +1 -0
- package/src/card/card.tsx +81 -0
- package/src/card/index.ts +1 -0
- package/src/carousel/carousel.tsx +211 -0
- package/src/carousel/carousel.types.ts +23 -0
- package/src/carousel/index.ts +2 -0
- package/src/chart/chart.libs.ts +27 -0
- package/src/chart/chart.tsx +260 -0
- package/src/chart/chart.types.ts +38 -0
- package/src/chart/index.ts +3 -0
- package/src/checkbox/checkbox.tsx +144 -0
- package/src/checkbox/checkbox.types.ts +24 -0
- package/src/checkbox/index.ts +2 -0
- package/src/collapsible/collapsible.tsx +151 -0
- package/src/collapsible/index.ts +1 -0
- package/src/combobox/combobox.tsx +132 -0
- package/src/combobox/index.ts +1 -0
- package/src/command/command.tsx +192 -0
- package/src/command/command.types.ts +11 -0
- package/src/command/index.ts +2 -0
- package/src/context-menu/context-menu.tsx +178 -0
- package/src/context-menu/index.ts +1 -0
- package/src/dialog/dialog-responsive.tsx +137 -0
- package/src/dialog/dialog.tsx +97 -0
- package/src/dialog/index.ts +2 -0
- package/src/direction/direction.tsx +13 -0
- package/src/direction/index.ts +1 -0
- package/src/drawer/drawer.tsx +185 -0
- package/src/drawer/index.ts +1 -0
- package/src/dropdown-menu/dropdown-menu.tsx +181 -0
- package/src/dropdown-menu/index.ts +1 -0
- package/src/empty/empty.constants.ts +15 -0
- package/src/empty/empty.tsx +73 -0
- package/src/empty/index.ts +2 -0
- package/src/field/field.constants.ts +22 -0
- package/src/field/field.tsx +203 -0
- package/src/field/index.ts +2 -0
- package/src/hover-card/hover-card.tsx +79 -0
- package/src/hover-card/index.ts +1 -0
- package/src/input/index.ts +1 -0
- package/src/input/input.tsx +45 -0
- package/src/input-group/index.ts +1 -0
- package/src/input-group/input-group.tsx +170 -0
- package/src/input-otp/index.ts +1 -0
- package/src/input-otp/input-otp.tsx +66 -0
- package/src/item/index.ts +2 -0
- package/src/item/item.constants.ts +22 -0
- package/src/item/item.tsx +185 -0
- package/src/json-editor/index.ts +4 -0
- package/src/json-editor/json-editor.hooks.ts +21 -0
- package/src/json-editor/json-editor.libs.ts +34 -0
- package/src/json-editor/json-editor.tsx +425 -0
- package/src/json-editor/json-editor.types.ts +80 -0
- package/src/json-editor/json-editor.view.tsx +110 -0
- package/src/json-editor/json-text-area.tsx +7 -0
- package/src/kbd/index.ts +1 -0
- package/src/kbd/kbd.tsx +39 -0
- package/src/label/index.ts +1 -0
- package/src/label/label.tsx +28 -0
- package/src/menubar/index.ts +1 -0
- package/src/menubar/menubar.tsx +213 -0
- package/src/navigation-menu/index.ts +1 -0
- package/src/navigation-menu/navigation-menu.tsx +152 -0
- package/src/pagination/index.ts +2 -0
- package/src/pagination/pagination.tsx +191 -0
- package/src/pagination/pagination.types.ts +17 -0
- package/src/popover/index.ts +1 -0
- package/src/popover/popover.tsx +35 -0
- package/src/preview-panel/index.ts +3 -0
- package/src/preview-panel/preview-panel-dialog.tsx +99 -0
- package/src/preview-panel/preview-panel.tsx +389 -0
- package/src/preview-panel/preview-panel.types.ts +49 -0
- package/src/progress/index.ts +1 -0
- package/src/progress/progress.tsx +32 -0
- package/src/radio-group/index.ts +1 -0
- package/src/radio-group/radio-group.tsx +92 -0
- package/src/resizable/index.ts +1 -0
- package/src/resizable/resizable.tsx +52 -0
- package/src/scroll-area/index.ts +1 -0
- package/src/scroll-area/scroll-area.tsx +30 -0
- package/src/select/index.ts +1 -0
- package/src/select/select.tsx +138 -0
- package/src/separator/index.ts +1 -0
- package/src/separator/separator.tsx +28 -0
- package/src/sheet/index.ts +2 -0
- package/src/sheet/sheet.constants.tsx +20 -0
- package/src/sheet/sheet.tsx +92 -0
- package/src/sidebar/index.ts +4 -0
- package/src/sidebar/sidebar.constants.ts +30 -0
- package/src/sidebar/sidebar.hooks.ts +13 -0
- package/src/sidebar/sidebar.tsx +676 -0
- package/src/sidebar/sidebar.types.ts +28 -0
- package/src/skeleton/index.ts +1 -0
- package/src/skeleton/skeleton.tsx +22 -0
- package/src/slider/index.ts +1 -0
- package/src/slider/slider.tsx +57 -0
- package/src/sonner/index.ts +4 -0
- package/src/sonner/sonner.chunks.tsx +80 -0
- package/src/sonner/sonner.libs.ts +13 -0
- package/src/sonner/sonner.tsx +31 -0
- package/src/sonner/sonner.types.ts +9 -0
- package/src/switch/index.ts +1 -0
- package/src/switch/switch.tsx +63 -0
- package/src/table/index.ts +1 -0
- package/src/table/table.tsx +95 -0
- package/src/tabs/index.ts +1 -0
- package/src/tabs/tabs.tsx +151 -0
- package/src/textarea/index.ts +1 -0
- package/src/textarea/textarea.tsx +24 -0
- package/src/toggle/index.ts +2 -0
- package/src/toggle/toggle.constants.ts +22 -0
- package/src/toggle/toggle.tsx +24 -0
- package/src/toggle-group/index.ts +1 -0
- package/src/toggle-group/toggle-group.tsx +69 -0
- package/src/tooltip/index.ts +1 -0
- package/src/tooltip/tooltip.tsx +32 -0
- package/src/upload/index.ts +1 -0
- package/src/upload/upload.constants.tsx +19 -0
- package/src/upload/upload.libs.ts +97 -0
- package/src/upload/upload.tsx +340 -0
- package/src/upload/upload.types.ts +44 -0
- 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,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
|
+
}
|