@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,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useVAAnimation - Animation utilities for Veristone components
|
|
3
|
+
* Provides stagger delays, scroll reveal, and animated counters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface RevealOptions {
|
|
7
|
+
threshold?: number
|
|
8
|
+
rootMargin?: string
|
|
9
|
+
once?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CountUpOptions {
|
|
13
|
+
duration?: number
|
|
14
|
+
easing?: (t: number) => number
|
|
15
|
+
separator?: string
|
|
16
|
+
decimal?: string
|
|
17
|
+
decimals?: number
|
|
18
|
+
prefix?: string
|
|
19
|
+
suffix?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useVAAnimation() {
|
|
23
|
+
/**
|
|
24
|
+
* Calculate stagger delay for list animations
|
|
25
|
+
* @param index - Item index in the list
|
|
26
|
+
* @param baseDelay - Base delay in milliseconds (default: 50)
|
|
27
|
+
* @returns CSS delay string
|
|
28
|
+
*/
|
|
29
|
+
const staggerDelay = (index: number, baseDelay = 50): string => {
|
|
30
|
+
return `${index * baseDelay}ms`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate stagger delay style object for v-bind
|
|
35
|
+
* @param index - Item index
|
|
36
|
+
* @param baseDelay - Base delay in ms
|
|
37
|
+
*/
|
|
38
|
+
const staggerStyle = (index: number, baseDelay = 50) => ({
|
|
39
|
+
animationDelay: staggerDelay(index, baseDelay)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Intersection Observer hook for scroll reveal animations
|
|
44
|
+
* @param options - Observer options
|
|
45
|
+
*/
|
|
46
|
+
const useRevealOnScroll = (options: RevealOptions = {}) => {
|
|
47
|
+
const {
|
|
48
|
+
threshold = 0.1,
|
|
49
|
+
rootMargin = '0px 0px -50px 0px',
|
|
50
|
+
once = true
|
|
51
|
+
} = options
|
|
52
|
+
|
|
53
|
+
const elementRef = shallowRef<HTMLElement | null>(null)
|
|
54
|
+
const isVisible = ref(false)
|
|
55
|
+
|
|
56
|
+
let observer: IntersectionObserver | null = null
|
|
57
|
+
|
|
58
|
+
const observe = () => {
|
|
59
|
+
const el = elementRef.value
|
|
60
|
+
if (!el || typeof IntersectionObserver === 'undefined') {
|
|
61
|
+
isVisible.value = true
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
observer = new IntersectionObserver(
|
|
66
|
+
(entries) => {
|
|
67
|
+
entries.forEach((entry) => {
|
|
68
|
+
if (entry.isIntersecting) {
|
|
69
|
+
isVisible.value = true
|
|
70
|
+
if (once && observer) {
|
|
71
|
+
observer.disconnect()
|
|
72
|
+
}
|
|
73
|
+
} else if (!once) {
|
|
74
|
+
isVisible.value = false
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
},
|
|
78
|
+
{ threshold, rootMargin }
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
observer.observe(el)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const unobserve = () => {
|
|
85
|
+
if (observer) {
|
|
86
|
+
observer.disconnect()
|
|
87
|
+
observer = null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onMounted(observe)
|
|
92
|
+
onUnmounted(unobserve)
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
elementRef,
|
|
96
|
+
isVisible
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Animated number counter
|
|
102
|
+
* @param endValue - Target value to count to
|
|
103
|
+
* @param options - Animation options
|
|
104
|
+
*/
|
|
105
|
+
const useCountUp = (
|
|
106
|
+
endValue: Ref<number> | number,
|
|
107
|
+
options: CountUpOptions = {}
|
|
108
|
+
) => {
|
|
109
|
+
const {
|
|
110
|
+
duration = 1000,
|
|
111
|
+
easing = easeOutExpo,
|
|
112
|
+
separator = ',',
|
|
113
|
+
decimal = '.',
|
|
114
|
+
decimals = 0,
|
|
115
|
+
prefix = '',
|
|
116
|
+
suffix = ''
|
|
117
|
+
} = options
|
|
118
|
+
|
|
119
|
+
const displayValue = ref('0')
|
|
120
|
+
const currentValue = ref(0)
|
|
121
|
+
let animationFrame: number | null = null
|
|
122
|
+
let startTime: number | null = null
|
|
123
|
+
let startValue = 0
|
|
124
|
+
|
|
125
|
+
const formatNumber = (num: number): string => {
|
|
126
|
+
const fixed = num.toFixed(decimals)
|
|
127
|
+
const parts = fixed.split('.')
|
|
128
|
+
const intPart = parts[0] || '0'
|
|
129
|
+
const decPart = parts[1]
|
|
130
|
+
|
|
131
|
+
const formattedInt = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, separator)
|
|
132
|
+
|
|
133
|
+
if (decimals > 0 && decPart) {
|
|
134
|
+
return `${prefix}${formattedInt}${decimal}${decPart}${suffix}`
|
|
135
|
+
}
|
|
136
|
+
return `${prefix}${formattedInt}${suffix}`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const animate = (timestamp: number) => {
|
|
140
|
+
if (!startTime) startTime = timestamp
|
|
141
|
+
|
|
142
|
+
const elapsed = timestamp - startTime
|
|
143
|
+
const progress = Math.min(elapsed / duration, 1)
|
|
144
|
+
const easedProgress = easing(progress)
|
|
145
|
+
|
|
146
|
+
const end = unref(endValue)
|
|
147
|
+
currentValue.value = startValue + (end - startValue) * easedProgress
|
|
148
|
+
displayValue.value = formatNumber(currentValue.value)
|
|
149
|
+
|
|
150
|
+
if (progress < 1) {
|
|
151
|
+
animationFrame = requestAnimationFrame(animate)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const start = (from = 0) => {
|
|
156
|
+
if (animationFrame) {
|
|
157
|
+
cancelAnimationFrame(animationFrame)
|
|
158
|
+
}
|
|
159
|
+
startValue = from
|
|
160
|
+
startTime = null
|
|
161
|
+
animationFrame = requestAnimationFrame(animate)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const reset = () => {
|
|
165
|
+
if (animationFrame) {
|
|
166
|
+
cancelAnimationFrame(animationFrame)
|
|
167
|
+
}
|
|
168
|
+
currentValue.value = 0
|
|
169
|
+
displayValue.value = formatNumber(0)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Watch for value changes
|
|
173
|
+
if (isRef(endValue)) {
|
|
174
|
+
watch(endValue, (_newVal, oldVal) => {
|
|
175
|
+
start(oldVal || 0)
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
onMounted(() => {
|
|
180
|
+
start(0)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
onUnmounted(() => {
|
|
184
|
+
if (animationFrame) {
|
|
185
|
+
cancelAnimationFrame(animationFrame)
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
displayValue,
|
|
191
|
+
currentValue,
|
|
192
|
+
start,
|
|
193
|
+
reset
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Animate a list of items with staggered entrance
|
|
199
|
+
* @param items - Reactive array of items
|
|
200
|
+
* @param delay - Delay between items in ms
|
|
201
|
+
*/
|
|
202
|
+
const useStaggeredList = <T>(items: Ref<T[]>, delay = 50) => {
|
|
203
|
+
const visibleItems = shallowRef<T[]>([])
|
|
204
|
+
let timeouts: ReturnType<typeof setTimeout>[] = []
|
|
205
|
+
|
|
206
|
+
const animateIn = () => {
|
|
207
|
+
// Clear existing timeouts
|
|
208
|
+
timeouts.forEach(clearTimeout)
|
|
209
|
+
timeouts = []
|
|
210
|
+
visibleItems.value = []
|
|
211
|
+
|
|
212
|
+
items.value.forEach((item, index) => {
|
|
213
|
+
const timeout = setTimeout(() => {
|
|
214
|
+
visibleItems.value = [...visibleItems.value, item]
|
|
215
|
+
}, index * delay)
|
|
216
|
+
timeouts.push(timeout)
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const animateOut = () => {
|
|
221
|
+
timeouts.forEach(clearTimeout)
|
|
222
|
+
timeouts = []
|
|
223
|
+
visibleItems.value = []
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
watch(items, animateIn, { immediate: true, deep: true })
|
|
227
|
+
|
|
228
|
+
onUnmounted(() => {
|
|
229
|
+
timeouts.forEach(clearTimeout)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
visibleItems,
|
|
234
|
+
animateIn,
|
|
235
|
+
animateOut
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Typewriter effect for text
|
|
241
|
+
* @param text - Text to type
|
|
242
|
+
* @param speed - Characters per second
|
|
243
|
+
*/
|
|
244
|
+
const useTypewriter = (text: Ref<string> | string, speed = 50) => {
|
|
245
|
+
const displayText = ref('')
|
|
246
|
+
let index = 0
|
|
247
|
+
let interval: ReturnType<typeof setInterval> | null = null
|
|
248
|
+
|
|
249
|
+
const start = () => {
|
|
250
|
+
const fullText = unref(text)
|
|
251
|
+
index = 0
|
|
252
|
+
displayText.value = ''
|
|
253
|
+
|
|
254
|
+
if (interval) clearInterval(interval)
|
|
255
|
+
|
|
256
|
+
interval = setInterval(() => {
|
|
257
|
+
if (index < fullText.length) {
|
|
258
|
+
displayText.value += fullText[index]
|
|
259
|
+
index++
|
|
260
|
+
} else {
|
|
261
|
+
if (interval) clearInterval(interval)
|
|
262
|
+
}
|
|
263
|
+
}, 1000 / speed)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const reset = () => {
|
|
267
|
+
if (interval) clearInterval(interval)
|
|
268
|
+
displayText.value = ''
|
|
269
|
+
index = 0
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (isRef(text)) {
|
|
273
|
+
watch(text, start)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
onMounted(start)
|
|
277
|
+
onUnmounted(() => {
|
|
278
|
+
if (interval) clearInterval(interval)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
displayText,
|
|
283
|
+
start,
|
|
284
|
+
reset
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
staggerDelay,
|
|
290
|
+
staggerStyle,
|
|
291
|
+
useRevealOnScroll,
|
|
292
|
+
useCountUp,
|
|
293
|
+
useStaggeredList,
|
|
294
|
+
useTypewriter
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Easing functions
|
|
299
|
+
function easeOutExpo(x: number): number {
|
|
300
|
+
return x === 1 ? 1 : 1 - Math.pow(2, -10 * x)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function easeOutCubic(x: number): number {
|
|
304
|
+
return 1 - Math.pow(1 - x, 3)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function easeOutQuart(x: number): number {
|
|
308
|
+
return 1 - Math.pow(1 - x, 4)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function easeInOutQuad(x: number): number {
|
|
312
|
+
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Export easing functions for custom use
|
|
316
|
+
export const easings = {
|
|
317
|
+
easeOutExpo,
|
|
318
|
+
easeOutCubic,
|
|
319
|
+
easeOutQuart,
|
|
320
|
+
easeInOutQuad
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Default export for convenience
|
|
324
|
+
export default useVAAnimation
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VA Utility Composable
|
|
3
|
+
* Common utility functions for the V-App layer
|
|
4
|
+
*/
|
|
5
|
+
export function useVAUtils() {
|
|
6
|
+
/**
|
|
7
|
+
* Format a number with thousand separators
|
|
8
|
+
*/
|
|
9
|
+
const formatNumber = (value: number): string => {
|
|
10
|
+
return new Intl.NumberFormat().format(value)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format currency
|
|
15
|
+
*/
|
|
16
|
+
const formatCurrency = (value: number, currency = 'USD'): string => {
|
|
17
|
+
return new Intl.NumberFormat('en-US', {
|
|
18
|
+
style: 'currency',
|
|
19
|
+
currency,
|
|
20
|
+
minimumFractionDigits: 0,
|
|
21
|
+
maximumFractionDigits: 0
|
|
22
|
+
}).format(value)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Format percentage
|
|
27
|
+
*/
|
|
28
|
+
const formatPercent = (value: number, decimals = 1): string => {
|
|
29
|
+
const sign = value >= 0 ? '+' : ''
|
|
30
|
+
return `${sign}${value.toFixed(decimals)}%`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format relative time (e.g., "2 min ago")
|
|
35
|
+
*/
|
|
36
|
+
const formatRelativeTime = (date: Date | string): string => {
|
|
37
|
+
const now = new Date()
|
|
38
|
+
const then = new Date(date)
|
|
39
|
+
const diffMs = now.getTime() - then.getTime()
|
|
40
|
+
const diffMins = Math.floor(diffMs / 60000)
|
|
41
|
+
const diffHours = Math.floor(diffMs / 3600000)
|
|
42
|
+
const diffDays = Math.floor(diffMs / 86400000)
|
|
43
|
+
|
|
44
|
+
if (diffMins < 1) return 'Just now'
|
|
45
|
+
if (diffMins < 60) return `${diffMins} min ago`
|
|
46
|
+
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`
|
|
47
|
+
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`
|
|
48
|
+
return then.toLocaleDateString()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Truncate text with ellipsis
|
|
53
|
+
*/
|
|
54
|
+
const truncate = (text: string, length: number): string => {
|
|
55
|
+
if (text.length <= length) return text
|
|
56
|
+
return text.slice(0, length) + '...'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate initials from name
|
|
61
|
+
*/
|
|
62
|
+
const getInitials = (name: string): string => {
|
|
63
|
+
return name
|
|
64
|
+
.split(' ')
|
|
65
|
+
.map(part => part[0])
|
|
66
|
+
.join('')
|
|
67
|
+
.toUpperCase()
|
|
68
|
+
.slice(0, 2)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const debounce = <T extends (...args: unknown[]) => unknown>(
|
|
72
|
+
fn: T,
|
|
73
|
+
delay: number
|
|
74
|
+
): ((...args: Parameters<T>) => void) => {
|
|
75
|
+
let timeoutId: ReturnType<typeof setTimeout>
|
|
76
|
+
return (...args: Parameters<T>) => {
|
|
77
|
+
clearTimeout(timeoutId)
|
|
78
|
+
timeoutId = setTimeout(() => fn(...args), delay)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type StatusColor = 'success' | 'warning' | 'error' | 'neutral' | 'info'
|
|
83
|
+
const statusColorMap: Record<string, StatusColor> = {
|
|
84
|
+
performing: 'success',
|
|
85
|
+
current: 'success',
|
|
86
|
+
default_30: 'warning',
|
|
87
|
+
default_60: 'warning',
|
|
88
|
+
default_90: 'error',
|
|
89
|
+
default_120: 'error',
|
|
90
|
+
default_maturity: 'error',
|
|
91
|
+
delinquent: 'error',
|
|
92
|
+
paid_off: 'neutral',
|
|
93
|
+
pending: 'warning',
|
|
94
|
+
approved: 'success',
|
|
95
|
+
rejected: 'error',
|
|
96
|
+
expired: 'neutral',
|
|
97
|
+
paid: 'success',
|
|
98
|
+
processing: 'info',
|
|
99
|
+
active: 'success',
|
|
100
|
+
inactive: 'neutral',
|
|
101
|
+
draft: 'neutral'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const getStatusColor = (status: string): StatusColor => {
|
|
105
|
+
return statusColorMap[status] || 'neutral'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
formatNumber,
|
|
110
|
+
formatCurrency,
|
|
111
|
+
formatPercent,
|
|
112
|
+
formatRelativeTime,
|
|
113
|
+
truncate,
|
|
114
|
+
getInitials,
|
|
115
|
+
debounce,
|
|
116
|
+
getStatusColor
|
|
117
|
+
}
|
|
118
|
+
}
|