@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,110 @@
1
+ <template>
2
+ <nav class="va-navigation">
3
+ <div class="va-navigation__links">
4
+ <NuxtLink
5
+ v-for="link in items"
6
+ :key="link.to"
7
+ :to="link.to"
8
+ :exact="link.exact"
9
+ class="va-navigation__link"
10
+ active-class="va-navigation__link--active"
11
+ >
12
+ <UIcon v-if="link.icon" :name="link.icon" class="va-navigation__link-icon" />
13
+ {{ link.label }}
14
+ </NuxtLink>
15
+ </div>
16
+ </nav>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ interface NavigationItem {
21
+ label: string
22
+ to: string
23
+ icon?: string
24
+ exact?: boolean
25
+ }
26
+
27
+ defineProps<{
28
+ items: NavigationItem[]
29
+ }>()
30
+ </script>
31
+
32
+ <style scoped>
33
+ .va-navigation {
34
+ margin-bottom: 2.5rem;
35
+ animation: vaNavFadeIn 0.8s ease-out 0.3s backwards;
36
+ }
37
+
38
+ .va-navigation__links {
39
+ display: flex;
40
+ gap: 0.5rem;
41
+ padding: 0.5rem;
42
+ background: rgba(255, 255, 255, 0.03);
43
+ backdrop-filter: blur(20px);
44
+ border: 1px solid rgba(212, 175, 55, 0.15);
45
+ border-radius: 12px;
46
+ overflow-x: auto;
47
+ }
48
+
49
+ .va-navigation__link {
50
+ position: relative;
51
+ display: inline-flex;
52
+ align-items: center;
53
+ gap: 0.5rem;
54
+ padding: 0.875rem 1.75rem;
55
+ font-size: 0.875rem;
56
+ font-weight: 600;
57
+ color: #9ca3af;
58
+ text-decoration: none;
59
+ border-radius: 8px;
60
+ transition: all 0.3s ease;
61
+ white-space: nowrap;
62
+ letter-spacing: 0.02em;
63
+ }
64
+
65
+ .va-navigation__link::before {
66
+ content: '';
67
+ position: absolute;
68
+ bottom: 0;
69
+ left: 50%;
70
+ transform: translateX(-50%);
71
+ width: 0;
72
+ height: 2px;
73
+ background: #d4af37;
74
+ transition: width 0.3s ease;
75
+ }
76
+
77
+ .va-navigation__link:hover {
78
+ color: #e8c96f;
79
+ background: rgba(212, 175, 55, 0.08);
80
+ }
81
+
82
+ .va-navigation__link--active {
83
+ color: #d4af37;
84
+ background: rgba(212, 175, 55, 0.12);
85
+ }
86
+
87
+ .va-navigation__link--active::before {
88
+ width: 80%;
89
+ }
90
+
91
+ .va-navigation__link-icon {
92
+ width: 1rem;
93
+ height: 1rem;
94
+ }
95
+
96
+ @keyframes vaNavFadeIn {
97
+ from {
98
+ opacity: 0;
99
+ }
100
+ to {
101
+ opacity: 1;
102
+ }
103
+ }
104
+
105
+ @media (max-width: 768px) {
106
+ .va-navigation__links {
107
+ overflow-x: scroll;
108
+ }
109
+ }
110
+ </style>
@@ -0,0 +1,169 @@
1
+ <script setup lang="ts">
2
+ type ActionColor = 'coral' | 'teal' | 'amber' | 'navy' | 'success' | 'gold'
3
+
4
+ interface QuickAction {
5
+ id: string
6
+ label: string
7
+ description?: string
8
+ icon: string
9
+ to?: string
10
+ color?: ActionColor
11
+ }
12
+
13
+ const props = withDefaults(defineProps<{
14
+ actions: QuickAction[]
15
+ title?: string
16
+ }>(), {
17
+ title: 'Quick Actions'
18
+ })
19
+
20
+ const emit = defineEmits<{
21
+ action: [action: QuickAction]
22
+ }>()
23
+
24
+ const actionColors: Record<ActionColor, { bg: string; color: string }> = {
25
+ coral: { bg: 'rgba(255, 92, 53, 0.1)', color: '#ff5c35' },
26
+ teal: { bg: 'rgba(0, 164, 189, 0.1)', color: '#00a4bd' },
27
+ amber: { bg: 'rgba(245, 166, 35, 0.1)', color: '#f5a623' },
28
+ navy: { bg: 'rgba(45, 62, 80, 0.1)', color: '#2d3e50' },
29
+ success: { bg: 'rgba(0, 189, 165, 0.1)', color: '#00bda5' },
30
+ gold: { bg: 'rgba(212, 175, 55, 0.1)', color: '#d4af37' }
31
+ }
32
+
33
+ function getColor(color?: ActionColor) {
34
+ return actionColors[color || 'coral'] || actionColors.coral
35
+ }
36
+
37
+ function handleAction(action: QuickAction) {
38
+ if (action.to) {
39
+ navigateTo(action.to)
40
+ } else {
41
+ emit('action', action)
42
+ }
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <div class="va-quick-actions">
48
+ <h3 v-if="title" class="va-quick-actions__title">{{ title }}</h3>
49
+
50
+ <div class="va-quick-actions__grid">
51
+ <button
52
+ v-for="(action, index) in actions"
53
+ :key="action.id"
54
+ class="va-quick-actions__item animate-fade-in-up"
55
+ :style="{ animationDelay: `${index * 0.05}s` }"
56
+ @click="handleAction(action)"
57
+ >
58
+ <div
59
+ class="va-quick-actions__icon"
60
+ :style="{
61
+ background: getColor(action.color).bg,
62
+ color: getColor(action.color).color
63
+ }"
64
+ >
65
+ <UIcon :name="action.icon" class="size-5" />
66
+ </div>
67
+ <div class="va-quick-actions__text">
68
+ <span class="va-quick-actions__label">{{ action.label }}</span>
69
+ <span v-if="action.description" class="va-quick-actions__description">
70
+ {{ action.description }}
71
+ </span>
72
+ </div>
73
+ <UIcon name="i-lucide-chevron-right" class="va-quick-actions__arrow size-4 text-muted" />
74
+ </button>
75
+ </div>
76
+ </div>
77
+ </template>
78
+
79
+ <style scoped>
80
+ .va-quick-actions {
81
+ background: var(--color-surface);
82
+ border: 1px solid var(--color-border);
83
+ border-radius: var(--radius-lg, 12px);
84
+ padding: 1.25rem;
85
+ }
86
+
87
+ .dark .va-quick-actions {
88
+ background: rgba(255, 255, 255, 0.02);
89
+ border-color: rgba(255, 255, 255, 0.08);
90
+ }
91
+
92
+ .va-quick-actions__title {
93
+ font-size: 0.875rem;
94
+ font-weight: 600;
95
+ color: var(--color-text-muted);
96
+ text-transform: uppercase;
97
+ letter-spacing: 0.05em;
98
+ margin-bottom: 1rem;
99
+ }
100
+
101
+ .va-quick-actions__grid {
102
+ display: grid;
103
+ gap: 0.75rem;
104
+ }
105
+
106
+ .va-quick-actions__item {
107
+ width: 100%;
108
+ text-align: left;
109
+ display: flex;
110
+ align-items: center;
111
+ gap: 0.75rem;
112
+ padding: 0.75rem;
113
+ background: transparent;
114
+ border: 1px solid transparent;
115
+ border-radius: 8px;
116
+ cursor: pointer;
117
+ transition: all 0.2s ease;
118
+ }
119
+
120
+ .va-quick-actions__item:hover {
121
+ background: rgba(255, 255, 255, 0.03);
122
+ border-color: rgba(255, 255, 255, 0.08);
123
+ }
124
+
125
+ .va-quick-actions__icon {
126
+ flex-shrink: 0;
127
+ width: 2.5rem;
128
+ height: 2.5rem;
129
+ display: flex;
130
+ align-items: center;
131
+ justify-content: center;
132
+ border-radius: 8px;
133
+ transition: transform 0.2s ease;
134
+ }
135
+
136
+ .va-quick-actions__item:hover .va-quick-actions__icon {
137
+ transform: scale(1.05);
138
+ }
139
+
140
+ .va-quick-actions__text {
141
+ flex: 1;
142
+ display: flex;
143
+ flex-direction: column;
144
+ gap: 0.125rem;
145
+ }
146
+
147
+ .va-quick-actions__label {
148
+ font-weight: 600;
149
+ font-size: 0.875rem;
150
+ color: var(--color-text, #e8eaed);
151
+ }
152
+
153
+ .va-quick-actions__description {
154
+ font-size: 0.75rem;
155
+ color: var(--color-text-muted);
156
+ }
157
+
158
+ .va-quick-actions__arrow {
159
+ flex-shrink: 0;
160
+ opacity: 0;
161
+ transform: translateX(-4px);
162
+ transition: all 0.2s ease;
163
+ }
164
+
165
+ .va-quick-actions__item:hover .va-quick-actions__arrow {
166
+ opacity: 1;
167
+ transform: translateX(0);
168
+ }
169
+ </style>
@@ -0,0 +1,109 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * VASlide - Veristone Slideover
4
+ * Matches XASlide API with V-App styling.
5
+ */
6
+ const props = withDefaults(defineProps<{
7
+ modelValue?: boolean
8
+ title?: string
9
+ side?: 'left' | 'right'
10
+ size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
11
+ triggerLabel?: string
12
+ triggerIcon?: string
13
+ triggerColor?: string
14
+ triggerVariant?: string
15
+ triggerSize?: string
16
+ }>(), {
17
+ modelValue: undefined,
18
+ title: '',
19
+ side: 'right',
20
+ size: 'md',
21
+ triggerLabel: '',
22
+ triggerIcon: '',
23
+ triggerColor: 'primary',
24
+ triggerVariant: 'solid',
25
+ triggerSize: 'sm'
26
+ })
27
+
28
+ const emit = defineEmits(['update:modelValue', 'open', 'close'])
29
+
30
+ // Handle v-model or internal state
31
+ const internalOpen = ref(false)
32
+ const isOpen = computed({
33
+ get: () => props.modelValue !== undefined ? props.modelValue : internalOpen.value,
34
+ set: (val) => {
35
+ if (props.modelValue !== undefined) emit('update:modelValue', val)
36
+ internalOpen.value = val
37
+ if (val) emit('open')
38
+ else emit('close')
39
+ }
40
+ })
41
+
42
+ const open = () => isOpen.value = true
43
+ const close = () => isOpen.value = false
44
+
45
+ defineExpose({ open, close })
46
+
47
+ // Variation: Use simpler size mapping
48
+ const widthClass = computed(() => {
49
+ switch (props.size) {
50
+ case 'sm': return 'max-w-xs'
51
+ case 'md': return 'max-w-md'
52
+ case 'lg': return 'max-w-lg'
53
+ case 'xl': return 'max-w-2xl' // Variation: XL is wider here
54
+ case 'full': return 'max-w-full'
55
+ default: return 'max-w-md'
56
+ }
57
+ })
58
+ </script>
59
+
60
+ <template>
61
+ <div>
62
+ <!-- Trigger -->
63
+ <slot name="trigger" :open="open">
64
+ <UButton
65
+ v-if="triggerLabel || triggerIcon"
66
+ :label="triggerLabel"
67
+ :icon="triggerIcon"
68
+ :color="triggerColor"
69
+ :variant="triggerVariant"
70
+ :size="triggerSize"
71
+ @click="open"
72
+ />
73
+ </slot>
74
+
75
+ <!-- Slideover -->
76
+ <USlideover
77
+ v-model="isOpen"
78
+ :side="side"
79
+ :ui="{ width: widthClass, overlay: { background: 'bg-gray-900/50 backdrop-blur-sm' } }"
80
+ >
81
+ <div class="flex flex-col h-full bg-white dark:bg-gray-900 shadow-xl">
82
+ <!-- Header -->
83
+ <div class="px-6 py-4 flex items-center justify-between border-b border-gray-100 dark:border-gray-800">
84
+ <slot name="header">
85
+ <h2 class="text-lg font-bold text-gray-900 dark:text-white">{{ title }}</h2>
86
+ </slot>
87
+ <UButton
88
+ color="gray"
89
+ variant="ghost"
90
+ icon="i-lucide-x"
91
+ size="sm"
92
+ class="-mr-2"
93
+ @click="close"
94
+ />
95
+ </div>
96
+
97
+ <!-- Body -->
98
+ <div class="flex-1 overflow-y-auto px-6 py-4">
99
+ <slot />
100
+ </div>
101
+
102
+ <!-- Footer -->
103
+ <div v-if="$slots.footer" class="px-6 py-4 border-t border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800/50">
104
+ <slot name="footer" />
105
+ </div>
106
+ </div>
107
+ </USlideover>
108
+ </div>
109
+ </template>
@@ -0,0 +1,259 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * VASlideover - Custom Slideover Component
4
+ * Pure Vue/Tailwind implementation without USlideover
5
+ *
6
+ * Features:
7
+ * - Side: left or right
8
+ * - Size variants: sm, md, lg, xl, full
9
+ * - Smooth slide transitions
10
+ * - Backdrop with optional blur
11
+ * - Accessible: focus trap, escape key, click outside to close
12
+ */
13
+ import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
14
+
15
+ const props = withDefaults(defineProps<{
16
+ modelValue?: boolean
17
+ title?: string
18
+ side?: 'left' | 'right'
19
+ size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
20
+ closable?: boolean
21
+ closeOnEscape?: boolean
22
+ closeOnClickOutside?: boolean
23
+ showBackdrop?: boolean
24
+ backdropBlur?: boolean
25
+ // Trigger props for self-contained usage
26
+ triggerLabel?: string
27
+ triggerIcon?: string
28
+ triggerColor?: string
29
+ triggerVariant?: string
30
+ triggerSize?: string
31
+ }>(), {
32
+ modelValue: undefined,
33
+ title: '',
34
+ side: 'right',
35
+ size: 'md',
36
+ closable: true,
37
+ closeOnEscape: true,
38
+ closeOnClickOutside: true,
39
+ showBackdrop: true,
40
+ backdropBlur: true,
41
+ triggerLabel: '',
42
+ triggerIcon: '',
43
+ triggerColor: 'primary',
44
+ triggerVariant: 'solid',
45
+ triggerSize: 'sm'
46
+ })
47
+
48
+ const emit = defineEmits<{
49
+ 'update:modelValue': [value: boolean]
50
+ 'open': []
51
+ 'close': []
52
+ }>()
53
+
54
+ // Slideover ref for focus trap
55
+ const slideoverRef = ref<HTMLElement | null>(null)
56
+ const { activate, deactivate } = useFocusTrap(slideoverRef, {
57
+ immediate: false,
58
+ allowOutsideClick: true,
59
+ escapeDeactivates: false
60
+ })
61
+
62
+ // Internal state for self-contained mode
63
+ const internalOpen = ref(false)
64
+
65
+ // Computed open state (supports both v-model and internal)
66
+ const isOpen = computed({
67
+ get: () => props.modelValue !== undefined ? props.modelValue : internalOpen.value,
68
+ set: (val) => {
69
+ if (props.modelValue !== undefined) {
70
+ emit('update:modelValue', val)
71
+ }
72
+ internalOpen.value = val
73
+ }
74
+ })
75
+
76
+ // Close slideover
77
+ const close = () => {
78
+ if (!props.closable) return
79
+ isOpen.value = false
80
+ emit('close')
81
+ }
82
+
83
+ // Open slideover
84
+ const open = () => {
85
+ isOpen.value = true
86
+ emit('open')
87
+ }
88
+
89
+ // Handle escape key
90
+ const handleKeydown = (e: KeyboardEvent) => {
91
+ if (e.key === 'Escape' && props.closeOnEscape && props.closable) {
92
+ close()
93
+ }
94
+ }
95
+
96
+ // Handle click outside
97
+ const handleBackdropClick = (e: MouseEvent) => {
98
+ if (e.target === e.currentTarget && props.closeOnClickOutside && props.closable) {
99
+ close()
100
+ }
101
+ }
102
+
103
+ // Watch for open state changes
104
+ watch(isOpen, (val) => {
105
+ if (val) {
106
+ document.body.style.overflow = 'hidden'
107
+ nextTick(() => {
108
+ activate()
109
+ })
110
+ } else {
111
+ document.body.style.overflow = ''
112
+ deactivate()
113
+ }
114
+ })
115
+
116
+ // Cleanup on unmount
117
+ onUnmounted(() => {
118
+ document.body.style.overflow = ''
119
+ deactivate()
120
+ })
121
+
122
+ // Size classes
123
+ const sizeClasses = computed(() => {
124
+ switch (props.size) {
125
+ case 'sm': return 'w-80 max-w-[90vw]'
126
+ case 'md': return 'w-96 max-w-[90vw]'
127
+ case 'lg': return 'w-[32rem] max-w-[90vw]'
128
+ case 'xl': return 'w-[42rem] max-w-[90vw]'
129
+ case 'full': return 'w-screen'
130
+ default: return 'w-96 max-w-[90vw]'
131
+ }
132
+ })
133
+
134
+ // Transition classes based on side
135
+ const transitionClasses = computed(() => {
136
+ if (props.side === 'left') {
137
+ return {
138
+ enterFrom: '-translate-x-full',
139
+ enterTo: 'translate-x-0',
140
+ leaveFrom: 'translate-x-0',
141
+ leaveTo: '-translate-x-full'
142
+ }
143
+ }
144
+ return {
145
+ enterFrom: 'translate-x-full',
146
+ enterTo: 'translate-x-0',
147
+ leaveFrom: 'translate-x-0',
148
+ leaveTo: 'translate-x-full'
149
+ }
150
+ })
151
+
152
+ // Position classes based on side
153
+ const positionClasses = computed(() => {
154
+ return props.side === 'left' ? 'left-0' : 'right-0'
155
+ })
156
+
157
+ // Expose methods
158
+ defineExpose({ open, close })
159
+ </script>
160
+
161
+ <template>
162
+ <div>
163
+ <!-- Trigger -->
164
+ <slot name="trigger" :open="open">
165
+ <UButton
166
+ v-if="triggerLabel || triggerIcon"
167
+ :label="triggerLabel"
168
+ :icon="triggerIcon"
169
+ :color="triggerColor"
170
+ :variant="triggerVariant"
171
+ :size="triggerSize"
172
+ @click="open"
173
+ />
174
+ </slot>
175
+
176
+ <!-- Slideover Panel -->
177
+ <Transition
178
+ enter-active-class="duration-300 ease-out"
179
+ enter-from-class="opacity-0"
180
+ enter-to-class="opacity-100"
181
+ leave-active-class="duration-200 ease-in"
182
+ leave-from-class="opacity-100"
183
+ leave-to-class="opacity-0"
184
+ >
185
+ <div
186
+ v-if="isOpen"
187
+ class="fixed inset-0 z-50"
188
+ @keydown="handleKeydown"
189
+ >
190
+ <!-- Backdrop -->
191
+ <div
192
+ v-if="showBackdrop"
193
+ class="absolute inset-0"
194
+ :class="backdropBlur ? 'bg-gray-900/60 backdrop-blur-sm' : 'bg-gray-900/60'"
195
+ @click="handleBackdropClick"
196
+ />
197
+
198
+ <!-- Panel -->
199
+ <Transition
200
+ enter-active-class="duration-300 ease-out transform"
201
+ :enter-from-class="transitionClasses.enterFrom"
202
+ :enter-to-class="transitionClasses.enterTo"
203
+ leave-active-class="duration-200 ease-in transform"
204
+ :leave-from-class="transitionClasses.leaveFrom"
205
+ :leave-to-class="transitionClasses.leaveTo"
206
+ >
207
+ <div
208
+ v-if="isOpen"
209
+ ref="slideoverRef"
210
+ class="fixed inset-y-0 flex flex-col bg-white dark:bg-gray-900 shadow-2xl border-gray-200 dark:border-gray-800"
211
+ :class="[
212
+ sizeClasses,
213
+ positionClasses,
214
+ side === 'left' ? 'border-r' : 'border-l'
215
+ ]"
216
+ role="dialog"
217
+ aria-modal="true"
218
+ :aria-labelledby="title ? 'va-slideover-title' : undefined"
219
+ >
220
+ <!-- Header -->
221
+ <div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800 shrink-0">
222
+ <slot name="header">
223
+ <h2
224
+ v-if="title"
225
+ id="va-slideover-title"
226
+ class="text-lg font-semibold text-gray-900 dark:text-white"
227
+ >
228
+ {{ title }}
229
+ </h2>
230
+ </slot>
231
+ <button
232
+ v-if="closable"
233
+ type="button"
234
+ class="p-1.5 rounded-lg text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500"
235
+ aria-label="Close slideover"
236
+ @click="close"
237
+ >
238
+ <UIcon name="i-lucide-x" class="w-5 h-5" />
239
+ </button>
240
+ </div>
241
+
242
+ <!-- Body -->
243
+ <div class="flex-1 overflow-y-auto px-6 py-4">
244
+ <slot />
245
+ </div>
246
+
247
+ <!-- Footer -->
248
+ <div
249
+ v-if="$slots.footer"
250
+ class="px-6 py-4 border-t border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800/50 shrink-0"
251
+ >
252
+ <slot name="footer" />
253
+ </div>
254
+ </div>
255
+ </Transition>
256
+ </div>
257
+ </Transition>
258
+ </div>
259
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ icon?: string
4
+ title?: string
5
+ description?: string
6
+ }>()
7
+ </script>
8
+
9
+ <template>
10
+ <div class="flex flex-col items-center justify-center py-12 px-4 text-center">
11
+ <div class="p-4 rounded-full bg-gray-50 dark:bg-gray-800/50 mb-4 ring-1 ring-gray-100 dark:ring-gray-800">
12
+ <UIcon :name="icon || 'i-lucide-inbox'" class="w-8 h-8 text-gray-400" />
13
+ </div>
14
+ <h3 class="text-base font-semibold text-gray-900 dark:text-white">{{ title || 'No data found' }}</h3>
15
+ <p v-if="description" class="mt-1 text-sm text-gray-500 max-w-sm mx-auto">{{ description }}</p>
16
+ <div v-if="$slots.action" class="mt-6">
17
+ <slot name="action" />
18
+ </div>
19
+ </div>
20
+ </template>
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ title?: string
4
+ description?: string
5
+ retryable?: boolean
6
+ }>()
7
+
8
+ defineEmits(['retry'])
9
+ </script>
10
+
11
+ <template>
12
+ <div class="rounded-lg bg-red-50 dark:bg-red-900/10 p-4 border border-red-100 dark:border-red-900/20">
13
+ <div class="flex items-start">
14
+ <div class="flex-shrink-0">
15
+ <UIcon name="i-lucide-alert-circle" class="w-5 h-5 text-red-400" />
16
+ </div>
17
+ <div class="ml-3 w-full">
18
+ <h3 class="text-sm font-medium text-red-800 dark:text-red-200">{{ title || 'Error' }}</h3>
19
+ <div class="mt-2 text-sm text-red-700 dark:text-red-300">
20
+ <p>{{ description || 'An unexpected error occurred.' }}</p>
21
+ </div>
22
+ <div v-if="retryable" class="mt-4">
23
+ <button
24
+ type="button"
25
+ class="rounded-md bg-red-50 dark:bg-red-900/20 px-2 py-1.5 text-sm font-medium text-red-800 dark:text-red-200 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-red-600 focus:ring-offset-2"
26
+ @click="$emit('retry')"
27
+ >
28
+ Try again
29
+ </button>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </template>