@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,266 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/*
|
|
3
|
+
One card at a time, advanced by the presenter's own clicks, with the room always told which of how
|
|
4
|
+
many it is looking at.
|
|
5
|
+
|
|
6
|
+
★ WHY THIS IS NOT JUST `v-switch`, which Slidev already ships and which does the swapping part.
|
|
7
|
+
Two things are missing from it for a deck read across a room, and both are the whole reason a
|
|
8
|
+
presenter reaches for a carousel instead of N slides:
|
|
9
|
+
|
|
10
|
+
· THE POSITION INDICATOR. `v-switch` shows item 3 with nothing on screen saying it is the third
|
|
11
|
+
of five. On a slide the page number does that job; inside a slide nothing does, and an
|
|
12
|
+
audience that cannot see how many are left stops tracking the argument and starts waiting.
|
|
13
|
+
· A FIXED BOX. `v-switch` sizes to whatever is currently in it, so the surrounding layout jumps
|
|
14
|
+
every time the presenter clicks — headings shift, the card grows, and the room's eye is
|
|
15
|
+
dragged back to the top of the slide. Here every item is laid out in the same grid cell and
|
|
16
|
+
the box is the size of the LARGEST, so only the content changes.
|
|
17
|
+
|
|
18
|
+
★★ AND WHY IT IS NOT AUTOMATIC. A web carousel advances on a timer. A presenting one must not:
|
|
19
|
+
the presenter is talking to item 2 and the deck must not move to item 3 until they say so. So this
|
|
20
|
+
spends CLICKS, exactly as `v-click` does, which also means the presenter's notes, the progress
|
|
21
|
+
bar, the slide's click count and the export all understand it for free.
|
|
22
|
+
|
|
23
|
+
The click plumbing mirrors @slidev/client's own VSwitch.ts — `calculateSince` to reserve a range,
|
|
24
|
+
`register` so the slide knows how many clicks it owns, `currentOffset` to read the position. It is
|
|
25
|
+
the documented shape; the only thing not copied is `makeId`, which is internal, and a module
|
|
26
|
+
counter is what it is anyway.
|
|
27
|
+
*/
|
|
28
|
+
import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue';
|
|
29
|
+
import { useSlideContext } from '@slidev/client';
|
|
30
|
+
import { assetUrl } from '../assetUrl.js';
|
|
31
|
+
|
|
32
|
+
const props = defineProps({
|
|
33
|
+
/**
|
|
34
|
+
* The cards, in order. Each is `{ title, body?, image? }`.
|
|
35
|
+
*
|
|
36
|
+
* `image` is a URL, drawn full-bleed above the title at a fixed 16:9 — every card shares one grid
|
|
37
|
+
* cell and therefore one width, so the ratio resolves to the same height for all of them and no
|
|
38
|
+
* card's crop can move the stage. The picture is decorative here; the card's own title carries the
|
|
39
|
+
* meaning, so it takes an empty alt rather than a duplicated one.
|
|
40
|
+
*/
|
|
41
|
+
items: {
|
|
42
|
+
type: Array,
|
|
43
|
+
required: true,
|
|
44
|
+
validator: (v) =>
|
|
45
|
+
Array.isArray(v) &&
|
|
46
|
+
v.length > 0 &&
|
|
47
|
+
v.every((i) => i && typeof i.title === 'string' && (i.image === undefined || typeof i.image === 'string')),
|
|
48
|
+
},
|
|
49
|
+
/** Where the first card lands in the slide's click sequence — Slidev's own `at` vocabulary
|
|
50
|
+
* ('+1' means "after whatever came before me"). */
|
|
51
|
+
at: { type: [Number, String], default: '+1' },
|
|
52
|
+
/**
|
|
53
|
+
* Maximum width of the device, in px. 0 (the default) lets it fill its column.
|
|
54
|
+
*
|
|
55
|
+
* A carousel shows ONE card at a time, so at full slide width a card with three lines of text is
|
|
56
|
+
* a very wide, very short box — the reading line runs past the measure and the picture above it
|
|
57
|
+
* has to be enormous to keep the proportion. Capping the width is what makes it read as a card
|
|
58
|
+
* rather than as a band. It stays left-aligned: the slide's own `align` decides where content
|
|
59
|
+
* sits, and a component that centred itself would fight it.
|
|
60
|
+
*/
|
|
61
|
+
width: { type: Number, default: 0 },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let nextId = 0;
|
|
65
|
+
const id = `deck-carousel-${nextId++}`;
|
|
66
|
+
const offset = ref(0);
|
|
67
|
+
const { $clicksContext: clicks } = useSlideContext();
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
A carousel of n items spends n-1 clicks: the first card is already showing when the slide arrives,
|
|
71
|
+
so only the MOVES between cards cost anything. Getting this wrong by one is not a visual bug — it
|
|
72
|
+
silently changes the slide's total click count, and the presenter's "next" at the end either
|
|
73
|
+
sticks or skips to the following slide a beat early.
|
|
74
|
+
*/
|
|
75
|
+
const steps = computed(() => Math.max(0, props.items.length - 1));
|
|
76
|
+
|
|
77
|
+
onMounted(() => {
|
|
78
|
+
const info = clicks?.calculateSince(props.at, steps.value);
|
|
79
|
+
// No clicks context, or none left to reserve — an overview thumbnail or a print pass. Showing the
|
|
80
|
+
// first card is the right resting state, and it is the same one reduced motion lands on.
|
|
81
|
+
if (!info) return;
|
|
82
|
+
clicks.register(id, info);
|
|
83
|
+
// ★ `currentOffset` is a REF, and reading it inside watchEffect is what makes this track. It
|
|
84
|
+
// counts from -1 before this component's first click, so +1 lands index 0 on arrival; clamped
|
|
85
|
+
// because the slide's offset keeps climbing once later components spend their own clicks.
|
|
86
|
+
watchEffect(() => {
|
|
87
|
+
offset.value = Math.min(steps.value, Math.max(0, info.currentOffset.value + 1));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
onUnmounted(() => {
|
|
92
|
+
clicks?.unregister(id);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const current = computed(() => Math.min(props.items.length - 1, Math.max(0, offset.value)));
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<template>
|
|
99
|
+
<div class="deck-carousel" :style="width ? { maxWidth: `${width}px` } : undefined">
|
|
100
|
+
<div class="deck-carousel-stage">
|
|
101
|
+
<!-- Every item is drawn in the SAME grid cell, so the stage is the height of the tallest and
|
|
102
|
+
nothing below it moves when the presenter clicks. `visibility` rather than `v-if`: a
|
|
103
|
+
removed item takes its height with it, which is the jump this exists to stop.
|
|
104
|
+
|
|
105
|
+
`--carousel-slot` is the card's position RELATIVE to the current one — -1 is the card
|
|
106
|
+
just left, +1 the card just right — which is what lets the stylesheet lay the whole reel
|
|
107
|
+
out on one axis without this component tracking a direction. Advancing raises `current`,
|
|
108
|
+
every slot drops by one, and the reel moves left because the arithmetic says so. -->
|
|
109
|
+
<div
|
|
110
|
+
v-for="(item, i) in items"
|
|
111
|
+
:key="i"
|
|
112
|
+
class="deck-carousel-item"
|
|
113
|
+
:class="{ 'is-current': i === current }"
|
|
114
|
+
:aria-hidden="i !== current"
|
|
115
|
+
:style="{ '--carousel-slot': i - current }"
|
|
116
|
+
>
|
|
117
|
+
<img v-if="item.image" class="deck-carousel-image" :src="assetUrl(item.image)" alt="" decoding="async" >
|
|
118
|
+
<p class="deck-carousel-title">{{ item.title }}</p>
|
|
119
|
+
<p v-if="item.body" class="deck-carousel-body">{{ item.body }}</p>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<!-- The indicator. Aria-hidden because it restates the position the cards already carry, and a
|
|
123
|
+
screen reader reading "1 of 5" as five bullet characters helps nobody. -->
|
|
124
|
+
<div class="deck-carousel-dots" aria-hidden="true">
|
|
125
|
+
<span
|
|
126
|
+
v-for="(item, i) in items"
|
|
127
|
+
:key="i"
|
|
128
|
+
class="deck-carousel-dot"
|
|
129
|
+
:class="{ 'is-current': i === current }"
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|
|
134
|
+
|
|
135
|
+
<style scoped>
|
|
136
|
+
.deck-carousel {
|
|
137
|
+
display: grid;
|
|
138
|
+
gap: var(--deck-space-4);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.deck-carousel-stage {
|
|
142
|
+
display: grid;
|
|
143
|
+
/* The reel is wider than the window onto it: cards sit a full width apart on either side and are
|
|
144
|
+
clipped until it is their turn. Without this a card in transit is seen sliding across whatever
|
|
145
|
+
the slide has beside the carousel. */
|
|
146
|
+
overflow: hidden;
|
|
147
|
+
}
|
|
148
|
+
/* ── HOW IT MOVES ──────────────────────────────────────────────────────────────────────────────
|
|
149
|
+
Cards are laid on one horizontal reel and the reel slides. Card `n` sits `n - current` widths to
|
|
150
|
+
the side, so the current one is at 0 and its neighbours are parked a full width plus a gap out,
|
|
151
|
+
where the stage clips them. Nothing here knows which way the presenter went: advancing raises
|
|
152
|
+
`current`, every slot drops by one, and the whole reel translates left.
|
|
153
|
+
|
|
154
|
+
★★ THIS USED TO CROSS-FADE, and the comment defending that said a positional animation "would be
|
|
155
|
+
stripped" by the reduced-motion floor. Stripped is exactly what should happen — and it is not
|
|
156
|
+
breakage. The floor rewrites `transition-property` to a colour/opacity allow-list, so under
|
|
157
|
+
reduced motion `transform` is not transitioned and the card simply ARRIVES at its slot. Same
|
|
158
|
+
resting frame, no movement. The old reasoning conflated two different things: the STAGE must
|
|
159
|
+
never move, which is what the shared grid cell guarantees, with the CARDS never moving, which is
|
|
160
|
+
the entire device.
|
|
161
|
+
|
|
162
|
+
★★ VISIBILITY IS DELAYED OUT AND IMMEDIATE IN, and that pair is load-bearing twice over. It is
|
|
163
|
+
what lets a leaving card stay on screen for the length of its own exit instead of vanishing on
|
|
164
|
+
the first frame; and it is what keeps the RESTING state exactly what it was before — every
|
|
165
|
+
non-current card `visibility: hidden` once the motion ends. That second half matters beyond
|
|
166
|
+
tidiness: scripts/deckAudit.ts measures a settled page and fails text that is painted but not
|
|
167
|
+
visible, so cards parked off-stage and left visible would read as a real defect on every carousel
|
|
168
|
+
slide. `0s linear var(--motion-base)` is a DELAY, not a duration — visibility cannot tween, it
|
|
169
|
+
flips, and the delay is what holds the flip until the slide is over. */
|
|
170
|
+
.deck-carousel-item {
|
|
171
|
+
/* All items stacked in one cell — the stage takes the height of the tallest. */
|
|
172
|
+
grid-area: 1 / 1;
|
|
173
|
+
background: var(--deck-surface);
|
|
174
|
+
border: 1px solid var(--deck-border);
|
|
175
|
+
border-radius: var(--deck-radius-card);
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
padding: var(--deck-card-pad);
|
|
178
|
+
visibility: hidden;
|
|
179
|
+
opacity: 0;
|
|
180
|
+
transform: translateX(calc(var(--carousel-slot, 0) * (100% + var(--deck-space-4))));
|
|
181
|
+
transition:
|
|
182
|
+
transform var(--motion-base) var(--motion-ease-out),
|
|
183
|
+
opacity var(--motion-base) var(--motion-ease-out),
|
|
184
|
+
visibility 0s linear var(--motion-base);
|
|
185
|
+
}
|
|
186
|
+
.deck-carousel-item.is-current {
|
|
187
|
+
visibility: visible;
|
|
188
|
+
opacity: 1;
|
|
189
|
+
/* No delay on the way IN — the arriving card has to be visible for the whole of its travel. */
|
|
190
|
+
transition:
|
|
191
|
+
transform var(--motion-base) var(--motion-ease-out),
|
|
192
|
+
opacity var(--motion-base) var(--motion-ease-out),
|
|
193
|
+
visibility 0s;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* AN ASPECT RATIO, and it is safe here for a reason worth stating: every card is laid out in the
|
|
197
|
+
SAME grid cell, so every card is the same width, so a ratio resolves to the same height for all
|
|
198
|
+
of them. The stage cannot be made to jump by one card's crop — which is the objection that would
|
|
199
|
+
otherwise force a fixed pixel height, and it does not apply. A ratio is the better answer because
|
|
200
|
+
it keeps its proportion when `width` changes; a pinned height would go letterbox on a narrow card
|
|
201
|
+
and postage-stamp on a wide one.
|
|
202
|
+
|
|
203
|
+
FULL BLEED to the card's edges: the picture is cancelled out of the card's own padding with a
|
|
204
|
+
negative margin on three sides, and takes the card's radius on the two corners it now owns. The
|
|
205
|
+
card clips (`overflow: hidden` on the item), or the image's square bottom corners would paint
|
|
206
|
+
over the rounded ones underneath. */
|
|
207
|
+
.deck-carousel-image {
|
|
208
|
+
display: block;
|
|
209
|
+
width: calc(100% + var(--deck-card-pad) * 2);
|
|
210
|
+
/* ★★ WITHOUT THIS THE BLEED SILENTLY DOES NOT HAPPEN. Slidev's preflight ships
|
|
211
|
+
`img, video { max-width: 100% }`, which clamps the width above straight back to the card's
|
|
212
|
+
CONTENT box — the computed width came out as exactly the content width, so the picture sat
|
|
213
|
+
flush on the left (where the negative margin still applied) and short by 2x the padding on the
|
|
214
|
+
right. Nothing errors; it just looks like a mistake. */
|
|
215
|
+
max-width: none;
|
|
216
|
+
aspect-ratio: 16 / 9;
|
|
217
|
+
object-fit: cover;
|
|
218
|
+
margin: calc(-1 * var(--deck-card-pad)) calc(-1 * var(--deck-card-pad)) var(--deck-card-pad);
|
|
219
|
+
border-radius: var(--deck-radius-card) var(--deck-radius-card) 0 0;
|
|
220
|
+
/* The card's own fill shows through while a remote image is still in flight, rather than a
|
|
221
|
+
transparent gap that reads as a broken card in the one second that matters most. */
|
|
222
|
+
background: var(--deck-surface-strong);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.deck-carousel-title {
|
|
226
|
+
font-family: var(--type-deck-title-family);
|
|
227
|
+
font-size: var(--type-deck-title-size);
|
|
228
|
+
font-weight: var(--type-deck-title-weight);
|
|
229
|
+
line-height: var(--type-deck-title-line-height);
|
|
230
|
+
color: var(--deck-fg);
|
|
231
|
+
margin: 0;
|
|
232
|
+
max-width: none;
|
|
233
|
+
}
|
|
234
|
+
.deck-carousel-body {
|
|
235
|
+
font-size: var(--type-deck-body-sm-size);
|
|
236
|
+
line-height: var(--type-deck-body-sm-line-height);
|
|
237
|
+
color: var(--deck-fg-muted);
|
|
238
|
+
margin: var(--deck-space-2) 0 0 0;
|
|
239
|
+
max-width: none;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.deck-carousel-dots {
|
|
243
|
+
display: flex;
|
|
244
|
+
gap: var(--deck-space-2);
|
|
245
|
+
justify-content: center;
|
|
246
|
+
}
|
|
247
|
+
/* A bar rather than a circle, and the current one is WIDER rather than merely darker. Read across a
|
|
248
|
+
room a filled circle and an outlined one of the same size are one shape at two weights; a
|
|
249
|
+
different LENGTH survives the distance. */
|
|
250
|
+
.deck-carousel-dot {
|
|
251
|
+
width: var(--deck-space-4);
|
|
252
|
+
height: var(--deck-space-1);
|
|
253
|
+
border-radius: var(--deck-radius-full);
|
|
254
|
+
background: var(--deck-border-strong);
|
|
255
|
+
/* `width` travels with the reel rather than snapping while the card is still moving. It is
|
|
256
|
+
outside the reduced-motion allow-list, like `transform`, so it lands instantly there — the
|
|
257
|
+
indicator and the card agree in both registers. */
|
|
258
|
+
transition:
|
|
259
|
+
background-color var(--motion-base) var(--motion-ease-out),
|
|
260
|
+
width var(--motion-base) var(--motion-ease-out);
|
|
261
|
+
}
|
|
262
|
+
.deck-carousel-dot.is-current {
|
|
263
|
+
width: var(--deck-space-7);
|
|
264
|
+
background: var(--deck-accent);
|
|
265
|
+
}
|
|
266
|
+
</style>
|