@modern-admin/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/dist/components/accordion.d.ts +7 -0
  2. package/dist/components/accordion.d.ts.map +1 -0
  3. package/dist/components/accordion.jsx +19 -0
  4. package/dist/components/accordion.jsx.map +1 -0
  5. package/dist/components/alert-dialog.d.ts +22 -0
  6. package/dist/components/alert-dialog.d.ts.map +1 -0
  7. package/dist/components/alert-dialog.jsx +27 -0
  8. package/dist/components/alert-dialog.jsx.map +1 -0
  9. package/dist/components/audit-timeline.d.ts +24 -0
  10. package/dist/components/audit-timeline.d.ts.map +1 -0
  11. package/dist/components/audit-timeline.jsx +60 -0
  12. package/dist/components/audit-timeline.jsx.map +1 -0
  13. package/dist/components/avatar.d.ts +6 -0
  14. package/dist/components/avatar.d.ts.map +1 -0
  15. package/dist/components/avatar.jsx +10 -0
  16. package/dist/components/avatar.jsx.map +1 -0
  17. package/dist/components/badge.d.ts +10 -0
  18. package/dist/components/badge.d.ts.map +1 -0
  19. package/dist/components/badge.jsx +19 -0
  20. package/dist/components/badge.jsx.map +1 -0
  21. package/dist/components/breadcrumb.d.ts +17 -0
  22. package/dist/components/breadcrumb.d.ts.map +1 -0
  23. package/dist/components/breadcrumb.jsx +27 -0
  24. package/dist/components/breadcrumb.jsx.map +1 -0
  25. package/dist/components/button.d.ts +12 -0
  26. package/dist/components/button.d.ts.map +1 -0
  27. package/dist/components/button.jsx +37 -0
  28. package/dist/components/button.jsx.map +1 -0
  29. package/dist/components/calendar.d.ts +9 -0
  30. package/dist/components/calendar.d.ts.map +1 -0
  31. package/dist/components/calendar.jsx +102 -0
  32. package/dist/components/calendar.jsx.map +1 -0
  33. package/dist/components/card.d.ts +8 -0
  34. package/dist/components/card.d.ts.map +1 -0
  35. package/dist/components/card.jsx +18 -0
  36. package/dist/components/card.jsx.map +1 -0
  37. package/dist/components/chart.d.ts +97 -0
  38. package/dist/components/chart.d.ts.map +1 -0
  39. package/dist/components/chart.jsx +233 -0
  40. package/dist/components/chart.jsx.map +1 -0
  41. package/dist/components/checkbox.d.ts +4 -0
  42. package/dist/components/checkbox.d.ts.map +1 -0
  43. package/dist/components/checkbox.jsx +11 -0
  44. package/dist/components/checkbox.jsx.map +1 -0
  45. package/dist/components/combobox.d.ts +46 -0
  46. package/dist/components/combobox.d.ts.map +1 -0
  47. package/dist/components/combobox.jsx +145 -0
  48. package/dist/components/combobox.jsx.map +1 -0
  49. package/dist/components/command.d.ts +80 -0
  50. package/dist/components/command.d.ts.map +1 -0
  51. package/dist/components/command.jsx +32 -0
  52. package/dist/components/command.jsx.map +1 -0
  53. package/dist/components/date-picker.d.ts +24 -0
  54. package/dist/components/date-picker.d.ts.map +1 -0
  55. package/dist/components/date-picker.jsx +149 -0
  56. package/dist/components/date-picker.jsx.map +1 -0
  57. package/dist/components/date-range-input.d.ts +22 -0
  58. package/dist/components/date-range-input.d.ts.map +1 -0
  59. package/dist/components/date-range-input.jsx +202 -0
  60. package/dist/components/date-range-input.jsx.map +1 -0
  61. package/dist/components/dialog.d.ts +19 -0
  62. package/dist/components/dialog.d.ts.map +1 -0
  63. package/dist/components/dialog.jsx +30 -0
  64. package/dist/components/dialog.jsx.map +1 -0
  65. package/dist/components/diff-view.d.ts +24 -0
  66. package/dist/components/diff-view.d.ts.map +1 -0
  67. package/dist/components/diff-view.jsx +69 -0
  68. package/dist/components/diff-view.jsx.map +1 -0
  69. package/dist/components/dropdown-menu.d.ts +27 -0
  70. package/dist/components/dropdown-menu.d.ts.map +1 -0
  71. package/dist/components/dropdown-menu.jsx +48 -0
  72. package/dist/components/dropdown-menu.jsx.map +1 -0
  73. package/dist/components/empty.d.ts +15 -0
  74. package/dist/components/empty.d.ts.map +1 -0
  75. package/dist/components/empty.jsx +27 -0
  76. package/dist/components/empty.jsx.map +1 -0
  77. package/dist/components/field.d.ts +23 -0
  78. package/dist/components/field.d.ts.map +1 -0
  79. package/dist/components/field.jsx +60 -0
  80. package/dist/components/field.jsx.map +1 -0
  81. package/dist/components/file-input.d.ts +50 -0
  82. package/dist/components/file-input.d.ts.map +1 -0
  83. package/dist/components/file-input.jsx +104 -0
  84. package/dist/components/file-input.jsx.map +1 -0
  85. package/dist/components/form.d.ts +20 -0
  86. package/dist/components/form.d.ts.map +1 -0
  87. package/dist/components/form.jsx +66 -0
  88. package/dist/components/form.jsx.map +1 -0
  89. package/dist/components/info-tooltip.d.ts +11 -0
  90. package/dist/components/info-tooltip.d.ts.map +1 -0
  91. package/dist/components/info-tooltip.jsx +17 -0
  92. package/dist/components/info-tooltip.jsx.map +1 -0
  93. package/dist/components/input.d.ts +13 -0
  94. package/dist/components/input.d.ts.map +1 -0
  95. package/dist/components/input.jsx +19 -0
  96. package/dist/components/input.jsx.map +1 -0
  97. package/dist/components/json-editor.d.ts +23 -0
  98. package/dist/components/json-editor.d.ts.map +1 -0
  99. package/dist/components/json-editor.jsx +143 -0
  100. package/dist/components/json-editor.jsx.map +1 -0
  101. package/dist/components/kbd.d.ts +15 -0
  102. package/dist/components/kbd.d.ts.map +1 -0
  103. package/dist/components/kbd.jsx +23 -0
  104. package/dist/components/kbd.jsx.map +1 -0
  105. package/dist/components/key-value-editor.d.ts +92 -0
  106. package/dist/components/key-value-editor.d.ts.map +1 -0
  107. package/dist/components/key-value-editor.jsx +187 -0
  108. package/dist/components/key-value-editor.jsx.map +1 -0
  109. package/dist/components/keyboard-shortcuts-help.d.ts +17 -0
  110. package/dist/components/keyboard-shortcuts-help.d.ts.map +1 -0
  111. package/dist/components/keyboard-shortcuts-help.jsx +97 -0
  112. package/dist/components/keyboard-shortcuts-help.jsx.map +1 -0
  113. package/dist/components/label.d.ts +5 -0
  114. package/dist/components/label.d.ts.map +1 -0
  115. package/dist/components/label.jsx +8 -0
  116. package/dist/components/label.jsx.map +1 -0
  117. package/dist/components/media-preview.d.ts +30 -0
  118. package/dist/components/media-preview.d.ts.map +1 -0
  119. package/dist/components/media-preview.jsx +189 -0
  120. package/dist/components/media-preview.jsx.map +1 -0
  121. package/dist/components/multi-file-input.d.ts +76 -0
  122. package/dist/components/multi-file-input.d.ts.map +1 -0
  123. package/dist/components/multi-file-input.jsx +131 -0
  124. package/dist/components/multi-file-input.jsx.map +1 -0
  125. package/dist/components/password-input.d.ts +10 -0
  126. package/dist/components/password-input.d.ts.map +1 -0
  127. package/dist/components/password-input.jsx +18 -0
  128. package/dist/components/password-input.jsx.map +1 -0
  129. package/dist/components/popover.d.ts +7 -0
  130. package/dist/components/popover.d.ts.map +1 -0
  131. package/dist/components/popover.jsx +11 -0
  132. package/dist/components/popover.jsx.map +1 -0
  133. package/dist/components/revision-timeline.d.ts +30 -0
  134. package/dist/components/revision-timeline.d.ts.map +1 -0
  135. package/dist/components/revision-timeline.jsx +42 -0
  136. package/dist/components/revision-timeline.jsx.map +1 -0
  137. package/dist/components/richtext-editor.d.ts +43 -0
  138. package/dist/components/richtext-editor.d.ts.map +1 -0
  139. package/dist/components/richtext-editor.jsx +319 -0
  140. package/dist/components/richtext-editor.jsx.map +1 -0
  141. package/dist/components/richtext-mode.d.ts +23 -0
  142. package/dist/components/richtext-mode.d.ts.map +1 -0
  143. package/dist/components/richtext-mode.js +36 -0
  144. package/dist/components/richtext-mode.js.map +1 -0
  145. package/dist/components/richtext-render.d.ts +8 -0
  146. package/dist/components/richtext-render.d.ts.map +1 -0
  147. package/dist/components/richtext-render.jsx +33 -0
  148. package/dist/components/richtext-render.jsx.map +1 -0
  149. package/dist/components/richtext-sync.d.ts +37 -0
  150. package/dist/components/richtext-sync.d.ts.map +1 -0
  151. package/dist/components/richtext-sync.js +46 -0
  152. package/dist/components/richtext-sync.js.map +1 -0
  153. package/dist/components/scroll-area.d.ts +5 -0
  154. package/dist/components/scroll-area.d.ts.map +1 -0
  155. package/dist/components/scroll-area.jsx +16 -0
  156. package/dist/components/scroll-area.jsx.map +1 -0
  157. package/dist/components/select.d.ts +36 -0
  158. package/dist/components/select.d.ts.map +1 -0
  159. package/dist/components/select.jsx +87 -0
  160. package/dist/components/select.jsx.map +1 -0
  161. package/dist/components/separator.d.ts +4 -0
  162. package/dist/components/separator.d.ts.map +1 -0
  163. package/dist/components/separator.jsx +6 -0
  164. package/dist/components/separator.jsx.map +1 -0
  165. package/dist/components/sheet.d.ts +29 -0
  166. package/dist/components/sheet.d.ts.map +1 -0
  167. package/dist/components/sheet.jsx +44 -0
  168. package/dist/components/sheet.jsx.map +1 -0
  169. package/dist/components/sidebar.d.ts +70 -0
  170. package/dist/components/sidebar.d.ts.map +1 -0
  171. package/dist/components/sidebar.jsx +245 -0
  172. package/dist/components/sidebar.jsx.map +1 -0
  173. package/dist/components/skeleton.d.ts +3 -0
  174. package/dist/components/skeleton.d.ts.map +1 -0
  175. package/dist/components/skeleton.jsx +6 -0
  176. package/dist/components/skeleton.jsx.map +1 -0
  177. package/dist/components/sonner.d.ts +6 -0
  178. package/dist/components/sonner.d.ts.map +1 -0
  179. package/dist/components/sonner.jsx +29 -0
  180. package/dist/components/sonner.jsx.map +1 -0
  181. package/dist/components/switch.d.ts +4 -0
  182. package/dist/components/switch.d.ts.map +1 -0
  183. package/dist/components/switch.jsx +8 -0
  184. package/dist/components/switch.jsx.map +1 -0
  185. package/dist/components/table.d.ts +10 -0
  186. package/dist/components/table.d.ts.map +1 -0
  187. package/dist/components/table.jsx +21 -0
  188. package/dist/components/table.jsx.map +1 -0
  189. package/dist/components/tabs.d.ts +7 -0
  190. package/dist/components/tabs.d.ts.map +1 -0
  191. package/dist/components/tabs.jsx +14 -0
  192. package/dist/components/tabs.jsx.map +1 -0
  193. package/dist/components/textarea.d.ts +4 -0
  194. package/dist/components/textarea.d.ts.map +1 -0
  195. package/dist/components/textarea.jsx +5 -0
  196. package/dist/components/textarea.jsx.map +1 -0
  197. package/dist/components/tooltip.d.ts +7 -0
  198. package/dist/components/tooltip.d.ts.map +1 -0
  199. package/dist/components/tooltip.jsx +11 -0
  200. package/dist/components/tooltip.jsx.map +1 -0
  201. package/dist/index.d.ts +52 -0
  202. package/dist/index.d.ts.map +1 -0
  203. package/dist/index.js +72 -0
  204. package/dist/index.js.map +1 -0
  205. package/dist/lib/theme.d.ts +11 -0
  206. package/dist/lib/theme.d.ts.map +1 -0
  207. package/dist/lib/theme.js +44 -0
  208. package/dist/lib/theme.js.map +1 -0
  209. package/dist/lib/utils.d.ts +3 -0
  210. package/dist/lib/utils.d.ts.map +1 -0
  211. package/dist/lib/utils.js +6 -0
  212. package/dist/lib/utils.js.map +1 -0
  213. package/dist/styles.css +242 -0
  214. package/package.json +85 -0
  215. package/src/components/accordion.tsx +48 -0
  216. package/src/components/alert-dialog.tsx +113 -0
  217. package/src/components/audit-timeline.tsx +102 -0
  218. package/src/components/avatar.tsx +42 -0
  219. package/src/components/badge.tsx +34 -0
  220. package/src/components/breadcrumb.tsx +99 -0
  221. package/src/components/button.tsx +58 -0
  222. package/src/components/calendar.tsx +176 -0
  223. package/src/components/card.tsx +60 -0
  224. package/src/components/chart.tsx +558 -0
  225. package/src/components/checkbox.tsx +23 -0
  226. package/src/components/combobox.tsx +264 -0
  227. package/src/components/command.tsx +120 -0
  228. package/src/components/date-picker.tsx +221 -0
  229. package/src/components/date-range-input.tsx +295 -0
  230. package/src/components/dialog.tsx +94 -0
  231. package/src/components/diff-view.tsx +182 -0
  232. package/src/components/dropdown-menu.tsx +165 -0
  233. package/src/components/empty.tsx +100 -0
  234. package/src/components/field.tsx +168 -0
  235. package/src/components/file-input.tsx +233 -0
  236. package/src/components/form.tsx +152 -0
  237. package/src/components/info-tooltip.tsx +40 -0
  238. package/src/components/input.tsx +55 -0
  239. package/src/components/json-editor.tsx +210 -0
  240. package/src/components/kbd.tsx +35 -0
  241. package/src/components/key-value-editor.tsx +423 -0
  242. package/src/components/keyboard-shortcuts-help.tsx +136 -0
  243. package/src/components/label.tsx +16 -0
  244. package/src/components/media-preview.tsx +278 -0
  245. package/src/components/multi-file-input.tsx +315 -0
  246. package/src/components/password-input.tsx +50 -0
  247. package/src/components/popover.tsx +26 -0
  248. package/src/components/revision-timeline.tsx +93 -0
  249. package/src/components/richtext-editor.tsx +624 -0
  250. package/src/components/richtext-mode.ts +39 -0
  251. package/src/components/richtext-render.tsx +51 -0
  252. package/src/components/richtext-sync.ts +57 -0
  253. package/src/components/scroll-area.tsx +41 -0
  254. package/src/components/select.tsx +200 -0
  255. package/src/components/separator.tsx +21 -0
  256. package/src/components/sheet.tsx +109 -0
  257. package/src/components/sidebar.tsx +660 -0
  258. package/src/components/skeleton.tsx +9 -0
  259. package/src/components/sonner.tsx +45 -0
  260. package/src/components/switch.tsx +24 -0
  261. package/src/components/table.tsx +93 -0
  262. package/src/components/tabs.tsx +57 -0
  263. package/src/components/textarea.tsx +18 -0
  264. package/src/components/tooltip.tsx +25 -0
  265. package/src/index.ts +342 -0
  266. package/src/lib/theme.ts +45 -0
  267. package/src/lib/utils.ts +6 -0
  268. package/src/styles.css +242 -0
@@ -0,0 +1,152 @@
1
+ // shadcn-style Form: thin wrappers around react-hook-form's `Controller` that
2
+ // pipe field state into accessible label/control/message slots. Apps render
3
+ // `<Form>` (== FormProvider) at the top, then use `<FormField>` per input.
4
+
5
+ import * as React from 'react'
6
+ import { Slot } from '@radix-ui/react-slot'
7
+ import {
8
+ Controller,
9
+ FormProvider,
10
+ useFormContext,
11
+ type ControllerProps,
12
+ type FieldPath,
13
+ type FieldValues,
14
+ } from 'react-hook-form'
15
+ import { cn } from '../lib/utils.js'
16
+ import { Label } from './label.js'
17
+
18
+ export const Form = FormProvider
19
+
20
+ interface FormFieldContextValue<
21
+ TFieldValues extends FieldValues = FieldValues,
22
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
23
+ > {
24
+ name: TName
25
+ }
26
+
27
+ const FormFieldContext = React.createContext<FormFieldContextValue | null>(null)
28
+
29
+ export const FormField = <
30
+ TFieldValues extends FieldValues = FieldValues,
31
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
32
+ >({
33
+ ...props
34
+ }: ControllerProps<TFieldValues, TName>): React.ReactElement => (
35
+ <FormFieldContext.Provider value={{ name: props.name } as FormFieldContextValue}>
36
+ <Controller {...props} />
37
+ </FormFieldContext.Provider>
38
+ )
39
+
40
+ interface FormItemContextValue {
41
+ id: string
42
+ }
43
+
44
+ const FormItemContext = React.createContext<FormItemContextValue | null>(null)
45
+
46
+ export const useFormField = (): {
47
+ id: string
48
+ name: string
49
+ formItemId: string
50
+ formDescriptionId: string
51
+ formMessageId: string
52
+ error?: { message?: string }
53
+ } => {
54
+ const fieldContext = React.useContext(FormFieldContext)
55
+ const itemContext = React.useContext(FormItemContext)
56
+ const { getFieldState, formState } = useFormContext()
57
+ if (!fieldContext) throw new Error('useFormField must be used within <FormField>')
58
+ if (!itemContext) throw new Error('useFormField must be used within <FormItem>')
59
+ const fieldState = getFieldState(fieldContext.name, formState)
60
+ const { id } = itemContext
61
+ return {
62
+ id,
63
+ name: fieldContext.name,
64
+ formItemId: `${id}-form-item`,
65
+ formDescriptionId: `${id}-form-item-description`,
66
+ formMessageId: `${id}-form-item-message`,
67
+ ...fieldState,
68
+ }
69
+ }
70
+
71
+ export const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
72
+ ({ className, ...props }, ref) => {
73
+ const id = React.useId()
74
+ return (
75
+ <FormItemContext.Provider value={{ id }}>
76
+ <div ref={ref} className={cn('space-y-2', className)} {...props} />
77
+ </FormItemContext.Provider>
78
+ )
79
+ },
80
+ )
81
+ FormItem.displayName = 'FormItem'
82
+
83
+ export const FormLabel = React.forwardRef<
84
+ React.ElementRef<typeof Label>,
85
+ React.ComponentPropsWithoutRef<typeof Label>
86
+ >(({ className, ...props }, ref) => {
87
+ const { error, formItemId } = useFormField()
88
+ return (
89
+ <Label
90
+ ref={ref}
91
+ className={cn(error && 'text-destructive', className)}
92
+ htmlFor={formItemId}
93
+ {...props}
94
+ />
95
+ )
96
+ })
97
+ FormLabel.displayName = 'FormLabel'
98
+
99
+ export const FormControl = React.forwardRef<
100
+ React.ElementRef<typeof Slot>,
101
+ React.ComponentPropsWithoutRef<typeof Slot>
102
+ >(({ ...props }, ref) => {
103
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
104
+ return (
105
+ <Slot
106
+ ref={ref}
107
+ id={formItemId}
108
+ aria-describedby={
109
+ !error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`
110
+ }
111
+ aria-invalid={!!error}
112
+ {...props}
113
+ />
114
+ )
115
+ })
116
+ FormControl.displayName = 'FormControl'
117
+
118
+ export const FormDescription = React.forwardRef<
119
+ HTMLParagraphElement,
120
+ React.HTMLAttributes<HTMLParagraphElement>
121
+ >(({ className, ...props }, ref) => {
122
+ const { formDescriptionId } = useFormField()
123
+ return (
124
+ <p
125
+ ref={ref}
126
+ id={formDescriptionId}
127
+ className={cn('text-sm text-muted-foreground', className)}
128
+ {...props}
129
+ />
130
+ )
131
+ })
132
+ FormDescription.displayName = 'FormDescription'
133
+
134
+ export const FormMessage = React.forwardRef<
135
+ HTMLParagraphElement,
136
+ React.HTMLAttributes<HTMLParagraphElement>
137
+ >(({ className, children, ...props }, ref) => {
138
+ const { error, formMessageId } = useFormField()
139
+ const body = error ? String(error.message ?? '') : children
140
+ if (!body) return null
141
+ return (
142
+ <p
143
+ ref={ref}
144
+ id={formMessageId}
145
+ className={cn('text-sm font-medium text-destructive', className)}
146
+ {...props}
147
+ >
148
+ {body}
149
+ </p>
150
+ )
151
+ })
152
+ FormMessage.displayName = 'FormMessage'
@@ -0,0 +1,40 @@
1
+ import * as React from 'react'
2
+ import { Info } from 'lucide-react'
3
+ import { cn } from '../lib/utils.js'
4
+ import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip.js'
5
+
6
+ export interface InfoTooltipProps {
7
+ content: React.ReactNode
8
+ ariaLabel?: string
9
+ className?: string
10
+ iconClassName?: string
11
+ side?: React.ComponentProps<typeof TooltipContent>['side']
12
+ }
13
+
14
+ export function InfoTooltip({
15
+ content,
16
+ ariaLabel,
17
+ className,
18
+ iconClassName,
19
+ side = 'top',
20
+ }: InfoTooltipProps): React.ReactElement {
21
+ return (
22
+ <Tooltip>
23
+ <TooltipTrigger asChild>
24
+ <button
25
+ type="button"
26
+ aria-label={ariaLabel}
27
+ className={cn(
28
+ 'inline-flex size-4 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
29
+ className,
30
+ )}
31
+ >
32
+ <Info className={cn('size-3.5', iconClassName)} />
33
+ </button>
34
+ </TooltipTrigger>
35
+ <TooltipContent side={side} className="max-w-80 whitespace-pre-wrap text-left leading-relaxed">
36
+ {content}
37
+ </TooltipContent>
38
+ </Tooltip>
39
+ )
40
+ }
@@ -0,0 +1,55 @@
1
+ import * as React from 'react'
2
+ import { X } from 'lucide-react'
3
+ import { cn } from '../lib/utils.js'
4
+
5
+ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
6
+ /**
7
+ * When provided, a clear button (×) is rendered on the right side of the
8
+ * input whenever it has a non-empty value. The button is hidden when the
9
+ * input is disabled or the value is empty.
10
+ */
11
+ onClear?: () => void
12
+ /** aria-label for the clear button. Defaults to "Clear". */
13
+ clearLabel?: string
14
+ }
15
+
16
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
17
+ ({ className, type, onClear, clearLabel, ...props }, ref) => {
18
+ const showClear = !!onClear && !!props.value && !props.disabled
19
+
20
+ const input = (
21
+ <input
22
+ type={type ?? 'text'}
23
+ ref={ref}
24
+ className={cn(
25
+ 'flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
26
+ // Hide native number-input spinner buttons (Firefox + WebKit/Blink).
27
+ '[appearance:textfield] [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none',
28
+ showClear && 'pr-8',
29
+ className,
30
+ )}
31
+ {...props}
32
+ />
33
+ )
34
+
35
+ if (!onClear) return input
36
+
37
+ return (
38
+ <div className="relative">
39
+ {input}
40
+ {showClear && (
41
+ <button
42
+ type="button"
43
+ tabIndex={-1}
44
+ aria-label={clearLabel ?? 'Clear'}
45
+ onClick={onClear}
46
+ className="absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground opacity-50 hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
47
+ >
48
+ <X className="size-3.5" />
49
+ </button>
50
+ )}
51
+ </div>
52
+ )
53
+ },
54
+ )
55
+ Input.displayName = 'Input'
@@ -0,0 +1,210 @@
1
+ // JsonEditor + JsonView — editing/displaying JSON-typed property values.
2
+ //
3
+ // Editor: a monospace Textarea with live parse, an inline "Format" button
4
+ // and an error band for parse failures. Calls onChange with the parsed
5
+ // value (object/array/primitive) — never with the raw string — so the form
6
+ // layer stores structured data.
7
+ //
8
+ // View: pretty-printed <pre> for show, single-line collapsed code for list.
9
+
10
+ import * as React from 'react'
11
+ import { AlertCircle, Wand2 } from 'lucide-react'
12
+ import { cn } from '../lib/utils.js'
13
+ import { Button } from './button.js'
14
+ import { Textarea } from './textarea.js'
15
+
16
+ const stringify = (value: unknown): string => {
17
+ if (value == null) return ''
18
+ if (typeof value === 'string') {
19
+ // Strings that are themselves JSON come back from some adapters.
20
+ // Normalize to pretty form; otherwise leave the raw string alone.
21
+ try {
22
+ return JSON.stringify(JSON.parse(value), null, 2)
23
+ } catch {
24
+ return value
25
+ }
26
+ }
27
+ try {
28
+ return JSON.stringify(value, null, 2)
29
+ } catch {
30
+ return String(value)
31
+ }
32
+ }
33
+
34
+ export interface JsonEditorProps {
35
+ value: unknown
36
+ onChange(next: unknown): void
37
+ onBlur?(): void
38
+ disabled?: boolean
39
+ placeholder?: string
40
+ rows?: number
41
+ className?: string
42
+ /** Translated label for the inline "Format" button. */
43
+ formatLabel?: string
44
+ /** Translated prefix for parse-error messages. */
45
+ invalidLabel?: string
46
+ }
47
+
48
+ // Canonical (key-stable, no whitespace) JSON serialization used to decide
49
+ // whether an externally arriving `value` is structurally identical to what
50
+ // the user is currently typing. Reference comparison would always fail
51
+ // because the parent typically returns a fresh object on every re-render.
52
+ const canonical = (value: unknown): string => {
53
+ if (value == null) return ''
54
+ try {
55
+ return JSON.stringify(value)
56
+ } catch {
57
+ return ''
58
+ }
59
+ }
60
+
61
+ const tryParse = (text: string): { ok: true; value: unknown } | { ok: false } => {
62
+ if (text.trim() === '') return { ok: true, value: null }
63
+ try {
64
+ return { ok: true, value: JSON.parse(text) as unknown }
65
+ } catch {
66
+ return { ok: false }
67
+ }
68
+ }
69
+
70
+ export function JsonEditor({
71
+ value,
72
+ onChange,
73
+ onBlur,
74
+ disabled,
75
+ placeholder = '{}',
76
+ rows = 8,
77
+ className,
78
+ formatLabel = 'Format',
79
+ invalidLabel = 'Invalid JSON',
80
+ }: JsonEditorProps): React.ReactElement {
81
+ const [draft, setDraft] = React.useState<string>(() => stringify(value))
82
+ const [error, setError] = React.useState<string | null>(null)
83
+
84
+ // Resync the textarea only when the *external* value differs structurally
85
+ // from what the user is currently typing. Without the canonical-form check,
86
+ // every keystroke that produces valid JSON would trigger
87
+ // onChange(parsed) → parent re-render → useEffect → setDraft(stringify(value))
88
+ // and the user's in-progress text would get auto-pretty-printed on every
89
+ // keystroke. By comparing canonical JSON, our own emissions become no-ops
90
+ // here, while genuine external resets (record reload, form.reset()) still
91
+ // refresh the draft.
92
+ React.useEffect(() => {
93
+ const parsed = tryParse(draft)
94
+ const draftCanonical = parsed.ok ? canonical(parsed.value) : '__invalid__'
95
+ if (draftCanonical === canonical(value)) return
96
+ setDraft(stringify(value))
97
+ setError(null)
98
+ // Intentionally depend only on `value`. `draft` is read via closure: we
99
+ // only want this to fire on external changes, not when the user types.
100
+ // eslint-disable-next-line react-hooks/exhaustive-deps
101
+ }, [value])
102
+
103
+ const handleChange = (next: string): void => {
104
+ setDraft(next)
105
+ if (next.trim() === '') {
106
+ setError(null)
107
+ onChange(null)
108
+ return
109
+ }
110
+ const parsed = tryParse(next)
111
+ if (parsed.ok) {
112
+ setError(null)
113
+ onChange(parsed.value)
114
+ } else {
115
+ try {
116
+ JSON.parse(next)
117
+ } catch (e) {
118
+ setError(e instanceof Error ? e.message : invalidLabel)
119
+ }
120
+ }
121
+ }
122
+
123
+ const format = (): void => {
124
+ if (draft.trim() === '') return
125
+ try {
126
+ const parsed = JSON.parse(draft) as unknown
127
+ const pretty = JSON.stringify(parsed, null, 2)
128
+ setDraft(pretty)
129
+ setError(null)
130
+ onChange(parsed)
131
+ } catch (e) {
132
+ setError(e instanceof Error ? e.message : invalidLabel)
133
+ }
134
+ }
135
+
136
+ return (
137
+ <div className={cn('space-y-2', className)}>
138
+ <div className="relative">
139
+ <Textarea
140
+ value={draft}
141
+ onChange={(e) => handleChange(e.target.value)}
142
+ onBlur={onBlur}
143
+ disabled={disabled}
144
+ rows={rows}
145
+ spellCheck={false}
146
+ placeholder={placeholder}
147
+ className={cn(
148
+ 'pr-20 font-mono text-xs leading-relaxed',
149
+ error && 'border-destructive focus-visible:ring-destructive/40',
150
+ )}
151
+ />
152
+ <Button
153
+ type="button"
154
+ variant="ghost"
155
+ size="sm"
156
+ onClick={format}
157
+ disabled={disabled || draft.trim() === ''}
158
+ className="absolute right-1 top-1 h-7 px-2 text-xs"
159
+ title={formatLabel}
160
+ >
161
+ <Wand2 className="size-3.5" />
162
+ <span className="hidden sm:inline">{formatLabel}</span>
163
+ </Button>
164
+ </div>
165
+ {error ? (
166
+ <p className="flex items-start gap-1.5 text-xs text-destructive">
167
+ <AlertCircle className="mt-0.5 size-3.5 shrink-0" />
168
+ <span className="break-all">
169
+ {invalidLabel}: {error}
170
+ </span>
171
+ </p>
172
+ ) : null}
173
+ </div>
174
+ )
175
+ }
176
+
177
+ export interface JsonViewProps {
178
+ value: unknown
179
+ className?: string
180
+ /** Render a single-line collapsed `<code>` instead of a pretty `<pre>`. */
181
+ inline?: boolean
182
+ }
183
+
184
+ export function JsonView({ value, className, inline }: JsonViewProps): React.ReactElement {
185
+ const text = React.useMemo(() => stringify(value), [value])
186
+ if (inline) {
187
+ const compact = text.replace(/\s+/g, ' ').trim()
188
+ return (
189
+ <code
190
+ className={cn(
191
+ 'line-clamp-1 max-w-[24rem] truncate font-mono text-xs text-muted-foreground',
192
+ className,
193
+ )}
194
+ title={text || undefined}
195
+ >
196
+ {compact || '—'}
197
+ </code>
198
+ )
199
+ }
200
+ return (
201
+ <pre
202
+ className={cn(
203
+ 'max-h-96 overflow-auto rounded-md border border-border bg-muted/40 p-3 font-mono text-xs leading-relaxed text-foreground',
204
+ className,
205
+ )}
206
+ >
207
+ <code>{text || '—'}</code>
208
+ </pre>
209
+ )
210
+ }
@@ -0,0 +1,35 @@
1
+ import * as React from 'react'
2
+ import { cn } from '../lib/utils.js'
3
+
4
+ /**
5
+ * Single keyboard key glyph. Renders a `<kbd>` styled like a typical
6
+ * key cap. Compose multiple `<Kbd>` siblings to spell out a chord:
7
+ * `<Kbd>Ctrl</Kbd>+<Kbd>S</Kbd>`. The cap uses `bg-muted` /
8
+ * `text-foreground/80` so it reads well on any neutral surface
9
+ * (cards, popovers, tooltips).
10
+ */
11
+ export const Kbd = React.forwardRef<
12
+ HTMLElement,
13
+ React.HTMLAttributes<HTMLElement>
14
+ >(({ className, ...props }, ref) => (
15
+ <kbd
16
+ ref={ref}
17
+ className={cn(
18
+ 'inline-flex h-5 min-w-[1.25rem] items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-[10px] font-medium text-foreground/80 shadow-[0_1px_0_0_var(--border)]',
19
+ className,
20
+ )}
21
+ {...props}
22
+ />
23
+ ))
24
+ Kbd.displayName = 'Kbd'
25
+
26
+ /**
27
+ * Resolve the platform-appropriate label for the primary modifier
28
+ * key. Renders `⌘` on macOS / iOS, `Ctrl` everywhere else.
29
+ */
30
+ export function getModKeyLabel(): string {
31
+ if (typeof navigator === 'undefined') return 'Ctrl'
32
+ const platform = navigator.platform || ''
33
+ const ua = navigator.userAgent || ''
34
+ return /Mac|iPhone|iPad|iPod/i.test(platform) || /Mac/i.test(ua) ? '⌘' : 'Ctrl'
35
+ }