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
@@ -1,9 +1,19 @@
1
1
  <script setup lang="ts">
2
- import { ref, nextTick, watch } from 'vue'
2
+ import { ref, computed, nextTick, watch } from 'vue'
3
3
  import { useRouter, useRoute } from 'vue-router'
4
- import { Bars3Icon, XMarkIcon } from '@heroicons/vue/24/outline'
4
+ import {
5
+ Bars3Icon,
6
+ XMarkIcon,
7
+ ChevronDoubleLeftIcon,
8
+ ChevronDoubleRightIcon,
9
+ MoonIcon,
10
+ SunIcon,
11
+ BellIcon,
12
+ } from '@heroicons/vue/24/outline'
5
13
  import { useTheme } from '../composables/useTheme'
6
14
  import { useMobileSidebar } from '../composables/useMobileSidebar'
15
+ import { useSidebarCollapse } from '../composables/useSidebarCollapse'
16
+ import { useNotifications } from '../composables/useNotifications'
7
17
  import type { NavSection } from '../types/sidebar'
8
18
 
9
19
  interface Props {
@@ -17,6 +27,10 @@ interface Props {
17
27
  version?: string
18
28
  /** Whether to render the dark/light toggle in the footer. */
19
29
  showThemeToggle?: boolean
30
+ /** Whether to render the desktop collapse toggle (icon-only rail). */
31
+ collapsible?: boolean
32
+ /** Whether to render the notifications bell button. */
33
+ showNotifications?: boolean
20
34
  }
21
35
 
22
36
  const props = withDefaults(defineProps<Props>(), {
@@ -24,12 +38,20 @@ const props = withDefaults(defineProps<Props>(), {
24
38
  appName: '',
25
39
  version: '',
26
40
  showThemeToggle: true,
41
+ collapsible: true,
42
+ showNotifications: false,
27
43
  })
28
44
 
29
45
  const router = useRouter()
30
46
  const route = useRoute()
31
47
  const { isDark, toggleTheme } = useTheme()
32
48
  const { mobileOpen, close: closeMobile, toggle: toggleMobile } = useMobileSidebar()
49
+ const { collapsed, toggle: toggleCollapsed } = useSidebarCollapse()
50
+ const { unreadCount, togglePanel: toggleNotifications } = useNotifications()
51
+
52
+ // Whether the icon-only rail styling applies. Only relevant on desktop
53
+ // (`lg:`) — on mobile the sidebar is always the full off-canvas panel.
54
+ const isRail = computed(() => props.collapsible && collapsed.value)
33
55
 
34
56
  // Refs needed for the mobile focus management.
35
57
  const closeMobileButton = ref<HTMLButtonElement | null>(null)
@@ -103,17 +125,21 @@ function navigate(name: string) {
103
125
  <aside
104
126
  role="navigation"
105
127
  aria-label="Main navigation"
106
- class="fixed top-0 left-0 h-screen w-60 flex flex-col border-r transition-transform duration-300 lg:pointer-events-auto"
128
+ class="fixed top-0 left-0 h-screen w-60 flex flex-col border-r transition-all duration-300 lg:pointer-events-auto"
107
129
  :class="[
108
130
  isDark ? 'bg-gray-900 border-gray-800' : 'bg-white border-gray-200',
109
131
  mobileOpen ? 'translate-x-0 z-50' : '-translate-x-full max-lg:pointer-events-none lg:translate-x-0 z-50',
132
+ collapsible && collapsed ? 'lg:w-16' : 'lg:w-60',
110
133
  ]"
111
134
  >
112
135
  <div class="flex flex-col h-full">
113
136
  <!-- Logo -->
114
- <div class="px-5 pt-5 pb-4 flex items-center justify-center relative">
137
+ <div
138
+ class="px-5 pt-5 pb-4 flex items-center justify-center relative"
139
+ :class="{ 'lg:px-2': isRail }"
140
+ >
115
141
  <button type="button" @click="goHome" class="cursor-pointer" aria-label="Go to home">
116
- <slot name="logo" :size="52" />
142
+ <slot name="logo" :size="isRail ? 36 : 52" />
117
143
  </button>
118
144
  <button
119
145
  ref="closeMobileButton"
@@ -130,7 +156,10 @@ function navigate(name: string) {
130
156
  <!-- Nav -->
131
157
  <nav class="flex-1 overflow-y-auto px-3 space-y-6">
132
158
  <div v-for="section in sections" :key="section.title">
133
- <p class="px-2 mb-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
159
+ <p
160
+ class="px-2 mb-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500"
161
+ :class="{ 'lg:hidden': isRail }"
162
+ >
134
163
  {{ section.title }}
135
164
  </p>
136
165
  <ul class="space-y-0.5">
@@ -138,27 +167,64 @@ function navigate(name: string) {
138
167
  <button
139
168
  type="button"
140
169
  :aria-current="isActive(item.name) ? 'page' : undefined"
170
+ :title="isRail ? item.label : undefined"
141
171
  @click="navigate(item.name)"
142
172
  class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors w-full text-left cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
143
- :class="isActive(item.name)
144
- ? (isDark
145
- ? 'bg-emerald-500/15 text-emerald-300'
146
- : 'bg-emerald-50 text-emerald-700')
147
- : (isDark
148
- ? 'text-gray-400 hover:text-gray-100 hover:bg-gray-800'
149
- : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100')"
173
+ :class="[
174
+ isActive(item.name)
175
+ ? (isDark
176
+ ? 'bg-emerald-500/15 text-emerald-300'
177
+ : 'bg-emerald-50 text-emerald-700')
178
+ : (isDark
179
+ ? 'text-gray-400 hover:text-gray-100 hover:bg-gray-800'
180
+ : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'),
181
+ { 'lg:justify-center': isRail },
182
+ ]"
150
183
  >
151
184
  <component :is="item.icon" class="w-[18px] h-[18px] shrink-0" />
152
- {{ item.label }}
185
+ <span :class="{ 'lg:hidden': isRail }">{{ item.label }}</span>
153
186
  </button>
154
187
  </li>
155
188
  </ul>
156
189
  </div>
157
190
  </nav>
158
191
 
159
- <!-- Bottom: Theme toggle, status slot, footer slot -->
192
+ <!-- Bottom: Notifications, theme toggle, status slot, footer slot -->
160
193
  <div class="px-4 py-4 border-t transition-colors space-y-3"
161
194
  :class="isDark ? 'border-gray-800' : 'border-gray-200'">
195
+ <!--
196
+ Notifications bell: toggles the shared `BaseNotificationPanel`
197
+ (via useNotifications). The unread badge reads the same shared
198
+ count. Drop a `<BaseNotificationPanel />` in your layout to
199
+ render the panel itself.
200
+ -->
201
+ <button
202
+ v-if="showNotifications"
203
+ type="button"
204
+ @click="toggleNotifications"
205
+ title="Notifications"
206
+ :aria-label="unreadCount > 0 ? `Notifications, ${unreadCount} unread` : 'Notifications'"
207
+ class="relative flex items-center gap-3 w-full px-3 py-2.5 rounded-lg text-sm font-medium transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
208
+ :class="[
209
+ isDark
210
+ ? 'text-gray-400 hover:text-gray-100 hover:bg-gray-800'
211
+ : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100',
212
+ { 'lg:justify-center': isRail },
213
+ ]"
214
+ >
215
+ <span class="relative shrink-0">
216
+ <BellIcon class="w-[18px] h-[18px]" aria-hidden="true" />
217
+ <!-- Unread badge -->
218
+ <span
219
+ v-if="unreadCount > 0"
220
+ class="absolute -top-1.5 -right-1.5 min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-semibold leading-4 text-center"
221
+ >
222
+ {{ unreadCount > 99 ? '99+' : unreadCount }}
223
+ </span>
224
+ </span>
225
+ <span :class="{ 'lg:hidden': isRail }">Notifications</span>
226
+ </button>
227
+
162
228
  <button
163
229
  v-if="showThemeToggle"
164
230
  type="button"
@@ -167,38 +233,81 @@ function navigate(name: string) {
167
233
  :aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
168
234
  :aria-pressed="isDark"
169
235
  class="flex items-center gap-3 w-full px-3 py-2.5 rounded-lg text-sm font-medium transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
170
- :class="isDark
171
- ? 'text-gray-400 hover:text-gray-100 hover:bg-gray-800'
172
- : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'"
236
+ :class="[
237
+ isDark
238
+ ? 'text-gray-400 hover:text-gray-100 hover:bg-gray-800'
239
+ : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100',
240
+ { 'lg:justify-center': isRail },
241
+ ]"
173
242
  >
174
- {{ isDark ? 'Switch to light' : 'Switch to dark' }}
243
+ <!-- Icon stand-in shown only on the collapsed desktop rail. -->
244
+ <component
245
+ :is="isDark ? SunIcon : MoonIcon"
246
+ class="hidden w-[18px] h-[18px] shrink-0"
247
+ :class="{ 'lg:block': isRail }"
248
+ aria-hidden="true"
249
+ />
250
+ <span :class="{ 'lg:hidden': isRail }">{{ isDark ? 'Switch to light' : 'Switch to dark' }}</span>
175
251
  <span class="ml-auto relative w-9 h-5 rounded-full transition-colors"
176
- :class="isDark ? 'bg-emerald-600' : 'bg-gray-300'">
252
+ :class="[isDark ? 'bg-emerald-600' : 'bg-gray-300', { 'lg:hidden': isRail }]">
177
253
  <span class="absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow-sm transition-transform duration-300"
178
254
  :class="isDark ? 'translate-x-4' : 'translate-x-0'" />
179
255
  </span>
180
256
  </button>
181
257
 
258
+ <!--
259
+ Desktop-only collapse toggle: shrinks the sidebar to an
260
+ icon-only rail. Hidden on mobile, where the off-canvas menu is
261
+ controlled by the open/close buttons instead.
262
+ -->
263
+ <button
264
+ v-if="collapsible"
265
+ type="button"
266
+ @click="toggleCollapsed"
267
+ :title="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
268
+ :aria-label="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
269
+ :aria-pressed="collapsed"
270
+ class="hidden lg:flex items-center gap-3 w-full px-3 py-2.5 rounded-lg text-sm font-medium transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
271
+ :class="[
272
+ isDark
273
+ ? 'text-gray-400 hover:text-gray-100 hover:bg-gray-800'
274
+ : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100',
275
+ { 'lg:justify-center': isRail },
276
+ ]"
277
+ >
278
+ <component
279
+ :is="collapsed ? ChevronDoubleRightIcon : ChevronDoubleLeftIcon"
280
+ class="w-[18px] h-[18px] shrink-0"
281
+ aria-hidden="true"
282
+ />
283
+ <span :class="{ 'lg:hidden': isRail }">Collapse</span>
284
+ </button>
285
+
182
286
  <!--
183
287
  `status` slot: consumers drop in their own indicator (e.g.
184
288
  health check, sync status). The wrapper handles spacing only.
289
+ Hidden on the collapsed desktop rail where there's no room.
185
290
  -->
186
- <slot name="status" />
291
+ <div :class="{ 'lg:hidden': isRail }">
292
+ <slot name="status" />
293
+ </div>
187
294
 
188
295
  <!--
189
296
  `footer` slot: links / version / etc. If not provided and
190
297
  either `appName` or `version` is set, we render a default
191
- "appName vVersion" line.
298
+ "appName vVersion" line. Hidden on the collapsed desktop rail.
192
299
  -->
193
- <slot name="footer">
194
- <div
195
- v-if="appName || version"
196
- class="flex items-center justify-center gap-2 text-xs"
197
- :class="isDark ? 'text-gray-500' : 'text-gray-500'"
198
- >
199
- <span>{{ appName }}<template v-if="appName && version"> </template><template v-if="version">v{{ version }}</template></span>
200
- </div>
201
- </slot>
300
+ <div :class="{ 'lg:hidden': isRail }">
301
+ <slot name="footer">
302
+ <div
303
+ v-if="appName || version"
304
+ class="flex items-center justify-center gap-2 text-xs"
305
+ :class="isDark ? 'text-gray-500' : 'text-gray-500'"
306
+ >
307
+ <span>{{ appName }}<template v-if="appName && version"> </template><template v-if="version">v{{ version }}</template></span>
308
+ </div>
309
+ </slot>
310
+ </div>
202
311
  </div>
203
312
  </div>
204
313
  </aside>
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { useNotifications } from './useNotifications'
3
+
4
+ describe('useNotifications', () => {
5
+ beforeEach(() => {
6
+ // Singleton state persists across tests — reset it each time.
7
+ const { clear, closePanel } = useNotifications()
8
+ clear()
9
+ closePanel()
10
+ })
11
+
12
+ it('counts only unread notifications', () => {
13
+ const { setNotifications, unreadCount } = useNotifications()
14
+ setNotifications([
15
+ { id: 1, title: 'a' },
16
+ { id: 2, title: 'b', read: true },
17
+ { id: 3, title: 'c' },
18
+ ])
19
+ expect(unreadCount.value).toBe(2)
20
+ })
21
+
22
+ it('markAllRead clears the unread count', () => {
23
+ const { setNotifications, markAllRead, unreadCount } = useNotifications()
24
+ setNotifications([
25
+ { id: 1, title: 'a' },
26
+ { id: 2, title: 'b' },
27
+ ])
28
+ expect(unreadCount.value).toBe(2)
29
+ markAllRead()
30
+ expect(unreadCount.value).toBe(0)
31
+ })
32
+
33
+ it('markRead marks a single notification read', () => {
34
+ const { setNotifications, markRead, unreadCount } = useNotifications()
35
+ setNotifications([
36
+ { id: 1, title: 'a' },
37
+ { id: 2, title: 'b' },
38
+ ])
39
+ markRead(1)
40
+ expect(unreadCount.value).toBe(1)
41
+ })
42
+
43
+ it('add prepends newest-first and remove deletes by id', () => {
44
+ const { add, remove, notifications } = useNotifications()
45
+ add({ id: 1, title: 'first' })
46
+ add({ id: 2, title: 'second' })
47
+ expect(notifications.value.map((n) => n.id)).toEqual([2, 1])
48
+ remove(1)
49
+ expect(notifications.value.map((n) => n.id)).toEqual([2])
50
+ })
51
+
52
+ it('toggles and sets the panel open state', () => {
53
+ const { open, openPanel, closePanel, togglePanel } = useNotifications()
54
+ expect(open.value).toBe(false)
55
+ openPanel()
56
+ expect(open.value).toBe(true)
57
+ togglePanel()
58
+ expect(open.value).toBe(false)
59
+ togglePanel()
60
+ expect(open.value).toBe(true)
61
+ closePanel()
62
+ expect(open.value).toBe(false)
63
+ })
64
+ })
@@ -0,0 +1,81 @@
1
+ import { ref, computed } from 'vue'
2
+ import type { NotificationItem } from '../types/notification'
3
+
4
+ /**
5
+ * Shared notification state for the sidebar bell + `BaseNotificationPanel`.
6
+ *
7
+ * Module-scoped singleton (mirrors useMobileSidebar / useToast) so the
8
+ * sidebar's unread badge, the bell's open/close toggle, and the panel all
9
+ * read and mutate the same source. The consuming app owns the data: call
10
+ * `setNotifications()` (or `add()`) to feed it in.
11
+ *
12
+ * There's only ever one notification surface per app, so a singleton is
13
+ * the right shape here.
14
+ */
15
+ const notifications = ref<NotificationItem[]>([])
16
+ const open = ref(false)
17
+
18
+ const unreadCount = computed(
19
+ () => notifications.value.filter((n) => !n.read).length,
20
+ )
21
+
22
+ export function useNotifications() {
23
+ function openPanel() {
24
+ open.value = true
25
+ }
26
+ function closePanel() {
27
+ open.value = false
28
+ }
29
+ function togglePanel() {
30
+ open.value = !open.value
31
+ }
32
+
33
+ /** Replace the whole list (typical after a fetch). */
34
+ function setNotifications(items: NotificationItem[]) {
35
+ notifications.value = items
36
+ }
37
+
38
+ /** Prepend a single notification (newest first). */
39
+ function add(item: NotificationItem) {
40
+ notifications.value = [item, ...notifications.value]
41
+ }
42
+
43
+ /** Remove one by id. */
44
+ function remove(id: NotificationItem['id']) {
45
+ notifications.value = notifications.value.filter((n) => n.id !== id)
46
+ }
47
+
48
+ /** Mark a single notification read. */
49
+ function markRead(id: NotificationItem['id']) {
50
+ notifications.value = notifications.value.map((n) =>
51
+ n.id === id ? { ...n, read: true } : n,
52
+ )
53
+ }
54
+
55
+ /** Mark every notification read (clears the badge). */
56
+ function markAllRead() {
57
+ notifications.value = notifications.value.map((n) =>
58
+ n.read ? n : { ...n, read: true },
59
+ )
60
+ }
61
+
62
+ /** Remove every notification. */
63
+ function clear() {
64
+ notifications.value = []
65
+ }
66
+
67
+ return {
68
+ notifications,
69
+ unreadCount,
70
+ open,
71
+ openPanel,
72
+ closePanel,
73
+ togglePanel,
74
+ setNotifications,
75
+ add,
76
+ remove,
77
+ markRead,
78
+ markAllRead,
79
+ clear,
80
+ }
81
+ }
@@ -0,0 +1,64 @@
1
+ import { ref, watch, effectScope } from 'vue'
2
+
3
+ /**
4
+ * Shared collapsed/expanded state for the desktop sidebar.
5
+ *
6
+ * Module-scoped singleton (mirrors useMobileSidebar) so `BaseSidebar` and
7
+ * the page layout (`BaseAppLayout`, which offsets `<main>` by the sidebar
8
+ * width) stay in sync. When collapsed, the sidebar shrinks to an icon-only
9
+ * rail on desktop.
10
+ *
11
+ * The preference is persisted to localStorage. The first consumer can pass
12
+ * an options object to override the key; subsequent callers reuse the same
13
+ * singleton and the option is only consulted on the first call.
14
+ */
15
+ export interface UseSidebarCollapseOptions {
16
+ /** localStorage key used to persist the preference. */
17
+ storageKey?: string
18
+ }
19
+
20
+ const collapsed = ref(false)
21
+
22
+ let initialized = false
23
+ let configuredStorageKey = 'mgv-sidebar-collapsed'
24
+
25
+ function initialize(storageKey: string) {
26
+ if (initialized) {
27
+ return
28
+ }
29
+ initialized = true
30
+ configuredStorageKey = storageKey
31
+
32
+ if (typeof window !== 'undefined') {
33
+ const saved = window.localStorage?.getItem(configuredStorageKey)
34
+ if (saved) {
35
+ collapsed.value = saved === 'true'
36
+ }
37
+
38
+ // Detached scope so the persistence watcher lives for the app's
39
+ // lifetime and is NOT torn down when the first consuming component
40
+ // unmounts.
41
+ const scope = effectScope(true)
42
+ scope.run(() => {
43
+ watch(collapsed, (value) => {
44
+ window.localStorage?.setItem(configuredStorageKey, String(value))
45
+ })
46
+ })
47
+ }
48
+ }
49
+
50
+ export function useSidebarCollapse(options?: UseSidebarCollapseOptions) {
51
+ initialize(options?.storageKey ?? configuredStorageKey)
52
+
53
+ function collapse() {
54
+ collapsed.value = true
55
+ }
56
+ function expand() {
57
+ collapsed.value = false
58
+ }
59
+ function toggle() {
60
+ collapsed.value = !collapsed.value
61
+ }
62
+
63
+ return { collapsed, collapse, expand, toggle }
64
+ }
package/src/index.ts CHANGED
@@ -30,6 +30,9 @@ export { default as BasePageHeader } from './components/BasePageHeader.vue'
30
30
  export { default as BaseToolbarButton } from './components/BaseToolbarButton.vue'
31
31
  export { default as BaseActionButton } from './components/BaseActionButton.vue'
32
32
  export { default as BaseCopyButton } from './components/BaseCopyButton.vue'
33
+ export { default as BaseGoogleSignInButton } from './components/BaseGoogleSignInButton.vue'
34
+ export { default as BaseLoginForm } from './components/BaseLoginForm.vue'
35
+ export { default as BaseNotificationPanel } from './components/BaseNotificationPanel.vue'
33
36
 
34
37
  // Composables
35
38
  export { useTheme, initTheme } from './composables/useTheme'
@@ -40,6 +43,9 @@ export { useEscapeKey } from './composables/useEscapeKey'
40
43
  export { useDebouncedRef } from './composables/useDebounce'
41
44
  export { useToast } from './composables/useToast'
42
45
  export { useMobileSidebar } from './composables/useMobileSidebar'
46
+ export { useSidebarCollapse } from './composables/useSidebarCollapse'
47
+ export type { UseSidebarCollapseOptions } from './composables/useSidebarCollapse'
48
+ export { useNotifications } from './composables/useNotifications'
43
49
 
44
50
  // Enums
45
51
  export { AlertEnum } from './enums/AlertEnum'
@@ -57,6 +63,8 @@ export { PositioningEnum } from './enums/PositioningEnum'
57
63
  export type { BreadCrumb } from './components/BaseBreadcrumb.vue'
58
64
  export type { NavItem, NavSection } from './types/sidebar'
59
65
  export type { EntityPickerItem } from './types/entityPicker'
66
+ export type { LoginCredentials } from './types/auth'
67
+ export type { NotificationItem } from './types/notification'
60
68
 
61
69
  // Utils
62
70
  export { getBaseColor, getBaseColorOf } from './utils/util'
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Credentials emitted by `BaseLoginForm` on submit.
3
+ *
4
+ * `remember` is only present when the form is rendered with the
5
+ * "remember me" checkbox enabled.
6
+ */
7
+ export interface LoginCredentials {
8
+ email: string
9
+ password: string
10
+ remember?: boolean
11
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A single entry rendered in `BaseNotificationPanel` and counted by the
3
+ * sidebar's unread badge.
4
+ *
5
+ * • `id` — stable key, used for list rendering and mark/remove ops.
6
+ * • `title` — bold headline line.
7
+ * • `message` — optional secondary line.
8
+ * • `time` — optional pre-formatted timestamp (e.g. "2h ago"). The
9
+ * library does not format dates for you.
10
+ * • `read` — unread entries are highlighted and count toward the badge.
11
+ * • `type` — drives the small status dot colour.
12
+ */
13
+ export interface NotificationItem {
14
+ id: string | number
15
+ title: string
16
+ message?: string
17
+ time?: string
18
+ read?: boolean
19
+ type?: 'info' | 'success' | 'warning' | 'error'
20
+ }
@@ -1,31 +0,0 @@
1
- interface Props {
2
- /**
3
- * Whether the sidebar should currently be rendered. Defaults to true.
4
- * Set to false for routes that should occupy the full viewport (e.g.
5
- * landing / marketing pages).
6
- */
7
- showSidebar?: boolean;
8
- /** Skip-link label. Defaults to "Skip to main content". */
9
- skipLinkLabel?: string;
10
- }
11
- declare function __VLS_template(): {
12
- attrs: Partial<{}>;
13
- slots: {
14
- sidebar?(_: {}): any;
15
- default?(_: {}): any;
16
- };
17
- refs: {};
18
- rootEl: HTMLDivElement;
19
- };
20
- type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
21
- declare const __VLS_component: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<Props> & Readonly<{}>, {
22
- showSidebar: boolean;
23
- skipLinkLabel: string;
24
- }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
25
- declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
26
- export default _default;
27
- type __VLS_WithTemplateSlots<T, S> = T & {
28
- new (): {
29
- $slots: S;
30
- };
31
- };
@@ -1,31 +0,0 @@
1
- type IconColor = 'emerald' | 'sky' | 'red' | 'amber';
2
- type __VLS_Props = {
3
- title: string;
4
- subtitle?: string;
5
- iconColor?: IconColor;
6
- maxWidthClass?: string;
7
- };
8
- declare function __VLS_template(): {
9
- attrs: Partial<{}>;
10
- slots: {
11
- icon?(_: {
12
- iconClass: string;
13
- }): any;
14
- actions?(_: {}): any;
15
- };
16
- refs: {};
17
- rootEl: any;
18
- };
19
- type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
20
- declare const __VLS_component: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
21
- subtitle: string;
22
- maxWidthClass: string;
23
- iconColor: IconColor;
24
- }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
25
- declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
26
- export default _default;
27
- type __VLS_WithTemplateSlots<T, S> = T & {
28
- new (): {
29
- $slots: S;
30
- };
31
- };
@@ -1,46 +0,0 @@
1
- import { NavSection } from '../types/sidebar';
2
- interface Props {
3
- /** Nav sections (groups of items). */
4
- sections: NavSection[];
5
- /** Route name to navigate to when the logo / home button is clicked. */
6
- homeRouteName?: string;
7
- /** Optional app name shown in the footer next to the version. */
8
- appName?: string;
9
- /** Optional version string shown in the footer. */
10
- version?: string;
11
- /** Whether to render the dark/light toggle in the footer. */
12
- showThemeToggle?: boolean;
13
- }
14
- declare function __VLS_template(): {
15
- attrs: Partial<{}>;
16
- slots: {
17
- logo?(_: {
18
- size: number;
19
- }): any;
20
- logo?(_: {
21
- size: number;
22
- }): any;
23
- status?(_: {}): any;
24
- footer?(_: {}): any;
25
- };
26
- refs: {
27
- closeMobileButton: HTMLButtonElement;
28
- };
29
- rootEl: HTMLDivElement;
30
- };
31
- type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
32
- declare const __VLS_component: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<Props> & Readonly<{}>, {
33
- homeRouteName: string;
34
- appName: string;
35
- version: string;
36
- showThemeToggle: boolean;
37
- }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
38
- closeMobileButton: HTMLButtonElement;
39
- }, HTMLDivElement>;
40
- declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
41
- export default _default;
42
- type __VLS_WithTemplateSlots<T, S> = T & {
43
- new (): {
44
- $slots: S;
45
- };
46
- };
@@ -1,27 +0,0 @@
1
- declare function __VLS_template(): {
2
- attrs: Partial<{}>;
3
- slots: {
4
- default?(_: {}): any;
5
- };
6
- refs: {};
7
- rootEl: HTMLDivElement;
8
- };
9
- type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
10
- declare const __VLS_component: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
11
- color: {
12
- type: StringConstructor;
13
- required: false;
14
- };
15
- }>, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
16
- color: {
17
- type: StringConstructor;
18
- required: false;
19
- };
20
- }>> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, HTMLDivElement>;
21
- declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
22
- export default _default;
23
- type __VLS_WithTemplateSlots<T, S> = T & {
24
- new (): {
25
- $slots: S;
26
- };
27
- };