@reliability-design/web 1.1.1 → 1.3.0

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 (69) hide show
  1. package/dist/components/rel-accordion/rel-accordion.ts +289 -150
  2. package/dist/components/rel-alert/rel-alert.ts +34 -34
  3. package/dist/components/rel-avatar/README.md +15 -4
  4. package/dist/components/rel-avatar/rel-avatar.ts +138 -94
  5. package/dist/components/rel-badge/README.md +7 -0
  6. package/dist/components/rel-badge/rel-badge.ts +39 -2
  7. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +6 -6
  8. package/dist/components/rel-button/rel-button.ts +96 -28
  9. package/dist/components/rel-calendar/rel-calendar.ts +15 -8
  10. package/dist/components/rel-card/rel-card.ts +9 -17
  11. package/dist/components/rel-cardmedia/rel-cardmedia.ts +20 -23
  12. package/dist/components/rel-carousel/rel-carousel.ts +596 -468
  13. package/dist/components/rel-checkbox/rel-checkbox.ts +68 -16
  14. package/dist/components/rel-chip/README.md +7 -0
  15. package/dist/components/rel-chip/rel-chip.ts +411 -244
  16. package/dist/components/rel-container/rel-container.ts +2 -2
  17. package/dist/components/rel-date-picker/rel-date-picker.ts +23 -19
  18. package/dist/components/rel-divider/rel-divider.ts +3 -3
  19. package/dist/components/rel-drawer/rel-drawer-footer.ts +0 -2
  20. package/dist/components/rel-drawer/rel-drawer.ts +46 -90
  21. package/dist/components/rel-kbd/rel-kbd.ts +94 -118
  22. package/dist/components/rel-layout/rel-layout.ts +49 -58
  23. package/dist/components/rel-menu/rel-menu.ts +359 -359
  24. package/dist/components/rel-menubar/rel-menubar-menu.css +3 -0
  25. package/dist/components/rel-menubar/rel-menubar-menu.ts +2 -1
  26. package/dist/components/rel-menubar/rel-menubar.ts +603 -605
  27. package/dist/components/rel-otp/rel-otp.ts +22 -25
  28. package/dist/components/rel-pagination/rel-pagination.ts +442 -381
  29. package/dist/components/rel-popover/rel-popover-content.ts +6 -5
  30. package/dist/components/rel-popover/rel-popover.ts +26 -7
  31. package/dist/components/rel-progress/rel-progress.ts +10 -9
  32. package/dist/components/rel-radio/rel-radio.ts +228 -172
  33. package/dist/components/rel-range-picker/rel-range-picker.css +349 -0
  34. package/dist/components/rel-range-picker/rel-range-picker.ts +1080 -0
  35. package/dist/components/rel-resizable/rel-resizable-handle.ts +8 -7
  36. package/dist/components/rel-segmented-button/rel-segmented-button.ts +41 -20
  37. package/dist/components/rel-select/README.md +34 -28
  38. package/dist/components/rel-select/rel-select.ts +1015 -275
  39. package/dist/components/rel-sheet/rel-sheet.ts +365 -684
  40. package/dist/components/rel-skeleton/rel-skeleton.ts +139 -0
  41. package/dist/components/rel-slider/rel-slider.ts +49 -27
  42. package/dist/components/rel-snackbar/rel-snackbar.ts +28 -28
  43. package/dist/components/rel-stepper/rel-stepper.ts +11 -10
  44. package/dist/components/rel-switch/rel-switch.ts +7 -6
  45. package/dist/components/rel-tab/rel-tab.ts +15 -29
  46. package/dist/components/rel-textfield/rel-textfield.ts +192 -35
  47. package/dist/components/rel-toast/rel-toast.ts +21 -13
  48. package/dist/components/rel-tooltip/rel-tooltip.ts +123 -123
  49. package/dist/components/rel-topbar/rel-topbar-actions.ts +1 -0
  50. package/dist/components/rel-topbar/rel-topbar-brand.ts +1 -0
  51. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +32 -2
  52. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +1 -1
  53. package/dist/components/rel-topbar/rel-topbar-item.ts +15 -10
  54. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +1 -0
  55. package/dist/components/rel-topbar/rel-topbar-profile.ts +30 -56
  56. package/dist/components/rel-topbar/rel-topbar-utilities.ts +1 -0
  57. package/dist/components/rel-topbar/rel-topbar.ts +137 -28
  58. package/dist/index.d.mts +310 -193
  59. package/dist/index.d.ts +310 -193
  60. package/dist/index.js +5562 -3526
  61. package/dist/index.js.map +1 -1
  62. package/dist/index.mjs +5328 -3284
  63. package/dist/index.mjs.map +1 -1
  64. package/dist/rel-menubar-menu-BEEQDAXW.css +3 -0
  65. package/dist/styles/vars-v2.css +525 -0
  66. package/dist/styles/vars.css +346 -346
  67. package/dist/{vars-TBB4BMSD.css → vars-I2PC74XD.css} +346 -346
  68. package/dist/vars-v2-VMGWTFJZ.css +525 -0
  69. package/package.json +2 -1
@@ -1,381 +1,442 @@
1
- import { LitElement, html, css } from "lit";
2
- import { customElement, property, state } from "lit/decorators.js";
3
-
4
- type DensityType = "comfortable" | "compact";
5
- type VariantType = "default" | "icons";
6
- type PageItem = { type: "page"; value: number } | { type: "ellipsis" } | { type: "prev" } | { type: "next" };
7
-
8
- @customElement("rel-pagination")
9
- export class RelPagination extends LitElement {
10
- @property({ type: Number }) page?: number;
11
- @property({ type: Number }) defaultPage = 1;
12
- @property({ type: Number }) totalPages = 0;
13
- @property({ type: Number }) siblingCount = 1;
14
- @property({ type: Number }) boundaryCount = 1;
15
- @property({ type: Boolean, reflect: true }) disabled = false;
16
- @property({ type: String }) density: DensityType = "comfortable";
17
- @property({ type: String }) variant: VariantType = "default";
18
- @property({ type: String }) hrefTemplate = "";
19
- @property({ type: String }) ariaLabel = "Pagination";
20
-
21
- @state() private currentPage = 1;
22
-
23
- static styles = css`
24
- :host {
25
- display: inline-block;
26
- font-family: var(--rel-fontFamilies-primary);
27
- }
28
-
29
- nav {
30
- display: flex;
31
- align-items: center;
32
- gap: var(--rel-spacing-space-2xs);
33
- }
34
-
35
- nav.compact {
36
- gap: var(--rel-spacing-space-3xs);
37
- }
38
-
39
- button,
40
- a {
41
- display: inline-flex;
42
- align-items: center;
43
- justify-content: center;
44
- min-width: 40px;
45
- height: 40px;
46
- padding: 0 var(--rel-spacing-space-sm);
47
- font-family: var(--rel-fontFamilies-primary);
48
- font-size: var(--rel-fontSizes-sm);
49
- font-weight: var(--rel-fontWeights-medium);
50
- color: var(--rel-colors-gray-heading);
51
- background: var(--rel-colors-white);
52
- border: var(--rel-borders-width-stroke-100) solid var(--rel-colors-gray-border);
53
- border-radius: var(--rel-borders-radius-border-radius-200);
54
- cursor: pointer;
55
- transition: all 0.15s ease;
56
- text-decoration: none;
57
- outline: none;
58
- user-select: none;
59
- }
60
-
61
- nav.compact button,
62
- nav.compact a {
63
- min-width: 32px;
64
- height: 32px;
65
- padding: 0 var(--rel-spacing-space-xs);
66
- font-size: var(--rel-fontSizes-xs);
67
- }
68
-
69
- button:hover:not(:disabled),
70
- a:hover:not([aria-disabled="true"]) {
71
- background: var(--rel-surface-secondary, #fafafa);
72
- border-color: var(--rel-colors-gray-primary, #333);
73
- }
74
-
75
- button:focus-visible,
76
- a:focus-visible {
77
- outline: 2px solid var(--rel-colors-blue-primary);
78
- outline-offset: 2px;
79
- }
80
-
81
- button:disabled,
82
- a[aria-disabled="true"] {
83
- opacity: 0.5;
84
- cursor: not-allowed;
85
- pointer-events: none;
86
- }
87
-
88
- button[aria-current="page"],
89
- a[aria-current="page"] {
90
- background: var(--rel-color-bg-selected);
91
- color: var(--rel-color-text-primary);
92
- border-color: var(--rel-colors-gray-heading);
93
- font-weight: var(--rel-fontWeights-semibold);
94
- }
95
-
96
- button[aria-current="page"]:hover,
97
- a[aria-current="page"]:hover {
98
- background: var(--rel-colors-gray-primary);
99
- border-color: var(--rel-colors-gray-primary);
100
- }
101
-
102
- .ellipsis {
103
- display: inline-flex;
104
- align-items: center;
105
- justify-content: center;
106
- min-width: 40px;
107
- height: 40px;
108
- color: var(--rel-colors-gray-placeholder);
109
- font-size: var(--rel-fontSizes-sm);
110
- user-select: none;
111
- }
112
-
113
- nav.compact .ellipsis {
114
- min-width: 32px;
115
- height: 32px;
116
- font-size: var(--rel-fontSizes-xs);
117
- }
118
-
119
- .icon {
120
- width: 16px;
121
- height: 16px;
122
- display: inline-block;
123
- }
124
-
125
- nav.compact .icon {
126
- width: 14px;
127
- height: 14px;
128
- }
129
-
130
- :host([disabled]) nav {
131
- opacity: 0.5;
132
- pointer-events: none;
133
- }
134
- `;
135
-
136
- connectedCallback() {
137
- super.connectedCallback();
138
- this.currentPage = this.page ?? this.defaultPage;
139
- }
140
-
141
- updated(changedProperties: Map<string, any>) {
142
- // Handle controlled mode: sync currentPage with page prop
143
- if (changedProperties.has("page") && this.page !== undefined) {
144
- this.currentPage = this.page;
145
- }
146
- }
147
-
148
- private get activePage(): number {
149
- return this.page ?? this.currentPage;
150
- }
151
-
152
- private generatePageItems(): PageItem[] {
153
- const items: PageItem[] = [];
154
- const current = this.activePage;
155
- const total = this.totalPages;
156
- const sibling = this.siblingCount;
157
- const boundary = this.boundaryCount;
158
-
159
- items.push({ type: "prev" });
160
-
161
- // Calculate page ranges
162
- const leftBoundary = Array.from({ length: Math.min(boundary, total) }, (_, i) => i + 1);
163
- const rightBoundary = Array.from(
164
- { length: Math.min(boundary, total) },
165
- (_, i) => total - boundary + i + 1
166
- ).filter((p) => p > 0);
167
-
168
- const leftSiblings = Array.from(
169
- { length: sibling },
170
- (_, i) => current - sibling + i
171
- ).filter((p) => p > boundary && p < current);
172
-
173
- const rightSiblings = Array.from(
174
- { length: sibling },
175
- (_, i) => current + i + 1
176
- ).filter((p) => p > current && p <= total - boundary);
177
-
178
- // Combine all visible pages
179
- const visiblePages = new Set([
180
- ...leftBoundary,
181
- ...leftSiblings,
182
- current,
183
- ...rightSiblings,
184
- ...rightBoundary,
185
- ]);
186
-
187
- const sortedPages = Array.from(visiblePages).sort((a, b) => a - b);
188
-
189
- // Build items with ellipsis
190
- sortedPages.forEach((page, index) => {
191
- if (index > 0) {
192
- const prevPage = sortedPages[index - 1];
193
- if (page - prevPage > 1) {
194
- items.push({ type: "ellipsis" });
195
- }
196
- }
197
- items.push({ type: "page", value: page });
198
- });
199
-
200
- items.push({ type: "next" });
201
-
202
- return items;
203
- }
204
-
205
- private handlePageChange(newPage: number) {
206
- if (this.disabled || newPage < 1 || newPage > this.totalPages || newPage === this.activePage) {
207
- return;
208
- }
209
-
210
- // If uncontrolled, update internal state
211
- if (this.page === undefined) {
212
- this.currentPage = newPage;
213
- }
214
-
215
- // Always emit event
216
- this.dispatchEvent(
217
- new CustomEvent("rel-change", {
218
- detail: { page: newPage },
219
- bubbles: true,
220
- composed: true,
221
- })
222
- );
223
- }
224
-
225
- private getHref(page: number): string {
226
- if (!this.hrefTemplate) return "#";
227
- return this.hrefTemplate.replace(/\{page\}/, String(page));
228
- }
229
-
230
- private handleKeyDown(event: KeyboardEvent, callback: () => void) {
231
- if (event.key === "Enter" || event.key === " ") {
232
- event.preventDefault();
233
- callback();
234
- }
235
- }
236
-
237
- private renderPrevIcon() {
238
- return html`
239
- <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
240
- <path d="M15 18l-6-6 6-6" />
241
- </svg>
242
- `;
243
- }
244
-
245
- private renderNextIcon() {
246
- return html`
247
- <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
248
- <path d="M9 18l6-6-6-6" />
249
- </svg>
250
- `;
251
- }
252
-
253
- private renderItem(item: PageItem) {
254
- const current = this.activePage;
255
- const useLinks = !!this.hrefTemplate;
256
-
257
- if (item.type === "ellipsis") {
258
- return html`<span class="ellipsis">…</span>`;
259
- }
260
-
261
- if (item.type === "prev") {
262
- const disabled = current === 1 || this.disabled;
263
- const targetPage = current - 1;
264
-
265
- if (useLinks) {
266
- const href = disabled ? "#" : this.getHref(targetPage);
267
- return html`
268
- <a
269
- href="${href}"
270
- aria-label="Go to previous page"
271
- aria-disabled="${disabled}"
272
- @click="${(e: Event) => {
273
- if (disabled) {
274
- e.preventDefault();
275
- return;
276
- }
277
- e.preventDefault();
278
- this.handlePageChange(targetPage);
279
- }}"
280
- >
281
- ${this.variant === "icons" ? this.renderPrevIcon() : "Previous"}
282
- </a>
283
- `;
284
- }
285
-
286
- return html`
287
- <button
288
- type="button"
289
- aria-label="Go to previous page"
290
- ?disabled="${disabled}"
291
- @click="${() => this.handlePageChange(targetPage)}"
292
- >
293
- ${this.variant === "icons" ? this.renderPrevIcon() : "Previous"}
294
- </button>
295
- `;
296
- }
297
-
298
- if (item.type === "next") {
299
- const disabled = current === this.totalPages || this.disabled;
300
- const targetPage = current + 1;
301
-
302
- if (useLinks) {
303
- const href = disabled ? "#" : this.getHref(targetPage);
304
- return html`
305
- <a
306
- href="${href}"
307
- aria-label="Go to next page"
308
- aria-disabled="${disabled}"
309
- @click="${(e: Event) => {
310
- if (disabled) {
311
- e.preventDefault();
312
- return;
313
- }
314
- e.preventDefault();
315
- this.handlePageChange(targetPage);
316
- }}"
317
- >
318
- ${this.variant === "icons" ? this.renderNextIcon() : "Next"}
319
- </a>
320
- `;
321
- }
322
-
323
- return html`
324
- <button
325
- type="button"
326
- aria-label="Go to next page"
327
- ?disabled="${disabled}"
328
- @click="${() => this.handlePageChange(targetPage)}"
329
- >
330
- ${this.variant === "icons" ? this.renderNextIcon() : "Next"}
331
- </button>
332
- `;
333
- }
334
-
335
- // Page item
336
- const pageNum = item.value;
337
- const isCurrent = pageNum === current;
338
-
339
- if (useLinks) {
340
- const href = this.getHref(pageNum);
341
- return html`
342
- <a
343
- href="${href}"
344
- aria-label="Go to page ${pageNum}"
345
- aria-current="${isCurrent ? "page" : "false"}"
346
- @click="${(e: Event) => {
347
- e.preventDefault();
348
- this.handlePageChange(pageNum);
349
- }}"
350
- >
351
- ${pageNum}
352
- </a>
353
- `;
354
- }
355
-
356
- return html`
357
- <button
358
- type="button"
359
- aria-label="Go to page ${pageNum}"
360
- aria-current="${isCurrent ? "page" : "false"}"
361
- @click="${() => this.handlePageChange(pageNum)}"
362
- >
363
- ${pageNum}
364
- </button>
365
- `;
366
- }
367
-
368
- render() {
369
- const items = this.generatePageItems();
370
-
371
- return html`
372
- <nav
373
- class="${this.density === "compact" ? "compact" : ""}"
374
- aria-label="${this.ariaLabel}"
375
- role="navigation"
376
- >
377
- ${items.map((item) => this.renderItem(item))}
378
- </nav>
379
- `;
380
- }
381
- }
1
+ import { LitElement, html, css } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import "../../styles/vars-v2.css";
4
+
5
+ @customElement("rel-pagination")
6
+ export class RelPagination extends LitElement {
7
+ /** Layout type from Figma: extended (numbered pages), compact (prev/next + page text), count-text (range + nav) */
8
+ @property({ type: String, reflect: true })
9
+ type: "extended" | "compact" | "count-text" = "extended";
10
+
11
+ /** Controlled current page */
12
+ @property({ type: Number }) page?: number;
13
+ /** Uncontrolled starting page */
14
+ @property({ type: Number }) defaultPage = 1;
15
+ /** Total number of pages */
16
+ @property({ type: Number }) totalPages = 0;
17
+ /** Total items (used with perPage to auto-compute totalPages in count-text mode) */
18
+ @property({ type: Number }) totalItems = 0;
19
+ /** Items per page (count-text mode) */
20
+ @property({ type: Number }) perPage = 10;
21
+ /** Show "Viewing page X of Y" label below extended pagination */
22
+ @property({ type: Boolean }) showLabel = false;
23
+ @property({ type: Boolean, reflect: true }) disabled = false;
24
+ @property({ type: String }) hrefTemplate = "";
25
+ @property({ type: String }) ariaLabel = "Pagination";
26
+
27
+ @state() private currentPage = 1;
28
+
29
+ static styles = css`
30
+ :host {
31
+ display: inline-block;
32
+ font-family: var(--rel-fontFamilies-primary, 'Poppins', sans-serif);
33
+ }
34
+
35
+ :host([type="count-text"]) {
36
+ display: block;
37
+ width: 100%;
38
+ }
39
+
40
+ :host([disabled]) {
41
+ opacity: 0.5;
42
+ cursor: not-allowed;
43
+ }
44
+
45
+ :host([disabled]) button {
46
+ pointer-events: none;
47
+ }
48
+
49
+ /* ── Root / Stack containers ──────────────────────────────── */
50
+
51
+ .pag-root {
52
+ display: flex;
53
+ flex-direction: column;
54
+ align-items: center;
55
+ gap: 16px;
56
+ }
57
+
58
+ .pag-stack {
59
+ display: inline-flex;
60
+ align-items: center;
61
+ gap: 12px;
62
+ }
63
+
64
+ .pag-count {
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: space-between;
68
+ gap: 16px;
69
+ width: 100%;
70
+ }
71
+
72
+ /* count-text right-side nav: no gap (40+24+40 = 104px, matches Figma) */
73
+ .pag-count-nav {
74
+ display: inline-flex;
75
+ align-items: center;
76
+ }
77
+
78
+ /* ── Page number list (tighter 4px gap) ──────────────────── */
79
+
80
+ .pag-numbers {
81
+ display: inline-flex;
82
+ align-items: center;
83
+ gap: 4px;
84
+ }
85
+
86
+ /* ── Prev / Next — circular buttons ──────────────────────── */
87
+
88
+ .pag-nav-btn {
89
+ display: inline-flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ width: 40px;
93
+ height: 40px;
94
+ padding: 0;
95
+ border-radius: 1000px;
96
+ border: 1px solid #e5e5e5;
97
+ background: #ffffff;
98
+ cursor: pointer;
99
+ color: #0b0b0b;
100
+ box-sizing: border-box;
101
+ transition: background 0.15s ease;
102
+ flex-shrink: 0;
103
+ }
104
+
105
+ .pag-nav-btn:hover:not(:disabled) {
106
+ background: #f5f5f5;
107
+ }
108
+
109
+ .pag-nav-btn:focus-visible {
110
+ outline: 2px solid var(--rel-color-border-focus, #0059f4);
111
+ outline-offset: 2px;
112
+ }
113
+
114
+ .pag-nav-btn:disabled {
115
+ opacity: 0.35;
116
+ cursor: not-allowed;
117
+ }
118
+
119
+ .pag-nav-btn svg {
120
+ width: 20px;
121
+ height: 20px;
122
+ }
123
+
124
+ /* ── Page number buttons — square with 8px radius ─────────── */
125
+
126
+ .pag-page-btn {
127
+ display: inline-flex;
128
+ align-items: center;
129
+ justify-content: center;
130
+ width: 40px;
131
+ height: 40px;
132
+ padding: 8px;
133
+ border-radius: 8px;
134
+ border: 1px solid #e5e5e5;
135
+ background: #ffffff;
136
+ cursor: pointer;
137
+ font-family: inherit;
138
+ font-size: 14px;
139
+ font-weight: 500;
140
+ letter-spacing: -0.3px;
141
+ color: #0b0b0b;
142
+ line-height: 22px;
143
+ box-sizing: border-box;
144
+ transition: background 0.15s ease, border-color 0.15s ease;
145
+ }
146
+
147
+ .pag-page-btn:hover {
148
+ background: #f5f5f5;
149
+ }
150
+
151
+ .pag-page-btn:focus-visible {
152
+ outline: 2px solid var(--rel-color-border-focus, #0059f4);
153
+ outline-offset: 2px;
154
+ }
155
+
156
+ .pag-page-btn[aria-current="page"] {
157
+ background: #e5e5e5;
158
+ border-color: #0b0b0b;
159
+ }
160
+
161
+ /* ── Ellipsis ─────────────────────────────────────────────── */
162
+
163
+ .pag-ellipsis {
164
+ display: inline-flex;
165
+ align-items: center;
166
+ justify-content: center;
167
+ width: 40px;
168
+ height: 40px;
169
+ font-size: 14px;
170
+ font-weight: 500;
171
+ letter-spacing: -0.3px;
172
+ color: #0b0b0b;
173
+ user-select: none;
174
+ }
175
+
176
+ /* ── Compact: "Page X of Y" ───────────────────────────────── */
177
+
178
+ .pag-page-text {
179
+ font-size: 14px;
180
+ font-weight: 500;
181
+ letter-spacing: -0.3px;
182
+ color: #0b0b0b;
183
+ line-height: 22px;
184
+ white-space: nowrap;
185
+ }
186
+
187
+ /* ── Count-text: "1-5 of 50" ─────────────────────────────── */
188
+
189
+ .pag-count-text {
190
+ font-size: 14px;
191
+ font-weight: 500;
192
+ letter-spacing: -0.3px;
193
+ color: #0b0b0b;
194
+ line-height: 22px;
195
+ white-space: nowrap;
196
+ }
197
+
198
+ /* ── Count-text: inline current page number ───────────────── */
199
+
200
+ .pag-current-num {
201
+ display: inline-flex;
202
+ align-items: center;
203
+ justify-content: center;
204
+ width: 24px;
205
+ height: 40px;
206
+ font-size: 14px;
207
+ font-weight: 500;
208
+ letter-spacing: -0.3px;
209
+ color: #0b0b0b;
210
+ line-height: 22px;
211
+ }
212
+
213
+ /* ── Extended sub-label ───────────────────────────────────── */
214
+
215
+ .pag-label {
216
+ font-size: 14px;
217
+ font-weight: 400;
218
+ letter-spacing: -0.3px;
219
+ color: #7d7d7d;
220
+ line-height: 22px;
221
+ text-align: center;
222
+ }
223
+
224
+ /* ── Dark mode ───────────────────────────────────────────── */
225
+
226
+ :host-context([data-theme="dark"]) .pag-nav-btn,
227
+ [data-theme="dark"] .pag-nav-btn {
228
+ background: #1a1a1a;
229
+ border-color: #333333;
230
+ color: #f5f5f5;
231
+ }
232
+
233
+ :host-context([data-theme="dark"]) .pag-nav-btn:hover:not(:disabled),
234
+ [data-theme="dark"] .pag-nav-btn:hover:not(:disabled) {
235
+ background: #2b2b2b;
236
+ }
237
+
238
+ :host-context([data-theme="dark"]) .pag-page-btn,
239
+ [data-theme="dark"] .pag-page-btn {
240
+ background: #1a1a1a;
241
+ border-color: #333333;
242
+ color: #f5f5f5;
243
+ }
244
+
245
+ :host-context([data-theme="dark"]) .pag-page-btn:hover,
246
+ [data-theme="dark"] .pag-page-btn:hover {
247
+ background: #2b2b2b;
248
+ }
249
+
250
+ :host-context([data-theme="dark"]) .pag-page-btn[aria-current="page"],
251
+ [data-theme="dark"] .pag-page-btn[aria-current="page"] {
252
+ background: #333333;
253
+ border-color: #f5f5f5;
254
+ }
255
+
256
+ :host-context([data-theme="dark"]) .pag-ellipsis,
257
+ :host-context([data-theme="dark"]) .pag-page-text,
258
+ :host-context([data-theme="dark"]) .pag-count-text,
259
+ :host-context([data-theme="dark"]) .pag-current-num,
260
+ [data-theme="dark"] .pag-ellipsis,
261
+ [data-theme="dark"] .pag-page-text,
262
+ [data-theme="dark"] .pag-count-text,
263
+ [data-theme="dark"] .pag-current-num {
264
+ color: #f5f5f5;
265
+ }
266
+
267
+ :host-context([data-theme="dark"]) .pag-label,
268
+ [data-theme="dark"] .pag-label {
269
+ color: #a3a3a3;
270
+ }
271
+ `;
272
+
273
+ connectedCallback() {
274
+ super.connectedCallback();
275
+ const totalPagesAttr = this.getAttribute("total-pages");
276
+ if (totalPagesAttr) this.totalPages = Number(totalPagesAttr);
277
+ const pageAttr = this.getAttribute("page");
278
+ if (pageAttr) this.page = Number(pageAttr);
279
+ const defaultPageAttr = this.getAttribute("default-page");
280
+ if (defaultPageAttr) this.defaultPage = Number(defaultPageAttr);
281
+ const totalItemsAttr = this.getAttribute("total-items");
282
+ if (totalItemsAttr) this.totalItems = Number(totalItemsAttr);
283
+ const perPageAttr = this.getAttribute("per-page");
284
+ if (perPageAttr) this.perPage = Number(perPageAttr);
285
+ this.currentPage = this.page ?? this.defaultPage;
286
+ }
287
+
288
+ updated(changedProperties: Map<string, unknown>) {
289
+ if (changedProperties.has("page") && this.page !== undefined) {
290
+ this.currentPage = this.page;
291
+ }
292
+ }
293
+
294
+ private get activePage(): number {
295
+ return this.page ?? this.currentPage;
296
+ }
297
+
298
+ private get computedTotalPages(): number {
299
+ if (this.totalPages > 0) return this.totalPages;
300
+ if (this.totalItems > 0 && this.perPage > 0) {
301
+ return Math.ceil(this.totalItems / this.perPage);
302
+ }
303
+ return 0;
304
+ }
305
+
306
+ private generatePageNumbers(): number[] {
307
+ const pages: number[] = [];
308
+ const current = this.activePage;
309
+ const total = this.computedTotalPages;
310
+ if (total <= 0) return pages;
311
+ if (total <= 7) {
312
+ for (let i = 1; i <= total; i++) pages.push(i);
313
+ } else if (current <= 4) {
314
+ for (let i = 1; i <= 5; i++) pages.push(i);
315
+ pages.push(-1);
316
+ pages.push(total);
317
+ } else if (current >= total - 3) {
318
+ pages.push(1);
319
+ pages.push(-1);
320
+ for (let i = total - 4; i <= total; i++) pages.push(i);
321
+ } else {
322
+ pages.push(1);
323
+ pages.push(-1);
324
+ pages.push(current - 1);
325
+ pages.push(current);
326
+ pages.push(current + 1);
327
+ pages.push(-1);
328
+ pages.push(total);
329
+ }
330
+ return pages;
331
+ }
332
+
333
+ private handlePageChange(newPage: number) {
334
+ const total = this.computedTotalPages;
335
+ if (this.disabled || newPage < 1 || newPage > total || newPage === this.activePage) return;
336
+ if (this.page === undefined) this.currentPage = newPage;
337
+ this.dispatchEvent(
338
+ new CustomEvent("rel-change", {
339
+ detail: { page: newPage },
340
+ bubbles: true,
341
+ composed: true,
342
+ })
343
+ );
344
+ }
345
+
346
+ private renderChevronLeft() {
347
+ return html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>`;
348
+ }
349
+
350
+ private renderChevronRight() {
351
+ return html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>`;
352
+ }
353
+
354
+ private renderNavBtn(dir: "prev" | "next") {
355
+ const current = this.activePage;
356
+ const total = this.computedTotalPages;
357
+ const isPrev = dir === "prev";
358
+ const isDisabled = isPrev ? current <= 1 : current >= total;
359
+ const targetPage = isPrev ? current - 1 : current + 1;
360
+
361
+ return html`
362
+ <button
363
+ type="button"
364
+ class="pag-nav-btn"
365
+ aria-label="${isPrev ? "Previous page" : "Next page"}"
366
+ ?disabled="${isDisabled}"
367
+ @click="${() => this.handlePageChange(targetPage)}"
368
+ >
369
+ ${isPrev ? this.renderChevronLeft() : this.renderChevronRight()}
370
+ </button>
371
+ `;
372
+ }
373
+
374
+ private renderExtended() {
375
+ const pages = this.generatePageNumbers();
376
+ const current = this.activePage;
377
+ const total = this.computedTotalPages;
378
+
379
+ return html`
380
+ <div class="pag-root" role="navigation" aria-label="${this.ariaLabel}">
381
+ <div class="pag-stack">
382
+ ${this.renderNavBtn("prev")}
383
+ <div class="pag-numbers">
384
+ ${pages.map((p) =>
385
+ p === -1
386
+ ? html`<span class="pag-ellipsis">...</span>`
387
+ : html`<button
388
+ type="button"
389
+ class="pag-page-btn"
390
+ aria-label="Page ${p}"
391
+ aria-current="${p === current ? "page" : "false"}"
392
+ @click="${() => this.handlePageChange(p)}"
393
+ >${p}</button>`
394
+ )}
395
+ </div>
396
+ ${this.renderNavBtn("next")}
397
+ </div>
398
+ ${this.showLabel
399
+ ? html`<span class="pag-label">Viewing page ${current} of ${total}</span>`
400
+ : null}
401
+ </div>
402
+ `;
403
+ }
404
+
405
+ private renderCompact() {
406
+ const current = this.activePage;
407
+ const total = this.computedTotalPages;
408
+
409
+ return html`
410
+ <div class="pag-stack" role="navigation" aria-label="${this.ariaLabel}">
411
+ ${this.renderNavBtn("prev")}
412
+ <span class="pag-page-text">Page ${current} of ${total}</span>
413
+ ${this.renderNavBtn("next")}
414
+ </div>
415
+ `;
416
+ }
417
+
418
+ private renderCountText() {
419
+ const current = this.activePage;
420
+ const total = this.computedTotalPages;
421
+ const effectiveTotalItems = this.totalItems > 0 ? this.totalItems : total * this.perPage;
422
+ const start = (current - 1) * this.perPage + 1;
423
+ const end = Math.min(current * this.perPage, effectiveTotalItems);
424
+
425
+ return html`
426
+ <div class="pag-count" role="navigation" aria-label="${this.ariaLabel}">
427
+ <span class="pag-count-text">${start}–${end} of ${effectiveTotalItems}</span>
428
+ <div class="pag-count-nav">
429
+ ${this.renderNavBtn("prev")}
430
+ <span class="pag-current-num">${current}</span>
431
+ ${this.renderNavBtn("next")}
432
+ </div>
433
+ </div>
434
+ `;
435
+ }
436
+
437
+ render() {
438
+ if (this.type === "compact") return this.renderCompact();
439
+ if (this.type === "count-text") return this.renderCountText();
440
+ return this.renderExtended();
441
+ }
442
+ }