cynx-lib 1.3.84 → 1.3.85

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 (40) hide show
  1. package/gen.js +77 -77
  2. package/package.json +5 -1
  3. package/ui/components/Drawer.jsx +208 -208
  4. package/ui/components/Pagination.jsx +145 -145
  5. package/ui/components/PrivacyBlur.jsx +603 -603
  6. package/utils/index.cjs +5 -0
  7. package/utils/index.js +22 -20
  8. package/utils/utils/duration.js +139 -139
  9. package/utils/utils/oidc.js +214 -214
  10. package/utils/utils/realtime.js +1634 -1634
  11. package/utils/utils/session.cjs +15 -2
  12. package/utils/utils/session.js +67 -58
  13. package/utils/utils/session_manager.cjs +490 -0
  14. package/utils/utils/session_manager.js +549 -0
  15. package/utils/utils/storage.js +393 -393
  16. package/utils/utils/ui.js +111 -111
  17. package/vue/components/Badge.vue +79 -79
  18. package/vue/components/Button.vue +209 -209
  19. package/vue/components/Checkbox.vue +103 -103
  20. package/vue/components/CircularProgress.vue +383 -383
  21. package/vue/components/Divider.vue +94 -94
  22. package/vue/components/Drawer.vue +243 -243
  23. package/vue/components/InfoBadge.vue +223 -223
  24. package/vue/components/Input.vue +341 -341
  25. package/vue/components/LinearProgress.vue +268 -268
  26. package/vue/components/Modal.vue +226 -226
  27. package/vue/components/Pagination.vue +199 -199
  28. package/vue/components/Select.vue +713 -713
  29. package/vue/components/Sidebar.vue +955 -955
  30. package/vue/components/Skeleton.vue +91 -91
  31. package/vue/components/Spinner.vue +263 -263
  32. package/vue/components/StatusTag.vue +89 -89
  33. package/vue/components/TabBar.vue +176 -176
  34. package/vue/components/Table.vue +388 -388
  35. package/vue/components/TimePicker.vue +399 -399
  36. package/vue/components/Toggle.vue +115 -115
  37. package/vue/components/XTable.vue +119 -119
  38. package/vue/composables/useAutoSkeletonRows.ts +71 -71
  39. package/vue/index.d.ts +67 -67
  40. package/vue/index.js +86 -86
@@ -1,388 +1,388 @@
1
- <script setup lang="ts">
2
- import { computed, ref, onMounted, onUnmounted, watch, nextTick } from "vue";
3
- import {
4
- ChevronUp,
5
- ChevronDown,
6
- ArrowUpDown,
7
- FileText,
8
- } from "lucide-vue-next";
9
- import Skeleton from "./Skeleton.vue";
10
- import { useAutoSkeletonRows } from "../composables/useAutoSkeletonRows";
11
-
12
- export interface TableColumn {
13
- key: string;
14
- title?: string;
15
- label?: string;
16
- width?: string | number;
17
- minWidth?: string | number;
18
- align?: "left" | "center" | "right";
19
- sortable?: boolean;
20
- fixed?: "left" | "right" | boolean;
21
- headerClass?: string;
22
- cellClass?: string;
23
- }
24
-
25
- const props = withDefaults(
26
- defineProps<{
27
- columns?: TableColumn[];
28
- data?: any[];
29
- loading?: boolean;
30
- rowKey?: string | ((row: any) => string | number);
31
- sortBy?: string;
32
- sortOrder?: "asc" | "desc" | string;
33
- emptyText?: string;
34
- height?: string | number;
35
- hoverable?: boolean;
36
- striped?: boolean;
37
- bordered?: boolean;
38
- skeletonRows?: number;
39
- autoSkeleton?: boolean;
40
- rowHeight?: number;
41
- }>(),
42
- {
43
- columns: () => [],
44
- data: () => [],
45
- loading: false,
46
- rowKey: "id",
47
- sortBy: "",
48
- sortOrder: "",
49
- emptyText: "No data available",
50
- hoverable: true,
51
- striped: false,
52
- bordered: true,
53
- skeletonRows: undefined,
54
- autoSkeleton: true,
55
- rowHeight: 40,
56
- }
57
- );
58
-
59
- const emit = defineEmits<{
60
- (e: "row-click", row: any, event: MouseEvent): void;
61
- (e: "sort-change", payload: { key: string; order: "asc" | "desc" | null }): void;
62
- }>();
63
-
64
- const rootRef = ref<HTMLElement | null>(null);
65
- const headerScrollRef = ref<HTMLElement | null>(null);
66
- const bodyScrollRef = ref<HTMLElement | null>(null);
67
- const gutterWidth = ref(0);
68
-
69
- const updateGutter = () => {
70
- if (bodyScrollRef.value) {
71
- const hasVScroll = bodyScrollRef.value.scrollHeight > bodyScrollRef.value.clientHeight;
72
- if (hasVScroll) {
73
- const scrollWidth = bodyScrollRef.value.offsetWidth - bodyScrollRef.value.clientWidth;
74
- gutterWidth.value = scrollWidth > 0 ? scrollWidth : 6;
75
- } else {
76
- gutterWidth.value = 0;
77
- }
78
- } else {
79
- gutterWidth.value = 0;
80
- }
81
- };
82
-
83
- let resizeObserver: ResizeObserver | null = null;
84
-
85
- onMounted(() => {
86
- if (bodyScrollRef.value && typeof ResizeObserver !== "undefined") {
87
- resizeObserver = new ResizeObserver(() => {
88
- updateGutter();
89
- });
90
- resizeObserver.observe(bodyScrollRef.value);
91
- }
92
- nextTick(updateGutter);
93
- });
94
-
95
- onUnmounted(() => {
96
- resizeObserver?.disconnect();
97
- });
98
-
99
- watch(
100
- () => [props.data, props.columns, props.loading],
101
- () => {
102
- nextTick(updateGutter);
103
- },
104
- { deep: true }
105
- );
106
-
107
- const handleBodyScroll = () => {
108
- if (headerScrollRef.value && bodyScrollRef.value) {
109
- headerScrollRef.value.scrollLeft = bodyScrollRef.value.scrollLeft;
110
- }
111
- };
112
-
113
- const { rows: autoRows } = useAutoSkeletonRows(rootRef, {
114
- rowHeight: props.rowHeight,
115
- bottomBuffer: 50,
116
- });
117
-
118
- const skeletonCount = computed(() => {
119
- if (props.skeletonRows && props.skeletonRows > 0) return props.skeletonRows;
120
- if (props.autoSkeleton) return autoRows.value;
121
- return 10;
122
- });
123
-
124
- const getRowKey = (row: any, index: number) => {
125
- if (typeof props.rowKey === "function") return props.rowKey(row);
126
- if (typeof props.rowKey === "string" && row[props.rowKey] !== undefined) return row[props.rowKey];
127
- return index;
128
- };
129
-
130
- const handleSort = (col: TableColumn) => {
131
- if (!col.sortable) return;
132
- const isCurrent = props.sortBy === col.key;
133
- let nextOrder: "asc" | "desc" | null = "asc";
134
-
135
- if (isCurrent) {
136
- if (props.sortOrder === "asc") nextOrder = "desc";
137
- else if (props.sortOrder === "desc") nextOrder = null;
138
- else nextOrder = "asc";
139
- }
140
-
141
- emit("sort-change", { key: col.key, order: nextOrder });
142
- };
143
-
144
- const formatWidth = (val?: string | number) => {
145
- if (val === undefined || val === null) return undefined;
146
- if (typeof val === "number") return `${val}px`;
147
- return val;
148
- };
149
-
150
- const titleWidths = ["68%", "82%", "55%", "75%", "62%", "88%", "70%", "64%"];
151
- const textWidths = ["80%", "65%", "88%", "72%", "58%", "84%", "62%", "76%"];
152
- const codeWidths = ["50%", "62%", "45%", "58%", "52%", "60%"];
153
-
154
- const getSkeletonWidth = (rIdx: number, cIdx: number, key: string) => {
155
- if (key === "title" || cIdx === 0) {
156
- return titleWidths[(rIdx + cIdx) % titleWidths.length];
157
- }
158
- if (key === "usdot" || key === "phone" || key === "id") {
159
- return codeWidths[(rIdx + cIdx) % codeWidths.length];
160
- }
161
- return textWidths[(rIdx + cIdx) % textWidths.length];
162
- };
163
- </script>
164
-
165
- <template>
166
- <div
167
- ref="rootRef"
168
- class="w-full flex flex-col overflow-hidden transition-colors"
169
- :class="[
170
- bordered ? 'border border-slate-200 dark:border-slate-800' : '',
171
- 'bg-white dark:bg-[#141629]'
172
- ]"
173
- :style="height ? { height: formatWidth(height) } : undefined"
174
- >
175
- <!-- Dedicated Fixed Header Container (Never moves on row scroll) -->
176
- <div
177
- ref="headerScrollRef"
178
- class="w-full overflow-hidden flex-shrink-0 border-b border-slate-200 dark:border-slate-800 bg-[#f8fafc] dark:bg-[#16192e]"
179
- >
180
- <table class="w-full border-collapse text-left text-xs table-fixed">
181
- <colgroup>
182
- <col
183
- v-for="col in columns"
184
- :key="`h-col-${col.key}`"
185
- :style="{
186
- width: formatWidth(col.width),
187
- minWidth: formatWidth(col.minWidth || col.width || 100),
188
- }"
189
- />
190
- <col v-if="gutterWidth > 0" :style="{ width: `${gutterWidth}px` }" />
191
- </colgroup>
192
- <thead>
193
- <tr>
194
- <th
195
- v-for="col in columns"
196
- :key="col.key"
197
- :style="{
198
- width: formatWidth(col.width),
199
- minWidth: formatWidth(col.minWidth || col.width || 100),
200
- textAlign: col.align || 'left',
201
- }"
202
- :class="[
203
- 'px-3.5 py-2.5 font-semibold text-[11px] uppercase tracking-wider text-slate-500 dark:text-slate-400 select-none transition-colors bg-[#f8fafc] dark:bg-[#16192e]',
204
- col.sortable ? 'cursor-pointer hover:text-slate-900 dark:hover:text-white' : '',
205
- col.headerClass || ''
206
- ]"
207
- @click="handleSort(col)"
208
- >
209
- <div
210
- class="flex items-center gap-1.5"
211
- :class="{
212
- 'justify-start': !col.align || col.align === 'left',
213
- 'justify-center': col.align === 'center',
214
- 'justify-end': col.align === 'right',
215
- }"
216
- >
217
- <slot :name="`header-${col.key}`" :column="col">
218
- <span>{{ col.title || col.label || col.key }}</span>
219
- </slot>
220
-
221
- <!-- Sort indicator icon -->
222
- <span v-if="col.sortable" class="inline-flex items-center text-slate-400">
223
- <ChevronUp
224
- v-if="sortBy === col.key && sortOrder === 'asc'"
225
- :size="13"
226
- class="text-[#12675D] dark:text-teal-400"
227
- />
228
- <ChevronDown
229
- v-else-if="sortBy === col.key && sortOrder === 'desc'"
230
- :size="13"
231
- class="text-[#12675D] dark:text-teal-400"
232
- />
233
- <ArrowUpDown
234
- v-else
235
- :size="11"
236
- class="opacity-40 hover:opacity-100 transition-opacity"
237
- />
238
- </span>
239
- </div>
240
- </th>
241
- <th
242
- v-if="gutterWidth > 0"
243
- class="bg-[#f8fafc] dark:bg-[#16192e] p-0 border-0 pointer-events-none"
244
- :style="{ width: `${gutterWidth}px`, minWidth: `${gutterWidth}px` }"
245
- />
246
- </tr>
247
- </thead>
248
- </table>
249
- </div>
250
-
251
- <!-- Dedicated Scrollable Body Container -->
252
- <div
253
- ref="bodyScrollRef"
254
- class="w-full overflow-x-auto overflow-y-auto custom-scrollbar flex-1 relative"
255
- @scroll="handleBodyScroll"
256
- >
257
- <table class="w-full border-collapse text-left text-xs table-fixed">
258
- <colgroup>
259
- <col
260
- v-for="col in columns"
261
- :key="`b-col-${col.key}`"
262
- :style="{
263
- width: formatWidth(col.width),
264
- minWidth: formatWidth(col.minWidth || col.width || 100),
265
- }"
266
- />
267
- </colgroup>
268
- <tbody class="divide-y divide-slate-100 dark:divide-slate-800/80">
269
- <!-- Loading State (Real matching heights and paddings) -->
270
- <template v-if="loading">
271
- <tr
272
- v-for="i in skeletonCount"
273
- :key="`sk-${i}`"
274
- :style="{ height: `${rowHeight}px` }"
275
- >
276
- <td
277
- v-for="(col, cIdx) in columns"
278
- :key="`sk-c-${col.key}`"
279
- :style="{
280
- textAlign: col.align || 'left',
281
- height: `${rowHeight}px`,
282
- }"
283
- class="px-3.5 py-2 align-middle"
284
- >
285
- <!-- Action button skeleton -->
286
- <div v-if="col.key === 'actions'" class="flex items-center justify-center">
287
- <Skeleton :width="28" :height="28" :radius="6" />
288
- </div>
289
-
290
- <!-- Centered badge skeleton -->
291
- <div v-else-if="col.align === 'center'" class="flex items-center justify-center">
292
- <Skeleton :width="44" :height="20" :radius="10" />
293
- </div>
294
-
295
- <!-- Text / Column skeleton -->
296
- <Skeleton
297
- v-else
298
- :width="getSkeletonWidth(i, cIdx, col.key)"
299
- :height="14"
300
- :radius="4"
301
- />
302
- </td>
303
- </tr>
304
- </template>
305
-
306
- <!-- Empty State -->
307
- <tr v-else-if="!data || data.length === 0">
308
- <td :colspan="columns.length || 1" class="py-16 text-center text-slate-400 dark:text-slate-500">
309
- <slot name="empty">
310
- <div class="flex flex-col items-center justify-center gap-2">
311
- <FileText :size="32" class="opacity-30" />
312
- <span class="text-xs">{{ emptyText }}</span>
313
- </div>
314
- </slot>
315
- </td>
316
- </tr>
317
-
318
- <!-- Data Rows -->
319
- <template v-else>
320
- <tr
321
- v-for="(row, rIdx) in data"
322
- :key="getRowKey(row, rIdx)"
323
- :style="{ height: `${rowHeight}px` }"
324
- :class="[
325
- 'transition-colors duration-100',
326
- hoverable ? 'hover:bg-slate-50/80 dark:hover:bg-slate-800/50 cursor-pointer' : '',
327
- striped && rIdx % 2 === 1 ? 'bg-slate-50/40 dark:bg-slate-900/30' : '',
328
- ]"
329
- @click="emit('row-click', row, $event)"
330
- >
331
- <td
332
- v-for="col in columns"
333
- :key="col.key"
334
- :style="{
335
- textAlign: col.align || 'left',
336
- height: `${rowHeight}px`,
337
- }"
338
- :class="[
339
- 'px-3.5 py-2 text-slate-700 dark:text-slate-200 text-xs font-normal align-middle',
340
- col.cellClass || ''
341
- ]"
342
- >
343
- <!-- Dedicated Column Cell Slot (e.g. #cell-title, #cell-actions) or default slot -->
344
- <slot
345
- :name="`cell-${col.key}`"
346
- :row="row"
347
- :column="col"
348
- :index="rIdx"
349
- :value="row[col.key]"
350
- >
351
- <slot
352
- :name="col.key"
353
- :row="row"
354
- :column="col"
355
- :index="rIdx"
356
- :value="row[col.key]"
357
- >
358
- <span>{{ row[col.key] != null && row[col.key] !== '' ? row[col.key] : '-' }}</span>
359
- </slot>
360
- </slot>
361
- </td>
362
- </tr>
363
- </template>
364
- </tbody>
365
- </table>
366
- </div>
367
- </div>
368
- </template>
369
-
370
- <style scoped>
371
- .custom-scrollbar {
372
- scrollbar-width: thin;
373
- }
374
- .custom-scrollbar::-webkit-scrollbar {
375
- width: 6px;
376
- height: 6px;
377
- }
378
- .custom-scrollbar::-webkit-scrollbar-track {
379
- background: transparent;
380
- }
381
- .custom-scrollbar::-webkit-scrollbar-thumb {
382
- background-color: rgba(148, 163, 184, 0.3);
383
- border-radius: 6px;
384
- }
385
- .custom-scrollbar::-webkit-scrollbar-thumb:hover {
386
- background-color: rgba(148, 163, 184, 0.5);
387
- }
388
- </style>
1
+ <script setup lang="ts">
2
+ import { computed, ref, onMounted, onUnmounted, watch, nextTick } from "vue";
3
+ import {
4
+ ChevronUp,
5
+ ChevronDown,
6
+ ArrowUpDown,
7
+ FileText,
8
+ } from "lucide-vue-next";
9
+ import Skeleton from "./Skeleton.vue";
10
+ import { useAutoSkeletonRows } from "../composables/useAutoSkeletonRows";
11
+
12
+ export interface TableColumn {
13
+ key: string;
14
+ title?: string;
15
+ label?: string;
16
+ width?: string | number;
17
+ minWidth?: string | number;
18
+ align?: "left" | "center" | "right";
19
+ sortable?: boolean;
20
+ fixed?: "left" | "right" | boolean;
21
+ headerClass?: string;
22
+ cellClass?: string;
23
+ }
24
+
25
+ const props = withDefaults(
26
+ defineProps<{
27
+ columns?: TableColumn[];
28
+ data?: any[];
29
+ loading?: boolean;
30
+ rowKey?: string | ((row: any) => string | number);
31
+ sortBy?: string;
32
+ sortOrder?: "asc" | "desc" | string;
33
+ emptyText?: string;
34
+ height?: string | number;
35
+ hoverable?: boolean;
36
+ striped?: boolean;
37
+ bordered?: boolean;
38
+ skeletonRows?: number;
39
+ autoSkeleton?: boolean;
40
+ rowHeight?: number;
41
+ }>(),
42
+ {
43
+ columns: () => [],
44
+ data: () => [],
45
+ loading: false,
46
+ rowKey: "id",
47
+ sortBy: "",
48
+ sortOrder: "",
49
+ emptyText: "No data available",
50
+ hoverable: true,
51
+ striped: false,
52
+ bordered: true,
53
+ skeletonRows: undefined,
54
+ autoSkeleton: true,
55
+ rowHeight: 40,
56
+ }
57
+ );
58
+
59
+ const emit = defineEmits<{
60
+ (e: "row-click", row: any, event: MouseEvent): void;
61
+ (e: "sort-change", payload: { key: string; order: "asc" | "desc" | null }): void;
62
+ }>();
63
+
64
+ const rootRef = ref<HTMLElement | null>(null);
65
+ const headerScrollRef = ref<HTMLElement | null>(null);
66
+ const bodyScrollRef = ref<HTMLElement | null>(null);
67
+ const gutterWidth = ref(0);
68
+
69
+ const updateGutter = () => {
70
+ if (bodyScrollRef.value) {
71
+ const hasVScroll = bodyScrollRef.value.scrollHeight > bodyScrollRef.value.clientHeight;
72
+ if (hasVScroll) {
73
+ const scrollWidth = bodyScrollRef.value.offsetWidth - bodyScrollRef.value.clientWidth;
74
+ gutterWidth.value = scrollWidth > 0 ? scrollWidth : 6;
75
+ } else {
76
+ gutterWidth.value = 0;
77
+ }
78
+ } else {
79
+ gutterWidth.value = 0;
80
+ }
81
+ };
82
+
83
+ let resizeObserver: ResizeObserver | null = null;
84
+
85
+ onMounted(() => {
86
+ if (bodyScrollRef.value && typeof ResizeObserver !== "undefined") {
87
+ resizeObserver = new ResizeObserver(() => {
88
+ updateGutter();
89
+ });
90
+ resizeObserver.observe(bodyScrollRef.value);
91
+ }
92
+ nextTick(updateGutter);
93
+ });
94
+
95
+ onUnmounted(() => {
96
+ resizeObserver?.disconnect();
97
+ });
98
+
99
+ watch(
100
+ () => [props.data, props.columns, props.loading],
101
+ () => {
102
+ nextTick(updateGutter);
103
+ },
104
+ { deep: true }
105
+ );
106
+
107
+ const handleBodyScroll = () => {
108
+ if (headerScrollRef.value && bodyScrollRef.value) {
109
+ headerScrollRef.value.scrollLeft = bodyScrollRef.value.scrollLeft;
110
+ }
111
+ };
112
+
113
+ const { rows: autoRows } = useAutoSkeletonRows(rootRef, {
114
+ rowHeight: props.rowHeight,
115
+ bottomBuffer: 50,
116
+ });
117
+
118
+ const skeletonCount = computed(() => {
119
+ if (props.skeletonRows && props.skeletonRows > 0) return props.skeletonRows;
120
+ if (props.autoSkeleton) return autoRows.value;
121
+ return 10;
122
+ });
123
+
124
+ const getRowKey = (row: any, index: number) => {
125
+ if (typeof props.rowKey === "function") return props.rowKey(row);
126
+ if (typeof props.rowKey === "string" && row[props.rowKey] !== undefined) return row[props.rowKey];
127
+ return index;
128
+ };
129
+
130
+ const handleSort = (col: TableColumn) => {
131
+ if (!col.sortable) return;
132
+ const isCurrent = props.sortBy === col.key;
133
+ let nextOrder: "asc" | "desc" | null = "asc";
134
+
135
+ if (isCurrent) {
136
+ if (props.sortOrder === "asc") nextOrder = "desc";
137
+ else if (props.sortOrder === "desc") nextOrder = null;
138
+ else nextOrder = "asc";
139
+ }
140
+
141
+ emit("sort-change", { key: col.key, order: nextOrder });
142
+ };
143
+
144
+ const formatWidth = (val?: string | number) => {
145
+ if (val === undefined || val === null) return undefined;
146
+ if (typeof val === "number") return `${val}px`;
147
+ return val;
148
+ };
149
+
150
+ const titleWidths = ["68%", "82%", "55%", "75%", "62%", "88%", "70%", "64%"];
151
+ const textWidths = ["80%", "65%", "88%", "72%", "58%", "84%", "62%", "76%"];
152
+ const codeWidths = ["50%", "62%", "45%", "58%", "52%", "60%"];
153
+
154
+ const getSkeletonWidth = (rIdx: number, cIdx: number, key: string) => {
155
+ if (key === "title" || cIdx === 0) {
156
+ return titleWidths[(rIdx + cIdx) % titleWidths.length];
157
+ }
158
+ if (key === "usdot" || key === "phone" || key === "id") {
159
+ return codeWidths[(rIdx + cIdx) % codeWidths.length];
160
+ }
161
+ return textWidths[(rIdx + cIdx) % textWidths.length];
162
+ };
163
+ </script>
164
+
165
+ <template>
166
+ <div
167
+ ref="rootRef"
168
+ class="w-full flex flex-col overflow-hidden transition-colors"
169
+ :class="[
170
+ bordered ? 'border border-slate-200 dark:border-slate-800' : '',
171
+ 'bg-white dark:bg-[#141629]'
172
+ ]"
173
+ :style="height ? { height: formatWidth(height) } : undefined"
174
+ >
175
+ <!-- Dedicated Fixed Header Container (Never moves on row scroll) -->
176
+ <div
177
+ ref="headerScrollRef"
178
+ class="w-full overflow-hidden flex-shrink-0 border-b border-slate-200 dark:border-slate-800 bg-[#f8fafc] dark:bg-[#16192e]"
179
+ >
180
+ <table class="w-full border-collapse text-left text-xs table-fixed">
181
+ <colgroup>
182
+ <col
183
+ v-for="col in columns"
184
+ :key="`h-col-${col.key}`"
185
+ :style="{
186
+ width: formatWidth(col.width),
187
+ minWidth: formatWidth(col.minWidth || col.width || 100),
188
+ }"
189
+ />
190
+ <col v-if="gutterWidth > 0" :style="{ width: `${gutterWidth}px` }" />
191
+ </colgroup>
192
+ <thead>
193
+ <tr>
194
+ <th
195
+ v-for="col in columns"
196
+ :key="col.key"
197
+ :style="{
198
+ width: formatWidth(col.width),
199
+ minWidth: formatWidth(col.minWidth || col.width || 100),
200
+ textAlign: col.align || 'left',
201
+ }"
202
+ :class="[
203
+ 'px-3.5 py-2.5 font-semibold text-[11px] uppercase tracking-wider text-slate-500 dark:text-slate-400 select-none transition-colors bg-[#f8fafc] dark:bg-[#16192e]',
204
+ col.sortable ? 'cursor-pointer hover:text-slate-900 dark:hover:text-white' : '',
205
+ col.headerClass || ''
206
+ ]"
207
+ @click="handleSort(col)"
208
+ >
209
+ <div
210
+ class="flex items-center gap-1.5"
211
+ :class="{
212
+ 'justify-start': !col.align || col.align === 'left',
213
+ 'justify-center': col.align === 'center',
214
+ 'justify-end': col.align === 'right',
215
+ }"
216
+ >
217
+ <slot :name="`header-${col.key}`" :column="col">
218
+ <span>{{ col.title || col.label || col.key }}</span>
219
+ </slot>
220
+
221
+ <!-- Sort indicator icon -->
222
+ <span v-if="col.sortable" class="inline-flex items-center text-slate-400">
223
+ <ChevronUp
224
+ v-if="sortBy === col.key && sortOrder === 'asc'"
225
+ :size="13"
226
+ class="text-[#12675D] dark:text-teal-400"
227
+ />
228
+ <ChevronDown
229
+ v-else-if="sortBy === col.key && sortOrder === 'desc'"
230
+ :size="13"
231
+ class="text-[#12675D] dark:text-teal-400"
232
+ />
233
+ <ArrowUpDown
234
+ v-else
235
+ :size="11"
236
+ class="opacity-40 hover:opacity-100 transition-opacity"
237
+ />
238
+ </span>
239
+ </div>
240
+ </th>
241
+ <th
242
+ v-if="gutterWidth > 0"
243
+ class="bg-[#f8fafc] dark:bg-[#16192e] p-0 border-0 pointer-events-none"
244
+ :style="{ width: `${gutterWidth}px`, minWidth: `${gutterWidth}px` }"
245
+ />
246
+ </tr>
247
+ </thead>
248
+ </table>
249
+ </div>
250
+
251
+ <!-- Dedicated Scrollable Body Container -->
252
+ <div
253
+ ref="bodyScrollRef"
254
+ class="w-full overflow-x-auto overflow-y-auto custom-scrollbar flex-1 relative"
255
+ @scroll="handleBodyScroll"
256
+ >
257
+ <table class="w-full border-collapse text-left text-xs table-fixed">
258
+ <colgroup>
259
+ <col
260
+ v-for="col in columns"
261
+ :key="`b-col-${col.key}`"
262
+ :style="{
263
+ width: formatWidth(col.width),
264
+ minWidth: formatWidth(col.minWidth || col.width || 100),
265
+ }"
266
+ />
267
+ </colgroup>
268
+ <tbody class="divide-y divide-slate-100 dark:divide-slate-800/80">
269
+ <!-- Loading State (Real matching heights and paddings) -->
270
+ <template v-if="loading">
271
+ <tr
272
+ v-for="i in skeletonCount"
273
+ :key="`sk-${i}`"
274
+ :style="{ height: `${rowHeight}px` }"
275
+ >
276
+ <td
277
+ v-for="(col, cIdx) in columns"
278
+ :key="`sk-c-${col.key}`"
279
+ :style="{
280
+ textAlign: col.align || 'left',
281
+ height: `${rowHeight}px`,
282
+ }"
283
+ class="px-3.5 py-2 align-middle"
284
+ >
285
+ <!-- Action button skeleton -->
286
+ <div v-if="col.key === 'actions'" class="flex items-center justify-center">
287
+ <Skeleton :width="28" :height="28" :radius="6" />
288
+ </div>
289
+
290
+ <!-- Centered badge skeleton -->
291
+ <div v-else-if="col.align === 'center'" class="flex items-center justify-center">
292
+ <Skeleton :width="44" :height="20" :radius="10" />
293
+ </div>
294
+
295
+ <!-- Text / Column skeleton -->
296
+ <Skeleton
297
+ v-else
298
+ :width="getSkeletonWidth(i, cIdx, col.key)"
299
+ :height="14"
300
+ :radius="4"
301
+ />
302
+ </td>
303
+ </tr>
304
+ </template>
305
+
306
+ <!-- Empty State -->
307
+ <tr v-else-if="!data || data.length === 0">
308
+ <td :colspan="columns.length || 1" class="py-16 text-center text-slate-400 dark:text-slate-500">
309
+ <slot name="empty">
310
+ <div class="flex flex-col items-center justify-center gap-2">
311
+ <FileText :size="32" class="opacity-30" />
312
+ <span class="text-xs">{{ emptyText }}</span>
313
+ </div>
314
+ </slot>
315
+ </td>
316
+ </tr>
317
+
318
+ <!-- Data Rows -->
319
+ <template v-else>
320
+ <tr
321
+ v-for="(row, rIdx) in data"
322
+ :key="getRowKey(row, rIdx)"
323
+ :style="{ height: `${rowHeight}px` }"
324
+ :class="[
325
+ 'transition-colors duration-100',
326
+ hoverable ? 'hover:bg-slate-50/80 dark:hover:bg-slate-800/50 cursor-pointer' : '',
327
+ striped && rIdx % 2 === 1 ? 'bg-slate-50/40 dark:bg-slate-900/30' : '',
328
+ ]"
329
+ @click="emit('row-click', row, $event)"
330
+ >
331
+ <td
332
+ v-for="col in columns"
333
+ :key="col.key"
334
+ :style="{
335
+ textAlign: col.align || 'left',
336
+ height: `${rowHeight}px`,
337
+ }"
338
+ :class="[
339
+ 'px-3.5 py-2 text-slate-700 dark:text-slate-200 text-xs font-normal align-middle',
340
+ col.cellClass || ''
341
+ ]"
342
+ >
343
+ <!-- Dedicated Column Cell Slot (e.g. #cell-title, #cell-actions) or default slot -->
344
+ <slot
345
+ :name="`cell-${col.key}`"
346
+ :row="row"
347
+ :column="col"
348
+ :index="rIdx"
349
+ :value="row[col.key]"
350
+ >
351
+ <slot
352
+ :name="col.key"
353
+ :row="row"
354
+ :column="col"
355
+ :index="rIdx"
356
+ :value="row[col.key]"
357
+ >
358
+ <span>{{ row[col.key] != null && row[col.key] !== '' ? row[col.key] : '-' }}</span>
359
+ </slot>
360
+ </slot>
361
+ </td>
362
+ </tr>
363
+ </template>
364
+ </tbody>
365
+ </table>
366
+ </div>
367
+ </div>
368
+ </template>
369
+
370
+ <style scoped>
371
+ .custom-scrollbar {
372
+ scrollbar-width: thin;
373
+ }
374
+ .custom-scrollbar::-webkit-scrollbar {
375
+ width: 6px;
376
+ height: 6px;
377
+ }
378
+ .custom-scrollbar::-webkit-scrollbar-track {
379
+ background: transparent;
380
+ }
381
+ .custom-scrollbar::-webkit-scrollbar-thumb {
382
+ background-color: rgba(148, 163, 184, 0.3);
383
+ border-radius: 6px;
384
+ }
385
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
386
+ background-color: rgba(148, 163, 184, 0.5);
387
+ }
388
+ </style>