@getvitops/generator 7.0.0 → 8.0.1
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 +133 -22
- package/assets/css/global.css +28 -0
- package/assets/css/index.css +2 -0
- package/assets/css/patterns/gallery.css +8 -4
- package/assets/css/patterns/lightbox.css +143 -66
- package/assets/css/patterns/review.css +91 -0
- package/assets/js/elements.js +7 -7
- package/config.schema.json +73 -0
- package/dist/index.d.mts +94 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +162 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,124 @@
|
|
|
1
1
|
# @getvitops/generator
|
|
2
2
|
|
|
3
|
+
## 8.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @getvitops/utils@8.0.1
|
|
8
|
+
|
|
9
|
+
## 8.0.0
|
|
10
|
+
|
|
11
|
+
### Major Changes
|
|
12
|
+
|
|
13
|
+
- Rebuild the lightbox on one dialog + the existing carousel, fixing the flicker on
|
|
14
|
+
prev/next and the stutter on open.
|
|
15
|
+
|
|
16
|
+
**Breaking (`css`/`bricks` and `tailwind` formats — `.lightbox-dialog`'s markup
|
|
17
|
+
contract changed):**
|
|
18
|
+
|
|
19
|
+
- `<Gallery />` now emits ONE `<dialog class="lightbox-dialog">` per gallery,
|
|
20
|
+
wrapping a `<Carousel />`, instead of one dialog per image. A `.gallery__trigger`
|
|
21
|
+
is now a plain `<a href="#slide-id">`, not a `command="show-modal"` button.
|
|
22
|
+
- `.lightbox-dialog__content` / `__image` / `__nav` / `__nav-button` /
|
|
23
|
+
`__caption` are gone. The dialog's child is `.lightbox-dialog__track` (a
|
|
24
|
+
`.carousel`) — prev/next, dots, arrow keys and swipe are the carousel's,
|
|
25
|
+
reused rather than reimplemented. `.lightbox-dialog__close` is unchanged.
|
|
26
|
+
- `<wc-gallery>` no longer builds nav buttons or steps between images — the
|
|
27
|
+
nested `<wc-carousel>` does. It fires `gallery-open` (unchanged shape) and
|
|
28
|
+
re-emits the carousel's own stepping as `gallery-change`.
|
|
29
|
+
- `packages/core/src/web-components/utils/gallery.ts` dropped `keyToAction` and
|
|
30
|
+
`swipeAction` (the carousel's own key/swipe handling covers both now).
|
|
31
|
+
`stepIndex`/`preloadTargets` are unchanged.
|
|
32
|
+
|
|
33
|
+
**Why:** stepping used to close one `<dialog>` and open another, which made
|
|
34
|
+
`animation.css`'s top-layer driver play a full exit-then-entry transition on
|
|
35
|
+
every step — visible as a flicker on prev/next, by construction, not a bug to
|
|
36
|
+
tune away. Opening also awaited the full image's `decode()` before starting the
|
|
37
|
+
view-transition morph, so the dialog visibly stalled on a slow network. One
|
|
38
|
+
dialog containing one carousel makes the first failure mode unrepresentable
|
|
39
|
+
(stepping is a scroll, not a close/open pair) and removes the wait for the
|
|
40
|
+
second (the morph starts immediately, painting the thumbnail as a CSS
|
|
41
|
+
background placeholder until the full image arrives).
|
|
42
|
+
|
|
43
|
+
**Also improved, not just fixed:** the no-JS fallback is no longer a _stripped_
|
|
44
|
+
lightbox — it's a genuine one. Each trigger is ordinary fragment navigation
|
|
45
|
+
(`:has(:target)` in `lightbox.css` reveals the dialog), which needs no Invoker
|
|
46
|
+
Commands polyfill in any engine, and it now has working prev/next with no
|
|
47
|
+
JavaScript at all — the previous per-dialog fallback had no way to express that.
|
|
48
|
+
|
|
49
|
+
**Migration:** a consumer using `<Gallery />` needs no changes — the component's
|
|
50
|
+
props are unchanged. Anyone who hand-authored `.lightbox-dialog` markup (per
|
|
51
|
+
`docs/concepts/patterns.md` / `docs/css/classes.md`) must move to the new single-
|
|
52
|
+
dialog-wrapping-a-carousel shape; see `patterns.astro`'s "Gallery, hand-authored"
|
|
53
|
+
demo for the reference markup. `<wc-gallery>` consumers listening for
|
|
54
|
+
`gallery-change` keep the same event shape (`{ index, total }`).
|
|
55
|
+
|
|
56
|
+
### Minor Changes
|
|
57
|
+
|
|
58
|
+
- Add Google Business Profile reviews: fetch, cache, render.
|
|
59
|
+
|
|
60
|
+
- **`vitops reviews`** (new CLI command) fetches a client's GBP reviews (legacy v4 API — the only
|
|
61
|
+
one that has them) and writes them to `.vitops/reviews.json`, meant to be committed so a build
|
|
62
|
+
never needs a live Google credential. `--dry` prints the plan; `--check` diffs against the cache
|
|
63
|
+
without writing. Requires the same `VITOPS_GOOGLE_CLIENT_ID`/`_CLIENT_SECRET`/`_REFRESH_TOKEN` (or
|
|
64
|
+
ADC login) `search setup` uses, scoped to `business.manage` — **and the calling GCP project must
|
|
65
|
+
be allowlisted by Google first**, a separate one-time approval from every other Google API here.
|
|
66
|
+
- **`site.reviews`** (config): which `organization.locations` to fetch, and per-location
|
|
67
|
+
`featured`/`minRating`/`sort`/`limit` selection. `schema` (JSON-LD) and `avatars` both default to
|
|
68
|
+
**off** — see the migration notes below.
|
|
69
|
+
- **`@getvitops/utils/reviews`** (new subpath): the pure plan/select/merge functions plus the GBP
|
|
70
|
+
fetcher and cache reader/writer, mirroring `./indexing`'s pure/IO split.
|
|
71
|
+
- **`<ReviewCard>`** (new Astro component, `@getvitops/astro`): one review card from a cache entry.
|
|
72
|
+
No web component — nothing here needs progressive enhancement. Composes the existing
|
|
73
|
+
`schemas/Review.astro` for JSON-LD only when both `schema` and `itemReviewed` are passed.
|
|
74
|
+
- **`<Marquee>`** (new Astro component): the tier-3 wrapper `wc-marquee`/`marquee.css` was missing.
|
|
75
|
+
- **`.review` pattern** (`css`/`bricks` and `tailwind` formats): a new config-generated
|
|
76
|
+
`patterns.items.review` entry plus `packages/core/css/patterns/review.css` for the structural
|
|
77
|
+
parts (rating, identity, body, reply, source link). Two new semantic icon names, `star` and
|
|
78
|
+
`star-half`, across every icon set.
|
|
79
|
+
|
|
80
|
+
**Migration:** no breaking change. A site adding `site.reviews` should read the "Google reviews"
|
|
81
|
+
section in AGENTS.md before enabling `schema` — Google's structured-data policy treats a business
|
|
82
|
+
marking up reviews of itself as ineligible for review rich results and risks a manual action, which
|
|
83
|
+
is why it defaults off.
|
|
84
|
+
|
|
85
|
+
- Repo-wide simplification pass: de-boilerplate the CLI, dedupe shared cache/plan
|
|
86
|
+
primitives in `@getvitops/utils`, drop dead code, and tighten dependency hygiene.
|
|
87
|
+
|
|
88
|
+
No breaking changes. (`vitops icons`'s `--site` flag was briefly going to be renamed to
|
|
89
|
+
`--input` for consistency with the other `Config`-anchored commands — deferred rather than
|
|
90
|
+
spending a major bump on a rename with no other breaking change riding along; it stays
|
|
91
|
+
`--site` for now.)
|
|
92
|
+
|
|
93
|
+
**Fixed:**
|
|
94
|
+
|
|
95
|
+
- `@getvitops/core`'s 12 runtime deps (`lit`, both `@oddbird` polyfills, both
|
|
96
|
+
`@webcomponents` packages, `colorjs.io`, etc.) were listed as `dependencies` despite
|
|
97
|
+
being bundled into `dist/elements.js`/`dist/polyfills.js`/`dist/deferred.js` — a
|
|
98
|
+
consumer installed them twice for nothing. Moved to `devDependencies`; no change to
|
|
99
|
+
what ships.
|
|
100
|
+
- Deleted `old-css-lib/` (59 files), superseded by `packages/core/css/` since the
|
|
101
|
+
in-memory bundling change — no consumer-facing effect, it was not part of any
|
|
102
|
+
published output.
|
|
103
|
+
- Deleted a dead Astro module (`packages/astro/src/scripts/deferred.ts`) that had
|
|
104
|
+
silently diverged from `packages/core/src/js/deferred.ts` and had zero importers.
|
|
105
|
+
|
|
106
|
+
**No consumer-facing change in `@getvitops/vite`** beyond the fixed-group version bump —
|
|
107
|
+
task-graph and caching fixes internal to this repo's own build.
|
|
108
|
+
|
|
109
|
+
Everything else (the `cli.ts` file split into `commands/{search,ads}.ts`, shared
|
|
110
|
+
`json-store.ts`/`plan-format.ts` helpers in `@getvitops/utils`, a `.github/actions/setup`
|
|
111
|
+
composite action, restatement-comment cleanup) is internal refactoring with no change to
|
|
112
|
+
any published API, CLI flag, or generated output.
|
|
113
|
+
|
|
114
|
+
### Patch Changes
|
|
115
|
+
|
|
116
|
+
- Updated dependencies
|
|
117
|
+
- Updated dependencies
|
|
118
|
+
- Updated dependencies
|
|
119
|
+
- Updated dependencies [a84839b]
|
|
120
|
+
- @getvitops/utils@8.0.0
|
|
121
|
+
|
|
3
122
|
## 7.0.0
|
|
4
123
|
|
|
5
124
|
### Major Changes
|
|
@@ -1012,28 +1131,20 @@ hidden` would pull it out of the accessible-name computation along with the visu
|
|
|
1012
1131
|
{
|
|
1013
1132
|
"designSystem": {
|
|
1014
1133
|
"themes": {
|
|
1015
|
-
"default": {
|
|
1016
|
-
/* … */
|
|
1017
|
-
},
|
|
1134
|
+
"default": {/* … */},
|
|
1018
1135
|
},
|
|
1019
1136
|
},
|
|
1020
1137
|
"organization": {
|
|
1021
1138
|
"name": "Acme",
|
|
1022
1139
|
"contact": "hq",
|
|
1023
1140
|
"locations": {
|
|
1024
|
-
"hq": {
|
|
1025
|
-
/* … */
|
|
1026
|
-
},
|
|
1141
|
+
"hq": {/* … */},
|
|
1027
1142
|
},
|
|
1028
1143
|
},
|
|
1029
1144
|
"site": {
|
|
1030
1145
|
"defaultLocale": "en",
|
|
1031
|
-
"domains": {
|
|
1032
|
-
|
|
1033
|
-
},
|
|
1034
|
-
"analytics": {
|
|
1035
|
-
/* … */
|
|
1036
|
-
},
|
|
1146
|
+
"domains": {/* … */},
|
|
1147
|
+
"analytics": {/* … */},
|
|
1037
1148
|
},
|
|
1038
1149
|
}
|
|
1039
1150
|
```
|
|
@@ -1198,7 +1309,7 @@ open-fade-in"` composes slide and fade. Applied at zero specificity with identit
|
|
|
1198
1309
|
Also new, and worth wiring at the same time — **`gitLastmod()` for real sitemap dates**:
|
|
1199
1310
|
|
|
1200
1311
|
```js
|
|
1201
|
-
import vitops, { gitLastmod } from
|
|
1312
|
+
import vitops, { gitLastmod } from "@getvitops/astro";
|
|
1202
1313
|
vitops({ sitemap: { serialize: await gitLastmod() } });
|
|
1203
1314
|
```
|
|
1204
1315
|
|
|
@@ -1383,8 +1494,8 @@ open-fade-in"` composes slide and fade. Applied at zero specificity with identit
|
|
|
1383
1494
|
|
|
1384
1495
|
```js
|
|
1385
1496
|
vitops({
|
|
1386
|
-
analytics: { googleAnalytics:
|
|
1387
|
-
consent: { policyUrl:
|
|
1497
|
+
analytics: { googleAnalytics: "G-XXXXXXXXXX", plausible: "acme.com" },
|
|
1498
|
+
consent: { policyUrl: "/legal/cookies" },
|
|
1388
1499
|
});
|
|
1389
1500
|
```
|
|
1390
1501
|
|
|
@@ -1524,7 +1635,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
|
|
|
1524
1635
|
|
|
1525
1636
|
```js
|
|
1526
1637
|
// astro.config.mjs
|
|
1527
|
-
getvitops({ css: { input:
|
|
1638
|
+
getvitops({ css: { input: "company.json", format: "css" } });
|
|
1528
1639
|
```
|
|
1529
1640
|
|
|
1530
1641
|
```
|
|
@@ -1575,11 +1686,11 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
|
|
|
1575
1686
|
getvitops({
|
|
1576
1687
|
fonts: [
|
|
1577
1688
|
{
|
|
1578
|
-
name:
|
|
1579
|
-
provider:
|
|
1580
|
-
cssVariable:
|
|
1581
|
-
weights: [
|
|
1582
|
-
subsets: [
|
|
1689
|
+
name: "League Spartan",
|
|
1690
|
+
provider: "fontsource",
|
|
1691
|
+
cssVariable: "--font-league-spartan",
|
|
1692
|
+
weights: ["100 900"],
|
|
1693
|
+
subsets: ["latin"],
|
|
1583
1694
|
preload: true,
|
|
1584
1695
|
},
|
|
1585
1696
|
],
|
|
@@ -2450,7 +2561,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
|
|
|
2450
2561
|
```js
|
|
2451
2562
|
// astro.config.mjs
|
|
2452
2563
|
getvitops({
|
|
2453
|
-
css: { input:
|
|
2564
|
+
css: { input: "design-system.json", format: "css", out: "src/styles" },
|
|
2454
2565
|
editor: true,
|
|
2455
2566
|
});
|
|
2456
2567
|
```
|
package/assets/css/global.css
CHANGED
|
@@ -75,3 +75,31 @@ body {
|
|
|
75
75
|
scroll-padding-block-start: var(--top-offset, 0);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
/* The focus ring must survive Windows High Contrast Mode, and as generated it
|
|
80
|
+
does not. Every pattern's `ring: true` state (`generate.ts`) emits `outline:
|
|
81
|
+
none; box-shadow: 0 0 0 3px <color>;` — a deliberate choice the rest of the
|
|
82
|
+
time, since a box-shadow ring can follow a border-radius where a native
|
|
83
|
+
outline cannot. But `forced-colors: active` forces `box-shadow` to `none`
|
|
84
|
+
unconditionally (CSS Color Adjust Module), and this framework already asked
|
|
85
|
+
for no native outline either — so a keyboard user in forced-colors mode gets
|
|
86
|
+
NO focus indicator at all, on every `:focus-visible` element in every
|
|
87
|
+
format. This is the single most severe instance of the class of bug
|
|
88
|
+
`forced-colors`/`prefers-contrast` coverage exists to catch (AGENTS.md's
|
|
89
|
+
design-system-gaps notes on `--surface-glass`/`--overlay`/role solids are
|
|
90
|
+
the same class, decorative rather than safety-critical).
|
|
91
|
+
|
|
92
|
+
`!important`, deliberately, for the same reason `:root`'s `font-size` above
|
|
93
|
+
gets it: this must win regardless of which pattern's `vitops.components`
|
|
94
|
+
rule set `outline: none`, and `!important` outranks a normal-priority
|
|
95
|
+
declaration regardless of cascade layer. The colour is the token every
|
|
96
|
+
other ring already uses (`--color-border-focus`, from `ui-primary`,
|
|
97
|
+
contrast-checked as a non-text boundary) — forced-colors mode remaps
|
|
98
|
+
`outline-color` to a system colour automatically, so this is really only
|
|
99
|
+
asserting THAT an outline renders, not which colour it renders in. */
|
|
100
|
+
@media (forced-colors: active) {
|
|
101
|
+
:focus-visible {
|
|
102
|
+
outline: 2px solid var(--color-border-focus) !important;
|
|
103
|
+
outline-offset: 2px !important;
|
|
104
|
+
}
|
|
105
|
+
}
|
package/assets/css/index.css
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
@import './generated/type-tokens.css';
|
|
7
7
|
@import './generated/tokens.css';
|
|
8
8
|
@import './generated/typography.css';
|
|
9
|
+
@import './generated/typography-headings.css';
|
|
9
10
|
@import './layout.css';
|
|
10
11
|
/* Must precede utilities.css: both are `vitops.utilities`, and utilities.css
|
|
11
12
|
re-emits `.<bp>-flex` AFTER its own `.hidden` so `class="hidden md-flex"`
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
@import './patterns/table.css';
|
|
54
55
|
@import './patterns/details.css';
|
|
55
56
|
@import './patterns/comment.css';
|
|
57
|
+
@import './patterns/review.css';
|
|
56
58
|
@import './patterns/banner.css';
|
|
57
59
|
@import './patterns/consent.css';
|
|
58
60
|
@import './patterns/list.css';
|
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* concerns — "a grid of images" and "view one full-screen" — and only the dialog
|
|
7
7
|
* half needs the `lightbox` name; the grid never was one.
|
|
8
8
|
*
|
|
9
|
-
* `<wc-gallery>` wraps this and
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* `<wc-gallery>` wraps this and the single `.lightbox-dialog` it opens, running the
|
|
10
|
+
* thumbnail → full-image view-transition morph; stepping between images is a
|
|
11
|
+
* `<wc-carousel>` scroll inside that dialog, not a second dialog. With no JS each
|
|
12
|
+
* `.gallery__trigger` is a real `<a href="#slide-id">` into the carousel's own
|
|
13
|
+
* `<li id="slide-id">`, and `:has(:target)` in lightbox.css is what reveals the
|
|
14
|
+
* dialog — so the grid + dialog already form a working modal lightbox, with
|
|
15
|
+
* prev/next, before any script runs.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
wc-gallery {
|
|
@@ -40,6 +43,7 @@ wc-gallery {
|
|
|
40
43
|
padding: 0;
|
|
41
44
|
background: none;
|
|
42
45
|
border: none;
|
|
46
|
+
text-decoration: none;
|
|
43
47
|
cursor: pointer;
|
|
44
48
|
|
|
45
49
|
&:focus-visible {
|
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
* Lightbox — the full-screen `<dialog>` a gallery trigger opens.
|
|
3
3
|
* Consumes the generated token cascade (src/css/generated/tokens.css).
|
|
4
4
|
*
|
|
5
|
-
* The thumbnail grid that drives this
|
|
5
|
+
* The thumbnail grid that drives this lives in `patterns/gallery.css`
|
|
6
6
|
* (`.gallery` / `.gallery__item` / `.gallery__trigger` / `.gallery__thumb`) — this
|
|
7
7
|
* partial owns only the dialog a trigger opens. `<Gallery />` emits both; this
|
|
8
8
|
* file has no opinion on what triggers it.
|
|
9
|
+
*
|
|
10
|
+
* **One dialog, one carousel** — not one dialog per image. Stepping between
|
|
11
|
+
* images is a scroll inside an already-open dialog (`.carousel`, patterns/
|
|
12
|
+
* carousel.css), so there is no close-one/open-another sequence for the top-layer
|
|
13
|
+
* driver in animation.css to double-animate. `<wc-gallery>` only opens/closes;
|
|
14
|
+
* `<wc-carousel>` (nested inside, `<Gallery />` emits both tags) owns prev/next,
|
|
15
|
+
* dots, arrow keys and swipe — reused rather than re-implemented.
|
|
9
16
|
*/
|
|
10
17
|
|
|
11
18
|
/* ── Lightbox dialog — full-screen, w/ view transitions ─────────────────────── */
|
|
@@ -22,7 +29,7 @@ dialog.lightbox-dialog {
|
|
|
22
29
|
background: var(--lightbox-dialog-bg, oklch(0 0 0 / 0.92));
|
|
23
30
|
color: #fff;
|
|
24
31
|
display: grid;
|
|
25
|
-
place-items:
|
|
32
|
+
place-items: stretch;
|
|
26
33
|
|
|
27
34
|
/* No scrim. Stated through popover.css's token rather than overriding its
|
|
28
35
|
::backdrop rule, so it keeps the shared fade timing. */
|
|
@@ -31,7 +38,8 @@ dialog.lightbox-dialog {
|
|
|
31
38
|
/* The entry, as effect vars rather than a keyframe — same approach as
|
|
32
39
|
dialog.css, and for the same reason: the top-layer driver in animation.css
|
|
33
40
|
then animates this OUT too, which an `animation:` on `[open]` never does
|
|
34
|
-
(it only ever plays once, on open).
|
|
41
|
+
(it only ever plays once, on open). Runs on real open/close ONLY — stepping
|
|
42
|
+
between slides never opens or closes this dialog, so it can never fire here. */
|
|
35
43
|
--opacity-from: 0;
|
|
36
44
|
--scale-x-from: 0.95;
|
|
37
45
|
--scale-y-from: 0.95;
|
|
@@ -40,26 +48,138 @@ dialog.lightbox-dialog {
|
|
|
40
48
|
display: grid;
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
/* ── No-JS fallback: `:target` + `:has()` ──────────────────────────────────
|
|
52
|
+
A `.gallery__trigger` is a real `<a href="#slide-id">`. Following it makes
|
|
53
|
+
that slide's `<li>` match `:target`, which this rule alone is what reveals
|
|
54
|
+
the dialog — no Invoker Commands, no polyfill, in every engine that has
|
|
55
|
+
`:has()` (Safari 15.4+, Chromium 105+, Firefox 121+). The browser's own
|
|
56
|
+
fragment-navigation scroll then lands the carousel on the right slide,
|
|
57
|
+
because the target lives inside `.carousel__track`.
|
|
58
|
+
|
|
59
|
+
`:not([open])` is load-bearing: once `<wc-gallery>` opens this for real via
|
|
60
|
+
`showModal()`, `[open]` is present and this rule must yield to it — a stale
|
|
61
|
+
`:target` from a previous no-JS visit must not fight the real modal, and
|
|
62
|
+
must not keep the dialog "open" by this rule alone once JS is stepping
|
|
63
|
+
between slides (which never changes the URL fragment). */
|
|
64
|
+
&:not([open]):has(:target) {
|
|
65
|
+
display: grid;
|
|
66
|
+
position: fixed;
|
|
67
|
+
inset: 0;
|
|
68
|
+
z-index: var(--lightbox-z, 100);
|
|
50
69
|
}
|
|
51
70
|
|
|
52
|
-
.lightbox-
|
|
71
|
+
.lightbox-dialog__track {
|
|
53
72
|
max-inline-size: 100%;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
the
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
/* Inert: `.carousel`'s implicit grid row is `auto`, so this percentage is
|
|
74
|
+
cyclic (CSS Sizing 3 §5.2) — it resolves against the row's own
|
|
75
|
+
content-derived size, i.e. against itself, and can never actually clamp
|
|
76
|
+
it. Kept only to state intent; the `<img>`/`<picture>` cap below is what
|
|
77
|
+
does the real work by capping the thing with an intrinsic size instead
|
|
78
|
+
of trying to cap a box that is sized BY that intrinsic size. */
|
|
79
|
+
max-block-size: 100%;
|
|
80
|
+
|
|
81
|
+
/* One source of truth for the slide's breathing room: the padding below
|
|
82
|
+
and the image's block cap both read these, so they cannot drift apart. */
|
|
83
|
+
--lightbox-pad-block: 5dvh;
|
|
84
|
+
--lightbox-pad-inline: 5dvw;
|
|
85
|
+
|
|
86
|
+
/* `.carousel__media`'s own rule reads this for its `object-fit` (default
|
|
87
|
+
`cover`, right for a thumbnail strip, wrong for a full image — nothing
|
|
88
|
+
here should be cropped). Nesting under `dialog.lightbox-dialog` gives
|
|
89
|
+
this whole block higher specificity than carousel.css's bare
|
|
90
|
+
`.carousel__media` rule regardless of import order, so it always wins.
|
|
91
|
+
Belt-and-braces only now: the image rule below sizes the box to the
|
|
92
|
+
image, so there's normally nothing left for `object-fit` to do. */
|
|
93
|
+
--carousel-media-fit: contain;
|
|
94
|
+
|
|
95
|
+
/* No `.carousel__track { max-block-size: 100% }` override here — same
|
|
96
|
+
cyclic-percentage trap as above, against `.carousel`'s own implicit
|
|
97
|
+
`auto` row. It has never applied. */
|
|
98
|
+
|
|
99
|
+
/* Breathing room against the edge-to-edge dialog, on the SLIDE rather than
|
|
100
|
+
the media box — a slide is the grid item `align-items: stretch` fills to
|
|
101
|
+
the track's full height, so padding here is what keeps the image off the
|
|
102
|
+
glass instead of the media box's aspect-ratio doing it. */
|
|
103
|
+
.carousel__slide {
|
|
104
|
+
box-sizing: border-box;
|
|
105
|
+
padding: var(--lightbox-pad-block) var(--lightbox-pad-inline);
|
|
106
|
+
|
|
107
|
+
/* carousel.css gives a slide `align-content: start` so a `cover`ed media
|
|
108
|
+
box (which fills the row) hugs the top. Here the media box is
|
|
109
|
+
deliberately SMALLER than the slide — it's sized to the image, not the
|
|
110
|
+
other way around — so it must be centred instead, or a 16/9 image
|
|
111
|
+
would hang from the top of a row sized by a 9/16 neighbour. */
|
|
112
|
+
align-content: center;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.carousel__media {
|
|
116
|
+
/* Full images vary wildly in ratio; drop carousel.css's fixed 16/9 box
|
|
117
|
+
(right for a thumbnail strip, wrong here) and let the image below
|
|
118
|
+
determine the box's size. carousel.css's `block-size: 100%` is
|
|
119
|
+
deliberately NOT reinstated here: against this slide's content-sized
|
|
120
|
+
`auto` row it's the same cyclic percentage as the caps above, and it
|
|
121
|
+
only ever resolved back to the content height it was meant to cap. */
|
|
122
|
+
aspect-ratio: unset;
|
|
123
|
+
|
|
124
|
+
/* Painted the instant a slide is visible, whether or not the full image
|
|
125
|
+
has loaded yet — the morph target is never an empty box. Set per-slide
|
|
126
|
+
by `<Gallery />` from the thumbnail URL; declared as a var rather than
|
|
127
|
+
a bare property so a slide that omits a thumb (no placeholder) simply
|
|
128
|
+
paints nothing extra. `contain` + `center` lands on the same rect the
|
|
129
|
+
image below resolves to, so the placeholder doesn't jump on load. */
|
|
130
|
+
background-image: var(--lightbox-placeholder, none);
|
|
131
|
+
background-size: contain;
|
|
132
|
+
background-position: center;
|
|
133
|
+
background-repeat: no-repeat;
|
|
134
|
+
|
|
135
|
+
/* `<wc-gallery>` assigns `view-transition-name: lightbox-active` to the
|
|
136
|
+
slide's `<img>` and the originating thumbnail at the moment it opens,
|
|
137
|
+
then clears it on close — that shared, runtime-assigned name is what
|
|
138
|
+
morphs one into the other. `match-element` (the auto-generated
|
|
139
|
+
per-element name) does NOT do this: it names each element uniquely,
|
|
140
|
+
so it can never morph a thumbnail into a different `<img>`. Do not put
|
|
141
|
+
a `view-transition-name` on the image here statically. */
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* THE FIX. carousel.css sizes the media to fill its box
|
|
145
|
+
(`inline-size`/`block-size: 100%`) so `object-fit: cover` has something
|
|
146
|
+
to crop — right for a thumbnail strip, backwards for a lightbox, where
|
|
147
|
+
the BOX should instead be sized to the image. With both axes `auto` and
|
|
148
|
+
both maxes set, the browser's replaced-element min/max algorithm
|
|
149
|
+
(CSS 2.1 §10.4) does a ratio-preserving clamp against whichever axis
|
|
150
|
+
binds — so the element's border box IS the visible image, with no
|
|
151
|
+
cyclic percentages and no dependency on how many rows the grid above
|
|
152
|
+
ends up with. That also matters for two things beyond sizing: it's the
|
|
153
|
+
`view-transition-name` target (WCGallery.ts), so the morph must not
|
|
154
|
+
land on an oversized box; and it keeps the letterbox area click-through
|
|
155
|
+
for the dialog's `closedby="any"`.
|
|
156
|
+
|
|
157
|
+
A DESCENDANT selector, not a direct-child `>`: an `ImageMetadata` src
|
|
158
|
+
renders `<picture><img></picture>` (ResponsiveImage.astro), and
|
|
159
|
+
carousel.css's own rule only reaches the non-replaced `<picture>` via
|
|
160
|
+
`> :is(img, picture, …)` — `object-fit` is a no-op there, and the real
|
|
161
|
+
`<img>` inside gets no sizing at all. This selector covers both the
|
|
162
|
+
bare-`<img>` demo and the `<picture>`-wrapped case. */
|
|
163
|
+
.carousel__media :is(img, picture) {
|
|
164
|
+
/* Undo carousel.css's `inline-size/block-size: 100%` — a fixed size on
|
|
165
|
+
either axis would defeat the ratio-preserving clamp below. */
|
|
166
|
+
inline-size: auto;
|
|
167
|
+
block-size: auto;
|
|
168
|
+
max-inline-size: 100%; /* of the padded slide, i.e. ~90dvw */
|
|
169
|
+
/* The one cap that actually binds (not a percentage, so not cyclic).
|
|
170
|
+
`--lightbox-caption-space` is 0 by default — nothing here uses a
|
|
171
|
+
caption today, but `<figcaption>` stacks below the image inside this
|
|
172
|
+
same box (Gallery.astro), so a captioned gallery must set it to
|
|
173
|
+
reserve room or the slide can exceed the viewport by that height. */
|
|
174
|
+
max-block-size: calc(
|
|
175
|
+
100dvh - 2 * var(--lightbox-pad-block) - var(--lightbox-caption-space, 0px)
|
|
176
|
+
);
|
|
177
|
+
margin-inline: auto; /* centre in the full-width figure; vertical centring is the slide's align-content */
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.carousel__caption {
|
|
181
|
+
color: inherit;
|
|
182
|
+
}
|
|
63
183
|
}
|
|
64
184
|
|
|
65
185
|
.lightbox-dialog__close {
|
|
@@ -77,6 +197,9 @@ dialog.lightbox-dialog {
|
|
|
77
197
|
display: flex;
|
|
78
198
|
align-items: center;
|
|
79
199
|
justify-content: center;
|
|
200
|
+
text-decoration: none;
|
|
201
|
+
font-size: 1.5rem;
|
|
202
|
+
line-height: 1;
|
|
80
203
|
transition: background-color 0.15s ease;
|
|
81
204
|
z-index: 10;
|
|
82
205
|
|
|
@@ -88,50 +211,4 @@ dialog.lightbox-dialog {
|
|
|
88
211
|
outline-offset: 2px;
|
|
89
212
|
}
|
|
90
213
|
}
|
|
91
|
-
|
|
92
|
-
.lightbox-dialog__nav {
|
|
93
|
-
position: absolute;
|
|
94
|
-
inset-block: 0;
|
|
95
|
-
display: flex;
|
|
96
|
-
align-items: center;
|
|
97
|
-
padding: 1rem;
|
|
98
|
-
|
|
99
|
-
&[data-direction='prev'] {
|
|
100
|
-
inset-inline-start: 0;
|
|
101
|
-
}
|
|
102
|
-
&[data-direction='next'] {
|
|
103
|
-
inset-inline-end: 0;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.lightbox-dialog__nav-button {
|
|
108
|
-
inline-size: 3rem;
|
|
109
|
-
block-size: 3rem;
|
|
110
|
-
padding: 0;
|
|
111
|
-
background: var(--lightbox-nav-bg, transparent);
|
|
112
|
-
border: none;
|
|
113
|
-
border-radius: 50%;
|
|
114
|
-
color: inherit;
|
|
115
|
-
cursor: pointer;
|
|
116
|
-
display: flex;
|
|
117
|
-
align-items: center;
|
|
118
|
-
justify-content: center;
|
|
119
|
-
transition: background-color 0.15s ease;
|
|
120
|
-
|
|
121
|
-
&:hover {
|
|
122
|
-
background: var(--lightbox-nav-hover-bg, oklch(1 0 0 / 0.15));
|
|
123
|
-
}
|
|
124
|
-
&:disabled {
|
|
125
|
-
opacity: 0.3;
|
|
126
|
-
cursor: not-allowed;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.lightbox-dialog__caption {
|
|
131
|
-
position: absolute;
|
|
132
|
-
inset-block-end: 0;
|
|
133
|
-
inset-inline: 0;
|
|
134
|
-
padding: 1rem 2rem;
|
|
135
|
-
text-align: center;
|
|
136
|
-
}
|
|
137
214
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review — the `.review` card's structure only.
|
|
3
|
+
*
|
|
4
|
+
* Box chrome (border/padding/background/shadow) is CONFIG-GENERATED
|
|
5
|
+
* (`patterns.items.review` in design-system.json), the same split `card.css`
|
|
6
|
+
* documents. This partial holds what the framework owns: the internal layout —
|
|
7
|
+
* rating, identity, body, reply, source link.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
.review {
|
|
11
|
+
.review__header {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-wrap: wrap;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: 0.75rem;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.review__avatar {
|
|
19
|
+
inline-size: 2.5rem;
|
|
20
|
+
block-size: 2.5rem;
|
|
21
|
+
border-radius: 50%;
|
|
22
|
+
object-fit: cover;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Shown instead of `.review__avatar` when `avatars` is off (the default) — see
|
|
26
|
+
the config-generated pattern's rationale in AGENTS.md's Reviews section. */
|
|
27
|
+
.review__initials {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
inline-size: 2.5rem;
|
|
32
|
+
block-size: 2.5rem;
|
|
33
|
+
border-radius: 50%;
|
|
34
|
+
background: var(--color-bg-neutral-muted);
|
|
35
|
+
color: var(--color-text-neutral);
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
font-size: 0.875em;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.review__identity {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
gap: 0.125rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.review__author {
|
|
47
|
+
font-weight: 600;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.review__date {
|
|
51
|
+
font-size: 0.875em;
|
|
52
|
+
opacity: 0.7;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.review__stars {
|
|
56
|
+
display: inline-flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
gap: 0.125rem;
|
|
59
|
+
margin-inline-start: auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.review__star--empty {
|
|
63
|
+
opacity: 0.35;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.review__body {
|
|
67
|
+
margin: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.review__reply {
|
|
71
|
+
margin-inline-start: 1.5rem;
|
|
72
|
+
padding-inline-start: 1rem;
|
|
73
|
+
border-inline-start: 2px solid var(--color-bg-surface-muted);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.review__reply-author {
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
font-size: 0.875em;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.review__source {
|
|
82
|
+
font-size: 0.875em;
|
|
83
|
+
align-self: start;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.review-list {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
gap: 1.5rem;
|
|
91
|
+
}
|