@kolkrabbi/kol-workshop 0.33.0 → 0.34.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 +6 -6
- package/src/docs/DocumentationReader.jsx +5 -2
- package/src/index.js +0 -2
- package/src/search/ResultRow.jsx +27 -22
- package/src/search/SearchPage.jsx +9 -13
- package/src/search/index.js +1 -0
- package/src/shell/RailSection.jsx +6 -3
- package/src/shell/ShellLayout.jsx +3 -2
- package/src/shell/ShellSidebar.jsx +33 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-workshop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.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,12 +13,12 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"d3": "^7.9.0",
|
|
16
|
-
"@kolkrabbi/kol-search": "^0.2.0",
|
|
17
16
|
"@kolkrabbi/kol-markdown": "^0.1.2",
|
|
17
|
+
"@kolkrabbi/kol-search": "^0.2.0",
|
|
18
18
|
"@kolkrabbi/kol-brand": "^0.1.3"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@kolkrabbi/kol-component": ">=0.
|
|
21
|
+
"@kolkrabbi/kol-component": ">=0.231.0",
|
|
22
22
|
"@kolkrabbi/kol-framework": ">=0.45.0",
|
|
23
23
|
"@kolkrabbi/kol-theme": ">=0.155.0",
|
|
24
24
|
"@kolkrabbi/kol-icons": ">=0.18.0",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@kolkrabbi/kol-icons": "^0.
|
|
31
|
-
"@kolkrabbi/kol-
|
|
30
|
+
"@kolkrabbi/kol-icons": "^0.31.0",
|
|
31
|
+
"@kolkrabbi/kol-component": "^0.231.0",
|
|
32
32
|
"@kolkrabbi/kol-theme": "^0.159.0",
|
|
33
|
-
"@kolkrabbi/kol-
|
|
33
|
+
"@kolkrabbi/kol-framework": "^0.48.0"
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"src",
|
|
@@ -85,6 +85,9 @@ const DocumentationReader = ({
|
|
|
85
85
|
routes = {},
|
|
86
86
|
docId: docIdProp,
|
|
87
87
|
showFrontmatter = true,
|
|
88
|
+
/* false → render the article only and leave the right rail to the page (a HOME sits on top of
|
|
89
|
+
* a page whose sections the rail must list — 2026-09-30, "This page (0)" on every home) */
|
|
90
|
+
rail = true,
|
|
88
91
|
}) => {
|
|
89
92
|
const {
|
|
90
93
|
docsIndex = '/docs',
|
|
@@ -309,7 +312,7 @@ const DocumentationReader = ({
|
|
|
309
312
|
* ShellTocContext then defaults to null, so calling it unguarded threw.
|
|
310
313
|
* No shell → no right rail, and the reader just renders its article. */
|
|
311
314
|
useEffect(() => {
|
|
312
|
-
if (!setTocContent) return undefined
|
|
315
|
+
if (!setTocContent || !rail) return undefined
|
|
313
316
|
setTocContent(
|
|
314
317
|
<DocReaderSidebar
|
|
315
318
|
key={docId}
|
|
@@ -324,7 +327,7 @@ const DocumentationReader = ({
|
|
|
324
327
|
/>
|
|
325
328
|
)
|
|
326
329
|
return () => setTocContent(null)
|
|
327
|
-
}, [setTocContent, docId, toc, allTags, topTags, related, docsIndex, components, docFilePath])
|
|
330
|
+
}, [rail, setTocContent, docId, toc, allTags, topTags, related, docsIndex, components, docFilePath])
|
|
328
331
|
|
|
329
332
|
if (!doc) {
|
|
330
333
|
return (
|
package/src/index.js
CHANGED
|
@@ -14,6 +14,4 @@ export * from './tags/index.js'
|
|
|
14
14
|
export * from './exhibit/index.js'
|
|
15
15
|
export * from './compositions/index.js'
|
|
16
16
|
export * from './search/index.js'
|
|
17
|
-
/* retiring — ContentRow replaces it (retirement ledger, 2026-09-30) */
|
|
18
|
-
export { default as ResultRow } from './search/ResultRow.jsx'
|
|
19
17
|
export * from './engine/index.js'
|
package/src/search/ResultRow.jsx
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
* @deprecated — use kol-component's `ContentRow` (variant `article`, `media={false}`). ResultRow was
|
|
3
|
-
* minted 2026-09-30 for the search page while the design system already shipped its row; it
|
|
4
|
-
* forwards to ContentRow and drops at 30 days on the retirement ledger. `variant` is ignored.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useNavigate } from 'react-router-dom'
|
|
8
|
-
import { ContentRow } from '@kolkrabbi/kol-component'
|
|
1
|
+
import { Link } from 'react-router-dom'
|
|
9
2
|
|
|
10
3
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
4
|
+
* ResultRow — one search result: a title, a meta line, an optional description, the whole row a
|
|
5
|
+
* link (2026-09-30, open questions Round 2 — the user: *"I like both, is this a component? if it
|
|
6
|
+
* is I would just have it available as variants … underline is more appealing currently"*).
|
|
7
|
+
*
|
|
8
|
+
* Two variants, each a complete pair — the wash needs its x padding, the underline must not have
|
|
9
|
+
* one (Round 1: x padding without a wash was drawn wrong):
|
|
10
|
+
* `underline` (default) — no ground; the title underlines and the row inks up on hover
|
|
11
|
+
* `wash` — x padding and a ground wash on hover
|
|
12
|
+
*
|
|
13
|
+
* @param {string} to the route the row opens
|
|
14
|
+
* @param {ReactNode} title the result's name (a highlighted node is fine)
|
|
15
|
+
* @param {string} [meta] kind · category · space · date
|
|
16
|
+
* @param {ReactNode} [description]
|
|
17
|
+
* @param {'underline'|'wash'} [variant='underline']
|
|
14
18
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
const VARIANTS = {
|
|
20
|
+
underline: { row: 'group flex flex-col gap-1 py-3 opacity-80 hover:opacity-100 focus-visible:opacity-100', title: 'group-hover:underline underline-offset-4' },
|
|
21
|
+
wash: { row: 'flex flex-col gap-1 px-3 py-3 hover:bg-fg-04', title: '' },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function ResultRow({ to, title, meta, description, variant = 'underline' }) {
|
|
25
|
+
const v = VARIANTS[variant] ?? VARIANTS.underline
|
|
17
26
|
return (
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
eyebrow={meta}
|
|
24
|
-
title={title}
|
|
25
|
-
body={description}
|
|
26
|
-
/>
|
|
27
|
+
<Link to={to} className={v.row}>
|
|
28
|
+
<span className={`kol-doc-body text-emphasis ${v.title}`.trim()}>{title}</span>
|
|
29
|
+
{meta && <span className="kol-helper-12 text-subtle">{meta}</span>}
|
|
30
|
+
{description && <span className="kol-doc-body">{description}</span>}
|
|
31
|
+
</Link>
|
|
27
32
|
)
|
|
28
33
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment, useMemo } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
import { Button,
|
|
2
|
+
import { useSearchParams } from 'react-router-dom'
|
|
3
|
+
import { Button, EmptyState, Input, Pill } from '@kolkrabbi/kol-component'
|
|
4
|
+
import ResultRow from './ResultRow.jsx'
|
|
4
5
|
import { createIndex, search, parseQuery } from '@kolkrabbi/kol-search'
|
|
5
6
|
import { DocHeader, DocSection } from '../docs/DocKit.jsx'
|
|
6
7
|
|
|
@@ -44,8 +45,7 @@ const readToken = (t) => (t.kind === 'term'
|
|
|
44
45
|
? `${t.field} ${t.value}`
|
|
45
46
|
: `${t.negate ? 'not ' : ''}${t.field === 'tags' ? 'tag' : t.field}: ${t.value}${t.via === 'smart' ? ' (from the word)' : ''}`)
|
|
46
47
|
|
|
47
|
-
export default function SearchPage({ items = [], spaces = [], facets = DEFAULT_FACETS, limit = 60 }) {
|
|
48
|
-
const navigate = useNavigate()
|
|
48
|
+
export default function SearchPage({ items = [], spaces = [], facets = DEFAULT_FACETS, limit = 60, rowVariant = 'underline' }) {
|
|
49
49
|
const [params, setParams] = useSearchParams()
|
|
50
50
|
const q = params.get('q') ?? ''
|
|
51
51
|
const index = useMemo(() => createIndex(items), [items])
|
|
@@ -125,16 +125,12 @@ export default function SearchPage({ items = [], spaces = [], facets = DEFAULT_F
|
|
|
125
125
|
<ol className="flex flex-col">
|
|
126
126
|
{out.results.map((r) => (
|
|
127
127
|
<li key={r.item.id} className="border-t border-fg-08 first:border-t-0">
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
variant="article"
|
|
132
|
-
media={false}
|
|
133
|
-
href={r.item.href}
|
|
134
|
-
onNavigate={(e) => { e.preventDefault(); navigate(r.item.href) }}
|
|
135
|
-
eyebrow={[r.item.kind, r.item.category, r.item.space, r.item.date].filter(Boolean).join(' · ')}
|
|
128
|
+
<ResultRow
|
|
129
|
+
to={r.item.href}
|
|
130
|
+
variant={rowVariant}
|
|
136
131
|
title={<Highlight text={r.item.title} ranges={r.highlights.title} />}
|
|
137
|
-
|
|
132
|
+
meta={[r.item.kind, r.item.category, r.item.space, r.item.date].filter(Boolean).join(' · ')}
|
|
133
|
+
description={r.item.description}
|
|
138
134
|
/>
|
|
139
135
|
</li>
|
|
140
136
|
))}
|
package/src/search/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { Link } from 'react-router-dom'
|
|
3
|
+
import { Icon } from '@kolkrabbi/kol-component'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* RailSection — THE rail ladder. One component owns every rail header, at
|
|
@@ -70,7 +71,7 @@ export default function RailSection({
|
|
|
70
71
|
onToggle,
|
|
71
72
|
defaultCollapsed = false,
|
|
72
73
|
onNavigate,
|
|
73
|
-
icon: IconComponent,
|
|
74
|
+
icon: IconComponent = Icon,
|
|
74
75
|
children,
|
|
75
76
|
}) {
|
|
76
77
|
const [internal, setInternal] = useState(defaultCollapsed)
|
|
@@ -120,14 +121,16 @@ export default function RailSection({
|
|
|
120
121
|
* the eyebrow felt expand-only. A category's door lives in the header tabs.
|
|
121
122
|
* L2 keeps the split (2026-08-02 ruling): its label is a chapter's only
|
|
122
123
|
* door to the landing page, and the chevron marks the toggle. */
|
|
123
|
-
|
|
124
|
+
/* L1 JOINS L2 (2026-09-30, user: every label opens its home — *"components from the top and
|
|
125
|
+
* components go to the same components home"*; reverses the 2026-08-09 pure-toggle eyebrow). */
|
|
126
|
+
const labelNode = to ? (
|
|
124
127
|
/* The label navigates; the rest of the row toggles. stopPropagation keeps
|
|
125
128
|
* one click from doing both. */
|
|
126
129
|
<Link
|
|
127
130
|
to={to}
|
|
128
131
|
onClick={(e) => {
|
|
132
|
+
/* the label opens the page, the chevron folds (audit § Homes) */
|
|
129
133
|
e.stopPropagation()
|
|
130
|
-
if (isCollapsed && toggle) toggle()
|
|
131
134
|
if (onNavigate) onNavigate(e)
|
|
132
135
|
}}
|
|
133
136
|
>
|
|
@@ -243,8 +243,7 @@ const ShellLayout = ({ routes = [], basePath = '/', brand: brandProp, brandLogoS
|
|
|
243
243
|
* app — it was a `?` sheet of its own here. `?` still opens it. */
|
|
244
244
|
const SHORTCUTS = [
|
|
245
245
|
{ section: 'Search', items: [
|
|
246
|
-
{ id: 'k', label: 'Search everything', combo: '⌘ K' },
|
|
247
|
-
{ id: 'slash', label: 'Search everything', combo: '/' },
|
|
246
|
+
{ id: 'k', label: 'Search everything', combo: '⌘ K , /' },
|
|
248
247
|
{ id: 'enter', label: 'All results, on the search page', combo: '↵' },
|
|
249
248
|
] },
|
|
250
249
|
{ section: 'Shell', items: [
|
|
@@ -536,6 +535,8 @@ const ShellLayout = ({ routes = [], basePath = '/', brand: brandProp, brandLogoS
|
|
|
536
535
|
query={searchQuery}
|
|
537
536
|
onQueryChange={setSearchQuery}
|
|
538
537
|
results={searchResults}
|
|
538
|
+
/* an empty palette opens on the space table, not a blank box */
|
|
539
|
+
suggestions={navItems.map((n) => ({ id: `space:${n.href}`, label: n.label, href: n.href, icon: 'arrow-right', group: 'Spaces' }))}
|
|
539
540
|
/* THE EXPANDED BODY (user ruling 2026-08-01). The tag browser is
|
|
540
541
|
* not a sibling overlay — it is this palette's second state. Enter
|
|
541
542
|
* commits the query and swaps the result rows for the full body;
|
|
@@ -37,39 +37,26 @@ const ShellSidebar = ({ routes = [], basePath = '/', onNavigate, label = 'Naviga
|
|
|
37
37
|
const navCollapsed = isControlled ? collapsed : internalCollapsed
|
|
38
38
|
const handleToggle = isControlled ? onToggle : () => setInternalCollapsed(prev => !prev)
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
initial[route.id] = !isActive
|
|
49
|
-
})
|
|
50
|
-
return initial
|
|
40
|
+
/* THE RAIL FOLLOWS YOU — INTO A CHILD ONLY (2026-09-30, reversing the 2026-09-28 "open the
|
|
41
|
+
* group you are in" on its landing page too). Landing on a page INSIDE a chapter opens that
|
|
42
|
+
* chapter and folds the rest; a chapter's own home opens nothing. Every other chapter starts
|
|
43
|
+
* folded — including ones this rail has never seen (a Group-by switch hands it new ids, and an
|
|
44
|
+
* unknown id used to read as open, so every chapter sprang open). */
|
|
45
|
+
const holdsPage = (route) => (route.children ?? []).some((c) => {
|
|
46
|
+
const cp = getChildPath(c, basePath).replace(/\/$/, '')
|
|
47
|
+
return normalizedPath === cp || normalizedPath.startsWith(cp + '/')
|
|
51
48
|
})
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* Arriving in a group opens it AND folds its siblings, so the open group is
|
|
56
|
-
* the one you are in. Opening another by hand still works until you move. */
|
|
57
|
-
const isRouteActive = (route) => {
|
|
58
|
-
const sectionPath = getSectionRootPath(route, basePath)
|
|
59
|
-
const own = sectionPath === basePath
|
|
60
|
-
? normalizedPath === basePath
|
|
61
|
-
: normalizedPath === sectionPath || normalizedPath.startsWith(sectionPath + '/')
|
|
62
|
-
return own || (route.children ?? []).some((c) => {
|
|
63
|
-
const cp = getChildPath(c, basePath).replace(/\/$/, '')
|
|
64
|
-
return normalizedPath === cp || normalizedPath.startsWith(cp + '/')
|
|
65
|
-
})
|
|
49
|
+
const followed = () => {
|
|
50
|
+
const hit = routes.find(holdsPage)
|
|
51
|
+
return hit ? Object.fromEntries(routes.map((r) => [r.id, r !== hit])) : null
|
|
66
52
|
}
|
|
53
|
+
const [collapsedSections, setCollapsedSections] = useState(() => followed() ?? {})
|
|
67
54
|
useEffect(() => {
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
setCollapsedSections(Object.fromEntries(routes.map((r) => [r.id, !active.includes(r)])))
|
|
55
|
+
const next = followed()
|
|
56
|
+
if (next) setCollapsedSections(next)
|
|
71
57
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
72
58
|
}, [normalizedPath, routes, basePath])
|
|
59
|
+
const isFolded = (route) => collapsedSections[route.id] ?? true
|
|
73
60
|
|
|
74
61
|
/* FOLD ALL (2026-09-30, the names audit: *"shortcut collapse/expand the categories"*). The
|
|
75
62
|
* shell's `C` key broadcasts one target state; every rail's chapters take it. */
|
|
@@ -80,7 +67,7 @@ const ShellSidebar = ({ routes = [], basePath = '/', onNavigate, label = 'Naviga
|
|
|
80
67
|
}, [routes])
|
|
81
68
|
|
|
82
69
|
const handleSectionClick = (route) => {
|
|
83
|
-
setCollapsedSections((prev) => ({ ...prev, [route.id]: !prev[route.id] }))
|
|
70
|
+
setCollapsedSections((prev) => ({ ...prev, [route.id]: !(prev[route.id] ?? true) }))
|
|
84
71
|
}
|
|
85
72
|
|
|
86
73
|
/* The L1 count is every leaf row under it — RailSection shows it only while
|
|
@@ -111,27 +98,34 @@ const ShellSidebar = ({ routes = [], basePath = '/', onNavigate, label = 'Naviga
|
|
|
111
98
|
* it should. `collapsible` is what that distinction is now. */
|
|
112
99
|
const hasChildren = route.children?.length > 0
|
|
113
100
|
|
|
101
|
+
/* A CHILDLESS ENTRY IS A ROW, not a chapter (2026-09-30): drawn at L2 it read as an
|
|
102
|
+
* empty category with a blank caret slot. */
|
|
103
|
+
if (!hasChildren) {
|
|
104
|
+
return (
|
|
105
|
+
<nav key={route.id} className="shell-nav-items">
|
|
106
|
+
<RailRow to={route.path ?? getSectionRootPath(route, basePath)} onNavigate={onNavigate}>
|
|
107
|
+
{route.label}
|
|
108
|
+
</RailRow>
|
|
109
|
+
</nav>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
114
113
|
return (
|
|
115
114
|
<div key={route.id} className="shell-nav-group">
|
|
116
115
|
<RailSection
|
|
117
116
|
level={2}
|
|
118
117
|
label={route.label}
|
|
119
|
-
count={
|
|
120
|
-
/* THE HEADER ALWAYS LINKS (2026-08-02)
|
|
121
|
-
*
|
|
122
|
-
* a dead header and its index had to ride as a row inside
|
|
123
|
-
* itself. RailSection already separates the two gestures —
|
|
124
|
-
* the label navigates, the rest of the row toggles — so a
|
|
125
|
-
* group can open its landing page and still collapse. */
|
|
118
|
+
count={route.children.length}
|
|
119
|
+
/* THE HEADER ALWAYS LINKS (2026-08-02): the label opens the chapter's home,
|
|
120
|
+
* the chevron folds. */
|
|
126
121
|
to={route.path ?? getSectionRootPath(route, basePath)}
|
|
127
|
-
|
|
128
|
-
collapsed={!!collapsedSections[route.id]}
|
|
122
|
+
collapsed={isFolded(route)}
|
|
129
123
|
onToggle={() => handleSectionClick(route)}
|
|
130
124
|
onNavigate={onNavigate}
|
|
131
125
|
icon={Icon}
|
|
132
126
|
>
|
|
133
127
|
<nav className="shell-nav-items">
|
|
134
|
-
{
|
|
128
|
+
{route.children.map((child) => (
|
|
135
129
|
<RailRow
|
|
136
130
|
key={child.id}
|
|
137
131
|
to={getChildPath(child, basePath)}
|