@questpie/admin 0.0.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/.turbo/turbo-build.log +108 -0
- package/CHANGELOG.md +10 -0
- package/README.md +556 -0
- package/STATUS.md +917 -0
- package/VALIDATION.md +602 -0
- package/components.json +24 -0
- package/dist/__tests__/setup.mjs +38 -0
- package/dist/__tests__/test-utils.mjs +45 -0
- package/dist/__tests__/vitest.d.mjs +3 -0
- package/dist/components/admin-app.mjs +69 -0
- package/dist/components/fields/array-field.mjs +190 -0
- package/dist/components/fields/checkbox-field.mjs +34 -0
- package/dist/components/fields/custom-field.mjs +32 -0
- package/dist/components/fields/date-field.mjs +41 -0
- package/dist/components/fields/datetime-field.mjs +42 -0
- package/dist/components/fields/email-field.mjs +37 -0
- package/dist/components/fields/embedded-collection.mjs +253 -0
- package/dist/components/fields/field-types.mjs +1 -0
- package/dist/components/fields/field-utils.mjs +10 -0
- package/dist/components/fields/field-wrapper.mjs +34 -0
- package/dist/components/fields/index.mjs +23 -0
- package/dist/components/fields/json-field.mjs +243 -0
- package/dist/components/fields/locale-badge.mjs +16 -0
- package/dist/components/fields/number-field.mjs +39 -0
- package/dist/components/fields/password-field.mjs +37 -0
- package/dist/components/fields/relation-field.mjs +104 -0
- package/dist/components/fields/relation-picker.mjs +229 -0
- package/dist/components/fields/relation-select.mjs +188 -0
- package/dist/components/fields/rich-text-editor/index.mjs +897 -0
- package/dist/components/fields/select-field.mjs +41 -0
- package/dist/components/fields/switch-field.mjs +34 -0
- package/dist/components/fields/text-field.mjs +38 -0
- package/dist/components/fields/textarea-field.mjs +38 -0
- package/dist/components/index.mjs +59 -0
- package/dist/components/primitives/checkbox-input.mjs +127 -0
- package/dist/components/primitives/date-input.mjs +303 -0
- package/dist/components/primitives/index.mjs +12 -0
- package/dist/components/primitives/number-input.mjs +104 -0
- package/dist/components/primitives/select-input.mjs +177 -0
- package/dist/components/primitives/tag-input.mjs +135 -0
- package/dist/components/primitives/text-input.mjs +39 -0
- package/dist/components/primitives/textarea-input.mjs +37 -0
- package/dist/components/primitives/toggle-input.mjs +31 -0
- package/dist/components/primitives/types.mjs +12 -0
- package/dist/components/ui/accordion.mjs +55 -0
- package/dist/components/ui/avatar.mjs +54 -0
- package/dist/components/ui/badge.mjs +34 -0
- package/dist/components/ui/button.mjs +48 -0
- package/dist/components/ui/card.mjs +58 -0
- package/dist/components/ui/checkbox.mjs +21 -0
- package/dist/components/ui/combobox.mjs +163 -0
- package/dist/components/ui/dialog.mjs +95 -0
- package/dist/components/ui/dropdown-menu.mjs +138 -0
- package/dist/components/ui/field.mjs +113 -0
- package/dist/components/ui/input-group.mjs +82 -0
- package/dist/components/ui/input.mjs +17 -0
- package/dist/components/ui/label.mjs +15 -0
- package/dist/components/ui/popover.mjs +56 -0
- package/dist/components/ui/scroll-area.mjs +38 -0
- package/dist/components/ui/select.mjs +100 -0
- package/dist/components/ui/separator.mjs +16 -0
- package/dist/components/ui/sheet.mjs +90 -0
- package/dist/components/ui/sidebar.mjs +387 -0
- package/dist/components/ui/skeleton.mjs +14 -0
- package/dist/components/ui/spinner.mjs +16 -0
- package/dist/components/ui/switch.mjs +22 -0
- package/dist/components/ui/table.mjs +68 -0
- package/dist/components/ui/tabs.mjs +48 -0
- package/dist/components/ui/textarea.mjs +15 -0
- package/dist/components/ui/tooltip.mjs +44 -0
- package/dist/config/component-registry.mjs +38 -0
- package/dist/config/index.mjs +129 -0
- package/dist/hooks/admin-provider.mjs +70 -0
- package/dist/hooks/index.mjs +7 -0
- package/dist/hooks/store.mjs +178 -0
- package/dist/hooks/use-auth.mjs +76 -0
- package/dist/hooks/use-collection-db.mjs +146 -0
- package/dist/hooks/use-collection.mjs +112 -0
- package/dist/hooks/use-global.mjs +46 -0
- package/dist/hooks/use-mobile.mjs +20 -0
- package/dist/lib/utils.mjs +10 -0
- package/dist/styles/index.css +336 -0
- package/dist/styles/index.mjs +1 -0
- package/dist/utils/index.mjs +9 -0
- package/dist/views/auth/auth-layout.mjs +52 -0
- package/dist/views/auth/forgot-password-form.mjs +148 -0
- package/dist/views/auth/index.mjs +6 -0
- package/dist/views/auth/login-form.mjs +156 -0
- package/dist/views/auth/reset-password-form.mjs +184 -0
- package/dist/views/collection/auto-form-fields.mjs +525 -0
- package/dist/views/collection/collection-form.mjs +91 -0
- package/dist/views/collection/collection-list.mjs +76 -0
- package/dist/views/collection/form-field.mjs +42 -0
- package/dist/views/collection/index.mjs +6 -0
- package/dist/views/common/index.mjs +4 -0
- package/dist/views/common/locale-switcher.mjs +39 -0
- package/dist/views/common/version-history.mjs +272 -0
- package/dist/views/index.mjs +9 -0
- package/dist/views/layout/admin-layout.mjs +40 -0
- package/dist/views/layout/admin-router.mjs +95 -0
- package/dist/views/layout/admin-sidebar.mjs +63 -0
- package/dist/views/layout/index.mjs +5 -0
- package/package.json +276 -0
- package/src/__tests__/setup.ts +44 -0
- package/src/__tests__/test-utils.tsx +49 -0
- package/src/__tests__/vitest.d.ts +9 -0
- package/src/components/admin-app.tsx +221 -0
- package/src/components/fields/array-field.tsx +237 -0
- package/src/components/fields/checkbox-field.tsx +47 -0
- package/src/components/fields/custom-field.tsx +50 -0
- package/src/components/fields/date-field.tsx +65 -0
- package/src/components/fields/datetime-field.tsx +67 -0
- package/src/components/fields/email-field.tsx +51 -0
- package/src/components/fields/embedded-collection.tsx +315 -0
- package/src/components/fields/field-types.ts +162 -0
- package/src/components/fields/field-utils.ts +6 -0
- package/src/components/fields/field-wrapper.tsx +52 -0
- package/src/components/fields/index.ts +66 -0
- package/src/components/fields/json-field.tsx +440 -0
- package/src/components/fields/locale-badge.tsx +15 -0
- package/src/components/fields/number-field.tsx +57 -0
- package/src/components/fields/password-field.tsx +51 -0
- package/src/components/fields/relation-field.tsx +243 -0
- package/src/components/fields/relation-picker.tsx +402 -0
- package/src/components/fields/relation-select.tsx +327 -0
- package/src/components/fields/rich-text-editor/index.tsx +1337 -0
- package/src/components/fields/select-field.tsx +61 -0
- package/src/components/fields/switch-field.tsx +47 -0
- package/src/components/fields/text-field.tsx +55 -0
- package/src/components/fields/textarea-field.tsx +55 -0
- package/src/components/index.ts +40 -0
- package/src/components/primitives/checkbox-input.tsx +193 -0
- package/src/components/primitives/date-input.tsx +401 -0
- package/src/components/primitives/index.ts +24 -0
- package/src/components/primitives/number-input.tsx +132 -0
- package/src/components/primitives/select-input.tsx +296 -0
- package/src/components/primitives/tag-input.tsx +200 -0
- package/src/components/primitives/text-input.tsx +49 -0
- package/src/components/primitives/textarea-input.tsx +46 -0
- package/src/components/primitives/toggle-input.tsx +36 -0
- package/src/components/primitives/types.ts +235 -0
- package/src/components/ui/accordion.tsx +72 -0
- package/src/components/ui/avatar.tsx +106 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +53 -0
- package/src/components/ui/card.tsx +94 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/combobox.tsx +290 -0
- package/src/components/ui/dialog.tsx +151 -0
- package/src/components/ui/dropdown-menu.tsx +254 -0
- package/src/components/ui/field.tsx +227 -0
- package/src/components/ui/input-group.tsx +149 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +18 -0
- package/src/components/ui/popover.tsx +88 -0
- package/src/components/ui/scroll-area.tsx +53 -0
- package/src/components/ui/select.tsx +192 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +127 -0
- package/src/components/ui/sidebar.tsx +723 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/spinner.tsx +10 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +99 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +70 -0
- package/src/config/component-registry.ts +190 -0
- package/src/config/index.ts +1099 -0
- package/src/hooks/README.md +269 -0
- package/src/hooks/admin-provider.tsx +110 -0
- package/src/hooks/index.ts +41 -0
- package/src/hooks/store.ts +248 -0
- package/src/hooks/use-auth.ts +168 -0
- package/src/hooks/use-collection-db.ts +209 -0
- package/src/hooks/use-collection.ts +156 -0
- package/src/hooks/use-global.ts +69 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/index.css +340 -0
- package/src/utils/index.ts +6 -0
- package/src/views/auth/auth-layout.tsx +77 -0
- package/src/views/auth/forgot-password-form.tsx +192 -0
- package/src/views/auth/index.ts +21 -0
- package/src/views/auth/login-form.tsx +229 -0
- package/src/views/auth/reset-password-form.tsx +232 -0
- package/src/views/collection/auto-form-fields.tsx +982 -0
- package/src/views/collection/collection-form.tsx +186 -0
- package/src/views/collection/collection-list.tsx +223 -0
- package/src/views/collection/form-field.tsx +52 -0
- package/src/views/collection/index.ts +15 -0
- package/src/views/common/index.ts +8 -0
- package/src/views/common/locale-switcher.tsx +45 -0
- package/src/views/common/version-history.tsx +406 -0
- package/src/views/index.ts +25 -0
- package/src/views/layout/admin-layout.tsx +117 -0
- package/src/views/layout/admin-router.tsx +206 -0
- package/src/views/layout/admin-sidebar.tsx +185 -0
- package/src/views/layout/index.ts +12 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.ts +13 -0
- package/vitest.config.ts +29 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JsonField Component
|
|
3
|
+
*
|
|
4
|
+
* JSON field with two editing modes:
|
|
5
|
+
* - "code" - Raw JSON editing with syntax highlighting
|
|
6
|
+
* - "form" - Structured form editing (if schema provided)
|
|
7
|
+
*
|
|
8
|
+
* Integrates with react-hook-form via Controller.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import { Controller, useFormContext, type Control } from "react-hook-form";
|
|
13
|
+
import { Code, ListBullets, WarningCircle } from "@phosphor-icons/react";
|
|
14
|
+
import { Button } from "../ui/button";
|
|
15
|
+
import { Textarea } from "../ui/textarea";
|
|
16
|
+
import {
|
|
17
|
+
Field,
|
|
18
|
+
FieldContent,
|
|
19
|
+
FieldDescription,
|
|
20
|
+
FieldError,
|
|
21
|
+
FieldLabel,
|
|
22
|
+
} from "../ui/field";
|
|
23
|
+
import { cn } from "../../lib/utils";
|
|
24
|
+
|
|
25
|
+
export type JsonFieldMode = "code" | "form";
|
|
26
|
+
|
|
27
|
+
export type JsonFieldProps = {
|
|
28
|
+
/**
|
|
29
|
+
* Field name (for react-hook-form)
|
|
30
|
+
*/
|
|
31
|
+
name: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Label for the field
|
|
35
|
+
*/
|
|
36
|
+
label?: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Description/help text
|
|
40
|
+
*/
|
|
41
|
+
description?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Is the field required
|
|
45
|
+
*/
|
|
46
|
+
required?: boolean;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Is the field disabled
|
|
50
|
+
*/
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Is the field readonly
|
|
55
|
+
*/
|
|
56
|
+
readOnly?: boolean;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Placeholder text for code mode
|
|
60
|
+
*/
|
|
61
|
+
placeholder?: string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Initial editing mode
|
|
65
|
+
* @default "code"
|
|
66
|
+
*/
|
|
67
|
+
defaultMode?: JsonFieldMode;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Allow switching between modes
|
|
71
|
+
* @default true
|
|
72
|
+
*/
|
|
73
|
+
allowModeSwitch?: boolean;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Minimum height for the editor
|
|
77
|
+
* @default 200
|
|
78
|
+
*/
|
|
79
|
+
minHeight?: number;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Maximum height for the editor (0 = no limit)
|
|
83
|
+
* @default 400
|
|
84
|
+
*/
|
|
85
|
+
maxHeight?: number;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Custom render function for form mode
|
|
89
|
+
* Receives current value and onChange handler
|
|
90
|
+
*/
|
|
91
|
+
renderForm?: (props: {
|
|
92
|
+
value: any;
|
|
93
|
+
onChange: (value: any) => void;
|
|
94
|
+
disabled?: boolean;
|
|
95
|
+
readOnly?: boolean;
|
|
96
|
+
}) => React.ReactNode;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Form control (optional, will use useFormContext if not provided)
|
|
100
|
+
*/
|
|
101
|
+
control?: Control<any>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Additional class name
|
|
105
|
+
*/
|
|
106
|
+
className?: string;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* JSON field with code editor and optional form mode.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```tsx
|
|
114
|
+
* // Basic JSON editor
|
|
115
|
+
* <JsonField
|
|
116
|
+
* name="metadata"
|
|
117
|
+
* label="Metadata"
|
|
118
|
+
* description="Additional metadata as JSON"
|
|
119
|
+
* />
|
|
120
|
+
*
|
|
121
|
+
* // With custom form mode
|
|
122
|
+
* <JsonField
|
|
123
|
+
* name="settings"
|
|
124
|
+
* label="Settings"
|
|
125
|
+
* renderForm={({ value, onChange }) => (
|
|
126
|
+
* <SettingsForm value={value} onChange={onChange} />
|
|
127
|
+
* )}
|
|
128
|
+
* />
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function JsonField({
|
|
132
|
+
name,
|
|
133
|
+
label,
|
|
134
|
+
description,
|
|
135
|
+
required,
|
|
136
|
+
disabled,
|
|
137
|
+
readOnly,
|
|
138
|
+
placeholder = '{\n "key": "value"\n}',
|
|
139
|
+
defaultMode = "code",
|
|
140
|
+
allowModeSwitch = true,
|
|
141
|
+
minHeight = 200,
|
|
142
|
+
maxHeight = 400,
|
|
143
|
+
renderForm,
|
|
144
|
+
control: controlProp,
|
|
145
|
+
className,
|
|
146
|
+
}: JsonFieldProps) {
|
|
147
|
+
const formContext = useFormContext();
|
|
148
|
+
const control = controlProp ?? formContext?.control;
|
|
149
|
+
const [mode, setMode] = React.useState<JsonFieldMode>(defaultMode);
|
|
150
|
+
|
|
151
|
+
if (!control) {
|
|
152
|
+
console.warn(
|
|
153
|
+
"JsonField: No form control found. Make sure to use within FormProvider or pass control prop.",
|
|
154
|
+
);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Only show mode switch if form rendering is available
|
|
159
|
+
const showModeSwitch = allowModeSwitch && renderForm;
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<Controller
|
|
163
|
+
name={name}
|
|
164
|
+
control={control}
|
|
165
|
+
rules={{
|
|
166
|
+
required: required ? `${label || name} is required` : undefined,
|
|
167
|
+
validate: (value) => {
|
|
168
|
+
if (!value) return true;
|
|
169
|
+
// In code mode, validate JSON
|
|
170
|
+
if (mode === "code" && typeof value === "string") {
|
|
171
|
+
try {
|
|
172
|
+
JSON.parse(value);
|
|
173
|
+
return true;
|
|
174
|
+
} catch {
|
|
175
|
+
return "Invalid JSON format";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
},
|
|
180
|
+
}}
|
|
181
|
+
render={({ field, fieldState }) => {
|
|
182
|
+
const error = fieldState.error?.message;
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<Field data-invalid={!!error} className={className}>
|
|
186
|
+
{/* Header with label and mode switch */}
|
|
187
|
+
<div className="flex items-center justify-between">
|
|
188
|
+
{label && (
|
|
189
|
+
<FieldLabel htmlFor={name}>
|
|
190
|
+
{label}
|
|
191
|
+
{required && <span className="text-destructive ml-1">*</span>}
|
|
192
|
+
</FieldLabel>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
{showModeSwitch && (
|
|
196
|
+
<div className="flex gap-1">
|
|
197
|
+
<Button
|
|
198
|
+
type="button"
|
|
199
|
+
variant={mode === "code" ? "secondary" : "ghost"}
|
|
200
|
+
size="icon-xs"
|
|
201
|
+
onClick={() => setMode("code")}
|
|
202
|
+
disabled={disabled}
|
|
203
|
+
title="Code editor"
|
|
204
|
+
>
|
|
205
|
+
<Code weight="bold" />
|
|
206
|
+
</Button>
|
|
207
|
+
<Button
|
|
208
|
+
type="button"
|
|
209
|
+
variant={mode === "form" ? "secondary" : "ghost"}
|
|
210
|
+
size="icon-xs"
|
|
211
|
+
onClick={() => setMode("form")}
|
|
212
|
+
disabled={disabled}
|
|
213
|
+
title="Form editor"
|
|
214
|
+
>
|
|
215
|
+
<ListBullets weight="bold" />
|
|
216
|
+
</Button>
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<FieldContent>
|
|
222
|
+
{mode === "code" ? (
|
|
223
|
+
<JsonCodeEditor
|
|
224
|
+
value={field.value}
|
|
225
|
+
onChange={field.onChange}
|
|
226
|
+
disabled={disabled}
|
|
227
|
+
readOnly={readOnly}
|
|
228
|
+
placeholder={placeholder}
|
|
229
|
+
minHeight={minHeight}
|
|
230
|
+
maxHeight={maxHeight}
|
|
231
|
+
error={!!error}
|
|
232
|
+
/>
|
|
233
|
+
) : renderForm ? (
|
|
234
|
+
<JsonFormEditor
|
|
235
|
+
value={field.value}
|
|
236
|
+
onChange={field.onChange}
|
|
237
|
+
disabled={disabled}
|
|
238
|
+
readOnly={readOnly}
|
|
239
|
+
renderForm={renderForm}
|
|
240
|
+
/>
|
|
241
|
+
) : (
|
|
242
|
+
<JsonCodeEditor
|
|
243
|
+
value={field.value}
|
|
244
|
+
onChange={field.onChange}
|
|
245
|
+
disabled={disabled}
|
|
246
|
+
readOnly={readOnly}
|
|
247
|
+
placeholder={placeholder}
|
|
248
|
+
minHeight={minHeight}
|
|
249
|
+
maxHeight={maxHeight}
|
|
250
|
+
error={!!error}
|
|
251
|
+
/>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{description && !error && (
|
|
255
|
+
<FieldDescription>{description}</FieldDescription>
|
|
256
|
+
)}
|
|
257
|
+
<FieldError>{error}</FieldError>
|
|
258
|
+
</FieldContent>
|
|
259
|
+
</Field>
|
|
260
|
+
);
|
|
261
|
+
}}
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Code editor for JSON (using Textarea for simplicity)
|
|
268
|
+
* Can be replaced with Monaco/CodeMirror in the future
|
|
269
|
+
*/
|
|
270
|
+
function JsonCodeEditor({
|
|
271
|
+
value,
|
|
272
|
+
onChange,
|
|
273
|
+
disabled,
|
|
274
|
+
readOnly,
|
|
275
|
+
placeholder,
|
|
276
|
+
minHeight,
|
|
277
|
+
maxHeight,
|
|
278
|
+
error,
|
|
279
|
+
}: {
|
|
280
|
+
value: any;
|
|
281
|
+
onChange: (value: any) => void;
|
|
282
|
+
disabled?: boolean;
|
|
283
|
+
readOnly?: boolean;
|
|
284
|
+
placeholder?: string;
|
|
285
|
+
minHeight?: number;
|
|
286
|
+
maxHeight?: number;
|
|
287
|
+
error?: boolean;
|
|
288
|
+
}) {
|
|
289
|
+
const [localValue, setLocalValue] = React.useState<string>(() => {
|
|
290
|
+
if (typeof value === "string") return value;
|
|
291
|
+
if (value === null || value === undefined) return "";
|
|
292
|
+
try {
|
|
293
|
+
return JSON.stringify(value, null, 2);
|
|
294
|
+
} catch {
|
|
295
|
+
return "";
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
const [parseError, setParseError] = React.useState<string | null>(null);
|
|
299
|
+
|
|
300
|
+
// Sync local value when external value changes
|
|
301
|
+
React.useEffect(() => {
|
|
302
|
+
if (typeof value === "string") {
|
|
303
|
+
setLocalValue(value);
|
|
304
|
+
} else if (value !== null && value !== undefined) {
|
|
305
|
+
try {
|
|
306
|
+
setLocalValue(JSON.stringify(value, null, 2));
|
|
307
|
+
} catch {
|
|
308
|
+
// Keep current local value if stringification fails
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
setLocalValue("");
|
|
312
|
+
}
|
|
313
|
+
}, [value]);
|
|
314
|
+
|
|
315
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
316
|
+
const newValue = e.target.value;
|
|
317
|
+
setLocalValue(newValue);
|
|
318
|
+
|
|
319
|
+
// Try to parse and update form value
|
|
320
|
+
if (!newValue.trim()) {
|
|
321
|
+
setParseError(null);
|
|
322
|
+
onChange(null);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const parsed = JSON.parse(newValue);
|
|
328
|
+
setParseError(null);
|
|
329
|
+
onChange(parsed);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
setParseError("Invalid JSON");
|
|
332
|
+
// Still update with raw string so validation can catch it
|
|
333
|
+
onChange(newValue);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const handleFormat = () => {
|
|
338
|
+
try {
|
|
339
|
+
const parsed = JSON.parse(localValue);
|
|
340
|
+
const formatted = JSON.stringify(parsed, null, 2);
|
|
341
|
+
setLocalValue(formatted);
|
|
342
|
+
setParseError(null);
|
|
343
|
+
onChange(parsed);
|
|
344
|
+
} catch {
|
|
345
|
+
// Can't format invalid JSON
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
return (
|
|
350
|
+
<div className="space-y-2">
|
|
351
|
+
<div className="relative">
|
|
352
|
+
<Textarea
|
|
353
|
+
value={localValue}
|
|
354
|
+
onChange={handleChange}
|
|
355
|
+
disabled={disabled}
|
|
356
|
+
readOnly={readOnly}
|
|
357
|
+
placeholder={placeholder}
|
|
358
|
+
className={cn(
|
|
359
|
+
"font-mono text-xs",
|
|
360
|
+
error || parseError ? "border-destructive" : "",
|
|
361
|
+
)}
|
|
362
|
+
style={{
|
|
363
|
+
minHeight: `${minHeight}px`,
|
|
364
|
+
maxHeight: maxHeight ? `${maxHeight}px` : undefined,
|
|
365
|
+
resize: maxHeight ? "none" : "vertical",
|
|
366
|
+
}}
|
|
367
|
+
aria-invalid={!!error || !!parseError}
|
|
368
|
+
/>
|
|
369
|
+
|
|
370
|
+
{/* Parse error indicator */}
|
|
371
|
+
{parseError && (
|
|
372
|
+
<div className="text-destructive absolute right-2 top-2 flex items-center gap-1 text-xs">
|
|
373
|
+
<WarningCircle weight="fill" className="size-3" />
|
|
374
|
+
{parseError}
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
{/* Format button */}
|
|
380
|
+
{!readOnly && !disabled && localValue && (
|
|
381
|
+
<div className="flex justify-end">
|
|
382
|
+
<Button
|
|
383
|
+
type="button"
|
|
384
|
+
variant="ghost"
|
|
385
|
+
size="xs"
|
|
386
|
+
onClick={handleFormat}
|
|
387
|
+
disabled={!!parseError}
|
|
388
|
+
>
|
|
389
|
+
Format JSON
|
|
390
|
+
</Button>
|
|
391
|
+
</div>
|
|
392
|
+
)}
|
|
393
|
+
</div>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Form-based editor wrapper
|
|
399
|
+
*/
|
|
400
|
+
function JsonFormEditor({
|
|
401
|
+
value,
|
|
402
|
+
onChange,
|
|
403
|
+
disabled,
|
|
404
|
+
readOnly,
|
|
405
|
+
renderForm,
|
|
406
|
+
}: {
|
|
407
|
+
value: any;
|
|
408
|
+
onChange: (value: any) => void;
|
|
409
|
+
disabled?: boolean;
|
|
410
|
+
readOnly?: boolean;
|
|
411
|
+
renderForm: JsonFieldProps["renderForm"];
|
|
412
|
+
}) {
|
|
413
|
+
// Ensure value is an object for form mode
|
|
414
|
+
const safeValue = React.useMemo(() => {
|
|
415
|
+
if (typeof value === "object" && value !== null) {
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
if (typeof value === "string") {
|
|
419
|
+
try {
|
|
420
|
+
return JSON.parse(value);
|
|
421
|
+
} catch {
|
|
422
|
+
return {};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return {};
|
|
426
|
+
}, [value]);
|
|
427
|
+
|
|
428
|
+
if (!renderForm) return null;
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
<div className="rounded-lg border p-4">
|
|
432
|
+
{renderForm({
|
|
433
|
+
value: safeValue,
|
|
434
|
+
onChange,
|
|
435
|
+
disabled,
|
|
436
|
+
readOnly,
|
|
437
|
+
})}
|
|
438
|
+
</div>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Badge } from "../ui/badge";
|
|
3
|
+
|
|
4
|
+
type LocaleBadgeProps = {
|
|
5
|
+
locale?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function LocaleBadge({ locale }: LocaleBadgeProps) {
|
|
9
|
+
if (!locale) return null;
|
|
10
|
+
return (
|
|
11
|
+
<Badge variant="secondary" className="uppercase text-[10px] tracking-wide">
|
|
12
|
+
{locale}
|
|
13
|
+
</Badge>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Controller } from "react-hook-form";
|
|
2
|
+
import { NumberInput } from "../primitives/number-input";
|
|
3
|
+
import { FieldWrapper } from "./field-wrapper";
|
|
4
|
+
import { useResolvedControl } from "./field-utils";
|
|
5
|
+
import type { NumberFieldProps } from "./field-types";
|
|
6
|
+
|
|
7
|
+
export function NumberField({
|
|
8
|
+
name,
|
|
9
|
+
label,
|
|
10
|
+
description,
|
|
11
|
+
placeholder,
|
|
12
|
+
required,
|
|
13
|
+
disabled,
|
|
14
|
+
localized,
|
|
15
|
+
locale,
|
|
16
|
+
control,
|
|
17
|
+
className,
|
|
18
|
+
min,
|
|
19
|
+
max,
|
|
20
|
+
step,
|
|
21
|
+
showButtons,
|
|
22
|
+
}: NumberFieldProps) {
|
|
23
|
+
const resolvedControl = useResolvedControl(control);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Controller
|
|
27
|
+
name={name}
|
|
28
|
+
control={resolvedControl}
|
|
29
|
+
render={({ field, fieldState }) => (
|
|
30
|
+
<FieldWrapper
|
|
31
|
+
name={name}
|
|
32
|
+
label={label}
|
|
33
|
+
description={description}
|
|
34
|
+
required={required}
|
|
35
|
+
disabled={disabled}
|
|
36
|
+
localized={localized}
|
|
37
|
+
locale={locale}
|
|
38
|
+
error={fieldState.error?.message}
|
|
39
|
+
>
|
|
40
|
+
<NumberInput
|
|
41
|
+
id={name}
|
|
42
|
+
value={field.value ?? null}
|
|
43
|
+
onChange={field.onChange}
|
|
44
|
+
placeholder={placeholder}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
min={min}
|
|
47
|
+
max={max}
|
|
48
|
+
step={step}
|
|
49
|
+
showButtons={showButtons}
|
|
50
|
+
aria-invalid={!!fieldState.error}
|
|
51
|
+
className={className}
|
|
52
|
+
/>
|
|
53
|
+
</FieldWrapper>
|
|
54
|
+
)}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Controller } from "react-hook-form";
|
|
2
|
+
import { TextInput } from "../primitives/text-input";
|
|
3
|
+
import { FieldWrapper } from "./field-wrapper";
|
|
4
|
+
import { useResolvedControl } from "./field-utils";
|
|
5
|
+
import type { BaseFieldProps } from "./field-types";
|
|
6
|
+
|
|
7
|
+
export function PasswordField({
|
|
8
|
+
name,
|
|
9
|
+
label,
|
|
10
|
+
description,
|
|
11
|
+
placeholder,
|
|
12
|
+
required,
|
|
13
|
+
disabled,
|
|
14
|
+
localized,
|
|
15
|
+
locale,
|
|
16
|
+
control,
|
|
17
|
+
className,
|
|
18
|
+
}: BaseFieldProps) {
|
|
19
|
+
const resolvedControl = useResolvedControl(control);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Controller
|
|
23
|
+
name={name}
|
|
24
|
+
control={resolvedControl}
|
|
25
|
+
render={({ field, fieldState }) => (
|
|
26
|
+
<FieldWrapper
|
|
27
|
+
name={name}
|
|
28
|
+
label={label}
|
|
29
|
+
description={description}
|
|
30
|
+
required={required}
|
|
31
|
+
disabled={disabled}
|
|
32
|
+
localized={localized}
|
|
33
|
+
locale={locale}
|
|
34
|
+
error={fieldState.error?.message}
|
|
35
|
+
>
|
|
36
|
+
<TextInput
|
|
37
|
+
id={name}
|
|
38
|
+
value={field.value ?? ""}
|
|
39
|
+
onChange={field.onChange}
|
|
40
|
+
type="password"
|
|
41
|
+
placeholder={placeholder}
|
|
42
|
+
disabled={disabled}
|
|
43
|
+
autoComplete="current-password"
|
|
44
|
+
aria-invalid={!!fieldState.error}
|
|
45
|
+
className={className}
|
|
46
|
+
/>
|
|
47
|
+
</FieldWrapper>
|
|
48
|
+
)}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|