@kolkrabbi/kol-workshop 0.5.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/compositions/WorkshopDefaultSidebar.jsx +7 -31
- package/src/compositions/WorkshopSidebar.jsx +35 -60
- package/src/docs/DocKit.jsx +21 -4
- package/src/docs/DocsFrontmatter.jsx +86 -21
- package/src/docs/DocumentationReader.jsx +68 -79
- package/src/engine/doc-helpers.js +31 -0
- package/src/engine/index.js +2 -1
- package/src/engine/parse-markdown.js +5 -1
- package/src/shell/RailRow.jsx +85 -0
- package/src/shell/RailSection.jsx +178 -0
- package/src/shell/ShellLayout.jsx +181 -25
- package/src/shell/ShellSidebar.jsx +50 -105
- package/src/shell/index.js +7 -0
- package/src/tags/TagModeContext.jsx +52 -6
- package/src/tags/TagModeOverlay.jsx +74 -115
- package/src/tags/TagPath.jsx +24 -0
- package/src/tags/index.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-workshop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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-component": "0.19.0",
|
|
18
|
+
"@kolkrabbi/kol-framework": "0.11.1",
|
|
17
19
|
"@kolkrabbi/kol-icons": "0.8.11",
|
|
18
|
-
"@kolkrabbi/kol-
|
|
19
|
-
"@kolkrabbi/kol-theme": "0.13.3",
|
|
20
|
-
"@kolkrabbi/kol-component": "0.14.3"
|
|
20
|
+
"@kolkrabbi/kol-theme": "0.18.0"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"react": "^18.3.0 || ^19.0.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useLocation, Link } from 'react-router-dom'
|
|
2
2
|
import { Icon } from '@kolkrabbi/kol-component'
|
|
3
|
+
import { RailRow } from '../shell'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* WorkshopDefaultSidebar — EXAMPLE composition. The contextual right rail:
|
|
@@ -28,17 +29,9 @@ const WorkshopDefaultSidebar = ({ routes = [], basePath = '/workshop' }) => {
|
|
|
28
29
|
{siblings.length > 1 && (
|
|
29
30
|
<div>
|
|
30
31
|
<div className="shell-sidebar-label kol-doc-eyebrow">{parentRoute.label}</div>
|
|
31
|
-
<nav className="
|
|
32
|
+
<nav className="shell-nav-items">
|
|
32
33
|
{siblings.map(child => (
|
|
33
|
-
<
|
|
34
|
-
key={child.id}
|
|
35
|
-
to={`${basePath}/${child.path}`}
|
|
36
|
-
className={`shell-nav-item block kol-mono-14 ${
|
|
37
|
-
location.pathname === `${basePath}/${child.path}` ? 'text-emphasis' : 'text-body'
|
|
38
|
-
}`}
|
|
39
|
-
>
|
|
40
|
-
{child.label}
|
|
41
|
-
</Link>
|
|
34
|
+
<RailRow key={child.id} to={`${basePath}/${child.path}`}>{child.label}</RailRow>
|
|
42
35
|
))}
|
|
43
36
|
</nav>
|
|
44
37
|
</div>
|
|
@@ -49,16 +42,10 @@ const WorkshopDefaultSidebar = ({ routes = [], basePath = '/workshop' }) => {
|
|
|
49
42
|
<div className="shell-sidebar-label kol-doc-eyebrow">Repository</div>
|
|
50
43
|
<div>
|
|
51
44
|
{links.live && (
|
|
52
|
-
<
|
|
53
|
-
<Icon name="external-link" size={14} />
|
|
54
|
-
Live site
|
|
55
|
-
</a>
|
|
45
|
+
<RailRow href={links.live} icon={<Icon name="external-link" size={14} />}>Live site</RailRow>
|
|
56
46
|
)}
|
|
57
47
|
{links.repo && (
|
|
58
|
-
<
|
|
59
|
-
<Icon name="external-link" size={14} />
|
|
60
|
-
GitHub
|
|
61
|
-
</a>
|
|
48
|
+
<RailRow href={links.repo} icon={<Icon name="external-link" size={14} />}>GitHub</RailRow>
|
|
62
49
|
)}
|
|
63
50
|
</div>
|
|
64
51
|
</div>
|
|
@@ -67,19 +54,8 @@ const WorkshopDefaultSidebar = ({ routes = [], basePath = '/workshop' }) => {
|
|
|
67
54
|
<div>
|
|
68
55
|
<div className="shell-sidebar-label kol-doc-eyebrow">Quick actions</div>
|
|
69
56
|
<div>
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
Workshop home
|
|
73
|
-
</Link>
|
|
74
|
-
<button
|
|
75
|
-
type="button"
|
|
76
|
-
className="shell-nav-item flex items-center gap-2 kol-mono-14 text-body transition-colors hover:text-emphasis"
|
|
77
|
-
onClick={() => navigator.clipboard.writeText(window.location.pathname)}
|
|
78
|
-
title="Copy page path to clipboard"
|
|
79
|
-
>
|
|
80
|
-
<Icon name="copy" size={14} />
|
|
81
|
-
Copy path
|
|
82
|
-
</button>
|
|
57
|
+
<RailRow to={basePath} icon={<Icon name="layout" size={14} />}>Workshop home</RailRow>
|
|
58
|
+
<RailRow onClick={() => navigator.clipboard.writeText(window.location.pathname)} icon={<Icon name="copy" size={14} />}>Copy path</RailRow>
|
|
83
59
|
</div>
|
|
84
60
|
</div>
|
|
85
61
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'react'
|
|
2
2
|
import { Link, useLocation } from 'react-router-dom'
|
|
3
|
-
import { ShellSidebar } from '../shell'
|
|
4
|
-
import { Icon
|
|
3
|
+
import { ShellSidebar, RailSection, RailRow } from '../shell'
|
|
4
|
+
import { Icon } from '@kolkrabbi/kol-component'
|
|
5
5
|
import {
|
|
6
6
|
extractDocNumber,
|
|
7
7
|
cleanTitle,
|
|
@@ -61,76 +61,51 @@ const DocsSidebar = ({ inventory = [], docHref, basePath, onNavigate, collapsed,
|
|
|
61
61
|
|
|
62
62
|
return (
|
|
63
63
|
<div className="space-y-4">
|
|
64
|
-
{/*
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
aria-label={collapsed ? 'Expand Documentation' : 'Collapse Documentation'}
|
|
77
|
-
aria-expanded={!collapsed}
|
|
78
|
-
className="flex items-center justify-center"
|
|
79
|
-
style={{ height: '16.5px', marginBottom: '8px' }}
|
|
80
|
-
>
|
|
81
|
-
<Icon
|
|
82
|
-
name="chevron-down"
|
|
83
|
-
size={10}
|
|
84
|
-
className={`stroke-[2.5] transition-transform ${collapsed ? '' : 'rotate-180'}`}
|
|
85
|
-
/>
|
|
86
|
-
</button>
|
|
87
|
-
</Tooltip>
|
|
88
|
-
</div>
|
|
89
|
-
|
|
90
|
-
{!collapsed && <div className="space-y-4">
|
|
64
|
+
{/* L1 from RailSection — the same rung and the same box as every other
|
|
65
|
+
* rail header. No count: the eyebrow names the material, the groups
|
|
66
|
+
* inside carry the tally (user ruling 2026-08-01). */}
|
|
67
|
+
<RailSection
|
|
68
|
+
level={1}
|
|
69
|
+
label="Documentation"
|
|
70
|
+
to={docHref()}
|
|
71
|
+
collapsed={collapsed}
|
|
72
|
+
onToggle={onToggle}
|
|
73
|
+
onNavigate={onNavigate}
|
|
74
|
+
>
|
|
75
|
+
<div className="space-y-4">
|
|
91
76
|
{Object.entries(groupedDocs)
|
|
92
77
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
93
78
|
.map(([major, docs]) => {
|
|
94
79
|
const isCollapsed = collapsedGroups[major]
|
|
95
80
|
return (
|
|
96
81
|
<div key={major} className="shell-nav-group">
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
82
|
+
<RailSection
|
|
83
|
+
level={2}
|
|
84
|
+
label={categoryLabels[major] || 'Other'}
|
|
85
|
+
count={docs.length}
|
|
86
|
+
collapsed={!!isCollapsed}
|
|
87
|
+
onToggle={() => toggleGroup(major)}
|
|
88
|
+
icon={Icon}
|
|
101
89
|
>
|
|
102
|
-
<span className="flex items-center gap-2">
|
|
103
|
-
<Icon
|
|
104
|
-
name="chevron-right"
|
|
105
|
-
size={12}
|
|
106
|
-
className={`h-3 w-3 transition-transform ${isCollapsed ? '' : 'rotate-90'}`}
|
|
107
|
-
/>
|
|
108
|
-
{categoryLabels[major] || 'Other'}
|
|
109
|
-
</span>
|
|
110
|
-
<span className="kol-mono-14 text-subtle">({docs.length})</span>
|
|
111
|
-
</button>
|
|
112
|
-
{!isCollapsed && (
|
|
113
90
|
<div className="shell-nav-items">
|
|
114
|
-
{docs.map((d) =>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
</Link>
|
|
126
|
-
)
|
|
127
|
-
})}
|
|
91
|
+
{docs.map((d) => (
|
|
92
|
+
<RailRow
|
|
93
|
+
key={d.id}
|
|
94
|
+
to={docHref(d.id)}
|
|
95
|
+
active={d.id === activeDocId}
|
|
96
|
+
trailing={extractDocNumber(d.id)}
|
|
97
|
+
onNavigate={onNavigate}
|
|
98
|
+
>
|
|
99
|
+
{cleanTitle(d.title, d.id)}
|
|
100
|
+
</RailRow>
|
|
101
|
+
))}
|
|
128
102
|
</div>
|
|
129
|
-
|
|
103
|
+
</RailSection>
|
|
130
104
|
</div>
|
|
131
105
|
)
|
|
132
106
|
})}
|
|
133
|
-
|
|
107
|
+
</div>
|
|
108
|
+
</RailSection>
|
|
134
109
|
</div>
|
|
135
110
|
)
|
|
136
111
|
}
|
package/src/docs/DocKit.jsx
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { Table } from '@kolkrabbi/kol-component'
|
|
14
|
+
import { renderInlineTokens } from './render-tokens.jsx'
|
|
15
|
+
import { processInlineMarkdown } from '../engine/parse-markdown.js'
|
|
14
16
|
|
|
15
17
|
export function DocHeader({ eyebrow, title, lede, children }) {
|
|
16
18
|
return (
|
|
@@ -38,11 +40,26 @@ export function DocSection({ id, title, lede, children }) {
|
|
|
38
40
|
* classes through Table's per-column seams, doc chrome via the .kol-doc-table
|
|
39
41
|
* class on Table's wrapper (the descendant rules out-specify Table's cell
|
|
40
42
|
* defaults). Renders UNFRAMED (flush minimal — user ruling: no border). */
|
|
43
|
+
/* CELLS PARSE INLINE MARKDOWN (user ruling 2026-08-01). `type` and `desc`
|
|
44
|
+
* carry backticked code — "`primary` is the grey fill" — and the cells printed
|
|
45
|
+
* the string raw, so every prop description showed its own backticks instead of
|
|
46
|
+
* a code chip. The vault's markdown got this right the whole time because it
|
|
47
|
+
* runs through `renderInlineTokens`; the props table simply never called it.
|
|
48
|
+
* `Table` has supported `column.render` all along (Table.jsx:35). Nothing new
|
|
49
|
+
* was written for this — two existing pieces that had never been introduced. */
|
|
50
|
+
const inline = (v, key) =>
|
|
51
|
+
typeof v === 'string' && v
|
|
52
|
+
? renderInlineTokens(processInlineMarkdown(v), key)
|
|
53
|
+
: v ?? '—'
|
|
54
|
+
|
|
41
55
|
const PROPS_COLUMNS = [
|
|
42
|
-
{ accessor: 'prop', header: 'Prop', className: 'kol-doc-table-value text-emphasis' },
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
{ accessor: 'prop', header: 'Prop', sortable: true, className: 'kol-doc-table-value text-emphasis' },
|
|
57
|
+
/* type + default are TOKENS, so they wear the token chip (user ruling
|
|
58
|
+
* 2026-08-01) — .kol-table-token, the same chip the tables already use.
|
|
59
|
+
* They rendered as bare text while every neighbouring surface chipped them. */
|
|
60
|
+
{ accessor: 'type', header: 'Type', className: 'kol-doc-table-value kol-doc-table-value--wrap text-meta', render: (r) => (r.type ? <code className="kol-table-token">{r.type}</code> : '—') },
|
|
61
|
+
{ accessor: 'def', header: 'Default', className: 'kol-doc-table-value text-meta', render: (r) => (r.def ? <code className="kol-table-token">{r.def}</code> : '—') },
|
|
62
|
+
{ accessor: 'desc', header: 'Description', render: (r) => inline(r.desc, `${r.prop}-desc`) },
|
|
46
63
|
]
|
|
47
64
|
|
|
48
65
|
export function DocTable({ rows }) {
|
|
@@ -1,24 +1,80 @@
|
|
|
1
1
|
import { Icon, Tag } from '@kolkrabbi/kol-component'
|
|
2
|
-
import { getTagColor } from '../engine'
|
|
3
2
|
import { useTagMode } from '../tags'
|
|
3
|
+
import TagPath from '../tags/TagPath.jsx'
|
|
4
4
|
|
|
5
|
+
/* EVERY field carries an icon (user ruling 2026-08-01). Ten of these were
|
|
6
|
+
* hard-coded `null`, so two rows had a glyph and the rest sat at a ragged left
|
|
7
|
+
* edge — a column that is sometimes iconned is worse than one that never is.
|
|
8
|
+
* Every name below is verified present in kol-icon-set-v1. */
|
|
5
9
|
const FIELD_ICONS = {
|
|
6
10
|
file: 'file',
|
|
7
|
-
title:
|
|
8
|
-
type: '
|
|
11
|
+
title: 'type',
|
|
12
|
+
type: 'library',
|
|
9
13
|
status: 'check',
|
|
10
|
-
updated:
|
|
11
|
-
created:
|
|
12
|
-
verified:
|
|
13
|
-
description:
|
|
14
|
-
audience:
|
|
15
|
-
aliases:
|
|
14
|
+
updated: 'refresh',
|
|
15
|
+
created: 'plus',
|
|
16
|
+
verified: 'shield-check',
|
|
17
|
+
description: 'message',
|
|
18
|
+
audience: 'user',
|
|
19
|
+
aliases: 'repeat',
|
|
20
|
+
sources: 'code',
|
|
21
|
+
superseded_by: 'arrow-right',
|
|
22
|
+
drift: 'alert-triangle',
|
|
16
23
|
version: 'hash-01',
|
|
24
|
+
tags: 'hash-02',
|
|
25
|
+
/* 2026-08-01: these three rendered iconless while the rule above promised
|
|
26
|
+
* every field carries one — the exact ragged edge that rule exists to stop.
|
|
27
|
+
* All verified present in kol-icon-set-v1. */
|
|
28
|
+
id: 'hash-01',
|
|
29
|
+
reuses: 'repeat',
|
|
30
|
+
slug: 'external-link',
|
|
31
|
+
/* provenance — folded in from the retired MetaRows panel 2026-08-01 */
|
|
32
|
+
source: 'code',
|
|
33
|
+
imported_from: 'arrow-right',
|
|
34
|
+
type_styles: 'type',
|
|
35
|
+
classes: 'library',
|
|
36
|
+
tokens: 'hash-01',
|
|
37
|
+
composes: 'component-01',
|
|
38
|
+
in_sets: 'grid',
|
|
39
|
+
used_in: 'layers',
|
|
17
40
|
/* legacy dialect (workshop-sample docs) — kept so those still render */
|
|
18
|
-
date:
|
|
41
|
+
date: 'journal',
|
|
19
42
|
category: 'folder',
|
|
20
|
-
|
|
21
|
-
|
|
43
|
+
modified: 'edit',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* AUTHORED labels, not the raw key (user ruling 2026-08-01). The row printed
|
|
47
|
+
* `{key}` straight from the frontmatter, so the column read lowercase and
|
|
48
|
+
* snake_cased. KOL forbids `text-transform` on a component — casing is a
|
|
49
|
+
* content decision — so the strings are written here in the case they render.
|
|
50
|
+
* A key with no entry falls back to its own name rather than vanishing. */
|
|
51
|
+
const FIELD_LABELS = {
|
|
52
|
+
file: 'File',
|
|
53
|
+
title: 'Title',
|
|
54
|
+
type: 'Type',
|
|
55
|
+
status: 'Status',
|
|
56
|
+
updated: 'Updated',
|
|
57
|
+
created: 'Created',
|
|
58
|
+
verified: 'Verified',
|
|
59
|
+
description: 'Description',
|
|
60
|
+
audience: 'Audience',
|
|
61
|
+
aliases: 'Aliases',
|
|
62
|
+
sources: 'Sources',
|
|
63
|
+
superseded_by: 'Superseded by',
|
|
64
|
+
drift: 'Drift',
|
|
65
|
+
version: 'Version',
|
|
66
|
+
tags: 'Tags',
|
|
67
|
+
source: 'Source',
|
|
68
|
+
imported_from: 'Imported from',
|
|
69
|
+
type_styles: 'Type styles',
|
|
70
|
+
classes: 'Classes',
|
|
71
|
+
tokens: 'Tokens',
|
|
72
|
+
composes: 'Composes',
|
|
73
|
+
in_sets: 'In sets',
|
|
74
|
+
used_in: 'Used in',
|
|
75
|
+
date: 'Date',
|
|
76
|
+
category: 'Category',
|
|
77
|
+
modified: 'Modified',
|
|
22
78
|
}
|
|
23
79
|
|
|
24
80
|
/* Reading ORDER, not an allowlist. The kol-docs contract first
|
|
@@ -37,7 +93,7 @@ const FIELD_ICONS = {
|
|
|
37
93
|
* so printing the raw wikilinks here would be the same thing twice. */
|
|
38
94
|
const FIELD_ORDER = [
|
|
39
95
|
'title', 'type', 'status', 'updated', 'tags',
|
|
40
|
-
'description', 'aliases',
|
|
96
|
+
'description', 'aliases', 'sources',
|
|
41
97
|
'created', 'verified', 'audience', 'superseded_by', 'drift',
|
|
42
98
|
'category', 'date', 'modified', 'version',
|
|
43
99
|
]
|
|
@@ -85,19 +141,28 @@ const DocsFrontmatter = ({ metadata, docId }) => {
|
|
|
85
141
|
<div key={key} className="docs-frontmatter-row">
|
|
86
142
|
<span className="docs-frontmatter-key kol-helper-12 text-meta">
|
|
87
143
|
{icon && <Icon name={icon} size={14} />}
|
|
88
|
-
{key}
|
|
144
|
+
{FIELD_LABELS[key] ?? key}
|
|
89
145
|
</span>
|
|
90
146
|
<span className="kol-mono-12 text-strong">
|
|
91
147
|
{key === 'tags' && Array.isArray(value) ? (
|
|
148
|
+
/* `naked`, the SAME Tag rendering the rail's own tag list uses
|
|
149
|
+
* (DocReaderSidebar). This was the default filled variant, so
|
|
150
|
+
* the one concept rendered as a solid pill here and as plain
|
|
151
|
+
* coloured text six inches to the right.
|
|
152
|
+
*
|
|
153
|
+
* NO `color` (user ruling 2026-08-01): passing one swaps the
|
|
154
|
+
* base class from `tag-control` to `tag tag--{color}`, and
|
|
155
|
+
* only `tag-control` has a `:hover` rule — so a coloured Tag
|
|
156
|
+
* silently loses its interaction state. Right component first;
|
|
157
|
+
* colour is its own decision, later.
|
|
158
|
+
* `size` is omitted: `sm` is the default and the only size. */
|
|
92
159
|
<span className="flex flex-wrap gap-1.5">
|
|
93
160
|
{value.map((tag) => (
|
|
94
|
-
<Tag
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
>
|
|
100
|
-
{tag}
|
|
161
|
+
<Tag key={tag} onClick={() => openTagMode(tag)}>
|
|
162
|
+
{/* the path renderer, not the raw string — namespace dims,
|
|
163
|
+
* leaf carries. Shared with the tag overlay so the two
|
|
164
|
+
* surfaces cannot spell a tag two ways. */}
|
|
165
|
+
<TagPath tag={tag} />
|
|
101
166
|
</Tag>
|
|
102
167
|
))}
|
|
103
168
|
</span>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useContext, useEffect, useMemo, useState } from 'react'
|
|
2
2
|
import { useParams, Link, useNavigate } from 'react-router-dom'
|
|
3
3
|
import { CodeBlock, Divider, DocsToc, Icon, Table, Tag } from '@kolkrabbi/kol-component'
|
|
4
|
-
import { ShellTocContext } from '../shell'
|
|
4
|
+
import { ShellTocContext, RailSection, RailRow } from '../shell'
|
|
5
5
|
import { useTagMode } from '../tags'
|
|
6
|
-
import { parseDocsMarkdown, isIndexFile
|
|
6
|
+
import { parseDocsMarkdown, isIndexFile } from '../engine'
|
|
7
7
|
import DocsHeader from './DocsHeader.jsx'
|
|
8
8
|
import DocsArticle from './DocsArticle.jsx'
|
|
9
9
|
import DocsFrontmatter from './DocsFrontmatter.jsx'
|
|
@@ -16,35 +16,28 @@ import { renderInlineTokens } from './render-tokens.jsx'
|
|
|
16
16
|
* two rails are one system and a reader shouldn't have to learn each side
|
|
17
17
|
* separately.
|
|
18
18
|
*
|
|
19
|
-
* These were bare text
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* These were bare text with no count and no hover. They carry both now, and
|
|
20
|
+
* the whole row is the toggle — with no chevron drawn at L1 (user ruling
|
|
21
|
+
* 2026-08-01): the section collapses and expands, the affordance just isn't an
|
|
22
|
+
* icon.
|
|
23
|
+
*
|
|
24
|
+
* It is a thin wrapper over RailSection, which owns the rung: the class, the
|
|
25
|
+
* box, and WHERE THE COUNT SITS. This header used to hand-write all three and
|
|
26
|
+
* borrowed `.shell-nav-group-header` + `.shell-sidebar-label` to do it, which
|
|
27
|
+
* is how the two rails ended up printing their counts on different rows.
|
|
23
28
|
*/
|
|
24
|
-
const SidebarSection = ({ sectionKey, label, count, collapsedSections, toggleSection, children }) =>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
name="chevron-right"
|
|
37
|
-
size={12}
|
|
38
|
-
className={`transition-transform ${isOpen ? 'rotate-90' : ''}`}
|
|
39
|
-
/>
|
|
40
|
-
{label}
|
|
41
|
-
</span>
|
|
42
|
-
{count > 0 && <span className="kol-mono-14 text-subtle">({count})</span>}
|
|
43
|
-
</button>
|
|
44
|
-
{isOpen && children}
|
|
45
|
-
</div>
|
|
46
|
-
)
|
|
47
|
-
}
|
|
29
|
+
const SidebarSection = ({ sectionKey, label, count, collapsedSections, toggleSection, children }) => (
|
|
30
|
+
<RailSection
|
|
31
|
+
level={2}
|
|
32
|
+
label={label}
|
|
33
|
+
count={count > 0 ? count : undefined}
|
|
34
|
+
collapsed={!!collapsedSections[sectionKey]}
|
|
35
|
+
onToggle={() => toggleSection(sectionKey)}
|
|
36
|
+
icon={Icon}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</RailSection>
|
|
40
|
+
)
|
|
48
41
|
|
|
49
42
|
const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, componentsHref, docFilePath }) => {
|
|
50
43
|
const navigate = useNavigate()
|
|
@@ -53,15 +46,21 @@ const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, compone
|
|
|
53
46
|
const toggleSection = (key) => setCollapsedSections(prev => ({ ...prev, [key]: !prev[key] }))
|
|
54
47
|
|
|
55
48
|
return (
|
|
56
|
-
|
|
49
|
+
/* THE SAME LADDER AS THE LEFT RAIL (user ruling 2026-08-01). One L1 eyebrow
|
|
50
|
+
* naming the body, then L2 groups that collapse and carry a chevron + a
|
|
51
|
+
* count. These four sections used to be four bare L1 eyebrows stacked with
|
|
52
|
+
* no parent and no chevron — the left rail's shape stated differently on
|
|
53
|
+
* the other side of the page. */
|
|
54
|
+
<RailSection level={1} label="This page">
|
|
55
|
+
<div className="space-y-6">
|
|
57
56
|
<SidebarSection
|
|
58
57
|
sectionKey="toc"
|
|
59
|
-
label="
|
|
58
|
+
label="Contents"
|
|
60
59
|
count={toc?.length}
|
|
61
60
|
collapsedSections={collapsedSections}
|
|
62
61
|
toggleSection={toggleSection}
|
|
63
62
|
>
|
|
64
|
-
<DocsToc toc={toc} />
|
|
63
|
+
<DocsToc toc={toc} root="#main" />
|
|
65
64
|
</SidebarSection>
|
|
66
65
|
|
|
67
66
|
{related.length > 0 && (
|
|
@@ -75,9 +74,7 @@ const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, compone
|
|
|
75
74
|
{/* ONE rail voice (user law): rows = the shell-nav-item idiom. */}
|
|
76
75
|
<nav className="flex flex-col">
|
|
77
76
|
{related.map((r) => (
|
|
78
|
-
<
|
|
79
|
-
{r.label}
|
|
80
|
-
</Link>
|
|
77
|
+
<RailRow key={r.id} to={r.href}>{r.label}</RailRow>
|
|
81
78
|
))}
|
|
82
79
|
</nav>
|
|
83
80
|
</SidebarSection>
|
|
@@ -94,49 +91,38 @@ const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, compone
|
|
|
94
91
|
* so this rail carried three different row idioms — under the
|
|
95
92
|
* "ONE rail voice" comment above. */}
|
|
96
93
|
<div>
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
>
|
|
102
|
-
<Icon name="arrow-left" size={14} />
|
|
103
|
-
Back
|
|
104
|
-
</button>
|
|
105
|
-
<Link to={docsIndexHref} className="shell-nav-item flex items-center gap-2 kol-mono-14 text-body transition-colors hover:text-emphasis">
|
|
106
|
-
<Icon name="book-open" size={14} />
|
|
107
|
-
All documentation
|
|
108
|
-
</Link>
|
|
109
|
-
<Link to={componentsHref} className="shell-nav-item flex items-center gap-2 kol-mono-14 text-body transition-colors hover:text-emphasis">
|
|
110
|
-
<Icon name="grid" size={14} />
|
|
111
|
-
View components
|
|
112
|
-
</Link>
|
|
113
|
-
<button
|
|
114
|
-
type="button"
|
|
115
|
-
className="shell-nav-item flex items-center gap-2 kol-mono-14 text-body transition-colors hover:text-emphasis"
|
|
116
|
-
onClick={() => navigator.clipboard.writeText(docFilePath(docId))}
|
|
117
|
-
title="Copy file path to clipboard"
|
|
118
|
-
>
|
|
119
|
-
<Icon name="copy" size={14} />
|
|
120
|
-
Copy path
|
|
121
|
-
</button>
|
|
94
|
+
<RailRow onClick={() => navigate(-1)} icon={<Icon name="arrow-left" size={14} />}>Back</RailRow>
|
|
95
|
+
<RailRow to={docsIndexHref} icon={<Icon name="book-open" size={14} />}>All documentation</RailRow>
|
|
96
|
+
<RailRow to={componentsHref} icon={<Icon name="grid" size={14} />}>View components</RailRow>
|
|
97
|
+
<RailRow onClick={() => navigator.clipboard.writeText(docFilePath(docId))} icon={<Icon name="copy" size={14} />}>Copy path</RailRow>
|
|
98
|
+
<RailRow onClick={() => openTagMode(null, { view: 'graph' })} icon={<Icon name="polygon" size={14} />}>Graph view</RailRow>
|
|
122
99
|
</div>
|
|
123
100
|
</SidebarSection>
|
|
124
101
|
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
102
|
+
{/* TAGS IS A CATEGORY (user ruling 2026-08-01) — not a bare tag dump.
|
|
103
|
+
* It holds the two ENTRY POINTS into the tag system plus this page's
|
|
104
|
+
* own tags. The graph view was reachable only through an unlabelled hex
|
|
105
|
+
* glyph floating at the corner of the overlay: a feature nobody could
|
|
106
|
+
* find unless they already knew it existed. Both entries are rail rows
|
|
107
|
+
* now, in the one row idiom. */}
|
|
108
|
+
<SidebarSection
|
|
109
|
+
sectionKey="tags"
|
|
110
|
+
count={allTags?.length}
|
|
111
|
+
label="Tags"
|
|
112
|
+
collapsedSections={collapsedSections}
|
|
113
|
+
toggleSection={toggleSection}
|
|
114
|
+
>
|
|
115
|
+
{/* NO Search row (user ruling 2026-08-01) — the nav search icon already
|
|
116
|
+
* opens the one modal, and a second door to it is the duplication we
|
|
117
|
+
* just spent an arc deleting. Graph view moved to Quick actions: it
|
|
118
|
+
* is an ACTION on the tag system, not one of this page's tags. */}
|
|
119
|
+
{allTags.length > 0 && (
|
|
120
|
+
/* No `color`, no `size` — see DocsFrontmatter. `sm` is the default
|
|
121
|
+
* and the only size; a colour would cost the hover state. */
|
|
122
|
+
<div className="flex flex-wrap gap-1.5 items-start min-w-0 w-full mt-2">
|
|
134
123
|
{allTags.map((tag) => (
|
|
135
124
|
<Tag
|
|
136
125
|
key={tag}
|
|
137
|
-
variant="naked"
|
|
138
|
-
size="lg"
|
|
139
|
-
color={getTagColor(tag)}
|
|
140
126
|
onClick={() => openTagMode(tag)}
|
|
141
127
|
className="max-w-full overflow-hidden text-ellipsis"
|
|
142
128
|
>
|
|
@@ -144,9 +130,10 @@ const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, compone
|
|
|
144
130
|
</Tag>
|
|
145
131
|
))}
|
|
146
132
|
</div>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
133
|
+
)}
|
|
134
|
+
</SidebarSection>
|
|
135
|
+
</div>
|
|
136
|
+
</RailSection>
|
|
150
137
|
)
|
|
151
138
|
}
|
|
152
139
|
|
|
@@ -406,9 +393,11 @@ const DocumentationReader = ({
|
|
|
406
393
|
|
|
407
394
|
if (!doc) {
|
|
408
395
|
return (
|
|
409
|
-
/* framed to a
|
|
410
|
-
*
|
|
411
|
-
|
|
396
|
+
/* framed to a tier that was removed from the scale at theme 0.11.22 and
|
|
397
|
+
* left behind here — a width nothing else still uses; now on the panel
|
|
398
|
+
* token. Left-anchored, not centred (2026-08-01): the frame centres in
|
|
399
|
+
* the viewport, the content does not centre in the frame. */
|
|
400
|
+
<div className="max-w-[var(--kol-content-panel)] py-16">
|
|
412
401
|
<DocsHeader title="Document Not Found" subtitle={`Could not find document: ${docId}`} />
|
|
413
402
|
<p className="kol-mono-12 mt-6">
|
|
414
403
|
<Link to={docsIndex} className="text-accent-primary">
|
|
@@ -9,6 +9,28 @@
|
|
|
9
9
|
export const capitalise = (value) =>
|
|
10
10
|
value ? value.charAt(0).toUpperCase() + value.slice(1) : value
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* fileLabel — a rail row's name, taken from the FILENAME (user ruling
|
|
14
|
+
* 2026-08-01).
|
|
15
|
+
*
|
|
16
|
+
* The tree used to label rows from the doc's H1, so `01-tokens.md` (opening
|
|
17
|
+
* `# Foundations — the token system`) rendered as "Foundations" — a row whose
|
|
18
|
+
* name appears nowhere in the folder, and two files whose H1s both began
|
|
19
|
+
* "Foundations" collapsed into two identical rows. A tree that mirrors a
|
|
20
|
+
* folder has to read the folder: what Finder shows is what the sidebar shows.
|
|
21
|
+
*
|
|
22
|
+
* 01-tokens.md → Tokens
|
|
23
|
+
* 04-layout-breakpoints.md → Layout breakpoints
|
|
24
|
+
*
|
|
25
|
+
* Titles still drive SEARCH and the page header; this is the tree label only.
|
|
26
|
+
*/
|
|
27
|
+
export const fileLabel = (file = '') => {
|
|
28
|
+
const base = file.split('/').pop()?.replace(/\.md$/i, '') ?? ''
|
|
29
|
+
const named = base.replace(/^\d+(\.\d+)*[-.]?\s*/, '')
|
|
30
|
+
if (!named) return capitalise(base)
|
|
31
|
+
return capitalise(named.replace(/[-_]+/g, ' ').trim())
|
|
32
|
+
}
|
|
33
|
+
|
|
12
34
|
// Index.md files (e.g. "00-metadata-index", "foundry-index") have no version dots
|
|
13
35
|
export const isIndexFile = (id) => id.endsWith('-index') && !id.includes('.')
|
|
14
36
|
|
|
@@ -60,6 +82,15 @@ export const cleanTitle = (title, id) => {
|
|
|
60
82
|
|
|
61
83
|
let cleaned = title.replace(/^\d+\.\d+\.\d+\s*/, '')
|
|
62
84
|
cleaned = cleaned.replace(/^[A-Za-z\s]+:\s*/, '')
|
|
85
|
+
/* SUBTITLE OFF (user ruling 2026-08-01): "dont Foundtation - the token
|
|
86
|
+
* system, just 'foundation'". A doc's `title` is written for the document —
|
|
87
|
+
* "Type classes — the two families and when to use which" — and a rail row
|
|
88
|
+
* is not the place to read a sentence: three of eight rows wrapped to two
|
|
89
|
+
* and three lines. Everything from the em/en dash on is the subtitle, so the
|
|
90
|
+
* row keeps the name and drops the explanation. The DOC's own title is
|
|
91
|
+
* untouched; this is the rail's label only.
|
|
92
|
+
* Spaced dash only — an unspaced one is a compound word, not a separator. */
|
|
93
|
+
cleaned = cleaned.replace(/\s+[—–]\s+.*$/, '')
|
|
63
94
|
cleaned = cleaned.trim() || title
|
|
64
95
|
|
|
65
96
|
const versionMatch = id.match(/^(\d+\.\d+)\.(\d+)/)
|