@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,15 @@
1
+ /**
2
+ * Auth module - User session authentication
3
+ *
4
+ * For entity-level permissions, see entity/auth/EntityAuthAdapter
5
+ */
6
+
7
+ export {
8
+ SessionAuthAdapter,
9
+ LocalStorageSessionAuthAdapter,
10
+ type LoginCredentials,
11
+ type SessionData,
12
+ type AuthUser,
13
+ type ISessionAuthAdapter,
14
+ type LocalStorageAuthConfig,
15
+ } from './SessionAuthAdapter'
@@ -0,0 +1,154 @@
1
+ /**
2
+ * ActiveStack - Sync navigation context from route (qdadm flavour).
3
+ *
4
+ * Composes a generic `Stack<EntityStackLevel>` from `@quazardous/qdcore` and
5
+ * adds qdadm-specific lookups (e.g. `getLevelByEntity`). The underlying levels
6
+ * dual-carry `name` (qdcore-aligned) and `entity` (legacy qdadm field), so all
7
+ * existing consumers continue to read `level.entity` unchanged.
8
+ *
9
+ * Stack only contains levels WITH IDs (entities with context).
10
+ * - /bots/bot-xyz/commands → stack = [bots(id:bot-xyz)]
11
+ * - /bots/bot-xyz/commands/cmd-123 → stack = [bots(id:bot-xyz), commands(id:cmd-123)]
12
+ *
13
+ * Signals emitted (via SignalBus):
14
+ * - stack:change — when stack levels change
15
+ *
16
+ * For Vue reactivity, use the `useActiveStack` composable.
17
+ *
18
+ * @example
19
+ * const stack = new ActiveStack(signalBus)
20
+ * signalBus.on('stack:change', ({ levels }) => console.log('Stack:', levels))
21
+ * stack.set([{ entity: 'bots', param: 'uuid', foreignKey: null, id: 'bot-123' }])
22
+ */
23
+
24
+ import { Stack, type ContentStackLevel } from '@quazardous/qdcore'
25
+ import type { SignalBus } from '../kernel/SignalBus'
26
+
27
+ /**
28
+ * qdadm stack level: entity-bound `ContentStackLevel`.
29
+ *
30
+ * `name` and `entity` always carry the same value. `name` is the qdcore-canonical
31
+ * field; `entity` is preserved for backwards compatibility with qdadm callers.
32
+ */
33
+ export interface EntityStackLevel extends ContentStackLevel {
34
+ type: 'entity'
35
+ /** qdcore-aligned name (= entity) */
36
+ name: string
37
+ /** Entity name (e.g., 'bots', 'commands') — alias of `name` */
38
+ entity: string
39
+ /** Route param name (e.g., 'uuid', 'id') */
40
+ param: string
41
+ /** Foreign key field for parent relation (null for root) */
42
+ foreignKey: string | null
43
+ /** Entity ID from route params */
44
+ id: string | null
45
+ }
46
+
47
+ /**
48
+ * Legacy alias kept for backwards compatibility. New code should prefer
49
+ * {@link EntityStackLevel}.
50
+ */
51
+ export type StackLevel = EntityStackLevel
52
+
53
+ /**
54
+ * Input shape accepted by {@link ActiveStack.set} — `name` is optional and
55
+ * defaults to `entity` so existing callers (`{ entity, param, foreignKey, id }`)
56
+ * keep working without changes.
57
+ */
58
+ export type EntityStackLevelInput = Omit<EntityStackLevel, 'type' | 'name'> & {
59
+ type?: 'entity'
60
+ name?: string
61
+ }
62
+
63
+ /**
64
+ * Stack change event payload
65
+ */
66
+ export interface StackChangePayload {
67
+ levels: EntityStackLevel[]
68
+ }
69
+
70
+ /**
71
+ * Normalize loose input into a fully-typed EntityStackLevel.
72
+ */
73
+ function normalize(input: EntityStackLevelInput): EntityStackLevel {
74
+ return {
75
+ type: 'entity',
76
+ name: input.name ?? input.entity,
77
+ entity: input.entity,
78
+ param: input.param,
79
+ foreignKey: input.foreignKey,
80
+ id: input.id,
81
+ }
82
+ }
83
+
84
+ export class ActiveStack {
85
+ private _stack: Stack<EntityStackLevel>
86
+
87
+ /**
88
+ * @param signalBus - Optional signal bus for events
89
+ */
90
+ constructor(signalBus: SignalBus | null = null) {
91
+ this._stack = new Stack<EntityStackLevel>(signalBus)
92
+ }
93
+
94
+ // ─── Mutators ─────────────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Replace entire stack (called on route change).
98
+ * Only emits if levels actually changed (handled by qdcore Stack).
99
+ * Accepts either fully-typed `EntityStackLevel` or the legacy
100
+ * `{ entity, param, foreignKey, id }` shape.
101
+ */
102
+ set(levels: EntityStackLevelInput[]): void {
103
+ this._stack.set(levels.map(normalize))
104
+ }
105
+
106
+ /**
107
+ * Clear the stack.
108
+ */
109
+ clear(): void {
110
+ this._stack.clear()
111
+ }
112
+
113
+ // ─── Accessors ────────────────────────────────────────────────────────────
114
+
115
+ getLevels(): EntityStackLevel[] {
116
+ return this._stack.getLevels()
117
+ }
118
+
119
+ getLevel(index: number): EntityStackLevel | null {
120
+ return this._stack.getLevel(index)
121
+ }
122
+
123
+ /**
124
+ * Get level by entity name (qdadm-specific helper).
125
+ */
126
+ getLevelByEntity(entity: string): EntityStackLevel | null {
127
+ return this._stack.getLevels().find(l => l.entity === entity) ?? null
128
+ }
129
+
130
+ getCurrent(): EntityStackLevel | null {
131
+ return this._stack.getCurrent()
132
+ }
133
+
134
+ getParent(): EntityStackLevel | null {
135
+ return this._stack.getParent()
136
+ }
137
+
138
+ getRoot(): EntityStackLevel | null {
139
+ return this._stack.getRoot()
140
+ }
141
+
142
+ getDepth(): number {
143
+ return this._stack.getDepth()
144
+ }
145
+
146
+ /**
147
+ * Underlying qdcore Stack instance (escape hatch for advanced use).
148
+ */
149
+ getCoreStack(): Stack<EntityStackLevel> {
150
+ return this._stack
151
+ }
152
+ }
153
+
154
+ export default ActiveStack
@@ -0,0 +1,292 @@
1
+ /**
2
+ * StackHydrator - Async hydration layer for ActiveStack
3
+ *
4
+ * Pure vanilla JS using SignalBus for events.
5
+ * Listens to stack:change and hydrates each level with entity data.
6
+ *
7
+ * Signals emitted:
8
+ * - stack:hydrated - when hydration completes (batched, emits once after all levels) { levels }
9
+ *
10
+ * For Vue reactivity, use useStackHydrator composable.
11
+ *
12
+ * @example
13
+ * const hydrator = new StackHydrator(activeStack, orchestrator, signalBus)
14
+ * signalBus.on('stack:hydrated', (event) => console.log('Hydrated:', event.data.levels))
15
+ */
16
+
17
+ import type { Hydrator } from '@quazardous/qdcore'
18
+ import type { ActiveStack, EntityStackLevel, StackLevel } from './ActiveStack'
19
+ import type { Orchestrator } from '../orchestrator/Orchestrator'
20
+ import type { SignalBus } from '../kernel/SignalBus'
21
+ import type { EntityRecord } from '../types'
22
+
23
+ /**
24
+ * Hydrated level with async state
25
+ */
26
+ export interface HydratedLevel {
27
+ /** Entity name */
28
+ entity: string
29
+ /** Route param name */
30
+ param: string
31
+ /** Foreign key field */
32
+ foreignKey: string | null
33
+ /** Entity ID */
34
+ id: string | null
35
+ /** Resolves when hydrated */
36
+ promise: Promise<void> | null
37
+ /** True while fetching */
38
+ loading: boolean
39
+ /** True when data is loaded */
40
+ hydrated: boolean
41
+ /** Error if fetch failed */
42
+ error: Error | null
43
+ /** Entity data */
44
+ data: EntityRecord | null
45
+ /** Resolved label */
46
+ label: string | null
47
+ }
48
+
49
+ /**
50
+ * Hydrated levels payload
51
+ */
52
+ export interface HydratedPayload {
53
+ levels: HydratedLevel[]
54
+ }
55
+
56
+ export class StackHydrator implements Hydrator<EntityStackLevel, HydratedLevel> {
57
+ private _activeStack: ActiveStack
58
+ private _orchestrator: Orchestrator
59
+ private _signalBus: SignalBus | null
60
+ private _levels: HydratedLevel[] = []
61
+ private _pendingEmit: Promise<void> | null = null
62
+ private _unsubscribe: (() => void) | null = null
63
+
64
+ constructor(
65
+ activeStack: ActiveStack,
66
+ orchestrator: Orchestrator,
67
+ signalBus: SignalBus | null = null
68
+ ) {
69
+ this._activeStack = activeStack
70
+ this._orchestrator = orchestrator
71
+ this._signalBus = signalBus
72
+
73
+ // Listen to stack changes via SignalBus
74
+ // QuarKernel passes (event, ctx) where event.data contains the payload
75
+ if (signalBus) {
76
+ this._unsubscribe = signalBus.on('stack:change', (event) => {
77
+ const levels = (event.data as { levels?: StackLevel[] })?.levels
78
+ if (levels) {
79
+ this._onStackChange(levels)
80
+ }
81
+ })
82
+ }
83
+ // Note: No initial check needed - stack is always empty at construction time
84
+ // The Kernel's router.afterEach will trigger stack:change after navigation
85
+ }
86
+
87
+ /**
88
+ * Cleanup
89
+ */
90
+ destroy(): void {
91
+ if (this._unsubscribe) {
92
+ this._unsubscribe()
93
+ this._unsubscribe = null
94
+ }
95
+ }
96
+
97
+ // ═══════════════════════════════════════════════════════════════════════════
98
+ // Stack change handling
99
+ // ═══════════════════════════════════════════════════════════════════════════
100
+
101
+ /**
102
+ * Handle stack change - rebuild hydrated levels
103
+ * @private
104
+ */
105
+ private _onStackChange(stackLevels: StackLevel[]): void {
106
+ // Create new hydrated levels (each starts its own async hydration)
107
+ this._levels = stackLevels.map((level, index) =>
108
+ this._createHydratedLevel(level, index)
109
+ )
110
+ // Don't emit here - each level will emit when hydration completes
111
+ }
112
+
113
+ /**
114
+ * Create a hydrated level with its own promise
115
+ * @private
116
+ */
117
+ private _createHydratedLevel(stackLevel: StackLevel, _index: number): HydratedLevel {
118
+ const level: HydratedLevel = {
119
+ // Sync config (copied from stack)
120
+ entity: stackLevel.entity,
121
+ param: stackLevel.param,
122
+ foreignKey: stackLevel.foreignKey,
123
+ id: stackLevel.id,
124
+
125
+ // Async state
126
+ promise: null,
127
+ loading: false,
128
+ hydrated: false,
129
+ error: null,
130
+ data: null,
131
+ label: null,
132
+ }
133
+
134
+ // Start hydration and store promise
135
+ level.promise = this._hydrate(level)
136
+
137
+ return level
138
+ }
139
+
140
+ /**
141
+ * Hydrate a single level
142
+ * @private
143
+ */
144
+ private async _hydrate(level: HydratedLevel): Promise<void> {
145
+ // All levels in stack have IDs (that's the new contract)
146
+ // But check anyway for safety
147
+ if (!level.id) {
148
+ const manager = this._orchestrator?.get(level.entity)
149
+ level.label = manager?.labelPlural ?? level.entity
150
+ level.hydrated = true
151
+ this._scheduleEmit()
152
+ return
153
+ }
154
+
155
+ level.loading = true
156
+ // Don't emit on loading start - reduces signal noise
157
+
158
+ try {
159
+ const manager = this._orchestrator?.get(level.entity)
160
+ if (!manager) {
161
+ level.label = level.id
162
+ level.hydrated = true
163
+ level.loading = false
164
+ this._scheduleEmit()
165
+ return
166
+ }
167
+
168
+ // Fetch entity data
169
+ level.data = await manager.get(level.id)
170
+ level.label = manager.getEntityLabel?.(level.data) ?? level.id
171
+ level.hydrated = true
172
+ } catch (err) {
173
+ level.error = err instanceof Error ? err : new Error(String(err))
174
+ level.label = level.id // Fallback to ID
175
+ level.hydrated = true // Mark as hydrated even on error
176
+ } finally {
177
+ level.loading = false
178
+ this._scheduleEmit()
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Schedule batched hydration signal (groups multiple level hydrations)
184
+ * Uses microtask to batch emissions within same tick
185
+ * @private
186
+ */
187
+ private _scheduleEmit(): void {
188
+ if (this._pendingEmit) return // Already scheduled
189
+
190
+ this._pendingEmit = Promise.resolve().then(() => {
191
+ this._pendingEmit = null
192
+ this._emit('stack:hydrated', { levels: this._levels })
193
+ })
194
+ }
195
+
196
+ // ═══════════════════════════════════════════════════════════════════════════
197
+ // Accessors
198
+ // ═══════════════════════════════════════════════════════════════════════════
199
+
200
+ /**
201
+ * All hydrated levels
202
+ */
203
+ getLevels(): HydratedLevel[] {
204
+ return this._levels
205
+ }
206
+
207
+ /**
208
+ * Get hydrated level by index
209
+ */
210
+ getLevel(index: number): HydratedLevel | null {
211
+ return this._levels[index] ?? null
212
+ }
213
+
214
+ /**
215
+ * Get hydrated level by entity name
216
+ */
217
+ getLevelByEntity(entity: string): HydratedLevel | null {
218
+ return this._levels.find(l => l.entity === entity) ?? null
219
+ }
220
+
221
+ /**
222
+ * Current hydrated level
223
+ */
224
+ getCurrent(): HydratedLevel | null {
225
+ return this._levels.at(-1) ?? null
226
+ }
227
+
228
+ /**
229
+ * Parent hydrated level
230
+ */
231
+ getParent(): HydratedLevel | null {
232
+ return this._levels.at(-2) ?? null
233
+ }
234
+
235
+ /**
236
+ * Root hydrated level
237
+ */
238
+ getRoot(): HydratedLevel | null {
239
+ return this._levels[0] ?? null
240
+ }
241
+
242
+ // ═══════════════════════════════════════════════════════════════════════════
243
+ // Manual update (for pages that load their own data)
244
+ // ═══════════════════════════════════════════════════════════════════════════
245
+
246
+ /**
247
+ * Manually set data for current level (skips fetch)
248
+ * Used by pages that already loaded the entity
249
+ */
250
+ setCurrentData(data: EntityRecord): void {
251
+ const level = this.getCurrent()
252
+ if (!level) return
253
+
254
+ const manager = this._orchestrator?.get(level.entity)
255
+ level.data = data
256
+ level.label = manager?.getEntityLabel?.(data) ?? level.id
257
+ level.hydrated = true
258
+ level.loading = false
259
+ this._scheduleEmit()
260
+ }
261
+
262
+ /**
263
+ * Manually set data for a level by entity name
264
+ */
265
+ setEntityData(entity: string, data: EntityRecord): void {
266
+ const level = this.getLevelByEntity(entity)
267
+ if (!level) return
268
+
269
+ const manager = this._orchestrator?.get(entity)
270
+ level.data = data
271
+ level.label = manager?.getEntityLabel?.(data) ?? level.id
272
+ level.hydrated = true
273
+ level.loading = false
274
+ this._scheduleEmit()
275
+ }
276
+
277
+ // ═══════════════════════════════════════════════════════════════════════════
278
+ // Internal
279
+ // ═══════════════════════════════════════════════════════════════════════════
280
+
281
+ /**
282
+ * Emit event via SignalBus
283
+ * @private
284
+ */
285
+ private _emit(signal: string, payload: HydratedPayload): void {
286
+ if (this._signalBus) {
287
+ this._signalBus.emit(signal, payload)
288
+ }
289
+ }
290
+ }
291
+
292
+ export default StackHydrator
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Chain Module - Active navigation stack management
3
+ *
4
+ * Provides:
5
+ * - ActiveStack: Sync container for navigation context (entity, param, foreignKey, id)
6
+ * - StackHydrator: Async layer for entity data and labels
7
+ * - useActiveStack: Composable to build and access the sync stack
8
+ * - useStackHydrator: Composable to access hydrated data
9
+ *
10
+ * @module chain
11
+ */
12
+
13
+ // Sync stack (context only)
14
+ export {
15
+ ActiveStack,
16
+ type StackLevel,
17
+ type EntityStackLevel,
18
+ type EntityStackLevelInput,
19
+ type StackChangePayload,
20
+ } from './ActiveStack'
21
+ export { useActiveStack, type UseActiveStackReturn } from './useActiveStack'
22
+
23
+ // Async hydration (data + labels)
24
+ export { StackHydrator, type HydratedLevel, type HydratedPayload } from './StackHydrator'
25
+ export { useStackHydrator, type StackHydratorReturn } from './useStackHydrator'
@@ -0,0 +1,120 @@
1
+ /**
2
+ * useActiveStack - Access sync navigation stack
3
+ *
4
+ * Provides reactive access to the navigation stack levels.
5
+ * Stack is rebuilt by Kernel on route change (single source of truth).
6
+ *
7
+ * IMPORTANT: Stack only contains levels WITH IDs.
8
+ * - /bots/bot-xyz/commands → stack = [bots(id:bot-xyz)]
9
+ * - /bots/bot-xyz/commands/cmd-123 → stack = [bots(id:bot-xyz), commands(id:cmd-123)]
10
+ *
11
+ * For hydrated data (labels, entity data), use useStackHydrator.
12
+ *
13
+ * Listens to SignalBus events for reactivity:
14
+ * - stack:change - when stack levels change
15
+ */
16
+
17
+ import { inject, ref, computed, onUnmounted, type ComputedRef, type Ref } from 'vue'
18
+ import type { ActiveStack, StackLevel } from './ActiveStack'
19
+ import type { SignalBus } from '../kernel/SignalBus'
20
+
21
+ /**
22
+ * Return type for useActiveStack composable
23
+ */
24
+ export interface UseActiveStackReturn {
25
+ /** Reactive levels array */
26
+ levels: Ref<StackLevel[]>
27
+ /** Current (deepest) level */
28
+ current: ComputedRef<StackLevel | null>
29
+ /** Parent level */
30
+ parent: ComputedRef<StackLevel | null>
31
+ /** Root level */
32
+ root: ComputedRef<StackLevel | null>
33
+ /** Stack depth */
34
+ depth: ComputedRef<number>
35
+ /** Get level by index */
36
+ getLevel: (index: number) => StackLevel | null
37
+ /** Get level by entity name */
38
+ getLevelByEntity: (entity: string) => StackLevel | null
39
+ /** Get current level (sync) */
40
+ getCurrent: () => StackLevel | null
41
+ /** Get parent level (sync) */
42
+ getParent: () => StackLevel | null
43
+ /** Get all levels (sync) */
44
+ getLevels: () => StackLevel[]
45
+ }
46
+
47
+ export function useActiveStack(): UseActiveStackReturn {
48
+ const activeStack = inject<ActiveStack | undefined>('qdadmActiveStack')
49
+ const signalBus = inject<SignalBus | undefined>('qdadmSignals')
50
+
51
+ if (!activeStack) {
52
+ console.warn('[useActiveStack] ActiveStack not provided')
53
+ return createEmptyStack()
54
+ }
55
+
56
+ // Reactive state - updated via SignalBus
57
+ const levels = ref<StackLevel[]>([...activeStack.getLevels()])
58
+
59
+ // Subscribe to stack changes
60
+ // QuarKernel passes (event, ctx) where event.data contains the payload
61
+ let unsubscribe: (() => void) | null = null
62
+ if (signalBus) {
63
+ unsubscribe = signalBus.on('stack:change', (event) => {
64
+ const newLevels = (event.data as { levels?: StackLevel[] })?.levels
65
+ if (newLevels) {
66
+ // Create new array reference to trigger reactivity
67
+ levels.value = [...newLevels]
68
+ }
69
+ })
70
+ }
71
+
72
+ // Cleanup on unmount
73
+ onUnmounted(() => {
74
+ if (unsubscribe) {
75
+ unsubscribe()
76
+ }
77
+ })
78
+
79
+ // Computed accessors
80
+ const current = computed(() => levels.value.at(-1) ?? null)
81
+ const parent = computed(() => levels.value.at(-2) ?? null)
82
+ const root = computed(() => levels.value[0] ?? null)
83
+ const depth = computed(() => levels.value.length)
84
+
85
+ return {
86
+ // Computed refs (reactive)
87
+ levels,
88
+ current,
89
+ parent,
90
+ root,
91
+ depth,
92
+
93
+ // Sync accessors (direct from vanilla JS instance)
94
+ getLevel: (i: number) => activeStack.getLevel(i),
95
+ getLevelByEntity: (e: string) => activeStack.getLevelByEntity(e),
96
+ getCurrent: () => activeStack.getCurrent(),
97
+ getParent: () => activeStack.getParent(),
98
+ getLevels: () => activeStack.getLevels(),
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Fallback when ActiveStack not available
104
+ */
105
+ function createEmptyStack(): UseActiveStackReturn {
106
+ return {
107
+ levels: ref([]),
108
+ current: computed(() => null),
109
+ parent: computed(() => null),
110
+ root: computed(() => null),
111
+ depth: computed(() => 0),
112
+ getLevel: () => null,
113
+ getLevelByEntity: () => null,
114
+ getCurrent: () => null,
115
+ getParent: () => null,
116
+ getLevels: () => [],
117
+ }
118
+ }
119
+
120
+ export default useActiveStack