@gunjo/ui 0.0.1-alpha.1 → 0.0.1-alpha.2
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/LICENSE +21 -0
- package/README.ja.md +90 -0
- package/README.md +52 -91
- package/package.json +47 -6
- package/src/components/display/Accordion.tsx +185 -0
- package/src/components/display/AccordionGroup.tsx +155 -0
- package/src/components/display/ActionDataTable.tsx +413 -0
- package/src/components/display/ActivityTimelineCard.tsx +483 -0
- package/src/components/display/AnalyticsCard.tsx +167 -0
- package/src/components/display/AssetCard.tsx +242 -0
- package/src/components/display/AssetGrid.tsx +164 -0
- package/src/components/display/Avatar.tsx +127 -0
- package/src/components/display/AvatarGroup.tsx +131 -0
- package/src/components/{atoms → display}/Badge.tsx +3 -3
- package/src/components/display/BarChart.tsx +247 -0
- package/src/components/{molecules → display}/Card.tsx +1 -1
- package/src/components/display/Carousel.tsx +593 -0
- package/src/components/display/ChartLegend.tsx +124 -0
- package/src/components/display/ChatMessage.tsx +382 -0
- package/src/components/display/ChoroplethMap.tsx +613 -0
- package/src/components/display/Code.tsx +42 -0
- package/src/components/display/CodeBlock.tsx +338 -0
- package/src/components/display/ColorSwatch.tsx +71 -0
- package/src/components/display/ConcentricProgressCard.tsx +545 -0
- package/src/components/display/DataTable.tsx +522 -0
- package/src/components/display/DistributionBar.tsx +102 -0
- package/src/components/display/DocNote.tsx +36 -0
- package/src/components/display/DonutChart.tsx +257 -0
- package/src/components/display/EmptyState.tsx +44 -0
- package/src/components/display/FileTree.tsx +180 -0
- package/src/components/display/GaugeChart.tsx +219 -0
- package/src/components/display/HeatmapChart.tsx +266 -0
- package/src/components/display/Icon.tsx +66 -0
- package/src/components/display/ImagePreview.tsx +140 -0
- package/src/components/{atoms → display}/Img.tsx +46 -12
- package/src/components/display/LabeledDonutCard.tsx +475 -0
- package/src/components/display/LineChart.tsx +464 -0
- package/src/components/{molecules → display}/List.tsx +20 -13
- package/src/components/display/MarkdownRenderer.tsx +157 -0
- package/src/components/display/MetadataList.tsx +81 -0
- package/src/components/display/MiniDistributionBarCard.tsx +314 -0
- package/src/components/display/PieChart.tsx +234 -0
- package/src/components/display/QuadrantMatrix.tsx +330 -0
- package/src/components/display/RadarChart.tsx +335 -0
- package/src/components/display/RadialBarChart.tsx +264 -0
- package/src/components/display/RetentionCohortCard.tsx +350 -0
- package/src/components/display/RibbonChart.tsx +618 -0
- package/src/components/display/SearchableAccordion.tsx +270 -0
- package/src/components/display/SegmentTimelineCard.tsx +452 -0
- package/src/components/display/SegmentedGaugeCard.tsx +607 -0
- package/src/components/display/Spacer.tsx +51 -0
- package/src/components/display/SparklineChart.tsx +394 -0
- package/src/components/display/StackedBarChart.tsx +393 -0
- package/src/components/display/Statistic.tsx +70 -0
- package/src/components/{molecules → display}/Table.tsx +22 -7
- package/src/components/display/Tag.tsx +80 -0
- package/src/components/display/TagEditor.tsx +141 -0
- package/src/components/display/Timeline.tsx +121 -0
- package/src/components/{atoms → display}/ToolPill.tsx +42 -18
- package/src/components/display/TreeView.tsx +226 -0
- package/src/components/display/chart-tooltip.tsx +423 -0
- package/src/components/display/chart-utils.ts +71 -0
- package/src/components/display/circular-chart-utils.ts +147 -0
- package/src/components/display/generated/default-variant-keys.ts +90 -0
- package/src/components/display/generated/variant-keys.ts +169 -0
- package/src/components/{atoms → feedback}/Alert.tsx +12 -5
- package/src/components/feedback/Banner.tsx +90 -0
- package/src/components/{molecules → feedback}/NotificationCenter.tsx +64 -31
- package/src/components/feedback/ProgressWidget.tsx +44 -0
- package/src/components/{atoms → feedback}/Spinner.tsx +2 -2
- package/src/components/{molecules → feedback}/StatusBar.tsx +4 -4
- package/src/components/feedback/StatusScreen.tsx +148 -0
- package/src/components/{molecules → feedback}/Stepper.tsx +10 -5
- package/src/components/feedback/Toast.tsx +108 -0
- package/src/components/feedback/ToastProvider.tsx +78 -0
- package/src/components/feedback/generated/default-variant-keys.ts +16 -0
- package/src/components/feedback/generated/variant-keys.ts +21 -0
- package/src/components/generated/component-manifest.ts +1568 -454
- package/src/components/generated/component-style-hints.ts +1958 -718
- package/src/components/{atoms → inputs}/ButtonVariants.ts +13 -3
- package/src/components/inputs/Calendar.tsx +212 -0
- package/src/components/inputs/ChatComposer.tsx +75 -0
- package/src/components/inputs/ChatInput.tsx +528 -0
- package/src/components/{atoms → inputs}/Checkbox.tsx +2 -2
- package/src/components/inputs/Combobox.tsx +175 -0
- package/src/components/inputs/CopyButton.tsx +187 -0
- package/src/components/inputs/DatePicker.tsx +519 -0
- package/src/components/inputs/DateRangePicker.tsx +878 -0
- package/src/components/inputs/EditableField.tsx +182 -0
- package/src/components/{organisms → inputs}/FileUploader.tsx +24 -9
- package/src/components/inputs/FilterButton.tsx +163 -0
- package/src/components/{molecules → inputs}/Form.tsx +20 -3
- package/src/components/{atoms → inputs}/Input.tsx +2 -0
- package/src/components/inputs/InputOTP.tsx +75 -0
- package/src/components/inputs/Mention.tsx +279 -0
- package/src/components/inputs/NumberInput.tsx +109 -0
- package/src/components/inputs/PasswordGroup.tsx +138 -0
- package/src/components/inputs/PasswordInput.tsx +74 -0
- package/src/components/inputs/PasswordRequirementList.tsx +96 -0
- package/src/components/inputs/PasswordStrengthMeter.tsx +93 -0
- package/src/components/inputs/PhoneInput.tsx +99 -0
- package/src/components/inputs/PostalCodeInput.tsx +98 -0
- package/src/components/inputs/RangeSlider.tsx +129 -0
- package/src/components/inputs/SearchInput.tsx +76 -0
- package/src/components/inputs/Select.tsx +39 -0
- package/src/components/{atoms → inputs}/Slider.tsx +18 -5
- package/src/components/{molecules → inputs}/SortButton.tsx +5 -2
- package/src/components/{atoms → inputs}/Switch.tsx +15 -4
- package/src/components/inputs/TagInput.tsx +114 -0
- package/src/components/{atoms → inputs}/Textarea.tsx +1 -0
- package/src/components/inputs/TimePicker.tsx +150 -0
- package/src/components/inputs/Toggle.tsx +48 -0
- package/src/components/{atoms → inputs}/ToggleGroup.tsx +2 -2
- package/src/components/inputs/TooltipButton.tsx +148 -0
- package/src/components/inputs/VoiceInputButton.tsx +317 -0
- package/src/components/inputs/calendar-holidays.ts +56 -0
- package/src/components/inputs/generated/default-variant-keys.ts +32 -0
- package/src/components/{atoms → inputs}/generated/variant-keys.ts +19 -27
- package/src/components/layout/AspectRatio.tsx +12 -0
- package/src/components/layout/AssetInspectorPanel.tsx +416 -0
- package/src/components/layout/Cluster.tsx +56 -0
- package/src/components/layout/CollapsiblePanelToggle.tsx +94 -0
- package/src/components/layout/Container.tsx +43 -0
- package/src/components/layout/DeviceFrame.tsx +227 -0
- package/src/components/layout/Grid.tsx +65 -0
- package/src/components/layout/HStack.tsx +73 -0
- package/src/components/{organisms → layout}/InspectorPanel.tsx +6 -5
- package/src/components/layout/MarqueeFrame.tsx +158 -0
- package/src/components/layout/Resizable.tsx +94 -0
- package/src/components/layout/ScrollArea.tsx +71 -0
- package/src/components/{organisms → layout}/SpatialCanvas.tsx +12 -7
- package/src/components/layout/VStack.tsx +69 -0
- package/src/components/layout/generated/default-variant-keys.ts +16 -0
- package/src/components/layout/generated/variant-keys.ts +21 -0
- package/src/components/{molecules → navigation}/Breadcrumb.tsx +5 -4
- package/src/components/navigation/Command.tsx +266 -0
- package/src/components/navigation/CommandPalette.tsx +83 -0
- package/src/components/navigation/DocumentPager.tsx +171 -0
- package/src/components/navigation/Footer.tsx +88 -0
- package/src/components/navigation/Header.tsx +80 -0
- package/src/components/{molecules → navigation}/Menubar.tsx +45 -12
- package/src/components/navigation/NavigationMenu.tsx +128 -0
- package/src/components/navigation/PageAside.tsx +84 -0
- package/src/components/{molecules → navigation}/Pagination.tsx +60 -7
- package/src/components/{organisms → navigation}/RightRail.tsx +1 -1
- package/src/components/navigation/Sidebar.tsx +223 -0
- package/src/components/navigation/SidebarItem.tsx +160 -0
- package/src/components/{molecules → navigation}/Tabs.tsx +2 -2
- package/src/components/navigation/TextLink.tsx +71 -0
- package/src/components/navigation/generated/default-variant-keys.ts +12 -0
- package/src/components/navigation/generated/variant-keys.ts +13 -0
- package/src/components/overlay/AIChatInput.tsx +5 -0
- package/src/components/overlay/AIChatMessage.tsx +6 -0
- package/src/components/overlay/AlertDialog.tsx +145 -0
- package/src/components/overlay/ChatPanel.tsx +180 -0
- package/src/components/{molecules → overlay}/ContextMenu.tsx +65 -29
- package/src/components/{molecules → overlay}/Dialog.tsx +21 -13
- package/src/components/overlay/Drawer.tsx +131 -0
- package/src/components/{molecules → overlay}/DropdownMenu.tsx +52 -17
- package/src/components/overlay/FloatingPanel.tsx +90 -0
- package/src/components/overlay/HoverCard.tsx +36 -0
- package/src/components/overlay/MediaLightbox.tsx +403 -0
- package/src/components/overlay/MediaPickerDialog.tsx +198 -0
- package/src/components/overlay/Modal.tsx +103 -0
- package/src/components/overlay/OnboardingFlow.tsx +172 -0
- package/src/components/overlay/Popover.tsx +36 -0
- package/src/components/overlay/ShareModal.tsx +324 -0
- package/src/components/{molecules → overlay}/Sheet.tsx +76 -19
- package/src/components/overlay/Tooltip.tsx +130 -0
- package/src/components/overlay/generated/default-variant-keys.ts +14 -0
- package/src/components/overlay/generated/variant-keys.ts +17 -0
- package/src/components/patterns/BlogTemplate.tsx +46 -0
- package/src/components/{templates → patterns}/DashboardTemplate.tsx +2 -2
- package/src/components/patterns/DocsTemplate.tsx +41 -0
- package/src/components/{templates → patterns}/MediaLibraryTemplate.tsx +1 -1
- package/src/components/patterns/OnboardingTemplate.tsx +32 -0
- package/src/components/patterns/PricingTemplate.tsx +106 -0
- package/src/globals.css +173 -22
- package/src/index.ts +177 -76
- package/tailwind-theme-extend.cjs +48 -3
- package/design/atoms-metadata.json +0 -82
- package/design/molecules-metadata.json +0 -130
- package/design/organisms-metadata.json +0 -38
- package/design/templates-metadata.json +0 -38
- package/src/components/atoms/Avatar.tsx +0 -57
- package/src/components/atoms/Select.tsx +0 -28
- package/src/components/atoms/generated/default-variant-keys.ts +0 -36
- package/src/components/molecules/AIChatInput.tsx +0 -140
- package/src/components/molecules/AIChatMessage.tsx +0 -109
- package/src/components/molecules/Accordion.tsx +0 -99
- package/src/components/molecules/Calendar.tsx +0 -60
- package/src/components/molecules/Carousel.tsx +0 -261
- package/src/components/molecules/Command.tsx +0 -152
- package/src/components/molecules/FilterButton.tsx +0 -133
- package/src/components/molecules/HoverCard.tsx +0 -29
- package/src/components/molecules/Modal.tsx +0 -66
- package/src/components/molecules/Popover.tsx +0 -31
- package/src/components/molecules/ProgressWidget.tsx +0 -40
- package/src/components/molecules/Resizable.tsx +0 -47
- package/src/components/molecules/ScrollArea.tsx +0 -48
- package/src/components/molecules/SidebarItem.tsx +0 -134
- package/src/components/molecules/Toast.tsx +0 -57
- package/src/components/molecules/Tooltip.tsx +0 -30
- package/src/components/molecules/generated/default-variant-keys.ts +0 -22
- package/src/components/molecules/generated/variant-keys.ts +0 -33
- package/src/components/organisms/CommandPalette.tsx +0 -58
- package/src/components/organisms/FloatingPanel.tsx +0 -46
- package/src/components/organisms/ShareModal.tsx +0 -182
- package/src/components/organisms/ToastProvider.tsx +0 -49
- /package/src/components/{atoms → display}/Kbd.tsx +0 -0
- /package/src/components/{atoms → display}/Separator.tsx +0 -0
- /package/src/components/{atoms → display}/Skeleton.tsx +0 -0
- /package/src/components/{atoms → feedback}/Progress.tsx +0 -0
- /package/src/components/{atoms → inputs}/Button.tsx +0 -0
- /package/src/components/{atoms → inputs}/Label.tsx +0 -0
- /package/src/components/{atoms → inputs}/RadioGroup.tsx +0 -0
- /package/src/components/{organisms → navigation}/AppRail.tsx +0 -0
- /package/src/components/{templates → patterns}/AuthTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/BannalyzeTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/ChatTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/EditorTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/KanbanTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/LandingTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/SettingsTemplate.tsx +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { IconPencil as Pencil } from "@tabler/icons-react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
import { Button } from "./Button"
|
|
8
|
+
import { Textarea } from "./Textarea"
|
|
9
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../overlay/Tooltip"
|
|
10
|
+
|
|
11
|
+
export interface EditableFieldLabels {
|
|
12
|
+
edit?: string
|
|
13
|
+
save?: string
|
|
14
|
+
cancel?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface EditableFieldProps
|
|
18
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "defaultValue" | "onChange"> {
|
|
19
|
+
label: React.ReactNode
|
|
20
|
+
value: string
|
|
21
|
+
onSave?: (value: string) => void | Promise<void>
|
|
22
|
+
labels?: EditableFieldLabels
|
|
23
|
+
placeholder?: string
|
|
24
|
+
minRows?: number
|
|
25
|
+
maxRows?: number
|
|
26
|
+
fieldClassName?: string
|
|
27
|
+
inputClassName?: string
|
|
28
|
+
disabled?: boolean
|
|
29
|
+
error?: React.ReactNode
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const rowHeight = 20
|
|
33
|
+
const verticalPadding = 18
|
|
34
|
+
const borderHeight = 2
|
|
35
|
+
|
|
36
|
+
function getRowsHeight(rows: number) {
|
|
37
|
+
return rows * rowHeight + verticalPadding
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const EditableField = React.forwardRef<HTMLDivElement, EditableFieldProps>(
|
|
41
|
+
(
|
|
42
|
+
{
|
|
43
|
+
label,
|
|
44
|
+
value,
|
|
45
|
+
onSave,
|
|
46
|
+
labels,
|
|
47
|
+
placeholder = "-",
|
|
48
|
+
minRows = 1,
|
|
49
|
+
maxRows = 3,
|
|
50
|
+
fieldClassName,
|
|
51
|
+
inputClassName,
|
|
52
|
+
disabled,
|
|
53
|
+
error,
|
|
54
|
+
className,
|
|
55
|
+
...props
|
|
56
|
+
},
|
|
57
|
+
ref
|
|
58
|
+
) => {
|
|
59
|
+
const [editing, setEditing] = React.useState(false)
|
|
60
|
+
const [draft, setDraft] = React.useState(value)
|
|
61
|
+
const [saving, setSaving] = React.useState(false)
|
|
62
|
+
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
|
|
63
|
+
const resolvedMinRows = Math.max(1, minRows)
|
|
64
|
+
const resolvedMaxRows = Math.max(resolvedMinRows, maxRows)
|
|
65
|
+
const minHeight = getRowsHeight(resolvedMinRows)
|
|
66
|
+
const maxHeight = getRowsHeight(resolvedMaxRows)
|
|
67
|
+
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
if (!editing) {
|
|
70
|
+
setDraft(value)
|
|
71
|
+
}
|
|
72
|
+
}, [editing, value])
|
|
73
|
+
|
|
74
|
+
React.useLayoutEffect(() => {
|
|
75
|
+
if (!editing || !textareaRef.current) return
|
|
76
|
+
const textarea = textareaRef.current
|
|
77
|
+
textarea.style.height = "auto"
|
|
78
|
+
const nextHeight = Math.min(maxHeight, Math.max(minHeight, textarea.scrollHeight + borderHeight))
|
|
79
|
+
textarea.style.height = `${nextHeight}px`
|
|
80
|
+
}, [draft, editing, maxHeight, minHeight])
|
|
81
|
+
|
|
82
|
+
const commit = async () => {
|
|
83
|
+
try {
|
|
84
|
+
setSaving(true)
|
|
85
|
+
await onSave?.(draft.trim())
|
|
86
|
+
setEditing(false)
|
|
87
|
+
} catch {
|
|
88
|
+
// Keep edit mode open so controlled error feedback can explain the failure.
|
|
89
|
+
} finally {
|
|
90
|
+
setSaving(false)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const surfaceClass = cn(
|
|
95
|
+
"box-border w-full min-w-0 rounded-md border bg-muted/20 px-3 py-2 text-sm leading-5 text-foreground shadow-none",
|
|
96
|
+
"whitespace-pre-wrap break-words",
|
|
97
|
+
error && "border-destructive-border",
|
|
98
|
+
fieldClassName
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div ref={ref} className={cn("relative grid w-full gap-1.5 p-0", className)} {...props} data-slot="editable-field">
|
|
103
|
+
<div className="relative flex h-7 items-center pr-28">
|
|
104
|
+
<div className="min-w-0 text-xs font-medium leading-5 text-foreground">{label}</div>
|
|
105
|
+
<div className="absolute right-0 top-0 flex h-7 shrink-0 items-center justify-end gap-1">
|
|
106
|
+
{onSave && !editing ? (
|
|
107
|
+
<Tooltip>
|
|
108
|
+
<TooltipTrigger asChild>
|
|
109
|
+
<Button
|
|
110
|
+
type="button"
|
|
111
|
+
variant="ghost"
|
|
112
|
+
size="icon"
|
|
113
|
+
className="h-7 w-7 text-muted-foreground"
|
|
114
|
+
aria-label={labels?.edit ?? "Edit"}
|
|
115
|
+
disabled={disabled}
|
|
116
|
+
onClick={() => {
|
|
117
|
+
setDraft(value)
|
|
118
|
+
setEditing(true)
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<Pencil className="h-3.5 w-3.5" aria-hidden="true" />
|
|
122
|
+
</Button>
|
|
123
|
+
</TooltipTrigger>
|
|
124
|
+
<TooltipContent>{labels?.edit ?? "Edit"}</TooltipContent>
|
|
125
|
+
</Tooltip>
|
|
126
|
+
) : null}
|
|
127
|
+
{editing ? (
|
|
128
|
+
<>
|
|
129
|
+
<Button
|
|
130
|
+
type="button"
|
|
131
|
+
variant="ghost"
|
|
132
|
+
size="sm"
|
|
133
|
+
className="h-7 px-2"
|
|
134
|
+
disabled={saving}
|
|
135
|
+
onClick={() => {
|
|
136
|
+
setDraft(value)
|
|
137
|
+
setEditing(false)
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
{labels?.cancel ?? "Cancel"}
|
|
141
|
+
</Button>
|
|
142
|
+
<Button type="button" size="sm" className="h-7 px-2" disabled={saving} onClick={commit}>
|
|
143
|
+
{labels?.save ?? "Save"}
|
|
144
|
+
</Button>
|
|
145
|
+
</>
|
|
146
|
+
) : null}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
{editing ? (
|
|
150
|
+
<Textarea
|
|
151
|
+
ref={textareaRef}
|
|
152
|
+
value={draft}
|
|
153
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
154
|
+
className={cn(surfaceClass, "block h-auto resize-none overflow-y-auto align-top", inputClassName)}
|
|
155
|
+
rows={resolvedMinRows}
|
|
156
|
+
aria-invalid={error ? true : undefined}
|
|
157
|
+
style={{ minHeight, maxHeight }}
|
|
158
|
+
/>
|
|
159
|
+
) : (
|
|
160
|
+
<div
|
|
161
|
+
className={cn(surfaceClass, "overflow-y-auto")}
|
|
162
|
+
style={{ minHeight, maxHeight }}
|
|
163
|
+
>
|
|
164
|
+
{value ? (
|
|
165
|
+
<span className="min-w-0 break-words">{value}</span>
|
|
166
|
+
) : (
|
|
167
|
+
<span className="text-muted-foreground">{placeholder}</span>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
{error ? (
|
|
172
|
+
<p className="text-xs leading-5 text-destructive" role="alert">
|
|
173
|
+
{error}
|
|
174
|
+
</p>
|
|
175
|
+
) : null}
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
EditableField.displayName = "EditableField"
|
|
181
|
+
|
|
182
|
+
export { EditableField }
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
-
import { Upload, X, FileText, CheckCircle, AlertCircle } from "
|
|
4
|
+
import { IconUpload as Upload, IconX as X, IconFileText as FileText, IconCircleCheck as CheckCircle, IconAlertCircle as AlertCircle } from "@tabler/icons-react";
|
|
5
5
|
import { cn } from "../../lib/utils"
|
|
6
|
-
import { Button } from "../
|
|
7
|
-
import { Progress } from "../
|
|
6
|
+
import { Button } from "../inputs/Button"
|
|
7
|
+
import { Progress } from "../feedback/Progress"
|
|
8
8
|
|
|
9
9
|
export interface FileUploaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
10
10
|
onValueChange?: (files: File[]) => void
|
|
@@ -12,6 +12,13 @@ export interface FileUploaderProps extends React.HTMLAttributes<HTMLDivElement>
|
|
|
12
12
|
maxSize?: number // in bytes
|
|
13
13
|
accept?: Record<string, string[]>
|
|
14
14
|
disabled?: boolean
|
|
15
|
+
labels?: {
|
|
16
|
+
browse?: React.ReactNode
|
|
17
|
+
drop?: React.ReactNode
|
|
18
|
+
maxSize?: (sizeMb: number) => React.ReactNode
|
|
19
|
+
removeFile?: string
|
|
20
|
+
fileTooLarge?: string
|
|
21
|
+
}
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
interface FileState {
|
|
@@ -22,10 +29,11 @@ interface FileState {
|
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
export const FileUploader = React.forwardRef<HTMLDivElement, FileUploaderProps>(
|
|
25
|
-
({ className, onValueChange, maxFiles = 1, maxSize = 1024 * 1024 * 5, accept, disabled, ...props }, ref) => {
|
|
32
|
+
({ className, onValueChange, maxFiles = 1, maxSize = 1024 * 1024 * 5, accept, disabled, labels, ...props }, ref) => {
|
|
26
33
|
const [dragActive, setDragActive] = React.useState(false)
|
|
27
34
|
const [files, setFiles] = React.useState<FileState[]>([])
|
|
28
35
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
36
|
+
const maxSizeMb = Math.round(maxSize / 1024 / 1024)
|
|
29
37
|
|
|
30
38
|
const handleDrag = React.useCallback((e: React.DragEvent) => {
|
|
31
39
|
e.preventDefault()
|
|
@@ -38,7 +46,7 @@ export const FileUploader = React.forwardRef<HTMLDivElement, FileUploaderProps>(
|
|
|
38
46
|
}, [])
|
|
39
47
|
|
|
40
48
|
const validateFile = (file: File): string | undefined => {
|
|
41
|
-
if (file.size > maxSize) return "File too large"
|
|
49
|
+
if (file.size > maxSize) return labels?.fileTooLarge ?? "File too large"
|
|
42
50
|
// Simple check, in real app use proper mime matching
|
|
43
51
|
return undefined
|
|
44
52
|
}
|
|
@@ -89,11 +97,16 @@ export const FileUploader = React.forwardRef<HTMLDivElement, FileUploaderProps>(
|
|
|
89
97
|
// For this UI component, we just accept the files.
|
|
90
98
|
|
|
91
99
|
return (
|
|
92
|
-
<div
|
|
100
|
+
<div
|
|
101
|
+
className={cn("grid w-full max-w-[640px] gap-4", className)}
|
|
102
|
+
ref={ref}
|
|
103
|
+
{...props}
|
|
104
|
+
data-slot="file-uploader"
|
|
105
|
+
>
|
|
93
106
|
<div
|
|
94
107
|
className={cn(
|
|
95
108
|
"relative flex flex-col items-center justify-center w-full h-32 rounded-lg border-2 border-dashed transition-colors",
|
|
96
|
-
dragActive ? "border-primary bg-primary
|
|
109
|
+
dragActive ? "border-primary-border bg-primary-subtle" : "border-muted-foreground/25 hover:bg-muted/50",
|
|
97
110
|
disabled && "opacity-60 cursor-not-allowed"
|
|
98
111
|
)}
|
|
99
112
|
onDragEnter={handleDrag}
|
|
@@ -112,10 +125,11 @@ export const FileUploader = React.forwardRef<HTMLDivElement, FileUploaderProps>(
|
|
|
112
125
|
<div className="flex flex-col items-center justify-center pt-5 pb-6 text-center cursor-pointer" onClick={() => inputRef.current?.click()}>
|
|
113
126
|
<Upload className="w-8 h-8 mb-3 text-muted-foreground" />
|
|
114
127
|
<p className="mb-1 text-sm text-foreground">
|
|
115
|
-
<span className="font-semibold">Click to upload</span>
|
|
128
|
+
<span className="font-semibold">{labels?.browse ?? "Click to upload"}</span>{" "}
|
|
129
|
+
{labels?.drop ?? "or drag and drop"}
|
|
116
130
|
</p>
|
|
117
131
|
<p className="text-xs text-muted-foreground">
|
|
118
|
-
|
|
132
|
+
{labels?.maxSize?.(maxSizeMb) ?? `Max size ${maxSizeMb}MB`}
|
|
119
133
|
</p>
|
|
120
134
|
</div>
|
|
121
135
|
</div>
|
|
@@ -137,6 +151,7 @@ export const FileUploader = React.forwardRef<HTMLDivElement, FileUploaderProps>(
|
|
|
137
151
|
size="icon"
|
|
138
152
|
className="h-6 w-6"
|
|
139
153
|
onClick={() => removeFile(index)}
|
|
154
|
+
aria-label={labels?.removeFile ?? "Remove file"}
|
|
140
155
|
>
|
|
141
156
|
<X className="h-4 w-4" />
|
|
142
157
|
</Button>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { IconFilter as FilterIcon, IconCheck as Check } from "@tabler/icons-react";
|
|
5
|
+
import { Button, ButtonProps } from "../inputs/Button"
|
|
6
|
+
import { Popover, PopoverTrigger, PopoverContent } from "../overlay/Popover"
|
|
7
|
+
import { Command, CommandGroup, CommandItem, CommandList } from "../navigation/Command"
|
|
8
|
+
import { cn } from "../../lib/utils"
|
|
9
|
+
import { Badge } from "../display/Badge"
|
|
10
|
+
import type { FilterButtonVariantKey } from "./generated/variant-keys"
|
|
11
|
+
import { filterButtonDefaultVariantKey } from "./generated/default-variant-keys"
|
|
12
|
+
|
|
13
|
+
export interface FilterOption {
|
|
14
|
+
label: string
|
|
15
|
+
value: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FilterButtonProps extends ButtonProps {
|
|
19
|
+
title?: string
|
|
20
|
+
icon?: React.ReactNode
|
|
21
|
+
options?: FilterOption[]
|
|
22
|
+
selectedValues?: Set<string>
|
|
23
|
+
onFilterChange?: (values: Set<string>) => void
|
|
24
|
+
clearLabel?: string
|
|
25
|
+
selectedLabel?: (count: number) => string
|
|
26
|
+
contentClassName?: string
|
|
27
|
+
contentAlign?: React.ComponentPropsWithoutRef<typeof PopoverContent>["align"]
|
|
28
|
+
portalContainer?: HTMLElement | null
|
|
29
|
+
children?: React.ReactNode
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const FilterButton = React.forwardRef<HTMLButtonElement, FilterButtonProps>(
|
|
33
|
+
(
|
|
34
|
+
{
|
|
35
|
+
className,
|
|
36
|
+
title = "Filter",
|
|
37
|
+
icon,
|
|
38
|
+
options = [],
|
|
39
|
+
selectedValues = new Set(),
|
|
40
|
+
onFilterChange,
|
|
41
|
+
clearLabel = "Clear filters",
|
|
42
|
+
selectedLabel = (count) => `${count} selected`,
|
|
43
|
+
contentClassName,
|
|
44
|
+
contentAlign = "start",
|
|
45
|
+
portalContainer,
|
|
46
|
+
children,
|
|
47
|
+
variant = "outline",
|
|
48
|
+
size = "sm",
|
|
49
|
+
...props
|
|
50
|
+
},
|
|
51
|
+
ref
|
|
52
|
+
) => {
|
|
53
|
+
const [isOpen, setIsOpen] = React.useState(false)
|
|
54
|
+
|
|
55
|
+
const triggerStyles: Record<FilterButtonVariantKey, string> = {
|
|
56
|
+
default: "inline-flex h-9 max-w-full flex-row items-center gap-2 rounded-md border border-dashed px-3 py-2",
|
|
57
|
+
popover: "inline-flex h-9 max-w-full flex-row items-center gap-2 rounded-md border border-primary-border bg-primary-subtle px-3 py-2 text-primary-subtle-foreground",
|
|
58
|
+
selected: "inline-flex h-9 max-w-full flex-row items-center gap-2 rounded-md border border-primary bg-primary-subtle px-3 py-2 text-primary-subtle-foreground",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const triggerVariant: FilterButtonVariantKey =
|
|
62
|
+
selectedValues.size > 0 ? "selected" : isOpen ? "popover" : filterButtonDefaultVariantKey
|
|
63
|
+
|
|
64
|
+
const handleSelect = (value: string) => {
|
|
65
|
+
const newSet = new Set(selectedValues)
|
|
66
|
+
if (newSet.has(value)) {
|
|
67
|
+
newSet.delete(value)
|
|
68
|
+
} else {
|
|
69
|
+
newSet.add(value)
|
|
70
|
+
}
|
|
71
|
+
onFilterChange?.(newSet)
|
|
72
|
+
}
|
|
73
|
+
const popoverContent = children ?? (
|
|
74
|
+
<Command>
|
|
75
|
+
<CommandList>
|
|
76
|
+
<CommandGroup>
|
|
77
|
+
{options.map((option) => {
|
|
78
|
+
const isSelected = selectedValues.has(option.value)
|
|
79
|
+
return (
|
|
80
|
+
<CommandItem
|
|
81
|
+
key={option.value}
|
|
82
|
+
onSelect={() => handleSelect(option.value)}
|
|
83
|
+
>
|
|
84
|
+
<div
|
|
85
|
+
className={cn(
|
|
86
|
+
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
|
87
|
+
isSelected
|
|
88
|
+
? "bg-primary text-primary-foreground"
|
|
89
|
+
: "opacity-50 [&_svg]:invisible"
|
|
90
|
+
)}
|
|
91
|
+
>
|
|
92
|
+
<Check className="h-4 w-4" />
|
|
93
|
+
</div>
|
|
94
|
+
<span>{option.label}</span>
|
|
95
|
+
</CommandItem>
|
|
96
|
+
)
|
|
97
|
+
})}
|
|
98
|
+
</CommandGroup>
|
|
99
|
+
{selectedValues.size > 0 && (
|
|
100
|
+
<>
|
|
101
|
+
<div className="h-px bg-border mx-1 my-1" />
|
|
102
|
+
<CommandGroup>
|
|
103
|
+
<CommandItem
|
|
104
|
+
onSelect={() => onFilterChange?.(new Set())}
|
|
105
|
+
className="justify-center text-center"
|
|
106
|
+
>
|
|
107
|
+
{clearLabel}
|
|
108
|
+
</CommandItem>
|
|
109
|
+
</CommandGroup>
|
|
110
|
+
</>
|
|
111
|
+
)}
|
|
112
|
+
</CommandList>
|
|
113
|
+
</Command>
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
118
|
+
<PopoverTrigger asChild>
|
|
119
|
+
<Button
|
|
120
|
+
ref={ref}
|
|
121
|
+
variant={variant}
|
|
122
|
+
size={size}
|
|
123
|
+
className={cn(triggerStyles[triggerVariant], className)}
|
|
124
|
+
{...props}
|
|
125
|
+
>
|
|
126
|
+
{icon ?? <FilterIcon className="h-4 w-4" aria-hidden="true" />}
|
|
127
|
+
<span className="min-w-0 truncate">{title}</span>
|
|
128
|
+
<Badge variant="secondary"
|
|
129
|
+
className={cn(
|
|
130
|
+
"ml-0.5 inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 text-xs leading-none",
|
|
131
|
+
selectedValues.size === 0 && "invisible"
|
|
132
|
+
)}
|
|
133
|
+
aria-hidden={selectedValues.size === 0 ? "true" : undefined}
|
|
134
|
+
aria-label={selectedValues.size > 0 ? selectedLabel(selectedValues.size) : undefined}
|
|
135
|
+
>
|
|
136
|
+
{selectedValues.size || 0}
|
|
137
|
+
</Badge>
|
|
138
|
+
</Button>
|
|
139
|
+
</PopoverTrigger>
|
|
140
|
+
{contentClassName ? (
|
|
141
|
+
<PopoverContent
|
|
142
|
+
className={contentClassName}
|
|
143
|
+
align={contentAlign}
|
|
144
|
+
portalContainer={portalContainer}
|
|
145
|
+
>
|
|
146
|
+
{popoverContent}
|
|
147
|
+
</PopoverContent>
|
|
148
|
+
) : (
|
|
149
|
+
<PopoverContent
|
|
150
|
+
className="w-[200px] p-0"
|
|
151
|
+
align={contentAlign}
|
|
152
|
+
portalContainer={portalContainer}
|
|
153
|
+
>
|
|
154
|
+
{popoverContent}
|
|
155
|
+
</PopoverContent>
|
|
156
|
+
)}
|
|
157
|
+
</Popover>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
FilterButton.displayName = "FilterButton"
|
|
162
|
+
|
|
163
|
+
export { FilterButton }
|
|
@@ -3,16 +3,28 @@ import { cva } from "class-variance-authority"
|
|
|
3
3
|
import { cn } from "../../lib/utils"
|
|
4
4
|
|
|
5
5
|
const formFieldVariants = cva("flex flex-col gap-1.5 p-4")
|
|
6
|
+
const formGroupVariants = cva(
|
|
7
|
+
"flex flex-col gap-1.5 p-0 [&_[data-slot=combobox]]:!w-full [&_[data-slot=date-picker]]:!w-full [&_[data-slot=date-range-picker]]:!w-full [&_[data-slot=editable-field]]:!w-full [&_[data-slot=file-uploader]]:!w-full [&_[data-slot=form-control]]:w-full [&_[data-slot=input]]:!w-full [&_[data-slot=input-otp]]:!w-full [&_[data-slot=mention]]:!w-full [&_[data-slot=number-input]]:!w-full [&_[data-slot=password-group]]:!w-full [&_[data-slot=password-input]]:!w-full [&_[data-slot=phone-input]]:!w-full [&_[data-slot=postal-code-input]]:!w-full [&_[data-slot=range-slider]]:!w-full [&_[data-slot=search-input]]:!w-full [&_[data-slot=select-control]]:!w-full [&_[data-slot=slider]]:!w-full [&_[data-slot=tag-input]]:!w-full [&_[data-slot=textarea]]:!w-full [&_[data-slot=time-picker]]:!w-full"
|
|
8
|
+
)
|
|
6
9
|
|
|
7
10
|
export interface FormFieldProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
8
11
|
|
|
9
12
|
const FormField = React.forwardRef<HTMLDivElement, FormFieldProps>(
|
|
10
13
|
({ className, ...props }, ref) => (
|
|
11
|
-
<div ref={ref} className={cn(formFieldVariants(), className)} {...props} />
|
|
14
|
+
<div ref={ref} className={cn(formFieldVariants(), className)} {...props} data-slot="form-field" />
|
|
12
15
|
)
|
|
13
16
|
)
|
|
14
17
|
FormField.displayName = "FormField"
|
|
15
18
|
|
|
19
|
+
export interface FormGroupProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
20
|
+
|
|
21
|
+
const FormGroup = React.forwardRef<HTMLDivElement, FormGroupProps>(
|
|
22
|
+
({ className, ...props }, ref) => (
|
|
23
|
+
<div ref={ref} className={cn(formGroupVariants(), className)} {...props} data-slot="form-group" />
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
FormGroup.displayName = "FormGroup"
|
|
27
|
+
|
|
16
28
|
export interface FormLabelProps
|
|
17
29
|
extends React.LabelHTMLAttributes<HTMLLabelElement> {}
|
|
18
30
|
|
|
@@ -25,6 +37,7 @@ const FormLabel = React.forwardRef<HTMLLabelElement, FormLabelProps>(
|
|
|
25
37
|
className
|
|
26
38
|
)}
|
|
27
39
|
{...props}
|
|
40
|
+
data-slot="form-label"
|
|
28
41
|
/>
|
|
29
42
|
)
|
|
30
43
|
)
|
|
@@ -34,7 +47,7 @@ export interface FormControlProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
34
47
|
|
|
35
48
|
const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
|
|
36
49
|
({ className, ...props }, ref) => (
|
|
37
|
-
<div ref={ref} className={cn(className)} {...props} />
|
|
50
|
+
<div ref={ref} className={cn(className)} {...props} data-slot="form-control" />
|
|
38
51
|
)
|
|
39
52
|
)
|
|
40
53
|
FormControl.displayName = "FormControl"
|
|
@@ -50,6 +63,7 @@ const FormDescription = React.forwardRef<
|
|
|
50
63
|
ref={ref}
|
|
51
64
|
className={cn("text-xs font-normal text-muted-foreground", className)}
|
|
52
65
|
{...props}
|
|
66
|
+
data-slot="form-description"
|
|
53
67
|
/>
|
|
54
68
|
))
|
|
55
69
|
FormDescription.displayName = "FormDescription"
|
|
@@ -63,6 +77,7 @@ const FormMessage = React.forwardRef<HTMLParagraphElement, FormMessageProps>(
|
|
|
63
77
|
ref={ref}
|
|
64
78
|
className={cn("text-xs font-medium text-destructive", className)}
|
|
65
79
|
{...props}
|
|
80
|
+
data-slot="form-message"
|
|
66
81
|
>
|
|
67
82
|
{children}
|
|
68
83
|
</p>
|
|
@@ -74,7 +89,7 @@ export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> {}
|
|
|
74
89
|
|
|
75
90
|
const Form = React.forwardRef<HTMLFormElement, FormProps>(
|
|
76
91
|
({ className, ...props }, ref) => (
|
|
77
|
-
<form ref={ref} className={cn("flex flex-col gap-4", className)} {...props} />
|
|
92
|
+
<form ref={ref} className={cn("flex flex-col gap-4", className)} {...props} data-slot="form" />
|
|
78
93
|
)
|
|
79
94
|
)
|
|
80
95
|
Form.displayName = "Form"
|
|
@@ -82,9 +97,11 @@ Form.displayName = "Form"
|
|
|
82
97
|
export {
|
|
83
98
|
Form,
|
|
84
99
|
FormField,
|
|
100
|
+
FormGroup,
|
|
85
101
|
FormLabel,
|
|
86
102
|
FormControl,
|
|
87
103
|
FormDescription,
|
|
88
104
|
FormMessage,
|
|
89
105
|
formFieldVariants,
|
|
106
|
+
formGroupVariants,
|
|
90
107
|
}
|
|
@@ -20,6 +20,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
20
20
|
type={type}
|
|
21
21
|
className={cn(
|
|
22
22
|
"inline-flex h-9 w-[280px] max-w-full items-center rounded-lg border border-input bg-transparent px-3 py-2 text-sm font-normal shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed",
|
|
23
|
+
"aria-invalid:border-destructive-border aria-invalid:ring-destructive-border aria-invalid:focus-visible:ring-destructive-border",
|
|
23
24
|
inputVariantClasses[inputState],
|
|
24
25
|
inputVariantClasses.placeholder,
|
|
25
26
|
className
|
|
@@ -28,6 +29,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
28
29
|
suppressHydrationWarning
|
|
29
30
|
disabled={disabled}
|
|
30
31
|
{...props}
|
|
32
|
+
data-slot="input"
|
|
31
33
|
/>
|
|
32
34
|
)
|
|
33
35
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { OTPInput, OTPInputContext } from "input-otp"
|
|
5
|
+
import { IconMinus as Minus } from "@tabler/icons-react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils"
|
|
8
|
+
|
|
9
|
+
const InputOTP = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof OTPInput>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof OTPInput>
|
|
12
|
+
>(({ className, containerClassName, ...props }, ref) => (
|
|
13
|
+
<OTPInput
|
|
14
|
+
ref={ref}
|
|
15
|
+
containerClassName={cn(
|
|
16
|
+
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
|
17
|
+
containerClassName
|
|
18
|
+
)}
|
|
19
|
+
className={cn("disabled:cursor-not-allowed", className)}
|
|
20
|
+
{...props}
|
|
21
|
+
data-slot="input-otp"
|
|
22
|
+
/>
|
|
23
|
+
))
|
|
24
|
+
InputOTP.displayName = "InputOTP"
|
|
25
|
+
|
|
26
|
+
const InputOTPGroup = React.forwardRef<
|
|
27
|
+
React.ElementRef<"div">,
|
|
28
|
+
React.ComponentPropsWithoutRef<"div">
|
|
29
|
+
>(({ className, ...props }, ref) => (
|
|
30
|
+
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
|
31
|
+
))
|
|
32
|
+
InputOTPGroup.displayName = "InputOTPGroup"
|
|
33
|
+
|
|
34
|
+
const InputOTPSlot = React.forwardRef<
|
|
35
|
+
React.ElementRef<"div">,
|
|
36
|
+
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
|
37
|
+
>(({ index, className, ...props }, ref) => {
|
|
38
|
+
const inputOTPContext = React.useContext(OTPInputContext)
|
|
39
|
+
const slot = inputOTPContext?.slots[index]
|
|
40
|
+
const char = slot?.char
|
|
41
|
+
const hasFakeCaret = slot?.hasFakeCaret ?? false
|
|
42
|
+
const isActive = slot?.isActive ?? false
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cn(
|
|
48
|
+
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
|
49
|
+
isActive && "z-10 ring-1 ring-ring",
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
>
|
|
54
|
+
{char}
|
|
55
|
+
{hasFakeCaret ? (
|
|
56
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
57
|
+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
|
58
|
+
</div>
|
|
59
|
+
) : null}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
InputOTPSlot.displayName = "InputOTPSlot"
|
|
64
|
+
|
|
65
|
+
const InputOTPSeparator = React.forwardRef<
|
|
66
|
+
React.ElementRef<"div">,
|
|
67
|
+
React.ComponentPropsWithoutRef<"div">
|
|
68
|
+
>(({ ...props }, ref) => (
|
|
69
|
+
<div ref={ref} role="separator" {...props}>
|
|
70
|
+
<Minus className="h-4 w-4 text-muted-foreground" />
|
|
71
|
+
</div>
|
|
72
|
+
))
|
|
73
|
+
InputOTPSeparator.displayName = "InputOTPSeparator"
|
|
74
|
+
|
|
75
|
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|