@kikiloaw/simple-table 1.0.0 β†’ 1.0.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.
Files changed (3) hide show
  1. package/README.md +369 -17
  2. package/package.json +2 -2
  3. package/src/SimpleTable.vue +109 -49
package/README.md CHANGED
@@ -212,6 +212,14 @@ const columns = [
212
212
  label: 'Email'
213
213
  },
214
214
 
215
+ // Auto-numbering (row numbers instead of data)
216
+ {
217
+ key: 'id',
218
+ label: '#',
219
+ autonumber: true,
220
+ width: '80px'
221
+ },
222
+
215
223
  // Sticky actions column (always visible)
216
224
  {
217
225
  key: 'actions',
@@ -233,6 +241,50 @@ Use custom sort keys when:
233
241
 
234
242
  ## 🎨 Features
235
243
 
244
+ ### Auto-Numbering
245
+
246
+ **Display sequential row numbers instead of actual data:**
247
+
248
+ ```vue
249
+ const columns = [
250
+ {
251
+ key: 'id',
252
+ label: '#',
253
+ autonumber: true,
254
+ width: '80px'
255
+ },
256
+ // ... other columns
257
+ ]
258
+ ```
259
+
260
+ **Features:**
261
+ - βœ… Displays 1, 2, 3, 4... for each data row
262
+ - βœ… Skips group headers (only counts data rows)
263
+ - βœ… Pagination-aware: Page 2 shows 11, 12, 13... (with 10 per page)
264
+ - βœ… Overrides actual column data
265
+ - βœ… Works with both server-side and client-side modes
266
+
267
+ **Example:**
268
+ ```
269
+ β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
270
+ β”‚ # β”‚ Name β”‚ Status β”‚
271
+ β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
272
+ β”‚ ACTIVE USERS β”‚ ← Header (not counted)
273
+ β”‚ 1 β”‚ John Doe β”‚ Active β”‚
274
+ β”‚ 2 β”‚ Jane Smith β”‚ Active β”‚
275
+ β”œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
276
+ β”‚ INACTIVE USERS β”‚ ← Header (not counted)
277
+ β”‚ 3 β”‚ Bob Johnson β”‚ Inactiveβ”‚
278
+ ```
279
+
280
+ **Perfect for:**
281
+ - Sequential numbering regardless of actual IDs
282
+ - User-friendly row references
283
+ - Tables with group headers
284
+ - Paginated lists with continuous numbering
285
+
286
+ ---
287
+
236
288
  ### Custom Sort Keys
237
289
 
238
290
  **Problem:** You want to display `department.name` but sort by `department_id`.
@@ -263,19 +315,22 @@ const columns = [
263
315
 
264
316
  **Use Case:** Add filters like status, department, date range, etc.
265
317
 
318
+ **Performance:** Query parameters are sent with every request but **do NOT auto-refetch**. This prevents unnecessary API calls when you have multiple filters. Call `refresh()` manually when ready.
319
+
266
320
  ```vue
267
321
  <script setup>
268
322
  import { ref } from 'vue'
269
323
 
324
+ const tableRef = ref()
270
325
  const filters = ref({
271
326
  status: 'active',
272
327
  department_id: 5,
273
328
  year: 2025
274
329
  })
275
330
 
276
- function updateStatus(newStatus) {
277
- filters.value.status = newStatus
278
- // Table automatically refetches!
331
+ function applyFilters() {
332
+ // After setting all filters, manually refresh
333
+ tableRef.value?.refresh()
279
334
  }
280
335
  </script>
281
336
 
@@ -291,10 +346,13 @@ function updateStatus(newStatus) {
291
346
  <option :value="1">IT</option>
292
347
  <option :value="5">HR</option>
293
348
  </select>
349
+
350
+ <button @click="applyFilters" class="btn">Apply Filters</button>
294
351
  </div>
295
352
 
296
353
  <!-- Table with filters -->
297
354
  <SimpleTable
355
+ ref="tableRef"
298
356
  fetch-url="/api/users"
299
357
  :columns="columns"
300
358
  :query-params="filters"
@@ -302,6 +360,8 @@ function updateStatus(newStatus) {
302
360
  </template>
303
361
  ```
304
362
 
363
+ **Important:** Query parameters are **NOT automatically watched**. This prevents multiple API calls when you have many filters. Call `tableRef.value?.refresh()` manually when you want to refetch.
364
+
305
365
  **API Request:**
306
366
  ```
307
367
  GET /api/users?page=1&per_page=10&status=active&department_id=5&year=2025
@@ -379,24 +439,316 @@ function handleCreate() {
379
439
 
380
440
  ### Custom Actions and Slots
381
441
 
382
- **Add custom buttons to the toolbar:**
442
+ **Add custom buttons to the toolbar with access to table data:**
383
443
 
384
444
  ```vue
385
- <SimpleTable :columns="columns" fetch-url="/api/users">
386
- <template #actions="{ rows }">
387
- <button @click="exportCustom(rows)" class="btn">
388
- Custom Export
389
- </button>
390
- <button @click="bulkDelete(rows)" class="btn btn-danger">
391
- Bulk Delete
392
- </button>
393
- </template>
394
- </SimpleTable>
445
+ <script setup>
446
+ import { Download, Printer, Trash } from 'lucide-vue-next'
447
+
448
+ const handleExport = (type, rows) => {
449
+ console.log(`Exporting ${rows.length} rows as ${type}`)
450
+
451
+ if (type === 'csv') {
452
+ const csv = rows.map(row =>
453
+ `${row.id},${row.name},${row.email}`
454
+ ).join('\n')
455
+
456
+ downloadCSV(csv, 'export.csv')
457
+ }
458
+ }
459
+
460
+ const handleBulkDelete = (rows) => {
461
+ const ids = rows.map(r => r.id)
462
+ if (confirm(`Delete ${ids.length} items?`)) {
463
+ axios.delete('/api/bulk-delete', { data: { ids } })
464
+ }
465
+ }
466
+
467
+ const downloadCSV = (content, filename) => {
468
+ const blob = new Blob([content], { type: 'text/csv' })
469
+ const url = URL.createObjectURL(blob)
470
+ const a = document.createElement('a')
471
+ a.href = url
472
+ a.download = filename
473
+ a.click()
474
+ }
475
+ </script>
476
+
477
+ <template>
478
+ <SimpleTable :columns="columns" fetch-url="/api/users">
479
+ <template #actions="{ rows, columns }">
480
+ <Button @click="handleExport('csv', rows)">
481
+ <Download class="mr-2 h-4 w-4" />
482
+ Export ({{ rows.length }})
483
+ </Button>
484
+
485
+ <Button @click="handleExport('excel', rows)">
486
+ <Download class="mr-2 h-4 w-4" />
487
+ Excel
488
+ </Button>
489
+
490
+ <Button @click="handleBulkDelete(rows)" variant="destructive">
491
+ <Trash class="mr-2 h-4 w-4" />
492
+ Bulk Delete
493
+ </Button>
494
+ </template>
495
+ </SimpleTable>
496
+ </template>
497
+ ```
498
+
499
+ **Slot Props:**
500
+ - **`rows`**: Currently visible table data (array of objects)
501
+ - **`columns`**: Column definitions (array of column config)
502
+
503
+ **Use Cases:**
504
+ - βœ… Custom export buttons (CSV, Excel, PDF)
505
+ - βœ… Bulk actions (delete, update, approve)
506
+ - βœ… Print functionality
507
+ - βœ… Custom filters or search
508
+ - βœ… Integration with third-party libraries
509
+
510
+
511
+ ---
512
+
513
+ ### Group Headers
514
+
515
+ **Organize your table data with full-width group headers:**
516
+
517
+ ```vue
518
+ <script setup>
519
+ const addGroupHeaders = (rows) => {
520
+ // Sort by category first
521
+ const sorted = [...rows].sort((a, b) =>
522
+ (a.category || '').localeCompare(b.category || '')
523
+ )
524
+
525
+ const result = []
526
+ let currentCategory = null
527
+
528
+ sorted.forEach(row => {
529
+ const category = row.category || 'Uncategorized'
530
+
531
+ // When category changes, add a header row
532
+ if (category !== currentCategory) {
533
+ result.push({
534
+ _isGroupHeader: true, // Special flag
535
+ _groupTitle: category, // Header text
536
+ // Add empty values for all columns
537
+ ...Object.fromEntries(
538
+ Object.keys(row).map(key => [key, ''])
539
+ )
540
+ })
541
+ currentCategory = category
542
+ }
543
+
544
+ result.push(row)
545
+ })
546
+
547
+ return result
548
+ }
549
+ </script>
550
+
551
+ <template>
552
+ <SimpleTable
553
+ :columns="columns"
554
+ :before-render="addGroupHeaders"
555
+ odd-row-color="bg-gray-50"
556
+ even-row-color="bg-white"
557
+ fetch-url="/api/data"
558
+ />
559
+ </template>
560
+ ```
561
+
562
+ **Result:**
563
+ ```
564
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
565
+ β”‚ CATEGORY A β”‚ ← Full-width header
566
+ β”œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
567
+ β”‚ 1 β”‚ Item 1 β”‚ $100 β”‚ Active β”‚
568
+ β”‚ 2 β”‚ Item 2 β”‚ $200 β”‚ Active β”‚
569
+ β”œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
570
+ β”‚ CATEGORY B β”‚ ← Full-width header
571
+ β”œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
572
+ β”‚ 3 β”‚ Item 3 β”‚ $150 β”‚ Inactive β”‚
573
+ ```
574
+
575
+ **How It Works:**
576
+ 1. Use `beforeRender` callback to transform data
577
+ 2. Insert rows with `_isGroupHeader: true` flag
578
+ 3. Component renders these as full-width cells with `colspan`
579
+ 4. Striping continues correctly across all rows
580
+
581
+ **Grouping Examples:**
582
+
583
+ ```vue
584
+ // Group by first letter
585
+ const groupByLetter = (rows) => {
586
+ const sorted = [...rows].sort((a, b) =>
587
+ a.name.localeCompare(b.name)
588
+ )
589
+
590
+ const result = []
591
+ let currentLetter = null
592
+
593
+ sorted.forEach(row => {
594
+ const letter = row.name.charAt(0).toUpperCase()
595
+
596
+ if (letter !== currentLetter) {
597
+ result.push({
598
+ _isGroupHeader: true,
599
+ _groupTitle: letter,
600
+ })
601
+ currentLetter = letter
602
+ }
603
+
604
+ result.push(row)
605
+ })
606
+
607
+ return result
608
+ }
609
+
610
+ // Group by date range
611
+ const groupByDate = (rows) => {
612
+ const sorted = [...rows].sort((a, b) =>
613
+ new Date(b.created_at) - new Date(a.created_at)
614
+ )
615
+
616
+ const result = []
617
+ let currentMonth = null
618
+
619
+ sorted.forEach(row => {
620
+ const month = new Date(row.created_at).toLocaleDateString('en', {
621
+ year: 'numeric',
622
+ month: 'long'
623
+ })
624
+
625
+ if (month !== currentMonth) {
626
+ result.push({
627
+ _isGroupHeader: true,
628
+ _groupTitle: month,
629
+ })
630
+ currentMonth = month
631
+ }
632
+
633
+ result.push(row)
634
+ })
635
+
636
+ return result
637
+ }
638
+
639
+ // Group by status
640
+ const groupByStatus = (rows) => {
641
+ const active = rows.filter(r => r.is_active)
642
+ const inactive = rows.filter(r => !r.is_active)
643
+
644
+ return [
645
+ { _isGroupHeader: true, _groupTitle: 'Active Items' },
646
+ ...active,
647
+ { _isGroupHeader: true, _groupTitle: 'Inactive Items' },
648
+ ...inactive
649
+ ]
650
+ }
651
+ ```
652
+
653
+ **Styling Group Headers:**
654
+
655
+ The component applies these classes to group header rows:
656
+ - `border-b border-gray-200` - Bottom border
657
+ - Font styling via the inner div
658
+ - Row striping colors (odd-row-color / even-row-color)
659
+
660
+ You can customize by overriding row colors:
661
+ ```vue
662
+ <SimpleTable
663
+ :before-render="addGroupHeaders"
664
+ odd-row-color="bg-blue-50"
665
+ even-row-color="bg-white"
666
+ />
667
+ ```
668
+
669
+ ---
670
+
671
+ ### Data Transformation (beforeRender)
672
+
673
+ **Transform data before it's rendered in the table:**
674
+
675
+ ```vue
676
+ <script setup>
677
+ import dayjs from 'dayjs'
678
+
679
+ const transformData = (rows) => {
680
+ return rows.map(row => ({
681
+ ...row,
682
+ // Add computed properties
683
+ full_name: `${row.first_name} ${row.last_name}`,
684
+
685
+ // Format dates
686
+ created_at_formatted: dayjs(row.created_at).format('MMM D, YYYY'),
687
+
688
+ // Add status badge classes
689
+ status_class: row.status === 'active' ? 'text-green-600' : 'text-red-600',
690
+
691
+ // Transform arrays
692
+ tags_joined: row.tags?.join(', ') || 'No tags',
693
+
694
+ // Add custom logic
695
+ is_urgent: row.priority > 8,
696
+ days_since_created: dayjs().diff(dayjs(row.created_at), 'days')
697
+ }))
698
+ }
699
+ </script>
700
+
701
+ <template>
702
+ <SimpleTable
703
+ :columns="columns"
704
+ fetch-url="/api/users"
705
+ :before-render="transformData"
706
+ />
707
+ </template>
708
+ ```
709
+
710
+ **When to Use:**
711
+ - βœ… Format dates, numbers, or currencies
712
+ - βœ… Combine multiple fields into one
713
+ - βœ… Add computed properties
714
+ - βœ… Transform nested objects to flat properties
715
+ - βœ… Add CSS classes based on data
716
+ - βœ… Filter unwanted rows (return modified array)
717
+
718
+ **Example - Adding Full Names:**
719
+ ```vue
720
+ <script setup>
721
+ const columns = [
722
+ { key: 'full_name', label: 'Name' }, // Not in API response
723
+ { key: 'email', label: 'Email' },
724
+ { key: 'created_at_formatted', label: 'Joined' }
725
+ ]
726
+
727
+ const beforeRender = (rows) => {
728
+ return rows.map(row => ({
729
+ ...row,
730
+ full_name: `${row.first_name} ${row.last_name}`,
731
+ created_at_formatted: new Date(row.created_at).toLocaleDateString()
732
+ }))
733
+ }
734
+ </script>
735
+
736
+ <template>
737
+ <SimpleTable
738
+ :columns="columns"
739
+ :before-render="beforeRender"
740
+ fetch-url="/api/users"
741
+ />
742
+ </template>
395
743
  ```
396
744
 
397
- **Access to:**
398
- - `rows`: Currently visible data
399
- - `columns`: Column definitions
745
+ **Execution Order:**
746
+ 1. Data fetched from API
747
+ 2. Filtering & Searching (client-side only)
748
+ 3. Sorting (client-side only)
749
+ 4. Pagination (client-side only)
750
+ 5. **`beforeRender` called** ← Your transformation here
751
+ 6. Rows rendered in table
400
752
 
401
753
  ---
402
754
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kikiloaw/simple-table",
3
- "version": "1.0.0",
3
+ "version": "1.0.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",
@@ -29,4 +29,4 @@
29
29
  "type": "git",
30
30
  "url": "git+https://github.com/kikiloaw/simple-table.git"
31
31
  }
32
- }
32
+ }
@@ -26,6 +26,7 @@ interface Props {
26
26
  class?: string
27
27
  fixed?: boolean // 'left' or 'right' could be added later, assuming 'right' for actions usually
28
28
  width?: string
29
+ autonumber?: boolean // If true, display auto-incremented row numbers instead of data
29
30
  }[]
30
31
  mode?: 'auto' | 'server' | 'client'
31
32
  protocol?: 'laravel' | 'datatables' // API request/response format
@@ -34,6 +35,9 @@ interface Props {
34
35
  pageSizes?: any[] // number[] or { label: string, value: number }[]
35
36
  fetchUrl?: string
36
37
 
38
+ // Callbacks
39
+ beforeRender?: (rows: any[]) => any[] // Transform data before rendering
40
+
37
41
  // Cache Props
38
42
  enableCache?: boolean // If true, cache responses by page/search/sort to avoid redundant requests
39
43
 
@@ -48,6 +52,7 @@ interface Props {
48
52
 
49
53
  const props = withDefaults(defineProps<Props>(), {
50
54
  data: () => [],
55
+ columns: () => [],
51
56
  mode: 'auto',
52
57
  protocol: 'laravel',
53
58
  searchable: true,
@@ -164,14 +169,6 @@ watch(() => props.data, (newVal) => {
164
169
  internalData.value = newVal
165
170
  }, { deep: true })
166
171
 
167
- // Watch queryParams and refetch when they change
168
- watch(() => props.queryParams, () => {
169
- if (isServerSide.value) {
170
- currentPage.value = 1 // Reset to first page when filters change
171
- fetchData()
172
- }
173
- }, { deep: true })
174
-
175
172
  // -- Computed: Mode Detection --
176
173
  const isServerSide = computed(() => {
177
174
  if (props.mode === 'server') return true
@@ -196,6 +193,7 @@ const serverMeta = computed(() => {
196
193
  return {
197
194
  current_page: meta.current_page ?? 1,
198
195
  last_page: meta.last_page ?? 1,
196
+ per_page: meta.per_page ?? currentPerPage.value,
199
197
  from: meta.from ?? 0,
200
198
  to: meta.to ?? 0,
201
199
  total: meta.total ?? 0,
@@ -205,39 +203,52 @@ const serverMeta = computed(() => {
205
203
 
206
204
  // -- Computed: Data Normalization --
207
205
  const tableData = computed(() => {
206
+ let result: any[] = []
207
+
208
208
  if (isServerSide.value) {
209
209
  const d = internalData.value as any
210
- return d.data || []
211
- }
212
-
213
- // Client Side Processing
214
- let items = [...(internalData.value as any[])]
210
+ result = d.data || []
211
+ } else {
212
+ // Client Side Processing
213
+ let items = [...(internalData.value as any[])]
215
214
 
216
- // 1. Filter
217
- if (searchQuery.value) {
218
- const lowerQuery = searchQuery.value.toLowerCase()
219
- items = items.filter((item) =>
220
- Object.values(item).some((val) =>
221
- String(val).toLowerCase().includes(lowerQuery)
215
+ // 1. Filter
216
+ if (searchQuery.value) {
217
+ const lowerQuery = searchQuery.value.toLowerCase()
218
+ items = items.filter((item) =>
219
+ Object.values(item).some((val) =>
220
+ String(val).toLowerCase().includes(lowerQuery)
221
+ )
222
222
  )
223
- )
224
- }
223
+ }
225
224
 
226
- // 2. Sort
227
- if (sortColumn.value) {
228
- items.sort((a, b) => {
229
- const valA = a[sortColumn.value]
230
- const valB = b[sortColumn.value]
231
- if (valA === valB) return 0
232
- const comparison = valA > valB ? 1 : -1
233
- return sortDirection.value === 'asc' ? comparison : -comparison
234
- })
235
- }
225
+ // 2. Sort
226
+ if (sortColumn.value) {
227
+ items.sort((a, b) => {
228
+ const valA = a[sortColumn.value]
229
+ const valB = b[sortColumn.value]
230
+ if (valA === valB) return 0
231
+ const comparison = valA > valB ? 1 : -1
232
+ return sortDirection.value === 'asc' ? comparison : -comparison
233
+ })
234
+ }
236
235
 
237
- // 3. Paginate
238
- const start = (currentPage.value - 1) * currentPerPage.value
239
- const end = start + currentPerPage.value
240
- return items.slice(start, end)
236
+ // 3. Paginate
237
+ const start = (currentPage.value - 1) * currentPerPage.value
238
+ const end = start + currentPerPage.value
239
+ result = items.slice(start, end)
240
+ }
241
+
242
+ // Apply beforeRender callback if provided
243
+ if (props.beforeRender && typeof props.beforeRender === 'function') {
244
+ const transformed = props.beforeRender(result)
245
+ // Ensure callback returns an array
246
+ if (Array.isArray(transformed)) {
247
+ result = transformed
248
+ }
249
+ }
250
+
251
+ return result
241
252
  })
242
253
 
243
254
  const totalPages = computed(() => {
@@ -539,6 +550,38 @@ function handlePageSizeChange(size: any) {
539
550
  fetchData()
540
551
  }
541
552
 
553
+ // Get row class with simple alternating stripes (all rows)
554
+ function getRowClass(row: any, idx: number) {
555
+ // Alternate: index 0 = white, index 1 = gray, index 2 = white, etc.
556
+ const isOdd = idx % 2 === 0 // Changed: even index = odd color (white)
557
+ return [
558
+ { [props.oddRowColor]: isOdd, [props.evenRowColor]: !isOdd },
559
+ row._isGroupHeader ? '' : props.hoverColor // No hover on headers
560
+ ]
561
+ }
562
+
563
+ // Get row number for auto-numbering (excluding group headers)
564
+ function getRowNumber(idx: number): number {
565
+ // Count only data rows before this index
566
+ let dataRowCount = 0
567
+ for (let i = 0; i <= idx; i++) {
568
+ if (!tableData.value[i]?._isGroupHeader) {
569
+ dataRowCount++
570
+ }
571
+ }
572
+
573
+ // Add offset for pagination
574
+ if (isServerSide.value) {
575
+ const currentPage = serverMeta.value?.current_page || 1
576
+ const perPage = serverMeta.value?.per_page || currentPerPage.value
577
+ const offset = (currentPage - 1) * perPage
578
+ return offset + dataRowCount
579
+ }
580
+
581
+ // Client-side: just return the count
582
+ return dataRowCount
583
+ }
584
+
542
585
  function handlePageChange(page: number) {
543
586
  if (page < 1 || page > totalPages.value) return
544
587
 
@@ -713,21 +756,38 @@ function getCellStyle(col: any) {
713
756
  v-for="(row, idx) in tableData"
714
757
  :key="idx"
715
758
  class="group"
716
- :class="[{ [evenRowColor]: Number(idx) % 2 === 0, [oddRowColor]: Number(idx) % 2 !== 0 }, hoverColor]"
759
+ :class="getRowClass(row, idx)"
717
760
  >
718
- <TableCell
719
- v-for="(col, cIdx) in columns"
720
- :key="col.key"
721
- :class="getCellClass(col, cIdx, columns.length, Number(idx))"
722
- :style="getCellStyle(col)"
723
- >
724
- <!-- Scoped Slot for custom cell rendering -->
725
- <div>
726
- <slot :name="`cell-${col.key}`" :row="row">
727
- {{ row[col.key] }}
728
- </slot>
729
- </div>
730
- </TableCell>
761
+ <!-- Group Header Row: Single cell spanning all columns -->
762
+ <template v-if="row._isGroupHeader">
763
+ <TableCell :colspan="columns.length" class="border-b border-gray-200">
764
+ <div class="px-2 py-2 font-semibold text-gray-700 text-sm uppercase tracking-wide">
765
+ {{ row._groupTitle }}
766
+ </div>
767
+ </TableCell>
768
+ </template>
769
+
770
+ <!-- Regular Data Row: Individual cells -->
771
+ <template v-else>
772
+ <TableCell
773
+ v-for="(col, cIdx) in columns"
774
+ :key="col.key"
775
+ :class="getCellClass(col, cIdx, columns.length, idx)"
776
+ :style="getCellStyle(col)"
777
+ >
778
+ <!-- Auto-numbering or custom cell rendering -->
779
+ <div>
780
+ <template v-if="col.autonumber">
781
+ {{ getRowNumber(idx) }}
782
+ </template>
783
+ <template v-else>
784
+ <slot :name="`cell-${col.key}`" :row="row">
785
+ {{ row[col.key] }}
786
+ </slot>
787
+ </template>
788
+ </div>
789
+ </TableCell>
790
+ </template>
731
791
  </TableRow>
732
792
  </template>
733
793
  <TableRow v-else>