scavold 0.2.1 → 0.5.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
@@ -9,6 +9,83 @@ one offered; a patch release does not.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.5.0] — 2026-10-04
13
+
14
+ ### Added
15
+
16
+ - A `group` container: content held together only to arrange it — an image with the
17
+ paragraph flowing around it, say — without claiming a meaning, rendered as a `<div>`.
18
+ It offers the same layouts as a section, for where no sectioning element fits.
19
+
20
+ ### Changed
21
+
22
+ - The option of `details` that makes blocks close each other is called "Accordion" in
23
+ the editor ("Akkordeon" in German), so it is not mistaken for the new group. It is
24
+ still written `group=` in Markdown.
25
+
26
+ ## [0.4.0] — 2026-10-04
27
+
28
+ ### Added
29
+
30
+ - Layout variants for `section`, `article` and `aside`: `layout=float-left` or
31
+ `float-right` lets the content flow around the first block, an image usually;
32
+ `split-left` or `split-right` sets block and content side by side. The editor offers
33
+ them as buttons with icons Scavold brings along, drawn after Font Awesome's solid
34
+ style; a site replaces one by keeping an icon of the same name in `.cratly/icons/`.
35
+ - The section kinds Scavold brings are described to the editor in English and German —
36
+ their names, hints and the choices of their options — so a German editor no longer
37
+ reads "Preload" or "metadata". A site's `.cratly.config.yaml` may give labels and
38
+ hints in several languages the same way, as a map of locale → text.
39
+ - Each section kind Scavold brings explains itself where the editor offers the types to
40
+ choose from, and links its description on a new page for authors at scavold.io — the
41
+ first of the documentation in German as well as English. A site's own containers do
42
+ the same with `hint` and `docs` in `.cratly.config.yaml`.
43
+
44
+ ### Changed
45
+
46
+ - The section-type manifest carries the texts of the built-in section kinds as maps of
47
+ locale → text, and their `docs` as maps of locale → address. A cratly editor deployed
48
+ before these were supported shows such a text as "[object Object]": update the editor
49
+ before a site moves to this version.
50
+
51
+ ## [0.3.0] — 2026-10-04
52
+
53
+ Tagged, not published to npm; its changes reach sites with 0.4.0.
54
+
55
+ ### Added
56
+
57
+ - A `card` container: a unit of its own, such as an image, a heading and a few lines.
58
+ `link` names a page and makes the whole card clickable, its "Read more" naming the
59
+ page for screen readers; links written into the card stay clickable.
60
+ - A `details` container: content collapsed under a title (`summary`) until the reader
61
+ expands it, rendered as a native `<details>`. `open` starts it expanded, and blocks
62
+ sharing a `group` close each other, so only one of them is open at a time.
63
+ - A `grid` container: the blocks written inside it side by side, at most `columns` (2,
64
+ 3 or 4) in a row, continuing in the next. A cell never gets narrower than
65
+ `--scavold-grid-min`, so a narrow screen drops columns by itself without breakpoints.
66
+ A theme declares the `sizes` of images in a grid by column count through
67
+ `enhanceApp( context, { gridImageSizes } )`.
68
+ - `provideImageSizes()`: a layout component gives the images inside it a `sizes`
69
+ value, replacing the site's default but not a per-image override.
70
+ - Image captions. The title of a Markdown image is its caption: an image standing alone
71
+ in its paragraph becomes a `<figure>` with a `<figcaption>`, one amid text shows it as
72
+ its tooltip. A grid of captioned images makes a gallery.
73
+ - Icons for section controls. A `boolean` prop and the choices of an `enum` prop in
74
+ `.cratly.config.yaml` may name an `icon`, kept as `.cratly/icons/<name>.svg` in the
75
+ site. The section-type manifest embeds the icons it refers to, so the cratly editor
76
+ can offer a toggle button or a group of buttons instead of a checkbox or a select. A
77
+ reference to a missing icon is reported during the build.
78
+
79
+ ### Changed
80
+
81
+ - The title of a Markdown image is shown: below an image standing alone, as its tooltip
82
+ amid text. A site that used image titles for something else will see them now.
83
+
84
+ ### Fixed
85
+
86
+ - A `sizes=` override in an image title no longer shows up as a tooltip
87
+ ("sizes=100vw") on the published image.
88
+
12
89
  ## [0.2.1] — 2026-10-04
13
90
 
14
91
  ### Fixed
@@ -369,7 +446,10 @@ First published release. Version `0.1.0` existed in-tree only.
369
446
  - Only images are processed out of `media_folder`; other file types (video, documents)
370
447
  are never copied into the build output and have to live in the static folder.
371
448
 
372
- [Unreleased]: https://gitlab.com/cratly/scavold/-/compare/v0.2.1...main
449
+ [Unreleased]: https://gitlab.com/cratly/scavold/-/compare/v0.5.0...main
450
+ [0.5.0]: https://gitlab.com/cratly/scavold/-/compare/v0.4.0...v0.5.0
451
+ [0.4.0]: https://gitlab.com/cratly/scavold/-/compare/v0.3.0...v0.4.0
452
+ [0.3.0]: https://gitlab.com/cratly/scavold/-/compare/v0.2.1...v0.3.0
373
453
  [0.2.1]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0...v0.2.1
374
454
  [0.2.0]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.12...v0.2.0
375
455
  [0.2.0-rc.12]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.11...v0.2.0-rc.12
package/COMPONENTS.md CHANGED
@@ -69,6 +69,48 @@ A folder on the way that has no `index.md` is named as text, a `<span>` in place
69
69
 
70
70
  ---
71
71
 
72
+ ### `<ScavoldCard>`
73
+
74
+ A unit of its own — an image, a heading and a few lines, say — set apart from the text
75
+ around it. Cards side by side go into a [grid](#scavoldgrid):
76
+
77
+ ```markdown
78
+ :::: grid columns=3
79
+ ::: card link=about/team.md
80
+ ![Our team at the pond](/team.jpg)
81
+
82
+ ### Who we are
83
+
84
+ Twelve people, one pond.
85
+ :::
86
+ ::::
87
+ ```
88
+
89
+ | Argument | Type | Description |
90
+ |---|---|---|
91
+ | `link` | `page-ref` | Page the card links to. Makes the whole card clickable; a reference to a page that does not exist leaves the card unlinked. |
92
+
93
+ ```html
94
+ <div class="scavold-card scavold-card--linked">
95
+ …
96
+ <a class="scavold-card__link" href="/about/team.html" aria-label="Read more: Team">Read more</a>
97
+ </div>
98
+ ```
99
+
100
+ The link's text is the same on every card, translated; its accessible name adds the
101
+ title of the page it leads to, so a list of links read out by a screen reader tells the
102
+ cards apart. Its click area is stretched over the whole card, while links written into
103
+ the card stay clickable on top.
104
+
105
+ Scavold styles only what keeps a card working: images stay within it, and the link
106
+ covers it. Its frame, padding and where its image sits are the theme's, through
107
+ `scavold-card` and `scavold-card--linked`.
108
+
109
+ Register a component under the name `ScavoldCard` before calling `scavoldEnhanceApp` to
110
+ render it differently, and resolve the arguments with [`useCard`](#usecard-props).
111
+
112
+ ---
113
+
72
114
  ### `<ScavoldContainer>`
73
115
 
74
116
  Fallback component for Markdown container blocks whose name has no dedicated
@@ -92,6 +134,11 @@ component for each container name they declare in `.cratly.config.yaml`.
92
134
 
93
135
  See [`useContainer`](#usecontainer-props) for building custom container components.
94
136
 
137
+ `:::group` is a built-in mapped to it: content held together only to arrange it, with no
138
+ meaning of its own, rendered as `<div class="group">`. It offers the same
139
+ [layout](#layout) as a section. Use it where no sectioning element fits — HTML keeps
140
+ `<div>` for exactly that.
141
+
95
142
  ---
96
143
 
97
144
  ### `<ScavoldCover>`
@@ -249,6 +296,94 @@ not promise.
249
296
 
250
297
  ---
251
298
 
299
+ ### `<ScavoldDetails>`
300
+
301
+ Content collapsed under a title until the reader expands it. Written as a container
302
+ block and rendered as a native `<details>` element, so keyboard operation, the expanded
303
+ state for screen readers and revealing the content on find in page come with it.
304
+
305
+ ```markdown
306
+ ::: details summary="Opening hours" group=faq
307
+ Monday to Friday, 9 am to 5 pm.
308
+ :::
309
+ ```
310
+
311
+ | Argument | Type | Description |
312
+ |---|---|---|
313
+ | `summary` | `string` | Title, always visible; clicking it expands or collapses the content. Left out, a translated "Details" stands in. |
314
+ | `open` | flag | Starts expanded. |
315
+ | `group` | `string` | Blocks with the same group close each other, so only one is expanded at a time. Rendered as the `name` attribute of `<details>`. |
316
+
317
+ ```html
318
+ <details class="scavold-details" name="faq">
319
+ <summary class="scavold-details__summary">Opening hours</summary>
320
+ <div class="scavold-details__content">…</div>
321
+ </details>
322
+ ```
323
+
324
+ Scavold adds no styles of its own; the browser's disclosure marker stays until a theme
325
+ replaces it. Register a component under the name `ScavoldDetails` before calling
326
+ `scavoldEnhanceApp` to render it differently, and resolve the arguments with
327
+ [`useDetails`](#usedetails-props).
328
+
329
+ ---
330
+
331
+ ### `<ScavoldGrid>`
332
+
333
+ Puts the blocks written inside it side by side, at most `columns` of them in a row; a
334
+ full row continues in the next. Each block is one cell — a paragraph, an image, or an
335
+ embedded container, which is how several blocks share a cell:
336
+
337
+ ```markdown
338
+ :::: grid columns=3
339
+ ![Pond in spring](/pond.jpg)
340
+
341
+ ![Meadow](/meadow.jpg)
342
+
343
+ ::: section
344
+ ### Opening hours
345
+ Monday to Friday.
346
+ :::
347
+ ::::
348
+ ```
349
+
350
+ The grid needs the longer fence when it contains a container; the cratly editor writes
351
+ it that way by itself.
352
+
353
+ | Argument | Type | Description |
354
+ |---|---|---|
355
+ | `columns` | `2` \| `3` \| `4` | Most cells side by side. Defaults to `2`. |
356
+
357
+ ```html
358
+ <div class="scavold-grid scavold-grid--3" style="--scavold-grid-columns: 3">…</div>
359
+ ```
360
+
361
+ A cell never gets narrower than `--scavold-grid-min` (default `14rem`), so on a narrow
362
+ screen the grid drops columns by itself, down to one, without breakpoints. Themes set
363
+ that variable and `--scavold-grid-gap` (default `1.5rem`), or replace the layout
364
+ through the `scavold-grid--<columns>` class. Margins of the cells themselves —
365
+ including the indent browsers give a `<figure>` — are removed; the gap spaces them.
366
+
367
+ Images in a grid take the `sizes` the theme declares for its column count, since only
368
+ the theme knows how wide a cell is at which screen width. A column count it leaves out
369
+ keeps the site's `image_sizes` of `.cratly.config.yaml` for its images: sharp in every column, but on a wide screen
370
+ a larger variant than a cell needs. An image with a `sizes=` override of its own keeps
371
+ it, and images nested deeper — in a card in a cell — take the grid's value as well.
372
+
373
+ ```js
374
+ // .vitepress/theme/index.js — cells 22rem wide in a 72rem column, two columns from 48rem
375
+ await scavoldEnhanceApp( context, {
376
+ gridImageSizes: {
377
+ 3: "(min-width: 72rem) 22rem, (min-width: 48rem) 50vw, 100vw",
378
+ },
379
+ } );
380
+ ```
381
+
382
+ Register a component under the name `ScavoldGrid` before calling `scavoldEnhanceApp`
383
+ to render it differently, and resolve the arguments with [`useGrid`](#usegrid-props).
384
+
385
+ ---
386
+
252
387
  ### `<ScavoldImage>`
253
388
 
254
389
  Renders a responsive `<picture>` element for local images. Not intended for direct
@@ -264,6 +399,7 @@ frontmatter field can use it directly.
264
399
  | `srcset` | `string` | `""` | `srcset` string for the original format (JPEG/PNG) |
265
400
  | `webp-srcset` | `string` | `""` | `srcset` string for the WebP variants |
266
401
  | `sizes` | `string` | `"100vw"` | CSS `sizes` attribute applied to all sources |
402
+ | `sizes-fallback` | `boolean` | `false` | Marks `sizes` as the site's default only, which a layout around the image may replace (see [`provideImageSizes`](#useimagesizes--provideimagesizes-sizes)). Set by the Markdown renderer. |
267
403
  | `alt` | `string` | `""` | Alt text for the `<img>` element |
268
404
  | `loading` | `string` | `"lazy"` | `loading` attribute of the `<img>`; `"eager"` for an image at the top of a page |
269
405
  | `fetchpriority` | `string` | — | `fetchpriority` attribute of the `<img>`, e.g. `"high"` for the largest image above the fold |
@@ -278,6 +414,30 @@ frontmatter field can use it directly.
278
414
  </picture>
279
415
  ```
280
416
 
417
+ #### Captions
418
+
419
+ The title of a Markdown image is its caption. An image standing alone in its paragraph
420
+ is rendered as a figure with the caption below it; amid text, where no figure fits, the
421
+ caption is the image's tooltip. A `sizes=` override in the title is taken out first and
422
+ never reaches the page:
423
+
424
+ ```markdown
425
+ ![Pond with water lilies](/pond.jpg "The pond in spring")
426
+ ![Pond with water lilies](/pond.jpg "sizes=100vw The pond in spring")
427
+ ```
428
+
429
+ ```html
430
+ <figure class="scavold-figure">
431
+ <picture>…</picture>
432
+ <figcaption class="scavold-figure__caption">The pond in spring</figcaption>
433
+ </figure>
434
+ ```
435
+
436
+ The alt text says what the image shows, for those who cannot see it; the caption adds
437
+ to it for everyone. Scavold adds no styles to a figure, so the indent browsers give
438
+ `<figure>` stays until a theme sets its own margins. A [grid](#scavoldgrid) of
439
+ captioned images is a gallery.
440
+
281
441
  ---
282
442
 
283
443
  ### `<ScavoldLayout>`
@@ -834,6 +994,36 @@ async function enhanceApp( context ) {
834
994
  }
835
995
  ```
836
996
 
997
+ #### Layout
998
+
999
+ `section`, `article` and `aside`, and the generic `group`, take a `layout` arranging their
1000
+ first block — an image, usually — and the content after it. Left out, the block stands
1001
+ above the content.
1002
+
1003
+ ```markdown
1004
+ ::: section layout=float-left
1005
+ ![The pond in spring](/pond.jpg)
1006
+
1007
+ Text flowing around the image, and on below it.
1008
+ :::
1009
+ ```
1010
+
1011
+ | `layout` | Arrangement |
1012
+ |---|---|
1013
+ | `float-left`, `float-right` | The content flows around the block and on below it. |
1014
+ | `split-left`, `split-right` | Block and content side by side; the content does not run below the block. |
1015
+
1016
+ The element carries the choice as `data-layout`, and `scavold/styles/layouts.css`, which
1017
+ `enhanceApp` brings along, implements it. A section narrower than 30rem stacks its block
1018
+ above the content again, whatever its layout. Themes set the block's width through
1019
+ `--scavold-layout-width` (default `40%`) and the space beside and below it through
1020
+ `--scavold-layout-gap` (default `1.5rem`), or replace the rules.
1021
+
1022
+ The cratly editor offers the choices as buttons with Scavold's own icons, drawn after
1023
+ Font Awesome's solid style. A site replaces one by keeping an icon of the same name —
1024
+ `stacked`, `float-left`, `float-right`, `split-left`, `split-right` — in its
1025
+ `.cratly/icons/`.
1026
+
837
1027
  ---
838
1028
 
839
1029
  ## Composables
@@ -884,6 +1074,24 @@ Leave the `autoplay` attribute off the element; it would start the video regardl
884
1074
 
885
1075
  ---
886
1076
 
1077
+ ### `useCard( props )`
1078
+
1079
+ ```js
1080
+ import { useCard, cardProps } from "scavold/composables/useCard.js";
1081
+ ```
1082
+
1083
+ Resolves the arguments of a `:::card` block for a site's own card component:
1084
+
1085
+ ```js
1086
+ const props = defineProps( { ...cardProps } );
1087
+ const { href, title } = useCard( props );
1088
+ ```
1089
+
1090
+ `href` is the URL of the linked page and `title` its title, both empty when the card
1091
+ links nowhere or to a page that does not exist.
1092
+
1093
+ ---
1094
+
887
1095
  ### `useContainer( props )`
888
1096
 
889
1097
  ```js
@@ -988,6 +1196,41 @@ are exported as well.
988
1196
 
989
1197
  ---
990
1198
 
1199
+ ### `useDetails( props )`
1200
+
1201
+ ```js
1202
+ import { useDetails, detailsProps } from "scavold/composables/useDetails.js";
1203
+ ```
1204
+
1205
+ Resolves the arguments of a `:::details` block for a site's own details component:
1206
+
1207
+ ```js
1208
+ const props = defineProps( { ...detailsProps } );
1209
+ const { summary, open, group } = useDetails( props );
1210
+ ```
1211
+
1212
+ `summary` is the trimmed title or an empty string, `open` a boolean, and `group` the
1213
+ group name or `undefined`.
1214
+
1215
+ ---
1216
+
1217
+ ### `useGrid( props )`
1218
+
1219
+ ```js
1220
+ import { useGrid, gridProps, GRID_COLUMNS } from "scavold/composables/useGrid.js";
1221
+ ```
1222
+
1223
+ Resolves the arguments of a `:::grid` block for a site's own grid component:
1224
+
1225
+ ```js
1226
+ const props = defineProps( { ...gridProps } );
1227
+ const { columns } = useGrid( props );
1228
+ ```
1229
+
1230
+ `columns` is one of `GRID_COLUMNS` (`2`, `3`, `4`), falling back to the first.
1231
+
1232
+ ---
1233
+
991
1234
  ### `useHierarchy()`
992
1235
 
993
1236
  ```js
@@ -1070,6 +1313,29 @@ const items = computed( () =>
1070
1313
 
1071
1314
  ---
1072
1315
 
1316
+ ### `useImageSizes()` / `provideImageSizes( sizes )`
1317
+
1318
+ ```js
1319
+ import { provideImageSizes, useImageSizes } from "scavold/composables/useImageSizes.js";
1320
+ ```
1321
+
1322
+ A layout component that knows how wide it draws its content provides a `sizes` value
1323
+ for the images inside it — a string or a ref:
1324
+
1325
+ ```js
1326
+ // SiteSidebar.vue — content 18rem wide from 64rem, full width below
1327
+ provideImageSizes( "(min-width: 64rem) 18rem, 100vw" );
1328
+ ```
1329
+
1330
+ Every `<ScavoldImage>` inside that carries only the site's default takes this value
1331
+ instead; one with a `sizes=` override, or given its `sizes` by a component like the
1332
+ cover, keeps its own. An empty value leaves the images with what the layout around
1333
+ provides. The value is resolved while rendering, so it is in the markup the server
1334
+ sends. `useImageSizes()` reads it, for a component rendering images of its own;
1335
+ `<ScavoldGrid>` provides one from `gridImageSizes`.
1336
+
1337
+ ---
1338
+
1073
1339
  ### `usePageList( props )`
1074
1340
 
1075
1341
  ```js
@@ -1323,6 +1589,9 @@ reserved), loses browser preload scanning (images load later), and produces a fl
1323
1589
  of empty space on every navigation during hydration. The `sizes` + `srcset` approach
1324
1590
  was specifically designed to avoid all of these.
1325
1591
 
1592
+ A layout wrapping Markdown content rather than rendering images itself — a sidebar, a
1593
+ grid — reaches the images inside through [`provideImageSizes`](#useimagesizes--provideimagesizes-sizes).
1594
+
1326
1595
  The global `image_sizes` in `.cratly.config.yaml` is a fallback for images that have
1327
1596
  no layout-aware component wrapping them. Set it to the most common case in the
1328
1597
  theme (typically the prose column width) and override via dedicated components for
@@ -0,0 +1,51 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { useCard, cardProps } from "../composables/useCard.js";
4
+ import { useScavoldI18n } from "../composables/useI18n.js";
5
+
6
+ const props = defineProps( cardProps );
7
+
8
+ const { href, title } = useCard( props );
9
+
10
+ const { t } = useScavoldI18n();
11
+ const more = t( "card.more" );
12
+
13
+ // The visible text is the same on every card; the accessible name says where each
14
+ // one leads, starting with the visible text as WCAG 2.5.3 asks.
15
+ const linkLabel = computed( () => ( title.value ? `${more.value}: ${title.value}` : more.value ) );
16
+ </script>
17
+
18
+ <template>
19
+ <div class="scavold-card" :class="[ props.class, { 'scavold-card--linked': href } ]">
20
+ <slot />
21
+ <a v-if="href" class="scavold-card__link" :href="href" :aria-label="linkLabel">{{ more }}</a>
22
+ </div>
23
+ </template>
24
+
25
+ <style scoped>
26
+ /*
27
+ * Functional only: how a card looks — its frame, its padding, where its image sits —
28
+ * is the theme's. What is set here keeps it working: images stay within the card,
29
+ * and the link of a linked card covers all of it, so the whole card can be clicked.
30
+ * Links written into the card stay clickable on top of that.
31
+ */
32
+ .scavold-card :deep(img) {
33
+ max-width: 100%;
34
+ height: auto;
35
+ }
36
+
37
+ .scavold-card--linked {
38
+ position: relative;
39
+ }
40
+
41
+ .scavold-card__link::after {
42
+ content: "";
43
+ position: absolute;
44
+ inset: 0;
45
+ }
46
+
47
+ .scavold-card--linked :deep(a:not(.scavold-card__link)) {
48
+ position: relative;
49
+ z-index: 1;
50
+ }
51
+ </style>
@@ -0,0 +1,23 @@
1
+ <script setup>
2
+ import { useDetails, detailsProps } from "../composables/useDetails.js";
3
+ import { useScavoldI18n } from "../composables/useI18n.js";
4
+
5
+ const props = defineProps( detailsProps );
6
+
7
+ const { summary, open, group } = useDetails( props );
8
+
9
+ const { t } = useScavoldI18n();
10
+ const fallbackSummary = t( "details.summary" );
11
+ </script>
12
+
13
+ <template>
14
+ <!-- Native <details>: keyboard operation, the expanded state for screen readers and
15
+ revealing the content on find in page come with the element. `name` makes
16
+ blocks sharing a group close each other. -->
17
+ <details class="scavold-details" :class="props.class" :open="open" :name="group">
18
+ <summary class="scavold-details__summary">{{ summary || fallbackSummary }}</summary>
19
+ <div class="scavold-details__content">
20
+ <slot />
21
+ </div>
22
+ </details>
23
+ </template>
@@ -0,0 +1,53 @@
1
+ <script setup>
2
+ import { computed, inject } from "vue";
3
+ import { useGrid, gridProps } from "../composables/useGrid.js";
4
+ import { GRID_IMAGE_SIZES, provideImageSizes } from "../composables/useImageSizes.js";
5
+
6
+ const props = defineProps( gridProps );
7
+
8
+ const { columns } = useGrid( props );
9
+
10
+ // The theme knows how wide a cell is at which screen width; without a value for this
11
+ // column count, images keep what applies around the grid.
12
+ const imageSizes = inject( GRID_IMAGE_SIZES, {} );
13
+ provideImageSizes( computed( () => imageSizes[columns.value] ?? "" ) );
14
+
15
+ const style = computed( () => ( { "--scavold-grid-columns": columns.value } ) );
16
+ </script>
17
+
18
+ <template>
19
+ <div class="scavold-grid" :class="[ props.class, `scavold-grid--${columns}` ]" :style="style">
20
+ <slot />
21
+ </div>
22
+ </template>
23
+
24
+ <style scoped>
25
+ /*
26
+ * Functional layout: every block written in the grid is a cell, at most
27
+ * --scavold-grid-columns of them side by side. A cell never gets narrower than
28
+ * --scavold-grid-min, so on a narrow screen the grid drops columns by itself,
29
+ * down to one, without breakpoints. Themes adjust both variables and the gap, or
30
+ * replace the rule entirely.
31
+ */
32
+ .scavold-grid {
33
+ --_gap: var(--scavold-grid-gap, 1.5rem);
34
+ --_min: var(--scavold-grid-min, 14rem);
35
+ --_fraction: calc((100% - (var(--scavold-grid-columns) - 1) * var(--_gap)) / var(--scavold-grid-columns));
36
+
37
+ display: grid;
38
+ gap: var(--_gap);
39
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, max(var(--_min), var(--_fraction))), 1fr));
40
+ }
41
+
42
+ /* The gap spaces the cells; margins of their own would only put them out of line. */
43
+ .scavold-grid > :slotted(*) {
44
+ min-width: 0;
45
+ margin: 0;
46
+ }
47
+
48
+ /* An image never reaches into the next cell, whatever the theme does elsewhere. */
49
+ .scavold-grid :deep(img) {
50
+ max-width: 100%;
51
+ height: auto;
52
+ }
53
+ </style>
@@ -1,14 +1,23 @@
1
1
  <script setup>
2
- defineProps( {
2
+ import { computed } from "vue";
3
+ import { useImageSizes } from "../composables/useImageSizes.js";
4
+
5
+ const props = defineProps( {
3
6
  src: { type: String, required: true },
4
7
  srcset: { type: String, default: "" },
5
8
  webpSrcset: { type: String, default: "" },
6
9
  sizes: { type: String, default: "100vw" },
10
+ // Set by the Markdown renderer when `sizes` is only the site's default, which a
11
+ // layout around the image may replace with a better fit (see useImageSizes).
12
+ sizesFallback: { type: Boolean, default: false },
7
13
  alt: { type: String, default: "" },
8
14
  // An image above the fold — a cover — is wanted at once, not when it scrolls into view.
9
15
  loading: { type: String, default: "lazy" },
10
16
  fetchpriority: { type: String, default: undefined },
11
17
  } );
18
+
19
+ const provided = useImageSizes();
20
+ const effectiveSizes = computed( () => ( props.sizesFallback && provided.value ) || props.sizes );
12
21
  </script>
13
22
 
14
23
  <template>
@@ -17,18 +26,18 @@ defineProps( {
17
26
  v-if="webpSrcset"
18
27
  type="image/webp"
19
28
  :srcset="webpSrcset"
20
- :sizes="sizes"
29
+ :sizes="effectiveSizes"
21
30
  />
22
31
  <source
23
32
  v-if="srcset"
24
33
  :srcset="srcset"
25
- :sizes="sizes"
34
+ :sizes="effectiveSizes"
26
35
  />
27
36
  <img
28
37
  :src="src"
29
38
  :alt="alt"
30
39
  :role="alt ? undefined : 'presentation'"
31
- :sizes="sizes"
40
+ :sizes="effectiveSizes"
32
41
  :loading="loading"
33
42
  :fetchpriority="fetchpriority"
34
43
  decoding="async"
@@ -0,0 +1,45 @@
1
+ import { computed } from "vue";
2
+ import { containerProps } from "./useContainer.js";
3
+ import { useHierarchy } from "./hierarchy";
4
+
5
+ /**
6
+ * Props definition for card container components. Spread into `defineProps`
7
+ * alongside any component-specific props.
8
+ */
9
+ export const cardProps = {
10
+ ...containerProps,
11
+
12
+ /** Page the card links to, as a path in the pages folder. */
13
+ dataLink: { type: String, default: null },
14
+ };
15
+
16
+ /**
17
+ * Composable for card container components.
18
+ *
19
+ * Parses the props emitted by the markdown-it container renderer for a
20
+ * `:::card` block and exposes derived values ready for binding:
21
+ *
22
+ * - `href` — URL of the linked page, or an empty string when the card links
23
+ * nowhere or to a page that does not exist
24
+ * - `title` — title of the linked page, or an empty string
25
+ *
26
+ * Custom card components should call this composable so they share identical
27
+ * attribute resolution without duplicating the logic.
28
+ *
29
+ * @param {object} props reactive props from defineProps
30
+ * @returns {{ href, title }}
31
+ */
32
+ export function useCard( props ) {
33
+ const { resolveByPath, nodeHref, nodeTitle } = useHierarchy();
34
+
35
+ const target = computed( () => {
36
+ const path = props.dataLink?.trim();
37
+
38
+ return path ? resolveByPath( path ) : undefined;
39
+ } );
40
+
41
+ const href = computed( () => ( target.value ? nodeHref( target.value ) : "" ) );
42
+ const title = computed( () => ( target.value ? nodeTitle( target.value ) : "" ) );
43
+
44
+ return { href, title };
45
+ }