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