@reliability-design/web 1.0.6 → 1.2.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 (78) hide show
  1. package/dist/components/rel-accordion/rel-accordion.ts +20 -31
  2. package/dist/components/rel-alert/rel-alert.ts +81 -11
  3. package/dist/components/rel-avatar/rel-avatar.ts +6 -3
  4. package/dist/components/rel-badge/rel-badge.ts +8 -3
  5. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +6 -6
  6. package/dist/components/rel-button/rel-button.ts +118 -16
  7. package/dist/components/rel-calendar/rel-calendar.ts +16 -9
  8. package/dist/components/rel-card/rel-card.ts +9 -17
  9. package/dist/components/rel-cardmedia/rel-cardmedia.ts +20 -23
  10. package/dist/components/rel-carousel/rel-carousel.ts +596 -469
  11. package/dist/components/rel-checkbox/rel-checkbox.ts +98 -15
  12. package/dist/components/rel-chip/rel-chip.ts +291 -186
  13. package/dist/components/rel-container/rel-container.ts +2 -2
  14. package/dist/components/rel-date-picker/rel-date-picker.ts +23 -19
  15. package/dist/components/rel-divider/rel-divider.ts +12 -3
  16. package/dist/components/rel-drawer/rel-drawer-footer.ts +27 -0
  17. package/dist/components/rel-drawer/rel-drawer.ts +57 -113
  18. package/dist/components/rel-kbd/rel-kbd.ts +94 -118
  19. package/dist/components/rel-layout/rel-layout.ts +49 -58
  20. package/dist/components/rel-menu/rel-menu.ts +359 -327
  21. package/dist/components/rel-menubar/rel-menubar-menu.css +3 -0
  22. package/dist/components/rel-menubar/rel-menubar-menu.ts +2 -1
  23. package/dist/components/rel-menubar/rel-menubar.ts +602 -576
  24. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +240 -143
  25. package/dist/components/rel-nav-menu/rel-nav-menu.ts +227 -331
  26. package/dist/components/rel-navbar/rel-navbar.ts +13 -0
  27. package/dist/components/rel-otp/rel-otp.ts +22 -25
  28. package/dist/components/rel-pagination/rel-pagination.ts +112 -63
  29. package/dist/components/rel-popover/rel-popover-content.ts +6 -5
  30. package/dist/components/rel-popover/rel-popover.ts +42 -20
  31. package/dist/components/rel-progress/rel-progress.ts +10 -9
  32. package/dist/components/rel-radio/rel-radio.ts +228 -155
  33. package/dist/components/rel-range-picker/rel-range-picker.css +349 -0
  34. package/dist/components/rel-range-picker/rel-range-picker.ts +1079 -0
  35. package/dist/components/rel-resizable/rel-resizable-handle.ts +12 -15
  36. package/dist/components/rel-segmented-button/rel-segmented-button.ts +41 -20
  37. package/dist/components/rel-select/rel-select.ts +112 -27
  38. package/dist/components/rel-sheet/rel-sheet.ts +362 -683
  39. package/dist/components/rel-sidebar/rel-sidebar.ts +13 -5
  40. package/dist/components/rel-slider/rel-slider.ts +74 -29
  41. package/dist/components/rel-snackbar/rel-snackbar.ts +67 -37
  42. package/dist/components/rel-stepper/rel-stepper.ts +43 -31
  43. package/dist/components/rel-switch/rel-switch.ts +26 -12
  44. package/dist/components/rel-tab/rel-tab.ts +57 -37
  45. package/dist/components/rel-table/rel-table-body.ts +5 -0
  46. package/dist/components/rel-table/rel-table-caption.ts +6 -1
  47. package/dist/components/rel-table/rel-table-cell.ts +42 -49
  48. package/dist/components/rel-table/rel-table-footer.ts +5 -0
  49. package/dist/components/rel-table/rel-table-head.ts +13 -1
  50. package/dist/components/rel-table/rel-table-header.ts +5 -0
  51. package/dist/components/rel-table/rel-table-row.ts +7 -3
  52. package/dist/components/rel-table/rel-table.ts +8 -2
  53. package/dist/components/rel-textfield/rel-textfield.ts +153 -44
  54. package/dist/components/rel-toast/rel-toast.ts +56 -22
  55. package/dist/components/rel-tooltip/rel-tooltip.ts +52 -9
  56. package/dist/components/rel-topbar/rel-topbar-actions.ts +24 -0
  57. package/dist/components/rel-topbar/rel-topbar-brand.ts +47 -0
  58. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +63 -0
  59. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +28 -0
  60. package/dist/components/rel-topbar/rel-topbar-item.ts +320 -0
  61. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +24 -0
  62. package/dist/components/rel-topbar/rel-topbar-profile.ts +222 -0
  63. package/dist/components/rel-topbar/rel-topbar-utilities.ts +24 -0
  64. package/dist/components/rel-topbar/rel-topbar.ts +438 -0
  65. package/dist/index.d.mts +375 -411
  66. package/dist/index.d.ts +375 -411
  67. package/dist/index.js +7587 -7980
  68. package/dist/index.js.map +1 -1
  69. package/dist/index.mjs +7647 -8023
  70. package/dist/index.mjs.map +1 -1
  71. package/dist/rel-menubar-menu-BEEQDAXW.css +3 -0
  72. package/dist/styles/vars-v2.css +525 -0
  73. package/dist/styles/vars.css +79 -27
  74. package/dist/{vars-ITL2K3KO.css → vars-I2PC74XD.css} +79 -27
  75. package/dist/vars-v2-VMGWTFJZ.css +525 -0
  76. package/package.json +3 -1
  77. package/dist/components/rel-quote-page/README.md +0 -166
  78. package/dist/components/rel-quote-page/rel-quote-page.ts +0 -816
@@ -1,469 +1,596 @@
1
- import { LitElement, html, css, nothing } from "lit";
2
- import { customElement, property, state, query } from "lit/decorators.js";
3
-
4
- @customElement("rel-carousel")
5
- export class RelCarousel extends LitElement {
6
- @property({ type: Number, reflect: true }) value = 0;
7
- @property({ type: Boolean }) loop = false;
8
- @property({ type: Boolean }) autoplay = false;
9
- @property({ type: Number }) interval = 5000;
10
- @property({ type: Boolean, attribute: "pause-on-hover" }) pauseOnHover = true;
11
- @property({ type: Boolean, attribute: "show-buttons" }) showButtons = true;
12
- @property({ type: Boolean, attribute: "show-dots" }) showDots = true;
13
- @property({ type: String }) orientation: "horizontal" | "vertical" = "horizontal";
14
- @property({ type: String }) align: "start" | "center" | "end" = "center";
15
- @property({ type: Number, attribute: "slides-per-view" }) slidesPerView = 1;
16
- @property({ type: Number }) gap?: number;
17
- @property({ type: Boolean, reflect: true }) disabled = false;
18
-
19
- @state() private slideCount = 0;
20
- @state() private isDragging = false;
21
- @state() private dragStartX = 0;
22
- @state() private currentTranslate = 0;
23
- @state() private prevTranslate = 0;
24
-
25
- @query(".viewport") private viewport?: HTMLElement;
26
- @query(".track") private track?: HTMLElement;
27
- @query("slot") private slotElement?: HTMLSlotElement;
28
-
29
- private autoplayTimer?: number;
30
- private animationId?: number;
31
- private prefersReducedMotion = false;
32
-
33
- static styles = css`
34
- :host {
35
- display: block;
36
- font-family: var(--rel-fontFamilies-primary);
37
- position: relative;
38
- user-select: none;
39
- }
40
-
41
- :host([disabled]) {
42
- opacity: 0.6;
43
- pointer-events: none;
44
- }
45
-
46
- .viewport {
47
- overflow: hidden;
48
- position: relative;
49
- width: 100%;
50
- }
51
-
52
- .track {
53
- display: flex;
54
- transition: transform 0.3s ease-out;
55
- will-change: transform;
56
- }
57
-
58
- .track.dragging {
59
- transition: none;
60
- }
61
-
62
- .track.no-transition {
63
- transition: none;
64
- }
65
-
66
- ::slotted(*) {
67
- flex: 0 0 100%;
68
- min-width: 0;
69
- }
70
-
71
- /* Navigation buttons */
72
- .nav-button {
73
- position: absolute;
74
- top: 50%;
75
- transform: translateY(-50%);
76
- z-index: 10;
77
- width: 40px;
78
- height: 40px;
79
- border-radius: 50%;
80
- border: 1px solid var(--rel-border-default);
81
- background: var(--rel-colors-white);
82
- color: var(--rel-colors-gray-primary);
83
- cursor: pointer;
84
- display: flex;
85
- align-items: center;
86
- justify-content: center;
87
- transition: all 0.15s ease;
88
- box-shadow: var(--rel-shadows-shadow-100);
89
- }
90
-
91
- .nav-button:hover:not(:disabled) {
92
- background: var(--rel-surface-secondary);
93
- }
94
-
95
- .nav-button:focus-visible {
96
- outline: 2px solid var(--rel-colors-blue-primary);
97
- outline-offset: 2px;
98
- }
99
-
100
- .nav-button:disabled {
101
- opacity: 0.4;
102
- cursor: not-allowed;
103
- }
104
-
105
- .nav-button.prev {
106
- left: 12px;
107
- }
108
-
109
- .nav-button.next {
110
- right: 12px;
111
- }
112
-
113
- .nav-button svg {
114
- width: 20px;
115
- height: 20px;
116
- }
117
-
118
- /* Dots indicator */
119
- .dots {
120
- display: flex;
121
- justify-content: center;
122
- gap: var(--rel-spacing-space-xs);
123
- margin-top: var(--rel-spacing-space-md);
124
- }
125
-
126
- .dot {
127
- width: 8px;
128
- height: 8px;
129
- border-radius: 50%;
130
- border: none;
131
- background: var(--rel-colors-gray-border);
132
- cursor: pointer;
133
- padding: 0;
134
- transition: all 0.15s ease;
135
- }
136
-
137
- .dot:hover {
138
- background: var(--rel-colors-gray-divider);
139
- }
140
-
141
- .dot.active {
142
- background: var(--rel-colors-green-primary);
143
- width: 24px;
144
- border-radius: 4px;
145
- }
146
-
147
- .dot:focus-visible {
148
- outline: 2px solid var(--rel-colors-blue-primary);
149
- outline-offset: 2px;
150
- }
151
-
152
- /* Reduced motion support */
153
- @media (prefers-reduced-motion: reduce) {
154
- .track {
155
- transition: none;
156
- }
157
- }
158
- `;
159
-
160
- connectedCallback() {
161
- super.connectedCallback();
162
- this.prefersReducedMotion = window.matchMedia(
163
- "(prefers-reduced-motion: reduce)"
164
- ).matches;
165
-
166
- if (this.autoplay) {
167
- this.startAutoplay();
168
- }
169
- }
170
-
171
- disconnectedCallback() {
172
- super.disconnectedCallback();
173
- this.stopAutoplay();
174
- if (this.animationId) {
175
- cancelAnimationFrame(this.animationId);
176
- }
177
- }
178
-
179
- private handleSlotChange() {
180
- const slot = this.slotElement;
181
- if (!slot) return;
182
-
183
- const elements = slot.assignedElements();
184
- this.slideCount = elements.length;
185
-
186
- // Ensure value is within bounds
187
- if (this.value >= this.slideCount) {
188
- this.value = this.slideCount - 1;
189
- }
190
- if (this.value < 0) {
191
- this.value = 0;
192
- }
193
-
194
- this.updateTransform();
195
- }
196
-
197
- private updateTransform() {
198
- if (!this.track || !this.viewport) return;
199
-
200
- const slideWidth = this.viewport.offsetWidth;
201
- const gapValue = this.gap ?? 0;
202
- const translate = -(this.value * (slideWidth + gapValue));
203
-
204
- this.currentTranslate = translate;
205
- this.prevTranslate = translate;
206
- this.track.style.transform = `translateX(${translate}px)`;
207
-
208
- if (this.gap) {
209
- this.track.style.gap = `${this.gap}px`;
210
- } else {
211
- this.track.style.gap = "";
212
- }
213
- }
214
-
215
- private goToPrevious() {
216
- if (this.disabled) return;
217
-
218
- if (this.value > 0) {
219
- this.value--;
220
- } else if (this.loop) {
221
- this.value = this.slideCount - 1;
222
- }
223
-
224
- this.updateTransform();
225
- this.emitChange();
226
- }
227
-
228
- private goToNext() {
229
- if (this.disabled) return;
230
-
231
- if (this.value < this.slideCount - 1) {
232
- this.value++;
233
- } else if (this.loop) {
234
- this.value = 0;
235
- }
236
-
237
- this.updateTransform();
238
- this.emitChange();
239
- }
240
-
241
- private goToSlide(index: number) {
242
- if (this.disabled || index < 0 || index >= this.slideCount) return;
243
-
244
- this.value = index;
245
- this.updateTransform();
246
- this.emitChange();
247
- }
248
-
249
- private emitChange() {
250
- this.dispatchEvent(
251
- new CustomEvent("rel-change", {
252
- detail: { value: this.value },
253
- bubbles: true,
254
- composed: true,
255
- })
256
- );
257
- }
258
-
259
- private startAutoplay() {
260
- if (!this.autoplay || this.disabled) return;
261
-
262
- this.stopAutoplay();
263
- this.autoplayTimer = window.setInterval(() => {
264
- this.goToNext();
265
- }, this.interval);
266
- }
267
-
268
- private stopAutoplay() {
269
- if (this.autoplayTimer) {
270
- clearInterval(this.autoplayTimer);
271
- this.autoplayTimer = undefined;
272
- }
273
- }
274
-
275
- private handlePointerDown(e: PointerEvent) {
276
- if (this.disabled || !this.track) return;
277
-
278
- this.isDragging = true;
279
- this.dragStartX = e.clientX;
280
- this.prevTranslate = this.currentTranslate;
281
-
282
- this.track.classList.add("dragging");
283
-
284
- if (this.autoplay && this.pauseOnHover) {
285
- this.stopAutoplay();
286
- }
287
-
288
- e.preventDefault();
289
- }
290
-
291
- private handlePointerMove(e: PointerEvent) {
292
- if (!this.isDragging || !this.track) return;
293
-
294
- const currentX = e.clientX;
295
- const deltaX = currentX - this.dragStartX;
296
- this.currentTranslate = this.prevTranslate + deltaX;
297
-
298
- if (!this.prefersReducedMotion && this.animationId) {
299
- cancelAnimationFrame(this.animationId);
300
- }
301
-
302
- this.animationId = requestAnimationFrame(() => {
303
- if (this.track) {
304
- this.track.style.transform = `translateX(${this.currentTranslate}px)`;
305
- }
306
- });
307
- }
308
-
309
- private handlePointerUp(e: PointerEvent) {
310
- if (!this.isDragging || !this.track || !this.viewport) return;
311
-
312
- this.isDragging = false;
313
- this.track.classList.remove("dragging");
314
-
315
- const deltaX = e.clientX - this.dragStartX;
316
- const slideWidth = this.viewport.offsetWidth;
317
- const threshold = slideWidth * 0.2; // 20% threshold
318
-
319
- if (Math.abs(deltaX) > threshold) {
320
- if (deltaX > 0 && (this.value > 0 || this.loop)) {
321
- this.goToPrevious();
322
- } else if (deltaX < 0 && (this.value < this.slideCount - 1 || this.loop)) {
323
- this.goToNext();
324
- } else {
325
- this.updateTransform();
326
- }
327
- } else {
328
- this.updateTransform();
329
- }
330
-
331
- if (this.autoplay) {
332
- this.startAutoplay();
333
- }
334
- }
335
-
336
- private handleKeyDown(e: KeyboardEvent) {
337
- if (this.disabled) return;
338
-
339
- switch (e.key) {
340
- case "ArrowLeft":
341
- e.preventDefault();
342
- this.goToPrevious();
343
- break;
344
- case "ArrowRight":
345
- e.preventDefault();
346
- this.goToNext();
347
- break;
348
- case "Home":
349
- e.preventDefault();
350
- this.goToSlide(0);
351
- break;
352
- case "End":
353
- e.preventDefault();
354
- this.goToSlide(this.slideCount - 1);
355
- break;
356
- }
357
- }
358
-
359
- private handleMouseEnter() {
360
- if (this.autoplay && this.pauseOnHover) {
361
- this.stopAutoplay();
362
- }
363
- }
364
-
365
- private handleMouseLeave() {
366
- if (this.autoplay && this.pauseOnHover) {
367
- this.startAutoplay();
368
- }
369
- }
370
-
371
- updated(changedProperties: Map<string, any>) {
372
- if (changedProperties.has("autoplay") || changedProperties.has("interval")) {
373
- if (this.autoplay) {
374
- this.startAutoplay();
375
- } else {
376
- this.stopAutoplay();
377
- }
378
- }
379
-
380
- if (changedProperties.has("value")) {
381
- this.updateTransform();
382
- }
383
- }
384
-
385
- render() {
386
- const canGoPrev = this.loop || this.value > 0;
387
- const canGoNext = this.loop || this.value < this.slideCount - 1;
388
-
389
- return html`
390
- <div
391
- class="viewport"
392
- part="viewport"
393
- role="region"
394
- aria-roledescription="carousel"
395
- aria-label="Carousel"
396
- @keydown=${this.handleKeyDown}
397
- @mouseenter=${this.handleMouseEnter}
398
- @mouseleave=${this.handleMouseLeave}
399
- tabindex="0"
400
- >
401
- <div
402
- class="track"
403
- part="track"
404
- @pointerdown=${this.handlePointerDown}
405
- @pointermove=${this.handlePointerMove}
406
- @pointerup=${this.handlePointerUp}
407
- @pointercancel=${this.handlePointerUp}
408
- >
409
- <slot @slotchange=${this.handleSlotChange}></slot>
410
- </div>
411
-
412
- ${this.showButtons
413
- ? html`
414
- <button
415
- class="nav-button prev"
416
- part="prev-button"
417
- aria-label="Previous slide"
418
- ?disabled=${!canGoPrev || this.disabled}
419
- @click=${this.goToPrevious}
420
- >
421
- <svg viewBox="0 0 16 16" fill="none">
422
- <path
423
- d="M10 12L6 8L10 4"
424
- stroke="currentColor"
425
- stroke-width="2"
426
- stroke-linecap="round"
427
- stroke-linejoin="round"
428
- />
429
- </svg>
430
- </button>
431
-
432
- <button
433
- class="nav-button next"
434
- part="next-button"
435
- aria-label="Next slide"
436
- ?disabled=${!canGoNext || this.disabled}
437
- @click=${this.goToNext}
438
- >
439
- <svg viewBox="0 0 16 16" fill="none">
440
- <path
441
- d="M6 4L10 8L6 12"
442
- stroke="currentColor"
443
- stroke-width="2"
444
- stroke-linecap="round"
445
- stroke-linejoin="round"
446
- />
447
- </svg>
448
- </button>
449
- `
450
- : nothing}
451
- </div>
452
-
453
- ${this.showDots && this.slideCount > 1
454
- ? html`
455
- <div class="dots" part="dots">
456
- ${Array.from({ length: this.slideCount }, (_, i) => html`
457
- <button
458
- class="dot ${i === this.value ? "active" : ""}"
459
- part="dot"
460
- aria-label="Go to slide ${i + 1}"
461
- @click=${() => this.goToSlide(i)}
462
- ></button>
463
- `)}
464
- </div>
465
- `
466
- : nothing}
467
- `;
468
- }
469
- }
1
+ import { LitElement, html, css, nothing } from "lit";
2
+ import { customElement, property, state, query } from "lit/decorators.js";
3
+ import "../../styles/vars-v2.css";
4
+
5
+ @customElement("rel-carousel")
6
+ export class RelCarousel extends LitElement {
7
+ @property({ type: Number, reflect: true }) value = 0;
8
+ @property({ type: Boolean }) loop = false;
9
+ @property({ type: Boolean }) autoplay = false;
10
+ @property({ type: Number }) interval = 5000;
11
+ @property({ type: Boolean, attribute: "pause-on-hover" }) pauseOnHover = true;
12
+ @property({ type: Boolean, attribute: "show-buttons" }) showButtons = false;
13
+ @property({ type: Boolean, attribute: "show-dots" }) showDots = false;
14
+ @property({ type: String }) orientation: "horizontal" | "vertical" = "horizontal";
15
+ @property({ type: String }) label: string = "Carousel";
16
+ @property({ type: Number }) gap?: number;
17
+ @property({ type: Boolean, reflect: true }) disabled = false;
18
+
19
+ @state() private slideCount = 0;
20
+ @state() private isDragging = false;
21
+ @state() private dragStartX = 0;
22
+ @state() private currentTranslate = 0;
23
+ @state() private prevTranslate = 0;
24
+
25
+ @query(".viewport") private viewport?: HTMLElement;
26
+ @query(".track") private track?: HTMLElement;
27
+ @query("slot") private slotElement?: HTMLSlotElement;
28
+
29
+ private autoplayTimer?: number;
30
+ private animationId?: number;
31
+ private prefersReducedMotion = false;
32
+ // Number of clone slides prepended before the real slides
33
+ private clonesBefore = 0;
34
+ private _silentJumping = false;
35
+ private _ro?: ResizeObserver;
36
+
37
+ static styles = css`
38
+ :host {
39
+ display: block;
40
+ font-family: var(--rel-fontFamilies-primary);
41
+ position: relative;
42
+ user-select: none;
43
+ }
44
+
45
+ :host([disabled]) {
46
+ opacity: 0.6;
47
+ pointer-events: none;
48
+ cursor: not-allowed;
49
+ }
50
+
51
+ .viewport {
52
+ overflow: hidden;
53
+ position: relative;
54
+ width: 100%;
55
+ display: block;
56
+ }
57
+
58
+ .track {
59
+ display: flex;
60
+ width: 100%;
61
+ transition: transform 0.3s ease-out;
62
+ will-change: transform;
63
+ }
64
+
65
+ .track.dragging {
66
+ transition: none;
67
+ }
68
+
69
+ .track.no-transition {
70
+ transition: none;
71
+ }
72
+
73
+ ::slotted(*) {
74
+ flex-shrink: 0 !important;
75
+ width: var(--rel-carousel-slide-width, 100%) !important;
76
+ min-width: 0 !important;
77
+ box-sizing: border-box !important;
78
+ }
79
+
80
+ /* Clone slides injected for seamless loop */
81
+ .carousel-clone {
82
+ flex: 0 0 100%;
83
+ min-width: 0;
84
+ box-sizing: border-box;
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ background: var(--rel-color-bg-tertiary, #f0f0f0);
89
+ }
90
+
91
+ /* Navigation buttons */
92
+ .nav-button {
93
+ position: absolute;
94
+ top: 50%;
95
+ transform: translateY(-50%);
96
+ z-index: 10;
97
+ width: var(--rel-carousel-nav-size, 40px);
98
+ height: var(--rel-carousel-nav-size, 40px);
99
+ border-radius: 50%;
100
+ border: 1px solid var(--rel-color-border-default);
101
+ background: var(--rel-color-bg-primary);
102
+ color: var(--rel-color-text-primary);
103
+ cursor: pointer;
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ transition: all 0.15s ease;
108
+ box-shadow: var(--rel-shadows-shadow-100);
109
+ }
110
+
111
+ .nav-button:hover:not(:disabled) {
112
+ background: var(--rel-color-bg-secondary);
113
+ }
114
+
115
+ .nav-button:focus-visible {
116
+ outline: 2px solid var(--rel-color-border-focus);
117
+ outline-offset: 2px;
118
+ }
119
+
120
+ .nav-button:disabled {
121
+ opacity: 0.4;
122
+ cursor: not-allowed;
123
+ }
124
+
125
+ .nav-button.prev {
126
+ left: 12px;
127
+ }
128
+
129
+ .nav-button.next {
130
+ right: 12px;
131
+ }
132
+
133
+ .nav-button svg {
134
+ width: 20px;
135
+ height: 20px;
136
+ }
137
+
138
+ /* Dots indicator */
139
+ .dots {
140
+ display: flex;
141
+ justify-content: center;
142
+ gap: var(--rel-spacing-space-xs);
143
+ margin-top: var(--rel-spacing-space-md);
144
+ }
145
+
146
+ .dot {
147
+ width: var(--rel-carousel-dot-size, 8px);
148
+ height: var(--rel-carousel-dot-size, 8px);
149
+ border-radius: 50%;
150
+ border: none;
151
+ background: var(--rel-color-border-default);
152
+ cursor: pointer;
153
+ padding: 0;
154
+ transition: all 0.15s ease;
155
+ }
156
+
157
+ .dot:hover {
158
+ background: var(--rel-color-border-divider);
159
+ }
160
+
161
+ .dot.active {
162
+ background: var(--rel-color-brand);
163
+ width: var(--rel-carousel-dot-size-active, 24px);
164
+ border-radius: 4px;
165
+ }
166
+
167
+ .dot:focus-visible {
168
+ outline: 2px solid var(--rel-color-border-focus);
169
+ outline-offset: 2px;
170
+ }
171
+
172
+ /* Reduced motion support */
173
+ @media (prefers-reduced-motion: reduce) {
174
+ .track {
175
+ transition: none;
176
+ }
177
+ }
178
+
179
+ :host-context([data-theme="dark"]) .nav-button,
180
+ :host-context(.dark) .nav-button {
181
+ background: var(--rel-color-bg-secondary);
182
+ border-color: var(--rel-color-border-default);
183
+ color: var(--rel-color-text-primary);
184
+ }
185
+
186
+ :host-context([data-theme="dark"]) .nav-button:hover,
187
+ :host-context(.dark) .nav-button:hover {
188
+ background: var(--rel-color-bg-tertiary);
189
+ }
190
+ `;
191
+
192
+ connectedCallback() {
193
+ super.connectedCallback();
194
+ this.prefersReducedMotion = window.matchMedia(
195
+ "(prefers-reduced-motion: reduce)"
196
+ ).matches;
197
+
198
+ if (this.autoplay) {
199
+ this.startAutoplay();
200
+ }
201
+ }
202
+
203
+ firstUpdated() {
204
+ const ro = new ResizeObserver(() => {
205
+ const w = this.viewport?.offsetWidth ?? 0;
206
+ if (w === 0) return;
207
+ this.style.setProperty('--rel-carousel-slide-width', `${w}px`);
208
+ this.renderRoot.querySelectorAll('.carousel-clone').forEach(el => {
209
+ (el as HTMLElement).style.width = `${w}px`;
210
+ });
211
+ this.updateTransform();
212
+ });
213
+ if (this.viewport) ro.observe(this.viewport);
214
+ this._ro = ro;
215
+
216
+ requestAnimationFrame(() => {
217
+ const w = this.viewport?.offsetWidth ?? 0;
218
+ if (w > 0) {
219
+ this.style.setProperty('--rel-carousel-slide-width', `${w}px`);
220
+ this.updateTransform();
221
+ }
222
+ });
223
+ }
224
+
225
+ disconnectedCallback() {
226
+ super.disconnectedCallback();
227
+ this._ro?.disconnect();
228
+ this.stopAutoplay();
229
+ if (this.animationId) {
230
+ cancelAnimationFrame(this.animationId);
231
+ }
232
+ }
233
+
234
+ private handleSlotChange() {
235
+ const slot = this.slotElement;
236
+ const track = this.track;
237
+ if (!slot || !track) return;
238
+
239
+ const elements = slot.assignedElements();
240
+ this.slideCount = elements.length;
241
+
242
+ // Clamp value to valid real-slide range
243
+ if (this.value >= this.slideCount) this.value = Math.max(0, this.slideCount - 1);
244
+ if (this.value < 0) this.value = 0;
245
+
246
+ // Remove any existing shadow-DOM clones
247
+ track.querySelectorAll(".carousel-clone").forEach((el) => el.remove());
248
+
249
+ const useClones = (this.loop || this.autoplay) && this.slideCount > 1;
250
+
251
+ if (useClones) {
252
+ this.clonesBefore = 1;
253
+
254
+ // Clone last slide → prepend before <slot>
255
+ const lastEl = elements[this.slideCount - 1] as HTMLElement;
256
+ const lastClone = lastEl.cloneNode(true) as HTMLElement;
257
+ lastClone.classList.add("carousel-clone");
258
+ lastClone.setAttribute("aria-hidden", "true");
259
+ lastClone.style.width = `${this.viewport?.offsetWidth ?? 0}px`;
260
+ lastClone.style.boxSizing = 'border-box';
261
+ track.insertBefore(lastClone, slot);
262
+
263
+ // Clone first slide → append after <slot>
264
+ const firstEl = elements[0] as HTMLElement;
265
+ const firstClone = firstEl.cloneNode(true) as HTMLElement;
266
+ firstClone.classList.add("carousel-clone");
267
+ firstClone.setAttribute("aria-hidden", "true");
268
+ firstClone.style.width = `${this.viewport?.offsetWidth ?? 0}px`;
269
+ firstClone.style.boxSizing = 'border-box';
270
+ track.appendChild(firstClone);
271
+ } else {
272
+ this.clonesBefore = 0;
273
+ }
274
+
275
+ requestAnimationFrame(() => this.updateTransform());
276
+
277
+ // If loop/autoplay clones were inserted but viewport has no width yet,
278
+ // retry once after paint when dimensions are available
279
+ if (useClones && this.unit() === 0) {
280
+ requestAnimationFrame(() => {
281
+ requestAnimationFrame(() => {
282
+ this.handleSlotChange();
283
+ });
284
+ });
285
+ }
286
+ }
287
+
288
+ private unit(): number {
289
+ const slideWidth = this.viewport?.offsetWidth ?? 0;
290
+ const gapValue = this.gap && this.gap > 0 ? this.gap : 0;
291
+ return slideWidth + gapValue;
292
+ }
293
+
294
+ private updateTransform() {
295
+ if (!this.track || !this.viewport) return;
296
+
297
+ const u = this.unit();
298
+ const translate = -((this.value + this.clonesBefore) * u);
299
+
300
+ this.currentTranslate = translate;
301
+ this.prevTranslate = translate;
302
+ this.track.style.transform = `translateX(${translate}px)`;
303
+ this.track.style.gap = this.gap && this.gap > 0 ? `${this.gap}px` : "";
304
+ }
305
+
306
+ private goToPrevious() {
307
+ if (this.disabled) return;
308
+
309
+ if (this.value > 0) {
310
+ this.value--;
311
+ this.updateTransform();
312
+ this.emitChange();
313
+ } else if (this.clonesBefore > 0) {
314
+ // Animate to the lastClone (position 0 in the track)
315
+ const translate = 0;
316
+ this.currentTranslate = translate;
317
+ this.prevTranslate = translate;
318
+ this.track!.style.transform = `translateX(${translate}px)`;
319
+ this.track!.addEventListener(
320
+ "transitionend",
321
+ () => this.silentJump(this.slideCount - 1),
322
+ { once: true }
323
+ );
324
+ }
325
+ }
326
+
327
+ private goToNext() {
328
+ if (this.disabled) return;
329
+
330
+ if (this.value < this.slideCount - 1) {
331
+ this.value++;
332
+ this.updateTransform();
333
+ this.emitChange();
334
+ } else if (this.clonesBefore > 0) {
335
+ // Animate to the firstClone (one position past the last real slide)
336
+ const cloneFirstPos = this.slideCount + this.clonesBefore;
337
+ const translate = -(cloneFirstPos * this.unit());
338
+ this.currentTranslate = translate;
339
+ this.prevTranslate = translate;
340
+ this.track!.style.transform = `translateX(${translate}px)`;
341
+ this.track!.addEventListener(
342
+ "transitionend",
343
+ () => this.silentJump(0),
344
+ { once: true }
345
+ );
346
+ }
347
+ }
348
+
349
+ // Silently reposition to newValue with no visible transition, then re-enable animation.
350
+ private silentJump(newValue: number) {
351
+ if (!this.track) return;
352
+ this.track.classList.add("no-transition");
353
+ this._silentJumping = true;
354
+ this.value = newValue;
355
+ this.updateTransform();
356
+ this._silentJumping = false;
357
+ requestAnimationFrame(() => {
358
+ requestAnimationFrame(() => {
359
+ this.track!.classList.remove("no-transition");
360
+ });
361
+ });
362
+ this.emitChange();
363
+ }
364
+
365
+ private goToSlide(index: number) {
366
+ if (this.disabled || index < 0 || index >= this.slideCount) return;
367
+
368
+ this.value = index;
369
+ this.updateTransform();
370
+ this.emitChange();
371
+ }
372
+
373
+ private emitChange() {
374
+ this.dispatchEvent(
375
+ new CustomEvent("rel-change", {
376
+ detail: { value: this.value },
377
+ bubbles: true,
378
+ composed: true,
379
+ })
380
+ );
381
+ }
382
+
383
+ private startAutoplay() {
384
+ if (!this.autoplay || this.disabled) return;
385
+
386
+ this.stopAutoplay();
387
+ this.autoplayTimer = window.setInterval(() => {
388
+ this.goToNext();
389
+ }, this.interval);
390
+ }
391
+
392
+ private stopAutoplay() {
393
+ if (this.autoplayTimer) {
394
+ clearInterval(this.autoplayTimer);
395
+ this.autoplayTimer = undefined;
396
+ }
397
+ }
398
+
399
+ private handlePointerDown(e: PointerEvent) {
400
+ if (this.disabled || !this.track) return;
401
+
402
+ this.isDragging = true;
403
+ this.dragStartX = e.clientX;
404
+ this.prevTranslate = this.currentTranslate;
405
+
406
+ this.track.classList.add("dragging");
407
+
408
+ if (this.autoplay && this.pauseOnHover) {
409
+ this.stopAutoplay();
410
+ }
411
+
412
+ e.preventDefault();
413
+ }
414
+
415
+ private handlePointerMove(e: PointerEvent) {
416
+ if (!this.isDragging || !this.track) return;
417
+
418
+ const currentX = e.clientX;
419
+ const deltaX = currentX - this.dragStartX;
420
+ this.currentTranslate = this.prevTranslate + deltaX;
421
+
422
+ if (!this.prefersReducedMotion && this.animationId) {
423
+ cancelAnimationFrame(this.animationId);
424
+ }
425
+
426
+ this.animationId = requestAnimationFrame(() => {
427
+ if (this.track) {
428
+ this.track.style.transform = `translateX(${this.currentTranslate}px)`;
429
+ }
430
+ });
431
+ }
432
+
433
+ private handlePointerUp(e: PointerEvent) {
434
+ if (!this.isDragging || !this.track || !this.viewport) return;
435
+
436
+ this.isDragging = false;
437
+ this.track.classList.remove("dragging");
438
+
439
+ const deltaX = e.clientX - this.dragStartX;
440
+ const slideWidth = this.viewport.offsetWidth;
441
+ const threshold = slideWidth * 0.2; // 20% threshold
442
+
443
+ if (Math.abs(deltaX) > threshold) {
444
+ if (deltaX > 0 && (this.value > 0 || this.clonesBefore > 0)) {
445
+ this.goToPrevious();
446
+ } else if (deltaX < 0 && (this.value < this.slideCount - 1 || this.clonesBefore > 0)) {
447
+ this.goToNext();
448
+ } else {
449
+ this.updateTransform();
450
+ }
451
+ } else {
452
+ this.updateTransform();
453
+ }
454
+
455
+ if (this.autoplay) {
456
+ this.startAutoplay();
457
+ }
458
+ }
459
+
460
+ private handleKeyDown(e: KeyboardEvent) {
461
+ if (this.disabled) return;
462
+
463
+ switch (e.key) {
464
+ case "ArrowLeft":
465
+ e.preventDefault();
466
+ this.goToPrevious();
467
+ break;
468
+ case "ArrowRight":
469
+ e.preventDefault();
470
+ this.goToNext();
471
+ break;
472
+ case "Home":
473
+ e.preventDefault();
474
+ this.goToSlide(0);
475
+ break;
476
+ case "End":
477
+ e.preventDefault();
478
+ this.goToSlide(this.slideCount - 1);
479
+ break;
480
+ }
481
+ }
482
+
483
+ private handleMouseEnter() {
484
+ if (this.autoplay && this.pauseOnHover) {
485
+ this.stopAutoplay();
486
+ }
487
+ }
488
+
489
+ private handleMouseLeave() {
490
+ if (this.autoplay && this.pauseOnHover) {
491
+ this.startAutoplay();
492
+ }
493
+ }
494
+
495
+ updated(changedProperties: Map<string, any>) {
496
+ if (changedProperties.has("autoplay") || changedProperties.has("interval")) {
497
+ if (this.autoplay) {
498
+ this.startAutoplay();
499
+ } else {
500
+ this.stopAutoplay();
501
+ }
502
+ }
503
+
504
+ // Only call updateTransform via Lit lifecycle for in-range values;
505
+ // out-of-range transitions (clone positions) are managed directly.
506
+ // Skip when _silentJumping — silentJump() calls updateTransform() itself.
507
+ if (changedProperties.has("value") && !this._silentJumping && this.value >= 0 && this.value < this.slideCount) {
508
+ this.updateTransform();
509
+ }
510
+ }
511
+
512
+ render() {
513
+ const canGoPrev = this.clonesBefore > 0 || this.value > 0;
514
+ const canGoNext = this.clonesBefore > 0 || this.value < this.slideCount - 1;
515
+
516
+ return html`
517
+ <div
518
+ class="viewport"
519
+ part="viewport"
520
+ role="region"
521
+ aria-roledescription="carousel"
522
+ aria-label=${this.label}
523
+ @keydown=${this.handleKeyDown}
524
+ @mouseenter=${this.handleMouseEnter}
525
+ @mouseleave=${this.handleMouseLeave}
526
+ tabindex="0"
527
+ >
528
+ <div
529
+ class="track"
530
+ part="track"
531
+ @pointerdown=${this.handlePointerDown}
532
+ @pointermove=${this.handlePointerMove}
533
+ @pointerup=${this.handlePointerUp}
534
+ @pointercancel=${this.handlePointerUp}
535
+ >
536
+ <slot @slotchange=${this.handleSlotChange}></slot>
537
+ </div>
538
+
539
+ ${this.showButtons
540
+ ? html`
541
+ <button
542
+ class="nav-button prev"
543
+ part="prev-button"
544
+ aria-label="Previous slide"
545
+ ?disabled=${!canGoPrev || this.disabled}
546
+ @click=${this.goToPrevious}
547
+ >
548
+ <svg viewBox="0 0 16 16" fill="none">
549
+ <path
550
+ d="M10 12L6 8L10 4"
551
+ stroke="currentColor"
552
+ stroke-width="2"
553
+ stroke-linecap="round"
554
+ stroke-linejoin="round"
555
+ />
556
+ </svg>
557
+ </button>
558
+
559
+ <button
560
+ class="nav-button next"
561
+ part="next-button"
562
+ aria-label="Next slide"
563
+ ?disabled=${!canGoNext || this.disabled}
564
+ @click=${this.goToNext}
565
+ >
566
+ <svg viewBox="0 0 16 16" fill="none">
567
+ <path
568
+ d="M6 4L10 8L6 12"
569
+ stroke="currentColor"
570
+ stroke-width="2"
571
+ stroke-linecap="round"
572
+ stroke-linejoin="round"
573
+ />
574
+ </svg>
575
+ </button>
576
+ `
577
+ : nothing}
578
+ </div>
579
+
580
+ ${this.showDots && this.slideCount > 1
581
+ ? html`
582
+ <div class="dots" part="dots">
583
+ ${Array.from({ length: this.slideCount }, (_, i) => html`
584
+ <button
585
+ class="dot ${i === this.value ? "active" : ""}"
586
+ part="dot"
587
+ aria-label="Go to slide ${i + 1}"
588
+ @click=${() => this.goToSlide(i)}
589
+ ></button>
590
+ `)}
591
+ </div>
592
+ `
593
+ : nothing}
594
+ `;
595
+ }
596
+ }