@getvitops/astro 5.0.0 → 6.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 +62 -0
- package/package.json +7 -5
- package/src/components/Carousel.astro +230 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
# @getvitops/astro
|
|
2
2
|
|
|
3
|
+
## 6.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- Carousel rework: a scroll strip that is operable and discoverable with no JS in every engine, and
|
|
8
|
+
real controls where the scroll-marker pseudo-elements are missing.
|
|
9
|
+
|
|
10
|
+
`::scroll-button()`, `::scroll-marker` and `::scroll-marker-group` are still Chromium-only, and
|
|
11
|
+
there is no usable polyfill (the one prototype targets the superseded syntax and only re-parses
|
|
12
|
+
inline `<style>` elements). So the carousel no longer assumes them: `<wc-carousel>` builds real
|
|
13
|
+
prev/next `<button>`s and a real dot nav where they are absent, styled from the same declarations
|
|
14
|
+
as the native ones. Which branch is live is decided **by the stylesheet** — one `@supports` sets
|
|
15
|
+
`--carousel-native-nav: 1` and the element reads it back — so the condition is written once and
|
|
16
|
+
the two cannot disagree.
|
|
17
|
+
|
|
18
|
+
**Breaking — `.carousel` is now a shell and `.carousel__track` is the scroller** (all formats). The
|
|
19
|
+
hint and the controls need a home outside the scrollport, and absolute positioning against the
|
|
20
|
+
shell replaces `position: fixed` + `anchor-name`/`position-area`, which pinned controls to a
|
|
21
|
+
viewport corner in any engine without anchor positioning. The 5.x single-element form is still
|
|
22
|
+
styled via `:not(:has(> .carousel__track))` and is promoted by the element at runtime, but it is
|
|
23
|
+
deprecated: wrap your slides in `<ul class="carousel__track">`.
|
|
24
|
+
|
|
25
|
+
**Breaking — looping is opt-in.** `<wc-carousel loop>` clones the slides; the default no longer
|
|
26
|
+
does. Cloning triples the markup, duplicates every image element and makes the scrollbar report 3×
|
|
27
|
+
the real content. `autoplay` implies `loop`. Migration: add `loop`.
|
|
28
|
+
|
|
29
|
+
**Breaking — `[part='autoplay']` is now `.carousel__autoplay`** (aliased for one release; `part`
|
|
30
|
+
only crosses a shadow boundary and this element is light DOM, so it was never a part). The button
|
|
31
|
+
is also a child of the shell rather than the track, where it used to occupy a slide-width column.
|
|
32
|
+
|
|
33
|
+
**Breaking — the host role is `group`, not `region`.** `region` is a landmark, so every carousel
|
|
34
|
+
grew an entry in the landmark list. Pass `landmark` to `<Carousel />` where it really is one.
|
|
35
|
+
|
|
36
|
+
**Changed (visual)** — the scrollbar is visible by default (`scrollbar-width: thin`, was `none`)
|
|
37
|
+
and a "Scroll for more" hint renders above the strip; both dim once the visitor scrolls, via a
|
|
38
|
+
scroll-progress timeline where available and a `data-scrolled` attribute otherwise. Opt out with
|
|
39
|
+
`.carousel--no-scrollbar` and `hint={false}`. `@getvitops/core`'s `global.css` also gains a narrow
|
|
40
|
+
media reset (`max-inline-size: 100%` on replaced elements, `block-size: auto` on `img`/`video`) —
|
|
41
|
+
deliberately without `display: block`, which would change inline images in prose.
|
|
42
|
+
|
|
43
|
+
**Added** — `@getvitops/astro/components/Carousel.astro`, a tier-3 wrapper emitting semantic
|
|
44
|
+
`<ul>`/`<li>`/`<figure>` markup with the right per-slide image priorities: the first slide eager
|
|
45
|
+
and high (the LCP candidate), the rest `lazy`/`low`/`async`. `loading="lazy"` is honoured for
|
|
46
|
+
images clipped by a horizontal scroll container, so off-screen slides defer with no JavaScript at
|
|
47
|
+
all. Also new: `carousel--markers-below` and `carousel--no-scrollbar`, and
|
|
48
|
+
`--carousel-slide-aspect`, which keeps slide heights equal whatever each image's aspect ratio is.
|
|
49
|
+
|
|
50
|
+
**Fixed** — `::scroll-button()` glyphs sat off-centre in their circles (`global.css`'s `*` reset
|
|
51
|
+
does not reach a pseudo-element, so the button was `content-box` with the UA's `padding: 1px 6px`
|
|
52
|
+
and `inline-size` + `aspect-ratio` sized an ellipse); the hover state was a no-op (it set the same
|
|
53
|
+
token as the base rule); slide heights varied with each image's aspect ratio; navigation used
|
|
54
|
+
physical `left`/`right` and so put "previous" on the wrong side in RTL; cloned slides duplicated
|
|
55
|
+
their `id`s; and `scrollBy(clientWidth)` overshot whenever `--carousel-slide-size` was not `100%`.
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- Updated dependencies
|
|
60
|
+
- @getvitops/core@6.0.0
|
|
61
|
+
- @getvitops/generator@6.0.0
|
|
62
|
+
- @getvitops/vite@6.0.0
|
|
63
|
+
- @getvitops/utils@6.0.0
|
|
64
|
+
|
|
3
65
|
## 5.0.0
|
|
4
66
|
|
|
5
67
|
### Major Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getvitops/astro",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Astro integration for the Vitops design system — favicons/PWA, web-component scripts, and CSS, via a getvitops() integration + <Head /> component.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"src/ads.ts",
|
|
17
17
|
"src/components/Subgrid.astro",
|
|
18
18
|
"src/components/Cards.astro",
|
|
19
|
+
"src/components/Carousel.astro",
|
|
19
20
|
"src/components/NodeRenderer.astro",
|
|
20
21
|
"src/components/WebComponentLoader.astro",
|
|
21
22
|
"src/components/Icon.astro",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"./Tracking.astro": "./src/components/Tracking.astro",
|
|
59
60
|
"./components/Subgrid.astro": "./src/components/Subgrid.astro",
|
|
60
61
|
"./components/Cards.astro": "./src/components/Cards.astro",
|
|
62
|
+
"./components/Carousel.astro": "./src/components/Carousel.astro",
|
|
61
63
|
"./components/NodeRenderer.astro": "./src/components/NodeRenderer.astro",
|
|
62
64
|
"./components/WebComponentLoader.astro": "./src/components/WebComponentLoader.astro",
|
|
63
65
|
"./components/Icon.astro": "./src/components/Icon.astro",
|
|
@@ -75,10 +77,10 @@
|
|
|
75
77
|
"access": "public"
|
|
76
78
|
},
|
|
77
79
|
"dependencies": {
|
|
78
|
-
"@getvitops/core": "
|
|
79
|
-
"@getvitops/generator": "
|
|
80
|
-
"@getvitops/utils": "
|
|
81
|
-
"@getvitops/vite": "
|
|
80
|
+
"@getvitops/core": "6.0.0",
|
|
81
|
+
"@getvitops/generator": "6.0.0",
|
|
82
|
+
"@getvitops/utils": "6.0.0",
|
|
83
|
+
"@getvitops/vite": "6.0.0"
|
|
82
84
|
},
|
|
83
85
|
"devDependencies": {
|
|
84
86
|
"@astrojs/sitemap": "^3.7.3",
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A scroll-snap carousel that works with no JavaScript, in every engine.
|
|
4
|
+
*
|
|
5
|
+
* Tier-3 in the framework's terms — an authoring convenience that emits the right
|
|
6
|
+
* `.carousel` markup and requires **no runtime JS**. What it renders stands on its
|
|
7
|
+
* own:
|
|
8
|
+
*
|
|
9
|
+
* - the slides are a real `<ul>`/`<li>`, so assistive tech announces list
|
|
10
|
+
* position and count;
|
|
11
|
+
* - the scroller carries `tabindex="0"`, so arrow keys scroll it — the keyboard
|
|
12
|
+
* route the platform gives any focusable scroll container, and the reason
|
|
13
|
+
* inclusive-components.design/a-content-slider/ focuses the scroller rather
|
|
14
|
+
* than building a widget;
|
|
15
|
+
* - `scroll-snap` and (in Chromium today) `::scroll-button()` / `::scroll-marker`
|
|
16
|
+
* give buttons and dots with no script at all;
|
|
17
|
+
* - a visible scrollbar and a "Scroll for more" hint say the strip scrolls, which
|
|
18
|
+
* was the missing signal wherever those pseudo-elements are absent;
|
|
19
|
+
* - off-screen images are lazy **without JS**: `loading="lazy"` is honoured for
|
|
20
|
+
* images clipped by a horizontal scroll container, so there is no `data-src` +
|
|
21
|
+
* IntersectionObserver dance to fail.
|
|
22
|
+
*
|
|
23
|
+
* **This component emits its own `<wc-carousel>`** — do NOT wrap it in one. That is
|
|
24
|
+
* the tier-3 rule ("the wrapper emits its `<wc-*>` tag with the accessible fallback
|
|
25
|
+
* inside"), and `Cards.astro`, `Tree.astro` and `CookieConsent.astro` all do the
|
|
26
|
+
* same. The element defers to an enclosing one, so a nested pair degrades to a
|
|
27
|
+
* no-op rather than cloning the same slides twice, but the single call is the
|
|
28
|
+
* intended shape.
|
|
29
|
+
*
|
|
30
|
+
* `<wc-carousel>` adds the opt-in cloned loop, autoplay, and — only where the
|
|
31
|
+
* browser lacks the scroll-marker pseudo-elements — real prev/next buttons and a
|
|
32
|
+
* real dot nav. If it never loads, that enhancement is all that is missing, which
|
|
33
|
+
* is why `enhance={false}` is a supported way to get the bare fragment.
|
|
34
|
+
*
|
|
35
|
+
* Three deliberate a11y choices:
|
|
36
|
+
*
|
|
37
|
+
* - **The group role is on the shell, the list role stays on the `<ul>`.** The
|
|
38
|
+
* inclusive-components article puts `role="region"` on the scroller because
|
|
39
|
+
* there the scroller *is* the container; a shell lets us have real list
|
|
40
|
+
* semantics *and* a named group. `group` rather than `region` because `region`
|
|
41
|
+
* is a landmark and four carousels would grow four landmark entries — pass
|
|
42
|
+
* `landmark` when the carousel really is a page-level landmark.
|
|
43
|
+
* - **The visible hint is `aria-hidden`, and a separate `.sr-only` element is the
|
|
44
|
+
* description.** The hint fades once the visitor scrolls, so as a description
|
|
45
|
+
* it would go stale; and its text is about a pointer affordance, whereas what a
|
|
46
|
+
* keyboard user needs described is the arrow keys. One string cannot honestly
|
|
47
|
+
* be both.
|
|
48
|
+
* - **`aria-describedby` sits on the `<ul>`**, which is the focusable thing, since
|
|
49
|
+
* a description is announced on focus.
|
|
50
|
+
*/
|
|
51
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
52
|
+
|
|
53
|
+
export interface CarouselSlide {
|
|
54
|
+
src: string;
|
|
55
|
+
alt: string;
|
|
56
|
+
/**
|
|
57
|
+
* Intrinsic size. Emitted as `width`/`height` so the UA derives an aspect ratio
|
|
58
|
+
* and the slide reserves space before the image lands. When unknown,
|
|
59
|
+
* `--carousel-slide-aspect` still reserves the box, so CLS is covered either way.
|
|
60
|
+
*/
|
|
61
|
+
width?: number;
|
|
62
|
+
height?: number;
|
|
63
|
+
srcset?: string;
|
|
64
|
+
sizes?: string;
|
|
65
|
+
caption?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface Props extends Omit<HTMLAttributes<'div'>, 'slot'> {
|
|
69
|
+
/** Accessible name. Required — an unnamed group is unfindable. */
|
|
70
|
+
label: string;
|
|
71
|
+
/** Image slides. Omit and use the default slot for richer slides. */
|
|
72
|
+
slides?: CarouselSlide[];
|
|
73
|
+
/** Milliseconds between slides. Implies `loop`. */
|
|
74
|
+
autoplay?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Clone the slides for a seamless infinite strip. **Off by default:** it triples
|
|
77
|
+
* the DOM and duplicates every image element, makes the scrollbar report 3× the
|
|
78
|
+
* real content, and most carousels do not want it.
|
|
79
|
+
*/
|
|
80
|
+
loop?: boolean;
|
|
81
|
+
/** Prev/next controls. */
|
|
82
|
+
buttons?: boolean;
|
|
83
|
+
/** Dot navigation. */
|
|
84
|
+
markers?: boolean;
|
|
85
|
+
/** Dots below the strip rather than overlaid on it. */
|
|
86
|
+
markersBelow?: boolean;
|
|
87
|
+
/** CSS multicol paging. Chromium-only; degrades to a plain strip elsewhere. */
|
|
88
|
+
columns?: number;
|
|
89
|
+
/** Dim and inert the off-screen slides. */
|
|
90
|
+
inert?: boolean;
|
|
91
|
+
/** `scroll-snap-stop: always` — never skip a slide in one fling. */
|
|
92
|
+
forceStop?: boolean;
|
|
93
|
+
/** Hide the scrollbar. Costs the no-JS "this scrolls" signal; be deliberate. */
|
|
94
|
+
noScrollbar?: boolean;
|
|
95
|
+
/** `--carousel-slide-aspect`, the ratio every slide's media box takes. */
|
|
96
|
+
aspect?: string;
|
|
97
|
+
/** Visible affordance text. `false` removes it. */
|
|
98
|
+
hint?: string | false;
|
|
99
|
+
/** Expose as a landmark (`role="region"`) rather than `role="group"`. */
|
|
100
|
+
landmark?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Emit the `<wc-carousel>` wrapper (default `true`).
|
|
103
|
+
*
|
|
104
|
+
* Set false where the element cannot run — an email, a PDF, a page that does not
|
|
105
|
+
* load `elements.js`. The carousel is unchanged; only the enhancement layer goes,
|
|
106
|
+
* so nothing is lost but the loop, autoplay and the non-Chromium control set.
|
|
107
|
+
*/
|
|
108
|
+
enhance?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Id prefix for the slide ids and the description. Defaults to a slug of
|
|
111
|
+
* `label`. Astro has no `useId`, and a random one would change on every build —
|
|
112
|
+
* so this is derived, deterministic and overridable when two carousels on a page
|
|
113
|
+
* share a label.
|
|
114
|
+
*/
|
|
115
|
+
id?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const {
|
|
119
|
+
label,
|
|
120
|
+
slides = [],
|
|
121
|
+
autoplay,
|
|
122
|
+
loop = false,
|
|
123
|
+
buttons = true,
|
|
124
|
+
markers = true,
|
|
125
|
+
markersBelow = false,
|
|
126
|
+
columns,
|
|
127
|
+
inert = false,
|
|
128
|
+
forceStop = false,
|
|
129
|
+
noScrollbar = false,
|
|
130
|
+
aspect = '16 / 9',
|
|
131
|
+
hint = 'Scroll for more',
|
|
132
|
+
landmark = false,
|
|
133
|
+
enhance = true,
|
|
134
|
+
id,
|
|
135
|
+
class: className,
|
|
136
|
+
role,
|
|
137
|
+
style,
|
|
138
|
+
...rest
|
|
139
|
+
} = Astro.props;
|
|
140
|
+
|
|
141
|
+
const uid =
|
|
142
|
+
id ??
|
|
143
|
+
`carousel-${
|
|
144
|
+
label
|
|
145
|
+
.toLowerCase()
|
|
146
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
147
|
+
.replace(/^-|-$/g, '') || 'x'
|
|
148
|
+
}`;
|
|
149
|
+
|
|
150
|
+
const classes = [
|
|
151
|
+
'carousel',
|
|
152
|
+
buttons && 'carousel--scroll-buttons',
|
|
153
|
+
markers && 'carousel--scroll-markers',
|
|
154
|
+
markersBelow && 'carousel--markers-below',
|
|
155
|
+
columns && 'carousel--auto-pages',
|
|
156
|
+
inert && 'carousel--inert',
|
|
157
|
+
forceStop && 'carousel--force-stop',
|
|
158
|
+
noScrollbar && 'carousel--no-scrollbar',
|
|
159
|
+
className,
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
const vars = [
|
|
163
|
+
`--carousel-slide-aspect:${aspect}`,
|
|
164
|
+
columns ? `--carousel-columns:${columns}` : '',
|
|
165
|
+
style ?? '',
|
|
166
|
+
]
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.join(';');
|
|
169
|
+
|
|
170
|
+
const Shell = enhance ? 'wc-carousel' : 'div';
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
<Shell
|
|
174
|
+
class:list={classes}
|
|
175
|
+
role={role ?? (landmark ? 'region' : 'group')}
|
|
176
|
+
aria-roledescription="carousel"
|
|
177
|
+
aria-label={label}
|
|
178
|
+
loop={enhance && (loop || (autoplay ?? 0) > 0) ? '' : undefined}
|
|
179
|
+
autoplay={enhance && autoplay ? autoplay : undefined}
|
|
180
|
+
style={vars}
|
|
181
|
+
{...rest}
|
|
182
|
+
>
|
|
183
|
+
<ul
|
|
184
|
+
class="carousel__track"
|
|
185
|
+
role="list"
|
|
186
|
+
tabindex="0"
|
|
187
|
+
aria-label={label}
|
|
188
|
+
aria-describedby={`${uid}-desc`}
|
|
189
|
+
>
|
|
190
|
+
{
|
|
191
|
+
slides.map((slide, i) => (
|
|
192
|
+
<li class="carousel__slide" id={`${uid}-slide-${i + 1}`}>
|
|
193
|
+
<figure class="carousel__media">
|
|
194
|
+
{/* The first slide is the LCP candidate, so it is eager and high
|
|
195
|
+
priority; every later one is lazy, low and async. A horizontal
|
|
196
|
+
scroll container clips them, and `loading="lazy"` is honoured for
|
|
197
|
+
that, so this is genuinely JS-free deferral. `decoding` is left
|
|
198
|
+
unset on the first: `sync` can block the main thread, and the UA's
|
|
199
|
+
own choice for an eager LCP image is better than ours. */}
|
|
200
|
+
<img
|
|
201
|
+
src={slide.src}
|
|
202
|
+
alt={slide.alt}
|
|
203
|
+
srcset={slide.srcset}
|
|
204
|
+
sizes={slide.sizes}
|
|
205
|
+
width={slide.width}
|
|
206
|
+
height={slide.height}
|
|
207
|
+
loading={i === 0 ? 'eager' : 'lazy'}
|
|
208
|
+
fetchpriority={i === 0 ? 'high' : 'low'}
|
|
209
|
+
decoding={i === 0 ? undefined : 'async'}
|
|
210
|
+
/>
|
|
211
|
+
{slide.caption && (
|
|
212
|
+
<figcaption class="carousel__caption font-caption">{slide.caption}</figcaption>
|
|
213
|
+
)}
|
|
214
|
+
</figure>
|
|
215
|
+
</li>
|
|
216
|
+
))
|
|
217
|
+
}
|
|
218
|
+
<slot />
|
|
219
|
+
</ul>
|
|
220
|
+
{
|
|
221
|
+
hint !== false && (
|
|
222
|
+
<p class="carousel__hint font-footnote" aria-hidden="true">
|
|
223
|
+
{hint}
|
|
224
|
+
</p>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
<p id={`${uid}-desc`} class="sr-only">
|
|
228
|
+
Use the left and right arrow keys, or scroll sideways, to move between slides.
|
|
229
|
+
</p>
|
|
230
|
+
</Shell>
|