scavold 0.2.0 → 0.4.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,75 @@ one offered; a patch release does not.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.4.0] — 2026-10-04
13
+
14
+ ### Added
15
+
16
+ - Layout variants for `section`, `article` and `aside`: `layout=float-left` or
17
+ `float-right` lets the content flow around the first block, an image usually;
18
+ `split-left` or `split-right` sets block and content side by side. The editor offers
19
+ them as buttons with icons Scavold brings along, drawn after Font Awesome's solid
20
+ style; a site replaces one by keeping an icon of the same name in `.cratly/icons/`.
21
+ - The section kinds Scavold brings are described to the editor in English and German —
22
+ their names, hints and the choices of their options — so a German editor no longer
23
+ reads "Preload" or "metadata". A site's `.cratly.config.yaml` may give labels and
24
+ hints in several languages the same way, as a map of locale → text.
25
+ - Each section kind Scavold brings explains itself where the editor offers the types to
26
+ choose from, and links its description on a new page for authors at scavold.io — the
27
+ first of the documentation in German as well as English. A site's own containers do
28
+ the same with `hint` and `docs` in `.cratly.config.yaml`.
29
+
30
+ ### Changed
31
+
32
+ - The section-type manifest carries the texts of the built-in section kinds as maps of
33
+ locale → text, and their `docs` as maps of locale → address. A cratly editor deployed
34
+ before these were supported shows such a text as "[object Object]": update the editor
35
+ before a site moves to this version.
36
+
37
+ ## [0.3.0] — 2026-10-04
38
+
39
+ ### Added
40
+
41
+ - A `card` container: a unit of its own, such as an image, a heading and a few lines.
42
+ `link` names a page and makes the whole card clickable, its "Read more" naming the
43
+ page for screen readers; links written into the card stay clickable.
44
+ - A `details` container: content collapsed under a title (`summary`) until the reader
45
+ expands it, rendered as a native `<details>`. `open` starts it expanded, and blocks
46
+ sharing a `group` close each other, so only one of them is open at a time.
47
+ - A `grid` container: the blocks written inside it side by side, at most `columns` (2,
48
+ 3 or 4) in a row, continuing in the next. A cell never gets narrower than
49
+ `--scavold-grid-min`, so a narrow screen drops columns by itself without breakpoints.
50
+ A theme declares the `sizes` of images in a grid by column count through
51
+ `enhanceApp( context, { gridImageSizes } )`.
52
+ - `provideImageSizes()`: a layout component gives the images inside it a `sizes`
53
+ value, replacing the site's default but not a per-image override.
54
+ - Image captions. The title of a Markdown image is its caption: an image standing alone
55
+ in its paragraph becomes a `<figure>` with a `<figcaption>`, one amid text shows it as
56
+ its tooltip. A grid of captioned images makes a gallery.
57
+ - Icons for section controls. A `boolean` prop and the choices of an `enum` prop in
58
+ `.cratly.config.yaml` may name an `icon`, kept as `.cratly/icons/<name>.svg` in the
59
+ site. The section-type manifest embeds the icons it refers to, so the cratly editor
60
+ can offer a toggle button or a group of buttons instead of a checkbox or a select. A
61
+ reference to a missing icon is reported during the build.
62
+
63
+ ### Changed
64
+
65
+ - The title of a Markdown image is shown: below an image standing alone, as its tooltip
66
+ amid text. A site that used image titles for something else will see them now.
67
+
68
+ ### Fixed
69
+
70
+ - A `sizes=` override in an image title no longer shows up as a tooltip
71
+ ("sizes=100vw") on the published image.
72
+
73
+ ## [0.2.1] — 2026-10-04
74
+
75
+ ### Fixed
76
+
77
+ - Menus and breadcrumbs no longer link a folder without an `index.md` — there is no page
78
+ to go to, and the link was dead. Such a folder is named as text above its pages where a
79
+ menu shows them, left out where a menu does not, and named as text in a breadcrumb.
80
+
12
81
  ## [0.2.0] — 2026-10-04
13
82
 
14
83
  ### Added
@@ -361,7 +430,10 @@ First published release. Version `0.1.0` existed in-tree only.
361
430
  - Only images are processed out of `media_folder`; other file types (video, documents)
362
431
  are never copied into the build output and have to live in the static folder.
363
432
 
364
- [Unreleased]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0...main
433
+ [Unreleased]: https://gitlab.com/cratly/scavold/-/compare/v0.4.0...main
434
+ [0.4.0]: https://gitlab.com/cratly/scavold/-/compare/v0.3.0...v0.4.0
435
+ [0.3.0]: https://gitlab.com/cratly/scavold/-/compare/v0.2.1...v0.3.0
436
+ [0.2.1]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0...v0.2.1
365
437
  [0.2.0]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.12...v0.2.0
366
438
  [0.2.0-rc.12]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.11...v0.2.0-rc.12
367
439
  [0.2.0-rc.11]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.10...v0.2.0-rc.11
package/COMPONENTS.md CHANGED
@@ -28,6 +28,9 @@ Each item in the trail carries `.active` (all ancestor items) and `.current` (th
28
28
  last item, if the current page is included). This makes it straightforward to style
29
29
  the current crumb differently in CSS.
30
30
 
31
+ A folder on the way that has no `index.md` is named as text, a `<span>` in place of the
32
+ `<a>`: it is part of where the page sits, but there is no page to go to.
33
+
31
34
  #### Props
32
35
 
33
36
  | Prop | Type | Default | Description |
@@ -66,6 +69,48 @@ the current crumb differently in CSS.
66
69
 
67
70
  ---
68
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
+
69
114
  ### `<ScavoldContainer>`
70
115
 
71
116
  Fallback component for Markdown container blocks whose name has no dedicated
@@ -246,6 +291,94 @@ not promise.
246
291
 
247
292
  ---
248
293
 
294
+ ### `<ScavoldDetails>`
295
+
296
+ Content collapsed under a title until the reader expands it. Written as a container
297
+ block and rendered as a native `<details>` element, so keyboard operation, the expanded
298
+ state for screen readers and revealing the content on find in page come with it.
299
+
300
+ ```markdown
301
+ ::: details summary="Opening hours" group=faq
302
+ Monday to Friday, 9 am to 5 pm.
303
+ :::
304
+ ```
305
+
306
+ | Argument | Type | Description |
307
+ |---|---|---|
308
+ | `summary` | `string` | Title, always visible; clicking it expands or collapses the content. Left out, a translated "Details" stands in. |
309
+ | `open` | flag | Starts expanded. |
310
+ | `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>`. |
311
+
312
+ ```html
313
+ <details class="scavold-details" name="faq">
314
+ <summary class="scavold-details__summary">Opening hours</summary>
315
+ <div class="scavold-details__content">…</div>
316
+ </details>
317
+ ```
318
+
319
+ Scavold adds no styles of its own; the browser's disclosure marker stays until a theme
320
+ replaces it. Register a component under the name `ScavoldDetails` before calling
321
+ `scavoldEnhanceApp` to render it differently, and resolve the arguments with
322
+ [`useDetails`](#usedetails-props).
323
+
324
+ ---
325
+
326
+ ### `<ScavoldGrid>`
327
+
328
+ Puts the blocks written inside it side by side, at most `columns` of them in a row; a
329
+ full row continues in the next. Each block is one cell — a paragraph, an image, or an
330
+ embedded container, which is how several blocks share a cell:
331
+
332
+ ```markdown
333
+ :::: grid columns=3
334
+ ![Pond in spring](/pond.jpg)
335
+
336
+ ![Meadow](/meadow.jpg)
337
+
338
+ ::: section
339
+ ### Opening hours
340
+ Monday to Friday.
341
+ :::
342
+ ::::
343
+ ```
344
+
345
+ The grid needs the longer fence when it contains a container; the cratly editor writes
346
+ it that way by itself.
347
+
348
+ | Argument | Type | Description |
349
+ |---|---|---|
350
+ | `columns` | `2` \| `3` \| `4` | Most cells side by side. Defaults to `2`. |
351
+
352
+ ```html
353
+ <div class="scavold-grid scavold-grid--3" style="--scavold-grid-columns: 3">…</div>
354
+ ```
355
+
356
+ A cell never gets narrower than `--scavold-grid-min` (default `14rem`), so on a narrow
357
+ screen the grid drops columns by itself, down to one, without breakpoints. Themes set
358
+ that variable and `--scavold-grid-gap` (default `1.5rem`), or replace the layout
359
+ through the `scavold-grid--<columns>` class. Margins of the cells themselves —
360
+ including the indent browsers give a `<figure>` — are removed; the gap spaces them.
361
+
362
+ Images in a grid take the `sizes` the theme declares for its column count, since only
363
+ the theme knows how wide a cell is at which screen width. A column count it leaves out
364
+ keeps the site's `image_sizes` of `.cratly.config.yaml` for its images: sharp in every column, but on a wide screen
365
+ a larger variant than a cell needs. An image with a `sizes=` override of its own keeps
366
+ it, and images nested deeper — in a card in a cell — take the grid's value as well.
367
+
368
+ ```js
369
+ // .vitepress/theme/index.js — cells 22rem wide in a 72rem column, two columns from 48rem
370
+ await scavoldEnhanceApp( context, {
371
+ gridImageSizes: {
372
+ 3: "(min-width: 72rem) 22rem, (min-width: 48rem) 50vw, 100vw",
373
+ },
374
+ } );
375
+ ```
376
+
377
+ Register a component under the name `ScavoldGrid` before calling `scavoldEnhanceApp`
378
+ to render it differently, and resolve the arguments with [`useGrid`](#usegrid-props).
379
+
380
+ ---
381
+
249
382
  ### `<ScavoldImage>`
250
383
 
251
384
  Renders a responsive `<picture>` element for local images. Not intended for direct
@@ -261,6 +394,7 @@ frontmatter field can use it directly.
261
394
  | `srcset` | `string` | `""` | `srcset` string for the original format (JPEG/PNG) |
262
395
  | `webp-srcset` | `string` | `""` | `srcset` string for the WebP variants |
263
396
  | `sizes` | `string` | `"100vw"` | CSS `sizes` attribute applied to all sources |
397
+ | `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. |
264
398
  | `alt` | `string` | `""` | Alt text for the `<img>` element |
265
399
  | `loading` | `string` | `"lazy"` | `loading` attribute of the `<img>`; `"eager"` for an image at the top of a page |
266
400
  | `fetchpriority` | `string` | — | `fetchpriority` attribute of the `<img>`, e.g. `"high"` for the largest image above the fold |
@@ -275,6 +409,30 @@ frontmatter field can use it directly.
275
409
  </picture>
276
410
  ```
277
411
 
412
+ #### Captions
413
+
414
+ The title of a Markdown image is its caption. An image standing alone in its paragraph
415
+ is rendered as a figure with the caption below it; amid text, where no figure fits, the
416
+ caption is the image's tooltip. A `sizes=` override in the title is taken out first and
417
+ never reaches the page:
418
+
419
+ ```markdown
420
+ ![Pond with water lilies](/pond.jpg "The pond in spring")
421
+ ![Pond with water lilies](/pond.jpg "sizes=100vw The pond in spring")
422
+ ```
423
+
424
+ ```html
425
+ <figure class="scavold-figure">
426
+ <picture>…</picture>
427
+ <figcaption class="scavold-figure__caption">The pond in spring</figcaption>
428
+ </figure>
429
+ ```
430
+
431
+ The alt text says what the image shows, for those who cannot see it; the caption adds
432
+ to it for everyone. Scavold adds no styles to a figure, so the indent browsers give
433
+ `<figure>` stays until a theme sets its own margins. A [grid](#scavoldgrid) of
434
+ captioned images is a gallery.
435
+
278
436
  ---
279
437
 
280
438
  ### `<ScavoldLayout>`
@@ -486,6 +644,10 @@ The `.active` class is set on any item that is an ancestor of the current page o
486
644
  current page itself. The `.current` class is set only on the item that exactly matches
487
645
  the current page.
488
646
 
647
+ A folder without an `index.md` has no page to link to. Where the menu shows the
648
+ pages below it, the folder is named as text — a `<span>` in place of the `<a>` — and
649
+ groups them; where it does not, the folder is left out, as it would lead nowhere.
650
+
489
651
  #### Label resolution
490
652
 
491
653
  Item labels are resolved in this order:
@@ -827,6 +989,35 @@ async function enhanceApp( context ) {
827
989
  }
828
990
  ```
829
991
 
992
+ #### Layout
993
+
994
+ `section`, `article` and `aside` take a `layout` arranging their first block — an image,
995
+ usually — and the content after it. Left out, the block stands above the content.
996
+
997
+ ```markdown
998
+ ::: section layout=float-left
999
+ ![The pond in spring](/pond.jpg)
1000
+
1001
+ Text flowing around the image, and on below it.
1002
+ :::
1003
+ ```
1004
+
1005
+ | `layout` | Arrangement |
1006
+ |---|---|
1007
+ | `float-left`, `float-right` | The content flows around the block and on below it. |
1008
+ | `split-left`, `split-right` | Block and content side by side; the content does not run below the block. |
1009
+
1010
+ The element carries the choice as `data-layout`, and `scavold/styles/layouts.css`, which
1011
+ `enhanceApp` brings along, implements it. A section narrower than 30rem stacks its block
1012
+ above the content again, whatever its layout. Themes set the block's width through
1013
+ `--scavold-layout-width` (default `40%`) and the space beside and below it through
1014
+ `--scavold-layout-gap` (default `1.5rem`), or replace the rules.
1015
+
1016
+ The cratly editor offers the choices as buttons with Scavold's own icons, drawn after
1017
+ Font Awesome's solid style. A site replaces one by keeping an icon of the same name —
1018
+ `stacked`, `float-left`, `float-right`, `split-left`, `split-right` — in its
1019
+ `.cratly/icons/`.
1020
+
830
1021
  ---
831
1022
 
832
1023
  ## Composables
@@ -877,6 +1068,24 @@ Leave the `autoplay` attribute off the element; it would start the video regardl
877
1068
 
878
1069
  ---
879
1070
 
1071
+ ### `useCard( props )`
1072
+
1073
+ ```js
1074
+ import { useCard, cardProps } from "scavold/composables/useCard.js";
1075
+ ```
1076
+
1077
+ Resolves the arguments of a `:::card` block for a site's own card component:
1078
+
1079
+ ```js
1080
+ const props = defineProps( { ...cardProps } );
1081
+ const { href, title } = useCard( props );
1082
+ ```
1083
+
1084
+ `href` is the URL of the linked page and `title` its title, both empty when the card
1085
+ links nowhere or to a page that does not exist.
1086
+
1087
+ ---
1088
+
880
1089
  ### `useContainer( props )`
881
1090
 
882
1091
  ```js
@@ -981,6 +1190,41 @@ are exported as well.
981
1190
 
982
1191
  ---
983
1192
 
1193
+ ### `useDetails( props )`
1194
+
1195
+ ```js
1196
+ import { useDetails, detailsProps } from "scavold/composables/useDetails.js";
1197
+ ```
1198
+
1199
+ Resolves the arguments of a `:::details` block for a site's own details component:
1200
+
1201
+ ```js
1202
+ const props = defineProps( { ...detailsProps } );
1203
+ const { summary, open, group } = useDetails( props );
1204
+ ```
1205
+
1206
+ `summary` is the trimmed title or an empty string, `open` a boolean, and `group` the
1207
+ group name or `undefined`.
1208
+
1209
+ ---
1210
+
1211
+ ### `useGrid( props )`
1212
+
1213
+ ```js
1214
+ import { useGrid, gridProps, GRID_COLUMNS } from "scavold/composables/useGrid.js";
1215
+ ```
1216
+
1217
+ Resolves the arguments of a `:::grid` block for a site's own grid component:
1218
+
1219
+ ```js
1220
+ const props = defineProps( { ...gridProps } );
1221
+ const { columns } = useGrid( props );
1222
+ ```
1223
+
1224
+ `columns` is one of `GRID_COLUMNS` (`2`, `3`, `4`), falling back to the first.
1225
+
1226
+ ---
1227
+
984
1228
  ### `useHierarchy()`
985
1229
 
986
1230
  ```js
@@ -1063,6 +1307,29 @@ const items = computed( () =>
1063
1307
 
1064
1308
  ---
1065
1309
 
1310
+ ### `useImageSizes()` / `provideImageSizes( sizes )`
1311
+
1312
+ ```js
1313
+ import { provideImageSizes, useImageSizes } from "scavold/composables/useImageSizes.js";
1314
+ ```
1315
+
1316
+ A layout component that knows how wide it draws its content provides a `sizes` value
1317
+ for the images inside it — a string or a ref:
1318
+
1319
+ ```js
1320
+ // SiteSidebar.vue — content 18rem wide from 64rem, full width below
1321
+ provideImageSizes( "(min-width: 64rem) 18rem, 100vw" );
1322
+ ```
1323
+
1324
+ Every `<ScavoldImage>` inside that carries only the site's default takes this value
1325
+ instead; one with a `sizes=` override, or given its `sizes` by a component like the
1326
+ cover, keeps its own. An empty value leaves the images with what the layout around
1327
+ provides. The value is resolved while rendering, so it is in the markup the server
1328
+ sends. `useImageSizes()` reads it, for a component rendering images of its own;
1329
+ `<ScavoldGrid>` provides one from `gridImageSizes`.
1330
+
1331
+ ---
1332
+
1066
1333
  ### `usePageList( props )`
1067
1334
 
1068
1335
  ```js
@@ -1316,6 +1583,9 @@ reserved), loses browser preload scanning (images load later), and produces a fl
1316
1583
  of empty space on every navigation during hydration. The `sizes` + `srcset` approach
1317
1584
  was specifically designed to avoid all of these.
1318
1585
 
1586
+ A layout wrapping Markdown content rather than rendering images itself — a sidebar, a
1587
+ grid — reaches the images inside through [`provideImageSizes`](#useimagesizes--provideimagesizes-sizes).
1588
+
1319
1589
  The global `image_sizes` in `.cratly.config.yaml` is a fallback for images that have
1320
1590
  no layout-aware component wrapping them. Set it to the most common case in the
1321
1591
  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"
@@ -19,7 +19,9 @@ const { nodeLabel, nodeHref } = useHierarchy();
19
19
  current: item.current,
20
20
  }"
21
21
  >
22
- <a :href="nodeHref( item.node )" :aria-current="item.current ? 'page' : undefined">{{ nodeLabel( item.node ) }}</a>
22
+ <a v-if="item.node.isPage" :href="nodeHref( item.node )" :aria-current="item.current ? 'page' : undefined">{{ nodeLabel( item.node ) }}</a>
23
+ <!-- A folder without an index.md: there is no page to go to, only the ones below. -->
24
+ <span v-else>{{ nodeLabel( item.node ) }}</span>
23
25
  <ScavoldMenuItems
24
26
  v-if="item.children.length"
25
27
  :items="item.children"
@@ -152,7 +152,10 @@ export function useHierarchy() {
152
152
  ? collectItems( node, remaining === -1 ? -1 : remaining - 1, activeOnly, expand )
153
153
  : [],
154
154
  };
155
- } );
155
+ } )
156
+ // A folder without an index.md is no page to link to: it only groups the
157
+ // pages below it, and where those are not shown it leads nowhere.
158
+ .filter( item => item.node.isPage || item.children.length );
156
159
  };
157
160
 
158
161
  /**
@@ -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
+ }