@humanforest/slidev-theme 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +67 -0
- package/README.md +577 -0
- package/assetUrl.js +38 -0
- package/components/Alert.vue +209 -0
- package/components/AreaBox.vue +152 -0
- package/components/AreaChart.vue +145 -0
- package/components/Avatar.vue +267 -0
- package/components/AvatarGroup.vue +94 -0
- package/components/BadgeStrip.vue +203 -0
- package/components/Bars.vue +207 -0
- package/components/BoroughShape.vue +127 -0
- package/components/BrowserFrame.vue +484 -0
- package/components/CalendarGrid.vue +480 -0
- package/components/Card.vue +70 -0
- package/components/Carousel.vue +266 -0
- package/components/ChartFrame.vue +426 -0
- package/components/ChordChart.vue +264 -0
- package/components/ComposedChart.vue +187 -0
- package/components/Contents.vue +86 -0
- package/components/DataTable.vue +244 -0
- package/components/DeckMap.vue +409 -0
- package/components/DeviceFrame.vue +331 -0
- package/components/DonutChart.vue +103 -0
- package/components/DottedMap.vue +126 -0
- package/components/Endorsement.vue +76 -0
- package/components/Grid.vue +169 -0
- package/components/GroupedBarChart.vue +141 -0
- package/components/IconTile.vue +116 -0
- package/components/Kpi.vue +188 -0
- package/components/LineChart.vue +133 -0
- package/components/Logo.vue +203 -0
- package/components/LondonMap.vue +348 -0
- package/components/MarkPair.vue +109 -0
- package/components/Marquee.vue +263 -0
- package/components/NestedDonutChart.vue +295 -0
- package/components/Placeholder.vue +88 -0
- package/components/SankeyChart.vue +223 -0
- package/components/ScatterChart.vue +142 -0
- package/components/StackedBarChart.vue +143 -0
- package/components/StatCard.vue +134 -0
- package/components/StatusTrack.vue +334 -0
- package/components/Timeline.vue +249 -0
- package/components/TimelineChart.vue +329 -0
- package/components/TreemapChart.vue +267 -0
- package/components/backgrounds.js +135 -0
- package/components/boroughGeometry.ts +59 -0
- package/components/chartData.ts +277 -0
- package/components/chartTip.ts +201 -0
- package/components/codeTheme.ts +239 -0
- package/components/deckPalette.ts +157 -0
- package/components/dottedMapGeometry.js +60 -0
- package/components/iconSlugs.js +55 -0
- package/components/iconUrl.js +51 -0
- package/components/mermaidTheme.ts +972 -0
- package/components/motionDuration.ts +41 -0
- package/components/tableRules.ts +257 -0
- package/confidential-mark.vue +151 -0
- package/custom-nav-controls.vue +79 -0
- package/endorsements/b-corp-black.svg +30 -0
- package/endorsements/b-corp-white.svg +30 -0
- package/global-top.vue +1662 -0
- package/icons/forest/bike-asterisk.svg +21 -0
- package/icons/forest/bike-down.svg +19 -0
- package/icons/forest/bike-off.svg +20 -0
- package/icons/forest/bike-up.svg +19 -0
- package/icons/forest/bike-x.svg +19 -0
- package/icons/forest/bike.svg +18 -0
- package/icons/forest/mark-fill.svg +6 -0
- package/icons/forest/mark.svg +6 -0
- package/icons/forest/parking.svg +3 -0
- package/icons/forest/star-fill.svg +4 -0
- package/icons/forest/star.svg +4 -0
- package/icons/forest/traffic-light-caution.svg +8 -0
- package/icons/forest/traffic-light-go.svg +8 -0
- package/icons/forest/traffic-light-lit.svg +8 -0
- package/icons/forest/traffic-light-stop.svg +8 -0
- package/icons/forest/traffic-light.svg +8 -0
- package/icons/lucide/battery.svg +1 -0
- package/icons/lucide/bike.svg +1 -0
- package/icons/lucide/chart-line.svg +1 -0
- package/icons/lucide/check.svg +1 -0
- package/icons/lucide/circle-check.svg +1 -0
- package/icons/lucide/clock.svg +1 -0
- package/icons/lucide/code.svg +1 -0
- package/icons/lucide/component.svg +1 -0
- package/icons/lucide/frame.svg +1 -0
- package/icons/lucide/layout-grid.svg +1 -0
- package/icons/lucide/map-pin.svg +1 -0
- package/icons/lucide/map.svg +1 -0
- package/icons/lucide/palette.svg +1 -0
- package/icons/lucide/table.svg +1 -0
- package/icons/lucide/triangle-alert.svg +1 -0
- package/icons/lucide/type.svg +1 -0
- package/icons/lucide/wrench.svg +1 -0
- package/layouts/bleed.vue +317 -0
- package/layouts/cover.vue +1514 -0
- package/layouts/default.vue +187 -0
- package/layouts/divider.vue +286 -0
- package/layouts/end.vue +384 -0
- package/layouts/quote.vue +218 -0
- package/layouts/split.vue +1875 -0
- package/layouts/stack.vue +944 -0
- package/layouts/statement.vue +67 -0
- package/manifest/chrome.ts +36 -0
- package/manifest/components.ts +49 -0
- package/manifest/index.ts +57 -0
- package/manifest/layouts.ts +37 -0
- package/manifest/manifest.json +6514 -0
- package/manifest/presets.ts +635 -0
- package/notch.js +458 -0
- package/notchPath.js +188 -0
- package/package.json +77 -0
- package/section.js +110 -0
- package/setup/mermaid-renderer.ts +2120 -0
- package/setup/mermaid.ts +30 -0
- package/setup/shiki.ts +44 -0
- package/setup/shortcuts.ts +28 -0
- package/slide-bottom.vue +305 -0
- package/slide-top.vue +268 -0
- package/styles/base.css +2611 -0
- package/styles/index.ts +71 -0
- package/styles/tokens.css +144 -0
- package/uno.config.ts +231 -0
- package/useNotchCuts.js +200 -0
package/setup/mermaid.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The FLOOR under setup/mermaid-renderer.ts, and it exists because of a failure that was invisible.
|
|
2
|
+
//
|
|
3
|
+
// The custom renderer is what gives this deck two registers (see that file for why a config alone
|
|
4
|
+
// cannot). While it was being written it failed to load — `mermaid` was not resolvable from this
|
|
5
|
+
// package, so Vite could not transform the module — and @slidev/client's own wiring is:
|
|
6
|
+
//
|
|
7
|
+
// export default [Object.values(setup0)[0], …].filter(Boolean)
|
|
8
|
+
// …
|
|
9
|
+
// for (const setup of mermaidRenderers) { const renderer = await setup(); if (renderer) { … } }
|
|
10
|
+
// // fallback: existing mermaid
|
|
11
|
+
//
|
|
12
|
+
// A setup module that fails to load is filtered out and Slidev falls back. So the deck rendered
|
|
13
|
+
// PERFECTLY WELL and completely unbranded: lavender nodes, 10px labels, no error in the console, no
|
|
14
|
+
// warning in the terminal, nothing on the page to look at and doubt. It was found by fetching the
|
|
15
|
+
// virtual setup module by hand and noticing the array had length 0.
|
|
16
|
+
//
|
|
17
|
+
// This file makes that failure mode land on Forest colours instead of mermaid's defaults. It cannot
|
|
18
|
+
// give a dark slide the dark palette — a MermaidSetup is wrapped in `createSingletonPromise` and
|
|
19
|
+
// resolved once, which is the whole reason the renderer exists — so a fallback render on a dark
|
|
20
|
+
// slide would still be wrong. But it would be wrong in a way anyone can SEE, which the lavender
|
|
21
|
+
// was not.
|
|
22
|
+
//
|
|
23
|
+
// The gate that catches the underlying cause is themeImports.static.test.ts: every import specifier
|
|
24
|
+
// in this directory has to resolve from this package.
|
|
25
|
+
import type { MermaidSetup } from '@slidev/types';
|
|
26
|
+
import { mermaidConfig } from '../components/mermaidTheme';
|
|
27
|
+
|
|
28
|
+
const setupMermaid: MermaidSetup = () => mermaidConfig('light');
|
|
29
|
+
|
|
30
|
+
export default setupMermaid;
|
package/setup/shiki.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Slidev discovers a theme's Shiki config through `setup/shiki.<ts|js|mts|mjs>` (@slidev/cli's
|
|
2
|
+
// node/virtual/setups.ts globs exactly that path across every root) and merges each root's return
|
|
3
|
+
// value, deck last. Returning `themes` rather than `theme` puts Shiki into DUAL mode: every token
|
|
4
|
+
// span carries `--shiki-light` and `--shiki-dark` and a stylesheet picks between them, which is the
|
|
5
|
+
// only shape that can serve a deck whose register changes per SLIDE. See components/codeTheme.ts for
|
|
6
|
+
// the palette and the measured reason the shipped default could not stay.
|
|
7
|
+
//
|
|
8
|
+
// ★ THIS FILE RUNS IN NODE AS WELL AS THE BROWSER. Slidev highlights magic-move steps at BUILD time
|
|
9
|
+
// (`shiki.codeToTokens` inside the magic-move codeblock transformer), so a theme that could only be
|
|
10
|
+
// assembled in front of a live document would produce un-highlighted animated code and nothing
|
|
11
|
+
// would say why. components/deckPalette.ts is written to that constraint — pure token arithmetic,
|
|
12
|
+
// no DOM.
|
|
13
|
+
//
|
|
14
|
+
// ★ LANGUAGES MUST BE NAMED. Shiki has every bundled grammar available as INPUT, but only the names
|
|
15
|
+
// in `languageNames` are actually loaded in the browser (@slidev/client's setup/shiki-options.ts
|
|
16
|
+
// seeds that set with markdown/vue/js/ts/html/css and nothing else). A ```sql fence with `sql`
|
|
17
|
+
// unlisted renders as plain text — no error, no warning, just a code block with no colour in it.
|
|
18
|
+
// So every language any Forest deck highlights is listed here, and codeTheme.static.test.ts checks
|
|
19
|
+
// the specimen book's own fences against this list so a new language cannot ship un-highlighted.
|
|
20
|
+
import { forestCodeThemes } from '../components/codeTheme';
|
|
21
|
+
|
|
22
|
+
/** Every language a Forest deck may highlight. Alphabetical; `vue` is already seeded but restated
|
|
23
|
+
* so the list reads as the complete answer to "what can a slide use". */
|
|
24
|
+
export const DECK_LANGS = [
|
|
25
|
+
'bash',
|
|
26
|
+
'css',
|
|
27
|
+
'go',
|
|
28
|
+
'html',
|
|
29
|
+
'javascript',
|
|
30
|
+
'json',
|
|
31
|
+
'markdown',
|
|
32
|
+
'python',
|
|
33
|
+
'sql',
|
|
34
|
+
'typescript',
|
|
35
|
+
'vue',
|
|
36
|
+
'yaml',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export default function setupShiki() {
|
|
40
|
+
return {
|
|
41
|
+
themes: forestCodeThemes(),
|
|
42
|
+
langs: DECK_LANGS,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// task-14-brief.md, requirement 6 — the keyboard toggle for the layout-debug overlay ("would it
|
|
2
|
+
// be possible if you add red lines for border in a way to debug it?"). Slidev discovers a theme's
|
|
3
|
+
// own shortcuts through `setup/shortcuts.<ts|mts|js|mjs>` at the theme root (@slidev/cli's
|
|
4
|
+
// node/virtual/setups.ts globs exactly that path across every root, theme included) and composes
|
|
5
|
+
// each file's default export over Slidev's own built-in shortcut list — see
|
|
6
|
+
// @slidev/client/setup/shortcuts.ts's own `for (const setup of setups)` loop.
|
|
7
|
+
//
|
|
8
|
+
// `key: 'l'` is a plain STRING, not a magicKeys ref — @slidev/client's own shortcut() function
|
|
9
|
+
// (logic/shortcuts.ts) converts a string key through `magicKeys[key]` internally, so a setup file
|
|
10
|
+
// never needs to import magicKeys itself to bind a single key. 'l' is free: Slidev's own defaults
|
|
11
|
+
// use space/arrows/pageup/pagedown/d(ark)/g(oto)/o(verview)/`` ` ``/f(ullscreen) and nothing else,
|
|
12
|
+
// checked against @slidev/client's setup/shortcuts.ts and logic/shortcuts.ts directly rather than
|
|
13
|
+
// assumed — 'l' for "layout" was free in both.
|
|
14
|
+
//
|
|
15
|
+
// NOTHING here decides whether the overlay is safe to render — this file only flips the ref.
|
|
16
|
+
// Slidev's own shortcut engine already refuses to fire ANY shortcut while `isPrintMode` is true
|
|
17
|
+
// (logic/shortcuts.ts: `enabled = and(not(isInputting), not(isOnFocus), not(isPrintMode),
|
|
18
|
+
// shortcutsEnabled)`), so this key cannot even be pressed during `slidev export`'s Playwright run
|
|
19
|
+
// — but global-top.vue does not rely on that either; see its own two-guard comment for why the
|
|
20
|
+
// render-time check is independent of whether this shortcut could theoretically fire.
|
|
21
|
+
import { toggleLayoutDebug } from '../debug/layoutDebug.js';
|
|
22
|
+
|
|
23
|
+
export default function setupLayoutDebugShortcut(_nav, shortcuts) {
|
|
24
|
+
return [
|
|
25
|
+
...shortcuts,
|
|
26
|
+
{ name: 'toggle_layout_debug', key: 'l', fn: toggleLayoutDebug },
|
|
27
|
+
];
|
|
28
|
+
}
|
package/slide-bottom.vue
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/*
|
|
3
|
+
Running chrome, mounted PER SLIDE — the bottom half.
|
|
4
|
+
|
|
5
|
+
It replaces global-bottom.vue, which decided visibility from a hard-coded Set of layout names —
|
|
6
|
+
a list that had to be edited by hand every time a layout was added, and which was already wrong:
|
|
7
|
+
a `layout: default` slide carrying `class: deck-dark` still got light-ground footer text.
|
|
8
|
+
|
|
9
|
+
slide-bottom.vue is mounted inside the slide with the route's frontmatter injected, so a slide
|
|
10
|
+
says `chrome: false` for itself and every layout stays ignorant of the chrome.
|
|
11
|
+
|
|
12
|
+
The page number is BACK in this row, and deliberately at the opposite end from the confidentiality
|
|
13
|
+
pill — Jose: "the page number i think it can go at the bottom, other extreme of the confidential
|
|
14
|
+
chip". The earlier note here argued the number and the section "only make sense read together";
|
|
15
|
+
that was wrong about what each is for. The section answers WHERE AM I and belongs at the top with
|
|
16
|
+
the mark, where a reader looks up. The page number is a reference handle ("slide 34") that gets
|
|
17
|
+
used out loud, and the foot of the slide is where an audience already looks for one. The two feet
|
|
18
|
+
are the two ends of one band. Beside it sits the confidentiality pill: a
|
|
19
|
+
watermark from `confidential: '<text>'` in a slide's own frontmatter, absent by default. Unlike
|
|
20
|
+
the page number and section, it is NOT gated on `currentPage > 1` — a page count is meaningless on
|
|
21
|
+
a cover slide, but a confidentiality marking is exactly the kind of thing a real deck still wants
|
|
22
|
+
on its title slide, so this only reads `chrome`, not the page number.
|
|
23
|
+
|
|
24
|
+
★ The key is `confidential`, not `info` — `info` is reserved on slide 1 (Slidev's own meta
|
|
25
|
+
description; see RESERVED_ON_SLIDE_ONE in test/props.ts) and a layout prop or a frontmatter read
|
|
26
|
+
named after it would silently see Slidev's own value instead of whatever an author intended.
|
|
27
|
+
|
|
28
|
+
★ FIX ROUND 1, MINOR — task-5-brief.md's Step 3 says "text from headmatter `confidential:`",
|
|
29
|
+
which reads as if a bare `confidential: '…'` in the deck's headmatter (slide 1's own frontmatter
|
|
30
|
+
block) marks the WHOLE DECK. It does not: this component reads `injectionFrontmatter`, which is
|
|
31
|
+
the CURRENT slide's own frontmatter only, and a bare headmatter key is only ever slide 1's own
|
|
32
|
+
frontmatter — it does not propagate. Measured live, both ways: `confidential: '…'` written
|
|
33
|
+
directly in the deck's headmatter shows up ONLY on slide 1 (and only if `chrome` allows it
|
|
34
|
+
there); wrapping the SAME key inside headmatter's own `defaults:` block —
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
theme: '@humanforest/slidev-theme'
|
|
38
|
+
defaults:
|
|
39
|
+
confidential: 'Private & Confidential'
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
— renders it on every slide that doesn't override or clear it, confirmed live on a slide with no
|
|
43
|
+
`confidential:` of its own. `defaults:` is Slidev's own headmatter-to-every-slide mechanism (it
|
|
44
|
+
is why this key is safe to use at all: RESERVED_ON_SLIDE_ONE already lists `defaults` itself as
|
|
45
|
+
reserved, precisely because Slidev owns this exact merge). This file's own behaviour needed no
|
|
46
|
+
code change for this — it already just reads whatever frontmatter object it was handed, correctly,
|
|
47
|
+
regardless of which of the two forms above put the value there — but the BRIEF's phrasing and this
|
|
48
|
+
comment both under-specified WHICH form produces deck-wide behaviour, and neither is Slidev
|
|
49
|
+
looking it up for you: get the form wrong and the pill silently never shows past slide 1.
|
|
50
|
+
|
|
51
|
+
★ AUTHORING NOTE, measured live: `bleed.vue`'s own `::overlay::` slot is documented to anchor
|
|
52
|
+
content bottom-left inside the same safe margin this pill uses (see the demo deck's own bleed
|
|
53
|
+
slide). Forcing `chrome: true` AND `confidential: '…'` on a bleed slide whose overlay ALSO
|
|
54
|
+
anchors bottom-left collides — the overlay content and this pill occupy the same corner, and
|
|
55
|
+
nothing here reflows around the other because neither knows the other exists. This is why every
|
|
56
|
+
bleed slide in the demo deck keeps `chrome: false`: full-bleed media is meant to run the overlay
|
|
57
|
+
right up to the edge, and the running chrome is exactly the kind of thing that edge shouldn't
|
|
58
|
+
have to make room for. An author who wants both needs to keep the overlay clear of the bottom-
|
|
59
|
+
left safe margin (`--deck-inset-chrome`, task-14-brief.md — was `--deck-space-3`/`--deck-grid *
|
|
60
|
+
2.5`, two different numbers for the same corner; now the one token this pill's own `left`/
|
|
61
|
+
`bottom` below actually use) themselves.
|
|
62
|
+
|
|
63
|
+
Reads `injectionFrontmatter`. SlideWrapper.vue:29 provides the RAW frontmatter object, not a
|
|
64
|
+
ref — `provideLocal(injectionFrontmatter, props.route.meta.slide.frontmatter)`. Do not reach
|
|
65
|
+
for `.value`.
|
|
66
|
+
|
|
67
|
+
The import below spells out the `.ts` extension. Without it (`from '@slidev/client/constants'`,
|
|
68
|
+
the extensionless subpath the package's own exports map advertises) Vite mis-resolves the
|
|
69
|
+
specifier in this monorepo's live dev server: it rewrites it to a bare filesystem path missing
|
|
70
|
+
the `/@fs/` prefix, the extension and the cache-busting query every other import in this file
|
|
71
|
+
gets, and the module fails to load entirely (verified live — blank slide, console "Failed to
|
|
72
|
+
fetch dynamically imported module"). @slidev/cli's own generated imports always spell out the
|
|
73
|
+
extension for exactly this family of subpath (see templateImportContextUtils in
|
|
74
|
+
node/vite/common.ts: `"@slidev/client/context.ts"`) — this file follows that precedent.
|
|
75
|
+
*/
|
|
76
|
+
import { computed, inject } from 'vue';
|
|
77
|
+
import { useNav } from '@slidev/client';
|
|
78
|
+
import { injectionCurrentPage, injectionFrontmatter } from '@slidev/client/constants.ts';
|
|
79
|
+
import { chromeShows, darkChromeAt, darkChromeParts, darkRegister } from './notch.js';
|
|
80
|
+
import { chromePillStyle } from './components/backgrounds.js';
|
|
81
|
+
// Explicit: this file sits at the package ROOT beside slide-bottom, not under components/,
|
|
82
|
+
// so Slidev's auto-registration does not reach it. It is chrome, and chrome lives here.
|
|
83
|
+
import ConfidentialMark from './confidential-mark.vue';
|
|
84
|
+
import { confidentialHidden } from './debug/reviewToggles.js';
|
|
85
|
+
|
|
86
|
+
const frontmatter = inject(injectionFrontmatter, {});
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
★★ THE CHROME OPTS ITSELF INTO THE DARK REGISTER, because nothing else can put it there.
|
|
90
|
+
|
|
91
|
+
`deck-dark` lands on `.slidev-layout` — that is where both `class: deck-dark` and the layouts'
|
|
92
|
+
`dark` prop write it — and SlideWrapper renders this chrome as a SIBLING of the layout, not a
|
|
93
|
+
descendant. So every --deck-* role inside the chrome kept resolving on the LIGHT register while
|
|
94
|
+
the slide beside it was dark. Measured on a `dark: true` slide before this: the page pill painted
|
|
95
|
+
forest-700 with white ink, identical to its light-register self.
|
|
96
|
+
|
|
97
|
+
Reading the same frontmatter this component already injects and stamping the class on its own root
|
|
98
|
+
is the only place the two can be reconciled without moving `deck-dark` onto the page element,
|
|
99
|
+
which no layout can reach.
|
|
100
|
+
*/
|
|
101
|
+
// Delegated to notch.js: a cover can resolve a dark ground from its VARIANT without saying so in
|
|
102
|
+
// frontmatter, and this row is a sibling of the layout so it cannot read the classes that resolution
|
|
103
|
+
// lands as. See darkRegister() — the same module, and the same reason, as darkChromeParts below.
|
|
104
|
+
const isDarkRegister = computed(() => darkRegister(frontmatter));
|
|
105
|
+
// The same per-end register the top row takes. This row's own end is 'none' on stack — the strip
|
|
106
|
+
// below a band is cream by definition — and the figure side on split. See darkChromeParts().
|
|
107
|
+
const darkParts = computed(() => darkChromeParts(frontmatter).bottom);
|
|
108
|
+
// From injectionCurrentPage, NOT useNav().currentPage — the /print and /overview routes carry no
|
|
109
|
+
// `:no` param, so useNav() reports 1 for every slide at once and an exported PDF would number every
|
|
110
|
+
// page "1". SlideWrapper.vue provides this per slide on every route. slide-top.vue's own comment
|
|
111
|
+
// records the measured failure this guards against.
|
|
112
|
+
const currentPage = inject(injectionCurrentPage, computed(() => 1));
|
|
113
|
+
|
|
114
|
+
/*
|
|
115
|
+
Beats opt out by declaring `chrome: false` in their own frontmatter — and a DIVIDER opts out by
|
|
116
|
+
being one, because it takes over the whole field and the pill it would draw names the section it
|
|
117
|
+
is itself announcing. `??` rather than `!== false`, so an explicit `chrome: true` still brings the
|
|
118
|
+
row back: the default moves, the lever does not.
|
|
119
|
+
|
|
120
|
+
DELEGATED to notch.js rather than written here, and that is the gate talking rather than taste:
|
|
121
|
+
frontmatter.static.test.ts holds every chrome file to naming NO layout, because chrome whose
|
|
122
|
+
visibility switches on the layout showing is a defect this theme has already had. The rule is
|
|
123
|
+
real, so the knowledge lives in the one module the gate exempts for exactly this — the same place
|
|
124
|
+
`split` is named for the mirror — and this file reads a predicate rather than a layout name.
|
|
125
|
+
|
|
126
|
+
★★ AND IT GATES THE PAGE NUMBER, NOT THE ROW. There used to be a `show` here — `chromeWanted` —
|
|
127
|
+
ANDed over the whole band, so `chrome: false` took the confidentiality marking down with the page
|
|
128
|
+
number. A page number is furniture: it is the deck telling you where you are, and a slide that
|
|
129
|
+
wants no furniture means it. A confidentiality marking is a statement about the CONTENT, and a
|
|
130
|
+
deck that drops its chrome for a full-bleed photograph has not stopped being confidential.
|
|
131
|
+
|
|
132
|
+
So the opt-out reaches the number (through `chromeShows`, which is `chromeWanted && page > 1`) and
|
|
133
|
+
the marking answers to `confidential:` alone. Both of the marking's exemptions now point the same
|
|
134
|
+
way — notch.js records the other one, the slide-1 gate, for the same reason.
|
|
135
|
+
*/
|
|
136
|
+
/*
|
|
137
|
+
The reviewer's preview-only override (custom-nav-controls.vue). It can only ever SUBTRACT: a slide
|
|
138
|
+
with no `confidential:` cannot be made to show one from the toolbar.
|
|
139
|
+
|
|
140
|
+
`isPrintMode` is re-checked HERE rather than trusted from the control that sets it. The nav bar
|
|
141
|
+
already refuses to render under print, so this is the second of two independent guards — and it is
|
|
142
|
+
the one that matters, because `slidev export` drives a real browser through this same client and a
|
|
143
|
+
toggle left on would otherwise ship a PDF missing its own confidentiality marking. A guard whose
|
|
144
|
+
failure mode is "the legal marking silently disappears" is worth stating twice.
|
|
145
|
+
*/
|
|
146
|
+
const { isPrintMode } = useNav();
|
|
147
|
+
const confidentialShows = computed(
|
|
148
|
+
() => !!frontmatter?.confidential && (isPrintMode.value || !confidentialHidden.value),
|
|
149
|
+
);
|
|
150
|
+
// The number carries the EXTRA gate the confidentiality pill does not: a page count is meaningless
|
|
151
|
+
// on a cover, while a confidentiality marking is exactly what a title slide still wants.
|
|
152
|
+
const showPage = computed(() => chromeShows(frontmatter, currentPage.value));
|
|
153
|
+
|
|
154
|
+
/*
|
|
155
|
+
★ THE MIRROR IS RETIRED — this pill sits at the same end on every slide.
|
|
156
|
+
|
|
157
|
+
Jose, on task-15-brief.md's notch: "if text is left or right, the notch position should adapt (and
|
|
158
|
+
move the page to the other side, along with the confidenciality chip)". The chip is this. The
|
|
159
|
+
decision is ./notch.js's, shared with slide-top.vue and split.vue rather than re-derived here, so
|
|
160
|
+
the page marker and this pill can never end up in opposite corners.
|
|
161
|
+
|
|
162
|
+
A bottom pill left in place while the top one mirrored would be worse than not mirroring at all:
|
|
163
|
+
on a `flip: true` split the panel's own bottom-left corner would sit over it.
|
|
164
|
+
*/
|
|
165
|
+
// (The pills' end of this row used to be computed from the mirror. It is a literal in the template
|
|
166
|
+
// now — see the note on the row-reverse rule below.)
|
|
167
|
+
// The page number alone — no "/ total". A running total tells a reader how much is left, which is
|
|
168
|
+
// a thing a presenter usually does NOT want advertised, and it made the pill wide enough to read as
|
|
169
|
+
// a label rather than a marker. The number is what anyone actually refers to ("slide 34").
|
|
170
|
+
const pageLabel = computed(() => String(currentPage.value));
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<template>
|
|
176
|
+
<div v-if="confidentialShows || showPage" :class="['deck-chrome-bottom', isDarkRegister ? 'deck-dark' : '']" data-pills-side="left">
|
|
177
|
+
<!-- Either child showing brings the band; neither one is gated on the other. See the note on
|
|
178
|
+
`showPage` above for why the marking does not answer to `chrome: false`. -->
|
|
179
|
+
<!-- The spacer is not decoration: with `justify-content: space-between` and only ONE child, a
|
|
180
|
+
lone page number would fall to the start of the row instead of holding its own end. -->
|
|
181
|
+
<ConfidentialMark
|
|
182
|
+
v-if="confidentialShows"
|
|
183
|
+
class="deck-chrome-confidential"
|
|
184
|
+
:class="darkChromeAt(darkParts, 'left') ? 'deck-dark' : ''"
|
|
185
|
+
:label="frontmatter.confidential"
|
|
186
|
+
/>
|
|
187
|
+
<span v-else class="deck-chrome-spacer" aria-hidden="true" />
|
|
188
|
+
<span
|
|
189
|
+
v-if="showPage"
|
|
190
|
+
class="deck-page-dot"
|
|
191
|
+
:class="darkChromeAt(darkParts, 'right') ? 'deck-dark' : ''"
|
|
192
|
+
:style="chromePillStyle()"
|
|
193
|
+
>{{ pageLabel }}</span>
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
196
|
+
|
|
197
|
+
<style scoped>
|
|
198
|
+
.deck-chrome-bottom {
|
|
199
|
+
position: absolute;
|
|
200
|
+
/* Chrome is inset --deck-inset-chrome on every edge it touches — the same token slide-top.vue's
|
|
201
|
+
row uses on all four of ITS edges, so the two bands sit the identical distance from the canvas. */
|
|
202
|
+
bottom: var(--deck-inset-chrome);
|
|
203
|
+
/* BOTH edges now, which the single-child version could not do. Its own comment warned that pinning
|
|
204
|
+
left AND right would "stretch a short confidentiality string into a wide empty pill" — true when
|
|
205
|
+
the PILL was the positioned box, and no longer true now that the positioned box is this row and
|
|
206
|
+
the pill is a shrink-wrapped child of it. */
|
|
207
|
+
left: var(--deck-inset-chrome);
|
|
208
|
+
right: var(--deck-inset-chrome);
|
|
209
|
+
display: flex;
|
|
210
|
+
/* flex-END, mirroring slide-top.vue's flex-start, and for the same reason. The band is the PAGE
|
|
211
|
+
MARKER's height (49) and the confidentiality chip is 31, so centring floated the chip's bottom
|
|
212
|
+
at 45 from the canvas where the band's own bottom is 36 — the chip read as not quite sitting
|
|
213
|
+
down. Reported by eye. Bottom-aligning gives the two ends of this band one shared baseline at
|
|
214
|
+
--deck-inset-chrome, exactly as the top row shares one at the same token. */
|
|
215
|
+
align-items: flex-end;
|
|
216
|
+
gap: var(--deck-space-2);
|
|
217
|
+
/* The two ends of one band: the confidentiality pill at one, the page number at the other. */
|
|
218
|
+
justify-content: space-between;
|
|
219
|
+
z-index: 20;
|
|
220
|
+
pointer-events: none;
|
|
221
|
+
}
|
|
222
|
+
/* The mirror. The pill travels to the TEXT side with the rest of the chrome (./notch.js owns that
|
|
223
|
+
decision), and the page number takes whichever end is left — so the pair always spans the foot
|
|
224
|
+
rather than crowding one corner.
|
|
225
|
+
|
|
226
|
+
★ THE row-reverse RULE WENT WITH THE MIRROR. It swapped which end was which on a `flip: true`
|
|
227
|
+
split; nothing flips now, so a selector for the other order would be one nothing can match. The
|
|
228
|
+
attribute stays as a literal because the two ends still need naming in the CSS below. */
|
|
229
|
+
/* The pill may shrink (BadgeStrip's own 20em cap and ellipsis do the truncating); the number never
|
|
230
|
+
does — a marker that squeezed would stop being one shape across the deck. */
|
|
231
|
+
.deck-chrome-confidential { min-width: 0; }
|
|
232
|
+
.deck-chrome-spacer { flex: none; }
|
|
233
|
+
/*
|
|
234
|
+
The page marker: a circle, not a pill — and now actually one at every digit count.
|
|
235
|
+
|
|
236
|
+
It carries BadgeStrip's background pair (via backgroundStyle, so fill and ink can never be mismatched —
|
|
237
|
+
13 of 24 fill x ink combinations fail body contrast) and its overline type role, so the two still
|
|
238
|
+
read as one family. What it does NOT share is the geometry: a pill is sized by its content's
|
|
239
|
+
width, which makes "8" and "216" different shapes, and a marker has to be the same shape on every
|
|
240
|
+
slide or it stops reading as a marker.
|
|
241
|
+
|
|
242
|
+
A STADIUM, NOT A CIRCLE — and it grows with the number.
|
|
243
|
+
|
|
244
|
+
It was a circle, squared off by declaring both axes from the deck's widest number, so the marker
|
|
245
|
+
was one constant shape whatever page it sat on. Jose: "the page should not be a circle but a full
|
|
246
|
+
rounded rectangle... and it should adapt the notch, even if it have 2, 3, 4 digits". A stadium
|
|
247
|
+
gets that for free: the height is the overline line box, the width is whatever the digits need
|
|
248
|
+
plus symmetric padding, and `min-width` equal to the height keeps a single digit from collapsing
|
|
249
|
+
into a lozenge narrower than it is tall. One digit still reads as a circle; four reads as a pill.
|
|
250
|
+
|
|
251
|
+
WHAT SURVIVES FROM THE CIRCLE, because both were measured and neither was about the shape:
|
|
252
|
+
|
|
253
|
+
1. NO TRACKING. The overline role tracks 0.08em, applied after the last character too, so a
|
|
254
|
+
tracked number sits ~2.4px left of its own optical centre. Tracking is a LABEL treatment; a
|
|
255
|
+
marker is a number in a box. The section pill keeps the role's tracking, which is what makes
|
|
256
|
+
the two read as label-and-marker rather than as two labels.
|
|
257
|
+
2. TABULAR FIGURES. Proportional digits change the box's width between slides — "11" is far
|
|
258
|
+
narrower than "18" — so the marker would twitch on every page turn.
|
|
259
|
+
|
|
260
|
+
The width is now CONTENT-driven rather than declared from `total`, which is what lets the notch
|
|
261
|
+
track it: slide-bottom measures the rendered box and publishes it, so the cut follows the real
|
|
262
|
+
number rather than the widest one the deck might reach.
|
|
263
|
+
*/
|
|
264
|
+
.deck-page-dot {
|
|
265
|
+
flex: none;
|
|
266
|
+
display: inline-flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
justify-content: center;
|
|
269
|
+
box-sizing: border-box;
|
|
270
|
+
height: var(--deck-page-dot-line);
|
|
271
|
+
/* Never narrower than it is tall — a lone "1" stays a circle rather than a slot. */
|
|
272
|
+
min-width: var(--deck-page-dot-line);
|
|
273
|
+
--deck-page-dot-line: calc(var(--type-deck-overline-size) * var(--type-deck-overline-line-height) + var(--deck-space-1) * 2);
|
|
274
|
+
padding-inline: var(--deck-space-2);
|
|
275
|
+
border-radius: var(--deck-radius-full);
|
|
276
|
+
font-family: var(--type-deck-overline-family);
|
|
277
|
+
font-size: var(--type-deck-overline-size);
|
|
278
|
+
line-height: var(--type-deck-overline-line-height);
|
|
279
|
+
font-weight: var(--type-deck-overline-weight);
|
|
280
|
+
letter-spacing: normal;
|
|
281
|
+
font-variant-numeric: tabular-nums;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/*
|
|
285
|
+
★★ THE REGISTER LANDS HERE TO RE-POINT THE PILLS, NEVER TO PAINT. styles/tokens.css gives
|
|
286
|
+
`.deck-dark` a ground of its own — right for a wrapper INSIDE a slide (a dark card band, a chart
|
|
287
|
+
well), wrong for this row, which floats over the slide and has never had a ground. Measured on the
|
|
288
|
+
specimen's alert slide the moment the per-end register landed: a forest-950 block behind the mark,
|
|
289
|
+
sitting on the forest-900 panel it was supposed to match.
|
|
290
|
+
|
|
291
|
+
★★ AND THE ROW ITSELF, NOT ONLY ITS CHILDREN. This rule listed the pills, the mark and the
|
|
292
|
+
confidential flag and stopped there — so the container kept the ground, and the container is the
|
|
293
|
+
one that spans the full 1848px. Invisible on a bare slide, because there is nothing behind it to
|
|
294
|
+
cover. Measured on a dark split slide carrying a full-height map: `deck-chrome-top.deck-dark` at
|
|
295
|
+
y 36 h 72 and `deck-chrome-bottom.deck-dark` at y 995 h 49, both painting forest-950 across the
|
|
296
|
+
whole width, over a figure spanning 36 to 1044 — two dark bands across the top and bottom of the
|
|
297
|
+
map, which read as a map that fails to fill its frame rather than as chrome with a ground.
|
|
298
|
+
*/
|
|
299
|
+
.deck-chrome-bottom.deck-dark,
|
|
300
|
+
.deck-chrome-pills.deck-dark,
|
|
301
|
+
.deck-chrome-mark.deck-dark,
|
|
302
|
+
.deck-chrome-confidential.deck-dark {
|
|
303
|
+
background: transparent;
|
|
304
|
+
}
|
|
305
|
+
</style>
|