@reliability-design/web 1.2.0 → 1.3.1

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 (41) hide show
  1. package/dist/components/rel-accordion/rel-accordion.ts +289 -135
  2. package/dist/components/rel-alert/rel-alert.ts +8 -1
  3. package/dist/components/rel-avatar/README.md +15 -4
  4. package/dist/components/rel-avatar/rel-avatar.ts +138 -96
  5. package/dist/components/rel-badge/README.md +7 -0
  6. package/dist/components/rel-badge/rel-badge.ts +61 -8
  7. package/dist/components/rel-button/rel-button.ts +64 -7
  8. package/dist/components/rel-chip/README.md +7 -0
  9. package/dist/components/rel-chip/rel-chip.ts +250 -130
  10. package/dist/components/rel-date-picker/rel-date-picker.ts +116 -14
  11. package/dist/components/rel-drawer/rel-drawer.ts +5 -2
  12. package/dist/components/rel-menubar/rel-menubar.ts +1 -0
  13. package/dist/components/rel-pagination/rel-pagination.ts +442 -425
  14. package/dist/components/rel-radio/rel-radio.ts +16 -16
  15. package/dist/components/rel-range-picker/rel-range-picker.css +349 -349
  16. package/dist/components/rel-range-picker/rel-range-picker.ts +1080 -1079
  17. package/dist/components/rel-select/README.md +34 -28
  18. package/dist/components/rel-select/rel-select.ts +1015 -346
  19. package/dist/components/rel-sheet/rel-sheet.ts +6 -3
  20. package/dist/components/rel-skeleton/rel-skeleton.ts +139 -0
  21. package/dist/components/rel-tab/rel-tab.ts +10 -4
  22. package/dist/components/rel-textfield/rel-textfield.ts +637 -349
  23. package/dist/components/rel-toast/rel-toast.ts +1 -6
  24. package/dist/components/rel-topbar/rel-topbar-actions.ts +4 -0
  25. package/dist/components/rel-topbar/rel-topbar-brand.ts +4 -0
  26. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +4 -0
  27. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +4 -0
  28. package/dist/components/rel-topbar/rel-topbar-item.ts +4 -0
  29. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +4 -0
  30. package/dist/components/rel-topbar/rel-topbar-profile.ts +3 -0
  31. package/dist/components/rel-topbar/rel-topbar-utilities.ts +4 -0
  32. package/dist/components/rel-topbar/rel-topbar.ts +6 -0
  33. package/dist/index.d.mts +239 -22
  34. package/dist/index.d.ts +239 -22
  35. package/dist/index.js +2609 -981
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +2621 -994
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/styles/vars.css +17 -0
  40. package/dist/{vars-I2PC74XD.css → vars-F54UGCHM.css} +17 -0
  41. package/package.json +1 -1
@@ -1,425 +1,442 @@
1
- import { LitElement, html, css } from "lit";
2
- import { customElement, property, state } from "lit/decorators.js";
3
- import "../../styles/vars-v2.css";
4
-
5
- type DensityType = "comfortable" | "compact";
6
- type VariantType = "default" | "icons";
7
- type PageItem =
8
- | { type: "page"; value: number }
9
- | { type: "ellipsis" }
10
- | { type: "spacer" }
11
- | { type: "prev" }
12
- | { type: "next" };
13
-
14
- @customElement("rel-pagination")
15
- export class RelPagination extends LitElement {
16
- @property({ type: Number }) page?: number;
17
- @property({ type: Number }) defaultPage = 1;
18
- @property({ type: Number }) totalPages = 0;
19
- @property({ type: Number }) siblingCount = 1;
20
- @property({ type: Number }) boundaryCount = 1;
21
- @property({ type: Boolean, reflect: true }) disabled = false;
22
- @property({ type: String }) density: DensityType = "comfortable";
23
- @property({ type: String }) variant: VariantType = "default";
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);
33
- }
34
-
35
- nav {
36
- display: flex;
37
- align-items: center;
38
- gap: var(--rel-spacing-space-2xs);
39
- min-width: max-content;
40
- }
41
-
42
- nav.compact {
43
- gap: var(--rel-spacing-space-3xs);
44
- }
45
-
46
- button,
47
- a {
48
- display: inline-flex;
49
- align-items: center;
50
- justify-content: center;
51
- width: 40px;
52
- height: 40px;
53
- padding: 0;
54
- font-family: var(--rel-fontFamilies-primary);
55
- font-size: var(--rel-fontSizes-sm);
56
- font-weight: var(--rel-fontWeights-medium);
57
- color: var(--rel-color-text-heading);
58
- background: var(--rel-color-bg-primary);
59
- border: var(--rel-borders-width-stroke-100) solid var(--rel-color-border-default);
60
- border-radius: var(--rel-borders-radius-border-radius-200);
61
- cursor: pointer;
62
- transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
63
- text-decoration: none;
64
- outline: none;
65
- user-select: none;
66
- }
67
-
68
- button.prev-btn, button.next-btn,
69
- a.prev-btn, a.next-btn {
70
- width: auto;
71
- padding: 0 var(--rel-spacing-space-sm);
72
- }
73
-
74
- nav.compact button,
75
- nav.compact a {
76
- width: 32px;
77
- height: 32px;
78
- padding: 0;
79
- font-size: var(--rel-fontSizes-xs);
80
- }
81
-
82
- nav.compact button.prev-btn, nav.compact button.next-btn,
83
- nav.compact a.prev-btn, nav.compact a.next-btn {
84
- width: auto;
85
- padding: 0 var(--rel-spacing-space-xs);
86
- }
87
-
88
- button:hover:not(:disabled),
89
- a:hover:not([aria-disabled="true"]) {
90
- background: var(--rel-color-bg-secondary);
91
- border-color: var(--rel-color-text-primary);
92
- }
93
-
94
- button:focus-visible,
95
- a:focus-visible {
96
- outline: 2px solid var(--rel-colors-blue-primary);
97
- outline-offset: 2px;
98
- }
99
-
100
- button:disabled,
101
- a[aria-disabled="true"] {
102
- opacity: 0.5;
103
- cursor: not-allowed;
104
- }
105
-
106
- button[aria-current="page"],
107
- a[aria-current="page"] {
108
- background: var(--rel-color-bg-selected);
109
- color: var(--rel-color-text-primary);
110
- border-color: var(--rel-color-text-heading);
111
- font-weight: var(--rel-fontWeights-semibold);
112
- }
113
-
114
- button[aria-current="page"]:hover,
115
- a[aria-current="page"]:hover {
116
- background: var(--rel-color-bg-selected);
117
- border-color: var(--rel-color-text-heading);
118
- }
119
-
120
- .ellipsis {
121
- display: inline-flex;
122
- align-items: center;
123
- justify-content: center;
124
- width: 40px;
125
- height: 40px;
126
- color: var(--rel-color-text-placeholder);
127
- font-size: var(--rel-fontSizes-sm);
128
- user-select: none;
129
- }
130
-
131
- nav.compact .ellipsis {
132
- width: 32px;
133
- height: 32px;
134
- font-size: var(--rel-fontSizes-xs);
135
- }
136
-
137
- .pagination-spacer {
138
- display: inline-flex;
139
- width: 40px;
140
- height: 40px;
141
- }
142
-
143
- nav.compact .pagination-spacer {
144
- width: 32px;
145
- height: 32px;
146
- }
147
-
148
- .icon {
149
- width: 16px;
150
- height: 16px;
151
- display: inline-block;
152
- }
153
-
154
- nav.compact .icon {
155
- width: 14px;
156
- height: 14px;
157
- }
158
-
159
- :host([disabled]) {
160
- cursor: not-allowed;
161
- }
162
-
163
- :host([disabled]) nav {
164
- opacity: 0.5;
165
- pointer-events: none;
166
- }
167
- `;
168
-
169
- connectedCallback() {
170
- super.connectedCallback();
171
- // Manually parse numeric attributes in case they were set before upgrade
172
- const totalPagesAttr = this.getAttribute('total-pages');
173
- if (totalPagesAttr) {
174
- this.totalPages = Number(totalPagesAttr);
175
- }
176
- const pageAttr = this.getAttribute('page');
177
- if (pageAttr) {
178
- this.page = Number(pageAttr);
179
- }
180
- const defaultPageAttr = this.getAttribute('default-page');
181
- if (defaultPageAttr) {
182
- this.defaultPage = Number(defaultPageAttr);
183
- }
184
- this.currentPage = this.page ?? this.defaultPage;
185
- }
186
-
187
- updated(changedProperties: Map<string, any>) {
188
- // Handle controlled mode: sync currentPage with page prop
189
- if (changedProperties.has("page") && this.page !== undefined) {
190
- this.currentPage = this.page;
191
- }
192
- }
193
-
194
- private get activePage(): number {
195
- return this.page ?? this.currentPage;
196
- }
197
-
198
- private generatePageItems(): PageItem[] {
199
- const items: PageItem[] = [];
200
- const current = this.activePage;
201
- const total = this.totalPages;
202
-
203
- if (total <= 0) return items;
204
-
205
- items.push({ type: "prev" });
206
-
207
- if (total <= 7) {
208
- for (let i = 1; i <= total; i++) {
209
- items.push({ type: "page", value: i });
210
- }
211
- } else if (current <= 4) {
212
- // Near start: [1, 2, 3, 4, 5] [ellipsis] [last]
213
- for (let i = 1; i <= 5; i++) {
214
- items.push({ type: "page", value: i });
215
- }
216
- items.push({ type: "ellipsis" });
217
- items.push({ type: "page", value: total });
218
- } else if (current >= total - 3) {
219
- // Near end: [1] [ellipsis] [last-4, last-3, last-2, last-1, last]
220
- items.push({ type: "page", value: 1 });
221
- items.push({ type: "ellipsis" });
222
- for (let i = total - 4; i <= total; i++) {
223
- items.push({ type: "page", value: i });
224
- }
225
- } else {
226
- // Middle: [1] [ellipsis] [current-1, current, current+1] [ellipsis] [last]
227
- items.push({ type: "page", value: 1 });
228
- items.push({ type: "ellipsis" });
229
- items.push({ type: "page", value: current - 1 });
230
- items.push({ type: "page", value: current });
231
- items.push({ type: "page", value: current + 1 });
232
- items.push({ type: "ellipsis" });
233
- items.push({ type: "page", value: total });
234
- }
235
-
236
- items.push({ type: "next" });
237
-
238
- return items;
239
- }
240
-
241
- private handlePageChange(newPage: number) {
242
- if (this.disabled || newPage < 1 || newPage > this.totalPages || newPage === this.activePage) {
243
- return;
244
- }
245
-
246
- // If uncontrolled, update internal state
247
- if (this.page === undefined) {
248
- this.currentPage = newPage;
249
- }
250
-
251
- // Always emit event
252
- this.dispatchEvent(
253
- new CustomEvent("rel-change", {
254
- detail: { page: newPage },
255
- bubbles: true,
256
- composed: true,
257
- })
258
- );
259
- }
260
-
261
- private getHref(page: number): string {
262
- if (!this.hrefTemplate) return "#";
263
- return this.hrefTemplate.replace(/\{page\}/, String(page));
264
- }
265
-
266
- private handleKeyDown(event: KeyboardEvent, callback: () => void) {
267
- if (event.key === "Enter" || event.key === " ") {
268
- event.preventDefault();
269
- callback();
270
- }
271
- }
272
-
273
- private renderPrevIcon() {
274
- return html`
275
- <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
276
- <path d="M15 18l-6-6 6-6" />
277
- </svg>
278
- `;
279
- }
280
-
281
- private renderNextIcon() {
282
- return html`
283
- <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
284
- <path d="M9 18l6-6-6-6" />
285
- </svg>
286
- `;
287
- }
288
-
289
- private renderItem(item: PageItem) {
290
- const current = this.activePage;
291
- const useLinks = !!this.hrefTemplate;
292
-
293
- if (item.type === "spacer") {
294
- return html`<span class="pagination-spacer"></span>`;
295
- }
296
-
297
- if (item.type === "ellipsis") {
298
- return html`<span class="ellipsis">…</span>`;
299
- }
300
-
301
- if (item.type === "prev") {
302
- const disabled = current === 1 || this.disabled;
303
- const targetPage = current - 1;
304
-
305
- if (useLinks) {
306
- const href = disabled ? "#" : this.getHref(targetPage);
307
- return html`
308
- <a
309
- href="${href}"
310
- class="prev-btn"
311
- aria-label="Go to previous page"
312
- aria-disabled="${disabled}"
313
- @click="${(e: Event) => {
314
- if (disabled) {
315
- e.preventDefault();
316
- return;
317
- }
318
- e.preventDefault();
319
- this.handlePageChange(targetPage);
320
- }}"
321
- >
322
- ${this.variant === "icons" ? this.renderPrevIcon() : "Previous"}
323
- </a>
324
- `;
325
- }
326
-
327
- return html`
328
- <button
329
- type="button"
330
- class="prev-btn"
331
- aria-label="Go to previous page"
332
- ?disabled="${disabled}"
333
- @click="${() => this.handlePageChange(targetPage)}"
334
- >
335
- ${this.variant === "icons" ? this.renderPrevIcon() : "Previous"}
336
- </button>
337
- `;
338
- }
339
-
340
- if (item.type === "next") {
341
- const disabled = current === this.totalPages || this.disabled;
342
- const targetPage = current + 1;
343
-
344
- if (useLinks) {
345
- const href = disabled ? "#" : this.getHref(targetPage);
346
- return html`
347
- <a
348
- href="${href}"
349
- class="next-btn"
350
- aria-label="Go to next page"
351
- aria-disabled="${disabled}"
352
- @click="${(e: Event) => {
353
- if (disabled) {
354
- e.preventDefault();
355
- return;
356
- }
357
- e.preventDefault();
358
- this.handlePageChange(targetPage);
359
- }}"
360
- >
361
- ${this.variant === "icons" ? this.renderNextIcon() : "Next"}
362
- </a>
363
- `;
364
- }
365
-
366
- return html`
367
- <button
368
- type="button"
369
- class="next-btn"
370
- aria-label="Go to next page"
371
- ?disabled="${disabled}"
372
- @click="${() => this.handlePageChange(targetPage)}"
373
- >
374
- ${this.variant === "icons" ? this.renderNextIcon() : "Next"}
375
- </button>
376
- `;
377
- }
378
-
379
- // Page item
380
- const pageNum = item.value;
381
- const isCurrent = pageNum === current;
382
-
383
- if (useLinks) {
384
- const href = this.getHref(pageNum);
385
- return html`
386
- <a
387
- href="${href}"
388
- aria-label="Go to page ${pageNum}"
389
- aria-current="${isCurrent ? "page" : "false"}"
390
- @click="${(e: Event) => {
391
- e.preventDefault();
392
- this.handlePageChange(pageNum);
393
- }}"
394
- >
395
- ${pageNum}
396
- </a>
397
- `;
398
- }
399
-
400
- return html`
401
- <button
402
- type="button"
403
- aria-label="Go to page ${pageNum}"
404
- aria-current="${isCurrent ? "page" : "false"}"
405
- @click="${() => this.handlePageChange(pageNum)}"
406
- >
407
- ${pageNum}
408
- </button>
409
- `;
410
- }
411
-
412
- render() {
413
- const items = this.generatePageItems();
414
-
415
- return html`
416
- <nav
417
- class="${this.density === "compact" ? "compact" : ""}"
418
- aria-label="${this.ariaLabel}"
419
- role="navigation"
420
- >
421
- ${items.map((item) => this.renderItem(item))}
422
- </nav>
423
- `;
424
- }
425
- }
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
+ }