@veristone/nuxt-v-app 0.1.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 (136) hide show
  1. package/README.md +42 -0
  2. package/app/app.vue +7 -0
  3. package/app/assets/css/v-app.css +313 -0
  4. package/app/components/V/A/Badge.vue +75 -0
  5. package/app/components/V/A/Btn/Add.vue +17 -0
  6. package/app/components/V/A/Btn/Back.vue +25 -0
  7. package/app/components/V/A/Btn/ConfirmDelete.vue +45 -0
  8. package/app/components/V/A/Btn/Edit.vue +35 -0
  9. package/app/components/V/A/Btn/Export.vue +28 -0
  10. package/app/components/V/A/Btn/Refresh.vue +21 -0
  11. package/app/components/V/A/Btn/Submit.vue +45 -0
  12. package/app/components/V/A/Btn/View.vue +23 -0
  13. package/app/components/V/A/Card.legacy.vue +291 -0
  14. package/app/components/V/A/Card.vue +108 -0
  15. package/app/components/V/A/CompanyMenu.vue +83 -0
  16. package/app/components/V/A/Data/KeyValue.vue +98 -0
  17. package/app/components/V/A/Data/StatusBadge.vue +44 -0
  18. package/app/components/V/A/DataField.vue +140 -0
  19. package/app/components/V/A/DataGrid.vue +43 -0
  20. package/app/components/V/A/DataTable.vue +144 -0
  21. package/app/components/V/A/EmptyState.vue +154 -0
  22. package/app/components/V/A/Fmt/Currency.vue +36 -0
  23. package/app/components/V/A/Fmt/DateTime.vue +34 -0
  24. package/app/components/V/A/Fmt/Percent.vue +47 -0
  25. package/app/components/V/A/LoadingState.vue +140 -0
  26. package/app/components/V/A/MetricCard.vue +129 -0
  27. package/app/components/V/A/Modal/Base.vue +195 -0
  28. package/app/components/V/A/Modal/Confirm.vue +92 -0
  29. package/app/components/V/A/Modal/Form.vue +105 -0
  30. package/app/components/V/A/Navigation.vue +110 -0
  31. package/app/components/V/A/QuickActions.vue +169 -0
  32. package/app/components/V/A/Slide.vue +109 -0
  33. package/app/components/V/A/Slideover.vue +259 -0
  34. package/app/components/V/A/State/Empty.vue +20 -0
  35. package/app/components/V/A/State/Error.vue +34 -0
  36. package/app/components/V/A/State/Loading.vue +33 -0
  37. package/app/components/V/A/StatsCard.vue +215 -0
  38. package/app/components/V/A/StatusBadge.vue +215 -0
  39. package/app/components/V/A/Table.vue +674 -0
  40. package/app/components/V/A/UserMenu.vue +127 -0
  41. package/app/components/V/A/WelcomeHeader.vue +96 -0
  42. package/app/components/V/Modal.vue +36 -0
  43. package/app/components/Va/Blocks/VaBlockGridCharts.vue +32 -0
  44. package/app/components/Va/Blocks/VaBlockGridKPI.vue +32 -0
  45. package/app/components/Va/Blocks/VaBlockGridTables.vue +23 -0
  46. package/app/components/Va/Blocks/VaBlockKpiGrid.vue +8 -0
  47. package/app/components/Va/Blocks/VaBlockSessionFilterBar.vue +8 -0
  48. package/app/components/Va/Cards/VaCardDonutChart.vue +59 -0
  49. package/app/components/Va/Cards/VaCardHeader.vue +10 -0
  50. package/app/components/Va/Cards/VaCardKpi.vue +17 -0
  51. package/app/components/Va/Cards/VaCardKpi2.vue +55 -0
  52. package/app/components/Va/Cards/VaCardLatestOrders.vue +82 -0
  53. package/app/components/Va/Cards/VaCardPopularProducts.vue +88 -0
  54. package/app/components/Va/Cards/VaCardRevenueBarChart.vue +49 -0
  55. package/app/components/Va/Cards/VaCardSubtitle.vue +5 -0
  56. package/app/components/Va/Cards/VaCardTitle.vue +5 -0
  57. package/app/components/Va/Cards/VaCardWithActiveUsers.vue +41 -0
  58. package/app/components/Va/Cards/VaCardWithChart.vue +135 -0
  59. package/app/components/Va/Cards/VaCardWithChartBlock.vue +26 -0
  60. package/app/components/Va/Cards/VaCardWithIndicator.vue +39 -0
  61. package/app/components/Va/Cards/VaCardWithProgressCircle.vue +34 -0
  62. package/app/components/Va/Cards/types.ts +11 -0
  63. package/app/components/Va/Charts/VaChartAppPerformanceBar.vue +118 -0
  64. package/app/components/Va/Charts/VaChartAppPerformanceBarChart.vue +118 -0
  65. package/app/components/Va/Charts/VaChartAreaMini.vue +127 -0
  66. package/app/components/Va/Charts/VaChartBarMini.vue +68 -0
  67. package/app/components/Va/Charts/VaChartCardinalMulti.vue +108 -0
  68. package/app/components/Va/Charts/VaChartColorBarChart.vue +78 -0
  69. package/app/components/Va/Charts/VaChartDonutHalf.vue +35 -0
  70. package/app/components/Va/Charts/VaChartDonutMini.vue +77 -0
  71. package/app/components/Va/Charts/VaChartExpensesBar.vue +58 -0
  72. package/app/components/Va/Charts/VaChartFinanceSummary.vue +96 -0
  73. package/app/components/Va/Charts/VaChartGoogleSearchConsole.vue +90 -0
  74. package/app/components/Va/Charts/VaChartIncomeBar.vue +82 -0
  75. package/app/components/Va/Charts/VaChartLegend.vue +25 -0
  76. package/app/components/Va/Charts/VaChartLineMini.vue +205 -0
  77. package/app/components/Va/Charts/VaChartRealtimeTraffic.vue +182 -0
  78. package/app/components/Va/Charts/VaChartRevenue.vue +43 -0
  79. package/app/components/Va/Charts/VaChartRevenueLine.vue +42 -0
  80. package/app/components/Va/Charts/VaChartRevenuevsCost.vue +84 -0
  81. package/app/components/Va/Charts/VaChartSearchIntent.vue +179 -0
  82. package/app/components/Va/Charts/VaChartSpendingTrend.vue +127 -0
  83. package/app/components/Va/Charts/VaChartStackedHorizontal.vue +64 -0
  84. package/app/components/Va/Charts/VaChartStepMinimal.vue +109 -0
  85. package/app/components/Va/Charts/VaChartStockComparisonLine.vue +86 -0
  86. package/app/components/Va/Charts/VaChartStocksPortfolioLine.vue +161 -0
  87. package/app/components/Va/Charts/VaChartStocksSectorLine.vue +223 -0
  88. package/app/components/Va/Charts/VaChartTasksCategories.vue +96 -0
  89. package/app/components/Va/Charts/VaChartTasksProgress.vue +130 -0
  90. package/app/components/Va/Charts/VaChartTrafficOverview.vue +112 -0
  91. package/app/components/Va/Charts/VaChartWebPerformanceLineChart.vue +114 -0
  92. package/app/components/Va/Charts/VaChartWinLostBar.vue +110 -0
  93. package/app/components/Va/Charts/VaChartWinLostDonut.vue +107 -0
  94. package/app/components/Va/Charts/VaChartWinLostLine.vue +111 -0
  95. package/app/components/Va/Charts/types.ts +10 -0
  96. package/app/components/Va/Dashboard/Navigation/types.ts +8 -0
  97. package/app/components/Va/Dashboard/VaDashboardKPICard.vue +31 -0
  98. package/app/components/Va/Dashboard/VaDashboardNavigation.vue +50 -0
  99. package/app/components/Va/Dashboard/VaDashboardPricePlan.vue +102 -0
  100. package/app/components/Va/Dashboard/VaDashboardUsageChart.vue +84 -0
  101. package/app/components/Va/Dashboard/VaDashboardUsageRequestChart.vue +46 -0
  102. package/app/components/Va/Layout/NotificationsSlideover.vue +169 -0
  103. package/app/components/Va/Layout/SideNav/types.ts +5 -0
  104. package/app/components/Va/Layout/SideNav.vue +108 -0
  105. package/app/components/Va/Layout/TeamsMenu.vue +57 -0
  106. package/app/components/Va/Layout/UserMenu.vue +57 -0
  107. package/app/composables/useDashboard.ts +25 -0
  108. package/app/composables/useVAAnimation.ts +324 -0
  109. package/app/composables/useVAUtils.ts +118 -0
  110. package/app/composables/useVCrud.ts +647 -0
  111. package/app/composables/useVFetch.ts +46 -0
  112. package/app/composables/useVFileUpload.ts +45 -0
  113. package/app/composables/useVToast.ts +73 -0
  114. package/app/composables/useXATableColumns.ts +456 -0
  115. package/app/data/BillingStats.ts +65 -0
  116. package/app/data/SearchData.ts +58 -0
  117. package/app/data/TasksData.ts +101 -0
  118. package/app/data/dashboardData.ts +113 -0
  119. package/app/layouts/default.vue +171 -0
  120. package/app/layouts/legacy.vue +61 -0
  121. package/app/pages/playground/base.vue +498 -0
  122. package/app/pages/playground/blocks.vue +108 -0
  123. package/app/pages/playground/buttons.vue +237 -0
  124. package/app/pages/playground/cards.vue +326 -0
  125. package/app/pages/playground/charts.vue +338 -0
  126. package/app/pages/playground/dashboard.vue +315 -0
  127. package/app/pages/playground/formatters.vue +329 -0
  128. package/app/pages/playground/index.vue +109 -0
  129. package/app/pages/playground/layout.vue +159 -0
  130. package/app/pages/playground/modals.vue +606 -0
  131. package/app/pages/playground/states.vue +282 -0
  132. package/app/pages/playground/tables.vue +618 -0
  133. package/app/pages/test-layout.vue +10 -0
  134. package/nuxt.config.ts +12 -0
  135. package/package.json +71 -0
  136. package/tsconfig.json +18 -0
@@ -0,0 +1,84 @@
1
+ <script setup lang="ts">
2
+ import type { BillingData } from '~/data/BillingStats'
3
+
4
+ const props = defineProps<{
5
+ data: Array<BillingData>
6
+ }>()
7
+
8
+ const colorMode = useColorMode()
9
+
10
+ const categories = computed(() => ({
11
+ apiCalls: {
12
+ name: 'API Calls',
13
+ color: '#5D5AA1'
14
+ },
15
+ storage: {
16
+ name: 'Storage (GB)',
17
+ color: '#7B9EA8'
18
+ },
19
+ users: {
20
+ name: 'Users',
21
+ color: '#f59e42'
22
+ }
23
+ }))
24
+
25
+ const markerConfig = {
26
+ id: 'area-chart',
27
+ config: {
28
+ apiCalls: {
29
+ type: 'circle' as const,
30
+ size: 8,
31
+ color: '#5D5AA1',
32
+ strokeColor: '#8A88C7',
33
+ strokeWidth: 1
34
+ },
35
+ storage: {
36
+ type: 'circle' as const,
37
+ size: 8,
38
+ color: '#7B9EA8',
39
+ strokeColor: '#A3C1CC',
40
+ strokeWidth: 1
41
+ },
42
+ users: {
43
+ type: 'circle' as const,
44
+ size: 8,
45
+ color: '#f59e42',
46
+ strokeColor: '#ffc285',
47
+ strokeWidth: 1
48
+ }
49
+ }
50
+ }
51
+
52
+ const xFormatter = (tick: number, _i?: number, _ticks?: number[]): string => {
53
+ return props.data[tick]?.month ?? ''
54
+ }
55
+ </script>
56
+
57
+ <template>
58
+ <AreaChart
59
+ :key="colorMode.value"
60
+ :data="data"
61
+ :height="240"
62
+ :x-num-ticks="12"
63
+ :y-num-ticks="4"
64
+ :categories="categories"
65
+ :x-formatter="xFormatter"
66
+ :y-grid-line="true"
67
+ :marker-config="markerConfig"
68
+ :curve-type="CurveType.MonotoneX"
69
+ :legend-position="LegendPosition.BottomCenter"
70
+ :hide-legend="false"
71
+ />
72
+ </template>
73
+
74
+ <style scoped>
75
+ #area-chart:deep(*[stroke='#5D5AA1']) {
76
+ marker: url('#area-chart-apiCalls');
77
+ }
78
+ #area-chart:deep(*[stroke='#7B9EA8']) {
79
+ marker: url('#area-chart-storage');
80
+ }
81
+ #area-chart:deep(*[stroke='#f59e42']) {
82
+ marker: url('#area-chart-users');
83
+ }
84
+ </style>
@@ -0,0 +1,46 @@
1
+ <script setup lang="ts">
2
+ import type { BillingData } from '~/data/BillingStats'
3
+
4
+ const props = defineProps<{
5
+ data: Array<BillingData>
6
+ }>()
7
+
8
+ const colorMode = useColorMode()
9
+
10
+ const categories = computed(() => ({
11
+ apiCalls: {
12
+ name: 'API Calls',
13
+ color: colorMode.value === 'dark' ? '#5D5AA1' : '#5D5AA1'
14
+ },
15
+ storage: {
16
+ name: 'Storage (GB)',
17
+ color: colorMode.value === 'dark' ? '#7B9EA8' : '#3b82f6'
18
+ },
19
+ users: {
20
+ name: 'Users',
21
+ color: colorMode.value === 'dark' ? '#fb923c' : '#f59e42'
22
+ }
23
+ }))
24
+
25
+ const xFormatter = (tick: number, _i?: number, _ticks?: number[]): string => {
26
+ return props.data[tick]?.month ?? ''
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <BarChart
32
+ :key="colorMode.value"
33
+ :data="data"
34
+ :height="240"
35
+ :stacked="true"
36
+ :x-num-ticks="12"
37
+ :categories="categories"
38
+ :x-formatter="xFormatter"
39
+ :y-grid-line="true"
40
+ :radius="4"
41
+ :bar-padding="0.7"
42
+ :y-axis="['apiCalls', 'storage', 'users']"
43
+ :legend-position="LegendPosition.BottomCenter"
44
+ :hide-legend="false"
45
+ />
46
+ </template>
@@ -0,0 +1,169 @@
1
+ <script setup lang="ts">
2
+ import { formatTimeAgo } from '@vueuse/core'
3
+ import type { Notification } from '~/types'
4
+ import type { TabsItem } from '@nuxt/ui'
5
+
6
+ const { isNotificationsSlideoverOpen } = useDashboard()
7
+
8
+ const { data: notifications } = await useFetch<Notification[]>('/api/notifications')
9
+
10
+ const tabs: TabsItem[] = [
11
+ {
12
+ label: 'Today',
13
+ value: 'today',
14
+ icon: 'i-lucide-sun'
15
+ },
16
+ {
17
+ label: 'This Week',
18
+ value: 'week',
19
+ icon: 'i-lucide-calendar'
20
+ },
21
+ {
22
+ label: 'Earlier',
23
+ value: 'earlier',
24
+ icon: 'i-lucide-history'
25
+ }
26
+ ]
27
+
28
+ const now = new Date()
29
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
30
+ const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
31
+
32
+ const currentFilter = ref('today')
33
+ const currentNotification = ref()
34
+
35
+ const filteredNotifications = computed(() => notifications?.value?.filter((notification) => {
36
+ const notificationDate = new Date(notification.date)
37
+ if (currentFilter.value === 'today') {
38
+ return notificationDate >= today
39
+ } else if (currentFilter.value === 'week') {
40
+ return notificationDate >= weekAgo && notificationDate < today
41
+ } else {
42
+ return notificationDate < weekAgo
43
+ }
44
+ }))
45
+
46
+ function toggleNotification(notification) {
47
+ currentNotification.value = notification
48
+ }
49
+
50
+ function isActive(notificationId: string) {
51
+ return currentNotification.value && currentNotification.value.id === notificationId
52
+ }
53
+ </script>
54
+
55
+ <template>
56
+ <USlideover
57
+ v-model:open="isNotificationsSlideoverOpen"
58
+ title="Notifications"
59
+ class="m-4 rounded-xl"
60
+ :ui="{
61
+ overlay: 'bg-black/50',
62
+ content: 'bg-default',
63
+ body: 'p-2 sm:p-4'
64
+ }"
65
+ >
66
+ <template #body>
67
+ <UTabs
68
+ v-model="currentFilter"
69
+ :items="tabs"
70
+ color="neutral"
71
+ >
72
+ <template #content>
73
+ <div class=" mt-4">
74
+ <div
75
+ v-for="notification in filteredNotifications"
76
+ :key="notification.id"
77
+ class=""
78
+ @click="toggleNotification(notification)"
79
+ >
80
+ <div
81
+ class="px-3 rounded-md hover:bg-elevated/50 flex items-start gap-5 relative block cursor-pointer"
82
+ :class="isActive(notification.id) ? 'py-4 bg-elevated/50' : 'py-4'"
83
+ >
84
+ <UChip
85
+ color="error"
86
+ :show="!!notification.unread"
87
+ inset
88
+ >
89
+ <UAvatar
90
+ v-bind="notification.sender.avatar"
91
+ :alt="notification.sender.name"
92
+ size="xl"
93
+ />
94
+ </UChip>
95
+
96
+ <div class="flex-1 min-w-0">
97
+ <div class="flex items-center justify-between">
98
+ <div class="flex items-center justify-start gap-2">
99
+ <span class="text-highlighted font-medium truncate">{{ notification.sender.name }}</span>
100
+
101
+ <time
102
+ :datetime="notification.date"
103
+ class="text-muted text-xs whitespace-nowrap"
104
+ v-text="formatTimeAgo(new Date(notification.date))"
105
+ />
106
+ </div>
107
+ <div>
108
+ <UDropdownMenu
109
+ :items="[
110
+ [
111
+ {
112
+ label: notification.sender.name,
113
+ type: 'label'
114
+ }
115
+ ],
116
+ [
117
+ {
118
+ label: 'Mark as read',
119
+ onSelect: () => {
120
+ // Handle mark as read
121
+ }
122
+ },
123
+ {
124
+ label: 'Edit',
125
+ onSelect: () => {
126
+ // Handle edit
127
+ }
128
+ }
129
+ ]
130
+ ]"
131
+ >
132
+ <UButton
133
+ icon="i-lucide-ellipsis-vertical"
134
+ variant="link"
135
+ size="xs"
136
+ />
137
+ </UDropdownMenu>
138
+ </div>
139
+ </div>
140
+
141
+ <p class="text-dimmed truncate text-sm">
142
+ {{ notification.body }}
143
+ </p>
144
+
145
+ <div
146
+ v-show="isActive(notification.id)"
147
+ class="flex gap-2 mt-2"
148
+ >
149
+ <UButton
150
+ label="Ignore"
151
+ variant="soft"
152
+ size="sm"
153
+ />
154
+ <UButton
155
+ label="Accept"
156
+ variant="solid"
157
+ color="primary"
158
+ size="sm"
159
+ />
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </template>
166
+ </UTabs>
167
+ </template>
168
+ </USlideover>
169
+ </template>
@@ -0,0 +1,5 @@
1
+ import type { NavigationMenuItem as NuxtNavigationMenuItem } from '@nuxt/ui'
2
+
3
+ export interface NavigationMenuItem extends NuxtNavigationMenuItem {
4
+ open?: boolean
5
+ }
@@ -0,0 +1,108 @@
1
+ <script lang="ts" setup>
2
+ import type { NavigationMenuItem } from './SideNav/types'
3
+
4
+ const props = withDefaults(defineProps<{
5
+ items: NavigationMenuItem[]
6
+ collapsed?: boolean
7
+ }>(), {
8
+ collapsed: false
9
+ })
10
+
11
+ const route = useRoute()
12
+
13
+ const openItems = ref(
14
+ props.items.reduce((acc, item, index) => {
15
+ acc[index] = item.open ?? true
16
+ return acc
17
+ }, {} as Record<number, boolean>)
18
+ )
19
+
20
+ const toggleItem = (index: number) => {
21
+ openItems.value[index] = !openItems.value[index]
22
+ }
23
+
24
+ const isItemOpen = (index: number) => {
25
+ return openItems.value[index] ?? true
26
+ }
27
+ </script>
28
+
29
+ <template>
30
+ <div>
31
+ <div class="space-y-4">
32
+ <NuxtLink
33
+ to="/"
34
+ class="mt-2 block"
35
+ >
36
+ <UButton
37
+ :ui="{ leadingIcon: 'size-4 mr-1' }"
38
+ class="w-full cursor-pointer"
39
+ icon="i-lucide-house"
40
+ variant="link"
41
+ active-variant="soft"
42
+ :active="route.path === '/'"
43
+ :label="collapsed ? '' : 'Overview'"
44
+ />
45
+ </NuxtLink>
46
+
47
+ <hr class="bg-(--ui-border) border-default">
48
+
49
+ <div
50
+ v-for="(item, itemKey) in props.items"
51
+ :key="itemKey"
52
+ class="border-b border-default"
53
+ :class="collapsed ? 'space-y-2' : ' px-2'"
54
+ >
55
+ <UButton
56
+ v-if="!collapsed"
57
+ variant="ghost"
58
+ color="neutral"
59
+ class="w-full justify-between uppercase text-xs font-medium tracking-wider mb-2 px-2"
60
+ @click="toggleItem(itemKey)"
61
+ >
62
+ <template #leading>
63
+ <span>{{ item.label }}</span>
64
+ </template>
65
+ <template #trailing>
66
+ <UIcon
67
+ name="i-lucide-chevron-down"
68
+ class="size-4 transition-transform duration-200"
69
+ :class="{ '-rotate-180': !isItemOpen(itemKey) }"
70
+ />
71
+ </template>
72
+ </UButton>
73
+ <div
74
+ v-if="!collapsed"
75
+ class="overflow-hidden transition-all duration-200"
76
+ :class="isItemOpen(itemKey) ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'"
77
+ >
78
+ <NuxtLink
79
+ v-for="child in item.children"
80
+ :key="child.label"
81
+ :to="child.to"
82
+ class="w-full block px-1.5 py-1.5 flex items-center gap-2 hover:text-default text-sm last:mb-4"
83
+ :class="route.path === child.to ? 'text-default' : 'text-muted'"
84
+ >
85
+ <UIcon
86
+ :name="child.icon"
87
+ class="text-muted size-4"
88
+ :class="collapsed ? '' : 'mr-1'"
89
+ />
90
+ <span v-if="!collapsed">{{ child.label }}</span>
91
+ </NuxtLink>
92
+ </div>
93
+ <template v-if="collapsed">
94
+ <UButton
95
+ v-for="child in item.children"
96
+ :key="child.label"
97
+ :to="child.to"
98
+ variant="link"
99
+ color="neutral"
100
+ :active="route.path === child.to"
101
+ class="w-full justify-center px-1.5"
102
+ :icon="child.icon"
103
+ />
104
+ </template>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </template>
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ import type { DropdownMenuItem } from '@nuxt/ui'
3
+
4
+ defineProps<{
5
+ collapsed?: boolean
6
+ }>()
7
+
8
+ const teams = ref([
9
+ {
10
+ label: 'Nuxt Charts',
11
+ avatar: {
12
+ src: '/logo.png',
13
+ alt: 'Nuxt Charts'
14
+ }
15
+ },
16
+ {
17
+ label: 'NuxtHub',
18
+ avatar: {
19
+ src: 'https://github.com/nuxt-hub.png',
20
+ alt: 'NuxtHub'
21
+ }
22
+ }
23
+ ])
24
+ const selectedTeam = ref(teams.value[0])
25
+
26
+ const items = computed<DropdownMenuItem[][]>(() => {
27
+ return [
28
+ teams.value.map(team => ({
29
+ ...team,
30
+ onSelect() {
31
+ selectedTeam.value = team
32
+ }
33
+ }))
34
+ ]
35
+ })
36
+ </script>
37
+
38
+ <template>
39
+ <UDropdownMenu
40
+ :items="items"
41
+ :content="{ align: 'center', collisionPadding: 12 }"
42
+ :ui="{
43
+ content: collapsed ? 'w-40' : 'w-(--reka-dropdown-menu-trigger-width)'
44
+ }"
45
+ >
46
+ <UButton
47
+ v-bind="{
48
+ ...selectedTeam,
49
+ label: collapsed ? undefined : selectedTeam?.label,
50
+ trailingIcon: collapsed ? undefined : 'i-lucide-chevrons-up-down'
51
+ }"
52
+ variant="ghost"
53
+ block
54
+ :square="collapsed"
55
+ />
56
+ </UDropdownMenu>
57
+ </template>
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ import type { DropdownMenuItem } from '@nuxt/ui'
3
+
4
+ defineProps<{
5
+ collapsed?: boolean
6
+ }>()
7
+
8
+ const user = ref({
9
+ name: 'Demo User',
10
+ avatar: {
11
+ src: 'https://api.dicebear.com/7.x/avataaars/svg?seed=demo',
12
+ alt: 'Demo User'
13
+ }
14
+ })
15
+
16
+ const items = computed<DropdownMenuItem[][]>(() => ([[{
17
+ type: 'label',
18
+ label: user.value.name,
19
+ avatar: user.value.avatar
20
+ }], [{
21
+ label: 'Profile',
22
+ icon: 'i-lucide-user'
23
+ }], [{
24
+ label: 'Log out',
25
+ icon: 'i-lucide-log-out'
26
+ }]]))
27
+ </script>
28
+
29
+ <template>
30
+ <UDropdownMenu
31
+ :items="items"
32
+ :content="{ align: 'center', collisionPadding: 12 }"
33
+ :ui="{ content: collapsed ? 'w-48' : 'w-(--reka-dropdown-menu-trigger-width)' }"
34
+ >
35
+ <UButton
36
+ v-bind="{
37
+ ...user,
38
+ label: collapsed ? undefined : user?.name,
39
+ trailingIcon: collapsed ? undefined : 'i-lucide-chevrons-up-down'
40
+ }"
41
+ color="neutral"
42
+ variant="ghost"
43
+ block
44
+ :square="collapsed"
45
+ class="data-[state=open]:bg-elevated"
46
+ :ui="{
47
+ trailingIcon: 'text-dimmed'
48
+ }"
49
+ />
50
+
51
+ <!-- REMOVED: #chip-leading slot causes Vue 3.5 SSR error "Cannot read properties of null (reading 'ce')"
52
+ This is a known issue with custom slots in UDropdownMenu during SSR rendering.
53
+ The chip color indicator is a nice-to-have feature that can be re-added when the upstream issue is fixed.
54
+ See: https://github.com/vuejs/core/issues/4344, https://github.com/nuxt/nuxt/issues/28468
55
+ -->
56
+ </UDropdownMenu>
57
+ </template>
@@ -0,0 +1,25 @@
1
+ import { createSharedComposable } from '@vueuse/core'
2
+
3
+ const _useDashboard = () => {
4
+ const route = useRoute()
5
+ const router = useRouter()
6
+ const isNotificationsSlideoverOpen = ref(false)
7
+
8
+ defineShortcuts({
9
+ 'g-h': () => router.push('/'),
10
+ 'g-i': () => router.push('/inbox'),
11
+ 'g-c': () => router.push('/customers'),
12
+ 'g-s': () => router.push('/settings'),
13
+ 'n': () => isNotificationsSlideoverOpen.value = !isNotificationsSlideoverOpen.value
14
+ })
15
+
16
+ watch(() => route.fullPath, () => {
17
+ isNotificationsSlideoverOpen.value = false
18
+ })
19
+
20
+ return {
21
+ isNotificationsSlideoverOpen
22
+ }
23
+ }
24
+
25
+ export const useDashboard = createSharedComposable(_useDashboard)