@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 +5 -5
- package/src/compositions/WorkshopDefaultSidebar.jsx +8 -32
- package/src/compositions/WorkshopSidebar.jsx +39 -64
- package/src/docs/DocKit.jsx +21 -4
- package/src/docs/DocsFrontmatter.jsx +147 -26
- package/src/docs/DocumentationReader.jsx +57 -141
- 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 +183 -0
- package/src/shell/RightRail.jsx +168 -0
- package/src/shell/ShellLayout.jsx +117 -41
- package/src/shell/ShellSidebar.jsx +61 -106
- package/src/shell/index.js +11 -0
- package/src/tags/TagModeContext.jsx +52 -6
- package/src/tags/TagModeOverlay.jsx +69 -120
- package/src/tags/TagPath.jsx +27 -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.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.
|
|
18
|
-
"@kolkrabbi/kol-
|
|
19
|
-
"@kolkrabbi/kol-
|
|
20
|
-
"@kolkrabbi/kol-
|
|
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="
|
|
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="
|
|
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,
|
|
@@ -60,77 +60,52 @@ const DocsSidebar = ({ inventory = [], docHref, basePath, onNavigate, collapsed,
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
return (
|
|
63
|
-
<div className="
|
|
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">
|
|
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
|
-
<
|
|
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
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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="
|
|
137
|
+
<div className="shell-rail-stack">
|
|
163
138
|
<ShellSidebar
|
|
164
139
|
routes={workshopRoutes}
|
|
165
140
|
basePath={basePath}
|
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,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:
|
|
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',
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
)}
|