@adia-ai/web-components 0.5.15 → 0.5.16
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.
- package/CHANGELOG.md +47 -0
- package/components/badge/class.js +5 -1
- package/components/button/class.js +5 -1
- package/components/code/class.js +42 -5
- package/components/code/code.a2ui.json +1 -1
- package/components/code/code.d.ts +27 -0
- package/components/code/code.yaml +7 -1
- package/components/segmented/class.js +22 -0
- package/components/slider/class.js +60 -6
- package/components/slider/slider.a2ui.json +10 -4
- package/components/slider/slider.css +142 -52
- package/components/slider/slider.test.js +18 -15
- package/components/slider/slider.yaml +8 -4
- package/components/swatch/class.js +36 -0
- package/components/swatch/swatch.test.js +106 -0
- package/components/swiper/class.js +247 -54
- package/components/swiper/swiper.a2ui.json +19 -0
- package/components/swiper/swiper.css +87 -21
- package/components/swiper/swiper.d.ts +6 -0
- package/components/swiper/swiper.yaml +18 -0
- package/components/tag/class.js +6 -1
- package/package.json +1 -1
|
@@ -46,13 +46,17 @@ export class UISwiper extends UIElement {
|
|
|
46
46
|
snap: { type: String, default: 'start', reflect: true },
|
|
47
47
|
gap: { type: String, default: '', reflect: true },
|
|
48
48
|
slidesPerView: { type: String, default: '', reflect: true, attribute: 'slides-per-view' },
|
|
49
|
+
chrome: { type: String, default: 'default', reflect: true },
|
|
50
|
+
label: { type: String, default: '' },
|
|
51
|
+
counter: { type: Boolean, default: false, reflect: true },
|
|
49
52
|
};
|
|
50
53
|
|
|
51
54
|
static template = () => null;
|
|
52
55
|
|
|
53
56
|
#track = null;
|
|
54
57
|
#timer = null;
|
|
55
|
-
#
|
|
58
|
+
#resizeObserver = null;
|
|
59
|
+
#scrollHandler = null;
|
|
56
60
|
#activeIndex = 0;
|
|
57
61
|
#bound = false;
|
|
58
62
|
#fallbackNav = false;
|
|
@@ -110,6 +114,9 @@ export class UISwiper extends UIElement {
|
|
|
110
114
|
this.#track.addEventListener('pointercancel', this.#onPointerUp);
|
|
111
115
|
// Capture-phase click suppression so a drag-on-button doesn't activate it.
|
|
112
116
|
this.#track.addEventListener('click', this.#onClickCapture, true);
|
|
117
|
+
// Suppress native HTML5 drag (e.g. on <img>) so it doesn't preempt
|
|
118
|
+
// the pointermove gesture before our threshold is reached.
|
|
119
|
+
this.#track.addEventListener('dragstart', this.#onDragStart);
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
if (this.autoplay) this.play();
|
|
@@ -117,15 +124,24 @@ export class UISwiper extends UIElement {
|
|
|
117
124
|
|
|
118
125
|
render() {
|
|
119
126
|
if (this.gap && this.#track) {
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
// Pure-numeric values ("0", "4") are space-token indices that
|
|
128
|
+
// resolve to `--a-space-N`. Anything else (e.g. "1rem", "8px",
|
|
129
|
+
// "var(--my-gap)") is treated as a raw CSS length — matches the
|
|
130
|
+
// example HTML's "Accepts any CSS length or spacing token" claim.
|
|
131
|
+
const isTokenIndex = /^[0-9]+$/.test(this.gap);
|
|
132
|
+
const value = isTokenIndex ? `var(--a-space-${this.gap})` : this.gap;
|
|
133
|
+
this.style.setProperty('--swiper-gap', value);
|
|
122
134
|
}
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
disconnected() {
|
|
126
138
|
this.pause();
|
|
127
|
-
this.#
|
|
128
|
-
this.#
|
|
139
|
+
this.#resizeObserver?.disconnect();
|
|
140
|
+
this.#resizeObserver = null;
|
|
141
|
+
if (this.#track && this.#scrollHandler) {
|
|
142
|
+
this.#track.removeEventListener('scroll', this.#scrollHandler);
|
|
143
|
+
}
|
|
144
|
+
this.#scrollHandler = null;
|
|
129
145
|
this.removeEventListener('pointerenter', this.#onPauseHover);
|
|
130
146
|
this.removeEventListener('pointerleave', this.#onResumeHover);
|
|
131
147
|
this.removeEventListener('focusin', this.#onPauseFocus);
|
|
@@ -137,6 +153,7 @@ export class UISwiper extends UIElement {
|
|
|
137
153
|
this.#track.removeEventListener('pointerup', this.#onPointerUp);
|
|
138
154
|
this.#track.removeEventListener('pointercancel', this.#onPointerUp);
|
|
139
155
|
this.#track.removeEventListener('click', this.#onClickCapture, true);
|
|
156
|
+
this.#track.removeEventListener('dragstart', this.#onDragStart);
|
|
140
157
|
}
|
|
141
158
|
this.#drag = null;
|
|
142
159
|
this.#track = null;
|
|
@@ -146,27 +163,30 @@ export class UISwiper extends UIElement {
|
|
|
146
163
|
// ── Public API ──
|
|
147
164
|
|
|
148
165
|
next() {
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
166
|
+
const lastPage = this.#getPageCount() - 1;
|
|
167
|
+
const currentPage = this.#slideToPageIndex(this.#activeIndex);
|
|
168
|
+
const nextPage = Math.min(currentPage + 1, lastPage);
|
|
169
|
+
if (nextPage !== currentPage || this.loop) {
|
|
170
|
+
const target = this.loop && nextPage === currentPage ? 0 : nextPage;
|
|
171
|
+
this.goTo(this.#pageToSlideIndex(target));
|
|
153
172
|
}
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
prev() {
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
176
|
+
const lastPage = this.#getPageCount() - 1;
|
|
177
|
+
const currentPage = this.#slideToPageIndex(this.#activeIndex);
|
|
178
|
+
const prevPage = Math.max(currentPage - 1, 0);
|
|
179
|
+
if (prevPage !== currentPage || this.loop) {
|
|
180
|
+
const target = this.loop && prevPage === currentPage ? lastPage : prevPage;
|
|
181
|
+
this.goTo(this.#pageToSlideIndex(target));
|
|
161
182
|
}
|
|
162
183
|
}
|
|
163
184
|
|
|
164
185
|
goTo(index) {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
186
|
+
const slide = this.slides[index];
|
|
187
|
+
if (!slide || !this.#track) return;
|
|
188
|
+
const target = Math.max(0, this.#snapTargetFor(slide));
|
|
189
|
+
this.#track.scrollTo({ left: target, behavior: 'smooth' });
|
|
170
190
|
}
|
|
171
191
|
|
|
172
192
|
play() {
|
|
@@ -174,9 +194,10 @@ export class UISwiper extends UIElement {
|
|
|
174
194
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
175
195
|
|
|
176
196
|
this.#timer = setInterval(() => {
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
197
|
+
const lastPage = this.#getPageCount() - 1;
|
|
198
|
+
const currentPage = this.#slideToPageIndex(this.#activeIndex);
|
|
199
|
+
const atEnd = currentPage >= lastPage;
|
|
200
|
+
if (atEnd && this.loop) this.goTo(this.#pageToSlideIndex(0));
|
|
180
201
|
else if (!atEnd) this.next();
|
|
181
202
|
else this.pause();
|
|
182
203
|
}, this.interval || 5000);
|
|
@@ -203,33 +224,123 @@ export class UISwiper extends UIElement {
|
|
|
203
224
|
}
|
|
204
225
|
|
|
205
226
|
#setupObserver() {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
227
|
+
// Scroll-based, snap-aware active tracking. `activeIndex` is the
|
|
228
|
+
// slide whose snap target sits closest to the current scrollLeft:
|
|
229
|
+
// start → slide whose left edge is at the track's left edge
|
|
230
|
+
// center → slide whose center is at the track's center
|
|
231
|
+
// end → slide whose right edge is at the track's right edge
|
|
232
|
+
//
|
|
233
|
+
// Replaces an earlier IntersectionObserver-based "leftmost visible"
|
|
234
|
+
// approach which couldn't distinguish the last two snap positions
|
|
235
|
+
// for center- and end-snap (multiple slides remained >50% visible).
|
|
236
|
+
this.#scrollHandler = () => {
|
|
237
|
+
const newIndex = this.#computeActiveIndex();
|
|
238
|
+
if (newIndex === this.#activeIndex) return;
|
|
239
|
+
this.#activeIndex = newIndex;
|
|
240
|
+
this.#updateFallbackDots();
|
|
241
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
242
|
+
bubbles: true,
|
|
243
|
+
detail: { index: newIndex, slide: this.slides[newIndex] },
|
|
244
|
+
}));
|
|
245
|
+
};
|
|
246
|
+
this.#track.addEventListener('scroll', this.#scrollHandler, { passive: true });
|
|
247
|
+
// Initial stamp (no event — there's no prior state to diff against).
|
|
248
|
+
this.#activeIndex = this.#computeActiveIndex();
|
|
249
|
+
}
|
|
222
250
|
|
|
223
|
-
|
|
224
|
-
|
|
251
|
+
// ── Snap & page helpers ──
|
|
252
|
+
// `--swiper-columns` is set on the host by `[slides-per-view]` and
|
|
253
|
+
// cascades to the track; the responsive @container rules set it on
|
|
254
|
+
// the track directly. Reading the computed value on the track sees
|
|
255
|
+
// both paths.
|
|
256
|
+
|
|
257
|
+
#getColumns() {
|
|
258
|
+
if (!this.#track) return 1;
|
|
259
|
+
const v = getComputedStyle(this.#track).getPropertyValue('--swiper-columns').trim();
|
|
260
|
+
const n = parseInt(v, 10);
|
|
261
|
+
return Number.isFinite(n) && n > 0 ? n : 1;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#getPageCount() {
|
|
265
|
+
const total = this.slides.length;
|
|
266
|
+
if (total === 0) return 0;
|
|
267
|
+
// Center-snap allows every slide to be the centered one (edge
|
|
268
|
+
// slides clamped at the scroll bounds), so each slide is its own
|
|
269
|
+
// "page". Start- and end-snap collapse equivalent positions to
|
|
270
|
+
// (slides − cols + 1) distinct pages.
|
|
271
|
+
if (this.snap === 'center') return total;
|
|
272
|
+
return Math.max(1, total - this.#getColumns() + 1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Snap target for a slide (in scrollLeft coordinates, unclamped).
|
|
276
|
+
#snapTargetFor(slide) {
|
|
277
|
+
const trackWidth = this.#track.clientWidth;
|
|
278
|
+
const left = slide.offsetLeft - this.#track.offsetLeft;
|
|
279
|
+
const width = slide.offsetWidth;
|
|
280
|
+
if (this.snap === 'center') return left + width / 2 - trackWidth / 2;
|
|
281
|
+
if (this.snap === 'end') return left + width - trackWidth;
|
|
282
|
+
return left;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Returns the slide index whose clamped snap target is closest to
|
|
286
|
+
// the current scrollLeft. Snap-mode-aware so the "active" slide
|
|
287
|
+
// matches the snap point currently aligned with the track.
|
|
288
|
+
#computeActiveIndex() {
|
|
289
|
+
if (!this.#track) return 0;
|
|
290
|
+
const slides = this.slides;
|
|
291
|
+
if (slides.length === 0) return 0;
|
|
292
|
+
const sl = this.#track.scrollLeft;
|
|
293
|
+
const maxScroll = Math.max(0, this.#track.scrollWidth - this.#track.clientWidth);
|
|
294
|
+
let bestIndex = 0;
|
|
295
|
+
let bestDistance = Infinity;
|
|
296
|
+
for (let i = 0; i < slides.length; i++) {
|
|
297
|
+
const target = Math.max(0, Math.min(maxScroll, this.#snapTargetFor(slides[i])));
|
|
298
|
+
const distance = Math.abs(sl - target);
|
|
299
|
+
if (distance < bestDistance) {
|
|
300
|
+
bestDistance = distance;
|
|
301
|
+
bestIndex = i;
|
|
302
|
+
}
|
|
225
303
|
}
|
|
304
|
+
return bestIndex;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Page↔slide mapping. Center- and start-snap use the slide index
|
|
308
|
+
// directly as page index; end-snap shifts by (cols−1) because page 0
|
|
309
|
+
// shows slide (cols−1) end-aligned.
|
|
310
|
+
#pageToSlideIndex(page) {
|
|
311
|
+
if (this.snap !== 'end') return page;
|
|
312
|
+
const cols = this.#getColumns();
|
|
313
|
+
return Math.min(this.slides.length - 1, page + cols - 1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#slideToPageIndex(slideIdx) {
|
|
317
|
+
if (this.snap !== 'end') return slideIdx;
|
|
318
|
+
const cols = this.#getColumns();
|
|
319
|
+
return Math.max(0, slideIdx - cols + 1);
|
|
226
320
|
}
|
|
227
321
|
|
|
228
322
|
#setupFallbackNav() {
|
|
229
323
|
if (this.#fallbackNav) return;
|
|
230
324
|
this.#fallbackNav = true;
|
|
231
325
|
|
|
232
|
-
|
|
326
|
+
if (this.chrome === 'toolbar') {
|
|
327
|
+
this.#stampToolbarChrome();
|
|
328
|
+
} else {
|
|
329
|
+
this.#stampDefaultChrome();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
this.#refreshDots();
|
|
333
|
+
|
|
334
|
+
// Container queries can change `--swiper-columns` mid-life; re-stamp
|
|
335
|
+
// dots when the host width crosses a breakpoint.
|
|
336
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
337
|
+
this.#resizeObserver = new ResizeObserver(() => this.#refreshDots());
|
|
338
|
+
this.#resizeObserver.observe(this);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
#stampDefaultChrome() {
|
|
343
|
+
// Overlay paddles (absolutely positioned over the track) + centered dot row.
|
|
233
344
|
const prev = document.createElement('button-ui');
|
|
234
345
|
prev.setAttribute('data-swiper-btn', '');
|
|
235
346
|
prev.setAttribute('data-swiper-prev', '');
|
|
@@ -249,31 +360,102 @@ export class UISwiper extends UIElement {
|
|
|
249
360
|
this.appendChild(prev);
|
|
250
361
|
this.appendChild(next);
|
|
251
362
|
|
|
252
|
-
// Stamp pagination dots
|
|
253
363
|
const dots = document.createElement('div');
|
|
254
364
|
dots.setAttribute('data-swiper-dots', '');
|
|
255
365
|
dots.setAttribute('role', 'tablist');
|
|
256
366
|
dots.setAttribute('aria-label', 'Slide indicators');
|
|
367
|
+
this.appendChild(dots);
|
|
368
|
+
}
|
|
257
369
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (i === 0) dot.setAttribute('aria-current', 'true');
|
|
263
|
-
dot.addEventListener('click', () => this.goTo(i));
|
|
264
|
-
dots.appendChild(dot);
|
|
265
|
-
});
|
|
370
|
+
#stampToolbarChrome() {
|
|
371
|
+
// Header row: [label] [spacer] [prev | next].
|
|
372
|
+
const head = document.createElement('div');
|
|
373
|
+
head.setAttribute('data-swiper-head', '');
|
|
266
374
|
|
|
267
|
-
|
|
375
|
+
const labelEl = document.createElement('span');
|
|
376
|
+
labelEl.setAttribute('data-swiper-label', '');
|
|
377
|
+
labelEl.textContent = this.label || '';
|
|
378
|
+
head.appendChild(labelEl);
|
|
379
|
+
|
|
380
|
+
const paddles = document.createElement('div');
|
|
381
|
+
paddles.setAttribute('data-swiper-paddles', '');
|
|
382
|
+
|
|
383
|
+
const prev = document.createElement('button-ui');
|
|
384
|
+
prev.setAttribute('icon', 'caret-left');
|
|
385
|
+
prev.setAttribute('variant', 'ghost');
|
|
386
|
+
prev.setAttribute('size', 'sm');
|
|
387
|
+
prev.setAttribute('aria-label', 'Previous slide');
|
|
388
|
+
prev.addEventListener('press', () => this.prev());
|
|
389
|
+
|
|
390
|
+
const next = document.createElement('button-ui');
|
|
391
|
+
next.setAttribute('icon', 'caret-right');
|
|
392
|
+
next.setAttribute('variant', 'ghost');
|
|
393
|
+
next.setAttribute('size', 'sm');
|
|
394
|
+
next.setAttribute('aria-label', 'Next slide');
|
|
395
|
+
next.addEventListener('press', () => this.next());
|
|
396
|
+
|
|
397
|
+
paddles.appendChild(prev);
|
|
398
|
+
paddles.appendChild(next);
|
|
399
|
+
head.appendChild(paddles);
|
|
400
|
+
|
|
401
|
+
// Insert head BEFORE the track so it sits above the slides.
|
|
402
|
+
this.insertBefore(head, this.#track);
|
|
403
|
+
|
|
404
|
+
// Footer row: [counter] [spacer] [dots].
|
|
405
|
+
const foot = document.createElement('div');
|
|
406
|
+
foot.setAttribute('data-swiper-foot', '');
|
|
407
|
+
|
|
408
|
+
const counterEl = document.createElement('span');
|
|
409
|
+
counterEl.setAttribute('data-swiper-counter', '');
|
|
410
|
+
foot.appendChild(counterEl);
|
|
411
|
+
|
|
412
|
+
const dots = document.createElement('div');
|
|
413
|
+
dots.setAttribute('data-swiper-dots', '');
|
|
414
|
+
dots.setAttribute('role', 'tablist');
|
|
415
|
+
dots.setAttribute('aria-label', 'Slide indicators');
|
|
416
|
+
foot.appendChild(dots);
|
|
417
|
+
|
|
418
|
+
this.appendChild(foot);
|
|
419
|
+
this.#updateCounter();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
#updateCounter() {
|
|
423
|
+
const el = this.querySelector('[data-swiper-counter]');
|
|
424
|
+
if (!el) return;
|
|
425
|
+
if (!this.counter) { el.textContent = ''; return; }
|
|
426
|
+
const cur = this.#slideToPageIndex(this.#activeIndex) + 1;
|
|
427
|
+
const total = this.#getPageCount();
|
|
428
|
+
el.textContent = `${cur} / ${total}`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#refreshDots() {
|
|
432
|
+
const dots = this.querySelector('[data-swiper-dots]');
|
|
433
|
+
if (!dots) return;
|
|
434
|
+
const desired = this.#getPageCount();
|
|
435
|
+
if (dots.children.length !== desired) {
|
|
436
|
+
while (dots.firstChild) dots.removeChild(dots.firstChild);
|
|
437
|
+
for (let i = 0; i < desired; i++) {
|
|
438
|
+
const dot = document.createElement('button');
|
|
439
|
+
dot.setAttribute('role', 'tab');
|
|
440
|
+
dot.setAttribute('aria-label', `Page ${i + 1}`);
|
|
441
|
+
dot.addEventListener('click', () => this.goTo(this.#pageToSlideIndex(i)));
|
|
442
|
+
dots.appendChild(dot);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
this.#updateFallbackDots();
|
|
268
446
|
}
|
|
269
447
|
|
|
270
448
|
#updateFallbackDots() {
|
|
271
449
|
if (!this.#fallbackNav) return;
|
|
272
450
|
const dots = this.querySelector('[data-swiper-dots]');
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
451
|
+
if (dots) {
|
|
452
|
+
const lastPage = this.#getPageCount() - 1;
|
|
453
|
+
const active = Math.min(Math.max(this.#slideToPageIndex(this.#activeIndex), 0), lastPage);
|
|
454
|
+
[...dots.children].forEach((dot, i) => {
|
|
455
|
+
dot.setAttribute('aria-current', i === active ? 'true' : 'false');
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
this.#updateCounter();
|
|
277
459
|
}
|
|
278
460
|
|
|
279
461
|
// ── Event handlers ──
|
|
@@ -370,4 +552,15 @@ export class UISwiper extends UIElement {
|
|
|
370
552
|
e.stopPropagation();
|
|
371
553
|
}
|
|
372
554
|
};
|
|
555
|
+
|
|
556
|
+
// Suppress native HTML5 drag (e.g. `<img>` ghost-drag) so it doesn't
|
|
557
|
+
// hijack the pointer gesture before `#onPointerMove` crosses its
|
|
558
|
+
// 5px threshold. Without this, dragging on top of an image inside
|
|
559
|
+
// a slide never reaches our pointermove handler. We only suppress
|
|
560
|
+
// when a swipe gesture is actually in progress (set by `#onPointerDown`)
|
|
561
|
+
// so genuinely-draggable content (e.g. a palette item the consumer
|
|
562
|
+
// wants to drag out of the swiper) is preserved when not swiping.
|
|
563
|
+
#onDragStart = (e) => {
|
|
564
|
+
if (this.#drag) e.preventDefault();
|
|
565
|
+
};
|
|
373
566
|
}
|
|
@@ -18,9 +18,23 @@
|
|
|
18
18
|
"type": "boolean",
|
|
19
19
|
"default": false
|
|
20
20
|
},
|
|
21
|
+
"chrome": {
|
|
22
|
+
"description": "Pagination chrome layout. `default` (overlay paddles + centered dots below) or `toolbar` (header row with label + paddles, footer row with counter + dots).",
|
|
23
|
+
"type": "string",
|
|
24
|
+
"enum": [
|
|
25
|
+
"default",
|
|
26
|
+
"toolbar"
|
|
27
|
+
],
|
|
28
|
+
"default": "default"
|
|
29
|
+
},
|
|
21
30
|
"component": {
|
|
22
31
|
"const": "Swiper"
|
|
23
32
|
},
|
|
33
|
+
"counter": {
|
|
34
|
+
"description": "When `chrome=\"toolbar\"`, show a \"current / total\" page counter in the footer (left).",
|
|
35
|
+
"type": "boolean",
|
|
36
|
+
"default": false
|
|
37
|
+
},
|
|
24
38
|
"gap": {
|
|
25
39
|
"description": "Gap between slides (token: sm, md, lg)",
|
|
26
40
|
"type": "string",
|
|
@@ -31,6 +45,11 @@
|
|
|
31
45
|
"type": "number",
|
|
32
46
|
"default": 5000
|
|
33
47
|
},
|
|
48
|
+
"label": {
|
|
49
|
+
"description": "Title rendered in the toolbar header (left). Only used when `chrome=\"toolbar\"`.",
|
|
50
|
+
"type": "string",
|
|
51
|
+
"default": ""
|
|
52
|
+
},
|
|
34
53
|
"loop": {
|
|
35
54
|
"description": "Infinite loop",
|
|
36
55
|
"type": "boolean",
|
|
@@ -28,11 +28,15 @@
|
|
|
28
28
|
|
|
29
29
|
/* ── Pagination dots ── */
|
|
30
30
|
/* Component-intrinsic visual constant; no --a-space-* equivalent */
|
|
31
|
-
--swiper-dot-size:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
--swiper-dot-
|
|
31
|
+
--swiper-dot-size: 6px;
|
|
32
|
+
/* Active dot is one space token (4px @ density=1) larger than the
|
|
33
|
+
inactive dot — small, deliberate emphasis without rearranging
|
|
34
|
+
the row. The hitbox stays at 1rem; only the visible circle grows. */
|
|
35
|
+
--swiper-dot-size-active: calc(var(--swiper-dot-size) + var(--a-space-1));
|
|
36
|
+
--swiper-dot-gap: var(--a-space-2);
|
|
37
|
+
--swiper-dot-bg: var(--a-border);
|
|
38
|
+
--swiper-dot-bg-active: var(--a-fg);
|
|
39
|
+
--swiper-dot-radius: var(--a-radius-full);
|
|
36
40
|
|
|
37
41
|
/* ── Host vertical rhythm ── */
|
|
38
42
|
--swiper-gap-vertical: var(--a-space-3);
|
|
@@ -67,6 +71,11 @@
|
|
|
67
71
|
gap: var(--swiper-gap-vertical);
|
|
68
72
|
width: 100%;
|
|
69
73
|
position: relative;
|
|
74
|
+
/* Host is the container so @container queries below read the
|
|
75
|
+
swiper's own width, not a wider ancestor's. Named to avoid
|
|
76
|
+
collision with any ancestor that also queries by size. */
|
|
77
|
+
container-type: inline-size;
|
|
78
|
+
container-name: swiper;
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
/* ── Scroll track (JS wraps slides into this) ── */
|
|
@@ -84,7 +93,6 @@
|
|
|
84
93
|
scrollbar-width: none;
|
|
85
94
|
-webkit-overflow-scrolling: touch;
|
|
86
95
|
gap: var(--swiper-gap);
|
|
87
|
-
container-type: inline-size;
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
:scope > [data-swiper-track]::-webkit-scrollbar { display: none; }
|
|
@@ -126,18 +134,22 @@
|
|
|
126
134
|
:scope[slides-per-view="3"] { --swiper-columns: 3; }
|
|
127
135
|
:scope[slides-per-view="4"] { --swiper-columns: 4; }
|
|
128
136
|
|
|
129
|
-
/* ── Responsive auto (container queries) ──
|
|
137
|
+
/* ── Responsive auto (container queries) ──
|
|
138
|
+
The host IS the named `swiper` container, so @container queries
|
|
139
|
+
match descendants of it. `:scope` can't be its own descendant —
|
|
140
|
+
we target the track (a descendant) so the cv resolves against
|
|
141
|
+
the swiper's own width. */
|
|
130
142
|
|
|
131
143
|
:scope:not([slides-per-view]) {
|
|
132
144
|
--swiper-columns: 1;
|
|
133
145
|
}
|
|
134
146
|
|
|
135
|
-
@container (min-width: 640px) {
|
|
136
|
-
:scope:not([slides-per-view]) { --swiper-columns: 2; }
|
|
147
|
+
@container swiper (min-width: 640px) {
|
|
148
|
+
:scope:not([slides-per-view]) > [data-swiper-track] { --swiper-columns: 2; }
|
|
137
149
|
}
|
|
138
150
|
|
|
139
|
-
@container (min-width: 960px) {
|
|
140
|
-
:scope:not([slides-per-view]) { --swiper-columns: 3; }
|
|
151
|
+
@container swiper (min-width: 960px) {
|
|
152
|
+
:scope:not([slides-per-view]) > [data-swiper-track] { --swiper-columns: 3; }
|
|
141
153
|
}
|
|
142
154
|
|
|
143
155
|
/* ══════════════════════════════════════════════════════════
|
|
@@ -166,7 +178,7 @@
|
|
|
166
178
|
Normal flow below the track — not inside the scroll area.
|
|
167
179
|
══════════════════════════════════════════════════════════ */
|
|
168
180
|
|
|
169
|
-
:scope
|
|
181
|
+
:scope [data-swiper-dots] {
|
|
170
182
|
display: flex;
|
|
171
183
|
align-items: center;
|
|
172
184
|
justify-content: center;
|
|
@@ -174,7 +186,12 @@
|
|
|
174
186
|
min-height: 1.5rem;
|
|
175
187
|
}
|
|
176
188
|
|
|
177
|
-
|
|
189
|
+
/* Dot geometry — every dot has the same 1rem hitbox so the row layout
|
|
190
|
+
is invariant across state changes. The visible circle inside is
|
|
191
|
+
`--swiper-dot-size` for idle dots and `--swiper-dot-size-active`
|
|
192
|
+
for the active one (one space token larger). Padding is computed
|
|
193
|
+
from the size so the dot stays centered in the hitbox. */
|
|
194
|
+
:scope [data-swiper-dots] > button {
|
|
178
195
|
width: var(--swiper-dot-size);
|
|
179
196
|
height: var(--swiper-dot-size);
|
|
180
197
|
border-radius: var(--swiper-dot-radius);
|
|
@@ -184,21 +201,70 @@
|
|
|
184
201
|
background-clip: content-box;
|
|
185
202
|
box-sizing: content-box;
|
|
186
203
|
cursor: pointer;
|
|
187
|
-
transition: background var(--swiper-duration) var(--swiper-easing)
|
|
188
|
-
transform var(--swiper-duration) var(--swiper-easing);
|
|
204
|
+
transition: background var(--swiper-duration) var(--swiper-easing);
|
|
189
205
|
}
|
|
190
206
|
|
|
191
|
-
|
|
192
|
-
|
|
207
|
+
/* Long-hand `background-color` (not the `background` shorthand!) so
|
|
208
|
+
`background-clip: content-box` from the base rule isn't reset to
|
|
209
|
+
its initial `border-box` value — the colored area would otherwise
|
|
210
|
+
fill the full 1rem hitbox and turn into a huge circle. */
|
|
211
|
+
:scope [data-swiper-dots] > button:hover {
|
|
212
|
+
background-color: var(--swiper-dot-bg-active);
|
|
193
213
|
}
|
|
194
214
|
|
|
195
|
-
|
|
215
|
+
/* Focus ring is rendered INSIDE the padding area (inset shadow), so
|
|
216
|
+
the dot's visual footprint stays the size of every other dot. */
|
|
217
|
+
:scope [data-swiper-dots] > button:focus-visible {
|
|
196
218
|
outline: none;
|
|
197
|
-
box-shadow: var(--a-focus-
|
|
219
|
+
box-shadow: inset 0 0 0 var(--a-focus-width) var(--a-focus-color);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
:scope [data-swiper-dots] > button[aria-current="true"] {
|
|
223
|
+
background-color: var(--swiper-dot-bg-active);
|
|
224
|
+
width: var(--swiper-dot-size-active);
|
|
225
|
+
height: var(--swiper-dot-size-active);
|
|
226
|
+
padding: calc((1rem - var(--swiper-dot-size-active)) / 2);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ══════════════════════════════════════════════════════════
|
|
230
|
+
Toolbar chrome — [chrome="toolbar"]
|
|
231
|
+
Header row: label | spacer | paddles
|
|
232
|
+
Footer row: counter | spacer | dots
|
|
233
|
+
Replaces the overlay paddles + centered dots layout.
|
|
234
|
+
══════════════════════════════════════════════════════════ */
|
|
235
|
+
|
|
236
|
+
:scope[chrome="toolbar"] > [data-swiper-head],
|
|
237
|
+
:scope[chrome="toolbar"] > [data-swiper-foot] {
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
justify-content: space-between;
|
|
241
|
+
gap: var(--a-space-3);
|
|
242
|
+
min-height: 2rem;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
:scope[chrome="toolbar"] > [data-swiper-head] > [data-swiper-label] {
|
|
246
|
+
font-size: var(--a-ui-md);
|
|
247
|
+
font-weight: var(--a-weight-semibold);
|
|
248
|
+
color: var(--a-fg);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
:scope[chrome="toolbar"] > [data-swiper-head] > [data-swiper-paddles] {
|
|
252
|
+
display: flex;
|
|
253
|
+
gap: var(--a-space-1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
:scope[chrome="toolbar"] > [data-swiper-foot] > [data-swiper-counter] {
|
|
257
|
+
font-size: var(--a-ui-sm);
|
|
258
|
+
color: var(--a-fg-subtle);
|
|
259
|
+
font-variant-numeric: tabular-nums;
|
|
198
260
|
}
|
|
199
261
|
|
|
200
|
-
|
|
201
|
-
|
|
262
|
+
/* In toolbar mode, the dots cluster lives inside the footer and is
|
|
263
|
+
right-aligned via space-between on the parent — drop the default
|
|
264
|
+
justify-content: center and zero out vertical breathing room. */
|
|
265
|
+
:scope[chrome="toolbar"] > [data-swiper-foot] > [data-swiper-dots] {
|
|
266
|
+
min-height: 0;
|
|
267
|
+
justify-content: flex-end;
|
|
202
268
|
}
|
|
203
269
|
|
|
204
270
|
/* ── Autoplay indicator ── */
|
|
@@ -31,10 +31,16 @@ export type SwiperChangeEvent = CustomEvent<SwiperChangeEventDetail>;
|
|
|
31
31
|
export class UISwiper extends UIElement {
|
|
32
32
|
/** Enable auto-advance */
|
|
33
33
|
autoplay: boolean;
|
|
34
|
+
/** Pagination chrome layout. `default` (overlay paddles + centered dots below) or `toolbar` (header row with label + paddles, footer row with counter + dots). */
|
|
35
|
+
chrome: 'default' | 'toolbar';
|
|
36
|
+
/** When `chrome="toolbar"`, show a "current / total" page counter in the footer (left). */
|
|
37
|
+
counter: boolean;
|
|
34
38
|
/** Gap between slides (token: sm, md, lg) */
|
|
35
39
|
gap: string;
|
|
36
40
|
/** Autoplay interval in ms */
|
|
37
41
|
interval: number;
|
|
42
|
+
/** Title rendered in the toolbar header (left). Only used when `chrome="toolbar"`. */
|
|
43
|
+
label: string;
|
|
38
44
|
/** Infinite loop */
|
|
39
45
|
loop: boolean;
|
|
40
46
|
/** Suppress the default pause-on-hover/focus behavior for autoplay. */
|
|
@@ -50,6 +50,24 @@ props:
|
|
|
50
50
|
description: Show peek of adjacent slides
|
|
51
51
|
type: boolean
|
|
52
52
|
default: false
|
|
53
|
+
chrome:
|
|
54
|
+
description: >-
|
|
55
|
+
Pagination chrome layout. `default` (overlay paddles +
|
|
56
|
+
centered dots below) or `toolbar` (header row with label +
|
|
57
|
+
paddles, footer row with counter + dots).
|
|
58
|
+
type: string
|
|
59
|
+
default: default
|
|
60
|
+
enum:
|
|
61
|
+
- default
|
|
62
|
+
- toolbar
|
|
63
|
+
label:
|
|
64
|
+
description: Title rendered in the toolbar header (left). Only used when `chrome="toolbar"`.
|
|
65
|
+
type: string
|
|
66
|
+
default: ''
|
|
67
|
+
counter:
|
|
68
|
+
description: When `chrome="toolbar"`, show a "current / total" page counter in the footer (left).
|
|
69
|
+
type: boolean
|
|
70
|
+
default: false
|
|
53
71
|
events:
|
|
54
72
|
autoplay-pause:
|
|
55
73
|
description: Fired on autoplay-pause.
|
package/components/tag/class.js
CHANGED
|
@@ -35,7 +35,12 @@ export class UITag extends UIElement {
|
|
|
35
35
|
|
|
36
36
|
static properties = {
|
|
37
37
|
text: { type: String, default: '', reflect: true },
|
|
38
|
-
|
|
38
|
+
// NOTE: `textContent` is intentionally NOT declared as a reactive
|
|
39
|
+
// prop — `installProps()` would override the native DOM setter, so
|
|
40
|
+
// `el.textContent = '…'` would silently no-op. `_api-table.js`,
|
|
41
|
+
// `catalog.examples.js`, and the drop-target demo all rely on
|
|
42
|
+
// `tag.textContent = …` working natively; the prop declaration
|
|
43
|
+
// broke that path. v0.5.x §327.
|
|
39
44
|
variant: { type: String, default: 'default', reflect: true },
|
|
40
45
|
size: { type: String, default: 'md', reflect: true },
|
|
41
46
|
removable: { type: Boolean, default: false, reflect: true },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/web-components",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.16",
|
|
4
4
|
"description": "AdiaUI web components — vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|