@makolabs/ripple 1.2.2 → 1.2.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.
@@ -2,11 +2,13 @@
2
2
  import { cn } from '../../helper/cls.js';
3
3
  import { table } from './table.js';
4
4
  import type { TableProps, SortDirection, SortState } from '../../index.js';
5
+ import Pagination from '../../elements/pagination/Pagination.svelte';
6
+ import Card from '../../layout/card/Card.svelte';
5
7
 
6
8
  let {
7
9
  data = [],
8
10
  columns = [],
9
- bordered = true,
11
+ bordered: externalBordered,
10
12
  striped = false,
11
13
  pageSize = 10,
12
14
  currentPage: externalCurrentPage,
@@ -36,9 +38,22 @@
36
38
  showPageSize = false,
37
39
  pageSizeOptions = [5, 10, 25, 50, 100],
38
40
  paginationPosition = 'bottom',
39
- paginationTemplate = 'full'
41
+ paginationTemplate = 'full',
42
+ title,
43
+ subtitle,
44
+ headerActions
40
45
  }: TableProps<any> = $props();
41
46
 
47
+ // Determine if we should use Card wrapper
48
+ const hasHeader = $derived(title !== undefined || subtitle !== undefined);
49
+
50
+ // Border logic:
51
+ // - If title/subtitle provided: force bordered=false (no borders when in Card)
52
+ // - If no title/subtitle: use externalBordered prop, defaulting to true
53
+ const bordered = $derived(
54
+ hasHeader ? false : externalBordered !== undefined ? externalBordered : true
55
+ );
56
+
42
57
  let sortColumn = $state('');
43
58
  let sortDirection = $state<SortDirection>(null);
44
59
  let internalCurrentPage = $state(externalCurrentPage || 1);
@@ -160,631 +175,499 @@
160
175
  onrowclick?.(row, index);
161
176
  }
162
177
 
163
- function goToFirstPage() {
164
- if (internalCurrentPage !== 1) {
165
- internalCurrentPage = 1;
166
- onpagechange?.(internalCurrentPage);
167
- }
168
- }
169
-
170
- function goToLastPage() {
171
- if (internalCurrentPage !== totalPages) {
172
- internalCurrentPage = totalPages;
173
- onpagechange?.(internalCurrentPage);
174
- }
175
- }
176
-
177
- function nextPage() {
178
- if (internalCurrentPage < totalPages) {
179
- internalCurrentPage++;
180
- onpagechange?.(internalCurrentPage);
181
- }
182
- }
183
-
184
- function prevPage() {
185
- if (internalCurrentPage > 1) {
186
- internalCurrentPage--;
187
- onpagechange?.(internalCurrentPage);
188
- }
178
+ function handlePageChange(page: number) {
179
+ internalCurrentPage = page;
180
+ onpagechange?.(page);
189
181
  }
190
182
 
191
- function goToPage(page: number) {
192
- if (page >= 1 && page <= totalPages && page !== internalCurrentPage) {
193
- internalCurrentPage = page;
194
- onpagechange?.(internalCurrentPage);
195
- }
196
- }
197
-
198
- function handlePageSizeChange(event: Event) {
199
- const select = event.target as HTMLSelectElement;
200
- const newPageSize = parseInt(select.value, 10);
183
+ function handlePageSizeChange(newPageSize: number) {
201
184
  internalPageSize = newPageSize;
202
185
 
203
186
  // Adjust current page if it would exceed the new total pages
204
187
  const newTotalPages = Math.ceil(effectiveTotalItems / newPageSize);
205
188
  if (internalCurrentPage > newTotalPages) {
206
189
  internalCurrentPage = newTotalPages || 1;
190
+ onpagechange?.(internalCurrentPage);
207
191
  }
208
192
 
209
193
  onpagesizechange?.(newPageSize);
210
- onpagechange?.(internalCurrentPage);
211
- }
212
-
213
- function getPageNumbers(): number[] {
214
- const pages: number[] = [];
215
- const maxPages = 5;
216
- const halfMax = Math.floor(maxPages / 2);
217
-
218
- let start = Math.max(1, internalCurrentPage - halfMax);
219
- let end = Math.min(totalPages, start + maxPages - 1);
220
-
221
- if (end - start + 1 < maxPages) {
222
- start = Math.max(1, end - maxPages + 1);
223
- }
224
-
225
- for (let i = start; i <= end; i++) {
226
- pages.push(i);
227
- }
228
-
229
- return pages;
230
194
  }
231
195
  </script>
232
196
 
233
- <div class={baseClasses}>
234
- {#if showPaginationControls && (paginationPosition === 'top' || paginationPosition === 'both')}
235
- <div class={footerClasses}>
236
- <div class={paginationClasses}>
237
- <div class="flex items-center gap-2">
238
- {#if showPageSize}
239
- <div class="flex items-center gap-2">
240
- <label for="table-page-size" class="text-default-500 text-sm">Show</label>
241
- <select
242
- id="table-page-size"
243
- class="border-default-200 rounded-md border px-2 py-1 text-sm"
244
- value={internalPageSize}
245
- onchange={handlePageSizeChange}
246
- >
247
- {#each pageSizeOptions as option}
248
- <option value={option}>{option}</option>
249
- {/each}
250
- </select>
251
- <span class="text-default-500 text-sm">entries</span>
197
+ {#if hasHeader}
198
+ <Card>
199
+ {#snippet custom()}
200
+ <!-- Header Section -->
201
+ <div class="mb-4 border-b border-gray-200 pb-3">
202
+ <div class="flex items-center justify-between">
203
+ <div>
204
+ {#if title}
205
+ <h2 class="text-lg font-semibold text-gray-900">{title}</h2>
206
+ {/if}
207
+ {#if subtitle}
208
+ <p class="mt-1 text-xs text-gray-500">{subtitle}</p>
209
+ {/if}
210
+ </div>
211
+ {#if headerActions}
212
+ <div class="flex items-center">
213
+ {@render headerActions()}
252
214
  </div>
253
215
  {/if}
254
- <span class="text-default-500 text-sm">
255
- Showing {Math.min(
256
- (internalCurrentPage - 1) * internalPageSize + 1,
257
- effectiveTotalItems
258
- )}
259
- to {Math.min(internalCurrentPage * internalPageSize, effectiveTotalItems)} of {effectiveTotalItems}
260
- entries
261
- </span>
262
- </div>
263
-
264
- <div class="flex items-center gap-1">
265
- {#if paginationTemplate === 'full'}
266
- <!-- First page button -->
267
- <button
268
- type="button"
269
- class={cn(
270
- 'relative inline-flex items-center rounded-md px-2 py-1 text-sm font-medium',
271
- internalCurrentPage === 1
272
- ? 'text-default-300 cursor-not-allowed'
273
- : 'text-default-700 hover:bg-default-100'
274
- )}
275
- onclick={goToFirstPage}
276
- disabled={internalCurrentPage === 1}
277
- aria-label="First page"
278
- >
279
- <!-- Double Chevron Left SVG -->
280
- <svg
281
- xmlns="http://www.w3.org/2000/svg"
282
- width="16"
283
- height="16"
284
- viewBox="0 0 24 24"
285
- fill="none"
286
- stroke="currentColor"
287
- stroke-width="2"
288
- stroke-linecap="round"
289
- stroke-linejoin="round"
290
- class="h-4 w-4"
291
- >
292
- <path d="m11 17-5-5 5-5"></path>
293
- <path d="m18 17-5-5 5-5"></path>
294
- </svg>
295
- </button>
296
- {/if}
297
-
298
- <!-- Previous page button -->
299
- <button
300
- type="button"
301
- class={cn(
302
- 'relative inline-flex items-center rounded-md px-2 py-1 text-sm font-medium',
303
- internalCurrentPage === 1
304
- ? 'text-default-300 cursor-not-allowed'
305
- : 'text-default-700 hover:bg-default-100'
306
- )}
307
- onclick={prevPage}
308
- disabled={internalCurrentPage === 1}
309
- aria-label="Previous page"
310
- >
311
- <!-- Chevron Left SVG -->
312
- <svg
313
- xmlns="http://www.w3.org/2000/svg"
314
- width="16"
315
- height="16"
316
- viewBox="0 0 24 24"
317
- fill="none"
318
- stroke="currentColor"
319
- stroke-width="2"
320
- stroke-linecap="round"
321
- stroke-linejoin="round"
322
- class="h-4 w-4"
323
- >
324
- <path d="m15 18-6-6 6-6"></path>
325
- </svg>
326
- </button>
327
-
328
- <!-- Page numbers -->
329
- {#if paginationTemplate === 'full'}
330
- {#each getPageNumbers() as pageNum}
331
- <button
332
- type="button"
333
- class={cn(
334
- 'relative inline-flex items-center rounded-md px-3 py-1 text-sm font-medium',
335
- internalCurrentPage === pageNum
336
- ? 'bg-primary-100 text-primary-700'
337
- : 'text-default-700 hover:bg-default-100'
338
- )}
339
- onclick={() => goToPage(pageNum)}
340
- aria-label={`Page ${pageNum}`}
341
- aria-current={internalCurrentPage === pageNum ? 'page' : undefined}
342
- >
343
- {pageNum}
344
- </button>
345
- {/each}
346
- {:else}
347
- <span class="text-default-500 px-2 text-sm">
348
- Page {internalCurrentPage} of {totalPages}
349
- </span>
350
- {/if}
351
-
352
- <!-- Next page button -->
353
- <button
354
- type="button"
355
- class={cn(
356
- 'relative inline-flex items-center rounded-md px-2 py-1 text-sm font-medium',
357
- internalCurrentPage === totalPages
358
- ? 'text-default-300 cursor-not-allowed'
359
- : 'text-default-700 hover:bg-default-100'
360
- )}
361
- onclick={nextPage}
362
- disabled={internalCurrentPage === totalPages}
363
- aria-label="Next page"
364
- >
365
- <!-- Chevron Right SVG -->
366
- <svg
367
- xmlns="http://www.w3.org/2000/svg"
368
- width="16"
369
- height="16"
370
- viewBox="0 0 24 24"
371
- fill="none"
372
- stroke="currentColor"
373
- stroke-width="2"
374
- stroke-linecap="round"
375
- stroke-linejoin="round"
376
- class="h-4 w-4"
377
- >
378
- <path d="m9 18 6-6-6-6"></path>
379
- </svg>
380
- </button>
381
-
382
- {#if paginationTemplate === 'full'}
383
- <!-- Last page button -->
384
- <button
385
- type="button"
386
- class={cn(
387
- 'relative inline-flex items-center rounded-md px-2 py-1 text-sm font-medium',
388
- internalCurrentPage === totalPages
389
- ? 'text-default-300 cursor-not-allowed'
390
- : 'text-default-700 hover:bg-default-100'
391
- )}
392
- onclick={goToLastPage}
393
- disabled={internalCurrentPage === totalPages}
394
- aria-label="Last page"
395
- >
396
- <!-- Double Chevron Right SVG -->
397
- <svg
398
- xmlns="http://www.w3.org/2000/svg"
399
- width="16"
400
- height="16"
401
- viewBox="0 0 24 24"
402
- fill="none"
403
- stroke="currentColor"
404
- stroke-width="2"
405
- stroke-linecap="round"
406
- stroke-linejoin="round"
407
- class="h-4 w-4"
408
- >
409
- <path d="m13 17 5-5-5-5"></path>
410
- <path d="m6 17 5-5-5-5"></path>
411
- </svg>
412
- </button>
413
- {/if}
414
216
  </div>
415
217
  </div>
416
- </div>
417
- {/if}
418
-
419
- <div class={wrapperClasses}>
420
- <table class={tableClasses}>
421
- <thead class={theadClasses}>
422
- <tr>
423
- {#if selectable}
424
- <th class={cn(thClasses, 'text-center')}>
425
- <input
426
- type="checkbox"
427
- onchange={() => {
428
- if (selected.length === data.length) {
429
- selected = [];
430
- } else {
431
- selected = [...data];
432
- }
433
- onselect(selected);
434
- }}
435
- checked={selected.length === data.length && data.length > 0}
436
- aria-label="Select all rows"
437
- />
438
- </th>
439
- {/if}
440
218
 
441
- {#each columns as column (column.key)}
442
- <th
443
- class={cn(
444
- thClasses,
445
- column.align === 'center' && 'text-center',
446
- column.align === 'right' && 'text-right',
447
- column.class
448
- )}
449
- style={column.width ? `width: ${column.width}` : undefined}
450
- >
451
- {#if column.sortable}
452
- <button
453
- type="button"
454
- class={sortButtonBaseClass()}
455
- onclick={() => toggleSort(column.sortKey || column.key)}
456
- aria-label={`Sort by ${column.header}`}
457
- >
458
- {column.header}
459
- <span class={sortIconBaseClass()}>
460
- {#if sortColumn === (column.sortKey || column.key)}
461
- {#if sortDirection === 'asc'}
462
- <svg
463
- xmlns="http://www.w3.org/2000/svg"
464
- viewBox="0 0 20 20"
465
- fill="currentColor"
466
- class="h-4 w-4"
467
- >
468
- <path
469
- d="M10 15a.75.75 0 01-.75-.75V7.612L6.058 10.8a.75.75 0 01-1.061-1.061l3.75-3.75a.75.75 0 011.06 0l3.75 3.75a.75.75 0 11-1.06 1.061L10.75 7.612v6.638A.75.75 0 0110 15z"
470
- />
471
- </svg>
472
- {:else if sortDirection === 'desc'}
473
- <svg
474
- xmlns="http://www.w3.org/2000/svg"
475
- viewBox="0 0 20 20"
476
- fill="currentColor"
477
- class="h-4 w-4"
478
- >
479
- <path
480
- d="M10 5a.75.75 0 01.75.75v6.638l3.192-3.187a.75.75 0 111.06 1.061l-3.75 3.75a.75.75 0 01-1.06 0l-3.75-3.75a.75.75 0 111.06-1.061L9.25 12.389V5.75A.75.75 0 0110 5z"
481
- />
482
- </svg>
483
- {/if}
219
+ <!-- Table Content -->
220
+ <div class={baseClasses}>
221
+ {#if showPaginationControls && (paginationPosition === 'top' || paginationPosition === 'both')}
222
+ <div class={footerClasses}>
223
+ <Pagination
224
+ currentPage={internalCurrentPage}
225
+ totalItems={effectiveTotalItems}
226
+ pageSize={internalPageSize}
227
+ onPageChange={handlePageChange}
228
+ onPageSizeChange={handlePageSizeChange}
229
+ {showPageSize}
230
+ {pageSizeOptions}
231
+ template={paginationTemplate === 'full' ? 'full' : 'compact'}
232
+ disabled={loading}
233
+ class={cn(paginationClasses, paginationClass)}
234
+ />
235
+ </div>
236
+ {/if}
237
+
238
+ <div class={wrapperClasses}>
239
+ <table class={tableClasses}>
240
+ <thead class={theadClasses}>
241
+ <tr>
242
+ {#if selectable}
243
+ <th class={cn(thClasses, 'text-center')}>
244
+ <input
245
+ type="checkbox"
246
+ onchange={() => {
247
+ if (selected.length === data.length) {
248
+ selected = [];
249
+ } else {
250
+ selected = [...data];
251
+ }
252
+ onselect(selected);
253
+ }}
254
+ checked={selected.length === data.length && data.length > 0}
255
+ aria-label="Select all rows"
256
+ />
257
+ </th>
258
+ {/if}
259
+
260
+ {#each columns as column (column.key)}
261
+ <th
262
+ class={cn(
263
+ thClasses,
264
+ column.align === 'center' && 'text-center',
265
+ column.align === 'right' && 'text-right',
266
+ column.class
267
+ )}
268
+ style={column.width ? `width: ${column.width}` : undefined}
269
+ >
270
+ {#if column.sortable}
271
+ <button
272
+ type="button"
273
+ class={sortButtonBaseClass()}
274
+ onclick={() => toggleSort(column.sortKey || column.key)}
275
+ aria-label={`Sort by ${column.header}`}
276
+ >
277
+ {column.header}
278
+ <span class={sortIconBaseClass()}>
279
+ {#if sortColumn === (column.sortKey || column.key)}
280
+ {#if sortDirection === 'asc'}
281
+ <svg
282
+ xmlns="http://www.w3.org/2000/svg"
283
+ viewBox="0 0 20 20"
284
+ fill="currentColor"
285
+ class="h-4 w-4"
286
+ >
287
+ <path
288
+ d="M10 15a.75.75 0 01-.75-.75V7.612L6.058 10.8a.75.75 0 01-1.061-1.061l3.75-3.75a.75.75 0 011.06 0l3.75 3.75a.75.75 0 11-1.06 1.061L10.75 7.612v6.638A.75.75 0 0110 15z"
289
+ />
290
+ </svg>
291
+ {:else if sortDirection === 'desc'}
292
+ <svg
293
+ xmlns="http://www.w3.org/2000/svg"
294
+ viewBox="0 0 20 20"
295
+ fill="currentColor"
296
+ class="h-4 w-4"
297
+ >
298
+ <path
299
+ d="M10 5a.75.75 0 01.75.75v6.638l3.192-3.187a.75.75 0 111.06 1.061l-3.75 3.75a.75.75 0 01-1.06 0l-3.75-3.75a.75.75 0 111.06-1.061L9.25 12.389V5.75A.75.75 0 0110 5z"
300
+ />
301
+ </svg>
302
+ {/if}
303
+ {:else}
304
+ <svg
305
+ xmlns="http://www.w3.org/2000/svg"
306
+ fill="none"
307
+ viewBox="0 0 24 24"
308
+ stroke-width="1.5"
309
+ stroke="currentColor"
310
+ class="h-4 w-4 opacity-40"
311
+ >
312
+ <path
313
+ stroke-linecap="round"
314
+ stroke-linejoin="round"
315
+ d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"
316
+ />
317
+ </svg>
318
+ {/if}
319
+ </span>
320
+ </button>
484
321
  {:else}
322
+ {column.header}
323
+ {/if}
324
+ </th>
325
+ {/each}
326
+ </tr>
327
+ </thead>
328
+
329
+ <tbody class={tbodyClasses}>
330
+ {#if loading}
331
+ <tr>
332
+ <td
333
+ colspan={selectable ? columns.length + 1 : columns.length}
334
+ class={cn(tdClasses, 'py-8 text-center')}
335
+ >
336
+ <div class="flex justify-center">
485
337
  <svg
338
+ class="text-default-500 h-6 w-6 animate-spin"
486
339
  xmlns="http://www.w3.org/2000/svg"
487
340
  fill="none"
488
341
  viewBox="0 0 24 24"
489
- stroke-width="1.5"
490
- stroke="currentColor"
491
- class="h-4 w-4 opacity-40"
492
342
  >
343
+ <circle
344
+ class="opacity-25"
345
+ cx="12"
346
+ cy="12"
347
+ r="10"
348
+ stroke="currentColor"
349
+ stroke-width="4"
350
+ ></circle>
493
351
  <path
494
- stroke-linecap="round"
495
- stroke-linejoin="round"
496
- d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"
497
- />
352
+ class="opacity-75"
353
+ fill="currentColor"
354
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
355
+ ></path>
498
356
  </svg>
499
- {/if}
500
- </span>
501
- </button>
357
+ </div>
358
+ </td>
359
+ </tr>
360
+ {:else if getPaginatedData().length === 0}
361
+ <tr>
362
+ <td
363
+ colspan={selectable ? columns.length + 1 : columns.length}
364
+ class={emptyStateClasses}
365
+ >
366
+ No data available
367
+ </td>
368
+ </tr>
502
369
  {:else}
503
- {column.header}
504
- {/if}
505
- </th>
506
- {/each}
507
- </tr>
508
- </thead>
509
-
510
- <tbody class={tbodyClasses}>
511
- {#if loading}
512
- <tr>
513
- <td
514
- colspan={selectable ? columns.length + 1 : columns.length}
515
- class={cn(tdClasses, 'py-8 text-center')}
516
- >
517
- <div class="flex justify-center">
518
- <svg
519
- class="text-default-500 h-6 w-6 animate-spin"
520
- xmlns="http://www.w3.org/2000/svg"
521
- fill="none"
522
- viewBox="0 0 24 24"
523
- >
524
- <circle
525
- class="opacity-25"
526
- cx="12"
527
- cy="12"
528
- r="10"
529
- stroke="currentColor"
530
- stroke-width="4"
531
- ></circle>
532
- <path
533
- class="opacity-75"
534
- fill="currentColor"
535
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
536
- ></path>
537
- </svg>
538
- </div>
539
- </td>
540
- </tr>
541
- {:else if getPaginatedData().length === 0}
542
- <tr>
543
- <td
544
- colspan={selectable ? columns.length + 1 : columns.length}
545
- class={emptyStateClasses}
546
- >
547
- No data available
548
- </td>
549
- </tr>
550
- {:else}
551
- {#each getPaginatedData() as row, rowIndex}
552
- <tr
553
- class={cn(trClasses, rowclass(row, rowIndex), {
554
- 'bg-primary-100': selectable && isRowSelected(row),
555
- 'cursor-pointer': onrowclick
556
- })}
557
- onclick={() => handleRowClick(row, rowIndex)}
558
- aria-selected={selectable && isRowSelected(row)}
559
- >
560
- {#if selectable}
561
- <td class={cn(tdClasses, 'text-center')}>
562
- <input
563
- type="checkbox"
564
- checked={isRowSelected(row)}
565
- onclick={(e) => {
566
- e.stopPropagation(); // Prevent row click
567
- toggleRowSelection(row);
568
- }}
569
- aria-label={`Select row ${rowIndex + 1}`}
570
- />
571
- </td>
572
- {/if}
370
+ {#each getPaginatedData() as row, rowIndex}
371
+ <tr
372
+ class={cn(trClasses, rowclass(row, rowIndex), {
373
+ 'bg-primary-100': selectable && isRowSelected(row),
374
+ 'cursor-pointer': onrowclick
375
+ })}
376
+ onclick={() => handleRowClick(row, rowIndex)}
377
+ aria-selected={selectable && isRowSelected(row)}
378
+ >
379
+ {#if selectable}
380
+ <td class={cn(tdClasses, 'text-center')}>
381
+ <input
382
+ type="checkbox"
383
+ checked={isRowSelected(row)}
384
+ onclick={(e) => {
385
+ e.stopPropagation(); // Prevent row click
386
+ toggleRowSelection(row);
387
+ }}
388
+ aria-label={`Select row ${rowIndex + 1}`}
389
+ />
390
+ </td>
391
+ {/if}
573
392
 
574
- {#each columns as column (column.key)}
575
- <td
576
- class={cn(
577
- tdClasses,
578
- column.align === 'center' && 'text-center',
579
- column.align === 'right' && 'text-right',
580
- column.class
581
- )}
582
- >
583
- {#if column.cell}
584
- {@render column.cell(row, column.key, rowIndex)}
585
- {:else if row[column.key] === undefined || row[column.key] === null}
586
- <span class="text-default-300">—</span>
587
- {:else}
588
- {row[column.key]}
393
+ {#each columns as column (column.key)}
394
+ <td
395
+ class={cn(
396
+ tdClasses,
397
+ column.align === 'center' && 'text-center',
398
+ column.align === 'right' && 'text-right',
399
+ column.class
400
+ )}
401
+ >
402
+ {#if column.cell}
403
+ {@render column.cell(row, column.key, rowIndex)}
404
+ {:else if row[column.key] === undefined || row[column.key] === null}
405
+ <span class="text-default-300">—</span>
406
+ {:else}
407
+ {row[column.key]}
408
+ {/if}
409
+ </td>
410
+ {/each}
411
+ </tr>
412
+ {#if expandedContent}
413
+ <tr class="expandedContent-row">
414
+ <td
415
+ colspan={selectable ? columns.length + 1 : columns.length}
416
+ class="border-0 p-0"
417
+ >
418
+ {@render expandedContent(row)}
419
+ </td>
420
+ </tr>
589
421
  {/if}
590
- </td>
591
- {/each}
592
- </tr>
593
- {#if expandedContent}
594
- <tr class="expandedContent-row">
595
- <td colspan={selectable ? columns.length + 1 : columns.length} class="border-0 p-0">
596
- {@render expandedContent(row)}
597
- </td>
598
- </tr>
599
- {/if}
600
- {/each}
601
- {/if}
602
- </tbody>
603
- </table>
604
- </div>
605
-
606
- {#if showPaginationControls && (paginationPosition === 'bottom' || paginationPosition === 'both')}
607
- <div class={footerClasses}>
608
- <div class={paginationClasses}>
609
- <div class="flex items-center gap-2">
610
- {#if showPageSize}
611
- <div class="flex items-center gap-2">
612
- <label for="table-page-size-bottom" class="text-default-500 text-sm">Show</label>
613
- <select
614
- id="table-page-size-bottom"
615
- class="border-default-200 rounded-md border px-2 py-1 text-sm"
616
- value={internalPageSize}
617
- onchange={handlePageSizeChange}
618
- >
619
- {#each pageSizeOptions as option}
620
- <option value={option}>{option}</option>
621
422
  {/each}
622
- </select>
623
- <span class="text-default-500 text-sm">entries</span>
624
- </div>
625
- {/if}
626
- <span class="text-default-500 text-sm">
627
- Showing {Math.min(
628
- (internalCurrentPage - 1) * internalPageSize + 1,
629
- effectiveTotalItems
630
- )}
631
- to {Math.min(internalCurrentPage * internalPageSize, effectiveTotalItems)} of {effectiveTotalItems}
632
- entries
633
- </span>
423
+ {/if}
424
+ </tbody>
425
+ </table>
634
426
  </div>
635
427
 
636
- <div class="flex items-center gap-1">
637
- {#if paginationTemplate === 'full'}
638
- <!-- First page button -->
639
- <button
640
- type="button"
641
- class={cn(
642
- 'relative inline-flex items-center rounded-md px-2 py-1 text-sm font-medium',
643
- internalCurrentPage === 1
644
- ? 'text-default-300 cursor-not-allowed'
645
- : 'text-default-700 hover:bg-default-100'
646
- )}
647
- onclick={goToFirstPage}
648
- disabled={internalCurrentPage === 1}
649
- aria-label="First page"
650
- >
651
- <!-- Double Chevron Left SVG -->
652
- <svg
653
- xmlns="http://www.w3.org/2000/svg"
654
- width="16"
655
- height="16"
656
- viewBox="0 0 24 24"
657
- fill="none"
658
- stroke="currentColor"
659
- stroke-width="2"
660
- stroke-linecap="round"
661
- stroke-linejoin="round"
662
- class="h-4 w-4"
663
- >
664
- <path d="m11 17-5-5 5-5"></path>
665
- <path d="m18 17-5-5 5-5"></path>
666
- </svg>
667
- </button>
668
- {/if}
428
+ {#if showPaginationControls && (paginationPosition === 'bottom' || paginationPosition === 'both')}
429
+ <div class={footerClasses}>
430
+ <Pagination
431
+ currentPage={internalCurrentPage}
432
+ totalItems={effectiveTotalItems}
433
+ pageSize={internalPageSize}
434
+ onPageChange={handlePageChange}
435
+ onPageSizeChange={handlePageSizeChange}
436
+ {showPageSize}
437
+ {pageSizeOptions}
438
+ template={paginationTemplate === 'full' ? 'full' : 'compact'}
439
+ disabled={loading}
440
+ class={cn(paginationClasses, paginationClass)}
441
+ />
442
+ </div>
443
+ {/if}
444
+ </div>
445
+ {/snippet}
446
+ </Card>
447
+ {:else}
448
+ <div class={baseClasses}>
449
+ {#if showPaginationControls && (paginationPosition === 'top' || paginationPosition === 'both')}
450
+ <div class={footerClasses}>
451
+ <Pagination
452
+ currentPage={internalCurrentPage}
453
+ totalItems={effectiveTotalItems}
454
+ pageSize={internalPageSize}
455
+ onPageChange={handlePageChange}
456
+ onPageSizeChange={handlePageSizeChange}
457
+ {showPageSize}
458
+ {pageSizeOptions}
459
+ template={paginationTemplate === 'full' ? 'full' : 'compact'}
460
+ disabled={loading}
461
+ class={cn(paginationClasses, paginationClass)}
462
+ />
463
+ </div>
464
+ {/if}
465
+
466
+ <div class={wrapperClasses}>
467
+ <table class={tableClasses}>
468
+ <thead class={theadClasses}>
469
+ <tr>
470
+ {#if selectable}
471
+ <th class={cn(thClasses, 'text-center')}>
472
+ <input
473
+ type="checkbox"
474
+ onchange={() => {
475
+ if (selected.length === data.length) {
476
+ selected = [];
477
+ } else {
478
+ selected = [...data];
479
+ }
480
+ onselect(selected);
481
+ }}
482
+ checked={selected.length === data.length && data.length > 0}
483
+ aria-label="Select all rows"
484
+ />
485
+ </th>
486
+ {/if}
669
487
 
670
- <!-- Previous page button -->
671
- <button
672
- type="button"
673
- class={cn(
674
- 'relative inline-flex items-center rounded-md px-2 py-1 text-sm font-medium',
675
- internalCurrentPage === 1
676
- ? 'text-default-300 cursor-not-allowed'
677
- : 'text-default-700 hover:bg-default-100'
678
- )}
679
- onclick={prevPage}
680
- disabled={internalCurrentPage === 1}
681
- aria-label="Previous page"
682
- >
683
- <!-- Chevron Left SVG -->
684
- <svg
685
- xmlns="http://www.w3.org/2000/svg"
686
- width="16"
687
- height="16"
688
- viewBox="0 0 24 24"
689
- fill="none"
690
- stroke="currentColor"
691
- stroke-width="2"
692
- stroke-linecap="round"
693
- stroke-linejoin="round"
694
- class="h-4 w-4"
695
- >
696
- <path d="m15 18-6-6 6-6"></path>
697
- </svg>
698
- </button>
699
-
700
- <!-- Page numbers -->
701
- {#if paginationTemplate === 'full'}
702
- {#each getPageNumbers() as pageNum}
703
- <button
704
- type="button"
488
+ {#each columns as column (column.key)}
489
+ <th
705
490
  class={cn(
706
- 'relative inline-flex items-center rounded-md px-3 py-1 text-sm font-medium',
707
- internalCurrentPage === pageNum
708
- ? 'bg-primary-100 text-primary-700'
709
- : 'text-default-700 hover:bg-default-100'
491
+ thClasses,
492
+ column.align === 'center' && 'text-center',
493
+ column.align === 'right' && 'text-right',
494
+ column.class
710
495
  )}
711
- onclick={() => goToPage(pageNum)}
712
- aria-label={`Page ${pageNum}`}
713
- aria-current={internalCurrentPage === pageNum ? 'page' : undefined}
496
+ style={column.width ? `width: ${column.width}` : undefined}
714
497
  >
715
- {pageNum}
716
- </button>
498
+ {#if column.sortable}
499
+ <button
500
+ type="button"
501
+ class={sortButtonBaseClass()}
502
+ onclick={() => toggleSort(column.sortKey || column.key)}
503
+ aria-label={`Sort by ${column.header}`}
504
+ >
505
+ {column.header}
506
+ <span class={sortIconBaseClass()}>
507
+ {#if sortColumn === (column.sortKey || column.key)}
508
+ {#if sortDirection === 'asc'}
509
+ <svg
510
+ xmlns="http://www.w3.org/2000/svg"
511
+ viewBox="0 0 20 20"
512
+ fill="currentColor"
513
+ class="h-4 w-4"
514
+ >
515
+ <path
516
+ d="M10 15a.75.75 0 01-.75-.75V7.612L6.058 10.8a.75.75 0 01-1.061-1.061l3.75-3.75a.75.75 0 011.06 0l3.75 3.75a.75.75 0 11-1.06 1.061L10.75 7.612v6.638A.75.75 0 0110 15z"
517
+ />
518
+ </svg>
519
+ {:else if sortDirection === 'desc'}
520
+ <svg
521
+ xmlns="http://www.w3.org/2000/svg"
522
+ viewBox="0 0 20 20"
523
+ fill="currentColor"
524
+ class="h-4 w-4"
525
+ >
526
+ <path
527
+ d="M10 5a.75.75 0 01.75.75v6.638l3.192-3.187a.75.75 0 111.06 1.061l-3.75 3.75a.75.75 0 01-1.06 0l-3.75-3.75a.75.75 0 111.06-1.061L9.25 12.389V5.75A.75.75 0 0110 5z"
528
+ />
529
+ </svg>
530
+ {/if}
531
+ {:else}
532
+ <svg
533
+ xmlns="http://www.w3.org/2000/svg"
534
+ fill="none"
535
+ viewBox="0 0 24 24"
536
+ stroke-width="1.5"
537
+ stroke="currentColor"
538
+ class="h-4 w-4 opacity-40"
539
+ >
540
+ <path
541
+ stroke-linecap="round"
542
+ stroke-linejoin="round"
543
+ d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"
544
+ />
545
+ </svg>
546
+ {/if}
547
+ </span>
548
+ </button>
549
+ {:else}
550
+ {column.header}
551
+ {/if}
552
+ </th>
717
553
  {/each}
554
+ </tr>
555
+ </thead>
556
+
557
+ <tbody class={tbodyClasses}>
558
+ {#if loading}
559
+ <tr>
560
+ <td
561
+ colspan={selectable ? columns.length + 1 : columns.length}
562
+ class={cn(tdClasses, 'py-8 text-center')}
563
+ >
564
+ <div class="flex justify-center">
565
+ <svg
566
+ class="text-default-500 h-6 w-6 animate-spin"
567
+ xmlns="http://www.w3.org/2000/svg"
568
+ fill="none"
569
+ viewBox="0 0 24 24"
570
+ >
571
+ <circle
572
+ class="opacity-25"
573
+ cx="12"
574
+ cy="12"
575
+ r="10"
576
+ stroke="currentColor"
577
+ stroke-width="4"
578
+ ></circle>
579
+ <path
580
+ class="opacity-75"
581
+ fill="currentColor"
582
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
583
+ ></path>
584
+ </svg>
585
+ </div>
586
+ </td>
587
+ </tr>
588
+ {:else if getPaginatedData().length === 0}
589
+ <tr>
590
+ <td
591
+ colspan={selectable ? columns.length + 1 : columns.length}
592
+ class={emptyStateClasses}
593
+ >
594
+ No data available
595
+ </td>
596
+ </tr>
718
597
  {:else}
719
- <span class="text-default-500 px-2 text-sm">
720
- Page {internalCurrentPage} of {totalPages}
721
- </span>
722
- {/if}
723
-
724
- <!-- Next page button -->
725
- <button
726
- type="button"
727
- class={cn(
728
- 'relative inline-flex items-center rounded-md px-2 py-1 text-sm font-medium',
729
- internalCurrentPage === totalPages
730
- ? 'text-default-300 cursor-not-allowed'
731
- : 'text-default-700 hover:bg-default-100'
732
- )}
733
- onclick={nextPage}
734
- disabled={internalCurrentPage === totalPages}
735
- aria-label="Next page"
736
- >
737
- <!-- Chevron Right SVG -->
738
- <svg
739
- xmlns="http://www.w3.org/2000/svg"
740
- width="16"
741
- height="16"
742
- viewBox="0 0 24 24"
743
- fill="none"
744
- stroke="currentColor"
745
- stroke-width="2"
746
- stroke-linecap="round"
747
- stroke-linejoin="round"
748
- class="h-4 w-4"
749
- >
750
- <path d="m9 18 6-6-6-6"></path>
751
- </svg>
752
- </button>
753
-
754
- {#if paginationTemplate === 'full'}
755
- <!-- Last page button -->
756
- <button
757
- type="button"
758
- class={cn(
759
- 'relative inline-flex items-center rounded-md px-2 py-1 text-sm font-medium',
760
- internalCurrentPage === totalPages
761
- ? 'text-default-300 cursor-not-allowed'
762
- : 'text-default-700 hover:bg-default-100'
763
- )}
764
- onclick={goToLastPage}
765
- disabled={internalCurrentPage === totalPages}
766
- aria-label="Last page"
767
- >
768
- <!-- Double Chevron Right SVG -->
769
- <svg
770
- xmlns="http://www.w3.org/2000/svg"
771
- width="16"
772
- height="16"
773
- viewBox="0 0 24 24"
774
- fill="none"
775
- stroke="currentColor"
776
- stroke-width="2"
777
- stroke-linecap="round"
778
- stroke-linejoin="round"
779
- class="h-4 w-4"
598
+ {#each getPaginatedData() as row, rowIndex}
599
+ <tr
600
+ class={cn(trClasses, rowclass(row, rowIndex), {
601
+ 'bg-primary-100': selectable && isRowSelected(row),
602
+ 'cursor-pointer': onrowclick
603
+ })}
604
+ onclick={() => handleRowClick(row, rowIndex)}
605
+ aria-selected={selectable && isRowSelected(row)}
780
606
  >
781
- <path d="m13 17 5-5-5-5"></path>
782
- <path d="m6 17 5-5-5-5"></path>
783
- </svg>
784
- </button>
607
+ {#if selectable}
608
+ <td class={cn(tdClasses, 'text-center')}>
609
+ <input
610
+ type="checkbox"
611
+ checked={isRowSelected(row)}
612
+ onclick={(e) => {
613
+ e.stopPropagation(); // Prevent row click
614
+ toggleRowSelection(row);
615
+ }}
616
+ aria-label={`Select row ${rowIndex + 1}`}
617
+ />
618
+ </td>
619
+ {/if}
620
+
621
+ {#each columns as column (column.key)}
622
+ <td
623
+ class={cn(
624
+ tdClasses,
625
+ column.align === 'center' && 'text-center',
626
+ column.align === 'right' && 'text-right',
627
+ column.class
628
+ )}
629
+ >
630
+ {#if column.cell}
631
+ {@render column.cell(row, column.key, rowIndex)}
632
+ {:else if row[column.key] === undefined || row[column.key] === null}
633
+ <span class="text-default-300">—</span>
634
+ {:else}
635
+ {row[column.key]}
636
+ {/if}
637
+ </td>
638
+ {/each}
639
+ </tr>
640
+ {#if expandedContent}
641
+ <tr class="expandedContent-row">
642
+ <td
643
+ colspan={selectable ? columns.length + 1 : columns.length}
644
+ class="border-0 p-0"
645
+ >
646
+ {@render expandedContent(row)}
647
+ </td>
648
+ </tr>
649
+ {/if}
650
+ {/each}
785
651
  {/if}
786
- </div>
787
- </div>
652
+ </tbody>
653
+ </table>
788
654
  </div>
789
- {/if}
790
- </div>
655
+
656
+ {#if showPaginationControls && (paginationPosition === 'bottom' || paginationPosition === 'both')}
657
+ <div class={footerClasses}>
658
+ <Pagination
659
+ currentPage={internalCurrentPage}
660
+ totalItems={effectiveTotalItems}
661
+ pageSize={internalPageSize}
662
+ onPageChange={handlePageChange}
663
+ onPageSizeChange={handlePageSizeChange}
664
+ {showPageSize}
665
+ {pageSizeOptions}
666
+ template={paginationTemplate === 'full' ? 'full' : 'compact'}
667
+ disabled={loading}
668
+ class={cn(paginationClasses, paginationClass)}
669
+ />
670
+ </div>
671
+ {/if}
672
+ </div>
673
+ {/if}