@getvitops/generator 5.0.0 → 7.0.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.
- package/CHANGELOG.md +363 -0
- package/assets/bricks/elements/carousel.php +55 -9
- package/assets/css/animation.css +9 -0
- package/assets/css/global.css +24 -0
- package/assets/css/index.css +2 -0
- package/assets/css/patterns/carousel.css +602 -179
- package/assets/css/patterns/counter.css +50 -0
- package/assets/css/patterns/forms.css +1 -1
- package/assets/css/patterns/gallery.css +60 -0
- package/assets/css/patterns/lightbox.css +22 -41
- package/assets/css/patterns/list.css +1 -1
- package/assets/css/patterns/tag.css +1 -2
- package/assets/css/patterns/tree.css +111 -8
- package/assets/css/utilities.css +17 -0
- package/assets/js/elements.js +7 -844
- package/assets/js/polyfills/invokers-polyfill.js +1 -0
- package/assets/js/polyfills/template-shadowroot.js +1 -14
- package/assets/js/polyfills.js +1 -1
- package/config.schema.json +100 -1
- package/dist/index.d.mts +63 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +108 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -1,112 +1,418 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Carousel —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Carousel — a scroll-snap strip that is fully operable, and *discoverable*, with
|
|
3
|
+
* no JavaScript in every engine, and that uses the native scroll-marker machinery
|
|
4
|
+
* (`::scroll-button()`, `::scroll-marker`, `::scroll-marker-group`, `::column`)
|
|
5
|
+
* wherever it exists.
|
|
6
|
+
*
|
|
7
|
+
* As of Aug 2026 that machinery is Chromium-135+ only — Safari is landing it,
|
|
8
|
+
* Firefox has it behind flags — and there is no usable polyfill: the only
|
|
9
|
+
* candidate (flackr/carousel's `polyfill/polyfill.js`) is a 2024 prototype on the
|
|
10
|
+
* superseded syntax that re-parses inline `<style>` elements and so never sees a
|
|
11
|
+
* linked stylesheet. So `WCCarousel` builds real `<button>`s and a real dot nav
|
|
12
|
+
* instead, and this file styles BOTH paths identically. See the `@supports` split
|
|
13
|
+
* below for why they can't be one selector list.
|
|
14
|
+
*
|
|
15
|
+
* ── Structure ────────────────────────────────────────────────────────────────
|
|
16
|
+
* .carousel shell. Not a scroller. The <wc-carousel> tag.
|
|
17
|
+
* ├ .carousel__track the scroll container (a <ul role="list"> from Astro).
|
|
18
|
+
* │ └ .carousel__slide a slide — rules apply to any direct child of the track.
|
|
19
|
+
* │ └ .carousel__media aspect-ratio box the image covers.
|
|
20
|
+
* ├ .carousel__hint "Scroll for more" — OUTSIDE the scroller, so it can't
|
|
21
|
+
* │ scroll away with the slides. That is the whole reason
|
|
22
|
+
* │ the shell exists.
|
|
23
|
+
* ├ .carousel__button JS-built prev/next (fallback path only).
|
|
24
|
+
* ├ .carousel__markers JS-built dot nav (fallback path only).
|
|
25
|
+
* └ .carousel__autoplay play/pause toggle, built by the element.
|
|
26
|
+
*
|
|
27
|
+
* The shell also replaces the previous `position: fixed` + `anchor-name` /
|
|
28
|
+
* `position-area` placement of the controls. That was wrong three ways: an engine
|
|
29
|
+
* without anchor positioning pinned the real autoplay button to a VIEWPORT corner
|
|
30
|
+
* (the oddbird polyfill supports neither `position-area` nor a pseudo-element
|
|
31
|
+
* anchor, so the postrender polyfill never rescued it); `anchor-scope: --carousel`
|
|
32
|
+
* was one global name doing per-instance work; and the shell is already outside
|
|
33
|
+
* the scrollport, so `position: absolute` against it neither scrolls nor clips
|
|
34
|
+
* without any of that machinery.
|
|
35
|
+
*
|
|
36
|
+
* ── Modifiers ────────────────────────────────────────────────────────────────
|
|
37
|
+
* carousel--scroll-buttons prev/next
|
|
38
|
+
* carousel--scroll-markers dot nav
|
|
39
|
+
* carousel--auto-pages CSS multicol paging (`::column`) — Chromium-only,
|
|
40
|
+
* degrades to a plain multicol strip elsewhere
|
|
41
|
+
* carousel--inert dim + inert the off-screen slides
|
|
42
|
+
* carousel--force-stop `scroll-snap-stop: always`
|
|
43
|
+
* carousel--no-scrollbar opt out of the visible scrollbar
|
|
44
|
+
* carousel--markers-below dots below the strip instead of overlaid
|
|
6
45
|
*/
|
|
7
46
|
|
|
47
|
+
/* An unregistered custom element is `display: inline`, and this one wraps a block
|
|
48
|
+
scroller — the KNOWN GAP recorded in js/elements.ts. The grid lives on
|
|
49
|
+
`.carousel`, which is what an author actually writes; the tag only needs to
|
|
50
|
+
stop being inline before the element upgrades. */
|
|
51
|
+
wc-carousel {
|
|
52
|
+
display: block;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ── Feature probe ───────────────────────────────────────────────────────────
|
|
56
|
+
ONE declaration of each condition. `WCCarousel` does NOT re-run these with
|
|
57
|
+
`CSS.supports`; it reads the resolved custom property back off the element, so
|
|
58
|
+
the stylesheet and the component cannot disagree about which branch is live.
|
|
59
|
+
Two hand-synced condition strings — one here, one in JS — is the drift this
|
|
60
|
+
avoids, and its failure mode is silent duplicate controls sitting under working
|
|
61
|
+
native ones.
|
|
62
|
+
|
|
63
|
+
It also answers the harder question correctly. The real question is not "does
|
|
64
|
+
this engine support ::scroll-button()" but "did MY stylesheet's native branch
|
|
65
|
+
apply". A page that loads elements.js without the framework CSS has no
|
|
66
|
+
pseudo-elements to collide with — the probe reads empty and the fallback is
|
|
67
|
+
built. `CSS.supports` would have said yes and built nothing. */
|
|
68
|
+
.carousel {
|
|
69
|
+
--carousel-native-nav: 0;
|
|
70
|
+
--carousel-scroll-timeline: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* `(scroll-marker-group: after)` is ANDed in because `selector()` support for a
|
|
74
|
+
functional pseudo-element is the weaker signal of the two. */
|
|
75
|
+
@supports selector(::scroll-marker) and selector(::scroll-button(inline-end)) and
|
|
76
|
+
(scroll-marker-group: after) {
|
|
77
|
+
.carousel {
|
|
78
|
+
--carousel-native-nav: 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@supports (animation-timeline: scroll()) {
|
|
83
|
+
.carousel {
|
|
84
|
+
--carousel-scroll-timeline: 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* ── Shell ───────────────────────────────────────────────────────────────────*/
|
|
89
|
+
|
|
8
90
|
.carousel {
|
|
91
|
+
position: relative; /* containing block for both control paths */
|
|
92
|
+
display: grid;
|
|
93
|
+
grid-template-columns: minmax(0, 1fr);
|
|
94
|
+
row-gap: var(--carousel-shell-gap, 0.5rem);
|
|
95
|
+
|
|
96
|
+
/* Hoist the track's scroll-progress timeline so the hint — a SIBLING of the
|
|
97
|
+
scroller, not a descendant — can read it. This is why the hint fade is not a
|
|
98
|
+
`scroll-state()` container query: a scroll-state query's rules apply to
|
|
99
|
+
descendants of the scroll container, and the hint deliberately isn't one.
|
|
100
|
+
`timeline-scope` also makes it order-independent, so the hint may precede the
|
|
101
|
+
track in source. */
|
|
102
|
+
timeline-scope: --carousel-scroll;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ── Track ───────────────────────────────────────────────────────────────────
|
|
106
|
+
The second selector is the deprecated 5.x single-element form, where
|
|
107
|
+
`.carousel` IS the scroller and the slides are its bare children. It has
|
|
108
|
+
nowhere to put the hint or the fallback controls, so it is kept only for one
|
|
109
|
+
major; migration is a single wrapper element. A selector list rather than a
|
|
110
|
+
`:is()` because `:has()` is Baseline and this stays readable. */
|
|
111
|
+
.carousel__track,
|
|
112
|
+
.carousel:not(:has(> .carousel__track)) {
|
|
113
|
+
margin: 0;
|
|
114
|
+
padding: var(--carousel-padding, 0);
|
|
115
|
+
list-style: none;
|
|
116
|
+
max-inline-size: 100%;
|
|
9
117
|
overflow-x: auto;
|
|
10
|
-
scroll-behavior: smooth;
|
|
11
118
|
overscroll-behavior-x: contain;
|
|
12
|
-
|
|
119
|
+
scroll-behavior: smooth;
|
|
13
120
|
scroll-snap-type: x mandatory;
|
|
14
|
-
scroll-padding: var(--carousel-scroll-padding,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
121
|
+
scroll-padding-inline: var(--carousel-scroll-padding, 0px);
|
|
122
|
+
|
|
123
|
+
/* The timeline the hint and the scrollbar fade read, declared here and
|
|
124
|
+
consumed on the shell's other children through `timeline-scope`. */
|
|
125
|
+
scroll-timeline: --carousel-scroll inline;
|
|
126
|
+
|
|
127
|
+
/* For `carousel--inert`, whose targets genuinely ARE descendants of the
|
|
128
|
+
scroller — the one place a scroll-state query is applicable here. */
|
|
129
|
+
container-type: scroll-state;
|
|
130
|
+
|
|
131
|
+
/* A thin, styled scrollbar is the default affordance. `scrollbar-width: none`
|
|
132
|
+
removed the only no-JS signal that the strip scrolls at all, on the
|
|
133
|
+
assumption the ::scroll-button()s were present — the exact assumption that
|
|
134
|
+
fails in every engine but one. `.carousel--no-scrollbar` opts out. */
|
|
135
|
+
scrollbar-width: thin;
|
|
136
|
+
scrollbar-color: var(--carousel-scrollbar-thumb, var(--color-bg-surface-bold))
|
|
137
|
+
var(--carousel-scrollbar-track, transparent);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.carousel--no-scrollbar > .carousel__track,
|
|
141
|
+
.carousel--no-scrollbar:not(:has(> .carousel__track)) {
|
|
142
|
+
scrollbar-width: none;
|
|
143
|
+
|
|
144
|
+
&::-webkit-scrollbar {
|
|
145
|
+
display: none;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
19
148
|
|
|
20
|
-
|
|
21
|
-
|
|
149
|
+
/* Non-paged (default) — one grid column per slide. */
|
|
150
|
+
.carousel:not(.carousel--auto-pages) > .carousel__track,
|
|
151
|
+
.carousel:not(.carousel--auto-pages):not(:has(> .carousel__track)) {
|
|
152
|
+
display: grid;
|
|
153
|
+
grid-auto-flow: column;
|
|
154
|
+
grid-auto-columns: var(--carousel-slide-size, 100%);
|
|
155
|
+
gap: var(--carousel-gap, 1rem);
|
|
156
|
+
/* Equal-height slides whatever the media. */
|
|
157
|
+
align-items: stretch;
|
|
158
|
+
|
|
159
|
+
> * {
|
|
160
|
+
scroll-snap-align: center;
|
|
161
|
+
min-inline-size: 0;
|
|
162
|
+
/* A slide is a stretched grid item, so its media box has a definite
|
|
163
|
+
block-size for `object-fit: cover` to fill. */
|
|
22
164
|
display: grid;
|
|
23
|
-
grid-
|
|
24
|
-
|
|
25
|
-
|
|
165
|
+
grid-template-rows: auto;
|
|
166
|
+
align-content: start;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
26
169
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
170
|
+
/* Paged — CSS multicol. `::column` is left ungated: an engine without it renders
|
|
171
|
+
a plain multicol strip, which still scrolls and snaps. */
|
|
172
|
+
.carousel--auto-pages > .carousel__track,
|
|
173
|
+
.carousel--auto-pages:not(:has(> .carousel__track)) {
|
|
174
|
+
columns: var(--carousel-columns, 1);
|
|
175
|
+
text-align: center;
|
|
30
176
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
177
|
+
> * {
|
|
178
|
+
break-inside: avoid;
|
|
179
|
+
display: inline-grid;
|
|
34
180
|
}
|
|
35
181
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
182
|
+
&::column {
|
|
183
|
+
scroll-snap-align: center;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
40
186
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
187
|
+
/* ── Slide media ─────────────────────────────────────────────────────────────
|
|
188
|
+
Equal-height slides regardless of image aspect ratio. This file previously had
|
|
189
|
+
no `img` rule at all and global.css no media reset, so a portrait photo in a
|
|
190
|
+
strip of landscapes set the row height for every slide and the rest floated in
|
|
191
|
+
it. The fix is the one `patterns/media.css` and `patterns/subgrid.css` already
|
|
192
|
+
use: give the media a box with a known ratio and let the image cover it. */
|
|
193
|
+
.carousel__media {
|
|
194
|
+
aspect-ratio: var(--carousel-slide-aspect, 16 / 9);
|
|
195
|
+
overflow: hidden;
|
|
196
|
+
margin: 0;
|
|
197
|
+
border-radius: var(--br-carousel-group);
|
|
198
|
+
|
|
199
|
+
> :is(img, picture, video, svg, canvas) {
|
|
200
|
+
display: block;
|
|
201
|
+
inline-size: 100%;
|
|
202
|
+
block-size: 100%;
|
|
203
|
+
object-fit: var(--carousel-media-fit, cover);
|
|
204
|
+
object-position: var(--carousel-media-position, center);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
45
207
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
208
|
+
/* Belt and braces for a slide that doesn't use the media wrapper. */
|
|
209
|
+
.carousel__track img {
|
|
210
|
+
max-inline-size: 100%;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.carousel__caption {
|
|
214
|
+
margin-block-start: 0.5rem;
|
|
215
|
+
color: var(--color-text-surface-muted);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ── Scroll buttons ──────────────────────────────────────────────────────────
|
|
219
|
+
Two blocks declaring the same thing, and they cannot be merged into one
|
|
220
|
+
selector list: a selector list is NOT forgiving, so one unparseable complex
|
|
221
|
+
selector drops the WHOLE rule. `.carousel__button, …::scroll-button(*) { … }`
|
|
222
|
+
would take the fallback button's styling down with the pseudo-element Firefox
|
|
223
|
+
doesn't know. `:is()` is no rescue either — it cannot contain pseudo-elements.
|
|
224
|
+
`carousel-parity.test.ts` asserts the two blocks declare the same properties. */
|
|
225
|
+
|
|
226
|
+
/* Fallback path — real <button>s built by WCCarousel when the probe reads 0. */
|
|
227
|
+
.carousel__button {
|
|
228
|
+
position: absolute;
|
|
229
|
+
inset-block: 0;
|
|
230
|
+
block-size: fit-content;
|
|
231
|
+
margin-block: auto;
|
|
232
|
+
z-index: var(--z-tier-raised);
|
|
233
|
+
box-sizing: border-box;
|
|
234
|
+
padding: 0;
|
|
235
|
+
border: 0;
|
|
236
|
+
line-height: 1;
|
|
237
|
+
display: grid;
|
|
238
|
+
place-content: center;
|
|
239
|
+
place-items: center;
|
|
240
|
+
inline-size: var(--carousel-button-size, 2.75rem);
|
|
241
|
+
aspect-ratio: 1;
|
|
242
|
+
border-radius: 50%;
|
|
243
|
+
font-size: var(--carousel-button-glyph, 1.5rem);
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
color: var(--carousel-button-color, var(--color-text-surface));
|
|
246
|
+
background-color: var(--carousel-button-bg, var(--color-bg-surface-muted));
|
|
247
|
+
box-shadow: var(--ds-carousel-group);
|
|
248
|
+
transition:
|
|
249
|
+
background-color 0.2s ease,
|
|
250
|
+
opacity 0.2s ease;
|
|
251
|
+
|
|
252
|
+
&:hover {
|
|
253
|
+
/* A REAL hover step. The old rule set the same `--color-bg-surface-muted` the
|
|
254
|
+
base rule sets, so hover was a no-op — the token migration flattened two
|
|
255
|
+
distinct oklch literals onto one variable. `muted` → `bold` is one ordinal
|
|
256
|
+
step on the surface ladder and reads in both schemes. */
|
|
257
|
+
background-color: var(--carousel-button-bg-hover, var(--color-bg-surface-bold));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
&:focus-visible {
|
|
261
|
+
outline: 2px solid var(--color-border-focus);
|
|
262
|
+
outline-offset: 2px;
|
|
49
263
|
}
|
|
50
264
|
|
|
51
|
-
|
|
52
|
-
|
|
265
|
+
&:disabled {
|
|
266
|
+
cursor: not-allowed;
|
|
267
|
+
opacity: 0.35;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.carousel__button--prev {
|
|
272
|
+
inset-inline-start: var(--carousel-button-inset, 0.5rem);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.carousel__button--next {
|
|
276
|
+
inset-inline-end: var(--carousel-button-inset, 0.5rem);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* Native path. */
|
|
280
|
+
@supports selector(::scroll-button(inline-end)) {
|
|
281
|
+
.carousel--scroll-buttons > .carousel__track,
|
|
282
|
+
.carousel--scroll-buttons:not(:has(> .carousel__track)) {
|
|
53
283
|
&::scroll-button(*) {
|
|
54
|
-
position:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
284
|
+
position: absolute;
|
|
285
|
+
inset-block: 0;
|
|
286
|
+
block-size: fit-content;
|
|
287
|
+
margin-block: auto;
|
|
288
|
+
z-index: var(--z-tier-raised);
|
|
289
|
+
/* global.css's reset is `*, *::before, *::after` — it does NOT reach a
|
|
290
|
+
::scroll-button(), so this pseudo is content-box carrying Chrome's UA
|
|
291
|
+
`padding: 1px 6px`. `inline-size: 44px` + `aspect-ratio: 1` therefore
|
|
292
|
+
sized the CONTENT box, giving a 56×46 border box: an ellipse with the
|
|
293
|
+
glyph off-centre. That, plus `line-height: normal` letting a 1.5rem
|
|
294
|
+
glyph ride its own half-leading, is the whole centring bug. `grid`
|
|
295
|
+
rather than `flex` because `place-items`' justify-items half is inert in
|
|
296
|
+
a flex container. */
|
|
297
|
+
box-sizing: border-box;
|
|
298
|
+
padding: 0;
|
|
299
|
+
border: 0;
|
|
300
|
+
line-height: 1;
|
|
301
|
+
display: grid;
|
|
63
302
|
place-content: center;
|
|
64
|
-
|
|
303
|
+
place-items: center;
|
|
304
|
+
inline-size: var(--carousel-button-size, 2.75rem);
|
|
305
|
+
aspect-ratio: 1;
|
|
65
306
|
border-radius: 50%;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
307
|
+
font-size: var(--carousel-button-glyph, 1.5rem);
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
color: var(--carousel-button-color, var(--color-text-surface));
|
|
310
|
+
background-color: var(--carousel-button-bg, var(--color-bg-surface-muted));
|
|
311
|
+
box-shadow: var(--ds-carousel-group);
|
|
69
312
|
transition:
|
|
70
|
-
|
|
71
|
-
|
|
313
|
+
background-color 0.2s ease,
|
|
314
|
+
opacity 0.2s ease;
|
|
72
315
|
}
|
|
73
316
|
|
|
74
317
|
&::scroll-button(*):hover {
|
|
75
|
-
|
|
76
|
-
background: var(--color-bg-surface-muted);
|
|
318
|
+
background-color: var(--carousel-button-bg-hover, var(--color-bg-surface-bold));
|
|
77
319
|
}
|
|
78
320
|
|
|
79
321
|
&::scroll-button(*):focus-visible {
|
|
80
|
-
outline: 2px solid
|
|
322
|
+
outline: 2px solid var(--color-border-focus);
|
|
81
323
|
outline-offset: 2px;
|
|
82
324
|
}
|
|
83
325
|
|
|
84
326
|
&::scroll-button(*):disabled {
|
|
85
327
|
cursor: not-allowed;
|
|
86
|
-
opacity: 0.
|
|
328
|
+
opacity: 0.35;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* Logical, not physical. `(left)`/`(right)` put "previous" on the wrong side
|
|
332
|
+
in an RTL document and pointed the arrow the wrong way. Single guillemets
|
|
333
|
+
centre far better in a circle than `\2190`/`\2192`, whose side bearings are
|
|
334
|
+
asymmetric. The alt text takes a var() so it can be localised without
|
|
335
|
+
re-declaring `content`. */
|
|
336
|
+
&::scroll-button(inline-start) {
|
|
337
|
+
inset-inline-start: var(--carousel-button-inset, 0.5rem);
|
|
338
|
+
content: '\2039' / var(--carousel-label-prev, 'Previous');
|
|
87
339
|
}
|
|
88
340
|
|
|
89
|
-
&::scroll-button(
|
|
90
|
-
|
|
91
|
-
content: '\
|
|
341
|
+
&::scroll-button(inline-end) {
|
|
342
|
+
inset-inline-end: var(--carousel-button-inset, 0.5rem);
|
|
343
|
+
content: '\203A' / var(--carousel-label-next, 'Next');
|
|
92
344
|
}
|
|
93
345
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
346
|
+
&:dir(rtl)::scroll-button(inline-start) {
|
|
347
|
+
content: '\203A' / var(--carousel-label-prev, 'Previous');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
&:dir(rtl)::scroll-button(inline-end) {
|
|
351
|
+
content: '\2039' / var(--carousel-label-next, 'Next');
|
|
97
352
|
}
|
|
98
353
|
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* ── Scroll markers ──────────────────────────────────────────────────────────
|
|
357
|
+
Same two-block split, same reason. With the shell in place
|
|
358
|
+
`scroll-marker-group: after` generates the group box after the scroller in the
|
|
359
|
+
shell's flow — so unlike the previous version it needs no anchor and no
|
|
360
|
+
`position: fixed`, just an absolutely positioned flex row against the shell.
|
|
361
|
+
`.carousel--markers-below` flows them under the strip instead. */
|
|
362
|
+
|
|
363
|
+
.carousel__markers {
|
|
364
|
+
position: absolute;
|
|
365
|
+
inset-inline: 0;
|
|
366
|
+
inset-block-end: var(--carousel-marker-offset, 1rem);
|
|
367
|
+
z-index: var(--z-tier-raised);
|
|
368
|
+
display: flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
justify-content: center;
|
|
371
|
+
gap: 0.5rem;
|
|
372
|
+
padding-block: 0.5rem;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.carousel--markers-below > .carousel__markers {
|
|
376
|
+
position: static;
|
|
377
|
+
inset: auto;
|
|
378
|
+
}
|
|
99
379
|
|
|
100
|
-
|
|
101
|
-
|
|
380
|
+
.carousel__marker {
|
|
381
|
+
box-sizing: border-box;
|
|
382
|
+
padding: 0;
|
|
383
|
+
border: 0;
|
|
384
|
+
inline-size: var(--carousel-marker-size, 0.75rem);
|
|
385
|
+
aspect-ratio: 1;
|
|
386
|
+
border-radius: 50%;
|
|
387
|
+
background-color: var(--carousel-marker-color, currentColor);
|
|
388
|
+
opacity: 0.4;
|
|
389
|
+
cursor: pointer;
|
|
390
|
+
transition: opacity 0.2s ease;
|
|
391
|
+
|
|
392
|
+
&[aria-selected='true'] {
|
|
393
|
+
opacity: 1;
|
|
394
|
+
background-color: var(
|
|
395
|
+
--carousel-marker-active-color,
|
|
396
|
+
var(--carousel-marker-color, currentColor)
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
&:focus-visible {
|
|
401
|
+
outline: 2px solid var(--color-border-focus);
|
|
402
|
+
outline-offset: 2px;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
@supports selector(::scroll-marker) {
|
|
407
|
+
.carousel--scroll-markers > .carousel__track,
|
|
408
|
+
.carousel--scroll-markers:not(:has(> .carousel__track)) {
|
|
102
409
|
scroll-marker-group: after;
|
|
103
410
|
|
|
104
411
|
&::scroll-marker-group {
|
|
105
|
-
position:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
margin-block-end: var(--carousel-marker-offset, 1rem);
|
|
412
|
+
position: absolute;
|
|
413
|
+
inset-inline: 0;
|
|
414
|
+
inset-block-end: var(--carousel-marker-offset, 1rem);
|
|
415
|
+
z-index: var(--z-tier-raised);
|
|
110
416
|
display: flex;
|
|
111
417
|
align-items: center;
|
|
112
418
|
justify-content: center;
|
|
@@ -114,156 +420,273 @@
|
|
|
114
420
|
padding-block: 0.5rem;
|
|
115
421
|
}
|
|
116
422
|
|
|
117
|
-
/* Non-paged: per
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
> [data-carousel-clone]::scroll-marker,
|
|
131
|
-
> [part='autoplay']::scroll-marker {
|
|
132
|
-
content: none;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
> *::scroll-marker:target-current {
|
|
136
|
-
opacity: 1;
|
|
137
|
-
background-color: var(
|
|
138
|
-
--carousel-marker-active-color,
|
|
139
|
-
var(--carousel-marker-color, currentColor)
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
> *::scroll-marker:focus-visible {
|
|
144
|
-
outline: 2px solid currentColor;
|
|
145
|
-
outline-offset: 2px;
|
|
146
|
-
}
|
|
423
|
+
/* Non-paged: one marker per slide. */
|
|
424
|
+
> *::scroll-marker {
|
|
425
|
+
content: '';
|
|
426
|
+
box-sizing: border-box;
|
|
427
|
+
padding: 0;
|
|
428
|
+
border: 0;
|
|
429
|
+
inline-size: var(--carousel-marker-size, 0.75rem);
|
|
430
|
+
aspect-ratio: 1;
|
|
431
|
+
border-radius: 50%;
|
|
432
|
+
background-color: var(--carousel-marker-color, currentColor);
|
|
433
|
+
opacity: 0.4;
|
|
434
|
+
cursor: pointer;
|
|
435
|
+
transition: opacity 0.2s ease;
|
|
147
436
|
}
|
|
148
437
|
|
|
149
|
-
/*
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
block-size: 0.75rem;
|
|
155
|
-
border-radius: 50%;
|
|
156
|
-
background-color: var(--carousel-marker-color, currentColor);
|
|
157
|
-
opacity: 0.4;
|
|
158
|
-
cursor: pointer;
|
|
159
|
-
transition: opacity 0.2s ease;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
&::column::scroll-marker:target-current {
|
|
163
|
-
opacity: 1;
|
|
164
|
-
background-color: var(
|
|
165
|
-
--carousel-marker-active-color,
|
|
166
|
-
var(--carousel-marker-color, currentColor)
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
&::column::scroll-marker:focus-visible {
|
|
171
|
-
outline: 2px solid currentColor;
|
|
172
|
-
outline-offset: 2px;
|
|
173
|
-
}
|
|
438
|
+
/* A clone is a duplicate of a slide the visitor can already reach, so it must
|
|
439
|
+
not add a dot. The autoplay button now lives on the shell, not in the
|
|
440
|
+
track, so it no longer needs suppressing here. */
|
|
441
|
+
> [data-carousel-clone]::scroll-marker {
|
|
442
|
+
content: none;
|
|
174
443
|
}
|
|
175
|
-
}
|
|
176
444
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
445
|
+
> *::scroll-marker:target-current {
|
|
446
|
+
opacity: 1;
|
|
447
|
+
background-color: var(
|
|
448
|
+
--carousel-marker-active-color,
|
|
449
|
+
var(--carousel-marker-color, currentColor)
|
|
450
|
+
);
|
|
451
|
+
}
|
|
181
452
|
|
|
182
|
-
|
|
183
|
-
|
|
453
|
+
> *::scroll-marker:focus-visible {
|
|
454
|
+
outline: 2px solid var(--color-border-focus);
|
|
455
|
+
outline-offset: 2px;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
184
458
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
459
|
+
/* Paged: one marker per column. Separate block because `::column::scroll-marker`
|
|
460
|
+
is a different origin and would otherwise double up with the `> *` rule. */
|
|
461
|
+
.carousel--scroll-markers.carousel--auto-pages > .carousel__track,
|
|
462
|
+
.carousel--scroll-markers.carousel--auto-pages:not(:has(> .carousel__track)) {
|
|
463
|
+
> *::scroll-marker {
|
|
464
|
+
content: none;
|
|
190
465
|
}
|
|
191
466
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
467
|
+
&::column::scroll-marker {
|
|
468
|
+
content: '';
|
|
469
|
+
box-sizing: border-box;
|
|
470
|
+
inline-size: var(--carousel-marker-size, 0.75rem);
|
|
471
|
+
aspect-ratio: 1;
|
|
472
|
+
border-radius: 50%;
|
|
473
|
+
background-color: var(--carousel-marker-color, currentColor);
|
|
474
|
+
opacity: 0.4;
|
|
475
|
+
cursor: pointer;
|
|
476
|
+
transition: opacity 0.2s ease;
|
|
195
477
|
}
|
|
196
|
-
}
|
|
197
478
|
|
|
198
|
-
|
|
199
|
-
& > [part='autoplay'] {
|
|
200
|
-
position: fixed;
|
|
201
|
-
position-anchor: --carousel;
|
|
202
|
-
position-area: block-end inline-end;
|
|
203
|
-
position-visibility: always;
|
|
204
|
-
margin-block-end: var(--carousel-marker-offset, 1rem);
|
|
205
|
-
margin-inline-end: 0.5rem;
|
|
206
|
-
z-index: var(--z-tier-raised);
|
|
207
|
-
display: flex;
|
|
208
|
-
align-items: center;
|
|
209
|
-
justify-content: center;
|
|
210
|
-
inline-size: 2rem;
|
|
211
|
-
block-size: 2rem;
|
|
212
|
-
border: none;
|
|
213
|
-
border-radius: 50%;
|
|
214
|
-
background: var(--color-bg-surface-muted);
|
|
215
|
-
color: var(--carousel-button-color, currentColor);
|
|
216
|
-
opacity: 0.7;
|
|
217
|
-
cursor: pointer;
|
|
218
|
-
font-size: 0.75rem;
|
|
219
|
-
transition: opacity 0.2s ease;
|
|
220
|
-
|
|
221
|
-
&:hover {
|
|
479
|
+
&::column::scroll-marker:target-current {
|
|
222
480
|
opacity: 1;
|
|
481
|
+
background-color: var(
|
|
482
|
+
--carousel-marker-active-color,
|
|
483
|
+
var(--carousel-marker-color, currentColor)
|
|
484
|
+
);
|
|
223
485
|
}
|
|
224
486
|
|
|
225
|
-
|
|
226
|
-
outline: 2px solid
|
|
487
|
+
&::column::scroll-marker:focus-visible {
|
|
488
|
+
outline: 2px solid var(--color-border-focus);
|
|
227
489
|
outline-offset: 2px;
|
|
228
490
|
}
|
|
229
491
|
}
|
|
230
492
|
|
|
231
|
-
/*
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
493
|
+
/* `--markers-below` has to reach the native group too, not just the fallback —
|
|
494
|
+
otherwise the modifier silently does nothing in the one browser that has the
|
|
495
|
+
pseudo-elements. */
|
|
496
|
+
.carousel--markers-below > .carousel__track::scroll-marker-group,
|
|
497
|
+
.carousel--markers-below:not(:has(> .carousel__track))::scroll-marker-group {
|
|
498
|
+
position: static;
|
|
499
|
+
inset: auto;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* The native group replaces the JS one; if both somehow exist, the native one
|
|
503
|
+
wins and the fallback is hidden rather than stacked on top of it. */
|
|
504
|
+
.carousel--scroll-markers > .carousel__markers {
|
|
505
|
+
display: none;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.carousel--markers-below {
|
|
510
|
+
--carousel-marker-offset: 0px;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/* ── Scroll-for-more hint ────────────────────────────────────────────────────*/
|
|
514
|
+
|
|
515
|
+
/* Above the strip and right-aligned, not overlaid on it: "Scroll for more" printed
|
|
516
|
+
across the first slide's image is frequently unreadable, and the shell exists so
|
|
517
|
+
the hint has somewhere to go that isn't the scrollport.
|
|
518
|
+
|
|
519
|
+
The row assignment is deliberate rather than incidental. Grid places explicitly
|
|
520
|
+
positioned items before auto-placed ones, so claiming row 1 here pushes the
|
|
521
|
+
track — which claims no row — to row 2. Omit the hint and nothing claims row 1,
|
|
522
|
+
the track auto-places there, and no empty track is left behind to collect
|
|
523
|
+
`row-gap`. That is why the track is NOT given `grid-row: 2` to match. */
|
|
524
|
+
.carousel__hint {
|
|
525
|
+
grid-row: 1;
|
|
526
|
+
grid-column: 1;
|
|
527
|
+
justify-self: end;
|
|
528
|
+
align-self: start;
|
|
529
|
+
margin: var(--carousel-hint-inset, 0.5rem);
|
|
530
|
+
z-index: var(--z-tier-raised);
|
|
531
|
+
pointer-events: none;
|
|
532
|
+
color: var(--color-text-surface-x-muted);
|
|
533
|
+
transition: opacity 0.3s ease;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
@keyframes carousel-hint-dim {
|
|
537
|
+
to {
|
|
538
|
+
opacity: var(--carousel-hint-dimmed, 0.2);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
@keyframes carousel-scrollbar-dim {
|
|
543
|
+
to {
|
|
544
|
+
scrollbar-color: var(--carousel-scrollbar-thumb-dim, var(--color-bg-surface-muted)) transparent;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/* Pure CSS, no JS: both read the TRACK's scroll progress through the timeline the
|
|
549
|
+
shell hoisted with `timeline-scope`. */
|
|
550
|
+
@supports (animation-timeline: scroll()) {
|
|
551
|
+
.carousel__hint {
|
|
552
|
+
animation: carousel-hint-dim linear both;
|
|
553
|
+
animation-timeline: --carousel-scroll;
|
|
554
|
+
animation-range: 0 var(--carousel-hint-range, 12%);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.carousel__track {
|
|
558
|
+
animation: carousel-scrollbar-dim linear both;
|
|
559
|
+
animation-timeline: --carousel-scroll;
|
|
560
|
+
animation-range: 0 var(--carousel-hint-range, 12%);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/* Engines with no scroll timelines get the same effect from the attribute
|
|
565
|
+
WCCarousel sets on first scroll — mirroring how animation.css cancels under
|
|
566
|
+
`@supports not (animation-timeline: view())`. */
|
|
567
|
+
@supports not (animation-timeline: scroll()) {
|
|
568
|
+
.carousel[data-scrolled] .carousel__hint {
|
|
569
|
+
opacity: var(--carousel-hint-dimmed, 0.2);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.carousel[data-scrolled] > .carousel__track {
|
|
573
|
+
scrollbar-color: var(--carousel-scrollbar-thumb-dim, var(--color-bg-surface-muted)) transparent;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* ── Autoplay toggle ─────────────────────────────────────────────────────────
|
|
578
|
+
A child of the SHELL, not the track. It used to be appended into the grid track
|
|
579
|
+
where it occupied a slide-width column, papered over with `scroll-snap-align:
|
|
580
|
+
unset` and a suppressed marker. `[part='autoplay']` is kept as a deprecated
|
|
581
|
+
alias for one release: `part` only crosses a shadow boundary and this element is
|
|
582
|
+
light DOM, so that was never a part — just an arbitrary attribute selector. */
|
|
583
|
+
.carousel__autoplay,
|
|
584
|
+
.carousel > [part='autoplay'] {
|
|
585
|
+
position: absolute;
|
|
586
|
+
inset-block-end: var(--carousel-marker-offset, 1rem);
|
|
587
|
+
inset-inline-end: 0.5rem;
|
|
588
|
+
z-index: var(--z-tier-raised);
|
|
589
|
+
box-sizing: border-box;
|
|
590
|
+
padding: 0;
|
|
591
|
+
border: 0;
|
|
592
|
+
line-height: 1;
|
|
593
|
+
display: grid;
|
|
594
|
+
place-content: center;
|
|
595
|
+
inline-size: 2rem;
|
|
596
|
+
aspect-ratio: 1;
|
|
597
|
+
border-radius: 50%;
|
|
598
|
+
background-color: var(--carousel-button-bg, var(--color-bg-surface-muted));
|
|
599
|
+
color: var(--carousel-button-color, var(--color-text-surface));
|
|
600
|
+
opacity: 0.7;
|
|
601
|
+
cursor: pointer;
|
|
602
|
+
font-size: 0.75rem;
|
|
603
|
+
transition: opacity 0.2s ease;
|
|
604
|
+
|
|
605
|
+
&:hover {
|
|
606
|
+
opacity: 1;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
&:focus-visible {
|
|
610
|
+
outline: 2px solid var(--color-border-focus);
|
|
611
|
+
outline-offset: 2px;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/* ── Inert off-screen slides ─────────────────────────────────────────────────
|
|
616
|
+
`interactivity: inert` is gated separately from the dimming: an engine without
|
|
617
|
+
it should dim only, rather than dim AND leave the content silently focusable in
|
|
618
|
+
a way the author didn't ask for. */
|
|
619
|
+
|
|
620
|
+
.carousel--inert:not(.carousel--auto-pages) > .carousel__track > *,
|
|
621
|
+
.carousel--inert:not(.carousel--auto-pages):not(:has(> .carousel__track)) > * {
|
|
622
|
+
container-type: scroll-state;
|
|
623
|
+
|
|
624
|
+
> * {
|
|
625
|
+
transition: opacity 0.5s ease;
|
|
626
|
+
|
|
627
|
+
@container not scroll-state(snapped) {
|
|
628
|
+
opacity: var(--carousel-inert-opacity, 0.25);
|
|
235
629
|
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
236
632
|
|
|
237
|
-
|
|
238
|
-
|
|
633
|
+
@supports (interactivity: inert) {
|
|
634
|
+
.carousel--inert:not(.carousel--auto-pages) > .carousel__track > * > *,
|
|
635
|
+
.carousel--inert:not(.carousel--auto-pages):not(:has(> .carousel__track)) > * > * {
|
|
636
|
+
@container not scroll-state(snapped) {
|
|
637
|
+
interactivity: inert;
|
|
239
638
|
}
|
|
240
639
|
}
|
|
241
640
|
}
|
|
242
641
|
|
|
642
|
+
.carousel--inert.carousel--auto-pages > .carousel__track > *,
|
|
643
|
+
.carousel--inert.carousel--auto-pages:not(:has(> .carousel__track)) > * {
|
|
644
|
+
animation: carousel-offscreen-inert linear both;
|
|
645
|
+
animation-timeline: view(x);
|
|
646
|
+
}
|
|
647
|
+
|
|
243
648
|
@keyframes carousel-offscreen-inert {
|
|
244
649
|
entry 0%,
|
|
245
650
|
exit 100% {
|
|
246
|
-
interactivity: inert;
|
|
247
651
|
opacity: var(--carousel-inert-opacity, 0.25);
|
|
248
652
|
}
|
|
249
653
|
|
|
250
654
|
entry 100%,
|
|
251
655
|
exit 0% {
|
|
252
|
-
interactivity: auto;
|
|
253
656
|
opacity: 1;
|
|
254
657
|
}
|
|
255
658
|
}
|
|
256
659
|
|
|
660
|
+
/* ── Force stop ──────────────────────────────────────────────────────────────*/
|
|
661
|
+
|
|
662
|
+
.carousel--force-stop:not(.carousel--auto-pages) > .carousel__track > *,
|
|
663
|
+
.carousel--force-stop:not(.carousel--auto-pages):not(:has(> .carousel__track)) > * {
|
|
664
|
+
scroll-snap-stop: always;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.carousel--force-stop.carousel--auto-pages > .carousel__track::column,
|
|
668
|
+
.carousel--force-stop.carousel--auto-pages:not(:has(> .carousel__track))::column {
|
|
669
|
+
scroll-snap-stop: always;
|
|
670
|
+
}
|
|
671
|
+
|
|
257
672
|
@media (prefers-reduced-motion: reduce) {
|
|
258
|
-
.
|
|
673
|
+
.carousel__track,
|
|
674
|
+
.carousel:not(:has(> .carousel__track)) {
|
|
259
675
|
scroll-behavior: auto;
|
|
676
|
+
}
|
|
260
677
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
678
|
+
.carousel__hint,
|
|
679
|
+
.carousel__button,
|
|
680
|
+
.carousel__marker,
|
|
681
|
+
.carousel__autoplay {
|
|
682
|
+
transition: none;
|
|
683
|
+
}
|
|
264
684
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
685
|
+
.carousel--inert > .carousel__track > * > * {
|
|
686
|
+
transition: none;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.carousel--inert.carousel--auto-pages > .carousel__track > * {
|
|
690
|
+
animation: none;
|
|
268
691
|
}
|
|
269
692
|
}
|