@troshab/slidev-theme-troshab 0.1.6 → 0.1.7
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/components/Callout.vue +0 -1
- package/components/Stepper.vue +6 -0
- package/components/StyledList.vue +1 -1
- package/package.json +1 -1
- package/scripts/generate-component-docs.mjs +759 -0
- package/setup/shiki.ts +12 -3
- package/styles/base.css +146 -65
- package/styles/motion.css +1 -1
package/components/Callout.vue
CHANGED
package/components/Stepper.vue
CHANGED
|
@@ -267,6 +267,12 @@ function isUpcoming(index: number) {
|
|
|
267
267
|
.stepper-step {
|
|
268
268
|
display: flex;
|
|
269
269
|
align-items: center;
|
|
270
|
+
/* Reserve horizontal space for .stepper-marker's transform: scale(1.15) on
|
|
271
|
+
* active state. Scaled marker's visible box extends ~6px past its layout
|
|
272
|
+
* box; without padding, step.scrollWidth exceeds clientWidth by 1-3px on
|
|
273
|
+
* short labels. Per CSS spec, transformed descendants count toward parent
|
|
274
|
+
* scrollable overflow. */
|
|
275
|
+
padding: 0 var(--transform-scale-comp);
|
|
270
276
|
}
|
|
271
277
|
|
|
272
278
|
.stepper-horizontal .stepper-step {
|
package/package.json
CHANGED
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* generate-component-docs.mjs — Sync component API docs from `.vue` source
|
|
4
|
+
*
|
|
5
|
+
* Reads `components/*.vue` entry stubs, extracts:
|
|
6
|
+
* 1. JSDoc header comment before `defineProps` (component description + prop @params)
|
|
7
|
+
* 2. `defineProps<{...}>()` / `withDefaults(defineProps<{...}>(), {...})` — props
|
|
8
|
+
* 3. `<slot name="X" />` usages in template + `useSlots()` refs — slot list
|
|
9
|
+
*
|
|
10
|
+
* Updates the matching `.md` file in the visualise-content skill (by default,
|
|
11
|
+
* `~/.claude/plugins/marketplaces/troshab-own-claude-code/plugins/troshab-own/skills/
|
|
12
|
+
* slidev-visualise-content/components/{Name}.md`) between auto-markers:
|
|
13
|
+
*
|
|
14
|
+
* <!-- BEGIN AUTO PROPS --> …generated Props table… <!-- END AUTO PROPS -->
|
|
15
|
+
* <!-- BEGIN AUTO SLOTS --> …generated Slots table… <!-- END AUTO SLOTS -->
|
|
16
|
+
* <!-- SCHEMA …JSON… -->
|
|
17
|
+
*
|
|
18
|
+
* Human-authored sections (## Usage, ## Notes, ## Examples in theme) are never
|
|
19
|
+
* touched. Missing .md → skeleton is created. Orphan .md (no matching .vue) →
|
|
20
|
+
* warning printed.
|
|
21
|
+
*
|
|
22
|
+
* Flags:
|
|
23
|
+
* --theme-dir DIR (default ~/slidev-theme-troshab)
|
|
24
|
+
* --skill-dir DIR (default visualise-content components folder)
|
|
25
|
+
* --check-only Exit 1 if any .md would change; print diff summary.
|
|
26
|
+
* --only NAMES Comma-separated component names (e.g. Stepper,Callout).
|
|
27
|
+
* Handy when debugging a single component.
|
|
28
|
+
* --no-write Print intended changes to stdout; don't touch files.
|
|
29
|
+
*
|
|
30
|
+
* Exit codes:
|
|
31
|
+
* 0 — all docs in sync (or write succeeded)
|
|
32
|
+
* 1 — drift detected with --check-only, or orphan/missing component
|
|
33
|
+
* 2 — fatal parsing error
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'node:fs'
|
|
37
|
+
import { join, resolve, dirname, basename } from 'node:path'
|
|
38
|
+
import { homedir } from 'node:os'
|
|
39
|
+
import { fileURLToPath } from 'node:url'
|
|
40
|
+
|
|
41
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
42
|
+
|
|
43
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const argv = process.argv.slice(2)
|
|
46
|
+
function flag(name) { return argv.includes(name) }
|
|
47
|
+
function opt(name, fallback) {
|
|
48
|
+
const i = argv.indexOf(name)
|
|
49
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : fallback
|
|
50
|
+
}
|
|
51
|
+
function expandTilde(p) { return p.startsWith('~') ? join(homedir(), p.slice(1)) : p }
|
|
52
|
+
|
|
53
|
+
const THEME_DIR = resolve(expandTilde(opt('--theme-dir', join(__dirname, '..'))))
|
|
54
|
+
const DEFAULT_SKILL_DIR = join(
|
|
55
|
+
homedir(),
|
|
56
|
+
'.claude', 'plugins', 'marketplaces', 'troshab-own-claude-code',
|
|
57
|
+
'plugins', 'troshab-own', 'skills', 'slidev-visualise-content', 'components',
|
|
58
|
+
)
|
|
59
|
+
const SKILL_DIR = resolve(expandTilde(opt('--skill-dir', DEFAULT_SKILL_DIR)))
|
|
60
|
+
const CHECK_ONLY = flag('--check-only')
|
|
61
|
+
const NO_WRITE = flag('--no-write')
|
|
62
|
+
const ONLY = opt('--only', null)
|
|
63
|
+
const ONLY_SET = ONLY ? new Set(ONLY.split(',').map(s => s.trim())) : null
|
|
64
|
+
|
|
65
|
+
// ─── .vue parser ──────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract the `<script setup lang="ts">` or plain `<script>` block, the template,
|
|
69
|
+
* and any top-of-file HTML comment (acts like JSDoc on entry-stub files).
|
|
70
|
+
* Returns { scriptSrc, templateSrc, headerComment } or null.
|
|
71
|
+
*/
|
|
72
|
+
function splitSfc(src) {
|
|
73
|
+
const scriptSetupMatch = src.match(/<script\s+setup(?:\s+[^>]*)?>([\s\S]*?)<\/script>/)
|
|
74
|
+
const plainScriptMatch = src.match(/<script(?:\s+[^>]*)?>([\s\S]*?)<\/script>/)
|
|
75
|
+
const scriptSrc = scriptSetupMatch ? scriptSetupMatch[1] : (plainScriptMatch ? plainScriptMatch[1] : '')
|
|
76
|
+
// Greedy + last `</template>` — avoids truncation at inner `<template v-if>`.
|
|
77
|
+
const templateMatch = src.match(/<template(?:\s[^>]*)?>([\s\S]*)<\/template>/)
|
|
78
|
+
const headerMatch = src.match(/^\s*<!--([\s\S]*?)-->/)
|
|
79
|
+
if (!scriptSrc && !headerMatch) return null
|
|
80
|
+
return {
|
|
81
|
+
scriptSrc,
|
|
82
|
+
templateSrc: templateMatch ? templateMatch[1] : '',
|
|
83
|
+
headerComment: headerMatch ? headerMatch[1] : '',
|
|
84
|
+
isSetup: !!scriptSetupMatch,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* If the entry-stub does `import X from '../components_base/X.vue'; export default X`,
|
|
90
|
+
* return the resolved path to the real implementation .vue. Otherwise null.
|
|
91
|
+
*/
|
|
92
|
+
function resolveReExport(scriptSrc, currentFile) {
|
|
93
|
+
const importMatch = scriptSrc.match(/import\s+\w+\s+from\s+['"]([^'"]+\.vue)['"]/)
|
|
94
|
+
if (!importMatch) return null
|
|
95
|
+
const exportMatch = scriptSrc.match(/export\s+default\s+\w+/)
|
|
96
|
+
if (!exportMatch) return null
|
|
97
|
+
const rel = importMatch[1]
|
|
98
|
+
return resolve(dirname(currentFile), rel)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extract the JSDoc header block right before the first `defineProps` or
|
|
103
|
+
* `withDefaults(defineProps`. Returns { description, paramDocs } where paramDocs
|
|
104
|
+
* maps propName → description string.
|
|
105
|
+
*
|
|
106
|
+
* If `fallbackHtmlComment` is provided (from an entry-stub <!-- … -->), it's
|
|
107
|
+
* parsed with the same Props:/Slots:/Usage: rules.
|
|
108
|
+
*/
|
|
109
|
+
function extractHeaderDoc(scriptSrc, fallbackHtmlComment) {
|
|
110
|
+
const defineIdx = scriptSrc.search(/(?:withDefaults\s*\(\s*)?defineProps\s*</)
|
|
111
|
+
|
|
112
|
+
let raw = ''
|
|
113
|
+
if (defineIdx >= 0) {
|
|
114
|
+
// Look backward for /** … */ ending just before defineProps.
|
|
115
|
+
const before = scriptSrc.slice(0, defineIdx)
|
|
116
|
+
const jsdocMatch = before.match(/\/\*\*[\s\S]*?\*\/(?=\s*(?:const\s+\w+\s*=\s*)?(?:withDefaults\s*\(\s*)?defineProps\s*<|\s*$)/)
|
|
117
|
+
const allMatches = [...before.matchAll(/\/\*\*[\s\S]*?\*\//g)]
|
|
118
|
+
raw = jsdocMatch ? jsdocMatch[0] : (allMatches.length ? allMatches[allMatches.length - 1][0] : '')
|
|
119
|
+
}
|
|
120
|
+
if (!raw && fallbackHtmlComment) {
|
|
121
|
+
// Treat the HTML comment as a JSDoc body (lines already look like " * foo"
|
|
122
|
+
// but may also just be plain lines — normalise accordingly).
|
|
123
|
+
raw = `/**\n${fallbackHtmlComment}\n*/`
|
|
124
|
+
}
|
|
125
|
+
if (!raw) return { description: '', paramDocs: {} }
|
|
126
|
+
|
|
127
|
+
// Clean leading " * " and trailing "*/".
|
|
128
|
+
const lines = raw
|
|
129
|
+
.replace(/^\s*\/\*\*\s*/, '')
|
|
130
|
+
.replace(/\*\/\s*$/, '')
|
|
131
|
+
.split('\n')
|
|
132
|
+
.map(l => l.replace(/^\s*\*\s?/, '').trimEnd())
|
|
133
|
+
|
|
134
|
+
const paramDocs = {}
|
|
135
|
+
const descLines = []
|
|
136
|
+
let inDescription = true
|
|
137
|
+
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
const paramMatch = line.match(/^@param\s+\{?[^}]*\}?\s*(\w+)\s*[-—]?\s*(.*)$/)
|
|
140
|
+
if (paramMatch) {
|
|
141
|
+
inDescription = false
|
|
142
|
+
paramDocs[paramMatch[1]] = paramMatch[2].trim()
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
// Some JSDocs list props inline like "value: number — Target number".
|
|
146
|
+
const inlineParamMatch = line.match(/^\s*(\w+):\s*[^—\-]+[—\-]\s+(.+)$/)
|
|
147
|
+
if (inlineParamMatch && !inDescription) {
|
|
148
|
+
paramDocs[inlineParamMatch[1]] = inlineParamMatch[2].trim()
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
// "Props:" section header switches into prop-list mode.
|
|
152
|
+
if (/^\s*Props:\s*$/i.test(line)) { inDescription = false; continue }
|
|
153
|
+
if (/^\s*Slots?:\s*$/i.test(line)) { inDescription = false; continue }
|
|
154
|
+
if (/^\s*Usage:?\s*$/i.test(line)) { inDescription = false; continue }
|
|
155
|
+
// Stop accumulating once we see a usage example, a code block, or a blank
|
|
156
|
+
// followed by an HTML-tag line — otherwise multi-line descriptions get
|
|
157
|
+
// concatenated with embedded `<Component …>` examples.
|
|
158
|
+
if (inDescription) {
|
|
159
|
+
const trimmed = line.trim()
|
|
160
|
+
if (!trimmed) { if (descLines.length) inDescription = false; continue }
|
|
161
|
+
if (/^</.test(trimmed)) { inDescription = false; continue }
|
|
162
|
+
descLines.push(trimmed)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Pass 2 for "Props:" block style ("value: number — description").
|
|
167
|
+
const propsSectionMatch = raw.match(/Props:\s*\n([\s\S]*?)(?:\n\s*(?:Slots?|Usage|Notes?|Example)s?:|\*\/)/i)
|
|
168
|
+
if (propsSectionMatch) {
|
|
169
|
+
for (const line of propsSectionMatch[1].split('\n')) {
|
|
170
|
+
const m = line.replace(/^\s*\*\s?/, '').match(/^\s*(\w+)[?!:]?\s*(?:[:\-]\s*)?[^—\-]*[—\-]\s+(.+)$/)
|
|
171
|
+
if (m && !paramDocs[m[1]]) paramDocs[m[1]] = m[2].trim()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
description: descLines.join(' ').replace(/\s+/g, ' ').trim(),
|
|
177
|
+
paramDocs,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Find defineProps<{...}>() or withDefaults(defineProps<{...}>(), {...}).
|
|
183
|
+
* Returns { propsBlock: string, defaultsBlock: string | null }.
|
|
184
|
+
*/
|
|
185
|
+
function extractPropsBlocks(scriptSrc) {
|
|
186
|
+
// Locate `defineProps<`.
|
|
187
|
+
const propsStart = scriptSrc.indexOf('defineProps<')
|
|
188
|
+
if (propsStart < 0) return null
|
|
189
|
+
|
|
190
|
+
// Balanced `<...>` extraction starting at the `<` after defineProps.
|
|
191
|
+
const openAngle = scriptSrc.indexOf('<', propsStart)
|
|
192
|
+
if (openAngle < 0) return null
|
|
193
|
+
let depth = 0
|
|
194
|
+
let typeStart = openAngle + 1
|
|
195
|
+
let typeEnd = -1
|
|
196
|
+
for (let i = openAngle; i < scriptSrc.length; i++) {
|
|
197
|
+
const c = scriptSrc[i]
|
|
198
|
+
if (c === '<') depth++
|
|
199
|
+
else if (c === '>') {
|
|
200
|
+
depth--
|
|
201
|
+
if (depth === 0) { typeEnd = i; break }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (typeEnd < 0) return null
|
|
205
|
+
const typeLiteral = scriptSrc.slice(typeStart, typeEnd).trim()
|
|
206
|
+
|
|
207
|
+
// Check if wrapped in withDefaults(..., {...}).
|
|
208
|
+
const withDefaultsIdx = scriptSrc.lastIndexOf('withDefaults', propsStart)
|
|
209
|
+
let defaultsBlock = null
|
|
210
|
+
if (withDefaultsIdx >= 0 && withDefaultsIdx < propsStart) {
|
|
211
|
+
// Locate the comma after defineProps<...>() and the { that starts defaults.
|
|
212
|
+
const afterClose = scriptSrc.indexOf(')', typeEnd)
|
|
213
|
+
if (afterClose > 0) {
|
|
214
|
+
const commaIdx = scriptSrc.indexOf(',', afterClose)
|
|
215
|
+
if (commaIdx > 0) {
|
|
216
|
+
const defBraceStart = scriptSrc.indexOf('{', commaIdx)
|
|
217
|
+
if (defBraceStart > 0) {
|
|
218
|
+
let bDepth = 0
|
|
219
|
+
let defEnd = -1
|
|
220
|
+
for (let i = defBraceStart; i < scriptSrc.length; i++) {
|
|
221
|
+
const c = scriptSrc[i]
|
|
222
|
+
if (c === '{') bDepth++
|
|
223
|
+
else if (c === '}') {
|
|
224
|
+
bDepth--
|
|
225
|
+
if (bDepth === 0) { defEnd = i; break }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (defEnd > 0) defaultsBlock = scriptSrc.slice(defBraceStart + 1, defEnd).trim()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { propsBlock: typeLiteral, defaultsBlock }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Parse `propName?: type` entries from the props-type literal. Handles union
|
|
239
|
+
* types (a|b|c), generics, nested arrays.
|
|
240
|
+
* Returns [{ name, type, required }].
|
|
241
|
+
*/
|
|
242
|
+
function parsePropsBlock(block) {
|
|
243
|
+
// Strip single-line "// …" and multi-line "/* … */" comments.
|
|
244
|
+
let stripped = block
|
|
245
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
246
|
+
.replace(/(^|\s)\/\/[^\n]*/g, (_, p) => p)
|
|
247
|
+
.trim()
|
|
248
|
+
|
|
249
|
+
// Remove outer `{...}` if present — defineProps<{ ... }>() passes the literal
|
|
250
|
+
// as `{ type?: '...'; title?: string }`; we need to walk inside the object.
|
|
251
|
+
if (stripped.startsWith('{') && stripped.endsWith('}')) {
|
|
252
|
+
stripped = stripped.slice(1, -1)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const entries = []
|
|
256
|
+
let depth = 0
|
|
257
|
+
let current = ''
|
|
258
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
259
|
+
const c = stripped[i]
|
|
260
|
+
if (c === '[' || c === '{' || c === '(') depth++
|
|
261
|
+
else if (c === ']' || c === '}' || c === ')') depth--
|
|
262
|
+
// `<` / `>` are tricky: `<T>` increases/decreases depth, but `|`/`>` in
|
|
263
|
+
// union/comparison contexts would too. For our case the prop-type literal
|
|
264
|
+
// usually stays balanced with the existing depth tracking.
|
|
265
|
+
if (c === '<' && /\w/.test(stripped[i - 1] || '')) depth++
|
|
266
|
+
else if (c === '>' && depth > 0 && stripped[i + 1] !== '=' && stripped[i - 1] !== '=') depth--
|
|
267
|
+
if ((c === ',' || c === ';' || c === '\n') && depth === 0) {
|
|
268
|
+
const piece = current.trim()
|
|
269
|
+
if (piece) entries.push(piece)
|
|
270
|
+
current = ''
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
current += c
|
|
274
|
+
}
|
|
275
|
+
if (current.trim()) entries.push(current.trim())
|
|
276
|
+
|
|
277
|
+
const props = []
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
// propName?: type
|
|
280
|
+
const m = entry.match(/^(\w+)(\??)\s*:\s*([\s\S]+)$/)
|
|
281
|
+
if (!m) continue
|
|
282
|
+
const [, name, optional, rawType] = m
|
|
283
|
+
props.push({ name, type: rawType.trim(), required: optional !== '?' })
|
|
284
|
+
}
|
|
285
|
+
return props
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Parse defaults block. Returns propName → default-value (as raw string).
|
|
290
|
+
*/
|
|
291
|
+
function parseDefaultsBlock(block) {
|
|
292
|
+
if (!block) return {}
|
|
293
|
+
const out = {}
|
|
294
|
+
let depth = 0
|
|
295
|
+
let current = ''
|
|
296
|
+
for (let i = 0; i < block.length; i++) {
|
|
297
|
+
const c = block[i]
|
|
298
|
+
if (c === '<' || c === '[' || c === '{' || c === '(') depth++
|
|
299
|
+
else if (c === '>' || c === ']' || c === '}' || c === ')') depth--
|
|
300
|
+
if ((c === ',' || c === '\n') && depth === 0) {
|
|
301
|
+
addDefault(out, current)
|
|
302
|
+
current = ''
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
current += c
|
|
306
|
+
}
|
|
307
|
+
addDefault(out, current)
|
|
308
|
+
return out
|
|
309
|
+
}
|
|
310
|
+
function addDefault(out, piece) {
|
|
311
|
+
const trimmed = piece.trim().replace(/^\/\/.*$/gm, '').trim()
|
|
312
|
+
if (!trimmed) return
|
|
313
|
+
const m = trimmed.match(/^(\w+)\s*:\s*([\s\S]+?)$/)
|
|
314
|
+
if (!m) return
|
|
315
|
+
out[m[1]] = m[2].trim()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Slots: grep static `<slot name="X" />` + dynamic `<slot :name="`step${i}`" />` +
|
|
320
|
+
* `slots.XXX` / `slots[`step${i}`]` refs. Dynamic names are emitted in their
|
|
321
|
+
* raw template form (e.g. "step${i}") so collapseSlots() can render them.
|
|
322
|
+
* Returns sorted array of unique slot names.
|
|
323
|
+
*/
|
|
324
|
+
function extractSlots(scriptSrc, templateSrc) {
|
|
325
|
+
const slots = new Set()
|
|
326
|
+
const staticSlotRe = /<slot(?:\s+name=["']([\w-]+)["'])?[\s\S]*?(?:\/>|>)/g
|
|
327
|
+
let m
|
|
328
|
+
while ((m = staticSlotRe.exec(templateSrc))) {
|
|
329
|
+
slots.add(m[1] || 'default')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Dynamic: <slot :name="`step${...}`" /> or <slot :name="'step' + idx" />.
|
|
333
|
+
const dynSlotRe = /<slot\s+:name=["']\s*`?([^`"'$]+)(?:\$\{[^}]+\}|[+\s]+[\w.]+)/g
|
|
334
|
+
while ((m = dynSlotRe.exec(templateSrc))) {
|
|
335
|
+
slots.add(`${m[1].trim()}N`)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// slots[`stepN`] / slots.stepN / $slots.stepN access patterns in script.
|
|
339
|
+
const accessRe = /(?:\$?slots)[\s]*\[[\s]*`([^`${]+)(?:\$\{[^}]+\})?`[\s]*\]/g
|
|
340
|
+
while ((m = accessRe.exec(scriptSrc))) {
|
|
341
|
+
slots.add(`${m[1].trim()}N`)
|
|
342
|
+
}
|
|
343
|
+
const dotRe = /(?:\$?slots)\.(\w+)/g
|
|
344
|
+
while ((m = dotRe.exec(scriptSrc))) slots.add(m[1])
|
|
345
|
+
|
|
346
|
+
const noise = new Set(['name', 'value', 'length', 'forEach', 'map'])
|
|
347
|
+
return [...slots].filter(s => !noise.has(s)).sort()
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ─── Collapse long slot lists (e.g. step1..stepN) ────────────────────────────
|
|
351
|
+
|
|
352
|
+
function collapseSlots(slots) {
|
|
353
|
+
// Group patterns like step1, step2, step3 → "step1..step3".
|
|
354
|
+
const groups = new Map()
|
|
355
|
+
const singles = []
|
|
356
|
+
for (const s of slots) {
|
|
357
|
+
const m = s.match(/^([a-zA-Z]+)(\d+)$/)
|
|
358
|
+
if (!m) { singles.push(s); continue }
|
|
359
|
+
const [, prefix, num] = m
|
|
360
|
+
const arr = groups.get(prefix) || []
|
|
361
|
+
arr.push(parseInt(num, 10))
|
|
362
|
+
groups.set(prefix, arr)
|
|
363
|
+
}
|
|
364
|
+
const out = [...singles]
|
|
365
|
+
for (const [prefix, nums] of groups) {
|
|
366
|
+
nums.sort((a, b) => a - b)
|
|
367
|
+
if (nums.length === 1) out.push(`${prefix}${nums[0]}`)
|
|
368
|
+
else out.push(`${prefix}${nums[0]}..${prefix}${nums[nums.length - 1]}`)
|
|
369
|
+
}
|
|
370
|
+
return out.sort()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── Markdown table renderers ────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
function renderPropsTable(props, paramDocs, defaults) {
|
|
376
|
+
if (!props.length) return '_No props._'
|
|
377
|
+
const rows = props.map(p => {
|
|
378
|
+
const def = defaults[p.name] ?? (p.required ? '—' : '—')
|
|
379
|
+
const desc = (paramDocs[p.name] || '').replace(/\|/g, '\\|')
|
|
380
|
+
return `| \`${p.name}\` | \`${p.type.replace(/\|/g, '\\|')}\` | \`${def.replace(/\|/g, '\\|')}\` | ${p.required ? 'yes' : 'no'} | ${desc} |`
|
|
381
|
+
})
|
|
382
|
+
return [
|
|
383
|
+
'| Prop | Type | Default | Required | Description |',
|
|
384
|
+
'|------|------|---------|----------|-------------|',
|
|
385
|
+
...rows,
|
|
386
|
+
].join('\n')
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Scan example_slides/*.md for usages of `<ComponentName`. Returns sorted
|
|
391
|
+
* array of { file, heading } tuples. `file` is relative to example_slides/.
|
|
392
|
+
*/
|
|
393
|
+
function findExamplesInTheme(themeDir, componentName) {
|
|
394
|
+
const dir = join(themeDir, 'example_slides')
|
|
395
|
+
if (!existsSync(dir)) return []
|
|
396
|
+
const out = []
|
|
397
|
+
for (const f of readdirSync(dir).sort()) {
|
|
398
|
+
if (!f.endsWith('.md')) continue
|
|
399
|
+
let src
|
|
400
|
+
try { src = readFileSync(join(dir, f), 'utf8') } catch { continue }
|
|
401
|
+
// Component tag: `<ComponentName` followed by whitespace, `/`, `>`, or end.
|
|
402
|
+
// Must not be inside a code fence — example_slides occasionally ship
|
|
403
|
+
// snippets showing wrong usage, but in practice the files are ~10-20 lines
|
|
404
|
+
// and any occurrence is a demo. So simple match is good enough.
|
|
405
|
+
const re = new RegExp('<' + componentName + '[\\s/>]', 'm')
|
|
406
|
+
if (!re.test(src)) continue
|
|
407
|
+
const headingMatch = src.match(/^#\s+(.+?)\s*$/m)
|
|
408
|
+
out.push({ file: f, heading: headingMatch ? headingMatch[1] : '(no heading)' })
|
|
409
|
+
}
|
|
410
|
+
return out
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function renderExamplesTable(examples) {
|
|
414
|
+
if (!examples.length) return '_No theme example slides reference this component yet._'
|
|
415
|
+
const rows = examples.map(e => `| [\`${e.file}\`](@../../../../slidev-theme-troshab/example_slides/${e.file}) | ${e.heading.replace(/\|/g, '\\|')} |`)
|
|
416
|
+
return [
|
|
417
|
+
'| File | Heading |',
|
|
418
|
+
'|------|---------|',
|
|
419
|
+
...rows,
|
|
420
|
+
].join('\n')
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function renderSlotsTable(slots, slotDescs = {}) {
|
|
424
|
+
if (!slots.length) return '_No slots._'
|
|
425
|
+
const rows = slots.map(s => {
|
|
426
|
+
const desc = (slotDescs[s] || '').replace(/\|/g, '\\|')
|
|
427
|
+
return `| \`${s}\` | ${desc} |`
|
|
428
|
+
})
|
|
429
|
+
return [
|
|
430
|
+
'| Slot | Description |',
|
|
431
|
+
'|------|-------------|',
|
|
432
|
+
...rows,
|
|
433
|
+
].join('\n')
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function renderSchema(name, props, defaults, slots) {
|
|
437
|
+
const obj = {
|
|
438
|
+
name,
|
|
439
|
+
props: Object.fromEntries(props.map(p => {
|
|
440
|
+
const item = { type: p.type, required: p.required }
|
|
441
|
+
// Detect pure literal-union enum: `'a' | 'b' | 'c'` (no mixed `string` etc.).
|
|
442
|
+
const literalMatches = [...p.type.matchAll(/['"]([^'"]+)['"]/g)].map(x => x[1])
|
|
443
|
+
const residue = p.type.replace(/['"][^'"]+['"]/g, '').replace(/\s*\|\s*/g, '').trim()
|
|
444
|
+
if (literalMatches.length > 0 && residue === '') {
|
|
445
|
+
item.enum = literalMatches
|
|
446
|
+
}
|
|
447
|
+
if (defaults[p.name] !== undefined) item.default = defaults[p.name]
|
|
448
|
+
return [p.name, item]
|
|
449
|
+
})),
|
|
450
|
+
slots,
|
|
451
|
+
}
|
|
452
|
+
return JSON.stringify(obj, null, 2)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ─── .md updating ────────────────────────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
const MARKERS = {
|
|
458
|
+
propsBegin: '<!-- BEGIN AUTO PROPS -->',
|
|
459
|
+
propsEnd: '<!-- END AUTO PROPS -->',
|
|
460
|
+
slotsBegin: '<!-- BEGIN AUTO SLOTS -->',
|
|
461
|
+
slotsEnd: '<!-- END AUTO SLOTS -->',
|
|
462
|
+
examplesBegin: '<!-- BEGIN AUTO EXAMPLES -->',
|
|
463
|
+
examplesEnd: '<!-- END AUTO EXAMPLES -->',
|
|
464
|
+
schemaBegin: '<!-- SCHEMA',
|
|
465
|
+
schemaEnd: '-->',
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function replaceBetween(src, begin, end, replacement) {
|
|
469
|
+
const s = src.indexOf(begin)
|
|
470
|
+
const e = src.indexOf(end, s + begin.length)
|
|
471
|
+
if (s < 0 || e < 0) return null
|
|
472
|
+
return src.slice(0, s + begin.length) + '\n' + replacement + '\n' + src.slice(e)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function upsertSchema(src, jsonBody) {
|
|
476
|
+
const block = `${MARKERS.schemaBegin}\n${jsonBody}\n${MARKERS.schemaEnd}`
|
|
477
|
+
const idx = src.indexOf(MARKERS.schemaBegin)
|
|
478
|
+
if (idx < 0) return src.trimEnd() + '\n\n' + block + '\n'
|
|
479
|
+
// Replace from schemaBegin up to the next `-->` line.
|
|
480
|
+
const afterBegin = src.indexOf('\n', idx)
|
|
481
|
+
const endIdx = src.indexOf(MARKERS.schemaEnd, afterBegin)
|
|
482
|
+
if (endIdx < 0) return src.trimEnd() + '\n\n' + block + '\n'
|
|
483
|
+
const trail = src.slice(endIdx + MARKERS.schemaEnd.length)
|
|
484
|
+
return src.slice(0, idx) + block + trail
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function generateSkeleton(name, description, propsTable, slotsTable, examplesTable, schema) {
|
|
488
|
+
return `# ${name}
|
|
489
|
+
${description ? `\n${description}\n` : ''}
|
|
490
|
+
## Props
|
|
491
|
+
|
|
492
|
+
${MARKERS.propsBegin}
|
|
493
|
+
${propsTable}
|
|
494
|
+
${MARKERS.propsEnd}
|
|
495
|
+
|
|
496
|
+
## Slots
|
|
497
|
+
|
|
498
|
+
${MARKERS.slotsBegin}
|
|
499
|
+
${slotsTable}
|
|
500
|
+
${MARKERS.slotsEnd}
|
|
501
|
+
|
|
502
|
+
## Usage
|
|
503
|
+
|
|
504
|
+
_TODO: add representative usage example(s)._
|
|
505
|
+
|
|
506
|
+
## Notes
|
|
507
|
+
|
|
508
|
+
_TODO: add constraints, common mistakes, layout tips._
|
|
509
|
+
|
|
510
|
+
## Examples in theme
|
|
511
|
+
|
|
512
|
+
${MARKERS.examplesBegin}
|
|
513
|
+
${examplesTable}
|
|
514
|
+
${MARKERS.examplesEnd}
|
|
515
|
+
|
|
516
|
+
${MARKERS.schemaBegin}
|
|
517
|
+
${schema}
|
|
518
|
+
${MARKERS.schemaEnd}
|
|
519
|
+
`
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Pull `| propName | type | default | ... | Description |` tables out of an
|
|
524
|
+
* existing `.md` so we can preserve the hand-written Description column even
|
|
525
|
+
* when the schema otherwise gets regenerated.
|
|
526
|
+
* Returns { propName → description }. Empty if no table found.
|
|
527
|
+
*/
|
|
528
|
+
function readExistingPropDescriptions(md, heading) {
|
|
529
|
+
const headingIdx = md.search(new RegExp(`^##\\s+${heading}\\s*$`, 'm'))
|
|
530
|
+
if (headingIdx < 0) return {}
|
|
531
|
+
const lines = md.slice(headingIdx).split('\n')
|
|
532
|
+
const out = {}
|
|
533
|
+
// Find the first header row: `| Prop | ... | Description |`
|
|
534
|
+
for (let i = 0; i < lines.length; i++) {
|
|
535
|
+
const row = lines[i]
|
|
536
|
+
if (!/^\|.*Description\s*\|?$/i.test(row) && !/^\|.*Description\s*\|/i.test(row)) continue
|
|
537
|
+
// Header row found at i; data rows start at i+2 (i+1 is `|---|---|` separator).
|
|
538
|
+
for (let j = i + 2; j < lines.length; j++) {
|
|
539
|
+
const r = lines[j].trim()
|
|
540
|
+
if (!r.startsWith('|')) break
|
|
541
|
+
const cells = r.split('|').map(c => c.trim()).filter(c => c.length > 0 || true)
|
|
542
|
+
// Leading and trailing | create empty cells; strip them.
|
|
543
|
+
const inner = r.replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim())
|
|
544
|
+
if (inner.length < 2) continue
|
|
545
|
+
const propName = inner[0].replace(/^`|`$/g, '').trim()
|
|
546
|
+
const desc = inner[inner.length - 1]
|
|
547
|
+
if (propName && desc) out[propName] = desc
|
|
548
|
+
}
|
|
549
|
+
break
|
|
550
|
+
}
|
|
551
|
+
return out
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function readExistingSlotDescriptions(md) {
|
|
555
|
+
const headingIdx = md.search(/^##\s+Slots\s*$/m)
|
|
556
|
+
if (headingIdx < 0) return {}
|
|
557
|
+
const lines = md.slice(headingIdx).split('\n')
|
|
558
|
+
const out = {}
|
|
559
|
+
for (let i = 0; i < lines.length; i++) {
|
|
560
|
+
if (!/^\|\s*Slot\s*\|/i.test(lines[i])) continue
|
|
561
|
+
for (let j = i + 2; j < lines.length; j++) {
|
|
562
|
+
const r = lines[j].trim()
|
|
563
|
+
if (!r.startsWith('|')) break
|
|
564
|
+
const inner = r.replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim())
|
|
565
|
+
if (inner.length < 2) continue
|
|
566
|
+
const slotName = inner[0].replace(/^`|`$/g, '').trim()
|
|
567
|
+
const desc = inner[inner.length - 1]
|
|
568
|
+
if (slotName && desc) out[slotName] = desc
|
|
569
|
+
}
|
|
570
|
+
break
|
|
571
|
+
}
|
|
572
|
+
return out
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function updateMd(currentMd, name, description, props, paramDocs, defaults, slots, slotDescs, examples, schema) {
|
|
576
|
+
const mergedParamDocs = { ...paramDocs }
|
|
577
|
+
const mergedSlotDescs = { ...slotDescs }
|
|
578
|
+
if (currentMd) {
|
|
579
|
+
const prev = readExistingPropDescriptions(currentMd, 'Props')
|
|
580
|
+
for (const [k, v] of Object.entries(prev)) {
|
|
581
|
+
if (!mergedParamDocs[k] || mergedParamDocs[k].length < v.length) mergedParamDocs[k] = v
|
|
582
|
+
}
|
|
583
|
+
const prevSlots = readExistingSlotDescriptions(currentMd)
|
|
584
|
+
for (const [k, v] of Object.entries(prevSlots)) {
|
|
585
|
+
if (!mergedSlotDescs[k] || mergedSlotDescs[k].length < v.length) mergedSlotDescs[k] = v
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const propsTable = renderPropsTable(props, mergedParamDocs, defaults)
|
|
590
|
+
const slotsTable = renderSlotsTable(slots, mergedSlotDescs)
|
|
591
|
+
const examplesTable = renderExamplesTable(examples)
|
|
592
|
+
|
|
593
|
+
if (!currentMd) return generateSkeleton(name, description, propsTable, slotsTable, examplesTable, schema)
|
|
594
|
+
|
|
595
|
+
let out = currentMd
|
|
596
|
+
out = upsertTableBlock(out, 'Props', MARKERS.propsBegin, MARKERS.propsEnd, propsTable)
|
|
597
|
+
out = upsertTableBlock(out, 'Slots', MARKERS.slotsBegin, MARKERS.slotsEnd, slotsTable)
|
|
598
|
+
out = upsertTableBlock(out, 'Examples in theme', MARKERS.examplesBegin, MARKERS.examplesEnd, examplesTable)
|
|
599
|
+
out = upsertSchema(out, schema)
|
|
600
|
+
return out
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Replace an existing `## Heading` table (|-rows) with a managed BEGIN/END block
|
|
605
|
+
* that contains the freshly-rendered table. Leaves non-table prose inside the
|
|
606
|
+
* section untouched (e.g. Stepper's `StepperItem: { … }` note lives on).
|
|
607
|
+
*
|
|
608
|
+
* If the markers already exist — simple replace-between.
|
|
609
|
+
* If the heading exists but no markers — find the first consecutive |-row run
|
|
610
|
+
* and wrap it with markers, replacing just those lines.
|
|
611
|
+
* If the heading is missing — append a new section after the previous managed
|
|
612
|
+
* block (or at EOF).
|
|
613
|
+
*/
|
|
614
|
+
function upsertTableBlock(md, heading, beginMarker, endMarker, table) {
|
|
615
|
+
// Fast path: markers exist.
|
|
616
|
+
const replaced = replaceBetween(md, beginMarker, endMarker, table)
|
|
617
|
+
if (replaced !== null) return replaced
|
|
618
|
+
|
|
619
|
+
const headingRe = new RegExp(`^##\\s+${heading}\\s*$`, 'm')
|
|
620
|
+
const headingMatch = md.match(headingRe)
|
|
621
|
+
if (headingMatch) {
|
|
622
|
+
const headingIdx = md.indexOf(headingMatch[0])
|
|
623
|
+
const afterHeading = md.indexOf('\n', headingIdx) + 1
|
|
624
|
+
const rest = md.slice(afterHeading)
|
|
625
|
+
const lines = rest.split('\n')
|
|
626
|
+
// Find start of the table (first line starting with `|`).
|
|
627
|
+
let tableStart = -1
|
|
628
|
+
let tableEnd = -1
|
|
629
|
+
for (let i = 0; i < lines.length; i++) {
|
|
630
|
+
const trimmed = lines[i].trim()
|
|
631
|
+
if (trimmed.startsWith('##')) break
|
|
632
|
+
if (tableStart < 0 && trimmed.startsWith('|')) tableStart = i
|
|
633
|
+
if (tableStart >= 0 && !trimmed.startsWith('|') && tableEnd < 0) {
|
|
634
|
+
tableEnd = i
|
|
635
|
+
break
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (tableStart >= 0) {
|
|
639
|
+
if (tableEnd < 0) tableEnd = lines.length
|
|
640
|
+
const before = lines.slice(0, tableStart).join('\n')
|
|
641
|
+
const after = lines.slice(tableEnd).join('\n')
|
|
642
|
+
const block = `${beginMarker}\n${table}\n${endMarker}`
|
|
643
|
+
const rebuilt = [before.trimEnd(), block, after.trimStart()].filter(Boolean).join('\n\n')
|
|
644
|
+
return md.slice(0, afterHeading) + rebuilt
|
|
645
|
+
}
|
|
646
|
+
// Heading exists but no table — inject block right after heading line.
|
|
647
|
+
const block = `\n${beginMarker}\n${table}\n${endMarker}\n`
|
|
648
|
+
return md.slice(0, afterHeading) + block + rest
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// No heading — append at EOF (before any schema comment) as a new section.
|
|
652
|
+
const schemaIdx = md.indexOf(MARKERS.schemaBegin)
|
|
653
|
+
const newSection = `\n## ${heading}\n\n${beginMarker}\n${table}\n${endMarker}\n`
|
|
654
|
+
if (schemaIdx < 0) return md.trimEnd() + '\n' + newSection
|
|
655
|
+
return md.slice(0, schemaIdx).trimEnd() + '\n' + newSection + '\n' + md.slice(schemaIdx)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ─── Orchestrate ─────────────────────────────────────────────────────────────
|
|
659
|
+
|
|
660
|
+
function listVueComponents() {
|
|
661
|
+
const dir = join(THEME_DIR, 'components')
|
|
662
|
+
if (!existsSync(dir)) { console.error(`[gen-docs] missing ${dir}`); process.exit(2) }
|
|
663
|
+
return readdirSync(dir).filter(f => f.endsWith('.vue')).map(f => ({
|
|
664
|
+
name: basename(f, '.vue'),
|
|
665
|
+
path: join(dir, f),
|
|
666
|
+
}))
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function listMdDocs() {
|
|
670
|
+
if (!existsSync(SKILL_DIR)) { mkdirSync(SKILL_DIR, { recursive: true }) }
|
|
671
|
+
return readdirSync(SKILL_DIR).filter(f => f.endsWith('.md'))
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function diffSummary(oldSrc, newSrc) {
|
|
675
|
+
if (oldSrc === newSrc) return null
|
|
676
|
+
const oldLines = (oldSrc || '').split('\n').length
|
|
677
|
+
const newLines = newSrc.split('\n').length
|
|
678
|
+
return `${oldLines} → ${newLines} lines`
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function main() {
|
|
682
|
+
const components = listVueComponents()
|
|
683
|
+
const existingMds = new Set(listMdDocs())
|
|
684
|
+
let changed = 0
|
|
685
|
+
let errors = 0
|
|
686
|
+
const orphans = []
|
|
687
|
+
|
|
688
|
+
for (const { name, path } of components) {
|
|
689
|
+
if (ONLY_SET && !ONLY_SET.has(name)) continue
|
|
690
|
+
const vueSrc = readFileSync(path, 'utf8')
|
|
691
|
+
const parts = splitSfc(vueSrc)
|
|
692
|
+
if (!parts) {
|
|
693
|
+
console.error(`[gen-docs] ${name}: could not parse SFC`)
|
|
694
|
+
errors++
|
|
695
|
+
continue
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Parse props from this file; if missing and it's a re-export, follow the
|
|
699
|
+
// import to the real implementation (components_base/X.vue).
|
|
700
|
+
let propsBlocks = extractPropsBlocks(parts.scriptSrc)
|
|
701
|
+
let slotsScriptSrc = parts.scriptSrc
|
|
702
|
+
let slotsTemplateSrc = parts.templateSrc
|
|
703
|
+
if (!propsBlocks) {
|
|
704
|
+
const reExport = resolveReExport(parts.scriptSrc, path)
|
|
705
|
+
if (reExport && existsSync(reExport)) {
|
|
706
|
+
const baseSrc = readFileSync(reExport, 'utf8')
|
|
707
|
+
const baseParts = splitSfc(baseSrc)
|
|
708
|
+
if (baseParts) {
|
|
709
|
+
propsBlocks = extractPropsBlocks(baseParts.scriptSrc)
|
|
710
|
+
slotsScriptSrc = baseParts.scriptSrc
|
|
711
|
+
slotsTemplateSrc = baseParts.templateSrc
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const { description, paramDocs } = extractHeaderDoc(parts.scriptSrc, parts.headerComment)
|
|
717
|
+
const props = propsBlocks ? parsePropsBlock(propsBlocks.propsBlock) : []
|
|
718
|
+
const defaults = propsBlocks ? parseDefaultsBlock(propsBlocks.defaultsBlock) : {}
|
|
719
|
+
const slotsRaw = extractSlots(slotsScriptSrc, slotsTemplateSrc)
|
|
720
|
+
const slots = collapseSlots(slotsRaw)
|
|
721
|
+
const examples = findExamplesInTheme(THEME_DIR, name)
|
|
722
|
+
|
|
723
|
+
const schema = renderSchema(name, props, defaults, slots)
|
|
724
|
+
|
|
725
|
+
const mdPath = join(SKILL_DIR, `${name}.md`)
|
|
726
|
+
const existingMd = existsSync(mdPath) ? readFileSync(mdPath, 'utf8') : ''
|
|
727
|
+
existingMds.delete(`${name}.md`)
|
|
728
|
+
|
|
729
|
+
const newMd = updateMd(existingMd, name, description, props, paramDocs, defaults, slots, {}, examples, schema)
|
|
730
|
+
const diff = diffSummary(existingMd, newMd)
|
|
731
|
+
if (diff) {
|
|
732
|
+
changed++
|
|
733
|
+
if (CHECK_ONLY) console.log(`[drift] ${name}.md — ${diff}`)
|
|
734
|
+
else if (NO_WRITE) console.log(`[would-write] ${name}.md — ${diff}`)
|
|
735
|
+
else { writeFileSync(mdPath, newMd, 'utf8'); console.log(`[write] ${name}.md — ${diff}`) }
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Orphans — .md files in skill-dir with no matching .vue.
|
|
740
|
+
for (const md of existingMds) {
|
|
741
|
+
if (ONLY_SET) continue
|
|
742
|
+
orphans.push(md)
|
|
743
|
+
}
|
|
744
|
+
if (orphans.length) {
|
|
745
|
+
console.log(`[orphan] ${orphans.length} .md file(s) without matching .vue in theme:`)
|
|
746
|
+
for (const o of orphans) console.log(` ${o}`)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (CHECK_ONLY) {
|
|
750
|
+
if (changed > 0 || errors > 0 || orphans.length > 0) process.exit(1)
|
|
751
|
+
console.log('[gen-docs] All docs in sync')
|
|
752
|
+
process.exit(0)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
console.log(`[gen-docs] ${components.length} components, ${changed} updated, ${errors} error(s), ${orphans.length} orphan(s)`)
|
|
756
|
+
process.exit(errors > 0 ? 2 : 0)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
main()
|
package/setup/shiki.ts
CHANGED
|
@@ -15,9 +15,10 @@ const theme = createCssVariablesTheme({
|
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Transformer that adds `data-lang`
|
|
19
|
-
* CSS in base.css uses
|
|
20
|
-
*
|
|
18
|
+
* Transformer that adds `data-lang` and `data-filename` attributes to <pre>.
|
|
19
|
+
* CSS in base.css uses:
|
|
20
|
+
* ::before { content: attr(data-lang) } — language label, left
|
|
21
|
+
* ::after { content: attr(data-filename) } — filename label, right
|
|
21
22
|
*/
|
|
22
23
|
const langLabelTransformer: ShikiTransformer = {
|
|
23
24
|
name: 'lang-label',
|
|
@@ -26,6 +27,14 @@ const langLabelTransformer: ShikiTransformer = {
|
|
|
26
27
|
if (lang && lang !== 'text') {
|
|
27
28
|
node.properties['data-lang'] = lang
|
|
28
29
|
}
|
|
30
|
+
// Parse {filename="..."} from code block meta
|
|
31
|
+
const meta = this.options.meta?.__raw
|
|
32
|
+
if (meta) {
|
|
33
|
+
const match = meta.match(/filename=["']([^"']+)["']/)
|
|
34
|
+
if (match) {
|
|
35
|
+
node.properties['data-filename'] = match[1]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
29
38
|
},
|
|
30
39
|
}
|
|
31
40
|
|
package/styles/base.css
CHANGED
|
@@ -52,19 +52,24 @@
|
|
|
52
52
|
--font-weight-semibold: 600;
|
|
53
53
|
--font-weight-bold: 700;
|
|
54
54
|
|
|
55
|
-
/* --- Line Heights (role-based) ---
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
--line-height-
|
|
60
|
-
--line-height-
|
|
55
|
+
/* --- Line Heights (role-based) ---
|
|
56
|
+
Chung (2004, Vision Research): optimal 1.25-1.5 for body.
|
|
57
|
+
Bringhurst "Elements of Typographic Style": headings 1.1-1.25, body 1.3-1.5.
|
|
58
|
+
Kolers et al. (1981): reading speed U-curve — extremes reduce speed. */
|
|
59
|
+
--line-height-heading: 1.3; /* IBM Plex Sans glyph box (1.275) + safety margin */
|
|
60
|
+
--line-height-body: 1.4; /* Chung mid — comfortable for slides */
|
|
61
|
+
--line-height-reading: 1.5; /* Chung upper — BDA recommended for paragraphs */
|
|
62
|
+
--line-height-list: 1.4; /* matches body for consistency */
|
|
63
|
+
--line-height-code: 1.5; /* mono needs more room due to fixed glyph width */
|
|
61
64
|
|
|
62
65
|
/* --- Letter & Word Spacing (dyslexia-friendly) ---
|
|
63
|
-
Zorzi et al. (2012, PNAS): letter-spacing
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
--letter-spacing-
|
|
66
|
+
Zorzi et al. (2012, PNAS 109:11455): letter-spacing +20% reading speed.
|
|
67
|
+
Pelli et al. (2007): crowding as dyslexic bottleneck → space symbols.
|
|
68
|
+
BDA 2018: word/letter ratio >= 3.5x. Ours: 4.0x (above minimum).
|
|
69
|
+
Presentation tradeoff: full BDA (0.12em/0.16em) too wide for large display type. */
|
|
70
|
+
--letter-spacing-body: 0.025em; /* slight spacing — Zorzi crowding benefit */
|
|
71
|
+
--word-spacing-body: 0.1em; /* word/letter ratio = 4.0x (BDA >= 3.5x) */
|
|
72
|
+
--letter-spacing-heading: 0.005em; /* near-neutral — large headlines don't need extra spacing */
|
|
68
73
|
|
|
69
74
|
/* --- Spacing System (8-point grid) ---
|
|
70
75
|
Based on 8px base unit for visual rhythm.
|
|
@@ -94,15 +99,43 @@
|
|
|
94
99
|
--slide-padding: var(--space-lg);
|
|
95
100
|
--slide-gap: var(--space-sm);
|
|
96
101
|
|
|
97
|
-
/* --- Safe-zone for projectors/TV overscan
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
/* --- Safe-zone for projectors/TV overscan ---
|
|
103
|
+
SMPTE RP 27.3: 5% action-safe zone. Equal inset on all sides — asymmetric % causes
|
|
104
|
+
aspect-ratio dependent visual shift. Using rem keeps proportional across canvas sizes. */
|
|
105
|
+
--safe-inset-x: 3rem; /* ~94px at 31.347px root */
|
|
106
|
+
--safe-inset-y: 3rem; /* equal to x for symmetric safe zone */
|
|
100
107
|
|
|
101
108
|
/* --- Border Width Tokens (rem-based, scale with canvas) --- */
|
|
102
109
|
--border-thin: 0.0625rem; /* ≈ 1px at 980 → ~2px at 1920 */
|
|
103
110
|
--border-medium: 0.125rem; /* ≈ 2px → ~4px */
|
|
104
111
|
--border-thick: 0.1875rem; /* ≈ 3px → ~6px */
|
|
105
112
|
--border-accent: 0.25rem; /* ≈ 4px → ~8px */
|
|
113
|
+
|
|
114
|
+
/* --- Fine spacing (< 8px, for tight UI elements) --- */
|
|
115
|
+
--space-3xs: 0.125rem; /* 2px */
|
|
116
|
+
--space-2xs: 0.25rem; /* 4px */
|
|
117
|
+
--space-xs-fine: 0.375rem; /* 6px */
|
|
118
|
+
--space-tight: 0.75rem; /* 12px — between xs and sm */
|
|
119
|
+
--space-code-pt: 2.25rem; /* 36px — code block top padding (for lang label) */
|
|
120
|
+
|
|
121
|
+
/* --- Border Radius Tokens --- */
|
|
122
|
+
--radius-xs: 0.25rem; /* 4px — inline code */
|
|
123
|
+
--radius-sm: 0.375rem; /* 6px — buttons, small cards */
|
|
124
|
+
--radius-md: 0.5rem; /* 8px — cards, panels */
|
|
125
|
+
--radius-pill: 9999px; /* full pill shape */
|
|
126
|
+
|
|
127
|
+
/* --- Transform Compensation ---
|
|
128
|
+
Padding to compensate for transform: scale() visual overflow beyond layout box.
|
|
129
|
+
Calculated for scale(1.15) on medium elements (~63px markers).
|
|
130
|
+
Formula: (scale - 1) * element_size / 2 ≈ 4.7px per side. */
|
|
131
|
+
--transform-scale-comp: 0.1875rem; /* 6px — for scale(1.15) on 63px elements */
|
|
132
|
+
|
|
133
|
+
/* --- Typography ratios (em-based, font-size relative) --- */
|
|
134
|
+
--font-size-smaller: 0.8em; /* for <sup>, <sub>, <small> */
|
|
135
|
+
--letter-spacing-emphasis: 0.05em; /* for small uppercase labels */
|
|
136
|
+
--underline-offset: 0.15em; /* text-underline-offset */
|
|
137
|
+
--em-quarter: 0.25em; /* list item spacing */
|
|
138
|
+
--em-half: 1.5em; /* indent / nested */
|
|
106
139
|
}
|
|
107
140
|
|
|
108
141
|
/* ============================================
|
|
@@ -138,6 +171,17 @@
|
|
|
138
171
|
text-align: left;
|
|
139
172
|
}
|
|
140
173
|
|
|
174
|
+
/* Mixed-font line-box fix: <p>/<li> containing inline <code> mix IBM Plex Sans
|
|
175
|
+
* and Plex Mono glyph metrics. Mono's effective descent is slightly larger
|
|
176
|
+
* than Sans at the same font-size, so the line box expands by ~1px — which
|
|
177
|
+
* rounds up to scrollHeight > clientHeight. Bumping line-height from 1.4 to
|
|
178
|
+
* 1.45 on these specific paragraphs absorbs the mixed-font rounding without
|
|
179
|
+
* visible impact on prose-only paragraphs. */
|
|
180
|
+
.slidev-layout p:has(code),
|
|
181
|
+
.slidev-layout li:has(code) {
|
|
182
|
+
line-height: 1.45;
|
|
183
|
+
}
|
|
184
|
+
|
|
141
185
|
/* --- Headings --- */
|
|
142
186
|
.slidev-layout h1,
|
|
143
187
|
.slidev-layout h2,
|
|
@@ -247,9 +291,9 @@
|
|
|
247
291
|
}
|
|
248
292
|
|
|
249
293
|
.slidev-layout pre {
|
|
250
|
-
border-radius:
|
|
251
|
-
margin-top:
|
|
252
|
-
margin-bottom:
|
|
294
|
+
border-radius: var(--radius-md);
|
|
295
|
+
margin-top: var(--space-sm);
|
|
296
|
+
margin-bottom: var(--space-sm);
|
|
253
297
|
white-space: pre-wrap;
|
|
254
298
|
overflow-wrap: break-word;
|
|
255
299
|
}
|
|
@@ -272,11 +316,21 @@
|
|
|
272
316
|
border: none;
|
|
273
317
|
}
|
|
274
318
|
|
|
319
|
+
/* Slidev wraps code blocks in .slidev-code-wrapper (block, no BFC). Inner
|
|
320
|
+
* <pre> inherits 1rem top/bottom margin from the .slidev-layout pre rule
|
|
321
|
+
* (~31px at canvas font-size). When wrapper is a flex item (slide-col-body),
|
|
322
|
+
* those margins do not collapse with siblings or out of the flex container,
|
|
323
|
+
* inflating body height beyond column clientHeight. Zero margin inside the
|
|
324
|
+
* wrapper; outer spacing is handled by the wrapper itself. */
|
|
325
|
+
.slidev-code-wrapper pre {
|
|
326
|
+
margin: 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
275
329
|
/* Language label bar above code blocks (via Shiki transformer data-lang attribute) */
|
|
276
330
|
.slidev-layout pre[data-lang] {
|
|
277
331
|
position: relative;
|
|
278
332
|
/* Override Slidev's --slidev-code-padding (applied with !important) to make room for the label */
|
|
279
|
-
--slidev-code-padding:
|
|
333
|
+
--slidev-code-padding: var(--space-code-pt) 0.5rem 0.5rem 0.5rem;
|
|
280
334
|
}
|
|
281
335
|
|
|
282
336
|
.slidev-layout pre[data-lang]::before {
|
|
@@ -285,7 +339,7 @@
|
|
|
285
339
|
top: 0;
|
|
286
340
|
left: 0;
|
|
287
341
|
right: 0;
|
|
288
|
-
padding:
|
|
342
|
+
padding: var(--space-2xs) 0.75rem;
|
|
289
343
|
font-family: var(--font-mono);
|
|
290
344
|
font-size: 0.8em;
|
|
291
345
|
font-weight: var(--font-weight-medium);
|
|
@@ -294,15 +348,29 @@
|
|
|
294
348
|
color: var(--color-text-tertiary);
|
|
295
349
|
border-bottom: var(--border-thin) solid var(--color-border);
|
|
296
350
|
background: var(--shiki-color-background, var(--color-bg-muted));
|
|
297
|
-
border-radius:
|
|
351
|
+
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* Filename label (right side of header bar) — via {filename="..."} in code fence meta */
|
|
355
|
+
.slidev-layout pre[data-filename]::after {
|
|
356
|
+
content: attr(data-filename);
|
|
357
|
+
position: absolute;
|
|
358
|
+
top: 0;
|
|
359
|
+
right: 0;
|
|
360
|
+
padding: var(--space-2xs) 0.75rem;
|
|
361
|
+
font-family: var(--font-mono);
|
|
362
|
+
font-size: 0.8em;
|
|
363
|
+
font-weight: var(--font-weight-normal);
|
|
364
|
+
color: var(--color-text-muted);
|
|
365
|
+
opacity: 0.7;
|
|
298
366
|
}
|
|
299
367
|
|
|
300
368
|
.slidev-layout code {
|
|
301
|
-
padding-left:
|
|
302
|
-
padding-right:
|
|
303
|
-
padding-top:
|
|
304
|
-
padding-bottom:
|
|
305
|
-
border-radius:
|
|
369
|
+
padding-left: var(--space-xs-fine);
|
|
370
|
+
padding-right: var(--space-xs-fine);
|
|
371
|
+
padding-top: var(--space-3xs);
|
|
372
|
+
padding-bottom: var(--space-3xs);
|
|
373
|
+
border-radius: var(--radius-xs);
|
|
306
374
|
background-color: var(--color-bg-muted);
|
|
307
375
|
}
|
|
308
376
|
|
|
@@ -317,18 +385,18 @@
|
|
|
317
385
|
|
|
318
386
|
.slidev-layout table {
|
|
319
387
|
width: 100%;
|
|
320
|
-
margin-top:
|
|
321
|
-
margin-bottom:
|
|
388
|
+
margin-top: var(--space-sm);
|
|
389
|
+
margin-bottom: var(--space-sm);
|
|
322
390
|
border-collapse: collapse;
|
|
323
391
|
font-variant-numeric: tabular-nums;
|
|
324
392
|
}
|
|
325
393
|
|
|
326
394
|
.slidev-layout th,
|
|
327
395
|
.slidev-layout td {
|
|
328
|
-
padding-left:
|
|
329
|
-
padding-right:
|
|
330
|
-
padding-top:
|
|
331
|
-
padding-bottom:
|
|
396
|
+
padding-left: var(--space-sm);
|
|
397
|
+
padding-right: var(--space-sm);
|
|
398
|
+
padding-top: var(--space-xs);
|
|
399
|
+
padding-bottom: var(--space-xs);
|
|
332
400
|
border: var(--border-thin) solid var(--color-border);
|
|
333
401
|
text-align: left;
|
|
334
402
|
}
|
|
@@ -343,9 +411,9 @@
|
|
|
343
411
|
============================================ */
|
|
344
412
|
|
|
345
413
|
.slidev-layout blockquote {
|
|
346
|
-
padding-left:
|
|
347
|
-
margin-top:
|
|
348
|
-
margin-bottom:
|
|
414
|
+
padding-left: var(--space-sm);
|
|
415
|
+
margin-top: var(--space-sm);
|
|
416
|
+
margin-bottom: var(--space-sm);
|
|
349
417
|
border-left: var(--border-accent) solid var(--color-border-strong);
|
|
350
418
|
color: var(--color-text-secondary);
|
|
351
419
|
font-style: normal; /* No italic for readability */
|
|
@@ -564,10 +632,17 @@
|
|
|
564
632
|
display: flex;
|
|
565
633
|
flex-direction: column;
|
|
566
634
|
flex: 1;
|
|
567
|
-
overflow: auto;
|
|
568
635
|
min-height: 0;
|
|
569
636
|
}
|
|
570
637
|
|
|
638
|
+
/* Prevent last-child margin from leaking into scrollHeight (margin-collapse artifact) */
|
|
639
|
+
.slide-single > *:last-child,
|
|
640
|
+
.slide-panel-content > *:last-child,
|
|
641
|
+
.slide-col > *:last-child,
|
|
642
|
+
.slide-col-body > *:last-child {
|
|
643
|
+
margin-bottom: 0;
|
|
644
|
+
}
|
|
645
|
+
|
|
571
646
|
.slide-grid {
|
|
572
647
|
display: flex;
|
|
573
648
|
flex: 1;
|
|
@@ -575,7 +650,6 @@
|
|
|
575
650
|
}
|
|
576
651
|
|
|
577
652
|
.slide-col {
|
|
578
|
-
overflow: hidden;
|
|
579
653
|
display: flex;
|
|
580
654
|
flex-direction: column;
|
|
581
655
|
min-width: 0;
|
|
@@ -601,13 +675,13 @@
|
|
|
601
675
|
}
|
|
602
676
|
|
|
603
677
|
/* Per-column backgrounds */
|
|
604
|
-
.slide-col-bg-soft { background: var(--color-bg-soft); border-radius:
|
|
605
|
-
.slide-col-bg-muted { background: var(--color-bg-muted); border-radius:
|
|
606
|
-
.slide-col-bg-primary { background: var(--color-primary); color: var(--color-primary-foreground); border-radius:
|
|
607
|
-
.slide-col-bg-success { background: var(--color-success); color: var(--color-success-foreground); border-radius:
|
|
608
|
-
.slide-col-bg-warning { background: var(--color-warning); color: var(--color-warning-foreground); border-radius:
|
|
609
|
-
.slide-col-bg-danger { background: var(--color-danger); color: var(--color-danger-foreground); border-radius:
|
|
610
|
-
.slide-col-bg-info { background: var(--color-info); color: var(--color-info-foreground); border-radius:
|
|
678
|
+
.slide-col-bg-soft { background: var(--color-bg-soft); border-radius: var(--radius-md); padding: var(--space-sm); }
|
|
679
|
+
.slide-col-bg-muted { background: var(--color-bg-muted); border-radius: var(--radius-md); padding: var(--space-sm); }
|
|
680
|
+
.slide-col-bg-primary { background: var(--color-primary); color: var(--color-primary-foreground); border-radius: var(--radius-md); padding: var(--space-sm); }
|
|
681
|
+
.slide-col-bg-success { background: var(--color-success); color: var(--color-success-foreground); border-radius: var(--radius-md); padding: var(--space-sm); }
|
|
682
|
+
.slide-col-bg-warning { background: var(--color-warning); color: var(--color-warning-foreground); border-radius: var(--radius-md); padding: var(--space-sm); }
|
|
683
|
+
.slide-col-bg-danger { background: var(--color-danger); color: var(--color-danger-foreground); border-radius: var(--radius-md); padding: var(--space-sm); }
|
|
684
|
+
.slide-col-bg-info { background: var(--color-info); color: var(--color-info-foreground); border-radius: var(--radius-md); padding: var(--space-sm); }
|
|
611
685
|
|
|
612
686
|
.slide-col-bg-primary h1, .slide-col-bg-primary h2, .slide-col-bg-primary h3,
|
|
613
687
|
.slide-col-bg-success h1, .slide-col-bg-success h2, .slide-col-bg-success h3,
|
|
@@ -616,7 +690,7 @@
|
|
|
616
690
|
.slide-col-bg-info h1, .slide-col-bg-info h2, .slide-col-bg-info h3 { color: inherit; }
|
|
617
691
|
|
|
618
692
|
/* Per-column border */
|
|
619
|
-
.slide-col-border { border: var(--border-thin) solid var(--color-border); border-radius:
|
|
693
|
+
.slide-col-border { border: var(--border-thin) solid var(--color-border); border-radius: var(--radius-md); padding: var(--space-sm); }
|
|
620
694
|
|
|
621
695
|
/* --- 7c. Split Mode --- */
|
|
622
696
|
.slide-split {
|
|
@@ -633,7 +707,6 @@
|
|
|
633
707
|
|
|
634
708
|
.slide-panel {
|
|
635
709
|
position: relative;
|
|
636
|
-
overflow: hidden;
|
|
637
710
|
}
|
|
638
711
|
|
|
639
712
|
.slide-panel-bg-only {
|
|
@@ -648,7 +721,6 @@
|
|
|
648
721
|
|
|
649
722
|
.slide-panel-content {
|
|
650
723
|
padding: var(--safe-inset-y) var(--safe-inset-x);
|
|
651
|
-
overflow: auto;
|
|
652
724
|
display: flex;
|
|
653
725
|
flex-direction: column;
|
|
654
726
|
justify-content: center;
|
|
@@ -682,6 +754,15 @@
|
|
|
682
754
|
margin-right: calc(var(--safe-inset-x) * -1);
|
|
683
755
|
}
|
|
684
756
|
|
|
757
|
+
/* When footer is present, reduce bottom padding to reclaim space */
|
|
758
|
+
.slidev-layout:has(.slide-footer) {
|
|
759
|
+
padding-bottom: 0;
|
|
760
|
+
}
|
|
761
|
+
/* Split panels: reduce bottom padding when footer takes over that space */
|
|
762
|
+
.slide-split:has(.slide-footer) .slide-panel-content {
|
|
763
|
+
padding-bottom: 1%;
|
|
764
|
+
}
|
|
765
|
+
|
|
685
766
|
/* ============================================
|
|
686
767
|
8. Component Styles
|
|
687
768
|
============================================ */
|
|
@@ -690,17 +771,17 @@
|
|
|
690
771
|
.tags-container {
|
|
691
772
|
display: flex;
|
|
692
773
|
flex-wrap: wrap;
|
|
693
|
-
gap:
|
|
694
|
-
margin-top:
|
|
695
|
-
margin-bottom:
|
|
774
|
+
gap: var(--space-xs);
|
|
775
|
+
margin-top: var(--space-sm);
|
|
776
|
+
margin-bottom: var(--space-sm);
|
|
696
777
|
}
|
|
697
778
|
|
|
698
779
|
.tag {
|
|
699
|
-
padding-left:
|
|
700
|
-
padding-right:
|
|
701
|
-
padding-top:
|
|
702
|
-
padding-bottom:
|
|
703
|
-
border-radius:
|
|
780
|
+
padding-left: var(--space-tight);
|
|
781
|
+
padding-right: var(--space-tight);
|
|
782
|
+
padding-top: var(--space-2xs);
|
|
783
|
+
padding-bottom: var(--space-2xs);
|
|
784
|
+
border-radius: var(--radius-pill);
|
|
704
785
|
font-size: var(--font-size-small);
|
|
705
786
|
background-color: var(--color-primary-soft);
|
|
706
787
|
color: var(--color-primary);
|
|
@@ -709,8 +790,8 @@
|
|
|
709
790
|
/* Button components */
|
|
710
791
|
.btn {
|
|
711
792
|
display: inline-block;
|
|
712
|
-
padding:
|
|
713
|
-
border-radius:
|
|
793
|
+
padding: var(--space-xs) 1rem;
|
|
794
|
+
border-radius: var(--radius-sm);
|
|
714
795
|
font-weight: var(--font-weight-medium);
|
|
715
796
|
text-decoration: none;
|
|
716
797
|
cursor: pointer;
|
|
@@ -799,7 +880,7 @@ a.btn-secondary:hover {
|
|
|
799
880
|
position: absolute;
|
|
800
881
|
inset: 5% 5%;
|
|
801
882
|
border: var(--border-thick) dashed var(--color-danger);
|
|
802
|
-
border-radius:
|
|
883
|
+
border-radius: var(--radius-md);
|
|
803
884
|
pointer-events: none;
|
|
804
885
|
}
|
|
805
886
|
|
|
@@ -825,9 +906,9 @@ a.btn-secondary:hover {
|
|
|
825
906
|
/* Enhanced blockquote callout base style */
|
|
826
907
|
.slidev-layout blockquote:has(p:first-child strong:first-child) {
|
|
827
908
|
border-left-width: var(--border-accent);
|
|
828
|
-
padding:
|
|
909
|
+
padding: var(--space-sm) 1rem 1rem 1.25rem;
|
|
829
910
|
background-color: var(--color-bg-soft);
|
|
830
|
-
border-radius: 0
|
|
911
|
+
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
|
831
912
|
}
|
|
832
913
|
|
|
833
914
|
/* Semantic callout classes — apply to blockquote */
|
|
@@ -862,7 +943,7 @@ a.btn-secondary:hover {
|
|
|
862
943
|
|
|
863
944
|
/* Ensure focus is not obscured */
|
|
864
945
|
.slidev-layout :focus-visible {
|
|
865
|
-
scroll-padding:
|
|
946
|
+
scroll-padding: var(--space-sm);
|
|
866
947
|
}
|
|
867
948
|
|
|
868
949
|
/* (Old layout sections 12-27 removed - replaced by section 7 above) */
|
|
@@ -915,19 +996,19 @@ a.btn-secondary:hover {
|
|
|
915
996
|
|
|
916
997
|
/* Ensure thought bubble circles are not clipped in overflow: auto containers */
|
|
917
998
|
.bubble-thought.bubble-bottom {
|
|
918
|
-
margin-bottom:
|
|
999
|
+
margin-bottom: var(--space-md);
|
|
919
1000
|
}
|
|
920
1001
|
|
|
921
1002
|
.bubble-thought.bubble-top {
|
|
922
|
-
margin-top:
|
|
1003
|
+
margin-top: var(--space-md);
|
|
923
1004
|
}
|
|
924
1005
|
|
|
925
1006
|
.bubble-thought.bubble-left {
|
|
926
|
-
margin-left:
|
|
1007
|
+
margin-left: var(--space-lg);
|
|
927
1008
|
}
|
|
928
1009
|
|
|
929
1010
|
.bubble-thought.bubble-right {
|
|
930
|
-
margin-right:
|
|
1011
|
+
margin-right: var(--space-lg);
|
|
931
1012
|
}
|
|
932
1013
|
|
|
933
1014
|
/* ============================================
|
|
@@ -935,7 +1016,7 @@ a.btn-secondary:hover {
|
|
|
935
1016
|
============================================ */
|
|
936
1017
|
|
|
937
1018
|
.device-mockup-placeholder {
|
|
938
|
-
padding:
|
|
1019
|
+
padding: var(--space-lg);
|
|
939
1020
|
text-align: center;
|
|
940
1021
|
}
|
|
941
1022
|
|
package/styles/motion.css
CHANGED
|
@@ -313,7 +313,7 @@
|
|
|
313
313
|
animation: entrance-slide-right var(--dur-moderate-02) var(--ease-enter) calc(var(--stagger-base) * 4) both;
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
.slide-grid > .slide-col:
|
|
316
|
+
.slide-grid > .slide-col:nth-child(2):last-child {
|
|
317
317
|
animation: entrance-slide-left var(--dur-moderate-02) var(--ease-enter) calc(var(--stagger-base) * 4) both;
|
|
318
318
|
}
|
|
319
319
|
|