@camstack/addon-admin-ui 0.1.1 → 0.1.3

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 (127) hide show
  1. package/dist/assets/index-DjELGD4R.css +1 -0
  2. package/dist/assets/index-w55PwKyu.js +598 -0
  3. package/{index.html → dist/index.html} +3 -1
  4. package/dist/server/addon.d.ts +11 -0
  5. package/dist/server/addon.js +50 -0
  6. package/dist/server/addon.js.map +1 -0
  7. package/package.json +5 -1
  8. package/src/App.tsx +0 -71
  9. package/src/components/addons/AddonCard.tsx +0 -339
  10. package/src/components/addons/AddonUploadZone.tsx +0 -307
  11. package/src/components/addons/CapabilityBadge.tsx +0 -55
  12. package/src/components/addons/CapabilityMap.tsx +0 -133
  13. package/src/components/addons/UpdatesList.tsx +0 -119
  14. package/src/components/agents/AgentCard.tsx +0 -281
  15. package/src/components/agents/AgentLogs.tsx +0 -231
  16. package/src/components/agents/ProcessList.tsx +0 -127
  17. package/src/components/agents/ProcessTree.tsx +0 -369
  18. package/src/components/agents/TaskList.tsx +0 -68
  19. package/src/components/cameras/CameraCard.tsx +0 -60
  20. package/src/components/cameras/LiveEventsPanel.tsx +0 -91
  21. package/src/components/cameras/ProviderSection.tsx +0 -50
  22. package/src/components/cameras/StreamArea.tsx +0 -107
  23. package/src/components/cameras/tabs/AddonsTab.tsx +0 -113
  24. package/src/components/cameras/tabs/CameraEventsTab.tsx +0 -129
  25. package/src/components/cameras/tabs/PipelineTab.tsx +0 -118
  26. package/src/components/cameras/tabs/StreamsTab.tsx +0 -114
  27. package/src/components/dashboard/BlockPicker.tsx +0 -54
  28. package/src/components/dashboard/BlockWrapper.tsx +0 -97
  29. package/src/components/dashboard/DashboardGrid.tsx +0 -160
  30. package/src/components/dashboard/block-registry.ts +0 -15
  31. package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +0 -39
  32. package/src/components/dashboard/blocks/StorageBlock.tsx +0 -66
  33. package/src/components/dashboard/blocks/SystemStatusBlock.tsx +0 -67
  34. package/src/components/dashboard/blocks/index.ts +0 -32
  35. package/src/components/device/DeviceHeader.tsx +0 -116
  36. package/src/components/device/FloatingPanel.tsx +0 -132
  37. package/src/components/device/FloatingPanelManager.tsx +0 -167
  38. package/src/components/device/PanelContent.tsx +0 -196
  39. package/src/components/device/QuickConfigWizard.tsx +0 -507
  40. package/src/components/device/tabs/DetectionConfigTab.tsx +0 -96
  41. package/src/components/device/tabs/EventsTab.tsx +0 -19
  42. package/src/components/device/tabs/LogsTab.tsx +0 -22
  43. package/src/components/device/tabs/OverviewTab.tsx +0 -104
  44. package/src/components/device/tabs/ProviderSettingsTab.tsx +0 -34
  45. package/src/components/device/tabs/RecordingTab.tsx +0 -47
  46. package/src/components/device/tabs/ReplTab.tsx +0 -153
  47. package/src/components/device/tabs/TrackTrailTab.tsx +0 -49
  48. package/src/components/device/tabs/ZonesTab.tsx +0 -98
  49. package/src/components/device/zone-editor/ZoneCanvas.tsx +0 -354
  50. package/src/components/device/zone-editor/ZoneForm.tsx +0 -128
  51. package/src/components/device/zone-editor/ZoneList.tsx +0 -150
  52. package/src/components/form-builder/FormBuilder.tsx +0 -135
  53. package/src/components/form-builder/FormField.tsx +0 -732
  54. package/src/components/form-builder/ModelSelector.tsx +0 -239
  55. package/src/components/integrations/AddDeviceDialog.tsx +0 -205
  56. package/src/components/integrations/CompactDeviceCard.tsx +0 -35
  57. package/src/components/integrations/DeviceCard.tsx +0 -29
  58. package/src/components/integrations/DeviceDiscoveryStep.tsx +0 -105
  59. package/src/components/integrations/DeviceGrid.tsx +0 -79
  60. package/src/components/integrations/DeviceGroupHeader.tsx +0 -17
  61. package/src/components/integrations/DiscoveredDeviceCard.tsx +0 -26
  62. package/src/components/integrations/IntegrationCard.tsx +0 -40
  63. package/src/components/integrations/IntegrationWizard.tsx +0 -171
  64. package/src/components/integrations/ProviderConfigForm.tsx +0 -89
  65. package/src/components/integrations/ProviderPicker.tsx +0 -91
  66. package/src/components/integrations/SnapshotPopover.tsx +0 -68
  67. package/src/components/metrics/AgentLoad.tsx +0 -113
  68. package/src/components/metrics/IntegrationUsage.tsx +0 -90
  69. package/src/components/metrics/PipelineStatus.tsx +0 -105
  70. package/src/components/metrics/ProcessResources.tsx +0 -139
  71. package/src/components/pipeline/PhaseSettings.tsx +0 -131
  72. package/src/components/shared/CapabilityBadges.tsx +0 -30
  73. package/src/components/shared/ProviderIcon.tsx +0 -42
  74. package/src/components/shared/StatusBadge.tsx +0 -23
  75. package/src/components/shared/WebRtcPlayer.tsx +0 -211
  76. package/src/components/timeline/EventMarker.tsx +0 -32
  77. package/src/components/timeline/TimelineBar.tsx +0 -131
  78. package/src/components/ui/ConfirmDialog.tsx +0 -115
  79. package/src/components/ui/ToastContainer.tsx +0 -92
  80. package/src/contexts/auth-context.tsx +0 -91
  81. package/src/hooks/useBackendClient.ts +0 -6
  82. package/src/hooks/useTheme.ts +0 -1
  83. package/src/i18n/en.json +0 -164
  84. package/src/i18n/index.ts +0 -29
  85. package/src/i18n/it.json +0 -164
  86. package/src/index.css +0 -63
  87. package/src/layouts/AddonPageLoader.tsx +0 -120
  88. package/src/layouts/AppLayout.tsx +0 -238
  89. package/src/layouts/ProtectedRoute.tsx +0 -25
  90. package/src/lib/addon-page-context.ts +0 -29
  91. package/src/lib/backend.ts +0 -16
  92. package/src/main.tsx +0 -21
  93. package/src/pages/AccessDenied.tsx +0 -22
  94. package/src/pages/Cameras.tsx +0 -127
  95. package/src/pages/Dashboard.tsx +0 -6
  96. package/src/pages/DeviceDetail.tsx +0 -175
  97. package/src/pages/IntegrationDetail.tsx +0 -224
  98. package/src/pages/Integrations.tsx +0 -330
  99. package/src/pages/Login.tsx +0 -106
  100. package/src/pages/Metrics.tsx +0 -18
  101. package/src/pages/PipelineConfig.tsx +0 -282
  102. package/src/pages/Showroom.tsx +0 -351
  103. package/src/pages/Timeline.tsx +0 -269
  104. package/src/pages/system/Addons.tsx +0 -525
  105. package/src/pages/system/Agents.tsx +0 -362
  106. package/src/pages/system/Logs.tsx +0 -131
  107. package/src/pages/system/Models.tsx +0 -102
  108. package/src/pages/system/Processes.tsx +0 -129
  109. package/src/pages/system/Repl.tsx +0 -148
  110. package/src/pages/system/Settings.tsx +0 -168
  111. package/src/pages/system/Users.tsx +0 -174
  112. package/src/server/addon.ts +0 -54
  113. package/src/types/config-ui.ts +0 -210
  114. package/src/types/dashboard.ts +0 -39
  115. package/tsconfig.json +0 -29
  116. package/tsconfig.server.json +0 -16
  117. package/tsup.config.ts +0 -20
  118. package/vite.config.ts +0 -68
  119. /package/{public → dist}/brand/logo-dark.svg +0 -0
  120. /package/{public → dist}/brand/logo-horizontal-dark.svg +0 -0
  121. /package/{public → dist}/brand/logo-horizontal-light.svg +0 -0
  122. /package/{public → dist}/brand/logo-light.svg +0 -0
  123. /package/{public → dist}/brand/logo-wide-dark.svg +0 -0
  124. /package/{public → dist}/brand/logo-wide-light.svg +0 -0
  125. /package/{public → dist}/favicon.svg +0 -0
  126. /package/{public → dist}/vendor/react-jsx-runtime.mjs +0 -0
  127. /package/{public → dist}/vendor/react.mjs +0 -0
@@ -1,732 +0,0 @@
1
- import { useState, useCallback, useRef } from 'react'
2
- import { Eye, EyeOff, X } from 'lucide-react'
3
- import type {
4
- ConfigField,
5
- ConfigGroupField,
6
- ConfigInfoField,
7
- ConfigMultiSelectField,
8
- ConfigNumberField,
9
- ConfigBooleanField,
10
- ConfigSelectField,
11
- ConfigColorField,
12
- ConfigTextAreaField,
13
- ConfigSliderField,
14
- ConfigTagsField,
15
- ConfigPasswordField,
16
- ConfigTextField,
17
- ConfigModelSelectorField,
18
- } from '../../types/config-ui'
19
- import { ModelSelector } from './ModelSelector'
20
- import type { TranslationFn } from './FormBuilder'
21
- import { resolveLabel } from './FormBuilder'
22
-
23
- // ---------------------------------------------------------------------------
24
- // Props
25
- // ---------------------------------------------------------------------------
26
-
27
- interface FormFieldProps {
28
- field: ConfigField
29
- values: Record<string, unknown>
30
- onChange: (key: string, value: unknown) => void
31
- disabled?: boolean
32
- translationFn?: TranslationFn
33
- }
34
-
35
- // ---------------------------------------------------------------------------
36
- // Helpers
37
- // ---------------------------------------------------------------------------
38
-
39
- const INPUT_CLASS =
40
- 'w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none disabled:opacity-50 disabled:cursor-not-allowed'
41
-
42
- const LABEL_CLASS = 'block text-xs font-medium text-foreground mb-1'
43
- const DESC_CLASS = 'text-[10px] text-foreground-subtle mt-0.5'
44
-
45
- function FieldWrapper({
46
- label,
47
- description,
48
- required,
49
- span,
50
- children,
51
- translationFn,
52
- }: {
53
- label?: string
54
- description?: string
55
- required?: boolean
56
- span?: number
57
- children: React.ReactNode
58
- translationFn?: TranslationFn
59
- }) {
60
- const colSpanClass =
61
- span === 2
62
- ? 'col-span-2'
63
- : span === 3
64
- ? 'col-span-3'
65
- : span === 4
66
- ? 'col-span-4'
67
- : 'col-span-1'
68
-
69
- const resolvedLabel = resolveLabel(label, translationFn)
70
- const resolvedDescription = resolveLabel(description, translationFn)
71
-
72
- return (
73
- <div className={colSpanClass}>
74
- {resolvedLabel !== undefined && (
75
- <label className={LABEL_CLASS}>
76
- {resolvedLabel}
77
- {required && <span className="text-danger ml-0.5">*</span>}
78
- </label>
79
- )}
80
- {children}
81
- {resolvedDescription && <p className={DESC_CLASS}>{resolvedDescription}</p>}
82
- </div>
83
- )
84
- }
85
-
86
- // ---------------------------------------------------------------------------
87
- // Field renderers
88
- // ---------------------------------------------------------------------------
89
-
90
- function TextField({
91
- field,
92
- value,
93
- onChange,
94
- disabled,
95
- translationFn,
96
- }: {
97
- field: ConfigTextField
98
- value: unknown
99
- onChange: (v: unknown) => void
100
- disabled?: boolean
101
- translationFn?: TranslationFn
102
- }) {
103
- return (
104
- <FieldWrapper label={field.label} description={field.description} required={field.required} span={field.span} translationFn={translationFn}>
105
- <input
106
- type={field.inputType ?? 'text'}
107
- className={INPUT_CLASS}
108
- value={value === undefined || value === null ? '' : String(value)}
109
- placeholder={field.placeholder}
110
- maxLength={field.maxLength}
111
- pattern={field.pattern}
112
- disabled={disabled || field.disabled}
113
- onChange={(e) => onChange(e.target.value)}
114
- />
115
- </FieldWrapper>
116
- )
117
- }
118
-
119
- function NumberField({
120
- field,
121
- value,
122
- onChange,
123
- disabled,
124
- translationFn,
125
- }: {
126
- field: ConfigNumberField
127
- value: unknown
128
- onChange: (v: unknown) => void
129
- disabled?: boolean
130
- translationFn?: TranslationFn
131
- }) {
132
- return (
133
- <FieldWrapper label={field.label} description={field.description} required={field.required} span={field.span} translationFn={translationFn}>
134
- <div className="flex items-center gap-1">
135
- <input
136
- type="number"
137
- className={INPUT_CLASS}
138
- value={value === undefined || value === null ? '' : String(value)}
139
- placeholder={field.placeholder}
140
- min={field.min}
141
- max={field.max}
142
- step={field.step}
143
- disabled={disabled || field.disabled}
144
- onChange={(e) => onChange(e.target.value === '' ? undefined : Number(e.target.value))}
145
- />
146
- {field.unit && (
147
- <span className="text-xs text-foreground-subtle whitespace-nowrap">{field.unit}</span>
148
- )}
149
- </div>
150
- </FieldWrapper>
151
- )
152
- }
153
-
154
- function BooleanField({
155
- field,
156
- value,
157
- onChange,
158
- disabled,
159
- translationFn,
160
- }: {
161
- field: ConfigBooleanField
162
- value: unknown
163
- onChange: (v: unknown) => void
164
- disabled?: boolean
165
- translationFn?: TranslationFn
166
- }) {
167
- const checked = Boolean(value)
168
- const isDisabled = disabled || field.disabled
169
-
170
- if (field.style === 'checkbox') {
171
- return (
172
- <FieldWrapper label={undefined} description={field.description} required={field.required} span={field.span} translationFn={translationFn}>
173
- <label className="flex items-center gap-2 cursor-pointer select-none">
174
- <input
175
- type="checkbox"
176
- className="h-3.5 w-3.5 rounded border-border accent-primary"
177
- checked={checked}
178
- disabled={isDisabled}
179
- onChange={(e) => onChange(e.target.checked)}
180
- />
181
- <span className="text-xs font-medium text-foreground">{resolveLabel(field.label, translationFn)}</span>
182
- {field.required && <span className="text-danger">*</span>}
183
- </label>
184
- </FieldWrapper>
185
- )
186
- }
187
-
188
- // Default: toggle switch
189
- return (
190
- <FieldWrapper label={undefined} description={field.description} required={field.required} span={field.span} translationFn={translationFn}>
191
- <div className="flex items-center justify-between">
192
- <span className={LABEL_CLASS + ' mb-0'}>
193
- {resolveLabel(field.label, translationFn)}
194
- {field.required && <span className="text-danger ml-0.5">*</span>}
195
- </span>
196
- <button
197
- type="button"
198
- role="switch"
199
- aria-checked={checked}
200
- disabled={isDisabled}
201
- onClick={() => !isDisabled && onChange(!checked)}
202
- className={[
203
- 'relative inline-flex h-4 w-8 shrink-0 items-center rounded-full transition-colors duration-150',
204
- checked ? 'bg-primary' : 'bg-foreground-subtle/30',
205
- isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
206
- ].join(' ')}
207
- >
208
- <span
209
- className={[
210
- 'inline-block h-3 w-3 rounded-full bg-white shadow transition-transform duration-150',
211
- checked ? 'translate-x-4' : 'translate-x-0.5',
212
- ].join(' ')}
213
- />
214
- </button>
215
- </div>
216
- </FieldWrapper>
217
- )
218
- }
219
-
220
- function SelectField({
221
- field,
222
- value,
223
- onChange,
224
- disabled,
225
- translationFn,
226
- }: {
227
- field: ConfigSelectField
228
- value: unknown
229
- onChange: (v: unknown) => void
230
- disabled?: boolean
231
- translationFn?: TranslationFn
232
- }) {
233
- return (
234
- <FieldWrapper label={field.label} description={field.description} required={field.required} span={field.span} translationFn={translationFn}>
235
- <select
236
- className={INPUT_CLASS}
237
- value={value === undefined || value === null ? '' : String(value)}
238
- disabled={disabled || field.disabled}
239
- onChange={(e) => onChange(e.target.value)}
240
- >
241
- {!field.required && <option value="">— Select —</option>}
242
- {field.options.map((opt) => (
243
- <option key={opt.value} value={opt.value}>
244
- {opt.label}
245
- </option>
246
- ))}
247
- </select>
248
- </FieldWrapper>
249
- )
250
- }
251
-
252
- function MultiSelectField({
253
- field,
254
- value,
255
- onChange,
256
- disabled,
257
- translationFn,
258
- }: {
259
- field: ConfigMultiSelectField
260
- value: unknown
261
- onChange: (v: unknown) => void
262
- disabled?: boolean
263
- translationFn?: TranslationFn
264
- }) {
265
- const selected = Array.isArray(value) ? (value as string[]) : []
266
-
267
- const toggle = (optValue: string) => {
268
- if (selected.includes(optValue)) {
269
- onChange(selected.filter((v) => v !== optValue))
270
- } else {
271
- if (field.maxItems !== undefined && selected.length >= field.maxItems) return
272
- onChange([...selected, optValue])
273
- }
274
- }
275
-
276
- return (
277
- <FieldWrapper label={field.label} description={field.description} required={field.required} span={field.span} translationFn={translationFn}>
278
- <div className="flex flex-wrap gap-2">
279
- {field.options.map((opt) => {
280
- const checked = selected.includes(opt.value)
281
- return (
282
- <label
283
- key={opt.value}
284
- className={[
285
- 'flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs cursor-pointer select-none transition-colors',
286
- checked
287
- ? 'border-primary bg-primary/10 text-primary'
288
- : 'border-border bg-background text-foreground hover:bg-surface',
289
- (disabled || field.disabled) ? 'opacity-50 cursor-not-allowed' : '',
290
- ].join(' ')}
291
- >
292
- <input
293
- type="checkbox"
294
- className="sr-only"
295
- checked={checked}
296
- disabled={disabled || field.disabled}
297
- onChange={() => toggle(opt.value)}
298
- />
299
- {opt.label}
300
- </label>
301
- )
302
- })}
303
- </div>
304
- </FieldWrapper>
305
- )
306
- }
307
-
308
- function PasswordField({
309
- field,
310
- value,
311
- onChange,
312
- disabled,
313
- translationFn,
314
- }: {
315
- field: ConfigPasswordField
316
- value: unknown
317
- onChange: (v: unknown) => void
318
- disabled?: boolean
319
- translationFn?: TranslationFn
320
- }) {
321
- const [show, setShow] = useState(false)
322
- const showToggle = field.showToggle !== false
323
-
324
- return (
325
- <FieldWrapper label={field.label} description={field.description} required={field.required} span={field.span} translationFn={translationFn}>
326
- <div className="relative">
327
- <input
328
- type={show ? 'text' : 'password'}
329
- className={INPUT_CLASS + (showToggle ? ' pr-9' : '')}
330
- value={value === undefined || value === null ? '' : String(value)}
331
- placeholder={field.placeholder}
332
- disabled={disabled || field.disabled}
333
- onChange={(e) => onChange(e.target.value)}
334
- />
335
- {showToggle && (
336
- <button
337
- type="button"
338
- className="absolute right-2 top-1/2 -translate-y-1/2 text-foreground-subtle hover:text-foreground"
339
- tabIndex={-1}
340
- onClick={() => setShow((s) => !s)}
341
- >
342
- {show ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
343
- </button>
344
- )}
345
- </div>
346
- </FieldWrapper>
347
- )
348
- }
349
-
350
- function TextAreaField({
351
- field,
352
- value,
353
- onChange,
354
- disabled,
355
- translationFn,
356
- }: {
357
- field: ConfigTextAreaField
358
- value: unknown
359
- onChange: (v: unknown) => void
360
- disabled?: boolean
361
- translationFn?: TranslationFn
362
- }) {
363
- return (
364
- <FieldWrapper label={field.label} description={field.description} required={field.required} span={field.span} translationFn={translationFn}>
365
- <textarea
366
- className={INPUT_CLASS + ' resize-y min-h-[60px]'}
367
- value={value === undefined || value === null ? '' : String(value)}
368
- placeholder={field.placeholder}
369
- rows={field.rows ?? 3}
370
- maxLength={field.maxLength}
371
- disabled={disabled || field.disabled}
372
- onChange={(e) => onChange(e.target.value)}
373
- />
374
- </FieldWrapper>
375
- )
376
- }
377
-
378
- function SliderField({
379
- field,
380
- value,
381
- onChange,
382
- disabled,
383
- translationFn,
384
- }: {
385
- field: ConfigSliderField
386
- value: unknown
387
- onChange: (v: unknown) => void
388
- disabled?: boolean
389
- translationFn?: TranslationFn
390
- }) {
391
- const numVal = value === undefined || value === null ? field.min : Number(value)
392
- const showValue = field.showValue !== false
393
-
394
- return (
395
- <FieldWrapper label={field.label} description={field.description} required={field.required} span={field.span} translationFn={translationFn}>
396
- <div className="flex items-center gap-3">
397
- <input
398
- type="range"
399
- className="flex-1 h-1 accent-primary cursor-pointer disabled:opacity-50"
400
- min={field.min}
401
- max={field.max}
402
- step={field.step ?? 1}
403
- value={numVal}
404
- disabled={disabled || field.disabled}
405
- onChange={(e) => onChange(Number(e.target.value))}
406
- />
407
- {showValue && (
408
- <span className="text-xs text-foreground tabular-nums min-w-[2.5rem] text-right">
409
- {numVal}
410
- {field.unit ? ` ${field.unit}` : ''}
411
- </span>
412
- )}
413
- </div>
414
- </FieldWrapper>
415
- )
416
- }
417
-
418
- function TagsField({
419
- field,
420
- value,
421
- onChange,
422
- disabled,
423
- translationFn,
424
- }: {
425
- field: ConfigTagsField
426
- value: unknown
427
- onChange: (v: unknown) => void
428
- disabled?: boolean
429
- translationFn?: TranslationFn
430
- }) {
431
- const tags = Array.isArray(value) ? (value as string[]) : []
432
- const [input, setInput] = useState('')
433
- const inputRef = useRef<HTMLInputElement>(null)
434
-
435
- const addTag = useCallback(
436
- (tag: string) => {
437
- const trimmed = tag.trim()
438
- if (!trimmed || tags.includes(trimmed)) return
439
- if (field.maxTags !== undefined && tags.length >= field.maxTags) return
440
- onChange([...tags, trimmed])
441
- setInput('')
442
- },
443
- [tags, field.maxTags, onChange],
444
- )
445
-
446
- const removeTag = (tag: string) => onChange(tags.filter((t) => t !== tag))
447
-
448
- const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
449
- if (e.key === 'Enter' || e.key === ',') {
450
- e.preventDefault()
451
- addTag(input)
452
- } else if (e.key === 'Backspace' && input === '' && tags.length > 0) {
453
- removeTag(tags[tags.length - 1]!)
454
- }
455
- }
456
-
457
- const isDisabled = disabled || field.disabled
458
-
459
- return (
460
- <FieldWrapper label={field.label} description={field.description} required={field.required} span={field.span} translationFn={translationFn}>
461
- <div
462
- className={[
463
- 'flex flex-wrap gap-1.5 rounded-md border border-border bg-background px-2 py-1.5 min-h-[38px] cursor-text',
464
- 'focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30',
465
- isDisabled ? 'opacity-50 cursor-not-allowed' : '',
466
- ].join(' ')}
467
- onClick={() => !isDisabled && inputRef.current?.focus()}
468
- >
469
- {tags.map((tag) => (
470
- <span
471
- key={tag}
472
- className="inline-flex items-center gap-1 rounded-md bg-primary/10 px-1.5 py-0.5 text-xs text-primary"
473
- >
474
- {tag}
475
- {!isDisabled && (
476
- <button type="button" onClick={() => removeTag(tag)} className="hover:text-danger">
477
- <X className="h-2.5 w-2.5" />
478
- </button>
479
- )}
480
- </span>
481
- ))}
482
- <input
483
- ref={inputRef}
484
- type="text"
485
- className="flex-1 min-w-[80px] bg-transparent text-sm text-foreground outline-none placeholder:text-foreground-disabled"
486
- value={input}
487
- placeholder={tags.length === 0 ? (field.placeholder ?? 'Add tags…') : ''}
488
- disabled={isDisabled}
489
- list={`tags-suggestions-${field.key}`}
490
- onChange={(e) => setInput(e.target.value)}
491
- onKeyDown={handleKeyDown}
492
- onBlur={() => { if (input.trim()) addTag(input) }}
493
- />
494
- {field.suggestions && (
495
- <datalist id={`tags-suggestions-${field.key}`}>
496
- {field.suggestions.map((s) => (
497
- <option key={s} value={s} />
498
- ))}
499
- </datalist>
500
- )}
501
- </div>
502
- </FieldWrapper>
503
- )
504
- }
505
-
506
- function ColorField({
507
- field,
508
- value,
509
- onChange,
510
- disabled,
511
- translationFn,
512
- }: {
513
- field: ConfigColorField
514
- value: unknown
515
- onChange: (v: unknown) => void
516
- disabled?: boolean
517
- translationFn?: TranslationFn
518
- }) {
519
- const strVal = value === undefined || value === null ? '#000000' : String(value)
520
-
521
- return (
522
- <FieldWrapper label={field.label} description={field.description} required={field.required} span={field.span} translationFn={translationFn}>
523
- <div className="flex items-center gap-2">
524
- <input
525
- type="color"
526
- className="h-9 w-9 rounded-md border border-border bg-background cursor-pointer p-0.5 disabled:opacity-50"
527
- value={strVal}
528
- disabled={disabled || field.disabled}
529
- onChange={(e) => onChange(e.target.value)}
530
- />
531
- <input
532
- type="text"
533
- className={INPUT_CLASS + ' flex-1 font-mono'}
534
- value={strVal}
535
- placeholder="#000000"
536
- disabled={disabled || field.disabled}
537
- onChange={(e) => onChange(e.target.value)}
538
- />
539
- </div>
540
- {field.presets && field.presets.length > 0 && (
541
- <div className="flex gap-1.5 mt-1.5">
542
- {field.presets.map((preset) => (
543
- <button
544
- key={preset}
545
- type="button"
546
- className="h-5 w-5 rounded-md border border-border hover:scale-110 transition-transform disabled:opacity-50"
547
- style={{ backgroundColor: preset }}
548
- disabled={disabled || field.disabled}
549
- title={preset}
550
- onClick={() => onChange(preset)}
551
- />
552
- ))}
553
- </div>
554
- )}
555
- </FieldWrapper>
556
- )
557
- }
558
-
559
- function GroupField({
560
- field,
561
- values,
562
- onChange,
563
- disabled,
564
- translationFn,
565
- }: {
566
- field: ConfigGroupField
567
- values: Record<string, unknown>
568
- onChange: (key: string, value: unknown) => void
569
- disabled?: boolean
570
- translationFn?: TranslationFn
571
- }) {
572
- const [collapsed, setCollapsed] = useState(field.defaultCollapsed ?? false)
573
- const isAccordion = field.style === 'accordion'
574
-
575
- const colSpanClass =
576
- field.span === 2
577
- ? 'col-span-2'
578
- : field.span === 3
579
- ? 'col-span-3'
580
- : field.span === 4
581
- ? 'col-span-4'
582
- : 'col-span-1'
583
-
584
- return (
585
- <div className={colSpanClass}>
586
- <div className="rounded-lg border border-border bg-surface/50 overflow-hidden">
587
- <div
588
- className={[
589
- 'px-3 py-2 flex items-center justify-between',
590
- isAccordion ? 'cursor-pointer hover:bg-surface' : '',
591
- ].join(' ')}
592
- onClick={isAccordion ? () => setCollapsed((c) => !c) : undefined}
593
- >
594
- <span className="text-xs font-semibold text-foreground">{resolveLabel(field.label, translationFn)}</span>
595
- {isAccordion && (
596
- <span className="text-foreground-subtle text-xs">{collapsed ? '▸' : '▾'}</span>
597
- )}
598
- </div>
599
- {!collapsed && (
600
- <div className="px-3 pb-3 grid grid-cols-2 gap-x-4 gap-y-3">
601
- {field.fields.map((subField) => (
602
- <FormField
603
- key={subField.key}
604
- field={subField}
605
- values={values}
606
- onChange={onChange}
607
- disabled={disabled}
608
- translationFn={translationFn}
609
- />
610
- ))}
611
- </div>
612
- )}
613
- </div>
614
- </div>
615
- )
616
- }
617
-
618
- function ModelSelectorFieldRenderer({
619
- field,
620
- value,
621
- onChange,
622
- disabled,
623
- translationFn,
624
- }: {
625
- field: ConfigModelSelectorField
626
- value: unknown
627
- onChange: (v: unknown) => void
628
- disabled?: boolean
629
- translationFn?: TranslationFn
630
- }) {
631
- return (
632
- <FieldWrapper label={field.label} description={field.description} required={field.required} span={field.span} translationFn={translationFn}>
633
- <ModelSelector field={field} value={value} onChange={onChange} disabled={disabled} />
634
- </FieldWrapper>
635
- )
636
- }
637
-
638
- function SeparatorField({ span }: { span?: number }) {
639
- const colSpanClass =
640
- span === 2
641
- ? 'col-span-2'
642
- : span === 3
643
- ? 'col-span-3'
644
- : span === 4
645
- ? 'col-span-4'
646
- : 'col-span-full'
647
-
648
- return (
649
- <div className={colSpanClass}>
650
- <hr className="border-border" />
651
- </div>
652
- )
653
- }
654
-
655
- const INFO_VARIANT_STYLES: Record<string, { border: string; bg: string; text: string }> = {
656
- info: { border: 'border-info', bg: 'bg-info/5', text: 'text-info' },
657
- warning: { border: 'border-warning', bg: 'bg-warning/5', text: 'text-warning' },
658
- success: { border: 'border-success', bg: 'bg-success/5', text: 'text-success' },
659
- danger: { border: 'border-danger', bg: 'bg-danger/5', text: 'text-danger' },
660
- }
661
-
662
- function InfoBox({ field }: { field: ConfigInfoField }) {
663
- const variant = field.variant ?? 'info'
664
- const styles = INFO_VARIANT_STYLES[variant] ?? INFO_VARIANT_STYLES['info']!
665
-
666
- return (
667
- <div className="col-span-full">
668
- <div className={`rounded-md border-l-4 px-3 py-2.5 ${styles.border} ${styles.bg}`}>
669
- {field.label && (
670
- <p className={`text-xs font-semibold mb-0.5 ${styles.text}`}>{field.label}</p>
671
- )}
672
- <p className="text-xs text-foreground-subtle leading-relaxed">{field.content}</p>
673
- </div>
674
- </div>
675
- )
676
- }
677
-
678
- // ---------------------------------------------------------------------------
679
- // Main FormField component
680
- // ---------------------------------------------------------------------------
681
-
682
- export function FormField({ field, values, onChange, disabled, translationFn }: FormFieldProps) {
683
- // Evaluate showWhen condition
684
- if ('showWhen' in field && field.showWhen) {
685
- const condition = field.showWhen
686
- const condVal = values[condition.field]
687
-
688
- let visible = true
689
- if (condition.equals !== undefined) visible = condVal === condition.equals
690
- else if (condition.notEquals !== undefined) visible = condVal !== condition.notEquals
691
- else if (condition.in !== undefined) visible = condition.in.includes(condVal)
692
- else if (condition.notIn !== undefined) visible = !condition.notIn.includes(condVal)
693
-
694
- if (!visible) return null
695
- }
696
-
697
- const value = values[field.key]
698
- const handleChange = (v: unknown) => onChange(field.key, v)
699
-
700
- switch (field.type) {
701
- case 'text':
702
- return <TextField field={field} value={value} onChange={handleChange} disabled={disabled} translationFn={translationFn} />
703
- case 'number':
704
- return <NumberField field={field} value={value} onChange={handleChange} disabled={disabled} translationFn={translationFn} />
705
- case 'boolean':
706
- return <BooleanField field={field} value={value} onChange={handleChange} disabled={disabled} translationFn={translationFn} />
707
- case 'select':
708
- return <SelectField field={field} value={value} onChange={handleChange} disabled={disabled} translationFn={translationFn} />
709
- case 'multiselect':
710
- return <MultiSelectField field={field} value={value} onChange={handleChange} disabled={disabled} translationFn={translationFn} />
711
- case 'password':
712
- return <PasswordField field={field} value={value} onChange={handleChange} disabled={disabled} translationFn={translationFn} />
713
- case 'textarea':
714
- return <TextAreaField field={field} value={value} onChange={handleChange} disabled={disabled} translationFn={translationFn} />
715
- case 'slider':
716
- return <SliderField field={field} value={value} onChange={handleChange} disabled={disabled} translationFn={translationFn} />
717
- case 'tags':
718
- return <TagsField field={field} value={value} onChange={handleChange} disabled={disabled} translationFn={translationFn} />
719
- case 'color':
720
- return <ColorField field={field} value={value} onChange={handleChange} disabled={disabled} translationFn={translationFn} />
721
- case 'group':
722
- return <GroupField field={field} values={values} onChange={onChange} disabled={disabled} translationFn={translationFn} />
723
- case 'separator':
724
- return <SeparatorField />
725
- case 'info':
726
- return <InfoBox field={field} />
727
- case 'model-selector':
728
- return <ModelSelectorFieldRenderer field={field} value={value} onChange={handleChange} disabled={disabled} translationFn={translationFn} />
729
- default:
730
- return null
731
- }
732
- }