@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.
- package/LICENSE +67 -0
- package/README.md +577 -0
- package/assetUrl.js +38 -0
- package/components/Alert.vue +209 -0
- package/components/AreaBox.vue +152 -0
- package/components/AreaChart.vue +145 -0
- package/components/Avatar.vue +267 -0
- package/components/AvatarGroup.vue +94 -0
- package/components/BadgeStrip.vue +203 -0
- package/components/Bars.vue +207 -0
- package/components/BoroughShape.vue +127 -0
- package/components/BrowserFrame.vue +484 -0
- package/components/CalendarGrid.vue +480 -0
- package/components/Card.vue +70 -0
- package/components/Carousel.vue +266 -0
- package/components/ChartFrame.vue +426 -0
- package/components/ChordChart.vue +264 -0
- package/components/ComposedChart.vue +187 -0
- package/components/Contents.vue +86 -0
- package/components/DataTable.vue +244 -0
- package/components/DeckMap.vue +409 -0
- package/components/DeviceFrame.vue +331 -0
- package/components/DonutChart.vue +103 -0
- package/components/DottedMap.vue +126 -0
- package/components/Endorsement.vue +76 -0
- package/components/Grid.vue +169 -0
- package/components/GroupedBarChart.vue +141 -0
- package/components/IconTile.vue +116 -0
- package/components/Kpi.vue +188 -0
- package/components/LineChart.vue +133 -0
- package/components/Logo.vue +203 -0
- package/components/LondonMap.vue +348 -0
- package/components/MarkPair.vue +109 -0
- package/components/Marquee.vue +263 -0
- package/components/NestedDonutChart.vue +295 -0
- package/components/Placeholder.vue +88 -0
- package/components/SankeyChart.vue +223 -0
- package/components/ScatterChart.vue +142 -0
- package/components/StackedBarChart.vue +143 -0
- package/components/StatCard.vue +134 -0
- package/components/StatusTrack.vue +334 -0
- package/components/Timeline.vue +249 -0
- package/components/TimelineChart.vue +329 -0
- package/components/TreemapChart.vue +267 -0
- package/components/backgrounds.js +135 -0
- package/components/boroughGeometry.ts +59 -0
- package/components/chartData.ts +277 -0
- package/components/chartTip.ts +201 -0
- package/components/codeTheme.ts +239 -0
- package/components/deckPalette.ts +157 -0
- package/components/dottedMapGeometry.js +60 -0
- package/components/iconSlugs.js +55 -0
- package/components/iconUrl.js +51 -0
- package/components/mermaidTheme.ts +972 -0
- package/components/motionDuration.ts +41 -0
- package/components/tableRules.ts +257 -0
- package/confidential-mark.vue +151 -0
- package/custom-nav-controls.vue +79 -0
- package/endorsements/b-corp-black.svg +30 -0
- package/endorsements/b-corp-white.svg +30 -0
- package/global-top.vue +1662 -0
- package/icons/forest/bike-asterisk.svg +21 -0
- package/icons/forest/bike-down.svg +19 -0
- package/icons/forest/bike-off.svg +20 -0
- package/icons/forest/bike-up.svg +19 -0
- package/icons/forest/bike-x.svg +19 -0
- package/icons/forest/bike.svg +18 -0
- package/icons/forest/mark-fill.svg +6 -0
- package/icons/forest/mark.svg +6 -0
- package/icons/forest/parking.svg +3 -0
- package/icons/forest/star-fill.svg +4 -0
- package/icons/forest/star.svg +4 -0
- package/icons/forest/traffic-light-caution.svg +8 -0
- package/icons/forest/traffic-light-go.svg +8 -0
- package/icons/forest/traffic-light-lit.svg +8 -0
- package/icons/forest/traffic-light-stop.svg +8 -0
- package/icons/forest/traffic-light.svg +8 -0
- package/icons/lucide/battery.svg +1 -0
- package/icons/lucide/bike.svg +1 -0
- package/icons/lucide/chart-line.svg +1 -0
- package/icons/lucide/check.svg +1 -0
- package/icons/lucide/circle-check.svg +1 -0
- package/icons/lucide/clock.svg +1 -0
- package/icons/lucide/code.svg +1 -0
- package/icons/lucide/component.svg +1 -0
- package/icons/lucide/frame.svg +1 -0
- package/icons/lucide/layout-grid.svg +1 -0
- package/icons/lucide/map-pin.svg +1 -0
- package/icons/lucide/map.svg +1 -0
- package/icons/lucide/palette.svg +1 -0
- package/icons/lucide/table.svg +1 -0
- package/icons/lucide/triangle-alert.svg +1 -0
- package/icons/lucide/type.svg +1 -0
- package/icons/lucide/wrench.svg +1 -0
- package/layouts/bleed.vue +317 -0
- package/layouts/cover.vue +1514 -0
- package/layouts/default.vue +187 -0
- package/layouts/divider.vue +286 -0
- package/layouts/end.vue +384 -0
- package/layouts/quote.vue +218 -0
- package/layouts/split.vue +1875 -0
- package/layouts/stack.vue +944 -0
- package/layouts/statement.vue +67 -0
- package/manifest/chrome.ts +36 -0
- package/manifest/components.ts +49 -0
- package/manifest/index.ts +57 -0
- package/manifest/layouts.ts +37 -0
- package/manifest/manifest.json +6514 -0
- package/manifest/presets.ts +635 -0
- package/notch.js +458 -0
- package/notchPath.js +188 -0
- package/package.json +77 -0
- package/section.js +110 -0
- package/setup/mermaid-renderer.ts +2120 -0
- package/setup/mermaid.ts +30 -0
- package/setup/shiki.ts +44 -0
- package/setup/shortcuts.ts +28 -0
- package/slide-bottom.vue +305 -0
- package/slide-top.vue +268 -0
- package/styles/base.css +2611 -0
- package/styles/index.ts +71 -0
- package/styles/tokens.css +144 -0
- package/uno.config.ts +231 -0
- package/useNotchCuts.js +200 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/*
|
|
3
|
+
The workhorse. Adds one thing over a bare slide: the editorial kicker.
|
|
4
|
+
|
|
5
|
+
A content slide in the system's voice is eyebrow → shout → content, and making the eyebrow a
|
|
6
|
+
layout prop means an author gets that rhythm from frontmatter instead of remembering to write
|
|
7
|
+
an <hgroup>. Writing the hgroup by hand still works — base.css styles it — this is the short
|
|
8
|
+
form for the common case.
|
|
9
|
+
*/
|
|
10
|
+
defineProps({
|
|
11
|
+
/** Optional kicker rendered above the content, giving the eyebrow → shout → content rhythm from frontmatter instead of a hand-written hgroup. */
|
|
12
|
+
eyebrow: { type: String, default: '' },
|
|
13
|
+
/**
|
|
14
|
+
* Vertical position of the content: anchored to the top (default), centred in the frame, or
|
|
15
|
+
* `body-center` — the heading holds the top and everything under it centres in what is left.
|
|
16
|
+
*
|
|
17
|
+
* `body-center` is for a slide whose body is SHORT AND WIDE, where the other two both read as
|
|
18
|
+
* wrong: a wide diagram is a few hundred units tall on a 1080 canvas, so `start` leaves half the
|
|
19
|
+
* slide empty below it, and `center` pulls the heading down into the middle with it, losing the
|
|
20
|
+
* running rhythm every other content slide has. This keeps the heading where the eye expects it
|
|
21
|
+
* and centres only the part that has spare room around it.
|
|
22
|
+
*/
|
|
23
|
+
align: { type: String, default: 'start', validator: (a) => ['start', 'center', 'body-center'].includes(a) },
|
|
24
|
+
/**
|
|
25
|
+
* Draws this slide on the DARK register — the deck's dark ground with light ink — by putting
|
|
26
|
+
* `deck-dark` on the layout root.
|
|
27
|
+
*
|
|
28
|
+
* ★★ THIS IS NOT SLIDEV'S DARK MODE, and the two must not be confused. Slidev has one:
|
|
29
|
+
* `colorSchema` in headmatter, toggling `html.dark` for the WHOLE deck as a viewer preference.
|
|
30
|
+
* This theme pins `colorSchema: 'light'` (package.json) and never toggles it, because a
|
|
31
|
+
* presentation's register is a design decision baked into the deck rather than something the
|
|
32
|
+
* audience picks — dark is punctuation here, not the ground. Slidev offers no per-slide
|
|
33
|
+
* colorSchema; its supported per-slide hook is `class:`, which is exactly what this writes.
|
|
34
|
+
*
|
|
35
|
+
* It replaces `class: 'deck-dark'` in frontmatter — same class, same element, said as a prop the
|
|
36
|
+
* layout documents instead of a raw class name an author had to know.
|
|
37
|
+
*
|
|
38
|
+
* ⚠ THE TWO DARKS DO INTERACT, and base.css records where: Slidev's own code stylesheet picks
|
|
39
|
+
* shiki's palette off `html.dark`, which stays LIGHT through a dark slide, so a code block on
|
|
40
|
+
* this register needs the theme's own override or it scores 2.43:1.
|
|
41
|
+
*/
|
|
42
|
+
dark: { type: Boolean, default: false },
|
|
43
|
+
/**
|
|
44
|
+
* Takes the whole slide onto one of the four named backgrounds — the same table split's panel,
|
|
45
|
+
* stack's band and a Card read, so a full-field slide and a panel of the same name are the same
|
|
46
|
+
* colour by construction. `paper` (the default) leaves the deck's own ground.
|
|
47
|
+
*
|
|
48
|
+
* ★★ IT IS A GROUND, NOT A PANEL, AND THERE IS NO NOTCH. A panel notches so the running chrome
|
|
49
|
+
* can seat against the slide ground BESIDE it; at full field there is no ground left outside to
|
|
50
|
+
* seat into, which is why `split` and `stack` both switch their own notch off when `bleed` is
|
|
51
|
+
* true. The chrome lands ON this ground instead. Measured, the pill follows the register and
|
|
52
|
+
* clears the 3:1 a shape is held to on every one: light 3.55 vivid to 5.66 neutral, dark 3.55
|
|
53
|
+
* vivid to 8.51 accent.
|
|
54
|
+
*
|
|
55
|
+
* Composes with `dark`, which pins the pair's dark half exactly as it does on a panel.
|
|
56
|
+
*/
|
|
57
|
+
background: {
|
|
58
|
+
type: String,
|
|
59
|
+
default: 'paper',
|
|
60
|
+
validator: (b) => ['paper', 'warm', 'accent', 'vivid', 'neutral'].includes(b),
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<div
|
|
67
|
+
class="slidev-layout default h-full"
|
|
68
|
+
:class="[
|
|
69
|
+
align === 'center' ? 'is-center' : '',
|
|
70
|
+
align === 'body-center' ? 'is-body-center' : '',
|
|
71
|
+
background !== 'paper' ? `bg-${background} deck-own-ground` : '',
|
|
72
|
+
dark ? 'deck-dark' : '',
|
|
73
|
+
]"
|
|
74
|
+
:data-deck-distributes="align === 'center' || align === 'body-center' ? 'y' : undefined"
|
|
75
|
+
>
|
|
76
|
+
<!--
|
|
77
|
+
★ `data-deck-distributes` IS FOR THE LAYOUT INSPECTOR, and it is declared here rather than
|
|
78
|
+
detected there. Both centred modes hand out their free space with AUTO MARGINS, so the gap they
|
|
79
|
+
open is leftover — whatever is left after the content — and can land on any number at all. The
|
|
80
|
+
inspector measures gaps against the spacing ladder and marks a miss as an off-ladder HAZARD,
|
|
81
|
+
which meant a working `body-center` slide reported a red band every time.
|
|
82
|
+
|
|
83
|
+
Detecting it there is not possible: `getComputedStyle` resolves `margin-top: auto` to its USED
|
|
84
|
+
value in px, so an auto margin and a hardcoded one are indistinguishable after the fact. The
|
|
85
|
+
layout that creates the free space is the only thing that knows, so it says so — next to the
|
|
86
|
+
class whose rules do it, rather than in a list somewhere else that the next centred layout would
|
|
87
|
+
have to remember to join.
|
|
88
|
+
-->
|
|
89
|
+
<p v-if="eyebrow" class="deck-eyebrow">{{ eyebrow }}</p>
|
|
90
|
+
<slot />
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
93
|
+
|
|
94
|
+
<style scoped>
|
|
95
|
+
/*
|
|
96
|
+
task-14b-brief.md — Jose: "heading should start not at the border top I guess, but have some
|
|
97
|
+
breathing space." Only the top-anchored case (`align: 'start'`, the default, plain block flow)
|
|
98
|
+
actually has that problem — a slide is already at the standard --deck-inset-text (96px) from the
|
|
99
|
+
edge, but a heading's own cap-height sits right at the TOP of that padding with nothing further
|
|
100
|
+
above it, which reads as flush rather than as air. `is-center` below doesn't have this problem at
|
|
101
|
+
all (centred content already reads as deliberate), so this is scoped to `:not(.is-center)` rather
|
|
102
|
+
than applied to the layout unconditionally.
|
|
103
|
+
|
|
104
|
+
--deck-heading-top-space is one full grid unit (40px) ON TOP of the standard text inset — chosen
|
|
105
|
+
because it is a clean, already-meaningful number in this system (it is what the OLD chrome/media
|
|
106
|
+
inset used to be, before task-14b's 36px revision freed it up) rather than a fraction invented for
|
|
107
|
+
this one rule, and because 40px reads as clearly MORE than the ambient inset rather than a marginal
|
|
108
|
+
nudge — total top padding on an ordinary content slide is 136px (96 + 40), about 13% of the 1080
|
|
109
|
+
canvas. Scoped here, not in base.css's shared `.slidev-layout` rule: applying it there would also
|
|
110
|
+
shift split.vue's figure escape maths and every centred beat layout's (cover/divider/statement/
|
|
111
|
+
quote/end) vertical centring asymmetrically, none of which have the flush-top problem this solves.
|
|
112
|
+
*/
|
|
113
|
+
.slidev-layout.default:not(.is-center) {
|
|
114
|
+
padding-top: calc(var(--deck-inset-text) + var(--deck-heading-top-space));
|
|
115
|
+
}
|
|
116
|
+
/* `align: 'start'` stays the plain block flow every existing slide already renders — only
|
|
117
|
+
'center' opts into a flex column, so today's decks are untouched. */
|
|
118
|
+
.default.is-center {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-direction: column;
|
|
121
|
+
justify-content: center;
|
|
122
|
+
}
|
|
123
|
+
/*
|
|
124
|
+
`body-center` is a flex column too, but left at flex-start: the free space is handed out by the
|
|
125
|
+
auto margins in base.css rather than by justify-content, which is what lets the heading keep the
|
|
126
|
+
top while the rest centres beneath it.
|
|
127
|
+
|
|
128
|
+
⚠ IT IS DELIBERATELY NOT `.is-center`. Three rules elsewhere key on `:not(.is-center)` — the
|
|
129
|
+
heading's top air, and two that clear the running chrome — and a `body-center` slide wants every
|
|
130
|
+
one of them, because its heading IS at the top. Sharing the class would have silently removed the
|
|
131
|
+
air this mode exists to keep.
|
|
132
|
+
*/
|
|
133
|
+
.default.is-body-center {
|
|
134
|
+
display: flex;
|
|
135
|
+
flex-direction: column;
|
|
136
|
+
justify-content: flex-start;
|
|
137
|
+
}
|
|
138
|
+
/*
|
|
139
|
+
★★ THE EYEBROW IS A RUNNING HEAD AND DOES NOT TRAVEL WITH THE CENTRED COLUMN.
|
|
140
|
+
|
|
141
|
+
It is a sibling of the slot, so `justify-content: center` centred the two together and the kicker
|
|
142
|
+
moved with whatever the slide happened to hold. MEASURED on apps/deck's type-ramp pages,
|
|
143
|
+
which are the case that found this: the eyebrow landed at y 263, 203 and 251 on three consecutive
|
|
144
|
+
pages of one catalogue section. A running head that jumps 60px page to page is worse than none,
|
|
145
|
+
because the eye tracks it.
|
|
146
|
+
|
|
147
|
+
★ IN FLOW, NOT PINNED, and that is the whole design of this rule. The obvious fix is
|
|
148
|
+
`position: absolute` on the eyebrow — and deckNotch.static.test.ts rejects it, correctly: all four
|
|
149
|
+
corners belong to the running chrome when it shows, so a layout that parks a box in one has to ask
|
|
150
|
+
`chromeShows()` first. This box does not want a corner at all; it wants the top of the flow with
|
|
151
|
+
the rest centred beneath it. Flexbox does exactly that with auto margins, which absorb the free
|
|
152
|
+
space before `justify-content` gets any: the first slot child takes it above, the last takes it
|
|
153
|
+
below, and the eyebrow keeps its natural place at the top. No absolute positioning, no corner, no
|
|
154
|
+
question to ask, and the eyebrow's own height is reserved because it never leaves the flow.
|
|
155
|
+
|
|
156
|
+
The extra air matches a top-anchored slide's: --deck-heading-top-space on the eyebrow itself
|
|
157
|
+
rather than on the layout's padding, so the running head sits on the same line either way.
|
|
158
|
+
|
|
159
|
+
⚠ THE AUTO MARGINS THEMSELVES LIVE IN base.css, NOT HERE, and that is not tidiness — a scoped rule
|
|
160
|
+
cannot reach them. `<style scoped>` stamps this component's own elements with a data attribute and
|
|
161
|
+
the slot content comes from the slide's markdown, which carries none, so
|
|
162
|
+
`.deck-eyebrow + *` compiled to a selector matching nothing and the first rung kept
|
|
163
|
+
`margin-top: 0`. Measured that way first: the eyebrow still wandered, 159 then 123. base.css is
|
|
164
|
+
where every other rule that reaches into a layout's slot already lives. Search `is-center` there.
|
|
165
|
+
*/
|
|
166
|
+
.default.is-center > .deck-eyebrow {
|
|
167
|
+
margin-top: var(--deck-heading-top-space);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* ── The full-field ground ─────────────────────────────────────────────────────────────────────
|
|
171
|
+
The FOLLOWING pair, so `dark` on the root re-points both halves — the same rule a panel, a band
|
|
172
|
+
and a Card run on, off the same table. Nothing here changes the layout: the content keeps the
|
|
173
|
+
text inset and the working voice, because this is the workhorse on a colour rather than a beat
|
|
174
|
+
slide. `divider` is the beat slide, and it re-sets the type as well as the ground.
|
|
175
|
+
|
|
176
|
+
The root also takes `deck-own-ground`, which `divider` does not need and this does: a divider
|
|
177
|
+
holds a title and a rule, and an author will write a bulleted LIST on here. base.css keys the
|
|
178
|
+
bullet and marker protection off that one class — without it a list on a saturated field paints
|
|
179
|
+
its markers in --deck-accent, which is the 1.00:1 defect that rule exists for. */
|
|
180
|
+
.default.bg-warm { background: var(--deck-bg-warm); color: var(--deck-on-bg-warm); }
|
|
181
|
+
.default.bg-accent { background: var(--deck-bg-accent); color: var(--deck-on-bg-accent); }
|
|
182
|
+
.default.bg-vivid { background: var(--deck-bg-vivid); color: var(--deck-on-bg-vivid); }
|
|
183
|
+
.default.bg-neutral { background: var(--deck-bg-neutral); color: var(--deck-on-bg-neutral); }
|
|
184
|
+
/* The text tiers are handed back to the pair's ink in base.css, keyed on the background classes
|
|
185
|
+
rather than on this layout — see the note there. A scoped copy lived here and was one of four,
|
|
186
|
+
which is how the stack band came to paint a near-black heading on a brown panel. */
|
|
187
|
+
</style>
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/*
|
|
3
|
+
The divider — the deck's punctuation, and the one place a slide takes over the whole field.
|
|
4
|
+
|
|
5
|
+
★★ ITS GROUND IS ITS RANK, NOT A COLOUR AN AUTHOR PICKS. `rank` says what the slide OPENS — a
|
|
6
|
+
section, a subsection, a topic — and the treatment follows from that: the ground, the headline
|
|
7
|
+
size and the rule all step down together, so a reader learns the ladder once and reads depth
|
|
8
|
+
everywhere. An author naming a colour per divider is choosing decoration where the deck wants
|
|
9
|
+
structure, and the specimen it produced proved the point: nine sections, nine different grounds,
|
|
10
|
+
none of them meaning anything.
|
|
11
|
+
|
|
12
|
+
⚠ IT HAD `background` AND `dark` INSTEAD, the same four named pairs a panel takes. They came off
|
|
13
|
+
rather than being kept as an override, and the reason is a gate rather than a preference: this
|
|
14
|
+
theme requires an enum prop's default to be a member of its own enum, so there is no way to spell
|
|
15
|
+
"unset, take the rank's" — every value including the default would have to win over the rank, and
|
|
16
|
+
a lever that always wins is not an override. A one-off ground that is not a rank is a `default`
|
|
17
|
+
slide with a `background`, which is the layout that exists for exactly that.
|
|
18
|
+
|
|
19
|
+
★★ THE LADDER, AND WHAT CARRIES IT. Each ground is one of the eight pairs the deck publishes, so
|
|
20
|
+
the ink is the one published against that fill — nothing here assembles a colour. Measured as how
|
|
21
|
+
far each departs from the deck's cream page:
|
|
22
|
+
|
|
23
|
+
section --deck-bg-vivid dark #046c4c 6.1 : 1 against the page
|
|
24
|
+
subsection --deck-bg-warm light #ffd6a6 1.30 : 1
|
|
25
|
+
topic --deck-bg-accent light #cef5dd 1.13 : 1
|
|
26
|
+
|
|
27
|
+
★★ ONE DARK CUT, THEN TWO PALE ONES, AND THAT SHAPE IS THE POINT. A section is the only rank that
|
|
28
|
+
takes the field — the deck's own green, the brand voice, a new part. Everything below it is a
|
|
29
|
+
pause inside something rather than the start of a thing, so neither takes the whole slide's
|
|
30
|
+
attention: the warm cut for a turn, the pale accent for the shallowest.
|
|
31
|
+
|
|
32
|
+
⚠ WHICH MEANS WEIGHT SEPARATES THE SECTION AND ALMOST NOTHING ELSE. Subsection and topic sit at
|
|
33
|
+
1.30 and 1.13 against the page — a fifth of a step apart, which no room will read as a ranking.
|
|
34
|
+
What tells THOSE two apart is HUE (warm against the accent's mint) and the TYPE, 72px against
|
|
35
|
+
60px. So the headline tier and the rule are load-bearing rather than decorative here: drop them
|
|
36
|
+
and the two lower ranks become the same slide in slightly different paint.
|
|
37
|
+
|
|
38
|
+
Every pair clears 3:1 for the headline and the rule at 0.6 (measured; the tightest is vivid-dark's
|
|
39
|
+
rule at 3.31).
|
|
40
|
+
|
|
41
|
+
★ AND THE TYPE STEPS WITH IT, so the rank survives a greyscale print and a reader who is not
|
|
42
|
+
looking at the ground. The headline drops a tier per rank, in tokens, so the ladder moves when the
|
|
43
|
+
scale does.
|
|
44
|
+
|
|
45
|
+
The rule is the ink, dimmed — currentColor, not a second token — so a background can never
|
|
46
|
+
assemble an ink that was not published against its fill.
|
|
47
|
+
|
|
48
|
+
── WHAT EACH RANK MAY CARRY ────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
★★ THE RANK DECIDES THE CONTENT, NOT ONLY THE PAINT, and that is what stops the three from being
|
|
51
|
+
one slide in three colours. A standfirst — the lead line under the title — is REQUIRED on a
|
|
52
|
+
section, OPTIONAL on a subsection and REFUSED on a topic:
|
|
53
|
+
|
|
54
|
+
section title + standfirst the reader is being asked to change subject
|
|
55
|
+
subsection title + standfirst? a turn inside something they are already in
|
|
56
|
+
topic title a beat, not an orientation
|
|
57
|
+
|
|
58
|
+
The argument is the reader's, not the author's. A section break is the one moment someone can be
|
|
59
|
+
genuinely lost — a new part, possibly after a break — so it earns a sentence saying what the part
|
|
60
|
+
is FOR. A topic is a punctuation mark inside a thought they are already following, and a sentence
|
|
61
|
+
there would be answering a question nobody asked.
|
|
62
|
+
|
|
63
|
+
⚠ IT ALSO CLOSES A GAP THIS LAYOUT HAD FROM THE START: `:deep(p)` and `:deep(h2)` were styled here
|
|
64
|
+
and drawn by NOTHING. Measured across both decks before this — 21 divider slides, not one carrying
|
|
65
|
+
anything but its heading. The tier existed, dimmed to 0.85, with no slide to prove it.
|
|
66
|
+
|
|
67
|
+
★ Enforced where it can be: the specimen's own generator refuses a section divider with no
|
|
68
|
+
standfirst and a topic divider with one, and the same rule is gated over the written markdown in
|
|
69
|
+
frontmatter.static.test.ts. A layout cannot validate its own slot, so the gate is where it lands.
|
|
70
|
+
|
|
71
|
+
⚠ IT CARRIED A KICKER NUMBER, `index`, AND IT WAS ANSWERING NOTHING. The claim for it was that a
|
|
72
|
+
section opener should say "part 03 of 09" and pair with the contents slide. It did not pair: the
|
|
73
|
+
contents lists SLIDE numbers — `4 · Type`, `10 · Colour` — while the divider showed a SECTION
|
|
74
|
+
ordinal, so the two numbers a reader sees for one section disagreed and neither led to the other.
|
|
75
|
+
What was left was a progress cue the room cannot act on, beside a running section pill and a slide
|
|
76
|
+
number that already answer "where am I". And below section rank it could only ever be a
|
|
77
|
+
placeholder: this deck has no 3.2, so every demo and subsection divider rendered a literal "00".
|
|
78
|
+
*/
|
|
79
|
+
import { computed, inject } from 'vue';
|
|
80
|
+
import { injectionFrontmatter } from '@slidev/client/constants.ts';
|
|
81
|
+
import { ICON_SLUGS } from '../components/iconSlugs';
|
|
82
|
+
import { iconUrlOrFallback } from '../components/iconUrl';
|
|
83
|
+
|
|
84
|
+
const props = defineProps({
|
|
85
|
+
/** What the slide opens — `section`, `subsection` or `topic`. It picks the ground, the headline
|
|
86
|
+
* tier and the rule together, so depth is one decision rather than three. Not a colour: a
|
|
87
|
+
* one-off ground belongs on a `default` slide with a `background`. */
|
|
88
|
+
rank: { type: String, default: 'section', validator: (v) => ['section', 'subsection', 'topic'].includes(v) },
|
|
89
|
+
/**
|
|
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
|
|
92
|
+
* subsection is not a new part. Ignored at the other ranks rather than refused, so changing a
|
|
93
|
+
* slide's rank never breaks it.
|
|
94
|
+
*/
|
|
95
|
+
icon: {
|
|
96
|
+
type: String,
|
|
97
|
+
default: '',
|
|
98
|
+
validator: (v) => (import.meta.env.DEV ? v === '' || ICON_SLUGS.includes(v) : true),
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
★★ THE KICKER IS RANK-SHAPED — one slot, and what belongs in it changes with the rank, because the
|
|
104
|
+
three ranks answer three different questions for the reader:
|
|
105
|
+
|
|
106
|
+
section an ICON what this part is ABOUT — a new subject needs a face
|
|
107
|
+
subsection the SECTION NAME where you ARE — the subject holds, the angle changes
|
|
108
|
+
topic nothing a beat inside a thought already being followed
|
|
109
|
+
|
|
110
|
+
A subsection is the only rank with somewhere to point AT: the part enclosing it. A section has no
|
|
111
|
+
parent to name, and repeating its own title is not orientation. A topic sits close enough to the
|
|
112
|
+
thing it interrupts that the question does not arise.
|
|
113
|
+
|
|
114
|
+
⚠ AND IT IS THE SLOT THE KICKER NUMBER USED TO SIT IN. That number came out because it lied — it
|
|
115
|
+
read as a section ordinal while the contents slide listed slide numbers, so the two disagreed and
|
|
116
|
+
neither led to the other. What replaces it is not a second number: an icon and a breadcrumb each
|
|
117
|
+
say something a reader cannot get anywhere else on the slide.
|
|
118
|
+
*/
|
|
119
|
+
const frontmatter = inject(injectionFrontmatter, {});
|
|
120
|
+
/*
|
|
121
|
+
★ THE PARENT COMES FROM `section`, WHICH IS ALREADY REQUIRED AND ALREADY MEANS THIS. Every divider
|
|
122
|
+
must declare the run it BELONGS to (frontmatter.static.test.ts) — for a section opener that is its
|
|
123
|
+
own title, for anything drawn INSIDE a section it is the enclosing one. So at subsection rank the
|
|
124
|
+
value IS the parent by construction, and there is no second key to keep in step.
|
|
125
|
+
|
|
126
|
+
Read through `injectionFrontmatter` rather than declared as a prop, because `section` is deck-wide
|
|
127
|
+
state section.js already owns: a second declaration here could disagree with it, and a layout prop
|
|
128
|
+
of that name would put it in the manifest as though a divider's section were a styling choice.
|
|
129
|
+
*/
|
|
130
|
+
const parent = computed(() => (props.rank === 'subsection' ? String(frontmatter?.section ?? '') : ''));
|
|
131
|
+
const iconMask = computed(() => (props.rank === 'section' && props.icon ? `url("${iconUrlOrFallback(props.icon)}")` : ''));
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<template>
|
|
135
|
+
<div
|
|
136
|
+
class="slidev-layout divider h-full flex flex-col justify-center"
|
|
137
|
+
:class="[`rank-${['section', 'subsection', 'topic'].includes(rank) ? rank : 'section'}`, rank === 'section' ? 'deck-dark' : '']"
|
|
138
|
+
>
|
|
139
|
+
<span
|
|
140
|
+
v-if="iconMask"
|
|
141
|
+
class="divider-icon"
|
|
142
|
+
:style="{ maskImage: iconMask, webkitMaskImage: iconMask }"
|
|
143
|
+
aria-hidden="true"
|
|
144
|
+
/>
|
|
145
|
+
<div v-else-if="parent" class="divider-parent">{{ parent }}</div>
|
|
146
|
+
<div v-if="iconMask || parent" class="divider-rule" />
|
|
147
|
+
<slot />
|
|
148
|
+
</div>
|
|
149
|
+
</template>
|
|
150
|
+
|
|
151
|
+
<style scoped>
|
|
152
|
+
/* The beat margin — see cover.vue's own note. Both edges, not just the left: the left-only version
|
|
153
|
+
this replaces rendered a 120px left margin against a 90px right one on every divider and closer,
|
|
154
|
+
with nothing in any file explaining the asymmetry. */
|
|
155
|
+
.divider {
|
|
156
|
+
padding-left: var(--deck-inset-display);
|
|
157
|
+
padding-right: var(--deck-inset-display);
|
|
158
|
+
}
|
|
159
|
+
/* The FOLLOWING pair, so the `deck-dark` the template hands the ONE dark rank re-points both halves
|
|
160
|
+
here — the same rule a panel takes. Section is the only rank that takes a dark half now; the two
|
|
161
|
+
pale ones read on the light register like any other slide. */
|
|
162
|
+
.divider.rank-section { background: var(--deck-bg-vivid); color: var(--deck-on-bg-vivid); }
|
|
163
|
+
.divider.rank-subsection { background: var(--deck-bg-warm); color: var(--deck-on-bg-warm); }
|
|
164
|
+
.divider.rank-topic { background: var(--deck-bg-accent); color: var(--deck-on-bg-accent); }
|
|
165
|
+
/* base.css gives h1/h2/p HARD colours at (0,2,0), which beat the inherited pair; every text tier on
|
|
166
|
+
a divider is the ink the background published, and nothing else. */
|
|
167
|
+
.divider :deep(h1),
|
|
168
|
+
.divider :deep(h2) {
|
|
169
|
+
color: currentColor;
|
|
170
|
+
}
|
|
171
|
+
.divider :deep(p) {
|
|
172
|
+
color: currentColor;
|
|
173
|
+
opacity: 0.85;
|
|
174
|
+
}
|
|
175
|
+
/* The divider is a beat slide — Mohr uppercase, not the layout-default working voice. */
|
|
176
|
+
.divider :deep(h1) {
|
|
177
|
+
font-family: var(--type-deck-display-family);
|
|
178
|
+
font-weight: var(--type-deck-display-weight);
|
|
179
|
+
text-transform: uppercase;
|
|
180
|
+
font-size: var(--type-deck-text-8xl);
|
|
181
|
+
line-height: 0.95;
|
|
182
|
+
}
|
|
183
|
+
/* One tier down per rank. The ladder is the point: a subsection that shouted as loudly as the
|
|
184
|
+
section above it would be telling the room they were the same thing. */
|
|
185
|
+
.divider.rank-subsection :deep(h1) { font-size: var(--type-deck-text-7xl); }
|
|
186
|
+
.divider.rank-topic :deep(h1) { font-size: var(--type-deck-text-6xl); }
|
|
187
|
+
/*
|
|
188
|
+
The standfirst — the lead role a cover's subtitle takes, not body copy, and `h1 + p` so it is the
|
|
189
|
+
line UNDER THE TITLE rather than any paragraph an author drops on the slide.
|
|
190
|
+
|
|
191
|
+
★★ IT STEPS WITH THE RANK, BECAUSE THE RATIO IS WHAT MATTERS AND THE RATIO COLLAPSED. Written
|
|
192
|
+
first as one size for both, on the argument that the heading ladder already carried depth and a
|
|
193
|
+
second ladder would say the same thing twice. MEASURED on the render, that argument was wrong:
|
|
194
|
+
|
|
195
|
+
section 96 over 60 1.60
|
|
196
|
+
subsection 72 over 60 1.20
|
|
197
|
+
|
|
198
|
+
A standfirst is subordinate to the title it explains, and at 1.20 it is not — the sentence and
|
|
199
|
+
the heading read as two lines of similar weight, so the eye has no order to take them in. The
|
|
200
|
+
section's 1.60 is the relationship the pair wants, and the body role holds it at 72 over 48.
|
|
201
|
+
|
|
202
|
+
★ A TOPIC CARRIES NO STANDFIRST AT ALL, which is what made the first argument look sound — there
|
|
203
|
+
are only two rungs to fill. It is still given a size, for the case the contract cannot reach: a
|
|
204
|
+
deck built on this theme elsewhere is not gated by this repo's tests, and an ungoverned paragraph
|
|
205
|
+
there would inherit the base 60 under a 60 heading, a ratio of 1.00.
|
|
206
|
+
|
|
207
|
+
The measure is the cover's own 20em — about forty characters of GT Haptik, a sentence rather than
|
|
208
|
+
a paragraph, and that cap is the point: a divider needing two lines of explanation is a slide.
|
|
209
|
+
*/
|
|
210
|
+
.divider :deep(h1 + p) {
|
|
211
|
+
margin-top: var(--deck-space-2);
|
|
212
|
+
font-size: var(--type-deck-subtitle-size);
|
|
213
|
+
line-height: var(--type-deck-subtitle-line-height);
|
|
214
|
+
max-width: 20em;
|
|
215
|
+
text-wrap: balance;
|
|
216
|
+
}
|
|
217
|
+
.divider.rank-subsection :deep(h1 + p) {
|
|
218
|
+
font-size: var(--type-deck-body-size);
|
|
219
|
+
line-height: var(--type-deck-body-line-height);
|
|
220
|
+
}
|
|
221
|
+
/*
|
|
222
|
+
★ TOPIC HAS NO STANDFIRST, AND THIS IS WHAT HAPPENS IF ONE ARRIVES ANYWAY. The rank contract
|
|
223
|
+
refuses it and frontmatter.static.test.ts gates every divider in this repo — but that gate reads
|
|
224
|
+
the slides.md files HERE, and a deck built on this theme elsewhere is not one of them. Without a
|
|
225
|
+
case of its own such a paragraph would inherit the base 60px under a 60px heading: a ratio of
|
|
226
|
+
1.00, the collapse the subsection rule above exists to fix, only complete.
|
|
227
|
+
|
|
228
|
+
Sized rather than hidden, deliberately. Dropping an author's sentence with no explanation is the
|
|
229
|
+
worse failure of the two — the contract is stated in the prop docs and enforced where this theme
|
|
230
|
+
owns the deck; where it does not, the type still has to hold up. 60 over 36 is 1.67, which is the
|
|
231
|
+
section's own relationship.
|
|
232
|
+
*/
|
|
233
|
+
.divider.rank-topic :deep(h1 + p) {
|
|
234
|
+
font-size: var(--type-deck-body-sm-size);
|
|
235
|
+
line-height: var(--type-deck-body-sm-line-height);
|
|
236
|
+
}
|
|
237
|
+
/*
|
|
238
|
+
The mark, drawn as a CSS mask in the ink the ground published — the technique IconTile and Logo
|
|
239
|
+
both use, for the reason IconTile records: an `i-forest-*` utility only exists once UnoCSS has
|
|
240
|
+
seen the literal class in source, and an `icon` PROP means the string is assembled at runtime,
|
|
241
|
+
which the scanner never sees.
|
|
242
|
+
|
|
243
|
+
Sized against the type ramp rather than pinned, so the mark moves the day the ramp is retuned.
|
|
244
|
+
*/
|
|
245
|
+
.divider-icon {
|
|
246
|
+
display: block;
|
|
247
|
+
width: var(--type-deck-text-6xl);
|
|
248
|
+
height: var(--type-deck-text-6xl);
|
|
249
|
+
margin-bottom: var(--deck-space-2);
|
|
250
|
+
background-color: currentColor;
|
|
251
|
+
mask-repeat: no-repeat;
|
|
252
|
+
mask-position: center;
|
|
253
|
+
mask-size: contain;
|
|
254
|
+
-webkit-mask-repeat: no-repeat;
|
|
255
|
+
-webkit-mask-position: center;
|
|
256
|
+
-webkit-mask-size: contain;
|
|
257
|
+
}
|
|
258
|
+
/* The breadcrumb takes the overline role — the deck's smallest, widest-tracked tier, which is what
|
|
259
|
+
a label above a title is everywhere else here. Dimmed like the rule, because it orients rather
|
|
260
|
+
than announces: the title is still the thing being read. */
|
|
261
|
+
.divider-parent {
|
|
262
|
+
font-family: var(--font-sans);
|
|
263
|
+
font-size: var(--type-deck-overline-size);
|
|
264
|
+
font-weight: var(--type-deck-overline-weight);
|
|
265
|
+
letter-spacing: 0.08em;
|
|
266
|
+
text-transform: uppercase;
|
|
267
|
+
opacity: 0.7;
|
|
268
|
+
margin-bottom: var(--deck-space-1);
|
|
269
|
+
}
|
|
270
|
+
/*
|
|
271
|
+
★ THE RULE BELONGS TO THE KICKER, not to the slide — and not to the icon either, which was the
|
|
272
|
+
first attempt. It is a separator, so it is drawn exactly when there are two things to separate:
|
|
273
|
+
a mark or a breadcrumb above, the title below. A topic has neither, and over a bare heading the
|
|
274
|
+
rule was separating the title from nothing at all.
|
|
275
|
+
*/
|
|
276
|
+
.divider-rule {
|
|
277
|
+
width: var(--deck-space-7);
|
|
278
|
+
height: 6px;
|
|
279
|
+
margin-bottom: var(--deck-space-3);
|
|
280
|
+
/* The ink, dimmed — not an accent. The old dark tone painted this in --deck-accent, a hue on top
|
|
281
|
+
of a register; on a warm or neutral field that would be the one green thing on the slide. */
|
|
282
|
+
background: currentColor;
|
|
283
|
+
opacity: 0.6;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
</style>
|