@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.
- package/dist/components/accordion.d.ts +7 -0
- package/dist/components/accordion.d.ts.map +1 -0
- package/dist/components/accordion.jsx +19 -0
- package/dist/components/accordion.jsx.map +1 -0
- package/dist/components/alert-dialog.d.ts +22 -0
- package/dist/components/alert-dialog.d.ts.map +1 -0
- package/dist/components/alert-dialog.jsx +27 -0
- package/dist/components/alert-dialog.jsx.map +1 -0
- package/dist/components/audit-timeline.d.ts +24 -0
- package/dist/components/audit-timeline.d.ts.map +1 -0
- package/dist/components/audit-timeline.jsx +60 -0
- package/dist/components/audit-timeline.jsx.map +1 -0
- package/dist/components/avatar.d.ts +6 -0
- package/dist/components/avatar.d.ts.map +1 -0
- package/dist/components/avatar.jsx +10 -0
- package/dist/components/avatar.jsx.map +1 -0
- package/dist/components/badge.d.ts +10 -0
- package/dist/components/badge.d.ts.map +1 -0
- package/dist/components/badge.jsx +19 -0
- package/dist/components/badge.jsx.map +1 -0
- package/dist/components/breadcrumb.d.ts +17 -0
- package/dist/components/breadcrumb.d.ts.map +1 -0
- package/dist/components/breadcrumb.jsx +27 -0
- package/dist/components/breadcrumb.jsx.map +1 -0
- package/dist/components/button.d.ts +12 -0
- package/dist/components/button.d.ts.map +1 -0
- package/dist/components/button.jsx +37 -0
- package/dist/components/button.jsx.map +1 -0
- package/dist/components/calendar.d.ts +9 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.jsx +102 -0
- package/dist/components/calendar.jsx.map +1 -0
- package/dist/components/card.d.ts +8 -0
- package/dist/components/card.d.ts.map +1 -0
- package/dist/components/card.jsx +18 -0
- package/dist/components/card.jsx.map +1 -0
- package/dist/components/chart.d.ts +97 -0
- package/dist/components/chart.d.ts.map +1 -0
- package/dist/components/chart.jsx +233 -0
- package/dist/components/chart.jsx.map +1 -0
- package/dist/components/checkbox.d.ts +4 -0
- package/dist/components/checkbox.d.ts.map +1 -0
- package/dist/components/checkbox.jsx +11 -0
- package/dist/components/checkbox.jsx.map +1 -0
- package/dist/components/combobox.d.ts +46 -0
- package/dist/components/combobox.d.ts.map +1 -0
- package/dist/components/combobox.jsx +145 -0
- package/dist/components/combobox.jsx.map +1 -0
- package/dist/components/command.d.ts +80 -0
- package/dist/components/command.d.ts.map +1 -0
- package/dist/components/command.jsx +32 -0
- package/dist/components/command.jsx.map +1 -0
- package/dist/components/date-picker.d.ts +24 -0
- package/dist/components/date-picker.d.ts.map +1 -0
- package/dist/components/date-picker.jsx +149 -0
- package/dist/components/date-picker.jsx.map +1 -0
- package/dist/components/date-range-input.d.ts +22 -0
- package/dist/components/date-range-input.d.ts.map +1 -0
- package/dist/components/date-range-input.jsx +202 -0
- package/dist/components/date-range-input.jsx.map +1 -0
- package/dist/components/dialog.d.ts +19 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.jsx +30 -0
- package/dist/components/dialog.jsx.map +1 -0
- package/dist/components/diff-view.d.ts +24 -0
- package/dist/components/diff-view.d.ts.map +1 -0
- package/dist/components/diff-view.jsx +69 -0
- package/dist/components/diff-view.jsx.map +1 -0
- package/dist/components/dropdown-menu.d.ts +27 -0
- package/dist/components/dropdown-menu.d.ts.map +1 -0
- package/dist/components/dropdown-menu.jsx +48 -0
- package/dist/components/dropdown-menu.jsx.map +1 -0
- package/dist/components/empty.d.ts +15 -0
- package/dist/components/empty.d.ts.map +1 -0
- package/dist/components/empty.jsx +27 -0
- package/dist/components/empty.jsx.map +1 -0
- package/dist/components/field.d.ts +23 -0
- package/dist/components/field.d.ts.map +1 -0
- package/dist/components/field.jsx +60 -0
- package/dist/components/field.jsx.map +1 -0
- package/dist/components/file-input.d.ts +50 -0
- package/dist/components/file-input.d.ts.map +1 -0
- package/dist/components/file-input.jsx +104 -0
- package/dist/components/file-input.jsx.map +1 -0
- package/dist/components/form.d.ts +20 -0
- package/dist/components/form.d.ts.map +1 -0
- package/dist/components/form.jsx +66 -0
- package/dist/components/form.jsx.map +1 -0
- package/dist/components/info-tooltip.d.ts +11 -0
- package/dist/components/info-tooltip.d.ts.map +1 -0
- package/dist/components/info-tooltip.jsx +17 -0
- package/dist/components/info-tooltip.jsx.map +1 -0
- package/dist/components/input.d.ts +13 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.jsx +19 -0
- package/dist/components/input.jsx.map +1 -0
- package/dist/components/json-editor.d.ts +23 -0
- package/dist/components/json-editor.d.ts.map +1 -0
- package/dist/components/json-editor.jsx +143 -0
- package/dist/components/json-editor.jsx.map +1 -0
- package/dist/components/kbd.d.ts +15 -0
- package/dist/components/kbd.d.ts.map +1 -0
- package/dist/components/kbd.jsx +23 -0
- package/dist/components/kbd.jsx.map +1 -0
- package/dist/components/key-value-editor.d.ts +92 -0
- package/dist/components/key-value-editor.d.ts.map +1 -0
- package/dist/components/key-value-editor.jsx +187 -0
- package/dist/components/key-value-editor.jsx.map +1 -0
- package/dist/components/keyboard-shortcuts-help.d.ts +17 -0
- package/dist/components/keyboard-shortcuts-help.d.ts.map +1 -0
- package/dist/components/keyboard-shortcuts-help.jsx +97 -0
- package/dist/components/keyboard-shortcuts-help.jsx.map +1 -0
- package/dist/components/label.d.ts +5 -0
- package/dist/components/label.d.ts.map +1 -0
- package/dist/components/label.jsx +8 -0
- package/dist/components/label.jsx.map +1 -0
- package/dist/components/media-preview.d.ts +30 -0
- package/dist/components/media-preview.d.ts.map +1 -0
- package/dist/components/media-preview.jsx +189 -0
- package/dist/components/media-preview.jsx.map +1 -0
- package/dist/components/multi-file-input.d.ts +76 -0
- package/dist/components/multi-file-input.d.ts.map +1 -0
- package/dist/components/multi-file-input.jsx +131 -0
- package/dist/components/multi-file-input.jsx.map +1 -0
- package/dist/components/password-input.d.ts +10 -0
- package/dist/components/password-input.d.ts.map +1 -0
- package/dist/components/password-input.jsx +18 -0
- package/dist/components/password-input.jsx.map +1 -0
- package/dist/components/popover.d.ts +7 -0
- package/dist/components/popover.d.ts.map +1 -0
- package/dist/components/popover.jsx +11 -0
- package/dist/components/popover.jsx.map +1 -0
- package/dist/components/revision-timeline.d.ts +30 -0
- package/dist/components/revision-timeline.d.ts.map +1 -0
- package/dist/components/revision-timeline.jsx +42 -0
- package/dist/components/revision-timeline.jsx.map +1 -0
- package/dist/components/richtext-editor.d.ts +43 -0
- package/dist/components/richtext-editor.d.ts.map +1 -0
- package/dist/components/richtext-editor.jsx +319 -0
- package/dist/components/richtext-editor.jsx.map +1 -0
- package/dist/components/richtext-mode.d.ts +23 -0
- package/dist/components/richtext-mode.d.ts.map +1 -0
- package/dist/components/richtext-mode.js +36 -0
- package/dist/components/richtext-mode.js.map +1 -0
- package/dist/components/richtext-render.d.ts +8 -0
- package/dist/components/richtext-render.d.ts.map +1 -0
- package/dist/components/richtext-render.jsx +33 -0
- package/dist/components/richtext-render.jsx.map +1 -0
- package/dist/components/richtext-sync.d.ts +37 -0
- package/dist/components/richtext-sync.d.ts.map +1 -0
- package/dist/components/richtext-sync.js +46 -0
- package/dist/components/richtext-sync.js.map +1 -0
- package/dist/components/scroll-area.d.ts +5 -0
- package/dist/components/scroll-area.d.ts.map +1 -0
- package/dist/components/scroll-area.jsx +16 -0
- package/dist/components/scroll-area.jsx.map +1 -0
- package/dist/components/select.d.ts +36 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/select.jsx +87 -0
- package/dist/components/select.jsx.map +1 -0
- package/dist/components/separator.d.ts +4 -0
- package/dist/components/separator.d.ts.map +1 -0
- package/dist/components/separator.jsx +6 -0
- package/dist/components/separator.jsx.map +1 -0
- package/dist/components/sheet.d.ts +29 -0
- package/dist/components/sheet.d.ts.map +1 -0
- package/dist/components/sheet.jsx +44 -0
- package/dist/components/sheet.jsx.map +1 -0
- package/dist/components/sidebar.d.ts +70 -0
- package/dist/components/sidebar.d.ts.map +1 -0
- package/dist/components/sidebar.jsx +245 -0
- package/dist/components/sidebar.jsx.map +1 -0
- package/dist/components/skeleton.d.ts +3 -0
- package/dist/components/skeleton.d.ts.map +1 -0
- package/dist/components/skeleton.jsx +6 -0
- package/dist/components/skeleton.jsx.map +1 -0
- package/dist/components/sonner.d.ts +6 -0
- package/dist/components/sonner.d.ts.map +1 -0
- package/dist/components/sonner.jsx +29 -0
- package/dist/components/sonner.jsx.map +1 -0
- package/dist/components/switch.d.ts +4 -0
- package/dist/components/switch.d.ts.map +1 -0
- package/dist/components/switch.jsx +8 -0
- package/dist/components/switch.jsx.map +1 -0
- package/dist/components/table.d.ts +10 -0
- package/dist/components/table.d.ts.map +1 -0
- package/dist/components/table.jsx +21 -0
- package/dist/components/table.jsx.map +1 -0
- package/dist/components/tabs.d.ts +7 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tabs.jsx +14 -0
- package/dist/components/tabs.jsx.map +1 -0
- package/dist/components/textarea.d.ts +4 -0
- package/dist/components/textarea.d.ts.map +1 -0
- package/dist/components/textarea.jsx +5 -0
- package/dist/components/textarea.jsx.map +1 -0
- package/dist/components/tooltip.d.ts +7 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip.jsx +11 -0
- package/dist/components/tooltip.jsx.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/theme.d.ts +11 -0
- package/dist/lib/theme.d.ts.map +1 -0
- package/dist/lib/theme.js +44 -0
- package/dist/lib/theme.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/styles.css +242 -0
- package/package.json +85 -0
- package/src/components/accordion.tsx +48 -0
- package/src/components/alert-dialog.tsx +113 -0
- package/src/components/audit-timeline.tsx +102 -0
- package/src/components/avatar.tsx +42 -0
- package/src/components/badge.tsx +34 -0
- package/src/components/breadcrumb.tsx +99 -0
- package/src/components/button.tsx +58 -0
- package/src/components/calendar.tsx +176 -0
- package/src/components/card.tsx +60 -0
- package/src/components/chart.tsx +558 -0
- package/src/components/checkbox.tsx +23 -0
- package/src/components/combobox.tsx +264 -0
- package/src/components/command.tsx +120 -0
- package/src/components/date-picker.tsx +221 -0
- package/src/components/date-range-input.tsx +295 -0
- package/src/components/dialog.tsx +94 -0
- package/src/components/diff-view.tsx +182 -0
- package/src/components/dropdown-menu.tsx +165 -0
- package/src/components/empty.tsx +100 -0
- package/src/components/field.tsx +168 -0
- package/src/components/file-input.tsx +233 -0
- package/src/components/form.tsx +152 -0
- package/src/components/info-tooltip.tsx +40 -0
- package/src/components/input.tsx +55 -0
- package/src/components/json-editor.tsx +210 -0
- package/src/components/kbd.tsx +35 -0
- package/src/components/key-value-editor.tsx +423 -0
- package/src/components/keyboard-shortcuts-help.tsx +136 -0
- package/src/components/label.tsx +16 -0
- package/src/components/media-preview.tsx +278 -0
- package/src/components/multi-file-input.tsx +315 -0
- package/src/components/password-input.tsx +50 -0
- package/src/components/popover.tsx +26 -0
- package/src/components/revision-timeline.tsx +93 -0
- package/src/components/richtext-editor.tsx +624 -0
- package/src/components/richtext-mode.ts +39 -0
- package/src/components/richtext-render.tsx +51 -0
- package/src/components/richtext-sync.ts +57 -0
- package/src/components/scroll-area.tsx +41 -0
- package/src/components/select.tsx +200 -0
- package/src/components/separator.tsx +21 -0
- package/src/components/sheet.tsx +109 -0
- package/src/components/sidebar.tsx +660 -0
- package/src/components/skeleton.tsx +9 -0
- package/src/components/sonner.tsx +45 -0
- package/src/components/switch.tsx +24 -0
- package/src/components/table.tsx +93 -0
- package/src/components/tabs.tsx +57 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/tooltip.tsx +25 -0
- package/src/index.ts +342 -0
- package/src/lib/theme.ts +45 -0
- package/src/lib/utils.ts +6 -0
- 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
|
+
}
|