scavold 0.2.0-rc.9 → 0.2.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/COMPONENTS.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Scavold component and composable reference
2
2
 
3
+ <!-- Components and composables are listed in alphabetical order, each group on its
4
+ own; the sectioning wrappers close the components as one entry. A new one goes
5
+ where its name sorts, never simply at the end. -->
6
+
3
7
  All components are registered globally by `enhanceApp()` and are available in any
4
8
  Markdown page or Vue template without explicit imports. Composables must be imported
5
9
  explicitly.
@@ -14,154 +18,302 @@ the broader "when and why" context that hover docs cannot convey.
14
18
 
15
19
  ---
16
20
 
17
- ### `<ScavoldMenu>`
21
+ ### `<ScavoldBreadcrumb>`
18
22
 
19
- Renders a `<nav>` element containing a nested list of page links derived from the
20
- site's page hierarchy. Nothing is rendered when the resolved item set is empty.
23
+ Renders a `<nav class="breadcrumb">` element containing a flat list of links
24
+ representing the path from the root to the current page. Uses the same
25
+ `<ul>/<li>/<a>` structure as `<ScavoldMenu>` so both components can share CSS.
21
26
 
22
- The component has three independent addressing modes — path-based, absolute (from
23
- root), and relative (from current page) — that cover all common navigation patterns
24
- without requiring knowledge of the current page's depth.
27
+ Each item in the trail carries `.active` (all ancestor items) and `.current` (the
28
+ last item, if the current page is included). This makes it straightforward to style
29
+ the current crumb differently in CSS.
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.
25
33
 
26
34
  #### Props
27
35
 
28
36
  | Prop | Type | Default | Description |
29
37
  |---|---|---|---|
30
- | `from-path` | `string` | — | Select the parent node by path. Supports `{locale}` (full BCP 47 tag, e.g. `de-CH`) and `{lang}` (primary language subtag only, e.g. `de`) placeholders, replaced with the current page's locale at runtime. Takes priority over `from-root` and `from`. Examples: `"de/footer"`, `"{lang}/footer"`, `"{locale}/footer"`. |
31
- | `from-root` | `number` | — | List items at this absolute depth from root. `1` = top-level pages, `2` = second level, etc. When set, `from` is ignored. If the current page has no ancestor at this depth, nothing is rendered. Ignored when `from-path` is set. |
32
- | `from` | `number` | `0` | List items relative to the current page. `0` = siblings, `1` = children, `-1` = aunt/uncle level (children of grandparent), etc. Ignored when `from-root` or `from-path` is set. |
33
- | `depth` | `number` | `0` | Additional levels to descend below the starting level. `0` = flat list, `1` = one level of children, `-1` = unlimited. |
34
- | `active-only` | `boolean` | `false` | When `true`, only expands children along the branch leading to the current page. Useful with `depth > 0` to show sub-items under the active section only. |
35
- | `expand` | `boolean` | `false` | When `true`, expands children of all nodes regardless of the active branch. Enables sitemap-style rendering. `active-only` takes precedence when both are set. |
36
- | `label` | `string` | — | Accessible label for the `<nav>` landmark (`aria-label`). Should be set whenever more than one `<ScavoldMenu>` appears on the same page so screen readers can distinguish them (e.g. `"Main navigation"`, `"Section navigation"`). |
38
+ | `include-current` | `boolean` | `true` | Include the current page as the last crumb |
39
+ | `include-root` | `boolean` | `false` | Include the root node (typically "Home") as the first crumb |
37
40
 
38
41
  #### Rendered markup
39
42
 
40
43
  ```html
41
- <nav>
44
+ <!-- on page de/leistungen.md with include-root omitted -->
45
+ <nav class="breadcrumb">
42
46
  <ul>
43
- <li class="active"> <!-- .active when node is current or an ancestor -->
44
- <a href="/section/">Section</a>
45
- <ul> <!-- nested only when children are included -->
46
- <li class="current">
47
- <a href="/section/page/">Page</a>
48
- </li>
49
- </ul>
47
+ <li class="active">
48
+ <a href="/de/">Deutsch</a> <!-- de/ folder node, label from de/index.md -->
49
+ </li>
50
+ <li class="active current">
51
+ <a href="/de/leistungen/">Leistungen</a>
50
52
  </li>
51
53
  </ul>
52
54
  </nav>
53
55
  ```
54
56
 
55
- The `.active` class is set on any item that is an ancestor of the current page or the
56
- current page itself. The `.current` class is set only on the item that exactly matches
57
- the current page.
57
+ #### Examples
58
58
 
59
- #### Label resolution
59
+ ```html
60
+ <!-- typical usage — section trail + current page -->
61
+ <ScavoldBreadcrumb />
60
62
 
61
- Item labels are resolved in this order:
63
+ <!-- trail without current page, e.g. when page heading serves as final crumb -->
64
+ <ScavoldBreadcrumb :include-current="false" />
62
65
 
63
- 1. `label` frontmatter — explicit navigation text, overrides everything
64
- 2. `title` frontmatter — page title, used when no `label` is set
65
- 3. First `#` heading in the page body — extracted at build time as a fallback when neither `label` nor `title` is declared in frontmatter
66
- 4. The last path segment of the page file (e.g. `about` from `de/about.md`) — last resort
66
+ <!-- full trail including home link -->
67
+ <ScavoldBreadcrumb include-root />
68
+ ```
67
69
 
68
- #### Hiding pages via front matter
70
+ ---
69
71
 
70
- A page can opt out of appearing in menus by setting `hide` in its front matter:
72
+ ### `<ScavoldContainer>`
71
73
 
72
- | Value | Effect |
73
- |---|---|
74
- | `false` / absent | Visible everywhere (default) |
75
- | `true` | Kept off every surface: menus, breadcrumbs, page lists and the feed |
76
- | `"menu"` | Kept out of menus only |
77
- | `"breadcrumb"` | Kept out of breadcrumbs only |
78
- | `"list"` | Kept out of page lists only |
79
- | `"feed"` | Left out of the RSS feed only |
80
- | `[ "menu", "feed" ]` | Kept off exactly the surfaces named |
74
+ Fallback component for Markdown container blocks whose name has no dedicated
75
+ registered component. Renders as the matching HTML sectioning element if the
76
+ container name is one (`section`, `aside`, etc.), otherwise as a `<div>`.
77
+
78
+ Any container name works without being declared first — undeclared names are caught
79
+ by this component. Because a typo would otherwise become a silent `<div>`, the build
80
+ reports each undeclared name once:
81
+
82
+ ```
83
+ [scavold] container ':::sectoin' is not declared in .cratly.config.yaml — rendering
84
+ it with ScavoldContainer. Declare it to silence this, or fix the name if it is a typo.
85
+ ```
86
+
87
+ Declaring the container in `.cratly.config.yaml` silences the report and gives it
88
+ typed properties in the cratly editor.
89
+
90
+ Not intended for direct use. Theme developers should instead create a dedicated
91
+ component for each container name they declare in `.cratly.config.yaml`.
92
+
93
+ See [`useContainer`](#usecontainer-props) for building custom container components.
81
94
 
82
- ```yaml
83
- ---
84
- hide: menu
85
95
  ---
96
+
97
+ ### `<ScavoldCover>`
98
+
99
+ Shows an image or a video across the full width of the window and at least as high as
100
+ the window, cropped to fit — a cover, or hero, at the top of a page. Written as a
101
+ `:::cover` container block; whatever the block contains is laid on top of the picture.
102
+
103
+ #### Markdown syntax
104
+
105
+ ```markdown
106
+ ::: cover src=/wind-farm.jpg focus="center top" label="Wind turbines at dusk"
107
+ # With new energy into the future
108
+ :::
109
+
110
+ ::: cover src=/turbines.mp4 poster=/turbines.jpg
111
+ :::
86
112
  ```
87
113
 
88
- #### Examples
114
+ #### Arguments
115
+
116
+ | Argument | Type | Default | Description |
117
+ |---|---|---|---|
118
+ | `src` | `media-file` | required | Image or video. A file ending in `.mp4`, `.m4v`, `.webm`, `.ogv` or `.mov` is played as a video, anything else is shown as an image. |
119
+ | `poster` | `media-file` | — | Only for a video: still image shown until it plays, and instead of playing for visitors who asked for reduced motion. |
120
+ | `focus` | `string` | `50% 50%` | Part of the picture kept visible when the window crops it, as a CSS `object-position` — `center top`, `30% 60%`. |
121
+ | `label` | `string` | — | Alternative text of an image, accessible label of a video. Left out, the picture is treated as decoration. |
122
+ | `tone` | `"light" \| "dark"` | — | Whether the content on top is meant to be light (over a dark picture) or dark (over a bright one). Only marks the cover with `scavold-cover--light` or `scavold-cover--dark`; what that looks like is the theme's. Left out, or with any other value, the cover carries neither class. |
123
+
124
+ #### Rendered markup
89
125
 
90
126
  ```html
91
- <!-- top-level pages — typical site header nav -->
92
- <ScavoldMenu :from-root="1" />
127
+ <!-- image, tone=light -->
128
+ <div class="scavold-cover scavold-cover--light" style="--scavold-cover-focus: center top">
129
+ <picture class="scavold-cover__media">
130
+ <source type="image/webp" srcset="..." sizes="(min-aspect-ratio: 2400/1350) 100vw, 177.78vh" />
131
+ <source srcset="..." sizes="..." />
132
+ <img src="..." alt="..." sizes="..." loading="eager" fetchpriority="high" decoding="async" />
133
+ </picture>
134
+ <div class="scavold-cover__overlay"><!-- block content --></div>
135
+ </div>
93
136
 
94
- <!-- second level of the active top-level section only -->
95
- <ScavoldMenu :from-root="2" active-only />
137
+ <!-- video -->
138
+ <div class="scavold-cover scavold-cover--video">
139
+ <video class="scavold-cover__media" src="..." poster="..." preload="metadata" muted loop playsinline></video>
140
+ <div class="scavold-cover__overlay"><!-- block content --></div>
141
+ <button type="button" class="scavold-cover__toggle" aria-label="Pause video">…</button>
142
+ </div>
143
+ ```
96
144
 
97
- <!-- children of the current page -->
98
- <ScavoldMenu :from="1" />
145
+ An image comes with all its variants. Its `sizes` value is derived from its aspect
146
+ ratio: a picture covering an upright phone screen is drawn much wider than the screen,
147
+ and the browser has to fetch a variant for that width, not for the width of the screen.
148
+ It is loaded at once rather than lazily, since a cover is the first thing on a page.
99
149
 
100
- <!-- siblings of the current page -->
101
- <ScavoldMenu />
150
+ A video plays muted in a loop, started by script — unless the visitor's system asks
151
+ for reduced motion, in which case the still image stays and the button offers to start
152
+ it. It plays while at least half of it is on screen — half of the window, for one higher
153
+ than the window — and pauses when scrolled away. A video the visitor paused stays
154
+ paused until they start it again, and one they turned the sound up on keeps playing
155
+ when scrolled away. The button is always there: moving content that lasts more
156
+ than five seconds has to be pausable (WCAG 2.2.2). Its label follows the state and comes
157
+ from the [`@scavold.cover.*` translations](#built-in-translation-keys).
102
158
 
103
- <!-- full site hierarchy as a sitemap -->
104
- <ScavoldMenu :from-root="1" :depth="-1" />
159
+ #### Fitting it into the theme
105
160
 
106
- <!-- two levels starting from the active top-level section, active branch expanded -->
107
- <ScavoldMenu :from-root="1" :depth="1" active-only />
161
+ The component ships functional layout CSS only. It leaves the column it is written in
162
+ by `margin-inline: calc(50% - 50vw)`, which assumes a column centred in the window. Two
163
+ things are the theme's to settle:
108
164
 
109
- <!-- fixed path — same footer nav regardless of locale -->
110
- <ScavoldMenu from-path="de/footer" label="Footer-Navigation" />
165
+ ```css
166
+ /* 100vw includes a classic scrollbar, so the cover would stick out by its width */
167
+ html { overflow-x: clip; }
111
168
 
112
- <!-- locale-aware via primary language tag — works when paths use "de", "en", etc. -->
113
- <ScavoldMenu from-path="{lang}/footer" label="Footer-Navigation" />
169
+ /* leave room for a fixed header; the default is the window height, 100svh */
170
+ .scavold-cover { --scavold-cover-height: calc(100svh - var(--header-height)); }
171
+ ```
114
172
 
115
- <!-- locale-aware via full BCP 47 tag — use when paths include region codes like "de-CH" -->
116
- <ScavoldMenu from-path="{locale}/footer" label="Footer-Navigation" />
173
+ The overlay is an empty grid cell over the picture; position its content, add a scrim
174
+ for legibility and restyle `.scavold-cover__toggle` as the design asks.
175
+
176
+ The `tone` an author picked is the theme's to honour. Scavold sets no colour for it;
177
+ a theme gives each tone the colours of its design, including headings and links its
178
+ prose styles colour on their own:
179
+
180
+ ```css
181
+ .scavold-cover--light .scavold-cover__overlay { color: #fff; }
182
+ .scavold-cover--dark .scavold-cover__overlay { color: #000; }
117
183
  ```
118
184
 
119
- ---
185
+ #### A cover opening the page
120
186
 
121
- ### `<ScavoldBreadcrumb>`
187
+ Where the theme's own parts go around a cover is the theme's choice; Scavold places
188
+ none of them. It only tells, in two ways, that a page opens with a container:
122
189
 
123
- Renders a `<nav class="breadcrumb">` element containing a flat list of links
124
- representing the path from the root to the current page. Uses the same
125
- `<ul>/<li>/<a>` structure as `<ScavoldMenu>` so both components can share CSS.
190
+ - the container carries an empty `data-leading` attribute;
191
+ - the page data carry `leadingContainer`, the name of that container (`"cover"`), or
192
+ `null` when the page opens with anything else. A layout renders its own parts before
193
+ the page content, so this is known at build time, ahead of the content.
126
194
 
127
- Each item in the trail carries `.active` (all ancestor items) and `.current` (the
128
- last item, if the current page is included). This makes it straightforward to style
129
- the current crumb differently in CSS.
195
+ A theme that wants its breadcrumb below a cover opening the page, rather than above
196
+ it, combines both. The layout leaves out the breadcrumb on such a page:
197
+
198
+ ```vue
199
+ <!-- Layout.vue -->
200
+ <script setup>
201
+ import { useData } from "vitepress";
202
+ const { page } = useData();
203
+ </script>
204
+
205
+ <template>
206
+ <ScavoldBreadcrumb v-if="page.leadingContainer !== 'cover'" />
207
+ <Content />
208
+ </template>
209
+ ```
210
+
211
+ …and the cover is mapped to a component of the site's own that wraps the built-in one
212
+ and adds the breadcrumb after it when it is the leading one:
213
+
214
+ ```yaml
215
+ # .cratly.config.yaml — the editor keeps offering the cover's own properties
216
+ containers:
217
+ cover:
218
+ component: SiteCover
219
+ ```
220
+
221
+ ```vue
222
+ <!-- .vitepress/theme/components/SiteCover.vue -->
223
+ <script setup>
224
+ import ScavoldCover from "scavold/components/ScavoldCover.vue";
225
+ import ScavoldBreadcrumb from "scavold/components/ScavoldBreadcrumb.vue";
226
+
227
+ defineOptions( { inheritAttrs: false } );
228
+ </script>
229
+
230
+ <template>
231
+ <ScavoldCover v-bind="$attrs"><slot /></ScavoldCover>
232
+ <ScavoldBreadcrumb v-if="$attrs['data-leading'] != null" />
233
+ </template>
234
+ ```
235
+
236
+ ```js
237
+ // .vitepress/theme/index.js — the container renders the component by name
238
+ async function enhanceApp( context ) {
239
+ await scavoldEnhanceApp( context );
240
+ context.app.component( "SiteCover", SiteCover );
241
+ }
242
+ ```
243
+
244
+ Below a cover the breadcrumb sits inside the content column, so the theme's prose
245
+ styles reach its list; keep them out where they differ from the breadcrumb's own.
246
+
247
+ Visual and reading order stay the same this way, which a reordering stylesheet could
248
+ not promise.
249
+
250
+ ---
251
+
252
+ ### `<ScavoldImage>`
253
+
254
+ Renders a responsive `<picture>` element for local images. Not intended for direct
255
+ use in templates — it is emitted automatically by the Markdown image renderer when a
256
+ local image path is encountered. Custom components that need to display a media-file
257
+ frontmatter field can use it directly.
130
258
 
131
259
  #### Props
132
260
 
133
261
  | Prop | Type | Default | Description |
134
262
  |---|---|---|---|
135
- | `include-current` | `boolean` | `true` | Include the current page as the last crumb |
136
- | `include-root` | `boolean` | `false` | Include the root node (typically "Home") as the first crumb |
263
+ | `src` | `string` | required | URL of the largest fallback image variant |
264
+ | `srcset` | `string` | `""` | `srcset` string for the original format (JPEG/PNG) |
265
+ | `webp-srcset` | `string` | `""` | `srcset` string for the WebP variants |
266
+ | `sizes` | `string` | `"100vw"` | CSS `sizes` attribute applied to all sources |
267
+ | `alt` | `string` | `""` | Alt text for the `<img>` element |
268
+ | `loading` | `string` | `"lazy"` | `loading` attribute of the `<img>`; `"eager"` for an image at the top of a page |
269
+ | `fetchpriority` | `string` | — | `fetchpriority` attribute of the `<img>`, e.g. `"high"` for the largest image above the fold |
137
270
 
138
271
  #### Rendered markup
139
272
 
140
273
  ```html
141
- <!-- on page de/leistungen.md with include-root omitted -->
142
- <nav class="breadcrumb">
143
- <ul>
144
- <li class="active">
145
- <a href="/de/">Deutsch</a> <!-- de/ folder node, label from de/index.md -->
146
- </li>
147
- <li class="active current">
148
- <a href="/de/leistungen/">Leistungen</a>
149
- </li>
150
- </ul>
151
- </nav>
274
+ <picture>
275
+ <source type="image/webp" srcset="..." sizes="..." />
276
+ <source srcset="..." sizes="..." />
277
+ <img src="..." alt="..." sizes="..." loading="lazy" decoding="async" />
278
+ </picture>
152
279
  ```
153
280
 
154
- #### Examples
281
+ ---
155
282
 
156
- ```html
157
- <!-- typical usage — section trail + current page -->
158
- <ScavoldBreadcrumb />
283
+ ### `<ScavoldLayout>`
159
284
 
160
- <!-- trail without current page, e.g. when page heading serves as final crumb -->
161
- <ScavoldBreadcrumb :include-current="false" />
285
+ Base layout wrapper that intercepts pages with locale-conditional redirect front
286
+ matter and renders `<ScavoldLocaleRedirect>` in their place. All other pages render
287
+ the default slot.
162
288
 
163
- <!-- full trail including home link -->
164
- <ScavoldBreadcrumb include-root />
289
+ Consuming themes wrap their own layout root element in this component so they
290
+ automatically inherit redirect handling without duplicating the detection logic:
291
+
292
+ ```vue
293
+ <template>
294
+ <ScavoldLayout>
295
+ <div class="site-shell">
296
+ <!-- header, main, footer … -->
297
+ </div>
298
+ </ScavoldLayout>
299
+ </template>
300
+ ```
301
+
302
+ Themes that need finer control can skip `<ScavoldLayout>` and compose the two
303
+ building blocks themselves using `useRedirect` and `<ScavoldLocaleRedirect>`:
304
+
305
+ ```vue
306
+ <script setup>
307
+ import { useRedirect } from "./scavold/composables/useRedirect.js";
308
+ import ScavoldLocaleRedirect from "./scavold/components/ScavoldLocaleRedirect.vue";
309
+
310
+ const { isLocaleRedirect } = useRedirect();
311
+ </script>
312
+
313
+ <template>
314
+ <ScavoldLocaleRedirect v-if="isLocaleRedirect" />
315
+ <div v-else class="site-shell"><!-- … --></div>
316
+ </template>
165
317
  ```
166
318
 
167
319
  ---
@@ -263,48 +415,10 @@ pages/
263
415
 
264
416
  ---
265
417
 
266
- ### `<ScavoldLayout>`
267
-
268
- Base layout wrapper that intercepts pages with locale-conditional redirect front
269
- matter and renders `<ScavoldLocaleRedirect>` in their place. All other pages render
270
- the default slot.
418
+ ### `<ScavoldLocaleRedirect>`
271
419
 
272
- Consuming themes wrap their own layout root element in this component so they
273
- automatically inherit redirect handling without duplicating the detection logic:
274
-
275
- ```vue
276
- <template>
277
- <ScavoldLayout>
278
- <div class="site-shell">
279
- <!-- header, main, footer … -->
280
- </div>
281
- </ScavoldLayout>
282
- </template>
283
- ```
284
-
285
- Themes that need finer control can skip `<ScavoldLayout>` and compose the two
286
- building blocks themselves using `useRedirect` and `<ScavoldLocaleRedirect>`:
287
-
288
- ```vue
289
- <script setup>
290
- import { useRedirect } from "./scavold/composables/useRedirect.js";
291
- import ScavoldLocaleRedirect from "./scavold/components/ScavoldLocaleRedirect.vue";
292
-
293
- const { isLocaleRedirect } = useRedirect();
294
- </script>
295
-
296
- <template>
297
- <ScavoldLocaleRedirect v-if="isLocaleRedirect" />
298
- <div v-else class="site-shell"><!-- … --></div>
299
- </template>
300
- ```
301
-
302
- ---
303
-
304
- ### `<ScavoldLocaleRedirect>`
305
-
306
- Handles locale-conditional redirect pages declared with an object-form `redirect`
307
- in front matter:
420
+ Handles locale-conditional redirect pages declared with an object-form `redirect`
421
+ in front matter:
308
422
 
309
423
  ```yaml
310
424
  ---
@@ -328,138 +442,140 @@ The component renders no visible content of its own. It is used automatically by
328
442
 
329
443
  ---
330
444
 
331
- ### `<ScavoldMenuItems>`
445
+ ### `<ScavoldMenu>`
332
446
 
333
- Internal recursive component used by `<ScavoldMenu>` to render `<ul>/<li>` trees.
334
- Not intended for direct use. Documented here for theme developers who want to build
335
- their own menu component using `useHierarchy`.
447
+ Renders a `<nav>` element containing a nested list of page links derived from the
448
+ site's page hierarchy. Nothing is rendered when the resolved item set is empty.
336
449
 
337
- ---
450
+ The component has three independent addressing modes — path-based, absolute (from
451
+ root), and relative (from current page) — that cover all common navigation patterns
452
+ without requiring knowledge of the current page's depth.
338
453
 
339
- ### `<ScavoldVideo>`
454
+ With `menu`, it renders a **named menu** instead: the pages choosing that menu in
455
+ their [`menus`](./FRONTMATTER.md#menus) front matter, wherever they sit among the folders.
456
+ The names are declared in `.cratly.config.yaml`; where a menu goes is up to the theme.
340
457
 
341
- Renders a `<video>` element for a video embedded via the `:::video` container block.
342
- Nothing is rendered when no `src` argument is provided.
458
+ #### Props
343
459
 
344
- #### Markdown syntax
460
+ | Prop | Type | Default | Description |
461
+ |---|---|---|---|
462
+ | `menu` | `string` | — | Render the named menu declared under this name in `.cratly.config.yaml`: the pages choosing it in their `menus` front matter, in page-tree order. `from-path` and `from-root` then narrow the part of the site searched — `from-path="{lang}"` keeps a multilingual site's menu to one language — and `from` is ignored. `depth`, `active-only` and `expand` descend below each listed page. A page choosing the menu along with one of its ancestors is listed below that ancestor when `depth` allows sub-pages, and beside it when the menu is flat. |
463
+ | `from-path` | `string` | — | Select the parent node by path. Supports `{locale}` (full BCP 47 tag, e.g. `de-CH`) and `{lang}` (primary language subtag only, e.g. `de`) placeholders, replaced with the current page's locale at runtime. Takes priority over `from-root` and `from`. Examples: `"de/footer"`, `"{lang}/footer"`, `"{locale}/footer"`. |
464
+ | `from-root` | `number` | — | List items at this absolute depth from root. `1` = top-level pages, `2` = second level, etc. When set, `from` is ignored. If the current page has no ancestor at this depth, nothing is rendered. Ignored when `from-path` is set. |
465
+ | `from` | `number` | `0` | List items relative to the current page. `0` = siblings, `1` = children, `-1` = aunt/uncle level (children of grandparent), etc. Ignored when `from-root` or `from-path` is set. |
466
+ | `depth` | `number` | `0` | Additional levels to descend below the starting level. `0` = flat list, `1` = one level of children, `-1` = unlimited. |
467
+ | `active-only` | `boolean` | `false` | When `true`, only expands children along the branch leading to the current page. Useful with `depth > 0` to show sub-items under the active section only. |
468
+ | `expand` | `boolean` | `false` | When `true`, expands children of all nodes regardless of the active branch. Enables sitemap-style rendering. `active-only` takes precedence when both are set. |
469
+ | `label` | `string` | — | Accessible label for the `<nav>` landmark (`aria-label`). Should be set whenever more than one `<ScavoldMenu>` appears on the same page so screen readers can distinguish them (e.g. `"Main navigation"`, `"Section navigation"`). |
345
470
 
346
- ```markdown
347
- ::: video src=./clip.mp4 poster=./thumb.jpg
348
- Optional caption or fallback text for browsers that do not support the video element.
349
- :::
471
+ #### Rendered markup
472
+
473
+ ```html
474
+ <nav>
475
+ <ul>
476
+ <li class="active"> <!-- .active when node is current or an ancestor -->
477
+ <a href="/section/">Section</a>
478
+ <ul> <!-- nested only when children are included -->
479
+ <li class="current">
480
+ <a href="/section/page/">Page</a>
481
+ </li>
482
+ </ul>
483
+ </li>
484
+ </ul>
485
+ </nav>
350
486
  ```
351
487
 
352
- #### Arguments
488
+ The `.active` class is set on any item that is an ancestor of the current page or the
489
+ current page itself. The `.current` class is set only on the item that exactly matches
490
+ the current page.
353
491
 
354
- | Argument | Type | Default | Description |
355
- |---|---|---|---|
356
- | `src` | `string` | required | URL of the video file |
357
- | `poster` | `string` | — | URL of the poster image shown before playback |
358
- | `autoplay` | flag | — | Play automatically on load. Forces `muted` (browser requirement) |
359
- | `loop` | flag | — | Loop the video when it ends |
360
- | `muted` | flag | — | Mute the audio track |
361
- | `overlay` | flag | — | Render the video as a background and lay the block's body content on top of it (hero/background mode). See below. |
362
- | `controls` | flag | — | Show native playback controls. Always shown in the default player; opt-in in `overlay` mode, which is chrome-free by default. |
363
- | `preload` | `"none" \| "metadata" \| "auto"` | `"metadata"` | Browser preload hint |
364
- | `label` | `string` | — | Accessible label (`aria-label`) announced by screen readers. Use when the surrounding context does not already describe the video. |
492
+ A folder without an `index.md` has no page to link to. Where the menu shows the
493
+ pages below it, the folder is named as text — a `<span>` in place of the `<a>` — and
494
+ groups them; where it does not, the folder is left out, as it would lead nowhere.
365
495
 
366
- Boolean flags are written without a value:
496
+ #### Label resolution
367
497
 
368
- ```markdown
369
- ::: video src=./clip.mp4 autoplay loop
370
- :::
371
- ```
498
+ Item labels are resolved in this order:
372
499
 
373
- #### Rendered markup
500
+ 1. `label` frontmatter — explicit navigation text, overrides everything
501
+ 2. `title` frontmatter — page title, used when no `label` is set
502
+ 3. First `#` heading in the page body — extracted at build time as a fallback when neither `label` nor `title` is declared in frontmatter
503
+ 4. The last path segment of the page file (e.g. `about` from `de/about.md`) — last resort
374
504
 
375
- ```html
376
- <video src="..." poster="..." preload="metadata" controls playsinline>
377
- <!-- slot content -->
378
- </video>
505
+ #### Hiding pages via front matter
506
+
507
+ A page can opt out of appearing in menus by setting `hide` in its front matter:
508
+
509
+ | Value | Effect |
510
+ |---|---|
511
+ | `false` / absent | Visible everywhere (default) |
512
+ | `true` | Kept off every surface: menus, breadcrumbs, page lists and the feed |
513
+ | `"menu"` | Kept out of menus only |
514
+ | `"breadcrumb"` | Kept out of breadcrumbs only |
515
+ | `"list"` | Kept out of page lists only |
516
+ | `"feed"` | Left out of the RSS feed only |
517
+ | `[ "menu", "feed" ]` | Kept off exactly the surfaces named |
518
+
519
+ ```yaml
520
+ ---
521
+ hide: menu
522
+ ---
379
523
  ```
380
524
 
381
- The `controls` and `playsinline` attributes are always present. `autoplay`, `loop`,
382
- `muted`, and `poster` are only emitted when the corresponding argument is set.
525
+ Hiding from menus rules over any choice in `menus`.
383
526
 
384
- #### Background mode (`overlay`)
527
+ #### Choosing named menus via front matter
385
528
 
386
- With the `overlay` flag the video becomes a background and the block's body content
387
- is layered on top of it — the classic hero pattern. The video is chrome-free by
388
- default (add `controls` to bring the native controls back), and a typical hero
389
- combines `overlay` with `autoplay` + `loop` (both imply/allow `muted`):
529
+ Without `menu`, the component lists only pages that leave `menus` out or choose
530
+ `auto` among others. A page choosing named menus only appears in those:
390
531
 
391
- ```markdown
392
- ::: video src=./hero.mp4 poster=./hero.jpg overlay autoplay loop
393
- # Welcome
394
- Content rendered on top of the looping background video.
395
- :::
532
+ ```yaml
533
+ ---
534
+ menus: footer # in the footer, not in the page tree's menus
535
+ ---
396
536
  ```
397
537
 
398
- ```html
399
- <div class="scavold-video scavold-video--overlay">
400
- <video class="scavold-video__media" src="..." poster="..." autoplay loop muted
401
- preload="metadata" playsinline></video>
402
- <div class="scavold-video__overlay">
403
- <!-- slot content -->
404
- </div>
405
- </div>
406
- ```
538
+ #### Examples
407
539
 
408
- The component only ships **functional** layout CSS (the video and the content share
409
- one CSS-grid cell, so the content defines the block height and the video covers the
410
- area behind it via `object-fit: cover`). All visual styling — a darkening scrim,
411
- text colour, alignment, a `min-height` for the hero — belongs to the consuming theme,
412
- which can hook onto the `.scavold-video`, `.scavold-video__media`, and
413
- `.scavold-video__overlay` classes:
540
+ ```html
541
+ <!-- top-level pages — typical site header nav -->
542
+ <ScavoldMenu :from-root="1" />
414
543
 
415
- ```css
416
- /* theme CSS */
417
- .scavold-video--overlay { min-height: 60vh; }
418
- .scavold-video__overlay {
419
- display: grid;
420
- place-content: center;
421
- padding: 2rem;
422
- color: white;
423
- background: rgba( 0, 0, 0, 0.4 ); /* scrim for legibility */
424
- }
425
- ```
544
+ <!-- second level of the active top-level section only -->
545
+ <ScavoldMenu :from-root="2" active-only />
426
546
 
427
- Without the `overlay` flag the block behaves exactly as before: a plain inline
428
- player whose body content is only fallback text for browsers that cannot play video.
547
+ <!-- children of the current page -->
548
+ <ScavoldMenu :from="1" />
429
549
 
430
- #### Captions and accessibility
550
+ <!-- siblings of the current page -->
551
+ <ScavoldMenu />
431
552
 
432
- WCAG requires captions for prerecorded video with audio. Add a `<track>` element via
433
- the slot:
553
+ <!-- full site hierarchy as a sitemap -->
554
+ <ScavoldMenu :from-root="1" :depth="-1" />
434
555
 
435
- ```markdown
436
- ::: video src=./clip.mp4
437
- <track kind="captions" src="./clip.vtt" srclang="de" label="Deutsch" default />
438
- :::
439
- ```
556
+ <!-- two levels starting from the active top-level section, active branch expanded -->
557
+ <ScavoldMenu :from-root="1" :depth="1" active-only />
440
558
 
441
- The `label` argument provides an `aria-label` on the `<video>` element for cases where
442
- the surrounding page context does not already describe the video.
559
+ <!-- fixed path — same footer nav regardless of locale -->
560
+ <ScavoldMenu from-path="de/footer" label="Footer-Navigation" />
443
561
 
444
- #### Replacing with a custom component
562
+ <!-- locale-aware via primary language tag — works when paths use "de", "en", etc. -->
563
+ <ScavoldMenu from-path="{lang}/footer" label="Footer-Navigation" />
445
564
 
446
- Register a component under the name `ScavoldVideo` before calling `scavoldEnhanceApp`,
447
- or map the `video` container name to your component in `augmentConfig`:
565
+ <!-- locale-aware via full BCP 47 tag — use when paths include region codes like "de-CH" -->
566
+ <ScavoldMenu from-path="{locale}/footer" label="Footer-Navigation" />
448
567
 
449
- ```js
450
- export default defineConfig( await augmentConfig( config, {
451
- containers: { video: "MyVideoPlayer" },
452
- } ) );
568
+ <!-- named menu: the pages choosing "footer", wherever they sit, within the current language -->
569
+ <ScavoldMenu menu="footer" from-path="{lang}" label="Footer-Navigation" />
453
570
  ```
454
571
 
455
- Custom components can reuse the resolution logic via `useVideo` and `videoProps`:
572
+ ---
456
573
 
457
- ```js
458
- import { useVideo, videoProps } from "./scavold/composables/useVideo.js";
574
+ ### `<ScavoldMenuItems>`
459
575
 
460
- const props = defineProps( { ...videoProps } );
461
- const { src, poster, autoplay, loop, muted, preload, overlay, controls } = useVideo( props );
462
- ```
576
+ Internal recursive component used by `<ScavoldMenu>` to render `<ul>/<li>` trees.
577
+ Not intended for direct use. Documented here for theme developers who want to build
578
+ their own menu component using `useHierarchy`.
463
579
 
464
580
  ---
465
581
 
@@ -598,90 +714,277 @@ them discoverable.
598
714
 
599
715
  ---
600
716
 
601
- ### `<ScavoldImage>`
717
+ ### `<ScavoldVideo>`
602
718
 
603
- Renders a responsive `<picture>` element for local images. Not intended for direct
604
- use in templates — it is emitted automatically by the Markdown image renderer when a
605
- local image path is encountered. Custom components that need to display a media-file
606
- frontmatter field can use it directly.
719
+ Renders a video player in the text column for a video embedded via the `:::video`
720
+ container block — a clip visitors watch and listen to, pause and wind back. Nothing is
721
+ rendered when no `src` argument is provided. For a video across the whole window,
722
+ with content laid on top, use [`<ScavoldCover>`](#scavoldcover).
607
723
 
608
- #### Props
724
+ #### Markdown syntax
609
725
 
610
- | Prop | Type | Default | Description |
726
+ ```markdown
727
+ ::: video src=./clip.mp4 poster=./thumb.jpg
728
+ Optional caption or fallback text for browsers that do not support the video element.
729
+ :::
730
+ ```
731
+
732
+ #### Arguments
733
+
734
+ | Argument | Type | Default | Description |
611
735
  |---|---|---|---|
612
- | `src` | `string` | required | URL of the largest fallback image variant |
613
- | `srcset` | `string` | `""` | `srcset` string for the original format (JPEG/PNG) |
614
- | `webp-srcset` | `string` | `""` | `srcset` string for the WebP variants |
615
- | `sizes` | `string` | `"100vw"` | CSS `sizes` attribute applied to all sources |
616
- | `alt` | `string` | `""` | Alt text for the `<img>` element |
736
+ | `src` | `string` | required | URL of the video file |
737
+ | `poster` | `string` | — | URL of the poster image shown before playback |
738
+ | `autoplay` | flag | — | Play automatically while on screen, unless the visitor asks for reduced motion. Forces `muted` (browser requirement) |
739
+ | `loop` | flag | — | Loop the video when it ends |
740
+ | `muted` | flag | — | Mute the audio track |
741
+ | `preload` | `"none" \| "metadata" \| "auto"` | `"metadata"` | Browser preload hint |
742
+ | `label` | `string` | — | Accessible label (`aria-label`) announced by screen readers. Use when the surrounding context does not already describe the video. |
743
+
744
+ Boolean flags are written without a value:
745
+
746
+ ```markdown
747
+ ::: video src=./clip.mp4 autoplay loop
748
+ :::
749
+ ```
617
750
 
618
751
  #### Rendered markup
619
752
 
620
753
  ```html
621
- <picture>
622
- <source type="image/webp" srcset="..." sizes="..." />
623
- <source srcset="..." sizes="..." />
624
- <img src="..." alt="..." sizes="..." loading="lazy" decoding="async" />
625
- </picture>
754
+ <video class="scavold-video" src="..." poster="..." preload="metadata" controls playsinline>
755
+ <!-- slot content -->
756
+ </video>
757
+ ```
758
+
759
+ The `controls` and `playsinline` attributes are always present: every video can be
760
+ paused, which WCAG 2.2.2 asks of anything moving for longer than five seconds. `loop`,
761
+ `muted` and `poster` are only emitted when the corresponding argument is set.
762
+
763
+ `autoplay` is not emitted: the attribute starts a video before hydration, wherever it
764
+ is on the page, and before anyone could ask whether the visitor wants reduced motion.
765
+ The video is started by script instead. A visitor whose system asks for reduced motion
766
+ finds it paused, with the controls to start it. For everyone else it plays while at
767
+ least half of it is on screen — half of the window, for one higher than the window —
768
+ and pauses when scrolled away. A video the visitor paused stays paused until they
769
+ start it again, and one they turned the sound up on keeps playing when scrolled away.
770
+
771
+ #### Captions and accessibility
772
+
773
+ WCAG requires captions for prerecorded video with audio. Add a `<track>` element via
774
+ the slot:
775
+
776
+ ```markdown
777
+ ::: video src=./clip.mp4
778
+ <track kind="captions" src="./clip.vtt" srclang="de" label="Deutsch" default />
779
+ :::
780
+ ```
781
+
782
+ The `label` argument provides an `aria-label` on the `<video>` element for cases where
783
+ the surrounding page context does not already describe the video.
784
+
785
+ #### Replacing with a custom component
786
+
787
+ Register a component under the name `ScavoldVideo` before calling `scavoldEnhanceApp`,
788
+ or map the `video` container name to your component in `augmentConfig`:
789
+
790
+ ```js
791
+ export default defineConfig( await augmentConfig( config, {
792
+ containers: { video: "MyVideoPlayer" },
793
+ } ) );
794
+ ```
795
+
796
+ Custom components can reuse the resolution logic via `useVideo` and `videoProps`:
797
+
798
+ ```js
799
+ import { useVideo, videoProps } from "./scavold/composables/useVideo.js";
800
+
801
+ const props = defineProps( { ...videoProps } );
802
+ const { src, poster, autoplay, loop, muted, preload, ariaLabel } = useVideo( props );
803
+ ```
804
+
805
+ ---
806
+
807
+ ### Sectioning wrappers
808
+
809
+ `<ScavoldArticle>`, `<ScavoldAside>`, `<ScavoldFooter>`, `<ScavoldHeader>`, `<ScavoldMain>`,
810
+ `<ScavoldNav>`, `<ScavoldSection>`
811
+
812
+ Thin wrapper components for the seven HTML sectioning elements. Each renders its
813
+ corresponding element, forwarding `class` and `data-*` attributes from the container
814
+ arguments.
815
+
816
+ These are the default targets for their respective container names. A theme developer
817
+ can replace any of them by registering a component with the same name before calling
818
+ `scavoldEnhanceApp`, or by passing an explicit override to `augmentConfig`:
819
+
820
+ ```js
821
+ // .vitepress/config.js
822
+ export default defineConfig( await augmentConfig( config, {
823
+ containers: {
824
+ section: "MySiteSection",
825
+ }
826
+ } ) );
827
+ ```
828
+
829
+ ```js
830
+ // .vitepress/theme/index.js
831
+ async function enhanceApp( context ) {
832
+ app.component( "ScavoldSection", MySiteSection ); // registered before scavold
833
+ await scavoldEnhanceApp( context );
834
+ }
835
+ ```
836
+
837
+ ---
838
+
839
+ ## Composables
840
+
841
+ ---
842
+
843
+ ### `formatTimestamp( timestamp, options )`
844
+
845
+ ```js
846
+ import { formatTimestamp } from "scavold/lib/dateFormat.js";
847
+ ```
848
+
849
+ Renders a timestamp the way page lists do, for a theme's own components — a byline
850
+ under a post's title, for instance. Free of Vue and VitePress, so build-time code can
851
+ use it too.
852
+
853
+ | Option | Type | Default | Description |
854
+ |---|---|---|---|
855
+ | `locale` | `string` | the runtime's | Locale to phrase the result in |
856
+ | `dateStyle` | `"short" \| "medium" \| "long" \| "full" \| "iso"` | `"long"` | Style of the date; `iso` writes `2020-10-20` in every language |
857
+ | `timeStyle` | same values | — | Style of the time of day; omitted, only the date is rendered |
858
+ | `timeZone` | `string` | `"UTC"` | IANA name of the zone the timestamp is read in |
859
+
860
+ The result depends on the arguments alone, never on the machine it runs on, so
861
+ pre-rendered markup and the browser agree — a timestamp that changes on hydration
862
+ would tear the markup apart. A timestamp that cannot be read renders as an empty
863
+ string rather than as `Invalid Date`.
864
+
865
+ ---
866
+
867
+ ### `useAutoplay( videoRef, enabled )`
868
+
869
+ ```js
870
+ import { useAutoplay } from "scavold/composables/useAutoplay.js";
871
+ ```
872
+
873
+ The autoplay behaviour of `<ScavoldVideo>` and `<ScavoldCover>`, for a site's own
874
+ video component: plays the video muted while at least half of it is on screen, pauses
875
+ it when scrolled away, leaves it alone once the visitor paused it or turned the sound
876
+ up, and does nothing for visitors who ask for reduced motion.
877
+
878
+ ```js
879
+ const video = ref( null ); // template ref of the <video>
880
+ useAutoplay( video, () => autoplay.value );
881
+ ```
882
+
883
+ Leave the `autoplay` attribute off the element; it would start the video regardless.
884
+
885
+ ---
886
+
887
+ ### `useContainer( props )`
888
+
889
+ ```js
890
+ import { useContainer, containerProps } from "./scavold/composables/useContainer.js";
891
+ ```
892
+
893
+ Shared composable for custom container components. Parses the props emitted by the
894
+ Markdown container renderer and exposes derived values ready for template binding.
895
+
896
+ #### Usage
897
+
898
+ ```vue
899
+ <script setup>
900
+ import { useContainer, containerProps } from "./scavold/composables/useContainer.js";
901
+
902
+ const props = defineProps( {
903
+ ...containerProps,
904
+ background: String, // custom KV argument: background=/media/hero.jpg
905
+ } );
906
+
907
+ const { containerName, classes, dataAttrs } = useContainer( props );
908
+ </script>
909
+
910
+ <template>
911
+ <section
912
+ :class="[containerName, classes]"
913
+ :style="background ? `--bg: url(${background})` : ''"
914
+ v-bind="dataAttrs"
915
+ >
916
+ <slot />
917
+ </section>
918
+ </template>
626
919
  ```
627
920
 
628
- ---
921
+ #### `containerProps`
629
922
 
630
- ### `<ScavoldContainer>`
923
+ The shared props definition every container component must declare. Spread it into
924
+ `defineProps` to avoid duplicating the base prop declarations.
631
925
 
632
- Fallback component for Markdown container blocks whose name has no dedicated
633
- registered component. Renders as the matching HTML sectioning element if the
634
- container name is one (`section`, `aside`, etc.), otherwise as a `<div>`.
926
+ | Prop | Type | Description |
927
+ |---|---|---|
928
+ | `class` | `string` | Space-separated boolean flags from the container opening line |
929
+ | `data-container` | `string` | The container name as written in Markdown |
635
930
 
636
- Any container name works without being declared first — undeclared names are caught
637
- by this component. Because a typo would otherwise become a silent `<div>`, the build
638
- reports each undeclared name once:
931
+ All `key=value` pairs from the opening line are passed as additional props with a
932
+ `data-` prefix (e.g. `background=/img.jpg` → prop `data-background`). Declare them
933
+ explicitly in `defineProps` to use them.
639
934
 
640
- ```
641
- [scavold] container ':::sectoin' is not declared in .cratly.config.yaml — rendering
642
- it with ScavoldContainer. Declare it to silence this, or fix the name if it is a typo.
643
- ```
935
+ Flags arrive **both ways**: as part of `class` and as an empty `data-<flag>` prop, so
936
+ a boolean container parameter can be declared as a prop (this is how `useVideo` reads
937
+ `autoplay` and `loop`) while purely presentational flags keep working as
938
+ class names. A flag a component does not declare falls through into the markup as an
939
+ empty attribute — `::: section highlight` renders `<section class="highlight"
940
+ data-highlight>`.
644
941
 
645
- Declaring the container in `.cratly.config.yaml` silences the report and gives it
646
- typed properties in the cratly editor.
942
+ A container that opens the page — its fence is the first thing in the page body —
943
+ additionally receives an empty `data-leading` attribute. It is information only and
944
+ changes nothing by itself; a component or a stylesheet can tell from it that this
945
+ block stands at the very top. The same fact reaches the layout ahead of the content as
946
+ `page.leadingContainer` (see [A cover opening the page](#a-cover-opening-the-page)).
647
947
 
648
- Not intended for direct use. Theme developers should instead create a dedicated
649
- component for each container name they declare in `.cratly.config.yaml`.
948
+ #### Returns
650
949
 
651
- See `useContainer` below for building custom container components.
950
+ | Name | Type | Description |
951
+ |---|---|---|
952
+ | `containerName` | `ComputedRef<string>` | The container name (value of `data-container`) |
953
+ | `rootTag` | `ComputedRef<string>` | `containerName` if it is a sectioning element, otherwise `"div"` |
954
+ | `classes` | `ComputedRef<string>` | The `class` prop value |
955
+ | `dataAttrs` | `ComputedRef<object>` | All `data-*` props as a plain object, suitable for `v-bind` |
652
956
 
653
- ---
957
+ #### Markdown syntax reminder
654
958
 
655
- ### `<ScavoldSection>`, `<ScavoldAside>`, `<ScavoldArticle>`, `<ScavoldHeader>`, `<ScavoldFooter>`, `<ScavoldNav>`, `<ScavoldMain>`
959
+ ```markdown
960
+ ::: hero dark centered background=/media/hero.jpg
961
+ Content here.
962
+ :::
963
+ ```
656
964
 
657
- Thin wrapper components for the seven HTML sectioning elements. Each renders its
658
- corresponding element, forwarding `class` and `data-*` attributes from the container
659
- arguments.
965
+ This emits `class="dark centered"`, `data-container="hero"`, and
966
+ `data-background="/media/hero.jpg"` onto the component.
660
967
 
661
- These are the default targets for their respective container names. A theme developer
662
- can replace any of them by registering a component with the same name before calling
663
- `scavoldEnhanceApp`, or by passing an explicit override to `augmentConfig`:
968
+ ---
969
+
970
+ ### `useCover( props )`
664
971
 
665
972
  ```js
666
- // .vitepress/config.js
667
- export default defineConfig( await augmentConfig( config, {
668
- containers: {
669
- section: "MySiteSection",
670
- }
671
- } ) );
973
+ import { useCover, coverProps } from "scavold/composables/useCover.js";
672
974
  ```
673
975
 
976
+ Resolves the arguments of a `:::cover` block for a site's own cover component, so it
977
+ renders the same picture the way its design asks:
978
+
674
979
  ```js
675
- // .vitepress/theme/index.js
676
- async function enhanceApp( context ) {
677
- app.component( "ScavoldSection", MySiteSection ); // registered before scavold
678
- await scavoldEnhanceApp( context );
679
- }
980
+ const props = defineProps( { ...coverProps } );
981
+ const { src, isVideo, srcset, webpSrcset, sizes, poster, focus, label, tone } = useCover( props );
680
982
  ```
681
983
 
682
- ---
683
-
684
- ## Composables
984
+ `srcset`, `webpSrcset` and `sizes` are filled for an image only; the build provides
985
+ the variants and the dimensions `sizes` is derived from. `tone` is `"light"`, `"dark"`
986
+ or empty, one of `COVER_TONES`. `isVideoUrl( url )` and `coverSizes( width, height )`
987
+ are exported as well.
685
988
 
686
989
  ---
687
990
 
@@ -767,46 +1070,6 @@ const items = computed( () =>
767
1070
 
768
1071
  ---
769
1072
 
770
- ### `useVideo( props )`
771
-
772
- ```js
773
- import { useVideo, videoProps } from "./scavold/composables/useVideo.js";
774
- ```
775
-
776
- Composable for custom video components. Resolves the `:::video` container props into
777
- clean, typed values ready for template binding. `autoplay` automatically forces `muted`
778
- to satisfy browser autoplay policies.
779
-
780
- #### `videoProps`
781
-
782
- Spread into `defineProps` to declare all video-related props at once.
783
-
784
- | Prop | Argument | Description |
785
- |---|---|---|
786
- | `dataSrc` | `src=…` | Video file URL |
787
- | `dataPoster` | `poster=…` | Poster image URL |
788
- | `dataAutoplay` | `autoplay` | Present when the `autoplay` flag is set |
789
- | `dataLoop` | `loop` | Present when the `loop` flag is set |
790
- | `dataMuted` | `muted` | Present when the `muted` flag is set |
791
- | `dataOverlay` | `overlay` | Present when the `overlay` flag is set (background mode) |
792
- | `dataControls` | `controls` | Present when the `controls` flag is set |
793
- | `dataPreload` | `preload=…` | One of `"none"`, `"metadata"`, `"auto"` |
794
-
795
- #### Returns
796
-
797
- | Name | Type | Description |
798
- |---|---|---|
799
- | `src` | `ComputedRef<string>` | Video URL |
800
- | `poster` | `ComputedRef<string>` | Poster URL, empty string if absent |
801
- | `autoplay` | `ComputedRef<boolean>` | `true` when `autoplay` flag is set |
802
- | `loop` | `ComputedRef<boolean>` | `true` when `loop` flag is set |
803
- | `muted` | `ComputedRef<boolean>` | `true` when `muted` flag is set or `autoplay` is true |
804
- | `overlay` | `ComputedRef<boolean>` | `true` when the `overlay` flag is set (render as background) |
805
- | `controls` | `ComputedRef<boolean>` | `true` in the default player; in overlay mode only when the `controls` flag is set |
806
- | `preload` | `ComputedRef<string>` | Resolved preload value, defaults to `"metadata"` |
807
-
808
- ---
809
-
810
1073
  ### `usePageList( props )`
811
1074
 
812
1075
  ```js
@@ -878,107 +1141,6 @@ argument arrives as `data…`, in the same way as `videoProps`: `dataFrom` for `
878
1141
 
879
1142
  ---
880
1143
 
881
- ### `formatTimestamp( timestamp, options )`
882
-
883
- ```js
884
- import { formatTimestamp } from "scavold/lib/dateFormat.js";
885
- ```
886
-
887
- Renders a timestamp the way page lists do, for a theme's own components — a byline
888
- under a post's title, for instance. Free of Vue and VitePress, so build-time code can
889
- use it too.
890
-
891
- | Option | Type | Default | Description |
892
- |---|---|---|---|
893
- | `locale` | `string` | the runtime's | Locale to phrase the result in |
894
- | `dateStyle` | `"short" \| "medium" \| "long" \| "full" \| "iso"` | `"long"` | Style of the date; `iso` writes `2020-10-20` in every language |
895
- | `timeStyle` | same values | — | Style of the time of day; omitted, only the date is rendered |
896
- | `timeZone` | `string` | `"UTC"` | IANA name of the zone the timestamp is read in |
897
-
898
- The result depends on the arguments alone, never on the machine it runs on, so
899
- pre-rendered markup and the browser agree — a timestamp that changes on hydration
900
- would tear the markup apart. A timestamp that cannot be read renders as an empty
901
- string rather than as `Invalid Date`.
902
-
903
- ---
904
-
905
- ### `useContainer( props )`
906
-
907
- ```js
908
- import { useContainer, containerProps } from "./scavold/composables/useContainer.js";
909
- ```
910
-
911
- Shared composable for custom container components. Parses the props emitted by the
912
- Markdown container renderer and exposes derived values ready for template binding.
913
-
914
- #### Usage
915
-
916
- ```vue
917
- <script setup>
918
- import { useContainer, containerProps } from "./scavold/composables/useContainer.js";
919
-
920
- const props = defineProps( {
921
- ...containerProps,
922
- background: String, // custom KV argument: background=/media/hero.jpg
923
- } );
924
-
925
- const { containerName, classes, dataAttrs } = useContainer( props );
926
- </script>
927
-
928
- <template>
929
- <section
930
- :class="[containerName, classes]"
931
- :style="background ? `--bg: url(${background})` : ''"
932
- v-bind="dataAttrs"
933
- >
934
- <slot />
935
- </section>
936
- </template>
937
- ```
938
-
939
- #### `containerProps`
940
-
941
- The shared props definition every container component must declare. Spread it into
942
- `defineProps` to avoid duplicating the base prop declarations.
943
-
944
- | Prop | Type | Description |
945
- |---|---|---|
946
- | `class` | `string` | Space-separated boolean flags from the container opening line |
947
- | `data-container` | `string` | The container name as written in Markdown |
948
-
949
- All `key=value` pairs from the opening line are passed as additional props with a
950
- `data-` prefix (e.g. `background=/img.jpg` → prop `data-background`). Declare them
951
- explicitly in `defineProps` to use them.
952
-
953
- Flags arrive **both ways**: as part of `class` and as an empty `data-<flag>` prop, so
954
- a boolean container parameter can be declared as a prop (this is how `useVideo` reads
955
- `overlay`, `autoplay` and `loop`) while purely presentational flags keep working as
956
- class names. A flag a component does not declare falls through into the markup as an
957
- empty attribute — `::: section highlight` renders `<section class="highlight"
958
- data-highlight>`.
959
-
960
- #### Returns
961
-
962
- | Name | Type | Description |
963
- |---|---|---|
964
- | `containerName` | `ComputedRef<string>` | The container name (value of `data-container`) |
965
- | `rootTag` | `ComputedRef<string>` | `containerName` if it is a sectioning element, otherwise `"div"` |
966
- | `classes` | `ComputedRef<string>` | The `class` prop value |
967
- | `dataAttrs` | `ComputedRef<object>` | All `data-*` props as a plain object, suitable for `v-bind` |
968
-
969
- #### Markdown syntax reminder
970
-
971
- ```markdown
972
- ::: hero dark centered background=/media/hero.jpg
973
- Content here.
974
- :::
975
- ```
976
-
977
- This emits `class="dark centered"`, `data-container="hero"`, and
978
- `data-background="/media/hero.jpg"` onto the component.
979
-
980
- ---
981
-
982
1144
  ### `useRedirect()`
983
1145
 
984
1146
  ```js
@@ -1040,6 +1202,8 @@ const label = t("nav.breadcrumb"); // ComputedRef<string>, reactive to locale ch
1040
1202
  | Key | Default (en) | Description |
1041
1203
  |---|---|---|
1042
1204
  | `@scavold.nav.breadcrumb` | `"Breadcrumb"` | `aria-label` on the breadcrumb `<nav>` |
1205
+ | `@scavold.cover.play` | `"Play video"` | Label of a cover video's button while the video is paused |
1206
+ | `@scavold.cover.pause` | `"Pause video"` | Label of a cover video's button while the video plays |
1043
1207
 
1044
1208
  #### Providing translations in your theme
1045
1209
 
@@ -1079,6 +1243,44 @@ the Scavold default.
1079
1243
 
1080
1244
  ---
1081
1245
 
1246
+ ### `useVideo( props )`
1247
+
1248
+ ```js
1249
+ import { useVideo, videoProps } from "./scavold/composables/useVideo.js";
1250
+ ```
1251
+
1252
+ Composable for custom video components. Resolves the `:::video` container props into
1253
+ clean, typed values ready for template binding. `autoplay` automatically forces `muted`
1254
+ to satisfy browser autoplay policies.
1255
+
1256
+ #### `videoProps`
1257
+
1258
+ Spread into `defineProps` to declare all video-related props at once.
1259
+
1260
+ | Prop | Argument | Description |
1261
+ |---|---|---|
1262
+ | `dataSrc` | `src=…` | Video file URL |
1263
+ | `dataPoster` | `poster=…` | Poster image URL |
1264
+ | `dataAutoplay` | `autoplay` | Present when the `autoplay` flag is set |
1265
+ | `dataLoop` | `loop` | Present when the `loop` flag is set |
1266
+ | `dataMuted` | `muted` | Present when the `muted` flag is set |
1267
+ | `dataPreload` | `preload=…` | One of `"none"`, `"metadata"`, `"auto"` |
1268
+ | `dataLabel` | `label=…` | Accessible label |
1269
+
1270
+ #### Returns
1271
+
1272
+ | Name | Type | Description |
1273
+ |---|---|---|
1274
+ | `src` | `ComputedRef<string>` | Video URL |
1275
+ | `poster` | `ComputedRef<string>` | Poster URL, empty string if absent |
1276
+ | `autoplay` | `ComputedRef<boolean>` | `true` when `autoplay` flag is set |
1277
+ | `loop` | `ComputedRef<boolean>` | `true` when `loop` flag is set |
1278
+ | `muted` | `ComputedRef<boolean>` | `true` when `muted` flag is set or `autoplay` is true |
1279
+ | `preload` | `ComputedRef<string>` | Resolved preload value, defaults to `"metadata"` |
1280
+ | `ariaLabel` | `ComputedRef<string \| undefined>` | Accessible label, `undefined` when the author set none |
1281
+
1282
+ ---
1283
+
1082
1284
  ## Design patterns
1083
1285
 
1084
1286
  ---
@@ -1107,6 +1309,12 @@ their context and pass it through to `<ScavoldImage>`:
1107
1309
 
1108
1310
  Content authors pick the component (`HeroImage`, `ThumbImage`, etc.) that matches
1109
1311
  their intent. The `sizes` value is baked into the component and invisible to them.
1312
+ The built-in [`<ScavoldCover>`](#scavoldcover) works this way.
1313
+
1314
+ A container argument declared as `media-file` that names an image reaches the
1315
+ component with everything it needs for this: besides its URL, as `data-<arg>`, the
1316
+ build adds `data-<arg>-srcset`, `data-<arg>-webp-srcset`, `data-<arg>-width` and
1317
+ `data-<arg>-height` — the variants and the upright dimensions of the source.
1110
1318
 
1111
1319
  **Why not measure at runtime?** A `ResizeObserver`-based approach that reads the
1112
1320
  container's pixel size and sets `src` dynamically is accurate but has serious
@@ -1119,4 +1327,3 @@ The global `image_sizes` in `.cratly.config.yaml` is a fallback for images that
1119
1327
  no layout-aware component wrapping them. Set it to the most common case in the
1120
1328
  theme (typically the prose column width) and override via dedicated components for
1121
1329
  everything else.
1122
-