@raclettejs/core 0.1.40 → 0.1.42

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 (38) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/dist/cli.js +104 -104
  3. package/dist/cli.js.map +4 -4
  4. package/dist/index.js.map +2 -2
  5. package/package.json +2 -2
  6. package/services/frontend/eslint.config.js +31 -9
  7. package/services/frontend/src/core/setup/socketBootstrap.ts +4 -3
  8. package/services/frontend/src/core/store/state.ts +2 -7
  9. package/services/frontend/src/core/store/types/index.ts +2 -0
  10. package/services/frontend/src/orchestrator/ProductOrchestrator.vue +33 -22
  11. package/services/frontend/src/orchestrator/components/LoadingWidgetState.vue +7 -3
  12. package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +179 -195
  13. package/services/frontend/src/orchestrator/components/dataExport/DataExporter.vue +79 -24
  14. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTable.vue +27 -14
  15. package/services/frontend/src/orchestrator/components/dataTable/BaseDataTableGroupSelectCheckbox.vue +42 -0
  16. package/services/frontend/src/orchestrator/components/index.ts +2 -0
  17. package/services/frontend/src/orchestrator/components/menu/ServerStatus.vue +2 -4
  18. package/services/frontend/src/orchestrator/components/menu/UserMenu.vue +1 -1
  19. package/services/frontend/src/orchestrator/components/welcomeScreen/AdminWelcomeScreen.vue +1 -1
  20. package/services/frontend/src/orchestrator/composables/index.ts +1 -0
  21. package/services/frontend/src/orchestrator/composables/useAppLoadingCoordinator.ts +77 -0
  22. package/services/frontend/src/orchestrator/composables/usePageNavigation.ts +1 -1
  23. package/services/frontend/src/orchestrator/composables/useUiState.ts +2 -0
  24. package/services/frontend/src/orchestrator/composables/useWidgetLifecycle.ts +65 -76
  25. package/services/frontend/src/orchestrator/composables/useWidgets/helperFunctions.ts +7 -4
  26. package/services/frontend/src/orchestrator/constants/index.ts +2 -0
  27. package/services/frontend/src/orchestrator/constants/widgetSlot.ts +7 -0
  28. package/services/frontend/src/orchestrator/exports.ts +1 -0
  29. package/services/frontend/src/orchestrator/helpers/staticWidgetRegistry.ts +9 -44
  30. package/services/frontend/src/orchestrator/helpers/widgetLayoutHelper.ts +37 -0
  31. package/services/frontend/src/orchestrator/helpers/widgetLoader.ts +0 -1
  32. package/services/frontend/src/orchestrator/helpers/widgetSlotHelper.ts +20 -0
  33. package/services/frontend/src/orchestrator/helpers/widgetVisuals.ts +11 -12
  34. package/services/frontend/src/orchestrator/helpers/wrapWidgetWithLifecycle.ts +32 -0
  35. package/services/frontend/src/orchestrator/router/routerHooks/afterEach.ts +4 -29
  36. package/services/frontend/src/orchestrator/router/routerHooks/beforeEach.ts +4 -1
  37. package/services/frontend/src/orchestrator/types/Widgets.ts +12 -6
  38. package/src/types.ts +7 -1
@@ -2,21 +2,29 @@ import { ref, type Ref } from "vue"
2
2
  import { isDevelopmentMode } from "@racletteCore/helpers/devMode"
3
3
  import log from "@racletteCore/lib/logger"
4
4
  import { scheduleAfterPaint } from "@racletteCore/lib/scheduleTask"
5
+ import { hasWidgetSlotContent } from "@racletteOrchestrator/helpers/widgetSlotHelper"
5
6
 
6
7
  export type OnAllWidgetsReady = (callback: () => void) => void
7
8
 
8
- interface WidgetLifecycleHooks {
9
+ export interface WidgetLifecycleHooks {
9
10
  onAllWidgetsReady: OnAllWidgetsReady
10
- onWidgetRendered: (element: HTMLElement) => void
11
+ onWidgetContainerMounted: (element: HTMLElement) => void
11
12
  onWidgetUnmounted: () => void
12
13
  }
13
14
 
14
- interface WidgetLifecycleManager {
15
+ export interface WidgetLifecycleManager {
15
16
  createLifecycleHooks: (widgetId: string) => WidgetLifecycleHooks
16
17
  allWidgetsReady: Ref<boolean>
17
18
  reset: () => void
18
19
  }
19
20
 
21
+ /**
22
+ * Tracks widget slot readiness for page navigation, modal overlays, and other
23
+ * `WidgetsLayoutLoader` instances with `trackReadiness`.
24
+ *
25
+ * Observes each widget slot container in `WidgetsLayoutLoader` and waits until
26
+ * plugin content appears in the DOM. Plugin developers do not interact with this.
27
+ */
20
28
  export function createWidgetLifecycleManager(
21
29
  totalWidgets: number,
22
30
  ): WidgetLifecycleManager {
@@ -24,6 +32,7 @@ export function createWidgetLifecycleManager(
24
32
  const allWidgetsReady = ref(false)
25
33
  const readyCallbacks = new Set<() => void>()
26
34
  const observers = new Map<string, MutationObserver>()
35
+ const observedContainers = new Map<string, HTMLElement>()
27
36
 
28
37
  const DEV_MODE = isDevelopmentMode()
29
38
 
@@ -34,91 +43,75 @@ export function createWidgetLifecycleManager(
34
43
  }
35
44
 
36
45
  const checkAllReady = () => {
37
- if (renderedWidgets.value.size === totalWidgets && !allWidgetsReady.value) {
38
- allWidgetsReady.value = true
39
-
40
- if (DEV_MODE) {
41
- log.widgets(
42
- "useWidgetLifecycle",
43
- `✅ All ${totalWidgets} widgets are fully rendered`,
44
- )
45
- }
46
-
47
- readyCallbacks.forEach((callback) => callback())
48
- readyCallbacks.clear()
46
+ if (renderedWidgets.value.size !== totalWidgets || allWidgetsReady.value) {
47
+ return
49
48
  }
50
- }
51
49
 
52
- const hasVisibleContent = (element: HTMLElement): boolean => {
53
- if (element.childNodes.length === 0) {
54
- return false
55
- }
50
+ allWidgetsReady.value = true
56
51
 
57
- const text = element.textContent?.trim()
58
- if (text?.length) {
59
- return true
52
+ if (DEV_MODE) {
53
+ log.widgets(
54
+ "useWidgetLifecycle",
55
+ `All ${totalWidgets} widgets are fully rendered`,
56
+ )
60
57
  }
61
58
 
62
- // DOM-only checks — avoid getBoundingClientRect / offset* (forced reflow).
63
- return (
64
- element.children.length > 0 ||
65
- element.querySelector(
66
- "table, img, canvas, svg, video, iframe, input, button, select, textarea, [role='row']",
67
- ) !== null
68
- )
59
+ readyCallbacks.forEach((callback) => callback())
60
+ readyCallbacks.clear()
61
+ }
62
+
63
+ const disconnectObserver = (id: string) => {
64
+ observers.get(id)?.disconnect()
65
+ observers.delete(id)
66
+ observedContainers.delete(id)
69
67
  }
70
68
 
71
- const scheduleContentCheck = (
72
- element: HTMLElement,
69
+ const scheduleContainerCheck = (
70
+ container: HTMLElement,
73
71
  onReady: () => void,
74
72
  ): (() => void) => {
75
73
  let scheduled = false
76
74
 
77
75
  const runCheck = () => {
78
76
  scheduled = false
79
- if (hasVisibleContent(element)) {
77
+ if (hasWidgetSlotContent(container)) {
80
78
  onReady()
81
79
  }
82
80
  }
83
81
 
84
- const scheduleCheck = () => {
82
+ return () => {
85
83
  if (scheduled) {
86
84
  return
87
85
  }
88
86
  scheduled = true
89
87
  scheduleAfterPaint(runCheck)
90
88
  }
91
-
92
- return scheduleCheck
93
89
  }
94
90
 
95
- const registerWidgetAsRendered = (id: string) => {
96
- if (!renderedWidgets.value.has(id)) {
97
- renderedWidgets.value.add(id)
91
+ const registerWidgetAsReady = (id: string) => {
92
+ if (renderedWidgets.value.has(id)) {
93
+ return
94
+ }
98
95
 
99
- if (DEV_MODE) {
100
- log.widgets(
101
- "useWidgetLifecycle",
102
- `📦 Widget rendered: ${id} (${renderedWidgets.value.size}/${totalWidgets})`,
103
- )
104
- }
96
+ renderedWidgets.value.add(id)
97
+ disconnectObserver(id)
105
98
 
106
- checkAllReady()
99
+ if (DEV_MODE) {
100
+ log.widgets(
101
+ "useWidgetLifecycle",
102
+ `Widget ready: ${id} (${renderedWidgets.value.size}/${totalWidgets})`,
103
+ )
107
104
  }
105
+
106
+ checkAllReady()
108
107
  }
109
108
 
110
109
  const unregisterWidget = (id: string) => {
111
110
  if (DEV_MODE) {
112
- log.widgets("useWidgetLifecycle", `📤 Widget unregistered: ${id}`)
113
- }
114
-
115
- // Clean up observer
116
- const observer = observers.get(id)
117
- if (observer) {
118
- observer.disconnect()
119
- observers.delete(id)
111
+ log.widgets("useWidgetLifecycle", `Widget unregistered: ${id}`)
120
112
  }
121
113
 
114
+ disconnectObserver(id)
122
115
  renderedWidgets.value.delete(id)
123
116
 
124
117
  if (renderedWidgets.value.size < totalWidgets) {
@@ -126,33 +119,30 @@ export function createWidgetLifecycleManager(
126
119
  }
127
120
  }
128
121
 
129
- const observeWidgetRendering = (id: string, element: HTMLElement) => {
130
- let observer: MutationObserver | null = null
122
+ const observeWidgetContainer = (id: string, container: HTMLElement) => {
123
+ if (renderedWidgets.value.has(id)) {
124
+ return
125
+ }
131
126
 
132
- const markReady = () => {
133
- observer?.disconnect()
134
- observer = null
135
- observers.delete(id)
136
- registerWidgetAsRendered(id)
127
+ if (observedContainers.get(id) === container) {
128
+ return
137
129
  }
138
130
 
139
- const scheduleCheck = scheduleContentCheck(element, markReady)
131
+ disconnectObserver(id)
132
+ observedContainers.set(id, container)
140
133
 
141
- observer = new MutationObserver(scheduleCheck)
142
- observer.observe(element, {
134
+ const scheduleCheck = scheduleContainerCheck(container, () =>
135
+ registerWidgetAsReady(id),
136
+ )
137
+
138
+ const observer = new MutationObserver(scheduleCheck)
139
+ observer.observe(container, {
143
140
  childList: true,
144
141
  subtree: true,
145
142
  characterData: true,
146
143
  })
147
144
  observers.set(id, observer)
148
145
  scheduleCheck()
149
-
150
- if (DEV_MODE) {
151
- log.widgets(
152
- "useWidgetLifecycle",
153
- `👀 Observing widget for content: ${id}`,
154
- )
155
- }
156
146
  }
157
147
 
158
148
  const createLifecycleHooks = (widgetId: string): WidgetLifecycleHooks => ({
@@ -163,24 +153,23 @@ export function createWidgetLifecycleManager(
163
153
  readyCallbacks.add(callback)
164
154
  }
165
155
  },
166
- onWidgetRendered: (element: HTMLElement) => {
167
- observeWidgetRendering(widgetId, element)
156
+ onWidgetContainerMounted: (element: HTMLElement) => {
157
+ observeWidgetContainer(widgetId, element)
168
158
  },
169
159
  onWidgetUnmounted: () => unregisterWidget(widgetId),
170
160
  })
171
161
 
172
162
  const reset = () => {
173
- // Disconnect all observers
174
163
  observers.forEach((observer) => observer.disconnect())
175
164
  observers.clear()
165
+ observedContainers.clear()
176
166
  renderedWidgets.value.clear()
177
167
  allWidgetsReady.value = false
178
168
  readyCallbacks.clear()
179
-
180
169
  markReadyIfNoWidgets()
181
170
 
182
171
  if (DEV_MODE) {
183
- log.widgets("useWidgetLifecycle", "🔄 Widget lifecycle manager reset")
172
+ log.widgets("useWidgetLifecycle", "Widget lifecycle manager reset")
184
173
  }
185
174
  }
186
175
 
@@ -6,7 +6,10 @@ import { forEachObjIndexed } from "ramda"
6
6
  import type { ConfigurableParameters, WidgetConf } from "@racletteOrchestrator"
7
7
  import { generatePluginKey } from "@shared/helper/pluginHelper"
8
8
  import type { PluginMetadata } from "@core"
9
- import type { RegisteredWidgetDeclaration, WidgetDeclaration } from "@racletteOrchestrator"
9
+ import type {
10
+ RegisteredWidgetDeclaration,
11
+ WidgetDeclaration,
12
+ } from "@racletteOrchestrator"
10
13
  import { normalizeWidgetVisuals } from "@racletteOrchestrator/helpers"
11
14
 
12
15
  // Plugin metadata files (unchanged)
@@ -134,9 +137,8 @@ export const getWidgetDeclacations = () => {
134
137
  }
135
138
 
136
139
  // Extract details and config (handle both default export and named exports)
137
- const details = (setupModule.details || setupModule.default?.details) as
138
- | WidgetDeclaration
139
- | undefined
140
+ const details = (setupModule.details ||
141
+ setupModule.default?.details) as WidgetDeclaration | undefined
140
142
  const config = setupModule.config || setupModule.default?.config
141
143
 
142
144
  if (!details) {
@@ -150,6 +152,7 @@ export const getWidgetDeclacations = () => {
150
152
  const registeredWidget: RegisteredWidgetDeclaration = {
151
153
  ...details,
152
154
  ...normalizedVisuals,
155
+ description: details.description?.trim() ?? "",
153
156
  pluginKey,
154
157
  author: pluginMetadata.author,
155
158
  name: widgetName,
@@ -0,0 +1,2 @@
1
+ export * from "./widgetSlotType"
2
+ export * from "./widgetSlot"
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Internal marker on {@link LoadingWidgetState} while a widget JS chunk loads.
3
+ * Not used in plugin widgets — only the orchestrator loading placeholder.
4
+ */
5
+ export const WIDGET_CHUNK_LOADING_ATTR = "data-raclette-widget-chunk-loading"
6
+
7
+ export const WIDGET_CHUNK_LOADING_SELECTOR = `[${WIDGET_CHUNK_LOADING_ATTR}]`
@@ -4,3 +4,4 @@ export * from "@racletteOrchestrator/helpers"
4
4
  export * from "@racletteOrchestrator/setup/i18n"
5
5
  export type * from "./types"
6
6
  export * from "./components"
7
+ export * from "./constants"
@@ -1,8 +1,8 @@
1
1
  import type { Component } from "vue"
2
- import { h } from "vue"
3
2
  import type { WidgetDeclaration } from "@racletteOrchestrator"
4
3
  import { registeredPlugins } from "@racletteCore/setup/plugin-system"
5
4
  import logger from "@racletteCore/lib/logger"
5
+ import { createEnhancedError } from "@racletteOrchestrator/helpers/widgetUtils"
6
6
 
7
7
  export type WidgetInfo = {
8
8
  widgetName: string
@@ -177,23 +177,12 @@ const initializeWidgetDiscovery = () => {
177
177
  // =====================================================
178
178
 
179
179
  const loadWidget = async (widgetInfo: WidgetInfo) => {
180
- try {
181
- const componentModule = await widgetInfo.componentLoader()
182
- const component = componentModule.default || componentModule
183
-
184
- const setup = await loadWidgetSetup(widgetInfo)
180
+ const componentModule = await widgetInfo.componentLoader()
181
+ const component = componentModule.default || componentModule
185
182
 
186
- return { component, ...setup }
187
- } catch (error) {
188
- logger.widgets("Failed to load widget", {
189
- widgetName: widgetInfo.widgetName,
190
- error: error instanceof Error ? error.message : String(error),
191
- })
183
+ const setup = await loadWidgetSetup(widgetInfo)
192
184
 
193
- return {
194
- component: createErrorComponent(widgetInfo.widgetName),
195
- }
196
- }
185
+ return { component, ...setup }
197
186
  }
198
187
 
199
188
  const loadWidgetSetup = async (widgetInfo: WidgetInfo) => {
@@ -216,16 +205,6 @@ const loadWidgetSetup = async (widgetInfo: WidgetInfo) => {
216
205
  }
217
206
  }
218
207
 
219
- const createErrorComponent = (widgetName: string): Component => ({
220
- name: `${widgetName}Error`,
221
- setup: () => () =>
222
- h(
223
- "div",
224
- { class: "p-4 text-red-500 border border-red-300 rounded" },
225
- `Failed to load widget: ${widgetName}`,
226
- ),
227
- })
228
-
229
208
  // =====================================================
230
209
  // PATH RESOLUTION HELPERS
231
210
  // =====================================================
@@ -318,7 +297,10 @@ export const loadWidgetComponent = async (
318
297
  }
319
298
  return component
320
299
  } catch (error) {
321
- const enhancedError = createEnhancedLoadError(error, pluginKey, widgetName)
300
+ const enhancedError = createEnhancedError(error, {
301
+ pluginKey,
302
+ name: widgetName,
303
+ })
322
304
  logger.widgets("Widget component load failed", {
323
305
  pluginKey,
324
306
  widgetName,
@@ -328,22 +310,5 @@ export const loadWidgetComponent = async (
328
310
  }
329
311
  }
330
312
 
331
- const createEnhancedLoadError = (
332
- error: unknown,
333
- pluginKey: string,
334
- widgetName: string,
335
- ) => {
336
- if (error instanceof Error) {
337
- const enhancedError = new Error(
338
- `Failed to load widget component ${pluginKey}/${widgetName}: ${error.message}`,
339
- )
340
- enhancedError.stack = error.stack
341
- return enhancedError
342
- }
343
- return new Error(
344
- `Failed to load widget component ${pluginKey}/${widgetName}: ${String(error)}`,
345
- )
346
- }
347
-
348
313
  // Initialize on module load
349
314
  initializeWidgetDiscovery()
@@ -0,0 +1,37 @@
1
+ import type { Composition } from "@shared/types/core/Composition.types"
2
+ import { isDefined } from "@vueuse/core"
3
+ import { chain, filter, join, length, map, pipe, pickBy } from "ramda"
4
+
5
+ type WidgetsLayout = Composition["widgetsLayout"]
6
+
7
+ /** Count slots that reference a widget instance. */
8
+ export const countWidgetsInLayout = (layout: WidgetsLayout): number =>
9
+ pipe(
10
+ chain((column) => column),
11
+ filter((slot) => Boolean(slot.widget)),
12
+ length,
13
+ )(layout)
14
+
15
+ /**
16
+ * Stable signature of widget UUIDs in layout order.
17
+ * Used to detect structural layout changes without deep-reacting to mutations.
18
+ */
19
+ export const getWidgetsLayoutSignature = (layout: WidgetsLayout): string =>
20
+ pipe(
21
+ chain((column) => map((slot) => slot.widget?.uuid ?? "", column)),
22
+ join("|"),
23
+ )(layout)
24
+
25
+ /** Drop undefined route param values before passing them to widgets. */
26
+ export const cleanRouteParams = (
27
+ params: Record<string, string | undefined> | undefined,
28
+ ): Record<string, string> => {
29
+ if (!params) {
30
+ return {}
31
+ }
32
+
33
+ return pickBy(
34
+ (value) => isDefined(value) && value !== "undefined",
35
+ params,
36
+ ) as Record<string, string>
37
+ }
@@ -38,7 +38,6 @@ export const widgetLoader = (
38
38
  loader: async () => {
39
39
  try {
40
40
  validateWidget(widget)
41
-
42
41
  const originalComponent = await loadWidgetComponent(
43
42
  widget.pluginKey!,
44
43
  widget.name!,
@@ -0,0 +1,20 @@
1
+ import { WIDGET_CHUNK_LOADING_SELECTOR } from "@racletteOrchestrator/constants/widgetSlot"
2
+
3
+ /**
4
+ * True when a widget slot container has rendered plugin content.
5
+ *
6
+ * Ignores the orchestrator's async chunk loader (`LoadingWidgetState`).
7
+ * Widget-owned loading UI (skeletons, spinners) inside the slot counts as content.
8
+ */
9
+ export const hasWidgetSlotContent = (container: HTMLElement): boolean => {
10
+ if (container.querySelector(WIDGET_CHUNK_LOADING_SELECTOR)) {
11
+ return false
12
+ }
13
+
14
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT)
15
+ if (walker.nextNode()) {
16
+ return true
17
+ }
18
+
19
+ return Boolean(container.textContent?.trim())
20
+ }
@@ -1,9 +1,9 @@
1
- export type WidgetVisualInput = {
2
- title?: string
3
- color?: string
4
- icon?: string
5
- images?: string[]
6
- }
1
+ import type { WidgetDeclaration } from "@racletteOrchestrator/types/Widgets"
2
+
3
+ export type WidgetVisualInput = Pick<
4
+ WidgetDeclaration,
5
+ "title" | "color" | "icon" | "images"
6
+ >
7
7
 
8
8
  export type WidgetVisuals = {
9
9
  color: string
@@ -27,10 +27,7 @@ export const generateColorFromName = (name: string): string => {
27
27
  }
28
28
 
29
29
  export const getWidgetInitials = (name: string): string => {
30
- const parts = name
31
- .trim()
32
- .split(/\s+/)
33
- .filter(Boolean)
30
+ const parts = name.trim().split(/\s+/).filter(Boolean)
34
31
 
35
32
  if (parts.length === 0) {
36
33
  return "W"
@@ -59,8 +56,10 @@ export const normalizeWidgetVisuals = (
59
56
  : ""
60
57
 
61
58
  const normalizedImages = Array.isArray(details.images)
62
- // Ignore invalid/empty entries to keep consumers simple.
63
- ? details.images.filter((image) => typeof image === "string" && image.trim())
59
+ ? // Ignore invalid/empty entries to keep consumers simple.
60
+ details.images.filter(
61
+ (image) => typeof image === "string" && image.trim(),
62
+ )
64
63
  : []
65
64
 
66
65
  return {
@@ -0,0 +1,32 @@
1
+ import type { WidgetBase } from "@racletteCore/store/types"
2
+ import type {
3
+ OnAllWidgetsReady,
4
+ WidgetLifecycleManager,
5
+ } from "@racletteOrchestrator/composables/useWidgetLifecycle"
6
+ import { h, type Component } from "vue"
7
+
8
+ const noopAllWidgetsReady: OnAllWidgetsReady = () => {}
9
+
10
+ /** Injects `onAllWidgetsReady` into loaded widget components. */
11
+ export const wrapWidgetWithLifecycle = (
12
+ widget: WidgetBase,
13
+ originalComponent: Component,
14
+ getLifecycleManager: () => WidgetLifecycleManager | null,
15
+ ): Component => ({
16
+ name: `${originalComponent.name || "Widget"}Wrapper`,
17
+ setup(props, context) {
18
+ const onAllWidgetsReady =
19
+ getLifecycleManager()?.createLifecycleHooks(widget.uuid)
20
+ .onAllWidgetsReady ?? noopAllWidgetsReady
21
+
22
+ return () =>
23
+ h(
24
+ originalComponent,
25
+ {
26
+ ...props,
27
+ onAllWidgetsReady,
28
+ },
29
+ context.slots,
30
+ )
31
+ },
32
+ })
@@ -1,31 +1,6 @@
1
- import { loadingHold } from "@racletteCore/store/state"
2
- import { $store } from "@racletteCore"
3
- import { scheduleAfterPaint } from "@racletteCore/lib/scheduleTask"
4
1
  import type { RouteLocationNormalized } from "vue-router"
5
- import { handleRouteChange } from "../routeStore"
6
2
 
7
- /**
8
- * afterEachHook
9
- * @param { object } store, mini-rx store instance
10
- *
11
- * removeLoading Animation
12
- */
13
- export default (to: RouteLocationNormalized, from: RouteLocationNormalized) => {
14
- scheduleAfterPaint(() => {
15
- handleRouteChange(to, from)
16
- })
17
-
18
- const loading = $store.getState(["ui", "loading"])
19
- if (loading) {
20
- setTimeout(() => {
21
- scheduleAfterPaint(() => {
22
- if ($store.getState(["ui", "loading"])) {
23
- $store.dispatch({
24
- type: "ui/update",
25
- payload: { loading: false },
26
- })
27
- }
28
- })
29
- }, loadingHold)
30
- }
31
- }
3
+ export default (
4
+ _to: RouteLocationNormalized,
5
+ _from: RouteLocationNormalized,
6
+ ) => {}
@@ -1,6 +1,6 @@
1
1
  import { $store } from "@racletteCore"
2
2
  import type { RouteLocationNormalized } from "vue-router"
3
- import { checkForDefaultSlotChange } from "../routeStore"
3
+ import { checkForDefaultSlotChange, handleRouteChange } from "../routeStore"
4
4
 
5
5
  export default async (
6
6
  to: RouteLocationNormalized,
@@ -16,6 +16,9 @@ export default async (
16
16
  }
17
17
  }
18
18
 
19
+ // Apply target composition slots before the loading gate remounts page widgets.
20
+ handleRouteChange(to, from)
21
+
19
22
  // show loading animation only, if the default slot's value changes
20
23
  if (checkForDefaultSlotChange(to, from)) {
21
24
  $store.dispatch({
@@ -38,22 +38,28 @@ export type ConfigurableParameters = Record<
38
38
  EditorSettings & { defaultValue: any }
39
39
  >
40
40
 
41
+ /** Catalog entry after registration (workbench picker, APIs). All fields are required. */
41
42
  export type RegisteredWidgetDeclaration = {
42
43
  pluginKey: string
43
44
  author: string
45
+ name: string
44
46
  title: string
47
+ description: string
45
48
  color: string
46
49
  icon: string
47
50
  images: string[]
48
- description: string
49
51
  configurableParameters?: ConfigurableParameters
50
- name: string
51
52
  }
52
53
 
53
- export type WidgetDeclaration = Omit<
54
- RegisteredWidgetDeclaration,
55
- "pluginKey" | "author" | "name" // name will be extracted via filename, cannot be overwritten
56
- > & {
54
+ /**
55
+ * Plugin-authored `details` in `frontend/widgets/<WidgetFolder>/setup.ts`.
56
+ *
57
+ * `pluginKey`, `author`, and `name` are added by the framework (from plugin metadata
58
+ * and the `*Widget.vue` filename). Optional visual fields may be omitted or left
59
+ * empty — `normalizeWidgetVisuals` fills defaults before producing a
60
+ * `RegisteredWidgetDeclaration`.
61
+ */
62
+ export type WidgetDeclaration = {
57
63
  title: string
58
64
  description?: string
59
65
  color?: string
package/src/types.ts CHANGED
@@ -21,9 +21,15 @@ export type RacletteConfig = {
21
21
  port: number
22
22
  name?: string
23
23
  volume?: string
24
+ /** Default Valkey DB index for all roles when role-specific value is unset (default: 0). */
24
25
  db?: number
26
+ /** Per-stack Valkey DB overrides (app / workbench / core). */
27
+ dbs?: {
28
+ app?: number
29
+ workbench?: number
30
+ core?: number
31
+ }
25
32
  volumes?: VolumeDefinition[] // Add custom volumes property
26
-
27
33
  }
28
34
  workbench?: {
29
35
  enabled: boolean