@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,140 @@
1
+ <template>
2
+ <div class="va-data-field" :class="[sizeClass, { 'va-data-field--highlight': highlight }]">
3
+ <dt class="va-data-field__label">
4
+ <UIcon v-if="icon" :name="icon" class="va-data-field__icon" />
5
+ <span>{{ label }}</span>
6
+ </dt>
7
+ <dd class="va-data-field__value" :class="valueClass">
8
+ <slot>{{ formattedValue }}</slot>
9
+ <span v-if="unit" class="va-data-field__unit">{{ unit }}</span>
10
+ </dd>
11
+ <p v-if="description" class="va-data-field__description">{{ description }}</p>
12
+ </div>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ type FieldSize = 'sm' | 'md' | 'lg' | 'xl'
17
+ type FieldVariant = 'default' | 'currency' | 'percentage' | 'date'
18
+
19
+ const props = withDefaults(defineProps<{
20
+ label: string
21
+ value?: string | number
22
+ unit?: string
23
+ icon?: string
24
+ size?: FieldSize
25
+ variant?: FieldVariant
26
+ highlight?: boolean
27
+ description?: string
28
+ mono?: boolean
29
+ }>(), {
30
+ value: '',
31
+ unit: '',
32
+ icon: '',
33
+ size: 'md',
34
+ variant: 'default',
35
+ highlight: false,
36
+ description: '',
37
+ mono: false
38
+ })
39
+
40
+ const { formatCurrency } = useVAUtils()
41
+
42
+ const sizeClass = computed(() => `va-data-field--${props.size}`)
43
+
44
+ const valueClass = computed(() => ({
45
+ 'va-data-field__value--mono': props.mono || props.variant === 'currency',
46
+ 'va-data-field__value--currency': props.variant === 'currency',
47
+ 'va-data-field__value--percentage': props.variant === 'percentage'
48
+ }))
49
+
50
+ const formattedValue = computed(() => {
51
+ if (!props.value && props.value !== 0) return '—'
52
+
53
+ switch (props.variant) {
54
+ case 'currency':
55
+ return formatCurrency(Number(props.value))
56
+ case 'percentage':
57
+ return `${props.value}`
58
+ default:
59
+ return props.value
60
+ }
61
+ })
62
+ </script>
63
+
64
+ <style scoped>
65
+ .va-data-field {
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 0.375rem;
69
+ }
70
+
71
+ .va-data-field--highlight {
72
+ position: relative;
73
+ padding-left: 1rem;
74
+ }
75
+
76
+ .va-data-field--highlight::before {
77
+ content: '';
78
+ position: absolute;
79
+ left: 0;
80
+ top: 0;
81
+ bottom: 0;
82
+ width: 3px;
83
+ background: linear-gradient(180deg, var(--color-gold, #d4af37) 0%, var(--color-gold-light, #e8c96f) 100%);
84
+ border-radius: 2px;
85
+ }
86
+
87
+ .va-data-field__label {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 0.5rem;
91
+ font-size: 0.75rem;
92
+ font-weight: 600;
93
+ color: var(--color-text-muted, #9ca3af);
94
+ text-transform: uppercase;
95
+ letter-spacing: 0.08em;
96
+ }
97
+
98
+ .va-data-field__icon {
99
+ width: 0.875rem;
100
+ height: 0.875rem;
101
+ opacity: 0.7;
102
+ }
103
+
104
+ .va-data-field__value {
105
+ font-weight: 600;
106
+ color: var(--color-text, #e8eaed);
107
+ line-height: 1.3;
108
+ }
109
+
110
+ .va-data-field__value--mono {
111
+ font-variant-numeric: tabular-nums;
112
+ }
113
+
114
+ .va-data-field__value--currency {
115
+ font-family: 'Cormorant Garamond', Georgia, serif;
116
+ letter-spacing: -0.01em;
117
+ }
118
+
119
+ .va-data-field__unit {
120
+ opacity: 0.7;
121
+ margin-left: 0.125rem;
122
+ }
123
+
124
+ .va-data-field--sm .va-data-field__value { font-size: 1rem; }
125
+ .va-data-field--md .va-data-field__value { font-size: 1.25rem; }
126
+ .va-data-field--lg .va-data-field__value { font-size: 1.75rem; }
127
+ .va-data-field--xl .va-data-field__value { font-size: 2.25rem; }
128
+
129
+ .va-data-field--lg .va-data-field__label,
130
+ .va-data-field--xl .va-data-field__label {
131
+ font-size: 0.8125rem;
132
+ }
133
+
134
+ .va-data-field__description {
135
+ font-size: 0.75rem;
136
+ color: var(--color-text-muted, #9ca3af);
137
+ opacity: 0.8;
138
+ margin-top: 0.125rem;
139
+ }
140
+ </style>
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <div class="va-data-grid" :class="`va-data-grid--${columns}`">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ type GridColumns = 1 | 2 | 3 | 4 | '1' | '2' | '3' | '4' | 'auto'
9
+
10
+ withDefaults(defineProps<{
11
+ columns?: GridColumns
12
+ }>(), {
13
+ columns: 2
14
+ })
15
+ </script>
16
+
17
+ <style scoped>
18
+ .va-data-grid {
19
+ display: grid;
20
+ gap: 1.5rem;
21
+ }
22
+
23
+ .va-data-grid--1 { grid-template-columns: 1fr; }
24
+ .va-data-grid--2 { grid-template-columns: repeat(2, 1fr); }
25
+ .va-data-grid--3 { grid-template-columns: repeat(3, 1fr); }
26
+ .va-data-grid--4 { grid-template-columns: repeat(4, 1fr); }
27
+ .va-data-grid--auto { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
28
+
29
+ @media (max-width: 768px) {
30
+ .va-data-grid--2,
31
+ .va-data-grid--3,
32
+ .va-data-grid--4 {
33
+ grid-template-columns: 1fr;
34
+ }
35
+ }
36
+
37
+ @media (min-width: 769px) and (max-width: 1024px) {
38
+ .va-data-grid--3,
39
+ .va-data-grid--4 {
40
+ grid-template-columns: repeat(2, 1fr);
41
+ }
42
+ }
43
+ </style>
@@ -0,0 +1,144 @@
1
+ <script setup lang="ts">
2
+ import type { TableColumn } from '@nuxt/ui'
3
+
4
+ const props = withDefaults(defineProps<{
5
+ title: string
6
+ rows: unknown[]
7
+ columns: TableColumn<unknown>[]
8
+ loading?: boolean
9
+ error?: Error | null
10
+ searchPlaceholder?: string
11
+ totalCount?: number
12
+ hasPrev?: boolean
13
+ hasNext?: boolean
14
+ showSearch?: boolean
15
+ showPagination?: boolean
16
+ showNewButton?: boolean
17
+ newButtonLabel?: string
18
+ newButtonPath?: string
19
+ }>(), {
20
+ loading: false,
21
+ error: null,
22
+ searchPlaceholder: 'Search...',
23
+ totalCount: 0,
24
+ hasPrev: false,
25
+ hasNext: false,
26
+ showSearch: true,
27
+ showPagination: true,
28
+ showNewButton: false,
29
+ newButtonLabel: 'New',
30
+ newButtonPath: ''
31
+ })
32
+
33
+ const emit = defineEmits<{
34
+ select: [row: unknown]
35
+ prev: []
36
+ next: []
37
+ new: []
38
+ }>()
39
+
40
+ const search = defineModel<string>('search', { default: '' })
41
+
42
+ function handleSelect(row: unknown) {
43
+ emit('select', row)
44
+ }
45
+
46
+ function handlePrev() {
47
+ emit('prev')
48
+ }
49
+
50
+ function handleNext() {
51
+ emit('next')
52
+ }
53
+
54
+ function handleNew() {
55
+ if (props.newButtonPath) {
56
+ navigateTo(props.newButtonPath)
57
+ } else {
58
+ emit('new')
59
+ }
60
+ }
61
+ </script>
62
+
63
+ <template>
64
+ <UDashboardPanel>
65
+ <template #header>
66
+ <UDashboardNavbar :title="title">
67
+ <template #leading>
68
+ <UDashboardSidebarCollapse />
69
+ </template>
70
+
71
+ <template #right>
72
+ <UInput
73
+ v-if="showSearch"
74
+ v-model="search"
75
+ icon="i-lucide-search"
76
+ :placeholder="searchPlaceholder"
77
+ class="w-64"
78
+ />
79
+ <UButton v-if="showNewButton" icon="i-lucide-plus" @click="handleNew">
80
+ {{ newButtonLabel }}
81
+ </UButton>
82
+ </template>
83
+ </UDashboardNavbar>
84
+ </template>
85
+
86
+ <template #body>
87
+ <section v-if="loading" class="flex justify-center py-12">
88
+ <UIcon name="i-lucide-loader-2" class="animate-spin text-4xl" />
89
+ </section>
90
+
91
+ <section v-else-if="error" class="p-6">
92
+ <UAlert
93
+ color="red"
94
+ variant="soft"
95
+ title="Error loading data"
96
+ :description="error.message"
97
+ />
98
+ </section>
99
+
100
+ <section v-else-if="rows.length > 0">
101
+ <UTable
102
+ :data="rows"
103
+ :columns="columns"
104
+ :loading="loading"
105
+ @select="handleSelect"
106
+ >
107
+ <template v-for="(_, name) in $slots" #[name]="slotData">
108
+ <slot :name="name" v-bind="slotData" />
109
+ </template>
110
+ </UTable>
111
+
112
+ <div
113
+ v-if="showPagination"
114
+ class="flex justify-between items-center p-4 border-t border-default"
115
+ >
116
+ <p class="text-sm text-muted">
117
+ Showing {{ rows.length }} of {{ totalCount }} items
118
+ </p>
119
+ <div class="flex gap-2">
120
+ <UButton variant="outline" :disabled="!hasPrev" @click="handlePrev">
121
+ Previous
122
+ </UButton>
123
+ <UButton variant="outline" :disabled="!hasNext" @click="handleNext">
124
+ Next
125
+ </UButton>
126
+ </div>
127
+ </div>
128
+ </section>
129
+
130
+ <section v-else class="p-6">
131
+ <div class="text-center py-12">
132
+ <slot name="empty">
133
+ <UIcon
134
+ name="i-lucide-inbox"
135
+ class="size-16 mx-auto mb-4 text-muted opacity-50"
136
+ />
137
+ <h2 class="text-2xl font-bold mb-2">No Data Found</h2>
138
+ <p class="text-muted">There are no items available at this time</p>
139
+ </slot>
140
+ </div>
141
+ </section>
142
+ </template>
143
+ </UDashboardPanel>
144
+ </template>
@@ -0,0 +1,154 @@
1
+ <template>
2
+ <div class="va-empty-state" :class="`va-empty-state--${size}`">
3
+ <div class="va-empty-state__container">
4
+ <div class="va-empty-state__icon-wrapper">
5
+ <UIcon :name="icon" class="va-empty-state__icon" />
6
+ <div class="va-empty-state__icon-ring" />
7
+ </div>
8
+
9
+ <div class="va-empty-state__content">
10
+ <h3 class="va-empty-state__title">{{ title }}</h3>
11
+ <p v-if="description" class="va-empty-state__description">{{ description }}</p>
12
+ </div>
13
+
14
+ <div v-if="$slots.default || actionLabel" class="va-empty-state__actions">
15
+ <slot>
16
+ <UButton
17
+ v-if="actionLabel"
18
+ :label="actionLabel"
19
+ :icon="actionIcon"
20
+ class="va-empty-state__btn"
21
+ @click="$emit('action')"
22
+ />
23
+ </slot>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ type EmptyStateSize = 'sm' | 'md' | 'lg'
31
+
32
+ withDefaults(defineProps<{
33
+ icon?: string
34
+ title?: string
35
+ description?: string
36
+ actionLabel?: string
37
+ actionIcon?: string
38
+ size?: EmptyStateSize
39
+ }>(), {
40
+ icon: 'i-lucide-inbox',
41
+ title: 'No Data Found',
42
+ description: '',
43
+ actionLabel: '',
44
+ actionIcon: 'i-lucide-plus',
45
+ size: 'md'
46
+ })
47
+
48
+ defineEmits<{
49
+ action: []
50
+ }>()
51
+ </script>
52
+
53
+ <style scoped>
54
+ .va-empty-state {
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ animation: vaEmptyFadeIn 0.4s ease-out;
59
+ }
60
+
61
+ @keyframes vaEmptyFadeIn {
62
+ from {
63
+ opacity: 0;
64
+ transform: translateY(8px);
65
+ }
66
+ to {
67
+ opacity: 1;
68
+ transform: translateY(0);
69
+ }
70
+ }
71
+
72
+ .va-empty-state--sm { padding: 2rem 1rem; }
73
+ .va-empty-state--md { padding: 4rem 2rem; }
74
+ .va-empty-state--lg { padding: 6rem 2rem; min-height: 400px; }
75
+
76
+ .va-empty-state__container {
77
+ display: flex;
78
+ flex-direction: column;
79
+ align-items: center;
80
+ text-align: center;
81
+ max-width: 400px;
82
+ }
83
+
84
+ .va-empty-state__icon-wrapper {
85
+ position: relative;
86
+ margin-bottom: 1.5rem;
87
+ }
88
+
89
+ .va-empty-state__icon {
90
+ position: relative;
91
+ z-index: 1;
92
+ font-size: 2.5rem;
93
+ color: #d4af37;
94
+ opacity: 0.8;
95
+ }
96
+
97
+ .va-empty-state--sm .va-empty-state__icon { font-size: 1.75rem; }
98
+ .va-empty-state--lg .va-empty-state__icon { font-size: 3.5rem; }
99
+
100
+ .va-empty-state__icon-ring {
101
+ position: absolute;
102
+ top: 50%;
103
+ left: 50%;
104
+ transform: translate(-50%, -50%);
105
+ width: 5rem;
106
+ height: 5rem;
107
+ background: radial-gradient(circle, rgba(212, 175, 55, 0.1) 0%, transparent 70%);
108
+ border-radius: 50%;
109
+ animation: vaEmptyPulse 3s ease-in-out infinite;
110
+ }
111
+
112
+ .va-empty-state--sm .va-empty-state__icon-ring { width: 3.5rem; height: 3.5rem; }
113
+ .va-empty-state--lg .va-empty-state__icon-ring { width: 7rem; height: 7rem; }
114
+
115
+ @keyframes vaEmptyPulse {
116
+ 0%, 100% { opacity: 0.6; transform: translate(-50%, -50%) scale(1); }
117
+ 50% { opacity: 1; transform: translate(-50%, -50%) scale(1.1); }
118
+ }
119
+
120
+ .va-empty-state__content { margin-bottom: 1.5rem; }
121
+
122
+ .va-empty-state__title {
123
+ font-size: 1.25rem;
124
+ font-weight: 700;
125
+ color: var(--color-text, #e8eaed);
126
+ margin-bottom: 0.5rem;
127
+ letter-spacing: -0.01em;
128
+ }
129
+
130
+ .va-empty-state--sm .va-empty-state__title { font-size: 1rem; }
131
+ .va-empty-state--lg .va-empty-state__title { font-size: 1.5rem; }
132
+
133
+ .va-empty-state__description {
134
+ font-size: 0.875rem;
135
+ color: var(--color-text-muted, #9ca3af);
136
+ line-height: 1.6;
137
+ }
138
+
139
+ .va-empty-state__actions {
140
+ display: flex;
141
+ gap: 0.75rem;
142
+ }
143
+
144
+ .va-empty-state__btn {
145
+ background: rgba(212, 175, 55, 0.1);
146
+ border: 1px solid rgba(212, 175, 55, 0.25);
147
+ color: #d4af37;
148
+ }
149
+
150
+ .va-empty-state__btn:hover {
151
+ background: rgba(212, 175, 55, 0.15);
152
+ border-color: rgba(212, 175, 55, 0.4);
153
+ }
154
+ </style>
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ const props = withDefaults(defineProps<{
3
+ value: number
4
+ currency?: string
5
+ locale?: string
6
+ showSign?: boolean
7
+ colorize?: boolean
8
+ }>(), {
9
+ currency: 'USD',
10
+ locale: 'en-US',
11
+ showSign: false,
12
+ colorize: false
13
+ })
14
+
15
+ const formatted = computed(() => {
16
+ const opts: Intl.NumberFormatOptions = {
17
+ style: 'currency',
18
+ currency: props.currency,
19
+ signDisplay: props.showSign ? 'always' : 'auto'
20
+ }
21
+ return new Intl.NumberFormat(props.locale, opts).format(props.value)
22
+ })
23
+
24
+ const colorClass = computed(() => {
25
+ if (!props.colorize) return ''
26
+ if (props.value > 0) return 'text-green-600 dark:text-green-400'
27
+ if (props.value < 0) return 'text-red-600 dark:text-red-400'
28
+ return 'text-gray-600 dark:text-gray-400'
29
+ })
30
+ </script>
31
+
32
+ <template>
33
+ <span :class="['font-variant-numeric tabular-nums', colorClass]">
34
+ {{ formatted }}
35
+ </span>
36
+ </template>
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import { useDateFormat } from '@vueuse/core'
3
+
4
+ const props = withDefaults(defineProps<{
5
+ value: string | number | Date
6
+ format?: 'short' | 'long' | 'full' | 'relative'
7
+ showTime?: boolean
8
+ locale?: string
9
+ }>(), {
10
+ format: 'short',
11
+ showTime: false,
12
+ locale: 'en-US'
13
+ })
14
+
15
+ const formatted = computed(() => {
16
+ if (!props.value) return '-'
17
+ const date = new Date(props.value)
18
+
19
+ if (props.format === 'full') {
20
+ return useDateFormat(date, 'MMMM D, YYYY' + (props.showTime ? ' h:mm A' : '')).value
21
+ }
22
+
23
+ const opts: Intl.DateTimeFormatOptions = {
24
+ dateStyle: props.format as any
25
+ }
26
+ if (props.showTime) opts.timeStyle = 'short'
27
+
28
+ return new Intl.DateTimeFormat(props.locale, opts).format(date)
29
+ })
30
+ </script>
31
+
32
+ <template>
33
+ <span class="whitespace-nowrap">{{ formatted }}</span>
34
+ </template>
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ const props = withDefaults(defineProps<{
3
+ value: number
4
+ decimals?: number
5
+ showSign?: boolean
6
+ colorize?: boolean
7
+ scale?: number // Allow passing 0.5 for 50% or 50 for 50%
8
+ }>(), {
9
+ decimals: 1,
10
+ showSign: false,
11
+ colorize: false,
12
+ scale: 1 // Default assumes value is 0-100. If 0-1, pass scale: 100? No, standard Intl uses 0-1.
13
+ // Wait, standard Intl Percent expects 0.5 -> 50%.
14
+ // Veristone variation: We'll accept standard 0-100 or 0-1 via heuristic or prop.
15
+ // Let's assume input is 0-100 based on typical business apps, OR match standard behavior.
16
+ })
17
+
18
+ // V-App Variation: Handle 0-100 inputs gracefully if > 1
19
+ const normalizedValue = computed(() => {
20
+ // Heuristic: If value > 1, assume it's a percentage (e.g. 50 = 50%), so divide by 100 for Intl
21
+ // unless user explicitly wants 5000%.
22
+ // Actually, let's stick to standard behavior: Input 0.5 = 50%.
23
+ return props.value
24
+ })
25
+
26
+ const formatted = computed(() => {
27
+ return new Intl.NumberFormat('en-US', {
28
+ style: 'percent',
29
+ minimumFractionDigits: props.decimals,
30
+ maximumFractionDigits: props.decimals,
31
+ signDisplay: props.showSign ? 'always' : 'auto'
32
+ }).format(normalizedValue.value)
33
+ })
34
+
35
+ const colorClass = computed(() => {
36
+ if (!props.colorize) return ''
37
+ if (props.value > 0) return 'text-green-600 dark:text-green-400'
38
+ if (props.value < 0) return 'text-red-600 dark:text-red-400'
39
+ return 'text-gray-600 dark:text-gray-400'
40
+ })
41
+ </script>
42
+
43
+ <template>
44
+ <span :class="['font-variant-numeric tabular-nums', colorClass]">
45
+ {{ formatted }}
46
+ </span>
47
+ </template>