@reliability-design/web 1.4.6 → 1.4.8

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 (148) hide show
  1. package/CHANGELOG.md +246 -49
  2. package/dist/components/rel-accordion/README.md +133 -132
  3. package/dist/components/rel-alert/README.md +145 -141
  4. package/dist/components/rel-autocomplete/README.md +172 -61
  5. package/dist/components/rel-avatar/README.md +118 -130
  6. package/dist/components/rel-badge/README.md +97 -93
  7. package/dist/components/rel-breadcrumb/README.md +96 -102
  8. package/dist/components/rel-button/README.md +190 -130
  9. package/dist/components/rel-calendar/README.md +109 -109
  10. package/dist/components/rel-card/README.md +75 -94
  11. package/dist/components/rel-cardmedia/README.md +100 -127
  12. package/dist/components/rel-carousel/README.md +186 -166
  13. package/dist/components/rel-checkbox/README.md +96 -75
  14. package/dist/components/rel-chip/README.md +138 -121
  15. package/dist/components/rel-container/README.md +107 -99
  16. package/dist/components/rel-date-picker/README.md +200 -289
  17. package/dist/components/rel-divider/README.md +88 -100
  18. package/dist/components/rel-drawer/README.md +187 -200
  19. package/dist/components/rel-kbd/README.md +141 -151
  20. package/dist/components/rel-layout/README.md +34 -0
  21. package/dist/components/rel-menu/README.md +180 -151
  22. package/dist/components/rel-menubar/README.md +218 -329
  23. package/dist/components/rel-otp/README.md +171 -440
  24. package/dist/components/rel-pagination/README.md +100 -424
  25. package/dist/components/rel-popover/README.md +203 -568
  26. package/dist/components/rel-progress/README.md +88 -110
  27. package/dist/components/rel-radio/README.md +131 -138
  28. package/dist/components/rel-range-picker/README.md +165 -0
  29. package/dist/components/rel-resizable/README.md +199 -140
  30. package/dist/components/rel-segmented-button/README.md +156 -141
  31. package/dist/components/rel-select/README.md +156 -93
  32. package/dist/components/rel-sheet/README.md +168 -224
  33. package/dist/components/rel-skeleton/README.md +104 -0
  34. package/dist/components/rel-slider/README.md +131 -135
  35. package/dist/components/rel-snackbar/README.md +168 -134
  36. package/dist/components/rel-stepper/README.md +162 -160
  37. package/dist/components/rel-switch/README.md +117 -102
  38. package/dist/components/rel-tab/README.md +173 -163
  39. package/dist/components/rel-textfield/README.md +176 -69
  40. package/dist/components/rel-time-picker/README.md +122 -157
  41. package/dist/components/rel-toast/README.md +165 -148
  42. package/dist/components/rel-tooltip/README.md +119 -111
  43. package/dist/components/rel-upload/README.md +220 -0
  44. package/dist/index.d.mts +1726 -447
  45. package/dist/index.d.ts +1726 -447
  46. package/dist/index.js +13715 -10870
  47. package/dist/index.js.map +1 -1
  48. package/dist/index.mjs +14018 -11163
  49. package/dist/index.mjs.map +1 -1
  50. package/dist/styles/focus-ring.ts +84 -0
  51. package/dist/styles/vars.css +892 -866
  52. package/dist/{vars-XNMTSRVN.css → vars-DXX6QOAG.css} +892 -866
  53. package/package.json +3 -2
  54. package/dist/components/rel-accordion/rel-accordion.ts +0 -336
  55. package/dist/components/rel-alert/rel-alert.ts +0 -305
  56. package/dist/components/rel-autocomplete/rel-autocomplete.ts +0 -720
  57. package/dist/components/rel-avatar/rel-avatar.ts +0 -142
  58. package/dist/components/rel-badge/rel-badge.ts +0 -175
  59. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +0 -106
  60. package/dist/components/rel-button/rel-button.ts +0 -560
  61. package/dist/components/rel-calendar/index.ts +0 -1
  62. package/dist/components/rel-calendar/rel-calendar.ts +0 -565
  63. package/dist/components/rel-card/rel-card.ts +0 -88
  64. package/dist/components/rel-cardmedia/rel-cardmedia.ts +0 -88
  65. package/dist/components/rel-carousel/index.ts +0 -1
  66. package/dist/components/rel-carousel/rel-carousel.ts +0 -723
  67. package/dist/components/rel-chart/README.md +0 -286
  68. package/dist/components/rel-chart/index.ts +0 -1
  69. package/dist/components/rel-chart/rel-chart.ts +0 -754
  70. package/dist/components/rel-checkbox/rel-checkbox.ts +0 -382
  71. package/dist/components/rel-chip/rel-chip.ts +0 -483
  72. package/dist/components/rel-container/rel-container.ts +0 -95
  73. package/dist/components/rel-date-picker/date-format.test.ts +0 -43
  74. package/dist/components/rel-date-picker/date-format.ts +0 -47
  75. package/dist/components/rel-date-picker/date-parse.test.ts +0 -116
  76. package/dist/components/rel-date-picker/date-parse.ts +0 -178
  77. package/dist/components/rel-date-picker/index.ts +0 -1
  78. package/dist/components/rel-date-picker/rel-date-picker.ts +0 -1676
  79. package/dist/components/rel-divider/rel-divider.ts +0 -54
  80. package/dist/components/rel-drawer/index.ts +0 -1
  81. package/dist/components/rel-drawer/rel-drawer-footer.ts +0 -27
  82. package/dist/components/rel-drawer/rel-drawer.ts +0 -607
  83. package/dist/components/rel-kbd/rel-kbd.ts +0 -188
  84. package/dist/components/rel-layout/rel-layout.ts +0 -49
  85. package/dist/components/rel-menu/rel-menu.ts +0 -541
  86. package/dist/components/rel-menubar/index.ts +0 -2
  87. package/dist/components/rel-menubar/rel-menubar-menu.css +0 -3
  88. package/dist/components/rel-menubar/rel-menubar-menu.ts +0 -17
  89. package/dist/components/rel-menubar/rel-menubar.ts +0 -777
  90. package/dist/components/rel-nav-menu/README.md +0 -289
  91. package/dist/components/rel-nav-menu/index.ts +0 -3
  92. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +0 -243
  93. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +0 -72
  94. package/dist/components/rel-nav-menu/rel-nav-menu.ts +0 -270
  95. package/dist/components/rel-navbar/README.md +0 -126
  96. package/dist/components/rel-navbar/rel-navbar.ts +0 -105
  97. package/dist/components/rel-otp/index.ts +0 -1
  98. package/dist/components/rel-otp/rel-otp.ts +0 -527
  99. package/dist/components/rel-pagination/index.ts +0 -1
  100. package/dist/components/rel-pagination/rel-pagination.ts +0 -522
  101. package/dist/components/rel-popover/index.ts +0 -2
  102. package/dist/components/rel-popover/rel-popover-content.ts +0 -81
  103. package/dist/components/rel-popover/rel-popover.ts +0 -475
  104. package/dist/components/rel-progress/rel-progress.ts +0 -234
  105. package/dist/components/rel-radio/rel-radio.ts +0 -481
  106. package/dist/components/rel-range-picker/rel-range-picker.ts +0 -1688
  107. package/dist/components/rel-resizable/index.ts +0 -3
  108. package/dist/components/rel-resizable/rel-resizable-group.ts +0 -405
  109. package/dist/components/rel-resizable/rel-resizable-handle.ts +0 -150
  110. package/dist/components/rel-resizable/rel-resizable-panel.ts +0 -58
  111. package/dist/components/rel-segmented-button/rel-segmented-button.ts +0 -217
  112. package/dist/components/rel-select/rel-select.ts +0 -1416
  113. package/dist/components/rel-sheet/rel-sheet.ts +0 -610
  114. package/dist/components/rel-sidebar/README.md +0 -209
  115. package/dist/components/rel-sidebar/rel-sidebar.ts +0 -1191
  116. package/dist/components/rel-skeleton/rel-skeleton.ts +0 -143
  117. package/dist/components/rel-slider/rel-slider.ts +0 -673
  118. package/dist/components/rel-snackbar/rel-snackbar.ts +0 -329
  119. package/dist/components/rel-stepper/rel-stepper.ts +0 -567
  120. package/dist/components/rel-switch/rel-switch.ts +0 -199
  121. package/dist/components/rel-tab/rel-tab.ts +0 -264
  122. package/dist/components/rel-table/README.md +0 -178
  123. package/dist/components/rel-table/rel-table-body.ts +0 -22
  124. package/dist/components/rel-table/rel-table-caption.ts +0 -30
  125. package/dist/components/rel-table/rel-table-cell.ts +0 -43
  126. package/dist/components/rel-table/rel-table-footer.ts +0 -24
  127. package/dist/components/rel-table/rel-table-head.ts +0 -47
  128. package/dist/components/rel-table/rel-table-header.ts +0 -22
  129. package/dist/components/rel-table/rel-table-row.ts +0 -37
  130. package/dist/components/rel-table/rel-table.ts +0 -62
  131. package/dist/components/rel-textfield/rel-textfield.ts +0 -1054
  132. package/dist/components/rel-time-picker/rel-time-picker.ts +0 -1482
  133. package/dist/components/rel-time-picker/time-parse.test.ts +0 -67
  134. package/dist/components/rel-time-picker/time-parse.ts +0 -85
  135. package/dist/components/rel-toast/rel-toast.ts +0 -206
  136. package/dist/components/rel-tooltip/rel-tooltip.ts +0 -193
  137. package/dist/components/rel-topbar/rel-topbar-actions.ts +0 -27
  138. package/dist/components/rel-topbar/rel-topbar-brand.ts +0 -54
  139. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +0 -69
  140. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +0 -31
  141. package/dist/components/rel-topbar/rel-topbar-item.ts +0 -324
  142. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +0 -27
  143. package/dist/components/rel-topbar/rel-topbar-profile.ts +0 -243
  144. package/dist/components/rel-topbar/rel-topbar-utilities.ts +0 -27
  145. package/dist/components/rel-topbar/rel-topbar.ts +0 -446
  146. package/dist/components/rel-upload/UPLOAD-DESIGN-CHANGES.md +0 -134
  147. package/dist/components/rel-upload/rel-upload-actions.ts +0 -163
  148. package/dist/components/rel-upload/rel-upload.ts +0 -968
@@ -1,723 +0,0 @@
1
- import { LitElement, html, css, nothing } from "lit";
2
- import { customElement, property, state, query } from "lit/decorators.js";
3
- import { announce } from "../../utils/announce";
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
-
15
- /**
16
- * Figma "Type" axis. When set it drives which controls render:
17
- * default = content only · only-indicators = dots · side-arrows = overlaid
18
- * arrows + dots · bottom-arrows = dots flanked by arrows below the slide.
19
- * When unset, the legacy show-buttons/show-dots booleans apply unchanged.
20
- */
21
- @property({ type: String, reflect: true })
22
- type?: "default" | "only-indicators" | "side-arrows" | "bottom-arrows";
23
-
24
- @property({ type: String, reflect: true }) orientation: "horizontal" | "vertical" = "horizontal";
25
- @property({ type: String }) label: string = "Carousel";
26
- @property({ type: Number }) gap?: number;
27
- @property({ type: Boolean, reflect: true }) disabled = false;
28
-
29
- @state() private slideCount = 0;
30
- @state() private isDragging = false;
31
- @state() private dragStartX = 0;
32
- @state() private currentTranslate = 0;
33
- @state() private prevTranslate = 0;
34
-
35
- @query(".viewport") private viewport?: HTMLElement;
36
- @query(".track") private track?: HTMLElement;
37
- @query("slot") private slotElement?: HTMLSlotElement;
38
-
39
- private autoplayTimer?: number;
40
- private animationId?: number;
41
- private prefersReducedMotion = false;
42
- // Number of clone slides prepended before the real slides
43
- private clonesBefore = 0;
44
- private _silentJumping = false;
45
- private _autoplayTick = false;
46
- private _ro?: ResizeObserver;
47
-
48
- static styles = css`
49
- :host {
50
- color: var(--rel-text-default);
51
- display: block;
52
- font-family: var(--rel-fonts-font-family);
53
- position: relative;
54
- user-select: none;
55
- }
56
-
57
- :host([disabled]) {
58
- opacity: 0.6;
59
- pointer-events: none;
60
- cursor: not-allowed;
61
- }
62
-
63
- .viewport {
64
- overflow: hidden;
65
- position: relative;
66
- width: 100%;
67
- display: block;
68
- }
69
-
70
- .track {
71
- display: flex;
72
- width: 100%;
73
- transition: transform 0.3s ease-out;
74
- will-change: transform;
75
- }
76
-
77
- /* Vertical orientation: column track inside a fixed-height viewport */
78
- :host([orientation="vertical"]) .viewport {
79
- height: var(--rel-carousel-height, 320px);
80
- }
81
-
82
- :host([orientation="vertical"]) .track {
83
- flex-direction: column;
84
- height: 100%;
85
- }
86
-
87
- :host([orientation="vertical"]) ::slotted(*) {
88
- width: 100% !important;
89
- height: var(--rel-carousel-slide-height, 100%) !important;
90
- }
91
-
92
- .track.dragging {
93
- transition: none;
94
- }
95
-
96
- .track.no-transition {
97
- transition: none;
98
- }
99
-
100
- ::slotted(*) {
101
- flex-shrink: 0 !important;
102
- width: var(--rel-carousel-slide-width, 100%) !important;
103
- min-width: 0 !important;
104
- box-sizing: border-box !important;
105
- }
106
-
107
- /* Clone slides injected for seamless loop */
108
- .carousel-clone {
109
- flex: 0 0 100%;
110
- min-width: 0;
111
- box-sizing: border-box;
112
- display: flex;
113
- align-items: center;
114
- justify-content: center;
115
- background: var(--rel-bg-neutral-elevated-1);
116
- }
117
-
118
- /* Default typography for slotted slide captions (Figma roles) */
119
- ::slotted(h1), ::slotted(h2), ::slotted(h3), ::slotted(h4) {
120
- font-family: var(--rel-headings-sm-font-family);
121
- font-weight: var(--rel-headings-sm-font-weight);
122
- font-size: var(--rel-headings-xs-font-size);
123
- line-height: var(--rel-headings-xs-line-height);
124
- letter-spacing: var(--rel-headings-xs-letter-spacing);
125
- margin: 0 0 var(--rel-spacing-2xs) 0;
126
- }
127
-
128
- ::slotted(p) {
129
- font-family: var(--rel-body-sm-regular-font-family);
130
- font-weight: var(--rel-body-sm-regular-font-weight);
131
- font-size: var(--rel-body-sm-regular-font-size);
132
- line-height: var(--rel-body-sm-regular-line-height);
133
- letter-spacing: var(--rel-body-sm-regular-letter-spacing);
134
- color: var(--rel-text-body);
135
- margin: 0 0 var(--rel-spacing-lg) 0;
136
- }
137
-
138
-
139
- /* Navigation buttons */
140
- .nav-button {
141
- position: absolute;
142
- top: 50%;
143
- transform: translateY(-50%);
144
- z-index: 10;
145
- width: var(--rel-carousel-nav-size, 40px);
146
- height: var(--rel-carousel-nav-size, 40px);
147
- border-radius: 50%;
148
- border: 1px solid var(--rel-border-default);
149
- background: var(--rel-bg-neutral);
150
- color: var(--rel-action-neutral-label-icons-ghost);
151
- cursor: pointer;
152
- display: flex;
153
- align-items: center;
154
- justify-content: center;
155
- transition: all 0.15s ease;
156
- box-shadow: var(--rel-shadows-shadow-100);
157
- }
158
-
159
- .nav-button:hover:not(:disabled) {
160
- background: var(--rel-bg-surface-elevated);
161
- }
162
-
163
- .nav-button:focus-visible {
164
- outline: 2px solid var(--rel-border-focus);
165
- outline-offset: 2px;
166
- }
167
-
168
- .nav-button:disabled {
169
- opacity: 0.4;
170
- cursor: not-allowed;
171
- }
172
-
173
- .nav-button.prev {
174
- left: var(--rel-spacing-sm);
175
- }
176
-
177
- .nav-button.next {
178
- right: var(--rel-spacing-sm);
179
- }
180
-
181
- /* Bottom arrows (Figma Type=Bottom Arrows): inline with the dots row */
182
- .dots .nav-button {
183
- position: static;
184
- transform: none;
185
- width: 32px;
186
- height: 32px;
187
- box-shadow: none;
188
- }
189
-
190
- .nav-button svg {
191
- width: 20px;
192
- height: 20px;
193
- }
194
-
195
- /* Dots indicator */
196
- .dots {
197
- display: flex;
198
- align-items: center;
199
- justify-content: center;
200
- gap: var(--rel-spacing-xs);
201
- margin-top: var(--rel-spacing-md);
202
- }
203
-
204
- .dot {
205
- width: var(--rel-carousel-dot-size, 8px);
206
- height: var(--rel-carousel-dot-size, 8px);
207
- border-radius: 50%;
208
- border: none;
209
- background: var(--rel-bg-neutral-elevated-2);
210
- cursor: pointer;
211
- padding: 0;
212
- transition: all 0.15s ease;
213
- }
214
-
215
- .dot:hover {
216
- background: var(--rel-bg-neutral-elevated-2);
217
- }
218
-
219
- .dot.active {
220
- background: var(--rel-bg-surface-brand);
221
- width: var(--rel-carousel-dot-size-active, 24px);
222
- border-radius: var(--rel-border-radius-xs);
223
- }
224
-
225
- .dot:focus-visible {
226
- outline: 2px solid var(--rel-border-focus);
227
- outline-offset: 2px;
228
- }
229
-
230
- /* Reduced motion support */
231
- @media (prefers-reduced-motion: reduce) {
232
- .track {
233
- transition: none;
234
- }
235
- }
236
-
237
- :host-context([data-theme="dark"]) .nav-button,
238
- :host-context(.dark) .nav-button {
239
- background: var(--rel-bg-surface-elevated);
240
- border-color: var(--rel-border-default);
241
- color: var(--rel-text-default);
242
- }
243
-
244
- :host-context([data-theme="dark"]) .nav-button:hover,
245
- :host-context(.dark) .nav-button:hover {
246
- background: var(--rel-bg-neutral-elevated-1);
247
- }
248
- `;
249
-
250
- connectedCallback() {
251
- super.connectedCallback();
252
- this.prefersReducedMotion = window.matchMedia(
253
- "(prefers-reduced-motion: reduce)"
254
- ).matches;
255
-
256
- // WCAG 2.2.2: auto-advance must pause while keyboard focus is inside the
257
- // carousel, not just on mouse hover.
258
- this.addEventListener("focusin", this.handleFocusIn);
259
- this.addEventListener("focusout", this.handleFocusOut);
260
-
261
- if (this.autoplay) {
262
- this.startAutoplay();
263
- }
264
- }
265
-
266
- firstUpdated() {
267
- const ro = new ResizeObserver(() => {
268
- const w = this.viewport?.offsetWidth ?? 0;
269
- const h = this.viewport?.offsetHeight ?? 0;
270
- if (w === 0 && h === 0) return;
271
- this.style.setProperty('--rel-carousel-slide-width', `${w}px`);
272
- this.style.setProperty('--rel-carousel-slide-height', `${h}px`);
273
- this.renderRoot.querySelectorAll('.carousel-clone').forEach(el => {
274
- if (this.vertical) (el as HTMLElement).style.height = `${h}px`;
275
- else (el as HTMLElement).style.width = `${w}px`;
276
- });
277
- this.updateTransform();
278
- });
279
- if (this.viewport) ro.observe(this.viewport);
280
- this._ro = ro;
281
-
282
- requestAnimationFrame(() => {
283
- const w = this.viewport?.offsetWidth ?? 0;
284
- const h = this.viewport?.offsetHeight ?? 0;
285
- if (w > 0 || h > 0) {
286
- this.style.setProperty('--rel-carousel-slide-width', `${w}px`);
287
- this.style.setProperty('--rel-carousel-slide-height', `${h}px`);
288
- this.updateTransform();
289
- }
290
- });
291
- }
292
-
293
- disconnectedCallback() {
294
- super.disconnectedCallback();
295
- this.removeEventListener("focusin", this.handleFocusIn);
296
- this.removeEventListener("focusout", this.handleFocusOut);
297
- this._ro?.disconnect();
298
- this.stopAutoplay();
299
- if (this.animationId) {
300
- cancelAnimationFrame(this.animationId);
301
- }
302
- }
303
-
304
- private handleSlotChange() {
305
- const slot = this.slotElement;
306
- const track = this.track;
307
- if (!slot || !track) return;
308
-
309
- const elements = slot.assignedElements();
310
- this.slideCount = elements.length;
311
-
312
- elements.forEach((el, i) => {
313
- el.setAttribute("role", "group");
314
- el.setAttribute("aria-roledescription", "slide");
315
- el.setAttribute("aria-label", `${i + 1} of ${elements.length}`);
316
- });
317
-
318
- // Clamp value to valid real-slide range
319
- if (this.value >= this.slideCount) this.value = Math.max(0, this.slideCount - 1);
320
- if (this.value < 0) this.value = 0;
321
-
322
- // Remove any existing shadow-DOM clones
323
- track.querySelectorAll(".carousel-clone").forEach((el) => el.remove());
324
-
325
- const useClones = (this.loop || this.autoplay) && this.slideCount > 1;
326
-
327
- if (useClones) {
328
- this.clonesBefore = 1;
329
-
330
- const sizeClone = (clone: HTMLElement) => {
331
- if (this.vertical) clone.style.height = `${this.viewport?.offsetHeight ?? 0}px`;
332
- else clone.style.width = `${this.viewport?.offsetWidth ?? 0}px`;
333
- clone.style.boxSizing = 'border-box';
334
- };
335
-
336
- // Clone last slide → prepend before <slot>
337
- const lastEl = elements[this.slideCount - 1] as HTMLElement;
338
- const lastClone = lastEl.cloneNode(true) as HTMLElement;
339
- lastClone.classList.add("carousel-clone");
340
- lastClone.setAttribute("aria-hidden", "true");
341
- sizeClone(lastClone);
342
- track.insertBefore(lastClone, slot);
343
-
344
- // Clone first slide → append after <slot>
345
- const firstEl = elements[0] as HTMLElement;
346
- const firstClone = firstEl.cloneNode(true) as HTMLElement;
347
- firstClone.classList.add("carousel-clone");
348
- firstClone.setAttribute("aria-hidden", "true");
349
- sizeClone(firstClone);
350
- track.appendChild(firstClone);
351
- } else {
352
- this.clonesBefore = 0;
353
- }
354
-
355
- requestAnimationFrame(() => this.updateTransform());
356
-
357
- // If loop/autoplay clones were inserted but viewport has no width yet,
358
- // retry once after paint when dimensions are available
359
- if (useClones && this.unit() === 0) {
360
- requestAnimationFrame(() => {
361
- requestAnimationFrame(() => {
362
- this.handleSlotChange();
363
- });
364
- });
365
- }
366
- }
367
-
368
- private get vertical(): boolean {
369
- return this.orientation === "vertical";
370
- }
371
-
372
- /** Translate along the orientation axis. */
373
- private axisTransform(px: number): string {
374
- return this.vertical ? `translateY(${px}px)` : `translateX(${px}px)`;
375
- }
376
-
377
- private unit(): number {
378
- const slideSize = (this.vertical ? this.viewport?.offsetHeight : this.viewport?.offsetWidth) ?? 0;
379
- const gapValue = this.gap && this.gap > 0 ? this.gap : 0;
380
- return slideSize + gapValue;
381
- }
382
-
383
- private updateTransform() {
384
- if (!this.track || !this.viewport) return;
385
-
386
- const u = this.unit();
387
- const translate = -((this.value + this.clonesBefore) * u);
388
-
389
- this.currentTranslate = translate;
390
- this.prevTranslate = translate;
391
- this.track.style.transform = this.axisTransform(translate);
392
- this.track.style.gap = this.gap && this.gap > 0 ? `${this.gap}px` : "";
393
- }
394
-
395
- private goToPrevious() {
396
- if (this.disabled) return;
397
-
398
- if (this.value > 0) {
399
- this.value--;
400
- this.updateTransform();
401
- this.emitChange();
402
- } else if (this.clonesBefore > 0) {
403
- // Animate to the lastClone (position 0 in the track)
404
- const translate = 0;
405
- this.currentTranslate = translate;
406
- this.prevTranslate = translate;
407
- this.track!.style.transform = this.axisTransform(translate);
408
- const fromAutoplay = this._autoplayTick;
409
- this.track!.addEventListener(
410
- "transitionend",
411
- () => {
412
- this._autoplayTick = fromAutoplay;
413
- this.silentJump(this.slideCount - 1);
414
- this._autoplayTick = false;
415
- },
416
- { once: true }
417
- );
418
- }
419
- }
420
-
421
- private goToNext() {
422
- if (this.disabled) return;
423
-
424
- if (this.value < this.slideCount - 1) {
425
- this.value++;
426
- this.updateTransform();
427
- this.emitChange();
428
- } else if (this.clonesBefore > 0) {
429
- // Animate to the firstClone (one position past the last real slide)
430
- const cloneFirstPos = this.slideCount + this.clonesBefore;
431
- const translate = -(cloneFirstPos * this.unit());
432
- this.currentTranslate = translate;
433
- this.prevTranslate = translate;
434
- this.track!.style.transform = this.axisTransform(translate);
435
- const fromAutoplay = this._autoplayTick;
436
- this.track!.addEventListener(
437
- "transitionend",
438
- () => {
439
- this._autoplayTick = fromAutoplay;
440
- this.silentJump(0);
441
- this._autoplayTick = false;
442
- },
443
- { once: true }
444
- );
445
- }
446
- }
447
-
448
- // Silently reposition to newValue with no visible transition, then re-enable animation.
449
- private silentJump(newValue: number) {
450
- if (!this.track) return;
451
- this.track.classList.add("no-transition");
452
- this._silentJumping = true;
453
- this.value = newValue;
454
- this.updateTransform();
455
- this._silentJumping = false;
456
- requestAnimationFrame(() => {
457
- requestAnimationFrame(() => {
458
- this.track!.classList.remove("no-transition");
459
- });
460
- });
461
- this.emitChange();
462
- }
463
-
464
- private goToSlide(index: number) {
465
- if (this.disabled || index < 0 || index >= this.slideCount) return;
466
-
467
- this.value = index;
468
- this.updateTransform();
469
- this.emitChange();
470
- }
471
-
472
- private emitChange() {
473
- if (!this._autoplayTick && this.slideCount > 0) {
474
- announce(`Slide ${this.value + 1} of ${this.slideCount}`);
475
- }
476
- this.dispatchEvent(
477
- new CustomEvent("rel-change", {
478
- detail: { value: this.value },
479
- bubbles: true,
480
- composed: true,
481
- })
482
- );
483
- }
484
-
485
- private startAutoplay() {
486
- if (!this.autoplay || this.disabled) return;
487
-
488
- this.stopAutoplay();
489
- this.autoplayTimer = window.setInterval(() => {
490
- this._autoplayTick = true;
491
- this.goToNext();
492
- this._autoplayTick = false;
493
- }, this.interval);
494
- }
495
-
496
- private stopAutoplay() {
497
- if (this.autoplayTimer) {
498
- clearInterval(this.autoplayTimer);
499
- this.autoplayTimer = undefined;
500
- }
501
- }
502
-
503
- private handlePointerDown(e: PointerEvent) {
504
- if (this.disabled || !this.track) return;
505
-
506
- this.isDragging = true;
507
- this.dragStartX = this.vertical ? e.clientY : e.clientX;
508
- this.prevTranslate = this.currentTranslate;
509
-
510
- this.track.classList.add("dragging");
511
-
512
- if (this.autoplay && this.pauseOnHover) {
513
- this.stopAutoplay();
514
- }
515
-
516
- e.preventDefault();
517
- }
518
-
519
- private handlePointerMove(e: PointerEvent) {
520
- if (!this.isDragging || !this.track) return;
521
-
522
- const current = this.vertical ? e.clientY : e.clientX;
523
- const delta = current - this.dragStartX;
524
- this.currentTranslate = this.prevTranslate + delta;
525
-
526
- if (!this.prefersReducedMotion && this.animationId) {
527
- cancelAnimationFrame(this.animationId);
528
- }
529
-
530
- this.animationId = requestAnimationFrame(() => {
531
- if (this.track) {
532
- this.track.style.transform = this.axisTransform(this.currentTranslate);
533
- }
534
- });
535
- }
536
-
537
- private handlePointerUp(e: PointerEvent) {
538
- if (!this.isDragging || !this.track || !this.viewport) return;
539
-
540
- this.isDragging = false;
541
- this.track.classList.remove("dragging");
542
-
543
- const delta = (this.vertical ? e.clientY : e.clientX) - this.dragStartX;
544
- const slideSize = this.vertical ? this.viewport.offsetHeight : this.viewport.offsetWidth;
545
- const threshold = slideSize * 0.2; // 20% threshold
546
-
547
- if (Math.abs(delta) > threshold) {
548
- if (delta > 0 && (this.value > 0 || this.clonesBefore > 0)) {
549
- this.goToPrevious();
550
- } else if (delta < 0 && (this.value < this.slideCount - 1 || this.clonesBefore > 0)) {
551
- this.goToNext();
552
- } else {
553
- this.updateTransform();
554
- }
555
- } else {
556
- this.updateTransform();
557
- }
558
-
559
- if (this.autoplay) {
560
- this.startAutoplay();
561
- }
562
- }
563
-
564
- private handleKeyDown(e: KeyboardEvent) {
565
- if (this.disabled) return;
566
-
567
- switch (e.key) {
568
- case "ArrowLeft":
569
- case "ArrowUp":
570
- e.preventDefault();
571
- this.goToPrevious();
572
- break;
573
- case "ArrowRight":
574
- case "ArrowDown":
575
- e.preventDefault();
576
- this.goToNext();
577
- break;
578
- case "Home":
579
- e.preventDefault();
580
- this.goToSlide(0);
581
- break;
582
- case "End":
583
- e.preventDefault();
584
- this.goToSlide(this.slideCount - 1);
585
- break;
586
- }
587
- }
588
-
589
- private handleFocusIn = () => {
590
- if (this.autoplay) this.stopAutoplay();
591
- };
592
-
593
- private handleFocusOut = (e: FocusEvent) => {
594
- if (!this.autoplay) return;
595
- const next = e.relatedTarget as Node | null;
596
- // Resume only once focus has fully left the carousel (light or shadow).
597
- if (next && (this.contains(next) || this.shadowRoot?.contains(next))) return;
598
- this.startAutoplay();
599
- };
600
-
601
- private handleMouseEnter() {
602
- if (this.autoplay && this.pauseOnHover) {
603
- this.stopAutoplay();
604
- }
605
- }
606
-
607
- private handleMouseLeave() {
608
- if (this.autoplay && this.pauseOnHover) {
609
- this.startAutoplay();
610
- }
611
- }
612
-
613
- updated(changedProperties: Map<string, any>) {
614
- if (changedProperties.has("autoplay") || changedProperties.has("interval")) {
615
- if (this.autoplay) {
616
- this.startAutoplay();
617
- } else {
618
- this.stopAutoplay();
619
- }
620
- }
621
-
622
- // Only call updateTransform via Lit lifecycle for in-range values;
623
- // out-of-range transitions (clone positions) are managed directly.
624
- // Skip when _silentJumping — silentJump() calls updateTransform() itself.
625
- if (changedProperties.has("value") && !this._silentJumping && this.value >= 0 && this.value < this.slideCount) {
626
- this.updateTransform();
627
- }
628
- }
629
-
630
- /** Where nav arrows render, honouring the Figma type when set. */
631
- private get buttonPlacement(): "side" | "bottom" | "none" {
632
- if (this.type === "side-arrows") return "side";
633
- if (this.type === "bottom-arrows") return "bottom";
634
- if (this.type === "only-indicators" || this.type === "default") return "none";
635
- return this.showButtons ? "side" : "none";
636
- }
637
-
638
- private get dotsVisible(): boolean {
639
- if (this.type === "default") return false;
640
- if (this.type) return true;
641
- return this.showDots;
642
- }
643
-
644
- private renderNavButton(direction: "prev" | "next", enabled: boolean) {
645
- const path = direction === "prev" ? "M10 12L6 8L10 4" : "M6 4L10 8L6 12";
646
- return html`
647
- <button
648
- class="nav-button ${direction}"
649
- part="${direction}-button"
650
- aria-label="${direction === "prev" ? "Previous" : "Next"} slide"
651
- ?disabled=${!enabled || this.disabled}
652
- @click=${direction === "prev" ? this.goToPrevious : this.goToNext}
653
- >
654
- <svg viewBox="0 0 16 16" fill="none">
655
- <path
656
- d="${path}"
657
- stroke="currentColor"
658
- stroke-width="2"
659
- stroke-linecap="round"
660
- stroke-linejoin="round"
661
- />
662
- </svg>
663
- </button>
664
- `;
665
- }
666
-
667
- render() {
668
- const canGoPrev = this.clonesBefore > 0 || this.value > 0;
669
- const canGoNext = this.clonesBefore > 0 || this.value < this.slideCount - 1;
670
-
671
- return html`
672
- <div
673
- class="viewport"
674
- part="viewport"
675
- role="region"
676
- aria-roledescription="carousel"
677
- aria-label=${this.label}
678
- @keydown=${this.handleKeyDown}
679
- @mouseenter=${this.handleMouseEnter}
680
- @mouseleave=${this.handleMouseLeave}
681
- tabindex="0"
682
- >
683
- <div
684
- class="track"
685
- part="track"
686
- @pointerdown=${this.handlePointerDown}
687
- @pointermove=${this.handlePointerMove}
688
- @pointerup=${this.handlePointerUp}
689
- @pointercancel=${this.handlePointerUp}
690
- >
691
- <slot @slotchange=${this.handleSlotChange}></slot>
692
- </div>
693
-
694
- ${this.buttonPlacement === "side"
695
- ? html`
696
- ${this.renderNavButton("prev", canGoPrev)}
697
- ${this.renderNavButton("next", canGoNext)}
698
- `
699
- : nothing}
700
- </div>
701
-
702
- ${(this.dotsVisible || this.buttonPlacement === "bottom") && this.slideCount > 1
703
- ? html`
704
- <div class="dots" part="dots">
705
- ${this.buttonPlacement === "bottom" ? this.renderNavButton("prev", canGoPrev) : nothing}
706
- ${this.dotsVisible
707
- ? Array.from({ length: this.slideCount }, (_, i) => html`
708
- <button
709
- class="dot ${i === this.value ? "active" : ""}"
710
- part="dot"
711
- aria-label="Go to slide ${i + 1}"
712
- aria-current="${i === this.value ? "true" : nothing}"
713
- @click=${() => this.goToSlide(i)}
714
- ></button>
715
- `)
716
- : nothing}
717
- ${this.buttonPlacement === "bottom" ? this.renderNavButton("next", canGoNext) : nothing}
718
- </div>
719
- `
720
- : nothing}
721
- `;
722
- }
723
- }