@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,207 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/*
|
|
3
|
+
A bar chart sized for a room.
|
|
4
|
+
|
|
5
|
+
Deliberately not @humanforest/charts: that package peer-depends on @nuxt/ui and @unovis/vue,
|
|
6
|
+
which is the right trade for a product surface with tooltips, crosshairs and live data, and
|
|
7
|
+
the wrong one for a slide that shows five numbers once. What matters for brand consistency is
|
|
8
|
+
that the COLOURS and the ORDER match — so the fills come from --deck-series-1…5, which alias
|
|
9
|
+
the published categorical palette in the order the product uses it. A deck chart and a
|
|
10
|
+
product chart of the same data are the same chart.
|
|
11
|
+
|
|
12
|
+
Horizontal by default: category labels on a slide are words, and words read horizontally
|
|
13
|
+
without rotating anyone's head.
|
|
14
|
+
|
|
15
|
+
Bars start at zero. Not because a truncated axis is always a lie — it isn't, and the research
|
|
16
|
+
does not support the absolute rule — but because a slide is read in seconds from the back of
|
|
17
|
+
a room, with no axis to inspect, so length has to be the whole message.
|
|
18
|
+
|
|
19
|
+
─────────────────────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
★ WHY THIS STILL EXISTS ALONGSIDE StackedBarChart.vue / GroupedBarChart.vue
|
|
21
|
+
|
|
22
|
+
Asked and answered, against the library rather than from memory, so the next reader does not
|
|
23
|
+
re-open it. FIRST, the half of the old answer that is WRONG: "the Unovis set cannot draw a
|
|
24
|
+
horizontal bar" is false. `@unovis/ts`'s StackedBar and GroupedBar both take
|
|
25
|
+
`orientation: Orientation.Horizontal` (components/{stacked-bar,grouped-bar}/config.d.ts). The
|
|
26
|
+
deck's two bar components simply do not expose it, which is a choice, not a limitation.
|
|
27
|
+
|
|
28
|
+
What is genuinely not expressible there is the rest of this component:
|
|
29
|
+
|
|
30
|
+
· A VALUE AT THE END OF EVERY BAR, IN A COLUMN. Neither bar mark carries a label accessor at
|
|
31
|
+
all — `VisXYLabels` is the only text mark in the library, and it positions in DATA space
|
|
32
|
+
with a background pill and clustering, so the figures would land at each bar's own end
|
|
33
|
+
rather than lining up. This component's `.bars-value` is a real grid column: right-aligned,
|
|
34
|
+
`--font-sans` (Mohr ships no tabular figures, so `tabular-nums` on the display face is a
|
|
35
|
+
silent no-op), digits aligned down the page. A slide that shows five numbers wants them
|
|
36
|
+
readable AS numbers, not inferred off a scale.
|
|
37
|
+
· NO AXIS AND NO GRID. The track IS the 100% reference — ONE track, shared by every row (see
|
|
38
|
+
`.bars`'s own subgrid note below; this is where a bar chart stops being a chart if it is got
|
|
39
|
+
wrong). An Unovis bar gets its category names from an axis, which is scale-positioned; here
|
|
40
|
+
the label column is `auto` — sized to the WIDEST label in the chart and ellipsised past 10em
|
|
41
|
+
— so four borough names do not reserve a fixed gutter.
|
|
42
|
+
· `highlight`, WITH A MEASURED NUMBER BEHIND IT. Stepping every other row back to 0.70 is not
|
|
43
|
+
a style value; see the long note on `.bars-row.is-muted` below for the 3:1 recomputation in
|
|
44
|
+
both registers that produced it. Rebuilding this on Unovis means re-deriving that through a
|
|
45
|
+
colour accessor and re-running the measurement for no brand gain.
|
|
46
|
+
|
|
47
|
+
So: a stack or a cluster of parts, with an axis, is StackedBarChart/GroupedBarChart. Five
|
|
48
|
+
directly-labelled rows with no chrome at all is this. They are different charts, and the fills
|
|
49
|
+
come from the same `seriesVar()` either way, so they can never drift into two palettes.
|
|
50
|
+
*/
|
|
51
|
+
import { computed } from 'vue'
|
|
52
|
+
// The series colour comes from the same accessor the Unovis-backed charts use, so the deck has ONE
|
|
53
|
+
// place that knows a series colour is --deck-series-N and ONE place that knows where the palette
|
|
54
|
+
// stops. (SIX now, not five — chartData.ts carries the number and the reason.) Before this the mapping was a template string inlined here, which is exactly how two chart
|
|
55
|
+
// systems drift into two palettes.
|
|
56
|
+
import { SERIES_CEILING, seriesVar } from './chartData'
|
|
57
|
+
|
|
58
|
+
const props = defineProps({
|
|
59
|
+
/** [{ label, value, series? }] — `series` (1–5) pins a colour; otherwise index order is used. */
|
|
60
|
+
data: { type: Array, required: true },
|
|
61
|
+
/** Force the axis top. Defaults to the largest value, so the longest bar fills the track. */
|
|
62
|
+
max: { type: Number, default: null },
|
|
63
|
+
/** Shown at the end of each bar. Pass a function for units, currency, whatever. */
|
|
64
|
+
format: { type: Function, default: (v) => new Intl.NumberFormat('en-GB').format(v) },
|
|
65
|
+
/** Single-hue: every bar takes the accent, and only `highlight` differs. Use when the bars are
|
|
66
|
+
one series and the comparison is between rows, not between categories. */
|
|
67
|
+
monochrome: { type: Boolean, default: false },
|
|
68
|
+
/** Index of the row that carries the point. Everything else steps back. */
|
|
69
|
+
highlight: { type: Number, default: null },
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const top = computed(() => props.max ?? (Math.max(...props.data.map((d) => d.value), 0) || 1))
|
|
73
|
+
|
|
74
|
+
const fillOf = (row, i) => {
|
|
75
|
+
if (props.monochrome) return 'var(--deck-accent)'
|
|
76
|
+
// `series` is 1-based at the call site (it names a --deck-series-N role); seriesVar is 0-based.
|
|
77
|
+
// The double modulo keeps a hand-written 0 or a negative out of seriesVar's RangeError — a slide
|
|
78
|
+
// with a typo in one row should still draw, in the wrong colour, rather than blanking.
|
|
79
|
+
const n = row.series ?? i + 1
|
|
80
|
+
return seriesVar((((n - 1) % SERIES_CEILING) + SERIES_CEILING) % SERIES_CEILING)
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<div class="bars">
|
|
86
|
+
<div
|
|
87
|
+
v-for="(row, i) in data"
|
|
88
|
+
:key="row.label"
|
|
89
|
+
class="bars-row"
|
|
90
|
+
:class="{ 'is-muted': highlight !== null && highlight !== i }"
|
|
91
|
+
>
|
|
92
|
+
<div class="bars-label">{{ row.label }}</div>
|
|
93
|
+
<div class="bars-track">
|
|
94
|
+
<div class="bars-fill" :style="{ width: `${(row.value / top) * 100}%`, background: fillOf(row, i) }" />
|
|
95
|
+
</div>
|
|
96
|
+
<div class="bars-value">{{ format(row.value) }}</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</template>
|
|
100
|
+
|
|
101
|
+
<style scoped>
|
|
102
|
+
/*
|
|
103
|
+
★★ ONE GRID FOR THE WHOLE CHART, NOT ONE PER ROW — the difference between a bar chart and a
|
|
104
|
+
picture of one.
|
|
105
|
+
|
|
106
|
+
This was `display: flex` here with `grid-template-columns: auto minmax(0, 1fr) auto` on each ROW,
|
|
107
|
+
which reads as the same three columns but is not: `auto` sizes against the row's OWN content, so
|
|
108
|
+
every row got a different label width and therefore a different track — and the track is what
|
|
109
|
+
line ~200 calls "the 100% reference". Measured on apps/deck-template's borough slide, in canvas
|
|
110
|
+
units, before this fix:
|
|
111
|
+
|
|
112
|
+
Hackney 41,200 label 140.1 track 483.0 100.00% → 483.0px
|
|
113
|
+
Islington 33,800 label 139.3 track 483.8 82.04% → 396.9px
|
|
114
|
+
Camden 28,100 label 138.1 track 485.1 68.20% → 330.8px
|
|
115
|
+
Southwark 19,400 label 173.5 track 449.6 47.09% → 211.7px
|
|
116
|
+
|
|
117
|
+
`label + track + value` is 792.0 in every row: the label steals directly from the track. The
|
|
118
|
+
percentages were computed correctly and applied to a reference that moved, so Southwark's bar
|
|
119
|
+
measured against Hackney's scale READ AS 18,058 — 6.9% under the 19,400 printed beside it —
|
|
120
|
+
purely because "Southwark" is a wider word than "Hackney". A chart that draws a longer word as a
|
|
121
|
+
smaller number is not a chart; it is a lie with a legend.
|
|
122
|
+
|
|
123
|
+
The fix is a SUBGRID: `.bars` owns the three tracks, and each row adopts them (`grid-template-
|
|
124
|
+
columns: subgrid`) instead of resolving its own. The label column is now `auto` over the WHOLE
|
|
125
|
+
chart — the widest label in the set — so every track starts at the same x and has the same width,
|
|
126
|
+
and equal values draw equal lengths by construction rather than by every label happening to be
|
|
127
|
+
the same length.
|
|
128
|
+
|
|
129
|
+
A row is still a real box (not `display: contents`), because `.bars-row.is-muted` needs something
|
|
130
|
+
to carry its opacity and a box that can transition. Subgrid inherits the parent's column gap, so
|
|
131
|
+
the 24px between label, track and value is still declared once.
|
|
132
|
+
*/
|
|
133
|
+
.bars {
|
|
134
|
+
display: grid;
|
|
135
|
+
/* Label and value size to their CONTENT; the track takes everything left. A fixed label
|
|
136
|
+
column in `ch` (which is what this was) reserved ~390px of a 830px half-slide for four
|
|
137
|
+
borough names and left the bars as stubs — `ch` is 0.78em in these faces, so a 14ch column
|
|
138
|
+
is 11em wide. `auto` sidesteps the unit question entirely. */
|
|
139
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
140
|
+
row-gap: var(--deck-space-2);
|
|
141
|
+
column-gap: var(--deck-space-3);
|
|
142
|
+
}
|
|
143
|
+
.bars-row {
|
|
144
|
+
display: grid;
|
|
145
|
+
grid-column: 1 / -1;
|
|
146
|
+
grid-template-columns: subgrid;
|
|
147
|
+
align-items: center;
|
|
148
|
+
/* Rows fade rather than move when one is singled out — colour and opacity survive the
|
|
149
|
+
reduced-motion floor, travel does not. */
|
|
150
|
+
transition: opacity var(--motion-base) var(--motion-ease-out);
|
|
151
|
+
}
|
|
152
|
+
/* 0.70, not a rounder-looking 0.5 or 0.45 (the value this was before) — MEASURED, not guessed, and
|
|
153
|
+
MEASURED AGAIN against the FLOOR THAT ACTUALLY APPLIES (fix round 1, IMPORTANT 8 — the first
|
|
154
|
+
pass here cited "the stricter 4.5:1 body floor" for `.bars-label`, which is wrong: at
|
|
155
|
+
--type-deck-body-sm-size (2.25rem = 36px), `.bars-label` is unconditionally WCAG large text —
|
|
156
|
+
36px clears the 24px large-text threshold on its own, independent of font-weight — so 3:1 is the
|
|
157
|
+
floor that actually governs it, not 4.5:1. `.bars-value` (--type-deck-body-size, 3rem = 48px,
|
|
158
|
+
weight 700) is large text by the same rule with even more room. Citing the wrong (stricter)
|
|
159
|
+
floor made the shipped value LOOK more conservative than the real requirement demanded.
|
|
160
|
+
|
|
161
|
+
scripts/deckAudit.ts's rendered-contrast sweep (task-8-brief.md CONTROLLER ADDENDUM 2) found
|
|
162
|
+
0.45 live: a muted row's own `.bars-label` (--deck-fg-muted, i.e. neutral-400 in the dark
|
|
163
|
+
register) blended over --deck-bg (forest-950) at 0.45 measures 2.27:1 — under even the CORRECT
|
|
164
|
+
3:1 floor. Recomputed against 3:1 for the worst real pair in EACH register (dark: neutral-400 on
|
|
165
|
+
forest-950; light: neutral-600 on warm-50): 0.60 → dark 2.99 / light 2.85 (fails), 0.65 → dark
|
|
166
|
+
3.28 / light 3.19 (clears, thin margin), 0.70 → dark 3.59 / light 3.55 (clears, real margin).
|
|
167
|
+
0.70 is the lowest of the tried candidates that clears 3:1 in BOTH registers with a margin
|
|
168
|
+
deliberately larger than rounding noise — picked over 0.85 (which also clears, at 4.63/5.05)
|
|
169
|
+
because a muted row is supposed to read as receded, and 0.85 barely fades at all once the
|
|
170
|
+
correct, more permissive floor is the one actually being cleared. */
|
|
171
|
+
.bars-row.is-muted {
|
|
172
|
+
opacity: 0.7;
|
|
173
|
+
}
|
|
174
|
+
.bars-label {
|
|
175
|
+
font-size: var(--type-deck-body-sm-size);
|
|
176
|
+
color: var(--deck-fg-muted);
|
|
177
|
+
text-align: right;
|
|
178
|
+
white-space: nowrap;
|
|
179
|
+
max-width: 10em; /* a category that needs more than this wants a different chart */
|
|
180
|
+
overflow: hidden;
|
|
181
|
+
text-overflow: ellipsis;
|
|
182
|
+
}
|
|
183
|
+
.bars-track {
|
|
184
|
+
/* No gridlines and no axis: the track itself is the 100% reference. */
|
|
185
|
+
background: var(--deck-surface);
|
|
186
|
+
border-radius: var(--deck-radius-item);
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
}
|
|
189
|
+
.bars-fill {
|
|
190
|
+
height: var(--deck-space-5);
|
|
191
|
+
border-radius: var(--deck-radius-item);
|
|
192
|
+
min-width: 2px;
|
|
193
|
+
}
|
|
194
|
+
/* Sans, not the display face. The values form a COLUMN, so their digits have to line up — and
|
|
195
|
+
Mohr ships no tabular figures, which makes `font-variant-numeric: tabular-nums` on it a silent
|
|
196
|
+
no-op. This is the same reason the system's .type-tabular role swaps the family rather than
|
|
197
|
+
just setting the feature. */
|
|
198
|
+
.bars-value {
|
|
199
|
+
font-family: var(--font-sans);
|
|
200
|
+
font-weight: 700;
|
|
201
|
+
font-size: var(--type-deck-body-size);
|
|
202
|
+
line-height: 1;
|
|
203
|
+
color: var(--deck-fg);
|
|
204
|
+
font-variant-numeric: tabular-nums;
|
|
205
|
+
white-space: nowrap;
|
|
206
|
+
}
|
|
207
|
+
</style>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/*
|
|
3
|
+
A single London borough as a styleable graphic — a glyph, not a chart.
|
|
4
|
+
|
|
5
|
+
Where DottedMap.vue draws the whole city as a dot grid, this hands you one outline, framed to its own
|
|
6
|
+
bounds and filled with currentColor, so it behaves like an icon: it has a natural size, it steps
|
|
7
|
+
up and down a ladder (`size`), and its path takes the ink of whatever colour the <svg> carries.
|
|
8
|
+
|
|
9
|
+
⚠ ONE HALF OF THAT IS STILL A PROMISE THIS FILE DOES NOT KEEP. The scoped rule below pins
|
|
10
|
+
`color: var(--deck-accent)` on the element itself, which OVERRIDES inheritance — so "it takes the
|
|
11
|
+
ink of whatever it sits in" is true of the path relative to the <svg>, and false of the <svg>
|
|
12
|
+
relative to the slide. A `text-*` utility on the tag is inert for exactly the reason a `h-*`
|
|
13
|
+
utility was before `size` existed. Left alone deliberately: unpinning it would repaint every
|
|
14
|
+
existing call site, which is a separate decision from adding a sizing API.
|
|
15
|
+
|
|
16
|
+
Deliberately does one thing. No data, no ramp, no labels, no wards, no selection — the moment a
|
|
17
|
+
slide wants any of those it wants a map, not a glyph. Keeping this presentational is the whole
|
|
18
|
+
point: it is the mark you put beside a number to say WHERE, at a size an audience can recognise
|
|
19
|
+
from the back of a room.
|
|
20
|
+
|
|
21
|
+
Geometry comes from the same generated file the product's own borough shapes read
|
|
22
|
+
(@humanforest/tokens/geo/london-boroughs.json), so a deck outline and a product outline are the
|
|
23
|
+
same outline. The lookup and framing live in boroughGeometry.ts, which names its source of truth.
|
|
24
|
+
*/
|
|
25
|
+
import { computed } from 'vue';
|
|
26
|
+
import boroughs from '@humanforest/tokens/geo/london-boroughs.json';
|
|
27
|
+
import { findBorough, frameViewBox } from './boroughGeometry';
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
The height ladder, in PIXELS. `md` is the NATURAL size — the same number the scoped rule below
|
|
31
|
+
falls back to when no size is asked for — so the ladder contains the default rather than sitting
|
|
32
|
+
beside it, and an author who writes size="md" gets exactly what an author who writes nothing gets.
|
|
33
|
+
The two spellings of that number are pinned to each other by
|
|
34
|
+
packages/slidev-theme/test/boroughShape.static.test.ts, which reads this file's own CSS.
|
|
35
|
+
|
|
36
|
+
★★ PX, NOT GRID UNITS, and the change is deliberate rather than cosmetic. This was
|
|
37
|
+
`{ sm: 6, md: 10, lg: 16, xl: 22 }` multiplied by --deck-grid, which meant a map's height was a
|
|
38
|
+
function of the LAYOUT MODULE. Moving the grid from 40 to 24 would have shrunk every borough
|
|
39
|
+
shape by 40% — 400px to 240px at md — silently, with nothing in the component or its tests
|
|
40
|
+
objecting, because the arithmetic still worked.
|
|
41
|
+
|
|
42
|
+
A figure's height is not layout rhythm. It rode the grid because the grid was a handy number to
|
|
43
|
+
multiply, and that coupling is exactly what turns a spacing decision into a resize of the
|
|
44
|
+
artwork. The spacing ladder is the thing that should follow the grid; a map is not.
|
|
45
|
+
*/
|
|
46
|
+
const SIZES = { sm: 240, md: 400, lg: 640, xl: 880 };
|
|
47
|
+
|
|
48
|
+
const props = defineProps({
|
|
49
|
+
/** Which borough: an ONS code (E09…) or a name ('Southwark'), matched case-insensitively. */
|
|
50
|
+
borough: { type: String, required: true },
|
|
51
|
+
/** Breathing room around the shape, as a fraction of its longer side. */
|
|
52
|
+
pad: { type: Number, default: 0.04 },
|
|
53
|
+
/** Assistive text. With it the graphic is announced as an image; without it it is decorative and hidden, the same call an icon makes. */
|
|
54
|
+
label: { type: String, default: null },
|
|
55
|
+
/** How tall the glyph draws: sm 240px · md 400px · lg 640px · xl 880px. md is the natural size, so a slide that says nothing draws exactly what it drew before this prop existed; xl needs a slide with no heading, since 880px plus an h1 clears the bottom of the canvas. */
|
|
56
|
+
size: {
|
|
57
|
+
type: String,
|
|
58
|
+
default: 'md',
|
|
59
|
+
// Repeats the ladder's own keys as a literal rather than reading Object.keys(SIZES): Vue hoists
|
|
60
|
+
// defineProps() out of setup() at build time, so a validator cannot close over a const declared
|
|
61
|
+
// in the same block — the same constraint Placeholder.vue's `ratio` validator documents.
|
|
62
|
+
validator: (s) => ['sm', 'md', 'lg', 'xl'].includes(s),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const area = computed(() => findBorough(boroughs.areas, props.borough));
|
|
67
|
+
const viewBox = computed(() => frameViewBox(area.value, props.pad));
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
Sizing lands as an inline CUSTOM PROPERTY, never as a class or an inline `height`, and that is the
|
|
71
|
+
whole fix. A scoped rule is `.borough-shape[data-v-…]` — two selectors' worth of specificity — so
|
|
72
|
+
a UnoCSS `h-*` utility on the tag was silently inert and the only way past it was !important. A
|
|
73
|
+
custom property is not in the specificity contest at all: the scoped rule stays the one place the
|
|
74
|
+
height is declared, and this only supplies the number it reads.
|
|
75
|
+
|
|
76
|
+
An unknown `size` resolves to NO inline property at all rather than to `calc(… * undefined)` —
|
|
77
|
+
an invalid declaration a browser drops, which would leave the element with no height whatsoever.
|
|
78
|
+
The var() fallback catches that case and draws the natural size instead.
|
|
79
|
+
|
|
80
|
+
Why a closed ladder and not a free CSS length: the same call Grid.vue's `gap` makes. A deck that
|
|
81
|
+
can express any height expresses four slightly different ones across twelve slides.
|
|
82
|
+
*/
|
|
83
|
+
const sizeStyle = computed(() => {
|
|
84
|
+
const px = SIZES[props.size];
|
|
85
|
+
return px ? { '--borough-shape-height': `${px}px` } : undefined;
|
|
86
|
+
});
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<template>
|
|
90
|
+
<svg
|
|
91
|
+
v-if="area"
|
|
92
|
+
class="borough-shape"
|
|
93
|
+
:style="sizeStyle"
|
|
94
|
+
:viewBox="viewBox"
|
|
95
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
96
|
+
:role="label ? 'img' : undefined"
|
|
97
|
+
:aria-label="label || undefined"
|
|
98
|
+
:aria-hidden="label ? undefined : 'true'"
|
|
99
|
+
>
|
|
100
|
+
<!-- currentColor so the shape takes the ink of the text around it; non-scaling-stroke keeps any
|
|
101
|
+
border a slide adds one hairline whatever the rendered size. -->
|
|
102
|
+
<path :d="area.d" fill="currentColor" vector-effect="non-scaling-stroke" />
|
|
103
|
+
</svg>
|
|
104
|
+
</template>
|
|
105
|
+
|
|
106
|
+
<style scoped>
|
|
107
|
+
.borough-shape {
|
|
108
|
+
/* Sized off its HEIGHT, unlike DottedMap.vue, and the difference is the aspect ratio. The dot map is
|
|
109
|
+
landscape, so filling the width it is given always fits; a borough is frequently PORTRAIT
|
|
110
|
+
(Southwark runs nearly 2:1 the other way), and an SVG told to fill a half-slide column's width
|
|
111
|
+
resolves to a shape taller than the 1080 canvas and runs off the bottom of the slide —
|
|
112
|
+
observed, not predicted. Height is the axis a slide actually constrains, so height is the axis
|
|
113
|
+
this sizes on.
|
|
114
|
+
|
|
115
|
+
Ten grid units is the NATURAL size, and it lives in the var() FALLBACK rather than as a bare
|
|
116
|
+
value so `size` can supply a different number without a specificity fight — see the sizeStyle
|
|
117
|
+
comment in the script block. In practice the prop always sets the property (its default, `md`,
|
|
118
|
+
IS this number), so the fallback is the safety net for an unrecognised size rather than the
|
|
119
|
+
common path: a slide that asks for nothing still draws 400px either way. Natural-by-default is
|
|
120
|
+
the deck's own convention for a mark — demos draw marks at natural size and pass no size. */
|
|
121
|
+
display: block;
|
|
122
|
+
height: var(--borough-shape-height, 400px);
|
|
123
|
+
width: auto;
|
|
124
|
+
max-width: 100%;
|
|
125
|
+
color: var(--deck-accent);
|
|
126
|
+
}
|
|
127
|
+
</style>
|