@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.
Files changed (203) hide show
  1. package/.turbo/turbo-build.log +108 -0
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +556 -0
  4. package/STATUS.md +917 -0
  5. package/VALIDATION.md +602 -0
  6. package/components.json +24 -0
  7. package/dist/__tests__/setup.mjs +38 -0
  8. package/dist/__tests__/test-utils.mjs +45 -0
  9. package/dist/__tests__/vitest.d.mjs +3 -0
  10. package/dist/components/admin-app.mjs +69 -0
  11. package/dist/components/fields/array-field.mjs +190 -0
  12. package/dist/components/fields/checkbox-field.mjs +34 -0
  13. package/dist/components/fields/custom-field.mjs +32 -0
  14. package/dist/components/fields/date-field.mjs +41 -0
  15. package/dist/components/fields/datetime-field.mjs +42 -0
  16. package/dist/components/fields/email-field.mjs +37 -0
  17. package/dist/components/fields/embedded-collection.mjs +253 -0
  18. package/dist/components/fields/field-types.mjs +1 -0
  19. package/dist/components/fields/field-utils.mjs +10 -0
  20. package/dist/components/fields/field-wrapper.mjs +34 -0
  21. package/dist/components/fields/index.mjs +23 -0
  22. package/dist/components/fields/json-field.mjs +243 -0
  23. package/dist/components/fields/locale-badge.mjs +16 -0
  24. package/dist/components/fields/number-field.mjs +39 -0
  25. package/dist/components/fields/password-field.mjs +37 -0
  26. package/dist/components/fields/relation-field.mjs +104 -0
  27. package/dist/components/fields/relation-picker.mjs +229 -0
  28. package/dist/components/fields/relation-select.mjs +188 -0
  29. package/dist/components/fields/rich-text-editor/index.mjs +897 -0
  30. package/dist/components/fields/select-field.mjs +41 -0
  31. package/dist/components/fields/switch-field.mjs +34 -0
  32. package/dist/components/fields/text-field.mjs +38 -0
  33. package/dist/components/fields/textarea-field.mjs +38 -0
  34. package/dist/components/index.mjs +59 -0
  35. package/dist/components/primitives/checkbox-input.mjs +127 -0
  36. package/dist/components/primitives/date-input.mjs +303 -0
  37. package/dist/components/primitives/index.mjs +12 -0
  38. package/dist/components/primitives/number-input.mjs +104 -0
  39. package/dist/components/primitives/select-input.mjs +177 -0
  40. package/dist/components/primitives/tag-input.mjs +135 -0
  41. package/dist/components/primitives/text-input.mjs +39 -0
  42. package/dist/components/primitives/textarea-input.mjs +37 -0
  43. package/dist/components/primitives/toggle-input.mjs +31 -0
  44. package/dist/components/primitives/types.mjs +12 -0
  45. package/dist/components/ui/accordion.mjs +55 -0
  46. package/dist/components/ui/avatar.mjs +54 -0
  47. package/dist/components/ui/badge.mjs +34 -0
  48. package/dist/components/ui/button.mjs +48 -0
  49. package/dist/components/ui/card.mjs +58 -0
  50. package/dist/components/ui/checkbox.mjs +21 -0
  51. package/dist/components/ui/combobox.mjs +163 -0
  52. package/dist/components/ui/dialog.mjs +95 -0
  53. package/dist/components/ui/dropdown-menu.mjs +138 -0
  54. package/dist/components/ui/field.mjs +113 -0
  55. package/dist/components/ui/input-group.mjs +82 -0
  56. package/dist/components/ui/input.mjs +17 -0
  57. package/dist/components/ui/label.mjs +15 -0
  58. package/dist/components/ui/popover.mjs +56 -0
  59. package/dist/components/ui/scroll-area.mjs +38 -0
  60. package/dist/components/ui/select.mjs +100 -0
  61. package/dist/components/ui/separator.mjs +16 -0
  62. package/dist/components/ui/sheet.mjs +90 -0
  63. package/dist/components/ui/sidebar.mjs +387 -0
  64. package/dist/components/ui/skeleton.mjs +14 -0
  65. package/dist/components/ui/spinner.mjs +16 -0
  66. package/dist/components/ui/switch.mjs +22 -0
  67. package/dist/components/ui/table.mjs +68 -0
  68. package/dist/components/ui/tabs.mjs +48 -0
  69. package/dist/components/ui/textarea.mjs +15 -0
  70. package/dist/components/ui/tooltip.mjs +44 -0
  71. package/dist/config/component-registry.mjs +38 -0
  72. package/dist/config/index.mjs +129 -0
  73. package/dist/hooks/admin-provider.mjs +70 -0
  74. package/dist/hooks/index.mjs +7 -0
  75. package/dist/hooks/store.mjs +178 -0
  76. package/dist/hooks/use-auth.mjs +76 -0
  77. package/dist/hooks/use-collection-db.mjs +146 -0
  78. package/dist/hooks/use-collection.mjs +112 -0
  79. package/dist/hooks/use-global.mjs +46 -0
  80. package/dist/hooks/use-mobile.mjs +20 -0
  81. package/dist/lib/utils.mjs +10 -0
  82. package/dist/styles/index.css +336 -0
  83. package/dist/styles/index.mjs +1 -0
  84. package/dist/utils/index.mjs +9 -0
  85. package/dist/views/auth/auth-layout.mjs +52 -0
  86. package/dist/views/auth/forgot-password-form.mjs +148 -0
  87. package/dist/views/auth/index.mjs +6 -0
  88. package/dist/views/auth/login-form.mjs +156 -0
  89. package/dist/views/auth/reset-password-form.mjs +184 -0
  90. package/dist/views/collection/auto-form-fields.mjs +525 -0
  91. package/dist/views/collection/collection-form.mjs +91 -0
  92. package/dist/views/collection/collection-list.mjs +76 -0
  93. package/dist/views/collection/form-field.mjs +42 -0
  94. package/dist/views/collection/index.mjs +6 -0
  95. package/dist/views/common/index.mjs +4 -0
  96. package/dist/views/common/locale-switcher.mjs +39 -0
  97. package/dist/views/common/version-history.mjs +272 -0
  98. package/dist/views/index.mjs +9 -0
  99. package/dist/views/layout/admin-layout.mjs +40 -0
  100. package/dist/views/layout/admin-router.mjs +95 -0
  101. package/dist/views/layout/admin-sidebar.mjs +63 -0
  102. package/dist/views/layout/index.mjs +5 -0
  103. package/package.json +276 -0
  104. package/src/__tests__/setup.ts +44 -0
  105. package/src/__tests__/test-utils.tsx +49 -0
  106. package/src/__tests__/vitest.d.ts +9 -0
  107. package/src/components/admin-app.tsx +221 -0
  108. package/src/components/fields/array-field.tsx +237 -0
  109. package/src/components/fields/checkbox-field.tsx +47 -0
  110. package/src/components/fields/custom-field.tsx +50 -0
  111. package/src/components/fields/date-field.tsx +65 -0
  112. package/src/components/fields/datetime-field.tsx +67 -0
  113. package/src/components/fields/email-field.tsx +51 -0
  114. package/src/components/fields/embedded-collection.tsx +315 -0
  115. package/src/components/fields/field-types.ts +162 -0
  116. package/src/components/fields/field-utils.ts +6 -0
  117. package/src/components/fields/field-wrapper.tsx +52 -0
  118. package/src/components/fields/index.ts +66 -0
  119. package/src/components/fields/json-field.tsx +440 -0
  120. package/src/components/fields/locale-badge.tsx +15 -0
  121. package/src/components/fields/number-field.tsx +57 -0
  122. package/src/components/fields/password-field.tsx +51 -0
  123. package/src/components/fields/relation-field.tsx +243 -0
  124. package/src/components/fields/relation-picker.tsx +402 -0
  125. package/src/components/fields/relation-select.tsx +327 -0
  126. package/src/components/fields/rich-text-editor/index.tsx +1337 -0
  127. package/src/components/fields/select-field.tsx +61 -0
  128. package/src/components/fields/switch-field.tsx +47 -0
  129. package/src/components/fields/text-field.tsx +55 -0
  130. package/src/components/fields/textarea-field.tsx +55 -0
  131. package/src/components/index.ts +40 -0
  132. package/src/components/primitives/checkbox-input.tsx +193 -0
  133. package/src/components/primitives/date-input.tsx +401 -0
  134. package/src/components/primitives/index.ts +24 -0
  135. package/src/components/primitives/number-input.tsx +132 -0
  136. package/src/components/primitives/select-input.tsx +296 -0
  137. package/src/components/primitives/tag-input.tsx +200 -0
  138. package/src/components/primitives/text-input.tsx +49 -0
  139. package/src/components/primitives/textarea-input.tsx +46 -0
  140. package/src/components/primitives/toggle-input.tsx +36 -0
  141. package/src/components/primitives/types.ts +235 -0
  142. package/src/components/ui/accordion.tsx +72 -0
  143. package/src/components/ui/avatar.tsx +106 -0
  144. package/src/components/ui/badge.tsx +48 -0
  145. package/src/components/ui/button.tsx +53 -0
  146. package/src/components/ui/card.tsx +94 -0
  147. package/src/components/ui/checkbox.tsx +27 -0
  148. package/src/components/ui/combobox.tsx +290 -0
  149. package/src/components/ui/dialog.tsx +151 -0
  150. package/src/components/ui/dropdown-menu.tsx +254 -0
  151. package/src/components/ui/field.tsx +227 -0
  152. package/src/components/ui/input-group.tsx +149 -0
  153. package/src/components/ui/input.tsx +20 -0
  154. package/src/components/ui/label.tsx +18 -0
  155. package/src/components/ui/popover.tsx +88 -0
  156. package/src/components/ui/scroll-area.tsx +53 -0
  157. package/src/components/ui/select.tsx +192 -0
  158. package/src/components/ui/separator.tsx +23 -0
  159. package/src/components/ui/sheet.tsx +127 -0
  160. package/src/components/ui/sidebar.tsx +723 -0
  161. package/src/components/ui/skeleton.tsx +13 -0
  162. package/src/components/ui/spinner.tsx +10 -0
  163. package/src/components/ui/switch.tsx +32 -0
  164. package/src/components/ui/table.tsx +99 -0
  165. package/src/components/ui/tabs.tsx +82 -0
  166. package/src/components/ui/textarea.tsx +18 -0
  167. package/src/components/ui/tooltip.tsx +70 -0
  168. package/src/config/component-registry.ts +190 -0
  169. package/src/config/index.ts +1099 -0
  170. package/src/hooks/README.md +269 -0
  171. package/src/hooks/admin-provider.tsx +110 -0
  172. package/src/hooks/index.ts +41 -0
  173. package/src/hooks/store.ts +248 -0
  174. package/src/hooks/use-auth.ts +168 -0
  175. package/src/hooks/use-collection-db.ts +209 -0
  176. package/src/hooks/use-collection.ts +156 -0
  177. package/src/hooks/use-global.ts +69 -0
  178. package/src/hooks/use-mobile.ts +21 -0
  179. package/src/lib/utils.ts +6 -0
  180. package/src/styles/index.css +340 -0
  181. package/src/utils/index.ts +6 -0
  182. package/src/views/auth/auth-layout.tsx +77 -0
  183. package/src/views/auth/forgot-password-form.tsx +192 -0
  184. package/src/views/auth/index.ts +21 -0
  185. package/src/views/auth/login-form.tsx +229 -0
  186. package/src/views/auth/reset-password-form.tsx +232 -0
  187. package/src/views/collection/auto-form-fields.tsx +982 -0
  188. package/src/views/collection/collection-form.tsx +186 -0
  189. package/src/views/collection/collection-list.tsx +223 -0
  190. package/src/views/collection/form-field.tsx +52 -0
  191. package/src/views/collection/index.ts +15 -0
  192. package/src/views/common/index.ts +8 -0
  193. package/src/views/common/locale-switcher.tsx +45 -0
  194. package/src/views/common/version-history.tsx +406 -0
  195. package/src/views/index.ts +25 -0
  196. package/src/views/layout/admin-layout.tsx +117 -0
  197. package/src/views/layout/admin-router.tsx +206 -0
  198. package/src/views/layout/admin-sidebar.tsx +185 -0
  199. package/src/views/layout/index.ts +12 -0
  200. package/tsconfig.json +13 -0
  201. package/tsconfig.tsbuildinfo +1 -0
  202. package/tsdown.config.ts +13 -0
  203. 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
+ }