@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,84 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * CopyableId - Read-only ID field with copy-to-clipboard functionality
4
+ *
5
+ * Usage:
6
+ * <CopyableId :value="entityId" label="ID" />
7
+ * <CopyableId :value="apiKey" label="API Key" />
8
+ */
9
+ import { ref, inject } from 'vue'
10
+ import Button from 'primevue/button'
11
+ import type { Orchestrator } from '../../orchestrator/Orchestrator'
12
+
13
+ interface Props {
14
+ value: string
15
+ label?: string
16
+ }
17
+
18
+ const props = withDefaults(defineProps<Props>(), {
19
+ label: 'ID'
20
+ })
21
+
22
+ const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
23
+ const copied = ref(false)
24
+
25
+ async function copyToClipboard(): Promise<void> {
26
+ try {
27
+ await navigator.clipboard.writeText(props.value)
28
+ copied.value = true
29
+ orchestrator?.toast.success('Copied', `${props.label} copied to clipboard`, 'CopyableId')
30
+ setTimeout(() => {
31
+ copied.value = false
32
+ }, 2000)
33
+ } catch {
34
+ orchestrator?.toast.error('Error', 'Failed to copy to clipboard', 'CopyableId')
35
+ }
36
+ }
37
+ </script>
38
+
39
+ <template>
40
+ <div class="form-field">
41
+ <label class="form-field-label">{{ label }}</label>
42
+ <div class="copyable-id">
43
+ <Button
44
+ type="button"
45
+ :icon="copied ? 'pi pi-check' : 'pi pi-copy'"
46
+ :severity="copied ? 'success' : 'secondary'"
47
+ size="small"
48
+ text
49
+ rounded
50
+ @click="copyToClipboard"
51
+ :aria-label="`Copy ${label}`"
52
+ class="copyable-id-button"
53
+ />
54
+ <code class="copyable-id-value">{{ value }}</code>
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <style scoped>
60
+ /*
61
+ * Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
62
+ * Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
63
+ */
64
+ .copyable-id {
65
+ display: inline-flex;
66
+ align-items: center;
67
+ gap: 0.5rem;
68
+ background: var(--p-surface-100);
69
+ padding: 0.5rem 0.75rem;
70
+ border-radius: 4px;
71
+ max-width: 100%;
72
+ }
73
+
74
+ .copyable-id-value {
75
+ font-family: monospace;
76
+ font-size: 0.875rem;
77
+ color: var(--p-surface-700);
78
+ word-break: break-all;
79
+ }
80
+
81
+ .copyable-id-button {
82
+ flex-shrink: 0;
83
+ }
84
+ </style>
@@ -0,0 +1,62 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * EmptyState - Reusable empty state display
4
+ *
5
+ * Usage:
6
+ * <EmptyState
7
+ * icon="pi-users"
8
+ * title="No agents found"
9
+ * description="Assign agents from their profile page"
10
+ * />
11
+ *
12
+ * <!-- With slot for custom content -->
13
+ * <EmptyState icon="pi-inbox" title="No items">
14
+ * <Button label="Create Item" @click="create" />
15
+ * </EmptyState>
16
+ */
17
+
18
+ type SizeVariant = 'sm' | 'md' | 'lg'
19
+
20
+ defineProps({
21
+ // PrimeIcon name (without "pi-" prefix if you want, both work)
22
+ icon: {
23
+ type: String,
24
+ default: 'inbox'
25
+ },
26
+ // Main message
27
+ title: {
28
+ type: String,
29
+ default: 'No items found'
30
+ },
31
+ // Secondary description (optional)
32
+ description: {
33
+ type: String,
34
+ default: ''
35
+ },
36
+ // Size variant
37
+ size: {
38
+ type: String as () => SizeVariant,
39
+ default: 'md', // sm, md, lg
40
+ validator: (v: string) => ['sm', 'md', 'lg'].includes(v)
41
+ }
42
+ })
43
+
44
+ // Normalize icon name (accept both "pi-inbox" and "inbox")
45
+ function getIconClass(icon: string): string {
46
+ if (icon.startsWith('pi-')) {
47
+ return `pi ${icon}`
48
+ }
49
+ return `pi pi-${icon}`
50
+ }
51
+ </script>
52
+
53
+ <template>
54
+ <div class="empty-state" :class="`empty-state--${size}`">
55
+ <i :class="getIconClass(icon)" class="empty-state-icon" />
56
+ <p class="empty-state-title">{{ title }}</p>
57
+ <small v-if="description" class="empty-state-description">{{ description }}</small>
58
+ <div v-if="$slots.default" class="empty-state-actions">
59
+ <slot />
60
+ </div>
61
+ </div>
62
+ </template>
@@ -0,0 +1,190 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, inject } from 'vue'
3
+
4
+ type SizeVariant = 'sm' | 'md' | 'lg'
5
+ type Severity = 'success' | 'warning' | 'danger'
6
+
7
+ interface ApiAdapter {
8
+ request: (method: string, endpoint: string) => Promise<Record<string, unknown>>
9
+ }
10
+
11
+ interface BarProps {
12
+ percentage: number
13
+ tier: number
14
+ color: string
15
+ severity: Severity
16
+ threshold: number
17
+ }
18
+
19
+ const props = defineProps({
20
+ value: {
21
+ type: Number,
22
+ required: true
23
+ },
24
+ showValue: {
25
+ type: Boolean,
26
+ default: true
27
+ },
28
+ size: {
29
+ type: String as () => SizeVariant,
30
+ default: 'md', // sm, md, lg
31
+ validator: (v: string) => ['sm', 'md', 'lg'].includes(v)
32
+ },
33
+ // Threshold configuration
34
+ threshold: {
35
+ type: Number as () => number | null,
36
+ default: null // If provided, use static threshold instead of loading from API
37
+ },
38
+ configEndpoint: {
39
+ type: String as () => string | null,
40
+ default: null // Endpoint to load threshold config from API
41
+ },
42
+ configThresholdKey: {
43
+ type: String,
44
+ default: 'threshold' // Key in config response containing threshold value
45
+ }
46
+ })
47
+
48
+ // Get API adapter (optional - only needed if loading from endpoint)
49
+ const api = inject<ApiAdapter | null>('apiAdapter', null)
50
+
51
+ // Threshold value
52
+ const loadedThreshold = ref<number>(8) // default fallback
53
+ const configLoaded = ref<boolean>(false)
54
+
55
+ // Computed threshold - use prop if provided, otherwise loaded value
56
+ const currentThreshold = computed<number>(() => props.threshold ?? loadedThreshold.value)
57
+
58
+ // Singleton pattern - load config once for all instances
59
+ let configPromise: Promise<void> | null = null
60
+
61
+ async function loadConfig(): Promise<void> {
62
+ // Skip if using static threshold or no endpoint configured
63
+ if (props.threshold !== null || !props.configEndpoint) {
64
+ configLoaded.value = true
65
+ return
66
+ }
67
+
68
+ // Skip if no API adapter
69
+ if (!api) {
70
+ console.warn('[IntensityBar] apiAdapter not provided, using default threshold')
71
+ configLoaded.value = true
72
+ return
73
+ }
74
+
75
+ if (configPromise) return configPromise
76
+
77
+ configPromise = api.request('GET', props.configEndpoint)
78
+ .then((data: Record<string, unknown>) => {
79
+ if (data?.[props.configThresholdKey]) {
80
+ loadedThreshold.value = data[props.configThresholdKey] as number
81
+ }
82
+ configLoaded.value = true
83
+ })
84
+ .catch(() => {
85
+ console.warn('[IntensityBar] Could not load config, using defaults')
86
+ configLoaded.value = true
87
+ })
88
+
89
+ return configPromise
90
+ }
91
+
92
+ // Calculate bar properties based on threshold tiers
93
+ const barProps = computed<BarProps>(() => {
94
+ const threshold = currentThreshold.value
95
+ const tier = Math.floor(props.value / threshold)
96
+ const withinTier = props.value % threshold
97
+ const percentage = (withinTier / threshold) * 100
98
+
99
+ // Color: green (tier 0), orange (tier 1), red (tier 2+)
100
+ let color = 'var(--p-green-500)'
101
+ let severity: Severity = 'success'
102
+ if (tier === 1) {
103
+ color = 'var(--p-orange-500)'
104
+ severity = 'warning'
105
+ } else if (tier >= 2) {
106
+ color = 'var(--p-red-500)'
107
+ severity = 'danger'
108
+ }
109
+
110
+ return { percentage, tier, color, severity, threshold }
111
+ })
112
+
113
+ // Size classes
114
+ const sizeClass = computed<string>(() => `intensity-bar--${props.size}`)
115
+
116
+ onMounted(() => {
117
+ loadConfig()
118
+ })
119
+ </script>
120
+
121
+ <template>
122
+ <div class="intensity-container" :class="sizeClass">
123
+ <div
124
+ class="intensity-bar"
125
+ :style="{ '--bar-color': barProps.color, '--bar-width': barProps.percentage + '%' }"
126
+ :title="`Tier ${barProps.tier} (threshold: ${barProps.threshold})`"
127
+ >
128
+ <div class="intensity-bar-fill"></div>
129
+ </div>
130
+ <span v-if="showValue" class="intensity-value" :style="{ color: barProps.color }">
131
+ {{ value.toFixed(2) }}
132
+ </span>
133
+ </div>
134
+ </template>
135
+
136
+ <style scoped>
137
+ /*
138
+ * Only keep styles here that REQUIRE scoping (:deep, dynamic binding, component-specific overrides).
139
+ * Generic/reusable styles belong in src/styles/ partials (see _forms.scss, _cards.scss, etc.).
140
+ */
141
+ .intensity-container {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 8px;
145
+ }
146
+
147
+ .intensity-bar {
148
+ background: var(--p-surface-200);
149
+ border-radius: 4px;
150
+ overflow: hidden;
151
+ /* Default size (md) */
152
+ width: 80px;
153
+ height: 8px;
154
+ }
155
+
156
+ .intensity-bar--sm .intensity-bar {
157
+ width: 60px;
158
+ height: 6px;
159
+ }
160
+
161
+ .intensity-bar--lg .intensity-bar {
162
+ width: 120px;
163
+ height: 10px;
164
+ }
165
+
166
+ .intensity-bar-fill {
167
+ height: 100%;
168
+ width: var(--bar-width);
169
+ background: var(--bar-color);
170
+ border-radius: 4px;
171
+ transition: width 0.3s ease, background 0.3s ease;
172
+ }
173
+
174
+ .intensity-value {
175
+ font-weight: 600;
176
+ min-width: 45px;
177
+ }
178
+
179
+ .intensity-bar--sm .intensity-value {
180
+ font-size: 0.8em;
181
+ }
182
+
183
+ .intensity-bar--md .intensity-value {
184
+ font-size: 0.9em;
185
+ }
186
+
187
+ .intensity-bar--lg .intensity-value {
188
+ font-size: 1em;
189
+ }
190
+ </style>
@@ -0,0 +1,101 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * RichCardsGrid - Grid of rich stat cards with title, value, subtitle
4
+ *
5
+ * Props:
6
+ * - cards: Array of card objects
7
+ * - columns: Number of columns (2, 3, 4, or 'auto')
8
+ *
9
+ * Card object:
10
+ * {
11
+ * name: 'users', // unique key
12
+ * title: 'Users', // card title
13
+ * value: 42, // main value
14
+ * subtitle: '10 active', // optional subtitle
15
+ * icon: 'pi pi-users', // optional icon
16
+ * severity: 'success', // optional: success, danger, warning, info
17
+ * onClick: () => {}, // optional click handler
18
+ * to: '/dashboard/users', // optional router link
19
+ * custom: true // use slot instead
20
+ * }
21
+ */
22
+ import { useRouter } from 'vue-router'
23
+ import type { PropType } from 'vue'
24
+
25
+ type CardSeverity = 'success' | 'danger' | 'warning' | 'info'
26
+ type ColumnsType = 'auto' | 2 | 3 | 4 | 5 | 6
27
+
28
+ export interface RichCard {
29
+ name: string
30
+ title?: string
31
+ value?: string | number
32
+ subtitle?: string
33
+ icon?: string
34
+ severity?: CardSeverity
35
+ onClick?: () => void
36
+ to?: string
37
+ custom?: boolean
38
+ class?: string
39
+ }
40
+
41
+ const router = useRouter()
42
+
43
+ defineProps({
44
+ cards: {
45
+ type: Array as PropType<RichCard[]>,
46
+ default: () => []
47
+ },
48
+ columns: {
49
+ type: [Number, String] as PropType<ColumnsType>,
50
+ default: 'auto',
51
+ validator: (v: ColumnsType) => ['auto', 2, 3, 4, 5, 6].includes(v)
52
+ }
53
+ })
54
+
55
+ function handleClick(card: RichCard): void {
56
+ if (card.onClick) {
57
+ card.onClick()
58
+ } else if (card.to) {
59
+ router.push(card.to)
60
+ }
61
+ }
62
+
63
+ function isClickable(card: RichCard): boolean {
64
+ return !!(card.onClick || card.to)
65
+ }
66
+
67
+ function getSeverityClass(severity: CardSeverity | undefined): string {
68
+ if (!severity) return ''
69
+ return `rich-card--${severity}`
70
+ }
71
+ </script>
72
+
73
+ <template>
74
+ <div v-if="cards.length > 0" class="rich-cards-grid" :class="`rich-cards-grid--cols-${columns}`">
75
+ <template v-for="card in cards" :key="card.name">
76
+ <!-- Custom card: render slot -->
77
+ <div v-if="card.custom" class="rich-card rich-card--custom" :class="card.class">
78
+ <slot :name="`card-${card.name}`" :card="card" ></slot>
79
+ </div>
80
+
81
+ <!-- Rich stat card -->
82
+ <div
83
+ v-else
84
+ class="rich-card"
85
+ :class="[
86
+ getSeverityClass(card.severity),
87
+ { 'rich-card--clickable': isClickable(card) },
88
+ card.class
89
+ ]"
90
+ @click="handleClick(card)"
91
+ >
92
+ <div class="rich-card-header">
93
+ <span class="rich-card-title">{{ card.title }}</span>
94
+ <i v-if="card.icon" :class="card.icon" class="rich-card-icon"></i>
95
+ </div>
96
+ <div class="rich-card-value">{{ card.value }}</div>
97
+ <div v-if="card.subtitle" class="rich-card-subtitle">{{ card.subtitle }}</div>
98
+ </div>
99
+ </template>
100
+ </div>
101
+ </template>
@@ -0,0 +1,78 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * FormActions - Reusable form action buttons
4
+ *
5
+ * Props:
6
+ * - isEdit: Boolean - Edit mode (changes labels)
7
+ * - saving: Boolean - Loading state
8
+ * - dirty: Boolean - Form has unsaved changes
9
+ *
10
+ * Emits:
11
+ * - save: Save and stay on form
12
+ * - saveAndClose: Save and navigate back
13
+ * - cancel: Cancel/close without saving
14
+ */
15
+
16
+ import Button from 'primevue/button'
17
+ import { useI18n } from '../../i18n/useI18n'
18
+
19
+ const { t } = useI18n()
20
+
21
+ interface Props {
22
+ isEdit?: boolean
23
+ saving?: boolean
24
+ dirty?: boolean
25
+ showSaveAndClose?: boolean
26
+ }
27
+
28
+ withDefaults(defineProps<Props>(), {
29
+ isEdit: false,
30
+ saving: false,
31
+ dirty: true, // Default true for backwards compatibility
32
+ showSaveAndClose: true
33
+ })
34
+
35
+ const emit = defineEmits<{
36
+ 'save': []
37
+ 'saveAndClose': []
38
+ 'cancel': []
39
+ }>()
40
+ </script>
41
+
42
+ <template>
43
+ <div class="form-actions">
44
+ <div class="form-actions-left">
45
+ <Button
46
+ type="button"
47
+ :label="isEdit ? t('core.actions.update') : t('core.actions.create')"
48
+ :loading="saving"
49
+ :disabled="!dirty || saving"
50
+ icon="pi pi-check"
51
+ @click="emit('save')"
52
+ v-tooltip.top="t('core.tooltips.saveAndContinue')"
53
+ />
54
+ <Button
55
+ v-if="showSaveAndClose"
56
+ type="button"
57
+ :label="isEdit ? t('core.actions.updateAndClose') : t('core.actions.createAndClose')"
58
+ :loading="saving"
59
+ :disabled="!dirty || saving"
60
+ icon="pi pi-check-circle"
61
+ severity="success"
62
+ @click="emit('saveAndClose')"
63
+ v-tooltip.top="t('core.tooltips.saveAndReturn')"
64
+ />
65
+ <span v-if="dirty" class="dirty-indicator" v-tooltip.top="t('core.tooltips.unsavedChanges')">
66
+ <i class="pi pi-circle-fill"></i>
67
+ </span>
68
+ </div>
69
+ <Button
70
+ type="button"
71
+ :label="t('core.actions.cancel')"
72
+ severity="secondary"
73
+ icon="pi pi-times"
74
+ @click="emit('cancel')"
75
+ :disabled="saving"
76
+ />
77
+ </div>
78
+ </template>
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * FormField - Wrapper for form fields with automatic dirty state and error display
4
+ *
5
+ * Usage:
6
+ * <FormField name="username" label="Username *">
7
+ * <InputText v-model="form.username" />
8
+ * </FormField>
9
+ *
10
+ * The parent form (useEntityItemFormPage) provides:
11
+ * - isFieldDirty: function to check if field is dirty
12
+ * - getFieldError: function to get field error message
13
+ * - handleFieldBlur: function to trigger validation on blur
14
+ * - formSubmitted: ref indicating if form was submitted
15
+ */
16
+ import { inject, computed, type Ref, type CSSProperties } from 'vue'
17
+
18
+ interface Props {
19
+ name: string
20
+ label?: string
21
+ hint?: string
22
+ fullWidth?: boolean
23
+ /** Override error message (useful for custom validation) */
24
+ error?: string | null
25
+ /** Show error only after form submission */
26
+ showErrorOnSubmit?: boolean
27
+ }
28
+
29
+ const props = withDefaults(defineProps<Props>(), {
30
+ label: '',
31
+ hint: '',
32
+ fullWidth: false,
33
+ error: null,
34
+ showErrorOnSubmit: false
35
+ })
36
+
37
+ // Inject from parent form (provided by useEntityItemFormPage)
38
+ const isFieldDirty = inject<(name: string) => boolean>('isFieldDirty', () => false)
39
+ const getFieldError = inject<(name: string) => string | null>('getFieldError', () => null)
40
+ const handleFieldBlur = inject<(name: string) => void>('handleFieldBlur', () => {})
41
+ const formSubmitted = inject<Ref<boolean>>('formSubmitted', { value: false } as Ref<boolean>)
42
+
43
+ const isDirty = computed((): boolean => isFieldDirty(props.name))
44
+
45
+ // Get error from prop or from form validation
46
+ const fieldError = computed((): string | null => {
47
+ if (props.error) return props.error
48
+ return getFieldError(props.name)
49
+ })
50
+
51
+ // Show error if: form submitted OR field was touched (validated on blur)
52
+ const showError = computed((): boolean => {
53
+ if (!fieldError.value) return false
54
+ if (props.showErrorOnSubmit) return formSubmitted.value
55
+ return true
56
+ })
57
+
58
+ const fieldClasses = computed((): (string | Record<string, boolean>)[] => [
59
+ 'form-field',
60
+ {
61
+ 'field-dirty': isDirty.value,
62
+ 'field-invalid': showError.value
63
+ }
64
+ ])
65
+
66
+ const fieldStyle = computed((): CSSProperties =>
67
+ props.fullWidth ? { gridColumn: '1 / -1' } : {}
68
+ )
69
+
70
+ function onBlur(): void {
71
+ handleFieldBlur(props.name)
72
+ }
73
+ </script>
74
+
75
+ <template>
76
+ <div :class="fieldClasses" :style="fieldStyle">
77
+ <label v-if="label" :for="name">{{ label }}</label>
78
+ <slot :onBlur="onBlur"></slot>
79
+ <small v-if="showError" class="field-error">{{ fieldError }}</small>
80
+ <small v-else-if="hint" class="field-hint">{{ hint }}</small>
81
+ </div>
82
+ </template>