@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,1875 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/*
|
|
3
|
+
The workhorse. Content on one side, a figure on the other — five of the reference deck's eleven
|
|
4
|
+
archetypes are this layout with different slot content.
|
|
5
|
+
|
|
6
|
+
★★ THE FIGURE IS `image` (A URL) + `alt` (THE TEXT), AND `image` IS SLIDEV'S OWN KEY.
|
|
7
|
+
|
|
8
|
+
It is not `src`: that is in Slidev's FRONTMATTER_FIELDS and would be stripped before frontmatter
|
|
9
|
+
ever became a prop. `image` was briefly used for the ALT TEXT, which broke on Slidev 52 — its
|
|
10
|
+
extractImagesUsage() harvests frontmatter `image` as a URL and emits a <link rel=preload
|
|
11
|
+
as=image>, so all ten of the specimen deck's alt strings became preloads for paths like
|
|
12
|
+
`/A woman cycling on a city road in daylight`.
|
|
13
|
+
|
|
14
|
+
The fix was to use the key the way Slidev does, not to dodge it: `image` holds the URL, this
|
|
15
|
+
layout renders the <img>, and the preload is then correct rather than a collision. `image` is
|
|
16
|
+
therefore NOT on test/props.ts's reserved list — it is the name to use, not one to avoid.
|
|
17
|
+
|
|
18
|
+
★ ONE image prop is the ceiling, and it is Slidev's, not a preference. extractImagesUsage()
|
|
19
|
+
(@slidev/parser core.mjs) iterates the literal list ["image", "backgroundImage", "background"]
|
|
20
|
+
and guards on `typeof val === 'string'`. MEASURED against the installed parser: a second key
|
|
21
|
+
(`image2`) is preloaded 0 times, and an `images: [a, b]` ARRAY fails the typeof guard and
|
|
22
|
+
preloads NEITHER — it silently costs the first image its preload too. A slide that needs a second
|
|
23
|
+
photograph writes markdown `` in the slot, which IS harvested (unconditionally, so a
|
|
24
|
+
query string survives) and does preload.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// Vue strips prop validators from a production build, so an out-of-range value (e.g. `ratio:
|
|
28
|
+
// '5:7'`, an AI's typo) would otherwise reach the template raw and build a class name (`is-5-7`)
|
|
29
|
+
// with no matching CSS rule — an unstyled grid, invisible until an overflow shows up in the audit.
|
|
30
|
+
// This lookup clamps the value the template actually renders, independent of the validator below.
|
|
31
|
+
const RATIO_CLASS = {
|
|
32
|
+
'1:1': 'is-1-1',
|
|
33
|
+
'3:2': 'is-3-2',
|
|
34
|
+
'2:3': 'is-2-3',
|
|
35
|
+
'2:1': 'is-2-1',
|
|
36
|
+
'1:2': 'is-1-2',
|
|
37
|
+
'3:1': 'is-3-1',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
★ THE MEDIA KIND IS A FACT ABOUT THE SLIDE, not only about the figure — so it is stamped on the
|
|
42
|
+
ROOT as well as on the figure, and clamped through a lookup for the same reason `ratio` is.
|
|
43
|
+
|
|
44
|
+
This is a fix, not a tidy-up. task-14b's conditional gutter is written `.is-content .split-grid` — a
|
|
45
|
+
DESCENDANT selector — but `is-content` was only ever put on `.split-figure`, which is a descendant of
|
|
46
|
+
`.split-grid`, not an ancestor. The rule therefore never matched anything, and every same-kind
|
|
47
|
+
split has been rendering with the mixed-kind zero gutter since. Measured live on
|
|
48
|
+
apps/deck-template's own two-column slide: two 870px columns in a 1740px row, i.e. no gap at all,
|
|
49
|
+
where the same-kind rule asks for a real gutter (36px at the time; the token is 96 now). Static
|
|
50
|
+
tests could not see it — deckInsets.static.test.ts asserts the rule EXISTS in the source, which it
|
|
51
|
+
did, correctly written, against a class that was never in the right place for it to match.
|
|
52
|
+
*/
|
|
53
|
+
/*
|
|
54
|
+
★ FOUR VALUES, AND `anchor` IS ONE OF THEM RATHER THAN THE ABSENCE OF THE OTHER THREE. Split's
|
|
55
|
+
text column has always started at the one-third line (see --deck-anchor-third and the note on
|
|
56
|
+
.split-grid) so headings do not jump between slides. That is the right default and stays it, but
|
|
57
|
+
it is not `start`, so it needs a name of its own — otherwise the enum would have three values and
|
|
58
|
+
a fourth unnamed state you get by omitting the prop, which is exactly the shape `frame: none` was.
|
|
59
|
+
*/
|
|
60
|
+
const ALIGN_CLASS = {
|
|
61
|
+
anchor: 'is-align-anchor',
|
|
62
|
+
start: 'is-align-start',
|
|
63
|
+
center: 'is-align-center',
|
|
64
|
+
'body-center': 'is-align-body-center',
|
|
65
|
+
end: 'is-align-end',
|
|
66
|
+
};
|
|
67
|
+
/* `anchor` has no meaning on a panel — it is the deck's one-third line, which the text column
|
|
68
|
+
stands on and a filled frame does not — so this vocabulary is the other three. */
|
|
69
|
+
const PANEL_ALIGN_CLASS = {
|
|
70
|
+
start: 'is-panel-align-start',
|
|
71
|
+
'title-start': 'is-panel-align-title-start',
|
|
72
|
+
center: 'is-panel-align-center',
|
|
73
|
+
'title-center': 'is-panel-align-title-center',
|
|
74
|
+
end: 'is-panel-align-end',
|
|
75
|
+
};
|
|
76
|
+
/* The two rungs that hold a heading's height back before aligning what is left. */
|
|
77
|
+
const BANDED_ALIGNS = new Set(['title-start', 'title-center']);
|
|
78
|
+
|
|
79
|
+
/*
|
|
80
|
+
The panel's register, and the same four stack's band takes. Split did not have this axis at all —
|
|
81
|
+
its panel was one hardcoded fill — so `frame: 'panel'` meant a different surface in each layout.
|
|
82
|
+
The values, the class names and the four rules below are stack's, read from the same tokens.
|
|
83
|
+
*/
|
|
84
|
+
/*
|
|
85
|
+
★★ THE TONE VALUES ARE THE PANEL GROUNDS, AND THEY ARE HUES RATHER THAN TREATMENTS.
|
|
86
|
+
|
|
87
|
+
This read `soft | subtle | warm | vivid` — two treatment words and two hue words in one enum, and
|
|
88
|
+
half of them reaching past the roles into the raw ramp (`--warm-100`, `--forest-300`) so they did
|
|
89
|
+
not follow the register at all. A panel on a dark slide kept its light fill under light ink.
|
|
90
|
+
|
|
91
|
+
Each value is now a NAMED GROUND from the engine (see PANEL in scripts/deck.ts), which publishes a
|
|
92
|
+
fill AND its ink for BOTH registers. So the ink is paired rather than inherited, and every value
|
|
93
|
+
moves with the slide:
|
|
94
|
+
|
|
95
|
+
warm peach on light, bark on dark accent the quiet green field
|
|
96
|
+
vivid the loud green, MIRRORED neutral no hue at all
|
|
97
|
+
|
|
98
|
+
The renames are `soft` → `accent` (same forest-100 on light) and `subtle` → `neutral`. `warm` and
|
|
99
|
+
`vivid` keep their names; warm deepens one step (warm-100 → warm-200) to be a ground rather than a
|
|
100
|
+
tint of the page.
|
|
101
|
+
|
|
102
|
+
★★ AND `dark` IS NOT ONE OF THEM, BECAUSE IT IS NOT A HUE — it is the OTHER axis. `background` says
|
|
103
|
+
which ground; `panelDark` says which register that ground is read in. They compose: every one of
|
|
104
|
+
the four has a light half and a dark half already, so a dark panel is `background` unchanged with the
|
|
105
|
+
register flipped, and the enum stays four values rather than five with one of them meaning
|
|
106
|
+
something different from the rest.
|
|
107
|
+
*/
|
|
108
|
+
const TONE_CLASS = {
|
|
109
|
+
warm: 'bg-warm',
|
|
110
|
+
accent: 'bg-accent',
|
|
111
|
+
vivid: 'bg-vivid',
|
|
112
|
+
neutral: 'bg-neutral',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
The text tier the two columns run at. `base` is the deck's body role, the same 48px every other
|
|
117
|
+
layout sets; `sm` steps the whole grid down to `body-sm` for a slide that genuinely has more to
|
|
118
|
+
say than the column holds at full size.
|
|
119
|
+
*/
|
|
120
|
+
const BODY_CLASS = {
|
|
121
|
+
base: 'is-body-base',
|
|
122
|
+
sm: 'is-body-sm',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const FRAME_CLASS = {
|
|
126
|
+
photo: 'is-photo',
|
|
127
|
+
panel: 'is-panel',
|
|
128
|
+
code: 'is-code',
|
|
129
|
+
figure: 'is-figure',
|
|
130
|
+
content: 'is-content',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
134
|
+
import { useNotchCuts } from '../useNotchCuts.js';
|
|
135
|
+
import { injectionCurrentPage, injectionFrontmatter } from '@slidev/client/constants.ts';
|
|
136
|
+
import deckTokens from '@humanforest/tokens/deck/deck.json';
|
|
137
|
+
import { chromeShows, coBrandFitted, hasNotch, notchShows } from '../notch.js';
|
|
138
|
+
import { assetUrl } from '../assetUrl.js';
|
|
139
|
+
|
|
140
|
+
const props = defineProps({
|
|
141
|
+
/** Content's share of the row against the figure's — e.g. '3:2' gives content the bigger half, '2:3' gives the figure the bigger half. Content-relative: independent of `flip`, so swapping sides never changes which one is bigger. The ladder stops at `3:1` because the share IS the content column's measure here, and the rung below that is narrower than a line wants. `stack` carries two rungs this does not — `1:3`, a band that dominates harder than two thirds, and `0:1`, the band with no strip at all — because a full-width band is the shape that needs them. */
|
|
142
|
+
ratio: {
|
|
143
|
+
type: String,
|
|
144
|
+
default: '1:1',
|
|
145
|
+
// Inlined rather than reading RATIO_CLASS: Vue hoists defineProps() out of setup() at build
|
|
146
|
+
// time, so a validator cannot close over a script-setup-local const — it compiles in dev and
|
|
147
|
+
// breaks the production build. The template clamps through RATIO_CLASS anyway, which is what
|
|
148
|
+
// actually protects a real deck, since validators are stripped from production builds.
|
|
149
|
+
validator: (r) => ['1:1', '3:2', '2:3', '2:1', '1:2', '3:1'].includes(r),
|
|
150
|
+
},
|
|
151
|
+
/**
|
|
152
|
+
* Mirror the layout: by default the content takes the left column and the figure the right; flipped, they swap.
|
|
153
|
+
* A pure mirror — it does not affect either one's share (see `ratio`), only where each appears.
|
|
154
|
+
*
|
|
155
|
+
* ★★ A BOOLEAN, AND SHARED WITH `stack` — which is the only shape that gives the two layouts one
|
|
156
|
+
* key with one DEFAULT. They invert deliberately (split leads with the title, stack subordinates
|
|
157
|
+
* it), so any value that names a POSITION comes out opposite between them: content-first on one
|
|
158
|
+
* is content-last on the other. `false` means "this layout's own arrangement" on both, and that
|
|
159
|
+
* is the only thing they genuinely share — there is no word for "figure right" and "band top" at
|
|
160
|
+
* once. It replaced `flip`, which had no third value in prospect either.
|
|
161
|
+
*
|
|
162
|
+
* ⚠ IT IS RELATIVE, WHICH IS THE TRADE. `flip: true` said where the content went; `flip` says
|
|
163
|
+
* "the other way from the default", so reading a slide needs the default in hand — and reversing
|
|
164
|
+
* a layout's default would silently change every flipped slide. The defaults are doctrine and do
|
|
165
|
+
* not move, which is what makes that acceptable.
|
|
166
|
+
*/
|
|
167
|
+
flip: { type: Boolean, default: false },
|
|
168
|
+
/**
|
|
169
|
+
* A CODE SURFACE — the panel IS the block, rather than a mat with a block sitting on it.
|
|
170
|
+
*
|
|
171
|
+
* ★★ IT EXISTS TO AVOID TWO NESTED WELLS. On `frame: panel` a code block draws its own ground and
|
|
172
|
+
* edge inside the panel's, so the slide is a card on a card — on the dark register a #191617 well
|
|
173
|
+
* inside a neutral-800 panel, 1.10:1 apart, which is two surfaces an audience cannot separate and
|
|
174
|
+
* one of them doing no work.
|
|
175
|
+
*
|
|
176
|
+
* Here the panel takes `--slidev-code-background` and the block inside it goes flat: no fill, no
|
|
177
|
+
* edge, no padding of its own. The magic-move container owns its surface the same way; this is
|
|
178
|
+
* that rule with a name.
|
|
179
|
+
*
|
|
180
|
+
* ★ AN EXPLICIT VALUE, NOT A `:has()` ON `panel`. A panel that silently changed ground because it
|
|
181
|
+
* happened to contain a code block is exactly the shape of `frame: none` — a positive behaviour
|
|
182
|
+
* named as an absence — which this layout already paid for once. It also makes the conflict
|
|
183
|
+
* visible: `background` names a tinted ground and a code frame has none, so the two do not compose, and
|
|
184
|
+
* an author reading `frame: code` can see that where a silent override would not tell them.
|
|
185
|
+
*/
|
|
186
|
+
/**
|
|
187
|
+
* How the far side is FRAMED: clipped as a photo, matted in a tinted panel, left bare for a
|
|
188
|
+
* figure centred in the cell, or not a figure at all — a second column of content.
|
|
189
|
+
*
|
|
190
|
+
* ★★ THIS PROP WAS CALLED `frame`, AND HALF ITS VALUES WERE NOT MEDIA. `content` is a second
|
|
191
|
+
* column of the author's own prose and `figure` is any component; only `photo` and `panel` frame
|
|
192
|
+
* media at all. Worse, `content` was called `none` — a positive behaviour (36px gutter, dropped
|
|
193
|
+
* to row 2, header spanning both columns) named as an absence, which cost a real bug: a live map
|
|
194
|
+
* declared `frame: content`, inherited the prose treatment, and began a third of the way down the
|
|
195
|
+
* slide with the space above it empty. `figure` had to be invented to separate the two jobs.
|
|
196
|
+
*
|
|
197
|
+
* Under the honest names the four are plainly four things: `photo` fills and crops to the card
|
|
198
|
+
* shape, `panel` centres at natural size inside a tinted card, `figure` is CAPPED to the cell and
|
|
199
|
+
* centred with no treatment at all, and `content` is not a figure at all.
|
|
200
|
+
*
|
|
201
|
+
* ⚠ THAT LINE USED TO READ "`figure` fills with no treatment", WHICH IS THE ONE THING IT DOES NOT
|
|
202
|
+
* DO — and it is the sentence an author reads when choosing. `.is-figure` centres its child and
|
|
203
|
+
* caps it on both axes (see the rule and its note further down); a child that wants the whole
|
|
204
|
+
* cell has to ask with its own height, and even then the cap is what it is measured against.
|
|
205
|
+
* Measured on the map-as-evidence slide: at `figure` a map declaring `height: 100%` drew 816px in
|
|
206
|
+
* a 1008px cell, centred with 96 above and below; at `photo` it drew 1008. The difference that
|
|
207
|
+
* matters when picking between the two is CROPPING — `photo` fills and cuts what does not fit,
|
|
208
|
+
* `figure` shows the whole thing and letterboxes.
|
|
209
|
+
*
|
|
210
|
+
* ★★ THE DEFAULT WAS `content`, AND IT WAS WRONG FOR EVERY PRESET THAT RELIED ON IT. Four do —
|
|
211
|
+
* `split` ("body copy beside a figure"), `map`, `map-inset` and `trend` — and all four want a
|
|
212
|
+
* figure. Measured on deck-template before this changed: every one rendered `is-content`, took
|
|
213
|
+
* the 36px same-kind gutter, and dropped its figure to grid row 2 starting at canvas y 360. That
|
|
214
|
+
* is the prose treatment, and a map or a chart starting a third of the way down with empty space
|
|
215
|
+
* above it is the exact failure the `figure` value was invented to fix — shipping by default on
|
|
216
|
+
* the recipes most likely to be copied.
|
|
217
|
+
*
|
|
218
|
+
* `figure` is the neutral one: it is centred in its cell and given no treatment. A layout called `split`
|
|
219
|
+
* that is given no further instruction should put something beside the words, not assume the
|
|
220
|
+
* far side is more words — that is the case worth stating explicitly, and six presets do.
|
|
221
|
+
*
|
|
222
|
+
* ★★ AND THE SLOT IS `aside`, NOT `right`, FOR THE SAME REASON THIS PROP IS NOT CALLED `media`.
|
|
223
|
+
* `right` named a POSITION the slot does not control: `flip` decides which half each column
|
|
224
|
+
* takes, so on `flip: true` the content moves right and the slot called `right` renders on the
|
|
225
|
+
* LEFT. Measured on deck-template's `chart-right`, which is exactly that combination — the
|
|
226
|
+
* ::right:: slot came out at grid column 1, left edge 96, while the content sat at 978.
|
|
227
|
+
*
|
|
228
|
+
* `figure` was the obvious replacement and is wrong the same way one level down: it would name a
|
|
229
|
+
* CONTENT KIND the slot does not control either, and at `frame: content` that slot holds a second
|
|
230
|
+
* column of prose. `aside` names the REGION, which is the one thing that never moves — split is
|
|
231
|
+
* always two columns and this is always the one the content is not in.
|
|
232
|
+
*/
|
|
233
|
+
frame: { type: String, default: 'figure', validator: (f) => ['photo', 'panel', 'figure', 'content', 'code'].includes(f) },
|
|
234
|
+
/**
|
|
235
|
+
* Take the figure to the CANVAS EDGE instead of the 36px media inset — same frame, same fill, same
|
|
236
|
+
* content, no ring of ground around it.
|
|
237
|
+
*
|
|
238
|
+
* A MODIFIER, not a fifth `frame`, because it is orthogonal to what the figure IS. A photo, a
|
|
239
|
+
* tinted panel and a component figure can each want the edge, and each keeps its own treatment
|
|
240
|
+
* when it goes there; a fifth enum value would have had to pick one of the three and would have
|
|
241
|
+
* made the other two unreachable. Inert on `frame: content`, which is a column of the author's own
|
|
242
|
+
* prose and wants the text inset it already has.
|
|
243
|
+
*
|
|
244
|
+
* `inset` keeps the ring of ground around the figure; `bleed` takes it to the canvas.
|
|
245
|
+
*
|
|
246
|
+
* ⚠ IT IS ONLY VISIBLE ON A FRAME THAT PAINTS A GROUND. What this gives up is the RING of deck
|
|
247
|
+
* ground around the figure, so on `photo`, `panel` and `code` — the three that paint one — the
|
|
248
|
+
* difference is the whole point of the slide. On this layout's DEFAULT frame, `figure`, there is
|
|
249
|
+
* no ground to give up: the box still reaches the canvas, but nothing about it is drawn, so the
|
|
250
|
+
* two values look nearly alike. Measured on a specimen pair that took the default and had to be
|
|
251
|
+
* moved to `panel` before it showed anything. Choose the frame before blaming the prop.
|
|
252
|
+
*
|
|
253
|
+
* ★ NAMED `edge`, NOT `bleed`, BECAUSE `bleed` IS ALSO A LAYOUT. One word cannot mean both "this
|
|
254
|
+
* slide is a full-canvas image" and "this figure reaches the edge" in the same frontmatter. The
|
|
255
|
+
* key carries the axis and the value carries the state, so `layout: bleed` keeps sole ownership
|
|
256
|
+
* of the word as a layout and `edge: bleed` is unambiguous beside it.
|
|
257
|
+
*
|
|
258
|
+
* `inset` is the deck's own word — --deck-inset-text, --deck-inset-media and --deck-inset-chrome
|
|
259
|
+
* are the three it is measured in — so naming the default costs an author no new concept. It is
|
|
260
|
+
* NOT called "notched": `notch: false` is a separate key that keeps the inset and drops only the
|
|
261
|
+
* cut, so a slide can be inset without being notched.
|
|
262
|
+
*
|
|
263
|
+
* ★ `bleed` SUPPRESSES THE NOTCH, and that is arithmetic rather than a preference. The cut exists
|
|
264
|
+
* to seat the running mark in the ring of ground around the figure; at inset 0 there is no ring,
|
|
265
|
+
* so there is nothing to bite. The chrome is then drawn OVER the figure — pair it with
|
|
266
|
+
* `chrome: false`, or use the `bleed` LAYOUT, whose scrim solves that across the whole canvas.
|
|
267
|
+
*/
|
|
268
|
+
/*
|
|
269
|
+
* ★★ `spill` IS `bleed` PLUS ONE MORE STEP, AND THE STEP IS THE POINT. `bleed` takes the figure's
|
|
270
|
+
* BOX to the canvas and then gives the inset back as padding, so the content lands exactly where
|
|
271
|
+
* it would have unbled — the frame changes, the thing inside it does not. `spill` takes the box
|
|
272
|
+
* there and lets what is inside run PAST it: a map drawn at half again the column's width, a
|
|
273
|
+
* render whose front wheel leaves the slide, a browser window whose foot is below the canvas.
|
|
274
|
+
*
|
|
275
|
+
* The cover's mosaic proved the move — a grid of pictures cut by two edges reads as a field the
|
|
276
|
+
* slide is a window onto, where the same pictures matted read as a gallery. This is that, for a
|
|
277
|
+
* figure a deck already has.
|
|
278
|
+
*
|
|
279
|
+
* ⚠ IT IS A CROP, SO WHAT IT CROPS HAS TO BE WORTH CROPPING. A photograph spills badly: the
|
|
280
|
+
* subject is what the picture is FOR, and pushing it off the edge is just a smaller photograph.
|
|
281
|
+
* What spills well is a thing with no single subject — a map, a repeating grid, a product render
|
|
282
|
+
* whose silhouette is legible from a third of it, a device or browser frame whose chrome is the
|
|
283
|
+
* shape being shown.
|
|
284
|
+
*/
|
|
285
|
+
edge: { type: String, default: 'inset', validator: (e) => ['inset', 'bleed', 'spill'].includes(e) },
|
|
286
|
+
/**
|
|
287
|
+
* The panel's register, for `frame: panel` only — exactly as on `stack`, whose band takes the same
|
|
288
|
+
* four. 'warm' is the deck's own mat; 'subtle' is the quiet grey fill; 'soft' is the accent tint;
|
|
289
|
+
* 'vivid' is the saturated one.
|
|
290
|
+
*
|
|
291
|
+
* ★ IT HAS NO EFFECT ON ANY OTHER FRAME, and that is scoped in the CSS rather than guarded here:
|
|
292
|
+
* every rule is `.split-figure.is-panel.tone-*`, so a photo or a figure carrying a tone renders
|
|
293
|
+
* exactly as it would without one. stack states the same thing about its own.
|
|
294
|
+
*/
|
|
295
|
+
background: { type: String, default: 'warm', validator: (t) => ['warm', 'accent', 'vivid', 'neutral'].includes(t) },
|
|
296
|
+
/**
|
|
297
|
+
* The text tier both columns run at — `base` (the deck's body role, 48px) or `sm` (`body-sm`,
|
|
298
|
+
* 36px, the dense tier).
|
|
299
|
+
*
|
|
300
|
+
* ★ THE DEFAULT IS `base`, AND IT USED TO BE THE ONLY BEHAVIOUR — the wrong one. This grid was
|
|
301
|
+
* pinned to body-sm on the reasoning that "two columns halve the measure, so both sides take the
|
|
302
|
+
* dense body tier". The measure being halved is exactly why the TIER should not be: a slide's
|
|
303
|
+
* body is 48px because that is what carries a room, and a column is not a reason for the back row
|
|
304
|
+
* to get 36. A narrow column simply wraps sooner, which is what a column is for.
|
|
305
|
+
*
|
|
306
|
+
* `sm` is the escape hatch for the case that argument does not cover: a slide whose column really
|
|
307
|
+
* does hold more than 48px allows and cannot be split in two. It is a deliberate, per-slide
|
|
308
|
+
* decision now rather than something every split inherited without asking.
|
|
309
|
+
*/
|
|
310
|
+
body: { type: String, default: 'base', validator: (b) => ['base', 'sm'].includes(b) },
|
|
311
|
+
/**
|
|
312
|
+
* Where the CONTENT column sits vertically. `anchor` is the deck's own one-third line — the
|
|
313
|
+
* default, and the reason headings do not jump between consecutive split slides. `start`, `center`
|
|
314
|
+
* and `end` are the vocabulary `bleed` and `statement` already use.
|
|
315
|
+
*
|
|
316
|
+
* `body-center` is the fifth and moves the column in two pieces rather than as a block: the
|
|
317
|
+
* HEADING keeps the top and everything under it centres in what is left. It is default.vue's mode
|
|
318
|
+
* of the same name, and it is the COLUMN'S HALF OF A PAIR — set it beside `panelAlign:
|
|
319
|
+
* title-center` and both sides of the slide read the same way, a title at the top with the
|
|
320
|
+
* substance centred in the space below it on each side. See the CSS rule for the auto margins
|
|
321
|
+
* that do it and why there are three of them.
|
|
322
|
+
*
|
|
323
|
+
* ★ THE FIGURE IS UNAFFECTED BY ALL FIVE: it spans `grid-row: 1 / 3`, so it fills the frame
|
|
324
|
+
* whatever the content does. Aligning what sits INSIDE the figure is `panelAlign`, a separate prop
|
|
325
|
+
* for a separate box — its own doc says why they cannot be one.
|
|
326
|
+
*/
|
|
327
|
+
align: {
|
|
328
|
+
type: String,
|
|
329
|
+
default: 'anchor',
|
|
330
|
+
validator: (a) => ['anchor', 'start', 'center', 'body-center', 'end'].includes(a),
|
|
331
|
+
},
|
|
332
|
+
/**
|
|
333
|
+
* Where the PANEL's own content sits vertically — `align` for the other half of the slide.
|
|
334
|
+
*
|
|
335
|
+
* Two of the five hold a band the height of the slide's h1 back before aligning what is left, so
|
|
336
|
+
* the panel reads against the column's BODY rather than against the whole column: `title-start`
|
|
337
|
+
* begins where that body begins, and `title-center` centres in the space under the band. Both
|
|
338
|
+
* work on every panel — plain, notched, bleeding — because the band is a margin on the panel's
|
|
339
|
+
* content rather than an addition to a top padding four different rules can decide, and both
|
|
340
|
+
* measure the real heading rather than deriving one, so a two-line title moves them too.
|
|
341
|
+
*
|
|
342
|
+
* There is no `title-end`: a top margin cannot move an item already pinned to the bottom, so it
|
|
343
|
+
* would be `end` under another name.
|
|
344
|
+
*
|
|
345
|
+
* `title-center` has a companion on the other half: `align: body-center` gives the COLUMN the same
|
|
346
|
+
* reading — heading at the top, body centred in what is left — and the two are meant to be set
|
|
347
|
+
* together on a slide that wants both sides to say it.
|
|
348
|
+
*
|
|
349
|
+
* ★★ IT IS A SECOND PROP BECAUSE THEY ARE TWO BOXES, NOT ONE. `align` moves the content column
|
|
350
|
+
* inside the grid band; a panel holds its children inside its own padding box, and on a bleeding
|
|
351
|
+
* panel that box starts lower than the band because the frame reaches the canvas edge and pads to
|
|
352
|
+
* clear the chrome. Two boxes with the same bottom and different tops give two different centres —
|
|
353
|
+
* 18px apart, measured — so a slide whose halves read as ROWS across the seam cannot get there by
|
|
354
|
+
* centring both. Setting both to `end` stands them on the bottom edge they already share, which
|
|
355
|
+
* needs no number and survives one half carrying a heading the other does not.
|
|
356
|
+
*
|
|
357
|
+
* Folding it into `align` was the alternative and would have moved every panel split in the deck,
|
|
358
|
+
* on a prop whose own documentation promises the figure is left alone.
|
|
359
|
+
*
|
|
360
|
+
* ★ SCOPED TO `panel`, as `background` is: it is the frame that holds content in a box of its own. A
|
|
361
|
+
* `code` frame stacks its children in a column, where the vertical axis is `justify-content`
|
|
362
|
+
* rather than `align-items`, and `photo`/`figure`/`content` have nothing to align.
|
|
363
|
+
*/
|
|
364
|
+
panelAlign: { type: String, default: 'center', validator: (a) => ['start', 'title-start', 'center', 'title-center', 'end'].includes(a) },
|
|
365
|
+
/**
|
|
366
|
+
* Run the PANEL in the dark register, leaving the rest of the slide light.
|
|
367
|
+
*
|
|
368
|
+
* ★★ A SECOND AXIS, NOT A FIFTH `background`. The four tones are HUES and each already publishes both
|
|
369
|
+
* halves (see PANEL in scripts/deck.ts): warm is peach on light and bark on dark, accent is the
|
|
370
|
+
* quiet green field either way. So "dark" is not a ground to pick, it is which half of the ground
|
|
371
|
+
* you picked gets used — and folding it into `background` would have made one value mean a register
|
|
372
|
+
* while the other four meant colours.
|
|
373
|
+
*
|
|
374
|
+
* It works by putting `.deck-dark` on the panel element itself. That class is a plain class in
|
|
375
|
+
* packages/tokens/deck/deck.css — bound alongside `.dark`, not scoped to a slide root — so every
|
|
376
|
+
* `--deck-*` role is re-declared AT the panel and everything inside inherits the dark values:
|
|
377
|
+
* prose, chips, alerts, charts, anything reading the roles. Nothing has to opt in a second time.
|
|
378
|
+
*
|
|
379
|
+
* ★ SCOPED TO THE FRAMES THAT BRING A FILL — `panel` and `code` — exactly as `background` is scoped to
|
|
380
|
+
* `panel`, and for a stronger reason. Those two paint a ground, so flipping the register repaints
|
|
381
|
+
* it under the ink. `photo`, `figure` and `content` bring no fill at all: the class would flip the
|
|
382
|
+
* INK to the dark register's near-white and leave it on the slide's own light ground.
|
|
383
|
+
*
|
|
384
|
+
* On `code` it is the whole point of a dark code surface: the ground is --slidev-code-background,
|
|
385
|
+
* which is register-aware, so the panel becomes the DARK well (--neutral-900) rather than a dark
|
|
386
|
+
* tint of the light one.
|
|
387
|
+
*
|
|
388
|
+
*
|
|
389
|
+
* ★★ AND THE RUNNING CHROME FOLLOWS IT, PER END. A bleeding dark panel leaves no ring of ground
|
|
390
|
+
* for the chrome to sit on, so the row lands directly on it and half the row is then over a dark
|
|
391
|
+
* ground while the other half is over a light one. Measured before that was answered: the page
|
|
392
|
+
* marker painted the LIGHT pill (forest-700) on forest-900, 1.09:1. `darkChromeParts()` in
|
|
393
|
+
* ../notch.js decides which end flips, from this same frontmatter, so the layout and the chrome
|
|
394
|
+
* cannot disagree about where the dark half is.
|
|
395
|
+
* The whole slide going dark is a different thing and has its own spelling: `dark: true`, which
|
|
396
|
+
* every layout that can carry a register now takes — `default`, `bleed`, `divider`, `split` and
|
|
397
|
+
* `stack`. `class: deck-dark` still works and is what the prop writes.
|
|
398
|
+
*/
|
|
399
|
+
panelDark: { type: Boolean, default: false },
|
|
400
|
+
/**
|
|
401
|
+
* Draw this slide on the dark register. Writes `deck-dark` on the layout root — the same class,
|
|
402
|
+
* the same element, said as a prop the layout documents rather than a raw class name an author
|
|
403
|
+
* had to know. `default` and `bleed` already spelled it this way; split did not, so a split slide
|
|
404
|
+
* was the one place in the deck where the register had to be written as `class: 'deck-dark'`.
|
|
405
|
+
*
|
|
406
|
+
* ★★ NOT `panelDark`, WHICH IS THE PROP ABOVE AND A DIFFERENT AXIS. That one darkens the PANEL's
|
|
407
|
+
* own fill on an otherwise light slide — a dark card on cream. This darkens the SLIDE, and the
|
|
408
|
+
* panel follows it like everything else. Both can be set; they answer different questions.
|
|
409
|
+
*
|
|
410
|
+
* ⚠ A LIVE MAP DOES NOT FOLLOW IT. The register is CSS custom properties and a Mapbox basemap is
|
|
411
|
+
* painted in WebGL from its own style, so a `dark` split slide carrying a map also needs
|
|
412
|
+
* `light-preset: night` on the map. DeckMap's own prop doc says the same from the other side.
|
|
413
|
+
*/
|
|
414
|
+
dark: { type: Boolean, default: false },
|
|
415
|
+
/** The figure's image URL. This is Slidev's own frontmatter key, used as Slidev means it: `extractImagesUsage()` harvests it and emits a `<link rel=preload as=image>`, so the URL belongs here rather than in an `<img>` a slide writes by hand. Slot content still wins when a slide provides its own figure. */
|
|
416
|
+
image: { type: String, default: '' },
|
|
417
|
+
/** Alt text — for `image` when there is one, and the label the `<Placeholder>` carries when there is not. */
|
|
418
|
+
alt: { type: String, default: '' },
|
|
419
|
+
/**
|
|
420
|
+
* TWO pictures instead of a picture and a column of prose — `[{ image, alt }, { image, alt }]`,
|
|
421
|
+
* content side first. One `image` is a figure; two here is a PAIR, and the content column becomes
|
|
422
|
+
* the second one.
|
|
423
|
+
*
|
|
424
|
+
* ★★ THE SAME KEY SHAPE THE MOSAIC USES, rather than a directional `imageLeft`. "Left" flips with
|
|
425
|
+
* `flip`, so a slide that set it and then mirrored would have had its two pictures swap meaning
|
|
426
|
+
* silently; content-first is the order every other prop on this layout is read in (`ratio` is
|
|
427
|
+
* content-first, and so is `frame`).
|
|
428
|
+
*
|
|
429
|
+
* ⚠ TWO, THREE OR FOUR. Two is a picture per column; three stacks the extra in the figure column;
|
|
430
|
+
* four is two and two. The split is `floor(n / 2)` to the content side, so the figure column — the
|
|
431
|
+
* one with no measure to protect — takes the odd one. One entry is `image` with extra steps and a
|
|
432
|
+
* fifth has nowhere to go without a grid this layout does not have, so the list clamps at four.
|
|
433
|
+
*
|
|
434
|
+
* ★ THE SLOT STILL WINS where it is written: a pair is what an author gets when they hand the
|
|
435
|
+
* layout two pictures and no words, which is what a diptych IS. Text in the default slot alongside
|
|
436
|
+
* a pair renders over the second picture, and that is the author's own decision to make.
|
|
437
|
+
*/
|
|
438
|
+
images: { type: Array, default: () => [] },
|
|
439
|
+
/**
|
|
440
|
+
* How the pictures of a set are separated: by the layout's own seam (`gap`, the default) or not at
|
|
441
|
+
* all (`flush`), where they meet and the set reads as one field cut into pieces.
|
|
442
|
+
*
|
|
443
|
+
* ★★ A SECOND AXIS, NOT A THIRD `edge` VALUE. `edge` is about the figure and the CANVAS — where
|
|
444
|
+
* the outer edges land. This is about the pictures and EACH OTHER. They cross: a flush set still
|
|
445
|
+
* insets from the canvas by default, and `edge: bleed` with `seam: gap` is four pictures at the
|
|
446
|
+
* canvas edge with the deck's own seam still between them.
|
|
447
|
+
*
|
|
448
|
+
* ⚠ FLUSH TAKES THE INNER CORNERS OFF AND KEEPS THE OUTER ONES. A rounded corner where two
|
|
449
|
+
* pictures meet draws a lens of ground between them and the set stops reading as one field; the
|
|
450
|
+
* corners on the OUTSIDE are the set's own edge against the slide, and squaring those makes a
|
|
451
|
+
* bleeding rectangle of a thing that has not bled. Inert unless `images` holds a set — a single
|
|
452
|
+
* figure has nothing to be flush WITH.
|
|
453
|
+
*/
|
|
454
|
+
seam: { type: String, default: 'gap', validator: (g) => ['gap', 'flush'].includes(g) },
|
|
455
|
+
/** Kicker above the heading. */
|
|
456
|
+
eyebrow: { type: String, default: '' },
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
/*
|
|
460
|
+
task-15-brief.md — THE NOTCH IS CONDITIONAL ON ITS OCCUPANT EXISTING.
|
|
461
|
+
|
|
462
|
+
The cut only happens when the running chrome is actually drawing a mark to put in it: a notch on
|
|
463
|
+
a `chrome: false` slide is a hole with nothing in it, which is the brief's own named failure case.
|
|
464
|
+
The predicate is ../notch.js's, not a second copy — slide-top.vue gates its own render on the
|
|
465
|
+
same function, so the two cannot disagree about whether there is a mark on this slide.
|
|
466
|
+
|
|
467
|
+
Read the same two injections slide-top.vue reads, and for the same reasons documented there:
|
|
468
|
+
`injectionFrontmatter` is the RAW object (not a ref), and `injectionCurrentPage` — never
|
|
469
|
+
useNav().currentPage — is the only page number that is correct on the param-less /print and
|
|
470
|
+
/overview routes the PDF export renders through.
|
|
471
|
+
*/
|
|
472
|
+
const frontmatter = inject(injectionFrontmatter, {});
|
|
473
|
+
const currentPage = inject(injectionCurrentPage, computed(() => 1));
|
|
474
|
+
// task-18 — `notch: false` joins the two existing conditions. All three live in ../notch.js so this
|
|
475
|
+
// layout, stack.vue and the chrome cannot disagree about whether there is a cut on this slide.
|
|
476
|
+
/*
|
|
477
|
+
★ `&& props.edge !== 'bleed'` IS APPLIED HERE RATHER THAN PASSED INTO hasNotch(), deliberately. That
|
|
478
|
+
function answers one question — "does this frame kind sit under the chrome" — for both layouts, and
|
|
479
|
+
it has already been given one argument too many once (a `flip` that made it wrong); a test pins its
|
|
480
|
+
arity at three for that reason. Bleeding is a property of THIS slide's frame, not of the frame KIND,
|
|
481
|
+
so the layout that knows about it is the one that applies it.
|
|
482
|
+
*/
|
|
483
|
+
/* ⚠ DECLARED BEFORE THE NOTCH INSTANCES BELOW, and that is load-bearing rather than tidy:
|
|
484
|
+
useNotchCuts() evaluates its `enabled` callback during setup, so a `pairColumns` declared after it
|
|
485
|
+
threw "Cannot access 'pairColumns' before initialization" and the slide rendered EMPTY — layout
|
|
486
|
+
gone, no fallback, one console line. */
|
|
487
|
+
/*
|
|
488
|
+
Content-first, clamped to four: see the prop. `pair` is the switch every rule below reads, so the
|
|
489
|
+
layout asks "is this a set of pictures" once rather than in five places, and the two columns are
|
|
490
|
+
derived from it rather than sliced at each use.
|
|
491
|
+
*/
|
|
492
|
+
const pair = computed(() => (props.images.length >= 2 ? props.images.slice(0, 4) : null))
|
|
493
|
+
const pairColumns = computed(() => {
|
|
494
|
+
if (!pair.value) return null
|
|
495
|
+
const n = pair.value.length
|
|
496
|
+
// floor, so an odd picture lands in the FIGURE column — the one with no measure to protect.
|
|
497
|
+
const cut = Math.floor(n / 2)
|
|
498
|
+
return { content: pair.value.slice(0, cut), figure: pair.value.slice(cut) }
|
|
499
|
+
})
|
|
500
|
+
const notched = computed(() =>
|
|
501
|
+
props.edge === 'inset'
|
|
502
|
+
&& hasNotch(props.frame, chromeShows(frontmatter, currentPage.value), notchShows(frontmatter)),
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
// The cut, measured. `notched` above still decides WHETHER this panel is cut — chrome showing, a
|
|
506
|
+
// real media side, `notch: false` not set — and this decides what shape the cut is, from whatever
|
|
507
|
+
// running chrome actually lands on the panel: the mark or a co-brand pair at one top corner, the
|
|
508
|
+
// page marker at the bottom, a section pill on a band. styles/base.css's own single-notch rule is
|
|
509
|
+
// what paints the first frame; this replaces it on mount. See ../useNotchCuts.js.
|
|
510
|
+
const figureEl = ref(null);
|
|
511
|
+
const { clip: notchClip } = useNotchCuts(figureEl, { enabled: () => notched.value });
|
|
512
|
+
/*
|
|
513
|
+
★★ THE PAIR'S OTHER PICTURE IS A FIGURE TOO, SO IT GETS ITS OWN CUT. Jose: "it should have the same
|
|
514
|
+
behaviour of the right side... same padding and all... and should notch". A picture standing where
|
|
515
|
+
the prose would is not a content column with an image in it — it is the second half of one object,
|
|
516
|
+
and half an object that keeps its square corner under the running mark is the tell.
|
|
517
|
+
|
|
518
|
+
Both instances are enabled on the same condition: useNotchCuts seats whatever actually TOUCHES the
|
|
519
|
+
box and returns null when nothing does, so the left column cuts for the section pill at its bottom
|
|
520
|
+
corner and the right one for the mark at its top, without either being told which is which.
|
|
521
|
+
*/
|
|
522
|
+
const pairEl = ref(null);
|
|
523
|
+
const { clip: pairClip } = useNotchCuts(pairEl, { enabled: () => notched.value && !!pairColumns.value });
|
|
524
|
+
|
|
525
|
+
/*
|
|
526
|
+
── `panelAlign: title-start` — the band, MEASURED ─────────────────────────────────────────────
|
|
527
|
+
The same trade useNotchCuts.js makes above, for the same reason: the number cannot be derived.
|
|
528
|
+
|
|
529
|
+
The CSS rule states a heading's height from the display role's own tokens, and that is right for a
|
|
530
|
+
one-line title on a plain panel. It is wrong twice over otherwise — a two-line title is a line
|
|
531
|
+
short, and a panel's top reservation is decided by whichever of four rules wins (card-pad, the
|
|
532
|
+
notch's max(), a bleeding panel's inset-text, or base.css's chrome clearance), which no expression
|
|
533
|
+
on this side can know. What the rung actually wants is "begin where the column's body begins", and
|
|
534
|
+
only the DOM can answer that.
|
|
535
|
+
|
|
536
|
+
★ MEASURED OFF THE HEADING, NEVER OFF THE BODY. This margin is what moves the body, so reading the
|
|
537
|
+
body back would be circular — the second measurement would include the first.
|
|
538
|
+
|
|
539
|
+
★ THE CSS VALUE IS THE FALLBACK, not a duplicate. It paints the first frame and it is what an
|
|
540
|
+
export gets if this never runs, so the rung degrades to the derived band rather than to nothing.
|
|
541
|
+
|
|
542
|
+
The scale is read off the figure itself (rect width against offset width) because the slide is
|
|
543
|
+
drawn inside a transform: rects come back in painted pixels and the margin is authored in canvas
|
|
544
|
+
ones, and mixing the two silently scales the band.
|
|
545
|
+
*/
|
|
546
|
+
const contentEl = ref(null);
|
|
547
|
+
const band = ref(null);
|
|
548
|
+
/*
|
|
549
|
+
⚠ THE HEADING IS OBSERVED, NOT THE COLUMN, and the difference is the whole reliability of this.
|
|
550
|
+
`.split-content` is a grid item in a `minmax(0, 1fr)` row, so its BOX does not change when the
|
|
551
|
+
heading inside it rewraps — a ResizeObserver on the column never fires. Caught by driving a
|
|
552
|
+
one-line title to four in the running deck: the band stayed at its one-line value and the panel
|
|
553
|
+
sat 288px above the body it was supposed to meet.
|
|
554
|
+
*/
|
|
555
|
+
let observedHeading = null;
|
|
556
|
+
|
|
557
|
+
const measureBand = () => {
|
|
558
|
+
const fig = figureEl.value;
|
|
559
|
+
const col = contentEl.value;
|
|
560
|
+
if (!fig || !col || !BANDED_ALIGNS.has(props.panelAlign) || props.frame !== 'panel') {
|
|
561
|
+
band.value = null;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const heading = col.querySelector('h1, hgroup');
|
|
565
|
+
if (!heading) {
|
|
566
|
+
band.value = null;
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (heading !== observedHeading) {
|
|
570
|
+
if (observedHeading) observer?.unobserve(observedHeading);
|
|
571
|
+
observer?.observe(heading);
|
|
572
|
+
observedHeading = heading;
|
|
573
|
+
}
|
|
574
|
+
const scale = fig.getBoundingClientRect().width / fig.offsetWidth || 1;
|
|
575
|
+
const figPad = parseFloat(getComputedStyle(fig).paddingTop) || 0;
|
|
576
|
+
const headingGap = parseFloat(getComputedStyle(heading).marginBottom) || 0;
|
|
577
|
+
const toBody = (heading.getBoundingClientRect().bottom - fig.getBoundingClientRect().top) / scale;
|
|
578
|
+
band.value = Math.max(0, Math.round(toBody + headingGap - figPad));
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
let observer = null;
|
|
582
|
+
onMounted(() => {
|
|
583
|
+
observer = new ResizeObserver(measureBand);
|
|
584
|
+
if (figureEl.value) observer.observe(figureEl.value);
|
|
585
|
+
if (contentEl.value) observer.observe(contentEl.value);
|
|
586
|
+
measureBand();
|
|
587
|
+
});
|
|
588
|
+
onBeforeUnmount(() => {
|
|
589
|
+
observer?.disconnect();
|
|
590
|
+
observer = null;
|
|
591
|
+
observedHeading = null;
|
|
592
|
+
});
|
|
593
|
+
watch(() => [props.panelAlign, props.frame, props.ratio, props.edge], measureBand);
|
|
594
|
+
|
|
595
|
+
const bandStyle = computed(() => (band.value === null ? null : { '--panel-align-band': `${band.value}px` }));
|
|
596
|
+
|
|
597
|
+
/*
|
|
598
|
+
CO-BRANDING. When the slide declares a `companion:`, the running chrome draws a wider lockup and
|
|
599
|
+
this cut has to be exactly that much wider. Both sides call the same pure function on the same
|
|
600
|
+
frontmatter (../notch.js's coBrandFitted()), so the occupant and the cut cannot be pinned to
|
|
601
|
+
different numbers.
|
|
602
|
+
|
|
603
|
+
That function CLAMPS the occupant to what this ratio's panel can actually seat, which is why it
|
|
604
|
+
takes the page number and the whole token file rather than just `chrome`. See ../notch.js's
|
|
605
|
+
notchBudgetPx().
|
|
606
|
+
|
|
607
|
+
Set INLINE, and only when there is a companion: `--deck-notch-occupant-w` is deliberately left
|
|
608
|
+
undeclared otherwise so a `class:` override on the slide still reaches the mask's own
|
|
609
|
+
var(…, var(--deck-chrome-mark)) fallback. See the same note in slide-top.vue.
|
|
610
|
+
*/
|
|
611
|
+
const companion = computed(() => coBrandFitted(frontmatter, currentPage.value, deckTokens));
|
|
612
|
+
const figureStyle = computed(() =>
|
|
613
|
+
notched.value && companion.value
|
|
614
|
+
? { '--deck-notch-occupant-w': `${companion.value.occupantPx}px` }
|
|
615
|
+
: undefined,
|
|
616
|
+
);
|
|
617
|
+
</script>
|
|
618
|
+
|
|
619
|
+
<template>
|
|
620
|
+
<div
|
|
621
|
+
class="slidev-layout split h-full flex flex-col"
|
|
622
|
+
:class="[
|
|
623
|
+
RATIO_CLASS[ratio] ?? 'is-1-1',
|
|
624
|
+
flip ? 'is-right' : 'is-left',
|
|
625
|
+
FRAME_CLASS[frame] ?? 'is-figure',
|
|
626
|
+
ALIGN_CLASS[align] ?? 'is-align-anchor',
|
|
627
|
+
PANEL_ALIGN_CLASS[panelAlign] ?? 'is-panel-align-center',
|
|
628
|
+
BODY_CLASS[body] ?? 'is-body-base',
|
|
629
|
+
edge === 'bleed' || edge === 'spill' ? 'is-bleed' : '',
|
|
630
|
+
edge === 'spill' ? 'is-spill' : '',
|
|
631
|
+
pair && seam === 'flush' ? 'is-flush' : '',
|
|
632
|
+
dark ? 'deck-dark' : '',
|
|
633
|
+
]"
|
|
634
|
+
>
|
|
635
|
+
<!--
|
|
636
|
+
Header and footer are GRID ITEMS, not siblings of the grid — see `.split-grid`'s own comment
|
|
637
|
+
for the anchor arithmetic that requires it.
|
|
638
|
+
-->
|
|
639
|
+
<div class="split-grid">
|
|
640
|
+
<!--
|
|
641
|
+
⚠ A FIGURE, NOT A CONTENT COLUMN WITH PICTURES IN IT. It carries the figure's own classes so
|
|
642
|
+
every rule that shapes the other half — the claw-back to the media inset, the card corner,
|
|
643
|
+
the cover crop, the cut — applies to this one by construction rather than by being restated.
|
|
644
|
+
`is-pair` moves it into the CONTENT column's track and mirrors the one thing that is
|
|
645
|
+
handed: which of its edges is the outer one.
|
|
646
|
+
-->
|
|
647
|
+
<div
|
|
648
|
+
v-if="pairColumns"
|
|
649
|
+
ref="pairEl"
|
|
650
|
+
class="split-figure is-photo is-pair"
|
|
651
|
+
:class="[
|
|
652
|
+
edge === 'bleed' || edge === 'spill' ? 'is-bleed' : '',
|
|
653
|
+
edge === 'spill' ? 'is-spill' : '',
|
|
654
|
+
notched ? 'has-notch deck-notch' : '',
|
|
655
|
+
]"
|
|
656
|
+
:data-notch-corner="notched ? (flip ? 'right' : 'left') : undefined"
|
|
657
|
+
:style="pairClip ? { clipPath: pairClip } : null"
|
|
658
|
+
>
|
|
659
|
+
<img v-for="(p, i) in pairColumns.content" :key="i" :src="assetUrl(p.image)" :alt="p.alt">
|
|
660
|
+
</div>
|
|
661
|
+
<div v-else ref="contentEl" class="split-content">
|
|
662
|
+
<p v-if="eyebrow" class="deck-eyebrow">{{ eyebrow }}</p>
|
|
663
|
+
<slot />
|
|
664
|
+
</div>
|
|
665
|
+
<!--
|
|
666
|
+
`has-notch` is this LAYOUT's marker (its own margin/reservation rules key off it, and
|
|
667
|
+
scripts/deckAudit.ts counts it); `deck-notch` is the SHARED cut, one rule in styles/base.css
|
|
668
|
+
that stack.vue's band wears too. `data-notch-corner` is which of the figure's own corners is
|
|
669
|
+
the outer one — physical, never logical, because "outer" flips with `flip`.
|
|
670
|
+
-->
|
|
671
|
+
<div
|
|
672
|
+
ref="figureEl"
|
|
673
|
+
class="split-figure"
|
|
674
|
+
:class="[FRAME_CLASS[frame] ?? 'is-figure', TONE_CLASS[background] ?? 'bg-warm', edge === 'bleed' || edge === 'spill' ? 'is-bleed' : '', edge === 'spill' ? 'is-spill' : '', panelDark && (frame === 'panel' || frame === 'code') ? 'deck-dark' : '', notched ? 'has-notch deck-notch' : '']"
|
|
675
|
+
:data-notch-corner="notched ? (flip ? 'left' : 'right') : undefined"
|
|
676
|
+
:style="[figureStyle, notchClip ? { clipPath: notchClip } : null, bandStyle]"
|
|
677
|
+
>
|
|
678
|
+
<slot name="aside">
|
|
679
|
+
<!-- ⚠ NO WRAPPING TEMPLATE TAG HERE, and that is about a gate rather than about Vue. The
|
|
680
|
+
anchor test slices this file from the opening template tag to the first CLOSING one to
|
|
681
|
+
find the footer inside the grid, so an inner one ends the slice early and the gate
|
|
682
|
+
reports the footer as missing from a layout that has not moved it. (Spelling the tag
|
|
683
|
+
out in this comment does it too — which is why it is described rather than written.)
|
|
684
|
+
A `v-for` over an empty list renders nothing, so the three cases sit as siblings. -->
|
|
685
|
+
<img v-for="(p, i) in (pairColumns?.figure ?? [])" :key="i" :src="assetUrl(p.image)" :alt="p.alt">
|
|
686
|
+
<img v-if="!pairColumns && frame === 'photo' && image" :src="assetUrl(image)" :alt="alt">
|
|
687
|
+
<Placeholder v-if="!pairColumns && frame === 'photo' && !image" :label="alt || 'Image'" />
|
|
688
|
+
</slot>
|
|
689
|
+
</div>
|
|
690
|
+
<div v-if="$slots.footer" class="split-footer"><slot name="footer" /></div>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
</template>
|
|
694
|
+
|
|
695
|
+
<style scoped>
|
|
696
|
+
/*
|
|
697
|
+
★★ NO VERTICAL PADDING RESET ANY MORE. This used to zero `padding-top`/`padding-bottom` so that
|
|
698
|
+
`--deck-anchor-third`, then written `calc(100% / 3)`, resolved against a full-canvas box. The
|
|
699
|
+
token is a canvas pixel now, so the box it sits in no longer has to be the canvas — and the layout
|
|
700
|
+
keeps the same four-sided inset every other layout gets from base.css.
|
|
701
|
+
|
|
702
|
+
That reset was the root of a whole class of bug: with the root carrying no vertical padding, every
|
|
703
|
+
region inside had to re-add its own edge clearance, and nothing enforced that it be right.
|
|
704
|
+
`.split-content` reserved 96px of bottom clearance unconditionally, so the first slide to use the
|
|
705
|
+
footer slot had a column 96px shorter than it needed sitting 120px above its own footer. Regions
|
|
706
|
+
no longer own edge clearance at all; the grid box IS the safe area.
|
|
707
|
+
*/
|
|
708
|
+
.slidev-layout.split {
|
|
709
|
+
/*
|
|
710
|
+
Which grid COLUMN each half lands in. Declared once here, on the root, rather than as an
|
|
711
|
+
`order` on each item: `order` only works on auto-placed items, and header/footer now have to be
|
|
712
|
+
placed EXPLICITLY (see `.split-grid` below), which means the content and figure columns have to
|
|
713
|
+
be nameable from those rules too. Physical column numbers, not logical ones — which track is
|
|
714
|
+
"the content's" flips with `flip`, never with writing direction.
|
|
715
|
+
*/
|
|
716
|
+
--split-content-col: 1;
|
|
717
|
+
--split-figure-col: 2;
|
|
718
|
+
}
|
|
719
|
+
.slidev-layout.split.is-right {
|
|
720
|
+
--split-content-col: 2;
|
|
721
|
+
--split-figure-col: 1;
|
|
722
|
+
}
|
|
723
|
+
.split-grid {
|
|
724
|
+
flex: 1;
|
|
725
|
+
display: grid;
|
|
726
|
+
column-gap: 0; /* task-14b-brief.md — the DEFAULT is "no gutter"; `.is-content` below restores one */
|
|
727
|
+
min-height: 0; /* lets the row shrink instead of overflowing the slide */
|
|
728
|
+
/*
|
|
729
|
+
task-14b-brief.md — THE VERTICAL ANCHOR. Jose: "text side should always start at 2/3 of the
|
|
730
|
+
slide vertically, not centered, so the headings dont jump between slides" — confirmed as the
|
|
731
|
+
one-third line, y = 360 on the 1080 canvas. Replaces the old `align-content: center` (which, on
|
|
732
|
+
the single `minmax(0, 1fr)` row task-13-brief.md pinned to full height, had nothing left to
|
|
733
|
+
centre — a row that already fills 100% of the container can't be moved within it by
|
|
734
|
+
`align-content` at all; this task looked closer and found that rule was dead code, not merely
|
|
735
|
+
the wrong VALUE).
|
|
736
|
+
|
|
737
|
+
Two explicit rows instead of one:
|
|
738
|
+
row 1 the remainder a spacer — nothing is placed in it directly.
|
|
739
|
+
row 2 minmax(0, 1fr) `.split-content` lives here, so its box starts exactly where
|
|
740
|
+
row 1 ends.
|
|
741
|
+
|
|
742
|
+
★★ ROW 1 IS THE ANCHOR MINUS THE ROOT'S OWN INSET, and the subtraction is the whole point.
|
|
743
|
+
`--deck-anchor-third` is a canvas coordinate — 360px, measured from the top of the SLIDE. The
|
|
744
|
+
grid box does not start there; it starts at the text inset, because this layout is an ordinary
|
|
745
|
+
padded box now. So the spacer only has to cover what is left between them. Naming the anchor
|
|
746
|
+
bare here would place the column at 456.
|
|
747
|
+
|
|
748
|
+
It was bare, and correct, while the token was `calc(100% / 3)` and this layout zeroed its own
|
|
749
|
+
vertical padding to make the grid box and the canvas the same thing. Both halves of that went
|
|
750
|
+
together: a percentage needs the canvas underneath it, and a layout with no padding makes every
|
|
751
|
+
region inside it responsible for its own edge clearance. See the note at the top of this
|
|
752
|
+
stylesheet for the bug that came out of it.
|
|
753
|
+
|
|
754
|
+
`.split-figure` spans BOTH rows (below) rather than living in one alongside the spacer, so it
|
|
755
|
+
still fills its own cell top to bottom — a photo must not float with dead space above it just
|
|
756
|
+
because the TEXT column has a fixed start. It needs no margin of its own to do that: the cell's
|
|
757
|
+
top edge already sits at the safe line. Where it reaches PAST that line — a notched figure going
|
|
758
|
+
to the chrome inset — it does so with a negative MARGIN, not padding, which would draw inside
|
|
759
|
+
`.is-photo`'s own border-radius/overflow box and shrink the visible photograph rather than
|
|
760
|
+
positioning the frame in its cell (and cannot be negative at all).
|
|
761
|
+
|
|
762
|
+
`column-gap` only, never the `gap` shorthand: `gap` sets row-gap too, and a row-gap between rows
|
|
763
|
+
1 and 2 would push row 2's start past the anchor, throwing off the one figure this whole
|
|
764
|
+
mechanism exists to hit exactly.
|
|
765
|
+
|
|
766
|
+
★★ THE HEADER/FOOTER DEFECT, and why there is a THIRD row now (task-15, fixing what task-14b
|
|
767
|
+
shipped). Back when the anchor was `calc(100% / 3)` it resolved against THIS grid's box — not
|
|
768
|
+
against the canvas. task-14b arranged for those to be the same thing by zeroing the layout's
|
|
769
|
+
vertical padding, but only for a bare split: `::footer::` used to render as a flex SIBLING of
|
|
770
|
+
this grid, below it, so on a slide carrying one the grid was shorter than the canvas and a third
|
|
771
|
+
of it landed somewhere else entirely — the text column started at y ~ 400, not 360. The anchor
|
|
772
|
+
exists so "the headings dont jump between slides" (Jose); the token was a contract that held
|
|
773
|
+
only over the input it was tested on.
|
|
774
|
+
|
|
775
|
+
A pixel anchor is immune to that particular failure, but the row placement below is NOT
|
|
776
|
+
decoration left over from it: the footer is still a grid item because an `auto` row is the only
|
|
777
|
+
thing that keeps a footer's height off the flexible row above it.
|
|
778
|
+
|
|
779
|
+
The fix is to place the slot INSIDE the grid instead of beside it, so nothing can shorten it:
|
|
780
|
+
|
|
781
|
+
row 1 --deck-anchor-third empty now that there is no header slot. Kept as a fixed length
|
|
782
|
+
because it IS the anchor: `.split-content` starts where this row
|
|
783
|
+
ends, and `align` is the prop that collapses it on purpose.
|
|
784
|
+
row 2 minmax(0, 1fr) `.split-content` — its box starts exactly at the anchor.
|
|
785
|
+
row 3 auto the footer, spanning both columns. An `auto` row takes its
|
|
786
|
+
height from the FLEXIBLE row above it, never from row 1's fixed
|
|
787
|
+
length, so a footer cannot move the anchor either.
|
|
788
|
+
*/
|
|
789
|
+
/*
|
|
790
|
+
★ THE ANCHOR MINUS THE INSET, because the grid now starts at the padded origin rather than at
|
|
791
|
+
the canvas edge. `--deck-anchor-third` is a CANVAS position (360) — that is what the ruler draws
|
|
792
|
+
and what scripts/deckAudit.ts asserts — so inside a box already inset by the text inset, row 1
|
|
793
|
+
is the difference. Written as the subtraction rather than as 264 so retuning either token moves
|
|
794
|
+
it, and so the reader can see which two numbers it is between.
|
|
795
|
+
*/
|
|
796
|
+
grid-template-rows: calc(var(--deck-anchor-third) - var(--deck-inset-text)) minmax(0, 1fr) auto;
|
|
797
|
+
}
|
|
798
|
+
/*
|
|
799
|
+
★★ THERE IS NO `header` SLOT, AND THE REMOVAL IS THE DESIGN DECISION IT LOOKS LIKE. It existed to
|
|
800
|
+
put a title in its own band above the content row, and it did two different things depending on
|
|
801
|
+
`frame` — spanning both columns at `content`, confined to the content column otherwise.
|
|
802
|
+
|
|
803
|
+
Both jobs turned out to be covered better elsewhere. A full-width title over an evenly divided
|
|
804
|
+
body is `default` plus a two-column `::grid`: measured against the split version, same title
|
|
805
|
+
position, same column top, and grid's columns 18px wider for having the narrower gutter — and the
|
|
806
|
+
h1 spans because an h1 spans, not because of a conditional rule. A title above the content column
|
|
807
|
+
with a figure running full height beside it is `align: start`: measured on the same content and the
|
|
808
|
+
same map, the two differed by 40px of title position and nothing else.
|
|
809
|
+
|
|
810
|
+
It also carried real weight for one slide's use: a conditional span, a row-sizing rule, a branch in
|
|
811
|
+
the audit's anchor gate, and a constant in that gate predicting where the content would start —
|
|
812
|
+
which assumed a one-line title, failed on a two-line one, and cost a misdiagnosis that outlived it.
|
|
813
|
+
*/
|
|
814
|
+
.split-footer {
|
|
815
|
+
grid-row: 3;
|
|
816
|
+
grid-column: 1 / -1;
|
|
817
|
+
}
|
|
818
|
+
/*
|
|
819
|
+
THE CONDITIONAL GUTTER. `frame` tells split.vue which case this is — `photo`/`panel`/`figure`
|
|
820
|
+
mean a real figure (mixed: text meets media, no gutter, the default above, with the clearance
|
|
821
|
+
living in the content column's padding instead); `content` means the far slot is a second CONTENT
|
|
822
|
+
column (same-kind: text meets text), which takes a real gutter because there is no figure edge to
|
|
823
|
+
separate the two.
|
|
824
|
+
|
|
825
|
+
★ 96, NOT THE 36 task-14b ASKED FOR. Jose's original rule was "both sides text or both sides
|
|
826
|
+
image: gutter 36px", and it stood until the ruler made the pairing visible: 36px is 1.00em at the
|
|
827
|
+
deck's 36px body, while text beside a figure was getting 2.67em. The case with no edge between
|
|
828
|
+
the columns had less than half the air of the case that has one. Both are `--deck-inset-text`
|
|
829
|
+
now — see the token's own note in scripts/deck.ts.
|
|
830
|
+
*/
|
|
831
|
+
.is-content .split-grid {
|
|
832
|
+
column-gap: var(--deck-split-gutter-same);
|
|
833
|
+
}
|
|
834
|
+
/*
|
|
835
|
+
── WHERE THE CONTENT COLUMN SITS VERTICALLY ───────────────────────────────────────────────────
|
|
836
|
+
Row 1 is the anchor: at `anchor` it is the one-third line and the content starts there. The other
|
|
837
|
+
three collapse it to 0 so row 2 spans the whole frame, and then `align-self` decides where in that
|
|
838
|
+
space the column sits. Three of the four therefore share one row change and differ by one line.
|
|
839
|
+
|
|
840
|
+
★ THE FIGURE DOES NOT MOVE, and that is what makes this safe to add. `.split-figure` spans
|
|
841
|
+
`grid-row: 1 / 3`, so it fills the frame no matter what row 1 is — a map stays full-height beside
|
|
842
|
+
a heading pushed to the bottom. Measured on all four values before this was committed.
|
|
843
|
+
|
|
844
|
+
*/
|
|
845
|
+
.is-align-center .split-grid,
|
|
846
|
+
.is-align-end .split-grid {
|
|
847
|
+
grid-template-rows: 0 minmax(0, 1fr) auto;
|
|
848
|
+
}
|
|
849
|
+
/*
|
|
850
|
+
★★ `start` TAKES THE TEXT INSET, NOT 0, AND THE FIRST CUT GOT THIS WRONG. Collapsing row 1 to
|
|
851
|
+
zero for all three put the heading at canvas y 0 — hard against the edge with no clearance at
|
|
852
|
+
all — because split's ROOT carries no vertical padding: the anchor row was the only thing holding
|
|
853
|
+
the column off the top, so removing it removed the inset too. Measured before the fix: heading top
|
|
854
|
+
0 against the 96px every other edge of the same column keeps.
|
|
855
|
+
|
|
856
|
+
center and end are unaffected by that trap: they push the column away from the top edge anyway,
|
|
857
|
+
and its own padding-bottom holds the other end.
|
|
858
|
+
*/
|
|
859
|
+
/*
|
|
860
|
+
★ ZERO, NOT THE TEXT INSET — the grid already starts at it. This was `var(--deck-inset-text)` back
|
|
861
|
+
when split's root had no vertical padding and row 1 was the only thing holding the column off the
|
|
862
|
+
edge. The root pads now, so naming the inset here applied it twice and put the column at 192.
|
|
863
|
+
`start` means "no anchor row at all"; the clearance is the root's, like every other layout's.
|
|
864
|
+
*/
|
|
865
|
+
/*
|
|
866
|
+
`body-center` collapses the anchor row for the same reason `start` does, and needs it more: its
|
|
867
|
+
whole premise is that the HEADING holds the top, so leaving the one-third row in place starts the
|
|
868
|
+
column 224px down and the free space it means to distribute is spent before it begins. Measured
|
|
869
|
+
before this rule: the column opened at 360 and its last block ran to 1014, 30px past the safe line.
|
|
870
|
+
*/
|
|
871
|
+
.is-align-start .split-grid,
|
|
872
|
+
.is-align-body-center .split-grid {
|
|
873
|
+
grid-template-rows: 0 minmax(0, 1fr) auto;
|
|
874
|
+
}
|
|
875
|
+
/*
|
|
876
|
+
★ `safe`, BECAUSE PLAIN `center` OVERFLOWS BOTH ENDS. Row 2 is `minmax(0, 1fr)` against a fixed
|
|
877
|
+
frame, so it never grows to fit the column: a column taller than 888 centres by running half its
|
|
878
|
+
surplus PAST THE TOP INSET, and the h1's own border box is the first thing outside the safe area.
|
|
879
|
+
Caught on the bled-code slide, whose three-line heading makes the column 892.75 — 2.375px out at
|
|
880
|
+
each end, which the audit prints as "2px past the top inset". `safe` falls back to `start` only
|
|
881
|
+
when the subject overflows, so a column that fits is untouched (measured: fitting column, top 244
|
|
882
|
+
either way; overflowing column, -2.375 unsafe against 0 safe).
|
|
883
|
+
|
|
884
|
+
★ THE SURPLUS THEN ALL GOES TO THE BOTTOM, where the last block's own margin absorbs it — 16px on
|
|
885
|
+
a `p`, which is what every one of these slides ends in. A column ending in a block with NO bottom
|
|
886
|
+
margin would put the whole surplus past 984 rather than half of it.
|
|
887
|
+
*/
|
|
888
|
+
.is-align-center .split-content { align-self: safe center; }
|
|
889
|
+
.is-align-end .split-content { align-self: end; }
|
|
890
|
+
/*
|
|
891
|
+
★★ `body-center` — THE HEADING HOLDS THE TOP AND EVERYTHING UNDER IT CENTRES IN WHAT IS LEFT. It is
|
|
892
|
+
default.vue's mode of the same name, on this layout's own column, and it pairs with the panel's
|
|
893
|
+
`title-center`: set both and the two halves of a split read the same way — a title at the top of
|
|
894
|
+
the slide, the substance centred in the space below it on each side.
|
|
895
|
+
|
|
896
|
+
The three autos are default.vue's, and so is the reason for the third. Flexbox splits free space
|
|
897
|
+
EQUALLY between auto margins, so the ratio comes from HOW MANY there are rather than from a
|
|
898
|
+
length: two give 1:1, and the `::after` makes it 1:2, which rides the body one sixth of the free
|
|
899
|
+
space above centre. The eye judges centring against the FRAME, and the heading occupying the top
|
|
900
|
+
is what a plain split cannot know about.
|
|
901
|
+
|
|
902
|
+
★ THE COLUMN MUST STRETCH, which is why no `align-self` is set here. The two rungs above pin the
|
|
903
|
+
column to a point in the row, leaving it its own height; auto margins need the row's full height
|
|
904
|
+
or there is no free space for them to absorb and the mode silently degrades to `start`.
|
|
905
|
+
|
|
906
|
+
★ GATED ON A HEADING, by construction rather than by a guard: with no `h1` the sibling selector
|
|
907
|
+
matches nothing, `:has()` is false, no auto is handed out, and the column stays top-anchored —
|
|
908
|
+
which is right, because there is no heading holding the top to centre underneath.
|
|
909
|
+
*/
|
|
910
|
+
.is-align-body-center .split-content {
|
|
911
|
+
display: flex;
|
|
912
|
+
flex-direction: column;
|
|
913
|
+
}
|
|
914
|
+
.is-align-body-center .split-content > :is(h1, hgroup) + * { margin-top: auto; }
|
|
915
|
+
.is-align-body-center .split-content:has(> :is(h1, hgroup)) > :last-child { margin-bottom: auto; }
|
|
916
|
+
.is-align-body-center .split-content:has(> :is(h1, hgroup))::after {
|
|
917
|
+
content: '';
|
|
918
|
+
margin-top: auto;
|
|
919
|
+
}
|
|
920
|
+
/* The panel's own vertical alignment — see `panelAlign` for why it is a second prop rather than a
|
|
921
|
+
fifth value of `align`. All four are written out, and the base `.is-panel` rule below no longer
|
|
922
|
+
carries `align-items`, so this is the ONE place a panel's cross-axis is decided. */
|
|
923
|
+
.is-panel-align-start .split-figure.is-panel { align-items: flex-start; }
|
|
924
|
+
.is-panel-align-center .split-figure.is-panel { align-items: center; }
|
|
925
|
+
.is-panel-align-end .split-figure.is-panel { align-items: flex-end; }
|
|
926
|
+
/*
|
|
927
|
+
★★ THE TWO TITLE RUNGS ARE ONE CONSTRUCTION, and the only difference is what they do with the
|
|
928
|
+
space under the band. The panel becomes a COLUMN: the band is a `::before` item, then
|
|
929
|
+
`title-start` parks what follows at the top of it, while `title-center` hands the remainder to
|
|
930
|
+
three auto margins that flexbox splits equally — 1:2, the same sixth of a lift `align: body-center`
|
|
931
|
+
takes, which is the whole reason those two are usable as a pair.
|
|
932
|
+
|
|
933
|
+
★ THE BAND IS MEASURED; the value here is the FALLBACK. It paints the first frame and is what an
|
|
934
|
+
export gets if the script never runs — see the script's own note for why it cannot be derived.
|
|
935
|
+
|
|
936
|
+
⚠ COLUMN MEANS THE AXES SWAP. Horizontal alignment moves from `justify-content` to `align-items`,
|
|
937
|
+
and the autos need a main axis to absorb, so both are restated here rather than left to the base
|
|
938
|
+
rule, which is a row.
|
|
939
|
+
|
|
940
|
+
⚠ HORIZONTAL IS UNTOUCHED — these rungs centre like every other panel. Making them `stretch`
|
|
941
|
+
instead was a real defect and the ladder is what caught it: the three plain rungs shrink-wrap their
|
|
942
|
+
occupant to 206px and these two blew it out to the full 860, so a ladder meant to move ONE variable
|
|
943
|
+
moved position AND width, and a reader could not tell which they were seeing. A half that needs to
|
|
944
|
+
fill its width asks for it where that belongs — Grid's `justify`, which takes the width it needs to
|
|
945
|
+
centre across.
|
|
946
|
+
|
|
947
|
+
There is no `title-end`: the band would sit above an item already pinned to the bottom, so it would
|
|
948
|
+
render identically to `end`. Simulated before ruling it out.
|
|
949
|
+
*/
|
|
950
|
+
.is-panel-align-title-start .split-figure.is-panel,
|
|
951
|
+
.is-panel-align-title-center .split-figure.is-panel {
|
|
952
|
+
flex-direction: column;
|
|
953
|
+
align-items: center;
|
|
954
|
+
justify-content: flex-start;
|
|
955
|
+
}
|
|
956
|
+
.is-panel-align-title-start .split-figure.is-panel::before,
|
|
957
|
+
.is-panel-align-title-center .split-figure.is-panel::before {
|
|
958
|
+
content: '';
|
|
959
|
+
flex: none;
|
|
960
|
+
height: var(--panel-align-band, calc(
|
|
961
|
+
var(--type-deck-display-size) * var(--type-deck-display-line-height) + var(--deck-heading-bottom-space)
|
|
962
|
+
));
|
|
963
|
+
}
|
|
964
|
+
.is-panel-align-title-center .split-figure.is-panel > * {
|
|
965
|
+
margin-top: auto;
|
|
966
|
+
margin-bottom: auto;
|
|
967
|
+
}
|
|
968
|
+
.is-panel-align-title-center .split-figure.is-panel::after {
|
|
969
|
+
content: '';
|
|
970
|
+
margin-top: auto;
|
|
971
|
+
}
|
|
972
|
+
.split-content {
|
|
973
|
+
/* Row 2 only — this is what makes the text column start exactly at the anchor line above. */
|
|
974
|
+
grid-row: 2;
|
|
975
|
+
grid-column: var(--split-content-col);
|
|
976
|
+
}
|
|
977
|
+
/*
|
|
978
|
+
The text column's INNER inset, against a figure.
|
|
979
|
+
|
|
980
|
+
Jose's rule is two clauses, and only the first was implemented: "text one side, image other side:
|
|
981
|
+
no gutter, and text side should have 90px margin horizontally". The zero gutter is a property of
|
|
982
|
+
the GRID (column-gap: 0, above); it says nothing about what the text column owes on its own edges.
|
|
983
|
+
The outer edge gets --deck-inset-text for free from the layout root's horizontal padding, so the
|
|
984
|
+
omission was invisible there — but the inner edge sits on the grid column boundary, which is
|
|
985
|
+
exactly the figure's edge. Measured on deck-template 18 before this rule: the figure ends at x=786
|
|
986
|
+
and `.split-content` began at x=786, so every line of body copy started flush against the
|
|
987
|
+
photograph while the same paragraph's right edge held a clean 90.
|
|
988
|
+
|
|
989
|
+
★★ WRITTEN AS `:not(.is-content)`, NOT AS A LIST OF THE FIGURE KINDS, and that is the whole
|
|
990
|
+
point of the rule rather than a tidier way to spell it. It WAS a list — `.is-photo, .is-panel` —
|
|
991
|
+
and `.is-figure` was simply never added to it, so text ran flush into a map with nothing between
|
|
992
|
+
them: content padding 0, and 0px from the last letter to the figure's edge, against the 96px every
|
|
993
|
+
photo and panel got. It hid while `content` was split's default, because those slides took the
|
|
994
|
+
36px same-kind gutter instead, which looked like clearance and is a different mechanism; making
|
|
995
|
+
`figure` the default (ae99c03) is what exposed it.
|
|
996
|
+
|
|
997
|
+
An allow-list of variants is a thing a new variant falls off, silently, and this one did. Every
|
|
998
|
+
other layout in this theme is immune to the same mistake by construction: stack puts its 40px
|
|
999
|
+
between band and strip on the GRID as a row-gap, quote puts its 120px on the flex container, and a
|
|
1000
|
+
gap on the parent is inherited by every variant there will ever be. Split cannot use a gap — the
|
|
1001
|
+
gutter is zero for a figure precisely BECAUSE the text column carries the clearance itself — so
|
|
1002
|
+
the next best thing is to state the ONE exception and let everything else be covered by default.
|
|
1003
|
+
|
|
1004
|
+
The exception is real: a `frame: content` split is text meeting text and already has a genuine
|
|
1005
|
+
96px gutter doing this job. Adding an inset there too would put 96 + 96 + 96 between two columns
|
|
1006
|
+
of prose — nearly eight ems, which is a chasm rather than a gutter.
|
|
1007
|
+
*/
|
|
1008
|
+
.is-left:not(.is-content) .split-content { padding-right: var(--deck-inset-text); }
|
|
1009
|
+
.is-right:not(.is-content) .split-content { padding-left: var(--deck-inset-text); }
|
|
1010
|
+
/*
|
|
1011
|
+
★★ THE ANCHOR ANCHORS THE HEADING, NOT THE COLUMN — and until this rule it only did the latter.
|
|
1012
|
+
|
|
1013
|
+
`.split-content`'s own top is y=360 on all 142 split slides in the three decks; the token is
|
|
1014
|
+
correct and always was. But an `eyebrow:` renders INSIDE that column, above the heading, so on a
|
|
1015
|
+
slide that carries one the HEADING landed 41px lower. Measured across deck-showcase's split
|
|
1016
|
+
slides: 112 with no eyebrow put their h1 at y=360, 12 with an eyebrow put it at y=401. Slides 13
|
|
1017
|
+
→ 16 and 41/42 → 43 are near-consecutive, so the jump is visible on the click-through. The
|
|
1018
|
+
anchor's stated reason is Jose's — "so the headings dont jump between slides" — and on an eyebrow
|
|
1019
|
+
slide they still jumped. The mechanism held; the purpose failed. (scripts/deckAudit.ts asserts
|
|
1020
|
+
`.split-content`'s own top and nothing else, which is why it reported 0 anchor failures.)
|
|
1021
|
+
|
|
1022
|
+
The eyebrow is therefore HUNG ABOVE THE ANCHOR: it is pulled up by exactly its own block
|
|
1023
|
+
(--deck-eyebrow-block, base.css — line box + the gap it holds below itself), so the element after
|
|
1024
|
+
it starts at the anchor whether it is there or not. Two properties this deliberately keeps:
|
|
1025
|
+
|
|
1026
|
+
· `.split-content`'s own box top is UNMOVED at 360 — the negative margin is on the eyebrow, not
|
|
1027
|
+
on the column — so every existing measurement of the anchor still reads 360.
|
|
1028
|
+
· The eyebrow does not move when the heading changes size, because the lift is the eyebrow's own
|
|
1029
|
+
height, not the heading's.
|
|
1030
|
+
|
|
1031
|
+
*/
|
|
1032
|
+
.split-content > .deck-eyebrow:first-child {
|
|
1033
|
+
margin-top: calc(-1 * var(--deck-eyebrow-block));
|
|
1034
|
+
}
|
|
1035
|
+
.split-figure {
|
|
1036
|
+
/*
|
|
1037
|
+
★★ A DIAGRAM IN HERE GETS THE COLUMN'S HEIGHT, NOT THE DEFAULT LAYOUT'S. mermaid's fitToColumn
|
|
1038
|
+
caps a tall diagram against a height budget it cannot measure for itself, so it reads this
|
|
1039
|
+
variable and falls back to the 720 a `default` slide leaves under its heading. A figure column
|
|
1040
|
+
carries no heading above it: 1080 canvas less the inset at each end. For a height-bound diagram
|
|
1041
|
+
— every sequence diagram is one — that is a quarter more of the whole drawing.
|
|
1042
|
+
*/
|
|
1043
|
+
--deck-diagram-max-h: calc(1080px - 2 * var(--deck-inset-text));
|
|
1044
|
+
/* Rows 1-2 — see the maths note on `.split-grid` above for why this is what "fills its cell top
|
|
1045
|
+
to bottom" actually means once the text column has its own fixed start. Explicitly `1 / 3`,
|
|
1046
|
+
not `1 / -1`: with the footer's own row now in the template, `-1` is the line AFTER it, and a
|
|
1047
|
+
figure spanning into the footer's row would be drawn straight over any footer content. */
|
|
1048
|
+
grid-row: 1 / 3;
|
|
1049
|
+
grid-column: var(--split-figure-col);
|
|
1050
|
+
/*
|
|
1051
|
+
★★ ZERO, BECAUSE THE ROOT'S OWN PADDING IS THE CLEARANCE NOW. These were both
|
|
1052
|
+
`var(--deck-inset-text)` — the figure paying its own way in a layout whose root had no vertical
|
|
1053
|
+
padding. With the root inset like every other layout, repeating it here would inset the figure
|
|
1054
|
+
twice and land it at 192.
|
|
1055
|
+
*/
|
|
1056
|
+
margin-top: 0;
|
|
1057
|
+
margin-bottom: 0;
|
|
1058
|
+
}
|
|
1059
|
+
/*
|
|
1060
|
+
★★ THE ANCHOR APPLIES UNIFORMLY — task-18-brief.md's ruling, replacing task-15's header-only
|
|
1061
|
+
scoping.
|
|
1062
|
+
|
|
1063
|
+
A `frame: content` figure side is documented as "nothing (a second content column)" — an author's own
|
|
1064
|
+
content, not a figure. It used to span rows 1-2 like a real photo, which put its top at
|
|
1065
|
+
--deck-inset-text (90px) while the text column beside it started at the anchor (360px): two
|
|
1066
|
+
columns of the same KIND, on the same slide, starting 270px apart.
|
|
1067
|
+
|
|
1068
|
+
task-15 built this fix and then reverted it to the header-only case, because the anchor caps a
|
|
1069
|
+
`frame: content` column at 630px instead of 900px and apps/deck-showcase #188 (a card grid in the
|
|
1070
|
+
figure slot) runs to y 1218 with the cap on, y 990 without — probed live, both ways. The ruling
|
|
1071
|
+
reverses that: the anchor exists so "the headings dont jump between slides" (Jose), and a rule
|
|
1072
|
+
whose position depends on what happens to be in the OTHER slot fails that purpose on its own
|
|
1073
|
+
terms. A heading is at 360 or it is not; "360 unless the neighbour is a card grid" is not an
|
|
1074
|
+
anchor. The overflow is one slide's content being too tall for the slot, which is a content
|
|
1075
|
+
problem with a content fix, and is deliberately NOT resolved by weakening the rule here.
|
|
1076
|
+
|
|
1077
|
+
Real photos and panels are unaffected — they still span rows 1-2 and fill their cell, because a
|
|
1078
|
+
photograph floating with dead space above it is a different (and wrong) thing. This is only ever
|
|
1079
|
+
about a second column of CONTENT.
|
|
1080
|
+
|
|
1081
|
+
*/
|
|
1082
|
+
.is-content .split-figure {
|
|
1083
|
+
/*
|
|
1084
|
+
The row is the whole rule now. It also said `margin-top: 0`, which was live while `.split-figure`
|
|
1085
|
+
carried the text inset — and is dead the moment the base is 0 too: the only rule that raises a
|
|
1086
|
+
figure is `.has-notch`, and hasNotch() requires a PANEL frame, so it can never match this
|
|
1087
|
+
selector. A declaration that restates the value it is already inheriting reads as a live override
|
|
1088
|
+
to the next person who moves the base, which is worse than not being there.
|
|
1089
|
+
*/
|
|
1090
|
+
grid-row: 2;
|
|
1091
|
+
}
|
|
1092
|
+
.is-1-1 .split-grid { grid-template-columns: 1fr 1fr; }
|
|
1093
|
+
/*
|
|
1094
|
+
Every other ratio is content:figure, and the grid's two tracks are POSITIONAL — track 1 is
|
|
1095
|
+
whichever element `order` (below) places there, not "the content one". So the fr values have
|
|
1096
|
+
to swap with `flip` for the share documented on `ratio` to stay attached to CONTENT rather than
|
|
1097
|
+
to a screen position.
|
|
1098
|
+
|
|
1099
|
+
Verified live before this fix: three of the six (ratio, side) combinations that differ were
|
|
1100
|
+
backwards — `ratio="2:3"` at the default `side="left"` put content in the WIDER track (the
|
|
1101
|
+
opposite of "2:3" read as content:figure), because a single un-keyed `.is-2-3` rule fixed the
|
|
1102
|
+
track sizes regardless of which element `order` had moved into them.
|
|
1103
|
+
*/
|
|
1104
|
+
.is-3-2.is-left .split-grid { grid-template-columns: 3fr 2fr; }
|
|
1105
|
+
.is-3-2.is-right .split-grid { grid-template-columns: 2fr 3fr; }
|
|
1106
|
+
.is-2-3.is-left .split-grid { grid-template-columns: 2fr 3fr; }
|
|
1107
|
+
.is-2-3.is-right .split-grid { grid-template-columns: 3fr 2fr; }
|
|
1108
|
+
.is-2-1.is-left .split-grid { grid-template-columns: 2fr 1fr; }
|
|
1109
|
+
.is-2-1.is-right .split-grid { grid-template-columns: 1fr 2fr; }
|
|
1110
|
+
.is-1-2.is-left .split-grid { grid-template-columns: 1fr 2fr; }
|
|
1111
|
+
.is-1-2.is-right .split-grid { grid-template-columns: 2fr 1fr; }
|
|
1112
|
+
.is-3-1.is-left .split-grid { grid-template-columns: 3fr 1fr; }
|
|
1113
|
+
.is-3-1.is-right .split-grid { grid-template-columns: 1fr 3fr; }
|
|
1114
|
+
/* `flip` is a pure mirror, and it is expressed ONLY as the two column numbers on the root above —
|
|
1115
|
+
there is no `order` here any more. `order` re-sequences AUTO-placed items, and header/footer are
|
|
1116
|
+
now placed explicitly by line (they have to be: they belong to particular rows), so a mixture of
|
|
1117
|
+
the two would have placed some items by order and others by line against the same tracks. */
|
|
1118
|
+
/*
|
|
1119
|
+
task-14-brief.md, numbers revised by task-14b-brief.md — THE FIGURE ESCAPES TO THE MEDIA INSET,
|
|
1120
|
+
HORIZONTALLY, ON ITS OUTER EDGE ONLY.
|
|
1121
|
+
|
|
1122
|
+
Text is inset for its measure (--deck-inset-text, 90px, from `.slidev-layout`'s own padding —
|
|
1123
|
+
see base.css); media is inset for its frame (--deck-inset-media, 36px) — a photo's hard edge
|
|
1124
|
+
reads as inset too far at the value that reads correctly around a paragraph. `.split-grid` is a
|
|
1125
|
+
descendant of `.slidev-layout` and inherits that 90px padding HORIZONTALLY (vertical is zeroed
|
|
1126
|
+
above, for the anchor), same as the content column, so the figure needs to CLAW BACK the 54px
|
|
1127
|
+
difference on its own outer edge only — the inner edge (against the gap that separates it from
|
|
1128
|
+
content) stays put, and the CONTENT side never moves at all.
|
|
1129
|
+
|
|
1130
|
+
A negative margin is the mechanism: `.split-figure` sits inside a `fr`-sized grid track, so a
|
|
1131
|
+
margin shifts the rendered box without feeding back into track sizing (unlike an `auto` track,
|
|
1132
|
+
where a margin would resize the column itself). `margin-right`/`margin-left`, not `margin-inline
|
|
1133
|
+
-end/-start`: which PHYSICAL side is "outer" flips with `flip` (see `--split-figure-col` on the
|
|
1134
|
+
root) but never with writing direction, so the physical property is correct and the logical one
|
|
1135
|
+
would be wrong here.
|
|
1136
|
+
|
|
1137
|
+
Scoped to the three FIGURE kinds — `.is-photo`, `.is-panel`, `.is-figure` — and NOT `.is-content`:
|
|
1138
|
+
`frame: content` is documented as "nothing (a second content column)" — an author's own content, not
|
|
1139
|
+
a figure, and content wants the TEXT inset it already has. Escaping it to the media inset would
|
|
1140
|
+
narrow its measure on the very edge the whole point of `--deck-inset-text` is to protect.
|
|
1141
|
+
*/
|
|
1142
|
+
.is-left .split-figure.is-photo,
|
|
1143
|
+
.is-left .split-figure.is-panel,
|
|
1144
|
+
.is-left .split-figure.is-code,
|
|
1145
|
+
.is-left .split-figure.is-figure {
|
|
1146
|
+
margin-right: calc(-1 * (var(--deck-inset-text) - var(--deck-inset-media)));
|
|
1147
|
+
}
|
|
1148
|
+
.is-right .split-figure.is-photo,
|
|
1149
|
+
.is-right .split-figure.is-panel,
|
|
1150
|
+
.is-right .split-figure.is-code,
|
|
1151
|
+
.is-right .split-figure.is-figure {
|
|
1152
|
+
margin-left: calc(-1 * (var(--deck-inset-text) - var(--deck-inset-media)));
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/*
|
|
1156
|
+
★★ `bleed` — THE SAME FIGURE, AT THE CANVAS EDGE. FOUR DECLARATIONS, AND NO TREATMENT OF ITS OWN.
|
|
1157
|
+
|
|
1158
|
+
Everything above claws back the DIFFERENCE between the text inset and the media inset, and stops at
|
|
1159
|
+
36. This gives up the whole text inset instead, so the figure's outer, top and bottom edges are the
|
|
1160
|
+
canvas edges: 0, 0 and 1080. The frame keeps whatever it already was — a photo still crops, a panel
|
|
1161
|
+
still carries its fill and padding, a component figure is still centred and capped — which is the
|
|
1162
|
+
point of making this a modifier rather than a fifth `frame` value.
|
|
1163
|
+
|
|
1164
|
+
The card radius comes off because a radius is what a thing sitting ON something has; flush to the
|
|
1165
|
+
canvas there is no ground for it to sit on and the corner would read as a mistake.
|
|
1166
|
+
|
|
1167
|
+
⚠ SCOPED AWAY FROM `.is-content`. That kind is a second column of the author's PROSE, and running
|
|
1168
|
+
body copy to the canvas edge is the one thing this must never do — measured on a first draft that
|
|
1169
|
+
did exactly that, the paragraph ran off the right of the slide and under the running mark.
|
|
1170
|
+
|
|
1171
|
+
The INNER edge is untouched: it still meets the content column's gutter, because a figure that bled
|
|
1172
|
+
inwards would run under the text rather than beside it.
|
|
1173
|
+
*/
|
|
1174
|
+
.is-left .split-figure.is-bleed:not(.is-content) { margin-right: calc(-1 * var(--deck-inset-text)); }
|
|
1175
|
+
.is-right .split-figure.is-bleed:not(.is-content) { margin-left: calc(-1 * var(--deck-inset-text)); }
|
|
1176
|
+
.split-figure.is-bleed:not(.is-content) {
|
|
1177
|
+
border-radius: 0;
|
|
1178
|
+
margin-top: calc(-1 * var(--deck-inset-text));
|
|
1179
|
+
}
|
|
1180
|
+
/* Scoped the way the notch's bottom escape is: a `::footer::` occupies the row below, so a figure
|
|
1181
|
+
bleeding past it would run under the footer rather than to the canvas. */
|
|
1182
|
+
.split-grid:not(:has(.split-footer)) .split-figure.is-bleed:not(.is-content) {
|
|
1183
|
+
margin-bottom: calc(-1 * var(--deck-inset-text));
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/*
|
|
1187
|
+
★★ THE SURFACE BLEEDS; THE CONTENT INSIDE IT MUST NOT.
|
|
1188
|
+
|
|
1189
|
+
Giving up the inset moves the box, and the padding rides along with it — so a bleeding panel put
|
|
1190
|
+
its content 36px closer to the slide edge than the same panel unbled, and a bleeding figure put its
|
|
1191
|
+
component right on the canvas edge. Measured, `frame: panel` at ratio 1:1: notched, the content box
|
|
1192
|
+
runs to x 1852; bleeding, to 1888. The frame is supposed to look identical apart from its own
|
|
1193
|
+
edges, and it did not.
|
|
1194
|
+
|
|
1195
|
+
So each bled edge gives the inset back as PADDING. The surface still reaches the canvas; the
|
|
1196
|
+
content lands exactly where it would have without the bleed. Written per frame because the two
|
|
1197
|
+
start from different places — a panel already insets its content by --deck-card-pad and a figure
|
|
1198
|
+
by nothing — and adding the same number to both would put the figure 32px in from where its
|
|
1199
|
+
unbled version sits.
|
|
1200
|
+
|
|
1201
|
+
`photo` is deliberately absent: an edge-to-edge photograph is the whole point of bleeding one, and
|
|
1202
|
+
padding it would show the ground behind. `content` is absent because it never bleeds at all.
|
|
1203
|
+
|
|
1204
|
+
⚠ THE TOP IS ONLY HALF-ANSWERED HERE. These values assume nothing is above the surface. When the
|
|
1205
|
+
chrome row IS drawn there is a mark to clear, and the rule that raises this lives in styles/base.css
|
|
1206
|
+
— a layout's own scoped CSS cannot see `.slidev-page`, which is where the chrome is.
|
|
1207
|
+
*/
|
|
1208
|
+
/*
|
|
1209
|
+
★★ A BLEEDING PANEL TAKES THE REGION INSET (96), NOT THE CARD INSET (68).
|
|
1210
|
+
|
|
1211
|
+
These gave the bled edge its inset back as padding — card pad 32 + media inset 36 — so the content
|
|
1212
|
+
landed exactly where the same panel puts it unbled. Correct arithmetic, wrong premise: it treats a
|
|
1213
|
+
bleeding panel as a card that happens to reach the edge. It is not one. A card's interior is inset
|
|
1214
|
+
from its own EDGES, which are drawn; a bleeding panel has no drawn edge left, so its interior is
|
|
1215
|
+
measuring against the canvas and against the seam with the content column — which is exactly the
|
|
1216
|
+
situation a text column is in, and --deck-inset-text is the number for it.
|
|
1217
|
+
|
|
1218
|
+
MEASURED on the register-comparison split (specimen 124) before this, which is the slide that found
|
|
1219
|
+
it. Seam at 960:
|
|
1220
|
+
|
|
1221
|
+
left column 96 -> 864 96 from the canvas edge, 96 from the seam 768 wide
|
|
1222
|
+
right panel 1028 -> 1852 68 from the seam, 68 from the canvas edge 824 wide
|
|
1223
|
+
|
|
1224
|
+
Both halves internally symmetric, and 56px apart from each other — so the same paragraph wrapped to
|
|
1225
|
+
three lines on the left and two on the right, on a slide whose whole claim is that the register is
|
|
1226
|
+
the only thing that differs. At the region inset both halves are 768 and the comparison is exact.
|
|
1227
|
+
|
|
1228
|
+
⚠ Only when it BLEEDS. A panel sitting on the ground keeps --deck-card-pad: it has edges, and 96
|
|
1229
|
+
inside a 924px card is a frame with a stamp in it. Jose asked whether the two sides should match and
|
|
1230
|
+
what it would cost; the answer is 3 slides, all bleeding panels, because that is the only case where
|
|
1231
|
+
a panel stops being a card.
|
|
1232
|
+
*/
|
|
1233
|
+
.split-figure.is-bleed.is-panel { padding-top: var(--deck-inset-text); }
|
|
1234
|
+
.split-figure.is-bleed.is-figure { padding-top: var(--deck-inset-media); }
|
|
1235
|
+
/*
|
|
1236
|
+
★★ A BLEEDING PANEL PADS BOTH SIDES EQUALLY, WHICH IS NOT WHAT "GIVE THE INSET BACK" ALONE GIVES.
|
|
1237
|
+
|
|
1238
|
+
The rule above is per-EDGE, so only the bled edge got the inset back and the panel's interior came
|
|
1239
|
+
out lopsided: measured at ratio 1:1, content ran 992-1852, i.e. 32px from the seam and 68px from
|
|
1240
|
+
the canvas edge. Both of those are hard edges of the same box — one against the slide, one against
|
|
1241
|
+
the content column — and a fill with twice as much air on one side as the other reads as a mistake
|
|
1242
|
+
rather than as a bleed.
|
|
1243
|
+
|
|
1244
|
+
So the panel takes the bled value on BOTH sides. It is the panel, not the figure, because a panel
|
|
1245
|
+
is a visible box whose interior is read as a box: `is-figure` brings no fill, its inner edge meets
|
|
1246
|
+
the content column's own 96px, and padding it again would push a map 68px off a seam that is not
|
|
1247
|
+
drawn.
|
|
1248
|
+
|
|
1249
|
+
The cost is 36px of panel width, paid on the inner edge. Jose asked the question that found it.
|
|
1250
|
+
*/
|
|
1251
|
+
.split-figure.is-bleed.is-panel { padding-left: var(--deck-inset-text); padding-right: var(--deck-inset-text); }
|
|
1252
|
+
.is-left .split-figure.is-bleed.is-figure { padding-right: var(--deck-inset-media); }
|
|
1253
|
+
.is-right .split-figure.is-bleed.is-figure { padding-left: var(--deck-inset-media); }
|
|
1254
|
+
/* Bottom only where the bottom actually bled — same `::footer::` scope as the margin above. */
|
|
1255
|
+
.split-grid:not(:has(.split-footer)) .split-figure.is-bleed.is-panel { padding-bottom: var(--deck-inset-text); }
|
|
1256
|
+
.split-grid:not(:has(.split-footer)) .split-figure.is-bleed.is-figure { padding-bottom: var(--deck-inset-media); }
|
|
1257
|
+
|
|
1258
|
+
/*
|
|
1259
|
+
A COMPONENT FIGURE — centred in its cell, capped to it, and brings no surface of its own.
|
|
1260
|
+
|
|
1261
|
+
It needs no grid-row of its own: `.split-figure` already spans rows 1-2, and the only thing that
|
|
1262
|
+
ever moved it was `.is-content`'s deliberate drop to row 2. What it does need is for the component
|
|
1263
|
+
inside it to take the height, which a plain block child will not do on its own.
|
|
1264
|
+
|
|
1265
|
+
★ NO MATTE, and that is the difference from `panel`. A panel is a mounting board — a tinted card
|
|
1266
|
+
with the media centred inside its padding, which is right for a screenshot and wrong for a map
|
|
1267
|
+
that has its own ground and its own corner radius.
|
|
1268
|
+
|
|
1269
|
+
★★ AND IT CLAWS BACK THE 54px LIKE THE OTHER TWO, above. It is a FIGURE, so its outer edge takes
|
|
1270
|
+
--deck-inset-media (36px), not the text inset the layout pads with — measured before this was
|
|
1271
|
+
added, the map sat 90px from the canvas edge while its own top and bottom were at 36px, which is
|
|
1272
|
+
the inset system applied to three of a box's four sides.
|
|
1273
|
+
*/
|
|
1274
|
+
/*
|
|
1275
|
+
★★ CENTRED AND FITTED, NOT STRETCHED — and the difference only shows on a figure that keeps its
|
|
1276
|
+
own aspect. The child used to take `flex: 1`, so it grew to the full height of the cell; a map or
|
|
1277
|
+
a chart that will not distort then drew itself at its own aspect inside that stretched box and sat
|
|
1278
|
+
at the TOP of it. Measured on the specimen's `frame: figure` slide: cell 924x1008, child stretched
|
|
1279
|
+
to 1008, and the map's own svg 780x601 against the cell's top edge with 407px of void beneath it.
|
|
1280
|
+
|
|
1281
|
+
Centring the CELL could not have fixed that, because the child already filled the cell — the empty
|
|
1282
|
+
space was inside the child. The stretch had to go first.
|
|
1283
|
+
|
|
1284
|
+
So the child takes its natural size, is capped to the cell on both axes, and is centred in what is
|
|
1285
|
+
left. A figure that genuinely wants the whole cell asks for it the same way it always did, with
|
|
1286
|
+
its own `height` — `::deck-map` and every chart already do, and they are unaffected because their
|
|
1287
|
+
height is declared rather than inherited from a stretch.
|
|
1288
|
+
*/
|
|
1289
|
+
.split-figure.is-figure {
|
|
1290
|
+
display: flex;
|
|
1291
|
+
flex-direction: column;
|
|
1292
|
+
align-items: center;
|
|
1293
|
+
justify-content: center;
|
|
1294
|
+
}
|
|
1295
|
+
.split-figure.is-figure > :deep(*) {
|
|
1296
|
+
/*
|
|
1297
|
+
`width: 100%` so it can GROW, both maxes so it can never spill. Capping alone stopped a figure
|
|
1298
|
+
overflowing and left it at whatever size it declared — the borough map sat 780 wide in a 924
|
|
1299
|
+
cell and simply never used the other 144.
|
|
1300
|
+
*/
|
|
1301
|
+
width: 100%;
|
|
1302
|
+
max-width: 100%;
|
|
1303
|
+
max-height: 100%;
|
|
1304
|
+
min-height: 0;
|
|
1305
|
+
}
|
|
1306
|
+
/*
|
|
1307
|
+
══ SPILL ══════════════════════════════════════════════════════════════════════════════════════
|
|
1308
|
+
|
|
1309
|
+
★★ THE THREE LINES ABOVE ARE WHAT SPILL HAS TO UNDO. `width: 100%` plus both maxes is the rule
|
|
1310
|
+
that makes a figure FIT: it grows a small drawing up to the cell and stops a large one escaping.
|
|
1311
|
+
A spilling figure is the deliberate opposite — it is sized past its cell and cropped by the canvas
|
|
1312
|
+
— so it needs all three released, not one. Released and then RE-anchored: with the maxes gone and
|
|
1313
|
+
nothing saying which way the overflow leaves, a centred child overflows both sides equally and the
|
|
1314
|
+
figure reads as badly cropped rather than as running off one edge.
|
|
1315
|
+
|
|
1316
|
+
⚠ WHICH EDGE IT LEAVES BY IS THE COLUMN'S, NOT THE SLIDE'S. `.is-left` puts the FIGURE on the
|
|
1317
|
+
right (that is why its bleed rule pulls `margin-right`), so its content anchors to the inner edge
|
|
1318
|
+
and overflows outwards, and `.is-right` mirrors it. Anchored the other way round, the drawing runs
|
|
1319
|
+
under the text column instead of off the canvas.
|
|
1320
|
+
*/
|
|
1321
|
+
/*
|
|
1322
|
+
★★ THE CAPS COME OFF AND NOTHING IS IMPOSED. A spilling component knows its own size — a map's
|
|
1323
|
+
`width`, a device frame's `height` — and what the figure was doing was capping it. Sizing the child
|
|
1324
|
+
here instead (a flat 150%) made the map's own `width` prop silently inert, which is the same defect
|
|
1325
|
+
in the other direction: a number an author states and the layout overrides.
|
|
1326
|
+
*/
|
|
1327
|
+
/*
|
|
1328
|
+
⚠ THE WIDTH IS STATED HERE, and two other answers were measured before this one. `auto` made the
|
|
1329
|
+
map fall back to its intrinsic SVG size and render SMALLER than unspilled (350px in a 780px
|
|
1330
|
+
column). Leaving it alone kept the base rule's `width: 100%`, so the figure filled its cell exactly
|
|
1331
|
+
as it always had and nothing spilled at all (751px, right edge at 1884, inside the canvas). The
|
|
1332
|
+
base rule has to be beaten, so the ratio lives here — and a component's own width prop is then
|
|
1333
|
+
inert on a spilling figure, which is why the specimen's map states none.
|
|
1334
|
+
*/
|
|
1335
|
+
.split-figure.is-spill.is-figure > :deep(*) {
|
|
1336
|
+
width: var(--spill, 150%);
|
|
1337
|
+
max-width: none;
|
|
1338
|
+
max-height: none;
|
|
1339
|
+
}
|
|
1340
|
+
/*
|
|
1341
|
+
⚠ THE ROOM STOPS CENTRING ITS WINDOW, AND THAT IS THE WHOLE OF IT — the cap stays on, unlike the
|
|
1342
|
+
band's case in stack.vue.
|
|
1343
|
+
|
|
1344
|
+
The rule above widens the figure's direct child, which for a browser window is
|
|
1345
|
+
`.deck-browser-room`; the WINDOW is a grandchild, so it kept the room's horizontal centring.
|
|
1346
|
+
Measured on the right-edge spill: the room ran 672-2490 as asked and the window sat 827-2335
|
|
1347
|
+
inside it, so 155px of ground opened between the copy and a window that was supposed to start at
|
|
1348
|
+
the column's inner edge. Jose, on the ruler overlay: "whats with this padding here?"
|
|
1349
|
+
|
|
1350
|
+
★★ AND BREAKING THE CAP AS WELL WAS THE OVER-CORRECTION. `max-width: none; width: 100%` filled the
|
|
1351
|
+
room — and the room is 150% of a column, so at 16:9 the window came out 1085px tall in a 1008px
|
|
1352
|
+
box and left the canvas at the TOP as well as the sides. A split spills SIDEWAYS; the cap is what
|
|
1353
|
+
keeps the other axis honest, and BrowserFrame computes it from the room's own height for exactly
|
|
1354
|
+
this. Jose: "can you put padding top bottom? so it doesnt overflow at the top?" — the padding was
|
|
1355
|
+
always there, the window was ignoring it.
|
|
1356
|
+
|
|
1357
|
+
So the window is as big as the height allows and hard against the inner edge, and whatever that
|
|
1358
|
+
leaves over goes out the outer side, which is what `spill` means. A band spills DOWNWARD and so
|
|
1359
|
+
breaks the same cap; the two layouts differ because their crop axes do.
|
|
1360
|
+
*/
|
|
1361
|
+
.split-figure.is-spill :deep(.deck-browser-room) {
|
|
1362
|
+
align-items: flex-start;
|
|
1363
|
+
}
|
|
1364
|
+
/* ⚠ MEDIA IS THE EXCEPTION, because a photograph has no size of its own worth honouring — an <img>
|
|
1365
|
+
released from its caps is its intrinsic 8917px. A spilling photo frame oversizes by a RATIO of the
|
|
1366
|
+
column and keeps the cover crop. */
|
|
1367
|
+
.split-figure.is-spill.is-photo > :deep(img) {
|
|
1368
|
+
width: var(--spill, 150%);
|
|
1369
|
+
max-width: none;
|
|
1370
|
+
height: 100%;
|
|
1371
|
+
object-fit: cover;
|
|
1372
|
+
}
|
|
1373
|
+
/*
|
|
1374
|
+
★★ AND THE CROP KEEPS THE FAR SIDE OF THE PICTURE. Jose, on the flipped slide: "if the edge spill
|
|
1375
|
+
and flip true, the image should be put showing the right side of the image... otherwise it will cut
|
|
1376
|
+
off the wrong part".
|
|
1377
|
+
|
|
1378
|
+
⚠ TWO PROPERTIES, AND ONLY ONE OF THEM IS `object-position` — which is the trap. It picks which
|
|
1379
|
+
part of the SOURCE fills the img's box; it says nothing about which part of that BOX survives the
|
|
1380
|
+
figure's clip. Set alone it looked right in the CSS and wrong on the slide: measured on the flipped
|
|
1381
|
+
spill, the figure ran 0 → 787 with the img at 0 → 1181, so the img hung off the figure's RIGHT edge
|
|
1382
|
+
and the visible 787 was its left end — the exact half the composition is meant to lose.
|
|
1383
|
+
|
|
1384
|
+
So the box is anchored first: on a figure that leaves by the LEFT edge the img is pushed to the
|
|
1385
|
+
figure's right, and the overflow goes left. Then `object-position` sends the source's own crop the
|
|
1386
|
+
same way, so the side that stays on the slide is the side that stays in the picture.
|
|
1387
|
+
*/
|
|
1388
|
+
.is-right .split-figure.is-spill.is-photo { justify-content: flex-end; }
|
|
1389
|
+
.is-left .split-figure.is-spill.is-photo { justify-content: flex-start; }
|
|
1390
|
+
.is-right .split-figure.is-spill.is-photo > :deep(img) { object-position: right center; }
|
|
1391
|
+
.is-left .split-figure.is-spill.is-photo > :deep(img) { object-position: left center; }
|
|
1392
|
+
.is-left .split-figure.is-spill.is-figure { align-items: flex-start; }
|
|
1393
|
+
.is-right .split-figure.is-spill.is-figure { align-items: flex-end; }
|
|
1394
|
+
/*
|
|
1395
|
+
★ AND THE VERTICAL ANCHOR IS THE TOP, so a figure taller than its cell — a browser window whose
|
|
1396
|
+
foot is meant to be below the canvas, three device frames standing on the bottom edge — grows
|
|
1397
|
+
downwards rather than centring and losing its head as well as its feet.
|
|
1398
|
+
*/
|
|
1399
|
+
.split-figure.is-spill.is-figure { justify-content: flex-start; }
|
|
1400
|
+
/*
|
|
1401
|
+
★★ THE SLIDE DOES THE CROPPING, AND IT HAS TO SAY SO. Nothing on this layout clipped before —
|
|
1402
|
+
a figure that fits needs no clip and the canvas is not a clipping context by default, so the
|
|
1403
|
+
overflow simply painted over the chrome and past the slide into the deck's own page. `overflow`
|
|
1404
|
+
on the root is what turns "sized past its cell" into "cropped by the slide edge".
|
|
1405
|
+
*/
|
|
1406
|
+
.split.is-spill { overflow: hidden; }
|
|
1407
|
+
|
|
1408
|
+
/*
|
|
1409
|
+
★★ AND THE DRAWING FITS ON WHICHEVER AXIS BINDS FIRST. An <svg> carrying a viewBox has an
|
|
1410
|
+
intrinsic ratio, so at width/height 100% its own default preserveAspectRatio="xMidYMid meet" does
|
|
1411
|
+
exactly what object-fit: contain does for a photo — grows until one axis touches, centres on the
|
|
1412
|
+
other, never distorts. That is the whole behaviour, and it comes from the SVG spec rather than
|
|
1413
|
+
from arithmetic here, so there is no ratio for this file to compute or keep in sync.
|
|
1414
|
+
|
|
1415
|
+
Scoped to the figure kind: `panel` mattes its content at natural size on purpose, and `photo`
|
|
1416
|
+
already crops with object-fit: cover.
|
|
1417
|
+
*/
|
|
1418
|
+
.split-figure.is-figure :deep(svg) {
|
|
1419
|
+
max-width: 100%;
|
|
1420
|
+
max-height: 100%;
|
|
1421
|
+
}
|
|
1422
|
+
/*
|
|
1423
|
+
THE FIGURE SIDE OWNS ITS OWN TREATMENT. An author writes the content and nothing else:
|
|
1424
|
+
|
|
1425
|
+
::aside::
|
|
1426
|
+
<img src="/warehouse.jpg" />
|
|
1427
|
+
|
|
1428
|
+
and gets a filled, corner-clipped photo. Before this, `.is-photo` carried NO CSS at all — the
|
|
1429
|
+
rounding and the fill lived only inside <Placeholder>, so the frame vanished the moment a real
|
|
1430
|
+
image replaced the placeholder and the author had to rebuild it by hand with utility classes.
|
|
1431
|
+
`frame`'s own description promises "a photo frame"; this is that frame.
|
|
1432
|
+
|
|
1433
|
+
`object-fit: cover` rather than a raw stretch is what makes filling safe for an <img> of any
|
|
1434
|
+
intrinsic ratio. Same rule and same reasoning as bleed.vue's `.bleed-media :deep(> *)`.
|
|
1435
|
+
|
|
1436
|
+
Scoped to real MEDIA, not `:deep(> *)`: a <Placeholder> is a <div> and keeps its own
|
|
1437
|
+
aspect-ratio behaviour, so `frame: photo` with no slot content still mocks up at its declared
|
|
1438
|
+
ratio instead of being stretched to the cell.
|
|
1439
|
+
|
|
1440
|
+
ESCAPE HATCH, so this cannot silently defeat a caller who wants something else: use
|
|
1441
|
+
`frame: content`, which carries no treatment at all, and bring your own figure.
|
|
1442
|
+
*/
|
|
1443
|
+
.split-figure.is-photo {
|
|
1444
|
+
border-radius: var(--deck-radius-card);
|
|
1445
|
+
overflow: hidden;
|
|
1446
|
+
display: flex;
|
|
1447
|
+
min-height: 0;
|
|
1448
|
+
}
|
|
1449
|
+
.split-figure.is-photo :deep(img),
|
|
1450
|
+
.split-figure.is-photo :deep(video),
|
|
1451
|
+
.split-figure.is-photo :deep(picture) {
|
|
1452
|
+
width: 100%;
|
|
1453
|
+
height: 100%;
|
|
1454
|
+
object-fit: cover;
|
|
1455
|
+
display: block;
|
|
1456
|
+
}
|
|
1457
|
+
/*
|
|
1458
|
+
══ THE PAIR ══════════════════════════════════════════════════════════════════════════════════
|
|
1459
|
+
|
|
1460
|
+
★★ THE CONTENT COLUMN TAKES THE FIGURE'S OWN TREATMENT, WORD FOR WORD, when it is holding the
|
|
1461
|
+
first of two pictures — the same corner, the same crop, the same clip. A diptych only reads if
|
|
1462
|
+
the two halves are the same KIND of object: one radius against none, or a cover crop against an
|
|
1463
|
+
image sized to its own ratio, and the slide is a photograph beside a picture of a photograph.
|
|
1464
|
+
|
|
1465
|
+
⚠ SCOPED TO `.is-picture`, WHICH THE LAYOUT ONLY ADDS WHEN `images` HOLDS TWO. Written as a bare
|
|
1466
|
+
`.split-content :deep(img)` it would have cropped every inline image in every prose column in the
|
|
1467
|
+
deck to the height of its box — a diagram, a mark, an emoji-sized glyph.
|
|
1468
|
+
*/
|
|
1469
|
+
/*
|
|
1470
|
+
★★ THE PAIR'S OTHER PICTURE SITS IN THE CONTENT COLUMN'S TRACK AND IS OTHERWISE A FIGURE. It
|
|
1471
|
+
carries `.split-figure.is-photo`, so the corner, the crop, the clip and the claw-back to the media
|
|
1472
|
+
inset are the rules that already shape the other half rather than a second set written to match
|
|
1473
|
+
them. Two things are its own: which track it sits in, and which of its edges is the outer one.
|
|
1474
|
+
|
|
1475
|
+
⚠ THE MIRRORED CLAW-BACK IS THE WHOLE OF THAT SECOND THING. Every `.split-figure` rule keys its
|
|
1476
|
+
outer edge off `.is-left`/`.is-right`, which name where the FIGURE is — so applied unchanged to a
|
|
1477
|
+
box in the other column they clawed the inner edge and left the outer one at the text inset: one
|
|
1478
|
+
picture 96 from the canvas beside another at 36, which is what "it looks off" was.
|
|
1479
|
+
*/
|
|
1480
|
+
.split-figure.is-pair {
|
|
1481
|
+
grid-column: var(--split-content-col);
|
|
1482
|
+
grid-row: 1 / 3;
|
|
1483
|
+
}
|
|
1484
|
+
.is-left .split-figure.is-pair { margin-right: 0; margin-left: calc(-1 * (var(--deck-inset-text) - var(--deck-inset-media))); }
|
|
1485
|
+
.is-right .split-figure.is-pair { margin-left: 0; margin-right: calc(-1 * (var(--deck-inset-text) - var(--deck-inset-media))); }
|
|
1486
|
+
/*
|
|
1487
|
+
★★ ONE SEAM, USED EVERYWHERE IN THE SET. The gutter between the two columns and the gap between
|
|
1488
|
+
two pictures stacked in one column are the same number — Jose: "the space in the middle should be
|
|
1489
|
+
the same separator". They were not: the grid's own column-gap is 0 on a mixed split (the figure's
|
|
1490
|
+
edge does the separating) and --deck-split-gutter-same (96) when two columns are the same KIND,
|
|
1491
|
+
while a stack inside a column used the grid gutter of 32. Three values in one picture set.
|
|
1492
|
+
|
|
1493
|
+
⚠ AND IT IS THE GRID GUTTER, NOT THE SAME-KIND TOKEN. 96 is the air two columns of DIFFERENT
|
|
1494
|
+
content need between them; a picture set is one object cut into pieces, and the seam inside an
|
|
1495
|
+
object is the same 32 a `::grid` of tiles uses.
|
|
1496
|
+
*/
|
|
1497
|
+
.split-grid:has(.split-figure.is-pair) { column-gap: var(--deck-space-4); }
|
|
1498
|
+
/*
|
|
1499
|
+
══ FLUSH ══════════════════════════════════════════════════════════════════════════════════════
|
|
1500
|
+
|
|
1501
|
+
★★ THE INNER CORNERS GO AND THE OUTER ONES STAY. Squaring all of them — which is what this did
|
|
1502
|
+
first — draws a hard rectangle in the middle of a slide that has not bled, and the set reads as a
|
|
1503
|
+
photograph someone forgot to finish rather than as one field. Jose: "flush doesn't look good, take
|
|
1504
|
+
a look at the images". The corners on the OUTSIDE are the set's own edge against the deck ground,
|
|
1505
|
+
and they are the same card corner every other figure on this layout carries.
|
|
1506
|
+
|
|
1507
|
+
⚠ WHICH CORNERS ARE OUTER DEPENDS ON THE COLUMN, AND THE COLUMN DEPENDS ON `flip`. `.is-pair` is
|
|
1508
|
+
the content-column picture, so it is the LEFT column on `.is-left` and the right one on
|
|
1509
|
+
`.is-right`; the figure is the mirror. Written in logical corners — `start-start` is top-left in
|
|
1510
|
+
this deck's writing mode — so each rule names the side rather than the corner twice.
|
|
1511
|
+
*/
|
|
1512
|
+
/*
|
|
1513
|
+
★★ THE SET'S OWN CORNER IS CAPTURED BEFORE THE TOKEN IS ZEROED. The rule below has to zero
|
|
1514
|
+
--deck-radius-card on the figure so useNotchCuts draws a square outline — and the picture corners
|
|
1515
|
+
read that same token, so zeroing it took the set's outer rounding with it: measured, all four
|
|
1516
|
+
pictures at 0 and the set a hard rectangle again. Held on the ROOT, which does not override the
|
|
1517
|
+
token, so the corners keep the card radius while the boxes under them go square.
|
|
1518
|
+
*/
|
|
1519
|
+
.split.is-flush { --set-corner: var(--deck-radius-card); }
|
|
1520
|
+
.split.is-flush .split-grid { column-gap: 0; }
|
|
1521
|
+
.split.is-flush .split-figure.is-photo {
|
|
1522
|
+
gap: 0;
|
|
1523
|
+
border-radius: 0;
|
|
1524
|
+
/*
|
|
1525
|
+
⚠ AND THE NOTCH'S OWN RADIUS GOES WITH IT. useNotchCuts reads --deck-radius-card off the panel's
|
|
1526
|
+
computed style and draws the whole outline — so the clip put a 48px arc back on EVERY corner,
|
|
1527
|
+
including the two at the seam, and a flush set had a bite taken out of the middle of it at the
|
|
1528
|
+
top and the bottom. `border-radius: 0` above cannot reach the clip; the token can, and the
|
|
1529
|
+
rounding the set does keep is drawn on the pictures rather than on the box.
|
|
1530
|
+
*/
|
|
1531
|
+
--deck-radius-card: 0px;
|
|
1532
|
+
}
|
|
1533
|
+
.split.is-flush .split-figure.is-photo > :deep(img) { border-radius: 0; }
|
|
1534
|
+
/* The left-hand column, whichever of the two boxes that is: first picture keeps the top-left corner,
|
|
1535
|
+
last keeps the bottom-left. A single picture in the column is both, so `:first-child` and
|
|
1536
|
+
`:last-child` both match it and it rounds the whole left side. */
|
|
1537
|
+
.split.is-flush.is-left .split-figure.is-pair > :deep(img):first-child,
|
|
1538
|
+
.split.is-flush.is-right .split-figure:not(.is-pair) > :deep(img):first-child {
|
|
1539
|
+
border-start-start-radius: var(--set-corner);
|
|
1540
|
+
}
|
|
1541
|
+
.split.is-flush.is-left .split-figure.is-pair > :deep(img):last-child,
|
|
1542
|
+
.split.is-flush.is-right .split-figure:not(.is-pair) > :deep(img):last-child {
|
|
1543
|
+
border-end-start-radius: var(--set-corner);
|
|
1544
|
+
}
|
|
1545
|
+
/* And the right-hand column, mirrored. */
|
|
1546
|
+
.split.is-flush.is-left .split-figure:not(.is-pair) > :deep(img):first-child,
|
|
1547
|
+
.split.is-flush.is-right .split-figure.is-pair > :deep(img):first-child {
|
|
1548
|
+
border-start-end-radius: var(--set-corner);
|
|
1549
|
+
}
|
|
1550
|
+
.split.is-flush.is-left .split-figure:not(.is-pair) > :deep(img):last-child,
|
|
1551
|
+
.split.is-flush.is-right .split-figure.is-pair > :deep(img):last-child {
|
|
1552
|
+
border-end-end-radius: var(--set-corner);
|
|
1553
|
+
}
|
|
1554
|
+
/*
|
|
1555
|
+
⚠ EXCEPT AT THE CANVAS EDGE, where there is no ground left for a corner to sit against: a bleeding
|
|
1556
|
+
flush set is the whole frame in pieces, and a rounded corner there cuts a notch out of the slide.
|
|
1557
|
+
|
|
1558
|
+
⚠⚠ AND IT HAS TO MATCH THE FOUR RULES ABOVE ON SPECIFICITY, WHICH IS WHY THE CHILD SELECTORS ARE
|
|
1559
|
+
REPEATED HERE. Written as a plain descendant it is one class short of them — corner longhands are
|
|
1560
|
+
resolved per property, so `border-start-start-radius` from a 5-class rule beat `border-radius: 0`
|
|
1561
|
+
from a 4-class one and the bleeding set kept all four corners. Measured on the flush+bleed slide:
|
|
1562
|
+
48px at every corner, against a canvas the pictures already reached.
|
|
1563
|
+
*/
|
|
1564
|
+
.split.is-flush.is-bleed .split-figure.is-photo > :deep(img):first-child,
|
|
1565
|
+
.split.is-flush.is-bleed .split-figure.is-photo > :deep(img):last-child { border-radius: 0; }
|
|
1566
|
+
/*
|
|
1567
|
+
★★ AND THE SET BLEEDS AS ONE OBJECT, WHICH TOOK A SECOND MIRROR. `edge` was bound to the figure
|
|
1568
|
+
element alone, so a bleeding set had one column at the canvas and the other still inset: measured
|
|
1569
|
+
on the flush+bleed slide, the figure ran 960 → 1920 and 0 → 1080 while the pair sat at 36 → 960 and
|
|
1570
|
+
96 → 984. The vertical claw-backs are side-agnostic and apply to both as written; the horizontal
|
|
1571
|
+
one names `.is-left`/`.is-right`, which is where the FIGURE is, so the pair takes the mirror.
|
|
1572
|
+
*/
|
|
1573
|
+
.is-left .split-figure.is-pair.is-bleed { margin-right: 0; margin-left: calc(-1 * var(--deck-inset-text)); }
|
|
1574
|
+
.is-right .split-figure.is-pair.is-bleed { margin-left: 0; margin-right: calc(-1 * var(--deck-inset-text)); }
|
|
1575
|
+
/*
|
|
1576
|
+
★★ EACH COLUMN IS A STACK, because a column can hold two. Three pictures is one and two; four is
|
|
1577
|
+
two and two. The gutter between two pictures in a column is the same one the layout puts between
|
|
1578
|
+
its columns — `--deck-space-4` is `.split-grid`'s own gap — so a 2x2 set reads as one grid rather
|
|
1579
|
+
than as two columns that happen to be beside each other.
|
|
1580
|
+
*/
|
|
1581
|
+
.split-figure.is-photo:has(> img + img) {
|
|
1582
|
+
flex-direction: column;
|
|
1583
|
+
gap: var(--deck-space-4);
|
|
1584
|
+
}
|
|
1585
|
+
.split-figure.is-photo > :deep(img) {
|
|
1586
|
+
/* `flex: 1` rather than `height: 100%`: two pictures in one column share it, one takes all of it. */
|
|
1587
|
+
flex: 1 1 0;
|
|
1588
|
+
width: 100%;
|
|
1589
|
+
min-height: 0;
|
|
1590
|
+
object-fit: cover;
|
|
1591
|
+
display: block;
|
|
1592
|
+
border-radius: var(--deck-radius-card);
|
|
1593
|
+
}
|
|
1594
|
+
/*
|
|
1595
|
+
★★ `--warm-100`, THE SAME MAT stack's BAND TAKES, because `frame: 'panel'` has to mean one thing.
|
|
1596
|
+
|
|
1597
|
+
This was `--deck-bg-subtle` — `color-mix(--deck-bg, --deck-fg 8%)`, the page ground pushed toward
|
|
1598
|
+
the ink — which renders grey. stack's band at the same `frame: 'panel'` defaults to `background: 'warm'`
|
|
1599
|
+
and renders `--warm-100`. So one prop name gave two different surfaces depending on which layout
|
|
1600
|
+
you were in, and this layout's own doc for the prop calls it "a TINTED box", which the grey was
|
|
1601
|
+
not. Jose, on a panel split: "why is the color neutral? isn't the default the warm color?"
|
|
1602
|
+
|
|
1603
|
+
The AXIS came with it: `background` below is stack's prop, its four values, and its four rules read from
|
|
1604
|
+
the same tokens — so the two layouts now agree about what a panel is and what can be done to one,
|
|
1605
|
+
rather than sharing a prop name and meaning different things by it.
|
|
1606
|
+
*/
|
|
1607
|
+
.split-figure.is-panel {
|
|
1608
|
+
border-radius: var(--deck-radius-card);
|
|
1609
|
+
padding: var(--deck-card-pad);
|
|
1610
|
+
display: flex;
|
|
1611
|
+
/* The cross axis is `panelAlign`'s, above. */
|
|
1612
|
+
justify-content: center;
|
|
1613
|
+
}
|
|
1614
|
+
/*
|
|
1615
|
+
The four registers, scoped to `.is-panel` so a background on any other frame is inert. The same four
|
|
1616
|
+
named backgrounds stack's bands take — one table, read by both — because a panel and a band of the
|
|
1617
|
+
same name should be the same colour, and two copies of that list would not stay so.
|
|
1618
|
+
|
|
1619
|
+
⚠ This named `--deck-accent-soft`, `--deck-bg-subtle`, `--warm-100` and `--forest-300`, which was
|
|
1620
|
+
the loose-fill palette that predates the one table.
|
|
1621
|
+
*/
|
|
1622
|
+
.split-figure.is-panel.bg-warm { background: var(--deck-bg-warm); color: var(--deck-on-bg-warm); }
|
|
1623
|
+
.split-figure.is-panel.bg-accent { background: var(--deck-bg-accent); color: var(--deck-on-bg-accent); }
|
|
1624
|
+
.split-figure.is-panel.bg-vivid { background: var(--deck-bg-vivid); color: var(--deck-on-bg-vivid); }
|
|
1625
|
+
.split-figure.is-panel.bg-neutral { background: var(--deck-bg-neutral); color: var(--deck-on-bg-neutral); }
|
|
1626
|
+
|
|
1627
|
+
/*
|
|
1628
|
+
── A CODE SURFACE ────────────────────────────────────────────────────────────────────────────
|
|
1629
|
+
The panel IS the block. It takes the code ground and its own edge, and the `pre` inside goes flat,
|
|
1630
|
+
so a code slide is ONE surface rather than a well inside a mat. See the `frame` prop's own note for
|
|
1631
|
+
why this is a named value rather than a `:has()` on `panel`.
|
|
1632
|
+
|
|
1633
|
+
The inner reset is the same three declarations the magic-move container has always used on its own
|
|
1634
|
+
`pre.shiki`, for the same reason: a container that owns the surface must strip the one its child
|
|
1635
|
+
would otherwise draw, or the two edges sit 4px apart and read as a mistake.
|
|
1636
|
+
*/
|
|
1637
|
+
.split-figure.is-code {
|
|
1638
|
+
background: var(--slidev-code-background);
|
|
1639
|
+
border: 1px solid var(--deck-code-border);
|
|
1640
|
+
border-radius: var(--deck-radius-card);
|
|
1641
|
+
padding: var(--deck-card-pad);
|
|
1642
|
+
display: flex;
|
|
1643
|
+
flex-direction: column;
|
|
1644
|
+
justify-content: center;
|
|
1645
|
+
min-width: 0;
|
|
1646
|
+
}
|
|
1647
|
+
/*
|
|
1648
|
+
★★ THE PADDING IS ZEROED AT ITS SOURCE, NOT OVERRIDDEN — one cause, two effects.
|
|
1649
|
+
|
|
1650
|
+
The obvious way to flatten the block is `padding: 0 !important` (Slidev's own rule carries
|
|
1651
|
+
`!important`, so nothing less wins). It works, and it leaves a 24px horizontal scrollbar on every
|
|
1652
|
+
code frame, because a SECOND rule reads the same token: base.css sizes each line
|
|
1653
|
+
|
|
1654
|
+
width: calc(100% + space-1 + space-2 + var(--slidev-code-padding))
|
|
1655
|
+
|
|
1656
|
+
so the row overhangs to the right by exactly the block's own right padding, which is what lets a
|
|
1657
|
+
lit band reach both padding edges. Flatten the padding without telling that rule and the overhang
|
|
1658
|
+
has nothing left to fill: measured on this frame, scrollWidth 965 against clientWidth 941 — the
|
|
1659
|
+
token, to the pixel.
|
|
1660
|
+
|
|
1661
|
+
Setting `--slidev-code-padding: 0px` collapses both at once: Slidev's padding resolves to zero with
|
|
1662
|
+
no `!important` needed, and the line's overhang term goes with it. The surface's own inset is the
|
|
1663
|
+
panel's, which is the whole point of the frame.
|
|
1664
|
+
*/
|
|
1665
|
+
.split-figure.is-code {
|
|
1666
|
+
--slidev-code-padding: 0px;
|
|
1667
|
+
}
|
|
1668
|
+
.split-figure.is-code :deep(.slidev-code-wrapper),
|
|
1669
|
+
.split-figure.is-code :deep(pre.slidev-code) {
|
|
1670
|
+
margin: 0 !important;
|
|
1671
|
+
background: none;
|
|
1672
|
+
border: none;
|
|
1673
|
+
border-radius: 0;
|
|
1674
|
+
}
|
|
1675
|
+
/*
|
|
1676
|
+
★★ AN EDGE AND A CLIP CANNOT SHARE AN ELEMENT. `clip-path` cuts the rendered box, border included,
|
|
1677
|
+
so a notched code surface drew its edge continuously on three sides and then simply lost it around
|
|
1678
|
+
the cut — a frame that stops. Measured on the specimen's own pair: `border: 1px` and a clip-path on
|
|
1679
|
+
the same figure.
|
|
1680
|
+
|
|
1681
|
+
The border is what a code surface has when it is a CARD on the slide. Notched, its outer corner is
|
|
1682
|
+
the mark's seat rather than a corner; bleeding, there is no ground for it to sit on at all. Neither
|
|
1683
|
+
is a card, so neither takes an edge. `panel` never hit this because it has no border to lose.
|
|
1684
|
+
*/
|
|
1685
|
+
/*
|
|
1686
|
+
★★ A NOTCHED CODE SURFACE RESERVES THE TOP AND NOT THE BOTTOM, which is where it parts company
|
|
1687
|
+
with a notched panel.
|
|
1688
|
+
|
|
1689
|
+
The shared reservation below puts `max(card-pad, notch-h)` at BOTH ends, and its own note is honest
|
|
1690
|
+
about why: the bottom "buys nothing but symmetry", so that media CENTRED in the panel shares the
|
|
1691
|
+
centre a viewer sees. A code surface is not centred media — it is a block that starts at the top of
|
|
1692
|
+
its box — so the mirrored 96px at the foot is 64px of code height bought for a reason that does not
|
|
1693
|
+
apply here. The top keeps it, because that is the mark's seat and it is structural.
|
|
1694
|
+
|
|
1695
|
+
The horizontal stays at --deck-card-pad, which is where a plain code block already insets its text,
|
|
1696
|
+
so a code frame and a code block agree on the one edge they can be compared across.
|
|
1697
|
+
*/
|
|
1698
|
+
.split-figure.is-code.has-notch {
|
|
1699
|
+
border: none;
|
|
1700
|
+
padding-top: max(var(--deck-card-pad), var(--notch-h));
|
|
1701
|
+
padding-bottom: var(--deck-card-pad);
|
|
1702
|
+
}
|
|
1703
|
+
/*
|
|
1704
|
+
★★ BLEEDING, IT KEEPS THE CODE'S OWN INSET HORIZONTALLY RATHER THAN THE REGION'S.
|
|
1705
|
+
|
|
1706
|
+
Every other bleeding panel takes --deck-inset-text on all four sides, so its content lines up with
|
|
1707
|
+
the content column across the seam — that is the rule the panel's own note argues for, and it is
|
|
1708
|
+
right for PROSE, which is what a content column holds.
|
|
1709
|
+
|
|
1710
|
+
A code surface is not prose. Its neighbour on the other side of the seam is a paragraph; its
|
|
1711
|
+
neighbour on every other slide is a code block, and a code block insets its text by
|
|
1712
|
+
--deck-card-pad. Matching the block is worth more than matching the column, because the reader
|
|
1713
|
+
compares code to code. So the horizontal follows the notched code surface above, and the vertical
|
|
1714
|
+
keeps the region inset it needs to stand off the canvas edge.
|
|
1715
|
+
*/
|
|
1716
|
+
.split-figure.is-code.is-bleed {
|
|
1717
|
+
border: none;
|
|
1718
|
+
border-radius: 0;
|
|
1719
|
+
padding: var(--deck-inset-text) var(--deck-card-pad);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/* Media matted inside a panel takes the CONCENTRIC radius — the card's corner minus its padding —
|
|
1723
|
+
so the inner curve sits parallel to the outer one instead of fighting it. */
|
|
1724
|
+
/*
|
|
1725
|
+
⚠ `:not(.mark-pair-companion)`. A co-branding tile is not matted media: MarkPair clips the
|
|
1726
|
+
partner's artwork to the MARK's corner — a ratio of the tile — and this rule outranked it
|
|
1727
|
+
(`.split-figure.is-panel[data-v-…] img` beats a single class), so a pair dropped into a panel came out with
|
|
1728
|
+
the panel's 16px nested radius on one half and the mark's on the other. Measured on the specimen's
|
|
1729
|
+
own MarkPair slide: 31.49px on the light half, 16px on the dark one, same component, same size.
|
|
1730
|
+
slide-top.vue carries the same exclusion for the same reason.
|
|
1731
|
+
*/
|
|
1732
|
+
.split-figure.is-panel :deep(img:not(.mark-pair-companion)),
|
|
1733
|
+
.split-figure.is-panel :deep(video),
|
|
1734
|
+
.split-figure.is-panel :deep(picture) {
|
|
1735
|
+
max-width: 100%;
|
|
1736
|
+
border-radius: var(--deck-radius-nested);
|
|
1737
|
+
display: block;
|
|
1738
|
+
}
|
|
1739
|
+
/* ── THE NOTCH ──────────────────────────────────────────────────────────────────────────────────
|
|
1740
|
+
task-15-brief.md. The panel's outer top corner is cut INWARD so the running Forest mark sits in
|
|
1741
|
+
the cut-out, and the panel's edge reads convex → concave → convex around it.
|
|
1742
|
+
|
|
1743
|
+
★ THE CUT ITSELF IS NOT HERE. It is one shared rule, `.deck-notch` in styles/base.css, because
|
|
1744
|
+
task-18 gave the theme a SECOND notched figure: stack.vue's band cuts the same corner around the
|
|
1745
|
+
same mark. Two copies of a five-layer composited mask is exactly the drift this codebase keeps
|
|
1746
|
+
re-discovering the hard way, so the shape moved out and both layouts wear the class. What stays
|
|
1747
|
+
here is what is genuinely split's: which corner is the outer one (bound in the template, since it
|
|
1748
|
+
flips with `flip`), the margin that raises the panel to meet the mark, and the band a notched
|
|
1749
|
+
PANEL reserves from its own content.
|
|
1750
|
+
|
|
1751
|
+
★ THE PANEL RISES TO MEET THE MARK. Without this, there is nothing to notch: the mark is inset
|
|
1752
|
+
--deck-inset-chrome (36px) from the top, the figure's own vertical bound is --deck-inset-text
|
|
1753
|
+
(90px), and the two boxes overlap by only 18px — a bite that shallow is a nick, not a seat, and
|
|
1754
|
+
what actually renders without it is a 72px mark parked on top of a photograph's corner. On a
|
|
1755
|
+
notched panel the TOP margin therefore drops to the chrome inset, which is also exactly why chrome
|
|
1756
|
+
and media share that number (scripts/deck.ts, above INSET): the mark's outer corner and the
|
|
1757
|
+
panel's outer corner become the SAME POINT, so the mark reads as seated in the corner with one
|
|
1758
|
+
clean ring of clearance — 36px of slide margin outside it, 36px of cut-out inside it.
|
|
1759
|
+
|
|
1760
|
+
★★ AND THE BOTTOM COMES WITH IT (task-26). This rule used to move the TOP only, on the reasoning
|
|
1761
|
+
that "there is no chrome in the panel's bottom corner to make room for". True, and it is an
|
|
1762
|
+
argument for not needing a notch at the bottom — not for keeping the bottom 54px further in than
|
|
1763
|
+
the other three sides. Measured, every one of the 86 media figures in the three decks rendered
|
|
1764
|
+
|
|
1765
|
+
outer edge 36 · top 36 · bottom 90
|
|
1766
|
+
|
|
1767
|
+
i.e. a frame hanging 54px higher off the bottom edge than off the other three, on every media
|
|
1768
|
+
slide in the deck, with deck.css's own token comment calling the asymmetry intended. Jose's rule
|
|
1769
|
+
is 36 on every side for media; the notch is what makes 36 safe at the top, and nothing ever made
|
|
1770
|
+
it unsafe at the bottom — the only bottom chrome is the `confidential:` pill, which renders only
|
|
1771
|
+
when a slide declares one and sits on the pills side, i.e. the opposite side of the slide from the
|
|
1772
|
+
figure on every split — the chrome no longer mirrors at all (see ../notch.js above hasNotch).
|
|
1773
|
+
|
|
1774
|
+
So a notched figure is 36 on all four sides and an un-notched one is 90 top and bottom — the
|
|
1775
|
+
vertical pair moves TOGETHER with the cut, which is what makes `notch: false`'s documented promise
|
|
1776
|
+
("EXACTLY its pre-notch geometry") true of both edges rather than one.
|
|
1777
|
+
|
|
1778
|
+
⚠ ALL OF IT DROPS TOGETHER ON `notch: false`. The mask (via the class), these margins and the
|
|
1779
|
+
reservation below all key off the same `notched` computed, so an opted-out figure returns to
|
|
1780
|
+
exactly its pre-notch geometry rather than keeping a raised top with no cut in it — see
|
|
1781
|
+
notchShows() in ../notch.js for what that restores and why the escape hatch exists.
|
|
1782
|
+
*/
|
|
1783
|
+
.split-figure.has-notch {
|
|
1784
|
+
/*
|
|
1785
|
+
The panel's own outer corner moves up to the mark's. See the note above.
|
|
1786
|
+
|
|
1787
|
+
★ NEGATIVE NOW, AND FOR THE SAME REASON THE HORIZONTAL CLAW-BACK IS: these used to be absolute
|
|
1788
|
+
positions (36 from a canvas edge the root did not pad). The root pads by the text inset today, so
|
|
1789
|
+
reaching the chrome/media inset is the DIFFERENCE between the two — exactly the shape the outer
|
|
1790
|
+
edge already uses. Same two numbers, expressed as the gap between them rather than restated.
|
|
1791
|
+
*/
|
|
1792
|
+
margin-top: calc(-1 * (var(--deck-inset-text) - var(--deck-inset-chrome)));
|
|
1793
|
+
}
|
|
1794
|
+
/*
|
|
1795
|
+
★★ THE BOTTOM ESCAPE ONLY EXISTS WHEN THERE IS NOTHING BELOW IT. A figure spans rows 1-2 and
|
|
1796
|
+
claws past the grid's bottom edge to reach the media inset — which is right when row 2 ends at the
|
|
1797
|
+
safe line, and wrong the moment a `::footer::` occupies row 3: the escape then reaches 60px PAST
|
|
1798
|
+
the row boundary and lands 36px inside the footer. Measured on the footer specimen: figure bottom
|
|
1799
|
+
971 against a footer starting at 935.
|
|
1800
|
+
|
|
1801
|
+
Scoped with `:not(:has(.split-footer))` rather than fixed by trimming the number, because the
|
|
1802
|
+
number is right — a figure with nothing under it should reach the media inset. It is the presence
|
|
1803
|
+
of a footer that removes the edge to escape to.
|
|
1804
|
+
*/
|
|
1805
|
+
.split-grid:not(:has(.split-footer)) .split-figure.has-notch {
|
|
1806
|
+
margin-bottom: calc(-1 * (var(--deck-inset-text) - var(--deck-inset-media)));
|
|
1807
|
+
}
|
|
1808
|
+
/*
|
|
1809
|
+
THE SPACE, as opposed to the shape. A mask clips; it does not reflow, so a tinted panel holding
|
|
1810
|
+
real content would have that content's corner sliced off — the asymmetry where a photo panel
|
|
1811
|
+
looks perfect and a text panel silently truncates a word. The cut's own height is reserved as top
|
|
1812
|
+
padding (never less than the card padding the panel already had), which keeps every child clear
|
|
1813
|
+
of the notch band regardless of what the panel holds or how it is aligned inside it.
|
|
1814
|
+
|
|
1815
|
+
A band rather than a corner-shaped exclusion: `.is-panel` centres its content with flex, and a
|
|
1816
|
+
float with `shape-outside` — the only way to actually wrap text around a corner — does not apply
|
|
1817
|
+
to a flex item at all. The band is the honest version of the same guarantee.
|
|
1818
|
+
|
|
1819
|
+
`.is-photo` deliberately gets none of this: clipping the photograph IS the effect.
|
|
1820
|
+
*/
|
|
1821
|
+
/*
|
|
1822
|
+
★★ RESERVED AT BOTH ENDS, SO WHAT SITS IN A NOTCHED PANEL IS ACTUALLY CENTRED.
|
|
1823
|
+
|
|
1824
|
+
The top reservation is the notch: content must clear the bite cut out of the corner for the running
|
|
1825
|
+
mark, so the panel keeps `--notch-h` (mark 72 + ring 24 = 96) above it. Only the top was reserved,
|
|
1826
|
+
which left the box 96/32 — and anything centred in it lands (96 - 32) / 2 = 32px LOW inside the
|
|
1827
|
+
panel. Measured on the video slide: phone y 142-1002 in a panel y 36-1044, dead centre of the
|
|
1828
|
+
CONTENT box and 32 off the centre of the panel. Jose spotted it as "the iphone is not centered
|
|
1829
|
+
vertically".
|
|
1830
|
+
|
|
1831
|
+
⚠ THE BOTTOM RESERVATION BUYS NOTHING BUT SYMMETRY, and that is the whole point of it. There is no
|
|
1832
|
+
notch down there and nothing to clear; the padding exists so the box a figure is centred in has the
|
|
1833
|
+
same centre as the panel a viewer sees. It costs 64px of usable height on every notched panel split
|
|
1834
|
+
— the honest price, paid once in the frame, rather than a magic offset on each slide that happens
|
|
1835
|
+
to hold something centred.
|
|
1836
|
+
*/
|
|
1837
|
+
.split-figure.is-panel.has-notch,
|
|
1838
|
+
/*
|
|
1839
|
+
★ `.is-figure` TAKES THE SAME RESERVATION, WITHOUT THE FILL. A naked figure had no padding at all,
|
|
1840
|
+
so it sat flush to the cell — which puts its top edge inside the notch's own bite and lets a wide
|
|
1841
|
+
one run under the running mark. Jose: "can we still give the padding to the naked version, so it
|
|
1842
|
+
doesnt overlap... similar treatment as panel, just without the bg."
|
|
1843
|
+
|
|
1844
|
+
The same expression rather than a bare `--notch-h`, so the two kinds cannot drift: whatever a
|
|
1845
|
+
notched panel reserves, a notched figure reserves. What `.is-figure` still does NOT get is the
|
|
1846
|
+
background, the radius and the horizontal card padding — it is a figure sitting on the page, not a
|
|
1847
|
+
matted one, and only the vertical clearance was ever the shared problem.
|
|
1848
|
+
*/
|
|
1849
|
+
.split-figure.is-figure.has-notch {
|
|
1850
|
+
padding-top: max(var(--deck-card-pad), var(--notch-h));
|
|
1851
|
+
padding-bottom: max(var(--deck-card-pad), var(--notch-h));
|
|
1852
|
+
}
|
|
1853
|
+
.split-content { min-width: 0; }
|
|
1854
|
+
.split-figure { min-width: 0; }
|
|
1855
|
+
/*
|
|
1856
|
+
★ THE SPLIT COLUMN RUNS AT THE BODY TIER BY DEFAULT, THE SAME AS EVERY OTHER SLIDE.
|
|
1857
|
+
|
|
1858
|
+
It used to step down to body-sm on the argument that "two columns halve the measure, so both sides
|
|
1859
|
+
take the dense body tier". The measure is halved and that is exactly why the tier should NOT be:
|
|
1860
|
+
a slide's body copy is 48px because that is what carries a room, and a column is not a reason for
|
|
1861
|
+
the audience at the back to get a smaller one. The measure is already handled where it belongs —
|
|
1862
|
+
a narrow column simply wraps sooner, which is what a column is for.
|
|
1863
|
+
|
|
1864
|
+
The cost is real and is paid deliberately: at 48px a split column fits about a third less copy
|
|
1865
|
+
than it did, so a slide that was full is now over. That is the deck telling the author the column
|
|
1866
|
+
holds less, which is a better failure than shrinking the type until it fits.
|
|
1867
|
+
*/
|
|
1868
|
+
.is-body-base .split-grid { font-size: var(--type-deck-body-size); line-height: var(--type-deck-body-line-height); }
|
|
1869
|
+
/* The dense tier, opted into per slide — see the `body` prop for when that is the honest answer. */
|
|
1870
|
+
.is-body-sm .split-grid { font-size: var(--type-deck-body-sm-size); line-height: var(--type-deck-body-sm-line-height); }
|
|
1871
|
+
/* No margin-bottom here: the h1 inside the band already carries `margin: 0 0 var(--deck-space-3)`
|
|
1872
|
+
(base.css), and against a row fixed at 360 the second one was invisible slack. With the row sized
|
|
1873
|
+
to its band it would double the title-to-body gap to 48 where `default` gives 24. */
|
|
1874
|
+
.split-footer { margin-top: var(--deck-space-3); }
|
|
1875
|
+
</style>
|