mgv-backoffice 1.18.0 → 1.20.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 (80) hide show
  1. package/README.md +152 -9
  2. package/dist/{src/components → components}/BaseActionButton.vue.d.ts +9 -13
  3. package/dist/{src/components → components}/BaseAlert.vue.d.ts +3 -2
  4. package/dist/components/BaseAppLayout.vue.d.ts +28 -0
  5. package/dist/{src/components → components}/BaseBadge.vue.d.ts +7 -11
  6. package/dist/{src/components → components}/BaseBreadcrumb.vue.d.ts +3 -2
  7. package/dist/{src/components → components}/BaseButton.vue.d.ts +9 -12
  8. package/dist/{src/components → components}/BaseCollapsibleSection.vue.d.ts +7 -11
  9. package/dist/{src/components → components}/BaseConfirmModal.vue.d.ts +9 -12
  10. package/dist/{src/components → components}/BaseCopyButton.vue.d.ts +2 -1
  11. package/dist/{src/components → components}/BaseEntityPickerModal.vue.d.ts +8 -16
  12. package/dist/components/BaseGoogleSignInButton.vue.d.ts +22 -0
  13. package/dist/{src/components → components}/BaseLine.vue.d.ts +2 -1
  14. package/dist/components/BaseLoginForm.vue.d.ts +51 -0
  15. package/dist/{src/components → components}/BaseLogo.vue.d.ts +2 -1
  16. package/dist/{src/components → components}/BaseModal.vue.d.ts +7 -11
  17. package/dist/{src/components → components}/BaseModalShell.vue.d.ts +19 -17
  18. package/dist/{src/components → components}/BaseNotFoundPage.vue.d.ts +3 -2
  19. package/dist/components/BaseNotificationPanel.vue.d.ts +19 -0
  20. package/dist/components/BasePageHeader.vue.d.ts +28 -0
  21. package/dist/{src/components → components}/BaseRow.vue.d.ts +8 -12
  22. package/dist/components/BaseSidebar.vue.d.ts +47 -0
  23. package/dist/{src/components → components}/BaseSpinner.vue.d.ts +2 -1
  24. package/dist/{src/components → components}/BaseTextInputModal.vue.d.ts +8 -16
  25. package/dist/{src/components → components}/BaseToast.vue.d.ts +2 -1
  26. package/dist/{src/components → components}/BaseToolbarButton.vue.d.ts +9 -13
  27. package/dist/components/ColoredSquares.vue.d.ts +23 -0
  28. package/dist/{src/components → components}/EarningsCard.vue.d.ts +3 -2
  29. package/dist/{src/components → components}/EuroAmount.vue.d.ts +2 -1
  30. package/dist/{src/components → components}/Pagination.vue.d.ts +3 -2
  31. package/dist/{src/components → components}/TrendArrow.vue.d.ts +2 -1
  32. package/dist/composables/useNotifications.d.ts +29 -0
  33. package/dist/composables/useSidebarCollapse.d.ts +22 -0
  34. package/dist/index.d.ts +62 -1
  35. package/dist/types/auth.d.ts +11 -0
  36. package/dist/types/notification.d.ts +20 -0
  37. package/dist/ui-lib.css +2 -2
  38. package/dist/ui-lib.js +1088 -671
  39. package/dist/ui-lib.umd.cjs +1 -1
  40. package/package.json +14 -13
  41. package/src/components/BaseAppLayout.vue +4 -1
  42. package/src/components/BaseGoogleSignInButton.vue +83 -0
  43. package/src/components/BaseLoginForm.test.ts +52 -0
  44. package/src/components/BaseLoginForm.vue +204 -0
  45. package/src/components/BaseNotificationPanel.vue +183 -0
  46. package/src/components/BaseSidebar.vue +140 -31
  47. package/src/composables/useNotifications.test.ts +64 -0
  48. package/src/composables/useNotifications.ts +81 -0
  49. package/src/composables/useSidebarCollapse.ts +64 -0
  50. package/src/index.ts +8 -0
  51. package/src/types/auth.ts +11 -0
  52. package/src/types/notification.ts +20 -0
  53. package/dist/src/components/BaseAppLayout.vue.d.ts +0 -31
  54. package/dist/src/components/BasePageHeader.vue.d.ts +0 -31
  55. package/dist/src/components/BaseSidebar.vue.d.ts +0 -46
  56. package/dist/src/components/ColoredSquares.vue.d.ts +0 -27
  57. package/dist/src/index.d.ts +0 -54
  58. /package/dist/{src/composables → composables}/useDebounce.d.ts +0 -0
  59. /package/dist/{src/composables → composables}/useEscapeKey.d.ts +0 -0
  60. /package/dist/{src/composables → composables}/useMobileSidebar.d.ts +0 -0
  61. /package/dist/{src/composables → composables}/useTheme.d.ts +0 -0
  62. /package/dist/{src/composables → composables}/useThemeClasses.d.ts +0 -0
  63. /package/dist/{src/composables → composables}/useToast.d.ts +0 -0
  64. /package/dist/{src/enums → enums}/AlertEnum.d.ts +0 -0
  65. /package/dist/{src/enums → enums}/BaseBadgeEnum.d.ts +0 -0
  66. /package/dist/{src/enums → enums}/BaseButtonEnum.d.ts +0 -0
  67. /package/dist/{src/enums → enums}/BaseButtonSizeEnum.d.ts +0 -0
  68. /package/dist/{src/enums → enums}/BaseLogoEnum.d.ts +0 -0
  69. /package/dist/{src/enums → enums}/BaseModalEnum.d.ts +0 -0
  70. /package/dist/{src/enums → enums}/BaseToastEnum.d.ts +0 -0
  71. /package/dist/{src/enums → enums}/ColorsEnums.d.ts +0 -0
  72. /package/dist/{src/enums → enums}/LineEnum.d.ts +0 -0
  73. /package/dist/{src/enums → enums}/PositioningEnum.d.ts +0 -0
  74. /package/dist/{src/types → types}/entityPicker.d.ts +0 -0
  75. /package/dist/{src/types → types}/sidebar.d.ts +0 -0
  76. /package/dist/{src/utils → utils}/format.d.ts +0 -0
  77. /package/dist/{src/utils → utils}/httpColors.d.ts +0 -0
  78. /package/dist/{src/utils → utils}/pnl.d.ts +0 -0
  79. /package/dist/{src/utils → utils}/sanitizeHtml.d.ts +0 -0
  80. /package/dist/{src/utils → utils}/util.d.ts +0 -0
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import BaseLoginForm from './BaseLoginForm.vue'
4
+
5
+ describe('BaseLoginForm', () => {
6
+ it('emits submit with the entered credentials', async () => {
7
+ const wrapper = mount(BaseLoginForm)
8
+ await wrapper.find('input[type="email"]').setValue('user@example.com')
9
+ await wrapper.find('input[name="password"]').setValue('hunter2')
10
+ await wrapper.find('form').trigger('submit.prevent')
11
+
12
+ expect(wrapper.emitted('submit')).toEqual([
13
+ [{ email: 'user@example.com', password: 'hunter2' }],
14
+ ])
15
+ })
16
+
17
+ it('includes remember when the checkbox is enabled', async () => {
18
+ const wrapper = mount(BaseLoginForm, { props: { showRemember: true } })
19
+ await wrapper.find('input[type="email"]').setValue('a@b.com')
20
+ await wrapper.find('input[name="password"]').setValue('pw')
21
+ await wrapper.find('input[type="checkbox"]').setValue(true)
22
+ await wrapper.find('form').trigger('submit.prevent')
23
+
24
+ expect(wrapper.emitted('submit')![0]).toEqual([
25
+ { email: 'a@b.com', password: 'pw', remember: true },
26
+ ])
27
+ })
28
+
29
+ it('emits google-sign-in when the Google button is clicked', async () => {
30
+ const wrapper = mount(BaseLoginForm)
31
+ // The Google button renders a <button> with the brand label.
32
+ const googleBtn = wrapper
33
+ .findAll('button')
34
+ .find((b) => b.text().includes('Sign in with Google'))
35
+ expect(googleBtn, 'expected a Google sign-in button').toBeTruthy()
36
+ await googleBtn!.trigger('click')
37
+ expect(wrapper.emitted('google-sign-in')).toHaveLength(1)
38
+ })
39
+
40
+ it('does not submit while loading', async () => {
41
+ const wrapper = mount(BaseLoginForm, { props: { loading: true } })
42
+ await wrapper.find('form').trigger('submit.prevent')
43
+ expect(wrapper.emitted('submit')).toBeUndefined()
44
+ })
45
+
46
+ it('renders the error banner', () => {
47
+ const wrapper = mount(BaseLoginForm, { props: { error: 'Invalid credentials' } })
48
+ const alert = wrapper.find('[role="alert"]')
49
+ expect(alert.exists()).toBe(true)
50
+ expect(alert.text()).toContain('Invalid credentials')
51
+ })
52
+ })
@@ -0,0 +1,204 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+ import { useTheme } from '../composables/useTheme'
4
+ import { useThemeClasses } from '../composables/useThemeClasses'
5
+ import BaseGoogleSignInButton from './BaseGoogleSignInButton.vue'
6
+ import type { LoginCredentials } from '../types/auth'
7
+
8
+ interface Props {
9
+ /** Card heading. Defaults to "Sign in". */
10
+ title?: string
11
+ /** Optional muted line under the heading. */
12
+ subtitle?: string
13
+ /** Submit button label. Defaults to "Sign in". */
14
+ submitLabel?: string
15
+ /** Disables the form and shows a spinner on the submit button. */
16
+ loading?: boolean
17
+ /** Disables the form and shows a spinner on the Google button. */
18
+ googleLoading?: boolean
19
+ /** Error message rendered above the form (e.g. "Invalid credentials"). */
20
+ error?: string
21
+ /** Render the "Sign in with Google" button + divider. Defaults to true. */
22
+ showGoogle?: boolean
23
+ /** Render the "Remember me" checkbox. Defaults to false. */
24
+ showRemember?: boolean
25
+ }
26
+
27
+ const props = withDefaults(defineProps<Props>(), {
28
+ title: 'Sign in',
29
+ subtitle: '',
30
+ submitLabel: 'Sign in',
31
+ loading: false,
32
+ googleLoading: false,
33
+ error: '',
34
+ showGoogle: true,
35
+ showRemember: false,
36
+ })
37
+
38
+ const emit = defineEmits<{
39
+ /** Email/password submit. */
40
+ submit: [credentials: LoginCredentials]
41
+ /** User chose "Sign in with Google". */
42
+ 'google-sign-in': []
43
+ }>()
44
+
45
+ const { isDark } = useTheme()
46
+ const t = useThemeClasses()
47
+
48
+ const email = ref('')
49
+ const password = ref('')
50
+ const remember = ref(false)
51
+ const showPassword = ref(false)
52
+
53
+ const busy = computed(() => props.loading || props.googleLoading)
54
+
55
+ function onSubmit() {
56
+ if (busy.value) return
57
+ emit('submit', {
58
+ email: email.value,
59
+ password: password.value,
60
+ ...(props.showRemember ? { remember: remember.value } : {}),
61
+ })
62
+ }
63
+ </script>
64
+
65
+ <template>
66
+ <!--
67
+ Presentational sign-in card. Owns its own input state and emits
68
+ `submit` (with credentials) / `google-sign-in`. It runs no auth
69
+ itself — the consuming app handles the actual request and feeds back
70
+ `loading` / `error`.
71
+ -->
72
+ <div
73
+ class="w-full max-w-sm mx-auto rounded-xl border shadow-sm p-6 sm:p-8"
74
+ :class="t.card"
75
+ >
76
+ <!-- Brand / logo slot above the heading. -->
77
+ <div v-if="$slots.logo" class="flex justify-center mb-6">
78
+ <slot name="logo" />
79
+ </div>
80
+
81
+ <div class="mb-6 text-center">
82
+ <h1 class="text-xl font-semibold" :class="t.primaryText">{{ title }}</h1>
83
+ <p v-if="subtitle" class="mt-1 text-sm" :class="t.mutedText">{{ subtitle }}</p>
84
+ </div>
85
+
86
+ <!-- Error banner -->
87
+ <div
88
+ v-if="error"
89
+ role="alert"
90
+ class="mb-4 rounded-lg border px-3 py-2 text-sm"
91
+ :class="isDark
92
+ ? 'bg-red-500/10 border-red-500/30 text-red-300'
93
+ : 'bg-red-50 border-red-200 text-red-700'"
94
+ >
95
+ {{ error }}
96
+ </div>
97
+
98
+ <!-- Google sign-in + divider -->
99
+ <template v-if="showGoogle">
100
+ <BaseGoogleSignInButton
101
+ :loading="googleLoading"
102
+ :disabled="loading"
103
+ @click="emit('google-sign-in')"
104
+ />
105
+ <div class="flex items-center gap-3 my-5">
106
+ <span class="h-px flex-1" :class="isDark ? 'bg-gray-700' : 'bg-gray-200'" />
107
+ <span class="text-xs uppercase tracking-wider" :class="t.dimTextAlt">or</span>
108
+ <span class="h-px flex-1" :class="isDark ? 'bg-gray-700' : 'bg-gray-200'" />
109
+ </div>
110
+ </template>
111
+
112
+ <form class="space-y-4" @submit.prevent="onSubmit">
113
+ <div>
114
+ <label for="login-email" class="block mb-1.5 text-sm font-medium" :class="t.label">
115
+ Email
116
+ </label>
117
+ <input
118
+ id="login-email"
119
+ v-model="email"
120
+ type="email"
121
+ name="email"
122
+ autocomplete="email"
123
+ required
124
+ :disabled="busy"
125
+ placeholder="you@example.com"
126
+ class="w-full h-11 px-3 rounded-lg border text-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 disabled:opacity-60"
127
+ :class="t.inputWithPlaceholder"
128
+ />
129
+ </div>
130
+
131
+ <div>
132
+ <div class="flex items-center justify-between mb-1.5">
133
+ <label for="login-password" class="block text-sm font-medium" :class="t.label">
134
+ Password
135
+ </label>
136
+ <!-- `forgot` slot: drop a "Forgot password?" link here. -->
137
+ <slot name="forgot" />
138
+ </div>
139
+ <div class="relative">
140
+ <input
141
+ id="login-password"
142
+ v-model="password"
143
+ :type="showPassword ? 'text' : 'password'"
144
+ name="password"
145
+ autocomplete="current-password"
146
+ required
147
+ :disabled="busy"
148
+ placeholder="••••••••"
149
+ class="w-full h-11 pl-3 pr-16 rounded-lg border text-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 disabled:opacity-60"
150
+ :class="t.inputWithPlaceholder"
151
+ />
152
+ <button
153
+ type="button"
154
+ @click="showPassword = !showPassword"
155
+ :aria-label="showPassword ? 'Hide password' : 'Show password'"
156
+ :aria-pressed="showPassword"
157
+ class="absolute inset-y-0 right-0 px-3 text-xs font-medium cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 rounded-r-lg"
158
+ :class="t.mutedText"
159
+ >
160
+ {{ showPassword ? 'Hide' : 'Show' }}
161
+ </button>
162
+ </div>
163
+ </div>
164
+
165
+ <label
166
+ v-if="showRemember"
167
+ class="flex items-center gap-2 text-sm cursor-pointer select-none"
168
+ :class="t.bodyText"
169
+ >
170
+ <input
171
+ v-model="remember"
172
+ type="checkbox"
173
+ :disabled="busy"
174
+ class="w-4 h-4 rounded border-gray-300 text-emerald-600 focus:ring-emerald-500"
175
+ />
176
+ Remember me
177
+ </label>
178
+
179
+ <button
180
+ type="submit"
181
+ :disabled="busy"
182
+ :aria-busy="loading"
183
+ class="inline-flex items-center justify-center gap-2 w-full h-11 px-4 rounded-lg text-sm font-semibold text-white bg-emerald-600 hover:bg-emerald-700 transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 disabled:opacity-60 disabled:cursor-not-allowed"
184
+ >
185
+ <svg
186
+ v-if="loading"
187
+ class="w-4 h-4 animate-spin"
188
+ viewBox="0 0 24 24"
189
+ fill="none"
190
+ aria-hidden="true"
191
+ >
192
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
193
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8V0C5.4 0 0 5.4 0 12h4z" />
194
+ </svg>
195
+ {{ submitLabel }}
196
+ </button>
197
+ </form>
198
+
199
+ <!-- `footer` slot: e.g. "Don't have an account? Sign up". -->
200
+ <div v-if="$slots.footer" class="mt-6 text-center text-sm" :class="t.mutedText">
201
+ <slot name="footer" />
202
+ </div>
203
+ </div>
204
+ </template>
@@ -0,0 +1,183 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { XMarkIcon, BellIcon } from '@heroicons/vue/24/outline'
4
+ import { useTheme } from '../composables/useTheme'
5
+ import { useThemeClasses } from '../composables/useThemeClasses'
6
+ import { useEscapeKey } from '../composables/useEscapeKey'
7
+ import { useNotifications } from '../composables/useNotifications'
8
+
9
+ interface Props {
10
+ /** Panel heading. Defaults to "Notifications". */
11
+ title?: string
12
+ /** Shown when there are no notifications. */
13
+ emptyText?: string
14
+ /** Render the "Mark all read" action when there are unread items. */
15
+ showMarkAllRead?: boolean
16
+ }
17
+
18
+ const props = withDefaults(defineProps<Props>(), {
19
+ title: 'Notifications',
20
+ emptyText: 'You have no notifications.',
21
+ showMarkAllRead: true,
22
+ })
23
+
24
+ const emit = defineEmits<{
25
+ /** A notification row was clicked (e.g. to navigate to its target). */
26
+ select: [id: import('../types/notification').NotificationItem['id']]
27
+ }>()
28
+
29
+ const { isDark } = useTheme()
30
+ const t = useThemeClasses()
31
+ const {
32
+ notifications,
33
+ unreadCount,
34
+ open,
35
+ closePanel,
36
+ markAllRead,
37
+ markRead,
38
+ } = useNotifications()
39
+
40
+ const dotClass: Record<string, string> = {
41
+ info: 'bg-sky-500',
42
+ success: 'bg-emerald-500',
43
+ warning: 'bg-amber-500',
44
+ error: 'bg-red-500',
45
+ }
46
+
47
+ const total = computed(() => notifications.value.length)
48
+
49
+ function onSelect(id: import('../types/notification').NotificationItem['id']) {
50
+ markRead(id)
51
+ emit('select', id)
52
+ }
53
+
54
+ useEscapeKey(() => {
55
+ if (open.value) closePanel()
56
+ })
57
+ </script>
58
+
59
+ <template>
60
+ <!--
61
+ Left-anchored notification drawer. Open/close state and the list live
62
+ in `useNotifications()`, so the sidebar bell and this panel stay in
63
+ sync. Teleported to <body> with a backdrop; slides in from the left.
64
+ -->
65
+ <Teleport to="body">
66
+ <!-- Backdrop -->
67
+ <Transition
68
+ enter-active-class="transition-opacity duration-200"
69
+ enter-from-class="opacity-0"
70
+ leave-active-class="transition-opacity duration-200"
71
+ leave-to-class="opacity-0"
72
+ >
73
+ <div
74
+ v-if="open"
75
+ class="fixed inset-0 z-[60] bg-black/50"
76
+ aria-hidden="true"
77
+ @click="closePanel"
78
+ />
79
+ </Transition>
80
+
81
+ <!-- Drawer -->
82
+ <Transition
83
+ enter-active-class="transition-transform duration-300 ease-out"
84
+ enter-from-class="-translate-x-full"
85
+ leave-active-class="transition-transform duration-300 ease-in"
86
+ leave-to-class="-translate-x-full"
87
+ >
88
+ <aside
89
+ v-if="open"
90
+ role="dialog"
91
+ aria-modal="true"
92
+ :aria-label="title"
93
+ class="fixed top-0 left-0 z-[70] h-screen w-full max-w-sm flex flex-col border-r shadow-xl"
94
+ :class="isDark ? 'bg-gray-900 border-gray-800' : 'bg-white border-gray-200'"
95
+ >
96
+ <!-- Header -->
97
+ <div
98
+ class="flex items-center justify-between gap-3 px-5 py-4 border-b shrink-0"
99
+ :class="t.border"
100
+ >
101
+ <h2 class="text-lg font-semibold flex items-center gap-2" :class="t.primaryText">
102
+ {{ title }}
103
+ <span class="text-sm font-normal" :class="t.dimTextAlt">— {{ total }}</span>
104
+ </h2>
105
+ <button
106
+ type="button"
107
+ @click="closePanel"
108
+ aria-label="Close notifications"
109
+ class="p-1.5 rounded-lg cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
110
+ :class="isDark ? 'text-gray-400 hover:bg-gray-800' : 'text-gray-500 hover:bg-gray-100'"
111
+ >
112
+ <XMarkIcon class="w-5 h-5" aria-hidden="true" />
113
+ </button>
114
+ </div>
115
+
116
+ <!-- Mark all read -->
117
+ <div
118
+ v-if="showMarkAllRead && unreadCount > 0"
119
+ class="px-5 py-2 border-b shrink-0 flex justify-end"
120
+ :class="t.border"
121
+ >
122
+ <button
123
+ type="button"
124
+ @click="markAllRead"
125
+ class="text-xs font-medium cursor-pointer transition-colors focus:outline-none focus-visible:underline"
126
+ :class="t.emeraldText"
127
+ >
128
+ Mark all as read
129
+ </button>
130
+ </div>
131
+
132
+ <!-- Body -->
133
+ <div class="flex-1 overflow-y-auto">
134
+ <!-- Empty state -->
135
+ <div
136
+ v-if="total === 0"
137
+ class="flex flex-col items-center justify-center text-center px-6 py-16 gap-3"
138
+ >
139
+ <BellIcon class="w-10 h-10" :class="t.illustration" aria-hidden="true" />
140
+ <p class="text-sm" :class="t.mutedText">{{ emptyText }}</p>
141
+ </div>
142
+
143
+ <!-- List -->
144
+ <ul v-else class="divide-y" :class="isDark ? 'divide-gray-800' : 'divide-gray-100'">
145
+ <li v-for="n in notifications" :key="n.id">
146
+ <button
147
+ type="button"
148
+ @click="onSelect(n.id)"
149
+ class="w-full text-left px-5 py-4 flex gap-3 transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-emerald-500"
150
+ :class="[
151
+ !n.read
152
+ ? (isDark ? 'bg-emerald-500/5' : 'bg-emerald-50/60')
153
+ : '',
154
+ isDark ? 'hover:bg-gray-800' : 'hover:bg-gray-50',
155
+ ]"
156
+ >
157
+ <span
158
+ class="mt-1.5 w-2 h-2 rounded-full shrink-0"
159
+ :class="n.read ? (isDark ? 'bg-gray-700' : 'bg-gray-300') : (dotClass[n.type ?? 'info'])"
160
+ aria-hidden="true"
161
+ />
162
+ <span class="min-w-0 flex-1">
163
+ <span class="flex items-baseline justify-between gap-2">
164
+ <span
165
+ class="text-sm truncate"
166
+ :class="[n.read ? 'font-medium' : 'font-semibold', t.primaryTextSoft]"
167
+ >
168
+ {{ n.title }}
169
+ </span>
170
+ <span v-if="n.time" class="text-xs shrink-0" :class="t.dimText">{{ n.time }}</span>
171
+ </span>
172
+ <span v-if="n.message" class="mt-0.5 block text-sm" :class="t.mutedText">
173
+ {{ n.message }}
174
+ </span>
175
+ </span>
176
+ </button>
177
+ </li>
178
+ </ul>
179
+ </div>
180
+ </aside>
181
+ </Transition>
182
+ </Teleport>
183
+ </template>