@kikiloaw/simple-table 1.0.2 → 1.0.4

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)
@@ -754,28 +1055,73 @@ const beforeRender = (rows) => {
754
1055
 
755
1056
  ### Custom Cell Rendering
756
1057
 
757
- **Customize how data is displayed in specific columns:**
1058
+ **Customize how data is displayed in any column using named slots.**
1059
+
1060
+ **Slot Syntax:** `#cell-{columnKey}="{ row }"`
1061
+
1062
+ #### 1. Conditional Styling & Badges
758
1063
 
759
1064
  ```vue
760
- <SimpleTable :columns="columns" fetch-url="/api/users">
761
- <!-- Custom status badge -->
762
- <template #cell-status="{ row }">
1065
+ <SimpleTable :columns="columns" :data="data">
1066
+ <!-- Status Badge with Conditional Color -->
1067
+ <template #cell-is_active="{ row }">
763
1068
  <span
764
- :class="row.status === 'active' ? 'badge-success' : 'badge-danger'"
1069
+ v-if="!row._isGroupHeader"
1070
+ :class="row.is_active ? 'text-green-600 font-bold' : 'text-red-600'"
765
1071
  >
766
- {{ row.status }}
1072
+ {{ row.is_active ? 'Active' : 'Inactive' }}
767
1073
  </span>
768
1074
  </template>
769
-
770
- <!-- Custom actions column -->
1075
+ </SimpleTable>
1076
+ ```
1077
+
1078
+ #### 2. Rendering Lists/Tags
1079
+
1080
+ ```vue
1081
+ <SimpleTable :columns="columns" :data="data">
1082
+ <!-- Loop through array data in a cell -->
1083
+ <template #cell-tags="{ row }">
1084
+ <div v-if="!row._isGroupHeader" class="flex gap-1 flex-wrap">
1085
+ <span
1086
+ v-for="tag in row.tags"
1087
+ :key="tag.id"
1088
+ class="bg-blue-100 text-blue-800 text-xs px-2 py-0.5 rounded"
1089
+ >
1090
+ {{ tag.name }}
1091
+ </span>
1092
+ </div>
1093
+ </template>
1094
+ </SimpleTable>
1095
+ ```
1096
+
1097
+ #### 3. Action Buttons
1098
+
1099
+ ```vue
1100
+ <SimpleTable :columns="columns" :data="data">
771
1101
  <template #cell-actions="{ row }">
772
- <button @click="edit(row)" class="btn-sm">Edit</button>
773
- <button @click="delete(row)" class="btn-sm btn-danger">Delete</button>
1102
+ <div v-if="!row._isGroupHeader" class="flex gap-2">
1103
+ <Button variant="outline" size="sm" @click="edit(row)">Edit</Button>
1104
+ <Button variant="destructive" size="sm" @click="remove(row)">Delete</Button>
1105
+ </div>
774
1106
  </template>
775
1107
  </SimpleTable>
776
1108
  ```
777
1109
 
778
- **Slot Naming:** `#cell-{columnKey}`
1110
+ #### ⚠️ Important: Handling Group Headers
1111
+
1112
+ If you use **Group Headers**, your custom slots will technically be available for the header row too (though hidden by the colspan). To avoid errors accessing properties that don't exist on the header row, **always check `!row._isGroupHeader`**.
1113
+
1114
+ ```vue
1115
+ <template #cell-price="{ row }">
1116
+ <!-- ❌ Bad: Might crash on header row where row.price is undefined -->
1117
+ ${{ row.price.toFixed(2) }}
1118
+
1119
+ <!-- ✅ Good: Safe check -->
1120
+ <span v-if="!row._isGroupHeader">
1121
+ ${{ row.price?.toFixed(2) }}
1122
+ </span>
1123
+ </template>
1124
+ ```
779
1125
 
780
1126
  ---
781
1127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kikiloaw/simple-table",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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",
@@ -45,6 +45,7 @@ interface Props {
45
45
  queryParams?: Record<string, any> // Additional parameters to send with every request (e.g., filters, user context)
46
46
 
47
47
  // Style Props
48
+ rowHeight?: number // Table row height in pixels (default: 38)
48
49
  oddRowColor?: string // Tailwind color class, e.g. 'bg-white'
49
50
  evenRowColor?: string // Tailwind color class, e.g. 'bg-gray-50'
50
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.
@@ -60,6 +61,7 @@ const props = withDefaults(defineProps<Props>(), {
60
61
  queryParams: () => ({}),
61
62
  perPage: 10,
62
63
  pageSizes: () => [10, 20, 30, 50, 100],
64
+ rowHeight: 38,
63
65
  oddRowColor: 'bg-background',
64
66
  evenRowColor: 'bg-background',
65
67
  hoverColor: 'hover:bg-muted/50'
@@ -88,6 +90,37 @@ const normalizedPageSizes = computed(() => {
88
90
  return []
89
91
  })
90
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
+
91
124
 
92
125
 
93
126
  // <template>
@@ -722,12 +755,14 @@ function getCellStyle(col: any) {
722
755
  <!-- We add min-w-full to Table to ensure it stretches -->
723
756
  <Table class="min-w-full table-fixed">
724
757
  <TableHeader>
725
- <TableRow>
758
+ <TableRow :style="{ height: densityConfig.cellHeight }">
726
759
  <TableHead
727
760
  v-for="(col, idx) in columns"
728
761
  :key="col.key"
729
762
  :class="getCellClass(col, idx, columns.length)"
730
763
  :style="getCellStyle(col)"
764
+ :height="densityConfig.headerHeight"
765
+ :padding="densityConfig.headerPadding"
731
766
  >
732
767
  <div
733
768
  v-if="col.sortable"
@@ -757,11 +792,12 @@ function getCellStyle(col: any) {
757
792
  :key="idx"
758
793
  class="group"
759
794
  :class="getRowClass(row, idx)"
795
+ :style="{ height: densityConfig.cellHeight }"
760
796
  >
761
797
  <!-- Group Header Row: Single cell spanning all columns -->
762
798
  <template v-if="row._isGroupHeader">
763
799
  <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">
800
+ <div :class="['px-2', densityConfig.groupHeaderPadding, 'font-semibold text-gray-700 text-sm uppercase tracking-wide']">
765
801
  {{ row._groupTitle }}
766
802
  </div>
767
803
  </TableCell>
@@ -774,6 +810,8 @@ function getCellStyle(col: any) {
774
810
  :key="col.key"
775
811
  :class="getCellClass(col, cIdx, columns.length, idx)"
776
812
  :style="getCellStyle(col)"
813
+ :padding="densityConfig.cellPadding"
814
+ :height="densityConfig.cellHeight"
777
815
  >
778
816
  <!-- Auto-numbering or custom cell rendering -->
779
817
  <div>
@@ -790,7 +828,7 @@ function getCellStyle(col: any) {
790
828
  </template>
791
829
  </TableRow>
792
830
  </template>
793
- <TableRow v-else>
831
+ <TableRow v-else :style="{ height: densityConfig.cellHeight }">
794
832
  <TableCell :colspan="columns.length" class="h-24 text-center">
795
833
  No results.
796
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
  "