@getvitops/astro 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 +229 -0
- package/package.json +12 -5
- package/src/components/Carousel.astro +241 -0
- package/src/components/Counter.astro +80 -0
- package/src/components/Gallery.astro +210 -0
- package/src/components/ResponsiveImage.astro +52 -0
- package/src/components/Tree.astro +11 -6
- package/src/components/TreeNode.astro +13 -3
- package/src/schemas/Carousel.astro +6 -27
- package/src/schemas/LocalBusiness.astro +4 -148
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,234 @@
|
|
|
1
1
|
# @getvitops/astro
|
|
2
2
|
|
|
3
|
+
## 7.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- aad43b7: A gallery/lightbox that is a fully working modal lightbox with **zero JavaScript**, plus
|
|
8
|
+
`ImageMetadata` support in `<Carousel />`.
|
|
9
|
+
|
|
10
|
+
Only `@getvitops/core`, `generator` and `astro` change here. This changeset makes **no
|
|
11
|
+
consumer-facing change** to `utils`, `cli` or `vite` — they still ship in this release because
|
|
12
|
+
the six share one version, but from a different changeset (`localbusiness-schema.md`) that
|
|
13
|
+
touches `utils` and `generator` for an unrelated reason.
|
|
14
|
+
|
|
15
|
+
**Breaking — `css`/`bricks`/`tailwind` (all formats):** the `.lightbox` thumbnail grid is now
|
|
16
|
+
`.gallery`, so it composes with the dialog it opens (`.lightbox-dialog`, unchanged) rather than
|
|
17
|
+
sharing its name. `vitops lint --fix` cannot cover this: `movedTokens()` renames colour token
|
|
18
|
+
names only, and there is no class-rename mechanism in `@getvitops/cli`. Migrate by hand:
|
|
19
|
+
|
|
20
|
+
| Old | New |
|
|
21
|
+
| ------------------ | ----------------- |
|
|
22
|
+
| `.lightbox` | `.gallery` |
|
|
23
|
+
| `.lightbox__item` | `.gallery__item` |
|
|
24
|
+
| `.lightbox__thumb` | `.gallery__thumb` |
|
|
25
|
+
|
|
26
|
+
`.lightbox-dialog` and its `__content` / `__image` / `__close` / `__nav` / `__nav-button` /
|
|
27
|
+
`__caption` parts are unchanged.
|
|
28
|
+
|
|
29
|
+
**Added — `css`/`bricks`/`tailwind` (all formats):** `patterns/gallery.css` — the `.gallery` grid,
|
|
30
|
+
now on the token cascade (`--br-gallery` etc.) it only half-had before.
|
|
31
|
+
|
|
32
|
+
**Added — `wc`:** `<wc-gallery>` (`@getvitops/core/elements`). Wraps a `.gallery` grid of
|
|
33
|
+
`command="show-modal"` triggers and the `.lightbox-dialog`s they open. With no JS, each trigger is
|
|
34
|
+
a real Invoker Commands button for its own dialog — open and close both work. `<wc-gallery>` adds
|
|
35
|
+
what the fallback deliberately cannot contain: prev/next, arrow keys, swipe (horizontal to step,
|
|
36
|
+
vertical to dismiss), an announced image counter, neighbour preload, and a thumbnail → full-image
|
|
37
|
+
`document.startViewTransition` morph where supported. Prev/next can't exist in the fallback — a
|
|
38
|
+
second `show-modal` invoker inside an already-open dialog stacks a second modal rather than
|
|
39
|
+
replacing the first — so it is generated by the element, never authored, the same rule
|
|
40
|
+
`<wc-tree>`'s search field follows.
|
|
41
|
+
|
|
42
|
+
**Added — `astro`:** `<Gallery />` (`@getvitops/astro/components/Gallery.astro`) — emits a
|
|
43
|
+
`.gallery` grid plus one `.lightbox-dialog` per image and its own `<wc-gallery>`; do not wrap it in
|
|
44
|
+
one. Images take `string | ImageMetadata`, rendered through a new internal `<ResponsiveImage />`
|
|
45
|
+
helper: a plain string renders the same raw `<img>` every component here used before, unchanged;
|
|
46
|
+
an `astro:assets` import renders through `<Picture />` for AVIF/WebP derivatives.
|
|
47
|
+
|
|
48
|
+
**Changed — `astro`:** `<Carousel />`'s `CarouselSlide.src` now also accepts `ImageMetadata`,
|
|
49
|
+
through the same `<ResponsiveImage />` helper, plus a new `formats` prop. Every existing
|
|
50
|
+
string-`src` call site renders byte-identical markup — purely additive.
|
|
51
|
+
|
|
52
|
+
**Added — `wc`:** the Invoker Commands API (`<button command commandfor>`) polyfill
|
|
53
|
+
(`invokers-polyfill`, feature-detected via `@getvitops/core/polyfills`) — resolves the TODO
|
|
54
|
+
`polyfills.ts` carried since 4.0. `<wc-gallery>`'s fallback and `<Drawer />` (which already emitted
|
|
55
|
+
`command="show-modal"` unconditionally) both now get real cross-browser coverage rather than
|
|
56
|
+
depending on it being "too new to rely on yet." Fetched only in an engine that doesn't support the
|
|
57
|
+
API natively, so a current browser downloads none of it.
|
|
58
|
+
|
|
59
|
+
**Migration:** rename `.lightbox` → `.gallery` and `.lightbox__item`/`.lightbox__thumb` →
|
|
60
|
+
`.gallery__item`/`.gallery__thumb` wherever authored (see table above); everything else is
|
|
61
|
+
additive. Consumers using the CSS pattern with no JS need no other change — the fallback markup is
|
|
62
|
+
unchanged in shape, only the class names moved.
|
|
63
|
+
|
|
64
|
+
### Minor Changes
|
|
65
|
+
|
|
66
|
+
- **Added — `utils`.** `itemListGraph()`, a pure schema.org `ItemList` JSON-LD builder — a sibling
|
|
67
|
+
of `breadcrumbGraph()`/`localBusinessGraph()`. Extracted from `@getvitops/astro`'s `<Carousel />`
|
|
68
|
+
so any consumer can build a valid `ItemList` (Google's "all-in-one page" carousel format:
|
|
69
|
+
`ListItem.item` carrying `@type`/`name`/`url`) for content that isn't one of the six Google
|
|
70
|
+
carousel rich-result types (`Article`/`Recipe`/`Movie`/`Course`/`Restaurant`/`Product`) — the
|
|
71
|
+
entity type is no longer constrained to that set.
|
|
72
|
+
|
|
73
|
+
**Changed — `astro`.** `<Carousel />`'s `CarouselItem['type']` now accepts any string, not just
|
|
74
|
+
the six carousel rich-result types — non-breaking, the union only widens. It's a thin wrapper
|
|
75
|
+
over `itemListGraph()` now; behaviour for existing callers is unchanged.
|
|
76
|
+
|
|
77
|
+
No consumer-facing change in this release outside `utils` and `astro` — `generator`, `cli` and
|
|
78
|
+
`vite` still ship in this release only because the six share one version.
|
|
79
|
+
|
|
80
|
+
**Fixed — `core` (`css`/`bricks`/`tailwind` build output, all formats).** The CSS and HTML
|
|
81
|
+
authored inside Lit's `css`/`html` tagged templates in `<wc-*>` component sources previously
|
|
82
|
+
survived JS minification verbatim — a JS minifier treats a template literal's cooked text as
|
|
83
|
+
opaque, so `elements.js` shipped ~690 lines of indented, unminified CSS (and the equivalent in
|
|
84
|
+
HTML) inside an otherwise-minified 886-line file. Two source-level build transforms now collapse
|
|
85
|
+
both before bundling: `postcss` + `postcss-lit` + `cssnano` for `css` templates, `minify-html-literals`
|
|
86
|
+
(`conservativeCollapse: true`, so whitespace between inline elements in light-DOM components never
|
|
87
|
+
collapses to zero) for `html` templates. `elements.js` drops from 886 lines / ~101 KB to 49 lines /
|
|
88
|
+
~96 KB. A second build step deduplicates the 17 `@license` comment blocks (only 6 are textually
|
|
89
|
+
distinct) down to one verbatim copy of each — required by BSD-3/MIT/Apache-2.0's notice-retention
|
|
90
|
+
terms either way, just no longer repeated once per source file that carries it. No API change;
|
|
91
|
+
build-time only, and none of the added tooling (`postcss`, `postcss-lit`, `cssnano`,
|
|
92
|
+
`minify-html-literals`) is a runtime or published dependency.
|
|
93
|
+
|
|
94
|
+
- 7b391b3: Add `<wc-counter>` — animates a number from a start value to the value already in its fallback
|
|
95
|
+
text, on intersection. The stat/percentage figure in a media tile ("0 → 94%"), a KPI row, a
|
|
96
|
+
metrics band.
|
|
97
|
+
|
|
98
|
+
No consumer-facing change in this release outside `core`, `generator` and `astro` — `utils`,
|
|
99
|
+
`cli` and `vite` still ship in this release only because the six share one version.
|
|
100
|
+
|
|
101
|
+
**Added — `css`/`bricks`/`tailwind` (all formats):** `.counter` / `.counter__value` /
|
|
102
|
+
`.counter__presentation` (`patterns/counter.css`). A registered `--_n` custom property drives the
|
|
103
|
+
count via a plain CSS `@keyframes` animation — the interpolation and the easing curve are CSS's,
|
|
104
|
+
never duplicated in JS.
|
|
105
|
+
|
|
106
|
+
**Added — `wc` (`@getvitops/core`).** `<wc-counter>` parses the final value out of its own
|
|
107
|
+
`.counter__value` fallback text — never from a separate attribute, so the no-JS reading and the
|
|
108
|
+
animated reading can't disagree — and renders it back through `Intl.NumberFormat`, seeded from the
|
|
109
|
+
fallback text's own separators (a grouped `1,284` never animates through ungrouped intermediates).
|
|
110
|
+
`counter()` cannot do that part: no decimals, no locale grouping, which is the one thing here that
|
|
111
|
+
genuinely needs JavaScript.
|
|
112
|
+
- Triggers on intersection via a single shared `IntersectionObserver`, not on upgrade — a counter
|
|
113
|
+
below the fold that finished before the visitor scrolled to it would have done nothing.
|
|
114
|
+
- `prefers-reduced-motion: reduce` means don't animate at all, checked before the observer is even
|
|
115
|
+
created, with `patterns/counter.css`'s own reduced-motion query as a second belt.
|
|
116
|
+
- The accessible name stays the final value for the whole animation: `.counter__value` is toggled
|
|
117
|
+
to the framework's `.sr-only` utility while running, never hidden — `display: none`/`visibility:
|
|
118
|
+
hidden` would pull it out of the accessible-name computation along with the visual hiding, and a
|
|
119
|
+
screen reader must never be read ~60 intermediate numbers. The animated text lives in a separate
|
|
120
|
+
`aria-hidden` presentation span it never reaches.
|
|
121
|
+
- `data-from` (default `0`), `data-duration` (ms, default `1200`), `data-easing` (`linear` |
|
|
122
|
+
`ease-in` | `ease-out` | `float-sine` | `float-bounce` — maps onto the real
|
|
123
|
+
`--custom-ease-*`/`--ease-float-*` tokens, so a curve edited in `animation.css` cannot drift out
|
|
124
|
+
of step with this element), `data-decimals` (inferred from the fallback text when absent).
|
|
125
|
+
- Never leaves an intermediate value on screen as the resting state: disconnecting mid-animation
|
|
126
|
+
restores the fallback immediately, and it was never mutated in the first place — only the
|
|
127
|
+
presentation span was ever written to.
|
|
128
|
+
|
|
129
|
+
**Added — `astro`.** `<Counter value="94%" />` emits its own `<wc-counter>` — do NOT wrap it in
|
|
130
|
+
one, same rule as `Gallery`/`Carousel`/`Tree`. `value` is the only source of the final figure;
|
|
131
|
+
there is no separate `to` prop, for the same one-source-of-truth reason the element itself parses
|
|
132
|
+
its fallback rather than taking a duplicate attribute. `enhance={false}` emits the bare fallback
|
|
133
|
+
for a page that cannot run `elements.js`.
|
|
134
|
+
|
|
135
|
+
### Patch Changes
|
|
136
|
+
|
|
137
|
+
- 2b50f9e: Extract the LocalBusiness JSON-LD builder out of `<LocalBusiness />` into a reusable
|
|
138
|
+
`localBusinessGraph()`, and add the location fields listing platforms (GBP, Bing Places, Apple
|
|
139
|
+
Business Connect) actually need.
|
|
140
|
+
|
|
141
|
+
**Added — `@getvitops/utils`.** `localBusinessGraph(options)` (+ its `LocalBusinessGraphOptions`,
|
|
142
|
+
`OpeningHoursSpecification` and `SpecialHoursSpecification` types) is now exported from
|
|
143
|
+
`@getvitops/utils`, alongside the existing `organizationGraph`/`articleGraph`/`breadcrumbGraph`/
|
|
144
|
+
`faqGraph` builders — same module, same shape, usable anywhere a JSON-LD graph is built outside an
|
|
145
|
+
Astro component.
|
|
146
|
+
|
|
147
|
+
**Added — `authoring`/`config` (`@getvitops/generator`).** Four new `organization.locations.<slug>`
|
|
148
|
+
fields: `hoursSpecial` (dated deviations from the recurring `hours` — holiday hours, a one-off
|
|
149
|
+
closure), `photos` (`ImageRefSchema[]` → JSON-LD `image`), `sameAs` (this location's own listing
|
|
150
|
+
URLs, distinct from `organization.sameAs`, which is the company overall), and `listings` (external
|
|
151
|
+
listing ids keyed by platform — `google` | `bing` | `apple` — not consumed by the generator today;
|
|
152
|
+
recorded so a future listings-sync command has a stable id to match against).
|
|
153
|
+
|
|
154
|
+
**Changed — `astro`.** `<LocalBusiness />`'s `Props` type is now `LocalBusinessGraphOptions`
|
|
155
|
+
(re-exported, not redefined) rather than a local interface that had drifted from what the builder
|
|
156
|
+
actually accepted. No markup or prop-name change for an existing consumer.
|
|
157
|
+
|
|
158
|
+
- Updated dependencies [c0c092b]
|
|
159
|
+
- Updated dependencies
|
|
160
|
+
- Updated dependencies [aad43b7]
|
|
161
|
+
- Updated dependencies [2b50f9e]
|
|
162
|
+
- Updated dependencies [a6a8b39]
|
|
163
|
+
- Updated dependencies [7b391b3]
|
|
164
|
+
- Updated dependencies [eb9ac79]
|
|
165
|
+
- @getvitops/utils@7.0.0
|
|
166
|
+
- @getvitops/generator@7.0.0
|
|
167
|
+
- @getvitops/core@7.0.0
|
|
168
|
+
- @getvitops/vite@7.0.0
|
|
169
|
+
|
|
170
|
+
## 6.0.0
|
|
171
|
+
|
|
172
|
+
### Major Changes
|
|
173
|
+
|
|
174
|
+
- Carousel rework: a scroll strip that is operable and discoverable with no JS in every engine, and
|
|
175
|
+
real controls where the scroll-marker pseudo-elements are missing.
|
|
176
|
+
|
|
177
|
+
`::scroll-button()`, `::scroll-marker` and `::scroll-marker-group` are still Chromium-only, and
|
|
178
|
+
there is no usable polyfill (the one prototype targets the superseded syntax and only re-parses
|
|
179
|
+
inline `<style>` elements). So the carousel no longer assumes them: `<wc-carousel>` builds real
|
|
180
|
+
prev/next `<button>`s and a real dot nav where they are absent, styled from the same declarations
|
|
181
|
+
as the native ones. Which branch is live is decided **by the stylesheet** — one `@supports` sets
|
|
182
|
+
`--carousel-native-nav: 1` and the element reads it back — so the condition is written once and
|
|
183
|
+
the two cannot disagree.
|
|
184
|
+
|
|
185
|
+
**Breaking — `.carousel` is now a shell and `.carousel__track` is the scroller** (all formats). The
|
|
186
|
+
hint and the controls need a home outside the scrollport, and absolute positioning against the
|
|
187
|
+
shell replaces `position: fixed` + `anchor-name`/`position-area`, which pinned controls to a
|
|
188
|
+
viewport corner in any engine without anchor positioning. The 5.x single-element form is still
|
|
189
|
+
styled via `:not(:has(> .carousel__track))` and is promoted by the element at runtime, but it is
|
|
190
|
+
deprecated: wrap your slides in `<ul class="carousel__track">`.
|
|
191
|
+
|
|
192
|
+
**Breaking — looping is opt-in.** `<wc-carousel loop>` clones the slides; the default no longer
|
|
193
|
+
does. Cloning triples the markup, duplicates every image element and makes the scrollbar report 3×
|
|
194
|
+
the real content. `autoplay` implies `loop`. Migration: add `loop`.
|
|
195
|
+
|
|
196
|
+
**Breaking — `[part='autoplay']` is now `.carousel__autoplay`** (aliased for one release; `part`
|
|
197
|
+
only crosses a shadow boundary and this element is light DOM, so it was never a part). The button
|
|
198
|
+
is also a child of the shell rather than the track, where it used to occupy a slide-width column.
|
|
199
|
+
|
|
200
|
+
**Breaking — the host role is `group`, not `region`.** `region` is a landmark, so every carousel
|
|
201
|
+
grew an entry in the landmark list. Pass `landmark` to `<Carousel />` where it really is one.
|
|
202
|
+
|
|
203
|
+
**Changed (visual)** — the scrollbar is visible by default (`scrollbar-width: thin`, was `none`)
|
|
204
|
+
and a "Scroll for more" hint renders above the strip; both dim once the visitor scrolls, via a
|
|
205
|
+
scroll-progress timeline where available and a `data-scrolled` attribute otherwise. Opt out with
|
|
206
|
+
`.carousel--no-scrollbar` and `hint={false}`. `@getvitops/core`'s `global.css` also gains a narrow
|
|
207
|
+
media reset (`max-inline-size: 100%` on replaced elements, `block-size: auto` on `img`/`video`) —
|
|
208
|
+
deliberately without `display: block`, which would change inline images in prose.
|
|
209
|
+
|
|
210
|
+
**Added** — `@getvitops/astro/components/Carousel.astro`, a tier-3 wrapper emitting semantic
|
|
211
|
+
`<ul>`/`<li>`/`<figure>` markup with the right per-slide image priorities: the first slide eager
|
|
212
|
+
and high (the LCP candidate), the rest `lazy`/`low`/`async`. `loading="lazy"` is honoured for
|
|
213
|
+
images clipped by a horizontal scroll container, so off-screen slides defer with no JavaScript at
|
|
214
|
+
all. Also new: `carousel--markers-below` and `carousel--no-scrollbar`, and
|
|
215
|
+
`--carousel-slide-aspect`, which keeps slide heights equal whatever each image's aspect ratio is.
|
|
216
|
+
|
|
217
|
+
**Fixed** — `::scroll-button()` glyphs sat off-centre in their circles (`global.css`'s `*` reset
|
|
218
|
+
does not reach a pseudo-element, so the button was `content-box` with the UA's `padding: 1px 6px`
|
|
219
|
+
and `inline-size` + `aspect-ratio` sized an ellipse); the hover state was a no-op (it set the same
|
|
220
|
+
token as the base rule); slide heights varied with each image's aspect ratio; navigation used
|
|
221
|
+
physical `left`/`right` and so put "previous" on the wrong side in RTL; cloned slides duplicated
|
|
222
|
+
their `id`s; and `scrollBy(clientWidth)` overshot whenever `--carousel-slide-size` was not `100%`.
|
|
223
|
+
|
|
224
|
+
### Patch Changes
|
|
225
|
+
|
|
226
|
+
- Updated dependencies
|
|
227
|
+
- @getvitops/core@6.0.0
|
|
228
|
+
- @getvitops/generator@6.0.0
|
|
229
|
+
- @getvitops/vite@6.0.0
|
|
230
|
+
- @getvitops/utils@6.0.0
|
|
231
|
+
|
|
3
232
|
## 5.0.0
|
|
4
233
|
|
|
5
234
|
### Major Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getvitops/astro",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.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,10 @@
|
|
|
16
16
|
"src/ads.ts",
|
|
17
17
|
"src/components/Subgrid.astro",
|
|
18
18
|
"src/components/Cards.astro",
|
|
19
|
+
"src/components/Carousel.astro",
|
|
20
|
+
"src/components/Gallery.astro",
|
|
21
|
+
"src/components/ResponsiveImage.astro",
|
|
22
|
+
"src/components/Counter.astro",
|
|
19
23
|
"src/components/NodeRenderer.astro",
|
|
20
24
|
"src/components/WebComponentLoader.astro",
|
|
21
25
|
"src/components/Icon.astro",
|
|
@@ -58,6 +62,9 @@
|
|
|
58
62
|
"./Tracking.astro": "./src/components/Tracking.astro",
|
|
59
63
|
"./components/Subgrid.astro": "./src/components/Subgrid.astro",
|
|
60
64
|
"./components/Cards.astro": "./src/components/Cards.astro",
|
|
65
|
+
"./components/Carousel.astro": "./src/components/Carousel.astro",
|
|
66
|
+
"./components/Gallery.astro": "./src/components/Gallery.astro",
|
|
67
|
+
"./components/Counter.astro": "./src/components/Counter.astro",
|
|
61
68
|
"./components/NodeRenderer.astro": "./src/components/NodeRenderer.astro",
|
|
62
69
|
"./components/WebComponentLoader.astro": "./src/components/WebComponentLoader.astro",
|
|
63
70
|
"./components/Icon.astro": "./src/components/Icon.astro",
|
|
@@ -75,10 +82,10 @@
|
|
|
75
82
|
"access": "public"
|
|
76
83
|
},
|
|
77
84
|
"dependencies": {
|
|
78
|
-
"@getvitops/core": "
|
|
79
|
-
"@getvitops/
|
|
80
|
-
"@getvitops/
|
|
81
|
-
"@getvitops/vite": "
|
|
85
|
+
"@getvitops/core": "7.0.0",
|
|
86
|
+
"@getvitops/utils": "7.0.0",
|
|
87
|
+
"@getvitops/generator": "7.0.0",
|
|
88
|
+
"@getvitops/vite": "7.0.0"
|
|
82
89
|
},
|
|
83
90
|
"devDependencies": {
|
|
84
91
|
"@astrojs/sitemap": "^3.7.3",
|
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
import ResponsiveImage from './ResponsiveImage.astro';
|
|
53
|
+
import type { ImageFormat } from './ResponsiveImage.astro';
|
|
54
|
+
|
|
55
|
+
export interface CarouselSlide {
|
|
56
|
+
/** A plain string renders a raw `<img>`; an `astro:assets` import renders through
|
|
57
|
+
* `<Picture />` for AVIF/WebP derivatives — see `ResponsiveImage.astro`. */
|
|
58
|
+
src: string | ImageMetadata;
|
|
59
|
+
alt: string;
|
|
60
|
+
/**
|
|
61
|
+
* Intrinsic size. Emitted as `width`/`height` so the UA derives an aspect ratio
|
|
62
|
+
* and the slide reserves space before the image lands. When unknown,
|
|
63
|
+
* `--carousel-slide-aspect` still reserves the box, so CLS is covered either way.
|
|
64
|
+
* Only meaningful for a plain-string `src` — an `ImageMetadata` source's natural
|
|
65
|
+
* size is already known.
|
|
66
|
+
*/
|
|
67
|
+
width?: number;
|
|
68
|
+
height?: number;
|
|
69
|
+
/** Only meaningful for a plain-string `src` — see `ResponsiveImage.astro`. */
|
|
70
|
+
srcset?: string;
|
|
71
|
+
sizes?: string;
|
|
72
|
+
caption?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface Props extends Omit<HTMLAttributes<'div'>, 'slot'> {
|
|
76
|
+
/** Accessible name. Required — an unnamed group is unfindable. */
|
|
77
|
+
label: string;
|
|
78
|
+
/** Image slides. Omit and use the default slot for richer slides. */
|
|
79
|
+
slides?: CarouselSlide[];
|
|
80
|
+
/** Passed to every `<Picture formats>` for an `ImageMetadata` slide. Default `['avif', 'webp']`. */
|
|
81
|
+
formats?: ImageFormat[];
|
|
82
|
+
/** Milliseconds between slides. Implies `loop`. */
|
|
83
|
+
autoplay?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Clone the slides for a seamless infinite strip. **Off by default:** it triples
|
|
86
|
+
* the DOM and duplicates every image element, makes the scrollbar report 3× the
|
|
87
|
+
* real content, and most carousels do not want it.
|
|
88
|
+
*/
|
|
89
|
+
loop?: boolean;
|
|
90
|
+
/** Prev/next controls. */
|
|
91
|
+
buttons?: boolean;
|
|
92
|
+
/** Dot navigation. */
|
|
93
|
+
markers?: boolean;
|
|
94
|
+
/** Dots below the strip rather than overlaid on it. */
|
|
95
|
+
markersBelow?: boolean;
|
|
96
|
+
/** CSS multicol paging. Chromium-only; degrades to a plain strip elsewhere. */
|
|
97
|
+
columns?: number;
|
|
98
|
+
/** Dim and inert the off-screen slides. */
|
|
99
|
+
inert?: boolean;
|
|
100
|
+
/** `scroll-snap-stop: always` — never skip a slide in one fling. */
|
|
101
|
+
forceStop?: boolean;
|
|
102
|
+
/** Hide the scrollbar. Costs the no-JS "this scrolls" signal; be deliberate. */
|
|
103
|
+
noScrollbar?: boolean;
|
|
104
|
+
/** `--carousel-slide-aspect`, the ratio every slide's media box takes. */
|
|
105
|
+
aspect?: string;
|
|
106
|
+
/** Visible affordance text. `false` removes it. */
|
|
107
|
+
hint?: string | false;
|
|
108
|
+
/** Expose as a landmark (`role="region"`) rather than `role="group"`. */
|
|
109
|
+
landmark?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Emit the `<wc-carousel>` wrapper (default `true`).
|
|
112
|
+
*
|
|
113
|
+
* Set false where the element cannot run — an email, a PDF, a page that does not
|
|
114
|
+
* load `elements.js`. The carousel is unchanged; only the enhancement layer goes,
|
|
115
|
+
* so nothing is lost but the loop, autoplay and the non-Chromium control set.
|
|
116
|
+
*/
|
|
117
|
+
enhance?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Id prefix for the slide ids and the description. Defaults to a slug of
|
|
120
|
+
* `label`. Astro has no `useId`, and a random one would change on every build —
|
|
121
|
+
* so this is derived, deterministic and overridable when two carousels on a page
|
|
122
|
+
* share a label.
|
|
123
|
+
*/
|
|
124
|
+
id?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const {
|
|
128
|
+
label,
|
|
129
|
+
slides = [],
|
|
130
|
+
formats = ['avif', 'webp'],
|
|
131
|
+
autoplay,
|
|
132
|
+
loop = false,
|
|
133
|
+
buttons = true,
|
|
134
|
+
markers = true,
|
|
135
|
+
markersBelow = false,
|
|
136
|
+
columns,
|
|
137
|
+
inert = false,
|
|
138
|
+
forceStop = false,
|
|
139
|
+
noScrollbar = false,
|
|
140
|
+
aspect = '16 / 9',
|
|
141
|
+
hint = 'Scroll for more',
|
|
142
|
+
landmark = false,
|
|
143
|
+
enhance = true,
|
|
144
|
+
id,
|
|
145
|
+
class: className,
|
|
146
|
+
role,
|
|
147
|
+
style,
|
|
148
|
+
...rest
|
|
149
|
+
} = Astro.props;
|
|
150
|
+
|
|
151
|
+
const uid =
|
|
152
|
+
id ??
|
|
153
|
+
`carousel-${
|
|
154
|
+
label
|
|
155
|
+
.toLowerCase()
|
|
156
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
157
|
+
.replace(/^-|-$/g, '') || 'x'
|
|
158
|
+
}`;
|
|
159
|
+
|
|
160
|
+
const classes = [
|
|
161
|
+
'carousel',
|
|
162
|
+
buttons && 'carousel--scroll-buttons',
|
|
163
|
+
markers && 'carousel--scroll-markers',
|
|
164
|
+
markersBelow && 'carousel--markers-below',
|
|
165
|
+
columns && 'carousel--auto-pages',
|
|
166
|
+
inert && 'carousel--inert',
|
|
167
|
+
forceStop && 'carousel--force-stop',
|
|
168
|
+
noScrollbar && 'carousel--no-scrollbar',
|
|
169
|
+
className,
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const vars = [
|
|
173
|
+
`--carousel-slide-aspect:${aspect}`,
|
|
174
|
+
columns ? `--carousel-columns:${columns}` : '',
|
|
175
|
+
style ?? '',
|
|
176
|
+
]
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.join(';');
|
|
179
|
+
|
|
180
|
+
const Shell = enhance ? 'wc-carousel' : 'div';
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
<Shell
|
|
184
|
+
class:list={classes}
|
|
185
|
+
role={role ?? (landmark ? 'region' : 'group')}
|
|
186
|
+
aria-roledescription="carousel"
|
|
187
|
+
aria-label={label}
|
|
188
|
+
loop={enhance && (loop || (autoplay ?? 0) > 0) ? '' : undefined}
|
|
189
|
+
autoplay={enhance && autoplay ? autoplay : undefined}
|
|
190
|
+
style={vars}
|
|
191
|
+
{...rest}
|
|
192
|
+
>
|
|
193
|
+
<ul
|
|
194
|
+
class="carousel__track"
|
|
195
|
+
role="list"
|
|
196
|
+
tabindex="0"
|
|
197
|
+
aria-label={label}
|
|
198
|
+
aria-describedby={`${uid}-desc`}
|
|
199
|
+
>
|
|
200
|
+
{
|
|
201
|
+
slides.map((slide, i) => (
|
|
202
|
+
<li class="carousel__slide" id={`${uid}-slide-${i + 1}`}>
|
|
203
|
+
<figure class="carousel__media">
|
|
204
|
+
{/* The first slide is the LCP candidate, so it is eager and high
|
|
205
|
+
priority; every later one is lazy, low and async. A horizontal
|
|
206
|
+
scroll container clips them, and `loading="lazy"` is honoured for
|
|
207
|
+
that, so this is genuinely JS-free deferral. `decoding` is left
|
|
208
|
+
unset on the first: `sync` can block the main thread, and the UA's
|
|
209
|
+
own choice for an eager LCP image is better than ours. */}
|
|
210
|
+
<ResponsiveImage
|
|
211
|
+
src={slide.src}
|
|
212
|
+
alt={slide.alt}
|
|
213
|
+
formats={formats}
|
|
214
|
+
srcset={slide.srcset}
|
|
215
|
+
sizes={slide.sizes}
|
|
216
|
+
width={slide.width}
|
|
217
|
+
height={slide.height}
|
|
218
|
+
loading={i === 0 ? 'eager' : 'lazy'}
|
|
219
|
+
fetchpriority={i === 0 ? 'high' : 'low'}
|
|
220
|
+
decoding={i === 0 ? undefined : 'async'}
|
|
221
|
+
/>
|
|
222
|
+
{slide.caption && (
|
|
223
|
+
<figcaption class="carousel__caption font-caption">{slide.caption}</figcaption>
|
|
224
|
+
)}
|
|
225
|
+
</figure>
|
|
226
|
+
</li>
|
|
227
|
+
))
|
|
228
|
+
}
|
|
229
|
+
<slot />
|
|
230
|
+
</ul>
|
|
231
|
+
{
|
|
232
|
+
hint !== false && (
|
|
233
|
+
<p class="carousel__hint font-footnote" aria-hidden="true">
|
|
234
|
+
{hint}
|
|
235
|
+
</p>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
<p id={`${uid}-desc`} class="sr-only">
|
|
239
|
+
Use the left and right arrow keys, or scroll sideways, to move between slides.
|
|
240
|
+
</p>
|
|
241
|
+
</Shell>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A `.counter` figure that counts up from a start value to `value` once it
|
|
4
|
+
* scrolls into view — the stat/percentage figure in a media tile ("0 → 94%"),
|
|
5
|
+
* a KPI row, a metrics band.
|
|
6
|
+
*
|
|
7
|
+
* Tier-3 in the framework's terms — an authoring convenience that emits the
|
|
8
|
+
* right `.counter` markup and needs no runtime JS to be usable: with no JS,
|
|
9
|
+
* `value` is simply the number on the page, in plain semantic text. Nothing is
|
|
10
|
+
* missing, there is only no count-up.
|
|
11
|
+
*
|
|
12
|
+
* **This component emits its own `<wc-counter>`** — do NOT wrap it in one.
|
|
13
|
+
* That is the tier-3 rule ("the wrapper emits its `<wc-*>` tag with the
|
|
14
|
+
* accessible fallback inside"), and `Cards.astro`, `Carousel.astro`,
|
|
15
|
+
* `Tree.astro` and `Gallery.astro` all do the same. The element defers to an
|
|
16
|
+
* enclosing one, so a nested pair degrades to a no-op rather than
|
|
17
|
+
* double-enhancing, but the single call is the intended shape.
|
|
18
|
+
*
|
|
19
|
+
* `<wc-counter>` adds the count-up itself, triggered once the figure scrolls
|
|
20
|
+
* into view and skipped entirely under `prefers-reduced-motion: reduce`. If it
|
|
21
|
+
* never loads, that animation is all that's missing — set `enhance={false}`
|
|
22
|
+
* for the bare fragment (an email, a PDF, a page that doesn't load
|
|
23
|
+
* `elements.js`).
|
|
24
|
+
*
|
|
25
|
+
* `value` is the ONLY source of the final figure — there is no separate `to`
|
|
26
|
+
* prop. `<wc-counter>` parses it back out of the rendered text at runtime, so
|
|
27
|
+
* this component cannot render one number while claiming to animate to a
|
|
28
|
+
* different one.
|
|
29
|
+
*/
|
|
30
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
31
|
+
|
|
32
|
+
export interface Props extends Omit<HTMLAttributes<'span'>, 'slot'> {
|
|
33
|
+
/** The finished figure, e.g. `"94%"`, `"$1,234.50"`, `"1,284×"`. */
|
|
34
|
+
value: string | number;
|
|
35
|
+
/** Starting value the animation counts from. Default `0`. */
|
|
36
|
+
from?: number;
|
|
37
|
+
/** Animation duration in ms. Default `1200`. */
|
|
38
|
+
duration?: number;
|
|
39
|
+
/**
|
|
40
|
+
* A named curve from the framework's animation tokens — `--custom-ease-in`,
|
|
41
|
+
* `--custom-ease-out`, `--ease-float-sine`, `--ease-float-bounce` — or
|
|
42
|
+
* `"linear"`. Default `--custom-ease-out`. An unrecognised name is ignored.
|
|
43
|
+
*/
|
|
44
|
+
easing?: 'linear' | 'ease-in' | 'ease-out' | 'float-sine' | 'float-bounce';
|
|
45
|
+
/** Decimal places to render. Inferred from `value`'s own text when absent. */
|
|
46
|
+
decimals?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Emit the `<wc-counter>` wrapper (default `true`).
|
|
49
|
+
*
|
|
50
|
+
* Set false where the element cannot run — an email, a PDF, a page that
|
|
51
|
+
* does not load `elements.js`. `value` is unchanged; only the count-up
|
|
52
|
+
* animation goes.
|
|
53
|
+
*/
|
|
54
|
+
enhance?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
value,
|
|
59
|
+
from,
|
|
60
|
+
duration,
|
|
61
|
+
easing,
|
|
62
|
+
decimals,
|
|
63
|
+
enhance = true,
|
|
64
|
+
class: className,
|
|
65
|
+
...rest
|
|
66
|
+
} = Astro.props;
|
|
67
|
+
|
|
68
|
+
const Shell = enhance ? 'wc-counter' : 'span';
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
<Shell
|
|
72
|
+
class:list={['counter', className]}
|
|
73
|
+
data-from={enhance ? from : undefined}
|
|
74
|
+
data-duration={enhance ? duration : undefined}
|
|
75
|
+
data-easing={enhance ? easing : undefined}
|
|
76
|
+
data-decimals={enhance ? decimals : undefined}
|
|
77
|
+
{...rest}
|
|
78
|
+
>
|
|
79
|
+
<span class="counter__value">{value}</span>
|
|
80
|
+
</Shell>
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A `.gallery` thumbnail grid whose triggers open per-image `.lightbox-dialog`s —
|
|
4
|
+
* a fully working modal lightbox with zero JavaScript.
|
|
5
|
+
*
|
|
6
|
+
* Tier-3 in the framework's terms — an authoring convenience that emits the right
|
|
7
|
+
* `.gallery` / `.lightbox-dialog` markup and needs no runtime JS to be usable:
|
|
8
|
+
*
|
|
9
|
+
* - each thumbnail is a real `command="show-modal" commandfor="…"` invoker
|
|
10
|
+
* button for its own `<dialog>`, so open and close both work with the
|
|
11
|
+
* Invoker Commands API alone (universal via `@getvitops/core/polyfills`);
|
|
12
|
+
* - the dialogs are real `<dialog closedby="any">`, so Escape and a backdrop
|
|
13
|
+
* click close them with no script either;
|
|
14
|
+
* - what's missing without JS is prev/next — deliberately, because a
|
|
15
|
+
* `command="show-modal"` button inside an already-open dialog would stack a
|
|
16
|
+
* second modal on top rather than replace the first. That control is
|
|
17
|
+
* generated by `<wc-gallery>`, never authored, the same rule `<wc-tree>`'s
|
|
18
|
+
* search field follows: a control that does nothing is worse than none.
|
|
19
|
+
*
|
|
20
|
+
* **This component emits its own `<wc-gallery>`** — do NOT wrap it in one. That
|
|
21
|
+
* is the tier-3 rule ("the wrapper emits its `<wc-*>` tag with the accessible
|
|
22
|
+
* fallback inside"), and `Cards.astro`, `Carousel.astro` and `Tree.astro` all do
|
|
23
|
+
* the same. The element defers to an enclosing one, so a nested pair degrades to
|
|
24
|
+
* a no-op rather than double-enhancing, but the single call is the intended shape.
|
|
25
|
+
*
|
|
26
|
+
* `<wc-gallery>` adds prev/next, arrow keys, swipe, an image counter, neighbour
|
|
27
|
+
* preload and a thumbnail → full-image view-transition morph. If it never loads,
|
|
28
|
+
* that enhancement is all that's missing — set `enhance={false}` for the bare
|
|
29
|
+
* fragment (an email, a PDF, a page that doesn't load `elements.js`).
|
|
30
|
+
*
|
|
31
|
+
* Images take `string | ImageMetadata` via `<ResponsiveImage />`: a plain string
|
|
32
|
+
* renders a raw `<img>` (unchanged from before `ImageMetadata` support existed);
|
|
33
|
+
* an `astro:assets` import renders through `<Picture />` for AVIF/WebP
|
|
34
|
+
* derivatives. `formats` is passed straight through, including a value
|
|
35
|
+
* astro:assets doesn't recognise yet — see `ResponsiveImage.astro`'s docblock for
|
|
36
|
+
* why (no hard-coded closed format union).
|
|
37
|
+
*/
|
|
38
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
39
|
+
import ResponsiveImage from './ResponsiveImage.astro';
|
|
40
|
+
import type { ImageFormat } from './ResponsiveImage.astro';
|
|
41
|
+
|
|
42
|
+
export interface GalleryImage {
|
|
43
|
+
/** Full-size image, shown in the dialog. */
|
|
44
|
+
src: string | ImageMetadata;
|
|
45
|
+
/** Thumbnail. Defaults to `src` — for an `ImageMetadata` source, rendered at `thumbWidth`. */
|
|
46
|
+
thumb?: string | ImageMetadata;
|
|
47
|
+
/** Pixel width to request for the thumbnail derivative when `src`/`thumb` is `ImageMetadata`. */
|
|
48
|
+
thumbWidth?: number;
|
|
49
|
+
alt: string;
|
|
50
|
+
caption?: string;
|
|
51
|
+
width?: number;
|
|
52
|
+
height?: number;
|
|
53
|
+
/** Only meaningful when `src` is a plain string — see `ResponsiveImage.astro`. */
|
|
54
|
+
srcset?: string;
|
|
55
|
+
sizes?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Props extends Omit<HTMLAttributes<'div'>, 'slot'> {
|
|
59
|
+
/** Accessible name for the grid. Required — an unnamed group is unfindable. */
|
|
60
|
+
label: string;
|
|
61
|
+
images?: GalleryImage[];
|
|
62
|
+
/** Passed to every `<Picture formats>`. Default `['avif', 'webp']`. */
|
|
63
|
+
formats?: ImageFormat[];
|
|
64
|
+
/** `--gallery-thumb-size` — minimum thumbnail track width. Default `150px`. */
|
|
65
|
+
thumbSize?: string;
|
|
66
|
+
/** `--gallery-gap`. Default `0.5rem`. */
|
|
67
|
+
gap?: string;
|
|
68
|
+
/** `--gallery-aspect` — every thumbnail's box ratio. Default `1`. */
|
|
69
|
+
aspect?: string;
|
|
70
|
+
/** Wrap prev/next past the ends instead of stopping there. */
|
|
71
|
+
loop?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Emit the `<wc-gallery>` wrapper (default `true`).
|
|
74
|
+
*
|
|
75
|
+
* Set false where the element cannot run — an email, a PDF, a page that does
|
|
76
|
+
* not load `elements.js`. The gallery is unchanged; only the enhancement
|
|
77
|
+
* layer goes, so nothing is lost but prev/next, swipe, the counter and the
|
|
78
|
+
* view-transition morph.
|
|
79
|
+
*/
|
|
80
|
+
enhance?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Id prefix for the dialog ids. Defaults to a slug of `label`. Astro has no
|
|
83
|
+
* `useId`, and a random one would change on every build — so this is derived,
|
|
84
|
+
* deterministic and overridable when two galleries on a page share a label.
|
|
85
|
+
*/
|
|
86
|
+
id?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const {
|
|
90
|
+
label,
|
|
91
|
+
images = [],
|
|
92
|
+
formats = ['avif', 'webp'],
|
|
93
|
+
thumbSize,
|
|
94
|
+
gap,
|
|
95
|
+
aspect,
|
|
96
|
+
loop = false,
|
|
97
|
+
enhance = true,
|
|
98
|
+
id,
|
|
99
|
+
class: className,
|
|
100
|
+
role,
|
|
101
|
+
style,
|
|
102
|
+
...rest
|
|
103
|
+
} = Astro.props;
|
|
104
|
+
|
|
105
|
+
const uid =
|
|
106
|
+
id ??
|
|
107
|
+
`gallery-${
|
|
108
|
+
label
|
|
109
|
+
.toLowerCase()
|
|
110
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
111
|
+
.replace(/^-|-$/g, '') || 'x'
|
|
112
|
+
}`;
|
|
113
|
+
|
|
114
|
+
const vars = [
|
|
115
|
+
thumbSize ? `--gallery-thumb-size:${thumbSize}` : '',
|
|
116
|
+
gap ? `--gallery-gap:${gap}` : '',
|
|
117
|
+
aspect ? `--gallery-aspect:${aspect}` : '',
|
|
118
|
+
style ?? '',
|
|
119
|
+
]
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.join(';');
|
|
122
|
+
|
|
123
|
+
const Shell = enhance ? 'wc-gallery' : 'div';
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
<Shell
|
|
127
|
+
class:list={['gallery-shell', className]}
|
|
128
|
+
role={role ?? 'group'}
|
|
129
|
+
aria-label={label}
|
|
130
|
+
loop={enhance && loop ? '' : undefined}
|
|
131
|
+
style={vars}
|
|
132
|
+
{...rest}
|
|
133
|
+
>
|
|
134
|
+
<ul class="gallery" role="list">
|
|
135
|
+
{
|
|
136
|
+
images.map((img, i) => {
|
|
137
|
+
const thumb = img.thumb ?? img.src;
|
|
138
|
+
return (
|
|
139
|
+
<li class="gallery__item">
|
|
140
|
+
<button
|
|
141
|
+
class="gallery__trigger"
|
|
142
|
+
type="button"
|
|
143
|
+
command="show-modal"
|
|
144
|
+
commandfor={`${uid}-img-${i}`}
|
|
145
|
+
>
|
|
146
|
+
<ResponsiveImage
|
|
147
|
+
src={thumb}
|
|
148
|
+
alt={img.alt}
|
|
149
|
+
formats={formats}
|
|
150
|
+
class="gallery__thumb"
|
|
151
|
+
width={typeof thumb === 'string' ? undefined : (img.thumbWidth ?? 400)}
|
|
152
|
+
loading={i === 0 ? 'eager' : 'lazy'}
|
|
153
|
+
fetchpriority={i === 0 ? 'high' : 'low'}
|
|
154
|
+
decoding={i === 0 ? undefined : 'async'}
|
|
155
|
+
/>
|
|
156
|
+
<span class="visually-hidden">View full size</span>
|
|
157
|
+
</button>
|
|
158
|
+
</li>
|
|
159
|
+
);
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
<slot />
|
|
163
|
+
</ul>
|
|
164
|
+
{
|
|
165
|
+
images.map((img, i) => (
|
|
166
|
+
<dialog id={`${uid}-img-${i}`} class="lightbox-dialog" closedby="any" aria-label={img.alt}>
|
|
167
|
+
<div class="lightbox-dialog__content">
|
|
168
|
+
<ResponsiveImage
|
|
169
|
+
src={img.src}
|
|
170
|
+
alt={img.alt}
|
|
171
|
+
formats={formats}
|
|
172
|
+
class="lightbox-dialog__image"
|
|
173
|
+
width={img.width}
|
|
174
|
+
height={img.height}
|
|
175
|
+
srcset={img.srcset}
|
|
176
|
+
sizes={img.sizes}
|
|
177
|
+
loading="lazy"
|
|
178
|
+
/>
|
|
179
|
+
{img.caption && <p class="lightbox-dialog__caption font-caption">{img.caption}</p>}
|
|
180
|
+
</div>
|
|
181
|
+
<button
|
|
182
|
+
class="lightbox-dialog__close"
|
|
183
|
+
type="button"
|
|
184
|
+
command="close"
|
|
185
|
+
commandfor={`${uid}-img-${i}`}
|
|
186
|
+
aria-label="Close"
|
|
187
|
+
>
|
|
188
|
+
×
|
|
189
|
+
</button>
|
|
190
|
+
</dialog>
|
|
191
|
+
))
|
|
192
|
+
}
|
|
193
|
+
</Shell>
|
|
194
|
+
|
|
195
|
+
<style>
|
|
196
|
+
/* Icon-only trigger keeps its label for screen readers. Component-scoped
|
|
197
|
+
rather than the framework's `sr-only`: that one is in the generator's
|
|
198
|
+
TW_CLASH list, so the tailwind format ships no rule for it. */
|
|
199
|
+
.visually-hidden {
|
|
200
|
+
position: absolute;
|
|
201
|
+
inline-size: 1px;
|
|
202
|
+
block-size: 1px;
|
|
203
|
+
padding: 0;
|
|
204
|
+
margin: -1px;
|
|
205
|
+
overflow: hidden;
|
|
206
|
+
clip: rect(0, 0, 0, 0);
|
|
207
|
+
white-space: nowrap;
|
|
208
|
+
border: 0;
|
|
209
|
+
}
|
|
210
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Internal helper — turns `string | ImageMetadata` into markup, so `Gallery` and
|
|
4
|
+
* `Carousel` render images identically and cannot drift from each other.
|
|
5
|
+
*
|
|
6
|
+
* Not exported from `package.json`: it is packaging detail, not a pattern a
|
|
7
|
+
* consumer places on a page (same treatment as `TreeNode.astro`), so it carries
|
|
8
|
+
* no `TIERS` entry either — `tiers.test.ts` only scans `exports`.
|
|
9
|
+
*
|
|
10
|
+
* A plain `string` renders the raw `<img>` path every Astro component in this
|
|
11
|
+
* package already used before `ImageMetadata` support existed — unchanged, so
|
|
12
|
+
* every existing string-`src` call site keeps rendering byte-identical markup.
|
|
13
|
+
* An `ImageMetadata` (an `astro:assets` image import) renders through
|
|
14
|
+
* `<Picture />`, which is what gets a consumer AVIF/WebP derivatives instead of
|
|
15
|
+
* shipping whatever format the source file happens to be in.
|
|
16
|
+
*
|
|
17
|
+
* `formats` defaults to `['avif', 'webp']` and is passed straight through to
|
|
18
|
+
* `<Picture formats>` — including a value astro:assets doesn't recognise yet.
|
|
19
|
+
* That is deliberate: Astro's `VALID_OUTPUT_FORMATS` today is
|
|
20
|
+
* `avif/png/webp/jpeg/jpg/svg` (no JPEG-XL — the sharp service can't emit it), so
|
|
21
|
+
* hard-coding a closed format union here would need editing the day a service
|
|
22
|
+
* adds one. `ImageFormat` mirrors Astro's own `ImageOutputFormat`
|
|
23
|
+
* (`… | (string & {})`) for exactly that reason, defined locally rather than
|
|
24
|
+
* imported — `astro:assets`'s ambient module declaration doesn't export it, and
|
|
25
|
+
* reaching into `astro/dist/**` for a type is not a stable import.
|
|
26
|
+
*/
|
|
27
|
+
import { Picture } from 'astro:assets';
|
|
28
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
29
|
+
|
|
30
|
+
export type ImageFormat = 'avif' | 'webp' | 'png' | 'jpeg' | 'jpg' | 'gif' | 'svg' | (string & {});
|
|
31
|
+
|
|
32
|
+
export interface Props extends Omit<HTMLAttributes<'img'>, 'src' | 'alt'> {
|
|
33
|
+
src: string | ImageMetadata;
|
|
34
|
+
alt: string;
|
|
35
|
+
formats?: ImageFormat[];
|
|
36
|
+
/**
|
|
37
|
+
* Only meaningful when `src` is a plain `string` — Astro computes its own
|
|
38
|
+
* `srcset` for an `ImageMetadata` source and this is not merged with it.
|
|
39
|
+
*/
|
|
40
|
+
srcset?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { src, alt, formats = ['avif', 'webp'], srcset, class: className, ...rest } = Astro.props;
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
typeof src === 'string' ? (
|
|
48
|
+
<img src={src} alt={alt} srcset={srcset} class={className} {...rest} />
|
|
49
|
+
) : (
|
|
50
|
+
<Picture src={src} alt={alt} formats={formats} class={className} {...rest} />
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
* position and depth. A `<div>` soup looks identical and tells a screen-reader
|
|
11
11
|
* user nothing about where they are;
|
|
12
12
|
* - every branch is a native `<details>`/`<summary>`, so expanded state and
|
|
13
|
-
* keyboard operation come from the platform
|
|
14
|
-
* `aria-expanded` bookkeeping —
|
|
15
|
-
*
|
|
13
|
+
* keyboard operation come from the platform with no JS at all. This
|
|
14
|
+
* fallback carries no `role="tree"` and no `aria-expanded` bookkeeping —
|
|
15
|
+
* those aren't hand-rolled into markup that would be silently wrong the
|
|
16
|
+
* instant JS failed to load; `<wc-tree>` applies them on upgrade, over a
|
|
17
|
+
* DOM it unwraps for the purpose (branches lose their `<details>`/
|
|
18
|
+
* `<summary>` entirely — see that element's own docblock for why and how);
|
|
16
19
|
* - nodes carry ids, so any node is linkable without JS.
|
|
17
20
|
*
|
|
18
21
|
* **This component emits its own `<wc-tree>`** — do NOT wrap it in one. That is the
|
|
@@ -22,9 +25,11 @@
|
|
|
22
25
|
* element now defers to an enclosing one, so it degrades to a no-op rather than two
|
|
23
26
|
* duelling toolbars, but the single call is the intended shape.
|
|
24
27
|
*
|
|
25
|
-
* `<wc-tree>` upgrades it with
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
+
* `<wc-tree>` upgrades it with WAI-ARIA tree semantics, roving-tabindex keyboard
|
|
29
|
+
* navigation, filter, expand/collapse-all and hash deep-linking. If that never
|
|
30
|
+
* loads, the fallback above — native nested `<details>`, Tab-and-Enter — is all
|
|
31
|
+
* that remains, which is why `enhance={false}` is a supported way to get the
|
|
32
|
+
* bare fragment on its own.
|
|
28
33
|
*/
|
|
29
34
|
import type { TreeItem } from './tree.ts';
|
|
30
35
|
import TreeNode from './TreeNode.astro';
|
|
@@ -6,13 +6,23 @@
|
|
|
6
6
|
*
|
|
7
7
|
* - a **branch** wraps its label in `<details>`/`<summary>` — a native
|
|
8
8
|
* disclosure button, so expanded state is announced and keyboard operation
|
|
9
|
-
* is free. The child `<ul>` lives inside the `<details>`, which
|
|
10
|
-
* it collapse with no JS.
|
|
9
|
+
* is free with no JS. The child `<ul>` lives inside the `<details>`, which
|
|
10
|
+
* is what makes it collapse with no JS.
|
|
11
11
|
* - a **leaf** is a plain `.tree__content` row. A `<details>` with nothing to
|
|
12
12
|
* disclose would still render a control and announce as collapsible.
|
|
13
13
|
*
|
|
14
|
+
* That description is the NO-JS contract only. `<wc-tree>` unwraps every
|
|
15
|
+
* branch's `<details>`/`<summary>` on upgrade — see its own docblock — so once
|
|
16
|
+
* it loads, "keyboard operation is free" stops meaning native Tab-and-Enter and
|
|
17
|
+
* starts meaning that element's roving-tabindex arrow-key navigation instead,
|
|
18
|
+
* and `role="treeitem"`/`role="group"` replace the `<li>`'s and `<ul>`'s
|
|
19
|
+
* implicit `listitem`/`list` roles. The markup below never changes to reflect
|
|
20
|
+
* that — it's the fallback `<wc-tree>` reads and transforms, not two things to
|
|
21
|
+
* keep in sync.
|
|
22
|
+
*
|
|
14
23
|
* The item class sits on the `<li>` in both cases, so `<wc-tree>`'s filter treats
|
|
15
|
-
* leaves and branches as one kind of thing and hiding one hides its whole row
|
|
24
|
+
* leaves and branches as one kind of thing and hiding one hides its whole row —
|
|
25
|
+
* true before and after upgrade, since the transform preserves the class.
|
|
16
26
|
*/
|
|
17
27
|
import type { TreeItem } from './tree.ts';
|
|
18
28
|
|
|
@@ -1,33 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
name: string;
|
|
5
|
-
url: string;
|
|
6
|
-
image?: string;
|
|
7
|
-
description?: string;
|
|
8
|
-
}
|
|
2
|
+
import { itemListGraph } from '@getvitops/utils';
|
|
3
|
+
import type { ItemListEntry, ItemListGraphOptions } from '@getvitops/utils';
|
|
9
4
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
export type { ItemListEntry };
|
|
6
|
+
export type CarouselItem = ItemListEntry;
|
|
7
|
+
export type Props = ItemListGraphOptions;
|
|
13
8
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const jsonLd = {
|
|
17
|
-
'@context': 'https://schema.org',
|
|
18
|
-
'@type': 'ItemList',
|
|
19
|
-
itemListElement: items.map((item, index) => ({
|
|
20
|
-
'@type': 'ListItem',
|
|
21
|
-
position: index + 1,
|
|
22
|
-
item: {
|
|
23
|
-
'@type': item.type,
|
|
24
|
-
name: item.name,
|
|
25
|
-
url: item.url,
|
|
26
|
-
...(item.image && { image: item.image }),
|
|
27
|
-
...(item.description && { description: item.description }),
|
|
28
|
-
},
|
|
29
|
-
})),
|
|
30
|
-
};
|
|
9
|
+
const jsonLd = itemListGraph(Astro.props);
|
|
31
10
|
---
|
|
32
11
|
|
|
33
12
|
<script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
|
@@ -1,154 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
opens: string;
|
|
5
|
-
closes: string;
|
|
6
|
-
validFrom?: string;
|
|
7
|
-
validThrough?: string;
|
|
8
|
-
}
|
|
2
|
+
import { localBusinessGraph } from '@getvitops/utils';
|
|
3
|
+
import type { LocalBusinessGraphOptions } from '@getvitops/utils';
|
|
9
4
|
|
|
10
|
-
export
|
|
11
|
-
type?: 'LocalBusiness' | 'Restaurant' | 'Store' | 'MedicalBusiness' | 'FinancialService' | 'FoodEstablishment' | 'HealthAndBeautyBusiness' | 'HomeAndConstructionBusiness' | 'LegalService' | 'LodgingBusiness' | 'ProfessionalService' | 'SportsActivityLocation';
|
|
12
|
-
name: string;
|
|
13
|
-
description?: string;
|
|
14
|
-
url?: string;
|
|
15
|
-
telephone?: string;
|
|
16
|
-
email?: string;
|
|
17
|
-
image?: string | string[];
|
|
18
|
-
priceRange?: string;
|
|
19
|
-
address: {
|
|
20
|
-
streetAddress: string;
|
|
21
|
-
addressLocality: string;
|
|
22
|
-
addressRegion?: string;
|
|
23
|
-
postalCode: string;
|
|
24
|
-
addressCountry: string;
|
|
25
|
-
};
|
|
26
|
-
geo?: {
|
|
27
|
-
latitude: number;
|
|
28
|
-
longitude: number;
|
|
29
|
-
};
|
|
30
|
-
openingHoursSpecification?: OpeningHoursSpecification[];
|
|
31
|
-
aggregateRating?: {
|
|
32
|
-
ratingValue: number;
|
|
33
|
-
reviewCount?: number;
|
|
34
|
-
ratingCount?: number;
|
|
35
|
-
bestRating?: number;
|
|
36
|
-
worstRating?: number;
|
|
37
|
-
};
|
|
38
|
-
review?: {
|
|
39
|
-
author: string;
|
|
40
|
-
datePublished: string;
|
|
41
|
-
reviewBody: string;
|
|
42
|
-
reviewRating?: {
|
|
43
|
-
ratingValue: number;
|
|
44
|
-
bestRating?: number;
|
|
45
|
-
worstRating?: number;
|
|
46
|
-
};
|
|
47
|
-
}[];
|
|
48
|
-
servesCuisine?: string[];
|
|
49
|
-
menu?: string;
|
|
50
|
-
acceptsReservations?: boolean;
|
|
51
|
-
hasMenu?: string;
|
|
52
|
-
paymentAccepted?: string[];
|
|
53
|
-
currenciesAccepted?: string;
|
|
54
|
-
areaServed?: string[];
|
|
55
|
-
sameAs?: string[];
|
|
56
|
-
}
|
|
5
|
+
export type Props = LocalBusinessGraphOptions;
|
|
57
6
|
|
|
58
|
-
const
|
|
59
|
-
type = 'LocalBusiness',
|
|
60
|
-
name,
|
|
61
|
-
description,
|
|
62
|
-
url,
|
|
63
|
-
telephone,
|
|
64
|
-
email,
|
|
65
|
-
image,
|
|
66
|
-
priceRange,
|
|
67
|
-
address,
|
|
68
|
-
geo,
|
|
69
|
-
openingHoursSpecification,
|
|
70
|
-
aggregateRating,
|
|
71
|
-
review,
|
|
72
|
-
servesCuisine,
|
|
73
|
-
menu,
|
|
74
|
-
acceptsReservations,
|
|
75
|
-
hasMenu,
|
|
76
|
-
paymentAccepted,
|
|
77
|
-
currenciesAccepted,
|
|
78
|
-
areaServed,
|
|
79
|
-
sameAs,
|
|
80
|
-
} = Astro.props;
|
|
81
|
-
|
|
82
|
-
const jsonLd = {
|
|
83
|
-
'@context': 'https://schema.org',
|
|
84
|
-
'@type': type,
|
|
85
|
-
name,
|
|
86
|
-
...(description && { description }),
|
|
87
|
-
...(url && { url }),
|
|
88
|
-
...(telephone && { telephone }),
|
|
89
|
-
...(email && { email }),
|
|
90
|
-
...(image && { image: Array.isArray(image) ? image : [image] }),
|
|
91
|
-
...(priceRange && { priceRange }),
|
|
92
|
-
address: {
|
|
93
|
-
'@type': 'PostalAddress',
|
|
94
|
-
streetAddress: address.streetAddress,
|
|
95
|
-
addressLocality: address.addressLocality,
|
|
96
|
-
...(address.addressRegion && { addressRegion: address.addressRegion }),
|
|
97
|
-
postalCode: address.postalCode,
|
|
98
|
-
addressCountry: address.addressCountry,
|
|
99
|
-
},
|
|
100
|
-
...(geo?.latitude != null && geo?.longitude != null && {
|
|
101
|
-
geo: {
|
|
102
|
-
'@type': 'GeoCoordinates',
|
|
103
|
-
latitude: geo.latitude,
|
|
104
|
-
longitude: geo.longitude,
|
|
105
|
-
},
|
|
106
|
-
}),
|
|
107
|
-
...(openingHoursSpecification && {
|
|
108
|
-
openingHoursSpecification: openingHoursSpecification.map(spec => ({
|
|
109
|
-
'@type': 'OpeningHoursSpecification',
|
|
110
|
-
dayOfWeek: spec.dayOfWeek,
|
|
111
|
-
opens: spec.opens,
|
|
112
|
-
closes: spec.closes,
|
|
113
|
-
...(spec.validFrom && { validFrom: spec.validFrom }),
|
|
114
|
-
...(spec.validThrough && { validThrough: spec.validThrough }),
|
|
115
|
-
})),
|
|
116
|
-
}),
|
|
117
|
-
...(aggregateRating && {
|
|
118
|
-
aggregateRating: {
|
|
119
|
-
'@type': 'AggregateRating',
|
|
120
|
-
ratingValue: aggregateRating.ratingValue,
|
|
121
|
-
...(aggregateRating.reviewCount && { reviewCount: aggregateRating.reviewCount }),
|
|
122
|
-
...(aggregateRating.ratingCount && { ratingCount: aggregateRating.ratingCount }),
|
|
123
|
-
bestRating: aggregateRating.bestRating ?? 5,
|
|
124
|
-
worstRating: aggregateRating.worstRating ?? 1,
|
|
125
|
-
},
|
|
126
|
-
}),
|
|
127
|
-
...(review && {
|
|
128
|
-
review: review.map(r => ({
|
|
129
|
-
'@type': 'Review',
|
|
130
|
-
author: { '@type': 'Person', name: r.author },
|
|
131
|
-
datePublished: r.datePublished,
|
|
132
|
-
reviewBody: r.reviewBody,
|
|
133
|
-
...(r.reviewRating && {
|
|
134
|
-
reviewRating: {
|
|
135
|
-
'@type': 'Rating',
|
|
136
|
-
ratingValue: r.reviewRating.ratingValue,
|
|
137
|
-
bestRating: r.reviewRating.bestRating ?? 5,
|
|
138
|
-
worstRating: r.reviewRating.worstRating ?? 1,
|
|
139
|
-
},
|
|
140
|
-
}),
|
|
141
|
-
})),
|
|
142
|
-
}),
|
|
143
|
-
...(servesCuisine && { servesCuisine }),
|
|
144
|
-
...(menu && { menu }),
|
|
145
|
-
...(acceptsReservations !== undefined && { acceptsReservations }),
|
|
146
|
-
...(hasMenu && { hasMenu }),
|
|
147
|
-
...(paymentAccepted && { paymentAccepted }),
|
|
148
|
-
...(currenciesAccepted && { currenciesAccepted }),
|
|
149
|
-
...(areaServed && { areaServed }),
|
|
150
|
-
...(sameAs && { sameAs }),
|
|
151
|
-
};
|
|
7
|
+
const jsonLd = localBusinessGraph(Astro.props);
|
|
152
8
|
---
|
|
153
9
|
|
|
154
10
|
<script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|