@bagelink/vue 1.6.36 → 1.6.39

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.6.36",
4
+ "version": "1.6.39",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -62,7 +62,8 @@
62
62
  "@types/signature_pad": "^4.0.0",
63
63
  "@vue-macros/reactivity-transform": "^1.1.6",
64
64
  "vue": "^3.5.16",
65
- "vue-component-type-helpers": "^2.2.10"
65
+ "vue-component-type-helpers": "^2.2.10",
66
+ "vue-toastification": "^2.0.0-rc.5"
66
67
  },
67
68
  "peerDependencies": {
68
69
  "@bagelink/sdk": "*",
@@ -70,11 +71,15 @@
70
71
  "vue-component-type-helpers": "^2.2.10",
71
72
  "vue": "*",
72
73
  "vue-draggable-next": "^2.2.1",
73
- "vue-router": "*"
74
+ "vue-router": "*",
75
+ "vue-toastification": "^2"
74
76
  },
75
77
  "peerDependenciesMeta": {
76
78
  "vue-draggable-next": {
77
79
  "optional": true
80
+ },
81
+ "vue-toastification": {
82
+ "optional": true
78
83
  }
79
84
  },
80
85
  "publishConfig": {
@@ -7,8 +7,6 @@ import {
7
7
  Title
8
8
  } from '@bagelink/vue'
9
9
  import {
10
- onMounted,
11
- onUnmounted,
12
10
  useSlots,
13
11
  watch
14
12
  } from 'vue'
@@ -43,27 +41,27 @@ let isVisible = $ref<boolean>(false)
43
41
  watch(
44
42
  () => props.visible,
45
43
  (val) => {
46
- if (val === isVisible || val === undefined) {return}
47
- if (val) {openModal()}
48
- else {closeModal()}
44
+ if (val === isVisible || val === undefined) { return }
45
+ if (val) { openModal() }
46
+ else { closeModal() }
49
47
  },
50
48
  { immediate: true },
51
49
  )
52
50
 
53
51
  const maxWidth = $computed(() => {
54
52
  const { width } = props
55
- if (width?.match(/px|em|rem|vw|vh|%/)) {return { 'max-width': width }}
56
- if (width?.match(/\d+/)) {return { 'max-width': `${width}px` }}
53
+ if (width?.match(/px|em|rem|vw|vh|%/)) { return { 'max-width': width } }
54
+ if (width?.match(/\d+/)) { return { 'max-width': `${width}px` } }
57
55
  return { 'max-width': '720px' }
58
56
  })
59
57
 
60
58
  // Computed properties for close button placement
61
- const isOverlay = $computed(() => 'overlay' === props.closePlacement || 'overlay-end' === props.closePlacement)
62
- const isHeader = $computed(() => 'header' === props.closePlacement || 'header-end' === props.closePlacement)
63
- const isFooter = $computed(() => 'footer' === props.closePlacement)
59
+ const isOverlay = $computed(() => props.closePlacement === 'overlay' || props.closePlacement === 'overlay-end')
60
+ const isHeader = $computed(() => props.closePlacement === 'header' || props.closePlacement === 'header-end')
61
+ const isFooter = $computed(() => props.closePlacement === 'footer')
64
62
 
65
63
  const overlayCloseClass = $computed(() => {
66
- if ('overlay-end' === props.closePlacement) {return 'top-1 end-1'}
64
+ if (props.closePlacement === 'overlay-end') { return 'top-1 end-1' }
67
65
  return 'top-1 start-1'
68
66
  })
69
67
 
@@ -74,21 +72,11 @@ function closeModal() {
74
72
 
75
73
  defineExpose({ closeModal })
76
74
 
77
- function escapeKeyClose(e: KeyboardEvent) {
78
- if (props.dismissable && 'Escape' === e.key) {
79
- closeModal()
80
- }
81
- }
82
-
83
75
  function openModal() {
84
76
  setTimeout(() => (isVisible = true), 1)
85
77
  }
86
78
 
87
- onMounted(() => { document.addEventListener('keydown', escapeKeyClose) })
88
-
89
- onUnmounted(() => {
90
- document.removeEventListener('keydown', escapeKeyClose)
91
- })
79
+ // Note: ESC key handling is now done centrally in ModalPlugin for proper stacking behavior
92
80
  </script>
93
81
 
94
82
  <template>
@@ -103,7 +103,7 @@ const input = $ref<HTMLInputElement>()
103
103
 
104
104
  // Use custom validation function
105
105
  function validateEmail(value: string) {
106
- if (!value) {return}
106
+ if (!value) { return }
107
107
 
108
108
  // Basic format validation
109
109
  if (!EMAIL_REGEX.test(value)) {
@@ -133,19 +133,19 @@ const debouncedEmit = useDebounceFn(() => { emit('debounce', inputVal) }, 700)
133
133
 
134
134
  // Validate input directly when value changes
135
135
  function validateInput() {
136
- if (!input) {return}
136
+ if (!input) { return }
137
137
 
138
138
  input.setCustomValidity('')
139
- if (!inputVal) {return}
139
+ if (!inputVal) { return }
140
140
  const validationResult = validateEmail(inputVal)
141
- if ('string' === typeof validationResult) {
141
+ if (typeof validationResult === 'string') {
142
142
  input.setCustomValidity(validationResult)
143
143
  }
144
144
  }
145
145
 
146
146
  // Perform server validation of email
147
147
  async function validateEmailWithServer(email: string) {
148
- if (!props.serverValidate || !email || !EMAIL_REGEX.test(email)) {return}
148
+ if (!props.serverValidate || !email || !EMAIL_REGEX.test(email)) { return }
149
149
 
150
150
  // If we've already validated this email, use cached result
151
151
  if (validatedEmails.has(email)) {
@@ -163,10 +163,10 @@ async function validateEmailWithServer(email: string) {
163
163
  headers: { 'Content-Type': 'application/json' }
164
164
  })
165
165
 
166
- if (!response.ok) {throw new Error('Validation service unavailable')}
166
+ if (!response.ok) { throw new Error('Validation service unavailable') }
167
167
 
168
168
  const result = await response.json()
169
- const isValid = 'valid' === result.status || true === result.has_mx
169
+ const isValid = result.status === 'valid' || result.has_mx === true
170
170
 
171
171
  isValidEmail.value = isValid
172
172
  validatedEmails.set(email, isValid)
@@ -186,7 +186,7 @@ async function validateEmailWithServer(email: string) {
186
186
 
187
187
  // Check for email typos and suggest corrections
188
188
  function checkForTypos(email: string) {
189
- if (!props.autocorrect || !email) {return}
189
+ if (!props.autocorrect || !email) { return }
190
190
 
191
191
  // Handle case where domain is incomplete (missing TLD)
192
192
  if (email.includes('@') && !email.includes('.')) {
@@ -203,7 +203,7 @@ function checkForTypos(email: string) {
203
203
  }
204
204
 
205
205
  // Standard typo checking for complete emails
206
- if (!email.includes('@')) {return}
206
+ if (!email.includes('@')) { return }
207
207
 
208
208
  const [username, domain] = email.split('@')
209
209
  const domainLower = domain.toLowerCase()
@@ -216,7 +216,7 @@ function checkForTypos(email: string) {
216
216
 
217
217
  // Find close matches using Levenshtein distance
218
218
  for (const commonDomain of COMMON_EMAIL_DOMAINS) {
219
- if (2 >= calculateLevenshteinDistance(domainLower, commonDomain)) {
219
+ if (calculateLevenshteinDistance(domainLower, commonDomain) <= 2) {
220
220
  suggestedCorrection.value = `${username}@${commonDomain}`.toLowerCase()
221
221
  return
222
222
  }
@@ -265,7 +265,7 @@ function calculateLevenshteinDistance(a: string, b: string): number {
265
265
  const debouncedServerValidate = useDebounceFn(() => validateEmailWithServer(inputVal), 1000)
266
266
 
267
267
  function updateInputVal() {
268
- if (props.disabled) {return}
268
+ if (props.disabled) { return }
269
269
 
270
270
  // Remove typo checking while typing - only do this on focusout now
271
271
  // checkForTypos(inputVal)
@@ -293,7 +293,7 @@ function handleFocusout(e: FocusEvent) {
293
293
  validateEmailWithServer(inputVal)
294
294
  }
295
295
 
296
- if (props.onFocusout) {props.onFocusout(e)}
296
+ if (props.onFocusout) { props.onFocusout(e) }
297
297
  }
298
298
 
299
299
  watch(
@@ -318,30 +318,27 @@ const focus = () => input?.focus()
318
318
  defineExpose({ focus, hasFocus })
319
319
 
320
320
  onMounted(() => {
321
- if (props.autofocus) {setTimeout(() => input?.focus(), 10)}
321
+ if (props.autofocus) { setTimeout(() => input?.focus(), 10) }
322
322
  // Don't auto-restore defaultValue - let user control their own content
323
323
  })
324
324
  </script>
325
325
 
326
326
  <template>
327
327
  <div
328
- class="bagel-input text-input"
329
- :class="{
328
+ class="bagel-input text-input" :class="{
330
329
  small,
331
330
  shrink,
332
331
  'textInputIconWrap': icon,
333
332
  'txtInputIconStart': iconStart,
334
333
  'is-validating': isValidating,
335
- }"
336
- :title="title"
334
+ }" :title="title"
337
335
  >
338
336
  <label :for="id">
339
337
  <div class="flex">
340
338
  {{ label }} <span v-if="required">*</span>
341
339
  <span v-if="helptext" class="opacity-7 light">{{ helptext }}</span>
342
340
  <span
343
- v-if="suggestedCorrection"
344
- class="pointer nowrap inline-block ms-auto color-red txt-10px p-0"
341
+ v-if="suggestedCorrection" class="pointer nowrap inline-block ms-auto color-red txt-10px p-0"
345
342
  @click.prevent="applyCorrection"
346
343
  >
347
344
  did you mean {{ suggestedCorrection }}?
@@ -349,32 +346,12 @@ onMounted(() => {
349
346
  <span v-if="isValidating" class="validating">Validating email...</span>
350
347
  </div>
351
348
  <input
352
- :id
353
- ref="input"
354
- v-model="inputVal"
355
- v-pattern:lower
356
- class="ltr"
357
- :name
358
- :title
359
- autocomplete="email"
360
- type="email"
361
- :placeholder="placeholder || label"
362
- :disabled
363
- :required
364
- v-bind="nativeInputAttrs"
365
- @focusout="handleFocusout"
366
- @focus="onFocus"
367
- @input="updateInputVal"
349
+ :id ref="input" v-model="inputVal" v-pattern:lower class="ltr" :name :title autocomplete="email"
350
+ type="email" :placeholder="placeholder || label" :disabled :required v-bind="nativeInputAttrs"
351
+ @focusout="handleFocusout" @focus="onFocus" @input="updateInputVal"
368
352
  >
369
- <Icon
370
- v-if="iconStart"
371
- class="iconStart"
372
- :icon="iconStart"
373
- />
374
- <Icon
375
- v-if="icon"
376
- :icon="icon"
377
- />
353
+ <Icon v-if="iconStart" class="iconStart" :icon="iconStart" />
354
+ <Icon v-if="icon" :icon="icon" />
378
355
  </label>
379
356
  </div>
380
357
  </template>
@@ -406,6 +383,7 @@ onMounted(() => {
406
383
  background: var(--bgl-code-bg) !important;
407
384
  color: var(--bgl-light-text) !important;
408
385
  }
386
+
409
387
  .code textarea::placeholder {
410
388
  color: var(--bgl-light-text) !important;
411
389
  opacity: 0.3;
@@ -433,22 +411,25 @@ onMounted(() => {
433
411
  .textInputIconWrap .bgl_icon-font {
434
412
  color: var(--input-color);
435
413
  position: absolute;
436
- inset-inline-end:calc(var(--input-height) / 3 - 0.25rem);
414
+ inset-inline-end: calc(var(--input-height) / 3 - 0.25rem);
437
415
  margin-top: calc(var(--input-height) / 2 + 0.1rem);
438
416
  line-height: 0;
439
417
  }
440
- .textInputIconWrap input{
418
+
419
+ .textInputIconWrap input {
441
420
  padding-inline-end: calc(var(--input-height) / 3 + 1.5rem);
442
421
  }
443
422
 
444
423
  .txtInputIconStart .iconStart {
445
424
  color: var(--input-color);
446
425
  position: absolute;
447
- inset-inline-start:calc(var(--input-height) / 3 - 0.25rem);
448
- margin-top: calc(var(--input-height) / 2 );
426
+ inset-inline-start: calc(var(--input-height) / 3 - 0.25rem);
427
+ margin-top: calc(var(--input-height) / 2);
449
428
  line-height: 0;
450
429
  }
451
- .txtInputIconStart input, .txtInputIconStart textarea{
430
+
431
+ .txtInputIconStart input,
432
+ .txtInputIconStart textarea {
452
433
  padding-inline-start: calc(var(--input-height) / 3 + 1.5rem);
453
434
  }
454
435
 
@@ -24,7 +24,7 @@ export function useBglSchema<T = { [key: string]: unknown }>(
24
24
 
25
25
  if (_schema) {
26
26
  return (
27
- _columns && 0 < _columns.length
27
+ _columns && _columns.length > 0
28
28
  ? _schema.filter(f => _columns.includes(f.id as string))
29
29
  : _schema
30
30
  ) as BglFormSchemaT<T>
@@ -39,7 +39,7 @@ export function localRef<T>(
39
39
  IfAny<T, Ref<T, T>, T> :
40
40
  Ref<UnwrapRef<T>, T | UnwrapRef<T>> {
41
41
  const storedValue = localStorage.getItem(key)
42
- const initial = null !== storedValue ? JSON.parse(storedValue) : initialValue
42
+ const initial = storedValue !== null ? JSON.parse(storedValue) : initialValue
43
43
  const value = ref<T>(initial)
44
44
 
45
45
  watch(() => value.value, (val) => {
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export {
9
9
  injectI18nT,
10
10
  } from './plugins/bagel'
11
11
  export { ModalPlugin, useModal } from './plugins/useModal'
12
+ export { type BagelToastOptions, type ToastApi, ToastPlugin, useToast } from './plugins/useToast'
12
13
  export * from './types'
13
14
  export * from './utils'
14
15
  export * from './utils/allCountries'
@@ -1,4 +1,4 @@
1
- import type { InjectionKey, Plugin } from 'vue'
1
+ import type { App, InjectionKey, Plugin } from 'vue'
2
2
  import type { ComponentProps } from 'vue-component-type-helpers'
3
3
  import type {
4
4
  ModalComponentProps,
@@ -9,7 +9,7 @@ import type {
9
9
  ConfirmModalUserOptions,
10
10
  ModalConfirmOptions
11
11
  } from './modalTypes'
12
- import { defineComponent, h, inject, reactive } from 'vue'
12
+ import { createApp, defineComponent, h, inject, reactive } from 'vue'
13
13
  import { Modal, ModalConfirm, ModalForm } from '../components'
14
14
 
15
15
  export interface ModalApi {
@@ -23,29 +23,49 @@ export interface ModalApi {
23
23
  ) => ModalFormComponentProps<T> | undefined
24
24
  hideModal: (index?: number) => void
25
25
  confirmModal: (options: ConfirmModalUserOptions) => Promise<boolean>
26
+ hideAllModals: () => void
26
27
  }
27
28
 
28
29
  export const ModalSymbol: InjectionKey<ModalApi> = Symbol('modal')
29
30
 
31
+ // Global singleton instance as fallback (created on first plugin install)
32
+ let globalModalApi: ModalApi | null = null
33
+
30
34
  export function useModal(): ModalApi {
31
- const modalApi = inject(ModalSymbol)
32
- if (!modalApi) {throw new Error('Modal API not provided')}
35
+ const modalApi = inject(ModalSymbol, null)
36
+ if (!modalApi) {
37
+ // Fallback to global singleton if injection fails
38
+ if (globalModalApi) {
39
+ return globalModalApi
40
+ }
41
+ throw new Error('Modal API not provided. Make sure ModalPlugin is installed via app.use(ModalPlugin)')
42
+ }
33
43
  return modalApi
34
44
  }
35
45
 
36
46
  export const ModalPlugin: Plugin = {
37
- install: (app) => {
47
+ install: (app: App) => {
48
+ const BASE_Z_INDEX = 1000
38
49
  const modalStack = reactive<ModalComponentProps<object>[]>([])
39
50
 
40
- const hideModal = (index: number) => {
41
- modalStack.splice(index, 1)
51
+ const hideModal = (index?: number) => {
52
+ if (index === undefined || index >= modalStack.length - 1) {
53
+ // Close top modal
54
+ modalStack.splice(modalStack.length - 1, 1)
55
+ } else {
56
+ modalStack.splice(index, 1)
57
+ }
58
+ }
59
+
60
+ const hideAllModals = () => {
61
+ modalStack.splice(0, modalStack.length)
42
62
  }
43
63
 
44
64
  const confirmModal = (options: ConfirmModalUserOptions): Promise<boolean> => {
45
65
  return new Promise((resolve) => {
46
- const confirmOptions = 'string' === typeof options ? { title: '', message: options } : options
66
+ const confirmOptions = typeof options === 'string' ? { title: '', message: options } : options
47
67
  modalStack.push({
48
- modalOptions: { ...confirmOptions, resolve },
68
+ modalOptions: reactive({ ...confirmOptions, resolve }),
49
69
  modalType: 'confirmModal',
50
70
  componentSlots: {},
51
71
  })
@@ -57,20 +77,23 @@ export const ModalPlugin: Plugin = {
57
77
  options: ModalOptions | ModalFormOptions<T>,
58
78
  slots: { [key: string]: any } = {}
59
79
  ): ModalComponentProps<T> | ModalFormComponentProps<T> | undefined => {
80
+ // Make options reactive so updates propagate
81
+ const reactiveOptions = reactive(options) as ModalOptions | ModalFormOptions<T>
82
+
60
83
  const modalComponent = {
61
- modalOptions: options,
84
+ modalOptions: reactiveOptions,
62
85
  modalType,
63
86
  componentSlots: slots,
64
87
  }
65
88
  modalStack.push(modalComponent)
66
89
 
67
- if ('modalForm' === modalType) {
90
+ if (modalType === 'modalForm') {
68
91
  return modalComponent as ModalFormComponentProps<T>
69
92
  }
70
93
  return modalComponent
71
94
  }
72
95
 
73
- app.provide(ModalSymbol, {
96
+ const api: ModalApi = {
74
97
  showModal: (options: ModalOptions, slots?: { [key: string]: any }) => showModal('modal', options, slots),
75
98
 
76
99
  showModalForm: <T extends { [key: string]: any }>(
@@ -80,16 +103,54 @@ export const ModalPlugin: Plugin = {
80
103
 
81
104
  confirmModal: (options: ConfirmModalUserOptions) => confirmModal(options),
82
105
 
83
- hideModal: (index = modalStack.length - 1) => { hideModal(index) },
84
- })
106
+ hideModal,
107
+
108
+ hideAllModals,
109
+ }
110
+
111
+ // Set global singleton on first install
112
+ if (!globalModalApi) {
113
+ globalModalApi = api
114
+ }
115
+
116
+ app.provide(ModalSymbol, api)
117
+
118
+ // Modal container component
119
+ const ModalContainerComponent = defineComponent({
120
+ name: 'ModalContainer',
121
+ setup() {
122
+ // Handle ESC key - only close top modal
123
+ const handleEscape = (e: KeyboardEvent) => {
124
+ if (e.key === 'Escape' && modalStack.length > 0) {
125
+ const topModal = modalStack[modalStack.length - 1]
126
+ // Check if top modal is dismissable
127
+ const modalOptions = topModal.modalOptions as ModalOptions
128
+ if (modalOptions.dismissable !== false) {
129
+ hideModal()
130
+ }
131
+ }
132
+ }
85
133
 
86
- const ModalComponent = defineComponent({
87
- data: () => ({ modalStack }),
134
+ return {
135
+ modalStack,
136
+ handleEscape,
137
+ }
138
+ },
139
+ mounted() {
140
+ document.addEventListener('keydown', this.handleEscape)
141
+ },
142
+ unmounted() {
143
+ document.removeEventListener('keydown', this.handleEscape)
144
+ },
88
145
  render() {
89
146
  return this.modalStack.map((modal, index) => {
147
+ // Calculate z-index based on stack position
148
+ const zIndex = BASE_Z_INDEX + index * 10
149
+
90
150
  const props = {
91
151
  ...modal.modalOptions,
92
152
  'visible': true,
153
+ 'zIndex': zIndex,
93
154
  'onUpdate:visible': () => { hideModal(index) },
94
155
  }
95
156
 
@@ -104,6 +165,36 @@ export const ModalPlugin: Plugin = {
104
165
  })
105
166
  },
106
167
  })
107
- app.component('ModalContainer', ModalComponent)
168
+
169
+ // Register the component so users can manually add it if needed
170
+ app.component('ModalContainer', ModalContainerComponent)
171
+
172
+ // Auto-mount modal container to document.body
173
+ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
174
+ // Wait for app to be mounted before injecting
175
+ app.mixin({
176
+ mounted() {
177
+ // Only run once on the root component
178
+ if (this.$root === this) {
179
+ const existingContainer = document.getElementById('bagelink-modal-root')
180
+ if (!existingContainer) {
181
+ // Create mount point
182
+ const container = document.createElement('div')
183
+ container.id = 'bagelink-modal-root'
184
+ document.body.appendChild(container)
185
+
186
+ // Create a separate app instance for modals to avoid context issues
187
+ const modalApp = createApp(ModalContainerComponent)
188
+
189
+ // Share the same context/plugins
190
+ modalApp._context = app._context
191
+
192
+ // Mount it
193
+ modalApp.mount(container)
194
+ }
195
+ }
196
+ }
197
+ })
198
+ }
108
199
  },
109
200
  }
@@ -0,0 +1,92 @@
1
+ import type { App, InjectionKey, Plugin } from 'vue'
2
+ import { inject } from 'vue'
3
+ import {
4
+ type PluginOptions as ToastOptions,
5
+ type ToastInterface,
6
+ POSITION,
7
+ } from 'vue-toastification'
8
+
9
+ export interface ToastApi {
10
+ success: (message: string, options?: any) => void
11
+ error: (message: string, options?: any) => void
12
+ info: (message: string, options?: any) => void
13
+ warning: (message: string, options?: any) => void
14
+ show: (message: string, options?: any) => void
15
+ clear: () => void
16
+ }
17
+
18
+ export const ToastSymbol: InjectionKey<ToastApi> = Symbol('toast')
19
+
20
+ // Global singleton instance as fallback (created on first plugin install)
21
+ let globalToastApi: ToastApi | null = null
22
+
23
+ export function useToast(): ToastApi {
24
+ const toastApi = inject(ToastSymbol, null)
25
+ if (!toastApi) {
26
+ // Fallback to global singleton if injection fails
27
+ if (globalToastApi) {
28
+ return globalToastApi
29
+ }
30
+ throw new Error('Toast API not provided. Make sure ToastPlugin is installed via app.use(ToastPlugin)')
31
+ }
32
+ return toastApi
33
+ }
34
+
35
+ export type BagelToastOptions = Partial<ToastOptions>
36
+
37
+ export const ToastPlugin: Plugin<BagelToastOptions[]> = {
38
+ install: async (app: App, options: BagelToastOptions = {}) => {
39
+ try {
40
+ // Dynamically import vue-toastification to avoid bundling if not used
41
+ const Toast = await import('vue-toastification')
42
+ const ToastInterface = Toast.default || Toast
43
+
44
+ const defaultOptions: ToastOptions = {
45
+ position: POSITION.TOP_RIGHT,
46
+ timeout: 3000,
47
+ closeOnClick: true,
48
+ pauseOnFocusLoss: true,
49
+ pauseOnHover: true,
50
+ draggable: true,
51
+ draggablePercent: 0.6,
52
+ showCloseButtonOnHover: false,
53
+ hideProgressBar: false,
54
+ closeButton: 'button',
55
+ icon: true,
56
+ rtl: false,
57
+ transition: 'Vue-Toastification__fade',
58
+ maxToasts: 5,
59
+ newestOnTop: true,
60
+ ...options,
61
+ }
62
+
63
+ // Install vue-toastification
64
+ app.use(ToastInterface as Plugin, defaultOptions)
65
+
66
+ // Get the toast instance
67
+ const toast: ToastInterface = app.config.globalProperties.$toast
68
+
69
+ // Create the API wrapper
70
+ const api: ToastApi = {
71
+ success: (message: string, options?: any) => toast.success(message, options),
72
+ error: (message: string, options?: any) => toast.error(message, options),
73
+ info: (message: string, options?: any) => toast.info(message, options),
74
+ warning: (message: string, options?: any) => toast.warning(message, options),
75
+ show: (message: string, options?: any) => toast(message, options),
76
+ clear: () => { toast.clear() },
77
+ }
78
+
79
+ // Set global singleton on first install
80
+ if (!globalToastApi) {
81
+ globalToastApi = api
82
+ }
83
+
84
+ app.provide(ToastSymbol, api)
85
+ } catch (error) {
86
+ console.error('Failed to load vue-toastification. Make sure it is installed:', error)
87
+ throw new Error(
88
+ 'vue-toastification is required for ToastPlugin. Install it with: pnpm add vue-toastification'
89
+ )
90
+ }
91
+ },
92
+ }