@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.
Files changed (124) hide show
  1. package/LICENSE +67 -0
  2. package/README.md +577 -0
  3. package/assetUrl.js +38 -0
  4. package/components/Alert.vue +209 -0
  5. package/components/AreaBox.vue +152 -0
  6. package/components/AreaChart.vue +145 -0
  7. package/components/Avatar.vue +267 -0
  8. package/components/AvatarGroup.vue +94 -0
  9. package/components/BadgeStrip.vue +203 -0
  10. package/components/Bars.vue +207 -0
  11. package/components/BoroughShape.vue +127 -0
  12. package/components/BrowserFrame.vue +484 -0
  13. package/components/CalendarGrid.vue +480 -0
  14. package/components/Card.vue +70 -0
  15. package/components/Carousel.vue +266 -0
  16. package/components/ChartFrame.vue +426 -0
  17. package/components/ChordChart.vue +264 -0
  18. package/components/ComposedChart.vue +187 -0
  19. package/components/Contents.vue +86 -0
  20. package/components/DataTable.vue +244 -0
  21. package/components/DeckMap.vue +409 -0
  22. package/components/DeviceFrame.vue +331 -0
  23. package/components/DonutChart.vue +103 -0
  24. package/components/DottedMap.vue +126 -0
  25. package/components/Endorsement.vue +76 -0
  26. package/components/Grid.vue +169 -0
  27. package/components/GroupedBarChart.vue +141 -0
  28. package/components/IconTile.vue +116 -0
  29. package/components/Kpi.vue +188 -0
  30. package/components/LineChart.vue +133 -0
  31. package/components/Logo.vue +203 -0
  32. package/components/LondonMap.vue +348 -0
  33. package/components/MarkPair.vue +109 -0
  34. package/components/Marquee.vue +263 -0
  35. package/components/NestedDonutChart.vue +295 -0
  36. package/components/Placeholder.vue +88 -0
  37. package/components/SankeyChart.vue +223 -0
  38. package/components/ScatterChart.vue +142 -0
  39. package/components/StackedBarChart.vue +143 -0
  40. package/components/StatCard.vue +134 -0
  41. package/components/StatusTrack.vue +334 -0
  42. package/components/Timeline.vue +249 -0
  43. package/components/TimelineChart.vue +329 -0
  44. package/components/TreemapChart.vue +267 -0
  45. package/components/backgrounds.js +135 -0
  46. package/components/boroughGeometry.ts +59 -0
  47. package/components/chartData.ts +277 -0
  48. package/components/chartTip.ts +201 -0
  49. package/components/codeTheme.ts +239 -0
  50. package/components/deckPalette.ts +157 -0
  51. package/components/dottedMapGeometry.js +60 -0
  52. package/components/iconSlugs.js +55 -0
  53. package/components/iconUrl.js +51 -0
  54. package/components/mermaidTheme.ts +972 -0
  55. package/components/motionDuration.ts +41 -0
  56. package/components/tableRules.ts +257 -0
  57. package/confidential-mark.vue +151 -0
  58. package/custom-nav-controls.vue +79 -0
  59. package/endorsements/b-corp-black.svg +30 -0
  60. package/endorsements/b-corp-white.svg +30 -0
  61. package/global-top.vue +1662 -0
  62. package/icons/forest/bike-asterisk.svg +21 -0
  63. package/icons/forest/bike-down.svg +19 -0
  64. package/icons/forest/bike-off.svg +20 -0
  65. package/icons/forest/bike-up.svg +19 -0
  66. package/icons/forest/bike-x.svg +19 -0
  67. package/icons/forest/bike.svg +18 -0
  68. package/icons/forest/mark-fill.svg +6 -0
  69. package/icons/forest/mark.svg +6 -0
  70. package/icons/forest/parking.svg +3 -0
  71. package/icons/forest/star-fill.svg +4 -0
  72. package/icons/forest/star.svg +4 -0
  73. package/icons/forest/traffic-light-caution.svg +8 -0
  74. package/icons/forest/traffic-light-go.svg +8 -0
  75. package/icons/forest/traffic-light-lit.svg +8 -0
  76. package/icons/forest/traffic-light-stop.svg +8 -0
  77. package/icons/forest/traffic-light.svg +8 -0
  78. package/icons/lucide/battery.svg +1 -0
  79. package/icons/lucide/bike.svg +1 -0
  80. package/icons/lucide/chart-line.svg +1 -0
  81. package/icons/lucide/check.svg +1 -0
  82. package/icons/lucide/circle-check.svg +1 -0
  83. package/icons/lucide/clock.svg +1 -0
  84. package/icons/lucide/code.svg +1 -0
  85. package/icons/lucide/component.svg +1 -0
  86. package/icons/lucide/frame.svg +1 -0
  87. package/icons/lucide/layout-grid.svg +1 -0
  88. package/icons/lucide/map-pin.svg +1 -0
  89. package/icons/lucide/map.svg +1 -0
  90. package/icons/lucide/palette.svg +1 -0
  91. package/icons/lucide/table.svg +1 -0
  92. package/icons/lucide/triangle-alert.svg +1 -0
  93. package/icons/lucide/type.svg +1 -0
  94. package/icons/lucide/wrench.svg +1 -0
  95. package/layouts/bleed.vue +317 -0
  96. package/layouts/cover.vue +1514 -0
  97. package/layouts/default.vue +187 -0
  98. package/layouts/divider.vue +286 -0
  99. package/layouts/end.vue +384 -0
  100. package/layouts/quote.vue +218 -0
  101. package/layouts/split.vue +1875 -0
  102. package/layouts/stack.vue +944 -0
  103. package/layouts/statement.vue +67 -0
  104. package/manifest/chrome.ts +36 -0
  105. package/manifest/components.ts +49 -0
  106. package/manifest/index.ts +57 -0
  107. package/manifest/layouts.ts +37 -0
  108. package/manifest/manifest.json +6514 -0
  109. package/manifest/presets.ts +635 -0
  110. package/notch.js +458 -0
  111. package/notchPath.js +188 -0
  112. package/package.json +77 -0
  113. package/section.js +110 -0
  114. package/setup/mermaid-renderer.ts +2120 -0
  115. package/setup/mermaid.ts +30 -0
  116. package/setup/shiki.ts +44 -0
  117. package/setup/shortcuts.ts +28 -0
  118. package/slide-bottom.vue +305 -0
  119. package/slide-top.vue +268 -0
  120. package/styles/base.css +2611 -0
  121. package/styles/index.ts +71 -0
  122. package/styles/tokens.css +144 -0
  123. package/uno.config.ts +231 -0
  124. 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>