Skip to content

Commit b7e4ac0

Browse files
committed
feat(docs): home-icon, parts/topics pill rows, refreshed walkthrough layout
scripts/tour.mts: - Add a single shared HOME_ICON_SVG so the docs renderer and the post-processor emit byte-identical markup (stable CSP hash). - Factor out renderPartsPillRow / renderTopicsPillRow so every doc page gets the same "Parts: 1 2 3 … 8" and "Topics: A B C …" pill rows at the top, with classes matching meander's part-nav so the existing CSS + pill-enrichment passes apply uniformly. walkthrough-overrides.css: - .wt-contents-row becomes a two-column flex (title/summary stack on the left, section-count pill on the right) with a reading-width cap so descriptions don't stretch edge-to-edge on wide monitors. - Badge gets a soft accent tint + tabular-nums for stable widths. - Tighter rhythm on .wt-contents / title / summary.
1 parent 579e030 commit b7e4ac0

2 files changed

Lines changed: 127 additions & 41 deletions

File tree

scripts/tour.mts

Lines changed: 82 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,70 @@ function validateDocFilenames(
202202
* paths surface all failures at once so editing errors in every doc
203203
* show up in one build, not one-per-build.
204204
*/
205+
/**
206+
* SVG home icon used in every page's nav. Defined once so the docs
207+
* renderer and the part-page post-processor emit the same bytes (CSP
208+
* hash stable across both). Single path, hard-coded stroke — no
209+
* external sprite or font dep.
210+
*/
211+
const HOME_ICON_SVG =
212+
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9.5 12 3l9 6.5V20a2 2 0 0 1-2 2h-4v-7h-6v7H5a2 2 0 0 1-2-2z"/></svg>'
213+
214+
const HOME_LINK_HTML =
215+
`<a class="wt-home-link" href="/" aria-label="Back to the table of contents" title="Back to the table of contents">${HOME_ICON_SVG}</a>`
216+
217+
/**
218+
* Render the "Parts: 1 2 3 … 8" pill row. Same HTML shape meander
219+
* emits for its own part-nav — classes and hrefs match so the CSS +
220+
* post-process pill-enrichment paths in this file apply uniformly.
221+
* Emitted at the top of every doc page so doc readers can jump into
222+
* any part with one click.
223+
*/
224+
function renderPartsPillRow(
225+
slug: string,
226+
partFilenames: ReadonlyMap<number, string>,
227+
): string {
228+
const ids = [...partFilenames.keys()].sort((a, b) => a - b)
229+
const pills = ids
230+
.map(id => `<a href="/${slug}/part/${id}">Part ${id}</a>`)
231+
.join('\n ')
232+
return (
233+
` <div class="part-nav">${HOME_LINK_HTML}<span class="wt-parts-label">Parts:</span>\n` +
234+
` ${pills}\n` +
235+
` </div>`
236+
)
237+
}
238+
239+
/**
240+
* Render the "Topics: A B C … Z" pill row. Mirrors the parts row
241+
* shape so the same CSS styles both. `activeFilename` marks the
242+
* current doc's pill with class="active"; pass `undefined` from part
243+
* pages so no pill is active.
244+
*/
245+
function renderTopicsPillRow(
246+
docs: ReadonlyMap<string, DocEntry>,
247+
activeFilename: string | undefined,
248+
): string {
249+
const pills = [...docs.values()]
250+
.map(d => {
251+
const cls = d.filename === activeFilename ? 'active' : ''
252+
const ariaLabel = d.summary
253+
? ` aria-label="${escapeHtml(d.title)}: ${escapeHtml(d.summary)}"`
254+
: ''
255+
return `<a class="${cls}" href="/${d.filename}.html" title="${escapeHtml(d.title)}"${ariaLabel}>${escapeHtml(d.title)}</a>`
256+
})
257+
.join('\n ')
258+
return (
259+
` <div class="part-nav wt-topics-nav"><span class="wt-parts-label">Topics:</span>\n` +
260+
` ${pills}\n` +
261+
` </div>`
262+
)
263+
}
264+
205265
async function renderDocs(
206266
docs: ReadonlyMap<string, DocEntry>,
207267
slug: string,
268+
partFilenames: ReadonlyMap<number, string>,
208269
repoRoot: string,
209270
tourDir: string,
210271
): Promise<void> {
@@ -217,12 +278,9 @@ async function renderDocs(
217278

218279
const entries = [...docs.values()]
219280

220-
// Build the topic-nav fragment once per build. Each doc swaps in
221-
// class="active" for its own anchor before emitting.
222-
const navLink = (d: DocEntry, active: boolean): string => {
223-
const cls = active ? 'active' : ''
224-
return `<a class="${cls}" href="/${slug}/${d.filename}.html" title="${escapeHtml(d.title)}"${d.summary ? ` aria-label="${escapeHtml(d.title)}: ${escapeHtml(d.summary)}"` : ''}>${escapeHtml(d.title)}</a>`
225-
}
281+
// Precompute the two pill rows once per build — same bytes on every
282+
// doc page except the "active" marker on the current Topics pill.
283+
const partsRow = renderPartsPillRow(slug, partFilenames)
226284

227285
const renderOne = async (doc: DocEntry): Promise<void> => {
228286
const sourcePath = path.join(repoRoot, doc.source)
@@ -233,9 +291,7 @@ async function renderDocs(
233291
}
234292
const markdown = await fs.readFile(sourcePath, 'utf8')
235293
const body = await marked.parse(markdown)
236-
const navPills = entries
237-
.map(d => navLink(d, d.filename === doc.filename))
238-
.join('\n ')
294+
const topicsRow = renderTopicsPillRow(docs, doc.filename)
239295
const summaryLine = doc.summary
240296
? ` <p>${escapeHtml(doc.summary)}</p>\n`
241297
: ''
@@ -258,10 +314,8 @@ async function renderDocs(
258314
` <header class="topbar">\n` +
259315
` <h1>${escapeHtml(doc.title)}</h1>\n` +
260316
summaryLine +
261-
` <div class="topic-nav">\n` +
262-
` <span class="wt-topics-label">Topics:</span>\n` +
263-
` ${navPills}\n` +
264-
` </div>\n` +
317+
`${partsRow}\n` +
318+
`${topicsRow}\n` +
265319
` </header>\n` +
266320
`\n` +
267321
` <main class="doc-body">\n` +
@@ -349,25 +403,28 @@ async function rewriteIndexContents(
349403

350404
// Build unified rows in stable order: parts 1..N first, then docs
351405
// in manifest order. Each row is a flat <div> carrying the same
352-
// shape — title link + muted description. Parts also get a lighter
353-
// "N sections" bonus annotation appended inline. Using <div>s (not
354-
// <ul>/<li>) sidesteps the browser-default bullet rendering that
355-
// made the old list look off-tempo.
406+
// shape — title link + muted description on the left (capped reading
407+
// width via CSS), optional section-count badge on the right. Using
408+
// <div>s (not <ul>/<li>) sidesteps the browser-default bullet
409+
// rendering that made the old list look off-tempo.
356410
const renderRow = (
357411
href: string,
358412
title: string,
359413
description: string,
360-
bonus?: string,
414+
badge?: string,
361415
): string => {
362-
const bonusHtml = bonus
363-
? ` <span class="wt-contents-bonus">· ${escapeHtml(bonus)}</span>`
416+
const badgeHtml = badge
417+
? ` <span class="wt-contents-badge">${escapeHtml(badge)}</span>\n`
364418
: ''
365419
return (
366420
` <div class="wt-contents-row">\n` +
367-
` <a class="wt-contents-title" href="${href}">${escapeHtml(title)}</a>\n` +
421+
` <div class="wt-contents-main">\n` +
422+
` <a class="wt-contents-title" href="${href}">${escapeHtml(title)}</a>\n` +
368423
(description
369-
? ` <p class="wt-contents-summary">${escapeHtml(description)}${bonusHtml}</p>\n`
424+
? ` <p class="wt-contents-summary">${escapeHtml(description)}</p>\n`
370425
: '') +
426+
` </div>\n` +
427+
badgeHtml +
371428
` </div>`
372429
)
373430
}
@@ -378,8 +435,8 @@ async function rewriteIndexContents(
378435
const title = partTitles.get(id) ?? `Part ${id}`
379436
const description = partObjectives.get(id) ?? ''
380437
const count = partCounts.get(id)
381-
const bonus = count !== undefined ? `${count} sections` : undefined
382-
rows.push(renderRow(`/${filename}.html`, title, description, bonus))
438+
const badge = count !== undefined ? `${count} sections` : undefined
439+
rows.push(renderRow(`/${filename}.html`, title, description, badge))
383440
}
384441
for (const d of docs.values()) {
385442
rows.push(renderRow(`/${d.filename}.html`, d.title, d.summary ?? ''))
@@ -923,7 +980,7 @@ async function generate(
923980
partFilenames,
924981
configPath ? path.resolve(configPath) : '<config>',
925982
)
926-
await renderDocs(docFilenames, slug, repoRoot, tourDir)
983+
await renderDocs(docFilenames, slug, partFilenames, repoRoot, tourDir)
927984
// Extend index.html with a Topics section pointing at each doc. Runs
928985
// after docs are rendered (no ordering dependency — the section just
929986
// links by filename) and before post-process so any hrefs get the

walkthrough-overrides.css

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -308,13 +308,11 @@ html[data-theme='dark'] .file-grid > .code-section:hover {
308308
* bright title does the "this is a link" job; the muted context sits
309309
* underneath like a dek line in a newspaper headline. */
310310
.wt-contents {
311-
/* Borrow the annotation-card surface tone so it visually anchors
312-
* like the rest of the page, but skip the inner padding because
313-
* .wt-contents-row carries its own rhythm. */
311+
/* Card surface to anchor the list against the page background. */
314312
background: var(--prose-bg);
315313
border: 1px solid var(--rule);
316314
border-radius: 8px;
317-
padding: 16px 20px;
315+
padding: 20px 24px;
318316
margin: 0 0 16px 0;
319317
}
320318
.wt-contents > h3 {
@@ -323,14 +321,33 @@ html[data-theme='dark'] .file-grid > .code-section:hover {
323321
letter-spacing: 0.08em;
324322
font-weight: 600;
325323
color: var(--eyebrow);
326-
margin: 0 0 12px 0;
324+
margin: 0 0 16px 0;
327325
}
326+
327+
/* Each row is a two-column flex: the main title+summary stack on
328+
* the left (capped width so descriptions don't sprint across
329+
* ultra-wide monitors), the section-count badge on the right. */
328330
.wt-contents-row {
329-
padding: 8px 0;
330-
border-top: 1px solid color-mix(in srgb, var(--rule) 50%, transparent);
331+
display: flex;
332+
align-items: flex-start;
333+
justify-content: space-between;
334+
gap: 16px;
335+
padding: 14px 0;
336+
border-top: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
331337
}
332338
.wt-contents-row:first-of-type {
333339
border-top: 0;
340+
padding-top: 4px;
341+
}
342+
.wt-contents-row:last-of-type {
343+
padding-bottom: 4px;
344+
}
345+
.wt-contents-main {
346+
/* Cap the reading width of the description column so prose doesn't
347+
* stretch edge-to-edge on wide monitors. Matches the doc-body
348+
* reading rhythm. */
349+
max-width: 72ch;
350+
min-width: 0;
334351
}
335352
.wt-contents-title {
336353
display: inline-block;
@@ -348,20 +365,32 @@ html[data-theme='dark'] .file-grid > .code-section:hover {
348365
background: color-mix(in srgb, var(--accent) 10%, transparent);
349366
}
350367
.wt-contents-summary {
351-
/* Muted, matches the "Generated from multiline source comments…"
352-
* dek line at the top of the topbar. Same tone as the patent line
368+
/* Muted one-liner — same tone as the "U.S. Patent No. …" dek line
353369
* in the socket.dev footer reference. */
354-
margin: 4px 0 0 0;
370+
margin: 6px 0 0 0;
355371
font-size: 13px;
356-
line-height: 1.45;
372+
line-height: 1.5;
357373
color: var(--eyebrow);
358374
}
359-
.wt-contents-bonus {
360-
/* Section-count annotation — even lighter than the summary itself
361-
* so it reads as "bonus metadata" rather than part of the prose. */
362-
color: color-mix(in srgb, var(--eyebrow) 70%, transparent);
375+
376+
/* Section-count badge — rides the right edge of the row. Pill shape
377+
* with a soft accent tint so it reads as "metadata," not as another
378+
* link or action. Tabular-nums keeps widths consistent across rows
379+
* so the column of badges is visually aligned. */
380+
.wt-contents-badge {
381+
flex: 0 0 auto;
382+
align-self: center;
383+
font-size: 11px;
384+
font-weight: 600;
363385
font-variant-numeric: tabular-nums;
364-
margin-left: 2px;
386+
text-transform: uppercase;
387+
letter-spacing: 0.04em;
388+
color: var(--eyebrow);
389+
background: color-mix(in srgb, var(--accent) 10%, transparent);
390+
border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
391+
padding: 3px 10px;
392+
border-radius: 999px;
393+
white-space: nowrap;
365394
}
366395

367396
/* ─── Topic doc pages ───────────────────────────────────────────

0 commit comments

Comments
 (0)