@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-workshop",
3
- "version": "0.5.1",
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-framework": "0.9.1",
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="space-y-0">
32
+ <nav className="shell-nav-items">
32
33
  {siblings.map(child => (
33
- <Link
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
- <a href={links.live} target="_blank" rel="noopener noreferrer" className="shell-nav-item flex items-center gap-2 kol-mono-14 text-body transition-colors hover:text-emphasis">
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
- <a href={links.repo} target="_blank" rel="noopener noreferrer" className="shell-nav-item flex items-center gap-2 kol-mono-14 text-body transition-colors hover:text-emphasis">
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
- <Link to={basePath} className="shell-nav-item flex items-center gap-2 kol-mono-14 text-body transition-colors hover:text-emphasis">
71
- <Icon name="layout" size={14} />
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, Tooltip } from '@kolkrabbi/kol-component'
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
- {/* kol-helper-10 types the wrapper box itself (was an untyped 16px box) */}
65
- <div className="shell-sidebar-toggle shell-sidebar-label kol-doc-eyebrow" style={{ justifyContent: 'space-between', paddingRight: '4px' }}>
66
- <Link to={docHref()} className="shell-sidebar-label kol-doc-eyebrow" onClick={(e) => {
67
- if (collapsed && onToggle) onToggle()
68
- if (onNavigate) onNavigate(e)
69
- }}>
70
- Documentation
71
- </Link>
72
- <Tooltip label={collapsed ? 'Expand Documentation' : 'Collapse Documentation'}>
73
- <button
74
- type="button"
75
- onClick={onToggle}
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
- <button
98
- type="button"
99
- className="shell-nav-group-header w-full text-left kol-mono-14 text-body"
100
- onClick={() => toggleGroup(major)}
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
- const isActive = d.id === activeDocId
116
- return (
117
- <Link
118
- key={d.id}
119
- to={docHref(d.id)}
120
- className={`shell-nav-item shell-nav-item-doc kol-mono-14 ${isActive ? 'text-emphasis' : 'text-body'}`}
121
- onClick={onNavigate}
122
- >
123
- <span className="shell-nav-item-title">{cleanTitle(d.title, d.id)}</span>
124
- <span className="shell-nav-item-id kol-mono-14 text-subtle">{extractDocNumber(d.id)}</span>
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
- </div>}
107
+ </div>
108
+ </RailSection>
134
109
  </div>
135
110
  )
136
111
  }
@@ -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
- { accessor: 'type', header: 'Type', className: 'kol-doc-table-value kol-doc-table-value--wrap text-meta' },
44
- { accessor: 'def', header: 'Default', className: 'kol-doc-table-value text-meta' },
45
- { accessor: 'desc', header: 'Description' },
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: null,
8
- type: 'folder',
11
+ title: 'type',
12
+ type: 'library',
9
13
  status: 'check',
10
- updated: null,
11
- created: null,
12
- verified: null,
13
- description: null,
14
- audience: null,
15
- aliases: null,
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: null,
41
+ date: 'journal',
19
42
  category: 'folder',
20
- tags: null,
21
- modified: null,
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
- key={tag}
96
- size="sm"
97
- color={getTagColor(tag)}
98
- onClick={() => openTagMode(tag)}
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, getTagColor } from '../engine'
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. They collapsed on click with no chevron, no count and
20
- * no hover, so the only way to discover they were interactive was to click
21
- * something that looked inert and watch the panel vanish. An affordance that
22
- * only announces itself after you've used it isn't one.
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
- const isOpen = !collapsedSections[sectionKey]
26
- return (
27
- <div>
28
- <button
29
- type="button"
30
- className="shell-nav-group-header w-full text-left shell-sidebar-label kol-doc-eyebrow"
31
- onClick={() => toggleSection(sectionKey)}
32
- aria-expanded={isOpen}
33
- >
34
- <span className="flex items-center gap-2">
35
- <Icon
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
- <div className="space-y-6">
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="On this page"
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
- <Link key={r.id} to={r.href} className="shell-nav-item block kol-mono-14 text-body transition-colors hover:text-emphasis">
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
- <button
98
- type="button"
99
- className="shell-nav-item flex items-center gap-2 kol-mono-14 text-body transition-colors hover:text-emphasis"
100
- onClick={() => navigate(-1)}
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
- {allTags.length > 0 && (
126
- <SidebarSection
127
- sectionKey="tags"
128
- count={allTags?.length}
129
- label="Tags"
130
- collapsedSections={collapsedSections}
131
- toggleSection={toggleSection}
132
- >
133
- <div className="flex flex-col gap-1.5 items-start min-w-0 w-full">
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
- </SidebarSection>
148
- )}
149
- </div>
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 1400px tier that was removed from the scale at theme
410
- * 0.11.22 and left behind here — a width nothing else still uses */
411
- <div className="mx-auto max-w-[var(--kol-content-panel)] py-16">
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+)/)