@humanforest/slidev-theme 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +67 -0
  2. package/README.md +577 -0
  3. package/assetUrl.js +38 -0
  4. package/components/Alert.vue +209 -0
  5. package/components/AreaBox.vue +152 -0
  6. package/components/AreaChart.vue +145 -0
  7. package/components/Avatar.vue +267 -0
  8. package/components/AvatarGroup.vue +94 -0
  9. package/components/BadgeStrip.vue +203 -0
  10. package/components/Bars.vue +207 -0
  11. package/components/BoroughShape.vue +127 -0
  12. package/components/BrowserFrame.vue +484 -0
  13. package/components/CalendarGrid.vue +480 -0
  14. package/components/Card.vue +70 -0
  15. package/components/Carousel.vue +266 -0
  16. package/components/ChartFrame.vue +426 -0
  17. package/components/ChordChart.vue +264 -0
  18. package/components/ComposedChart.vue +187 -0
  19. package/components/Contents.vue +86 -0
  20. package/components/DataTable.vue +244 -0
  21. package/components/DeckMap.vue +409 -0
  22. package/components/DeviceFrame.vue +331 -0
  23. package/components/DonutChart.vue +103 -0
  24. package/components/DottedMap.vue +126 -0
  25. package/components/Endorsement.vue +76 -0
  26. package/components/Grid.vue +169 -0
  27. package/components/GroupedBarChart.vue +141 -0
  28. package/components/IconTile.vue +116 -0
  29. package/components/Kpi.vue +188 -0
  30. package/components/LineChart.vue +133 -0
  31. package/components/Logo.vue +203 -0
  32. package/components/LondonMap.vue +348 -0
  33. package/components/MarkPair.vue +109 -0
  34. package/components/Marquee.vue +263 -0
  35. package/components/NestedDonutChart.vue +295 -0
  36. package/components/Placeholder.vue +88 -0
  37. package/components/SankeyChart.vue +223 -0
  38. package/components/ScatterChart.vue +142 -0
  39. package/components/StackedBarChart.vue +143 -0
  40. package/components/StatCard.vue +134 -0
  41. package/components/StatusTrack.vue +334 -0
  42. package/components/Timeline.vue +249 -0
  43. package/components/TimelineChart.vue +329 -0
  44. package/components/TreemapChart.vue +267 -0
  45. package/components/backgrounds.js +135 -0
  46. package/components/boroughGeometry.ts +59 -0
  47. package/components/chartData.ts +277 -0
  48. package/components/chartTip.ts +201 -0
  49. package/components/codeTheme.ts +239 -0
  50. package/components/deckPalette.ts +157 -0
  51. package/components/dottedMapGeometry.js +60 -0
  52. package/components/iconSlugs.js +55 -0
  53. package/components/iconUrl.js +51 -0
  54. package/components/mermaidTheme.ts +972 -0
  55. package/components/motionDuration.ts +41 -0
  56. package/components/tableRules.ts +257 -0
  57. package/confidential-mark.vue +151 -0
  58. package/custom-nav-controls.vue +79 -0
  59. package/endorsements/b-corp-black.svg +30 -0
  60. package/endorsements/b-corp-white.svg +30 -0
  61. package/global-top.vue +1662 -0
  62. package/icons/forest/bike-asterisk.svg +21 -0
  63. package/icons/forest/bike-down.svg +19 -0
  64. package/icons/forest/bike-off.svg +20 -0
  65. package/icons/forest/bike-up.svg +19 -0
  66. package/icons/forest/bike-x.svg +19 -0
  67. package/icons/forest/bike.svg +18 -0
  68. package/icons/forest/mark-fill.svg +6 -0
  69. package/icons/forest/mark.svg +6 -0
  70. package/icons/forest/parking.svg +3 -0
  71. package/icons/forest/star-fill.svg +4 -0
  72. package/icons/forest/star.svg +4 -0
  73. package/icons/forest/traffic-light-caution.svg +8 -0
  74. package/icons/forest/traffic-light-go.svg +8 -0
  75. package/icons/forest/traffic-light-lit.svg +8 -0
  76. package/icons/forest/traffic-light-stop.svg +8 -0
  77. package/icons/forest/traffic-light.svg +8 -0
  78. package/icons/lucide/battery.svg +1 -0
  79. package/icons/lucide/bike.svg +1 -0
  80. package/icons/lucide/chart-line.svg +1 -0
  81. package/icons/lucide/check.svg +1 -0
  82. package/icons/lucide/circle-check.svg +1 -0
  83. package/icons/lucide/clock.svg +1 -0
  84. package/icons/lucide/code.svg +1 -0
  85. package/icons/lucide/component.svg +1 -0
  86. package/icons/lucide/frame.svg +1 -0
  87. package/icons/lucide/layout-grid.svg +1 -0
  88. package/icons/lucide/map-pin.svg +1 -0
  89. package/icons/lucide/map.svg +1 -0
  90. package/icons/lucide/palette.svg +1 -0
  91. package/icons/lucide/table.svg +1 -0
  92. package/icons/lucide/triangle-alert.svg +1 -0
  93. package/icons/lucide/type.svg +1 -0
  94. package/icons/lucide/wrench.svg +1 -0
  95. package/layouts/bleed.vue +317 -0
  96. package/layouts/cover.vue +1514 -0
  97. package/layouts/default.vue +187 -0
  98. package/layouts/divider.vue +286 -0
  99. package/layouts/end.vue +384 -0
  100. package/layouts/quote.vue +218 -0
  101. package/layouts/split.vue +1875 -0
  102. package/layouts/stack.vue +944 -0
  103. package/layouts/statement.vue +67 -0
  104. package/manifest/chrome.ts +36 -0
  105. package/manifest/components.ts +49 -0
  106. package/manifest/index.ts +57 -0
  107. package/manifest/layouts.ts +37 -0
  108. package/manifest/manifest.json +6514 -0
  109. package/manifest/presets.ts +635 -0
  110. package/notch.js +458 -0
  111. package/notchPath.js +188 -0
  112. package/package.json +77 -0
  113. package/section.js +110 -0
  114. package/setup/mermaid-renderer.ts +2120 -0
  115. package/setup/mermaid.ts +30 -0
  116. package/setup/shiki.ts +44 -0
  117. package/setup/shortcuts.ts +28 -0
  118. package/slide-bottom.vue +305 -0
  119. package/slide-top.vue +268 -0
  120. package/styles/base.css +2611 -0
  121. package/styles/index.ts +71 -0
  122. package/styles/tokens.css +144 -0
  123. package/uno.config.ts +231 -0
  124. package/useNotchCuts.js +200 -0
@@ -0,0 +1,67 @@
1
+ <script setup>
2
+ /*
3
+ One sentence, the whole slide. The tier below a section divider: still a shout, but on the
4
+ deck's own ground rather than a full-bleed field, so it can carry a real claim instead of a
5
+ chapter name. Bold the part that matters — `strong` is the accent.
6
+ */
7
+ defineProps({
8
+ /** Optional kicker line rendered above the slide's one claim, e.g. a chapter or context label. */
9
+ eyebrow: { type: String, default: '' },
10
+ /** Horizontal position of the claim: ragged left (default), centred, or ragged right. */
11
+ align: { type: String, default: 'start', validator: (a) => ['start', 'center', 'end'].includes(a) },
12
+ /** Vertical position of the claim on the slide: top, optically centred (default), or bottom. */
13
+ valign: { type: String, default: 'center', validator: (v) => ['start', 'center', 'end'].includes(v) },
14
+ })
15
+ </script>
16
+
17
+ <template>
18
+ <div
19
+ class="slidev-layout statement h-full flex flex-col"
20
+ :class="[`is-h-${align}`, `is-v-${valign}`]"
21
+ >
22
+ <p v-if="eyebrow" class="deck-eyebrow">{{ eyebrow }}</p>
23
+ <slot />
24
+ </div>
25
+ </template>
26
+
27
+ <style scoped>
28
+ /* The beat margin — see cover.vue's own note. Both edges, not just the left: the left-only version
29
+ this replaces rendered a 120px left margin against a 90px right one on every divider and closer,
30
+ with nothing in any file explaining the asymmetry. */
31
+ .statement {
32
+ padding-left: var(--deck-inset-display);
33
+ padding-right: var(--deck-inset-display);
34
+ }
35
+ /*
36
+ Nine positions, as three classes on each axis rather than one `is-center` special case. The old
37
+ rule styled only 'center' so that an unstyled 'start' stayed pixel-identical; with a third value
38
+ on each axis that trick stops paying, because `end` needs a rule of its own either way and two
39
+ half-specified axes are harder to read than six explicit ones.
40
+
41
+ ★ THE TEXT ALIGNMENT HAS TO FOLLOW THE BOX. `align-items` moves the h1's BOX, and the h1 carries
42
+ `max-width: 14em` — so an end-aligned box whose text is still ragged-left puts the claim on the
43
+ right of the slide with its short last line hanging off the left of its own column. The two are
44
+ one decision, not two.
45
+ */
46
+ .statement.is-h-start { align-items: flex-start; text-align: left; }
47
+ .statement.is-h-center { align-items: center; text-align: center; }
48
+ .statement.is-h-end { align-items: flex-end; text-align: right; }
49
+
50
+ /* Vertical. `center` is the historical hard-coded `justify-center`, kept as the default so every
51
+ existing statement slide renders exactly as it did. */
52
+ .statement.is-v-start { justify-content: flex-start; }
53
+ .statement.is-v-center { justify-content: center; }
54
+ .statement.is-v-end { justify-content: flex-end; }
55
+ /* Statement is a beat slide — Mohr uppercase, not the layout-default working voice. */
56
+ .statement :deep(h1) {
57
+ font-family: var(--type-deck-display-family);
58
+ font-weight: var(--type-deck-display-weight);
59
+ text-transform: uppercase;
60
+ font-size: var(--type-deck-text-8xl); /* 6rem / 96px — ramp 8xl */
61
+ line-height: 0.95;
62
+ max-width: 14em; /* ≈20 characters of Mohr uppercase */
63
+ }
64
+ .statement :deep(strong) {
65
+ color: var(--deck-accent);
66
+ }
67
+ </style>
@@ -0,0 +1,36 @@
1
+ // The chrome half of LAYOUT_ROLES' own doc comment (packages/slidev-theme/manifest/layouts.ts) —
2
+ // read that file first. `slide-top.vue`/`slide-bottom.vue` live at the package ROOT
3
+ // (packages/slidev-theme/), not under layouts/ or components/ — see the re-brief note in
4
+ // scripts/deckCoverage.ts's DIRS for why that split matters and what it cost once already.
5
+ // Keyed by filename without its extension, matching how scripts/deckManifest.ts names every
6
+ // other entry.
7
+ //
8
+ // Neither file declares a prop today (both read frontmatter directly via `inject`, not via
9
+ // `defineProps`), so their manifest entries carry an empty `props` array — that is the
10
+ // "genuinely no props" case scripts/deckManifest.ts's liveness gate is required to tell apart
11
+ // from a parse failure, not a gap in this table.
12
+ //
13
+ // task-6-fix-1.md's ★ MINOR — both files gate their own visibility on the SAME frontmatter key,
14
+ // `chrome: false`, and neither role string named it before this fix: a manifest reader had no way
15
+ // to learn that a slide can suppress the running chrome at all, let alone how. Recorded here, in
16
+ // prose, the same pattern `confidential:` already used in slide-bottom's own role — `chrome:` is
17
+ // not a declared PROP (Vue's prop system has no visibility here at all; both files just `inject`
18
+ // the raw frontmatter object), so it cannot appear in `props` the way a real prop would; naming it
19
+ // in the role text is the only place in this manifest it CAN appear.
20
+ // task-18 — two more frontmatter keys with the same shape as `chrome:` and `confidential:`: read by
21
+ // injection, never declared as props, so prose here is the only place in this manifest they CAN
22
+ // appear. `companion:` widens the running mark into a co-branded lockup (and the notch cut for it
23
+ // follows automatically, because both are computed from one function in
24
+ // packages/slidev-theme/notch.js); `notch: false` is the per-slide escape hatch from the cut.
25
+ export const CHROME_ROLES: Record<string, string> = {
26
+ 'custom-nav-controls': 'The theme\'s own buttons in Slidev\'s nav toolbar — a REVIEWER\'s controls, not deck content. Slidev renders this file automatically (@slidev/client/internals/NavControls.vue imports `#slidev/custom-nav-controls`); a theme without it contributes nothing. Two toggles: hide the confidentiality chip, and draw the layout inset frames (the same flag the `l` shortcut sets). Both are PREVIEW-ONLY OVERRIDES that can only ever subtract — a slide with no `confidential:` cannot be made to show one from here. Neither can reach an exported PDF: `slidev export` launches a separate browser process, so the refs start false in it.',
27
+ 'slide-top': 'Running chrome, mounted per slide — the page-number and section pills top-left, the Forest mark top-right. A slide opts out of both with `chrome: false` in its own frontmatter, or out of the SECTION PILL alone with `section: false` — the notched panel or band on that slide gets its plain corner back automatically, because the cut is measured from whatever chrome is actually on it. `section: \'…\'` on a DIVIDER renames the pill for that whole run of slides instead. CO-BRANDING: `companion: /companion.svg` draws a companion\'s mark beside the Forest one at the same height, at the same size and clipped to the mark\'s own rounded square, whatever the source artwork is, with `companionName:` supplying its alt text — the notched panel or band on the same slide widens its cut to match, with no other change. `notch: false` keeps the running chrome but drops the cut, returning the figure to its full corner.',
28
+ 'slide-bottom': 'Running chrome, mounted per slide — the confidentiality watermark bottom-left, from a slide\'s own `confidential:` frontmatter. A slide opts out with `chrome: false`, the same key `slide-top` reads.',
29
+ // task-14-brief.md, requirement 6 — mounted ONCE globally (Slidev's `global-top.vue` convention,
30
+ // not `slide-top`/`slide-bottom`'s per-slide one), because it is not a per-slide, frontmatter-
31
+ // gated concern the way the other two chrome files are: it draws the same red-line frames on
32
+ // every slide regardless of that slide's own `chrome:` setting, since the point is to let a
33
+ // reviewer check ANY slide's geometry, including the ones that hide the running chrome.
34
+ 'confidential-mark': 'The standing confidentiality marking itself — a stroked, unfilled pill in --deck-fg-subtle, drawn by `slide-bottom` from a slide\'s own `confidential:` frontmatter. Chrome rather than a component because an author never places one: a deck sets it once in the headmatter\'s `defaults:` (or a slide declares its own), and it is suppressed with the same `chrome: false` the rest of the running chrome reads. It was `BadgeStrip variant="outline"` until it earned its own file — it paints no background pair, drops the uppercase and the tracking, runs at 0.6 of the overline size, which is what keeps a legal marking quiet without putting it under the reading floor.',
35
+ 'global-top': 'The layout-debug (red-line) overlay — draws the text/media/chrome inset frames and the chrome bands so the two-track spacing system can be checked by eye. OFF by default; toggled with the `l` key (setup/shortcuts.ts), never by frontmatter, and forced off whenever the route is in print/export mode regardless of the toggle. A review tool, not per-slide chrome — see README.md\'s "Debugging layout" section.',
36
+ };
@@ -0,0 +1,49 @@
1
+ // The component half of LAYOUT_ROLES' own doc comment (packages/slidev-theme/manifest/layouts.ts)
2
+ // — read that file first; everything said there about WHY this table exists, what it must never
3
+ // claim, and what scripts/deckManifest.ts checks it against applies here unchanged.
4
+ //
5
+ // Every entry is a close paraphrase of that component's own top-of-file doc comment in
6
+ // packages/slidev-theme/components/*.vue.
7
+ export const COMPONENT_ROLES: Record<string, string> = {
8
+ AreaChart: 'An area chart sized for a room — stacked by default, because a stack claims the parts sum to a whole; `overlaid` switches the claim.',
9
+ BadgeStrip: 'An array-driven row of dark tiles — the deck\'s tag list.',
10
+ Alert: 'A subtle status panel — a tinted block naming one of four states, with its own lamp. The colour IS the message, which is what separates it from a Callout.',
11
+ Avatar: 'One person: their face in a ring of the page\'s own ink, their name on a chip riding the ring\'s bottom edge, and what they do under that. Three diameters, and `size` steps the two caption sizes and the space between them along with the circle — from a row of six to one person given the whole slide. With no photograph it falls back to their initials rather than to an empty circle. It draws ONE person: a team is several of these in a `Grid`, which is the component that already owns columns and gutters.',
12
+ AvatarGroup: 'A row of faces overlapping into one object — how many people, not which ones. It composes `Avatar` with no name and no role, so the circle and its ring are that component\'s; what lives here is the negative gap and the stacking order, at three overlaps from a long row to a crowd. The captioned counterpart is a `Grid` of `Avatar`, which introduces people rather than counting them.',
13
+ Bars: 'A horizontal bar chart sized for a room: bars start at zero, coloured from the deck\'s own series roles.',
14
+ BoroughShape: 'A single London borough as a styleable glyph — one outline, framed to itself, filled with currentColor, drawn at one of four heights whose middle step is the natural size.',
15
+ LondonMap: 'All 33 London boroughs in one frame, in true relative position — the vector counterpart to the dot grid, with any subset lifted into the accent.',
16
+ CalendarGrid: 'A year of days as a grid of cells shaded by intensity — the mark for seasonality, where the question is which weeks are busy rather than what any one day was.',
17
+ Contents: 'The deck\'s contents page, derived from the dividers themselves — every `divider` slide with a title becomes an entry, at `rank: section` or nested under the section above it at `rank: subsection`, and `hideInToc` on a divider opts it out. Takes no props: a contents page has nothing to configure, and the deck it belongs to is the only argument it needs.',
18
+ Endorsement: 'A third-party certification mark — Certified B Corporation today, as B Lab supplies it. Two sanctioned colourways — `positive` (black, for light grounds) and `reverse` (white, for dark grounds and photographs) — and the component SWAPS FILES between them rather than tinting one, because the licence forbids recolouring. There is deliberately no colour prop. Verra is not here: its mark needs written approval, and the docs page renders a placeholder rather than the real thing.',
19
+ Carousel: 'One card at a time, advanced by the presenter\'s own clicks, in a box the size of the largest so nothing below it moves — with a position indicator, which is what `v-switch` cannot give a room.',
20
+ Card: 'The deck\'s one container — no fill at all, a quiet fill one step away from the slide ground, or one of the four named backgrounds, which brings its own ink with it; `dark` renders the last two in the dark register, and `border` adds an edge, drawn in the card\'s own ink, which no background carries by default.',
21
+ ChartFrame: 'The ceremony every deck chart shares: the room-scale Unovis type ramp, the legend and the empty state. Marks stay in the slot.',
22
+ ChordChart: 'Flows BETWEEN the members of one set, drawn as a ring — where a Sankey shows a quantity moving through stages, this shows it moving among peers.',
23
+ ComposedChart: 'Bars and a line on one shared x axis — volume as columns with a rate drawn over them, the two-mark slide a single chart type cannot make.',
24
+ DataTable: 'A table sized for a room — a few rows, a few columns, a header that reads from the back, and figures in the tabular treatment. Rows truncate and say so on the slide; columns refuse past five.',
25
+ DonutChart: 'Up to five slices of one whole, with the total in the hole — the number a part-to-whole chart cannot show any other way.',
26
+ DeckMap: 'A live Mapbox GL map on Forest\'s own house style — the real basemap, for a slide that needs streets rather than the dot grid\'s abstraction. Non-interactive by default, and draws an honest panel when the build carries no token.',
27
+ DottedMap: 'London as a dot grid — the design system\'s own map primitive, sampled from real borough geometry.',
28
+ GroupedBarChart: 'A cluster of bars per category, every one measured from the same zero — the honest mark for independent series that share an axis.',
29
+ Grid: 'The deck\'s one grid — a closed set of gutters and columns, optionally driven by an `items` array.',
30
+ IconTile: 'A single Forest icon on a rounded warm tile.',
31
+ Kpi: 'A KPI on a slide — one figure, what it is, and optionally how it has moved (arrow = direction, colour = judgement).',
32
+ LineChart: 'A line chart sized for a room. A line claims the space between its points, so the x axis has to be a real sequence.',
33
+ Logo: 'The Forest identity, in the four forms the brand ships: mark, wordmark, lockup, lockup-vertical.',
34
+ MarkPair: 'Forest\'s mark with a partner\'s beside it at one height — the co-branding object. The partner\'s artwork is covered into a square and clipped to the mark\'s own corner with a ring in the inherited ink, and every one of those numbers is a RATIO of `size`, so the pair holds its proportions at the chrome\'s 72 and at a poster\'s 120 alike. With no `src` it is the mark alone, which is what the running chrome draws on a slide with no `companion:`.',
35
+ Marquee: 'A band of words moving slowly across the slide — the deck\'s one piece of motion that is content rather than transition. Legible standing still, which is the state reduced motion and an export both leave it in.',
36
+ NestedDonutChart: 'A donut with concentric rings — part-to-whole where the parts themselves break down, with the inner ring naming what the outer one is made of.',
37
+ AreaBox: 'A named region of a layout, drawn as a filled labelled block — the piece that lets a frame be shown as its areas instead of its content.',
38
+ BrowserFrame: 'A browser window around a mock — three lights, an address pill and a page area whose `ratio` is the VIEWPORT rather than the window. Drawn entirely from deck tokens: the deck\'s counterpart to the design system\'s FBrowserFrame, which cannot be used here because it is written in Nuxt UI\'s semantic classes.',
39
+ DeviceFrame: 'A phone drawn around a live screen, at the model\'s true logical points and clipped to the render\'s own aperture. Metrics and art come from `@humanforest/frames`; only the presentation is the deck\'s own. The renders are Wikimedia Commons vectors under CC BY-SA 4.0, so attribution travels with them.',
40
+ Placeholder: '"An image goes here" — a deliberately obvious placeholder, hatched and dashed rather than iconic, drawn in CSS so overflow measurements stay repeatable.',
41
+ SankeyChart: 'Flow through stages, where the width of every ribbon IS the quantity — the mark for a total that arrives, splits, and lands somewhere in full.',
42
+ ScatterChart: 'Two measurements against each other, one dot per observation — the one deck chart whose message is a shape, not a number.',
43
+ StackedBarChart: 'A column per category, split into the parts that make it up. Bars start at zero; the segments sum to the column.',
44
+ StatCard: 'A stat inside a card — the deck\'s Kpi figure, framed by the deck\'s one container.',
45
+ StatusTrack: 'One asset read left to right as a row of equal-width buckets — a bike\'s week, a bay\'s ninety days. `state` colours a condition from the status roles; `intensity` buckets a quantity onto the sequential ramp. The deck\'s re-implementation of packages/charts FStatusTrack.',
46
+ Timeline: 'A sequence of moments on one rule, horizontal for up to about five stops and vertical when each one has more to say, with a single stop optionally marked as the present.',
47
+ TimelineChart: 'Labelled rows of bars spanning time — when each thing ran, and against what. The presentational sequence-of-stops on a rule is `Timeline`, a different component entirely.',
48
+ TreemapChart: 'Part-to-whole where the parts are too many or too uneven for a donut — area is the quantity, and a group can nest inside it.',
49
+ };
@@ -0,0 +1,57 @@
1
+ // The manifest package's public surface — the one module scripts/deckManifest.ts (the generator)
2
+ // and packages/slidev-theme/test/manifest.static.test.ts (its tests) both import from, rather than
3
+ // each reaching into layouts.ts/components.ts/chrome.ts/presets.ts separately. Also where the
4
+ // shared TypeScript shapes for a generated manifest.json live, so the generator and its tests
5
+ // agree on the shape by construction rather than by convention.
6
+ export { LAYOUT_ROLES } from './layouts';
7
+ export { COMPONENT_ROLES } from './components';
8
+ export { CHROME_ROLES } from './chrome';
9
+ export { PRESETS, validatePreset, type Preset } from './presets';
10
+
11
+ /**
12
+ * One prop, as it appears inside a layout/component/chrome manifest entry. Mirrors `PropDetail`
13
+ * from packages/slidev-theme/test/props.ts minus `enumRef`, which is resolved to real values (or
14
+ * fails the build) before a manifest entry exists at all — see scripts/deckManifest.ts.
15
+ *
16
+ * FIX ROUND 1, IMPORTANT 3 — `required` is carried through explicitly rather than folded into
17
+ * `default === null`: several real props (Bars.vue's `max`, Kpi.vue's `precision`/`delta`) declare
18
+ * a genuine `null`/`undefined` default and are NOT required, so `default === null` alone cannot
19
+ * tell "no default was ever declared" apart from "the default really is null" — `required` is the
20
+ * field that can. `default` is now a real value of the JSON type it represents (see `PropDetail`'s
21
+ * own doc comment for the defect this fixes and the measured blast radius), never quoted source
22
+ * text; `hasDynamicDefault`/`defaultSource` carry the cases `default` cannot represent.
23
+ */
24
+ export type ManifestProp = {
25
+ name: string;
26
+ type: string | null;
27
+ enum: string[] | null;
28
+ /** A real value of the JSON type it represents. `null` means EITHER "no default was declared"
29
+ * (check `required`) OR "the default genuinely is null/undefined" — see `hasDynamicDefault`
30
+ * before trusting either reading, and never treat `default === null` alone as "required". */
31
+ default: string | number | boolean | null;
32
+ /** True when `default` could not represent the real default (an arrow function or factory) —
33
+ * `default` is always `null` when this is `true`. Check this BEFORE reading `default` as "the
34
+ * default is null". */
35
+ hasDynamicDefault: boolean;
36
+ /** The raw declared-default source text, verbatim, for a human reading the manifest — never a
37
+ * value to assign. `null` only when there is no `default:` key in source at all. */
38
+ defaultSource: string | null;
39
+ required: boolean;
40
+ description: string;
41
+ };
42
+
43
+ /** One layout, component, or chrome file. */
44
+ export type ManifestEntry = {
45
+ name: string;
46
+ role: string;
47
+ props: ManifestProp[];
48
+ slots: string[];
49
+ };
50
+
51
+ export type Manifest = {
52
+ $generated: string;
53
+ layouts: ManifestEntry[];
54
+ components: ManifestEntry[];
55
+ chrome: ManifestEntry[];
56
+ presets: import('./presets').Preset[];
57
+ };
@@ -0,0 +1,37 @@
1
+ // The one thing about a layout the source itself cannot mechanically hand over: a short,
2
+ // human-facing statement of what it's FOR. Everything else in a layout's manifest entry — its
3
+ // props, their types/enums/defaults/descriptions, its slots — is derived straight from
4
+ // packages/slidev-theme/layouts/*.vue by scripts/deckManifest.ts (via
5
+ // packages/slidev-theme/test/props.ts's shared parser). This table is the one place role text is
6
+ // hand-authored, and it exists for the same reason scripts/docsCoverage.ts's own HOMES map is
7
+ // hand-written rather than derived: which layout is "the opener" versus "the workhorse" is an
8
+ // editorial judgement no glob can infer.
9
+ //
10
+ // Every entry below is a close paraphrase of that layout's own top-of-file doc comment in
11
+ // packages/slidev-theme/layouts/*.vue — never an independent claim about behaviour the source
12
+ // doesn't already make. scripts/deckManifest.ts fails the build if a layout exists on disk with
13
+ // no entry here (a new layout landing without a role, silently emitting `role: ''` into the
14
+ // manifest, is exactly the kind of gap this project keeps re-discovering the hard way) and,
15
+ // separately, if an entry here names a layout that no longer exists on disk (dead metadata,
16
+ // pointing Task 8's showcase generator at nothing).
17
+ // task-6-fix-1.md's ★ MINOR — `chrome: false` (a frontmatter key the running chrome reads, see
18
+ // packages/slidev-theme/manifest/chrome.ts) appeared nowhere in the manifest, so a reader had no
19
+ // way to learn that the retired template deck paired it with six of these eight
20
+ // layouts EVERY time it uses them: cover, divider, bleed, quote, statement and end all carry
21
+ // `chrome: false` on every single occurrence in that deck (measured: 6/6 each); `default` and
22
+ // `split` carry it on NONE of theirs (0/10, 0/4). That is a real, consistent convention — these
23
+ // six all take over the WHOLE field for one claim or one piece of media, which is exactly what the
24
+ // running chrome's quiet corners would visually compete with — not a mechanical fact the layout
25
+ // component itself enforces (`chrome:` is read by the two chrome files, not by any layout, and a
26
+ // `default`/`split` slide is free to set it too). Recorded as a convention note, not a rule.
27
+ export const LAYOUT_ROLES: Record<string, string> = {
28
+ bleed: 'Full-bleed media with content laid over it — the only layout that escapes the slide\'s padding. Commonly paired with `chrome: false`.',
29
+ cover: 'The opener. Light ground, one Mohr shout at the top of the ramp, the full lockup signing it. Commonly paired with `chrome: false`.',
30
+ default: 'The workhorse. A bare content slide, plus the optional eyebrow → shout → content rhythm from frontmatter.',
31
+ divider: 'The deck\'s punctuation — the one place a slide takes over the whole field. `rank` says what it opens, and the ground, the headline and the rule step down together. Commonly paired with `chrome: false`.',
32
+ end: 'The closer, in four shapes: the closing line over the lockup, two display words split around one of the brand\'s own renders, the wordmark alone at display scale, or a photograph edge to edge under a scrim. Takes the same two grounds an opener does, so a deck that states neither opens and closes on the same colour. Carries no certification — a sign-off ends on the brand, not on somebody else\'s mark. Commonly paired with `chrome: false`.',
33
+ quote: 'A pull quote — the one slide where the sans face runs large, because a quote is somebody else speaking. Commonly paired with `chrome: false`.',
34
+ split: 'The other workhorse. Content on one side, a figure on the other.',
35
+ stack: 'The horizontal counterpart of `split` — a hero band across the top, the title demoted to a strip below it. The one arrangement where the figure leads and the title captions it.',
36
+ statement: 'One sentence, the whole slide — a shout on the deck\'s own ground, one tier below a section divider. Commonly paired with `chrome: false`.',
37
+ };