@kolkrabbi/kol-workshop 0.6.0 → 0.18.1

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.6.0",
3
+ "version": "0.18.1",
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.15.1",
18
- "@kolkrabbi/kol-framework": "0.10.1",
19
- "@kolkrabbi/kol-theme": "0.13.4",
20
- "@kolkrabbi/kol-icons": "0.8.11"
17
+ "@kolkrabbi/kol-component": "0.24.0",
18
+ "@kolkrabbi/kol-icons": "0.10.0",
19
+ "@kolkrabbi/kol-framework": "0.13.0",
20
+ "@kolkrabbi/kol-theme": "0.30.1"
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:
@@ -24,21 +25,13 @@ const WorkshopDefaultSidebar = ({ routes = [], basePath = '/workshop' }) => {
24
25
  const links = currentChild?.links
25
26
 
26
27
  return (
27
- <div className="space-y-4">
28
+ <div className="shell-rail-stack-inner">
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,
@@ -60,77 +60,52 @@ const DocsSidebar = ({ inventory = [], docHref, basePath, onNavigate, collapsed,
60
60
  }
61
61
 
62
62
  return (
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">
63
+ <div className="shell-rail-stack-inner">
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="shell-rail-stack-inner">
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
- <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
- })}
128
- </div>
129
- )}
90
+ <nav className="shell-nav-items">
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
+ ))}
102
+ </nav>
103
+ </RailSection>
130
104
  </div>
131
105
  )
132
106
  })}
133
- </div>}
107
+ </div>
108
+ </RailSection>
134
109
  </div>
135
110
  )
136
111
  }
@@ -159,7 +134,7 @@ const WorkshopSidebar = ({
159
134
  )
160
135
 
161
136
  return (
162
- <div className="space-y-6">
137
+ <div className="shell-rail-stack">
163
138
  <ShellSidebar
164
139
  routes={workshopRoutes}
165
140
  basePath={basePath}
@@ -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,93 @@
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',
78
+ }
79
+
80
+ /* THE FALLBACK IS A LABEL TOO (user ruling 2026-08-01). `FIELD_LABELS[key] ??
81
+ * key` printed an unnamed key raw, so a doc carrying `imported_from` before it
82
+ * was listed rendered the row as `imported_from` — snake_case and lowercase, in
83
+ * a column where every neighbour is sentence case. *"its very amateur that you
84
+ * cant start a sentence or word with a Capital?"*
85
+ *
86
+ * Casing is authored at the data layer, never by CSS (the no-auto-text-transform
87
+ * law) — so the fallback authors it here, where the string is made. */
88
+ const humanise = (key) => {
89
+ const words = String(key).replace(/[-_]+/g, ' ').trim()
90
+ return words.charAt(0).toUpperCase() + words.slice(1)
22
91
  }
23
92
 
24
93
  /* Reading ORDER, not an allowlist. The kol-docs contract first
@@ -36,13 +105,33 @@ const FIELD_ICONS = {
36
105
  * `related` is the one deliberate omission — the rail renders it as live links,
37
106
  * so printing the raw wikilinks here would be the same thing twice. */
38
107
  const FIELD_ORDER = [
39
- 'title', 'type', 'status', 'updated', 'tags',
40
- 'description', 'aliases',
41
- 'created', 'verified', 'audience', 'superseded_by', 'drift',
108
+ /* `created` sits ABOVE `updated` (user ruling 2026-08-01) — a document's age
109
+ * reads oldest-first, and `updated` on its own says nothing about how long
110
+ * the thing has existed. */
111
+ 'title', 'type', 'status', 'created', 'updated', 'tags',
112
+ 'description', 'aliases', 'sources',
113
+ 'verified', 'audience', 'superseded_by', 'drift',
42
114
  'category', 'date', 'modified', 'version',
43
115
  ]
44
116
 
45
- const HIDDEN = new Set(['related'])
117
+ /* `related` — the rail renders it as live links; printing the raw wikilinks
118
+ * here would be the same thing twice.
119
+ *
120
+ * `aliases` — user ruling 2026-08-01: *"why is this a category in frontmatter?
121
+ * what purpose … its just not relevant"*. It is an OBSIDIAN field, not a tag
122
+ * and not a document fact: `.kol/docs-framework/01-conventions.md:65` says it
123
+ * feeds the Quick Switcher and search autocomplete and explicitly does **not**
124
+ * affect wikilink resolution. Nothing in this app reads it. It stays in the
125
+ * file for Obsidian and stops taking a row in a panel that renders facts. */
126
+ const HIDDEN = new Set(['related', 'aliases'])
127
+
128
+ /* VALUES ARE SENTENCE CASE TOO (user ruling 2026-08-01). The labels were fixed
129
+ * and the values were not, so the panel read `Type reference` / `Status
130
+ * active` — a capitalised label pointing at a lowercase word. These two fields
131
+ * carry closed enums from the kol-docs contract, so the case is authored here
132
+ * against the key, never with a text-transform. Every other value is content
133
+ * and is printed exactly as written. */
134
+ const CASED_VALUE_FIELDS = new Set(['type', 'status'])
46
135
 
47
136
  /** Contract order first, then anything else the doc carries, alphabetically. */
48
137
  const orderFields = (metadata) => {
@@ -85,26 +174,58 @@ const DocsFrontmatter = ({ metadata, docId }) => {
85
174
  <div key={key} className="docs-frontmatter-row">
86
175
  <span className="docs-frontmatter-key kol-helper-12 text-meta">
87
176
  {icon && <Icon name={icon} size={14} />}
88
- {key}
177
+ {FIELD_LABELS[key] ?? humanise(key)}
89
178
  </span>
90
- <span className="kol-mono-12 text-strong">
179
+ <span className="docs-frontmatter-value kol-mono-12 text-strong">
91
180
  {key === 'tags' && Array.isArray(value) ? (
181
+ /* `naked`, the SAME Tag rendering the rail's own tag list uses
182
+ * (DocReaderSidebar). This was the default filled variant, so
183
+ * the one concept rendered as a solid pill here and as plain
184
+ * coloured text six inches to the right.
185
+ *
186
+ * NO `color` (user ruling 2026-08-01): passing one swaps the
187
+ * base class from `tag-control` to `tag tag--{color}`, and
188
+ * only `tag-control` has a `:hover` rule — so a coloured Tag
189
+ * silently loses its interaction state. Right component first;
190
+ * colour is its own decision, later.
191
+ * `size` is omitted: `sm` is the default and the only size. */
92
192
  <span className="flex flex-wrap gap-1.5">
93
193
  {value.map((tag) => (
94
- <Tag
95
- key={tag}
96
- size="sm"
97
- color={getTagColor(tag)}
98
- onClick={() => openTagMode(tag)}
99
- >
100
- {tag}
194
+ <Tag key={tag} onClick={() => openTagMode(tag)}>
195
+ {/* the path renderer, not the raw string — namespace dims,
196
+ * leaf carries. Shared with the tag overlay so the two
197
+ * surfaces cannot spell a tag two ways. */}
198
+ <TagPath tag={tag} />
101
199
  </Tag>
102
200
  ))}
103
201
  </span>
104
202
  ) : DATE_FIELDS.has(key) ? (
105
203
  formatDate(String(value))
204
+ ) : CASED_VALUE_FIELDS.has(key) ? (
205
+ humanise(String(value))
106
206
  ) : Array.isArray(value) ? (
107
- value.join(' · ')
207
+ /* STACKED, ONE PER LINE (user ruling 2026-08-01). Arrays were
208
+ * `value.join(' · ')` — a single run-on string, so `sources`
209
+ * with three file paths ran straight off the right edge of the
210
+ * panel and the reader could not tell where one path ended and
211
+ * the next began. A list of paths IS a list; the dot-join was
212
+ * printing it as a sentence.
213
+ *
214
+ * `break-all` because these are paths, not words: a wrapped
215
+ * path must break at the character, never leave a gap and drop
216
+ * the whole segment to the next line.
217
+ *
218
+ * THE LIST SCROLLS PAST ITS CAP (user question 2026-08-01:
219
+ * "I just wonder what it will be when the sources are f.e. more
220
+ * then 15?"). Unbounded, fifteen paths push the article fifteen
221
+ * lines down and the metadata panel becomes the page. No
222
+ * file-type icons — a path already ends in its own extension,
223
+ * and a glyph restating it is decoration. */
224
+ <span className="docs-frontmatter-list">
225
+ {value.map((item, i) => (
226
+ <span key={i} className="break-all">{String(item)}</span>
227
+ ))}
228
+ </span>
108
229
  ) : (
109
230
  String(value)
110
231
  )}