@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 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": "5.0.0",
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": "5.0.0",
79
- "@getvitops/generator": "5.0.0",
80
- "@getvitops/utils": "5.0.0",
81
- "@getvitops/vite": "5.0.0"
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>