@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,934 @@
1
+ /**
2
+ * ZoneRegistry - Manages named zones and block registrations
3
+ *
4
+ * Inspired by Twig/Symfony block system. Zones are named slots in layouts
5
+ * where blocks (components) can be injected with weight ordering.
6
+ *
7
+ * **Vue Reactivity**: The registry uses Vue's reactivity system internally.
8
+ * When blocks are added, removed, or modified, zones automatically trigger
9
+ * re-renders in components using them.
10
+ *
11
+ * Supports block operations:
12
+ * - 'add' (default): Simply add block to zone
13
+ * - 'replace': Substitute an existing block entirely
14
+ * - 'extend': Insert block before/after an existing block
15
+ * - 'wrap': Wrap an existing block with a decorator component
16
+ *
17
+ * Usage:
18
+ * ```ts
19
+ * const registry = new ZoneRegistry()
20
+ *
21
+ * // Define zones with optional defaults
22
+ * registry.defineZone('header', { default: DefaultHeader })
23
+ * registry.defineZone('sidebar')
24
+ *
25
+ * // Register blocks with weight (lower = first)
26
+ * registry.registerBlock('header', { component: Logo, weight: 10, id: 'logo' })
27
+ * registry.registerBlock('header', { component: UserMenu, weight: 90 })
28
+ *
29
+ * // Replace a block entirely
30
+ * registry.registerBlock('header', {
31
+ * component: CustomLogo,
32
+ * operation: 'replace',
33
+ * replaces: 'logo'
34
+ * })
35
+ *
36
+ * // Extend: insert after an existing block
37
+ * registry.registerBlock('header', {
38
+ * component: ExtraMenuItem,
39
+ * operation: 'extend',
40
+ * after: 'logo'
41
+ * })
42
+ *
43
+ * // Extend: insert before an existing block
44
+ * registry.registerBlock('header', {
45
+ * component: Announcements,
46
+ * operation: 'extend',
47
+ * before: 'logo'
48
+ * })
49
+ *
50
+ * // Wrap: decorate a block with before/after content
51
+ * registry.registerBlock('header', {
52
+ * component: BorderWrapper,
53
+ * operation: 'wrap',
54
+ * wraps: 'logo'
55
+ * })
56
+ *
57
+ * // Unregister blocks at runtime
58
+ * registry.unregisterBlock('header', 'logo')
59
+ *
60
+ * // Get blocks sorted by weight
61
+ * registry.getBlocks('header') // [Announcements, CustomLogo@10, ExtraMenuItem, UserMenu@90]
62
+ * ```
63
+ */
64
+
65
+ import { shallowRef, type ShallowRef, type Component } from 'vue'
66
+
67
+ /**
68
+ * Block operation types
69
+ */
70
+ export type BlockOperation = 'add' | 'replace' | 'extend' | 'wrap'
71
+
72
+ /**
73
+ * Wrapper info for wrapped blocks
74
+ */
75
+ export interface WrapperInfo {
76
+ component: Component
77
+ props?: Record<string, unknown>
78
+ id?: string | null
79
+ }
80
+
81
+ /**
82
+ * Block configuration
83
+ */
84
+ export interface BlockConfig {
85
+ /** Vue component to render */
86
+ component: Component
87
+ /** Ordering weight (lower = first) */
88
+ weight?: number
89
+ /** Props to pass to component */
90
+ props?: Record<string, unknown>
91
+ /** Unique identifier for duplicate detection */
92
+ id?: string | null
93
+ /** Block operation */
94
+ operation?: BlockOperation
95
+ /** Block ID to replace (required if operation='replace') */
96
+ replaces?: string | null
97
+ /** Block ID to insert before (for operation='extend') */
98
+ before?: string | null
99
+ /** Block ID to insert after (for operation='extend') */
100
+ after?: string | null
101
+ /** Block ID to wrap (required if operation='wrap') */
102
+ wraps?: string | null
103
+ /** Wrappers applied to this block */
104
+ wrappers?: WrapperInfo[]
105
+ }
106
+
107
+ /**
108
+ * Internal block representation with normalized fields
109
+ */
110
+ interface InternalBlock {
111
+ component: Component
112
+ weight: number
113
+ props: Record<string, unknown>
114
+ id: string | null
115
+ operation: BlockOperation
116
+ replaces: string | null
117
+ before: string | null
118
+ after: string | null
119
+ wraps: string | null
120
+ wrappers?: WrapperInfo[]
121
+ }
122
+
123
+ /**
124
+ * Zone configuration
125
+ */
126
+ export interface ZoneConfig {
127
+ /** Default component if no blocks registered */
128
+ default: Component | null
129
+ /** Registered blocks */
130
+ blocks: InternalBlock[]
131
+ }
132
+
133
+ /**
134
+ * Zone definition options
135
+ */
136
+ export interface ZoneDefineOptions {
137
+ /** Default component when no blocks are registered */
138
+ default?: Component | null
139
+ }
140
+
141
+ /**
142
+ * Zone info for listing
143
+ */
144
+ export interface ZoneListInfo {
145
+ name: string
146
+ blockCount: number
147
+ }
148
+
149
+ /**
150
+ * Detailed zone info for debugging
151
+ */
152
+ export interface ZoneDetailInfo {
153
+ name: string
154
+ hasDefault: boolean
155
+ blockCount: number
156
+ blocks: Array<{
157
+ id: string | null
158
+ weight: number
159
+ hasProps: boolean
160
+ }>
161
+ }
162
+
163
+ /**
164
+ * Zone inspection result
165
+ */
166
+ export interface ZoneInspection {
167
+ name: string
168
+ blocks: Array<{
169
+ id: string | null
170
+ weight: number
171
+ component: string
172
+ wrappers?: Array<{
173
+ id: string | null
174
+ component: string
175
+ }>
176
+ }>
177
+ default: string | null
178
+ }
179
+
180
+ /**
181
+ * ZoneRegistry creation options
182
+ */
183
+ export interface ZoneRegistryOptions {
184
+ debug?: boolean
185
+ }
186
+
187
+ const DEFAULT_WEIGHT = 50
188
+
189
+ export class ZoneRegistry {
190
+ /**
191
+ * Zone storage: name -> ZoneConfig
192
+ */
193
+ private _zones: Map<string, ZoneConfig>
194
+
195
+ /**
196
+ * Cache for sorted blocks (invalidated on registerBlock)
197
+ */
198
+ private _sortedCache: Map<string, BlockConfig[]>
199
+
200
+ /**
201
+ * Wrap graph for cycle detection: zoneName -> Map<wrapperId, targetId>
202
+ */
203
+ private _wrapGraph: Map<string, Map<string, string>>
204
+
205
+ /**
206
+ * Debug mode for warnings
207
+ */
208
+ private _debug: boolean
209
+
210
+ /**
211
+ * Version counter for reactivity - triggers re-renders when blocks change
212
+ * Using shallowRef for lightweight reactivity that tracks mutation count
213
+ */
214
+ private _version: ShallowRef<number>
215
+
216
+ constructor() {
217
+ this._zones = new Map()
218
+ this._sortedCache = new Map()
219
+ this._wrapGraph = new Map()
220
+ this._debug = false
221
+ this._version = shallowRef(0)
222
+ }
223
+
224
+ /**
225
+ * Get the reactive version ref for tracking changes
226
+ *
227
+ * Components can watch this ref to re-render when blocks change.
228
+ * Use `useZoneRegistry()` composable for convenient access.
229
+ *
230
+ * @returns Reactive version counter
231
+ */
232
+ getVersionRef(): ShallowRef<number> {
233
+ return this._version
234
+ }
235
+
236
+ /**
237
+ * Trigger Vue reactivity update
238
+ *
239
+ * Called after any mutation that should cause Zone components to re-render.
240
+ */
241
+ private _triggerUpdate(): void {
242
+ this._version.value++
243
+ }
244
+
245
+ /**
246
+ * Enable or disable debug mode
247
+ *
248
+ * When enabled, warnings are logged for missing replace targets, etc.
249
+ *
250
+ * @param enabled - Whether to enable debug mode
251
+ * @returns This instance for chaining
252
+ */
253
+ setDebug(enabled: boolean): this {
254
+ this._debug = !!enabled
255
+ return this
256
+ }
257
+
258
+ /**
259
+ * Define a new zone
260
+ *
261
+ * If zone already exists, merges options (updating default if provided).
262
+ *
263
+ * @param name - Zone name (e.g., 'header', 'sidebar')
264
+ * @param options - Zone options
265
+ * @returns This instance for chaining
266
+ */
267
+ defineZone(name: string, options: ZoneDefineOptions = {}): this {
268
+ if (!name || typeof name !== 'string') {
269
+ throw new Error('[ZoneRegistry] Zone name must be a non-empty string')
270
+ }
271
+
272
+ const existing = this._zones.get(name)
273
+ if (existing) {
274
+ // Merge: update default if provided
275
+ if (options.default !== undefined) {
276
+ existing.default = options.default
277
+ }
278
+ } else {
279
+ this._zones.set(name, {
280
+ default: options.default ?? null,
281
+ blocks: [],
282
+ })
283
+ }
284
+
285
+ return this
286
+ }
287
+
288
+ /**
289
+ * Register a block in a zone
290
+ *
291
+ * Blocks are sorted by weight (ascending). Equal weights maintain insertion order.
292
+ *
293
+ * Operations:
294
+ * - 'add' (default): Simply add the block to the zone
295
+ * - 'replace': Substitute an existing block by ID
296
+ * - 'extend': Insert block before/after an existing block
297
+ * - 'wrap': Wrap an existing block with a decorator component
298
+ *
299
+ * @param zoneName - Target zone name
300
+ * @param blockConfig - Block configuration
301
+ * @returns This instance for chaining
302
+ * @throws Error If component is not provided
303
+ * @throws Error If operation is 'replace' but replaces is not specified
304
+ * @throws Error If operation is 'extend' but neither before nor after is specified
305
+ * @throws Error If operation is 'extend' and both before and after are specified
306
+ * @throws Error If operation is 'wrap' but wraps is not specified
307
+ * @throws Error If operation is 'wrap' but id is not specified
308
+ * @throws Error If wrap would create a circular dependency
309
+ */
310
+ registerBlock(zoneName: string, blockConfig: BlockConfig): this {
311
+ if (!zoneName || typeof zoneName !== 'string') {
312
+ throw new Error('[ZoneRegistry] Zone name must be a non-empty string')
313
+ }
314
+
315
+ if (!blockConfig || !blockConfig.component) {
316
+ throw new Error('[ZoneRegistry] Block must have a component')
317
+ }
318
+
319
+ const operation = blockConfig.operation || 'add'
320
+
321
+ // Validate replace operation
322
+ if (operation === 'replace' && !blockConfig.replaces) {
323
+ throw new Error(
324
+ `[ZoneRegistry] Block with operation 'replace' must specify 'replaces' target ID`
325
+ )
326
+ }
327
+
328
+ // Validate extend operation
329
+ if (operation === 'extend') {
330
+ const hasBefore = !!blockConfig.before
331
+ const hasAfter = !!blockConfig.after
332
+
333
+ if (!hasBefore && !hasAfter) {
334
+ throw new Error(
335
+ `[ZoneRegistry] Block with operation 'extend' must specify either 'before' or 'after' target ID`
336
+ )
337
+ }
338
+ if (hasBefore && hasAfter) {
339
+ throw new Error(
340
+ `[ZoneRegistry] Block with operation 'extend' cannot specify both 'before' and 'after'`
341
+ )
342
+ }
343
+ }
344
+
345
+ // Validate wrap operation
346
+ if (operation === 'wrap') {
347
+ if (!blockConfig.wraps) {
348
+ throw new Error(
349
+ `[ZoneRegistry] Block with operation 'wrap' must specify 'wraps' target ID`
350
+ )
351
+ }
352
+ if (!blockConfig.id) {
353
+ throw new Error(
354
+ `[ZoneRegistry] Block with operation 'wrap' must have an 'id' for cycle detection`
355
+ )
356
+ }
357
+ // Check for circular wrap dependency
358
+ if (this._wouldCreateWrapCycle(zoneName, blockConfig.id, blockConfig.wraps)) {
359
+ throw new Error(
360
+ `[ZoneRegistry] Circular wrap dependency detected: "${blockConfig.id}" wrapping "${blockConfig.wraps}" would create a cycle`
361
+ )
362
+ }
363
+ }
364
+
365
+ // Auto-create zone if not exists (DX decision: reduce friction)
366
+ if (!this._zones.has(zoneName)) {
367
+ this.defineZone(zoneName)
368
+ }
369
+
370
+ const zone = this._zones.get(zoneName)!
371
+
372
+ // Prepare normalized block config
373
+ const block: InternalBlock = {
374
+ component: blockConfig.component,
375
+ weight: blockConfig.weight ?? DEFAULT_WEIGHT,
376
+ props: blockConfig.props ?? {},
377
+ id: blockConfig.id ?? null,
378
+ operation,
379
+ replaces: blockConfig.replaces ?? null,
380
+ before: blockConfig.before ?? null,
381
+ after: blockConfig.after ?? null,
382
+ wraps: blockConfig.wraps ?? null,
383
+ }
384
+
385
+ // Update wrap graph for cycle detection (wrap operation only)
386
+ if (operation === 'wrap') {
387
+ if (!this._wrapGraph.has(zoneName)) {
388
+ this._wrapGraph.set(zoneName, new Map())
389
+ }
390
+ this._wrapGraph.get(zoneName)!.set(block.id!, block.wraps!)
391
+ }
392
+
393
+ // Check for duplicate ID
394
+ if (block.id) {
395
+ const existingIndex = zone.blocks.findIndex((b) => b.id === block.id)
396
+ if (existingIndex !== -1) {
397
+ // Warn in debug mode - duplicates shouldn't happen with proper module init
398
+ if (this._debug) {
399
+ console.warn(
400
+ `[qdadm:zones] Duplicate block ID "${block.id}" in zone "${zoneName}". ` +
401
+ `This may indicate a module is being initialized multiple times.`
402
+ )
403
+ }
404
+ // Replace existing block (backward compatibility)
405
+ zone.blocks[existingIndex] = block
406
+ } else {
407
+ zone.blocks.push(block)
408
+ }
409
+ } else {
410
+ zone.blocks.push(block)
411
+ }
412
+
413
+ // Invalidate cache for this zone
414
+ this._sortedCache.delete(zoneName)
415
+
416
+ // Trigger Vue reactivity update
417
+ this._triggerUpdate()
418
+
419
+ // Dev mode logging for block registration
420
+ if (this._debug) {
421
+ const blockDesc = block.id || '(anonymous)'
422
+ const opDesc = operation === 'add' ? '' : ` [${operation}]`
423
+ console.debug(`[qdadm:zones] Registered block in zone: ${zoneName}, ${blockDesc}${opDesc}`)
424
+ }
425
+
426
+ return this
427
+ }
428
+
429
+ /**
430
+ * Check if adding a wrap would create a cycle
431
+ *
432
+ * Detects cycles by traversing the wrap graph from the wrapper.
433
+ * If the target eventually wraps back to the wrapper, it's a cycle.
434
+ *
435
+ * @param zoneName - Zone name
436
+ * @param wrapperId - ID of the wrapper block being added
437
+ * @param targetId - ID of the block being wrapped
438
+ * @returns True if adding this wrap would create a cycle
439
+ */
440
+ private _wouldCreateWrapCycle(zoneName: string, wrapperId: string, targetId: string): boolean {
441
+ // Self-wrap is a cycle
442
+ if (wrapperId === targetId) {
443
+ return true
444
+ }
445
+
446
+ const zoneGraph = this._wrapGraph.get(zoneName)
447
+ if (!zoneGraph) {
448
+ return false
449
+ }
450
+
451
+ // Check if the target (or anything it wraps) eventually wraps the wrapper
452
+ // Start from targetId and follow the chain to see if we reach wrapperId
453
+ const visited = new Set<string>()
454
+ let current: string | undefined = targetId
455
+
456
+ while (current && !visited.has(current)) {
457
+ visited.add(current)
458
+ // What does 'current' wrap?
459
+ const wrapsWhat = zoneGraph.get(current)
460
+ if (wrapsWhat === wrapperId) {
461
+ // Found a cycle: target -> ... -> wrapper
462
+ return true
463
+ }
464
+ current = wrapsWhat
465
+ }
466
+
467
+ return false
468
+ }
469
+
470
+ /**
471
+ * Get sorted blocks for a zone
472
+ *
473
+ * Returns blocks sorted by weight (ascending) after applying all block operations.
474
+ * Uses cached result if available.
475
+ *
476
+ * Operations:
477
+ * - 'add': Block is included directly
478
+ * - 'replace': Replaces target block, inherits target's weight if not specified
479
+ * - 'extend': Inserts block before/after target, uses target's weight for positioning
480
+ * - 'wrap': Adds wrappers array to target block containing wrapper components
481
+ *
482
+ * @param zoneName - Zone name
483
+ * @returns Sorted array of block configs (empty if zone undefined)
484
+ */
485
+ getBlocks(zoneName: string): BlockConfig[] {
486
+ if (!this._zones.has(zoneName)) {
487
+ return []
488
+ }
489
+
490
+ // Return cached sorted result if available
491
+ if (this._sortedCache.has(zoneName)) {
492
+ return this._sortedCache.get(zoneName)!
493
+ }
494
+
495
+ const zone = this._zones.get(zoneName)!
496
+
497
+ // Separate blocks by operation
498
+ const addBlocks: InternalBlock[] = []
499
+ const replaceBlocks: InternalBlock[] = []
500
+ const extendBlocks: InternalBlock[] = []
501
+ const wrapBlocks: InternalBlock[] = []
502
+
503
+ for (const block of zone.blocks) {
504
+ if (block.operation === 'replace') {
505
+ replaceBlocks.push(block)
506
+ } else if (block.operation === 'extend') {
507
+ extendBlocks.push(block)
508
+ } else if (block.operation === 'wrap') {
509
+ wrapBlocks.push(block)
510
+ } else {
511
+ addBlocks.push(block)
512
+ }
513
+ }
514
+
515
+ // Start with add blocks
516
+ const result: InternalBlock[] = [...addBlocks]
517
+
518
+ // Apply replace operations first
519
+ for (const replaceBlock of replaceBlocks) {
520
+ const targetIndex = result.findIndex((b) => b.id === replaceBlock.replaces)
521
+
522
+ if (targetIndex === -1) {
523
+ // Target not found - warn but still add the replacement at its own weight
524
+ if (this._debug) {
525
+ console.warn(
526
+ `[ZoneRegistry] Replace target "${replaceBlock.replaces}" not found in zone "${zoneName}". ` +
527
+ `Adding replacement block at default weight.`
528
+ )
529
+ }
530
+ result.push(replaceBlock)
531
+ } else {
532
+ // Replace: use target's weight if replacement doesn't specify one
533
+ const targetBlock = result[targetIndex]!
534
+ const hasExplicitWeight = replaceBlock.weight !== DEFAULT_WEIGHT
535
+ const finalWeight = hasExplicitWeight ? replaceBlock.weight : targetBlock.weight
536
+
537
+ const replacement: InternalBlock = {
538
+ ...replaceBlock,
539
+ weight: finalWeight,
540
+ }
541
+ result.splice(targetIndex, 1, replacement)
542
+ }
543
+ }
544
+
545
+ // Sort by weight before applying extend operations
546
+ // This ensures targets are in their final positions
547
+ result.sort((a, b) => a.weight - b.weight)
548
+
549
+ // Apply extend operations (in registration order)
550
+ for (const extendBlock of extendBlocks) {
551
+ const targetId = extendBlock.before || extendBlock.after
552
+ const insertBefore = !!extendBlock.before
553
+ const targetIndex = result.findIndex((b) => b.id === targetId)
554
+
555
+ if (targetIndex === -1) {
556
+ // Target not found - warn and fall back to weight-based positioning
557
+ if (this._debug) {
558
+ console.warn(
559
+ `[ZoneRegistry] Extend target "${targetId}" not found in zone "${zoneName}". ` +
560
+ `Adding block at its own weight.`
561
+ )
562
+ }
563
+ // Insert at position based on weight
564
+ const insertIndex = result.findIndex((b) => b.weight > extendBlock.weight)
565
+ if (insertIndex === -1) {
566
+ result.push(extendBlock)
567
+ } else {
568
+ result.splice(insertIndex, 0, extendBlock)
569
+ }
570
+ } else {
571
+ // Insert before or after target
572
+ const insertIndex = insertBefore ? targetIndex : targetIndex + 1
573
+ result.splice(insertIndex, 0, extendBlock)
574
+ }
575
+ }
576
+
577
+ // Apply wrap operations
578
+ // Build wrapper chains: for each target, collect all wrappers sorted by weight
579
+ if (wrapBlocks.length > 0) {
580
+ // Group wraps by target ID
581
+ const wrapsByTarget = new Map<string, InternalBlock[]>()
582
+ for (const wrapBlock of wrapBlocks) {
583
+ if (!wrapsByTarget.has(wrapBlock.wraps!)) {
584
+ wrapsByTarget.set(wrapBlock.wraps!, [])
585
+ }
586
+ wrapsByTarget.get(wrapBlock.wraps!)!.push(wrapBlock)
587
+ }
588
+
589
+ // Collect IDs of blocks in result for orphan detection
590
+ const resultBlockIds = new Set(result.map((b) => b.id).filter(Boolean) as string[])
591
+
592
+ // Apply wrappers to blocks in result
593
+ for (let i = 0; i < result.length; i++) {
594
+ const block = result[i]
595
+ if (!block || !block.id) continue
596
+
597
+ // Collect all wrappers for this block (direct and nested)
598
+ const allWrappers = this._collectWrapChain(block.id, wrapsByTarget)
599
+
600
+ if (allWrappers.length > 0) {
601
+ // Sort wrappers by weight (lower weight = outer wrapper)
602
+ allWrappers.sort((a, b) => a.weight - b.weight)
603
+
604
+ result[i] = {
605
+ component: block.component,
606
+ weight: block.weight,
607
+ props: block.props,
608
+ id: block.id,
609
+ operation: block.operation,
610
+ replaces: block.replaces,
611
+ before: block.before,
612
+ after: block.after,
613
+ wraps: block.wraps,
614
+ wrappers: allWrappers.map((w) => ({
615
+ component: w.component,
616
+ props: w.props,
617
+ id: w.id,
618
+ })),
619
+ }
620
+ }
621
+ }
622
+
623
+ // Warn about orphaned wrappers (wrappers targeting non-existent blocks)
624
+ if (this._debug) {
625
+ for (const [targetId, wrappers] of wrapsByTarget.entries()) {
626
+ // Check if target exists in result blocks or as a wrapper ID
627
+ const wrapperIds = new Set(wrapBlocks.map((w) => w.id))
628
+ if (!resultBlockIds.has(targetId) && !wrapperIds.has(targetId)) {
629
+ for (const wrapper of wrappers) {
630
+ console.warn(
631
+ `[ZoneRegistry] Wrap target "${targetId}" not found in zone "${zoneName}". ` +
632
+ `Wrapper "${wrapper.id}" will be ignored.`
633
+ )
634
+ }
635
+ }
636
+ }
637
+ }
638
+ }
639
+
640
+ // Clean up internal operation fields for external consumers
641
+ const cleaned: BlockConfig[] = result.map(({ component, weight, props, id, wrappers }) => ({
642
+ component,
643
+ weight,
644
+ props,
645
+ id,
646
+ ...(wrappers ? { wrappers } : {}),
647
+ }))
648
+
649
+ // Cache the result
650
+ this._sortedCache.set(zoneName, cleaned)
651
+
652
+ return cleaned
653
+ }
654
+
655
+ /**
656
+ * Collect all wrappers for a block, including nested wrappers
657
+ *
658
+ * If WrapperA wraps MainContent and WrapperB wraps WrapperA,
659
+ * returns [WrapperA, WrapperB] (inner to outer before sorting)
660
+ *
661
+ * @param targetId - Block ID being wrapped
662
+ * @param wrapsByTarget - Map of target ID -> wrapper blocks
663
+ * @returns Array of wrapper block configs
664
+ */
665
+ private _collectWrapChain(
666
+ targetId: string,
667
+ wrapsByTarget: Map<string, InternalBlock[]>
668
+ ): InternalBlock[] {
669
+ const directWrappers = wrapsByTarget.get(targetId) || []
670
+
671
+ if (directWrappers.length === 0) {
672
+ return []
673
+ }
674
+
675
+ // Collect nested wrappers (wrappers of wrappers)
676
+ const allWrappers = [...directWrappers]
677
+
678
+ for (const wrapper of directWrappers) {
679
+ const nestedWrappers = this._collectWrapChain(wrapper.id!, wrapsByTarget)
680
+ allWrappers.push(...nestedWrappers)
681
+ }
682
+
683
+ return allWrappers
684
+ }
685
+
686
+ /**
687
+ * Get zone default component
688
+ *
689
+ * @param zoneName - Zone name
690
+ * @returns Default component or null
691
+ */
692
+ getDefault(zoneName: string): Component | null {
693
+ const zone = this._zones.get(zoneName)
694
+ return zone?.default ?? null
695
+ }
696
+
697
+ /**
698
+ * Check if a zone has any blocks registered
699
+ *
700
+ * @param zoneName - Zone name
701
+ * @returns Whether zone has blocks
702
+ */
703
+ hasBlocks(zoneName: string): boolean {
704
+ const zone = this._zones.get(zoneName)
705
+ return zone ? zone.blocks.length > 0 : false
706
+ }
707
+
708
+ /**
709
+ * Check if a zone is defined
710
+ *
711
+ * @param zoneName - Zone name
712
+ * @returns Whether zone exists
713
+ */
714
+ hasZone(zoneName: string): boolean {
715
+ return this._zones.has(zoneName)
716
+ }
717
+
718
+ /**
719
+ * List all defined zones with metadata
720
+ *
721
+ * Useful for debugging and introspection. Returns zone names with block counts.
722
+ *
723
+ * @returns Array of zone info objects
724
+ */
725
+ listZones(): ZoneListInfo[] {
726
+ const result: ZoneListInfo[] = []
727
+ for (const [name, zone] of this._zones) {
728
+ result.push({
729
+ name,
730
+ blockCount: zone.blocks.length,
731
+ })
732
+ }
733
+ return result
734
+ }
735
+
736
+ /**
737
+ * Get zone info for debugging
738
+ *
739
+ * @param zoneName - Zone name
740
+ * @returns Zone info or null if not defined
741
+ */
742
+ getZoneInfo(zoneName: string): ZoneDetailInfo | null {
743
+ const zone = this._zones.get(zoneName)
744
+ if (!zone) return null
745
+
746
+ return {
747
+ name: zoneName,
748
+ hasDefault: zone.default !== null,
749
+ blockCount: zone.blocks.length,
750
+ blocks: zone.blocks.map((b) => ({
751
+ id: b.id,
752
+ weight: b.weight,
753
+ hasProps: Object.keys(b.props).length > 0,
754
+ })),
755
+ }
756
+ }
757
+
758
+ /**
759
+ * Inspect a zone for debugging - detailed view with component info
760
+ *
761
+ * Returns zone details including blocks with component names,
762
+ * suitable for DevTools console inspection.
763
+ *
764
+ * @param zoneName - Zone name
765
+ * @returns Detailed zone inspection or null if not defined
766
+ */
767
+ inspect(zoneName: string): ZoneInspection | null {
768
+ const zone = this._zones.get(zoneName)
769
+ if (!zone) return null
770
+
771
+ // Get sorted blocks for accurate representation
772
+ const sortedBlocks = this.getBlocks(zoneName)
773
+
774
+ return {
775
+ name: zoneName,
776
+ blocks: sortedBlocks.map((b) => ({
777
+ id: b.id ?? null,
778
+ weight: b.weight ?? DEFAULT_WEIGHT,
779
+ component: this._getComponentName(b.component),
780
+ ...(b.wrappers
781
+ ? {
782
+ wrappers: b.wrappers.map((w) => ({
783
+ id: w.id ?? null,
784
+ component: this._getComponentName(w.component),
785
+ })),
786
+ }
787
+ : {}),
788
+ })),
789
+ default: zone.default ? this._getComponentName(zone.default) : null,
790
+ }
791
+ }
792
+
793
+ /**
794
+ * Get a human-readable component name for debugging
795
+ *
796
+ * @param component - Vue component
797
+ * @returns Component name or fallback
798
+ */
799
+ private _getComponentName(component: Component): string {
800
+ if (!component) return '(none)'
801
+ if (typeof component === 'string') return component
802
+ const comp = component as { name?: string; __name?: string }
803
+ return comp.name || comp.__name || '(anonymous)'
804
+ }
805
+
806
+ /**
807
+ * Remove all blocks from a zone
808
+ *
809
+ * @param zoneName - Zone name
810
+ * @returns This instance for chaining
811
+ */
812
+ clearZone(zoneName: string): this {
813
+ const zone = this._zones.get(zoneName)
814
+ if (zone) {
815
+ const hadBlocks = zone.blocks.length > 0
816
+ zone.blocks = []
817
+ this._sortedCache.delete(zoneName)
818
+ this._wrapGraph.delete(zoneName)
819
+
820
+ // Trigger Vue reactivity update only if we removed blocks
821
+ if (hadBlocks) {
822
+ this._triggerUpdate()
823
+ }
824
+ }
825
+ return this
826
+ }
827
+
828
+ /**
829
+ * Remove a specific block by ID
830
+ *
831
+ * Also available as `unregisterBlock()` for semantic clarity in runtime scenarios.
832
+ *
833
+ * @param zoneName - Zone name
834
+ * @param blockId - Block ID to remove
835
+ * @returns True if block was found and removed
836
+ */
837
+ removeBlock(zoneName: string, blockId: string): boolean {
838
+ const zone = this._zones.get(zoneName)
839
+ if (!zone) {
840
+ if (this._debug) {
841
+ console.warn(`[ZoneRegistry] Cannot remove block "${blockId}": zone "${zoneName}" not found`)
842
+ }
843
+ return false
844
+ }
845
+
846
+ const index = zone.blocks.findIndex((b) => b.id === blockId)
847
+ if (index === -1) {
848
+ if (this._debug) {
849
+ console.warn(`[ZoneRegistry] Block "${blockId}" not found in zone "${zoneName}"`)
850
+ }
851
+ return false
852
+ }
853
+
854
+ const block = zone.blocks[index]!
855
+ zone.blocks.splice(index, 1)
856
+ this._sortedCache.delete(zoneName)
857
+
858
+ // Remove from wrap graph if it was a wrap operation
859
+ if (block.operation === 'wrap') {
860
+ const zoneGraph = this._wrapGraph.get(zoneName)
861
+ if (zoneGraph) {
862
+ zoneGraph.delete(blockId)
863
+ }
864
+ }
865
+
866
+ // Trigger Vue reactivity update
867
+ this._triggerUpdate()
868
+
869
+ if (this._debug) {
870
+ console.debug(`[qdadm:zones] Unregistered block from zone: ${zoneName}, ${blockId}`)
871
+ }
872
+
873
+ return true
874
+ }
875
+
876
+ /**
877
+ * Unregister a block from a zone at runtime
878
+ *
879
+ * Alias for `removeBlock()` with semantic clarity for runtime block management.
880
+ * Use this when dynamically adding/removing blocks based on user state, feature flags, etc.
881
+ *
882
+ * @param zoneName - Zone name
883
+ * @param blockId - Block ID to unregister
884
+ * @returns True if block was found and removed
885
+ *
886
+ * @example
887
+ * // Register a block when component mounts
888
+ * onMounted(() => {
889
+ * registry.registerBlock('sidebar', { component: AdBanner, weight: 50, id: 'ad-banner' })
890
+ * })
891
+ *
892
+ * // Unregister when component unmounts
893
+ * onUnmounted(() => {
894
+ * registry.unregisterBlock('sidebar', 'ad-banner')
895
+ * })
896
+ */
897
+ unregisterBlock(zoneName: string, blockId: string): boolean {
898
+ return this.removeBlock(zoneName, blockId)
899
+ }
900
+
901
+ /**
902
+ * Clear all zones and blocks
903
+ *
904
+ * Useful for testing.
905
+ *
906
+ * @returns This instance for chaining
907
+ */
908
+ clear(): this {
909
+ const hadZones = this._zones.size > 0
910
+ this._zones.clear()
911
+ this._sortedCache.clear()
912
+ this._wrapGraph.clear()
913
+
914
+ // Trigger Vue reactivity update only if we had zones
915
+ if (hadZones) {
916
+ this._triggerUpdate()
917
+ }
918
+ return this
919
+ }
920
+ }
921
+
922
+ /**
923
+ * Create a new ZoneRegistry instance
924
+ *
925
+ * @param options - Registry options
926
+ * @returns ZoneRegistry instance
927
+ */
928
+ export function createZoneRegistry(options: ZoneRegistryOptions = {}): ZoneRegistry {
929
+ const registry = new ZoneRegistry()
930
+ if (options.debug) {
931
+ registry.setDebug(true)
932
+ }
933
+ return registry
934
+ }