@makolabs/ripple 0.0.1 → 0.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.
Files changed (144) hide show
  1. package/README.md +575 -8
  2. package/dist/adapters/storage/BaseAdapter.d.ts +20 -0
  3. package/dist/adapters/storage/BaseAdapter.js +171 -0
  4. package/dist/adapters/storage/S3Adapter.d.ts +21 -0
  5. package/dist/adapters/storage/S3Adapter.js +194 -0
  6. package/dist/adapters/storage/index.d.ts +3 -0
  7. package/dist/adapters/storage/index.js +3 -0
  8. package/dist/adapters/storage/types.d.ts +102 -0
  9. package/dist/adapters/storage/types.js +4 -0
  10. package/dist/button/Button.svelte +48 -0
  11. package/dist/button/Button.svelte.d.ts +4 -0
  12. package/dist/button/button.d.ts +113 -0
  13. package/dist/button/button.js +168 -0
  14. package/dist/charts/Chart.svelte +545 -0
  15. package/dist/charts/Chart.svelte.d.ts +4 -0
  16. package/dist/drawer/Drawer.svelte +224 -0
  17. package/dist/drawer/Drawer.svelte.d.ts +4 -0
  18. package/dist/drawer/drawer.d.ts +160 -0
  19. package/dist/drawer/drawer.js +80 -0
  20. package/dist/elements/accordion/Accordion.svelte +98 -0
  21. package/dist/elements/accordion/Accordion.svelte.d.ts +4 -0
  22. package/dist/elements/accordion/accordion.d.ts +227 -0
  23. package/dist/elements/accordion/accordion.js +138 -0
  24. package/dist/elements/alert/Alert.svelte +57 -0
  25. package/dist/elements/alert/Alert.svelte.d.ts +4 -0
  26. package/dist/elements/badge/Badge.svelte +43 -0
  27. package/dist/elements/badge/Badge.svelte.d.ts +4 -0
  28. package/dist/elements/badge/badge.d.ts +181 -0
  29. package/dist/elements/badge/badge.js +65 -0
  30. package/dist/elements/dropdown/Dropdown.svelte +234 -0
  31. package/dist/elements/dropdown/Dropdown.svelte.d.ts +4 -0
  32. package/dist/elements/dropdown/Select.svelte +333 -0
  33. package/dist/elements/dropdown/Select.svelte.d.ts +4 -0
  34. package/dist/elements/dropdown/dropdown.d.ts +251 -0
  35. package/dist/elements/dropdown/dropdown.js +95 -0
  36. package/dist/elements/dropdown/select.d.ts +200 -0
  37. package/dist/elements/dropdown/select.js +82 -0
  38. package/dist/elements/file-upload/FileUpload.svelte +135 -0
  39. package/dist/elements/file-upload/FileUpload.svelte.d.ts +4 -0
  40. package/dist/elements/file-upload/FilesPreview.svelte +93 -0
  41. package/dist/elements/file-upload/FilesPreview.svelte.d.ts +4 -0
  42. package/dist/elements/progress/Progress.svelte +145 -0
  43. package/dist/elements/progress/Progress.svelte.d.ts +4 -0
  44. package/dist/elements/timeline/Timeline.svelte +92 -0
  45. package/dist/elements/timeline/Timeline.svelte.d.ts +7 -0
  46. package/dist/file-browser/FileBrowser.svelte +877 -0
  47. package/dist/file-browser/FileBrowser.svelte.d.ts +14 -0
  48. package/dist/file-browser/index.d.ts +1 -0
  49. package/dist/file-browser/index.js +1 -0
  50. package/dist/filters/CompactFilters.svelte +147 -0
  51. package/dist/filters/CompactFilters.svelte.d.ts +4 -0
  52. package/dist/filters/index.d.ts +1 -0
  53. package/dist/filters/index.js +1 -0
  54. package/dist/forms/Checkbox.svelte +54 -0
  55. package/dist/forms/Checkbox.svelte.d.ts +4 -0
  56. package/dist/forms/DateRange.svelte +493 -0
  57. package/dist/forms/DateRange.svelte.d.ts +4 -0
  58. package/dist/forms/Form.svelte +39 -0
  59. package/dist/forms/Form.svelte.d.ts +4 -0
  60. package/dist/forms/Input.svelte +86 -0
  61. package/dist/forms/Input.svelte.d.ts +4 -0
  62. package/dist/forms/NumberInput.svelte +159 -0
  63. package/dist/forms/NumberInput.svelte.d.ts +4 -0
  64. package/dist/forms/RadioInputs.svelte +64 -0
  65. package/dist/forms/RadioInputs.svelte.d.ts +4 -0
  66. package/dist/forms/RadioPill.svelte +66 -0
  67. package/dist/forms/RadioPill.svelte.d.ts +4 -0
  68. package/dist/forms/Slider.svelte +342 -0
  69. package/dist/forms/Slider.svelte.d.ts +4 -0
  70. package/dist/forms/Tags.svelte +181 -0
  71. package/dist/forms/Tags.svelte.d.ts +4 -0
  72. package/dist/forms/Toggle.svelte +132 -0
  73. package/dist/forms/Toggle.svelte.d.ts +4 -0
  74. package/dist/forms/slider.d.ts +143 -0
  75. package/dist/forms/slider.js +62 -0
  76. package/dist/header/Breadcrumbs.svelte +73 -0
  77. package/dist/header/Breadcrumbs.svelte.d.ts +4 -0
  78. package/dist/header/PageHeader.svelte +68 -0
  79. package/dist/header/PageHeader.svelte.d.ts +4 -0
  80. package/dist/header/breadcrumbs.d.ts +226 -0
  81. package/dist/header/breadcrumbs.js +87 -0
  82. package/dist/helper/cls.d.ts +1 -0
  83. package/dist/helper/cls.js +4 -0
  84. package/dist/helper/date.d.ts +7 -0
  85. package/dist/helper/date.js +15 -0
  86. package/dist/helper/nav.svelte.d.ts +6 -0
  87. package/dist/helper/nav.svelte.js +23 -0
  88. package/dist/index.d.ts +856 -1
  89. package/dist/index.js +78 -1
  90. package/dist/layout/card/Card.svelte +41 -0
  91. package/dist/layout/card/Card.svelte.d.ts +4 -0
  92. package/dist/layout/card/MetricCard.svelte +64 -0
  93. package/dist/layout/card/MetricCard.svelte.d.ts +4 -0
  94. package/dist/layout/card/StatsCard.svelte +266 -0
  95. package/dist/layout/card/StatsCard.svelte.d.ts +4 -0
  96. package/dist/layout/card/card.d.ts +128 -0
  97. package/dist/layout/card/card.js +51 -0
  98. package/dist/layout/card/metric-card.d.ts +49 -0
  99. package/dist/layout/card/metric-card.js +10 -0
  100. package/dist/layout/card/stats-card.d.ts +191 -0
  101. package/dist/layout/card/stats-card.js +73 -0
  102. package/dist/layout/navbar/Navbar.svelte +206 -0
  103. package/dist/layout/navbar/Navbar.svelte.d.ts +4 -0
  104. package/dist/layout/navbar/navbar.d.ts +205 -0
  105. package/dist/layout/navbar/navbar.js +98 -0
  106. package/dist/layout/sidebar/NavGroup.svelte +91 -0
  107. package/dist/layout/sidebar/NavGroup.svelte.d.ts +4 -0
  108. package/dist/layout/sidebar/NavItem.svelte +29 -0
  109. package/dist/layout/sidebar/NavItem.svelte.d.ts +4 -0
  110. package/dist/layout/sidebar/Sidebar.svelte +193 -0
  111. package/dist/layout/sidebar/Sidebar.svelte.d.ts +4 -0
  112. package/dist/layout/table/Cells.svelte +111 -0
  113. package/dist/layout/table/Cells.svelte.d.ts +27 -0
  114. package/dist/layout/table/Table.svelte +790 -0
  115. package/dist/layout/table/Table.svelte.d.ts +4 -0
  116. package/dist/layout/table/table.d.ts +256 -0
  117. package/dist/layout/table/table.js +141 -0
  118. package/dist/layout/tabs/Tab.svelte +60 -0
  119. package/dist/layout/tabs/Tab.svelte.d.ts +4 -0
  120. package/dist/layout/tabs/TabContent.svelte +30 -0
  121. package/dist/layout/tabs/TabContent.svelte.d.ts +4 -0
  122. package/dist/layout/tabs/TabGroup.svelte +62 -0
  123. package/dist/layout/tabs/TabGroup.svelte.d.ts +4 -0
  124. package/dist/layout/tabs/tabs.d.ts +140 -0
  125. package/dist/layout/tabs/tabs.js +298 -0
  126. package/dist/modal/Modal.svelte +207 -0
  127. package/dist/modal/Modal.svelte.d.ts +4 -0
  128. package/dist/modal/modal.d.ts +211 -0
  129. package/dist/modal/modal.js +81 -0
  130. package/dist/sonner/sonner.svelte +13 -0
  131. package/dist/sonner/sonner.svelte.d.ts +4 -0
  132. package/dist/types/variants.d.ts +1 -0
  133. package/dist/types/variants.js +1 -0
  134. package/dist/utils/Portal.svelte +108 -0
  135. package/dist/utils/Portal.svelte.d.ts +8 -0
  136. package/dist/utils/dateUtils.d.ts +7 -0
  137. package/dist/utils/dateUtils.js +26 -0
  138. package/dist/variants.d.ts +30 -0
  139. package/dist/variants.js +36 -0
  140. package/package.json +39 -6
  141. package/dist/layout/Card.svelte +0 -179
  142. package/dist/layout/Card.svelte.d.ts +0 -208
  143. package/dist/layout/index.d.ts +0 -1
  144. package/dist/layout/index.js +0 -1
@@ -0,0 +1,790 @@
1
+ <script lang="ts">
2
+ import { cn } from '../../helper/cls.js';
3
+ import { table } from './table.js';
4
+ import type { TableProps, SortDirection, SortState } from '../../index.js';
5
+
6
+ let {
7
+ data = [],
8
+ columns = [],
9
+ bordered = true,
10
+ striped = false,
11
+ pageSize = 10,
12
+ currentPage: externalCurrentPage,
13
+ totalItems,
14
+ selectable = false,
15
+ selected = $bindable([]),
16
+ onrowclick,
17
+ onsort = () => {},
18
+ onselect = () => {},
19
+ onpagechange,
20
+ onpagesizechange,
21
+ class: classname = '',
22
+ wrapperclass: wrapperClass = '',
23
+ tableclass: tableClass = '',
24
+ theadclass: theadClass = '',
25
+ tbodyclass: tbodyClass = '',
26
+ trclass: trClass = 'bg-white',
27
+ thclass: thClass = '',
28
+ tdclass: tdClass = '',
29
+ footerclass: footerClass = '',
30
+ paginationclass: paginationClass = '',
31
+ rowclass = () => '',
32
+ loading = false,
33
+ expandedContent,
34
+ pagination = true,
35
+ showPagination = true,
36
+ showPageSize = false,
37
+ pageSizeOptions = [5, 10, 25, 50, 100],
38
+ paginationPosition = 'bottom',
39
+ paginationTemplate = 'full'
40
+ }: TableProps<any> = $props();
41
+
42
+ let sortColumn = $state('');
43
+ let sortDirection = $state<SortDirection>(null);
44
+ let internalCurrentPage = $state(externalCurrentPage || 1);
45
+ let internalPageSize = $state(pageSize);
46
+
47
+ // Use external current page if provided
48
+ $effect(() => {
49
+ if (externalCurrentPage !== undefined) {
50
+ internalCurrentPage = externalCurrentPage;
51
+ }
52
+ });
53
+
54
+ // Pagination is automatically determined by pageSize and pagination prop
55
+ const showPaginationControls = $derived(
56
+ pagination &&
57
+ showPagination &&
58
+ (data.length > internalPageSize || totalItems > internalPageSize)
59
+ );
60
+
61
+ // Calculate total items and pages
62
+ const effectiveTotalItems = $derived(totalItems !== undefined ? totalItems : data.length);
63
+ const totalPages = $derived(Math.ceil(effectiveTotalItems / internalPageSize));
64
+
65
+ const {
66
+ base: baseClass,
67
+ wrapper: wrapperBaseClass,
68
+ table: tableBaseClass,
69
+ thead: theadBaseClass,
70
+ tbody: tbodyBaseClass,
71
+ tr: trBaseClass,
72
+ th: thBaseClass,
73
+ td: tdBaseClass,
74
+ footer: footerBaseClass,
75
+ pagination: paginationBaseClass,
76
+ emptyState: emptyStateBaseClass,
77
+ sortButton: sortButtonBaseClass,
78
+ sortIcon: sortIconBaseClass
79
+ } = $derived(
80
+ table({
81
+ bordered,
82
+ striped
83
+ })
84
+ );
85
+
86
+ const baseClasses = $derived(cn(baseClass(), classname));
87
+ const wrapperClasses = $derived(cn(wrapperBaseClass(), wrapperClass));
88
+ const tableClasses = $derived(cn(tableBaseClass(), tableClass));
89
+ const theadClasses = $derived(cn(theadBaseClass(), theadClass));
90
+ const tbodyClasses = $derived(cn(tbodyBaseClass(), tbodyClass));
91
+ const trClasses = $derived(cn(trBaseClass(), trClass));
92
+ const thClasses = $derived(cn(thBaseClass(), thClass));
93
+ const tdClasses = $derived(cn(tdBaseClass(), tdClass));
94
+ const footerClasses = $derived(cn(footerBaseClass(), footerClass));
95
+ const paginationClasses = $derived(cn(paginationBaseClass(), paginationClass));
96
+ const emptyStateClasses = $derived(emptyStateBaseClass());
97
+
98
+ // Handle pagination
99
+ function getPaginatedData() {
100
+ // If no pagination or all data fits on one page, return all data
101
+ if (!pagination || data.length <= internalPageSize) {
102
+ return data;
103
+ }
104
+
105
+ // If external data source might be handling pagination
106
+ if (totalItems !== undefined) {
107
+ // If data.length is less than or equal to pageSize, assume it's already paginated
108
+ if (data.length <= internalPageSize) {
109
+ return data;
110
+ }
111
+ }
112
+
113
+ // Otherwise, handle pagination internally
114
+ const startIndex = (internalCurrentPage - 1) * internalPageSize;
115
+ return data.slice(startIndex, startIndex + internalPageSize);
116
+ }
117
+
118
+ // Handle sorting
119
+ function toggleSort(column: string) {
120
+ const columnDef = columns.find((col) => (col.sortKey || col.key) === column);
121
+ if (!columnDef?.sortable) return;
122
+
123
+ if (sortColumn === column) {
124
+ // Cycle through: asc -> desc -> null
125
+ if (sortDirection === 'asc') {
126
+ sortDirection = 'desc';
127
+ } else if (sortDirection === 'desc') {
128
+ sortDirection = null;
129
+ sortColumn = '';
130
+ } else {
131
+ sortDirection = 'asc';
132
+ }
133
+ } else {
134
+ sortColumn = column;
135
+ sortDirection = 'asc';
136
+ }
137
+
138
+ const newSortState: SortState = { column: sortColumn, direction: sortDirection };
139
+ onsort(newSortState);
140
+ }
141
+
142
+ function toggleRowSelection(row: any) {
143
+ if (!selectable) return;
144
+
145
+ const index = selected.findIndex((r) => r === row);
146
+ if (index === -1) {
147
+ selected = [...selected, row];
148
+ } else {
149
+ selected = selected.filter((r) => r !== row);
150
+ }
151
+
152
+ onselect(selected);
153
+ }
154
+
155
+ function isRowSelected(row: any) {
156
+ return selected.includes(row);
157
+ }
158
+
159
+ function handleRowClick(row: any, index: number) {
160
+ onrowclick?.(row, index);
161
+ }
162
+
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
+ }
189
+ }
190
+
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);
201
+ internalPageSize = newPageSize;
202
+
203
+ // Adjust current page if it would exceed the new total pages
204
+ const newTotalPages = Math.ceil(effectiveTotalItems / newPageSize);
205
+ if (internalCurrentPage > newTotalPages) {
206
+ internalCurrentPage = newTotalPages || 1;
207
+ }
208
+
209
+ 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
+ }
231
+ </script>
232
+
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>
252
+ </div>
253
+ {/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
+ </div>
415
+ </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
+
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}
484
+ {:else}
485
+ <svg
486
+ xmlns="http://www.w3.org/2000/svg"
487
+ fill="none"
488
+ viewBox="0 0 24 24"
489
+ stroke-width="1.5"
490
+ stroke="currentColor"
491
+ class="h-4 w-4 opacity-40"
492
+ >
493
+ <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
+ />
498
+ </svg>
499
+ {/if}
500
+ </span>
501
+ </button>
502
+ {: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}
573
+
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]}
589
+ {/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
+ {/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>
634
+ </div>
635
+
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}
669
+
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"
705
+ 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'
710
+ )}
711
+ onclick={() => goToPage(pageNum)}
712
+ aria-label={`Page ${pageNum}`}
713
+ aria-current={internalCurrentPage === pageNum ? 'page' : undefined}
714
+ >
715
+ {pageNum}
716
+ </button>
717
+ {/each}
718
+ {: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"
780
+ >
781
+ <path d="m13 17 5-5-5-5"></path>
782
+ <path d="m6 17 5-5-5-5"></path>
783
+ </svg>
784
+ </button>
785
+ {/if}
786
+ </div>
787
+ </div>
788
+ </div>
789
+ {/if}
790
+ </div>