@humanforest/slidev-theme 0.1.1 → 0.1.3

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.
@@ -54,6 +54,21 @@ const props = defineProps({
54
54
  const src = computed(() => (props.tone === 'reverse' ? reverseUrl : positiveUrl));
55
55
  </script>
56
56
 
57
+ <!--
58
+ `alt` names the certification rather than the picture — a reader who cannot see it needs to know
59
+ the deck carries the certification, which is what the mark is there to say.
60
+
61
+ ★★ AND THE COMMENT LIVES OUT HERE, NOT INSIDE `<template>`. A comment beside the `<img>` is a
62
+ SECOND ROOT NODE: Vue compiles the template to `createElementBlock(Fragment, [img, comment])`,
63
+ and a fragment root has nowhere to put inherited attributes, so it drops them silently. That is
64
+ not a dev-only detail — Slidev builds with comments ON, so the shipped bundle carries the
65
+ fragment too. `cover.vue` passes `class="cover-endorsement"` to place the mark in the slide's
66
+ bottom-left corner; the class never arrived, so the mark stayed `position: static` and stretched
67
+ to 1728 x 136 — a band across the foot of every mosaic cover, on a 1920 canvas. With the class it
68
+ draws 82 x 136 at left 120px, bottom 96px, which is `--deck-inset-display` and `--deck-inset-text`
69
+ exactly. Both numbers measured in the built dist, not inferred. The same drop disabled
70
+ `.cover-endorsement-stacked` on the poster variant.
71
+ -->
57
72
  <template>
58
73
  <img
59
74
  class="deck-endorsement"
@@ -61,10 +76,6 @@ const src = computed(() => (props.tone === 'reverse' ? reverseUrl : positiveUrl)
61
76
  :style="{ height: `${size}px` }"
62
77
  alt="Certified B Corporation"
63
78
  >
64
- <!--
65
- `alt` names the certification rather than the picture — a reader who cannot see it needs to know
66
- the deck carries the certification, which is what the mark is there to say.
67
- -->
68
79
  </template>
69
80
 
70
81
  <style scoped>
@@ -0,0 +1,70 @@
1
+ // task-14-brief.md, requirement 6 — the shared toggle behind the layout-debug (red-line) overlay.
2
+ // Split out from global-top.vue itself so setup/shortcuts.ts can flip it without importing a .vue
3
+ // file into a plain Slidev setup module — the same reasoning backgrounds.js/iconSlugs.js are split
4
+ // from the components that read them.
5
+ //
6
+ // OFF BY DEFAULT, ALWAYS — this is the one property every other guarantee in this module and in
7
+ // global-top.vue is built on top of:
8
+ // - `ref(false)`, never read from anywhere at import time — no localStorage/sessionStorage
9
+ // restore, no query-string read, nothing that could start this ref at `true` on a page this
10
+ // module has never seen interacted with. A fresh page load — which is EXACTLY what `slidev
11
+ // export`'s Playwright run does; it never presses a key or replays a prior session — starts
12
+ // here, at `false`, unconditionally.
13
+ // - No persistence at all. Nothing in this module writes to storage, so there is nothing for a
14
+ // later, unrelated page load (an export tab reusing a browser profile a presenter had toggled
15
+ // debug on in, during a live review) to inherit. The cost is that the toggle does not survive
16
+ // a manual reload — accepted deliberately, because the alternative is a mechanism that could
17
+ // leak state into an export, which is the one outcome requirement 6 names as worse than not
18
+ // having this tool at all.
19
+ //
20
+ // This module does NOT decide whether the overlay is currently SAFE to show — it has no way to
21
+ // know it's running inside an export. global-top.vue is what gates the actual render on
22
+ // `useNav().isPrintMode` as well as this ref, so even a hypothetical future bug that flipped
23
+ // `layoutDebugEnabled` true during an export still could not paint anything — see that file's own
24
+ // comment for the two-guard shape and why neither guard alone is treated as sufficient.
25
+ import { reactive, ref } from 'vue';
26
+
27
+ /** @type {import('vue').Ref<boolean>} */
28
+ export const layoutDebugEnabled = ref(false);
29
+
30
+ /** Flips the overlay. The one write site this ref has — used by setup/shortcuts.ts's 'l' binding.
31
+ * Never called from a layout, a component, or a slide. */
32
+ export function toggleLayoutDebug() {
33
+ layoutDebugEnabled.value = !layoutDebugEnabled.value;
34
+ }
35
+
36
+ /*
37
+ THE FIVE TIERS, individually switchable — one on/off for the whole overlay was not enough once it
38
+ started drawing element boxes and gaps as well as rules. A dense slide carries 40-120 element
39
+ hairlines, which is exactly the texture you want when checking whether a paragraph sits where you
40
+ think and exactly the noise you do not want when checking a split's columns.
41
+
42
+ SAME NO-PERSISTENCE DISCIPLINE as `layoutDebugEnabled`, and for the same reason: a plain object
43
+ with literal defaults, never read from storage or a query string, so an export starts here every
44
+ time. All five start ON: boxing the content is half of what makes this a schematic rather than a
45
+ set of rules, so it is there the first time you open it. `element` is the one to switch off when a
46
+ dense slide gets busy, which is exactly why the switches exist.
47
+
48
+ Not a second nav button and not five more shortcuts: the key already names every tier, so the
49
+ key's own rows are the switches. See global-top.vue.
50
+ */
51
+ export const layoutDebugTiers = reactive({
52
+ /*
53
+ ★ THE GRID WASH IS A TIER LIKE THE REST, and it was the one thing the overlay drew that nothing
54
+ could switch off. It is also the densest thing on screen — a 40px lattice under every other
55
+ line — so the moment a reader wants to check one measurement it is the first thing in the way.
56
+ Jose, doing exactly that: "can you remove the grid for a moment? too many lines i want to see if
57
+ everything is correct".
58
+ */
59
+ grid: true,
60
+ frame: true,
61
+ region: true,
62
+ gap: true,
63
+ element: true,
64
+ hazard: true,
65
+ });
66
+
67
+ /** Flips one tier. Called only from the overlay's own key. */
68
+ export function toggleTier(id) {
69
+ if (id in layoutDebugTiers) layoutDebugTiers[id] = !layoutDebugTiers[id];
70
+ }
@@ -0,0 +1,34 @@
1
+ // Preview-only overrides for the running chrome, driven from the nav bar.
2
+ //
3
+ // WHAT THIS IS FOR. Reviewing a deck means asking "what does this look like without the
4
+ // confidentiality chip / with the inset frames drawn", and the honest answers were previously "edit
5
+ // the frontmatter and reload" and "remember that the key is `l`". Slidev renders a theme's own
6
+ // `custom-nav-controls.vue` inside its toolbar (@slidev/client/internals/NavControls.vue imports
7
+ // `#slidev/custom-nav-controls`), which is the right home for both: a reviewer's control, sitting
8
+ // with the other reviewer's controls, rather than a frontmatter edit that has to be undone.
9
+ //
10
+ // ★★ AN OVERRIDE, NEVER A SETTING — and specifically never one that can reach an export.
11
+ //
12
+ // These refs only ever SUBTRACT from what the frontmatter already asked for. A slide with no
13
+ // `confidential:` cannot be made to show one here, because that would let a reviewer see a deck the
14
+ // deck does not describe. And every consumer must gate on `isPrintMode` the way global-top.vue
15
+ // already does for the debug overlay: `slidev export` drives a real Playwright browser through the
16
+ // same client, so a toggle left on at export time would otherwise silently ship a PDF missing its
17
+ // own confidentiality marking. That is a worse failure than the inconvenience it saves.
18
+ //
19
+ // The debug overlay's own flag stays in ./layoutDebug.js — it already had a home, a keyboard
20
+ // shortcut and a test, and moving it here to make one tidy module would be churn. This file
21
+ // re-exports it so the nav control has a single import.
22
+ import { ref } from 'vue';
23
+
24
+ export { layoutDebugEnabled, toggleLayoutDebug } from './layoutDebug.js';
25
+
26
+ /**
27
+ * Hide the confidentiality chip in PREVIEW only. False by default, so a fresh load always shows
28
+ * exactly what the frontmatter says.
29
+ */
30
+ export const confidentialHidden = ref(false);
31
+
32
+ export function toggleConfidential() {
33
+ confidentialHidden.value = !confidentialHidden.value;
34
+ }
package/global-top.vue CHANGED
@@ -58,9 +58,53 @@ import { useNav } from '@slidev/client';
58
58
  import { layoutDebugEnabled, layoutDebugTiers, toggleTier } from './debug/layoutDebug.js';
59
59
  import deckTokens from '@humanforest/tokens/deck/deck.json';
60
60
 
61
- const { isPrintMode, slides, currentPage } = useNav();
61
+ const { isPrintMode, slides, currentPage, prevRoute, nextRoute } = useNav();
62
62
  const show = computed(() => layoutDebugEnabled.value && !isPrintMode.value);
63
63
 
64
+ /*
65
+ ── NEIGHBOUR CHUNK WARM-UP ────────────────────────────────────────────────────────────────────
66
+ ★★ NOT PART OF THE OVERLAY. It lives here because Slidev gives a theme exactly one seam that is
67
+ mounted once for the whole deck rather than re-mounted per slide, and this is it — the same
68
+ reason the overlay itself is here. It needs `useNav()`, which is a composable and so cannot run
69
+ from `setup/main.ts` where this would otherwise belong.
70
+
71
+ WHAT IT FIXES. The deck's headmatter sets `preload: false` (scripts/deckSpecimen.ts's
72
+ HEAD_DEFAULTS) because Slidev otherwise mounts every route three seconds after load — measured
73
+ at 245 slides: 33,435 nodes and 1.7s of blocking time. But `preloadRoute()` in
74
+ @slidev/client's internals/SlidesShow.vue gates TWO separate things on that one key:
75
+
76
+ watchEffect(() => { preloadRoute(current); preloadRoute(prev); preloadRoute(next) }) // ← A
77
+ watchEffect(() => setTimeout(() => routes.forEach(preloadRoute), 3000)) // ← B
78
+
79
+ B is the stampede. A is not, and turning off B took A with it. So nothing was fetched ahead any
80
+ more, and a slide's own chunk was pulled on arrival instead.
81
+
82
+ ★★ THAT SHOWED AS A WHITE FLASH ON EVERY FORWARD STEP OF THE DEPLOYED DECK, and it did not
83
+ reproduce locally, which is why the comment in HEAD_DEFAULTS priced the trade at "about 30ms" —
84
+ a warm dev server. MEASURED on the gh-pages build instead, stepping onto a cold slide: the
85
+ wrapper lands in the DOM with NO `.slidev-layout` inside it for 278ms while `md-*.js`,
86
+ `LondonMap-*.js` and `LondonMap-*.css` are in flight. An empty slide paints Slidev's own
87
+ `bg-main` on `#slide-content` (internals/SlideContainer.vue), which is white. `SlideLoading`
88
+ never appears either — the async component's `delay` is 300ms and the gap is shorter.
89
+
90
+ ⚠ NO COMPOSITING FIX CAN REACH THIS, which is worth stating because styles/base.css already
91
+ carries one for a DIFFERENT white leak on the same seam. `useViewTransition` starts the
92
+ transition 50ms after `beforeResolve` and ends it on `router.afterEach`, and neither waits for
93
+ the slide's async component. The API is asked to cross-fade into a frame that is genuinely
94
+ white, and does so correctly.
95
+
96
+ WHY `load()` AND NOT `preloadRoute()`. The DOM cost that made B unaffordable comes from
97
+ `meta.__preloaded`, which is what puts a route into SlidesShow's `loadedRoutes` and mounts it.
98
+ `route.load()` only runs the slide's dynamic import, filling the `componentsCache` entry that
99
+ `#slidev/slides` keys its `defineAsyncComponent` loader off. So this buys back A's warm chunk
100
+ without buying back any of B's mounted DOM: still one page in the document, and a re-measured
101
+ navigation onto an already-loaded slide reports a single mutation with the layout ALREADY
102
+ present — zero frames of empty slide, zero requests.
103
+
104
+ ±1 only, matching what A itself did. A jump from the goto dialog still lands cold.
105
+ */
106
+ watch([prevRoute, nextRoute], routes => routes.forEach(route => route?.load()), { immediate: true });
107
+
64
108
  /*
65
109
  task-14b-brief.md — the overlay now also has to show split.vue's Y-ANCHOR and its CONDITIONAL
66
110
  gutter, and unlike the three edge insets, both depend on the CURRENT SLIDE's own frontmatter
package/layouts/cover.vue CHANGED
@@ -75,13 +75,12 @@ const props = defineProps({
75
75
  validator: (b) => BEAT_GROUNDS.includes(b),
76
76
  },
77
77
  /**
78
- * WHAT SHAPE the opener is, as opposed to what colour. Six compositions, and every one of them
78
+ * WHAT SHAPE the opener is, as opposed to what colour. Four compositions, and every one of them
79
79
  * takes every `background` and both registers — the two props are orthogonal by construction,
80
80
  * which is the whole reason the ground is not baked into the variant names.
81
81
  *
82
82
  * · `plain` — type alone on the ground. The opener with nothing to look at but the words.
83
83
  * · `inset` — the title in a panel floating on the ground, air all round it.
84
- * · `mark` — the Forest mark oversized as the composition, the title over it.
85
84
  * · `poster` — a photograph edge to edge, the identity stacked over a scrim at the right.
86
85
  * · `mosaic` — the title on a field, beside a staggered grid of pictures bleeding off the edge.
87
86
  *
@@ -88,7 +88,7 @@ const props = defineProps({
88
88
  rank: { type: String, default: 'section', validator: (v) => ['section', 'subsection', 'topic'].includes(v) },
89
89
  /**
90
90
  * The part's own mark, drawn above the rule — `<namespace>:<name>`, the same reference IconTile
91
- * takes (`forest:bike`, `lucide:layers`). SECTION RANK ONLY: it is the identity of a part, and a
91
+ * takes (`forest:bike`, `lucide:map`). SECTION RANK ONLY: it is the identity of a part, and a
92
92
  * subsection is not a new part. Ignored at the other ranks rather than refused, so changing a
93
93
  * slide's rank never breaks it.
94
94
  */
@@ -163,7 +163,7 @@
163
163
  "hasDynamicDefault": false,
164
164
  "defaultSource": "'plain'",
165
165
  "required": false,
166
- "description": "WHAT SHAPE the opener is, as opposed to what colour. Six compositions, and every one of them takes every `background` and both registers — the two props are orthogonal by construction, which is the whole reason the ground is not baked into the variant names. · `plain` — type alone on the ground. The opener with nothing to look at but the words. · `inset` — the title in a panel floating on the ground, air all round it. · `mark` — the Forest mark oversized as the composition, the title over it. · `poster` — a photograph edge to edge, the identity stacked over a scrim at the right. · `mosaic` — the title on a field, beside a staggered grid of pictures bleeding off the edge."
166
+ "description": "WHAT SHAPE the opener is, as opposed to what colour. Four compositions, and every one of them takes every `background` and both registers — the two props are orthogonal by construction, which is the whole reason the ground is not baked into the variant names. · `plain` — type alone on the ground. The opener with nothing to look at but the words. · `inset` — the title in a panel floating on the ground, air all round it. · `poster` — a photograph edge to edge, the identity stacked over a scrim at the right. · `mosaic` — the title on a field, beside a staggered grid of pictures bleeding off the edge."
167
167
  },
168
168
  {
169
169
  "name": "endorsements",
@@ -296,7 +296,7 @@
296
296
  "hasDynamicDefault": false,
297
297
  "defaultSource": "''",
298
298
  "required": false,
299
- "description": "The part's own mark, drawn above the rule — `<namespace>:<name>`, the same reference IconTile takes (`forest:bike`, `lucide:layers`). SECTION RANK ONLY: it is the identity of a part, and a subsection is not a new part. Ignored at the other ranks rather than refused, so changing a slide's rank never breaks it."
299
+ "description": "The part's own mark, drawn above the rule — `<namespace>:<name>`, the same reference IconTile takes (`forest:bike`, `lucide:map`). SECTION RANK ONLY: it is the identity of a part, and a subsection is not a new part. Ignored at the other ranks rather than refused, so changing a slide's rank never breaks it."
300
300
  }
301
301
  ],
302
302
  "slots": [
@@ -6237,7 +6237,7 @@
6237
6237
  "role": "Several DIFFERENT kinds of reading at once — figures beside a figure with a shape, not one form repeated across a uniform row.",
6238
6238
  "layout": "default",
6239
6239
  "props": {},
6240
- "fill": "A `::grid` (`:cols: 2` or 3) mixing two or three `::stat-card` tiles with one figure tile — a status-board read, not a single chart type repeated. `::bars` labels every row and prints its own value, and `::dotted-map` draws no key at all, so neither needs anything around it; a tile whose figure carries several series does need a key, and every Unovis mark gets one from `::chart-frame`, which is where the `height`, `legend` and `legend-position` each mark forwards land (the empty state reaches it as the mark's own `empty`). Two of the frame's defaults are wrong inside a cell: the 480px plot is deeper than a grid cell is wide, and a bottom legend eats what is left of it — set `:height:` down and `legend-position: 'top'`, or `:legend: false` and label the marks. Reach for `::chart-frame` directly only when the tile's figure is not one of the shipped marks: it is the one thing in the deck that puts a room-scale series key, and the honest empty well, under a figure built by hand.",
6240
+ "fill": "A `::grid` (`:cols: 2` or 3) mixing two or three `::stat-card` tiles with one figure tile — a status-board read, not a single chart type repeated. `::bars` labels every row and prints its own value, and `::dotted-map` draws no key at all, so neither needs anything around it; a tile whose figure carries several series does need a key, and every Unovis mark gets one from `::chart-frame`, which is where the `height`, `legend` and `legend-position` each mark forwards land (the empty state reaches it as the mark's own `empty`). Two of the frame's defaults are wrong inside a cell: the 480px plot is deeper than a grid cell is wide, and a bottom legend eats what is left of it — set `:height:` down and `legend-position: 'top-left'`, or `:legend: false` and label the marks. Reach for `::chart-frame` directly only when the tile's figure is not one of the shipped marks: it is the one thing in the deck that puts a room-scale series key, and the honest empty well, under a figure built by hand.",
6241
6241
  "use": "An operating-review slide that needs to show the state of several different things on one screen, the way a real dashboard does.",
6242
6242
  "avoid": "Several figures of the SAME kind that should read as one comparison — `metrics` keeps every tile the same shape on one baseline; mixing figure types into that row is exactly what `dashboard` is for, and exactly what breaks a `metrics` row's own promise."
6243
6243
  },
@@ -6382,7 +6382,7 @@
6382
6382
  "props": {
6383
6383
  "ratio": "2:3"
6384
6384
  },
6385
- "fill": "Heading and a single-sentence reading of the movement in the default slot content; the chart in the `aside` slot — `::line-chart` when the series are independent trajectories to be compared, `::area-chart` when they sum to one whole (its default stack makes exactly that claim; pass `:overlaid: true` when they do not). Every `label` in `data` is a point on a real sequence, in order. In the props block every NON-STRING value is colon-prefixed — `:data:`, `:series:`, `:overlaid:`, `:height:`, `:max-ticks:` — while string props go bare (`y-label: 'Trips'`, `legend-position: 'top'`); drop the colon and the value arrives as a string, so an array plots nothing and a `false` reads as truthy. Set `yLabel` unless the heading already carries the unit, and leave `height` at its 480 default: with the heading in the content column rather than a `::header::`, the `frame: content` figure column starts at the 360px vertical anchor and split.vue caps it at 630px, which 480 plus a legend row just fits. (A `::header::` would size row 1 to the 130 band instead and free the 592 a full-width chart gets — a different recipe, not this one.)",
6385
+ "fill": "Heading and a single-sentence reading of the movement in the default slot content; the chart in the `aside` slot — `::line-chart` when the series are independent trajectories to be compared, `::area-chart` when they sum to one whole (its default stack makes exactly that claim; pass `:overlaid: true` when they do not). Every `label` in `data` is a point on a real sequence, in order. In the props block every NON-STRING value is colon-prefixed — `:data:`, `:series:`, `:overlaid:`, `:height:`, `:max-ticks:` — while string props go bare (`y-label: 'Trips'`, `legend-position: 'bottom-center'`); drop the colon and the value arrives as a string, so an array plots nothing and a `false` reads as truthy. Set `yLabel` unless the heading already carries the unit, and leave `height` at its 480 default: with the heading in the content column rather than a `::header::`, the `frame: content` figure column starts at the 360px vertical anchor and split.vue caps it at 630px, which 480 plus a legend row just fits. (A `::header::` would size row 1 to the 130 band instead and free the 592 a full-width chart gets — a different recipe, not this one.)",
6386
6386
  "use": "A slide whose claim is a direction — growth, a decline, a curve that flattened — where the audience needs to see the shape and read the takeaway in the same breath.",
6387
6387
  "avoid": "A comparison across categories: `chart`'s bars are the mark for that, and a line drawn across category labels claims a rate of change between things that have no 'between'. Also avoid it for two points and a delta — that is `stat-hero`, and a line through two dots dresses a single subtraction up as a trajectory."
6388
6388
  },
@@ -6391,7 +6391,7 @@
6391
6391
  "role": "Two measurements read against each other — the one chart slide whose answer is the shape of a cloud, not a figure anyone can read off an axis.",
6392
6392
  "layout": "default",
6393
6393
  "props": {},
6394
- "fill": "The takeaway as the slide's heading, and `::scatter-chart` under it filling the frame — `data` rows of `{ x, y }`, plus `size` (scaled by `sizeRange`) for a third measure and `group` when the dots split into named sets, which is the only case where the legend says anything. In the props block every NON-STRING value is colon-prefixed — `:data:`, `:height:`, `:labels:`, `:size-range:` — while string props go bare (`x-label: 'Parking bays'`, `legend-position: 'top'`); drop the colon and the value arrives as a string, so an array plots nothing and `labels: false` reads as truthy. Set both `xLabel` and `yLabel` — neither axis of a scatter is self-evident — turn `:labels: true` on only when there are few enough dots to name, about six before they collide, and set `:height: 592`, which is what every full-width chart under a heading in the specimen deck uses; the 480 default is sized for a split's figure column and leaves the foot of the slide empty here.",
6394
+ "fill": "The takeaway as the slide's heading, and `::scatter-chart` under it filling the frame — `data` rows of `{ x, y }`, plus `size` (scaled by `sizeRange`) for a third measure and `group` when the dots split into named sets, which is the only case where the legend says anything. In the props block every NON-STRING value is colon-prefixed — `:data:`, `:height:`, `:labels:`, `:size-range:` — while string props go bare (`x-label: 'Parking bays'`, `legend-position: 'bottom-center'`); drop the colon and the value arrives as a string, so an array plots nothing and `labels: false` reads as truthy. Set both `xLabel` and `yLabel` — neither axis of a scatter is self-evident — turn `:labels: true` on only when there are few enough dots to name, about six before they collide, and set `:height: 592`, which is what every full-width chart under a heading in the specimen deck uses; the 480 default is sized for a split's figure column and leaves the foot of the slide empty here.",
6395
6395
  "use": "A relationship between two measures — parking bays against trips, price against usage — where the point is whether the dots line up at all, and how tightly.",
6396
6396
  "avoid": "Anything measured over time: `trend` is that, and plotting (date, value) pairs as dots throws away the ordering a line's own segments carry, leaving the audience to re-sequence the cloud by eye. And do not move this into `trend`'s `split` figure column — that column caps at 630px and narrows the x range, pressing the cloud into a blob, which is the one compression a correlation slide cannot survive."
6397
6397
  },
@@ -324,7 +324,7 @@ export const PRESETS: Preset[] = [
324
324
  role: 'Several DIFFERENT kinds of reading at once — figures beside a figure with a shape, not one form repeated across a uniform row.',
325
325
  layout: 'default',
326
326
  props: {},
327
- fill: 'A `::grid` (`:cols: 2` or 3) mixing two or three `::stat-card` tiles with one figure tile — a status-board read, not a single chart type repeated. `::bars` labels every row and prints its own value, and `::dotted-map` draws no key at all, so neither needs anything around it; a tile whose figure carries several series does need a key, and every Unovis mark gets one from `::chart-frame`, which is where the `height`, `legend` and `legend-position` each mark forwards land (the empty state reaches it as the mark\'s own `empty`). Two of the frame\'s defaults are wrong inside a cell: the 480px plot is deeper than a grid cell is wide, and a bottom legend eats what is left of it — set `:height:` down and `legend-position: \'top\'`, or `:legend: false` and label the marks. Reach for `::chart-frame` directly only when the tile\'s figure is not one of the shipped marks: it is the one thing in the deck that puts a room-scale series key, and the honest empty well, under a figure built by hand.',
327
+ fill: 'A `::grid` (`:cols: 2` or 3) mixing two or three `::stat-card` tiles with one figure tile — a status-board read, not a single chart type repeated. `::bars` labels every row and prints its own value, and `::dotted-map` draws no key at all, so neither needs anything around it; a tile whose figure carries several series does need a key, and every Unovis mark gets one from `::chart-frame`, which is where the `height`, `legend` and `legend-position` each mark forwards land (the empty state reaches it as the mark\'s own `empty`). Two of the frame\'s defaults are wrong inside a cell: the 480px plot is deeper than a grid cell is wide, and a bottom legend eats what is left of it — set `:height:` down and `legend-position: \'top-left\'`, or `:legend: false` and label the marks. Reach for `::chart-frame` directly only when the tile\'s figure is not one of the shipped marks: it is the one thing in the deck that puts a room-scale series key, and the honest empty well, under a figure built by hand.',
328
328
  use: 'An operating-review slide that needs to show the state of several different things on one screen, the way a real dashboard does.',
329
329
  avoid: 'Several figures of the SAME kind that should read as one comparison — `metrics` keeps every tile the same shape on one baseline; mixing figure types into that row is exactly what `dashboard` is for, and exactly what breaks a `metrics` row\'s own promise.',
330
330
  },
@@ -459,7 +459,7 @@ export const PRESETS: Preset[] = [
459
459
  role: 'A quantity moving through time, with the one sentence that says what the movement means sitting beside it.',
460
460
  layout: 'split',
461
461
  props: { ratio: '2:3' },
462
- fill: 'Heading and a single-sentence reading of the movement in the default slot content; the chart in the `aside` slot — `::line-chart` when the series are independent trajectories to be compared, `::area-chart` when they sum to one whole (its default stack makes exactly that claim; pass `:overlaid: true` when they do not). Every `label` in `data` is a point on a real sequence, in order. In the props block every NON-STRING value is colon-prefixed — `:data:`, `:series:`, `:overlaid:`, `:height:`, `:max-ticks:` — while string props go bare (`y-label: \'Trips\'`, `legend-position: \'top\'`); drop the colon and the value arrives as a string, so an array plots nothing and a `false` reads as truthy. Set `yLabel` unless the heading already carries the unit, and leave `height` at its 480 default: with the heading in the content column rather than a `::header::`, the `frame: content` figure column starts at the 360px vertical anchor and split.vue caps it at 630px, which 480 plus a legend row just fits. (A `::header::` would size row 1 to the 130 band instead and free the 592 a full-width chart gets — a different recipe, not this one.)',
462
+ fill: 'Heading and a single-sentence reading of the movement in the default slot content; the chart in the `aside` slot — `::line-chart` when the series are independent trajectories to be compared, `::area-chart` when they sum to one whole (its default stack makes exactly that claim; pass `:overlaid: true` when they do not). Every `label` in `data` is a point on a real sequence, in order. In the props block every NON-STRING value is colon-prefixed — `:data:`, `:series:`, `:overlaid:`, `:height:`, `:max-ticks:` — while string props go bare (`y-label: \'Trips\'`, `legend-position: \'bottom-center\'`); drop the colon and the value arrives as a string, so an array plots nothing and a `false` reads as truthy. Set `yLabel` unless the heading already carries the unit, and leave `height` at its 480 default: with the heading in the content column rather than a `::header::`, the `frame: content` figure column starts at the 360px vertical anchor and split.vue caps it at 630px, which 480 plus a legend row just fits. (A `::header::` would size row 1 to the 130 band instead and free the 592 a full-width chart gets — a different recipe, not this one.)',
463
463
  use: 'A slide whose claim is a direction — growth, a decline, a curve that flattened — where the audience needs to see the shape and read the takeaway in the same breath.',
464
464
  avoid: 'A comparison across categories: `chart`\'s bars are the mark for that, and a line drawn across category labels claims a rate of change between things that have no \'between\'. Also avoid it for two points and a delta — that is `stat-hero`, and a line through two dots dresses a single subtraction up as a trajectory.',
465
465
  },
@@ -468,7 +468,7 @@ export const PRESETS: Preset[] = [
468
468
  role: 'Two measurements read against each other — the one chart slide whose answer is the shape of a cloud, not a figure anyone can read off an axis.',
469
469
  layout: 'default',
470
470
  props: {},
471
- fill: 'The takeaway as the slide\'s heading, and `::scatter-chart` under it filling the frame — `data` rows of `{ x, y }`, plus `size` (scaled by `sizeRange`) for a third measure and `group` when the dots split into named sets, which is the only case where the legend says anything. In the props block every NON-STRING value is colon-prefixed — `:data:`, `:height:`, `:labels:`, `:size-range:` — while string props go bare (`x-label: \'Parking bays\'`, `legend-position: \'top\'`); drop the colon and the value arrives as a string, so an array plots nothing and `labels: false` reads as truthy. Set both `xLabel` and `yLabel` — neither axis of a scatter is self-evident — turn `:labels: true` on only when there are few enough dots to name, about six before they collide, and set `:height: 592`, which is what every full-width chart under a heading in the specimen deck uses; the 480 default is sized for a split\'s figure column and leaves the foot of the slide empty here.',
471
+ fill: 'The takeaway as the slide\'s heading, and `::scatter-chart` under it filling the frame — `data` rows of `{ x, y }`, plus `size` (scaled by `sizeRange`) for a third measure and `group` when the dots split into named sets, which is the only case where the legend says anything. In the props block every NON-STRING value is colon-prefixed — `:data:`, `:height:`, `:labels:`, `:size-range:` — while string props go bare (`x-label: \'Parking bays\'`, `legend-position: \'bottom-center\'`); drop the colon and the value arrives as a string, so an array plots nothing and `labels: false` reads as truthy. Set both `xLabel` and `yLabel` — neither axis of a scatter is self-evident — turn `:labels: true` on only when there are few enough dots to name, about six before they collide, and set `:height: 592`, which is what every full-width chart under a heading in the specimen deck uses; the 480 default is sized for a split\'s figure column and leaves the foot of the slide empty here.',
472
472
  use: 'A relationship between two measures — parking bays against trips, price against usage — where the point is whether the dots line up at all, and how tightly.',
473
473
  avoid: 'Anything measured over time: `trend` is that, and plotting (date, value) pairs as dots throws away the ordering a line\'s own segments carry, leaving the audience to re-sequence the cloud by eye. And do not move this into `trend`\'s `split` figure column — that column caps at 630px and narrows the x range, pressing the cloud into a blob, which is the one compression a correlation slide cannot survive.',
474
474
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanforest/slidev-theme",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Forest brand theme for Slidev — the guardrail. Layouts + components wired to @humanforest/tokens, so any deck is on-brand by construction. Do not hand-pick colours/fonts here; everything aliases the engine tokens.",
5
5
  "keywords": [
6
6
  "slidev-theme",
@@ -19,6 +19,7 @@
19
19
  "slidev": {
20
20
  "colorSchema": "light",
21
21
  "defaults": {
22
+ "comark": true,
22
23
  "transition": "view-transition",
23
24
  "aspectRatio": "16/9",
24
25
  "canvasWidth": 1920,
@@ -31,10 +32,10 @@
31
32
  }
32
33
  },
33
34
  "dependencies": {
34
- "@humanforest/fonts": "^0.1.1",
35
- "@humanforest/frames": "^0.1.1",
36
- "@humanforest/tokens": "^0.1.1",
37
- "@humanforest/ui": "^0.1.1",
35
+ "@humanforest/fonts": "^0.1.3",
36
+ "@humanforest/frames": "^0.1.3",
37
+ "@humanforest/tokens": "^0.1.3",
38
+ "@humanforest/ui": "^0.1.3",
38
39
  "@iconify-json/lucide": "^1.2.0",
39
40
  "@iconify-json/vscode-icons": "^1.2.75",
40
41
  "@unovis/ts": "^1.6.7",
@@ -59,6 +60,7 @@
59
60
  "components",
60
61
  "styles",
61
62
  "setup",
63
+ "debug",
62
64
  "global-top.vue",
63
65
  "slide-top.vue",
64
66
  "slide-bottom.vue",