@gunjo/ui 0.0.1-alpha.0 → 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.
Files changed (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +90 -0
  3. package/README.md +52 -91
  4. package/package.json +47 -6
  5. package/src/components/display/Accordion.tsx +185 -0
  6. package/src/components/display/AccordionGroup.tsx +155 -0
  7. package/src/components/display/ActionDataTable.tsx +413 -0
  8. package/src/components/display/ActivityTimelineCard.tsx +483 -0
  9. package/src/components/display/AnalyticsCard.tsx +167 -0
  10. package/src/components/display/AssetCard.tsx +242 -0
  11. package/src/components/display/AssetGrid.tsx +164 -0
  12. package/src/components/display/Avatar.tsx +127 -0
  13. package/src/components/display/AvatarGroup.tsx +131 -0
  14. package/src/components/{atoms → display}/Badge.tsx +3 -3
  15. package/src/components/display/BarChart.tsx +247 -0
  16. package/src/components/{molecules → display}/Card.tsx +1 -1
  17. package/src/components/display/Carousel.tsx +593 -0
  18. package/src/components/display/ChartLegend.tsx +124 -0
  19. package/src/components/display/ChatMessage.tsx +382 -0
  20. package/src/components/display/ChoroplethMap.tsx +613 -0
  21. package/src/components/display/Code.tsx +42 -0
  22. package/src/components/display/CodeBlock.tsx +338 -0
  23. package/src/components/display/ColorSwatch.tsx +71 -0
  24. package/src/components/display/ConcentricProgressCard.tsx +545 -0
  25. package/src/components/display/DataTable.tsx +522 -0
  26. package/src/components/display/DistributionBar.tsx +102 -0
  27. package/src/components/display/DocNote.tsx +36 -0
  28. package/src/components/display/DonutChart.tsx +257 -0
  29. package/src/components/display/EmptyState.tsx +44 -0
  30. package/src/components/display/FileTree.tsx +180 -0
  31. package/src/components/display/GaugeChart.tsx +219 -0
  32. package/src/components/display/HeatmapChart.tsx +266 -0
  33. package/src/components/display/Icon.tsx +66 -0
  34. package/src/components/display/ImagePreview.tsx +140 -0
  35. package/src/components/{atoms → display}/Img.tsx +46 -12
  36. package/src/components/display/LabeledDonutCard.tsx +475 -0
  37. package/src/components/display/LineChart.tsx +464 -0
  38. package/src/components/{molecules → display}/List.tsx +20 -13
  39. package/src/components/display/MarkdownRenderer.tsx +157 -0
  40. package/src/components/display/MetadataList.tsx +81 -0
  41. package/src/components/display/MiniDistributionBarCard.tsx +314 -0
  42. package/src/components/display/PieChart.tsx +234 -0
  43. package/src/components/display/QuadrantMatrix.tsx +330 -0
  44. package/src/components/display/RadarChart.tsx +335 -0
  45. package/src/components/display/RadialBarChart.tsx +264 -0
  46. package/src/components/display/RetentionCohortCard.tsx +350 -0
  47. package/src/components/display/RibbonChart.tsx +618 -0
  48. package/src/components/display/SearchableAccordion.tsx +270 -0
  49. package/src/components/display/SegmentTimelineCard.tsx +452 -0
  50. package/src/components/display/SegmentedGaugeCard.tsx +607 -0
  51. package/src/components/display/Spacer.tsx +51 -0
  52. package/src/components/display/SparklineChart.tsx +394 -0
  53. package/src/components/display/StackedBarChart.tsx +393 -0
  54. package/src/components/display/Statistic.tsx +70 -0
  55. package/src/components/{molecules → display}/Table.tsx +22 -7
  56. package/src/components/display/Tag.tsx +80 -0
  57. package/src/components/display/TagEditor.tsx +141 -0
  58. package/src/components/display/Timeline.tsx +121 -0
  59. package/src/components/{atoms → display}/ToolPill.tsx +42 -18
  60. package/src/components/display/TreeView.tsx +226 -0
  61. package/src/components/display/chart-tooltip.tsx +423 -0
  62. package/src/components/display/chart-utils.ts +71 -0
  63. package/src/components/display/circular-chart-utils.ts +147 -0
  64. package/src/components/display/generated/default-variant-keys.ts +90 -0
  65. package/src/components/display/generated/variant-keys.ts +169 -0
  66. package/src/components/{atoms → feedback}/Alert.tsx +12 -5
  67. package/src/components/feedback/Banner.tsx +90 -0
  68. package/src/components/{molecules → feedback}/NotificationCenter.tsx +64 -31
  69. package/src/components/feedback/ProgressWidget.tsx +44 -0
  70. package/src/components/{atoms → feedback}/Spinner.tsx +2 -2
  71. package/src/components/{molecules → feedback}/StatusBar.tsx +4 -4
  72. package/src/components/feedback/StatusScreen.tsx +148 -0
  73. package/src/components/{molecules → feedback}/Stepper.tsx +10 -5
  74. package/src/components/feedback/Toast.tsx +108 -0
  75. package/src/components/feedback/ToastProvider.tsx +78 -0
  76. package/src/components/feedback/generated/default-variant-keys.ts +16 -0
  77. package/src/components/feedback/generated/variant-keys.ts +21 -0
  78. package/src/components/generated/component-manifest.ts +1568 -454
  79. package/src/components/generated/component-style-hints.ts +1958 -718
  80. package/src/components/{atoms → inputs}/ButtonVariants.ts +13 -3
  81. package/src/components/inputs/Calendar.tsx +212 -0
  82. package/src/components/inputs/ChatComposer.tsx +75 -0
  83. package/src/components/inputs/ChatInput.tsx +528 -0
  84. package/src/components/{atoms → inputs}/Checkbox.tsx +2 -2
  85. package/src/components/inputs/Combobox.tsx +175 -0
  86. package/src/components/inputs/CopyButton.tsx +187 -0
  87. package/src/components/inputs/DatePicker.tsx +519 -0
  88. package/src/components/inputs/DateRangePicker.tsx +878 -0
  89. package/src/components/inputs/EditableField.tsx +182 -0
  90. package/src/components/{organisms → inputs}/FileUploader.tsx +24 -9
  91. package/src/components/inputs/FilterButton.tsx +163 -0
  92. package/src/components/{molecules → inputs}/Form.tsx +20 -3
  93. package/src/components/{atoms → inputs}/Input.tsx +2 -0
  94. package/src/components/inputs/InputOTP.tsx +75 -0
  95. package/src/components/inputs/Mention.tsx +279 -0
  96. package/src/components/inputs/NumberInput.tsx +109 -0
  97. package/src/components/inputs/PasswordGroup.tsx +138 -0
  98. package/src/components/inputs/PasswordInput.tsx +74 -0
  99. package/src/components/inputs/PasswordRequirementList.tsx +96 -0
  100. package/src/components/inputs/PasswordStrengthMeter.tsx +93 -0
  101. package/src/components/inputs/PhoneInput.tsx +99 -0
  102. package/src/components/inputs/PostalCodeInput.tsx +98 -0
  103. package/src/components/inputs/RangeSlider.tsx +129 -0
  104. package/src/components/inputs/SearchInput.tsx +76 -0
  105. package/src/components/inputs/Select.tsx +39 -0
  106. package/src/components/{atoms → inputs}/Slider.tsx +18 -5
  107. package/src/components/{molecules → inputs}/SortButton.tsx +5 -2
  108. package/src/components/{atoms → inputs}/Switch.tsx +15 -4
  109. package/src/components/inputs/TagInput.tsx +114 -0
  110. package/src/components/{atoms → inputs}/Textarea.tsx +1 -0
  111. package/src/components/inputs/TimePicker.tsx +150 -0
  112. package/src/components/inputs/Toggle.tsx +48 -0
  113. package/src/components/{atoms → inputs}/ToggleGroup.tsx +2 -2
  114. package/src/components/inputs/TooltipButton.tsx +148 -0
  115. package/src/components/inputs/VoiceInputButton.tsx +317 -0
  116. package/src/components/inputs/calendar-holidays.ts +56 -0
  117. package/src/components/inputs/generated/default-variant-keys.ts +32 -0
  118. package/src/components/{atoms → inputs}/generated/variant-keys.ts +19 -27
  119. package/src/components/layout/AspectRatio.tsx +12 -0
  120. package/src/components/layout/AssetInspectorPanel.tsx +416 -0
  121. package/src/components/layout/Cluster.tsx +56 -0
  122. package/src/components/layout/CollapsiblePanelToggle.tsx +94 -0
  123. package/src/components/layout/Container.tsx +43 -0
  124. package/src/components/layout/DeviceFrame.tsx +227 -0
  125. package/src/components/layout/Grid.tsx +65 -0
  126. package/src/components/layout/HStack.tsx +73 -0
  127. package/src/components/{organisms → layout}/InspectorPanel.tsx +6 -5
  128. package/src/components/layout/MarqueeFrame.tsx +158 -0
  129. package/src/components/layout/Resizable.tsx +94 -0
  130. package/src/components/layout/ScrollArea.tsx +71 -0
  131. package/src/components/{organisms → layout}/SpatialCanvas.tsx +12 -7
  132. package/src/components/layout/VStack.tsx +69 -0
  133. package/src/components/layout/generated/default-variant-keys.ts +16 -0
  134. package/src/components/layout/generated/variant-keys.ts +21 -0
  135. package/src/components/{molecules → navigation}/Breadcrumb.tsx +5 -4
  136. package/src/components/navigation/Command.tsx +266 -0
  137. package/src/components/navigation/CommandPalette.tsx +83 -0
  138. package/src/components/navigation/DocumentPager.tsx +171 -0
  139. package/src/components/navigation/Footer.tsx +88 -0
  140. package/src/components/navigation/Header.tsx +80 -0
  141. package/src/components/{molecules → navigation}/Menubar.tsx +45 -12
  142. package/src/components/navigation/NavigationMenu.tsx +128 -0
  143. package/src/components/navigation/PageAside.tsx +84 -0
  144. package/src/components/{molecules → navigation}/Pagination.tsx +60 -7
  145. package/src/components/{organisms → navigation}/RightRail.tsx +1 -1
  146. package/src/components/navigation/Sidebar.tsx +223 -0
  147. package/src/components/navigation/SidebarItem.tsx +160 -0
  148. package/src/components/{molecules → navigation}/Tabs.tsx +2 -2
  149. package/src/components/navigation/TextLink.tsx +71 -0
  150. package/src/components/navigation/generated/default-variant-keys.ts +12 -0
  151. package/src/components/navigation/generated/variant-keys.ts +13 -0
  152. package/src/components/overlay/AIChatInput.tsx +5 -0
  153. package/src/components/overlay/AIChatMessage.tsx +6 -0
  154. package/src/components/overlay/AlertDialog.tsx +145 -0
  155. package/src/components/overlay/ChatPanel.tsx +180 -0
  156. package/src/components/{molecules → overlay}/ContextMenu.tsx +65 -29
  157. package/src/components/{molecules → overlay}/Dialog.tsx +21 -13
  158. package/src/components/overlay/Drawer.tsx +131 -0
  159. package/src/components/{molecules → overlay}/DropdownMenu.tsx +52 -17
  160. package/src/components/overlay/FloatingPanel.tsx +90 -0
  161. package/src/components/overlay/HoverCard.tsx +36 -0
  162. package/src/components/overlay/MediaLightbox.tsx +403 -0
  163. package/src/components/overlay/MediaPickerDialog.tsx +198 -0
  164. package/src/components/overlay/Modal.tsx +103 -0
  165. package/src/components/overlay/OnboardingFlow.tsx +172 -0
  166. package/src/components/overlay/Popover.tsx +36 -0
  167. package/src/components/overlay/ShareModal.tsx +324 -0
  168. package/src/components/{molecules → overlay}/Sheet.tsx +76 -19
  169. package/src/components/overlay/Tooltip.tsx +130 -0
  170. package/src/components/overlay/generated/default-variant-keys.ts +14 -0
  171. package/src/components/overlay/generated/variant-keys.ts +17 -0
  172. package/src/components/patterns/BlogTemplate.tsx +46 -0
  173. package/src/components/{templates → patterns}/DashboardTemplate.tsx +2 -2
  174. package/src/components/patterns/DocsTemplate.tsx +41 -0
  175. package/src/components/{templates → patterns}/MediaLibraryTemplate.tsx +1 -1
  176. package/src/components/patterns/OnboardingTemplate.tsx +32 -0
  177. package/src/components/patterns/PricingTemplate.tsx +106 -0
  178. package/src/globals.css +173 -22
  179. package/src/index.ts +177 -76
  180. package/tailwind-theme-extend.cjs +48 -3
  181. package/design/atoms-metadata.json +0 -82
  182. package/design/molecules-metadata.json +0 -130
  183. package/design/organisms-metadata.json +0 -38
  184. package/design/templates-metadata.json +0 -38
  185. package/src/components/atoms/Avatar.tsx +0 -57
  186. package/src/components/atoms/Select.tsx +0 -28
  187. package/src/components/atoms/generated/default-variant-keys.ts +0 -36
  188. package/src/components/molecules/AIChatInput.tsx +0 -140
  189. package/src/components/molecules/AIChatMessage.tsx +0 -109
  190. package/src/components/molecules/Accordion.tsx +0 -99
  191. package/src/components/molecules/Calendar.tsx +0 -60
  192. package/src/components/molecules/Carousel.tsx +0 -261
  193. package/src/components/molecules/Command.tsx +0 -152
  194. package/src/components/molecules/FilterButton.tsx +0 -133
  195. package/src/components/molecules/HoverCard.tsx +0 -29
  196. package/src/components/molecules/Modal.tsx +0 -66
  197. package/src/components/molecules/Popover.tsx +0 -31
  198. package/src/components/molecules/ProgressWidget.tsx +0 -40
  199. package/src/components/molecules/Resizable.tsx +0 -47
  200. package/src/components/molecules/ScrollArea.tsx +0 -48
  201. package/src/components/molecules/SidebarItem.tsx +0 -134
  202. package/src/components/molecules/Toast.tsx +0 -57
  203. package/src/components/molecules/Tooltip.tsx +0 -30
  204. package/src/components/molecules/generated/default-variant-keys.ts +0 -22
  205. package/src/components/molecules/generated/variant-keys.ts +0 -33
  206. package/src/components/organisms/CommandPalette.tsx +0 -58
  207. package/src/components/organisms/FloatingPanel.tsx +0 -46
  208. package/src/components/organisms/ShareModal.tsx +0 -182
  209. package/src/components/organisms/ToastProvider.tsx +0 -49
  210. /package/src/components/{atoms → display}/Kbd.tsx +0 -0
  211. /package/src/components/{atoms → display}/Separator.tsx +0 -0
  212. /package/src/components/{atoms → display}/Skeleton.tsx +0 -0
  213. /package/src/components/{atoms → feedback}/Progress.tsx +0 -0
  214. /package/src/components/{atoms → inputs}/Button.tsx +0 -0
  215. /package/src/components/{atoms → inputs}/Label.tsx +0 -0
  216. /package/src/components/{atoms → inputs}/RadioGroup.tsx +0 -0
  217. /package/src/components/{organisms → navigation}/AppRail.tsx +0 -0
  218. /package/src/components/{templates → patterns}/AuthTemplate.tsx +0 -0
  219. /package/src/components/{templates → patterns}/BannalyzeTemplate.tsx +0 -0
  220. /package/src/components/{templates → patterns}/ChatTemplate.tsx +0 -0
  221. /package/src/components/{templates → patterns}/EditorTemplate.tsx +0 -0
  222. /package/src/components/{templates → patterns}/KanbanTemplate.tsx +0 -0
  223. /package/src/components/{templates → patterns}/LandingTemplate.tsx +0 -0
  224. /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 "lucide-react"
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 "../atoms/Button"
7
- import { Progress } from "../atoms/Progress"
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 className={cn("grid gap-4 w-[640px] w-full", className)} ref={ref} {...props}>
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/5" : "border-muted-foreground/25 hover:bg-muted/50",
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> or drag and drop
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
- Max size {Math.round(maxSize / 1024 / 1024)}MB
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 }