@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 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
- 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
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.0",
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"
@@ -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-stone-100',
67
- hoverColor: 'hover:bg-stone-200'
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, // exposing fetchData too just in case
647
- clearCache // expose cache clearing method
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 = col.class || ''
691
+ let classes = ''
655
692
 
656
693
  // Base classes
657
- // 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
658
698
 
659
699
  if (col.fixed) {
660
700
  // Sticky logic
661
- let stickyClass = ' whitespace-nowrap' // Prevent content wrapping in sticky columns
662
- if (index === totalCols - 1) {
663
- // Last Column -> Right Sticky
664
- 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'
665
712
  } else {
666
- // All other fixed columns -> Left Sticky
667
- 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'
668
716
  }
669
717
 
670
718
  // Determine background
671
- // Sticky cells need opaque bg. We rely on the props.
672
- let bgClass = 'bg-background' // Fallback
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
- 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
+ }
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-background'
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
- 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'
707
775
  }
708
- // Smart default: If fixed but no width, force a safe width (e.g. 100px) to ensure background covers content
776
+
709
777
  if (col.fixed) {
710
- 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
711
796
  }
712
- return {}
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 overflow-x-auto relative">
762
- <!-- We add min-w-full to Table to ensure it stretches -->
763
- <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">
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 '@/lib/utils'
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 '@/lib/utils'
3
+ import { cn } from '../../lib/utils'
4
4
 
5
5
  const props = defineProps<{
6
6
  class?: HTMLAttributes['class']
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { type HTMLAttributes, computed } from 'vue'
3
- import { cn } from '@/lib/utils'
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 || '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)"
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 '@/lib/utils'
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 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
  "
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { type HTMLAttributes, computed } from 'vue'
3
- import { cn } from '@/lib/utils'
3
+ import { cn } from '../../lib/utils'
4
4
 
5
5
  const props = defineProps<{
6
6
  class?: HTMLAttributes['class']
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { type HTMLAttributes, computed } from 'vue'
3
- import { cn } from '@/lib/utils'
3
+ import { cn } from '../../lib/utils'
4
4
 
5
5
  const props = defineProps<{
6
6
  class?: HTMLAttributes['class']
@@ -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
+ }