@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
package/layouts/end.vue
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/*
|
|
3
|
+
The closer. Four shapes, the same question the opener answers in reverse: what is the last thing
|
|
4
|
+
on the screen while the room is still looking at it.
|
|
5
|
+
|
|
6
|
+
★★ THE VARIANTS ARE COMPOSITIONS, NOT COLOURS — the same split cover.vue documents at length. A
|
|
7
|
+
ground is `background` plus the register, and every shape here takes both, so the two props stay
|
|
8
|
+
orthogonal and the names stay about arrangement.
|
|
9
|
+
|
|
10
|
+
⚠ AND NO PILL EITHER. `cover` takes a `badge` — a strapline under the title, where a title is
|
|
11
|
+
already the loud thing and a pill beside it is a second, quieter line. A closer has no such
|
|
12
|
+
hierarchy: on `signoff` the pill lands between the two words and reads as a third voice, and on
|
|
13
|
+
`wordmark` it turns the identity into a lockup with a caption. Jose, twice, on two different
|
|
14
|
+
variants: "remove this". So there is no `badge` prop here.
|
|
15
|
+
|
|
16
|
+
⚠ AND THE CLOSER CARRIES NO CERTIFICATION. `cover` does, on its brand variant, because an opener
|
|
17
|
+
is where a deck states what it is; a sign-off that ends on a compliance mark ends on somebody
|
|
18
|
+
else's logo. Jose, on this layout specifically: "fuck bcorp in end layout". There is no
|
|
19
|
+
`endorsements` prop here and there should not be one.
|
|
20
|
+
*/
|
|
21
|
+
import { computed } from 'vue';
|
|
22
|
+
import { BEAT_GROUND, END_GROUNDS, END_PHOTO_GROUND } from '../notch.js';
|
|
23
|
+
import { assetUrl } from '../assetUrl.js';
|
|
24
|
+
|
|
25
|
+
const props = defineProps({
|
|
26
|
+
/**
|
|
27
|
+
* WHAT SHAPE the closer is.
|
|
28
|
+
*
|
|
29
|
+
* · `plain` — the closing line and the lockup under it. Type alone on the ground.
|
|
30
|
+
* · `signoff` — two display words split around one of the brand's own renders, the second word
|
|
31
|
+
* in front of it. The loudest close the deck has.
|
|
32
|
+
* · `wordmark` — the wordmark alone at display scale, centred. The quietest.
|
|
33
|
+
* · `lockup` — the same, composed: mark over wordmark, at the brand's own proportions.
|
|
34
|
+
* · `photo` — a photograph edge to edge under a scrim, the closing line and lockup over it.
|
|
35
|
+
*
|
|
36
|
+
* ★ `wordmark` AND `lockup` ARE TWO SHAPES, NOT ONE WITH A SWITCH. The wordmark is a WORD held
|
|
37
|
+
* alone and reads as a signature; the vertical lockup is the mark ON the word and reads as a
|
|
38
|
+
* stamp. Which one closes a deck is the same kind of choice as which one opens it, and
|
|
39
|
+
* `Logo` already draws both from one set of proportions — this layout only says which.
|
|
40
|
+
*/
|
|
41
|
+
variant: {
|
|
42
|
+
type: String,
|
|
43
|
+
default: 'plain',
|
|
44
|
+
validator: (v) => ['plain', 'signoff', 'wordmark', 'lockup', 'photo'].includes(v),
|
|
45
|
+
},
|
|
46
|
+
/**
|
|
47
|
+
* The ground. `auto` resolves to the brand pair, `paper` leaves the deck's own surface, and the
|
|
48
|
+
* four named pairs are the same table `default`, `split`'s panel, `stack`'s band and a Card read.
|
|
49
|
+
*
|
|
50
|
+
* ★ THE TWO BOOKENDS RESOLVE `auto` FROM ONE TABLE (notch.js's BEAT_GROUND), so a deck that
|
|
51
|
+
* states neither opens and closes on the same colour by construction rather than by an author
|
|
52
|
+
* remembering to match them.
|
|
53
|
+
*
|
|
54
|
+
* ★★ AND THE CLOSER TAKES MORE GROUNDS THAN THE OPENER. cover.vue narrows itself to two because a
|
|
55
|
+
* deck states its identity there; a closer is the last frame and is the slide a deck is allowed to
|
|
56
|
+
* be loud on. See END_GROUNDS in notch.js, which the chrome's own register reader clamps against.
|
|
57
|
+
*/
|
|
58
|
+
background: {
|
|
59
|
+
type: String,
|
|
60
|
+
default: 'auto',
|
|
61
|
+
validator: (b) => END_GROUNDS.includes(b),
|
|
62
|
+
},
|
|
63
|
+
/** Force the register. Unstated, the ground decides — which is the whole point of `auto`. */
|
|
64
|
+
dark: { type: Boolean, default: null },
|
|
65
|
+
/**
|
|
66
|
+
* The two display words of a `signoff`, split around the render: `lead` sits above it and behind
|
|
67
|
+
* it, `trail` below it and in front.
|
|
68
|
+
*
|
|
69
|
+
* ★ TWO PROPS, NOT ONE STRING THIS LAYOUT SPLITS. "Thank you" splits at the space; "Ride with
|
|
70
|
+
* us" does not, and a layout guessing where a phrase breaks is a layout that breaks it in the
|
|
71
|
+
* wrong place on the one slide nobody re-reads. The author says which word goes where.
|
|
72
|
+
*/
|
|
73
|
+
lead: { type: String, default: '' },
|
|
74
|
+
/** The second word — the one that overlaps the render from in front. */
|
|
75
|
+
trail: { type: String, default: '' },
|
|
76
|
+
/**
|
|
77
|
+
* The picture: one of the brand's renders on `signoff` (a cutout, drawn over the ground), a
|
|
78
|
+
* photograph on `photo` (bled to the frame, under a scrim). Slidev's own key, so
|
|
79
|
+
* `extractImagesUsage()` preloads it either way.
|
|
80
|
+
*/
|
|
81
|
+
image: { type: String, default: '' },
|
|
82
|
+
/** Alt text for that picture. */
|
|
83
|
+
alt: { type: String, default: '' },
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const VARIANT_CLASS = {
|
|
87
|
+
plain: 'is-plain',
|
|
88
|
+
signoff: 'is-signoff',
|
|
89
|
+
wordmark: 'is-wordmark',
|
|
90
|
+
lockup: 'is-lockup',
|
|
91
|
+
photo: 'is-photo',
|
|
92
|
+
}
|
|
93
|
+
/* Looked up rather than interpolated, so an invented variant renders the default composition
|
|
94
|
+
instead of minting an unstyled `is-<whatever>` — a validator is stripped in production. */
|
|
95
|
+
const variantClass = computed(() => VARIANT_CLASS[props.variant] ?? 'is-plain')
|
|
96
|
+
|
|
97
|
+
const stated = computed(() => (END_GROUNDS.includes(props.background) ? props.background : 'auto'))
|
|
98
|
+
const ground = computed(() => {
|
|
99
|
+
/* The photograph IS the ground on this one — the same fact cover.vue records for its poster. Light
|
|
100
|
+
ink on a dark picture, fixed, and notch.js agrees so the chrome reads the same slide. */
|
|
101
|
+
if (END_PHOTO_GROUND.includes(props.variant)) return { background: 'paper', dark: true }
|
|
102
|
+
const unstated = stated.value === 'auto' && props.dark === null
|
|
103
|
+
const g = unstated ? BEAT_GROUND : {}
|
|
104
|
+
return {
|
|
105
|
+
background: g.background ?? (stated.value === 'auto' ? 'paper' : stated.value),
|
|
106
|
+
dark: g.dark ?? (props.dark === true),
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
★ THE RENDER'S OWN SIZE, AS A SHARE OF THE FRAME RATHER THAN A PIXEL COUNT. A cutout has no box of
|
|
112
|
+
its own — it is a bike on nothing — so what decides its scale is how much of the slide it should
|
|
113
|
+
occupy against two words the same height as it. Stated here so the two numbers are read together.
|
|
114
|
+
*/
|
|
115
|
+
const RENDER_WIDTH = '56%'
|
|
116
|
+
const RENDER_HEIGHT = '66%'
|
|
117
|
+
|
|
118
|
+
/* The wordmark variant's own scale. `size` on <Logo> is the MARK's height for every form, so 300
|
|
119
|
+
draws the wordmark 581px across — a wordmark that IS the composition rather than a signature in a
|
|
120
|
+
corner, and still short of the 697px the poster's own stack settled on for a narrower column. */
|
|
121
|
+
const END_WORDMARK_PX = 300
|
|
122
|
+
/* The composed form is sized from its MARK, and a mark over a wordmark is a taller object than the
|
|
123
|
+
wordmark alone — 260 draws the stack about the height 300 draws the word, which is what keeps the
|
|
124
|
+
two variants the same weight in the frame rather than the same number. */
|
|
125
|
+
const END_LOCKUP_PX = 260
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<template>
|
|
129
|
+
<div
|
|
130
|
+
class="slidev-layout end h-full relative overflow-hidden"
|
|
131
|
+
:class="[
|
|
132
|
+
variantClass,
|
|
133
|
+
ground.background !== 'paper' ? `bg-${ground.background}` : '',
|
|
134
|
+
ground.dark ? 'deck-dark' : '',
|
|
135
|
+
]"
|
|
136
|
+
>
|
|
137
|
+
<!--
|
|
138
|
+
★★ ONE GRID CELL, THREE LAYERS, AND THE ORDER IS THE DESIGN. Jose, describing the reference:
|
|
139
|
+
"the 'thank' is back and above the bike, and the 'you' is top and below the bike". So the two
|
|
140
|
+
words are not a heading split over two lines with a picture between them — they are two planes
|
|
141
|
+
with the render BETWEEN them, and the composition only reads if the second word passes in
|
|
142
|
+
front of the wheels.
|
|
143
|
+
|
|
144
|
+
Every child takes the same `grid-area`, so they stack; `align-self` places each one in the
|
|
145
|
+
frame and `z-index` decides which passes in front. No absolute positioning: a corner-pinned
|
|
146
|
+
box would have to ask whether the chrome is there (deckNotch.static gates exactly that), and
|
|
147
|
+
none of these sits in a corner — they sit on top of each other.
|
|
148
|
+
-->
|
|
149
|
+
<div v-if="variant === 'signoff'" class="end-stage">
|
|
150
|
+
<p v-if="lead" class="end-word is-lead">{{ lead }}</p>
|
|
151
|
+
<div class="end-render">
|
|
152
|
+
<img v-if="image" :src="assetUrl(image)" :alt="alt">
|
|
153
|
+
<Placeholder v-else :label="alt || 'Closing render'" ratio="16:9" />
|
|
154
|
+
</div>
|
|
155
|
+
<p v-if="trail" class="end-word is-trail">{{ trail }}</p>
|
|
156
|
+
<!-- The slot still runs, under the stack: a closer that also wants a line of its own can
|
|
157
|
+
write one, and a slide that writes nothing draws nothing. -->
|
|
158
|
+
<div class="end-note"><slot /></div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<!--
|
|
162
|
+
The identity as the last frame, in whichever of its two forms the deck closes on. No heading
|
|
163
|
+
over it and nothing under it: the identity IS the sign-off here, and a second mark beside it
|
|
164
|
+
would be the mark twice.
|
|
165
|
+
|
|
166
|
+
★ THE VERTICAL LOCKUP RATHER THAN A MARK STACKED ON A WORDMARK BY HAND. `Logo` centres the mark
|
|
167
|
+
over the wordmark's midline and holds the canonical gap — its own note records that a
|
|
168
|
+
hand-copied gap made this form 1.7x too airy. One element, the brand's own proportions.
|
|
169
|
+
-->
|
|
170
|
+
<div v-else-if="variant === 'wordmark' || variant === 'lockup'" class="end-stage is-quiet">
|
|
171
|
+
<Logo
|
|
172
|
+
:variant="variant === 'lockup' ? 'lockup-vertical' : 'wordmark'"
|
|
173
|
+
:size="variant === 'lockup' ? END_LOCKUP_PX : END_WORDMARK_PX"
|
|
174
|
+
class="end-wordmark"
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<template v-else>
|
|
179
|
+
<!-- The photograph, edge to edge, with the scrim drawn over it by `.is-photo::after`. -->
|
|
180
|
+
<div v-if="variant === 'photo'" class="end-media">
|
|
181
|
+
<img v-if="image" :src="assetUrl(image)" :alt="alt">
|
|
182
|
+
<Placeholder v-else :label="alt || 'Closing image'" ratio="16:9" />
|
|
183
|
+
</div>
|
|
184
|
+
<div class="end-content">
|
|
185
|
+
<slot />
|
|
186
|
+
<!-- The closer signs off with the full identity, not the bare tile. -->
|
|
187
|
+
<Logo variant="lockup" class="end-logo" :size="88" />
|
|
188
|
+
</div>
|
|
189
|
+
</template>
|
|
190
|
+
</div>
|
|
191
|
+
</template>
|
|
192
|
+
|
|
193
|
+
<style scoped>
|
|
194
|
+
/* The beat margin — see cover.vue's own note. Both edges, not just the left: the left-only version
|
|
195
|
+
this replaces rendered a 120px left margin against a 90px right one on every divider and closer,
|
|
196
|
+
with nothing in any file explaining the asymmetry. */
|
|
197
|
+
.end {
|
|
198
|
+
padding-left: var(--deck-inset-display);
|
|
199
|
+
padding-right: var(--deck-inset-display);
|
|
200
|
+
}
|
|
201
|
+
/*
|
|
202
|
+
★★ THE NAMED GROUND IS PAINTED HERE, and without this rule the class is inert. `.bg-vivid` on the
|
|
203
|
+
root is only a marker: base.css keys ink, eyebrows and bullets off it but paints nothing, and
|
|
204
|
+
`.deck-dark { background: var(--deck-bg) }` then wins — so a closer that asked for the vivid pair
|
|
205
|
+
rendered on the deck's own dark ground instead. MEASURED on this slide before the rule existed:
|
|
206
|
+
forest-950 (0.052 0.162 0.142) where the cover beside it, which carries the same class AND its own
|
|
207
|
+
paint rule, drew forest-700 (0.182 0.417 0.307). cover.vue's note above its four says the same
|
|
208
|
+
thing; `end` takes two of them because a bookend takes two grounds.
|
|
209
|
+
*/
|
|
210
|
+
.end.bg-warm { background: var(--deck-bg-warm); color: var(--deck-on-bg-warm); }
|
|
211
|
+
.end.bg-accent { background: var(--deck-bg-accent); color: var(--deck-on-bg-accent); }
|
|
212
|
+
.end.bg-vivid { background: var(--deck-bg-vivid); color: var(--deck-on-bg-vivid); }
|
|
213
|
+
.end.bg-neutral { background: var(--deck-bg-neutral); color: var(--deck-on-bg-neutral); }
|
|
214
|
+
|
|
215
|
+
/* Only the flowed variants centre their content in the frame; the staged ones fill it. */
|
|
216
|
+
.end-content {
|
|
217
|
+
height: 100%;
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
justify-content: center;
|
|
221
|
+
align-items: flex-start;
|
|
222
|
+
position: relative;
|
|
223
|
+
z-index: 1;
|
|
224
|
+
}
|
|
225
|
+
/* The closer is a beat slide — Mohr uppercase, not the layout-default working voice. */
|
|
226
|
+
.end :deep(h1) {
|
|
227
|
+
font-family: var(--type-deck-display-family);
|
|
228
|
+
font-weight: var(--type-deck-display-weight);
|
|
229
|
+
text-transform: uppercase;
|
|
230
|
+
font-size: var(--type-deck-text-8xl); /* 6rem / 96px — ramp 8xl, pairs with the statement tier */
|
|
231
|
+
}
|
|
232
|
+
.end-logo {
|
|
233
|
+
margin-top: var(--deck-space-6);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* ── signoff: two words, a render between them ───────────────────────────────────────────────── */
|
|
237
|
+
/*
|
|
238
|
+
The stage bleeds past the layout's own display margin, because a word used as a FIELD is not type
|
|
239
|
+
in a measure — it is a shape the render sits in front of, and holding it to a 120px margin on both
|
|
240
|
+
sides shrank it to the size of a heading. The margin still governs everything flowed.
|
|
241
|
+
*/
|
|
242
|
+
.end-stage {
|
|
243
|
+
display: grid;
|
|
244
|
+
place-items: center;
|
|
245
|
+
height: 100%;
|
|
246
|
+
margin-inline: calc(-1 * var(--deck-inset-display));
|
|
247
|
+
}
|
|
248
|
+
.end-stage > * {
|
|
249
|
+
grid-area: 1 / 1;
|
|
250
|
+
}
|
|
251
|
+
/*
|
|
252
|
+
★★ THE WORDS ARE A TONE OF THE GROUND, AND A SOLID ONE. Two wrong answers came first. `currentColor`
|
|
253
|
+
is white on the dark register, so the wash LIGHTENED — a word lighter than its ground reads as text
|
|
254
|
+
the deck forgot to finish rather than as a field behind an object. Black at 22% darkened correctly
|
|
255
|
+
on every ground and was still an alpha wash: it composites against whatever passes behind it, so
|
|
256
|
+
the word changed value where the render's shadow crossed it. Jose: "make it solid, not opacity".
|
|
257
|
+
|
|
258
|
+
So each ground names the tone one step down its own ramp, as a token. Three states, because the
|
|
259
|
+
ground has three: the vivid pair on either register, and the deck's own paper.
|
|
260
|
+
*/
|
|
261
|
+
/* The deck's paper is warm-50, so the tone steps down its own family. 200 (#FFD6A6) was the first
|
|
262
|
+
step tried and read as a highlight rather than a tone against cream — two steps down is where the
|
|
263
|
+
word reads as a field. */
|
|
264
|
+
.end { --end-word-ink: var(--warm-300); }
|
|
265
|
+
/*
|
|
266
|
+
★★ ONE TONE PER GROUND PER REGISTER, AND EVERY ONE IS A RAMP STEP OF THAT GROUND'S OWN FAMILY.
|
|
267
|
+
Not `--deck-bg-<name>-mark`, which is the obvious reach and is wrong here: that role is the colour
|
|
268
|
+
a SMALL shape needs to clear 3:1 on the ground, so on every dark half it is a LIGHT tint —
|
|
269
|
+
forest-200 on vivid — and a word lighter than its ground is the failure this whole rule exists to
|
|
270
|
+
avoid. A field wants the quiet neighbour, not the legible one.
|
|
271
|
+
*/
|
|
272
|
+
.end.bg-vivid { --end-word-ink: var(--forest-700); }
|
|
273
|
+
.end.bg-vivid.deck-dark { --end-word-ink: var(--forest-950); }
|
|
274
|
+
/* Brown on peach, and the deeper brown on the dark half's warm-700. */
|
|
275
|
+
.end.bg-warm { --end-word-ink: var(--warm-700); }
|
|
276
|
+
.end.bg-warm.deck-dark { --end-word-ink: var(--warm-950); }
|
|
277
|
+
/* The mint pair: a green step down from forest-100, and on its forest-950 half the same lift the
|
|
278
|
+
deck's own dark ground takes, for the same reason — there is nothing below 950. */
|
|
279
|
+
.end.bg-accent { --end-word-ink: var(--forest-300); }
|
|
280
|
+
.end.bg-accent.deck-dark { --end-word-ink: var(--forest-900); }
|
|
281
|
+
/* Two steps down carries on every other pair and not on this one — the grey ramp has no chroma to
|
|
282
|
+
separate 300 from 100, so the word read as a printing artefact rather than a tone. 400. */
|
|
283
|
+
.end.bg-neutral { --end-word-ink: var(--neutral-400); }
|
|
284
|
+
.end.bg-neutral.deck-dark { --end-word-ink: var(--neutral-700); }
|
|
285
|
+
/* ⚠ ON THE DECK'S OWN DARK GROUND THE TONE GOES UP, NOT DOWN. That ground IS forest-950, the bottom
|
|
286
|
+
of the ramp, so there is nothing darker to step to — the tint lifts to 900 instead. A tone of the
|
|
287
|
+
ground is what this has to be; which direction it lies in is the ground's business, not the
|
|
288
|
+
word's. */
|
|
289
|
+
.end.deck-dark:not(.bg-vivid) { --end-word-ink: var(--forest-900); }
|
|
290
|
+
.end-word {
|
|
291
|
+
margin: 0;
|
|
292
|
+
font-family: var(--type-deck-headline-family);
|
|
293
|
+
font-weight: var(--type-deck-headline-weight);
|
|
294
|
+
/*
|
|
295
|
+
⚠ OFF THE RAMP ON PURPOSE, and this is the one place in the theme that is true. The deck ramp
|
|
296
|
+
tops out at --type-deck-text-9xl (128px), which is the biggest a line of READING is allowed to
|
|
297
|
+
be in a room. These two words are not read as a line — they are the composition, and at 128 they
|
|
298
|
+
sat under the render like a caption. 320px is the height that makes the render's 66% and the
|
|
299
|
+
word's cap height the same order of size. Drawn at 320 first: the word came out 901px across
|
|
300
|
+
against a 1190px render, which read as a picture with type behind it rather than as one object.
|
|
301
|
+
*/
|
|
302
|
+
font-size: 360px;
|
|
303
|
+
line-height: 0.82;
|
|
304
|
+
letter-spacing: -0.02em;
|
|
305
|
+
color: var(--end-word-ink);
|
|
306
|
+
text-transform: none;
|
|
307
|
+
white-space: nowrap;
|
|
308
|
+
}
|
|
309
|
+
/* Above and behind; below and in front. The two z-indexes ARE the brief. */
|
|
310
|
+
.end-word.is-lead {
|
|
311
|
+
align-self: start;
|
|
312
|
+
z-index: 0;
|
|
313
|
+
}
|
|
314
|
+
.end-word.is-trail {
|
|
315
|
+
align-self: end;
|
|
316
|
+
z-index: 2;
|
|
317
|
+
}
|
|
318
|
+
.end-render {
|
|
319
|
+
display: flex;
|
|
320
|
+
align-items: center;
|
|
321
|
+
justify-content: center;
|
|
322
|
+
z-index: 1;
|
|
323
|
+
width: 100%;
|
|
324
|
+
}
|
|
325
|
+
.end-render :deep(img) {
|
|
326
|
+
width: v-bind(RENDER_WIDTH);
|
|
327
|
+
max-height: v-bind(RENDER_HEIGHT);
|
|
328
|
+
object-fit: contain;
|
|
329
|
+
display: block;
|
|
330
|
+
}
|
|
331
|
+
.end-render :deep(.deck-placeholder) {
|
|
332
|
+
width: v-bind(RENDER_WIDTH);
|
|
333
|
+
}
|
|
334
|
+
/* A closing line under a stage is a footnote to it: small, quiet, and out of the composition's way. */
|
|
335
|
+
.end-note {
|
|
336
|
+
align-self: end;
|
|
337
|
+
justify-self: start;
|
|
338
|
+
z-index: 3;
|
|
339
|
+
margin-inline-start: var(--deck-inset-display);
|
|
340
|
+
}
|
|
341
|
+
.end-note :deep(h1) {
|
|
342
|
+
font-size: var(--type-deck-text-5xl);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* ── wordmark: the identity alone ────────────────────────────────────────────────────────────── */
|
|
346
|
+
.end-stage.is-quiet {
|
|
347
|
+
grid-auto-flow: row;
|
|
348
|
+
place-content: center;
|
|
349
|
+
gap: var(--deck-space-5);
|
|
350
|
+
}
|
|
351
|
+
.end-stage.is-quiet > * {
|
|
352
|
+
grid-area: auto;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* ── photo: the photograph, and the words over it ────────────────────────────────────────────── */
|
|
356
|
+
.end-media {
|
|
357
|
+
position: absolute;
|
|
358
|
+
inset: 0;
|
|
359
|
+
overflow: hidden;
|
|
360
|
+
z-index: 0;
|
|
361
|
+
}
|
|
362
|
+
.end-media :deep(img) {
|
|
363
|
+
width: 100%;
|
|
364
|
+
height: 100%;
|
|
365
|
+
object-fit: cover;
|
|
366
|
+
display: block;
|
|
367
|
+
}
|
|
368
|
+
.end-media :deep(.deck-placeholder) {
|
|
369
|
+
width: 100%;
|
|
370
|
+
height: 100%;
|
|
371
|
+
}
|
|
372
|
+
/*
|
|
373
|
+
The scrim, from the side the words are on. Black rather than the ground's own ink for the reason
|
|
374
|
+
bleed.vue's full scrim is: a green wash over a photograph tints the photograph, and what this has
|
|
375
|
+
to do is make white type legible over whatever the picture happens to be doing.
|
|
376
|
+
*/
|
|
377
|
+
.end.is-photo::after {
|
|
378
|
+
content: '';
|
|
379
|
+
position: absolute;
|
|
380
|
+
inset: 0;
|
|
381
|
+
background: linear-gradient(to right, rgb(0 0 0 / 74%), rgb(0 0 0 / 52%) 42%, transparent 78%);
|
|
382
|
+
z-index: 0;
|
|
383
|
+
}
|
|
384
|
+
</style>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { assetUrl } from '../assetUrl.js';
|
|
3
|
+
/*
|
|
4
|
+
A pull quote. The one slide where the sans face gets to be large — the display face is the
|
|
5
|
+
brand SHOUTING, and a quote is somebody else speaking, so it stays in GT Haptik.
|
|
6
|
+
|
|
7
|
+
The quotation mark is drawn as a decorative glyph in the accent, set in the display face and
|
|
8
|
+
hung outside the text block, which is the system's editorial treatment for a lifted quote.
|
|
9
|
+
|
|
10
|
+
★ THE COMPANION IMAGE IS A HALF, NOT A SPLIT. `image` puts a portrait beside the words — the one
|
|
11
|
+
thing a quote wants a picture of is the person saying it — and `flip` says which wall the WORDS take, the picture taking the other — the same reading `split` and `bleed` give it.
|
|
12
|
+
Deliberately ONE arrangement and a fixed half, rather than split.vue's ratio ladder: a quote is
|
|
13
|
+
already capped at 17em of measure, so a wider content column buys nothing and a narrower one
|
|
14
|
+
breaks the line-length the layout exists to protect. An author who wants a ratio wants `split`.
|
|
15
|
+
|
|
16
|
+
The figure carries split's own treatment — filled, corner-clipped, cover-fitted — but NO notch,
|
|
17
|
+
and that is measured rather than assumed. A quote keeps the display inset at the sides and the
|
|
18
|
+
text inset top and bottom, which puts the figure at 1020..1800 x 180..900 against a mark sitting
|
|
19
|
+
at 1812..1884 x 36..108: clear on BOTH axes, by 12px horizontally and 72 vertically. The cut was
|
|
20
|
+
wired here first and could never fire, so it came back out. A figure that reached the chrome would
|
|
21
|
+
need it — see split.vue — but this one cannot.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
defineProps({
|
|
25
|
+
/** Who said it. */
|
|
26
|
+
attribution: { type: String, default: '' },
|
|
27
|
+
/** Their role, company, or the source. */
|
|
28
|
+
source: { type: String, default: '' },
|
|
29
|
+
/** A companion image — a portrait of whoever is speaking. Omit for a quote that owns the whole slide. */
|
|
30
|
+
image: { type: String, default: '' },
|
|
31
|
+
/** Assistive text for that image. */
|
|
32
|
+
alt: { type: String, default: '' },
|
|
33
|
+
/**
|
|
34
|
+
* Which physical side the CONTENT sits on; the image takes the other. Ignored without an image.
|
|
35
|
+
*
|
|
36
|
+
* ★★ THIS USED TO MEAN THE OPPOSITE, and it was the only layout where it did. `split` and `bleed`
|
|
37
|
+
* both read `flip` as the CONTENT's side; quote read it as the IMAGE's, so `flip: false` put words
|
|
38
|
+
* on the left in two layouts and the photograph on the left in the third. One prop name, two
|
|
39
|
+
* meanings, and nothing in the type system to say which one a slide was getting.
|
|
40
|
+
*
|
|
41
|
+
* The default moved with the meaning — 'right' (image right) became 'left' (content left), which
|
|
42
|
+
* is the SAME rendering — so a quote that does not state a side is unaffected. A quote that does
|
|
43
|
+
* state one now gets the mirror of what it used to, which is the point: it is the layout that was
|
|
44
|
+
* lying, not the slides.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* Mirror the layout: by default the WORDS take the left wall and the picture the right; flipped, they swap.
|
|
48
|
+
* Ignored without an image.
|
|
49
|
+
*
|
|
50
|
+
* ★★ A BOOLEAN, AND SHARED WITH `stack` — which is the only shape that gives the two layouts one
|
|
51
|
+
* key with one DEFAULT. They invert deliberately (split leads with the title, stack subordinates
|
|
52
|
+
* it), so any value that names a POSITION comes out opposite between them: content-first on one
|
|
53
|
+
* is content-last on the other. `false` means "this layout's own arrangement" on both, and that
|
|
54
|
+
* is the only thing they genuinely share — there is no word for "figure right" and "band top" at
|
|
55
|
+
* once. It replaced `flip`, which had no third value in prospect either.
|
|
56
|
+
*
|
|
57
|
+
* ⚠ IT IS RELATIVE, WHICH IS THE TRADE. `flip: true` said where the content went; `flip` says
|
|
58
|
+
* "the other way from the default", so reading a slide needs the default in hand — and reversing
|
|
59
|
+
* a layout's default would silently change every flipped slide. The defaults are doctrine and do
|
|
60
|
+
* not move, which is what makes that acceptable.
|
|
61
|
+
*/
|
|
62
|
+
flip: { type: Boolean, default: false },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<template>
|
|
68
|
+
<div
|
|
69
|
+
class="slidev-layout quote h-full"
|
|
70
|
+
:class="[image ? 'has-figure' : 'is-plain', flip ? 'is-right' : 'is-left']"
|
|
71
|
+
>
|
|
72
|
+
<div class="quote-content">
|
|
73
|
+
<div class="quote-mark" aria-hidden="true">“</div>
|
|
74
|
+
<div class="quote-body">
|
|
75
|
+
<slot />
|
|
76
|
+
</div>
|
|
77
|
+
<div v-if="attribution || source" class="quote-attribution">
|
|
78
|
+
<span v-if="attribution" class="quote-who">{{ attribution }}</span>
|
|
79
|
+
<span v-if="source" class="quote-source">{{ source }}</span>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div v-if="image" class="quote-figure">
|
|
83
|
+
<img :src="assetUrl(image)" :alt="alt">
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</template>
|
|
87
|
+
|
|
88
|
+
<style scoped>
|
|
89
|
+
/* The beat margin — see cover.vue's own note. Both edges, not just the left: the left-only version
|
|
90
|
+
this replaces rendered a 120px left margin against a 90px right one on every divider and closer,
|
|
91
|
+
with nothing in any file explaining the asymmetry. */
|
|
92
|
+
.quote {
|
|
93
|
+
padding-left: var(--deck-inset-display);
|
|
94
|
+
padding-right: var(--deck-inset-display);
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
}
|
|
98
|
+
/*
|
|
99
|
+
★ TWO THIRDS FOR THE WORDS, ONE THIRD LEFT EMPTY. Without an image the quote is the only thing on
|
|
100
|
+
the slide, and letting it run the full column made a wall of 7xl type with nothing to sit against.
|
|
101
|
+
The blank third is not slack — it is what makes a lone quote read as placed rather than as
|
|
102
|
+
overflowing, and it is the same restraint the beat layouts get from their display inset.
|
|
103
|
+
|
|
104
|
+
A fraction of the CONTENT box, so it holds whatever the display inset is: 2/3 of 1680 = 1120,
|
|
105
|
+
with 560 left over. The paragraph's own measure cap comes off here — two rules capping one column
|
|
106
|
+
is how a block ends up narrower than either intended, and the column is now the measure.
|
|
107
|
+
*/
|
|
108
|
+
.quote.is-plain .quote-content { flex: 0 0 66.6667%; max-width: 66.6667%; }
|
|
109
|
+
.quote.is-plain .quote-body { width: auto; }
|
|
110
|
+
/*
|
|
111
|
+
★ `pretty`, NOT `balance`, once the column is the measure. `balance` evens the lines out, which is
|
|
112
|
+
right for a heading and wrong here: given a 1120px column it returned four short lines about half
|
|
113
|
+
that wide, so the slide read as a third of quote and two thirds of blank — the opposite of what
|
|
114
|
+
the 2/3 rule was for. `pretty` fills the measure and only fixes the last-line orphan.
|
|
115
|
+
|
|
116
|
+
Kept as `balance` beside a figure, where the column is half a slide and short lines are the point.
|
|
117
|
+
*/
|
|
118
|
+
.quote.is-plain .quote-body :deep(p) { max-width: none; text-wrap: pretty; }
|
|
119
|
+
.quote-content {
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
justify-content: center;
|
|
123
|
+
}
|
|
124
|
+
/*
|
|
125
|
+
Two halves, and the ORDER is what `flip` moves — not a `direction` or a float. The figure is last
|
|
126
|
+
in the DOM so a screen reader hears the quote before the portrait of whoever said it, and `order`
|
|
127
|
+
puts it on the left visually without disturbing that.
|
|
128
|
+
*/
|
|
129
|
+
.quote.has-figure {
|
|
130
|
+
gap: var(--deck-inset-display);
|
|
131
|
+
}
|
|
132
|
+
.quote.has-figure .quote-content { flex: 1 1 0; min-width: 0; }
|
|
133
|
+
.quote.has-figure .quote-figure { flex: 1 1 0; min-width: 0; }
|
|
134
|
+
/* Content on the right puts the figure first. `order` rather than `direction` or a float, so
|
|
135
|
+
the DOM keeps the quote before the portrait of whoever said it for a screen reader. */
|
|
136
|
+
.quote.has-figure.is-right .quote-figure { order: -1; }
|
|
137
|
+
|
|
138
|
+
/* The figure keeps split's own treatment, so a portrait here and a portrait there are the same
|
|
139
|
+
picture: filled, corner-clipped, and cover-fitted rather than stretched. */
|
|
140
|
+
.quote-figure {
|
|
141
|
+
align-self: stretch;
|
|
142
|
+
margin-top: var(--deck-inset-text);
|
|
143
|
+
margin-bottom: var(--deck-inset-text);
|
|
144
|
+
border-radius: var(--deck-radius-card);
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
display: flex;
|
|
147
|
+
}
|
|
148
|
+
.quote-figure img {
|
|
149
|
+
width: 100%;
|
|
150
|
+
height: 100%;
|
|
151
|
+
object-fit: cover;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.quote-mark {
|
|
155
|
+
font-family: var(--font-display);
|
|
156
|
+
font-size: var(--type-deck-text-9xl);
|
|
157
|
+
line-height: 0.6;
|
|
158
|
+
color: var(--deck-accent);
|
|
159
|
+
margin-bottom: var(--deck-space-2);
|
|
160
|
+
}
|
|
161
|
+
/*
|
|
162
|
+
★ THE BOX HUGS THE MEASURE. The cap lives on the paragraph — it has to, because `em` on this div
|
|
163
|
+
would resolve against its inherited body size rather than the quote's own 7xl — which left the
|
|
164
|
+
DIV spanning the full column while the words stopped at 17em. Harmless to look at, wrong to
|
|
165
|
+
inspect: every box in this deck is something a ruler or an audit can be pointed at, and one
|
|
166
|
+
claiming width it never uses reports a measure the slide does not have.
|
|
167
|
+
|
|
168
|
+
`fit-content` is min(max-content, available), which IS the paragraph's cap once the cap is set,
|
|
169
|
+
so the two cannot disagree.
|
|
170
|
+
*/
|
|
171
|
+
.quote-body {
|
|
172
|
+
width: fit-content;
|
|
173
|
+
max-width: 100%;
|
|
174
|
+
}
|
|
175
|
+
.quote-body :deep(p) {
|
|
176
|
+
font-family: var(--font-sans);
|
|
177
|
+
font-size: var(--type-deck-text-7xl);
|
|
178
|
+
line-height: 1.2;
|
|
179
|
+
font-weight: 400;
|
|
180
|
+
color: var(--deck-fg);
|
|
181
|
+
max-width: 17em; /* ≈34 characters of GT Haptik */
|
|
182
|
+
text-wrap: balance;
|
|
183
|
+
}
|
|
184
|
+
/* ★ Beside a figure the column IS the measure, so the 17em cap would apply a second time and hold
|
|
185
|
+
the quote to about half its own half. The cap above is a beat-layout cap — display type being
|
|
186
|
+
shaped into a two- or three-line block — which is why it survives base.css uncapping running
|
|
187
|
+
prose, and why it still has to be switched off where a column has already chosen the width. */
|
|
188
|
+
/*
|
|
189
|
+
★★ ONE STEP DOWN, NOT TWO, AND THE STEP IS THE COLUMN'S. A figure takes a third of the slide, so
|
|
190
|
+
the words get 780px where a plain quote gets 1120 — 70% of the measure. The size dropped from 7xl
|
|
191
|
+
to 5xl, which is 48px against 72: 67% of the type for 70% of the width, compounding a reduction
|
|
192
|
+
the column had already made. Measured side by side, the same voice read two sizes apart depending
|
|
193
|
+
on whether somebody was pictured saying it, which is not a distinction this layout means to draw.
|
|
194
|
+
|
|
195
|
+
6xl (60px) is the step the column actually asks for. The quote is still the loudest thing on the
|
|
196
|
+
slide and still sets in three lines; what goes away is the impression that a pictured quote is a
|
|
197
|
+
quieter one.
|
|
198
|
+
*/
|
|
199
|
+
.quote.has-figure .quote-body :deep(p) {
|
|
200
|
+
max-width: none;
|
|
201
|
+
font-size: var(--type-deck-text-6xl);
|
|
202
|
+
}
|
|
203
|
+
.quote-attribution {
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: baseline;
|
|
206
|
+
gap: var(--deck-space-2);
|
|
207
|
+
margin-top: var(--deck-space-5);
|
|
208
|
+
}
|
|
209
|
+
.quote-who {
|
|
210
|
+
font-size: var(--type-deck-label-size);
|
|
211
|
+
font-weight: var(--type-deck-label-weight);
|
|
212
|
+
color: var(--deck-fg);
|
|
213
|
+
}
|
|
214
|
+
.quote-source {
|
|
215
|
+
font-size: var(--type-deck-caption-size);
|
|
216
|
+
color: var(--deck-fg-muted);
|
|
217
|
+
}
|
|
218
|
+
</style>
|