@quazardous/qdadm 1.19.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 (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. package/src/zones/zones.ts +191 -0
@@ -0,0 +1,264 @@
1
+ <script setup lang="ts">
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
+ * - guardInvalidJson: Block switching to structured view while JSON is invalid (default: true)
35
+ * - invalidJsonMessage: Error message shown when JSON is invalid (default: 'Invalid JSON — fix errors before switching views')
36
+ *
37
+ * Events:
38
+ * - update:modelValue, update:jsonValue, update:mode: standard v-model
39
+ * - json-error: emitted with true when JSON becomes invalid, false when valid again
40
+ */
41
+
42
+ import { ref, computed, watch } from 'vue'
43
+ import SelectButton from 'primevue/selectbutton'
44
+ import Message from 'primevue/message'
45
+ import VanillaJsonEditor from './VanillaJsonEditor.vue'
46
+ import type { Mode } from 'vanilla-jsoneditor'
47
+
48
+ type JsonValue = Record<string, unknown> | unknown[] | string | null
49
+ type ViewMode = 'structured' | 'json'
50
+
51
+ interface ViewModeOption {
52
+ label: string
53
+ value: ViewMode
54
+ icon: string
55
+ }
56
+
57
+ interface Props {
58
+ modelValue?: JsonValue
59
+ jsonValue?: JsonValue | null
60
+ mode?: ViewMode | null
61
+ label?: string | null
62
+ jsonHeight?: string
63
+ jsonMode?: Mode
64
+ defaultMode?: ViewMode
65
+ showToggle?: boolean
66
+ guardInvalidJson?: boolean
67
+ invalidJsonMessage?: string
68
+ }
69
+
70
+ const props = withDefaults(defineProps<Props>(), {
71
+ modelValue: () => ({}),
72
+ jsonValue: null,
73
+ mode: null,
74
+ label: null,
75
+ jsonHeight: '400px',
76
+ jsonMode: 'text' as Mode,
77
+ defaultMode: 'structured',
78
+ showToggle: true,
79
+ guardInvalidJson: true,
80
+ invalidJsonMessage: 'Invalid JSON — fix errors before switching views'
81
+ })
82
+
83
+ const emit = defineEmits<{
84
+ 'update:modelValue': [value: JsonValue]
85
+ 'update:jsonValue': [value: JsonValue]
86
+ 'update:mode': [value: ViewMode]
87
+ 'json-error': [hasError: boolean]
88
+ }>()
89
+
90
+ // Internal view mode (used when not controlled externally)
91
+ const internalMode = ref<ViewMode>(props.defaultMode)
92
+
93
+ // Invalid JSON state, tracked from VanillaJsonEditor error/update events
94
+ const hasJsonError = ref<boolean>(false)
95
+
96
+ // Computed view mode - use external if provided, else internal
97
+ const viewMode = computed<ViewMode>({
98
+ get: () => props.mode ?? internalMode.value,
99
+ set: (val: ViewMode) => {
100
+ // Guard: block json → structured while JSON is invalid
101
+ if (
102
+ props.guardInvalidJson &&
103
+ hasJsonError.value &&
104
+ (props.mode ?? internalMode.value) === 'json' &&
105
+ val === 'structured'
106
+ ) {
107
+ return
108
+ }
109
+ if (props.mode !== null) {
110
+ emit('update:mode', val)
111
+ } else {
112
+ internalMode.value = val
113
+ }
114
+ }
115
+ })
116
+
117
+ const viewModeOptions: ViewModeOption[] = [
118
+ { label: 'Structured', value: 'structured', icon: 'pi pi-list' },
119
+ { label: 'JSON', value: 'json', icon: 'pi pi-code' }
120
+ ]
121
+
122
+ // Computed JSON value - use separate jsonValue if provided, else modelValue
123
+ const effectiveJsonValue = computed<JsonValue>(() => {
124
+ return props.jsonValue !== null ? props.jsonValue : props.modelValue
125
+ })
126
+
127
+ // Handle JSON editor updates (fires only when JSON is valid)
128
+ function onJsonUpdate(newValue: JsonValue): void {
129
+ if (hasJsonError.value) {
130
+ hasJsonError.value = false
131
+ emit('json-error', false)
132
+ }
133
+ if (props.jsonValue !== null) {
134
+ // Using separate jsonValue binding
135
+ emit('update:jsonValue', newValue)
136
+ } else {
137
+ // Using modelValue for both
138
+ emit('update:modelValue', newValue)
139
+ }
140
+ }
141
+
142
+ // Track invalid JSON state from the editor
143
+ function onJsonError(): void {
144
+ if (!hasJsonError.value) {
145
+ hasJsonError.value = true
146
+ emit('json-error', true)
147
+ }
148
+ }
149
+
150
+ // Sync internal mode with prop if it changes
151
+ watch(() => props.mode, (newMode: ViewMode | null | undefined) => {
152
+ if (newMode !== null && newMode !== undefined) {
153
+ internalMode.value = newMode
154
+ }
155
+ })
156
+ </script>
157
+
158
+ <template>
159
+ <div class="json-structured-field">
160
+ <!-- Header with label and mode toggle -->
161
+ <div v-if="showToggle || label" class="field-header">
162
+ <label v-if="label" class="field-label">{{ label }}</label>
163
+ <SelectButton
164
+ v-if="showToggle"
165
+ v-model="viewMode"
166
+ :options="viewModeOptions"
167
+ optionLabel="label"
168
+ optionValue="value"
169
+ :allowEmpty="false"
170
+ class="mode-toggle"
171
+ >
172
+ <template #option="{ option }">
173
+ <i :class="option.icon"></i>
174
+ <span class="toggle-label">{{ option.label }}</span>
175
+ </template>
176
+ </SelectButton>
177
+ </div>
178
+
179
+ <!-- JSON View -->
180
+ <div v-if="viewMode === 'json'" class="json-view">
181
+ <Message
182
+ v-if="guardInvalidJson && hasJsonError"
183
+ severity="error"
184
+ :closable="false"
185
+ class="json-error"
186
+ >
187
+ {{ invalidJsonMessage }}
188
+ </Message>
189
+ <VanillaJsonEditor
190
+ :modelValue="effectiveJsonValue"
191
+ :mode="jsonMode"
192
+ :height="jsonHeight"
193
+ @update:modelValue="onJsonUpdate"
194
+ @error="onJsonError"
195
+ />
196
+ </div>
197
+
198
+ <!-- Structured View (slot) -->
199
+ <div v-else class="structured-view">
200
+ <slot></slot>
201
+ </div>
202
+ </div>
203
+ </template>
204
+
205
+ <style scoped>
206
+ /*
207
+ * Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
208
+ * Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
209
+ */
210
+ .json-structured-field {
211
+ width: 100%;
212
+ }
213
+
214
+ .field-header {
215
+ display: flex;
216
+ justify-content: space-between;
217
+ align-items: center;
218
+ margin-bottom: 0.75rem;
219
+ flex-wrap: wrap;
220
+ gap: 0.5rem;
221
+ }
222
+
223
+ .field-label {
224
+ font-weight: 600;
225
+ font-size: 0.95rem;
226
+ }
227
+
228
+ .mode-toggle {
229
+ flex-shrink: 0;
230
+ }
231
+
232
+ /* Smaller toggle buttons */
233
+ .mode-toggle :deep(.p-button) {
234
+ padding: 0.35rem 0.6rem;
235
+ font-size: 0.8rem;
236
+ }
237
+
238
+ .mode-toggle :deep(.p-button i) {
239
+ font-size: 0.75rem;
240
+ margin-right: 0.25rem;
241
+ }
242
+
243
+ .toggle-label {
244
+ display: none;
245
+ }
246
+
247
+ @media (min-width: 640px) {
248
+ .toggle-label {
249
+ display: inline;
250
+ }
251
+ }
252
+
253
+ .json-view {
254
+ margin-top: 0.25rem;
255
+ }
256
+
257
+ .json-error {
258
+ margin-bottom: 0.5rem;
259
+ }
260
+
261
+ .structured-view {
262
+ margin-top: 0.25rem;
263
+ }
264
+ </style>
@@ -0,0 +1,51 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * JsonViewer - Reusable JSON viewer component
4
+ *
5
+ * Simple formatted JSON display with syntax highlighting.
6
+ *
7
+ * Usage:
8
+ * <JsonViewer :model-value="jsonData" height="300px" />
9
+ */
10
+
11
+ import { computed } from 'vue'
12
+ import { highlightJson } from '../../composables/useJsonSyntax'
13
+
14
+ type JsonValue = Record<string, unknown> | unknown[] | string | number | boolean | null
15
+
16
+ interface Props {
17
+ modelValue?: JsonValue
18
+ height?: string
19
+ }
20
+
21
+ const props = withDefaults(defineProps<Props>(), {
22
+ modelValue: () => ({}),
23
+ height: '300px'
24
+ })
25
+
26
+ // Parse value if string
27
+ const parsedValue = computed<JsonValue>(() => {
28
+ if (typeof props.modelValue === 'string') {
29
+ try {
30
+ return JSON.parse(props.modelValue) as JsonValue
31
+ } catch {
32
+ return props.modelValue
33
+ }
34
+ }
35
+ return props.modelValue
36
+ })
37
+
38
+ // Format JSON with syntax highlighting
39
+ const formattedJson = computed<string>(() => {
40
+ if (parsedValue.value === null || parsedValue.value === undefined) {
41
+ return '<span class="json-null">null</span>'
42
+ }
43
+ return highlightJson(JSON.stringify(parsedValue.value, null, 2))
44
+ })
45
+ </script>
46
+
47
+ <template>
48
+ <div class="json-viewer" :style="{ maxHeight: height }">
49
+ <pre class="json-content" v-html="formattedJson"></pre>
50
+ </div>
51
+ </template>
@@ -0,0 +1,314 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * KeyValueEditor - Reusable component for editing key-value pairs
4
+ *
5
+ * Props:
6
+ * - modelValue: Array of {key, value} objects
7
+ * - label: Field label
8
+ * - help: Help text
9
+ * - keyPlaceholder: Placeholder for key input
10
+ * - valuePlaceholder: Placeholder for value input (text mode only)
11
+ * - keySuggestions: Array of strings for autocomplete on key input
12
+ * - valueType: 'number' (default) or 'text'
13
+ * - min: Minimum value (default: 0, number mode only)
14
+ * - max: Maximum value (default: 1, number mode only)
15
+ * - step: Slider step (default: 0.1, number mode only)
16
+ * - showSign: Show +/- sign for values (default: false, number mode only)
17
+ * - colorize: Color negative/positive values (default: false, number mode only)
18
+ */
19
+
20
+ import { ref, computed } from 'vue'
21
+ import Button from 'primevue/button'
22
+ import InputText from 'primevue/inputtext'
23
+ import AutoComplete from 'primevue/autocomplete'
24
+ import Slider from 'primevue/slider'
25
+
26
+ interface KeyValueItem {
27
+ key: string
28
+ value: string | number
29
+ }
30
+
31
+ interface Props {
32
+ modelValue?: KeyValueItem[]
33
+ label?: string
34
+ help?: string
35
+ keyPlaceholder?: string
36
+ valuePlaceholder?: string
37
+ keySuggestions?: string[]
38
+ valueType?: 'number' | 'text'
39
+ min?: number
40
+ max?: number
41
+ step?: number
42
+ showSign?: boolean
43
+ colorize?: boolean
44
+ }
45
+
46
+ const props = withDefaults(defineProps<Props>(), {
47
+ modelValue: () => [],
48
+ label: 'Key-Value Pairs',
49
+ help: '',
50
+ keyPlaceholder: 'Key',
51
+ valuePlaceholder: 'Value',
52
+ keySuggestions: () => [],
53
+ valueType: 'number',
54
+ min: 0,
55
+ max: 1,
56
+ step: 0.1,
57
+ showSign: false,
58
+ colorize: false
59
+ })
60
+
61
+ const emit = defineEmits<{
62
+ 'update:modelValue': [value: KeyValueItem[]]
63
+ }>()
64
+
65
+ const items = computed<KeyValueItem[]>({
66
+ get: () => props.modelValue || [],
67
+ set: (value: KeyValueItem[]) => emit('update:modelValue', value)
68
+ })
69
+
70
+ const isTextMode = computed<boolean>(() => props.valueType === 'text')
71
+ const hasKeySuggestions = computed<boolean>(() => props.keySuggestions.length > 0)
72
+ const filteredKeySuggestions = ref<string[]>([])
73
+
74
+ const newItem = ref<KeyValueItem>({
75
+ key: '',
76
+ value: props.valueType === 'text' ? '' : (props.min + props.max) / 2
77
+ })
78
+
79
+ // Filter key suggestions for autocomplete (exclude already used keys)
80
+ function searchKeys(event: { query: string }): void {
81
+ const query = event.query.toLowerCase()
82
+ const usedKeys = items.value.map(item => item.key)
83
+ filteredKeySuggestions.value = props.keySuggestions
84
+ .filter(k => !usedKeys.includes(k))
85
+ .filter(k => k.toLowerCase().includes(query))
86
+ }
87
+
88
+ function formatValue(value: string | number): string {
89
+ if (isTextMode.value) {
90
+ return String(value)
91
+ }
92
+ const formatted = Number(value).toFixed(2)
93
+ if (props.showSign && Number(value) > 0) {
94
+ return '+' + formatted
95
+ }
96
+ return formatted
97
+ }
98
+
99
+ function addItem(): void {
100
+ if (!newItem.value.key) return
101
+ if (isTextMode.value && !newItem.value.value) return
102
+ const updated = [...items.value, { ...newItem.value }]
103
+ emit('update:modelValue', updated)
104
+ newItem.value = {
105
+ key: '',
106
+ value: isTextMode.value ? '' : (props.min + props.max) / 2
107
+ }
108
+ }
109
+
110
+ function removeItem(index: number): void {
111
+ const updated = items.value.filter((_, i) => i !== index)
112
+ emit('update:modelValue', updated)
113
+ }
114
+
115
+ function updateItemValue(index: number, value: string | number): void {
116
+ const updated = [...items.value]
117
+ const item = updated[index]
118
+ if (item) {
119
+ updated[index] = { key: item.key, value }
120
+ emit('update:modelValue', updated)
121
+ }
122
+ }
123
+ </script>
124
+
125
+ <template>
126
+ <div class="form-field">
127
+ <label>{{ label }}</label>
128
+ <div class="kv-list">
129
+ <div v-if="items.length === 0" class="kv-empty">
130
+ No items added yet
131
+ </div>
132
+ <div v-for="(item, index) in items" :key="index" class="kv-item">
133
+ <span class="kv-key">{{ item.key }}</span>
134
+ <div class="kv-value-container">
135
+ <!-- Text mode: show text input -->
136
+ <template v-if="isTextMode">
137
+ <InputText
138
+ :modelValue="String(item.value)"
139
+ @update:modelValue="(v: string | undefined) => v !== undefined && updateItemValue(index, v)"
140
+ class="kv-text-input"
141
+ />
142
+ </template>
143
+ <!-- Number mode: show slider -->
144
+ <template v-else>
145
+ <span
146
+ class="kv-value"
147
+ :class="{
148
+ negative: colorize && Number(item.value) < 0,
149
+ positive: colorize && Number(item.value) > 0
150
+ }"
151
+ >
152
+ {{ formatValue(item.value) }}
153
+ </span>
154
+ <Slider
155
+ :modelValue="Number(item.value)"
156
+ @update:modelValue="(v: number | number[]) => updateItemValue(index, Array.isArray(v) ? v[0] ?? 0 : v)"
157
+ :min="min"
158
+ :max="max"
159
+ :step="step"
160
+ style="width: 100px"
161
+ />
162
+ </template>
163
+ </div>
164
+ <Button
165
+ icon="pi pi-times"
166
+ severity="danger"
167
+ text
168
+ rounded
169
+ size="small"
170
+ @click="removeItem(index)"
171
+ />
172
+ </div>
173
+ </div>
174
+ <div class="kv-add">
175
+ <Button
176
+ icon="pi pi-plus"
177
+ @click="addItem"
178
+ :disabled="!newItem.key || (isTextMode && !newItem.value)"
179
+ v-tooltip.top="'Add item'"
180
+ />
181
+ <!-- With autocomplete suggestions -->
182
+ <AutoComplete
183
+ v-if="hasKeySuggestions"
184
+ v-model="newItem.key"
185
+ :suggestions="filteredKeySuggestions"
186
+ @complete="searchKeys"
187
+ :placeholder="keyPlaceholder"
188
+ class="kv-add-input"
189
+ @keyup.enter="addItem"
190
+ />
191
+ <!-- Without suggestions: plain input -->
192
+ <InputText
193
+ v-else
194
+ v-model="newItem.key"
195
+ :placeholder="keyPlaceholder"
196
+ class="kv-add-input"
197
+ @keyup.enter="addItem"
198
+ />
199
+ <!-- Text mode: show text input for value -->
200
+ <template v-if="isTextMode">
201
+ <InputText
202
+ :modelValue="String(newItem.value)"
203
+ @update:modelValue="(v: string | undefined) => newItem.value = v ?? ''"
204
+ :placeholder="valuePlaceholder"
205
+ class="kv-add-value"
206
+ @keyup.enter="addItem"
207
+ />
208
+ </template>
209
+ <!-- Number mode: show slider -->
210
+ <template v-else>
211
+ <div class="kv-slider">
212
+ <span>{{ formatValue(newItem.value) }}</span>
213
+ <Slider
214
+ :modelValue="Number(newItem.value)"
215
+ @update:modelValue="(v: number | number[]) => newItem.value = Array.isArray(v) ? v[0] ?? 0 : v"
216
+ :min="min"
217
+ :max="max"
218
+ :step="step"
219
+ style="width: 100px"
220
+ />
221
+ </div>
222
+ </template>
223
+ </div>
224
+ <small v-if="help" class="form-field-help">{{ help }}</small>
225
+ </div>
226
+ </template>
227
+
228
+ <style scoped>
229
+ /*
230
+ * Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
231
+ * Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
232
+ */
233
+ .kv-list {
234
+ display: flex;
235
+ flex-direction: column;
236
+ gap: 0.5rem;
237
+ margin-bottom: 0.5rem;
238
+ }
239
+
240
+ .kv-item {
241
+ display: flex;
242
+ align-items: center;
243
+ gap: 0.5rem;
244
+ padding: 0.5rem;
245
+ background: var(--p-surface-50);
246
+ border-radius: 0.25rem;
247
+ }
248
+
249
+ .kv-key {
250
+ font-weight: 500;
251
+ min-width: 120px;
252
+ }
253
+
254
+ .kv-value-container {
255
+ display: flex;
256
+ align-items: center;
257
+ gap: 0.5rem;
258
+ flex: 1;
259
+ }
260
+
261
+ .kv-value {
262
+ min-width: 50px;
263
+ text-align: right;
264
+ font-family: monospace;
265
+ }
266
+
267
+ .kv-value.negative {
268
+ color: var(--p-red-500);
269
+ }
270
+
271
+ .kv-value.positive {
272
+ color: var(--p-green-500);
273
+ }
274
+
275
+ .kv-add {
276
+ display: flex;
277
+ align-items: center;
278
+ gap: 0.5rem;
279
+ }
280
+
281
+ .kv-add-input {
282
+ flex: 1;
283
+ }
284
+
285
+ .kv-add-value {
286
+ flex: 1;
287
+ min-width: 150px;
288
+ }
289
+
290
+ .kv-text-input {
291
+ flex: 1;
292
+ }
293
+
294
+ .kv-slider {
295
+ display: flex;
296
+ align-items: center;
297
+ gap: 0.5rem;
298
+ }
299
+
300
+ .kv-slider span {
301
+ min-width: 50px;
302
+ text-align: right;
303
+ font-family: monospace;
304
+ }
305
+
306
+ .kv-empty {
307
+ padding: 0.75rem;
308
+ color: var(--p-text-secondary);
309
+ font-style: italic;
310
+ background: var(--p-surface-50);
311
+ border-radius: 0.25rem;
312
+ text-align: center;
313
+ }
314
+ </style>