@kolkrabbi/kol-workshop 0.3.3 → 0.3.5
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-workshop",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "KOL workshop/docs system — a handrolled markdown engine, search, tag system (incl. a d3 tag graph), docs viewer, and docs shell. Sits above @kolkrabbi/kol-{theme,icons,component,framework}; content is consumer-injected (the package never globs docs).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"d3": "^7.9.0",
|
|
16
16
|
"@kolkrabbi/kol-brand": "^0.1.2",
|
|
17
17
|
"@kolkrabbi/kol-component": "0.14.1",
|
|
18
|
-
"@kolkrabbi/kol-icons": "0.8.
|
|
19
|
-
"@kolkrabbi/kol-framework": "0.
|
|
20
|
-
"@kolkrabbi/kol-theme": "0.13.
|
|
18
|
+
"@kolkrabbi/kol-icons": "0.8.11",
|
|
19
|
+
"@kolkrabbi/kol-framework": "0.8.1",
|
|
20
|
+
"@kolkrabbi/kol-theme": "0.13.3"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"react": "^18.3.0 || ^19.0.0",
|
package/src/docs/DocsArticle.jsx
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
const join = (...classes) => classes.filter(Boolean).join(' ')
|
|
2
2
|
|
|
3
|
+
/* NOT .kol-prose (wave-4 retype, 2026-07-30 — user law: one layout = one style
|
|
4
|
+
* system; prose is the blog/CMS voice, never docs surfaces). Every element the
|
|
5
|
+
* reader renders is typed explicitly through the kol-doc-* roles, mirroring
|
|
6
|
+
* showcase/src/lib/mdx-components.jsx — the SAME dialect MDX pages speak.
|
|
7
|
+
* Vertical rhythm comes from the flex gap (the MdxDoc content idiom), not
|
|
8
|
+
* prose margins. */
|
|
3
9
|
const DocsArticle = ({ children, className }) => (
|
|
4
|
-
<article className={join('docs-article
|
|
10
|
+
<article className={join('docs-article flex w-full min-w-0 flex-col gap-6', className)}>{children}</article>
|
|
5
11
|
)
|
|
6
12
|
|
|
7
13
|
export default DocsArticle
|
package/src/docs/DocsHeader.jsx
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
const join = (...classes) => classes.filter(Boolean).join(' ')
|
|
2
2
|
|
|
3
|
+
/* Doc voice (wave-4 retype, 2026-07-30): title/subtitle ride the DocHeader
|
|
4
|
+
* contract roles (kol-doc-heading / kol-doc-lede) instead of the old
|
|
5
|
+
* kol-sans-heading-02 + kol-mono-14 pair — one header voice across the
|
|
6
|
+
* reader, MDX pages and the Doc* kit. */
|
|
3
7
|
const DocsHeader = ({ title, subtitle, meta = [], className }) => (
|
|
4
|
-
<header className={join('docs-header', className)}>
|
|
5
|
-
{title ? <h1 className="kol-
|
|
6
|
-
{subtitle ? <p className="kol-
|
|
8
|
+
<header className={join('docs-header flex flex-col gap-3', className)}>
|
|
9
|
+
{title ? <h1 className="kol-doc-heading">{title}</h1> : null}
|
|
10
|
+
{subtitle ? <p className="kol-doc-lede">{subtitle}</p> : null}
|
|
7
11
|
{Array.isArray(meta) && meta.length > 0 ? (
|
|
8
12
|
<div className="docs-meta kol-helper-10 text-body">
|
|
9
13
|
{meta.map(({ label, value }) => (
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { useContext, useEffect, useMemo, useState } from 'react'
|
|
2
2
|
import { useParams, Link, useNavigate } from 'react-router-dom'
|
|
3
|
-
import { CodeBlock, Divider, DocsToc, Icon, Tag } from '@kolkrabbi/kol-component'
|
|
3
|
+
import { CodeBlock, Divider, DocsToc, Icon, Table, Tag } from '@kolkrabbi/kol-component'
|
|
4
4
|
import { ShellTocContext } from '../shell'
|
|
5
5
|
import { useTagMode } from '../tags'
|
|
6
6
|
import { parseDocsMarkdown, isIndexFile, getTagColor } from '../engine'
|
|
7
7
|
import DocsHeader from './DocsHeader.jsx'
|
|
8
8
|
import DocsArticle from './DocsArticle.jsx'
|
|
9
9
|
import DocsFrontmatter from './DocsFrontmatter.jsx'
|
|
10
|
+
import { DocSection } from './DocKit.jsx'
|
|
10
11
|
import { renderInlineTokens } from './render-tokens.jsx'
|
|
11
12
|
|
|
12
13
|
const SidebarSection = ({ sectionKey, label, collapsedSections, toggleSection, children }) => (
|
|
@@ -22,7 +23,7 @@ const SidebarSection = ({ sectionKey, label, collapsedSections, toggleSection, c
|
|
|
22
23
|
</div>
|
|
23
24
|
)
|
|
24
25
|
|
|
25
|
-
const DocReaderSidebar = ({ toc, allTags, docId, docsIndexHref, componentsHref, docFilePath }) => {
|
|
26
|
+
const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, componentsHref, docFilePath }) => {
|
|
26
27
|
const navigate = useNavigate()
|
|
27
28
|
const { openTagMode } = useTagMode()
|
|
28
29
|
const [collapsedSections, setCollapsedSections] = useState({})
|
|
@@ -39,6 +40,24 @@ const DocReaderSidebar = ({ toc, allTags, docId, docsIndexHref, componentsHref,
|
|
|
39
40
|
<DocsToc toc={toc} />
|
|
40
41
|
</SidebarSection>
|
|
41
42
|
|
|
43
|
+
{related.length > 0 && (
|
|
44
|
+
<SidebarSection
|
|
45
|
+
sectionKey="related"
|
|
46
|
+
label="Related"
|
|
47
|
+
collapsedSections={collapsedSections}
|
|
48
|
+
toggleSection={toggleSection}
|
|
49
|
+
>
|
|
50
|
+
{/* ONE rail voice (user law): rows = the shell-nav-item idiom. */}
|
|
51
|
+
<nav className="flex flex-col">
|
|
52
|
+
{related.map((r) => (
|
|
53
|
+
<Link key={r.id} to={r.href} className="shell-nav-item block kol-mono-14 text-body transition-colors hover:text-emphasis">
|
|
54
|
+
{r.label}
|
|
55
|
+
</Link>
|
|
56
|
+
))}
|
|
57
|
+
</nav>
|
|
58
|
+
</SidebarSection>
|
|
59
|
+
)}
|
|
60
|
+
|
|
42
61
|
<SidebarSection
|
|
43
62
|
sectionKey="actions"
|
|
44
63
|
label="Quick actions"
|
|
@@ -139,6 +158,7 @@ const DocumentationReader = ({
|
|
|
139
158
|
|
|
140
159
|
const { docId } = useParams()
|
|
141
160
|
const setTocContent = useContext(ShellTocContext)
|
|
161
|
+
const { openTagMode } = useTagMode()
|
|
142
162
|
|
|
143
163
|
// Build a Set of known doc IDs for fast lookup
|
|
144
164
|
const knownDocIds = useMemo(
|
|
@@ -162,9 +182,87 @@ const DocumentationReader = ({
|
|
|
162
182
|
return null
|
|
163
183
|
}
|
|
164
184
|
|
|
165
|
-
// Bind the resolved link + tag helpers so call sites stay terse.
|
|
185
|
+
// Bind the resolved link + tag helpers so call sites stay terse. Hashtag
|
|
186
|
+
// pills open TAG MODE (wave-4 parity) — the tagHref Link was a dead route
|
|
187
|
+
// in any consumer without a ?tag= index page.
|
|
166
188
|
const renderTokens = (tokens, tokenKey) =>
|
|
167
|
-
renderInlineTokens(tokens, tokenKey, resolveDocLink, tagHref)
|
|
189
|
+
renderInlineTokens(tokens, tokenKey, resolveDocLink, tagHref, openTagMode)
|
|
190
|
+
|
|
191
|
+
/* ONE block renderer for intro + section content (wave-4 retype, 2026-07-30
|
|
192
|
+
* — the two switches had already drifted). Every element is typed through
|
|
193
|
+
* the kol-doc-* dialect of showcase mdx-components.jsx: doc-body running
|
|
194
|
+
* text (self-caps at the measure), heading-05 h3/h4 (h4 = weight 400, the
|
|
195
|
+
* prose-h4 precedent), panel-capped furniture (width law: tables/code cap
|
|
196
|
+
* at --kol-content-panel), markdown tables through THE kol-component Table
|
|
197
|
+
* as a dynamic-column DocTable sibling (no parallel table markup). */
|
|
198
|
+
const renderBlock = (block, blockKey) => {
|
|
199
|
+
switch (block.type) {
|
|
200
|
+
case 'paragraph':
|
|
201
|
+
return (
|
|
202
|
+
<p key={blockKey} className="kol-doc-body">
|
|
203
|
+
{block.tokens ? renderTokens(block.tokens, blockKey) : block.content}
|
|
204
|
+
</p>
|
|
205
|
+
)
|
|
206
|
+
case 'heading3':
|
|
207
|
+
return (
|
|
208
|
+
<h3 key={blockKey} id={block.id} className="kol-sans-heading-05 text-emphasis scroll-mt-20 mt-2">
|
|
209
|
+
{block.content}
|
|
210
|
+
</h3>
|
|
211
|
+
)
|
|
212
|
+
case 'heading4':
|
|
213
|
+
return (
|
|
214
|
+
<h4 key={blockKey} id={block.id} className="kol-sans-heading-05 font-normal text-emphasis scroll-mt-20">
|
|
215
|
+
{block.content}
|
|
216
|
+
</h4>
|
|
217
|
+
)
|
|
218
|
+
case 'list': {
|
|
219
|
+
const ListComponent = block.ordered ? 'ol' : 'ul'
|
|
220
|
+
const listClass = `flex ${block.ordered ? 'list-decimal' : 'list-disc'} flex-col gap-2 pl-5 kol-doc-body`
|
|
221
|
+
return block.items ? (
|
|
222
|
+
<ListComponent key={blockKey} className={listClass}>
|
|
223
|
+
{block.items.map((item, itemIndex) => (
|
|
224
|
+
<li key={itemIndex}>
|
|
225
|
+
{item.tokens ? renderTokens(item.tokens, `${blockKey}-item-${itemIndex}`) : item.content || item}
|
|
226
|
+
</li>
|
|
227
|
+
))}
|
|
228
|
+
</ListComponent>
|
|
229
|
+
) : null
|
|
230
|
+
}
|
|
231
|
+
case 'blockquote':
|
|
232
|
+
return (
|
|
233
|
+
<blockquote key={blockKey} className="docs-callout kol-doc-body">
|
|
234
|
+
{block.tokens ? renderTokens(block.tokens, blockKey) : block.content}
|
|
235
|
+
</blockquote>
|
|
236
|
+
)
|
|
237
|
+
case 'divider':
|
|
238
|
+
return <Divider key={blockKey} className="docs-divider" opacity="12" />
|
|
239
|
+
case 'code':
|
|
240
|
+
return (
|
|
241
|
+
<div key={blockKey} className="max-w-[var(--kol-content-panel)]">
|
|
242
|
+
<CodeBlock code={block.lines.join('\n')} language={block.lang} />
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
case 'table':
|
|
246
|
+
return (
|
|
247
|
+
<div key={blockKey} className="max-w-[var(--kol-content-panel)]">
|
|
248
|
+
<Table
|
|
249
|
+
variant="simple"
|
|
250
|
+
columns={block.headers.map((header, i) => ({ accessor: `c${i}`, header }))}
|
|
251
|
+
rows={block.rows.map((row, rowIndex) => ({
|
|
252
|
+
id: rowIndex,
|
|
253
|
+
...Object.fromEntries(row.map((cell, cellIndex) => [
|
|
254
|
+
`c${cellIndex}`,
|
|
255
|
+
cell.tokens ? renderTokens(cell.tokens, `${blockKey}-row-${rowIndex}-cell-${cellIndex}`) : cell.content,
|
|
256
|
+
])),
|
|
257
|
+
}))}
|
|
258
|
+
className="kol-doc-table"
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
)
|
|
262
|
+
default:
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
}
|
|
168
266
|
|
|
169
267
|
const doc = useMemo(() => {
|
|
170
268
|
return inventory.find((d) => d.id === docId)
|
|
@@ -218,6 +316,35 @@ const DocumentationReader = ({
|
|
|
218
316
|
return h1Block?.content || null
|
|
219
317
|
}, [introBlocks])
|
|
220
318
|
|
|
319
|
+
/* Cross-references (wave-4 parity): frontmatter `related:` entries are
|
|
320
|
+
* wikilinks — `[[target|display]]`, target relative to the doc's own
|
|
321
|
+
* folder. Resolved against the inventory by full path first (handles
|
|
322
|
+
* `../INDEX`-style hops), then by bare basename id as the fallback for
|
|
323
|
+
* hand-typed targets. Unresolvable entries are dropped, not rendered dead. */
|
|
324
|
+
const related = useMemo(() => {
|
|
325
|
+
const entries = doc?.metadata?.related
|
|
326
|
+
if (!Array.isArray(entries) || !doc?.file) return []
|
|
327
|
+
return entries
|
|
328
|
+
.map((entry) => {
|
|
329
|
+
const m = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/.exec(String(entry))
|
|
330
|
+
const target = (m ? m[1] : String(entry)).trim()
|
|
331
|
+
const display = (m?.[2] || target.split('/').pop().replace(/\.md$/, '')).trim()
|
|
332
|
+
const segs = doc.file.split('/').slice(0, -1)
|
|
333
|
+
for (const seg of target.replace(/\.md$/, '').split('/')) {
|
|
334
|
+
if (seg === '..') segs.pop()
|
|
335
|
+
else if (seg && seg !== '.') segs.push(seg)
|
|
336
|
+
}
|
|
337
|
+
const full = segs.join('/')
|
|
338
|
+
const basename = target.split('/').pop().replace(/\.md$/, '')
|
|
339
|
+
const hit =
|
|
340
|
+
inventory.find((d) => d.file.replace(/\.md$/, '') === full) ??
|
|
341
|
+
inventory.find((d) => d.file.replace(/\.md$/, '').toLowerCase() === `${full}/index`.toLowerCase()) ??
|
|
342
|
+
inventory.find((d) => d.id === basename)
|
|
343
|
+
return hit ? { id: hit.id, href: docHref(hit.id), label: display } : null
|
|
344
|
+
})
|
|
345
|
+
.filter(Boolean)
|
|
346
|
+
}, [doc, inventory, docHref])
|
|
347
|
+
|
|
221
348
|
/* The reader is usable OUTSIDE a ShellLayout (a plain page, a lobby route):
|
|
222
349
|
* ShellTocContext then defaults to null, so calling it unguarded threw.
|
|
223
350
|
* No shell → no right rail, and the reader just renders its article. */
|
|
@@ -228,6 +355,7 @@ const DocumentationReader = ({
|
|
|
228
355
|
key={docId}
|
|
229
356
|
toc={toc}
|
|
230
357
|
allTags={allTags}
|
|
358
|
+
related={related}
|
|
231
359
|
docId={docId}
|
|
232
360
|
docsIndexHref={docsIndex}
|
|
233
361
|
componentsHref={components}
|
|
@@ -235,7 +363,7 @@ const DocumentationReader = ({
|
|
|
235
363
|
/>
|
|
236
364
|
)
|
|
237
365
|
return () => setTocContent(null)
|
|
238
|
-
}, [setTocContent, docId, toc, allTags, docsIndex, components, docFilePath])
|
|
366
|
+
}, [setTocContent, docId, toc, allTags, related, docsIndex, components, docFilePath])
|
|
239
367
|
|
|
240
368
|
if (!doc) {
|
|
241
369
|
return (
|
|
@@ -254,172 +382,19 @@ const DocumentationReader = ({
|
|
|
254
382
|
<DocsArticle>
|
|
255
383
|
<DocsFrontmatter metadata={doc.metadata} docId={docId} />
|
|
256
384
|
{docTitle && (
|
|
257
|
-
<h1>{docTitle}</h1>
|
|
385
|
+
<h1 className="kol-doc-heading">{docTitle}</h1>
|
|
258
386
|
)}
|
|
259
387
|
{/* Render intro blocks (excluding H1 which is docTitle) */}
|
|
260
|
-
{introBlocks.filter(b => b.type !== 'heading1').map((block, index) =>
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
case 'paragraph':
|
|
264
|
-
return (
|
|
265
|
-
<p key={blockKey}>
|
|
266
|
-
{block.tokens ? renderTokens(block.tokens, blockKey) : block.content}
|
|
267
|
-
</p>
|
|
268
|
-
)
|
|
269
|
-
case 'heading3':
|
|
270
|
-
return (
|
|
271
|
-
<h3 key={blockKey} id={block.id}>
|
|
272
|
-
{block.content}
|
|
273
|
-
</h3>
|
|
274
|
-
)
|
|
275
|
-
case 'heading4':
|
|
276
|
-
return (
|
|
277
|
-
<h4 key={blockKey} id={block.id}>
|
|
278
|
-
{block.content}
|
|
279
|
-
</h4>
|
|
280
|
-
)
|
|
281
|
-
case 'list':
|
|
282
|
-
return block.items ? (
|
|
283
|
-
<ul key={blockKey} className="docs-list tight">
|
|
284
|
-
{block.items.map((item, i) => (
|
|
285
|
-
<li key={i}>
|
|
286
|
-
{item.tokens ? renderTokens(item.tokens, `${blockKey}-item-${i}`) : item.content}
|
|
287
|
-
</li>
|
|
288
|
-
))}
|
|
289
|
-
</ul>
|
|
290
|
-
) : null
|
|
291
|
-
case 'blockquote':
|
|
292
|
-
return (
|
|
293
|
-
<blockquote key={blockKey} className="docs-callout">
|
|
294
|
-
{block.tokens ? renderTokens(block.tokens, blockKey) : block.content}
|
|
295
|
-
</blockquote>
|
|
296
|
-
)
|
|
297
|
-
case 'divider':
|
|
298
|
-
return <Divider key={blockKey} className="docs-divider" opacity="12" />
|
|
299
|
-
case 'code':
|
|
300
|
-
return (
|
|
301
|
-
<CodeBlock
|
|
302
|
-
key={blockKey}
|
|
303
|
-
code={block.lines.join('\n')}
|
|
304
|
-
language={block.lang}
|
|
305
|
-
/>
|
|
306
|
-
)
|
|
307
|
-
case 'table':
|
|
308
|
-
return (
|
|
309
|
-
<div key={blockKey} className="kol-table-wrapper kol-table--simple">
|
|
310
|
-
<table className="kol-table">
|
|
311
|
-
<thead className="kol-table-thead">
|
|
312
|
-
<tr>
|
|
313
|
-
{block.headers.map((header, headerIndex) => (
|
|
314
|
-
<th key={headerIndex} className="kol-table-cell-title">{header}</th>
|
|
315
|
-
))}
|
|
316
|
-
</tr>
|
|
317
|
-
</thead>
|
|
318
|
-
<tbody>
|
|
319
|
-
{block.rows.map((row, rowIndex) => (
|
|
320
|
-
<tr key={rowIndex} className="kol-table-row">
|
|
321
|
-
{row.map((cell, cellIndex) => (
|
|
322
|
-
<td key={cellIndex} className="kol-table-cell-text">
|
|
323
|
-
{cell.tokens ? renderTokens(cell.tokens, `${blockKey}-row-${rowIndex}-cell-${cellIndex}`) : cell.content}
|
|
324
|
-
</td>
|
|
325
|
-
))}
|
|
326
|
-
</tr>
|
|
327
|
-
))}
|
|
328
|
-
</tbody>
|
|
329
|
-
</table>
|
|
330
|
-
</div>
|
|
331
|
-
)
|
|
332
|
-
default:
|
|
333
|
-
return null
|
|
334
|
-
}
|
|
335
|
-
})}
|
|
388
|
+
{introBlocks.filter(b => b.type !== 'heading1').map((block, index) =>
|
|
389
|
+
renderBlock(block, `intro-${block.type}-${index}`)
|
|
390
|
+
)}
|
|
336
391
|
|
|
392
|
+
{/* Sections through the packaged DocSection — the same anchored
|
|
393
|
+
* rule + h2 contract MDX pages compose with. */}
|
|
337
394
|
{sections.map(({ heading, id, blocks }) => (
|
|
338
|
-
<
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const blockKey = `${id}-${block.type}-${index}`
|
|
342
|
-
|
|
343
|
-
switch (block.type) {
|
|
344
|
-
case 'heading3':
|
|
345
|
-
return (
|
|
346
|
-
<h3 key={blockKey} id={block.id}>
|
|
347
|
-
{block.content}
|
|
348
|
-
</h3>
|
|
349
|
-
)
|
|
350
|
-
case 'heading4':
|
|
351
|
-
return (
|
|
352
|
-
<h4 key={blockKey} id={block.id}>
|
|
353
|
-
{block.content}
|
|
354
|
-
</h4>
|
|
355
|
-
)
|
|
356
|
-
case 'paragraph':
|
|
357
|
-
return (
|
|
358
|
-
<p key={blockKey}>
|
|
359
|
-
{block.tokens ? renderTokens(block.tokens, blockKey) : block.content}
|
|
360
|
-
</p>
|
|
361
|
-
)
|
|
362
|
-
case 'list': {
|
|
363
|
-
const listClass = block.ordered
|
|
364
|
-
? 'docs-list docs-list--ordered tight'
|
|
365
|
-
: 'docs-list tight'
|
|
366
|
-
const ListComponent = block.ordered ? 'ol' : 'ul'
|
|
367
|
-
return (
|
|
368
|
-
<ListComponent key={blockKey} className={listClass}>
|
|
369
|
-
{block.items.map((item, itemIndex) => (
|
|
370
|
-
<li key={itemIndex}>
|
|
371
|
-
{item.tokens ? renderTokens(item.tokens, `${blockKey}-item-${itemIndex}`) : item.content || item}
|
|
372
|
-
</li>
|
|
373
|
-
))}
|
|
374
|
-
</ListComponent>
|
|
375
|
-
)
|
|
376
|
-
}
|
|
377
|
-
case 'code':
|
|
378
|
-
return (
|
|
379
|
-
<CodeBlock
|
|
380
|
-
key={blockKey}
|
|
381
|
-
code={block.lines.join('\n')}
|
|
382
|
-
language={block.lang}
|
|
383
|
-
/>
|
|
384
|
-
)
|
|
385
|
-
case 'blockquote':
|
|
386
|
-
return (
|
|
387
|
-
<blockquote key={blockKey} className="docs-callout">
|
|
388
|
-
{block.tokens ? renderTokens(block.tokens, blockKey) : block.content}
|
|
389
|
-
</blockquote>
|
|
390
|
-
)
|
|
391
|
-
case 'divider':
|
|
392
|
-
return <Divider key={blockKey} className="docs-divider" opacity="12" />
|
|
393
|
-
case 'table':
|
|
394
|
-
return (
|
|
395
|
-
<div key={blockKey} className="kol-table-wrapper kol-table--simple">
|
|
396
|
-
<table className="kol-table">
|
|
397
|
-
<thead className="kol-table-thead">
|
|
398
|
-
<tr>
|
|
399
|
-
{block.headers.map((header, headerIndex) => (
|
|
400
|
-
<th key={headerIndex} className="kol-table-cell-title">{header}</th>
|
|
401
|
-
))}
|
|
402
|
-
</tr>
|
|
403
|
-
</thead>
|
|
404
|
-
<tbody>
|
|
405
|
-
{block.rows.map((row, rowIndex) => (
|
|
406
|
-
<tr key={rowIndex} className="kol-table-row">
|
|
407
|
-
{row.map((cell, cellIndex) => (
|
|
408
|
-
<td key={cellIndex} className="kol-table-cell-text">
|
|
409
|
-
{cell.tokens ? renderTokens(cell.tokens, `${blockKey}-row-${rowIndex}-cell-${cellIndex}`) : cell.content}
|
|
410
|
-
</td>
|
|
411
|
-
))}
|
|
412
|
-
</tr>
|
|
413
|
-
))}
|
|
414
|
-
</tbody>
|
|
415
|
-
</table>
|
|
416
|
-
</div>
|
|
417
|
-
)
|
|
418
|
-
default:
|
|
419
|
-
return null
|
|
420
|
-
}
|
|
421
|
-
})}
|
|
422
|
-
</section>
|
|
395
|
+
<DocSection key={id} id={id} title={heading}>
|
|
396
|
+
{blocks.map((block, index) => renderBlock(block, `${id}-${block.type}-${index}`))}
|
|
397
|
+
</DocSection>
|
|
423
398
|
))}
|
|
424
399
|
</DocsArticle>
|
|
425
400
|
)
|
|
@@ -14,12 +14,17 @@ import { Link } from 'react-router-dom'
|
|
|
14
14
|
* @param {Function} tagHref (tag) => href for `#hashtag` pills. Default
|
|
15
15
|
* keeps hashtags route-agnostic (`/docs?tag=…`);
|
|
16
16
|
* the reader passes a configured href in.
|
|
17
|
+
* @param {Function} onTagClick (tag) => void — when provided, hashtag pills
|
|
18
|
+
* render as buttons (tag mode) instead of Links;
|
|
19
|
+
* a consumer without a tag route stops shipping
|
|
20
|
+
* dead `/docs?tag=…` links (wave-4 parity).
|
|
17
21
|
*/
|
|
18
22
|
export const renderInlineTokens = (
|
|
19
23
|
tokens,
|
|
20
24
|
key = '',
|
|
21
25
|
resolveDocLink = null,
|
|
22
|
-
tagHref = (tag) => `/docs?tag=${encodeURIComponent(tag)}
|
|
26
|
+
tagHref = (tag) => `/docs?tag=${encodeURIComponent(tag)}`,
|
|
27
|
+
onTagClick = null
|
|
23
28
|
) => {
|
|
24
29
|
if (!Array.isArray(tokens)) return null
|
|
25
30
|
|
|
@@ -37,15 +42,19 @@ export const renderInlineTokens = (
|
|
|
37
42
|
return <em key={tokenKey}>{token.content}</em>
|
|
38
43
|
|
|
39
44
|
case 'code':
|
|
40
|
-
return <code key={tokenKey}>{token.content}</code>
|
|
45
|
+
return <code key={tokenKey} className="kol-doc-code-inline">{token.content}</code>
|
|
41
46
|
|
|
42
47
|
case 'link': {
|
|
48
|
+
/* One link idiom across doc surfaces (wave-4 retype): the anchor
|
|
49
|
+
* treatment from showcase mdx-components.jsx — colorless per the
|
|
50
|
+
* link law, emphasis + quiet underline. */
|
|
51
|
+
const linkCls = 'text-emphasis underline decoration-fg-16 underline-offset-2 hover:decoration-current'
|
|
43
52
|
// For .md links, try to resolve to an app route
|
|
44
53
|
if (resolveDocLink && token.url.includes('.md')) {
|
|
45
54
|
const route = resolveDocLink(token.url)
|
|
46
55
|
if (route) {
|
|
47
56
|
return (
|
|
48
|
-
<Link key={tokenKey} to={route} className=
|
|
57
|
+
<Link key={tokenKey} to={route} className={linkCls}>
|
|
49
58
|
{token.text}
|
|
50
59
|
</Link>
|
|
51
60
|
)
|
|
@@ -54,7 +63,7 @@ export const renderInlineTokens = (
|
|
|
54
63
|
return token.text
|
|
55
64
|
}
|
|
56
65
|
return (
|
|
57
|
-
<a key={tokenKey} href={token.url} className=
|
|
66
|
+
<a key={tokenKey} href={token.url} className={linkCls}>
|
|
58
67
|
{token.text}
|
|
59
68
|
</a>
|
|
60
69
|
)
|
|
@@ -78,16 +87,26 @@ export const renderInlineTokens = (
|
|
|
78
87
|
</code>
|
|
79
88
|
)
|
|
80
89
|
|
|
81
|
-
case 'hashtag':
|
|
90
|
+
case 'hashtag': {
|
|
91
|
+
const pillCls = 'inline-tag-pill kol-helper-12 bg-fg-08 text-strong'
|
|
92
|
+
if (onTagClick) {
|
|
93
|
+
return (
|
|
94
|
+
<button
|
|
95
|
+
key={tokenKey}
|
|
96
|
+
type="button"
|
|
97
|
+
className={pillCls}
|
|
98
|
+
onClick={() => onTagClick(token.tag)}
|
|
99
|
+
>
|
|
100
|
+
#{token.tag}
|
|
101
|
+
</button>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
82
104
|
return (
|
|
83
|
-
<Link
|
|
84
|
-
key={tokenKey}
|
|
85
|
-
to={tagHref(token.tag)}
|
|
86
|
-
className="inline-tag-pill kol-helper-12 bg-fg-08 text-strong"
|
|
87
|
-
>
|
|
105
|
+
<Link key={tokenKey} to={tagHref(token.tag)} className={pillCls}>
|
|
88
106
|
#{token.tag}
|
|
89
107
|
</Link>
|
|
90
108
|
)
|
|
109
|
+
}
|
|
91
110
|
|
|
92
111
|
default:
|
|
93
112
|
return null
|
|
@@ -20,6 +20,19 @@ export function buildInventory(modules) {
|
|
|
20
20
|
const lines = raw.split(/\r?\n/)
|
|
21
21
|
const headingLine = lines.find((line) => line.startsWith('# ')) ?? ''
|
|
22
22
|
const title = headingLine.replace(/^#\s+/, '').trim()
|
|
23
|
+
/* Section headings (## / ###) ride the inventory so search can match a
|
|
24
|
+
* doc by its CONTENT, not just title/tags — the workshop reference
|
|
25
|
+
* behavior (matchSearchItems already takes `headings`; consumers shipped
|
|
26
|
+
* it empty because nothing extracted them — wave-4 parity, 2026-07-30). */
|
|
27
|
+
const headings = []
|
|
28
|
+
let inFence = false
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
if (/^\s*```/.test(line)) { inFence = !inFence; continue }
|
|
31
|
+
if (!inFence && /^#{2,3}\s/.test(line)) {
|
|
32
|
+
const text = line.replace(/^#{2,3}\s+/, '').trim()
|
|
33
|
+
if (text) headings.push(text)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
23
36
|
const metadata = parseFrontmatter(raw)
|
|
24
37
|
const filename = normalisedPath.split('/').pop() ?? ''
|
|
25
38
|
const parentFolder = normalisedPath.split('/').slice(-2, -1)[0] ?? ''
|
|
@@ -33,7 +46,7 @@ export function buildInventory(modules) {
|
|
|
33
46
|
.replace(/^Design System\s*[-–—:]\s*/i, '')
|
|
34
47
|
.replace(/\bCheat Sheet\b/i, 'Quicklook')
|
|
35
48
|
|
|
36
|
-
return { id, file: normalisedPath, title: cleanTitle, metadata, parentFolder }
|
|
49
|
+
return { id, file: normalisedPath, title: cleanTitle, metadata, headings, parentFolder }
|
|
37
50
|
})
|
|
38
51
|
|
|
39
52
|
/* Collision safety (2026-07-30): filename-only ids collide across folders in
|