@kolkrabbi/kol-workshop 0.3.4 → 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",
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"d3": "^7.9.0",
|
|
16
16
|
"@kolkrabbi/kol-brand": "^0.1.2",
|
|
17
|
-
"@kolkrabbi/kol-icons": "0.8.11",
|
|
18
17
|
"@kolkrabbi/kol-component": "0.14.1",
|
|
19
|
-
"@kolkrabbi/kol-
|
|
20
|
-
"@kolkrabbi/kol-
|
|
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",
|
|
@@ -23,7 +23,7 @@ const SidebarSection = ({ sectionKey, label, collapsedSections, toggleSection, c
|
|
|
23
23
|
</div>
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
-
const DocReaderSidebar = ({ toc, allTags, docId, docsIndexHref, componentsHref, docFilePath }) => {
|
|
26
|
+
const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, componentsHref, docFilePath }) => {
|
|
27
27
|
const navigate = useNavigate()
|
|
28
28
|
const { openTagMode } = useTagMode()
|
|
29
29
|
const [collapsedSections, setCollapsedSections] = useState({})
|
|
@@ -40,6 +40,24 @@ const DocReaderSidebar = ({ toc, allTags, docId, docsIndexHref, componentsHref,
|
|
|
40
40
|
<DocsToc toc={toc} />
|
|
41
41
|
</SidebarSection>
|
|
42
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
|
+
|
|
43
61
|
<SidebarSection
|
|
44
62
|
sectionKey="actions"
|
|
45
63
|
label="Quick actions"
|
|
@@ -140,6 +158,7 @@ const DocumentationReader = ({
|
|
|
140
158
|
|
|
141
159
|
const { docId } = useParams()
|
|
142
160
|
const setTocContent = useContext(ShellTocContext)
|
|
161
|
+
const { openTagMode } = useTagMode()
|
|
143
162
|
|
|
144
163
|
// Build a Set of known doc IDs for fast lookup
|
|
145
164
|
const knownDocIds = useMemo(
|
|
@@ -163,9 +182,11 @@ const DocumentationReader = ({
|
|
|
163
182
|
return null
|
|
164
183
|
}
|
|
165
184
|
|
|
166
|
-
// 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.
|
|
167
188
|
const renderTokens = (tokens, tokenKey) =>
|
|
168
|
-
renderInlineTokens(tokens, tokenKey, resolveDocLink, tagHref)
|
|
189
|
+
renderInlineTokens(tokens, tokenKey, resolveDocLink, tagHref, openTagMode)
|
|
169
190
|
|
|
170
191
|
/* ONE block renderer for intro + section content (wave-4 retype, 2026-07-30
|
|
171
192
|
* — the two switches had already drifted). Every element is typed through
|
|
@@ -295,6 +316,35 @@ const DocumentationReader = ({
|
|
|
295
316
|
return h1Block?.content || null
|
|
296
317
|
}, [introBlocks])
|
|
297
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
|
+
|
|
298
348
|
/* The reader is usable OUTSIDE a ShellLayout (a plain page, a lobby route):
|
|
299
349
|
* ShellTocContext then defaults to null, so calling it unguarded threw.
|
|
300
350
|
* No shell → no right rail, and the reader just renders its article. */
|
|
@@ -305,6 +355,7 @@ const DocumentationReader = ({
|
|
|
305
355
|
key={docId}
|
|
306
356
|
toc={toc}
|
|
307
357
|
allTags={allTags}
|
|
358
|
+
related={related}
|
|
308
359
|
docId={docId}
|
|
309
360
|
docsIndexHref={docsIndex}
|
|
310
361
|
componentsHref={components}
|
|
@@ -312,7 +363,7 @@ const DocumentationReader = ({
|
|
|
312
363
|
/>
|
|
313
364
|
)
|
|
314
365
|
return () => setTocContent(null)
|
|
315
|
-
}, [setTocContent, docId, toc, allTags, docsIndex, components, docFilePath])
|
|
366
|
+
}, [setTocContent, docId, toc, allTags, related, docsIndex, components, docFilePath])
|
|
316
367
|
|
|
317
368
|
if (!doc) {
|
|
318
369
|
return (
|
|
@@ -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
|
|
|
@@ -82,16 +87,26 @@ export const renderInlineTokens = (
|
|
|
82
87
|
</code>
|
|
83
88
|
)
|
|
84
89
|
|
|
85
|
-
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
|
+
}
|
|
86
104
|
return (
|
|
87
|
-
<Link
|
|
88
|
-
key={tokenKey}
|
|
89
|
-
to={tagHref(token.tag)}
|
|
90
|
-
className="inline-tag-pill kol-helper-12 bg-fg-08 text-strong"
|
|
91
|
-
>
|
|
105
|
+
<Link key={tokenKey} to={tagHref(token.tag)} className={pillCls}>
|
|
92
106
|
#{token.tag}
|
|
93
107
|
</Link>
|
|
94
108
|
)
|
|
109
|
+
}
|
|
95
110
|
|
|
96
111
|
default:
|
|
97
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
|