@kolkrabbi/kol-workshop 0.3.6 → 0.5.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.3.6",
3
+ "version": "0.5.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",
@@ -13,11 +13,11 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "d3": "^7.9.0",
16
- "@kolkrabbi/kol-brand": "^0.1.2",
17
- "@kolkrabbi/kol-component": "0.14.1",
18
16
  "@kolkrabbi/kol-icons": "0.8.11",
17
+ "@kolkrabbi/kol-component": "0.14.3",
18
+ "@kolkrabbi/kol-brand": "^0.1.2",
19
19
  "@kolkrabbi/kol-theme": "0.13.3",
20
- "@kolkrabbi/kol-framework": "0.8.1"
20
+ "@kolkrabbi/kol-framework": "0.9.1"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "react": "^18.3.0 || ^19.0.0",
@@ -33,7 +33,7 @@ const WorkshopDefaultSidebar = ({ routes = [], basePath = '/workshop' }) => {
33
33
  <Link
34
34
  key={child.id}
35
35
  to={`${basePath}/${child.path}`}
36
- className={`shell-sidebar-link block kol-mono-14 ${
36
+ className={`shell-nav-item block kol-mono-14 ${
37
37
  location.pathname === `${basePath}/${child.path}` ? 'text-emphasis' : 'text-body'
38
38
  }`}
39
39
  >
@@ -47,15 +47,15 @@ const WorkshopDefaultSidebar = ({ routes = [], basePath = '/workshop' }) => {
47
47
  {links && (
48
48
  <div>
49
49
  <div className="shell-sidebar-label kol-doc-eyebrow">Repository</div>
50
- <div className="space-y-1">
50
+ <div>
51
51
  {links.live && (
52
- <a href={links.live} target="_blank" rel="noopener noreferrer" className="shell-sidebar-action kol-mono-14 text-body">
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
53
  <Icon name="external-link" size={14} />
54
54
  Live site
55
55
  </a>
56
56
  )}
57
57
  {links.repo && (
58
- <a href={links.repo} target="_blank" rel="noopener noreferrer" className="shell-sidebar-action kol-mono-14 text-body">
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
59
  <Icon name="external-link" size={14} />
60
60
  GitHub
61
61
  </a>
@@ -66,14 +66,14 @@ const WorkshopDefaultSidebar = ({ routes = [], basePath = '/workshop' }) => {
66
66
 
67
67
  <div>
68
68
  <div className="shell-sidebar-label kol-doc-eyebrow">Quick actions</div>
69
- <div className="space-y-1">
70
- <Link to={basePath} className="shell-sidebar-action kol-mono-14 text-body">
69
+ <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
71
  <Icon name="layout" size={14} />
72
72
  Workshop home
73
73
  </Link>
74
74
  <button
75
75
  type="button"
76
- className="shell-sidebar-action kol-mono-14 text-body"
76
+ className="shell-nav-item flex items-center gap-2 kol-mono-14 text-body transition-colors hover:text-emphasis"
77
77
  onClick={() => navigator.clipboard.writeText(window.location.pathname)}
78
78
  title="Copy page path to clipboard"
79
79
  >
@@ -96,7 +96,7 @@ const DocsSidebar = ({ inventory = [], docHref, basePath, onNavigate, collapsed,
96
96
  <div key={major} className="shell-nav-group">
97
97
  <button
98
98
  type="button"
99
- className="shell-nav-group-header w-full text-left kol-helper-14 text-body"
99
+ className="shell-nav-group-header w-full text-left kol-mono-14 text-body"
100
100
  onClick={() => toggleGroup(major)}
101
101
  >
102
102
  <span className="flex items-center gap-2">
@@ -107,7 +107,7 @@ const DocsSidebar = ({ inventory = [], docHref, basePath, onNavigate, collapsed,
107
107
  />
108
108
  {categoryLabels[major] || 'Other'}
109
109
  </span>
110
- <span className="kol-helper-12 text-subtle">({docs.length})</span>
110
+ <span className="kol-mono-14 text-subtle">({docs.length})</span>
111
111
  </button>
112
112
  {!isCollapsed && (
113
113
  <div className="shell-nav-items">
@@ -121,7 +121,7 @@ const DocsSidebar = ({ inventory = [], docHref, basePath, onNavigate, collapsed,
121
121
  onClick={onNavigate}
122
122
  >
123
123
  <span className="shell-nav-item-title">{cleanTitle(d.title, d.id)}</span>
124
- <span className="shell-nav-item-id kol-helper-10 text-subtle">{extractDocNumber(d.id)}</span>
124
+ <span className="shell-nav-item-id kol-mono-14 text-subtle">{extractDocNumber(d.id)}</span>
125
125
  </Link>
126
126
  )
127
127
  })}
@@ -5,16 +5,59 @@ import { useTagMode } from '../tags'
5
5
  const FIELD_ICONS = {
6
6
  file: 'file',
7
7
  title: null,
8
+ type: 'folder',
9
+ status: 'check',
10
+ updated: null,
11
+ created: null,
12
+ verified: null,
13
+ description: null,
14
+ audience: null,
15
+ aliases: null,
8
16
  version: 'hash-01',
17
+ /* legacy dialect (workshop-sample docs) — kept so those still render */
9
18
  date: null,
10
- status: 'check',
11
- 'content-type': null,
12
19
  category: 'folder',
13
20
  tags: null,
14
- modified: null
21
+ modified: null,
22
+ }
23
+
24
+ /* Reading ORDER, not an allowlist. The kol-docs contract first
25
+ * (`.kol/docs-framework/01-conventions.md`: required title/type/status/updated/
26
+ * tags, then recommended description/aliases, then optional), legacy
27
+ * sample-dialect keys after, and ANY key not named here after that — so a field
28
+ * can never be silently dropped for being unknown.
29
+ *
30
+ * This was a filter over ['title','category','date','tags','modified'] — the
31
+ * workshop-SAMPLE dialect. No kol-docs document carries category/date/modified,
32
+ * so it admitted title + tags and discarded the seven fields the parser had
33
+ * already handed it. The panel wasn't missing data; it was screening for the
34
+ * wrong schema, and screening is now the thing it doesn't do.
35
+ *
36
+ * `related` is the one deliberate omission — the rail renders it as live links,
37
+ * so printing the raw wikilinks here would be the same thing twice. */
38
+ const FIELD_ORDER = [
39
+ 'title', 'type', 'status', 'updated', 'tags',
40
+ 'description', 'aliases',
41
+ 'created', 'verified', 'audience', 'superseded_by', 'drift',
42
+ 'category', 'date', 'modified', 'version',
43
+ ]
44
+
45
+ const HIDDEN = new Set(['related'])
46
+
47
+ /** Contract order first, then anything else the doc carries, alphabetically. */
48
+ const orderFields = (metadata) => {
49
+ const present = Object.keys(metadata).filter(
50
+ (k) => !HIDDEN.has(k) && metadata[k] != null && metadata[k] !== '' &&
51
+ !(Array.isArray(metadata[k]) && metadata[k].length === 0)
52
+ )
53
+ const known = FIELD_ORDER.filter((k) => present.includes(k))
54
+ const rest = present.filter((k) => !FIELD_ORDER.includes(k)).sort()
55
+ return [...known, ...rest]
15
56
  }
16
57
 
17
- const FIELD_ORDER = ['title', 'category', 'date', 'tags', 'modified']
58
+ /* Every field rendered as a date. `updated`/`created`/`verified` are the
59
+ * kol-docs names; `date`/`modified` are the sample dialect's. */
60
+ const DATE_FIELDS = new Set(['updated', 'created', 'verified', 'date', 'modified'])
18
61
 
19
62
  const formatDate = (dateStr) => {
20
63
  if (!dateStr) return null
@@ -27,7 +70,7 @@ const DocsFrontmatter = ({ metadata, docId }) => {
27
70
  const { openTagMode } = useTagMode()
28
71
  if (!metadata || Object.keys(metadata).length === 0) return null
29
72
 
30
- const fields = FIELD_ORDER.filter(key => metadata[key] != null && metadata[key] !== '')
73
+ const fields = orderFields(metadata)
31
74
 
32
75
  if (fields.length === 0) return null
33
76
 
@@ -58,8 +101,10 @@ const DocsFrontmatter = ({ metadata, docId }) => {
58
101
  </Tag>
59
102
  ))}
60
103
  </span>
61
- ) : key === 'date' || key === 'modified' ? (
104
+ ) : DATE_FIELDS.has(key) ? (
62
105
  formatDate(String(value))
106
+ ) : Array.isArray(value) ? (
107
+ value.join(' · ')
63
108
  ) : (
64
109
  String(value)
65
110
  )}
@@ -10,18 +10,41 @@ import DocsFrontmatter from './DocsFrontmatter.jsx'
10
10
  import { DocSection } from './DocKit.jsx'
11
11
  import { renderInlineTokens } from './render-tokens.jsx'
12
12
 
13
- const SidebarSection = ({ sectionKey, label, collapsedSections, toggleSection, children }) => (
14
- <div>
15
- <button
16
- type="button"
17
- className="shell-sidebar-toggle shell-sidebar-label kol-doc-eyebrow"
18
- onClick={() => toggleSection(sectionKey)}
19
- >
20
- {label}
21
- </button>
22
- {!collapsedSections[sectionKey] && children}
23
- </div>
24
- )
13
+ /**
14
+ * A rail section header. Same anatomy as the LEFT tree's group header — a
15
+ * rotating chevron, the label, and the item count pushed right — because the
16
+ * two rails are one system and a reader shouldn't have to learn each side
17
+ * separately.
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.
23
+ */
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
+ }
25
48
 
26
49
  const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, componentsHref, docFilePath }) => {
27
50
  const navigate = useNavigate()
@@ -34,6 +57,7 @@ const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, compone
34
57
  <SidebarSection
35
58
  sectionKey="toc"
36
59
  label="On this page"
60
+ count={toc?.length}
37
61
  collapsedSections={collapsedSections}
38
62
  toggleSection={toggleSection}
39
63
  >
@@ -44,6 +68,7 @@ const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, compone
44
68
  <SidebarSection
45
69
  sectionKey="related"
46
70
  label="Related"
71
+ count={related.length}
47
72
  collapsedSections={collapsedSections}
48
73
  toggleSection={toggleSection}
49
74
  >
@@ -64,26 +89,30 @@ const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, compone
64
89
  collapsedSections={collapsedSections}
65
90
  toggleSection={toggleSection}
66
91
  >
67
- <div className="space-y-1">
92
+ {/* Quick actions wear the SAME row as On-this-page and Related. They
93
+ * were `.shell-sidebar-action`, a third geometry with no left indent,
94
+ * so this rail carried three different row idioms — under the
95
+ * "ONE rail voice" comment above. */}
96
+ <div>
68
97
  <button
69
98
  type="button"
70
- className="shell-sidebar-action kol-mono-14 text-body"
99
+ className="shell-nav-item flex items-center gap-2 kol-mono-14 text-body transition-colors hover:text-emphasis"
71
100
  onClick={() => navigate(-1)}
72
101
  >
73
102
  <Icon name="arrow-left" size={14} />
74
103
  Back
75
104
  </button>
76
- <Link to={docsIndexHref} className="shell-sidebar-action kol-mono-14 text-body">
105
+ <Link to={docsIndexHref} className="shell-nav-item flex items-center gap-2 kol-mono-14 text-body transition-colors hover:text-emphasis">
77
106
  <Icon name="book-open" size={14} />
78
107
  All documentation
79
108
  </Link>
80
- <Link to={componentsHref} className="shell-sidebar-action kol-mono-14 text-body">
109
+ <Link to={componentsHref} className="shell-nav-item flex items-center gap-2 kol-mono-14 text-body transition-colors hover:text-emphasis">
81
110
  <Icon name="grid" size={14} />
82
111
  View components
83
112
  </Link>
84
113
  <button
85
114
  type="button"
86
- className="shell-sidebar-action kol-mono-14 text-body"
115
+ className="shell-nav-item flex items-center gap-2 kol-mono-14 text-body transition-colors hover:text-emphasis"
87
116
  onClick={() => navigator.clipboard.writeText(docFilePath(docId))}
88
117
  title="Copy file path to clipboard"
89
118
  >
@@ -96,6 +125,7 @@ const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, compone
96
125
  {allTags.length > 0 && (
97
126
  <SidebarSection
98
127
  sectionKey="tags"
128
+ count={allTags?.length}
99
129
  label="Tags"
100
130
  collapsedSections={collapsedSections}
101
131
  toggleSection={toggleSection}
@@ -376,7 +406,9 @@ const DocumentationReader = ({
376
406
 
377
407
  if (!doc) {
378
408
  return (
379
- <div className="max-w-[1400px] mx-auto px-10 py-16">
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">
380
412
  <DocsHeader title="Document Not Found" subtitle={`Could not find document: ${docId}`} />
381
413
  <p className="kol-mono-12 mt-6">
382
414
  <Link to={docsIndex} className="text-accent-primary">
@@ -85,14 +85,41 @@ export const cleanTitle = (title, id) => {
85
85
 
86
86
  const TAG_COLORS = ['blue', 'teal', 'green', 'yellow', 'red', 'orange', 'purple', 'dark']
87
87
 
88
- export const getTagColor = (tag) => {
88
+ /* Colour BY NAMESPACE, from the closed taxonomy in `.kol/docs-framework/
89
+ * 03-tag-taxonomy.md`. Ten top-level namespaces, ten stable assignments —
90
+ * so `domain/*` is always one colour and the eye can group by prefix.
91
+ *
92
+ * This used to hash the whole tag string into TAG_COLORS. That made the colour
93
+ * a function of spelling rather than meaning: `#domain/design-system` and
94
+ * `#domain/iconography` landed on different hues for no reason a reader could
95
+ * see, and renaming a tag silently recoloured it. The taxonomy existed the
96
+ * whole time and nothing consulted it. */
97
+ const NAMESPACE_COLORS = {
98
+ domain: 'blue',
99
+ pattern: 'purple',
100
+ provider: 'orange',
101
+ integration: 'teal',
102
+ project: 'green',
103
+ audience: 'yellow',
104
+ brand: 'red',
105
+ editor: 'teal',
106
+ framework: 'dark',
107
+ archive: 'dark',
108
+ }
109
+
110
+ /** Stable fallback for tags outside the taxonomy — still deterministic, but
111
+ * only reached when a tag carries no known namespace. */
112
+ const hashColor = (s) => {
89
113
  let hash = 0
90
- for (let i = 0; i < tag.length; i++) {
91
- hash = ((hash << 5) - hash + tag.charCodeAt(i)) | 0
92
- }
114
+ for (let i = 0; i < s.length; i++) hash = ((hash << 5) - hash + s.charCodeAt(i)) | 0
93
115
  return TAG_COLORS[Math.abs(hash) % TAG_COLORS.length]
94
116
  }
95
117
 
118
+ export const getTagColor = (tag) => {
119
+ const namespace = String(tag).replace(/^#/, '').split('/')[0]
120
+ return NAMESPACE_COLORS[namespace] ?? hashColor(namespace)
121
+ }
122
+
96
123
  /** Group docs by major version number.
97
124
  *
98
125
  * Three id dialects are grouped: dotted (`0.0.1-slug`), folder-index
@@ -44,8 +44,14 @@ const MainColumn = ({ children, fullHeight }) => (
44
44
  </main>
45
45
  )
46
46
 
47
+ /* `xl`, not `lg` — the grid only declares a third column at xl (gridCols
48
+ * below). Rendering this at lg put THREE children in a TWO-column grid between
49
+ * 1024 and 1279px: the rail wrapped to an implicit second row and `h-full`
50
+ * split the height between them, so main got ~373px of a 900px window and the
51
+ * page read as empty. The breakpoint here and the one in gridCols are one
52
+ * decision and must not be stated twice differently. */
47
53
  const TocColumn = ({ children }) => (
48
- <aside aria-label="Table of contents" className="shell-sidebar-sticky hidden lg:block shrink-0 h-full min-h-0 overflow-y-auto overflow-x-hidden overscroll-none pt-6 md:pt-6 lg:pt-8 pb-8">
54
+ <aside aria-label="Table of contents" className="shell-sidebar-sticky hidden xl:block shrink-0 h-full min-h-0 overflow-y-auto overflow-x-hidden overscroll-none pt-6 md:pt-6 lg:pt-8 pb-8">
49
55
  {children}
50
56
  </aside>
51
57
  )
@@ -76,6 +82,14 @@ const ShellLayout = ({ routes = [], basePath = '/', brand: brandProp, brandLogoS
76
82
  }, [])
77
83
 
78
84
  const effectiveTocContent = tocContent ?? defaultTocContent
85
+ /* KNOWN, deliberately not fixed here: an element is always truthy, so
86
+ * `<AutoToc/>` reserves a rail even on pages where it renders null. The
87
+ * obvious fix — measure the mounted column — deadlocks: once the column
88
+ * unmounts the probe is gone and can never report content again. The real
89
+ * fix is a consumer-side signal (a render-prop returning null, or an
90
+ * explicit `hasToc` prop), which is an API change. Filed, not improvised.
91
+ * The bug this caused — a half-height main column at 1024–1279px — was the
92
+ * `lg` vs `xl` mismatch on TocColumn above, and that IS fixed. */
79
93
  const hasToc = Boolean(effectiveTocContent)
80
94
  const showNav = !navCollapsed
81
95
  const showToc = hasToc && !tocCollapsed
@@ -183,7 +197,17 @@ const ShellLayout = ({ routes = [], basePath = '/', brand: brandProp, brandLogoS
183
197
  * main scrolls unless a page locks it (ShellFullHeightContext — embeds).
184
198
  * overscroll-none stops chaining between regions and the edge bounce. */}
185
199
  <div className="flex-1 overflow-hidden">
186
- <div className="h-full w-full px-4 md:px-5 lg:px-6">
200
+ {/* THE frame (kol-theme "Content widths"): ONE cap at the shell
201
+ * token, centred, on the framework padding ramp. This wrapper
202
+ * carried `w-full px-4 md:px-5 lg:px-6` — no cap at all, and
203
+ * Tailwind's 16/20/24 steps instead of the ramp's 20/32/48 — so
204
+ * every consumer page inherited the raw viewport (measured 2152px
205
+ * of frame at a 2200px window against an 1800px law). Every
206
+ * page-level width fix is downstream of this line. */}
207
+ <div
208
+ className="mx-auto h-full w-full max-w-[var(--kol-content-shell)]"
209
+ style={{ paddingInline: 'var(--kol-pad-section-x)' }}
210
+ >
187
211
  {/* gap lives in .shell-content-grid (theme) — a gap-8 utility here
188
212
  * outranks the layered theme rule at every width and killed the
189
213
  * 48px wide step (ARCHITECTURE §5: component geometry in its own
@@ -75,7 +75,12 @@ const ShellSidebar = ({ routes = [], basePath = '/', onNavigate, label = 'Naviga
75
75
  {label}
76
76
  </Link>
77
77
  ) : (
78
- <button type="button" className="kol-helper-10 text-meta" onClick={handleToggle}>{label}</button>
78
+ /* Same voice as the labelTo branch. This read `kol-helper-10
79
+ * text-meta`, so the one rail mounted without a labelTo (the vault's
80
+ * "Documentation") rendered its label at a different size and colour
81
+ * from its two siblings — a type fork caused purely by which props
82
+ * the consumer happened to pass. */
83
+ <button type="button" className="shell-sidebar-label kol-doc-eyebrow" onClick={handleToggle}>{label}</button>
79
84
  )}
80
85
  <Tooltip label={navCollapsed ? `Expand ${label}` : `Collapse ${label}`}>
81
86
  <button
@@ -99,25 +104,49 @@ const ShellSidebar = ({ routes = [], basePath = '/', onNavigate, label = 'Naviga
99
104
  {routes.map((route) => {
100
105
  const isExpanded = !collapsedSections[route.id]
101
106
 
107
+ /* A group with no children is not a group — it is a link. It used to
108
+ * render as a header anyway: a chevron that rotated over an empty
109
+ * body, no count, and no navigation, so clicking "Icons" or
110
+ * "Components" in the tree did nothing at all while looking like it
111
+ * should. Now the chevron and the toggle only appear when there is
112
+ * something to expand, and a childless row goes where it says. */
113
+ const hasChildren = route.children?.length > 0
114
+ const headerClass = 'shell-nav-group-header w-full text-left kol-mono-14 text-body'
115
+
102
116
  return (
103
117
  <div key={route.id} className="shell-nav-group">
104
- <button
105
- type="button"
106
- className="shell-nav-group-header w-full text-left kol-helper-14 text-body"
107
- onClick={() => handleSectionClick(route)}
108
- >
109
- <span className="flex items-center gap-2">
110
- <Icon
111
- name="chevron-right"
112
- size={12}
113
- className={`transition-transform ${isExpanded ? 'rotate-90' : ''}`}
114
- />
115
- {route.label}
116
- </span>
117
- {route.children?.length > 0 && (
118
- <span className="kol-helper-12 text-subtle">({route.children.length})</span>
119
- )}
120
- </button>
118
+ {hasChildren ? (
119
+ <button
120
+ type="button"
121
+ className={headerClass}
122
+ onClick={() => handleSectionClick(route)}
123
+ aria-expanded={isExpanded}
124
+ >
125
+ <span className="flex items-center gap-2">
126
+ <Icon
127
+ name="chevron-right"
128
+ size={12}
129
+ className={`transition-transform ${isExpanded ? 'rotate-90' : ''}`}
130
+ />
131
+ {route.label}
132
+ </span>
133
+ <span className="kol-mono-14 text-subtle">({route.children.length})</span>
134
+ </button>
135
+ ) : (
136
+ <Link
137
+ to={getSectionRootPath(route, basePath)}
138
+ className={headerClass}
139
+ onClick={onNavigate}
140
+ >
141
+ {/* aligned with the expandable rows' label column — the
142
+ * chevron's width, kept as space so the two kinds of row
143
+ * share a left edge */}
144
+ <span className="flex items-center gap-2">
145
+ <span aria-hidden="true" style={{ width: 12 }} />
146
+ {route.label}
147
+ </span>
148
+ </Link>
149
+ )}
121
150
 
122
151
  {isExpanded && route.children?.length > 0 && (
123
152
  <div className="shell-nav-items">
@@ -119,18 +119,18 @@ const TagGraph = ({ docs, activeTag, onTagClick, allDocs, tagHref = defaultTagHr
119
119
  // Node circles
120
120
  node.append('circle')
121
121
  .attr('r', d => Math.max(8, Math.min(16, 6 + d.count * 2)))
122
+ /* Palette tokens, not copies. This held eight hardcoded hexes that were
123
+ * transcriptions of --kol-palette-* (#3740D3 IS palette-blue) — correct
124
+ * the day they were typed and frozen ever since, so a retuned palette
125
+ * would move every chip and leave the graph behind. The chip rules in
126
+ * kol-components-molecules.css resolve the variables; the graph now
127
+ * reads the same ones. `dark` has no palette entry — it is a surface
128
+ * role — so it falls back rather than emitting an invalid var(). */
122
129
  .attr('fill', d => {
123
- const colorMap = {
124
- blue: '#3740D3',
125
- teal: '#49a0a2',
126
- green: '#66a44c',
127
- yellow: '#ffe32e',
128
- red: '#ce4646',
129
- orange: '#db8000',
130
- purple: '#9437FF',
131
- dark: '#3a3a40'
132
- }
133
- return colorMap[getTagColor(d.id)] || '#49a0a2'
130
+ const key = getTagColor(d.id)
131
+ return key === 'dark'
132
+ ? 'var(--kol-surface-on-primary)'
133
+ : `var(--kol-palette-${key}, var(--kol-palette-teal))`
134
134
  })
135
135
  .attr('opacity', d => activeTag && d.id !== activeTag ? 0.4 : 0.9)
136
136
  .attr('stroke', d => d.id === activeTag ? 'var(--kol-accent-primary)' : 'none')
@@ -34,7 +34,9 @@ const TagModeOverlay = () => {
34
34
 
35
35
  return (
36
36
  <article className="docs-article">
37
- <div className="max-w-[864px] mx-auto">
37
+ {/* was max-w-[864px] — an improvised value in the ~900–1200 band the
38
+ * panel token exists to settle (kol-theme "Content widths"). */}
39
+ <div className="mx-auto max-w-[var(--kol-content-panel)]">
38
40
  <div className="flex items-center justify-start gap-1 mb-3">
39
41
  {hasFilters && (
40
42
  <Tooltip label={viewMode === 'graph' ? 'List view' : 'Graph view'}>