@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.
- package/README.md +42 -0
- package/app/app.vue +7 -0
- package/app/assets/css/v-app.css +313 -0
- package/app/components/V/A/Badge.vue +75 -0
- package/app/components/V/A/Btn/Add.vue +17 -0
- package/app/components/V/A/Btn/Back.vue +25 -0
- package/app/components/V/A/Btn/ConfirmDelete.vue +45 -0
- package/app/components/V/A/Btn/Edit.vue +35 -0
- package/app/components/V/A/Btn/Export.vue +28 -0
- package/app/components/V/A/Btn/Refresh.vue +21 -0
- package/app/components/V/A/Btn/Submit.vue +45 -0
- package/app/components/V/A/Btn/View.vue +23 -0
- package/app/components/V/A/Card.legacy.vue +291 -0
- package/app/components/V/A/Card.vue +108 -0
- package/app/components/V/A/CompanyMenu.vue +83 -0
- package/app/components/V/A/Data/KeyValue.vue +98 -0
- package/app/components/V/A/Data/StatusBadge.vue +44 -0
- package/app/components/V/A/DataField.vue +140 -0
- package/app/components/V/A/DataGrid.vue +43 -0
- package/app/components/V/A/DataTable.vue +144 -0
- package/app/components/V/A/EmptyState.vue +154 -0
- package/app/components/V/A/Fmt/Currency.vue +36 -0
- package/app/components/V/A/Fmt/DateTime.vue +34 -0
- package/app/components/V/A/Fmt/Percent.vue +47 -0
- package/app/components/V/A/LoadingState.vue +140 -0
- package/app/components/V/A/MetricCard.vue +129 -0
- package/app/components/V/A/Modal/Base.vue +195 -0
- package/app/components/V/A/Modal/Confirm.vue +92 -0
- package/app/components/V/A/Modal/Form.vue +105 -0
- package/app/components/V/A/Navigation.vue +110 -0
- package/app/components/V/A/QuickActions.vue +169 -0
- package/app/components/V/A/Slide.vue +109 -0
- package/app/components/V/A/Slideover.vue +259 -0
- package/app/components/V/A/State/Empty.vue +20 -0
- package/app/components/V/A/State/Error.vue +34 -0
- package/app/components/V/A/State/Loading.vue +33 -0
- package/app/components/V/A/StatsCard.vue +215 -0
- package/app/components/V/A/StatusBadge.vue +215 -0
- package/app/components/V/A/Table.vue +674 -0
- package/app/components/V/A/UserMenu.vue +127 -0
- package/app/components/V/A/WelcomeHeader.vue +96 -0
- package/app/components/V/Modal.vue +36 -0
- package/app/components/Va/Blocks/VaBlockGridCharts.vue +32 -0
- package/app/components/Va/Blocks/VaBlockGridKPI.vue +32 -0
- package/app/components/Va/Blocks/VaBlockGridTables.vue +23 -0
- package/app/components/Va/Blocks/VaBlockKpiGrid.vue +8 -0
- package/app/components/Va/Blocks/VaBlockSessionFilterBar.vue +8 -0
- package/app/components/Va/Cards/VaCardDonutChart.vue +59 -0
- package/app/components/Va/Cards/VaCardHeader.vue +10 -0
- package/app/components/Va/Cards/VaCardKpi.vue +17 -0
- package/app/components/Va/Cards/VaCardKpi2.vue +55 -0
- package/app/components/Va/Cards/VaCardLatestOrders.vue +82 -0
- package/app/components/Va/Cards/VaCardPopularProducts.vue +88 -0
- package/app/components/Va/Cards/VaCardRevenueBarChart.vue +49 -0
- package/app/components/Va/Cards/VaCardSubtitle.vue +5 -0
- package/app/components/Va/Cards/VaCardTitle.vue +5 -0
- package/app/components/Va/Cards/VaCardWithActiveUsers.vue +41 -0
- package/app/components/Va/Cards/VaCardWithChart.vue +135 -0
- package/app/components/Va/Cards/VaCardWithChartBlock.vue +26 -0
- package/app/components/Va/Cards/VaCardWithIndicator.vue +39 -0
- package/app/components/Va/Cards/VaCardWithProgressCircle.vue +34 -0
- package/app/components/Va/Cards/types.ts +11 -0
- package/app/components/Va/Charts/VaChartAppPerformanceBar.vue +118 -0
- package/app/components/Va/Charts/VaChartAppPerformanceBarChart.vue +118 -0
- package/app/components/Va/Charts/VaChartAreaMini.vue +127 -0
- package/app/components/Va/Charts/VaChartBarMini.vue +68 -0
- package/app/components/Va/Charts/VaChartCardinalMulti.vue +108 -0
- package/app/components/Va/Charts/VaChartColorBarChart.vue +78 -0
- package/app/components/Va/Charts/VaChartDonutHalf.vue +35 -0
- package/app/components/Va/Charts/VaChartDonutMini.vue +77 -0
- package/app/components/Va/Charts/VaChartExpensesBar.vue +58 -0
- package/app/components/Va/Charts/VaChartFinanceSummary.vue +96 -0
- package/app/components/Va/Charts/VaChartGoogleSearchConsole.vue +90 -0
- package/app/components/Va/Charts/VaChartIncomeBar.vue +82 -0
- package/app/components/Va/Charts/VaChartLegend.vue +25 -0
- package/app/components/Va/Charts/VaChartLineMini.vue +205 -0
- package/app/components/Va/Charts/VaChartRealtimeTraffic.vue +182 -0
- package/app/components/Va/Charts/VaChartRevenue.vue +43 -0
- package/app/components/Va/Charts/VaChartRevenueLine.vue +42 -0
- package/app/components/Va/Charts/VaChartRevenuevsCost.vue +84 -0
- package/app/components/Va/Charts/VaChartSearchIntent.vue +179 -0
- package/app/components/Va/Charts/VaChartSpendingTrend.vue +127 -0
- package/app/components/Va/Charts/VaChartStackedHorizontal.vue +64 -0
- package/app/components/Va/Charts/VaChartStepMinimal.vue +109 -0
- package/app/components/Va/Charts/VaChartStockComparisonLine.vue +86 -0
- package/app/components/Va/Charts/VaChartStocksPortfolioLine.vue +161 -0
- package/app/components/Va/Charts/VaChartStocksSectorLine.vue +223 -0
- package/app/components/Va/Charts/VaChartTasksCategories.vue +96 -0
- package/app/components/Va/Charts/VaChartTasksProgress.vue +130 -0
- package/app/components/Va/Charts/VaChartTrafficOverview.vue +112 -0
- package/app/components/Va/Charts/VaChartWebPerformanceLineChart.vue +114 -0
- package/app/components/Va/Charts/VaChartWinLostBar.vue +110 -0
- package/app/components/Va/Charts/VaChartWinLostDonut.vue +107 -0
- package/app/components/Va/Charts/VaChartWinLostLine.vue +111 -0
- package/app/components/Va/Charts/types.ts +10 -0
- package/app/components/Va/Dashboard/Navigation/types.ts +8 -0
- package/app/components/Va/Dashboard/VaDashboardKPICard.vue +31 -0
- package/app/components/Va/Dashboard/VaDashboardNavigation.vue +50 -0
- package/app/components/Va/Dashboard/VaDashboardPricePlan.vue +102 -0
- package/app/components/Va/Dashboard/VaDashboardUsageChart.vue +84 -0
- package/app/components/Va/Dashboard/VaDashboardUsageRequestChart.vue +46 -0
- package/app/components/Va/Layout/NotificationsSlideover.vue +169 -0
- package/app/components/Va/Layout/SideNav/types.ts +5 -0
- package/app/components/Va/Layout/SideNav.vue +108 -0
- package/app/components/Va/Layout/TeamsMenu.vue +57 -0
- package/app/components/Va/Layout/UserMenu.vue +57 -0
- package/app/composables/useDashboard.ts +25 -0
- package/app/composables/useVAAnimation.ts +324 -0
- package/app/composables/useVAUtils.ts +118 -0
- package/app/composables/useVCrud.ts +647 -0
- package/app/composables/useVFetch.ts +46 -0
- package/app/composables/useVFileUpload.ts +45 -0
- package/app/composables/useVToast.ts +73 -0
- package/app/composables/useXATableColumns.ts +456 -0
- package/app/data/BillingStats.ts +65 -0
- package/app/data/SearchData.ts +58 -0
- package/app/data/TasksData.ts +101 -0
- package/app/data/dashboardData.ts +113 -0
- package/app/layouts/default.vue +171 -0
- package/app/layouts/legacy.vue +61 -0
- package/app/pages/playground/base.vue +498 -0
- package/app/pages/playground/blocks.vue +108 -0
- package/app/pages/playground/buttons.vue +237 -0
- package/app/pages/playground/cards.vue +326 -0
- package/app/pages/playground/charts.vue +338 -0
- package/app/pages/playground/dashboard.vue +315 -0
- package/app/pages/playground/formatters.vue +329 -0
- package/app/pages/playground/index.vue +109 -0
- package/app/pages/playground/layout.vue +159 -0
- package/app/pages/playground/modals.vue +606 -0
- package/app/pages/playground/states.vue +282 -0
- package/app/pages/playground/tables.vue +618 -0
- package/app/pages/test-layout.vue +10 -0
- package/nuxt.config.ts +12 -0
- package/package.json +71 -0
- 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,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)
|