@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,45 @@
1
+ /**
2
+ * useXFileUpload - Helper for FormData uploads.
3
+ * Ported from @xenterprises/nuxt-x-app.
4
+ */
5
+ export const useXFileUpload = () => {
6
+ const { success: toastSuccess, error: toastError } = useVToast()
7
+ const loading = ref(false)
8
+
9
+ const upload = async (url: string, file: File | File[], extraData: Record<string, any> = {}) => {
10
+ loading.value = true
11
+ const formData = new FormData()
12
+
13
+ if (Array.isArray(file)) {
14
+ file.forEach(f => formData.append('file', f))
15
+ } else {
16
+ formData.append('file', file)
17
+ }
18
+
19
+ Object.keys(extraData).forEach(key => {
20
+ formData.append(key, extraData[key])
21
+ })
22
+
23
+ try {
24
+ const { data, error } = await useVFetch(url, {
25
+ method: 'POST',
26
+ body: formData
27
+ }, 'Upload')
28
+
29
+ if (error.value) throw error.value
30
+
31
+ toastSuccess('Upload Successful', 'Files uploaded successfully.')
32
+ return data.value
33
+ } catch (err: any) {
34
+ toastError('Upload Failed', err.message || 'An error occurred during upload.')
35
+ throw err
36
+ } finally {
37
+ loading.value = false
38
+ }
39
+ }
40
+
41
+ return {
42
+ upload,
43
+ loading
44
+ }
45
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * useVToast - A composable to display toast notifications.
3
+ * Ported from @xenterprises/nuxt-x-app to match Nuxt V patterns.
4
+ */
5
+ export function useVToast() {
6
+ const toast = useToast()
7
+ const timeout = 3000
8
+
9
+ const success = (title: string, description?: string) => {
10
+ toast.add({
11
+ title,
12
+ description,
13
+ icon: 'i-lucide-check-circle',
14
+ color: 'green',
15
+ timeout
16
+ })
17
+ }
18
+
19
+ const info = (title: string, description?: string) => {
20
+ toast.add({
21
+ title,
22
+ description,
23
+ icon: 'i-lucide-info',
24
+ color: 'blue',
25
+ timeout
26
+ })
27
+ }
28
+
29
+ const error = (title: string, description?: string) => {
30
+ toast.add({
31
+ title,
32
+ description,
33
+ icon: 'i-lucide-alert-circle',
34
+ color: 'red',
35
+ timeout
36
+ })
37
+ }
38
+
39
+ const warning = (title: string, description?: string) => {
40
+ toast.add({
41
+ title,
42
+ description,
43
+ icon: 'i-lucide-alert-triangle',
44
+ color: 'amber',
45
+ timeout
46
+ })
47
+ }
48
+
49
+ const checkError = (
50
+ err: any,
51
+ friendlyName: string,
52
+ action: string,
53
+ successColor: string = 'green'
54
+ ) => {
55
+ if (err.value) {
56
+ error(
57
+ `${friendlyName} ${action} failed`,
58
+ err.value.message || 'An unexpected error occurred.'
59
+ )
60
+ return false
61
+ } else {
62
+ toast.add({
63
+ title: `${friendlyName} ${action} successful`,
64
+ icon: 'i-lucide-check-circle',
65
+ color: successColor as any,
66
+ timeout
67
+ })
68
+ return true
69
+ }
70
+ }
71
+
72
+ return { success, info, error, warning, checkError }
73
+ }
@@ -0,0 +1,456 @@
1
+ import { h, resolveComponent } from 'vue'
2
+ import type { TableColumn } from '@nuxt/ui'
3
+
4
+ /**
5
+ * Composable for creating common table column patterns
6
+ */
7
+ export function useXATableColumns<T extends Record<string, any>>() {
8
+ const UBadge = resolveComponent('UBadge')
9
+ const UButton = resolveComponent('UButton')
10
+ const UDropdownMenu = resolveComponent('UDropdownMenu')
11
+ const UAvatar = resolveComponent('UAvatar')
12
+
13
+ /**
14
+ * Create a text column with optional formatting
15
+ */
16
+ function textColumn(
17
+ accessorKey: keyof T & string,
18
+ header: string,
19
+ options?: {
20
+ truncate?: boolean
21
+ maxWidth?: string
22
+ class?: string
23
+ }
24
+ ): TableColumn<T> {
25
+ return {
26
+ accessorKey,
27
+ header,
28
+ cell: ({ row }) => {
29
+ const value = row.getValue(accessorKey)
30
+ const classes = [options?.class]
31
+ if (options?.truncate) {
32
+ classes.push('truncate')
33
+ }
34
+ return h('span', {
35
+ class: classes.filter(Boolean).join(' '),
36
+ style: options?.maxWidth ? { maxWidth: options.maxWidth } : undefined,
37
+ title: options?.truncate ? String(value) : undefined
38
+ }, String(value ?? ''))
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Create a date column with formatting
45
+ */
46
+ function dateColumn(
47
+ accessorKey: keyof T & string,
48
+ header: string,
49
+ options?: {
50
+ format?: Intl.DateTimeFormatOptions
51
+ locale?: string
52
+ relative?: boolean
53
+ }
54
+ ): TableColumn<T> {
55
+ const defaultFormat: Intl.DateTimeFormatOptions = {
56
+ day: 'numeric',
57
+ month: 'short',
58
+ year: 'numeric'
59
+ }
60
+
61
+ return {
62
+ accessorKey,
63
+ header,
64
+ cell: ({ row }) => {
65
+ const value = row.getValue(accessorKey)
66
+ if (!value) return '-'
67
+
68
+ const date = new Date(value as string | number | Date)
69
+ if (isNaN(date.getTime())) return String(value)
70
+
71
+ if (options?.relative) {
72
+ return formatRelativeTime(date)
73
+ }
74
+
75
+ return date.toLocaleDateString(
76
+ options?.locale ?? 'en-US',
77
+ options?.format ?? defaultFormat
78
+ )
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Create a datetime column with time
85
+ */
86
+ function dateTimeColumn(
87
+ accessorKey: keyof T & string,
88
+ header: string,
89
+ options?: {
90
+ format?: Intl.DateTimeFormatOptions
91
+ locale?: string
92
+ }
93
+ ): TableColumn<T> {
94
+ const defaultFormat: Intl.DateTimeFormatOptions = {
95
+ day: 'numeric',
96
+ month: 'short',
97
+ hour: '2-digit',
98
+ minute: '2-digit',
99
+ hour12: false
100
+ }
101
+
102
+ return {
103
+ accessorKey,
104
+ header,
105
+ cell: ({ row }) => {
106
+ const value = row.getValue(accessorKey)
107
+ if (!value) return '-'
108
+
109
+ const date = new Date(value as string | number | Date)
110
+ if (isNaN(date.getTime())) return String(value)
111
+
112
+ return date.toLocaleString(
113
+ options?.locale ?? 'en-US',
114
+ options?.format ?? defaultFormat
115
+ )
116
+ }
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Create a currency/money column
122
+ */
123
+ function currencyColumn(
124
+ accessorKey: keyof T & string,
125
+ header: string,
126
+ options?: {
127
+ currency?: string
128
+ locale?: string
129
+ minimumFractionDigits?: number
130
+ maximumFractionDigits?: number
131
+ }
132
+ ): TableColumn<T> {
133
+ return {
134
+ accessorKey,
135
+ header: () => h('div', { class: 'text-right' }, header),
136
+ cell: ({ row }) => {
137
+ const value = row.getValue(accessorKey)
138
+ if (value === null || value === undefined) return h('div', { class: 'text-right' }, '-')
139
+
140
+ const amount = Number(value)
141
+ if (isNaN(amount)) return h('div', { class: 'text-right' }, String(value))
142
+
143
+ const formatted = new Intl.NumberFormat(options?.locale ?? 'en-US', {
144
+ style: 'currency',
145
+ currency: options?.currency ?? 'USD',
146
+ minimumFractionDigits: options?.minimumFractionDigits ?? 2,
147
+ maximumFractionDigits: options?.maximumFractionDigits ?? 2
148
+ }).format(amount)
149
+
150
+ return h('div', { class: 'text-right font-medium' }, formatted)
151
+ }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Create a number column with formatting
157
+ */
158
+ function numberColumn(
159
+ accessorKey: keyof T & string,
160
+ header: string,
161
+ options?: {
162
+ locale?: string
163
+ minimumFractionDigits?: number
164
+ maximumFractionDigits?: number
165
+ align?: 'left' | 'center' | 'right'
166
+ }
167
+ ): TableColumn<T> {
168
+ const align = options?.align ?? 'right'
169
+ const alignClass = {
170
+ left: 'text-left',
171
+ center: 'text-center',
172
+ right: 'text-right'
173
+ }[align]
174
+
175
+ return {
176
+ accessorKey,
177
+ header: () => h('div', { class: alignClass }, header),
178
+ cell: ({ row }) => {
179
+ const value = row.getValue(accessorKey)
180
+ if (value === null || value === undefined) return h('div', { class: alignClass }, '-')
181
+
182
+ const num = Number(value)
183
+ if (isNaN(num)) return h('div', { class: alignClass }, String(value))
184
+
185
+ const formatted = new Intl.NumberFormat(options?.locale ?? 'en-US', {
186
+ minimumFractionDigits: options?.minimumFractionDigits,
187
+ maximumFractionDigits: options?.maximumFractionDigits
188
+ }).format(num)
189
+
190
+ return h('div', { class: alignClass }, formatted)
191
+ }
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Create a badge/status column
197
+ */
198
+ function badgeColumn<K extends keyof T & string>(
199
+ accessorKey: K,
200
+ header: string,
201
+ colorMap: Record<string, 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'>,
202
+ options?: {
203
+ variant?: 'solid' | 'outline' | 'soft' | 'subtle'
204
+ capitalize?: boolean
205
+ labelMap?: Record<string, string>
206
+ }
207
+ ): TableColumn<T> {
208
+ return {
209
+ accessorKey,
210
+ header,
211
+ cell: ({ row }) => {
212
+ const value = String(row.getValue(accessorKey) ?? '')
213
+ const color = colorMap[value] ?? 'neutral'
214
+ const label = options?.labelMap?.[value] ?? value
215
+
216
+ return h(UBadge, {
217
+ color,
218
+ variant: options?.variant ?? 'subtle',
219
+ class: options?.capitalize !== false ? 'capitalize' : ''
220
+ }, () => label)
221
+ }
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Create a boolean column with badge display
227
+ */
228
+ function booleanColumn(
229
+ accessorKey: keyof T & string,
230
+ header: string,
231
+ options?: {
232
+ trueLabel?: string
233
+ falseLabel?: string
234
+ trueColor?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'
235
+ falseColor?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'
236
+ }
237
+ ): TableColumn<T> {
238
+ return {
239
+ accessorKey,
240
+ header,
241
+ cell: ({ row }) => {
242
+ const value = Boolean(row.getValue(accessorKey))
243
+ return h(UBadge, {
244
+ color: value ? (options?.trueColor ?? 'success') : (options?.falseColor ?? 'neutral'),
245
+ variant: 'subtle'
246
+ }, () => value ? (options?.trueLabel ?? 'Yes') : (options?.falseLabel ?? 'No'))
247
+ }
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Create an avatar column
253
+ */
254
+ function avatarColumn(
255
+ accessorKey: keyof T & string,
256
+ header: string,
257
+ options?: {
258
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
259
+ nameKey?: keyof T & string
260
+ fallbackIcon?: string
261
+ }
262
+ ): TableColumn<T> {
263
+ return {
264
+ accessorKey,
265
+ header,
266
+ cell: ({ row }) => {
267
+ const src = row.getValue(accessorKey) as string | undefined
268
+ const name = options?.nameKey ? (row.original as T)[options.nameKey] as string : undefined
269
+
270
+ return h(UAvatar, {
271
+ src,
272
+ alt: name,
273
+ size: options?.size ?? 'sm',
274
+ icon: options?.fallbackIcon ?? 'i-lucide-user'
275
+ })
276
+ }
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Create a user column with avatar and name/email
282
+ */
283
+ function userColumn(
284
+ config: {
285
+ avatarKey?: keyof T & string
286
+ nameKey: keyof T & string
287
+ emailKey?: keyof T & string
288
+ },
289
+ header: string
290
+ ): TableColumn<T> {
291
+ return {
292
+ id: 'user',
293
+ header,
294
+ cell: ({ row }) => {
295
+ const original = row.original as T
296
+ const avatar = config.avatarKey ? original[config.avatarKey] as string : undefined
297
+ const name = original[config.nameKey] as string
298
+ const email = config.emailKey ? original[config.emailKey] as string : undefined
299
+
300
+ return h('div', { class: 'flex items-center gap-3' }, [
301
+ h(UAvatar, {
302
+ src: avatar,
303
+ alt: name,
304
+ size: 'sm',
305
+ icon: 'i-lucide-user'
306
+ }),
307
+ h('div', { class: 'flex flex-col' }, [
308
+ h('span', { class: 'font-medium text-gray-900 dark:text-white' }, name),
309
+ email ? h('span', { class: 'text-sm text-gray-500 dark:text-gray-400' }, email) : null
310
+ ].filter(Boolean))
311
+ ])
312
+ }
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Create an actions column with dropdown menu
318
+ */
319
+ function actionsColumn(
320
+ getActions: (row: T) => Array<{
321
+ label: string
322
+ icon?: string
323
+ color?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'
324
+ disabled?: boolean
325
+ onClick: () => void
326
+ } | 'separator'>,
327
+ options?: {
328
+ header?: string
329
+ icon?: string
330
+ }
331
+ ): TableColumn<T> {
332
+ return {
333
+ id: 'actions',
334
+ header: () => h('div', { class: 'text-right' }, options?.header ?? ''),
335
+ cell: ({ row }) => {
336
+ const actions = getActions(row.original as T)
337
+ const items = actions.map(action => {
338
+ if (action === 'separator') {
339
+ return { type: 'separator' as const }
340
+ }
341
+ return {
342
+ label: action.label,
343
+ icon: action.icon,
344
+ color: action.color,
345
+ disabled: action.disabled,
346
+ onSelect: action.onClick
347
+ }
348
+ })
349
+
350
+ return h('div', { class: 'text-right' }, [
351
+ h(UDropdownMenu, {
352
+ items: [items]
353
+ }, {
354
+ default: () => h(UButton, {
355
+ icon: options?.icon ?? 'i-lucide-ellipsis-vertical',
356
+ color: 'neutral',
357
+ variant: 'ghost',
358
+ size: 'xs'
359
+ })
360
+ })
361
+ ])
362
+ },
363
+ enableSorting: false,
364
+ enableHiding: false,
365
+ size: 60
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Create a link column
371
+ */
372
+ function linkColumn(
373
+ accessorKey: keyof T & string,
374
+ header: string,
375
+ getHref: (row: T) => string,
376
+ options?: {
377
+ external?: boolean
378
+ class?: string
379
+ }
380
+ ): TableColumn<T> {
381
+ return {
382
+ accessorKey,
383
+ header,
384
+ cell: ({ row }) => {
385
+ const value = row.getValue(accessorKey)
386
+ const href = getHref(row.original as T)
387
+
388
+ return h('a', {
389
+ href,
390
+ target: options?.external ? '_blank' : undefined,
391
+ rel: options?.external ? 'noopener noreferrer' : undefined,
392
+ class: options?.class ?? 'text-primary-500 hover:text-primary-600 hover:underline'
393
+ }, String(value ?? ''))
394
+ }
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Create a sortable column header
400
+ */
401
+ function sortableHeader(
402
+ accessorKey: keyof T & string,
403
+ header: string
404
+ ): TableColumn<T> {
405
+ return {
406
+ accessorKey,
407
+ header: ({ column }) => {
408
+ const isSorted = column.getIsSorted()
409
+ return h(UButton, {
410
+ color: 'neutral',
411
+ variant: 'ghost',
412
+ label: header,
413
+ icon: isSorted
414
+ ? (isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow')
415
+ : 'i-lucide-arrow-up-down',
416
+ class: '-mx-2.5',
417
+ onClick: () => column.toggleSorting(column.getIsSorted() === 'asc')
418
+ })
419
+ }
420
+ }
421
+ }
422
+
423
+ return {
424
+ textColumn,
425
+ dateColumn,
426
+ dateTimeColumn,
427
+ currencyColumn,
428
+ numberColumn,
429
+ badgeColumn,
430
+ booleanColumn,
431
+ avatarColumn,
432
+ userColumn,
433
+ actionsColumn,
434
+ linkColumn,
435
+ sortableHeader
436
+ }
437
+ }
438
+
439
+ // Helper function for relative time
440
+ function formatRelativeTime(date: Date): string {
441
+ const now = new Date()
442
+ const diffMs = now.getTime() - date.getTime()
443
+ const diffSecs = Math.floor(diffMs / 1000)
444
+ const diffMins = Math.floor(diffSecs / 60)
445
+ const diffHours = Math.floor(diffMins / 60)
446
+ const diffDays = Math.floor(diffHours / 24)
447
+
448
+ if (diffSecs < 60) return 'just now'
449
+ if (diffMins < 60) return `${diffMins}m ago`
450
+ if (diffHours < 24) return `${diffHours}h ago`
451
+ if (diffDays < 7) return `${diffDays}d ago`
452
+
453
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
454
+ }
455
+
456
+ export type { TableColumn }
@@ -0,0 +1,65 @@
1
+ // Billing data for dashboard components
2
+
3
+ export interface BillingData {
4
+ month: string
5
+ apiCalls: number
6
+ storage: number
7
+ users: number
8
+ }
9
+
10
+ export const BillingData: BillingData[] = [
11
+ { month: 'Jan', apiCalls: 120, storage: 80, users: 320 },
12
+ { month: 'Feb', apiCalls: 150, storage: 85, users: 340 },
13
+ { month: 'Mar', apiCalls: 170, storage: 90, users: 360 },
14
+ { month: 'Apr', apiCalls: 140, storage: 95, users: 355 },
15
+ { month: 'May', apiCalls: 180, storage: 100, users: 370 },
16
+ { month: 'Jun', apiCalls: 210, storage: 105, users: 390 },
17
+ { month: 'Jul', apiCalls: 190, storage: 110, users: 400 },
18
+ { month: 'Aug', apiCalls: 200, storage: 115, users: 410 },
19
+ { month: 'Sep', apiCalls: 220, storage: 120, users: 420 },
20
+ { month: 'Oct', apiCalls: 230, storage: 125, users: 430 },
21
+ { month: 'Nov', apiCalls: 210, storage: 130, users: 440 },
22
+ { month: 'Dec', apiCalls: 240, storage: 135, users: 450 }
23
+ ]
24
+
25
+ export interface KPIStat {
26
+ label: string
27
+ value: string
28
+ change: string
29
+ changeType: 'positive' | 'negative' | 'neutral'
30
+ }
31
+
32
+ // Calculate stats based on BillingData
33
+ const latest = BillingData[BillingData.length - 1]
34
+ const prev = BillingData[BillingData.length - 2]
35
+
36
+ export const BillingStats: KPIStat[] = [
37
+ {
38
+ label: 'Current Plan',
39
+ value: 'Basic',
40
+ change: '+0%',
41
+ changeType: 'neutral'
42
+ },
43
+ {
44
+ label: 'Monthly Spend',
45
+ value: '$0.00',
46
+ change: '+0%',
47
+ changeType: 'neutral'
48
+ },
49
+ {
50
+ label: 'Usage This Month',
51
+ value: latest ? latest.apiCalls.toLocaleString() : '-',
52
+ change:
53
+ latest && prev && prev.apiCalls
54
+ ? `${(latest.apiCalls - prev.apiCalls) / prev.apiCalls > 0 ? '+' : ''}${(((latest.apiCalls - prev.apiCalls) / prev.apiCalls) * 100).toFixed(0)}%`
55
+ : '+0%',
56
+ changeType:
57
+ latest && prev && prev.apiCalls
58
+ ? latest.apiCalls > prev.apiCalls
59
+ ? 'positive'
60
+ : latest.apiCalls < prev.apiCalls
61
+ ? 'negative'
62
+ : 'neutral'
63
+ : 'neutral'
64
+ }
65
+ ]
@@ -0,0 +1,58 @@
1
+ // Search data for chart components
2
+
3
+ export interface QueryData {
4
+ query: string
5
+ impressions: number
6
+ clicks: number
7
+ ctr: number
8
+ averagePosition: number
9
+ }
10
+
11
+ export interface DetailedDateData {
12
+ date: string
13
+ queries: QueryData[]
14
+ }
15
+
16
+ export const DetailedSearchQueryData: DetailedDateData[] = [
17
+ {
18
+ date: '2023-01-01',
19
+ queries: [
20
+ { query: 'example query 1', impressions: 1500, clicks: 100, ctr: 6.67, averagePosition: 20.1 },
21
+ { query: 'another search term', impressions: 2000, clicks: 150, ctr: 7.5, averagePosition: 25.3 },
22
+ { query: 'product type A', impressions: 800, clicks: 60, ctr: 7.5, averagePosition: 30.5 },
23
+ { query: 'brand name xyz', impressions: 1000, clicks: 78, ctr: 7.8, averagePosition: 18.9 }
24
+ ]
25
+ },
26
+ {
27
+ date: '2023-01-08',
28
+ queries: [
29
+ { query: 'example query 1', impressions: 1700, clicks: 120, ctr: 7.06, averagePosition: 18.5 },
30
+ { query: 'another search term', impressions: 2200, clicks: 170, ctr: 7.73, averagePosition: 23.0 },
31
+ { query: 'product type A', impressions: 900, clicks: 70, ctr: 7.78, averagePosition: 28.1 },
32
+ { query: 'brand name xyz', impressions: 1100, clicks: 85, ctr: 7.73, averagePosition: 17.5 },
33
+ { query: 'service offering B', impressions: 350, clicks: 27, ctr: 7.71, averagePosition: 33.2 }
34
+ ]
35
+ },
36
+ {
37
+ date: '2023-01-15',
38
+ queries: [
39
+ { query: 'example query 1', impressions: 1850, clicks: 135, ctr: 7.3, averagePosition: 17.2 },
40
+ { query: 'another search term', impressions: 2400, clicks: 185, ctr: 7.71, averagePosition: 21.5 },
41
+ { query: 'product type A', impressions: 1000, clicks: 80, ctr: 8.0, averagePosition: 26.8 },
42
+ { query: 'brand name xyz', impressions: 1200, clicks: 90, ctr: 7.5, averagePosition: 16.0 },
43
+ { query: 'service offering B', impressions: 400, clicks: 30, ctr: 7.5, averagePosition: 31.5 },
44
+ { query: 'how to do something', impressions: 200, clicks: 15, ctr: 7.5, averagePosition: 40.0 }
45
+ ]
46
+ },
47
+ {
48
+ date: '2023-01-22',
49
+ queries: [
50
+ { query: 'example query 1', impressions: 2000, clicks: 150, ctr: 7.5, averagePosition: 16.0 },
51
+ { query: 'another search term', impressions: 2600, clicks: 200, ctr: 7.69, averagePosition: 20.0 },
52
+ { query: 'product type A', impressions: 1100, clicks: 85, ctr: 7.73, averagePosition: 25.5 },
53
+ { query: 'brand name xyz', impressions: 1300, clicks: 100, ctr: 7.69, averagePosition: 15.0 },
54
+ { query: 'service offering B', impressions: 450, clicks: 35, ctr: 7.78, averagePosition: 30.0 },
55
+ { query: 'how to do something', impressions: 250, clicks: 20, ctr: 8.0, averagePosition: 38.0 }
56
+ ]
57
+ }
58
+ ]