@kikiloaw/simple-table 1.0.6 → 1.1.1

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 CHANGED
@@ -6,6 +6,8 @@
6
6
  [![Vue 3](https://img.shields.io/badge/Vue-3.x-brightgreen.svg)](https://vuejs.org/)
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
8
8
 
9
+ > **🚀 Explore the Demo**: Check out the [SimpleTable Demo Repository](https://github.com/kikiloaw/simple-table-demo.git) for a complete documentation site with live server-side examples, seeding, and source code.
10
+
9
11
  ---
10
12
 
11
13
  ## ✨ Why SimpleTable?
@@ -476,7 +478,8 @@ SimpleTable supports three data modes:
476
478
  sortable: true, // Optional: Enable sorting
477
479
  width: '200px', // Optional: Fixed column width
478
480
  fixed: true, // Optional: Sticky column (left for first, right for last, others left). **Requires `width` to be set.**
479
- class: 'text-center' // Optional: Additional CSS classes
481
+ align: 'right', // Optional: Text alignment ('left', 'center', 'right'). Default: 'left'
482
+ class: 'text-red-500' // Optional: Additional CSS classes
480
483
  }
481
484
  ```
482
485
 
@@ -1400,6 +1403,28 @@ table.value?.refresh()
1400
1403
  | `@update:sort` | `{ column, direction }` | Emitted when sort changes |
1401
1404
  | `@page-change` | `number` | Emitted when page changes |
1402
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 |
1403
1428
 
1404
1429
  ---
1405
1430
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kikiloaw/simple-table",
3
- "version": "1.0.6",
3
+ "version": "1.1.1",
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",
@@ -63,10 +63,12 @@ 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-stone-100',
67
- hoverColor: 'hover:bg-stone-200'
66
+ evenRowColor: 'bg-gray-50',
67
+ hoverColor: 'hover:bg-gray-100'
68
68
  })
69
69
 
70
+
71
+
70
72
  // ...
71
73
 
72
74
  // -- Computed: Page Sizes Normalization --
@@ -163,7 +165,7 @@ const densityConfig = computed(() => {
163
165
  </div>
164
166
  */
165
167
 
166
- const emit = defineEmits(['update:search', 'update:sort', 'page-change'])
168
+ const emit = defineEmits(['update:search', 'update:sort', 'page-change', 'fetched'])
167
169
 
168
170
  // -- State --
169
171
  const searchQuery = ref('')
@@ -464,6 +466,7 @@ async function fetchData(params: any = {}) {
464
466
  }
465
467
 
466
468
  internalData.value = data
469
+ emit('fetched', data)
467
470
 
468
471
  // Store in cache if enabled
469
472
  if (props.enableCache) {
@@ -583,6 +586,13 @@ function handlePageSizeChange(size: any) {
583
586
  fetchData()
584
587
  }
585
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
+
586
596
  // Get row class with simple alternating stripes (all rows)
587
597
  function getRowClass(row: any, idx: number) {
588
598
  // Alternate: index 0 = white, index 1 = gray, index 2 = white, etc.
@@ -640,71 +650,152 @@ onMounted(() => {
640
650
 
641
651
  defineExpose({
642
652
  refresh,
643
- fetchData, // exposing fetchData too just in case
644
- clearCache // expose cache clearing method
653
+ fetchData,
654
+ clearCache
645
655
  })
646
656
 
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
+ }
688
+
647
689
  // -- Helper Styles --
648
690
  function getCellClass(col: any, index: number, totalCols: number, rowIndex: number = -1) {
649
- let classes = col.class || ''
691
+ let classes = ''
650
692
 
651
693
  // Base classes
652
- // Removed whitespace-nowrap to allow wrapping
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
653
698
 
654
699
  if (col.fixed) {
655
700
  // Sticky logic
656
- let stickyClass = ' whitespace-nowrap' // Prevent content wrapping in sticky columns
657
- if (index === totalCols - 1) {
658
- // Last Column -> Right Sticky
659
- stickyClass = ' sticky right-0 z-10 shadow-[-4px_0_8px_-2px_rgba(0,0,0,0.1)] border-l border-stone-300'
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'
660
712
  } else {
661
- // All other fixed columns -> Left Sticky
662
- stickyClass = ' sticky left-0 z-10 shadow-[4px_0_8px_-2px_rgba(0,0,0,0.1)] border-r border-stone-300'
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'
663
716
  }
664
717
 
665
718
  // Determine background
666
- // Sticky cells need opaque bg. We rely on the props.
667
- let bgClass = 'bg-background' // Fallback
719
+ // Sticky cells need opaque bg.
720
+ let bgClass = ''
668
721
 
669
722
  if (rowIndex !== -1) {
670
723
  // Body Row
671
724
  const isOdd = rowIndex % 2 === 0
672
- bgClass = isOdd ? props.oddRowColor : props.evenRowColor
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
+ }
673
731
 
674
732
  // Should also match hover
675
- // If the row has a hover class (like hover:bg-muted), the sticky cell needs group-hover:bg-muted to match.
676
- // We assume hoverColor is passed as 'hover:bg-...'
677
- // We try to convert 'hover:bg-...' to 'group-hover:bg-...'
678
733
  if (props.hoverColor) {
679
734
  const hoverParts = props.hoverColor.split(':')
680
735
  if (hoverParts.length > 1) {
681
- // e.g. ['hover', 'bg-blue-100'] -> 'group-hover:bg-blue-100'
682
736
  bgClass += ` group-hover:${hoverParts[1]}`
683
- // Also handle things like 'hover:bg-muted/50'
684
737
  if (hoverParts.length > 2) {
685
738
  bgClass = bgClass + ':' + hoverParts.slice(2).join(':')
686
739
  }
687
740
  }
688
741
  }
689
742
  } else {
690
- // Header Row
691
- bgClass = 'bg-background'
743
+ // Header Row - use custom class if provided, otherwise default to white
744
+ bgClass = col.class || 'bg-white' // Must be opaque
692
745
  }
693
746
 
694
- classes += stickyClass + ' ' + bgClass
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'
753
+ }
754
+
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
+ }
695
761
  }
696
762
  return classes
697
763
  }
698
764
 
699
- function getCellStyle(col: any) {
765
+ function getCellStyle(col: any, index: number, totalCols: number) {
766
+ const style: any = {}
767
+
700
768
  if (col.width) {
701
- return { width: col.width, minWidth: col.width, maxWidth: col.width }
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'
702
775
  }
703
- // Smart default: If fixed but no width, force a safe width (e.g. 100px) to ensure background covers content
776
+
704
777
  if (col.fixed) {
705
- return { width: '100px', minWidth: '100px' }
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
706
796
  }
707
- return {}
797
+
798
+ return style
708
799
  }
709
800
 
710
801
  </script>
@@ -753,33 +844,35 @@ function getCellStyle(col: any) {
753
844
  </div>
754
845
 
755
846
  <!-- Table -->
756
- <div class="border bg-background overflow-x-auto relative">
757
- <!-- We add min-w-full to Table to ensure it stretches -->
758
- <Table class="min-w-full table-auto">
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">
759
851
  <TableHeader>
760
852
  <TableRow :style="{ height: densityConfig.cellHeight }">
761
853
  <TableHead
762
854
  v-for="(col, idx) in columns"
763
855
  :key="col.key"
764
856
  :class="getCellClass(col, idx, columns.length)"
765
- :style="getCellStyle(col)"
857
+ :style="getCellStyle(col, idx, columns.length)"
766
858
  :height="densityConfig.headerHeight"
767
859
  :padding="densityConfig.headerPadding"
768
860
  >
769
861
  <div
770
862
  v-if="col.sortable"
771
- 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)"
772
865
  @click="handleSort(col)"
773
866
  >
774
867
  <div>{{ col.label }}</div>
775
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>
776
869
  </div>
777
- <div v-else>{{ col.label }}</div>
870
+ <div v-else class="w-full flex" :class="getHeaderJustifyClass(col)">{{ col.label }}</div>
778
871
  </TableHead>
779
872
  </TableRow>
780
873
  </TableHeader>
781
874
  <TableBody>
782
- <template v-if="isLoading">
875
+ <template v-if="isLoading && tableData.length === 0">
783
876
  <TableRow>
784
877
  <TableCell :colspan="columns.length" class="h-24 text-center">
785
878
  <div class="flex items-center justify-center">
@@ -799,7 +892,12 @@ function getCellStyle(col: any) {
799
892
  <!-- Group Header Row: Single cell spanning all columns -->
800
893
  <template v-if="row._isGroupHeader">
801
894
  <TableCell :colspan="columns.length" class="border-b border-gray-200">
802
- <div :class="['px-2', densityConfig.groupHeaderPadding, 'font-semibold text-gray-700 text-sm uppercase tracking-wide']">
895
+ <div :class="[
896
+ 'px-2',
897
+ densityConfig.groupHeaderPadding,
898
+ 'font-semibold text-sm uppercase tracking-wide',
899
+ row._groupTitleClass || 'text-gray-700'
900
+ ]">
803
901
  {{ row._groupTitle }}
804
902
  </div>
805
903
  </TableCell>
@@ -811,7 +909,7 @@ function getCellStyle(col: any) {
811
909
  v-for="(col, cIdx) in columns"
812
910
  :key="col.key"
813
911
  :class="getCellClass(col, cIdx, columns.length, idx)"
814
- :style="getCellStyle(col)"
912
+ :style="getCellStyle(col, cIdx, columns.length)"
815
913
  :padding="densityConfig.cellPadding"
816
914
  :height="densityConfig.cellHeight"
817
915
  >
@@ -837,6 +935,12 @@ function getCellStyle(col: any) {
837
935
  </TableRow>
838
936
  </TableBody>
839
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>
840
944
  </div>
841
945
 
842
946
  <!-- Pagination -->
@@ -893,3 +997,28 @@ function getCellStyle(col: any) {
893
997
  </div>
894
998
  </div>
895
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>
@@ -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 />
@@ -7,6 +7,7 @@ const props = defineProps<{
7
7
  style?: any
8
8
  padding?: string
9
9
  height?: string
10
+ colspan?: number | string
10
11
  }>()
11
12
 
12
13
  const delegatedProps = computed(() => {
@@ -25,7 +26,7 @@ const cellStyle = computed(() => {
25
26
 
26
27
  <template>
27
28
  <td
28
- :class="cn(props.padding || 'p-2', 'border-b border-stone-300 align-middle [&:has([role=checkbox])]:pr-0', props.class)"
29
+ :class="cn(props.padding || 'px-2.5 py-2', 'border-b border-stone-300 align-middle [&:has([role=checkbox])]:pr-0', props.class)"
29
30
  :style="cellStyle"
30
31
  v-bind="delegatedProps"
31
32
  >
@@ -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 text-left align-middle font-bold text-muted-foreground [&:has([role=checkbox])]:pr-0',
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
  "