@kikiloaw/simple-table 1.1.0 → 1.1.2
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 +24 -1
- package/package.json +5 -1
- package/src/SimpleTable.vue +155 -36
- package/src/components/table/Table.vue +2 -2
- package/src/components/table/TableBody.vue +1 -1
- package/src/components/table/TableCell.vue +2 -2
- package/src/components/table/TableHead.vue +3 -3
- package/src/components/table/TableHeader.vue +1 -1
- package/src/components/table/TableRow.vue +1 -1
- package/src/lib/utils.js +15 -0
package/README.md
CHANGED
|
@@ -478,7 +478,8 @@ SimpleTable supports three data modes:
|
|
|
478
478
|
sortable: true, // Optional: Enable sorting
|
|
479
479
|
width: '200px', // Optional: Fixed column width
|
|
480
480
|
fixed: true, // Optional: Sticky column (left for first, right for last, others left). **Requires `width` to be set.**
|
|
481
|
-
|
|
481
|
+
align: 'right', // Optional: Text alignment ('left', 'center', 'right'). Default: 'left'
|
|
482
|
+
class: 'text-red-500' // Optional: Additional CSS classes
|
|
482
483
|
}
|
|
483
484
|
```
|
|
484
485
|
|
|
@@ -1402,6 +1403,28 @@ table.value?.refresh()
|
|
|
1402
1403
|
| `@update:sort` | `{ column, direction }` | Emitted when sort changes |
|
|
1403
1404
|
| `@page-change` | `number` | Emitted when page changes |
|
|
1404
1405
|
| `@export` | `{ format, data }` | Emitted when export is triggered |
|
|
1406
|
+
| `@fetched` | `Object` (Response) | Emitted when data is successfully fetched from API. Contains raw response. |
|
|
1407
|
+
|
|
1408
|
+
---
|
|
1409
|
+
|
|
1410
|
+
## 🔧 Exposed Methods
|
|
1411
|
+
|
|
1412
|
+
Access these methods via template ref:
|
|
1413
|
+
|
|
1414
|
+
```vue
|
|
1415
|
+
<script setup>
|
|
1416
|
+
const tableRef = ref()
|
|
1417
|
+
|
|
1418
|
+
// ...
|
|
1419
|
+
tableRef.value?.refresh()
|
|
1420
|
+
</script>
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
| Method | Parameters | Description |
|
|
1424
|
+
|--------|------------|-------------|
|
|
1425
|
+
| `refresh()` | None | Resets page to 1 and refetches data |
|
|
1426
|
+
| `fetchData(params)` | `params: Object` | Fetch data with specific custom parameters |
|
|
1427
|
+
| `clearCache()` | None | Clears the client-side response cache |
|
|
1405
1428
|
|
|
1406
1429
|
---
|
|
1407
1430
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kikiloaw/simple-table",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "A lightweight, dependency-light DataTable component for Vue 3 with Tailwind CSS",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"@vueuse/core": "^10.0.0 || ^11.0.0 || ^12.0.0",
|
|
26
26
|
"@inertiajs/vue3": "^1.0.0 || ^2.0.0"
|
|
27
27
|
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"clsx": "^2.1.1",
|
|
30
|
+
"tailwind-merge": "^3.2.0"
|
|
31
|
+
},
|
|
28
32
|
"repository": {
|
|
29
33
|
"type": "git",
|
|
30
34
|
"url": "git+https://github.com/kikiloaw/simple-table.git"
|
package/src/SimpleTable.vue
CHANGED
|
@@ -63,8 +63,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
63
63
|
pageSizes: () => [10, 20, 30, 50, 100],
|
|
64
64
|
rowHeight: 38,
|
|
65
65
|
oddRowColor: 'bg-white',
|
|
66
|
-
evenRowColor: 'bg-
|
|
67
|
-
hoverColor: 'hover:bg-
|
|
66
|
+
evenRowColor: 'bg-gray-50',
|
|
67
|
+
hoverColor: 'hover:bg-gray-100'
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
|
|
@@ -586,6 +586,13 @@ function handlePageSizeChange(size: any) {
|
|
|
586
586
|
fetchData()
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
+
// Helper for header content alignment (justify for flex)
|
|
590
|
+
function getHeaderJustifyClass(col: any) {
|
|
591
|
+
if (col.align === 'center') return 'justify-center'
|
|
592
|
+
if (col.align === 'right') return 'justify-end'
|
|
593
|
+
return 'justify-start'
|
|
594
|
+
}
|
|
595
|
+
|
|
589
596
|
// Get row class with simple alternating stripes (all rows)
|
|
590
597
|
function getRowClass(row: any, idx: number) {
|
|
591
598
|
// Alternate: index 0 = white, index 1 = gray, index 2 = white, etc.
|
|
@@ -643,73 +650,152 @@ onMounted(() => {
|
|
|
643
650
|
|
|
644
651
|
defineExpose({
|
|
645
652
|
refresh,
|
|
646
|
-
fetchData,
|
|
647
|
-
clearCache
|
|
653
|
+
fetchData,
|
|
654
|
+
clearCache
|
|
648
655
|
})
|
|
649
656
|
|
|
650
657
|
|
|
658
|
+
// -- Helper Functions --
|
|
659
|
+
|
|
660
|
+
function isColFixed(col: any) {
|
|
661
|
+
return !!col.fixed
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Calculate left offset dynamically
|
|
665
|
+
function getStickyLeftOffset(index: number) {
|
|
666
|
+
let offset = 0
|
|
667
|
+
for (let i = 0; i < index; i++) {
|
|
668
|
+
const col = props.columns[i]
|
|
669
|
+
// Only consider left-fixed columns
|
|
670
|
+
const isRightFixed = (col.fixed && i === props.columns.length - 1)
|
|
671
|
+
|
|
672
|
+
if (col.fixed && !isRightFixed) {
|
|
673
|
+
let w = 100 // Default width
|
|
674
|
+
if (col.width) {
|
|
675
|
+
if (typeof col.width === 'string') {
|
|
676
|
+
// Extract number from "80px", "100", etc.
|
|
677
|
+
const match = col.width.match(/^(\d+(\.\d+)?)/)
|
|
678
|
+
if (match) w = parseFloat(match[1])
|
|
679
|
+
} else if (typeof col.width === 'number') {
|
|
680
|
+
w = col.width
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
offset += w
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return offset
|
|
687
|
+
}
|
|
651
688
|
|
|
652
689
|
// -- Helper Styles --
|
|
653
690
|
function getCellClass(col: any, index: number, totalCols: number, rowIndex: number = -1) {
|
|
654
|
-
let classes =
|
|
691
|
+
let classes = ''
|
|
655
692
|
|
|
656
693
|
// Base classes
|
|
657
|
-
|
|
694
|
+
|
|
695
|
+
// Add text alignment (default: left)
|
|
696
|
+
const alignClass = col.align === 'center' ? ' text-center' : col.align === 'right' ? ' text-right' : ' text-left'
|
|
697
|
+
classes += alignClass
|
|
658
698
|
|
|
659
699
|
if (col.fixed) {
|
|
660
700
|
// Sticky logic
|
|
661
|
-
let stickyClass = ' whitespace-nowrap'
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
701
|
+
let stickyClass = ' whitespace-nowrap'
|
|
702
|
+
|
|
703
|
+
// Check if this is the LAST left-fixed column
|
|
704
|
+
// It is the last left-fixed if:
|
|
705
|
+
// 1. It is NOT the last column (which is right-fixed)
|
|
706
|
+
// 2. The NEXT column is NOT fixed OR is the last column (right-fixed)
|
|
707
|
+
const isRightFixed = index === totalCols - 1
|
|
708
|
+
|
|
709
|
+
if (isRightFixed) {
|
|
710
|
+
// Last Column -> Right Sticky (shadow on LEFT side)
|
|
711
|
+
stickyClass = ' sticky right-0 z-50 fixed-column-boundary-left'
|
|
665
712
|
} else {
|
|
666
|
-
//
|
|
667
|
-
|
|
713
|
+
// Left Sticky - must have high z-index to stay on top of scrolling content
|
|
714
|
+
// Apply consistent shadow and border to all left-fixed columns (shadow on RIGHT side)
|
|
715
|
+
stickyClass = ' sticky z-50'
|
|
668
716
|
}
|
|
669
717
|
|
|
670
718
|
// Determine background
|
|
671
|
-
// Sticky cells need opaque bg.
|
|
672
|
-
let bgClass = '
|
|
719
|
+
// Sticky cells need opaque bg.
|
|
720
|
+
let bgClass = ''
|
|
673
721
|
|
|
674
722
|
if (rowIndex !== -1) {
|
|
675
723
|
// Body Row
|
|
676
724
|
const isOdd = rowIndex % 2 === 0
|
|
677
|
-
|
|
725
|
+
// Use custom class if provided, otherwise use row colors
|
|
726
|
+
if (col.class) {
|
|
727
|
+
bgClass = col.class
|
|
728
|
+
} else {
|
|
729
|
+
bgClass = isOdd ? (props.oddRowColor || 'bg-white') : (props.evenRowColor || 'bg-gray-50')
|
|
730
|
+
}
|
|
678
731
|
|
|
679
732
|
// Should also match hover
|
|
680
|
-
// If the row has a hover class (like hover:bg-muted), the sticky cell needs group-hover:bg-muted to match.
|
|
681
|
-
// We assume hoverColor is passed as 'hover:bg-...'
|
|
682
|
-
// We try to convert 'hover:bg-...' to 'group-hover:bg-...'
|
|
683
733
|
if (props.hoverColor) {
|
|
684
734
|
const hoverParts = props.hoverColor.split(':')
|
|
685
735
|
if (hoverParts.length > 1) {
|
|
686
|
-
// e.g. ['hover', 'bg-blue-100'] -> 'group-hover:bg-blue-100'
|
|
687
736
|
bgClass += ` group-hover:${hoverParts[1]}`
|
|
688
|
-
// Also handle things like 'hover:bg-muted/50'
|
|
689
737
|
if (hoverParts.length > 2) {
|
|
690
738
|
bgClass = bgClass + ':' + hoverParts.slice(2).join(':')
|
|
691
739
|
}
|
|
692
740
|
}
|
|
693
741
|
}
|
|
694
742
|
} else {
|
|
695
|
-
// Header Row
|
|
696
|
-
bgClass = 'bg-
|
|
743
|
+
// Header Row - use custom class if provided, otherwise default to white
|
|
744
|
+
bgClass = col.class || 'bg-white' // Must be opaque
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Check if this is the last left-fixed column (boundary)
|
|
748
|
+
const nextCol = props.columns[index + 1]
|
|
749
|
+
const isLastLeftFixed = nextCol && !nextCol.fixed
|
|
750
|
+
|
|
751
|
+
if (isLastLeftFixed) {
|
|
752
|
+
classes += ' fixed-column-boundary-right !pr-6'
|
|
697
753
|
}
|
|
698
754
|
|
|
699
|
-
classes += stickyClass + ' ' + bgClass
|
|
755
|
+
classes += stickyClass + ' ' + bgClass + ' !bg-opacity-100'
|
|
756
|
+
} else {
|
|
757
|
+
// Non-fixed column - just add custom class if provided
|
|
758
|
+
if (col.class) {
|
|
759
|
+
classes += ' ' + col.class
|
|
760
|
+
}
|
|
700
761
|
}
|
|
701
762
|
return classes
|
|
702
763
|
}
|
|
703
764
|
|
|
704
|
-
function getCellStyle(col: any) {
|
|
765
|
+
function getCellStyle(col: any, index: number, totalCols: number) {
|
|
766
|
+
const style: any = {}
|
|
767
|
+
|
|
705
768
|
if (col.width) {
|
|
706
|
-
|
|
769
|
+
style.width = col.width
|
|
770
|
+
style.minWidth = col.width
|
|
771
|
+
style.maxWidth = col.width
|
|
772
|
+
} else if (col.fixed) {
|
|
773
|
+
style.width = '100px' // Default fixed width if not processing
|
|
774
|
+
style.minWidth = '100px'
|
|
707
775
|
}
|
|
708
|
-
|
|
776
|
+
|
|
709
777
|
if (col.fixed) {
|
|
710
|
-
|
|
778
|
+
// Handle Left vs Right
|
|
779
|
+
if (index !== totalCols - 1) {
|
|
780
|
+
// Left Sticky: Calculate directly
|
|
781
|
+
const left = getStickyLeftOffset(index)
|
|
782
|
+
style.left = `${left}px`
|
|
783
|
+
|
|
784
|
+
// Only add separator to the LAST left-fixed column (the boundary)
|
|
785
|
+
const nextCol = props.columns[index + 1]
|
|
786
|
+
const isLastLeftFixed = nextCol && !nextCol.fixed
|
|
787
|
+
|
|
788
|
+
if (isLastLeftFixed) {
|
|
789
|
+
// Add position relative so the ::after pseudo-element works
|
|
790
|
+
style.position = 'sticky' // Already sticky, but make it explicit
|
|
791
|
+
}
|
|
792
|
+
} else {
|
|
793
|
+
// Right sticky - will use CSS class for border
|
|
794
|
+
}
|
|
795
|
+
// Right sticky is handled by CSS class right-0
|
|
711
796
|
}
|
|
712
|
-
|
|
797
|
+
|
|
798
|
+
return style
|
|
713
799
|
}
|
|
714
800
|
|
|
715
801
|
</script>
|
|
@@ -758,33 +844,35 @@ function getCellStyle(col: any) {
|
|
|
758
844
|
</div>
|
|
759
845
|
|
|
760
846
|
<!-- Table -->
|
|
761
|
-
<div class="border bg-background
|
|
762
|
-
|
|
763
|
-
|
|
847
|
+
<div class="border bg-background relative">
|
|
848
|
+
<div class="overflow-x-auto">
|
|
849
|
+
<!-- We add min-w-full to Table to ensure it stretches -->
|
|
850
|
+
<Table class="min-w-full table-auto">
|
|
764
851
|
<TableHeader>
|
|
765
852
|
<TableRow :style="{ height: densityConfig.cellHeight }">
|
|
766
853
|
<TableHead
|
|
767
854
|
v-for="(col, idx) in columns"
|
|
768
855
|
:key="col.key"
|
|
769
856
|
:class="getCellClass(col, idx, columns.length)"
|
|
770
|
-
:style="getCellStyle(col)"
|
|
857
|
+
:style="getCellStyle(col, idx, columns.length)"
|
|
771
858
|
:height="densityConfig.headerHeight"
|
|
772
859
|
:padding="densityConfig.headerPadding"
|
|
773
860
|
>
|
|
774
861
|
<div
|
|
775
862
|
v-if="col.sortable"
|
|
776
|
-
class="flex items-center space-x-2 cursor-pointer select-none hover:text-foreground"
|
|
863
|
+
class="flex items-center space-x-2 cursor-pointer select-none hover:text-foreground w-full"
|
|
864
|
+
:class="getHeaderJustifyClass(col)"
|
|
777
865
|
@click="handleSort(col)"
|
|
778
866
|
>
|
|
779
867
|
<div>{{ col.label }}</div>
|
|
780
868
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 opacity-50 flex-shrink-0"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
|
781
869
|
</div>
|
|
782
|
-
<div v-else>{{ col.label }}</div>
|
|
870
|
+
<div v-else class="w-full flex" :class="getHeaderJustifyClass(col)">{{ col.label }}</div>
|
|
783
871
|
</TableHead>
|
|
784
872
|
</TableRow>
|
|
785
873
|
</TableHeader>
|
|
786
874
|
<TableBody>
|
|
787
|
-
<template v-if="isLoading">
|
|
875
|
+
<template v-if="isLoading && tableData.length === 0">
|
|
788
876
|
<TableRow>
|
|
789
877
|
<TableCell :colspan="columns.length" class="h-24 text-center">
|
|
790
878
|
<div class="flex items-center justify-center">
|
|
@@ -821,7 +909,7 @@ function getCellStyle(col: any) {
|
|
|
821
909
|
v-for="(col, cIdx) in columns"
|
|
822
910
|
:key="col.key"
|
|
823
911
|
:class="getCellClass(col, cIdx, columns.length, idx)"
|
|
824
|
-
:style="getCellStyle(col)"
|
|
912
|
+
:style="getCellStyle(col, cIdx, columns.length)"
|
|
825
913
|
:padding="densityConfig.cellPadding"
|
|
826
914
|
:height="densityConfig.cellHeight"
|
|
827
915
|
>
|
|
@@ -847,6 +935,12 @@ function getCellStyle(col: any) {
|
|
|
847
935
|
</TableRow>
|
|
848
936
|
</TableBody>
|
|
849
937
|
</Table>
|
|
938
|
+
</div>
|
|
939
|
+
|
|
940
|
+
<!-- Loading Overlay -->
|
|
941
|
+
<div v-if="isLoading && tableData.length > 0" class="absolute inset-0 bg-background/50 flex items-center justify-center z-[60]">
|
|
942
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-8 w-8 animate-spin text-primary"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
|
943
|
+
</div>
|
|
850
944
|
</div>
|
|
851
945
|
|
|
852
946
|
<!-- Pagination -->
|
|
@@ -903,3 +997,28 @@ function getCellStyle(col: any) {
|
|
|
903
997
|
</div>
|
|
904
998
|
</div>
|
|
905
999
|
</template>
|
|
1000
|
+
|
|
1001
|
+
<style scoped>
|
|
1002
|
+
/* Fixed column boundary separator (DataTables approach) */
|
|
1003
|
+
.fixed-column-boundary-right::after {
|
|
1004
|
+
content: "";
|
|
1005
|
+
position: absolute;
|
|
1006
|
+
top: 0;
|
|
1007
|
+
right: 0;
|
|
1008
|
+
bottom: 0;
|
|
1009
|
+
width: 10px;
|
|
1010
|
+
box-shadow: rgba(0, 0, 0, 0.2) 6px 0px 4px -4px inset;
|
|
1011
|
+
pointer-events: none;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.fixed-column-boundary-left::before {
|
|
1015
|
+
content: "";
|
|
1016
|
+
position: absolute;
|
|
1017
|
+
top: 0;
|
|
1018
|
+
left: -10px;
|
|
1019
|
+
bottom: 0;
|
|
1020
|
+
width: 10px;
|
|
1021
|
+
box-shadow: rgba(0, 0, 0, 0.2) -6px 0px 4px -4px inset;
|
|
1022
|
+
pointer-events: none;
|
|
1023
|
+
}
|
|
1024
|
+
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { type HTMLAttributes, computed } from 'vue'
|
|
3
|
-
import { cn } from '
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
4
|
|
|
5
5
|
const props = defineProps<{
|
|
6
6
|
class?: HTMLAttributes['class']
|
|
@@ -16,7 +16,7 @@ const delegatedProps = computed(() => {
|
|
|
16
16
|
<template>
|
|
17
17
|
<div class="relative w-full overflow-auto">
|
|
18
18
|
<table
|
|
19
|
-
:class="cn('w-full caption-bottom text-sm border-separate border-spacing-0', props.class)"
|
|
19
|
+
:class="cn('w-full caption-bottom text-sm !border-separate border-spacing-0', props.class)"
|
|
20
20
|
v-bind="delegatedProps"
|
|
21
21
|
>
|
|
22
22
|
<slot />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { type HTMLAttributes, computed } from 'vue'
|
|
3
|
-
import { cn } from '
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
4
|
|
|
5
5
|
const props = defineProps<{
|
|
6
6
|
class?: HTMLAttributes['class']
|
|
@@ -26,7 +26,7 @@ const cellStyle = computed(() => {
|
|
|
26
26
|
|
|
27
27
|
<template>
|
|
28
28
|
<td
|
|
29
|
-
:class="cn(props.padding || '
|
|
29
|
+
:class="cn(props.padding || 'px-2.5 py-2', 'border-b border-stone-300 align-middle [&:has([role=checkbox])]:pr-0', props.class)"
|
|
30
30
|
:style="cellStyle"
|
|
31
31
|
v-bind="delegatedProps"
|
|
32
32
|
>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { type HTMLAttributes, computed } from 'vue'
|
|
3
|
-
import { cn } from '
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
4
|
|
|
5
5
|
const props = defineProps<{
|
|
6
6
|
class?: HTMLAttributes['class']
|
|
@@ -20,8 +20,8 @@ const delegatedProps = computed(() => {
|
|
|
20
20
|
:class="
|
|
21
21
|
cn(
|
|
22
22
|
props.height || 'h-[38px]',
|
|
23
|
-
props.padding || 'px-2',
|
|
24
|
-
'border-b border-stone-300
|
|
23
|
+
props.padding || 'px-2.5',
|
|
24
|
+
'border-b border-stone-300 align-middle font-bold text-muted-foreground [&:has([role=checkbox])]:pr-0',
|
|
25
25
|
props.class,
|
|
26
26
|
)
|
|
27
27
|
"
|
package/src/lib/utils.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function urlIsActive(urlToCheck, currentUrl) {
|
|
9
|
+
const href = typeof urlToCheck === 'string' ? urlToCheck : urlToCheck?.url;
|
|
10
|
+
return href === currentUrl;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function toUrl(href) {
|
|
14
|
+
return typeof href === 'string' ? href : href?.url;
|
|
15
|
+
}
|