@kikiloaw/simple-table 1.0.1 → 1.0.3

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
@@ -87,6 +87,307 @@ public function getData(Request $request)
87
87
 
88
88
  ---
89
89
 
90
+ ## 📦 Using Predefined/Static Data
91
+
92
+ **Want to use static data instead of an API?** SimpleTable handles this perfectly in client-side mode!
93
+
94
+ ### Basic Setup
95
+
96
+ ```vue
97
+ <script setup lang="ts">
98
+ import SimpleTable from '@kikiloaw/simple-table'
99
+
100
+ // Define your static data
101
+ const data = [
102
+ { id: 1, name: 'John Doe', email: 'john@example.com', status: 'active' },
103
+ { id: 2, name: 'Jane Smith', email: 'jane@example.com', status: 'active' },
104
+ { id: 3, name: 'Bob Johnson', email: 'bob@example.com', status: 'inactive' },
105
+ // ... more rows
106
+ ]
107
+
108
+ const columns = [
109
+ { key: 'id', label: '#', sortable: true, width: '80px' },
110
+ { key: 'name', label: 'Name', sortable: true },
111
+ { key: 'email', label: 'Email', sortable: true },
112
+ { key: 'status', label: 'Status', width: '120px' }
113
+ ]
114
+
115
+ const pageSizes = [
116
+ { label: '50 Rows', value: 50 },
117
+ { label: '100 Rows', value: 100 },
118
+ ]
119
+ </script>
120
+
121
+ <template>
122
+ <SimpleTable
123
+ :data="data"
124
+ :columns="columns"
125
+ :page-sizes="pageSizes"
126
+ :per-page="50"
127
+ mode="client"
128
+ searchable
129
+ />
130
+ </template>
131
+ ```
132
+
133
+ ### Key Props for Static Data
134
+
135
+ | Prop | Required | Default | Description |
136
+ |------|----------|---------|-------------|
137
+ | `:data` | **Yes** | `[]` | Your static array of objects |
138
+ | `mode` | **Yes** | `'auto'` | Set to `"client"` for static data |
139
+ | `:per-page` | Recommended | `10` | Initial page size (should match first option in pageSizes) |
140
+ | `:page-sizes` | Optional | `[10,20,30,50,100]` | Available page size options |
141
+
142
+ ### ⚠️ Common Pitfalls
143
+
144
+ #### 1. **Don't Mix Static Data with `fetch-url`**
145
+
146
+ ❌ **Wrong:**
147
+ ```vue
148
+ <!-- This will ignore your static data! -->
149
+ <SimpleTable
150
+ :data="myData"
151
+ fetch-url="/api/users" <!-- ❌ Conflicts with :data -->
152
+ />
153
+ ```
154
+
155
+ ✅ **Correct:**
156
+ ```vue
157
+ <!-- Remove fetch-url when using static data -->
158
+ <SimpleTable
159
+ :data="myData"
160
+ mode="client"
161
+ />
162
+ ```
163
+
164
+ #### 2. **Set Initial Page Size to Match Your Options**
165
+
166
+ ❌ **Wrong:**
167
+ ```vue
168
+ <!-- Component defaults to 10, but you only have 50/100 options -->
169
+ <SimpleTable
170
+ :data="data"
171
+ :page-sizes="[{ label: '50 Rows', value: 50 }, { label: '100 Rows', value: 100 }]"
172
+ <!-- ❌ Will show "10 Rows" which doesn't exist in dropdown -->
173
+ />
174
+ ```
175
+
176
+ ✅ **Correct:**
177
+ ```vue
178
+ <SimpleTable
179
+ :data="data"
180
+ :page-sizes="[50, 100]"
181
+ :per-page="50" <!-- ✅ Matches first option -->
182
+ />
183
+ ```
184
+
185
+ #### 3. **Columns Must Match Your Data Structure**
186
+
187
+ ❌ **Wrong:**
188
+ ```vue
189
+ <script setup>
190
+ const data = [
191
+ { CourseCode: 'CS101', Description: 'Intro to CS', Units: 3 }
192
+ ]
193
+
194
+ const columns = [
195
+ { key: 'course_code', label: 'Code' }, // ❌ Wrong key!
196
+ { key: 'description', label: 'Name' }, // ❌ Wrong key!
197
+ ]
198
+ </script>
199
+ ```
200
+
201
+ ✅ **Correct:**
202
+ ```vue
203
+ <script setup>
204
+ const data = [
205
+ { CourseCode: 'CS101', Description: 'Intro to CS', Units: 3 }
206
+ ]
207
+
208
+ const columns = [
209
+ { key: 'CourseCode', label: 'Code' }, // ✅ Matches data
210
+ { key: 'Description', label: 'Name' }, // ✅ Matches data
211
+ { key: 'Units', label: 'Units' },
212
+ ]
213
+ </script>
214
+ ```
215
+
216
+ ### Features Available in Client-Side Mode
217
+
218
+ ✅ **Works:**
219
+ - Client-side searching (filters through your data array)
220
+ - Client-side sorting (by sortable columns)
221
+ - Client-side pagination (chunks your data into pages)
222
+ - Data transformation via `beforeRender`
223
+ - Custom cell rendering
224
+ - Group headers
225
+ - Auto-numbering
226
+
227
+ ❌ **Not Available:**
228
+ - Server-side sorting (data is sorted locally)
229
+ - API caching (no API calls)
230
+ - Query parameters (no server to send them to)
231
+
232
+ ### Complete Example with Group Headers
233
+
234
+ ```vue
235
+ <script setup lang="ts">
236
+ import SimpleTable from '@kikiloaw/simple-table'
237
+
238
+ // Static course data
239
+ const data = [
240
+ {
241
+ CourseCode: 'CS101',
242
+ Description: 'Intro to Computer Science',
243
+ Units: 3,
244
+ Grade: 'A',
245
+ semester: '1st Semester, 2023-2024'
246
+ },
247
+ {
248
+ CourseCode: 'MATH101',
249
+ Description: 'Calculus I',
250
+ Units: 4,
251
+ Grade: 'B+',
252
+ semester: '1st Semester, 2023-2024'
253
+ },
254
+ {
255
+ CourseCode: 'CS102',
256
+ Description: 'Data Structures',
257
+ Units: 3,
258
+ Grade: 'A-',
259
+ semester: '2nd Semester, 2023-2024'
260
+ },
261
+ ]
262
+
263
+ const columns = [
264
+ { key: 'CourseCode', label: 'Course Code', sortable: true, width: '150px' },
265
+ { key: 'Description', label: 'Description', sortable: true, width: '300px' },
266
+ { key: 'Units', label: 'Units', width: '80px' },
267
+ { key: 'Grade', label: 'Grade', sortable: true, width: '80px' },
268
+ ]
269
+
270
+ // Add group headers by semester
271
+ const addGroupHeaders = (rows) => {
272
+ // DON'T sort here if your data is already in the correct order!
273
+ // Sorting will override your predefined order
274
+
275
+ const result = []
276
+ let currentSemester = null
277
+
278
+ rows.forEach(row => {
279
+ const semester = row.semester || 'No Semester'
280
+
281
+ // When semester changes, add a header row
282
+ if (semester !== currentSemester) {
283
+ result.push({
284
+ _isGroupHeader: true,
285
+ _groupTitle: semester,
286
+ // Empty values for all columns
287
+ CourseCode: '',
288
+ Description: semester,
289
+ Units: '',
290
+ Grade: '',
291
+ })
292
+ currentSemester = semester
293
+ }
294
+
295
+ result.push(row)
296
+ })
297
+
298
+ return result
299
+ }
300
+ </script>
301
+
302
+ <template>
303
+ <SimpleTable
304
+ :data="data"
305
+ :columns="columns"
306
+ :page-sizes="[50, 100]"
307
+ :per-page="50"
308
+ :before-render="addGroupHeaders"
309
+ mode="client"
310
+ searchable
311
+ odd-row-color="bg-gray-50"
312
+ even-row-color="bg-white"
313
+ hover-color="hover:bg-green-100"
314
+ />
315
+ </template>
316
+
317
+ ### 📏 Row Height Control
318
+
319
+ Control the exact height of your table rows with the `rowHeight` prop:
320
+
321
+ ```vue
322
+ <template>
323
+ <SimpleTable
324
+ :data="data"
325
+ :columns="columns"
326
+ :row-height="38" <!-- Rows will be exactly 38px tall -->
327
+ />
328
+ </template>
329
+ ```
330
+
331
+ **How it works:**
332
+ - Sets the `height` style on both header and data rows
333
+ - Automatically adjusts cell padding based on the height
334
+ - Default: `38px` (compact and readable)
335
+
336
+ **Recommended Values:**
337
+
338
+ | Height | Padding | Use Case |
339
+ |--------|---------|----------|
340
+ | `30-36px` | `p-2` (8px) | **Extra compact** - Maximum rows visible, dense data |
341
+ | `38-42px` | `p-2` (8px) | **Standard** - Good balance (default: 38px) |
342
+ | `44-55px` | `p-3` (12px) | **Comfortable** - Easy to read, spacious |
343
+ | `56px+` | `p-4` (16px) | **Very spacious** - Accessibility-friendly, large text |
344
+
345
+ **Examples:**
346
+
347
+ ```vue
348
+ <!-- Ultra compact for dashboards -->
349
+ <SimpleTable :row-height="32" />
350
+
351
+ <!-- Default - balanced -->
352
+ <SimpleTable :row-height="38" /> <!-- or omit for default -->
353
+
354
+ <!-- Comfortable reading -->
355
+ <SimpleTable :row-height="48" />
356
+
357
+ <!-- Accessibility-friendly -->
358
+ <SimpleTable :row-height="60" />
359
+
360
+ <!-- No prop = uses default 38px -->
361
+ <SimpleTable :data="data" :columns="columns" />
362
+ ```
363
+
364
+ **Padding Auto-Adjustment:**
365
+
366
+ The component automatically adjusts internal padding:
367
+ - **< 44px**: Uses `p-2` (8px) - Compact
368
+ - **44-55px**: Uses `p-3` (12px) - Normal
369
+ - **56px+**: Uses `p-4` (16px) - Comfortable
370
+
371
+ ### 💡 Best Practices
372
+
373
+ 1. **Data Order Preservation**
374
+ - If your data is already sorted correctly, **don't sort it again** in `beforeRender`
375
+ - Let users sort by clicking column headers if needed
376
+
377
+ 2. **Performance**
378
+ - Client-side mode works great for **< 1,000 rows**
379
+ - For larger datasets, consider server-side mode with `fetch-url`
380
+
381
+ 3. **Reactivity**
382
+ - Use `ref()` or `reactive()` if your data changes
383
+ - The table will automatically update when data changes
384
+
385
+ 4. **Page Size Options**
386
+ - Keep options reasonable: `[10, 25, 50, 100]`
387
+ - Set `:per-page` to match your first option
388
+
389
+ ---
390
+
90
391
  ## 📖 Table of Contents
91
392
 
92
393
  - [Core Concepts](#-core-concepts)
@@ -212,6 +513,14 @@ const columns = [
212
513
  label: 'Email'
213
514
  },
214
515
 
516
+ // Auto-numbering (row numbers instead of data)
517
+ {
518
+ key: 'id',
519
+ label: '#',
520
+ autonumber: true,
521
+ width: '80px'
522
+ },
523
+
215
524
  // Sticky actions column (always visible)
216
525
  {
217
526
  key: 'actions',
@@ -233,6 +542,50 @@ Use custom sort keys when:
233
542
 
234
543
  ## 🎨 Features
235
544
 
545
+ ### Auto-Numbering
546
+
547
+ **Display sequential row numbers instead of actual data:**
548
+
549
+ ```vue
550
+ const columns = [
551
+ {
552
+ key: 'id',
553
+ label: '#',
554
+ autonumber: true,
555
+ width: '80px'
556
+ },
557
+ // ... other columns
558
+ ]
559
+ ```
560
+
561
+ **Features:**
562
+ - ✅ Displays 1, 2, 3, 4... for each data row
563
+ - ✅ Skips group headers (only counts data rows)
564
+ - ✅ Pagination-aware: Page 2 shows 11, 12, 13... (with 10 per page)
565
+ - ✅ Overrides actual column data
566
+ - ✅ Works with both server-side and client-side modes
567
+
568
+ **Example:**
569
+ ```
570
+ ┌────┬──────────────────┬─────────┐
571
+ │ # │ Name │ Status │
572
+ ├────┼──────────────────┼─────────┤
573
+ │ ACTIVE USERS │ ← Header (not counted)
574
+ │ 1 │ John Doe │ Active │
575
+ │ 2 │ Jane Smith │ Active │
576
+ ├────┴──────────────────┴─────────┤
577
+ │ INACTIVE USERS │ ← Header (not counted)
578
+ │ 3 │ Bob Johnson │ Inactive│
579
+ ```
580
+
581
+ **Perfect for:**
582
+ - Sequential numbering regardless of actual IDs
583
+ - User-friendly row references
584
+ - Tables with group headers
585
+ - Paginated lists with continuous numbering
586
+
587
+ ---
588
+
236
589
  ### Custom Sort Keys
237
590
 
238
591
  **Problem:** You want to display `department.name` but sort by `department_id`.
@@ -456,6 +809,248 @@ const downloadCSV = (content, filename) => {
456
809
  - ✅ Integration with third-party libraries
457
810
 
458
811
 
812
+ ---
813
+
814
+ ### Group Headers
815
+
816
+ **Organize your table data with full-width group headers:**
817
+
818
+ ```vue
819
+ <script setup>
820
+ const addGroupHeaders = (rows) => {
821
+ // Sort by category first
822
+ const sorted = [...rows].sort((a, b) =>
823
+ (a.category || '').localeCompare(b.category || '')
824
+ )
825
+
826
+ const result = []
827
+ let currentCategory = null
828
+
829
+ sorted.forEach(row => {
830
+ const category = row.category || 'Uncategorized'
831
+
832
+ // When category changes, add a header row
833
+ if (category !== currentCategory) {
834
+ result.push({
835
+ _isGroupHeader: true, // Special flag
836
+ _groupTitle: category, // Header text
837
+ // Add empty values for all columns
838
+ ...Object.fromEntries(
839
+ Object.keys(row).map(key => [key, ''])
840
+ )
841
+ })
842
+ currentCategory = category
843
+ }
844
+
845
+ result.push(row)
846
+ })
847
+
848
+ return result
849
+ }
850
+ </script>
851
+
852
+ <template>
853
+ <SimpleTable
854
+ :columns="columns"
855
+ :before-render="addGroupHeaders"
856
+ odd-row-color="bg-gray-50"
857
+ even-row-color="bg-white"
858
+ fetch-url="/api/data"
859
+ />
860
+ </template>
861
+ ```
862
+
863
+ **Result:**
864
+ ```
865
+ ┌────────────────────────────────────────┐
866
+ │ CATEGORY A │ ← Full-width header
867
+ ├─────┬──────────┬──────────┬────────────┤
868
+ │ 1 │ Item 1 │ $100 │ Active │
869
+ │ 2 │ Item 2 │ $200 │ Active │
870
+ ├─────┴──────────┴──────────┴────────────┤
871
+ │ CATEGORY B │ ← Full-width header
872
+ ├─────┬──────────┬──────────┬────────────┤
873
+ │ 3 │ Item 3 │ $150 │ Inactive │
874
+ ```
875
+
876
+ **How It Works:**
877
+ 1. Use `beforeRender` callback to transform data
878
+ 2. Insert rows with `_isGroupHeader: true` flag
879
+ 3. Component renders these as full-width cells with `colspan`
880
+ 4. Striping continues correctly across all rows
881
+
882
+ **Grouping Examples:**
883
+
884
+ ```vue
885
+ // Group by first letter
886
+ const groupByLetter = (rows) => {
887
+ const sorted = [...rows].sort((a, b) =>
888
+ a.name.localeCompare(b.name)
889
+ )
890
+
891
+ const result = []
892
+ let currentLetter = null
893
+
894
+ sorted.forEach(row => {
895
+ const letter = row.name.charAt(0).toUpperCase()
896
+
897
+ if (letter !== currentLetter) {
898
+ result.push({
899
+ _isGroupHeader: true,
900
+ _groupTitle: letter,
901
+ })
902
+ currentLetter = letter
903
+ }
904
+
905
+ result.push(row)
906
+ })
907
+
908
+ return result
909
+ }
910
+
911
+ // Group by date range
912
+ const groupByDate = (rows) => {
913
+ const sorted = [...rows].sort((a, b) =>
914
+ new Date(b.created_at) - new Date(a.created_at)
915
+ )
916
+
917
+ const result = []
918
+ let currentMonth = null
919
+
920
+ sorted.forEach(row => {
921
+ const month = new Date(row.created_at).toLocaleDateString('en', {
922
+ year: 'numeric',
923
+ month: 'long'
924
+ })
925
+
926
+ if (month !== currentMonth) {
927
+ result.push({
928
+ _isGroupHeader: true,
929
+ _groupTitle: month,
930
+ })
931
+ currentMonth = month
932
+ }
933
+
934
+ result.push(row)
935
+ })
936
+
937
+ return result
938
+ }
939
+
940
+ // Group by status
941
+ const groupByStatus = (rows) => {
942
+ const active = rows.filter(r => r.is_active)
943
+ const inactive = rows.filter(r => !r.is_active)
944
+
945
+ return [
946
+ { _isGroupHeader: true, _groupTitle: 'Active Items' },
947
+ ...active,
948
+ { _isGroupHeader: true, _groupTitle: 'Inactive Items' },
949
+ ...inactive
950
+ ]
951
+ }
952
+ ```
953
+
954
+ **Styling Group Headers:**
955
+
956
+ The component applies these classes to group header rows:
957
+ - `border-b border-gray-200` - Bottom border
958
+ - Font styling via the inner div
959
+ - Row striping colors (odd-row-color / even-row-color)
960
+
961
+ You can customize by overriding row colors:
962
+ ```vue
963
+ <SimpleTable
964
+ :before-render="addGroupHeaders"
965
+ odd-row-color="bg-blue-50"
966
+ even-row-color="bg-white"
967
+ />
968
+ ```
969
+
970
+ ---
971
+
972
+ ### Data Transformation (beforeRender)
973
+
974
+ **Transform data before it's rendered in the table:**
975
+
976
+ ```vue
977
+ <script setup>
978
+ import dayjs from 'dayjs'
979
+
980
+ const transformData = (rows) => {
981
+ return rows.map(row => ({
982
+ ...row,
983
+ // Add computed properties
984
+ full_name: `${row.first_name} ${row.last_name}`,
985
+
986
+ // Format dates
987
+ created_at_formatted: dayjs(row.created_at).format('MMM D, YYYY'),
988
+
989
+ // Add status badge classes
990
+ status_class: row.status === 'active' ? 'text-green-600' : 'text-red-600',
991
+
992
+ // Transform arrays
993
+ tags_joined: row.tags?.join(', ') || 'No tags',
994
+
995
+ // Add custom logic
996
+ is_urgent: row.priority > 8,
997
+ days_since_created: dayjs().diff(dayjs(row.created_at), 'days')
998
+ }))
999
+ }
1000
+ </script>
1001
+
1002
+ <template>
1003
+ <SimpleTable
1004
+ :columns="columns"
1005
+ fetch-url="/api/users"
1006
+ :before-render="transformData"
1007
+ />
1008
+ </template>
1009
+ ```
1010
+
1011
+ **When to Use:**
1012
+ - ✅ Format dates, numbers, or currencies
1013
+ - ✅ Combine multiple fields into one
1014
+ - ✅ Add computed properties
1015
+ - ✅ Transform nested objects to flat properties
1016
+ - ✅ Add CSS classes based on data
1017
+ - ✅ Filter unwanted rows (return modified array)
1018
+
1019
+ **Example - Adding Full Names:**
1020
+ ```vue
1021
+ <script setup>
1022
+ const columns = [
1023
+ { key: 'full_name', label: 'Name' }, // Not in API response
1024
+ { key: 'email', label: 'Email' },
1025
+ { key: 'created_at_formatted', label: 'Joined' }
1026
+ ]
1027
+
1028
+ const beforeRender = (rows) => {
1029
+ return rows.map(row => ({
1030
+ ...row,
1031
+ full_name: `${row.first_name} ${row.last_name}`,
1032
+ created_at_formatted: new Date(row.created_at).toLocaleDateString()
1033
+ }))
1034
+ }
1035
+ </script>
1036
+
1037
+ <template>
1038
+ <SimpleTable
1039
+ :columns="columns"
1040
+ :before-render="beforeRender"
1041
+ fetch-url="/api/users"
1042
+ />
1043
+ </template>
1044
+ ```
1045
+
1046
+ **Execution Order:**
1047
+ 1. Data fetched from API
1048
+ 2. Filtering & Searching (client-side only)
1049
+ 3. Sorting (client-side only)
1050
+ 4. Pagination (client-side only)
1051
+ 5. **`beforeRender` called** ← Your transformation here
1052
+ 6. Rows rendered in table
1053
+
459
1054
  ---
460
1055
 
461
1056
  ### Custom Cell Rendering
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kikiloaw/simple-table",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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",
@@ -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
 
@@ -41,6 +45,7 @@ interface Props {
41
45
  queryParams?: Record<string, any> // Additional parameters to send with every request (e.g., filters, user context)
42
46
 
43
47
  // Style Props
48
+ rowHeight?: number // Table row height in pixels (default: 38)
44
49
  oddRowColor?: string // Tailwind color class, e.g. 'bg-white'
45
50
  evenRowColor?: string // Tailwind color class, e.g. 'bg-gray-50'
46
51
  hoverColor?: string // Tailwind color class for hover, e.g. 'hover:bg-gray-100'. If passed, we'll try to apply group-hover for fixed cols.
@@ -56,6 +61,7 @@ const props = withDefaults(defineProps<Props>(), {
56
61
  queryParams: () => ({}),
57
62
  perPage: 10,
58
63
  pageSizes: () => [10, 20, 30, 50, 100],
64
+ rowHeight: 38,
59
65
  oddRowColor: 'bg-background',
60
66
  evenRowColor: 'bg-background',
61
67
  hoverColor: 'hover:bg-muted/50'
@@ -84,6 +90,37 @@ const normalizedPageSizes = computed(() => {
84
90
  return []
85
91
  })
86
92
 
93
+ // -- Computed: Row height-based sizing --
94
+ const densityConfig = computed(() => {
95
+ const height = props.rowHeight || 38
96
+
97
+ // Calculate padding based on height
98
+ // For 38px height: use p-2 (8px)
99
+ // For 48px height: use p-3 (12px)
100
+ // For 56px+ height: use p-4 (16px)
101
+ let cellPadding = 'p-2'
102
+ let headerPadding = 'px-2'
103
+ let groupHeaderPadding = 'py-1'
104
+
105
+ if (height >= 56) {
106
+ cellPadding = 'p-4'
107
+ headerPadding = 'px-4'
108
+ groupHeaderPadding = 'py-2'
109
+ } else if (height >= 44) {
110
+ cellPadding = 'p-3'
111
+ headerPadding = 'px-3'
112
+ groupHeaderPadding = 'py-1.5'
113
+ }
114
+
115
+ return {
116
+ cellPadding,
117
+ cellHeight: `${height}px`,
118
+ headerHeight: `h-[${height}px]`,
119
+ headerPadding,
120
+ groupHeaderPadding
121
+ }
122
+ })
123
+
87
124
 
88
125
 
89
126
  // <template>
@@ -189,6 +226,7 @@ const serverMeta = computed(() => {
189
226
  return {
190
227
  current_page: meta.current_page ?? 1,
191
228
  last_page: meta.last_page ?? 1,
229
+ per_page: meta.per_page ?? currentPerPage.value,
192
230
  from: meta.from ?? 0,
193
231
  to: meta.to ?? 0,
194
232
  total: meta.total ?? 0,
@@ -198,39 +236,52 @@ const serverMeta = computed(() => {
198
236
 
199
237
  // -- Computed: Data Normalization --
200
238
  const tableData = computed(() => {
239
+ let result: any[] = []
240
+
201
241
  if (isServerSide.value) {
202
242
  const d = internalData.value as any
203
- return d.data || []
204
- }
205
-
206
- // Client Side Processing
207
- let items = [...(internalData.value as any[])]
243
+ result = d.data || []
244
+ } else {
245
+ // Client Side Processing
246
+ let items = [...(internalData.value as any[])]
208
247
 
209
- // 1. Filter
210
- if (searchQuery.value) {
211
- const lowerQuery = searchQuery.value.toLowerCase()
212
- items = items.filter((item) =>
213
- Object.values(item).some((val) =>
214
- String(val).toLowerCase().includes(lowerQuery)
248
+ // 1. Filter
249
+ if (searchQuery.value) {
250
+ const lowerQuery = searchQuery.value.toLowerCase()
251
+ items = items.filter((item) =>
252
+ Object.values(item).some((val) =>
253
+ String(val).toLowerCase().includes(lowerQuery)
254
+ )
215
255
  )
216
- )
217
- }
256
+ }
218
257
 
219
- // 2. Sort
220
- if (sortColumn.value) {
221
- items.sort((a, b) => {
222
- const valA = a[sortColumn.value]
223
- const valB = b[sortColumn.value]
224
- if (valA === valB) return 0
225
- const comparison = valA > valB ? 1 : -1
226
- return sortDirection.value === 'asc' ? comparison : -comparison
227
- })
228
- }
258
+ // 2. Sort
259
+ if (sortColumn.value) {
260
+ items.sort((a, b) => {
261
+ const valA = a[sortColumn.value]
262
+ const valB = b[sortColumn.value]
263
+ if (valA === valB) return 0
264
+ const comparison = valA > valB ? 1 : -1
265
+ return sortDirection.value === 'asc' ? comparison : -comparison
266
+ })
267
+ }
229
268
 
230
- // 3. Paginate
231
- const start = (currentPage.value - 1) * currentPerPage.value
232
- const end = start + currentPerPage.value
233
- return items.slice(start, end)
269
+ // 3. Paginate
270
+ const start = (currentPage.value - 1) * currentPerPage.value
271
+ const end = start + currentPerPage.value
272
+ result = items.slice(start, end)
273
+ }
274
+
275
+ // Apply beforeRender callback if provided
276
+ if (props.beforeRender && typeof props.beforeRender === 'function') {
277
+ const transformed = props.beforeRender(result)
278
+ // Ensure callback returns an array
279
+ if (Array.isArray(transformed)) {
280
+ result = transformed
281
+ }
282
+ }
283
+
284
+ return result
234
285
  })
235
286
 
236
287
  const totalPages = computed(() => {
@@ -532,6 +583,38 @@ function handlePageSizeChange(size: any) {
532
583
  fetchData()
533
584
  }
534
585
 
586
+ // Get row class with simple alternating stripes (all rows)
587
+ function getRowClass(row: any, idx: number) {
588
+ // Alternate: index 0 = white, index 1 = gray, index 2 = white, etc.
589
+ const isOdd = idx % 2 === 0 // Changed: even index = odd color (white)
590
+ return [
591
+ { [props.oddRowColor]: isOdd, [props.evenRowColor]: !isOdd },
592
+ row._isGroupHeader ? '' : props.hoverColor // No hover on headers
593
+ ]
594
+ }
595
+
596
+ // Get row number for auto-numbering (excluding group headers)
597
+ function getRowNumber(idx: number): number {
598
+ // Count only data rows before this index
599
+ let dataRowCount = 0
600
+ for (let i = 0; i <= idx; i++) {
601
+ if (!tableData.value[i]?._isGroupHeader) {
602
+ dataRowCount++
603
+ }
604
+ }
605
+
606
+ // Add offset for pagination
607
+ if (isServerSide.value) {
608
+ const currentPage = serverMeta.value?.current_page || 1
609
+ const perPage = serverMeta.value?.per_page || currentPerPage.value
610
+ const offset = (currentPage - 1) * perPage
611
+ return offset + dataRowCount
612
+ }
613
+
614
+ // Client-side: just return the count
615
+ return dataRowCount
616
+ }
617
+
535
618
  function handlePageChange(page: number) {
536
619
  if (page < 1 || page > totalPages.value) return
537
620
 
@@ -672,12 +755,14 @@ function getCellStyle(col: any) {
672
755
  <!-- We add min-w-full to Table to ensure it stretches -->
673
756
  <Table class="min-w-full table-fixed">
674
757
  <TableHeader>
675
- <TableRow>
758
+ <TableRow :style="{ height: densityConfig.cellHeight }">
676
759
  <TableHead
677
760
  v-for="(col, idx) in columns"
678
761
  :key="col.key"
679
762
  :class="getCellClass(col, idx, columns.length)"
680
763
  :style="getCellStyle(col)"
764
+ :height="densityConfig.headerHeight"
765
+ :padding="densityConfig.headerPadding"
681
766
  >
682
767
  <div
683
768
  v-if="col.sortable"
@@ -706,24 +791,44 @@ function getCellStyle(col: any) {
706
791
  v-for="(row, idx) in tableData"
707
792
  :key="idx"
708
793
  class="group"
709
- :class="[{ [evenRowColor]: Number(idx) % 2 === 0, [oddRowColor]: Number(idx) % 2 !== 0 }, hoverColor]"
794
+ :class="getRowClass(row, idx)"
795
+ :style="{ height: densityConfig.cellHeight }"
710
796
  >
711
- <TableCell
712
- v-for="(col, cIdx) in columns"
713
- :key="col.key"
714
- :class="getCellClass(col, cIdx, columns.length, Number(idx))"
715
- :style="getCellStyle(col)"
716
- >
717
- <!-- Scoped Slot for custom cell rendering -->
718
- <div>
719
- <slot :name="`cell-${col.key}`" :row="row">
720
- {{ row[col.key] }}
721
- </slot>
722
- </div>
723
- </TableCell>
797
+ <!-- Group Header Row: Single cell spanning all columns -->
798
+ <template v-if="row._isGroupHeader">
799
+ <TableCell :colspan="columns.length" class="border-b border-gray-200">
800
+ <div :class="['px-2', densityConfig.groupHeaderPadding, 'font-semibold text-gray-700 text-sm uppercase tracking-wide']">
801
+ {{ row._groupTitle }}
802
+ </div>
803
+ </TableCell>
804
+ </template>
805
+
806
+ <!-- Regular Data Row: Individual cells -->
807
+ <template v-else>
808
+ <TableCell
809
+ v-for="(col, cIdx) in columns"
810
+ :key="col.key"
811
+ :class="getCellClass(col, cIdx, columns.length, idx)"
812
+ :style="getCellStyle(col)"
813
+ :padding="densityConfig.cellPadding"
814
+ :height="densityConfig.cellHeight"
815
+ >
816
+ <!-- Auto-numbering or custom cell rendering -->
817
+ <div>
818
+ <template v-if="col.autonumber">
819
+ {{ getRowNumber(idx) }}
820
+ </template>
821
+ <template v-else>
822
+ <slot :name="`cell-${col.key}`" :row="row">
823
+ {{ row[col.key] }}
824
+ </slot>
825
+ </template>
826
+ </div>
827
+ </TableCell>
828
+ </template>
724
829
  </TableRow>
725
830
  </template>
726
- <TableRow v-else>
831
+ <TableRow v-else :style="{ height: densityConfig.cellHeight }">
727
832
  <TableCell :colspan="columns.length" class="h-24 text-center">
728
833
  No results.
729
834
  </TableCell>
@@ -4,18 +4,29 @@ import { cn } from '@/lib/utils'
4
4
 
5
5
  const props = defineProps<{
6
6
  class?: HTMLAttributes['class']
7
+ style?: any
8
+ padding?: string
9
+ height?: string
7
10
  }>()
8
11
 
9
12
  const delegatedProps = computed(() => {
10
- const { class: _, ...delegated } = props
13
+ const { class: _, padding: __, height: ___, style: ____, ...delegated } = props
11
14
 
12
15
  return delegated
13
16
  })
17
+
18
+ const cellStyle = computed(() => {
19
+ const baseStyle = (props as any).style || {}
20
+ const heightStyle = props.height ? { minHeight: props.height } : {}
21
+
22
+ return { ...baseStyle, ...heightStyle }
23
+ })
14
24
  </script>
15
25
 
16
26
  <template>
17
27
  <td
18
- :class="cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', props.class)"
28
+ :class="cn(props.padding || 'p-2', 'align-middle [&:has([role=checkbox])]:pr-0', props.class)"
29
+ :style="cellStyle"
19
30
  v-bind="delegatedProps"
20
31
  >
21
32
  <slot />
@@ -4,10 +4,12 @@ import { cn } from '@/lib/utils'
4
4
 
5
5
  const props = defineProps<{
6
6
  class?: HTMLAttributes['class']
7
+ height?: string
8
+ padding?: string
7
9
  }>()
8
10
 
9
11
  const delegatedProps = computed(() => {
10
- const { class: _, ...delegated } = props
12
+ const { class: _, height: __, padding: ___, ...delegated } = props
11
13
 
12
14
  return delegated
13
15
  })
@@ -17,7 +19,9 @@ const delegatedProps = computed(() => {
17
19
  <th
18
20
  :class="
19
21
  cn(
20
- 'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
22
+ props.height || 'h-[38px]',
23
+ props.padding || 'px-2',
24
+ 'text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
21
25
  props.class,
22
26
  )
23
27
  "