svelte-comp 1.3.5 → 1.3.6

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 (46) hide show
  1. package/LICENSE.md +21 -21
  2. package/README.md +101 -101
  3. package/dist/App.svelte +1046 -1046
  4. package/dist/Container.svelte +59 -59
  5. package/dist/app.css +234 -234
  6. package/dist/app.d.ts +10 -10
  7. package/dist/lib/Accordion.svelte +155 -155
  8. package/dist/lib/Badge.svelte +44 -44
  9. package/dist/lib/Button.svelte +185 -185
  10. package/dist/lib/Calendar.svelte +384 -384
  11. package/dist/lib/Card.svelte +103 -103
  12. package/dist/lib/Carousel.svelte +293 -293
  13. package/dist/lib/CheckBox.svelte +210 -210
  14. package/dist/lib/CodeView.svelte +308 -308
  15. package/dist/lib/ColorPicker.svelte +159 -159
  16. package/dist/lib/ContextMenu.svelte +328 -328
  17. package/dist/lib/DatePicker.svelte +246 -246
  18. package/dist/lib/Dialog.svelte +233 -233
  19. package/dist/lib/Field.svelte +299 -299
  20. package/dist/lib/FilePicker.svelte +295 -295
  21. package/dist/lib/Form.svelte +438 -438
  22. package/dist/lib/Hamburger.svelte +217 -217
  23. package/dist/lib/InstallPWA.svelte +94 -94
  24. package/dist/lib/Menu.svelte +623 -623
  25. package/dist/lib/NoticeBase.svelte +140 -140
  26. package/dist/lib/PaginatedCard.svelte +73 -73
  27. package/dist/lib/Pagination.svelte +119 -119
  28. package/dist/lib/PrimaryColorSelect.svelte +111 -111
  29. package/dist/lib/ProgressBar.svelte +141 -141
  30. package/dist/lib/ProgressCircle.svelte +190 -190
  31. package/dist/lib/Radio.svelte +189 -189
  32. package/dist/lib/SearchInput.svelte +104 -104
  33. package/dist/lib/Select.svelte +524 -524
  34. package/dist/lib/Slider.svelte +253 -253
  35. package/dist/lib/Splitter.svelte +159 -159
  36. package/dist/lib/Switch.svelte +168 -168
  37. package/dist/lib/Table.svelte +299 -299
  38. package/dist/lib/Tabs.svelte +213 -213
  39. package/dist/lib/ThemeToggle.svelte +128 -128
  40. package/dist/lib/TimePicker.svelte +312 -312
  41. package/dist/lib/TimePickerNew.svelte +634 -634
  42. package/dist/lib/Toast.svelte +123 -123
  43. package/dist/lib/Tooltip.svelte +110 -110
  44. package/dist/lib/Topbar.svelte +112 -112
  45. package/dist/styles.css +234 -234
  46. package/package.json +52 -52
@@ -1,140 +1,140 @@
1
- <!-- src/lib/NoticeBase.svelte -->
2
- <script lang="ts">
3
- /**
4
- * @component NoticeBase
5
- * @description Shared base for Toast and Badge visuals.
6
- *
7
- * @prop title {string} - Optional title displayed above the message
8
- *
9
- * @prop message {string} - Notice text content
10
- *
11
- * @prop variant {ToastVariant} - Visual style
12
- * @options success|danger|warning|info
13
- * @default info
14
- *
15
- * @prop showIcon {boolean} - Shows an icon matching the variant
16
- * @default true
17
- *
18
- * @prop inline {boolean} - Inline layout without overlay styling
19
- * @default false
20
- *
21
- * @prop size {"sm" | "md"} - Size preset for spacing and typography
22
- * @default "sm"
23
- *
24
- * @prop end {Snippet} - Trailing content (e.g. close button)
25
- *
26
- * @prop class {string} - Additional wrapper classes
27
- * @default ""
28
- *
29
- * @note Used by Toast and Badge to keep styles consistent.
30
- */
31
- import type { Snippet } from "svelte";
32
- import type { ToastVariant } from "./types";
33
- import { cx } from "../utils";
34
-
35
- type Props = {
36
- title?: string;
37
- message: string;
38
- variant?: ToastVariant;
39
- showIcon?: boolean;
40
- class?: string;
41
- inline?: boolean;
42
- size?: "sm" | "md";
43
- end?: Snippet;
44
- };
45
-
46
- let {
47
- title,
48
- message,
49
- variant = "info",
50
- showIcon = true,
51
- inline = false,
52
- size = "sm",
53
- end,
54
- class: externalClass = "",
55
- }: Props = $props();
56
-
57
- function variantClasses(v: ToastVariant) {
58
- switch (v) {
59
- case "success":
60
- return "bg-[var(--color-bg-success)] text-[var(--color-text-success)]";
61
- case "danger":
62
- return "bg-[var(--color-bg-danger)] text-[var(--color-text-danger)]";
63
- case "warning":
64
- return "bg-[var(--color-bg-warning)] text-[var(--color-text-warning)]";
65
- default:
66
- return "bg-[var(--color-bg-page)] text-[var(--color-text-default)]";
67
- }
68
- }
69
-
70
- const sizeClasses = $derived(
71
- size === "md"
72
- ? "gap-[calc(var(--spacing-sm)+var(--spacing-xs))] px-[var(--spacing-md)] py-[calc(var(--spacing-sm)+var(--spacing-xs))] rounded-[var(--radius-lg)]"
73
- : "gap-[var(--spacing-sm)] px-[calc(var(--spacing-sm)+var(--spacing-xs))] py-[calc(var(--spacing-sm)+var(--spacing-xs)/2)] rounded-[var(--radius-md)]"
74
- );
75
-
76
- const iconClass = $derived(size === "md" ? "w-5 h-5" : "w-4 h-4");
77
-
78
- const titleClass = $derived(
79
- size === "md"
80
- ? "font-[var(--font-weight-medium)] truncate [font-size:var(--text-md)] max-sm:[font-size:var(--text-sm)]"
81
- : "font-[var(--font-weight-medium)] truncate [font-size:var(--text-sm)]"
82
- );
83
-
84
- const messageClass = $derived(
85
- size === "md"
86
- ? "line-clamp-3 [font-size:var(--text-sm)] max-sm:[font-size:var(--text-xs)]"
87
- : "truncate [font-size:var(--text-xs)]"
88
- );
89
-
90
- const rootClass = $derived(
91
- cx(
92
- "flex items-center border border-[var(--border-color-default)]",
93
- sizeClasses,
94
- !inline && "shadow-[0_8px_16px_var(--shadow-color)] backdrop-blur-sm",
95
- variantClasses(variant),
96
- externalClass
97
- )
98
- );
99
- </script>
100
-
101
- <div class={rootClass} role="status" aria-live="polite">
102
- {#if showIcon}
103
- <span class={cx(iconClass, "flex-shrink-0")} aria-hidden="true">
104
- {#if variant === "success"}
105
- <svg fill="none" viewBox="0 0 26 26">
106
- <path d="M8.5 14L11.1 16.6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
107
- <path d="M18.2 10L11.6 16.6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
108
- <path d="M13 25C19.6 25 25 19.6 25 13C25 6.4 19.6 1 13 1C6.4 1 1 6.4 1 13C1 19.6 6.4 25 13 25Z" stroke="currentColor" stroke-width="2"/>
109
- </svg>
110
- {:else if variant === "danger"}
111
- <svg fill="none" viewBox="0 0 26 26">
112
- <path d="M13 25C19.6 25 25 19.6 25 13C25 6.4 19.6 1 13 1C6.4 1 1 6.4 1 13C1 19.6 6.4 25 13 25Z" stroke="currentColor" stroke-width="2"/>
113
- <path d="M9 9.5L16.7 17.3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
114
- <path d="M16.7 9.5L9 17.3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
115
- </svg>
116
- {:else if variant === "warning"}
117
- <svg fill="none" viewBox="0 0 27 27">
118
- <path d="M4.6 25.9H22.5C25.2 25.9 26.8 23 25.6 20.6L16.6 3.8C15.3 1.3 11.8 1.3 10.5 3.8L1.5 20.6C0.3 23 1.9 25.9 4.6 25.9Z" stroke="currentColor" stroke-width="2"/>
119
- <path d="M13.9 18H13.2L11.9 9.3C11.9 8.6 12.5 8 13.2 8H13.9C14.6 8 15.2 8.6 15.2 9.3L13.9 18Z" fill="currentColor"/>
120
- <circle cx="13.5" cy="20.6" r="1.3" fill="currentColor"/>
121
- </svg>
122
- {:else}
123
- <svg fill="none" viewBox="0 0 26 26">
124
- <path d="M13 25C19.6 25 25 19.6 25 13C25 6.4 19.6 1 13 1C6.4 1 1 6.4 1 13C1 19.6 6.4 25 13 25Z" stroke="currentColor" stroke-width="2"/>
125
- <circle cx="13" cy="7.7" r="1.3" fill="currentColor"/>
126
- <rect x="11.6" y="10.3" width="2.7" height="9.4" rx="1.3" fill="currentColor"/>
127
- </svg>
128
- {/if}
129
- </span>
130
- {/if}
131
-
132
- <div class="flex-1 min-w-0">
133
- {#if title}
134
- <div class={titleClass}>{title}</div>
135
- {/if}
136
- <div class={messageClass} title={message}>{message}</div>
137
- </div>
138
-
139
- {@render end?.()}
140
- </div>
1
+ <!-- src/lib/NoticeBase.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * @component NoticeBase
5
+ * @description Shared base for Toast and Badge visuals.
6
+ *
7
+ * @prop title {string} - Optional title displayed above the message
8
+ *
9
+ * @prop message {string} - Notice text content
10
+ *
11
+ * @prop variant {ToastVariant} - Visual style
12
+ * @options success|danger|warning|info
13
+ * @default info
14
+ *
15
+ * @prop showIcon {boolean} - Shows an icon matching the variant
16
+ * @default true
17
+ *
18
+ * @prop inline {boolean} - Inline layout without overlay styling
19
+ * @default false
20
+ *
21
+ * @prop size {"sm" | "md"} - Size preset for spacing and typography
22
+ * @default "sm"
23
+ *
24
+ * @prop end {Snippet} - Trailing content (e.g. close button)
25
+ *
26
+ * @prop class {string} - Additional wrapper classes
27
+ * @default ""
28
+ *
29
+ * @note Used by Toast and Badge to keep styles consistent.
30
+ */
31
+ import type { Snippet } from "svelte";
32
+ import type { ToastVariant } from "./types";
33
+ import { cx } from "../utils";
34
+
35
+ type Props = {
36
+ title?: string;
37
+ message: string;
38
+ variant?: ToastVariant;
39
+ showIcon?: boolean;
40
+ class?: string;
41
+ inline?: boolean;
42
+ size?: "sm" | "md";
43
+ end?: Snippet;
44
+ };
45
+
46
+ let {
47
+ title,
48
+ message,
49
+ variant = "info",
50
+ showIcon = true,
51
+ inline = false,
52
+ size = "sm",
53
+ end,
54
+ class: externalClass = "",
55
+ }: Props = $props();
56
+
57
+ function variantClasses(v: ToastVariant) {
58
+ switch (v) {
59
+ case "success":
60
+ return "bg-[var(--color-bg-success)] text-[var(--color-text-success)]";
61
+ case "danger":
62
+ return "bg-[var(--color-bg-danger)] text-[var(--color-text-danger)]";
63
+ case "warning":
64
+ return "bg-[var(--color-bg-warning)] text-[var(--color-text-warning)]";
65
+ default:
66
+ return "bg-[var(--color-bg-page)] text-[var(--color-text-default)]";
67
+ }
68
+ }
69
+
70
+ const sizeClasses = $derived(
71
+ size === "md"
72
+ ? "gap-[calc(var(--spacing-sm)+var(--spacing-xs))] px-[var(--spacing-md)] py-[calc(var(--spacing-sm)+var(--spacing-xs))] rounded-[var(--radius-lg)]"
73
+ : "gap-[var(--spacing-sm)] px-[calc(var(--spacing-sm)+var(--spacing-xs))] py-[calc(var(--spacing-sm)+var(--spacing-xs)/2)] rounded-[var(--radius-md)]"
74
+ );
75
+
76
+ const iconClass = $derived(size === "md" ? "w-5 h-5" : "w-4 h-4");
77
+
78
+ const titleClass = $derived(
79
+ size === "md"
80
+ ? "font-[var(--font-weight-medium)] truncate [font-size:var(--text-md)] max-sm:[font-size:var(--text-sm)]"
81
+ : "font-[var(--font-weight-medium)] truncate [font-size:var(--text-sm)]"
82
+ );
83
+
84
+ const messageClass = $derived(
85
+ size === "md"
86
+ ? "line-clamp-3 [font-size:var(--text-sm)] max-sm:[font-size:var(--text-xs)]"
87
+ : "truncate [font-size:var(--text-xs)]"
88
+ );
89
+
90
+ const rootClass = $derived(
91
+ cx(
92
+ "flex items-center border border-[var(--border-color-default)]",
93
+ sizeClasses,
94
+ !inline && "shadow-[0_8px_16px_var(--shadow-color)] backdrop-blur-sm",
95
+ variantClasses(variant),
96
+ externalClass
97
+ )
98
+ );
99
+ </script>
100
+
101
+ <div class={rootClass} role="status" aria-live="polite">
102
+ {#if showIcon}
103
+ <span class={cx(iconClass, "flex-shrink-0")} aria-hidden="true">
104
+ {#if variant === "success"}
105
+ <svg fill="none" viewBox="0 0 26 26">
106
+ <path d="M8.5 14L11.1 16.6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
107
+ <path d="M18.2 10L11.6 16.6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
108
+ <path d="M13 25C19.6 25 25 19.6 25 13C25 6.4 19.6 1 13 1C6.4 1 1 6.4 1 13C1 19.6 6.4 25 13 25Z" stroke="currentColor" stroke-width="2"/>
109
+ </svg>
110
+ {:else if variant === "danger"}
111
+ <svg fill="none" viewBox="0 0 26 26">
112
+ <path d="M13 25C19.6 25 25 19.6 25 13C25 6.4 19.6 1 13 1C6.4 1 1 6.4 1 13C1 19.6 6.4 25 13 25Z" stroke="currentColor" stroke-width="2"/>
113
+ <path d="M9 9.5L16.7 17.3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
114
+ <path d="M16.7 9.5L9 17.3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
115
+ </svg>
116
+ {:else if variant === "warning"}
117
+ <svg fill="none" viewBox="0 0 27 27">
118
+ <path d="M4.6 25.9H22.5C25.2 25.9 26.8 23 25.6 20.6L16.6 3.8C15.3 1.3 11.8 1.3 10.5 3.8L1.5 20.6C0.3 23 1.9 25.9 4.6 25.9Z" stroke="currentColor" stroke-width="2"/>
119
+ <path d="M13.9 18H13.2L11.9 9.3C11.9 8.6 12.5 8 13.2 8H13.9C14.6 8 15.2 8.6 15.2 9.3L13.9 18Z" fill="currentColor"/>
120
+ <circle cx="13.5" cy="20.6" r="1.3" fill="currentColor"/>
121
+ </svg>
122
+ {:else}
123
+ <svg fill="none" viewBox="0 0 26 26">
124
+ <path d="M13 25C19.6 25 25 19.6 25 13C25 6.4 19.6 1 13 1C6.4 1 1 6.4 1 13C1 19.6 6.4 25 13 25Z" stroke="currentColor" stroke-width="2"/>
125
+ <circle cx="13" cy="7.7" r="1.3" fill="currentColor"/>
126
+ <rect x="11.6" y="10.3" width="2.7" height="9.4" rx="1.3" fill="currentColor"/>
127
+ </svg>
128
+ {/if}
129
+ </span>
130
+ {/if}
131
+
132
+ <div class="flex-1 min-w-0">
133
+ {#if title}
134
+ <div class={titleClass}>{title}</div>
135
+ {/if}
136
+ <div class={messageClass} title={message}>{message}</div>
137
+ </div>
138
+
139
+ {@render end?.()}
140
+ </div>
@@ -1,73 +1,73 @@
1
- <!-- src/lib/PaginatedCard.svelte -->
2
- <script lang="ts">
3
- /**
4
- * @component PaginatedCard
5
- * @description A card component with built-in pagination. Renders items page by page inside a `Card` and appends `Pagination` in the footer.
6
- *
7
- * @prop items {Snippet[]} - Array of renderable snippets for each item
8
- * @default []
9
- *
10
- * @prop itemsPerPage {number} - Items per page (must be >= 1)
11
- * @default 1
12
- *
13
- * @prop header {Snippet} - Optional `Card` header content
14
- *
15
- * @prop footer {Snippet} - Custom footer content shown above pagination
16
- *
17
- * @prop class {string} - Extra classes passed to the underlying `Card`
18
- * @default ""
19
- *
20
- * @note Maintains internal `currentPage` state (starts at `1`).
21
- * @note `totalPages` is clamped to at least `1`; empty `items` still yields one page.
22
- * @note Pagination is always visible; your `footer` snippet renders before it.
23
- * @note Uses `Pagination.svelte` internally with `{ currentPage, totalPages, onPageChange }`.
24
- * @note `itemsPerPage` must be `>= 1`; smaller values are not supported.
25
- */
26
- import Card from "./Card.svelte";
27
- import Pagination from "./Pagination.svelte";
28
- import type { Snippet } from "svelte";
29
-
30
- type Props = {
31
- items?: Snippet[];
32
- itemsPerPage?: number;
33
- header?: Snippet;
34
- footer?: Snippet;
35
- class?: string;
36
- };
37
-
38
- let {
39
- items = [],
40
- itemsPerPage = 1,
41
- header,
42
- footer,
43
- class: externalClass = "",
44
- }: Props = $props();
45
-
46
- let currentPage = $state(1);
47
-
48
- const totalPages = $derived(
49
- Math.max(1, Math.ceil(items.length / itemsPerPage))
50
- );
51
-
52
- const pageItems = $derived(
53
- items.slice(
54
- (currentPage - 1) * itemsPerPage,
55
- (currentPage - 1) * itemsPerPage + itemsPerPage
56
- )
57
- );
58
-
59
- function handlePageChange(p: number) {
60
- currentPage = p;
61
- }
62
- </script>
63
-
64
- {#snippet composedFooter()}
65
- {#if footer}{@render footer?.()}{/if}
66
- <Pagination {currentPage} {totalPages} onPageChange={handlePageChange} />
67
- {/snippet}
68
-
69
- <Card class={externalClass} {header} footer={composedFooter}>
70
- {#each pageItems as it, idx (idx)}
71
- {@render it?.()}
72
- {/each}
73
- </Card>
1
+ <!-- src/lib/PaginatedCard.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * @component PaginatedCard
5
+ * @description A card component with built-in pagination. Renders items page by page inside a `Card` and appends `Pagination` in the footer.
6
+ *
7
+ * @prop items {Snippet[]} - Array of renderable snippets for each item
8
+ * @default []
9
+ *
10
+ * @prop itemsPerPage {number} - Items per page (must be >= 1)
11
+ * @default 1
12
+ *
13
+ * @prop header {Snippet} - Optional `Card` header content
14
+ *
15
+ * @prop footer {Snippet} - Custom footer content shown above pagination
16
+ *
17
+ * @prop class {string} - Extra classes passed to the underlying `Card`
18
+ * @default ""
19
+ *
20
+ * @note Maintains internal `currentPage` state (starts at `1`).
21
+ * @note `totalPages` is clamped to at least `1`; empty `items` still yields one page.
22
+ * @note Pagination is always visible; your `footer` snippet renders before it.
23
+ * @note Uses `Pagination.svelte` internally with `{ currentPage, totalPages, onPageChange }`.
24
+ * @note `itemsPerPage` must be `>= 1`; smaller values are not supported.
25
+ */
26
+ import Card from "./Card.svelte";
27
+ import Pagination from "./Pagination.svelte";
28
+ import type { Snippet } from "svelte";
29
+
30
+ type Props = {
31
+ items?: Snippet[];
32
+ itemsPerPage?: number;
33
+ header?: Snippet;
34
+ footer?: Snippet;
35
+ class?: string;
36
+ };
37
+
38
+ let {
39
+ items = [],
40
+ itemsPerPage = 1,
41
+ header,
42
+ footer,
43
+ class: externalClass = "",
44
+ }: Props = $props();
45
+
46
+ let currentPage = $state(1);
47
+
48
+ const totalPages = $derived(
49
+ Math.max(1, Math.ceil(items.length / itemsPerPage))
50
+ );
51
+
52
+ const pageItems = $derived(
53
+ items.slice(
54
+ (currentPage - 1) * itemsPerPage,
55
+ (currentPage - 1) * itemsPerPage + itemsPerPage
56
+ )
57
+ );
58
+
59
+ function handlePageChange(p: number) {
60
+ currentPage = p;
61
+ }
62
+ </script>
63
+
64
+ {#snippet composedFooter()}
65
+ {#if footer}{@render footer?.()}{/if}
66
+ <Pagination {currentPage} {totalPages} onPageChange={handlePageChange} />
67
+ {/snippet}
68
+
69
+ <Card class={externalClass} {header} footer={composedFooter}>
70
+ {#each pageItems as it, idx (idx)}
71
+ {@render it?.()}
72
+ {/each}
73
+ </Card>
@@ -1,119 +1,119 @@
1
- <!-- src/lib/Pagination.svelte -->
2
- <script lang="ts">
3
- /**
4
- * @component Pagination
5
- * @description Compact pagination component for table or list navigation.
6
- *
7
- * @prop currentPage {number} - The active page number (1-based)
8
- *
9
- * @prop totalPages {number} - Total number of pages available
10
- *
11
- * @prop onPageChange {(page: number) => void} - Fired when a page button is clicked
12
- *
13
- * @prop class {string} - Custom classes applied to the pagination wrapper
14
- * @default ""
15
- *
16
- * @note Displays “Page X of Y” and numbered page buttons.
17
- * @note Prev/next buttons are disabled at the edges.
18
- * @note Shows up to 3 numbered buttons centered around the current page.
19
- * @note Uses `aria-current=\"page\"` on the active page for accessibility.
20
- * @note Buttons are native `<button>` elements for keyboard support.
21
- */
22
- import { cx, times } from "../utils";
23
- import Button from "./Button.svelte";
24
-
25
- type Props = {
26
- currentPage: number;
27
- totalPages: number;
28
- onPageChange: (page: number) => void;
29
- class?: string;
30
- };
31
-
32
- let {
33
- currentPage,
34
- totalPages,
35
- onPageChange,
36
- class: externalClass = "",
37
- }: Props = $props();
38
-
39
- const wrapperClass = $derived(
40
- cx(
41
- "flex flex-wrap items-center justify-center gap-2 text-xs text-[var(--color-text-muted)] py-0.5 overflow-visible",
42
- externalClass
43
- )
44
- );
45
-
46
- function nextPage() {
47
- if (currentPage < totalPages) onPageChange(currentPage + 1);
48
- }
49
-
50
- function prevPage() {
51
- if (currentPage > 1) onPageChange(currentPage - 1);
52
- }
53
-
54
- function getVisiblePages(): number[] {
55
- const maxVisible = 3;
56
- if (totalPages <= maxVisible) return times(totalPages, (i) => i + 1);
57
-
58
- let start = currentPage - 1;
59
- if (start < 1) start = 1;
60
- if (start + maxVisible - 1 > totalPages)
61
- start = totalPages - maxVisible + 1;
62
-
63
- return times(maxVisible, (i) => start + i);
64
- }
65
- </script>
66
-
67
- <div class={wrapperClass}>
68
- <span class="pagination-count">Page {currentPage} of {totalPages}</span>
69
-
70
- <Button
71
- onClick={prevPage}
72
- disabled={currentPage === 1}
73
- sz="xs"
74
- variant="secondary"
75
- class="pagination-btn"
76
- >
77
- &lt;
78
- </Button>
79
-
80
- {#each getVisiblePages() as page (page)}
81
- <Button
82
- onClick={() => onPageChange(page)}
83
- sz="xs"
84
- variant={currentPage === page ? "primary" : "secondary"}
85
- aria-current={currentPage === page ? "page" : undefined}
86
- class="pagination-btn"
87
- >
88
- {page}
89
- </Button>
90
- {/each}
91
-
92
- <Button
93
- onClick={nextPage}
94
- disabled={currentPage === totalPages}
95
- sz="xs"
96
- variant="secondary"
97
- class="pagination-btn"
98
- >
99
- &gt;
100
- </Button>
101
- </div>
102
-
103
- <style>
104
- @media (max-width: 640px) {
105
- :global(.pagination-btn) {
106
- font-size: 10px;
107
- line-height: 1;
108
- height: 20px;
109
- padding: 0 6px;
110
- }
111
- }
112
-
113
- @media (max-width: 480px) {
114
- :global(.pagination-count) {
115
- display: none;
116
- }
117
- }
118
- </style>
119
-
1
+ <!-- src/lib/Pagination.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * @component Pagination
5
+ * @description Compact pagination component for table or list navigation.
6
+ *
7
+ * @prop currentPage {number} - The active page number (1-based)
8
+ *
9
+ * @prop totalPages {number} - Total number of pages available
10
+ *
11
+ * @prop onPageChange {(page: number) => void} - Fired when a page button is clicked
12
+ *
13
+ * @prop class {string} - Custom classes applied to the pagination wrapper
14
+ * @default ""
15
+ *
16
+ * @note Displays “Page X of Y” and numbered page buttons.
17
+ * @note Prev/next buttons are disabled at the edges.
18
+ * @note Shows up to 3 numbered buttons centered around the current page.
19
+ * @note Uses `aria-current=\"page\"` on the active page for accessibility.
20
+ * @note Buttons are native `<button>` elements for keyboard support.
21
+ */
22
+ import { cx, times } from "../utils";
23
+ import Button from "./Button.svelte";
24
+
25
+ type Props = {
26
+ currentPage: number;
27
+ totalPages: number;
28
+ onPageChange: (page: number) => void;
29
+ class?: string;
30
+ };
31
+
32
+ let {
33
+ currentPage,
34
+ totalPages,
35
+ onPageChange,
36
+ class: externalClass = "",
37
+ }: Props = $props();
38
+
39
+ const wrapperClass = $derived(
40
+ cx(
41
+ "flex flex-wrap items-center justify-center gap-2 text-xs text-[var(--color-text-muted)] py-0.5 overflow-visible",
42
+ externalClass
43
+ )
44
+ );
45
+
46
+ function nextPage() {
47
+ if (currentPage < totalPages) onPageChange(currentPage + 1);
48
+ }
49
+
50
+ function prevPage() {
51
+ if (currentPage > 1) onPageChange(currentPage - 1);
52
+ }
53
+
54
+ function getVisiblePages(): number[] {
55
+ const maxVisible = 3;
56
+ if (totalPages <= maxVisible) return times(totalPages, (i) => i + 1);
57
+
58
+ let start = currentPage - 1;
59
+ if (start < 1) start = 1;
60
+ if (start + maxVisible - 1 > totalPages)
61
+ start = totalPages - maxVisible + 1;
62
+
63
+ return times(maxVisible, (i) => start + i);
64
+ }
65
+ </script>
66
+
67
+ <div class={wrapperClass}>
68
+ <span class="pagination-count">Page {currentPage} of {totalPages}</span>
69
+
70
+ <Button
71
+ onClick={prevPage}
72
+ disabled={currentPage === 1}
73
+ sz="xs"
74
+ variant="secondary"
75
+ class="pagination-btn"
76
+ >
77
+ &lt;
78
+ </Button>
79
+
80
+ {#each getVisiblePages() as page (page)}
81
+ <Button
82
+ onClick={() => onPageChange(page)}
83
+ sz="xs"
84
+ variant={currentPage === page ? "primary" : "secondary"}
85
+ aria-current={currentPage === page ? "page" : undefined}
86
+ class="pagination-btn"
87
+ >
88
+ {page}
89
+ </Button>
90
+ {/each}
91
+
92
+ <Button
93
+ onClick={nextPage}
94
+ disabled={currentPage === totalPages}
95
+ sz="xs"
96
+ variant="secondary"
97
+ class="pagination-btn"
98
+ >
99
+ &gt;
100
+ </Button>
101
+ </div>
102
+
103
+ <style>
104
+ @media (max-width: 640px) {
105
+ :global(.pagination-btn) {
106
+ font-size: 10px;
107
+ line-height: 1;
108
+ height: 20px;
109
+ padding: 0 6px;
110
+ }
111
+ }
112
+
113
+ @media (max-width: 480px) {
114
+ :global(.pagination-count) {
115
+ display: none;
116
+ }
117
+ }
118
+ </style>
119
+