@veristone/nuxt-v-app 0.2.10 → 0.2.14

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 (28) hide show
  1. package/app/components/V/A/Card/DonutChart.vue +15 -3
  2. package/app/components/V/A/Card/RevenueBarChart.vue +1 -8
  3. package/app/components/V/A/Chart/AppPerformanceBar.vue +19 -7
  4. package/app/components/V/A/Chart/AppPerformanceBarChart.vue +19 -7
  5. package/app/components/V/A/Chart/AreaMini.vue +1 -2
  6. package/app/components/V/A/Chart/BarMini.vue +0 -1
  7. package/app/components/V/A/Chart/ColorBarChart.vue +8 -5
  8. package/app/components/V/A/Chart/ExpensesBar.vue +1 -1
  9. package/app/components/V/A/Chart/FinanceSummary.vue +14 -7
  10. package/app/components/V/A/Chart/GoogleSearchConsole.vue +0 -1
  11. package/app/components/V/A/Chart/Legend.vue +15 -7
  12. package/app/components/V/A/Chart/Revenue.vue +2 -2
  13. package/app/components/V/A/Chart/RevenueLine.vue +3 -3
  14. package/app/components/V/A/Chart/RevenuevsCost.vue +0 -5
  15. package/app/components/V/A/Chart/SearchIntent.vue +1 -1
  16. package/app/components/V/A/Chart/SpendingTrend.vue +0 -1
  17. package/app/components/V/A/Chart/StockComparisonLine.vue +0 -4
  18. package/app/components/V/A/Chart/StocksPortfolioLine.vue +0 -4
  19. package/app/components/V/A/Chart/StocksSectorLine.vue +0 -4
  20. package/app/components/V/A/Chart/TrafficOverview.vue +2 -3
  21. package/app/components/V/A/Chart/WebPerformanceLineChart.vue +19 -7
  22. package/app/components/V/A/Chart/WinLostDonut.vue +15 -8
  23. package/app/components/V/A/Chart/WinLostLine.vue +0 -5
  24. package/app/components/V/A/Table/index.vue +63 -70
  25. package/app/composables/useResponsiveHeight.ts +44 -0
  26. package/app/data/AppPerformance.ts +107 -0
  27. package/app/data/WebsiteStatistics.ts +122 -0
  28. package/package.json +1 -1
@@ -5,11 +5,23 @@ interface DonutChartData {
5
5
  color: string;
6
6
  }
7
7
 
8
- defineProps<{
8
+ const props = defineProps<{
9
9
  data: DonutChartData[];
10
10
  title: string;
11
11
  totalValue: string;
12
12
  }>();
13
+
14
+ const categories = computed<Record<string, BulletLegendItemInterface>>(() =>
15
+ Object.fromEntries(
16
+ props.data.map((item) => [
17
+ item.name,
18
+ {
19
+ name: item.name,
20
+ color: item.color,
21
+ },
22
+ ])
23
+ )
24
+ );
13
25
  </script>
14
26
  <template>
15
27
  <UCard>
@@ -24,9 +36,9 @@ defineProps<{
24
36
  <DonutChart
25
37
  :data="data.map((i) => i.value)"
26
38
  :height="200"
27
- :labels="data"
39
+ :categories="categories"
28
40
  :hide-legend="true"
29
- :radius="0"
41
+ :radius="4"
30
42
  >
31
43
  <div class="absolute text-center">
32
44
  <div class="font-semibold">Categories</div>
@@ -1,11 +1,4 @@
1
1
  <script setup lang="ts">
2
- const LegendPosition = {
3
- Top: "top",
4
- Bottom: "bottom",
5
- Left: "left",
6
- Right: "right",
7
- };
8
-
9
2
  interface BarChartCategory {
10
3
  name: string;
11
4
  color: string;
@@ -41,7 +34,7 @@ defineProps<{
41
34
  :radius="4"
42
35
  :x-formatter="xFormatter"
43
36
  :y-formatter="yFormatter"
44
- :legend-position="LegendPosition.Bottom"
37
+ :legend-position="LegendPosition.BottomCenter"
45
38
  :hide-legend="false"
46
39
  :y-grid-line="false"
47
40
  />
@@ -5,10 +5,21 @@ import {
5
5
  getMonthlyPerformanceData
6
6
  } from '~/data/AppPerformance'
7
7
 
8
- defineProps<{
9
- categories: Record<string, BulletLegendItemInterface>
8
+ const props = defineProps<{
9
+ categories?: Record<string, BulletLegendItemInterface>
10
10
  }>()
11
11
 
12
+ const defaultCategories: Record<string, BulletLegendItemInterface> = {
13
+ subscriptions: {
14
+ name: 'Subscriptions',
15
+ color: 'var(--color-orange-400)'
16
+ },
17
+ downloads: {
18
+ name: 'Downloads',
19
+ color: 'var(--color-lime-400)'
20
+ }
21
+ }
22
+
12
23
  const isSettingsOpen = ref<boolean>(false)
13
24
 
14
25
  function toggleSettings() {
@@ -32,6 +43,12 @@ const dataViewOptions = [
32
43
 
33
44
  const selectedDataView = ref('daily')
34
45
 
46
+ const categories = computed(() =>
47
+ props.categories && Object.keys(props.categories).length > 0
48
+ ? props.categories
49
+ : defaultCategories
50
+ )
51
+
35
52
  const performanceData = computed(() => {
36
53
  switch (selectedDataView.value) {
37
54
  case 'weekly':
@@ -46,10 +63,6 @@ const performanceData = computed(() => {
46
63
 
47
64
  const xFormatter = (i: number): string =>
48
65
  String(performanceData.value[i]?.date ?? '')
49
-
50
- function handleDataViewChange(value: string) {
51
- console.log('Data view changed to:', value)
52
- }
53
66
  </script>
54
67
 
55
68
  <template>
@@ -84,7 +97,6 @@ function handleDataViewChange(value: string) {
84
97
  :items="dataViewOptions"
85
98
  placeholder="Select data view"
86
99
  class="w-full focus:ring-0"
87
- @update:model-value="handleDataViewChange"
88
100
  />
89
101
  <UButton
90
102
  variant="soft"
@@ -5,10 +5,21 @@ import {
5
5
  getMonthlyPerformanceData
6
6
  } from '~/data/AppPerformance'
7
7
 
8
- defineProps<{
9
- categories: Record<string, BulletLegendItemInterface>
8
+ const props = defineProps<{
9
+ categories?: Record<string, BulletLegendItemInterface>
10
10
  }>()
11
11
 
12
+ const defaultCategories: Record<string, BulletLegendItemInterface> = {
13
+ subscriptions: {
14
+ name: 'Subscriptions',
15
+ color: 'var(--color-orange-400)'
16
+ },
17
+ downloads: {
18
+ name: 'Downloads',
19
+ color: 'var(--color-lime-400)'
20
+ }
21
+ }
22
+
12
23
  const isSettingsOpen = ref<boolean>(false)
13
24
 
14
25
  function toggleSettings() {
@@ -32,6 +43,12 @@ const dataViewOptions = [
32
43
 
33
44
  const selectedDataView = ref('daily')
34
45
 
46
+ const categories = computed(() =>
47
+ props.categories && Object.keys(props.categories).length > 0
48
+ ? props.categories
49
+ : defaultCategories
50
+ )
51
+
35
52
  const performanceData = computed(() => {
36
53
  switch (selectedDataView.value) {
37
54
  case 'weekly':
@@ -46,10 +63,6 @@ const performanceData = computed(() => {
46
63
 
47
64
  const xFormatter = (i: number): string =>
48
65
  String(performanceData.value[i]?.date ?? '')
49
-
50
- function handleDataViewChange(value: string) {
51
- console.log('Data view changed to:', value)
52
- }
53
66
  </script>
54
67
 
55
68
  <template>
@@ -84,7 +97,6 @@ function handleDataViewChange(value: string) {
84
97
  :items="dataViewOptions"
85
98
  placeholder="Select data view"
86
99
  class="w-full focus:ring-0"
87
- @update:model-value="handleDataViewChange"
88
100
  />
89
101
  <UButton
90
102
  variant="soft"
@@ -91,10 +91,9 @@ const formatCurrency = (value: number) => {
91
91
  :data="chartData"
92
92
  :height="90"
93
93
  :categories="categories"
94
- :y-axis="['amount']"
95
94
  :y-num-ticks="2"
96
95
  :y-grid-line="true"
97
- :legend-position="LegendPosition.Bottom"
96
+ :legend-position="LegendPosition.BottomCenter"
98
97
  :hide-y-axis="true"
99
98
  :hide-x-axis="true"
100
99
  :hide-legend="true"
@@ -56,7 +56,6 @@ function formatDate(dateString: Date) {
56
56
  :height="80"
57
57
  :categories="categories"
58
58
  :y-axis="['amount']"
59
- :curve-type="CurveType.Basis"
60
59
  :hide-y-axis="true"
61
60
  :x-formatter="(i) => `${chartData[i]?.month}`"
62
61
  :min-max-ticks-only="false"
@@ -1,8 +1,11 @@
1
1
  <script lang="ts" setup>
2
- const props = defineProps<{
3
- title: string
2
+ const props = withDefaults(defineProps<{
3
+ title?: string
4
4
  color?: string
5
- }>()
5
+ }>(), {
6
+ title: 'Performance Metrics',
7
+ color: 'var(--ui-primary)'
8
+ })
6
9
 
7
10
  const RevenueData = [
8
11
  {
@@ -37,12 +40,12 @@ const RevenueData = [
37
40
  }
38
41
  ]
39
42
 
40
- const RevenueCategories = {
43
+ const RevenueCategories = computed(() => ({
41
44
  desktop: {
42
45
  name: 'Desktop',
43
46
  color: props.color
44
47
  }
45
- }
48
+ }))
46
49
 
47
50
  const xFormatter = (i: number): string => `${RevenueData[i]?.month}`
48
51
  const yFormatter = (i: number) => i.toString()
@@ -44,7 +44,7 @@ const yFormatter = (value: number): string => `$${(value / 1000).toFixed(1)}k`;
44
44
  :y-axis="['sales' ,'counter']"
45
45
  :x-formatter="xFormatter"
46
46
  :y-formatter="yFormatter"
47
- :legend-position="LegendPosition.Top"
47
+ :legend-position="LegendPosition.TopCenter"
48
48
  :hide-legend="false"
49
49
  :y-grid-line="true"
50
50
  :x-grid-line="false"
@@ -28,6 +28,19 @@ const spendingData = ref<SpendingCategory[]>([
28
28
  },
29
29
  ]);
30
30
 
31
+ const spendingCategories = computed<Record<string, BulletLegendItemInterface>>(
32
+ () =>
33
+ Object.fromEntries(
34
+ spendingData.value.map((item) => [
35
+ item.name,
36
+ {
37
+ name: item.name,
38
+ color: item.color,
39
+ },
40
+ ])
41
+ )
42
+ );
43
+
31
44
  const totalSpent = computed(() =>
32
45
  spendingData.value.reduce((sum, item) => sum + item.value, 0)
33
46
  );
@@ -49,13 +62,7 @@ const formatCurrency = (amount: number) => {
49
62
  :data="spendingData.map((i) => i.value)"
50
63
  :height="200"
51
64
  :arc-width="10"
52
- :labels="
53
- spendingData.map((item) => ({
54
- name: item.name,
55
- color: item.color,
56
- percentage: item.value,
57
- }))
58
- "
65
+ :categories="spendingCategories"
59
66
  :pad-angle="0.1"
60
67
  :hide-legend="true"
61
68
  :radius="4"
@@ -83,7 +83,6 @@ const xFormatter = (tick: number, _i?: number, _ticks?: number[]): string => {
83
83
  :group-padding="0.2"
84
84
  :bar-padding="0.5"
85
85
  :y-axis="['clicks', 'impressions', 'averagePosition', 'ctr']"
86
- :curve-type="CurveType.Linear"
87
86
  :legend-position="LegendPosition.BottomCenter"
88
87
  />
89
88
  </UCard>
@@ -1,24 +1,32 @@
1
1
  <script lang="ts" setup>
2
- import type { ChartCategories } from '~/types'
2
+ type LegendItem = Pick<BulletLegendItemInterface, 'name' | 'color'>
3
3
 
4
- defineProps<{
5
- categories: ChartCategories
4
+ const props = defineProps<{
5
+ categories?: Record<string, BulletLegendItemInterface> | null
6
+ items?: LegendItem[] | null
6
7
  }>()
8
+
9
+ const legendEntries = computed(() =>
10
+ props.items?.length ? props.items : Object.values(props.categories ?? {})
11
+ )
12
+
13
+ const getColor = (color?: string | string[]) =>
14
+ Array.isArray(color) ? color[0] ?? 'currentColor' : color ?? 'currentColor'
7
15
  </script>
8
16
 
9
17
  <template>
10
18
  <div class="flex flex-wrap items-center justify-end gap-x-6 gap-y-2">
11
19
  <div
12
- v-for="(category, categoryKey) in categories"
13
- :key="categoryKey"
20
+ v-for="(item, index) in legendEntries"
21
+ :key="`${item.name}-${index}`"
14
22
  class="flex items-center gap-2"
15
23
  >
16
24
  <span
17
25
  class="h-2 w-2 rounded-full"
18
- :style="{ backgroundColor: category.color }"
26
+ :style="{ backgroundColor: getColor(item.color) }"
19
27
  />
20
28
  <span class="text-xs text-(--vis-legend-label-color)">
21
- {{ category.name }}
29
+ {{ item.name }}
22
30
  </span>
23
31
  </div>
24
32
  </div>
@@ -36,8 +36,8 @@ const yFormatter = (tick: number, _i?: number, _ticks?: number[]) =>
36
36
  :y-num-ticks="4"
37
37
  :x-formatter="xFormatter"
38
38
  :y-formatter="yFormatter"
39
- :legend-position="LegendPosition.Top"
39
+ :legend-position="LegendPosition.TopCenter"
40
40
  :hide-legend="true"
41
41
  :y-grid-line="true"
42
42
  />
43
- </template>
43
+ </template>
@@ -29,14 +29,14 @@ const yFormatter = (value: number): string => `$${(value / 1000).toFixed(1)}k`;
29
29
  :data="chartData"
30
30
  :height="180"
31
31
  :x-num-ticks="4"
32
- :y-num-ticks="3.5"
32
+ :y-num-ticks="4"
33
33
  :categories="categories"
34
34
  :x-formatter="xFormatter"
35
35
  :y-formatter="yFormatter"
36
36
  :curve-type="CurveType.Linear"
37
- :legend-position="LegendPosition.Top"
37
+ :legend-position="LegendPosition.TopCenter"
38
38
  :y-grid-line="true"
39
39
  :hide-x-axis="true"
40
40
  :hide-legend="true"
41
41
  />
42
- </template>
42
+ </template>
@@ -70,13 +70,8 @@ const { height } = useResponsiveHeight({
70
70
  <LineChart
71
71
  :data="chartData"
72
72
  :categories="chartCategories"
73
- :y-axis="['Income', 'Expense']"
74
73
  :x-formatter="xAxisFormatter"
75
74
  :height="height"
76
- :stacked="false"
77
- :radius="3"
78
- :group-padding="0.2"
79
- :bar-padding="0.2"
80
75
  :x-grid-line="true"
81
76
  :y-grid-line="true"
82
77
  :y-num-ticks="4"
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import type { TabsItem } from '#ui/types'
2
+ import type { TabsItem } from '@nuxt/ui'
3
3
 
4
4
  const tabItems: TabsItem[] = [
5
5
  {
@@ -105,7 +105,6 @@ const formatCurrency = (value: number) => {
105
105
  :data="chartData"
106
106
  :height="240"
107
107
  :categories="categories"
108
- :y-axis="['amount']"
109
108
  :y-num-ticks="3"
110
109
  :y-grid-line="true"
111
110
  :curve-type="CurveType.MonotoneX"
@@ -70,12 +70,8 @@ const xAxisFormatter = (tick: number): string => {
70
70
  <LineChart
71
71
  :data="chartData"
72
72
  :categories="chartCategories"
73
- :y-axis="['stockA', 'stockB', 'stockC']"
74
73
  :x-formatter="xAxisFormatter"
75
74
  :height="220"
76
- :radius="3"
77
- :group-padding="0.2"
78
- :bar-padding="0.2"
79
75
  :x-grid-line="true"
80
76
  :y-grid-line="true"
81
77
  :x-domain-line="true"
@@ -145,13 +145,9 @@ const xAxisFormatter = (tick: number): string => {
145
145
  <LineChart
146
146
  :data="chartData"
147
147
  :categories="chartCategories"
148
- :y-axis="['portfolioValue']"
149
148
  :x-formatter="xAxisFormatter"
150
149
  :y-formatter="(i: number) => i / 1000 + 'K'"
151
150
  :height="220"
152
- :radius="3"
153
- :group-padding="0.2"
154
- :bar-padding="0.2"
155
151
  :x-grid-line="true"
156
152
  :y-grid-line="true"
157
153
  :x-domain-line="true"
@@ -207,13 +207,9 @@ const xAxisFormatter = (tick: number): string => {
207
207
  <LineChart
208
208
  :data="chartData"
209
209
  :categories="chartCategories"
210
- :y-axis="['domestic', 'international']"
211
210
  :x-formatter="xAxisFormatter"
212
211
  :y-formatter="(i: number) => i / 1000 + 'K'"
213
212
  :height="220"
214
- :radius="3"
215
- :group-padding="0.2"
216
- :bar-padding="0.2"
217
213
  :x-grid-line="true"
218
214
  :y-grid-line="true"
219
215
  :x-domain-line="true"
@@ -67,7 +67,7 @@ const categories: Record<string, Category> = {
67
67
 
68
68
  const xFormatter = (i: number) => `${engagementData[i]?.date}`
69
69
 
70
- const yFormatter = (value: number) => value + 'K'
70
+ const yFormatter = (value: number) => `${value.toFixed(1)}m`
71
71
  </script>
72
72
 
73
73
  <template>
@@ -95,8 +95,7 @@ const yFormatter = (value: number) => value + 'K'
95
95
  :y-num-ticks="3"
96
96
  :x-formatter="xFormatter"
97
97
  :y-formatter="yFormatter"
98
- :y-min="0"
99
- :y-max="600000"
98
+ :y-domain="[0, 8]"
100
99
  :x-grid-line="true"
101
100
  :legend-position="LegendPosition.BottomCenter"
102
101
  :curve-type="CurveType.Linear"
@@ -5,10 +5,21 @@ import {
5
5
  getMonthlyWebsiteStatistics
6
6
  } from '~/data/WebsiteStatistics'
7
7
 
8
- defineProps<{
9
- categories: Record<string, BulletLegendItemInterface>
8
+ const props = defineProps<{
9
+ categories?: Record<string, BulletLegendItemInterface>
10
10
  }>()
11
11
 
12
+ const defaultCategories: Record<string, BulletLegendItemInterface> = {
13
+ conversions: {
14
+ name: 'Conversions',
15
+ color: 'var(--color-orange-400)'
16
+ },
17
+ bounceRate: {
18
+ name: 'Bounce Rate',
19
+ color: 'var(--color-lime-400)'
20
+ }
21
+ }
22
+
12
23
  const isSettingsOpen = ref<boolean>(false)
13
24
 
14
25
  function toggleSettings() {
@@ -32,6 +43,12 @@ const dataViewOptions = [
32
43
 
33
44
  const selectedDataView = ref('daily')
34
45
 
46
+ const categories = computed(() =>
47
+ props.categories && Object.keys(props.categories).length > 0
48
+ ? props.categories
49
+ : defaultCategories
50
+ )
51
+
35
52
  const WebsiteStatistics = computed(() => {
36
53
  switch (selectedDataView.value) {
37
54
  case 'weekly':
@@ -46,10 +63,6 @@ const WebsiteStatistics = computed(() => {
46
63
 
47
64
  const xFormatter = (i: number): string =>
48
65
  String(WebsiteStatistics.value[i]?.date ?? '')
49
-
50
- function handleDataViewChange(value: string) {
51
- console.log('Data view changed to:', value)
52
- }
53
66
  </script>
54
67
 
55
68
  <template>
@@ -84,7 +97,6 @@ function handleDataViewChange(value: string) {
84
97
  :items="dataViewOptions"
85
98
  placeholder="Select data view"
86
99
  class="w-full focus:ring-0"
87
- @update:model-value="handleDataViewChange"
88
100
  />
89
101
  <UButton
90
102
  variant="soft"
@@ -36,9 +36,22 @@ const winLossData = computed(() => [
36
36
  }
37
37
  ])
38
38
 
39
+ const winLossCategories = computed<Record<string, BulletLegendItemInterface>>(
40
+ () =>
41
+ Object.fromEntries(
42
+ winLossData.value.map(item => [
43
+ item.name,
44
+ {
45
+ name: item.name,
46
+ color: item.color
47
+ }
48
+ ])
49
+ )
50
+ )
51
+
39
52
  const percentage = computed(() => {
40
53
  const total = win.value + loss.value
41
- if (total === 0) return '0%'
54
+ if (total === 0) return 0
42
55
  return Math.round((win.value / total) * 100)
43
56
  })
44
57
  </script>
@@ -56,13 +69,7 @@ const percentage = computed(() => {
56
69
  :data="winLossData.map((i) => i.value)"
57
70
  :height="160"
58
71
  :arc-width="10"
59
- :labels="
60
- winLossData.map((item) => ({
61
- name: item.name,
62
- color: item.color,
63
- percentage: item.value
64
- }))
65
- "
72
+ :categories="winLossCategories"
66
73
  :pad-angle="0.1"
67
74
  :hide-legend="true"
68
75
  :radius="4"
@@ -83,13 +83,8 @@ const xAxisFormatter = (tick: number): string => {
83
83
  <LineChart
84
84
  :data="chartData"
85
85
  :categories="chartCategories"
86
- :y-axis="['Income', 'Expense']"
87
86
  :x-formatter="xAxisFormatter"
88
87
  :height="160"
89
- :stacked="false"
90
- :radius="3"
91
- :group-padding="0.2"
92
- :bar-padding="0.2"
93
88
  :hide-legend="true"
94
89
  :x-grid-line="true"
95
90
  :y-grid-line="true"
@@ -52,11 +52,12 @@
52
52
  <USelect
53
53
  v-for="filter in props.filters"
54
54
  :key="filter.key"
55
- v-model="filterValues[filter.key]"
55
+ :model-value="(filterValues[filter.key] as string)"
56
56
  :items="filter.options"
57
57
  :placeholder="filter.label"
58
58
  size="sm"
59
59
  class="w-40"
60
+ @update:model-value="(v: string) => filterValues[filter.key] = v"
60
61
  />
61
62
  </template>
62
63
 
@@ -166,50 +167,44 @@
166
167
  </template>
167
168
 
168
169
  <script setup lang="ts">
169
- /**
170
- * VATable - Veristone data table component built on Nuxt UI's UTable + TanStack Table.
171
- *
172
- * @props
173
- * name - Optional card header title
174
- * data - Row data array
175
- * columns - TanStack TableColumn definitions; supports `meta.preset` for cell rendering
176
- * loading - Shows skeleton on initial load; shows inline spinner on subsequent refreshes
177
- * initialPageLimit - Default page size (default: 10)
178
- * selectable - Enables row checkboxes; exposes `selectedRows` via template ref
179
- * showSearch - Renders a global search input in the toolbar
180
- * filters - Array of { key, label, options } for toolbar USelect dropdowns
181
- * onRefresh - Async callback; shows a refresh spinner button in the toolbar
182
- * onRowClick - Called with the row's original data when a row is clicked
183
- * defaultSort - Column key to sort by on mount
184
- * defaultSortDesc - Sort descending when defaultSort is set (default: false)
185
- * manualPagination - Enables server-side pagination mode
186
- * total - Total row count for server-side pagination
187
- *
188
- * @models (v-model)
189
- * sorting - SortingState — sync sort state with parent
190
- * filterValues - Record<string, unknown> — sync active filter values with parent
191
- * globalFilter - string sync search input value with parent
192
- * page - number (1-based) — current page in manual pagination mode
193
- * itemsPerPage - number — page size in manual pagination mode
194
- *
195
- * @slots
196
- * header-right - Content rendered on the right side of the card header
197
- * toolbar-left - Custom controls (e.g. filter selects) injected into the toolbar
198
- * next to the search input and built-in filter dropdowns
199
- * Example:
200
- * <template #toolbar-left>
201
- * <USelect v-model="status" :items="opts" placeholder="Status" size="sm" />
202
- * </template>
203
- * bulk-actions - Shown in the toolbar when rows are selected; receives
204
- * { selected, count, clear }
205
- * actions-cell - Renders an "Actions" column; receives { row }
206
- * [column]-cell - Override cell rendering for a specific column by accessor key
207
- *
208
- * @exposes
209
- * selectedRows - Array of selected row originals
210
- * rowSelection - Raw TanStack row selection state
211
- * clearSelection - Clears the current row selection
212
- */
170
+ import type { SortingState } from '@tanstack/vue-table'
171
+ import { getPaginationRowModel } from '@tanstack/vue-table'
172
+ import type { TableColumn, DropdownMenuItem } from '@nuxt/ui'
173
+ import CellRenderer from './CellRenderer.vue'
174
+
175
+ interface FilterOption {
176
+ key: string
177
+ label: string
178
+ options: { label: string; value: string }[]
179
+ }
180
+
181
+ const props = withDefaults(
182
+ defineProps<{
183
+ name?: string
184
+ data?: Record<string, unknown>[]
185
+ columns: TableColumn<any>[]
186
+ loading?: boolean
187
+ initialPageLimit?: number
188
+ selectable?: boolean
189
+ showSearch?: boolean
190
+ filters?: FilterOption[]
191
+ onRefresh?: () => Promise<void>
192
+ onRowClick?: (row: any) => void
193
+ defaultSort?: string
194
+ defaultSortDesc?: boolean
195
+ manualPagination?: boolean
196
+ total?: number
197
+ }>(),
198
+ {
199
+ data: () => [],
200
+ loading: false,
201
+ initialPageLimit: 10,
202
+ selectable: false,
203
+ showSearch: true,
204
+ defaultSortDesc: false,
205
+ manualPagination: false,
206
+ }
207
+ )
213
208
 
214
209
  // Sorting state - v-model support
215
210
  const sorting = defineModel<SortingState>("sorting", {
@@ -397,41 +392,40 @@ const tableColumns = computed(() => {
397
392
  'expiresAt', 'completedAt', 'created_at', 'updated_at'
398
393
  ]
399
394
 
400
- const columns = props.columns.map(col => {
401
- const hasDataAccessor = Boolean(col.accessorKey || (col as any).accessorFn || (col as any).key);
402
- const colDef = {
395
+ const columns: TableColumn<any>[] = props.columns.map((col) => {
396
+ const accessorKey = (col as any).accessorKey as string | undefined
397
+ const hasDataAccessor = Boolean(accessorKey || (col as any).accessorFn || (col as any).key)
398
+ const colDef: TableColumn<any> = {
403
399
  ...col,
404
400
  enableSorting: col.enableSorting ?? hasDataAccessor,
405
- };
401
+ }
406
402
 
407
- // Auto-detect date columns based on accessorKey
408
- const isDateColumn = dateColumnPatterns.includes(col.accessorKey)
403
+ const isDateColumn = accessorKey ? dateColumnPatterns.includes(accessorKey) : false
409
404
  const hasPreset = col.meta?.preset && col.meta.preset !== 'actions'
410
405
 
411
406
  if (hasPreset || isDateColumn) {
412
- // Use existing preset or auto-detected date preset
413
407
  const preset = col.meta?.preset || 'date'
414
408
  const format = col.meta?.format || 'relative'
415
409
 
416
- colDef.cell = ({ row }) => {
417
- const value = row?.original?.[col.accessorKey as string];
410
+ colDef.cell = ({ row }: { row: any }) => {
411
+ const value = accessorKey ? row?.original?.[accessorKey] : undefined
418
412
  return h(CellRenderer, {
419
413
  value,
420
414
  column: { ...col, meta: { ...col.meta, preset, format } },
421
415
  row: row?.original
422
- });
423
- };
416
+ })
417
+ }
424
418
  }
425
419
 
426
- return colDef;
427
- });
420
+ return colDef
421
+ })
428
422
 
429
423
  if (props.selectable) {
430
- const selectColumn: TableColumn<unknown> = {
424
+ const selectColumn: TableColumn<any> = {
431
425
  id: 'select',
432
426
  enableSorting: false,
433
427
  enableHiding: false,
434
- header: ({ table }) =>
428
+ header: ({ table }: { table: any }) =>
435
429
  h(UCheckbox, {
436
430
  modelValue: table.getIsSomePageRowsSelected()
437
431
  ? 'indeterminate'
@@ -440,7 +434,7 @@ const tableColumns = computed(() => {
440
434
  table.toggleAllPageRowsSelected(!!value),
441
435
  'aria-label': 'Select all rows',
442
436
  }),
443
- cell: ({ row }) =>
437
+ cell: ({ row }: { row: any }) =>
444
438
  h(UCheckbox, {
445
439
  modelValue: row.getIsSelected(),
446
440
  'onUpdate:modelValue': (value: boolean | 'indeterminate') =>
@@ -453,21 +447,20 @@ const tableColumns = computed(() => {
453
447
  td: 'w-10',
454
448
  },
455
449
  },
456
- };
450
+ }
457
451
 
458
- columns.unshift(selectColumn);
452
+ columns.unshift(selectColumn)
459
453
  }
460
454
 
461
455
  // Add actions column if slot provided AND no actions column already exists
462
- if (slots['actions-cell'] && !columns.some(c => c.id === 'actions' || c.accessorKey === 'actions')) {
463
- const actionsColumn: TableColumn<unknown> = {
456
+ if (slots['actions-cell'] && !columns.some((c: any) => c.id === 'actions' || c.accessorKey === 'actions')) {
457
+ const actionsColumn: TableColumn<any> = {
464
458
  id: 'actions',
465
459
  header: 'Actions',
466
460
  enableSorting: false,
467
461
  enableHiding: false,
468
- cell: ({ row }) => {
469
- // Render slot content
470
- return h('div', {}, slots['actions-cell']?.({ row }));
462
+ cell: ({ row }: { row: any }) => {
463
+ return h('div', {}, slots['actions-cell']?.({ row }))
471
464
  },
472
465
  meta: {
473
466
  class: {
@@ -475,9 +468,9 @@ const tableColumns = computed(() => {
475
468
  td: 'text-right',
476
469
  },
477
470
  },
478
- };
471
+ }
479
472
 
480
- columns.push(actionsColumn);
473
+ columns.push(actionsColumn)
481
474
  }
482
475
 
483
476
  return columns;
@@ -0,0 +1,44 @@
1
+ import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
2
+ import { computed } from 'vue'
3
+
4
+ interface ResponsiveHeightOptions {
5
+ default: number
6
+ sm?: number
7
+ md?: number
8
+ lg?: number
9
+ xl?: number
10
+ '2xl'?: number
11
+ }
12
+
13
+ export function useResponsiveHeight(options: ResponsiveHeightOptions) {
14
+ const breakpoints = useBreakpoints(breakpointsTailwind)
15
+
16
+ const height = computed(() => {
17
+ if (
18
+ options['2xl'] !== undefined
19
+ && breakpoints.greaterOrEqual('2xl').value
20
+ ) {
21
+ return options['2xl']
22
+ }
23
+
24
+ if (options.xl !== undefined && breakpoints.greaterOrEqual('xl').value) {
25
+ return options.xl
26
+ }
27
+
28
+ if (options.lg !== undefined && breakpoints.greaterOrEqual('lg').value) {
29
+ return options.lg
30
+ }
31
+
32
+ if (options.md !== undefined && breakpoints.greaterOrEqual('md').value) {
33
+ return options.md
34
+ }
35
+
36
+ if (options.sm !== undefined && breakpoints.greaterOrEqual('sm').value) {
37
+ return options.sm
38
+ }
39
+
40
+ return options.default
41
+ })
42
+
43
+ return { height }
44
+ }
@@ -0,0 +1,107 @@
1
+ import { computed } from 'vue'
2
+
3
+ export interface AppPerformanceItem {
4
+ date: string
5
+ subscriptions: number
6
+ downloads: number
7
+ }
8
+
9
+ const startDate = new Date('2025-01-01T00:00:00Z')
10
+
11
+ const sourceDailyData: AppPerformanceItem[] = Array.from(
12
+ { length: 90 },
13
+ (_, index) => {
14
+ const date = new Date(startDate)
15
+ date.setUTCDate(startDate.getUTCDate() + index)
16
+
17
+ return {
18
+ date: date.toISOString().slice(0, 10),
19
+ subscriptions: Math.max(
20
+ 48,
21
+ 86
22
+ + Math.round(Math.sin(index / 2.8) * 22)
23
+ + index
24
+ + ((index % 6) - 2) * 4
25
+ ),
26
+ downloads: Math.max(
27
+ 96,
28
+ 148
29
+ + Math.round(Math.cos(index / 3.4) * 28)
30
+ + index * 2
31
+ + ((index % 5) - 2) * 6
32
+ )
33
+ }
34
+ }
35
+ )
36
+
37
+ export const getDailyPerformanceData = computed(() => {
38
+ const formatter = new Intl.DateTimeFormat('en-US', {
39
+ month: 'short',
40
+ day: '2-digit'
41
+ })
42
+
43
+ return sourceDailyData.slice(0, 30).map(item => ({
44
+ ...item,
45
+ date: formatter.format(new Date(item.date))
46
+ }))
47
+ })
48
+
49
+ export const getWeeklyPerformanceData = computed((): AppPerformanceItem[] => {
50
+ const aggregated = new Map<
51
+ number,
52
+ { subscriptions: number, downloads: number }
53
+ >()
54
+
55
+ for (let index = 0; index < sourceDailyData.length; index++) {
56
+ const item = sourceDailyData[index]
57
+ if (!item) continue
58
+
59
+ const week = Math.floor(index / 7) + 1
60
+
61
+ if (!aggregated.has(week)) {
62
+ aggregated.set(week, {
63
+ subscriptions: 0,
64
+ downloads: 0
65
+ })
66
+ }
67
+
68
+ const current = aggregated.get(week)!
69
+ current.subscriptions += item.subscriptions
70
+ current.downloads += item.downloads
71
+ }
72
+
73
+ return Array.from(aggregated.entries()).map(([week, value]) => ({
74
+ date: `Week ${String(week).padStart(2, '0')}`,
75
+ subscriptions: value.subscriptions,
76
+ downloads: value.downloads
77
+ }))
78
+ })
79
+
80
+ export const getMonthlyPerformanceData = computed((): AppPerformanceItem[] => {
81
+ const aggregated = new Map<string, { subscriptions: number, downloads: number }>()
82
+ const formatter = new Intl.DateTimeFormat('en-US', {
83
+ month: 'short',
84
+ year: '2-digit'
85
+ })
86
+
87
+ for (const item of sourceDailyData) {
88
+ const monthKey = item.date.slice(0, 7)
89
+
90
+ if (!aggregated.has(monthKey)) {
91
+ aggregated.set(monthKey, {
92
+ subscriptions: 0,
93
+ downloads: 0
94
+ })
95
+ }
96
+
97
+ const current = aggregated.get(monthKey)!
98
+ current.subscriptions += item.subscriptions
99
+ current.downloads += item.downloads
100
+ }
101
+
102
+ return Array.from(aggregated.entries()).map(([monthKey, value]) => ({
103
+ date: formatter.format(new Date(`${monthKey}-01`)),
104
+ subscriptions: value.subscriptions,
105
+ downloads: value.downloads
106
+ }))
107
+ })
@@ -0,0 +1,122 @@
1
+ import { computed } from 'vue'
2
+
3
+ export interface WebsitePerformanceMetric {
4
+ date: string
5
+ bounceRate: number
6
+ conversions: number
7
+ }
8
+
9
+ const startDate = new Date('2025-01-01T00:00:00Z')
10
+
11
+ const sourceDailyData: WebsitePerformanceMetric[] = Array.from(
12
+ { length: 90 },
13
+ (_, index) => {
14
+ const date = new Date(startDate)
15
+ date.setUTCDate(startDate.getUTCDate() + index)
16
+
17
+ return {
18
+ date: date.toISOString().slice(0, 10),
19
+ bounceRate: Math.max(
20
+ 28,
21
+ Math.min(
22
+ 62,
23
+ 46
24
+ + Math.round(Math.sin(index / 4) * 5)
25
+ - (index % 6)
26
+ )
27
+ ),
28
+ conversions: Math.max(
29
+ 72,
30
+ 138
31
+ + Math.round(Math.cos(index / 3.1) * 18)
32
+ + Math.round(index * 0.7)
33
+ + ((index % 4) - 1) * 6
34
+ )
35
+ }
36
+ }
37
+ )
38
+
39
+ export const getDailyWebsiteStatistics = computed(() => {
40
+ const formatter = new Intl.DateTimeFormat('en-US', {
41
+ month: 'short',
42
+ day: '2-digit'
43
+ })
44
+
45
+ return sourceDailyData.slice(0, 30).map(item => ({
46
+ ...item,
47
+ date: formatter.format(new Date(item.date))
48
+ }))
49
+ })
50
+
51
+ export const getWeeklyWebsiteStatistics = computed(
52
+ (): WebsitePerformanceMetric[] => {
53
+ const aggregated = new Map<
54
+ number,
55
+ { bounceRate: number, conversions: number, count: number }
56
+ >()
57
+
58
+ for (let index = 0; index < sourceDailyData.length; index++) {
59
+ const item = sourceDailyData[index]
60
+ if (!item) continue
61
+
62
+ const week = Math.floor(index / 7) + 1
63
+
64
+ if (!aggregated.has(week)) {
65
+ aggregated.set(week, {
66
+ bounceRate: 0,
67
+ conversions: 0,
68
+ count: 0
69
+ })
70
+ }
71
+
72
+ const current = aggregated.get(week)!
73
+ current.bounceRate += item.bounceRate
74
+ current.conversions += item.conversions
75
+ current.count++
76
+ }
77
+
78
+ return Array.from(aggregated.entries()).map(([week, value]) => ({
79
+ date: `Week ${String(week).padStart(2, '0')}`,
80
+ bounceRate: Math.round(value.bounceRate / value.count),
81
+ conversions: value.conversions
82
+ }))
83
+ }
84
+ )
85
+
86
+ export const getMonthlyWebsiteStatistics = computed(
87
+ (): WebsitePerformanceMetric[] => {
88
+ const aggregated = new Map<
89
+ string,
90
+ { bounceRate: number, conversions: number, count: number }
91
+ >()
92
+ const formatter = new Intl.DateTimeFormat('en-US', {
93
+ month: 'short',
94
+ year: '2-digit'
95
+ })
96
+
97
+ for (const item of sourceDailyData) {
98
+ const monthKey = item.date.slice(0, 7)
99
+
100
+ if (!aggregated.has(monthKey)) {
101
+ aggregated.set(monthKey, {
102
+ bounceRate: 0,
103
+ conversions: 0,
104
+ count: 0
105
+ })
106
+ }
107
+
108
+ const current = aggregated.get(monthKey)!
109
+ current.bounceRate += item.bounceRate
110
+ current.conversions += item.conversions
111
+ current.count++
112
+ }
113
+
114
+ return Array.from(aggregated.entries()).map(([monthKey, value]) => ({
115
+ date: formatter.format(new Date(`${monthKey}-01`)),
116
+ bounceRate: Math.round(value.bounceRate / value.count),
117
+ conversions: value.conversions
118
+ }))
119
+ }
120
+ )
121
+
122
+ export const WebsiteStatistics = getMonthlyWebsiteStatistics
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veristone/nuxt-v-app",
3
- "version": "0.2.10",
3
+ "version": "0.2.14",
4
4
  "description": "Veristone Nuxt App Layer - Shared components, composables, and layouts",
5
5
  "type": "module",
6
6
  "private": false,