@makolabs/ripple 1.2.1 → 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.
@@ -0,0 +1,464 @@
1
+ <script lang="ts">
2
+ import { cn } from '../../helper/cls.js';
3
+ import type { ClassValue } from 'tailwind-variants';
4
+
5
+ /**
6
+ * Pagination component props
7
+ */
8
+ export interface PaginationProps {
9
+ /** Current page number (1-indexed). If not provided, component manages state internally. */
10
+ currentPage?: number;
11
+ /** Total number of items across all pages */
12
+ totalItems: number;
13
+ /** Number of items per page */
14
+ pageSize?: number;
15
+ /** Callback when page changes */
16
+ onPageChange?: (page: number) => void;
17
+ /** Callback when page size changes */
18
+ onPageSizeChange?: (pageSize: number) => void;
19
+ /** Callback when first page is clicked */
20
+ onFirstPage?: () => void;
21
+ /** Callback when last page is clicked */
22
+ onLastPage?: () => void;
23
+ /** Whether pagination controls are disabled */
24
+ disabled?: boolean;
25
+ /** Show page size selector */
26
+ showPageSize?: boolean;
27
+ /** Available page size options */
28
+ pageSizeOptions?: number[];
29
+ /** Pagination template mode: 'full' shows page numbers, 'compact' shows "Page X of Y" */
30
+ template?: 'full' | 'compact';
31
+ /** Show "Showing X to Y of Z results" text */
32
+ showInfo?: boolean;
33
+ /** Show navigation buttons (first, prev, next, last) */
34
+ showNavigation?: boolean;
35
+ /** Show first/last page buttons */
36
+ showFirstLast?: boolean;
37
+ /** Show page number buttons (only applies when template='full') */
38
+ showPageNumbers?: boolean;
39
+ /** Maximum number of visible page buttons */
40
+ maxVisiblePages?: number;
41
+ /** Always show first and last page numbers when there are many pages */
42
+ alwaysShowFirstLast?: boolean;
43
+ /** Hide pagination when there's only one page */
44
+ hideWhenSinglePage?: boolean;
45
+ /** Hide pagination when there are no items */
46
+ hideWhenNoItems?: boolean;
47
+ /** Custom class for wrapper */
48
+ class?: ClassValue;
49
+ /** Custom class for info text */
50
+ infoClass?: ClassValue;
51
+ /** Custom class for navigation buttons */
52
+ buttonClass?: ClassValue;
53
+ /** Custom class for active page button */
54
+ activeButtonClass?: ClassValue;
55
+ /** Use internal state management (when currentPage is not provided) */
56
+ internalState?: boolean;
57
+ }
58
+
59
+ let {
60
+ currentPage: externalCurrentPage,
61
+ totalItems,
62
+ pageSize: externalPageSize,
63
+ onPageChange,
64
+ onPageSizeChange,
65
+ onFirstPage,
66
+ onLastPage,
67
+ disabled = false,
68
+ showPageSize = false,
69
+ pageSizeOptions = [5, 10, 25, 50, 100],
70
+ template = 'full',
71
+ showInfo = true,
72
+ showNavigation = true,
73
+ showFirstLast = true,
74
+ showPageNumbers = true,
75
+ maxVisiblePages = 5,
76
+ alwaysShowFirstLast = true,
77
+ hideWhenSinglePage = true,
78
+ hideWhenNoItems = true,
79
+ class: wrapperClass = '',
80
+ infoClass = '',
81
+ buttonClass = '',
82
+ activeButtonClass = '',
83
+ internalState = false
84
+ }: PaginationProps = $props();
85
+
86
+ // Internal state management
87
+ const defaultPageSize = 10;
88
+ let internalCurrentPage = $state(externalCurrentPage ?? 1);
89
+ let internalPageSize = $state(externalPageSize ?? defaultPageSize);
90
+
91
+ // Use external values if provided, otherwise use internal state or defaults
92
+ const currentPage = $derived(
93
+ externalCurrentPage !== undefined
94
+ ? externalCurrentPage
95
+ : internalState
96
+ ? internalCurrentPage
97
+ : 1
98
+ );
99
+ const pageSize = $derived(
100
+ externalPageSize !== undefined
101
+ ? externalPageSize
102
+ : internalState
103
+ ? internalPageSize
104
+ : defaultPageSize
105
+ );
106
+
107
+ // Sync external page size changes
108
+ $effect(() => {
109
+ if (externalPageSize !== undefined) {
110
+ internalPageSize = externalPageSize;
111
+ }
112
+ });
113
+
114
+ // Sync external current page changes
115
+ $effect(() => {
116
+ if (externalCurrentPage !== undefined) {
117
+ internalCurrentPage = externalCurrentPage;
118
+ }
119
+ });
120
+
121
+ // Validation: ensure pageSize is valid
122
+ const validPageSize = $derived(Math.max(1, pageSize));
123
+ // Validation: ensure totalItems is non-negative
124
+ const validTotalItems = $derived(Math.max(0, totalItems));
125
+
126
+ // Computed values
127
+ const totalPages = $derived(Math.ceil(validTotalItems / validPageSize));
128
+ const startItem = $derived(
129
+ Math.min((currentPage - 1) * validPageSize + 1, validTotalItems)
130
+ );
131
+ const endItem = $derived(Math.min(currentPage * validPageSize, validTotalItems));
132
+
133
+ // Ensure currentPage is within valid range
134
+ const validCurrentPage = $derived(
135
+ Math.max(1, Math.min(currentPage, totalPages || 1))
136
+ );
137
+
138
+ // Navigation functions
139
+ function goToPage(page: number) {
140
+ if (!disabled && page >= 1 && page <= totalPages && page !== validCurrentPage) {
141
+ if (internalState && externalCurrentPage === undefined) {
142
+ internalCurrentPage = page;
143
+ }
144
+ onPageChange?.(page);
145
+ }
146
+ }
147
+
148
+ function firstPage() {
149
+ if (!disabled && validCurrentPage > 1) {
150
+ const targetPage = 1;
151
+ if (internalState && externalCurrentPage === undefined) {
152
+ internalCurrentPage = targetPage;
153
+ }
154
+ onPageChange?.(targetPage);
155
+ onFirstPage?.();
156
+ }
157
+ }
158
+
159
+ function lastPage() {
160
+ if (!disabled && validCurrentPage < totalPages) {
161
+ const targetPage = totalPages;
162
+ if (internalState && externalCurrentPage === undefined) {
163
+ internalCurrentPage = targetPage;
164
+ }
165
+ onPageChange?.(targetPage);
166
+ onLastPage?.();
167
+ }
168
+ }
169
+
170
+ function nextPage() {
171
+ if (!disabled && validCurrentPage < totalPages) {
172
+ const targetPage = validCurrentPage + 1;
173
+ if (internalState && externalCurrentPage === undefined) {
174
+ internalCurrentPage = targetPage;
175
+ }
176
+ onPageChange?.(targetPage);
177
+ }
178
+ }
179
+
180
+ function prevPage() {
181
+ if (!disabled && validCurrentPage > 1) {
182
+ const targetPage = validCurrentPage - 1;
183
+ if (internalState && externalCurrentPage === undefined) {
184
+ internalCurrentPage = targetPage;
185
+ }
186
+ onPageChange?.(targetPage);
187
+ }
188
+ }
189
+
190
+ function handlePageSizeChange(event: Event) {
191
+ if (disabled) return;
192
+ const select = event.target as HTMLSelectElement;
193
+ const newPageSize = parseInt(select.value, 10);
194
+
195
+ if (isNaN(newPageSize) || newPageSize < 1) return;
196
+
197
+ // Calculate new total pages
198
+ const newTotalPages = Math.ceil(validTotalItems / newPageSize);
199
+
200
+ // Adjust current page if it would exceed the new total pages
201
+ let adjustedPage = validCurrentPage;
202
+ if (validCurrentPage > newTotalPages) {
203
+ adjustedPage = newTotalPages || 1;
204
+ if (internalState && externalCurrentPage === undefined) {
205
+ internalCurrentPage = adjustedPage;
206
+ }
207
+ onPageChange?.(adjustedPage);
208
+ }
209
+
210
+ // Update page size
211
+ if (internalState && externalPageSize === undefined) {
212
+ internalPageSize = newPageSize;
213
+ }
214
+ onPageSizeChange?.(newPageSize);
215
+ }
216
+
217
+ /**
218
+ * Calculate which page numbers to display
219
+ */
220
+ function getPageNumbers(): number[] {
221
+ if (totalPages <= maxVisiblePages) {
222
+ // Show all pages if total is less than or equal to max
223
+ return Array.from({ length: totalPages }, (_, i) => i + 1);
224
+ }
225
+
226
+ const pages: number[] = [];
227
+ const halfMax = Math.floor(maxVisiblePages / 2);
228
+
229
+ // Calculate start and end of visible range, centered around current page
230
+ let start = Math.max(1, validCurrentPage - halfMax);
231
+ let end = Math.min(totalPages, start + maxVisiblePages - 1);
232
+
233
+ // Adjust if we're near the end
234
+ if (end - start + 1 < maxVisiblePages) {
235
+ start = Math.max(1, end - maxVisiblePages + 1);
236
+ }
237
+
238
+ // Build page numbers array
239
+ for (let i = start; i <= end; i++) {
240
+ pages.push(i);
241
+ }
242
+
243
+ return pages;
244
+ }
245
+
246
+ // Determine if we should show pagination
247
+ const shouldShowPagination = $derived(
248
+ !hideWhenNoItems || validTotalItems > 0
249
+ );
250
+ const shouldShowControls = $derived(
251
+ !hideWhenSinglePage || totalPages > 1
252
+ );
253
+
254
+ // Default classes using ripple-ui design system
255
+ const defaultWrapperClass = 'flex items-center justify-between p-4';
256
+ const defaultInfoClass = 'text-default-500 text-sm';
257
+ const defaultButtonClass = 'relative inline-flex items-center rounded-md px-2 py-1 text-sm font-medium';
258
+ const defaultActiveButtonClass = 'bg-primary-100 text-primary-700';
259
+ const defaultInactiveButtonClass = 'text-default-700 hover:bg-default-100';
260
+ const defaultDisabledButtonClass = 'text-default-300 cursor-not-allowed';
261
+ const defaultPageButtonClass = 'relative inline-flex items-center rounded-md px-3 py-1 text-sm font-medium';
262
+ </script>
263
+
264
+ {#if shouldShowPagination}
265
+ <div class={cn(defaultWrapperClass, wrapperClass)}>
266
+ {#if showInfo}
267
+ <div class="flex items-center gap-2">
268
+ {#if showPageSize}
269
+ <div class="flex items-center gap-2">
270
+ <label for="pagination-page-size" class="text-default-500 text-sm">Show</label>
271
+ <select
272
+ id="pagination-page-size"
273
+ class={cn(
274
+ 'border-default-200 rounded-md border px-2 py-1 text-sm',
275
+ disabled && 'cursor-not-allowed opacity-50'
276
+ )}
277
+ value={pageSize}
278
+ onchange={handlePageSizeChange}
279
+ disabled={disabled}
280
+ >
281
+ {#each pageSizeOptions as option}
282
+ <option value={option}>{option}</option>
283
+ {/each}
284
+ </select>
285
+ <span class="text-default-500 text-sm">entries</span>
286
+ </div>
287
+ {/if}
288
+ <span class={cn(defaultInfoClass, infoClass)}>
289
+ Showing {startItem} to {endItem} of {validTotalItems} entries
290
+ </span>
291
+ </div>
292
+ {/if}
293
+
294
+ {#if shouldShowControls && showNavigation}
295
+ <div class="flex items-center gap-1">
296
+ {#if showFirstLast}
297
+ <button
298
+ onclick={firstPage}
299
+ disabled={disabled || validCurrentPage === 1}
300
+ class={cn(
301
+ defaultButtonClass,
302
+ buttonClass,
303
+ disabled || validCurrentPage === 1
304
+ ? defaultDisabledButtonClass
305
+ : defaultInactiveButtonClass
306
+ )}
307
+ aria-label="First page"
308
+ type="button"
309
+ >
310
+ «
311
+ </button>
312
+ {/if}
313
+
314
+ <button
315
+ onclick={prevPage}
316
+ disabled={disabled || validCurrentPage === 1}
317
+ class={cn(
318
+ defaultButtonClass,
319
+ buttonClass,
320
+ disabled || validCurrentPage === 1
321
+ ? defaultDisabledButtonClass
322
+ : defaultInactiveButtonClass
323
+ )}
324
+ aria-label="Previous page"
325
+ type="button"
326
+ >
327
+
328
+ </button>
329
+
330
+ {#if template === 'full' && showPageNumbers}
331
+ {#if totalPages <= maxVisiblePages}
332
+ <!-- Show all pages if total is less than or equal to maxVisiblePages -->
333
+ {#each Array(totalPages) as _, i}
334
+ {@const pageNum = i + 1}
335
+ <button
336
+ onclick={() => goToPage(pageNum)}
337
+ disabled={disabled}
338
+ class={cn(
339
+ defaultPageButtonClass,
340
+ buttonClass,
341
+ validCurrentPage === pageNum
342
+ ? cn(defaultActiveButtonClass, activeButtonClass)
343
+ : defaultInactiveButtonClass
344
+ )}
345
+ aria-label="Page {pageNum}"
346
+ aria-current={validCurrentPage === pageNum ? 'page' : undefined}
347
+ type="button"
348
+ >
349
+ {pageNum}
350
+ </button>
351
+ {/each}
352
+ {:else}
353
+ <!-- Smart pagination for many pages -->
354
+ {@const pageNumbers = getPageNumbers()}
355
+ {@const showFirst = alwaysShowFirstLast && pageNumbers[0] > 1}
356
+ {@const showLast = alwaysShowFirstLast && pageNumbers[pageNumbers.length - 1] < totalPages}
357
+
358
+ {#if showFirst}
359
+ <button
360
+ onclick={() => goToPage(1)}
361
+ disabled={disabled}
362
+ class={cn(
363
+ defaultPageButtonClass,
364
+ buttonClass,
365
+ validCurrentPage === 1
366
+ ? cn(defaultActiveButtonClass, activeButtonClass)
367
+ : defaultInactiveButtonClass
368
+ )}
369
+ aria-label="Page 1"
370
+ aria-current={validCurrentPage === 1 ? 'page' : undefined}
371
+ type="button"
372
+ >
373
+ 1
374
+ </button>
375
+ {#if pageNumbers[0] > 2}
376
+ <span class="px-2 text-sm text-default-400">...</span>
377
+ {/if}
378
+ {/if}
379
+
380
+ {#each pageNumbers as pageNum}
381
+ <button
382
+ onclick={() => goToPage(pageNum)}
383
+ disabled={disabled}
384
+ class={cn(
385
+ defaultPageButtonClass,
386
+ buttonClass,
387
+ validCurrentPage === pageNum
388
+ ? cn(defaultActiveButtonClass, activeButtonClass)
389
+ : defaultInactiveButtonClass
390
+ )}
391
+ aria-label="Page {pageNum}"
392
+ aria-current={validCurrentPage === pageNum ? 'page' : undefined}
393
+ type="button"
394
+ >
395
+ {pageNum}
396
+ </button>
397
+ {/each}
398
+
399
+ {#if showLast}
400
+ {#if pageNumbers[pageNumbers.length - 1] < totalPages - 1}
401
+ <span class="px-2 text-sm text-default-400">...</span>
402
+ {/if}
403
+ <button
404
+ onclick={() => goToPage(totalPages)}
405
+ disabled={disabled}
406
+ class={cn(
407
+ defaultPageButtonClass,
408
+ buttonClass,
409
+ validCurrentPage === totalPages
410
+ ? cn(defaultActiveButtonClass, activeButtonClass)
411
+ : defaultInactiveButtonClass
412
+ )}
413
+ aria-label="Page {totalPages}"
414
+ aria-current={validCurrentPage === totalPages ? 'page' : undefined}
415
+ type="button"
416
+ >
417
+ {totalPages}
418
+ </button>
419
+ {/if}
420
+ {/if}
421
+ {:else if template === 'compact'}
422
+ <span class="text-default-500 px-2 text-sm">
423
+ Page {validCurrentPage} of {totalPages}
424
+ </span>
425
+ {/if}
426
+
427
+ <button
428
+ onclick={nextPage}
429
+ disabled={disabled || validCurrentPage === totalPages}
430
+ class={cn(
431
+ defaultButtonClass,
432
+ buttonClass,
433
+ disabled || validCurrentPage === totalPages
434
+ ? defaultDisabledButtonClass
435
+ : defaultInactiveButtonClass
436
+ )}
437
+ aria-label="Next page"
438
+ type="button"
439
+ >
440
+
441
+ </button>
442
+
443
+ {#if showFirstLast}
444
+ <button
445
+ onclick={lastPage}
446
+ disabled={disabled || validCurrentPage === totalPages}
447
+ class={cn(
448
+ defaultButtonClass,
449
+ buttonClass,
450
+ disabled || validCurrentPage === totalPages
451
+ ? defaultDisabledButtonClass
452
+ : defaultInactiveButtonClass
453
+ )}
454
+ aria-label="Last page"
455
+ type="button"
456
+ >
457
+ »
458
+ </button>
459
+ {/if}
460
+ </div>
461
+ {/if}
462
+ </div>
463
+ {/if}
464
+
@@ -0,0 +1,57 @@
1
+ import type { ClassValue } from 'tailwind-variants';
2
+ /**
3
+ * Pagination component props
4
+ */
5
+ export interface PaginationProps {
6
+ /** Current page number (1-indexed). If not provided, component manages state internally. */
7
+ currentPage?: number;
8
+ /** Total number of items across all pages */
9
+ totalItems: number;
10
+ /** Number of items per page */
11
+ pageSize?: number;
12
+ /** Callback when page changes */
13
+ onPageChange?: (page: number) => void;
14
+ /** Callback when page size changes */
15
+ onPageSizeChange?: (pageSize: number) => void;
16
+ /** Callback when first page is clicked */
17
+ onFirstPage?: () => void;
18
+ /** Callback when last page is clicked */
19
+ onLastPage?: () => void;
20
+ /** Whether pagination controls are disabled */
21
+ disabled?: boolean;
22
+ /** Show page size selector */
23
+ showPageSize?: boolean;
24
+ /** Available page size options */
25
+ pageSizeOptions?: number[];
26
+ /** Pagination template mode: 'full' shows page numbers, 'compact' shows "Page X of Y" */
27
+ template?: 'full' | 'compact';
28
+ /** Show "Showing X to Y of Z results" text */
29
+ showInfo?: boolean;
30
+ /** Show navigation buttons (first, prev, next, last) */
31
+ showNavigation?: boolean;
32
+ /** Show first/last page buttons */
33
+ showFirstLast?: boolean;
34
+ /** Show page number buttons (only applies when template='full') */
35
+ showPageNumbers?: boolean;
36
+ /** Maximum number of visible page buttons */
37
+ maxVisiblePages?: number;
38
+ /** Always show first and last page numbers when there are many pages */
39
+ alwaysShowFirstLast?: boolean;
40
+ /** Hide pagination when there's only one page */
41
+ hideWhenSinglePage?: boolean;
42
+ /** Hide pagination when there are no items */
43
+ hideWhenNoItems?: boolean;
44
+ /** Custom class for wrapper */
45
+ class?: ClassValue;
46
+ /** Custom class for info text */
47
+ infoClass?: ClassValue;
48
+ /** Custom class for navigation buttons */
49
+ buttonClass?: ClassValue;
50
+ /** Custom class for active page button */
51
+ activeButtonClass?: ClassValue;
52
+ /** Use internal state management (when currentPage is not provided) */
53
+ internalState?: boolean;
54
+ }
55
+ declare const Pagination: import("svelte").Component<PaginationProps, {}, "">;
56
+ type Pagination = ReturnType<typeof Pagination>;
57
+ export default Pagination;
package/dist/index.d.ts CHANGED
@@ -229,6 +229,9 @@ export type TableProps<T extends DataRow = any> = {
229
229
  onpagesizechange?: (pageSize: number) => void;
230
230
  paginationPosition?: 'top' | 'bottom' | 'both';
231
231
  paginationTemplate?: 'simple' | 'full';
232
+ title?: string;
233
+ subtitle?: string;
234
+ headerActions?: Snippet;
232
235
  };
233
236
  export type BreadcrumbItem = {
234
237
  label: string;
@@ -347,6 +350,8 @@ export { default as Card } from './layout/card/Card.svelte';
347
350
  export { default as MetricCard } from './layout/card/MetricCard.svelte';
348
351
  export { default as RankedCard } from './layout/card/RankedCard.svelte';
349
352
  export { default as Alert } from './elements/alert/Alert.svelte';
353
+ export { default as Pagination } from './elements/pagination/Pagination.svelte';
354
+ export type { PaginationProps } from './elements/pagination/Pagination.svelte';
350
355
  export type TabProps = {
351
356
  value: string;
352
357
  label: string;
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ export { default as MetricCard } from './layout/card/MetricCard.svelte';
33
33
  export { default as RankedCard } from './layout/card/RankedCard.svelte';
34
34
  // Elements - Alert
35
35
  export { default as Alert } from './elements/alert/Alert.svelte';
36
+ export { default as Pagination } from './elements/pagination/Pagination.svelte';
36
37
  export { default as Tab } from './layout/tabs/Tab.svelte';
37
38
  export { default as TabContent } from './layout/tabs/TabContent.svelte';
38
39
  export { default as TabGroup } from './layout/tabs/TabGroup.svelte';
@@ -65,7 +65,6 @@
65
65
 
66
66
  // Reactively compute the active states based on the current route
67
67
  const navigationItems = $derived(items.map((item) => processNavigationItem(item)));
68
- $inspect(navigationItems);
69
68
 
70
69
  const sidebarClasses = $derived(
71
70
  clsx(