qdadm 0.29.0 → 0.31.0

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 (49) hide show
  1. package/package.json +11 -2
  2. package/src/components/forms/FormPage.vue +1 -1
  3. package/src/components/index.js +5 -3
  4. package/src/components/layout/AppLayout.vue +13 -1
  5. package/src/components/layout/Zone.vue +40 -23
  6. package/src/composables/index.js +2 -1
  7. package/src/composables/useAuth.js +43 -4
  8. package/src/composables/useCurrentEntity.js +44 -0
  9. package/src/composables/useFormPageBuilder.js +3 -3
  10. package/src/composables/useNavContext.js +24 -8
  11. package/src/composables/useSSEBridge.js +118 -0
  12. package/src/debug/AuthCollector.js +254 -0
  13. package/src/debug/Collector.js +235 -0
  14. package/src/debug/DebugBridge.js +163 -0
  15. package/src/debug/DebugModule.js +215 -0
  16. package/src/debug/EntitiesCollector.js +376 -0
  17. package/src/debug/ErrorCollector.js +66 -0
  18. package/src/debug/LocalStorageAdapter.js +150 -0
  19. package/src/debug/SignalCollector.js +87 -0
  20. package/src/debug/ToastCollector.js +82 -0
  21. package/src/debug/ZonesCollector.js +300 -0
  22. package/src/debug/components/DebugBar.vue +1232 -0
  23. package/src/debug/components/ObjectTree.vue +194 -0
  24. package/src/debug/components/index.js +8 -0
  25. package/src/debug/components/panels/AuthPanel.vue +103 -0
  26. package/src/debug/components/panels/EntitiesPanel.vue +616 -0
  27. package/src/debug/components/panels/EntriesPanel.vue +188 -0
  28. package/src/debug/components/panels/ToastsPanel.vue +112 -0
  29. package/src/debug/components/panels/ZonesPanel.vue +232 -0
  30. package/src/debug/components/panels/index.js +8 -0
  31. package/src/debug/index.js +31 -0
  32. package/src/editors/index.js +12 -0
  33. package/src/entity/EntityManager.js +142 -20
  34. package/src/entity/storage/MockApiStorage.js +17 -1
  35. package/src/entity/storage/index.js +9 -2
  36. package/src/gen/index.js +3 -0
  37. package/src/index.js +7 -0
  38. package/src/kernel/Kernel.js +661 -48
  39. package/src/kernel/KernelContext.js +385 -0
  40. package/src/kernel/Module.js +111 -0
  41. package/src/kernel/ModuleLoader.js +573 -0
  42. package/src/kernel/SSEBridge.js +354 -0
  43. package/src/kernel/SignalBus.js +10 -7
  44. package/src/kernel/index.js +19 -0
  45. package/src/toast/ToastBridgeModule.js +70 -0
  46. package/src/toast/ToastListener.vue +47 -0
  47. package/src/toast/index.js +15 -0
  48. package/src/toast/useSignalToast.js +113 -0
  49. package/src/composables/useSSE.js +0 -212
@@ -0,0 +1,354 @@
1
+ /**
2
+ * SSEBridge - Server-Sent Events to SignalBus bridge
3
+ *
4
+ * Manages a single SSE connection and emits all events to SignalBus.
5
+ * Components subscribe via signals.on('sse:eventName', handler) instead of
6
+ * managing their own EventSource connections.
7
+ *
8
+ * Benefits:
9
+ * - Single SSE connection per app (vs. one per component)
10
+ * - Decoupled event handling via SignalBus
11
+ * - Automatic reconnection with configurable delay
12
+ * - Connection status available via signals
13
+ *
14
+ * Signal naming:
15
+ * - `sse:connected` - Emitted when connection established
16
+ * - `sse:disconnected` - Emitted when connection lost
17
+ * - `sse:error` - Emitted on connection error
18
+ * - `sse:{eventName}` - SSE event forwarded (e.g., sse:task:completed)
19
+ *
20
+ * Usage in Kernel:
21
+ * ```js
22
+ * new Kernel({
23
+ * sse: {
24
+ * url: '/api/events',
25
+ * reconnectDelay: 5000,
26
+ * signalPrefix: 'sse',
27
+ * autoConnect: true
28
+ * }
29
+ * })
30
+ * ```
31
+ *
32
+ * Usage in components:
33
+ * ```js
34
+ * const signals = inject('qdadmSignals')
35
+ *
36
+ * // Subscribe to specific SSE events
37
+ * signals.on('sse:task:completed', ({ data }) => {
38
+ * console.log('Task completed:', data)
39
+ * })
40
+ *
41
+ * // Subscribe to all SSE events via wildcard
42
+ * signals.on('sse:*', ({ event, data }) => {
43
+ * console.log(`SSE event ${event}:`, data)
44
+ * })
45
+ * ```
46
+ */
47
+
48
+ export const SSE_SIGNALS = {
49
+ CONNECTED: 'sse:connected',
50
+ DISCONNECTED: 'sse:disconnected',
51
+ ERROR: 'sse:error',
52
+ MESSAGE: 'sse:message'
53
+ }
54
+
55
+ export class SSEBridge {
56
+ /**
57
+ * @param {object} options
58
+ * @param {SignalBus} options.signals - SignalBus instance
59
+ * @param {string} options.url - SSE endpoint URL
60
+ * @param {number} options.reconnectDelay - Delay before reconnect (ms), 0 to disable
61
+ * @param {string} options.signalPrefix - Prefix for emitted signals (default: 'sse')
62
+ * @param {boolean} options.autoConnect - Connect immediately (default: false, waits for auth:login)
63
+ * @param {boolean} options.withCredentials - Include credentials in request
64
+ * @param {string} options.tokenParam - Query param name for auth token
65
+ * @param {function} options.getToken - Function to get auth token
66
+ * @param {boolean} options.debug - Enable debug logging
67
+ */
68
+ constructor(options) {
69
+ const {
70
+ signals,
71
+ url,
72
+ reconnectDelay = 5000,
73
+ signalPrefix = 'sse',
74
+ autoConnect = false,
75
+ withCredentials = false,
76
+ tokenParam = 'token',
77
+ getToken = null,
78
+ debug = false
79
+ } = options
80
+
81
+ if (!signals) {
82
+ throw new Error('[SSEBridge] signals (SignalBus) is required')
83
+ }
84
+ if (!url) {
85
+ throw new Error('[SSEBridge] url is required')
86
+ }
87
+
88
+ this._signals = signals
89
+ this._url = url
90
+ this._reconnectDelay = reconnectDelay
91
+ this._signalPrefix = signalPrefix
92
+ this._withCredentials = withCredentials
93
+ this._tokenParam = tokenParam
94
+ this._getToken = getToken
95
+ this._debug = debug
96
+
97
+ this._eventSource = null
98
+ this._reconnectTimer = null
99
+ this._connected = false
100
+ this._reconnecting = false
101
+
102
+ // Auto-connect or wait for auth:login
103
+ if (autoConnect) {
104
+ this.connect()
105
+ } else {
106
+ // Wait for auth:login signal to connect
107
+ this._signals.once('auth:login').then(() => {
108
+ this._log('Received auth:login, connecting SSE')
109
+ this.connect()
110
+ })
111
+ }
112
+
113
+ // Disconnect on auth:logout
114
+ this._signals.on('auth:logout', () => {
115
+ this._log('Received auth:logout, disconnecting SSE')
116
+ this.disconnect()
117
+ })
118
+ }
119
+
120
+ /**
121
+ * Build signal name with prefix
122
+ * @param {string} eventName - Event name
123
+ * @returns {string} Full signal name
124
+ */
125
+ _buildSignal(eventName) {
126
+ return `${this._signalPrefix}:${eventName}`
127
+ }
128
+
129
+ /**
130
+ * Debug logging
131
+ */
132
+ _log(...args) {
133
+ if (this._debug) {
134
+ console.debug('[SSEBridge]', ...args)
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Build SSE URL with auth token
140
+ * @returns {string}
141
+ */
142
+ _buildUrl() {
143
+ const token = this._getToken?.()
144
+ const sseUrl = new URL(this._url, window.location.origin)
145
+
146
+ if (token && this._tokenParam) {
147
+ sseUrl.searchParams.set(this._tokenParam, token)
148
+ }
149
+
150
+ return sseUrl.toString()
151
+ }
152
+
153
+ /**
154
+ * Connect to SSE endpoint
155
+ */
156
+ connect() {
157
+ // Clean up existing
158
+ if (this._eventSource) {
159
+ this._eventSource.close()
160
+ this._eventSource = null
161
+ }
162
+
163
+ // Clear pending reconnect
164
+ if (this._reconnectTimer) {
165
+ clearTimeout(this._reconnectTimer)
166
+ this._reconnectTimer = null
167
+ }
168
+
169
+ try {
170
+ const url = this._buildUrl()
171
+ this._log('Connecting to', url)
172
+
173
+ this._eventSource = new EventSource(url, {
174
+ withCredentials: this._withCredentials
175
+ })
176
+
177
+ this._eventSource.onopen = () => {
178
+ this._connected = true
179
+ this._reconnecting = false
180
+ this._log('Connected')
181
+
182
+ this._signals.emit(SSE_SIGNALS.CONNECTED, {
183
+ url: this._url,
184
+ timestamp: new Date()
185
+ })
186
+ }
187
+
188
+ this._eventSource.onerror = (err) => {
189
+ this._connected = false
190
+ this._log('Connection error')
191
+
192
+ this._signals.emit(SSE_SIGNALS.ERROR, {
193
+ error: 'Connection error',
194
+ timestamp: new Date()
195
+ })
196
+
197
+ // Close broken connection
198
+ if (this._eventSource) {
199
+ this._eventSource.close()
200
+ this._eventSource = null
201
+ }
202
+
203
+ this._signals.emit(SSE_SIGNALS.DISCONNECTED, {
204
+ timestamp: new Date()
205
+ })
206
+
207
+ // Schedule reconnect
208
+ this._scheduleReconnect()
209
+ }
210
+
211
+ // Handle generic message events (event: message)
212
+ this._eventSource.onmessage = (event) => {
213
+ this._handleEvent('message', event)
214
+ }
215
+
216
+ // For named events, we need to add listeners dynamically
217
+ // Since EventSource doesn't expose event names, we use a wrapper approach:
218
+ // The server should send events with `event:` field, we handle them generically
219
+
220
+ // Note: Named events require explicit addEventListener.
221
+ // To support arbitrary event names, the app should either:
222
+ // 1. Use `message` event type and include event name in data
223
+ // 2. Pre-register known event names via registerEvents()
224
+
225
+ } catch (err) {
226
+ this._log('Connect error:', err.message)
227
+ this._signals.emit(SSE_SIGNALS.ERROR, {
228
+ error: err.message,
229
+ timestamp: new Date()
230
+ })
231
+ this._scheduleReconnect()
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Register listeners for specific SSE event types
237
+ * Required because EventSource requires explicit addEventListener for named events.
238
+ *
239
+ * @param {string[]} eventNames - Event names to listen for
240
+ */
241
+ registerEvents(eventNames) {
242
+ if (!this._eventSource) {
243
+ this._log('Cannot register events: not connected')
244
+ return
245
+ }
246
+
247
+ for (const eventName of eventNames) {
248
+ this._eventSource.addEventListener(eventName, (event) => {
249
+ this._handleEvent(eventName, event)
250
+ })
251
+ this._log('Registered event:', eventName)
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Handle incoming SSE event
257
+ * @param {string} eventName - Event name
258
+ * @param {MessageEvent} event - SSE event
259
+ */
260
+ _handleEvent(eventName, event) {
261
+ let data
262
+ try {
263
+ data = JSON.parse(event.data)
264
+ } catch {
265
+ data = event.data
266
+ }
267
+
268
+ const signal = this._buildSignal(eventName)
269
+ this._log(`Emitting ${signal}:`, data)
270
+
271
+ this._signals.emit(signal, {
272
+ event: eventName,
273
+ data,
274
+ timestamp: new Date(),
275
+ lastEventId: event.lastEventId
276
+ })
277
+ }
278
+
279
+ /**
280
+ * Schedule reconnection
281
+ */
282
+ _scheduleReconnect() {
283
+ if (this._reconnectDelay <= 0) return
284
+ if (this._reconnectTimer) return
285
+
286
+ this._reconnecting = true
287
+ this._log(`Reconnecting in ${this._reconnectDelay}ms`)
288
+
289
+ this._reconnectTimer = setTimeout(() => {
290
+ this._reconnectTimer = null
291
+ if (!this._connected) {
292
+ this.connect()
293
+ }
294
+ }, this._reconnectDelay)
295
+ }
296
+
297
+ /**
298
+ * Disconnect from SSE endpoint
299
+ */
300
+ disconnect() {
301
+ if (this._reconnectTimer) {
302
+ clearTimeout(this._reconnectTimer)
303
+ this._reconnectTimer = null
304
+ }
305
+
306
+ if (this._eventSource) {
307
+ this._eventSource.close()
308
+ this._eventSource = null
309
+ }
310
+
311
+ if (this._connected) {
312
+ this._connected = false
313
+ this._signals.emit(SSE_SIGNALS.DISCONNECTED, {
314
+ timestamp: new Date()
315
+ })
316
+ }
317
+
318
+ this._reconnecting = false
319
+ this._log('Disconnected')
320
+ }
321
+
322
+ /**
323
+ * Reconnect (disconnect + connect)
324
+ */
325
+ reconnect() {
326
+ this.disconnect()
327
+ this.connect()
328
+ }
329
+
330
+ /**
331
+ * Check if connected
332
+ * @returns {boolean}
333
+ */
334
+ isConnected() {
335
+ return this._connected
336
+ }
337
+
338
+ /**
339
+ * Check if reconnecting
340
+ * @returns {boolean}
341
+ */
342
+ isReconnecting() {
343
+ return this._reconnecting
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Factory function to create SSEBridge
349
+ * @param {object} options - SSEBridge options
350
+ * @returns {SSEBridge}
351
+ */
352
+ export function createSSEBridge(options) {
353
+ return new SSEBridge(options)
354
+ }
@@ -25,6 +25,14 @@ export const SIGNALS = {
25
25
  ENTITY_UPDATED: 'entity:updated',
26
26
  ENTITY_DELETED: 'entity:deleted',
27
27
 
28
+ // Auth lifecycle signals
29
+ AUTH_LOGIN: 'auth:login',
30
+ AUTH_LOGOUT: 'auth:logout',
31
+ AUTH_EXPIRED: 'auth:expired', // Emitted on 401/403 API responses
32
+
33
+ // API error signals
34
+ API_ERROR: 'api:error', // Emitted on any API error { status, message, url }
35
+
28
36
  // Pattern for entity-specific signals
29
37
  // Use buildSignal(entityName, action) for these
30
38
  }
@@ -118,13 +126,8 @@ export class SignalBus {
118
126
  * @returns {Promise<void>}
119
127
  */
120
128
  async emitEntity(entityName, action, data) {
121
- const specificSignal = buildSignal(entityName, action)
122
- const genericSignal = buildSignal('entity', action)
123
-
124
- // Emit both specific and generic signals
125
- // Specific first, then generic
126
- await this.emit(specificSignal, { entity: entityName, data })
127
- await this.emit(genericSignal, { entity: entityName, data })
129
+ const signal = buildSignal('entity', action)
130
+ await this.emit(signal, { entity: entityName, data })
128
131
  }
129
132
 
130
133
  /**
@@ -16,3 +16,22 @@ export {
16
16
  EventRouter,
17
17
  createEventRouter,
18
18
  } from './EventRouter.js'
19
+ export {
20
+ SSEBridge,
21
+ createSSEBridge,
22
+ SSE_SIGNALS,
23
+ } from './SSEBridge.js'
24
+ export {
25
+ Module,
26
+ } from './Module.js'
27
+ export {
28
+ KernelContext,
29
+ createKernelContext,
30
+ } from './KernelContext.js'
31
+ export {
32
+ ModuleLoader,
33
+ createModuleLoader,
34
+ ModuleNotFoundError,
35
+ CircularDependencyError,
36
+ ModuleLoadError,
37
+ } from './ModuleLoader.js'
@@ -0,0 +1,70 @@
1
+ /**
2
+ * ToastBridgeModule - Bridges signal bus to PrimeVue Toast
3
+ *
4
+ * This module integrates toast functionality via the signal bus:
5
+ * - Registers ToastListener component to handle toast:* signals
6
+ * - Provides useSignalToast() composable for emitting toasts
7
+ *
8
+ * Usage in modules:
9
+ * import { useSignalToast } from 'qdadm'
10
+ * const toast = useSignalToast()
11
+ * toast.success('Saved!', 'Your changes have been saved')
12
+ * toast.error('Error', 'Something went wrong')
13
+ *
14
+ * @example
15
+ * import { createKernel, ToastBridgeModule } from 'qdadm'
16
+ *
17
+ * const kernel = createKernel()
18
+ * kernel.use(new ToastBridgeModule())
19
+ * await kernel.boot()
20
+ */
21
+
22
+ import { Module } from '../kernel/Module.js'
23
+ import ToastListener from './ToastListener.vue'
24
+
25
+ /**
26
+ * Zone name for toast listener component
27
+ * Prefixed with _ to hide from ZonesCollector (internal zone)
28
+ */
29
+ export const TOAST_ZONE = '_app:toasts'
30
+
31
+ /**
32
+ * ToastBridgeModule - Handles toast notifications via signals
33
+ */
34
+ export class ToastBridgeModule extends Module {
35
+ /**
36
+ * Module identifier
37
+ * @type {string}
38
+ */
39
+ static name = 'toast-bridge'
40
+
41
+ /**
42
+ * No dependencies
43
+ * @type {string[]}
44
+ */
45
+ static requires = []
46
+
47
+ /**
48
+ * High priority - should load early
49
+ * @type {number}
50
+ */
51
+ static priority = 10
52
+
53
+ /**
54
+ * Connect module to kernel
55
+ * @param {import('../kernel/KernelContext.js').KernelContext} ctx
56
+ */
57
+ async connect(ctx) {
58
+ // Define the toast zone
59
+ ctx.zone(TOAST_ZONE)
60
+
61
+ // Register ToastListener component in the zone
62
+ ctx.block(TOAST_ZONE, {
63
+ id: 'toast-listener',
64
+ component: ToastListener,
65
+ weight: 0
66
+ })
67
+ }
68
+ }
69
+
70
+ export default ToastBridgeModule
@@ -0,0 +1,47 @@
1
+ <script setup>
2
+ /**
3
+ * ToastListener - Bridges signal bus to PrimeVue Toast
4
+ *
5
+ * Invisible component that listens to toast:* signals and displays
6
+ * them using PrimeVue's Toast component.
7
+ *
8
+ * This component should be registered in a zone via ToastBridgeModule.
9
+ */
10
+ import { onMounted, onUnmounted, inject } from 'vue'
11
+ import { useToast } from 'primevue/usetoast'
12
+
13
+ const toast = useToast()
14
+ const signals = inject('qdadmSignals')
15
+
16
+ let unsubscribe = null
17
+
18
+ onMounted(() => {
19
+ if (!signals) {
20
+ console.warn('[ToastListener] No signals bus injected')
21
+ return
22
+ }
23
+
24
+ // Listen to all toast signals
25
+ unsubscribe = signals.on('toast:*', (event) => {
26
+ const severity = event.name.split(':')[1] // toast:success -> success
27
+ toast.add({
28
+ severity,
29
+ summary: event.data?.summary,
30
+ detail: event.data?.detail,
31
+ life: event.data?.life ?? 3000
32
+ })
33
+ })
34
+ })
35
+
36
+ onUnmounted(() => {
37
+ if (unsubscribe) {
38
+ unsubscribe()
39
+ unsubscribe = null
40
+ }
41
+ })
42
+ </script>
43
+
44
+ <template>
45
+ <!-- Invisible listener component -->
46
+ <span style="display: none" />
47
+ </template>
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Toast Module - Signal-based toast notifications
3
+ *
4
+ * Provides toast notifications that go through the signal bus,
5
+ * allowing for proper debugging and interception.
6
+ *
7
+ * Components:
8
+ * - ToastBridgeModule: Registers ToastListener for handling signals
9
+ * - ToastListener: Vue component that displays toasts via PrimeVue
10
+ * - useSignalToast: Composable for emitting toast signals
11
+ */
12
+
13
+ export { ToastBridgeModule, TOAST_ZONE } from './ToastBridgeModule.js'
14
+ export { useSignalToast } from './useSignalToast.js'
15
+ export { default as ToastListener } from './ToastListener.vue'
@@ -0,0 +1,113 @@
1
+ /**
2
+ * useSignalToast - Composable for emitting toast notifications via signals
3
+ *
4
+ * This composable provides a simple API for showing toasts that go through
5
+ * the signal bus. Works with ToastBridgeModule which handles displaying
6
+ * the toasts via PrimeVue.
7
+ *
8
+ * @example
9
+ * import { useSignalToast } from 'qdadm'
10
+ *
11
+ * // With explicit emitter name
12
+ * const toast = useSignalToast('MyComponent')
13
+ * toast.success('Saved!', 'Your changes have been saved')
14
+ *
15
+ * // Without emitter (defaults to 'unknown')
16
+ * const toast = useSignalToast()
17
+ * toast.error('Error', 'Something went wrong')
18
+ */
19
+
20
+ import { inject, getCurrentInstance } from 'vue'
21
+
22
+ /**
23
+ * Composable for emitting toast notifications via signal bus
24
+ * @param {string} [emitter] - Identifier for the toast source (component/module name)
25
+ * @returns {object} Toast API
26
+ */
27
+ export function useSignalToast(emitter) {
28
+ const signals = inject('qdadmSignals')
29
+
30
+ // Try to auto-detect emitter from current component if not provided
31
+ const instance = getCurrentInstance()
32
+ const resolvedEmitter =
33
+ emitter || instance?.type?.name || instance?.type?.__name || 'unknown'
34
+
35
+ if (!signals) {
36
+ console.warn('[useSignalToast] No signals bus injected - toasts will not work')
37
+ // Return no-op functions
38
+ return {
39
+ success: () => {},
40
+ error: () => {},
41
+ info: () => {},
42
+ warn: () => {},
43
+ add: () => {}
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Show a toast notification
49
+ * @param {string} severity - Toast severity (success, info, warn, error)
50
+ * @param {string} summary - Toast title
51
+ * @param {string} [detail] - Toast detail message
52
+ * @param {number} [life=3000] - Duration in ms (0 for sticky)
53
+ */
54
+ function add(severity, summary, detail, life = 3000) {
55
+ signals.emit(`toast:${severity}`, { summary, detail, life, emitter: resolvedEmitter })
56
+ }
57
+
58
+ return {
59
+ /**
60
+ * Show success toast
61
+ * @param {string} summary - Toast title
62
+ * @param {string} [detail] - Toast detail message
63
+ * @param {number} [life=3000] - Duration in ms
64
+ */
65
+ success(summary, detail, life = 3000) {
66
+ add('success', summary, detail, life)
67
+ },
68
+
69
+ /**
70
+ * Show error toast
71
+ * @param {string} summary - Toast title
72
+ * @param {string} [detail] - Toast detail message
73
+ * @param {number} [life=5000] - Duration in ms (longer for errors)
74
+ */
75
+ error(summary, detail, life = 5000) {
76
+ add('error', summary, detail, life)
77
+ },
78
+
79
+ /**
80
+ * Show info toast
81
+ * @param {string} summary - Toast title
82
+ * @param {string} [detail] - Toast detail message
83
+ * @param {number} [life=3000] - Duration in ms
84
+ */
85
+ info(summary, detail, life = 3000) {
86
+ add('info', summary, detail, life)
87
+ },
88
+
89
+ /**
90
+ * Show warning toast
91
+ * @param {string} summary - Toast title
92
+ * @param {string} [detail] - Toast detail message
93
+ * @param {number} [life=4000] - Duration in ms
94
+ */
95
+ warn(summary, detail, life = 4000) {
96
+ add('warn', summary, detail, life)
97
+ },
98
+
99
+ /**
100
+ * Generic add method (compatible with PrimeVue toast.add)
101
+ * @param {object} options - Toast options
102
+ * @param {string} options.severity - Toast severity
103
+ * @param {string} options.summary - Toast title
104
+ * @param {string} [options.detail] - Toast detail
105
+ * @param {number} [options.life] - Duration in ms
106
+ */
107
+ add(options) {
108
+ add(options.severity || 'info', options.summary, options.detail, options.life)
109
+ }
110
+ }
111
+ }
112
+
113
+ export default useSignalToast