qdadm 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +270 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/package.json +48 -0
  5. package/src/assets/logo.svg +6 -0
  6. package/src/components/BoolCell.vue +28 -0
  7. package/src/components/dialogs/BulkStatusDialog.vue +43 -0
  8. package/src/components/dialogs/MultiStepDialog.vue +321 -0
  9. package/src/components/dialogs/SimpleDialog.vue +108 -0
  10. package/src/components/dialogs/UnsavedChangesDialog.vue +87 -0
  11. package/src/components/display/CardsGrid.vue +155 -0
  12. package/src/components/display/CopyableId.vue +92 -0
  13. package/src/components/display/EmptyState.vue +114 -0
  14. package/src/components/display/IntensityBar.vue +171 -0
  15. package/src/components/display/RichCardsGrid.vue +220 -0
  16. package/src/components/editors/JsonEditorFoldable.vue +467 -0
  17. package/src/components/editors/JsonStructuredField.vue +218 -0
  18. package/src/components/editors/JsonViewer.vue +91 -0
  19. package/src/components/editors/KeyValueEditor.vue +314 -0
  20. package/src/components/editors/LanguageEditor.vue +245 -0
  21. package/src/components/editors/ScopeEditor.vue +341 -0
  22. package/src/components/editors/VanillaJsonEditor.vue +185 -0
  23. package/src/components/forms/FormActions.vue +104 -0
  24. package/src/components/forms/FormField.vue +64 -0
  25. package/src/components/forms/FormTab.vue +217 -0
  26. package/src/components/forms/FormTabs.vue +108 -0
  27. package/src/components/index.js +44 -0
  28. package/src/components/layout/AppLayout.vue +430 -0
  29. package/src/components/layout/Breadcrumb.vue +106 -0
  30. package/src/components/layout/PageHeader.vue +75 -0
  31. package/src/components/layout/PageLayout.vue +93 -0
  32. package/src/components/lists/ActionButtons.vue +41 -0
  33. package/src/components/lists/ActionColumn.vue +37 -0
  34. package/src/components/lists/FilterBar.vue +53 -0
  35. package/src/components/lists/ListPage.vue +319 -0
  36. package/src/composables/index.js +19 -0
  37. package/src/composables/useApp.js +43 -0
  38. package/src/composables/useAuth.js +49 -0
  39. package/src/composables/useBareForm.js +143 -0
  40. package/src/composables/useBreadcrumb.js +221 -0
  41. package/src/composables/useDirtyState.js +103 -0
  42. package/src/composables/useEntityTitle.js +121 -0
  43. package/src/composables/useForm.js +254 -0
  44. package/src/composables/useGuardStore.js +37 -0
  45. package/src/composables/useJsonSyntax.js +101 -0
  46. package/src/composables/useListPageBuilder.js +1176 -0
  47. package/src/composables/useNavigation.js +89 -0
  48. package/src/composables/usePageBuilder.js +334 -0
  49. package/src/composables/useStatus.js +146 -0
  50. package/src/composables/useSubEditor.js +165 -0
  51. package/src/composables/useTabSync.js +110 -0
  52. package/src/composables/useUnsavedChangesGuard.js +122 -0
  53. package/src/entity/EntityManager.js +540 -0
  54. package/src/entity/index.js +11 -0
  55. package/src/entity/storage/ApiStorage.js +146 -0
  56. package/src/entity/storage/LocalStorage.js +220 -0
  57. package/src/entity/storage/MemoryStorage.js +201 -0
  58. package/src/entity/storage/index.js +10 -0
  59. package/src/index.js +29 -0
  60. package/src/kernel/Kernel.js +234 -0
  61. package/src/kernel/index.js +7 -0
  62. package/src/module/index.js +16 -0
  63. package/src/module/moduleRegistry.js +222 -0
  64. package/src/orchestrator/Orchestrator.js +141 -0
  65. package/src/orchestrator/index.js +8 -0
  66. package/src/orchestrator/useOrchestrator.js +61 -0
  67. package/src/plugin.js +142 -0
  68. package/src/styles/_alerts.css +48 -0
  69. package/src/styles/_code.css +33 -0
  70. package/src/styles/_dialogs.css +17 -0
  71. package/src/styles/_markdown.css +82 -0
  72. package/src/styles/_show-pages.css +84 -0
  73. package/src/styles/index.css +16 -0
  74. package/src/styles/main.css +845 -0
  75. package/src/styles/theme/components.css +286 -0
  76. package/src/styles/theme/index.css +10 -0
  77. package/src/styles/theme/tokens.css +125 -0
  78. package/src/styles/theme/utilities.css +172 -0
  79. package/src/utils/debugInjector.js +261 -0
  80. package/src/utils/formatters.js +165 -0
  81. package/src/utils/index.js +35 -0
  82. package/src/utils/transformers.js +105 -0
@@ -0,0 +1,467 @@
1
+ <script setup>
2
+ /**
3
+ * JsonEditorFoldable - JSON editor with collapsible sections
4
+ *
5
+ * Displays each top-level key as a foldable section.
6
+ * Long content (like "content" field) can be collapsed to reduce visual noise.
7
+ *
8
+ * Usage:
9
+ * <JsonEditorFoldable v-model="jsonData" :defaultExpanded="['content']" />
10
+ */
11
+
12
+ import { ref, computed, watch } from 'vue'
13
+ import Button from 'primevue/button'
14
+ import Textarea from 'primevue/textarea'
15
+ import InputText from 'primevue/inputtext'
16
+ import Message from 'primevue/message'
17
+ import { getJsonValueType, getJsonPreview } from '../../composables/useJsonSyntax'
18
+
19
+ const props = defineProps({
20
+ modelValue: {
21
+ type: [Object, null],
22
+ default: () => ({})
23
+ },
24
+ defaultExpanded: {
25
+ type: Array,
26
+ default: () => ['content'] // "content" expanded by default
27
+ },
28
+ height: {
29
+ type: String,
30
+ default: '400px'
31
+ }
32
+ })
33
+
34
+ const emit = defineEmits(['update:modelValue'])
35
+
36
+ // Internal state: track which sections are expanded
37
+ const expandedSections = ref(new Set(props.defaultExpanded))
38
+
39
+ // Internal copy of data for editing
40
+ const localData = ref({})
41
+
42
+ // Parse error for raw JSON mode
43
+ const parseError = ref(null)
44
+ const rawJsonMode = ref(false)
45
+ const rawJsonText = ref('')
46
+
47
+ // Sync from modelValue
48
+ watch(() => props.modelValue, (newVal) => {
49
+ if (newVal && typeof newVal === 'object') {
50
+ localData.value = JSON.parse(JSON.stringify(newVal))
51
+ rawJsonText.value = JSON.stringify(newVal, null, 2)
52
+ } else {
53
+ localData.value = {}
54
+ rawJsonText.value = '{}'
55
+ }
56
+ }, { immediate: true, deep: true })
57
+
58
+ // Computed: sorted keys with "content" first if present
59
+ const sortedKeys = computed(() => {
60
+ const keys = Object.keys(localData.value || {})
61
+ // Put 'content' first, then sort rest alphabetically
62
+ return keys.sort((a, b) => {
63
+ if (a === 'content') return -1
64
+ if (b === 'content') return 1
65
+ return a.localeCompare(b)
66
+ })
67
+ })
68
+
69
+ function isExpanded(key) {
70
+ return expandedSections.value.has(key)
71
+ }
72
+
73
+ function toggleSection(key) {
74
+ if (expandedSections.value.has(key)) {
75
+ expandedSections.value.delete(key)
76
+ } else {
77
+ expandedSections.value.add(key)
78
+ }
79
+ // Trigger reactivity
80
+ expandedSections.value = new Set(expandedSections.value)
81
+ }
82
+
83
+ function expandAll() {
84
+ expandedSections.value = new Set(sortedKeys.value)
85
+ }
86
+
87
+ function collapseAll() {
88
+ expandedSections.value = new Set()
89
+ }
90
+
91
+ // Update a specific field
92
+ function updateField(key, newValue) {
93
+ const type = getJsonValueType(localData.value[key])
94
+
95
+ if (type === 'string') {
96
+ localData.value[key] = newValue
97
+ } else if (type === 'number') {
98
+ localData.value[key] = parseFloat(newValue) || 0
99
+ } else if (type === 'boolean') {
100
+ localData.value[key] = newValue === 'true' || newValue === true
101
+ } else if (type === 'array' || type === 'object') {
102
+ // Parse as JSON for complex types
103
+ try {
104
+ localData.value[key] = JSON.parse(newValue)
105
+ } catch {
106
+ // Invalid JSON, don't update
107
+ return
108
+ }
109
+ }
110
+
111
+ emitUpdate()
112
+ }
113
+
114
+ function emitUpdate() {
115
+ emit('update:modelValue', JSON.parse(JSON.stringify(localData.value)))
116
+ }
117
+
118
+ // Raw JSON mode handling
119
+ function toggleRawMode() {
120
+ if (rawJsonMode.value) {
121
+ // Switching from raw to structured - parse the raw JSON
122
+ try {
123
+ localData.value = JSON.parse(rawJsonText.value)
124
+ parseError.value = null
125
+ emitUpdate()
126
+ } catch (e) {
127
+ parseError.value = e.message
128
+ return // Don't switch if invalid
129
+ }
130
+ } else {
131
+ // Switching to raw mode - serialize current data
132
+ rawJsonText.value = JSON.stringify(localData.value, null, 2)
133
+ }
134
+ rawJsonMode.value = !rawJsonMode.value
135
+ }
136
+
137
+ function onRawJsonInput(event) {
138
+ rawJsonText.value = event.target.value
139
+ try {
140
+ JSON.parse(rawJsonText.value)
141
+ parseError.value = null
142
+ } catch (e) {
143
+ parseError.value = e.message
144
+ }
145
+ }
146
+
147
+ function saveRawJson() {
148
+ try {
149
+ localData.value = JSON.parse(rawJsonText.value)
150
+ parseError.value = null
151
+ emitUpdate()
152
+ } catch (e) {
153
+ parseError.value = e.message
154
+ }
155
+ }
156
+
157
+ // Get appropriate editor for value type
158
+ function isMultiline(value) {
159
+ if (typeof value !== 'string') return false
160
+ return value.length > 100 || value.includes('\n')
161
+ }
162
+ </script>
163
+
164
+ <template>
165
+ <div class="json-editor-foldable">
166
+ <!-- Toolbar -->
167
+ <div class="editor-toolbar">
168
+ <div class="toolbar-left">
169
+ <Button
170
+ :icon="rawJsonMode ? 'pi pi-list' : 'pi pi-code'"
171
+ :label="rawJsonMode ? 'Structured' : 'Raw JSON'"
172
+ size="small"
173
+ severity="secondary"
174
+ text
175
+ @click="toggleRawMode"
176
+ />
177
+ </div>
178
+ <div class="toolbar-right" v-if="!rawJsonMode">
179
+ <Button
180
+ icon="pi pi-plus"
181
+ label="Expand All"
182
+ size="small"
183
+ severity="secondary"
184
+ text
185
+ @click="expandAll"
186
+ />
187
+ <Button
188
+ icon="pi pi-minus"
189
+ label="Collapse All"
190
+ size="small"
191
+ severity="secondary"
192
+ text
193
+ @click="collapseAll"
194
+ />
195
+ </div>
196
+ </div>
197
+
198
+ <!-- Structured Mode -->
199
+ <div v-if="!rawJsonMode" class="sections-container" :style="{ maxHeight: height }">
200
+ <div
201
+ v-for="key in sortedKeys"
202
+ :key="key"
203
+ class="section"
204
+ :class="{ expanded: isExpanded(key) }"
205
+ >
206
+ <div class="section-header" @click="toggleSection(key)">
207
+ <i
208
+ class="pi"
209
+ :class="isExpanded(key) ? 'pi-chevron-down' : 'pi-chevron-right'"
210
+ ></i>
211
+ <span class="section-key">{{ key }}</span>
212
+ <span class="section-type">{{ getJsonValueType(localData[key]) }}</span>
213
+ <span v-if="!isExpanded(key)" class="section-preview">{{ getJsonPreview(localData[key]) }}</span>
214
+ </div>
215
+
216
+ <div v-if="isExpanded(key)" class="section-content">
217
+ <!-- String (multiline) -->
218
+ <Textarea
219
+ v-if="getJsonValueType(localData[key]) === 'string' && isMultiline(localData[key])"
220
+ :modelValue="localData[key]"
221
+ @update:modelValue="updateField(key, $event)"
222
+ rows="8"
223
+ class="w-full field-textarea"
224
+ autoResize
225
+ />
226
+
227
+ <!-- String (single line) -->
228
+ <InputText
229
+ v-else-if="getJsonValueType(localData[key]) === 'string'"
230
+ :modelValue="localData[key]"
231
+ @update:modelValue="updateField(key, $event)"
232
+ class="w-full"
233
+ />
234
+
235
+ <!-- Number -->
236
+ <InputText
237
+ v-else-if="getJsonValueType(localData[key]) === 'number'"
238
+ type="number"
239
+ :modelValue="String(localData[key])"
240
+ @update:modelValue="updateField(key, $event)"
241
+ class="w-full"
242
+ />
243
+
244
+ <!-- Boolean -->
245
+ <select
246
+ v-else-if="getJsonValueType(localData[key]) === 'boolean'"
247
+ :value="String(localData[key])"
248
+ @change="updateField(key, $event.target.value)"
249
+ class="field-select"
250
+ >
251
+ <option value="true">true</option>
252
+ <option value="false">false</option>
253
+ </select>
254
+
255
+ <!-- Array or Object - show as JSON -->
256
+ <Textarea
257
+ v-else-if="getJsonValueType(localData[key]) === 'array' || getJsonValueType(localData[key]) === 'object'"
258
+ :modelValue="JSON.stringify(localData[key], null, 2)"
259
+ @update:modelValue="updateField(key, $event)"
260
+ rows="6"
261
+ class="w-full field-json"
262
+ autoResize
263
+ />
264
+
265
+ <!-- Null -->
266
+ <span v-else-if="getJsonValueType(localData[key]) === 'null'" class="null-value">null</span>
267
+ </div>
268
+ </div>
269
+
270
+ <div v-if="sortedKeys.length === 0" class="empty-state">
271
+ <i class="pi pi-inbox"></i>
272
+ <span>No data</span>
273
+ </div>
274
+ </div>
275
+
276
+ <!-- Raw JSON Mode -->
277
+ <div v-else class="raw-json-container" :style="{ height }">
278
+ <textarea
279
+ class="raw-json-textarea"
280
+ :class="{ 'has-error': parseError }"
281
+ :value="rawJsonText"
282
+ @input="onRawJsonInput"
283
+ spellcheck="false"
284
+ ></textarea>
285
+ <div v-if="parseError" class="raw-json-actions">
286
+ <Message severity="error" :closable="false" class="parse-error">
287
+ {{ parseError }}
288
+ </Message>
289
+ </div>
290
+ <div v-else class="raw-json-actions">
291
+ <Button
292
+ label="Apply Changes"
293
+ icon="pi pi-check"
294
+ size="small"
295
+ @click="saveRawJson"
296
+ />
297
+ </div>
298
+ </div>
299
+ </div>
300
+ </template>
301
+
302
+ <style scoped>
303
+ .json-editor-foldable {
304
+ display: flex;
305
+ flex-direction: column;
306
+ border: 1px solid var(--p-surface-300);
307
+ border-radius: 0.375rem;
308
+ background: var(--p-surface-0);
309
+ }
310
+
311
+ .editor-toolbar {
312
+ display: flex;
313
+ justify-content: space-between;
314
+ align-items: center;
315
+ padding: 0.5rem;
316
+ border-bottom: 1px solid var(--p-surface-200);
317
+ background: var(--p-surface-50);
318
+ }
319
+
320
+ .toolbar-left,
321
+ .toolbar-right {
322
+ display: flex;
323
+ gap: 0.25rem;
324
+ }
325
+
326
+ .sections-container {
327
+ overflow-y: auto;
328
+ padding: 0.5rem;
329
+ }
330
+
331
+ .section {
332
+ border: 1px solid var(--p-surface-200);
333
+ border-radius: 0.375rem;
334
+ margin-bottom: 0.5rem;
335
+ background: var(--p-surface-0);
336
+ }
337
+
338
+ .section:last-child {
339
+ margin-bottom: 0;
340
+ }
341
+
342
+ .section-header {
343
+ display: flex;
344
+ align-items: center;
345
+ gap: 0.5rem;
346
+ padding: 0.75rem;
347
+ cursor: pointer;
348
+ user-select: none;
349
+ background: var(--p-surface-50);
350
+ border-radius: 0.375rem;
351
+ }
352
+
353
+ .section.expanded .section-header {
354
+ border-bottom: 1px solid var(--p-surface-200);
355
+ border-radius: 0.375rem 0.375rem 0 0;
356
+ }
357
+
358
+ .section-header:hover {
359
+ background: var(--p-surface-100);
360
+ }
361
+
362
+ .section-header .pi {
363
+ font-size: 0.75rem;
364
+ color: var(--p-surface-500);
365
+ }
366
+
367
+ .section-key {
368
+ font-weight: 600;
369
+ color: var(--p-primary-700);
370
+ }
371
+
372
+ .section-type {
373
+ font-size: 0.75rem;
374
+ color: var(--p-surface-400);
375
+ padding: 0.125rem 0.375rem;
376
+ background: var(--p-surface-100);
377
+ border-radius: 0.25rem;
378
+ }
379
+
380
+ .section-preview {
381
+ flex: 1;
382
+ font-size: 0.8125rem;
383
+ color: var(--p-surface-500);
384
+ overflow: hidden;
385
+ text-overflow: ellipsis;
386
+ white-space: nowrap;
387
+ text-align: right;
388
+ }
389
+
390
+ .section-content {
391
+ padding: 0.75rem;
392
+ }
393
+
394
+ .field-textarea,
395
+ .field-json {
396
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
397
+ font-size: 0.8125rem;
398
+ line-height: 1.5;
399
+ }
400
+
401
+ .field-json {
402
+ background: var(--p-surface-50);
403
+ }
404
+
405
+ .field-select {
406
+ padding: 0.5rem;
407
+ border: 1px solid var(--p-surface-300);
408
+ border-radius: 0.375rem;
409
+ background: var(--p-surface-0);
410
+ font-size: 0.875rem;
411
+ }
412
+
413
+ .null-value {
414
+ color: var(--p-surface-400);
415
+ font-style: italic;
416
+ }
417
+
418
+ .empty-state {
419
+ display: flex;
420
+ flex-direction: column;
421
+ align-items: center;
422
+ justify-content: center;
423
+ padding: 2rem;
424
+ color: var(--p-surface-400);
425
+ gap: 0.5rem;
426
+ }
427
+
428
+ .empty-state .pi {
429
+ font-size: 2rem;
430
+ }
431
+
432
+ /* Raw JSON mode */
433
+ .raw-json-container {
434
+ display: flex;
435
+ flex-direction: column;
436
+ }
437
+
438
+ .raw-json-textarea {
439
+ flex: 1;
440
+ padding: 1rem;
441
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
442
+ font-size: 0.8125rem;
443
+ line-height: 1.5;
444
+ border: none;
445
+ outline: none;
446
+ resize: none;
447
+ background: var(--p-surface-50);
448
+ }
449
+
450
+ .raw-json-textarea.has-error {
451
+ background: var(--p-red-50);
452
+ }
453
+
454
+ .raw-json-actions {
455
+ padding: 0.5rem;
456
+ border-top: 1px solid var(--p-surface-200);
457
+ background: var(--p-surface-50);
458
+ }
459
+
460
+ .parse-error {
461
+ margin: 0;
462
+ }
463
+
464
+ .w-full {
465
+ width: 100%;
466
+ }
467
+ </style>
@@ -0,0 +1,218 @@
1
+ <script setup>
2
+ /**
3
+ * JsonStructuredField - Reusable field with structured/JSON toggle
4
+ *
5
+ * A form field that shows either a structured view (slot) or raw JSON editor.
6
+ * Uses a mini SelectButton to toggle between views.
7
+ *
8
+ * Usage patterns:
9
+ *
10
+ * 1. Simple - same v-model for both views:
11
+ * <JsonStructuredField v-model="myData">
12
+ * <MyStructuredEditor v-model="myData" />
13
+ * </JsonStructuredField>
14
+ *
15
+ * 2. With separate JSON binding (for complex computed mappings):
16
+ * <JsonStructuredField v-model="myData" :jsonValue="jsonComputed" @update:jsonValue="onJsonUpdate">
17
+ * <MyStructuredEditor v-model="myData" />
18
+ * </JsonStructuredField>
19
+ *
20
+ * 3. Controlled mode (parent manages viewMode):
21
+ * <JsonStructuredField v-model="myData" v-model:mode="viewMode">
22
+ * <MyStructuredEditor v-model="myData" />
23
+ * </JsonStructuredField>
24
+ *
25
+ * Props:
26
+ * - modelValue: The JSON object to edit (used for JSON view if jsonValue not provided)
27
+ * - jsonValue: Optional separate binding for JSON editor (for complex computed mappings)
28
+ * - mode: Optional v-model for view mode ('structured' or 'json')
29
+ * - label: Optional label for the field
30
+ * - jsonHeight: Height of JSON editor (default: 400px)
31
+ * - jsonMode: Mode for JSON editor - 'tree' or 'text' (default: 'text')
32
+ * - defaultMode: Initial mode if not using v-model:mode (default: 'structured')
33
+ * - showToggle: Whether to show the toggle (default: true, useful to hide in nested usage)
34
+ */
35
+
36
+ import { ref, computed, watch } from 'vue'
37
+ import SelectButton from 'primevue/selectbutton'
38
+ import VanillaJsonEditor from './VanillaJsonEditor.vue'
39
+
40
+ const props = defineProps({
41
+ modelValue: {
42
+ type: [Object, Array],
43
+ default: () => ({})
44
+ },
45
+ jsonValue: {
46
+ type: [Object, Array],
47
+ default: null
48
+ },
49
+ mode: {
50
+ type: String,
51
+ default: null
52
+ },
53
+ label: {
54
+ type: String,
55
+ default: null
56
+ },
57
+ jsonHeight: {
58
+ type: String,
59
+ default: '400px'
60
+ },
61
+ jsonMode: {
62
+ type: String,
63
+ default: 'text',
64
+ validator: (v) => ['tree', 'text'].includes(v)
65
+ },
66
+ defaultMode: {
67
+ type: String,
68
+ default: 'structured',
69
+ validator: (v) => ['structured', 'json'].includes(v)
70
+ },
71
+ showToggle: {
72
+ type: Boolean,
73
+ default: true
74
+ }
75
+ })
76
+
77
+ const emit = defineEmits(['update:modelValue', 'update:jsonValue', 'update:mode'])
78
+
79
+ // Internal view mode (used when not controlled externally)
80
+ const internalMode = ref(props.defaultMode)
81
+
82
+ // Computed view mode - use external if provided, else internal
83
+ const viewMode = computed({
84
+ get: () => props.mode ?? internalMode.value,
85
+ set: (val) => {
86
+ if (props.mode !== null) {
87
+ emit('update:mode', val)
88
+ } else {
89
+ internalMode.value = val
90
+ }
91
+ }
92
+ })
93
+
94
+ const viewModeOptions = [
95
+ { label: 'Structured', value: 'structured', icon: 'pi pi-list' },
96
+ { label: 'JSON', value: 'json', icon: 'pi pi-code' }
97
+ ]
98
+
99
+ // Computed JSON value - use separate jsonValue if provided, else modelValue
100
+ const effectiveJsonValue = computed(() => {
101
+ return props.jsonValue !== null ? props.jsonValue : props.modelValue
102
+ })
103
+
104
+ // Handle JSON editor updates
105
+ function onJsonUpdate(newValue) {
106
+ if (props.jsonValue !== null) {
107
+ // Using separate jsonValue binding
108
+ emit('update:jsonValue', newValue)
109
+ } else {
110
+ // Using modelValue for both
111
+ emit('update:modelValue', newValue)
112
+ }
113
+ }
114
+
115
+ // Forward modelValue updates (for structured view)
116
+ // eslint-disable-next-line no-unused-vars
117
+ function emitUpdate(newValue) {
118
+ emit('update:modelValue', newValue)
119
+ }
120
+
121
+ // Sync internal mode with prop if it changes
122
+ watch(() => props.mode, (newMode) => {
123
+ if (newMode !== null) {
124
+ internalMode.value = newMode
125
+ }
126
+ })
127
+ </script>
128
+
129
+ <template>
130
+ <div class="json-structured-field">
131
+ <!-- Header with label and mode toggle -->
132
+ <div v-if="showToggle || label" class="field-header">
133
+ <label v-if="label" class="field-label">{{ label }}</label>
134
+ <SelectButton
135
+ v-if="showToggle"
136
+ v-model="viewMode"
137
+ :options="viewModeOptions"
138
+ optionLabel="label"
139
+ optionValue="value"
140
+ :allowEmpty="false"
141
+ class="mode-toggle"
142
+ >
143
+ <template #option="{ option }">
144
+ <i :class="option.icon"></i>
145
+ <span class="toggle-label">{{ option.label }}</span>
146
+ </template>
147
+ </SelectButton>
148
+ </div>
149
+
150
+ <!-- JSON View -->
151
+ <div v-if="viewMode === 'json'" class="json-view">
152
+ <VanillaJsonEditor
153
+ :modelValue="effectiveJsonValue"
154
+ @update:modelValue="onJsonUpdate"
155
+ :mode="jsonMode"
156
+ :height="jsonHeight"
157
+ />
158
+ </div>
159
+
160
+ <!-- Structured View (slot) -->
161
+ <div v-else class="structured-view">
162
+ <slot></slot>
163
+ </div>
164
+ </div>
165
+ </template>
166
+
167
+ <style scoped>
168
+ .json-structured-field {
169
+ width: 100%;
170
+ }
171
+
172
+ .field-header {
173
+ display: flex;
174
+ justify-content: space-between;
175
+ align-items: center;
176
+ margin-bottom: 0.75rem;
177
+ flex-wrap: wrap;
178
+ gap: 0.5rem;
179
+ }
180
+
181
+ .field-label {
182
+ font-weight: 600;
183
+ font-size: 0.95rem;
184
+ }
185
+
186
+ .mode-toggle {
187
+ flex-shrink: 0;
188
+ }
189
+
190
+ /* Smaller toggle buttons */
191
+ .mode-toggle :deep(.p-button) {
192
+ padding: 0.35rem 0.6rem;
193
+ font-size: 0.8rem;
194
+ }
195
+
196
+ .mode-toggle :deep(.p-button i) {
197
+ font-size: 0.75rem;
198
+ margin-right: 0.25rem;
199
+ }
200
+
201
+ .toggle-label {
202
+ display: none;
203
+ }
204
+
205
+ @media (min-width: 640px) {
206
+ .toggle-label {
207
+ display: inline;
208
+ }
209
+ }
210
+
211
+ .json-view {
212
+ margin-top: 0.25rem;
213
+ }
214
+
215
+ .structured-view {
216
+ margin-top: 0.25rem;
217
+ }
218
+ </style>