@gentleduck/registry-ui 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/index.css +3 -0
- package/package.json +59 -0
- package/src/_old/_table/index.ts +5 -0
- package/src/_old/_table/table-advanced.constants.tsx +24 -0
- package/src/_old/_table/table-advanced.tsx +311 -0
- package/src/_old/_table/table-advanced.types.ts +272 -0
- package/src/_old/_table/table.constants.ts +2 -0
- package/src/_old/_table/table.hook.tsx +115 -0
- package/src/_old/_table/table.lib.ts +85 -0
- package/src/_old/_table/table.tsx +916 -0
- package/src/_old/_table/table.types.ts +118 -0
- package/src/_old/_table/todo.md +11 -0
- package/src/_old/_upload/index.ts +9 -0
- package/src/_old/_upload/todo.md +38 -0
- package/src/_old/_upload/upload-advanced-chunks.tsx +1624 -0
- package/src/_old/_upload/upload-advanced.tsx +507 -0
- package/src/_old/_upload/upload-sonner.tsx +58 -0
- package/src/_old/_upload/upload.assets.tsx +239 -0
- package/src/_old/_upload/upload.constants.tsx +75 -0
- package/src/_old/_upload/upload.dto.ts +19 -0
- package/src/_old/_upload/upload.lib.tsx +630 -0
- package/src/_old/_upload/upload.tsx +491 -0
- package/src/_old/_upload/upload.types.ts +436 -0
- package/src/accordion/accordion.tsx +247 -0
- package/src/accordion/index.ts +1 -0
- package/src/alert/alert.constants.ts +17 -0
- package/src/alert/alert.tsx +52 -0
- package/src/alert/index.ts +2 -0
- package/src/alert-dialog/alert-dialog.tsx +107 -0
- package/src/alert-dialog/index.ts +1 -0
- package/src/aspect-ratio/aspect-ratio.tsx +33 -0
- package/src/aspect-ratio/index.ts +1 -0
- package/src/audio/audio-record.tsx +776 -0
- package/src/audio/audio-visualizer.tsx +377 -0
- package/src/audio/audio.libs.ts +5 -0
- package/src/audio/audio.types.ts +50 -0
- package/src/audio/index.ts +2 -0
- package/src/avatar/avatar.tsx +78 -0
- package/src/avatar/index.ts +1 -0
- package/src/badge/badge.constants.ts +38 -0
- package/src/badge/badge.tsx +19 -0
- package/src/badge/index.ts +2 -0
- package/src/breadcrumb/breadcrumb.tsx +119 -0
- package/src/breadcrumb/index.ts +1 -0
- package/src/button/button.constants.ts +44 -0
- package/src/button/button.tsx +79 -0
- package/src/button/button.types.ts +38 -0
- package/src/button/index.ts +3 -0
- package/src/button-group/button-group.constants.ts +26 -0
- package/src/button-group/button-group.tsx +65 -0
- package/src/button-group/index.ts +2 -0
- package/src/calendar/calendar.tsx +191 -0
- package/src/calendar/index.ts +1 -0
- package/src/card/card.tsx +81 -0
- package/src/card/index.ts +1 -0
- package/src/carousel/carousel.tsx +211 -0
- package/src/carousel/carousel.types.ts +23 -0
- package/src/carousel/index.ts +2 -0
- package/src/chart/chart.libs.ts +27 -0
- package/src/chart/chart.tsx +260 -0
- package/src/chart/chart.types.ts +38 -0
- package/src/chart/index.ts +3 -0
- package/src/checkbox/checkbox.tsx +144 -0
- package/src/checkbox/checkbox.types.ts +24 -0
- package/src/checkbox/index.ts +2 -0
- package/src/collapsible/collapsible.tsx +151 -0
- package/src/collapsible/index.ts +1 -0
- package/src/combobox/combobox.tsx +132 -0
- package/src/combobox/index.ts +1 -0
- package/src/command/command.tsx +192 -0
- package/src/command/command.types.ts +11 -0
- package/src/command/index.ts +2 -0
- package/src/context-menu/context-menu.tsx +178 -0
- package/src/context-menu/index.ts +1 -0
- package/src/dialog/dialog-responsive.tsx +137 -0
- package/src/dialog/dialog.tsx +97 -0
- package/src/dialog/index.ts +2 -0
- package/src/direction/direction.tsx +13 -0
- package/src/direction/index.ts +1 -0
- package/src/drawer/drawer.tsx +185 -0
- package/src/drawer/index.ts +1 -0
- package/src/dropdown-menu/dropdown-menu.tsx +181 -0
- package/src/dropdown-menu/index.ts +1 -0
- package/src/empty/empty.constants.ts +15 -0
- package/src/empty/empty.tsx +73 -0
- package/src/empty/index.ts +2 -0
- package/src/field/field.constants.ts +22 -0
- package/src/field/field.tsx +203 -0
- package/src/field/index.ts +2 -0
- package/src/hover-card/hover-card.tsx +79 -0
- package/src/hover-card/index.ts +1 -0
- package/src/input/index.ts +1 -0
- package/src/input/input.tsx +45 -0
- package/src/input-group/index.ts +1 -0
- package/src/input-group/input-group.tsx +170 -0
- package/src/input-otp/index.ts +1 -0
- package/src/input-otp/input-otp.tsx +66 -0
- package/src/item/index.ts +2 -0
- package/src/item/item.constants.ts +22 -0
- package/src/item/item.tsx +185 -0
- package/src/json-editor/index.ts +4 -0
- package/src/json-editor/json-editor.hooks.ts +21 -0
- package/src/json-editor/json-editor.libs.ts +34 -0
- package/src/json-editor/json-editor.tsx +425 -0
- package/src/json-editor/json-editor.types.ts +80 -0
- package/src/json-editor/json-editor.view.tsx +110 -0
- package/src/json-editor/json-text-area.tsx +7 -0
- package/src/kbd/index.ts +1 -0
- package/src/kbd/kbd.tsx +39 -0
- package/src/label/index.ts +1 -0
- package/src/label/label.tsx +28 -0
- package/src/menubar/index.ts +1 -0
- package/src/menubar/menubar.tsx +213 -0
- package/src/navigation-menu/index.ts +1 -0
- package/src/navigation-menu/navigation-menu.tsx +152 -0
- package/src/pagination/index.ts +2 -0
- package/src/pagination/pagination.tsx +191 -0
- package/src/pagination/pagination.types.ts +17 -0
- package/src/popover/index.ts +1 -0
- package/src/popover/popover.tsx +35 -0
- package/src/preview-panel/index.ts +3 -0
- package/src/preview-panel/preview-panel-dialog.tsx +99 -0
- package/src/preview-panel/preview-panel.tsx +389 -0
- package/src/preview-panel/preview-panel.types.ts +49 -0
- package/src/progress/index.ts +1 -0
- package/src/progress/progress.tsx +32 -0
- package/src/radio-group/index.ts +1 -0
- package/src/radio-group/radio-group.tsx +92 -0
- package/src/resizable/index.ts +1 -0
- package/src/resizable/resizable.tsx +52 -0
- package/src/scroll-area/index.ts +1 -0
- package/src/scroll-area/scroll-area.tsx +30 -0
- package/src/select/index.ts +1 -0
- package/src/select/select.tsx +138 -0
- package/src/separator/index.ts +1 -0
- package/src/separator/separator.tsx +28 -0
- package/src/sheet/index.ts +2 -0
- package/src/sheet/sheet.constants.tsx +20 -0
- package/src/sheet/sheet.tsx +92 -0
- package/src/sidebar/index.ts +4 -0
- package/src/sidebar/sidebar.constants.ts +30 -0
- package/src/sidebar/sidebar.hooks.ts +13 -0
- package/src/sidebar/sidebar.tsx +676 -0
- package/src/sidebar/sidebar.types.ts +28 -0
- package/src/skeleton/index.ts +1 -0
- package/src/skeleton/skeleton.tsx +22 -0
- package/src/slider/index.ts +1 -0
- package/src/slider/slider.tsx +57 -0
- package/src/sonner/index.ts +4 -0
- package/src/sonner/sonner.chunks.tsx +80 -0
- package/src/sonner/sonner.libs.ts +13 -0
- package/src/sonner/sonner.tsx +31 -0
- package/src/sonner/sonner.types.ts +9 -0
- package/src/switch/index.ts +1 -0
- package/src/switch/switch.tsx +63 -0
- package/src/table/index.ts +1 -0
- package/src/table/table.tsx +95 -0
- package/src/tabs/index.ts +1 -0
- package/src/tabs/tabs.tsx +151 -0
- package/src/textarea/index.ts +1 -0
- package/src/textarea/textarea.tsx +24 -0
- package/src/toggle/index.ts +2 -0
- package/src/toggle/toggle.constants.ts +22 -0
- package/src/toggle/toggle.tsx +24 -0
- package/src/toggle-group/index.ts +1 -0
- package/src/toggle-group/toggle-group.tsx +69 -0
- package/src/tooltip/index.ts +1 -0
- package/src/tooltip/tooltip.tsx +32 -0
- package/src/upload/index.ts +1 -0
- package/src/upload/upload.constants.tsx +19 -0
- package/src/upload/upload.libs.ts +97 -0
- package/src/upload/upload.tsx +340 -0
- package/src/upload/upload.types.ts +44 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '@gentleduck/libs/cn'
|
|
4
|
+
import { Portal } from '@gentleduck/primitives/portal'
|
|
5
|
+
import { Maximize } from 'lucide-react'
|
|
6
|
+
import * as React from 'react'
|
|
7
|
+
import type { FieldValues } from 'react-hook-form'
|
|
8
|
+
import { useController } from 'react-hook-form'
|
|
9
|
+
import { toast } from 'sonner'
|
|
10
|
+
import {
|
|
11
|
+
AlertDialog,
|
|
12
|
+
AlertDialogAction,
|
|
13
|
+
AlertDialogCancel,
|
|
14
|
+
AlertDialogContent,
|
|
15
|
+
AlertDialogDescription,
|
|
16
|
+
AlertDialogFooter,
|
|
17
|
+
AlertDialogHeader,
|
|
18
|
+
AlertDialogTitle,
|
|
19
|
+
} from '../alert-dialog'
|
|
20
|
+
import { Button } from '../button'
|
|
21
|
+
import { Field, FieldDescription, FieldError, FieldLabel } from '../field'
|
|
22
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../popover'
|
|
23
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '../sheet'
|
|
24
|
+
import { useJsonEditorHotkeys } from './json-editor.hooks'
|
|
25
|
+
import { formatJson, isObjectLike, safeStringify, tryParseJson } from './json-editor.libs'
|
|
26
|
+
import type { JsonEditorText, JsonTextareaFieldProps } from './json-editor.types'
|
|
27
|
+
import { JsonEditorView } from './json-editor.view'
|
|
28
|
+
|
|
29
|
+
export function JsonTextareaField<TFieldValues extends FieldValues>(
|
|
30
|
+
props: JsonTextareaFieldProps<TFieldValues>,
|
|
31
|
+
): React.JSX.Element {
|
|
32
|
+
const {
|
|
33
|
+
control,
|
|
34
|
+
name,
|
|
35
|
+
label,
|
|
36
|
+
description,
|
|
37
|
+
className,
|
|
38
|
+
actionsClassName,
|
|
39
|
+
isEditable = true,
|
|
40
|
+
allowArray = true,
|
|
41
|
+
mode = 'inline',
|
|
42
|
+
rows = 12,
|
|
43
|
+
placeholder = '{\n "theme": "dark"\n}',
|
|
44
|
+
lineNumbers = true,
|
|
45
|
+
lineHeightPx = 20,
|
|
46
|
+
dir,
|
|
47
|
+
lang,
|
|
48
|
+
expandMode = 'sheet',
|
|
49
|
+
sheetSide = 'right',
|
|
50
|
+
sheetTitle = 'Edit JSON',
|
|
51
|
+
text: textProp,
|
|
52
|
+
onExpandEditor,
|
|
53
|
+
} = props
|
|
54
|
+
|
|
55
|
+
const t: Required<JsonEditorText> = {
|
|
56
|
+
format: textProp?.format ?? 'Format',
|
|
57
|
+
cancel: textProp?.cancel ?? 'Cancel',
|
|
58
|
+
save: textProp?.save ?? 'Save',
|
|
59
|
+
full: textProp?.full ?? 'Full',
|
|
60
|
+
close: textProp?.close ?? 'Close',
|
|
61
|
+
keepEditing: textProp?.keepEditing ?? 'Keep editing',
|
|
62
|
+
discard: textProp?.discard ?? 'Discard',
|
|
63
|
+
discardTitle: textProp?.discardTitle ?? 'Discard changes?',
|
|
64
|
+
discardDescription:
|
|
65
|
+
textProp?.discardDescription ?? 'You have unsaved changes in the editor. If you close now, they will be lost.',
|
|
66
|
+
statusHint: textProp?.statusHint ?? 'Ctrl/Cmd + Enter: Save, Esc: Cancel',
|
|
67
|
+
sheetStatusHint: textProp?.sheetStatusHint ?? 'Ctrl/Cmd + Enter: Save, Esc: Close',
|
|
68
|
+
unsavedChanges: textProp?.unsavedChanges ?? 'Unsaved changes',
|
|
69
|
+
saved: textProp?.saved ?? 'Saved',
|
|
70
|
+
nullPreview: textProp?.nullPreview ?? 'NULL',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { field, fieldState } = useController({ control, name })
|
|
74
|
+
const committedText = React.useMemo(() => safeStringify(field.value), [field.value])
|
|
75
|
+
|
|
76
|
+
const [draft, setDraft] = React.useState(committedText)
|
|
77
|
+
const [dirty, setDirty] = React.useState(false)
|
|
78
|
+
const [scrollTop, setScrollTop] = React.useState(0)
|
|
79
|
+
|
|
80
|
+
React.useEffect(() => {
|
|
81
|
+
if (dirty) return
|
|
82
|
+
setDraft(committedText)
|
|
83
|
+
}, [committedText, dirty])
|
|
84
|
+
|
|
85
|
+
const [popoverOpen, setPopoverOpen] = React.useState(false)
|
|
86
|
+
|
|
87
|
+
React.useEffect(() => {
|
|
88
|
+
if (mode !== 'popover') {
|
|
89
|
+
setPopoverOpen(false)
|
|
90
|
+
}
|
|
91
|
+
}, [mode])
|
|
92
|
+
|
|
93
|
+
const [sheetOpen, setSheetOpen] = React.useState(false)
|
|
94
|
+
const [sheetDraft, setSheetDraft] = React.useState('')
|
|
95
|
+
const [sheetDirty, setSheetDirty] = React.useState(false)
|
|
96
|
+
const [sheetScrollTop, setSheetScrollTop] = React.useState(0)
|
|
97
|
+
const [confirmDiscardOpen, setConfirmDiscardOpen] = React.useState(false)
|
|
98
|
+
|
|
99
|
+
const validateAndCommitValue = React.useCallback(
|
|
100
|
+
(text: string): boolean => {
|
|
101
|
+
if (!isEditable) return false
|
|
102
|
+
|
|
103
|
+
const parsed = tryParseJson(text)
|
|
104
|
+
if (!parsed.ok) {
|
|
105
|
+
toast.error(parsed.message || 'Please enter valid JSON')
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (parsed.value === null) {
|
|
110
|
+
field.onChange(null)
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!allowArray && Array.isArray(parsed.value)) {
|
|
115
|
+
toast.error('Value must be a JSON object (arrays are not allowed).')
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!allowArray && !isObjectLike(parsed.value)) {
|
|
120
|
+
toast.error('Value must be a JSON object.')
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
field.onChange(parsed.value)
|
|
125
|
+
return true
|
|
126
|
+
},
|
|
127
|
+
[allowArray, field, isEditable],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const cancelInline = React.useCallback(() => {
|
|
131
|
+
setDraft(committedText)
|
|
132
|
+
setDirty(false)
|
|
133
|
+
if (mode === 'popover') {
|
|
134
|
+
setPopoverOpen(false)
|
|
135
|
+
}
|
|
136
|
+
}, [committedText, mode])
|
|
137
|
+
|
|
138
|
+
const saveInline = React.useCallback(() => {
|
|
139
|
+
if (!validateAndCommitValue(draft)) return
|
|
140
|
+
setDirty(false)
|
|
141
|
+
if (mode === 'popover') {
|
|
142
|
+
setPopoverOpen(false)
|
|
143
|
+
}
|
|
144
|
+
}, [draft, mode, validateAndCommitValue])
|
|
145
|
+
|
|
146
|
+
const formatInline = React.useCallback(() => {
|
|
147
|
+
if (!isEditable) return
|
|
148
|
+
|
|
149
|
+
const result = formatJson(draft)
|
|
150
|
+
if (!result.ok) {
|
|
151
|
+
toast.error(result.message || 'Please enter valid JSON to format')
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setDraft(result.formatted)
|
|
156
|
+
setDirty(true)
|
|
157
|
+
}, [draft, isEditable])
|
|
158
|
+
|
|
159
|
+
const inlineHotkeys = useJsonEditorHotkeys({
|
|
160
|
+
enabled: mode === 'inline' || popoverOpen,
|
|
161
|
+
onEscape: cancelInline,
|
|
162
|
+
onSave: saveInline,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const openSheet = React.useCallback(() => {
|
|
166
|
+
setSheetDraft(draft)
|
|
167
|
+
setSheetDirty(false)
|
|
168
|
+
setSheetScrollTop(0)
|
|
169
|
+
setSheetOpen(true)
|
|
170
|
+
}, [draft])
|
|
171
|
+
|
|
172
|
+
const requestCloseSheet = React.useCallback(() => {
|
|
173
|
+
if (!sheetDirty) {
|
|
174
|
+
setSheetOpen(false)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
setConfirmDiscardOpen(true)
|
|
179
|
+
}, [sheetDirty])
|
|
180
|
+
|
|
181
|
+
const discardSheetChanges = React.useCallback(() => {
|
|
182
|
+
setConfirmDiscardOpen(false)
|
|
183
|
+
setSheetDirty(false)
|
|
184
|
+
setSheetOpen(false)
|
|
185
|
+
}, [])
|
|
186
|
+
|
|
187
|
+
const saveSheet = React.useCallback(() => {
|
|
188
|
+
if (!validateAndCommitValue(sheetDraft)) return
|
|
189
|
+
|
|
190
|
+
setDraft(sheetDraft)
|
|
191
|
+
setDirty(false)
|
|
192
|
+
setSheetDirty(false)
|
|
193
|
+
setSheetOpen(false)
|
|
194
|
+
}, [sheetDraft, validateAndCommitValue])
|
|
195
|
+
|
|
196
|
+
const formatSheet = React.useCallback(() => {
|
|
197
|
+
if (!isEditable) return
|
|
198
|
+
|
|
199
|
+
const result = formatJson(sheetDraft)
|
|
200
|
+
if (!result.ok) {
|
|
201
|
+
toast.error(result.message || 'Please enter valid JSON to format')
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
setSheetDraft(result.formatted)
|
|
206
|
+
setSheetDirty(true)
|
|
207
|
+
}, [isEditable, sheetDraft])
|
|
208
|
+
|
|
209
|
+
const sheetHotkeys = useJsonEditorHotkeys({
|
|
210
|
+
enabled: sheetOpen,
|
|
211
|
+
onEscape: requestCloseSheet,
|
|
212
|
+
onSave: saveSheet,
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const handleExpand = React.useCallback(() => {
|
|
216
|
+
if (expandMode === 'none') return
|
|
217
|
+
|
|
218
|
+
if (expandMode === 'callback') {
|
|
219
|
+
const parsed = tryParseJson(draft)
|
|
220
|
+
onExpandEditor?.({
|
|
221
|
+
name,
|
|
222
|
+
rawText: draft,
|
|
223
|
+
value: parsed.ok ? parsed.value : field.value,
|
|
224
|
+
})
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
openSheet()
|
|
229
|
+
}, [draft, expandMode, field.value, name, onExpandEditor, openSheet])
|
|
230
|
+
|
|
231
|
+
const canFormatDraft = React.useMemo(() => {
|
|
232
|
+
const parsed = tryParseJson(draft)
|
|
233
|
+
return parsed.ok && parsed.value !== null
|
|
234
|
+
}, [draft])
|
|
235
|
+
|
|
236
|
+
const canFormatSheet = React.useMemo(() => {
|
|
237
|
+
const parsed = tryParseJson(sheetDraft)
|
|
238
|
+
return parsed.ok && parsed.value !== null
|
|
239
|
+
}, [sheetDraft])
|
|
240
|
+
|
|
241
|
+
const preview = React.useMemo(() => {
|
|
242
|
+
if (!committedText.trim()) return t.nullPreview
|
|
243
|
+
|
|
244
|
+
const oneLine = committedText.replace(/\s+/g, ' ').trim()
|
|
245
|
+
return oneLine.length > 120 ? `${oneLine.slice(0, 117)}...` : oneLine
|
|
246
|
+
}, [committedText])
|
|
247
|
+
|
|
248
|
+
const inlineEditor = (
|
|
249
|
+
<div className="space-y-2" data-slot="json-editor-inline">
|
|
250
|
+
<JsonEditorView
|
|
251
|
+
dir={dir}
|
|
252
|
+
lang={lang}
|
|
253
|
+
lineHeightPx={lineHeightPx}
|
|
254
|
+
lineNumbers={lineNumbers}
|
|
255
|
+
onChange={(value) => {
|
|
256
|
+
setDraft(value)
|
|
257
|
+
setDirty(true)
|
|
258
|
+
}}
|
|
259
|
+
onKeyDown={inlineHotkeys}
|
|
260
|
+
onScroll={setScrollTop}
|
|
261
|
+
placeholder={placeholder}
|
|
262
|
+
readOnly={!isEditable}
|
|
263
|
+
rows={rows}
|
|
264
|
+
scrollTop={scrollTop}
|
|
265
|
+
value={draft}
|
|
266
|
+
/>
|
|
267
|
+
|
|
268
|
+
<div
|
|
269
|
+
className="flex items-center justify-between gap-2 text-muted-foreground text-xs"
|
|
270
|
+
data-slot="json-editor-status">
|
|
271
|
+
<span>{t.statusHint}</span>
|
|
272
|
+
{dirty ? <span className="text-foreground">{t.unsavedChanges}</span> : <span>{t.saved}</span>}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<Field className={cn('space-y-3', className)} data-slot="json-editor-field" dir={dir}>
|
|
279
|
+
<div className="mb-1 flex items-start justify-between gap-4" data-slot="json-editor-header">
|
|
280
|
+
<div className="space-y-1">
|
|
281
|
+
<FieldLabel className="font-semibold text-base">{label}</FieldLabel>
|
|
282
|
+
{description ? <FieldDescription>{description}</FieldDescription> : null}
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<div className={cn('flex items-center gap-2', actionsClassName)} data-slot="json-editor-actions">
|
|
286
|
+
<Button
|
|
287
|
+
disabled={!isEditable || !canFormatDraft}
|
|
288
|
+
onClick={formatInline}
|
|
289
|
+
size="sm"
|
|
290
|
+
type="button"
|
|
291
|
+
variant="outline">
|
|
292
|
+
{t.format}
|
|
293
|
+
</Button>
|
|
294
|
+
|
|
295
|
+
{dirty ? (
|
|
296
|
+
<>
|
|
297
|
+
<Button onClick={cancelInline} size="sm" type="button" variant="outline">
|
|
298
|
+
{t.cancel}
|
|
299
|
+
</Button>
|
|
300
|
+
<Button disabled={!isEditable} onClick={saveInline} size="sm" type="button">
|
|
301
|
+
{t.save}
|
|
302
|
+
</Button>
|
|
303
|
+
</>
|
|
304
|
+
) : null}
|
|
305
|
+
|
|
306
|
+
{expandMode !== 'none' ? (
|
|
307
|
+
<Button onClick={handleExpand} size="sm" type="button" variant="outline">
|
|
308
|
+
<Maximize aria-hidden="true" size={14} />
|
|
309
|
+
<span className="ms-2">{t.full}</span>
|
|
310
|
+
</Button>
|
|
311
|
+
) : null}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<div>
|
|
316
|
+
{mode === 'inline' ? (
|
|
317
|
+
inlineEditor
|
|
318
|
+
) : (
|
|
319
|
+
<div data-slot="json-editor-popover">
|
|
320
|
+
<Popover onOpenChange={setPopoverOpen} open={popoverOpen}>
|
|
321
|
+
<PopoverTrigger asChild>
|
|
322
|
+
<Button
|
|
323
|
+
className="w-full justify-start overflow-hidden text-start font-mono text-xs"
|
|
324
|
+
type="button"
|
|
325
|
+
variant="outline">
|
|
326
|
+
<span className="truncate">{preview}</span>
|
|
327
|
+
</Button>
|
|
328
|
+
</PopoverTrigger>
|
|
329
|
+
<PopoverContent className="w-[min(96vw,720px)] p-2">{inlineEditor}</PopoverContent>
|
|
330
|
+
</Popover>
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
{fieldState.error ? <FieldError errors={[fieldState.error]} /> : null}
|
|
336
|
+
|
|
337
|
+
{expandMode === 'sheet' ? (
|
|
338
|
+
<>
|
|
339
|
+
<Sheet
|
|
340
|
+
onOpenChange={(nextOpen) => {
|
|
341
|
+
if (nextOpen) {
|
|
342
|
+
openSheet()
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
requestCloseSheet()
|
|
347
|
+
}}
|
|
348
|
+
open={sheetOpen}>
|
|
349
|
+
<SheetContent className="w-full sm:max-w-3xl" dir={dir} side={sheetSide}>
|
|
350
|
+
<SheetHeader>
|
|
351
|
+
<SheetTitle>{sheetTitle}</SheetTitle>
|
|
352
|
+
</SheetHeader>
|
|
353
|
+
|
|
354
|
+
<div className="mt-4 space-y-3" data-slot="json-editor-sheet-content">
|
|
355
|
+
<JsonEditorView
|
|
356
|
+
dir={dir}
|
|
357
|
+
lang={lang}
|
|
358
|
+
lineHeightPx={lineHeightPx}
|
|
359
|
+
lineNumbers={lineNumbers}
|
|
360
|
+
onChange={(value) => {
|
|
361
|
+
setSheetDraft(value)
|
|
362
|
+
setSheetDirty(true)
|
|
363
|
+
}}
|
|
364
|
+
onKeyDown={sheetHotkeys}
|
|
365
|
+
onScroll={setSheetScrollTop}
|
|
366
|
+
placeholder={placeholder}
|
|
367
|
+
readOnly={!isEditable}
|
|
368
|
+
rows={24}
|
|
369
|
+
scrollTop={sheetScrollTop}
|
|
370
|
+
value={sheetDraft}
|
|
371
|
+
/>
|
|
372
|
+
|
|
373
|
+
<div className="flex items-center justify-between gap-2" data-slot="json-editor-sheet-actions">
|
|
374
|
+
<div className="text-muted-foreground text-xs">{t.sheetStatusHint}</div>
|
|
375
|
+
|
|
376
|
+
<div className="flex items-center gap-2">
|
|
377
|
+
<Button
|
|
378
|
+
disabled={!isEditable || !canFormatSheet}
|
|
379
|
+
onClick={formatSheet}
|
|
380
|
+
size="sm"
|
|
381
|
+
type="button"
|
|
382
|
+
variant="outline">
|
|
383
|
+
{t.format}
|
|
384
|
+
</Button>
|
|
385
|
+
|
|
386
|
+
<Button onClick={requestCloseSheet} size="sm" type="button" variant="outline">
|
|
387
|
+
{t.close}
|
|
388
|
+
</Button>
|
|
389
|
+
|
|
390
|
+
<Button disabled={!isEditable} onClick={saveSheet} size="sm" type="button">
|
|
391
|
+
{t.save}
|
|
392
|
+
</Button>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
</SheetContent>
|
|
397
|
+
</Sheet>
|
|
398
|
+
</>
|
|
399
|
+
) : null}
|
|
400
|
+
|
|
401
|
+
<Portal>
|
|
402
|
+
<AlertDialog onOpenChange={setConfirmDiscardOpen} open={confirmDiscardOpen}>
|
|
403
|
+
<AlertDialogContent>
|
|
404
|
+
<AlertDialogHeader>
|
|
405
|
+
<AlertDialogTitle>{t.discardTitle}</AlertDialogTitle>
|
|
406
|
+
<AlertDialogDescription>{t.discardDescription}</AlertDialogDescription>
|
|
407
|
+
</AlertDialogHeader>
|
|
408
|
+
<AlertDialogFooter>
|
|
409
|
+
<AlertDialogCancel asChild onClick={() => setConfirmDiscardOpen(false)}>
|
|
410
|
+
<Button variant="outline" size="sm">
|
|
411
|
+
{t.keepEditing}
|
|
412
|
+
</Button>
|
|
413
|
+
</AlertDialogCancel>
|
|
414
|
+
<AlertDialogAction asChild onClick={discardSheetChanges}>
|
|
415
|
+
<Button variant="default" size="sm">
|
|
416
|
+
{t.discard}
|
|
417
|
+
</Button>
|
|
418
|
+
</AlertDialogAction>
|
|
419
|
+
</AlertDialogFooter>
|
|
420
|
+
</AlertDialogContent>
|
|
421
|
+
</AlertDialog>
|
|
422
|
+
</Portal>
|
|
423
|
+
</Field>
|
|
424
|
+
)
|
|
425
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Control, FieldPath, FieldValues } from 'react-hook-form'
|
|
2
|
+
|
|
3
|
+
export type JsonEditorMode = 'inline' | 'popover'
|
|
4
|
+
export type JsonEditorExpandMode = 'none' | 'callback' | 'sheet'
|
|
5
|
+
|
|
6
|
+
export type JsonEditorExpandPayload<TFieldValues extends FieldValues> = {
|
|
7
|
+
name: FieldPath<TFieldValues>
|
|
8
|
+
rawText: string
|
|
9
|
+
value: unknown
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type JsonTextareaFieldProps<TFieldValues extends FieldValues> = {
|
|
13
|
+
control: Control<TFieldValues>
|
|
14
|
+
name: FieldPath<TFieldValues>
|
|
15
|
+
label: string
|
|
16
|
+
description?: string
|
|
17
|
+
className?: string
|
|
18
|
+
actionsClassName?: string
|
|
19
|
+
|
|
20
|
+
isEditable?: boolean
|
|
21
|
+
allowArray?: boolean
|
|
22
|
+
|
|
23
|
+
mode?: JsonEditorMode
|
|
24
|
+
rows?: number
|
|
25
|
+
placeholder?: string
|
|
26
|
+
|
|
27
|
+
lineNumbers?: boolean
|
|
28
|
+
lineHeightPx?: number
|
|
29
|
+
|
|
30
|
+
dir?: 'ltr' | 'rtl'
|
|
31
|
+
lang?: string
|
|
32
|
+
|
|
33
|
+
expandMode?: JsonEditorExpandMode
|
|
34
|
+
sheetSide?: 'left' | 'right'
|
|
35
|
+
sheetTitle?: string
|
|
36
|
+
|
|
37
|
+
text?: JsonEditorText
|
|
38
|
+
|
|
39
|
+
onExpandEditor?: (payload: JsonEditorExpandPayload<TFieldValues>) => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type JsonEditorText = {
|
|
43
|
+
format?: string
|
|
44
|
+
cancel?: string
|
|
45
|
+
save?: string
|
|
46
|
+
full?: string
|
|
47
|
+
close?: string
|
|
48
|
+
keepEditing?: string
|
|
49
|
+
discard?: string
|
|
50
|
+
discardTitle?: string
|
|
51
|
+
discardDescription?: string
|
|
52
|
+
statusHint?: string
|
|
53
|
+
sheetStatusHint?: string
|
|
54
|
+
unsavedChanges?: string
|
|
55
|
+
saved?: string
|
|
56
|
+
nullPreview?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type JsonParseResult = { ok: true; value: unknown } | { ok: false; message: string }
|
|
60
|
+
|
|
61
|
+
export type JsonEditorViewProps = {
|
|
62
|
+
value: string
|
|
63
|
+
onChange: (value: string) => void
|
|
64
|
+
onScroll?: (scrollTop: number) => void
|
|
65
|
+
scrollTop: number
|
|
66
|
+
rows: number
|
|
67
|
+
placeholder: string
|
|
68
|
+
readOnly: boolean
|
|
69
|
+
lineNumbers: boolean
|
|
70
|
+
lineHeightPx: number
|
|
71
|
+
dir?: 'ltr' | 'rtl'
|
|
72
|
+
lang?: string
|
|
73
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type UseJsonEditorHotkeysOptions = {
|
|
77
|
+
enabled: boolean
|
|
78
|
+
onEscape: () => void
|
|
79
|
+
onSave: () => void
|
|
80
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '@gentleduck/libs/cn'
|
|
4
|
+
import { type Direction, useDirection } from '@gentleduck/primitives/direction'
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { Textarea } from '../textarea'
|
|
7
|
+
import type { JsonEditorViewProps } from './json-editor.types'
|
|
8
|
+
|
|
9
|
+
const LOCALE_NUMBERING_SYSTEMS: Record<string, string> = {
|
|
10
|
+
ar: 'arab',
|
|
11
|
+
bn: 'beng',
|
|
12
|
+
fa: 'arabext',
|
|
13
|
+
gu: 'gujr',
|
|
14
|
+
hi: 'deva',
|
|
15
|
+
km: 'khmr',
|
|
16
|
+
kn: 'knda',
|
|
17
|
+
lo: 'laoo',
|
|
18
|
+
ml: 'mlym',
|
|
19
|
+
mr: 'deva',
|
|
20
|
+
my: 'mymr',
|
|
21
|
+
ne: 'deva',
|
|
22
|
+
or: 'orya',
|
|
23
|
+
pa: 'guru',
|
|
24
|
+
ps: 'arabext',
|
|
25
|
+
ta: 'tamldec',
|
|
26
|
+
te: 'telu',
|
|
27
|
+
th: 'thai',
|
|
28
|
+
ur: 'arabext',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function JsonEditorView({
|
|
32
|
+
value,
|
|
33
|
+
onChange,
|
|
34
|
+
onScroll,
|
|
35
|
+
scrollTop,
|
|
36
|
+
rows,
|
|
37
|
+
placeholder,
|
|
38
|
+
readOnly,
|
|
39
|
+
lineNumbers,
|
|
40
|
+
lineHeightPx,
|
|
41
|
+
dir,
|
|
42
|
+
lang,
|
|
43
|
+
onKeyDown,
|
|
44
|
+
}: JsonEditorViewProps) {
|
|
45
|
+
const direction = useDirection(dir as Direction)
|
|
46
|
+
const lineCount = React.useMemo(() => {
|
|
47
|
+
const count = value ? value.split(/\r\n|\r|\n/).length : 1
|
|
48
|
+
return Math.max(1, count)
|
|
49
|
+
}, [value])
|
|
50
|
+
|
|
51
|
+
const numbers = React.useMemo(() => {
|
|
52
|
+
if (!lang) {
|
|
53
|
+
return Array.from({ length: lineCount }, (_, i) => String(i + 1)).join('\n')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Build formatter; if the locale already includes a -u-nu- extension, use as-is.
|
|
57
|
+
// Otherwise append the native numbering system for known locales so that
|
|
58
|
+
// environments defaulting to latn still produce locale-appropriate digits.
|
|
59
|
+
let localeTag = lang
|
|
60
|
+
if (!lang.includes('-u-') || !lang.includes('-nu-')) {
|
|
61
|
+
const [base = ''] = lang.toLowerCase().split('-')
|
|
62
|
+
const numberingSystem = LOCALE_NUMBERING_SYSTEMS[base]
|
|
63
|
+
if (numberingSystem) {
|
|
64
|
+
localeTag = `${lang}-u-nu-${numberingSystem}`
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const fmt = new Intl.NumberFormat(localeTag, { useGrouping: false })
|
|
69
|
+
return Array.from({ length: lineCount }, (_, i) => fmt.format(i + 1)).join('\n')
|
|
70
|
+
}, [lineCount, lang])
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="overflow-hidden rounded-md border bg-background" data-slot="json-editor-shell" dir={direction}>
|
|
74
|
+
<div className="relative" data-slot="json-editor-container">
|
|
75
|
+
{lineNumbers ? (
|
|
76
|
+
<div className="absolute inset-y-0 start-0 w-12 border-e bg-muted/30" data-slot="json-editor-gutter">
|
|
77
|
+
<pre
|
|
78
|
+
aria-hidden
|
|
79
|
+
className="select-none px-2 py-2 text-end font-mono text-muted-foreground text-xs"
|
|
80
|
+
data-slot="json-editor-line-numbers"
|
|
81
|
+
style={{
|
|
82
|
+
lineHeight: `${lineHeightPx}px`,
|
|
83
|
+
transform: `translateY(-${scrollTop}px)`,
|
|
84
|
+
}}>
|
|
85
|
+
{numbers}
|
|
86
|
+
</pre>
|
|
87
|
+
</div>
|
|
88
|
+
) : null}
|
|
89
|
+
|
|
90
|
+
<Textarea
|
|
91
|
+
className={cn(
|
|
92
|
+
'w-full resize-y bg-transparent px-3 py-2 font-mono text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-ring',
|
|
93
|
+
lineNumbers ? 'ps-14' : '',
|
|
94
|
+
)}
|
|
95
|
+
data-slot="json-editor-textarea"
|
|
96
|
+
dir="ltr"
|
|
97
|
+
onChange={(event) => onChange(event.currentTarget.value)}
|
|
98
|
+
onKeyDown={onKeyDown}
|
|
99
|
+
onScroll={(event) => onScroll?.(event.currentTarget.scrollTop)}
|
|
100
|
+
placeholder={placeholder}
|
|
101
|
+
readOnly={readOnly}
|
|
102
|
+
rows={rows}
|
|
103
|
+
spellCheck={false}
|
|
104
|
+
style={{ lineHeight: `${lineHeightPx}px` }}
|
|
105
|
+
value={value}
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
package/src/kbd/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './kbd'
|
package/src/kbd/kbd.tsx
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { cn } from '@gentleduck/libs/cn'
|
|
2
|
+
import { type Direction, useDirection } from '@gentleduck/primitives/direction'
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
|
|
5
|
+
const Kbd = React.forwardRef<HTMLElement, React.ComponentPropsWithoutRef<'kbd'>>(
|
|
6
|
+
({ className, dir, ...props }, ref) => {
|
|
7
|
+
const direction = useDirection(dir as Direction)
|
|
8
|
+
return (
|
|
9
|
+
<kbd
|
|
10
|
+
className={cn(
|
|
11
|
+
'pointer-events-none inline-flex h-5 w-fit min-w-5 select-none items-center justify-center gap-1 rounded-sm bg-muted px-1 font-medium font-sans text-muted-foreground text-xs',
|
|
12
|
+
"[&_svg:not([class*='size-'])]:size-3",
|
|
13
|
+
className,
|
|
14
|
+
)}
|
|
15
|
+
data-slot="kbd"
|
|
16
|
+
dir={direction}
|
|
17
|
+
ref={ref}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
},
|
|
22
|
+
)
|
|
23
|
+
Kbd.displayName = 'Kbd'
|
|
24
|
+
|
|
25
|
+
const KbdGroup = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
|
|
26
|
+
({ className, ...props }, ref) => {
|
|
27
|
+
return (
|
|
28
|
+
<kbd
|
|
29
|
+
className={cn('inline-flex items-center gap-1', className)}
|
|
30
|
+
data-slot="kbd-group"
|
|
31
|
+
ref={ref as React.Ref<HTMLElement>}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
KbdGroup.displayName = 'KbdGroup'
|
|
38
|
+
|
|
39
|
+
export { Kbd, KbdGroup }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './label'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '@gentleduck/libs/cn'
|
|
4
|
+
import { type Direction, useDirection } from '@gentleduck/primitives/direction'
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
|
|
7
|
+
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
|
|
8
|
+
|
|
9
|
+
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(({ className, htmlFor, dir, ...props }, ref) => {
|
|
10
|
+
const direction = useDirection(dir as Direction)
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<label
|
|
14
|
+
className={cn(
|
|
15
|
+
'text-balance font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
data-slot="label"
|
|
19
|
+
dir={direction}
|
|
20
|
+
htmlFor={htmlFor}
|
|
21
|
+
ref={ref}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
Label.displayName = 'Label'
|
|
27
|
+
|
|
28
|
+
export { Label }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './menubar'
|