@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,114 @@
1
+ <script lang="ts" setup>
2
+ import {
3
+ getDailyWebsiteStatistics,
4
+ getWeeklyWebsiteStatistics,
5
+ getMonthlyWebsiteStatistics
6
+ } from '~/data/WebsiteStatistics'
7
+
8
+ defineProps<{
9
+ categories: Record<string, BulletLegendItemInterface>
10
+ }>()
11
+
12
+ const isSettingsOpen = ref<boolean>(false)
13
+
14
+ function toggleSettings() {
15
+ isSettingsOpen.value = !isSettingsOpen.value
16
+ }
17
+
18
+ const dataViewOptions = [
19
+ {
20
+ label: 'Daily',
21
+ value: 'daily'
22
+ },
23
+ {
24
+ label: 'Weekly',
25
+ value: 'weekly'
26
+ },
27
+ {
28
+ label: 'Monthly',
29
+ value: 'monthly'
30
+ }
31
+ ]
32
+
33
+ const selectedDataView = ref('daily')
34
+
35
+ const WebsiteStatistics = computed(() => {
36
+ switch (selectedDataView.value) {
37
+ case 'weekly':
38
+ return getWeeklyWebsiteStatistics.value
39
+ case 'monthly':
40
+ return getMonthlyWebsiteStatistics.value
41
+ case 'daily':
42
+ default:
43
+ return getDailyWebsiteStatistics.value
44
+ }
45
+ })
46
+
47
+ const xFormatter = (i: number): string =>
48
+ String(WebsiteStatistics.value[i]?.date ?? '')
49
+
50
+ function handleDataViewChange(value: string) {
51
+ console.log('Data view changed to:', value)
52
+ }
53
+ </script>
54
+
55
+ <template>
56
+ <UCard
57
+ :ui="{
58
+ body: 'sm:p-4 p-0'
59
+ }"
60
+ >
61
+ <template #header>
62
+ <div class="w-full flex items-center justify-between">
63
+ <h2 class="text-lg font-medium text-highlighted">
64
+ Web Performance
65
+ </h2>
66
+ <UButton
67
+ variant="ghost"
68
+ :icon="`i-lucide-${!isSettingsOpen ? 'settings' : 'x'}`"
69
+ @click="toggleSettings"
70
+ />
71
+ </div>
72
+ </template>
73
+
74
+ <div
75
+ v-if="isSettingsOpen"
76
+ class="h-[304px] p-4"
77
+ >
78
+ <div class="w-full space-y-4">
79
+ <label class="block text-sm font-medium text-highlighted mb-2">
80
+ Settings
81
+ </label>
82
+ <USelect
83
+ v-model="selectedDataView"
84
+ :items="dataViewOptions"
85
+ placeholder="Select data view"
86
+ class="w-full focus:ring-0"
87
+ @update:model-value="handleDataViewChange"
88
+ />
89
+ <UButton
90
+ variant="soft"
91
+ class="w-full flex items-center justify-center"
92
+ label="Filter data"
93
+ @click="toggleSettings"
94
+ />
95
+ </div>
96
+ </div>
97
+
98
+ <div
99
+ v-if="!isSettingsOpen"
100
+ class="pt-2"
101
+ >
102
+ <LineChart
103
+ :data="WebsiteStatistics"
104
+ :height="240"
105
+ y-label="Performance"
106
+ :x-num-ticks="6"
107
+ :categories="categories"
108
+ :x-formatter="xFormatter"
109
+ :legend-position="LegendPosition.TopRight"
110
+ :curve-type="CurveType.MonotoneX"
111
+ />
112
+ </div>
113
+ </UCard>
114
+ </template>
@@ -0,0 +1,110 @@
1
+ <script lang="ts" setup>
2
+ import type { TabsItem } from '@nuxt/ui'
3
+
4
+ interface DailyData {
5
+ day: string
6
+ Income: number
7
+ Expense: number
8
+ }
9
+
10
+ const items = ref<TabsItem[]>([
11
+ {
12
+ label: 'Year'
13
+ },
14
+ {
15
+ label: 'Month'
16
+ },
17
+ {
18
+ label: 'Week'
19
+ },
20
+ {
21
+ label: 'Day'
22
+ }
23
+ ])
24
+
25
+ const chartData = ref<DailyData[]>([
26
+ {
27
+ day: 'Jan',
28
+ Income: 8500,
29
+ Expense: 4000
30
+ },
31
+ {
32
+ day: 'Feb',
33
+ Income: 6500,
34
+ Expense: 9000
35
+ },
36
+ {
37
+ day: 'Mar',
38
+ Income: 7500,
39
+ Expense: 3500
40
+ },
41
+ {
42
+ day: 'Apr',
43
+ Income: 5800,
44
+ Expense: 7000
45
+ },
46
+ {
47
+ day: 'May',
48
+ Income: 9200,
49
+ Expense: 6500
50
+ },
51
+ {
52
+ day: 'Jun',
53
+ Income: 9800,
54
+ Expense: 4500
55
+ },
56
+ {
57
+ day: 'Jul',
58
+ Income: 5000,
59
+ Expense: 8500
60
+ }
61
+ ])
62
+
63
+ const chartCategories: Record<string, BulletLegendItemInterface> = {
64
+ Income: {
65
+ name: 'Income',
66
+ color: '#10b981'
67
+ },
68
+ Expense: {
69
+ name: 'Expense',
70
+ color: '#f59e0b'
71
+ }
72
+ }
73
+
74
+ const xAxisFormatter = (tick: number): string => {
75
+ // tick is the index of the data point
76
+ const months = chartData.value.map(item => item.day)
77
+ return months[tick] || ''
78
+ }
79
+ </script>
80
+
81
+ <template>
82
+ <div class="space-y-8">
83
+ <BarChart
84
+ :data="chartData"
85
+ :categories="chartCategories"
86
+ :y-axis="['Income', 'Expense']"
87
+ :x-formatter="xAxisFormatter"
88
+ :height="160"
89
+ :stacked="false"
90
+ :radius="3"
91
+ :group-padding="0.2"
92
+ :bar-padding="0.2"
93
+ :hide-legend="true"
94
+ :x-grid-line="true"
95
+ :y-grid-line="true"
96
+ :y-num-ticks="4"
97
+ />
98
+ <UTabs
99
+ :content="false"
100
+ :items="items"
101
+ color="neutral"
102
+ class="w-full"
103
+ />
104
+ <p class="text-center text-dimmed px-16">
105
+ Your win % is higher on <span class="text-primary">8%</span> compared to
106
+ <span class="text-primary">47 winning</span> / <span>33 losing</span> past
107
+ month
108
+ </p>
109
+ </div>
110
+ </template>
@@ -0,0 +1,107 @@
1
+ <script lang="ts" setup>
2
+ import type { TabsItem } from '@nuxt/ui'
3
+
4
+ const props = defineProps<{
5
+ hideText?: boolean
6
+ }>()
7
+
8
+ const items = ref<TabsItem[]>([
9
+ {
10
+ label: 'Year'
11
+ },
12
+ {
13
+ label: 'Month'
14
+ },
15
+ {
16
+ label: 'Week'
17
+ },
18
+ {
19
+ label: 'Day'
20
+ }
21
+ ])
22
+
23
+ const win = ref(32)
24
+ const loss = ref(18)
25
+
26
+ const winLossData = computed(() => [
27
+ {
28
+ color: '#10b981',
29
+ name: 'Winning',
30
+ value: win.value
31
+ },
32
+ {
33
+ color: '#ef4444',
34
+ name: 'Lost',
35
+ value: loss.value
36
+ }
37
+ ])
38
+
39
+ const percentage = computed(() => {
40
+ const total = win.value + loss.value
41
+ if (total === 0) return '0%'
42
+ return Math.round((win.value / total) * 100)
43
+ })
44
+ </script>
45
+
46
+ <template>
47
+ <UCard>
48
+ <div class="space-y-8">
49
+ <h2 class="text-lg font-semibold">
50
+ Spending Trend
51
+ </h2>
52
+
53
+ <Flex class="gap-8 space-y-4">
54
+ <div class="relative w-46">
55
+ <DonutChart
56
+ :data="winLossData.map((i) => i.value)"
57
+ :height="160"
58
+ :arc-width="10"
59
+ :labels="
60
+ winLossData.map((item) => ({
61
+ name: item.name,
62
+ color: item.color,
63
+ percentage: item.value
64
+ }))
65
+ "
66
+ :pad-angle="0.1"
67
+ :hide-legend="true"
68
+ :radius="4"
69
+ >
70
+ <div
71
+ class="absolute text-center w-full top-1/2 left-1/2"
72
+ style="transform: translate(-50%, -50%)"
73
+ >
74
+ <div class="text-2xl font-bold">
75
+ {{ percentage }}<span class="text-toned">%</span>
76
+ </div>
77
+ </div>
78
+ </DonutChart>
79
+ </div>
80
+ <FlexCol class="ml-6 h-[160px] w-[120px]">
81
+ <FlexCol class="h-1/2 space-y-2">
82
+ <div class="text-muted">
83
+ Winning trades
84
+ </div>
85
+ <div class="text-3xl font-bold">
86
+ {{ winLossData[0]?.value ?? 0 }}
87
+ </div>
88
+ </FlexCol>
89
+ <FlexCol class="h-1/2 space-y-2">
90
+ <div class="text-muted">
91
+ Losing trades
92
+ </div>
93
+ <div class="text-3xl font-bold">
94
+ {{ winLossData[1]?.value ?? 0 }}
95
+ </div>
96
+ </FlexCol>
97
+ </FlexCol>
98
+ </Flex>
99
+ <UTabs
100
+ :content="false"
101
+ :items="items"
102
+ color="neutral"
103
+ class="w-full"
104
+ />
105
+ </div>
106
+ </UCard>
107
+ </template>
@@ -0,0 +1,111 @@
1
+ <script lang="ts" setup>
2
+ import type { TabsItem } from '@nuxt/ui'
3
+
4
+ interface DailyData {
5
+ day: string
6
+ Income: number
7
+ Expense: number
8
+ }
9
+
10
+ const items = ref<TabsItem[]>([
11
+ {
12
+ label: 'Year'
13
+ },
14
+ {
15
+ label: 'Month'
16
+ },
17
+ {
18
+ label: 'Week'
19
+ },
20
+ {
21
+ label: 'Day'
22
+ }
23
+ ])
24
+
25
+ const chartData = ref<DailyData[]>([
26
+ {
27
+ day: 'Jan',
28
+ Income: 8500,
29
+ Expense: 4000
30
+ },
31
+ {
32
+ day: 'Feb',
33
+ Income: 6500,
34
+ Expense: 9000
35
+ },
36
+ {
37
+ day: 'Mar',
38
+ Income: 7500,
39
+ Expense: 3500
40
+ },
41
+ {
42
+ day: 'Apr',
43
+ Income: 5800,
44
+ Expense: 7000
45
+ },
46
+ {
47
+ day: 'May',
48
+ Income: 9200,
49
+ Expense: 6500
50
+ },
51
+ {
52
+ day: 'Jun',
53
+ Income: 9800,
54
+ Expense: 4500
55
+ },
56
+ {
57
+ day: 'Jul',
58
+ Income: 5000,
59
+ Expense: 8500
60
+ }
61
+ ])
62
+
63
+ const chartCategories: Record<string, BulletLegendItemInterface> = {
64
+ Income: {
65
+ name: 'Income',
66
+ color: '#10b981'
67
+ },
68
+ Expense: {
69
+ name: 'Expense',
70
+ color: '#f59e0b'
71
+ }
72
+ }
73
+
74
+ const xAxisFormatter = (tick: number): string => {
75
+ // tick is the index of the data point
76
+ const months = chartData.value.map(item => item.day)
77
+ return months[tick] || ''
78
+ }
79
+ </script>
80
+
81
+ <template>
82
+ <div class="space-y-8">
83
+ <LineChart
84
+ :data="chartData"
85
+ :categories="chartCategories"
86
+ :y-axis="['Income', 'Expense']"
87
+ :x-formatter="xAxisFormatter"
88
+ :height="160"
89
+ :stacked="false"
90
+ :radius="3"
91
+ :group-padding="0.2"
92
+ :bar-padding="0.2"
93
+ :hide-legend="true"
94
+ :x-grid-line="true"
95
+ :y-grid-line="true"
96
+ :y-num-ticks="4"
97
+ />
98
+ <UTabs
99
+ :content="false"
100
+ :items="items"
101
+ color="neutral"
102
+ class="w-full"
103
+ />
104
+
105
+ <p class="text-center text-dimmed px-16">
106
+ Your win % is higher on <span class="text-primary">8%</span> compared to
107
+ <span class="text-primary">47 winning</span> / <span>33 losing</span> past
108
+ month
109
+ </p>
110
+ </div>
111
+ </template>
@@ -0,0 +1,10 @@
1
+ export interface SpendingData {
2
+ month: number
3
+ amount: number
4
+ previous: number
5
+ }
6
+
7
+ export interface BulletLegendItemInterface {
8
+ name: string
9
+ color: string
10
+ }
@@ -0,0 +1,8 @@
1
+ export interface NavSection {
2
+ label: string
3
+ children: Array<{
4
+ label: string
5
+ href: string
6
+ leadingIcon?: string
7
+ }>
8
+ }
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+ export interface KPIStat {
3
+ label: string
4
+ value: string | number
5
+ change?: string | number
6
+ changeType?: 'positive' | 'negative' | 'neutral'
7
+ }
8
+
9
+ defineProps<KPIStat>()
10
+ </script>
11
+
12
+ <template>
13
+ <UCard :key="label">
14
+ <div class="space-y-2">
15
+ <div class="flex items-center justify-between">
16
+ <h2 class="text-xl font-semibold">
17
+ {{ value }}
18
+ </h2>
19
+ <UBadge
20
+ color="neutral"
21
+ size="lg"
22
+ >
23
+ {{ change }}
24
+ </UBadge>
25
+ </div>
26
+ <p class="text-muted">
27
+ {{ label }}
28
+ </p>
29
+ </div>
30
+ </UCard>
31
+ </template>
@@ -0,0 +1,50 @@
1
+ <script setup lang="ts">
2
+ import { useRoute, useRouter } from '#imports'
3
+ import type { NavSection } from './Navigation/types'
4
+
5
+ const props = defineProps<{
6
+ sections: NavSection[]
7
+ }>()
8
+
9
+ const route = useRoute()
10
+ const router = useRouter()
11
+
12
+ const items = computed(() =>
13
+ props.sections
14
+ .flatMap(section => section.children)
15
+ .map(child => ({
16
+ label: child.label,
17
+ icon: child.leadingIcon,
18
+ value: child.href
19
+ }))
20
+ )
21
+
22
+ const active = ref<string | undefined>(items.value[0]?.value)
23
+
24
+ watch(
25
+ () => [route.path, route.hash, items.value],
26
+ () => {
27
+ const match = items.value.find(item => item.value === route.path || item.value === route.hash)
28
+ active.value = match ? match.value : items.value[0]?.value
29
+ },
30
+ { immediate: true }
31
+ )
32
+
33
+ watch(
34
+ active,
35
+ (val) => {
36
+ if (val && val !== route.path) {
37
+ router.push(val)
38
+ }
39
+ }
40
+ )
41
+ </script>
42
+
43
+ <template>
44
+ <UTabs
45
+ v-model="active"
46
+ :items="items"
47
+ color="neutral"
48
+ :content="false"
49
+ />
50
+ </template>
@@ -0,0 +1,102 @@
1
+ <script setup lang="ts">
2
+ interface Feature {
3
+ label: string
4
+ available: boolean
5
+ }
6
+
7
+ export interface PricePlanProps {
8
+ name: string
9
+ description: string
10
+ price: string
11
+ priceSuffix?: string
12
+ per?: string
13
+ features: Feature[]
14
+ current?: boolean
15
+ badge?: string
16
+ buttonLabel?: string
17
+ }
18
+
19
+ defineProps<PricePlanProps>()
20
+ </script>
21
+
22
+ <template>
23
+ <UCard
24
+ :class="[
25
+ 'relative transition-all duration-200',
26
+ current
27
+ ? 'ring-2 ring-success bg-success/5'
28
+ : 'hover:shadow-lg hover:-translate-y-1'
29
+ ]"
30
+ >
31
+ <div
32
+ v-if="badge"
33
+ class="absolute -top-3 left-1/2 -translate-x-1/2"
34
+ >
35
+ <UBadge
36
+ :label="badge"
37
+ variant="solid"
38
+ color="success"
39
+ size="sm"
40
+ />
41
+ </div>
42
+
43
+ <div class="text-center">
44
+ <div>
45
+ <h3 class="text-xl font-bold text-highlighted">
46
+ {{ name }}
47
+ </h3>
48
+ <p class="text-dimmed mt-1">
49
+ {{ description }}
50
+ </p>
51
+ </div>
52
+
53
+ <div class="border-y border-default my-4 py-4">
54
+ <div class="mb-4">
55
+ <div class="text-3xl font-bold text-success space-x-2">
56
+ <span>{{ price }}</span>
57
+ <span
58
+ v-if="priceSuffix"
59
+ class="text-base text-toned"
60
+ >{{
61
+ priceSuffix
62
+ }}</span>
63
+ </div>
64
+ <div
65
+ v-if="per"
66
+ class="text-sm text-muted"
67
+ >
68
+ {{ per }}
69
+ </div>
70
+ </div>
71
+
72
+ <UButton
73
+ :label="buttonLabel"
74
+ :variant="current ? 'outline' : 'solid'"
75
+ :color="current ? 'success' : 'success'"
76
+ class="w-full justify-center"
77
+ :disabled="current"
78
+ :icon="current ? 'i-lucide-check' : 'i-lucide-arrow-up-right'"
79
+ />
80
+ </div>
81
+
82
+ <div class="space-y-4 mt-8">
83
+ <div
84
+ v-for="(feature, i) in features"
85
+ :key="i"
86
+ class="flex items-center justify-start gap-2 text-sm"
87
+ >
88
+ <UIcon
89
+ :name="feature.available ? 'i-lucide-check' : 'i-lucide-x'"
90
+ :class="feature.available ? 'text-success' : 'text-error'"
91
+ size="16"
92
+ />
93
+ <span
94
+ :class="{
95
+ 'text-dimmed': !feature.available
96
+ }"
97
+ >{{ feature.label }}</span>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </UCard>
102
+ </template>