@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-loading-state" :class="`va-loading-state--${size}`">
3
+ <div class="va-loading-state__container">
4
+ <div class="va-loading-state__ring">
5
+ <div class="va-loading-state__ring-inner" />
6
+ <UIcon v-if="icon" :name="icon" class="va-loading-state__icon" />
7
+ </div>
8
+
9
+ <div v-if="text || description" class="va-loading-state__content">
10
+ <p v-if="text" class="va-loading-state__text">{{ text }}</p>
11
+ <p v-if="description" class="va-loading-state__description">{{ description }}</p>
12
+ </div>
13
+ </div>
14
+
15
+ <div v-if="showSkeleton" class="va-loading-state__skeleton">
16
+ <USkeleton v-for="i in skeletonRows" :key="i" class="va-loading-state__skeleton-row" />
17
+ </div>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ type LoadingSize = 'sm' | 'md' | 'lg'
23
+
24
+ withDefaults(defineProps<{
25
+ size?: LoadingSize
26
+ text?: string
27
+ description?: string
28
+ icon?: string
29
+ showSkeleton?: boolean
30
+ skeletonRows?: number
31
+ }>(), {
32
+ size: 'md',
33
+ text: 'Loading...',
34
+ description: '',
35
+ icon: '',
36
+ showSkeleton: false,
37
+ skeletonRows: 3
38
+ })
39
+ </script>
40
+
41
+ <style scoped>
42
+ .va-loading-state {
43
+ display: flex;
44
+ flex-direction: column;
45
+ align-items: center;
46
+ justify-content: center;
47
+ animation: vaLoadingFadeIn 0.3s ease-out;
48
+ }
49
+
50
+ @keyframes vaLoadingFadeIn {
51
+ from { opacity: 0; }
52
+ to { opacity: 1; }
53
+ }
54
+
55
+ .va-loading-state--sm { padding: 2rem 1rem; }
56
+ .va-loading-state--md { padding: 3rem 1.5rem; }
57
+ .va-loading-state--lg { padding: 5rem 2rem; min-height: 400px; }
58
+
59
+ .va-loading-state__container {
60
+ display: flex;
61
+ flex-direction: column;
62
+ align-items: center;
63
+ gap: 1.25rem;
64
+ }
65
+
66
+ .va-loading-state__ring {
67
+ position: relative;
68
+ width: 48px;
69
+ height: 48px;
70
+ }
71
+
72
+ .va-loading-state--sm .va-loading-state__ring { width: 32px; height: 32px; }
73
+ .va-loading-state--lg .va-loading-state__ring { width: 64px; height: 64px; }
74
+
75
+ .va-loading-state__ring::before {
76
+ content: '';
77
+ position: absolute;
78
+ inset: 0;
79
+ border: 2px solid rgba(212, 175, 55, 0.1);
80
+ border-radius: 50%;
81
+ }
82
+
83
+ .va-loading-state__ring-inner {
84
+ position: absolute;
85
+ inset: 0;
86
+ border: 2px solid transparent;
87
+ border-top-color: #d4af37;
88
+ border-radius: 50%;
89
+ animation: vaLoadingSpin 1s linear infinite;
90
+ }
91
+
92
+ @keyframes vaLoadingSpin {
93
+ to { transform: rotate(360deg); }
94
+ }
95
+
96
+ .va-loading-state__icon {
97
+ position: absolute;
98
+ top: 50%;
99
+ left: 50%;
100
+ transform: translate(-50%, -50%);
101
+ color: #d4af37;
102
+ opacity: 0.6;
103
+ }
104
+
105
+ .va-loading-state--sm .va-loading-state__icon { width: 14px; height: 14px; }
106
+ .va-loading-state--md .va-loading-state__icon { width: 18px; height: 18px; }
107
+ .va-loading-state--lg .va-loading-state__icon { width: 24px; height: 24px; }
108
+
109
+ .va-loading-state__content { text-align: center; }
110
+
111
+ .va-loading-state__text {
112
+ font-size: 0.875rem;
113
+ font-weight: 600;
114
+ color: var(--color-text, #e8eaed);
115
+ letter-spacing: 0.02em;
116
+ }
117
+
118
+ .va-loading-state__description {
119
+ font-size: 0.8125rem;
120
+ color: var(--color-text-muted, #9ca3af);
121
+ margin-top: 0.25rem;
122
+ }
123
+
124
+ .va-loading-state__skeleton {
125
+ width: 100%;
126
+ max-width: 600px;
127
+ margin-top: 2rem;
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 0.75rem;
131
+ }
132
+
133
+ .va-loading-state__skeleton-row {
134
+ height: 1rem;
135
+ border-radius: 6px;
136
+ }
137
+
138
+ .va-loading-state__skeleton-row:nth-child(odd) { width: 100%; }
139
+ .va-loading-state__skeleton-row:nth-child(even) { width: 75%; }
140
+ </style>
@@ -0,0 +1,129 @@
1
+ <template>
2
+ <div class="va-metric-card" :style="{ animationDelay }">
3
+ <div class="va-metric-accent" />
4
+ <div class="va-metric-content">
5
+ <div class="va-metric-label">{{ label }}</div>
6
+ <div class="va-metric-value" :class="{ 'va-metric-date': isDate }">
7
+ <slot>{{ value }}</slot>
8
+ <span v-if="unit" class="va-metric-unit">{{ unit }}</span>
9
+ </div>
10
+ </div>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ defineProps<{
16
+ label: string
17
+ value?: string | number
18
+ unit?: string
19
+ isDate?: boolean
20
+ animationDelay?: string
21
+ }>()
22
+ </script>
23
+
24
+ <style scoped>
25
+ @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;500;600;700&display=swap');
26
+
27
+ .va-metric-card {
28
+ position: relative;
29
+ background: rgba(255, 255, 255, 0.03);
30
+ backdrop-filter: blur(20px);
31
+ border: 1px solid rgba(212, 175, 55, 0.15);
32
+ border-radius: 16px;
33
+ padding: 2rem;
34
+ overflow: hidden;
35
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
36
+ animation: fadeInUp 0.6s ease-out backwards;
37
+ }
38
+
39
+ .va-metric-card::before {
40
+ content: '';
41
+ position: absolute;
42
+ top: 0;
43
+ left: 0;
44
+ right: 0;
45
+ bottom: 0;
46
+ background: linear-gradient(
47
+ 135deg,
48
+ rgba(212, 175, 55, 0.03) 0%,
49
+ transparent 100%
50
+ );
51
+ opacity: 0;
52
+ transition: opacity 0.4s ease;
53
+ }
54
+
55
+ .va-metric-card:hover {
56
+ background: rgba(255, 255, 255, 0.06);
57
+ border-color: #d4af37;
58
+ transform: translateY(-4px);
59
+ box-shadow:
60
+ 0 12px 40px rgba(0, 0, 0, 0.3),
61
+ 0 0 0 1px #d4af37;
62
+ }
63
+
64
+ .va-metric-card:hover::before {
65
+ opacity: 1;
66
+ }
67
+
68
+ .va-metric-accent {
69
+ position: absolute;
70
+ top: 0;
71
+ left: 0;
72
+ width: 3px;
73
+ height: 0;
74
+ background: linear-gradient(
75
+ 180deg,
76
+ #d4af37 0%,
77
+ #e8c96f 100%
78
+ );
79
+ transition: height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
80
+ }
81
+
82
+ .va-metric-card:hover .va-metric-accent {
83
+ height: 100%;
84
+ }
85
+
86
+ .va-metric-content {
87
+ position: relative;
88
+ z-index: 1;
89
+ }
90
+
91
+ .va-metric-label {
92
+ font-size: 0.75rem;
93
+ font-weight: 600;
94
+ color: #9ca3af;
95
+ text-transform: uppercase;
96
+ letter-spacing: 0.1em;
97
+ margin-bottom: 0.75rem;
98
+ }
99
+
100
+ .va-metric-value {
101
+ font-family: 'Cormorant Garamond', serif;
102
+ font-size: 2.25rem;
103
+ font-weight: 600;
104
+ color: #faf8f3;
105
+ line-height: 1.2;
106
+ letter-spacing: -0.01em;
107
+ }
108
+
109
+ .va-metric-unit {
110
+ font-size: 1.5rem;
111
+ opacity: 0.7;
112
+ margin-left: 0.125rem;
113
+ }
114
+
115
+ .va-metric-date {
116
+ font-size: 1.75rem;
117
+ }
118
+
119
+ @keyframes fadeInUp {
120
+ from {
121
+ opacity: 0;
122
+ transform: translateY(20px);
123
+ }
124
+ to {
125
+ opacity: 1;
126
+ transform: translateY(0);
127
+ }
128
+ }
129
+ </style>
@@ -0,0 +1,195 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * VAModalBase - Custom Modal Component
4
+ * Pure Vue/Tailwind implementation without UModal
5
+ *
6
+ * Features:
7
+ * - Size variants: sm, md, lg, xl, full
8
+ * - Backdrop with optional blur
9
+ * - Accessible: focus trap, escape key, click outside to close
10
+ * - Header/body/footer slots
11
+ * - Smooth enter/leave transitions
12
+ */
13
+ import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
14
+
15
+ const props = withDefaults(defineProps<{
16
+ modelValue?: boolean
17
+ title?: string
18
+ size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
19
+ closable?: boolean
20
+ closeOnEscape?: boolean
21
+ closeOnClickOutside?: boolean
22
+ showBackdrop?: boolean
23
+ backdropBlur?: boolean
24
+ }>(), {
25
+ modelValue: false,
26
+ title: '',
27
+ size: 'md',
28
+ closable: true,
29
+ closeOnEscape: true,
30
+ closeOnClickOutside: true,
31
+ showBackdrop: true,
32
+ backdropBlur: true
33
+ })
34
+
35
+ const emit = defineEmits<{
36
+ 'update:modelValue': [value: boolean]
37
+ 'open': []
38
+ 'close': []
39
+ }>()
40
+
41
+ // Modal ref for focus trap
42
+ const modalRef = ref<HTMLElement | null>(null)
43
+ const { activate, deactivate } = useFocusTrap(modalRef, {
44
+ immediate: false,
45
+ allowOutsideClick: true,
46
+ escapeDeactivates: false // We handle escape ourselves
47
+ })
48
+
49
+ // Computed open state
50
+ const isOpen = computed({
51
+ get: () => props.modelValue,
52
+ set: (val) => emit('update:modelValue', val)
53
+ })
54
+
55
+ // Close modal
56
+ const close = () => {
57
+ if (!props.closable) return
58
+ isOpen.value = false
59
+ emit('close')
60
+ }
61
+
62
+ // Open modal
63
+ const open = () => {
64
+ isOpen.value = true
65
+ emit('open')
66
+ }
67
+
68
+ // Handle escape key
69
+ const handleKeydown = (e: KeyboardEvent) => {
70
+ if (e.key === 'Escape' && props.closeOnEscape && props.closable) {
71
+ close()
72
+ }
73
+ }
74
+
75
+ // Handle click outside
76
+ const handleBackdropClick = (e: MouseEvent) => {
77
+ if (e.target === e.currentTarget && props.closeOnClickOutside && props.closable) {
78
+ close()
79
+ }
80
+ }
81
+
82
+ // Watch for open state changes
83
+ watch(isOpen, (val) => {
84
+ if (val) {
85
+ document.body.style.overflow = 'hidden'
86
+ nextTick(() => {
87
+ activate()
88
+ })
89
+ } else {
90
+ document.body.style.overflow = ''
91
+ deactivate()
92
+ }
93
+ })
94
+
95
+ // Cleanup on unmount
96
+ onUnmounted(() => {
97
+ document.body.style.overflow = ''
98
+ deactivate()
99
+ })
100
+
101
+ // Size classes
102
+ const sizeClasses = computed(() => {
103
+ switch (props.size) {
104
+ case 'sm': return 'max-w-sm'
105
+ case 'md': return 'max-w-md'
106
+ case 'lg': return 'max-w-lg'
107
+ case 'xl': return 'max-w-2xl'
108
+ case 'full': return 'max-w-[calc(100vw-2rem)] max-h-[calc(100vh-2rem)] h-full'
109
+ default: return 'max-w-md'
110
+ }
111
+ })
112
+
113
+ // Expose methods
114
+ defineExpose({ open, close })
115
+ </script>
116
+
117
+ <template>
118
+ <Teleport to="body">
119
+ <Transition
120
+ enter-active-class="duration-200 ease-out"
121
+ enter-from-class="opacity-0"
122
+ enter-to-class="opacity-100"
123
+ leave-active-class="duration-150 ease-in"
124
+ leave-from-class="opacity-100"
125
+ leave-to-class="opacity-0"
126
+ >
127
+ <div
128
+ v-if="isOpen"
129
+ class="fixed inset-0 z-50 flex items-center justify-center p-4"
130
+ :class="[
131
+ showBackdrop ? (backdropBlur ? 'bg-gray-900/60 backdrop-blur-sm' : 'bg-gray-900/60') : ''
132
+ ]"
133
+ @click="handleBackdropClick"
134
+ @keydown="handleKeydown"
135
+ >
136
+ <Transition
137
+ enter-active-class="duration-200 ease-out"
138
+ enter-from-class="opacity-0 scale-95 translate-y-4"
139
+ enter-to-class="opacity-100 scale-100 translate-y-0"
140
+ leave-active-class="duration-150 ease-in"
141
+ leave-from-class="opacity-100 scale-100 translate-y-0"
142
+ leave-to-class="opacity-0 scale-95 translate-y-4"
143
+ >
144
+ <div
145
+ v-if="isOpen"
146
+ ref="modalRef"
147
+ class="relative w-full bg-white dark:bg-gray-900 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-800 flex flex-col overflow-hidden"
148
+ :class="[sizeClasses, size === 'full' ? '' : 'max-h-[calc(100vh-2rem)]']"
149
+ role="dialog"
150
+ aria-modal="true"
151
+ :aria-labelledby="title ? 'va-modal-title' : undefined"
152
+ >
153
+ <!-- Header -->
154
+ <div
155
+ v-if="title || $slots.header || closable"
156
+ class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800 shrink-0"
157
+ >
158
+ <slot name="header">
159
+ <h2
160
+ v-if="title"
161
+ id="va-modal-title"
162
+ class="text-lg font-semibold text-gray-900 dark:text-white"
163
+ >
164
+ {{ title }}
165
+ </h2>
166
+ </slot>
167
+ <button
168
+ v-if="closable"
169
+ type="button"
170
+ 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"
171
+ aria-label="Close modal"
172
+ @click="close"
173
+ >
174
+ <UIcon name="i-lucide-x" class="w-5 h-5" />
175
+ </button>
176
+ </div>
177
+
178
+ <!-- Body -->
179
+ <div class="flex-1 overflow-y-auto px-6 py-4">
180
+ <slot />
181
+ </div>
182
+
183
+ <!-- Footer -->
184
+ <div
185
+ v-if="$slots.footer"
186
+ class="px-6 py-4 border-t border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800/50 shrink-0"
187
+ >
188
+ <slot name="footer" />
189
+ </div>
190
+ </div>
191
+ </Transition>
192
+ </div>
193
+ </Transition>
194
+ </Teleport>
195
+ </template>
@@ -0,0 +1,92 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * VAModalConfirm - Veristone Confirmation Modal
4
+ */
5
+ const props = withDefaults(defineProps<{
6
+ modelValue?: boolean
7
+ title?: string
8
+ message?: string
9
+ variant?: 'info' | 'warning' | 'danger'
10
+ confirmText?: string
11
+ cancelText?: string
12
+ loading?: boolean
13
+ }>(), {
14
+ modelValue: false,
15
+ title: 'Confirm Action',
16
+ message: 'Are you sure you want to proceed?',
17
+ variant: 'warning',
18
+ confirmText: 'Confirm',
19
+ cancelText: 'Cancel'
20
+ })
21
+
22
+ const emit = defineEmits(['update:modelValue', 'confirm', 'cancel'])
23
+
24
+ const isOpen = computed({
25
+ get: () => props.modelValue,
26
+ set: (val) => emit('update:modelValue', val)
27
+ })
28
+
29
+ const confirmColor = computed(() => {
30
+ switch (props.variant) {
31
+ case 'danger': return 'red'
32
+ case 'warning': return 'orange'
33
+ case 'info': return 'blue'
34
+ default: return 'primary'
35
+ }
36
+ })
37
+
38
+ const iconName = computed(() => {
39
+ switch (props.variant) {
40
+ case 'danger': return 'i-lucide-alert-triangle'
41
+ case 'warning': return 'i-lucide-alert-circle'
42
+ case 'info': return 'i-lucide-info'
43
+ default: return 'i-lucide-help-circle'
44
+ }
45
+ })
46
+
47
+ const handleConfirm = () => {
48
+ emit('confirm')
49
+ if (!props.loading) isOpen.value = false
50
+ }
51
+
52
+ const handleCancel = () => {
53
+ emit('cancel')
54
+ isOpen.value = false
55
+ }
56
+ </script>
57
+
58
+ <template>
59
+ <UModal v-model="isOpen">
60
+ <div class="p-6">
61
+ <div class="flex items-start gap-4">
62
+ <div class="flex-shrink-0 p-2 rounded-full" :class="`bg-${confirmColor}-50 dark:bg-${confirmColor}-900/20 text-${confirmColor}-600 dark:text-${confirmColor}-400`">
63
+ <UIcon :name="iconName" class="w-6 h-6" />
64
+ </div>
65
+ <div class="flex-1">
66
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white leading-6">
67
+ {{ title }}
68
+ </h3>
69
+ <p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
70
+ {{ message }}
71
+ </p>
72
+ </div>
73
+ </div>
74
+
75
+ <div class="mt-6 flex justify-end gap-3">
76
+ <UButton
77
+ :label="cancelText"
78
+ color="gray"
79
+ variant="ghost"
80
+ @click="handleCancel"
81
+ />
82
+ <UButton
83
+ :label="confirmText"
84
+ :color="confirmColor"
85
+ variant="solid"
86
+ :loading="loading"
87
+ @click="handleConfirm"
88
+ />
89
+ </div>
90
+ </div>
91
+ </UModal>
92
+ </template>
@@ -0,0 +1,105 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * VAModalForm - Veristone Form Modal (CRUD aware)
4
+ */
5
+ const props = withDefaults(defineProps<{
6
+ modelValue?: boolean
7
+ title?: string
8
+ endpoint?: string
9
+ recordId?: string | number
10
+ initialData?: any
11
+ size?: string
12
+ loading?: boolean
13
+ error?: string
14
+ }>(), {
15
+ modelValue: false,
16
+ title: 'Edit Record',
17
+ size: 'md'
18
+ })
19
+
20
+ const emit = defineEmits(['update:modelValue', 'submit', 'success', 'error'])
21
+
22
+ const { findOne, create, update, loading: crudLoading, errorState } = useVCrud(props.endpoint || '')
23
+ const internalOpen = ref(false)
24
+ const formData = ref({ ...(props.initialData || {}) })
25
+ const isEdit = computed(() => !!props.recordId)
26
+
27
+ const isOpen = computed({
28
+ get: () => props.modelValue !== undefined ? props.modelValue : internalOpen.value,
29
+ set: (val) => {
30
+ emit('update:modelValue', val)
31
+ internalOpen.value = val
32
+ }
33
+ })
34
+
35
+ // Fetch data on open if endpoint/id provided
36
+ watch(isOpen, async (val) => {
37
+ if (val && props.endpoint && props.recordId) {
38
+ try {
39
+ formData.value = await findOne(props.recordId)
40
+ } catch (e) {
41
+ // Error handled by composable
42
+ }
43
+ } else if (val && !props.recordId) {
44
+ formData.value = { ...(props.initialData || {}) }
45
+ }
46
+ })
47
+
48
+ const handleSubmit = async (data: any = formData.value) => {
49
+ emit('submit', data)
50
+
51
+ if (props.endpoint) {
52
+ try {
53
+ if (isEdit.value) {
54
+ await update(props.recordId!, data)
55
+ } else {
56
+ await create(data)
57
+ }
58
+ emit('success')
59
+ isOpen.value = false
60
+ } catch (e) {
61
+ emit('error', e)
62
+ }
63
+ }
64
+ }
65
+ </script>
66
+
67
+ <template>
68
+ <UModal v-model="isOpen" :ui="{ width: size === 'lg' ? 'sm:max-w-4xl' : 'sm:max-w-xl' }">
69
+ <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
70
+ <template #header>
71
+ <div class="flex items-center justify-between">
72
+ <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
73
+ {{ title }}
74
+ </h3>
75
+ <UButton color="gray" variant="ghost" icon="i-lucide-x" class="-my-1" @click="isOpen = false" />
76
+ </div>
77
+ </template>
78
+
79
+ <div class="p-6">
80
+ <slot
81
+ :state="formData"
82
+ :loading="loading || crudLoading"
83
+ :error="error || errorState"
84
+ :submit="handleSubmit"
85
+ >
86
+ <!-- Default content if no slot -->
87
+ <form @submit.prevent="handleSubmit()">
88
+ <div class="space-y-4">
89
+ <!-- If we knew fields we'd render them, but this is generic -->
90
+ <p class="text-gray-500 italic">Form content goes here...</p>
91
+ </div>
92
+ </form>
93
+ </slot>
94
+
95
+ <div v-if="error || errorState" class="mt-4 p-3 rounded bg-red-50 text-red-600 text-sm">
96
+ {{ error || errorState }}
97
+ </div>
98
+ </div>
99
+
100
+ <template v-if="$slots.footer" #footer>
101
+ <slot name="footer" :submit="handleSubmit" :loading="loading || crudLoading" />
102
+ </template>
103
+ </UCard>
104
+ </UModal>
105
+ </template>