@kolkrabbi/kol-workshop 0.1.11 → 0.2.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.1
|
|
3
|
+
"version": "0.2.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-icons": "0.8.
|
|
18
|
-
"@kolkrabbi/kol-
|
|
19
|
-
"@kolkrabbi/kol-
|
|
20
|
-
"@kolkrabbi/kol-
|
|
17
|
+
"@kolkrabbi/kol-icons": "0.8.10",
|
|
18
|
+
"@kolkrabbi/kol-framework": "0.6.2",
|
|
19
|
+
"@kolkrabbi/kol-theme": "0.12.0",
|
|
20
|
+
"@kolkrabbi/kol-component": "0.12.19"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"react": "^18.3.0 || ^19.0.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'react'
|
|
2
2
|
import { Link, useLocation } from 'react-router-dom'
|
|
3
3
|
import { ShellSidebar } from '../shell'
|
|
4
|
-
import { Icon } from '@kolkrabbi/kol-component'
|
|
4
|
+
import { Icon, Tooltip } from '@kolkrabbi/kol-component'
|
|
5
5
|
import {
|
|
6
6
|
extractDocNumber,
|
|
7
7
|
cleanTitle,
|
|
@@ -69,19 +69,22 @@ const DocsSidebar = ({ inventory = [], docHref, basePath, onNavigate, collapsed,
|
|
|
69
69
|
}}>
|
|
70
70
|
Documentation
|
|
71
71
|
</Link>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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>
|
|
85
88
|
</div>
|
|
86
89
|
|
|
87
90
|
{!collapsed && <div className="space-y-4">
|
|
@@ -211,7 +211,11 @@ const DocumentationReader = ({
|
|
|
211
211
|
return h1Block?.content || null
|
|
212
212
|
}, [introBlocks])
|
|
213
213
|
|
|
214
|
+
/* The reader is usable OUTSIDE a ShellLayout (a plain page, a lobby route):
|
|
215
|
+
* ShellTocContext then defaults to null, so calling it unguarded threw.
|
|
216
|
+
* No shell → no right rail, and the reader just renders its article. */
|
|
214
217
|
useEffect(() => {
|
|
218
|
+
if (!setTocContent) return undefined
|
|
215
219
|
setTocContent(
|
|
216
220
|
<DocReaderSidebar
|
|
217
221
|
key={docId}
|
|
@@ -93,18 +93,30 @@ export const getTagColor = (tag) => {
|
|
|
93
93
|
return TAG_COLORS[Math.abs(hash) % TAG_COLORS.length]
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/** Group docs by major version number.
|
|
96
|
+
/** Group docs by major version number.
|
|
97
|
+
*
|
|
98
|
+
* Three id dialects are grouped: dotted (`0.0.1-slug`), folder-index
|
|
99
|
+
* (`04-pages-index`), and PLAIN NUMBERED (`04-layout-breakpoints`) — the last
|
|
100
|
+
* one was previously unmatched, so a vault of `NN-slug.md` files produced a
|
|
101
|
+
* tree holding only its INDEX files and every real doc was search-only
|
|
102
|
+
* (fixed 2026-07-30). Multi-word folder names work too: the old
|
|
103
|
+
* `^(\d+)-[a-z]+-index$` rejected any hyphen, silently dropping ids like
|
|
104
|
+
* `09-ds-adoption-index`.
|
|
105
|
+
*/
|
|
97
106
|
export const groupDocsByMajor = (docs) => {
|
|
98
107
|
const groups = {}
|
|
99
108
|
docs.forEach((d) => {
|
|
100
109
|
const majorMatch = d.id.match(/^(\d+)\./)
|
|
101
|
-
const indexMatch = d.id.match(/^(\d+)-[a-z]
|
|
110
|
+
const indexMatch = d.id.match(/^(\d+)-[a-z][a-z0-9-]*-index$/)
|
|
111
|
+
const plainMatch = d.id.match(/^(\d+)-/)
|
|
102
112
|
|
|
103
113
|
let major = null
|
|
104
114
|
if (majorMatch) {
|
|
105
115
|
major = majorMatch[1]
|
|
106
116
|
} else if (indexMatch) {
|
|
107
117
|
major = indexMatch[1].replace(/^0/, '')
|
|
118
|
+
} else if (plainMatch) {
|
|
119
|
+
major = plainMatch[1].replace(/^0(?=\d)/, '')
|
|
108
120
|
} else if (isIndexFile(d.id)) {
|
|
109
121
|
const titleMatch = d.title.match(/^(\d+)\./)
|
|
110
122
|
if (titleMatch) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createContext, useState, useEffect, Suspense } from 'react'
|
|
2
2
|
import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom'
|
|
3
|
-
import { Icon } from '@kolkrabbi/kol-icons'
|
|
4
3
|
import { ShellHeader } from '@kolkrabbi/kol-framework'
|
|
5
4
|
import ShellSidebar from './ShellSidebar.jsx'
|
|
6
|
-
import { ShellDrawer, ShellSearchOverlay } from '@kolkrabbi/kol-component'
|
|
5
|
+
import { Button, ShellDrawer, ShellSearchOverlay, Tooltip } from '@kolkrabbi/kol-component'
|
|
6
|
+
import { matchSearchItems } from '../engine/search.js'
|
|
7
7
|
import { Asset } from '@kolkrabbi/kol-brand/svg'
|
|
8
8
|
|
|
9
9
|
// Pages can register right-rail TOC content via this context.
|
|
@@ -22,13 +22,14 @@ export const ShellFullHeightContext = createContext(null)
|
|
|
22
22
|
export const ShellTocCollapsedContext = createContext(null)
|
|
23
23
|
|
|
24
24
|
const NavColumn = ({ children }) => (
|
|
25
|
-
<aside className="shell-sidebar-sticky hidden lg:block shrink-0 h-full min-h-0 overflow-y-auto overscroll-none pt-6 md:pt-6 lg:pt-8 pb-8">
|
|
25
|
+
<aside aria-label="Navigation" className="shell-sidebar-sticky hidden lg:block shrink-0 h-full min-h-0 overflow-y-auto overscroll-none pt-6 md:pt-6 lg:pt-8 pb-8">
|
|
26
26
|
{children}
|
|
27
27
|
</aside>
|
|
28
28
|
)
|
|
29
29
|
|
|
30
30
|
const MainColumn = ({ children, fullHeight }) => (
|
|
31
31
|
<main
|
|
32
|
+
id="main"
|
|
32
33
|
className={`w-full min-w-0 h-full min-h-0 ${fullHeight ? 'overflow-hidden flex flex-col' : 'overflow-y-auto overscroll-none'}`}
|
|
33
34
|
style={fullHeight ? undefined : { scrollbarGutter: 'stable' }}
|
|
34
35
|
>
|
|
@@ -40,16 +41,17 @@ const MainColumn = ({ children, fullHeight }) => (
|
|
|
40
41
|
)
|
|
41
42
|
|
|
42
43
|
const TocColumn = ({ children }) => (
|
|
43
|
-
<aside className="shell-sidebar-sticky hidden lg:block shrink-0 h-full min-h-0 overflow-y-auto overscroll-none pt-6 md:pt-6 lg:pt-8 pb-8">
|
|
44
|
+
<aside aria-label="Table of contents" className="shell-sidebar-sticky hidden lg:block shrink-0 h-full min-h-0 overflow-y-auto overscroll-none pt-6 md:pt-6 lg:pt-8 pb-8">
|
|
44
45
|
{children}
|
|
45
46
|
</aside>
|
|
46
47
|
)
|
|
47
48
|
|
|
48
|
-
const ShellLayout = ({ routes = [], basePath = '/', brandLogoSrc, brandLogoAlt = '', renderSidebar, searchItems, defaultTocContent }) => {
|
|
49
|
+
const ShellLayout = ({ routes = [], basePath = '/', brandLogoSrc, brandLogoAlt = '', renderSidebar, searchItems, defaultTocContent, isActive: isActiveProp }) => {
|
|
49
50
|
const [isNavDrawerOpen, setIsNavDrawerOpen] = useState(false)
|
|
50
51
|
const [navCollapsed, setNavCollapsed] = useState(false)
|
|
51
52
|
const [tocCollapsed, setTocCollapsed] = useState(false)
|
|
52
53
|
const [isSearchOpen, setIsSearchOpen] = useState(false)
|
|
54
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
53
55
|
const [tocContent, setTocContent] = useState(null)
|
|
54
56
|
const [isFullHeight, setIsFullHeight] = useState(false)
|
|
55
57
|
const location = useLocation()
|
|
@@ -92,7 +94,10 @@ const ShellLayout = ({ routes = [], basePath = '/', brandLogoSrc, brandLogoAlt =
|
|
|
92
94
|
href: r.path ? joinPath(r.path) : basePath,
|
|
93
95
|
icon: r.icon,
|
|
94
96
|
}))
|
|
95
|
-
|
|
97
|
+
/* Prefix matching is the default, but a consumer can override it: a tab whose
|
|
98
|
+
* href targets a CHILD page (Docs → /docs/shell-and-layout) must still light
|
|
99
|
+
* up across the whole /docs prefix, which self-matching can't express. */
|
|
100
|
+
const isActive = isActiveProp ?? ((href) => location.pathname === href || location.pathname.startsWith(`${href}/`))
|
|
96
101
|
const handleNavigate = (event, item) => {
|
|
97
102
|
if (item.href) {
|
|
98
103
|
event.preventDefault()
|
|
@@ -105,25 +110,34 @@ const ShellLayout = ({ routes = [], basePath = '/', brandLogoSrc, brandLogoAlt =
|
|
|
105
110
|
</Link>
|
|
106
111
|
) : (
|
|
107
112
|
// Two separate wordmarks: KOLKRABBI holds the logo slot (reserves the 256px
|
|
108
|
-
// nav column at lg+)
|
|
113
|
+
// nav column at lg+) and links to the SITE home; WORKSHOP falls at the
|
|
114
|
+
// content-column edge and links to the shell root. Both h-6.
|
|
109
115
|
<>
|
|
110
|
-
<Link to=
|
|
116
|
+
<Link to="/" className="shell-header-logo hidden md:flex shrink-0 items-center text-emphasis lg:w-64">
|
|
111
117
|
<Asset name="kol-wordmark" title="Kolkrabbi" className="inline-flex [&>svg]:h-6 [&>svg]:w-auto" />
|
|
112
118
|
</Link>
|
|
113
|
-
<
|
|
119
|
+
<Link to={basePath} className="shell-header-logo flex items-center text-emphasis">
|
|
120
|
+
<Asset name="wordmark-workshop" title="Workshop" className="inline-flex [&>svg]:h-6 [&>svg]:w-auto" />
|
|
121
|
+
</Link>
|
|
114
122
|
</>
|
|
115
123
|
)
|
|
116
124
|
const searchTrigger = (
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
aria-label="Search"
|
|
121
|
-
className="flex h-9 w-9 items-center justify-center rounded p-0 bg-transparent border-0 cursor-pointer transition-colors text-fg-64 hover:bg-fg-08 hover:text-emphasis"
|
|
122
|
-
>
|
|
123
|
-
<Icon name="search" size={18} />
|
|
124
|
-
</button>
|
|
125
|
+
<Tooltip label="Search">
|
|
126
|
+
<Button variant="ghost" quiet iconOnly="search" iconSize={18} onClick={() => setIsSearchOpen(true)} aria-label="Search" />
|
|
127
|
+
</Tooltip>
|
|
125
128
|
)
|
|
126
129
|
|
|
130
|
+
/* searchItems ({ id, label, href, sectionLabel?, tags?, headings?, keywords? })
|
|
131
|
+
* → the overlay's row shape ({ id, label, group?, hint? }), matched by the
|
|
132
|
+
* engine. hint surfaces WHY a row matched when the label didn't. */
|
|
133
|
+
const searchResults = matchSearchItems(searchItems ?? [], searchQuery).map((item, i) => ({
|
|
134
|
+
id: item.id ?? item.href ?? `${item.label}-${i}`,
|
|
135
|
+
label: item.label,
|
|
136
|
+
group: item.sectionLabel ?? item.group,
|
|
137
|
+
hint: item.matchedHeading ?? item.matchedKeyword ?? item.hint,
|
|
138
|
+
href: item.href,
|
|
139
|
+
}))
|
|
140
|
+
|
|
127
141
|
return (
|
|
128
142
|
<ShellTocContext.Provider value={setTocContent}>
|
|
129
143
|
<ShellFullHeightContext.Provider value={setIsFullHeight}>
|
|
@@ -137,8 +151,12 @@ const ShellLayout = ({ routes = [], basePath = '/', brandLogoSrc, brandLogoAlt =
|
|
|
137
151
|
actions={searchTrigger}
|
|
138
152
|
onMenuClick={() => {
|
|
139
153
|
if (window.matchMedia('(min-width: 1024px)').matches) {
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
/* Both rails to ONE target state — independent `!p` flips made
|
|
155
|
+
* them oppose each other once the dock buttons had diverged
|
|
156
|
+
* them. Collapse if anything is open, else reopen both. */
|
|
157
|
+
const collapse = !navCollapsed || !tocCollapsed
|
|
158
|
+
setNavCollapsed(collapse)
|
|
159
|
+
setTocCollapsed(collapse)
|
|
142
160
|
} else {
|
|
143
161
|
setIsNavDrawerOpen(true)
|
|
144
162
|
}
|
|
@@ -178,8 +196,10 @@ const ShellLayout = ({ routes = [], basePath = '/', brandLogoSrc, brandLogoAlt =
|
|
|
178
196
|
</div>
|
|
179
197
|
</div>
|
|
180
198
|
|
|
199
|
+
{/* `open`, not `isOpen` — the old prop name silently kept the drawer
|
|
200
|
+
* shut, so there was no navigation at all below lg. */}
|
|
181
201
|
<ShellDrawer
|
|
182
|
-
|
|
202
|
+
open={isNavDrawerOpen}
|
|
183
203
|
onClose={() => setIsNavDrawerOpen(false)}
|
|
184
204
|
>
|
|
185
205
|
{renderSidebar
|
|
@@ -188,12 +208,22 @@ const ShellLayout = ({ routes = [], basePath = '/', brandLogoSrc, brandLogoAlt =
|
|
|
188
208
|
}
|
|
189
209
|
</ShellDrawer>
|
|
190
210
|
|
|
211
|
+
{/* The overlay owns no filtering — the shell holds the query and
|
|
212
|
+
* feeds it results (the pre-0.12 API `isOpen/routes/items` matched
|
|
213
|
+
* nothing, so ⌘K rendered an empty box and searchItems was dead).
|
|
214
|
+
* Selecting routes to item.href; matchSearchItems is the engine's. */}
|
|
191
215
|
<ShellSearchOverlay
|
|
192
|
-
|
|
193
|
-
onClose={() => setIsSearchOpen(false)}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
216
|
+
open={isSearchOpen}
|
|
217
|
+
onClose={() => { setIsSearchOpen(false); setSearchQuery('') }}
|
|
218
|
+
query={searchQuery}
|
|
219
|
+
onQueryChange={setSearchQuery}
|
|
220
|
+
results={searchResults}
|
|
221
|
+
onSelect={(item) => {
|
|
222
|
+
setIsSearchOpen(false)
|
|
223
|
+
setSearchQuery('')
|
|
224
|
+
if (item?.href) navigate(item.href)
|
|
225
|
+
}}
|
|
226
|
+
placeholder="Search…"
|
|
197
227
|
/>
|
|
198
228
|
</div>
|
|
199
229
|
</ShellTocCollapsedContext.Provider>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
2
|
import { NavLink, Link, useLocation } from 'react-router-dom'
|
|
3
|
-
import { Icon } from '@kolkrabbi/kol-component'
|
|
3
|
+
import { Icon, Tooltip } from '@kolkrabbi/kol-component'
|
|
4
4
|
|
|
5
5
|
const getSectionRootPath = (route, basePath) => {
|
|
6
6
|
if (route.path !== undefined && route.path !== null) {
|
|
@@ -77,19 +77,22 @@ const ShellSidebar = ({ routes = [], basePath = '/', onNavigate, label = 'Naviga
|
|
|
77
77
|
) : (
|
|
78
78
|
<button type="button" className="kol-helper-10 text-meta" onClick={handleToggle}>{label}</button>
|
|
79
79
|
)}
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
<Tooltip label={navCollapsed ? `Expand ${label}` : `Collapse ${label}`}>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={handleToggle}
|
|
84
|
+
aria-label={navCollapsed ? `Expand ${label}` : `Collapse ${label}`}
|
|
85
|
+
aria-expanded={!navCollapsed}
|
|
86
|
+
className="flex items-center justify-center"
|
|
87
|
+
style={{ height: '16.5px', marginBottom: '8px' }}
|
|
88
|
+
>
|
|
89
|
+
<Icon
|
|
90
|
+
name="chevron-down"
|
|
91
|
+
size={10}
|
|
92
|
+
className={`stroke-[2.5] transition-transform ${navCollapsed ? '' : 'rotate-180'}`}
|
|
93
|
+
/>
|
|
94
|
+
</button>
|
|
95
|
+
</Tooltip>
|
|
93
96
|
</div>
|
|
94
97
|
|
|
95
98
|
{!navCollapsed && <div className="space-y-4">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react'
|
|
2
2
|
import { Link } from 'react-router-dom'
|
|
3
|
-
import { Input, Tag, Button } from '@kolkrabbi/kol-component'
|
|
3
|
+
import { Input, Tag, Button, Tooltip } from '@kolkrabbi/kol-component'
|
|
4
4
|
import { useTagMode } from './TagModeContext.jsx'
|
|
5
5
|
import TagGraph from './TagGraph.jsx'
|
|
6
6
|
import { extractDocNumber, cleanTitle, getTagColor } from '../engine/index.js'
|
|
@@ -37,29 +37,34 @@ const TagModeOverlay = () => {
|
|
|
37
37
|
<div className="max-w-[864px] mx-auto">
|
|
38
38
|
<div className="flex items-center justify-start gap-1 mb-3">
|
|
39
39
|
{hasFilters && (
|
|
40
|
+
<Tooltip label={viewMode === 'graph' ? 'List view' : 'Graph view'}>
|
|
41
|
+
<Button
|
|
42
|
+
variant="outline"
|
|
43
|
+
quiet
|
|
44
|
+
size="sm"
|
|
45
|
+
iconOnly={viewMode === 'graph' ? 'view-list' : 'polygon'}
|
|
46
|
+
onClick={() => setViewMode(viewMode === 'graph' ? 'list' : 'graph')}
|
|
47
|
+
aria-label={viewMode === 'graph' ? 'List view' : 'Graph view'}
|
|
48
|
+
/>
|
|
49
|
+
</Tooltip>
|
|
50
|
+
)}
|
|
51
|
+
<Tooltip label="Close tag mode">
|
|
40
52
|
<Button
|
|
41
53
|
variant="outline"
|
|
42
54
|
quiet
|
|
43
55
|
size="sm"
|
|
44
|
-
iconOnly=
|
|
45
|
-
onClick={
|
|
46
|
-
aria-label=
|
|
56
|
+
iconOnly="x"
|
|
57
|
+
onClick={closeTagMode}
|
|
58
|
+
aria-label="Close tag mode"
|
|
47
59
|
/>
|
|
48
|
-
|
|
49
|
-
<Button
|
|
50
|
-
variant="outline"
|
|
51
|
-
quiet
|
|
52
|
-
size="sm"
|
|
53
|
-
iconOnly="x"
|
|
54
|
-
onClick={closeTagMode}
|
|
55
|
-
aria-label="Close tag mode"
|
|
56
|
-
/>
|
|
60
|
+
</Tooltip>
|
|
57
61
|
</div>
|
|
58
62
|
|
|
59
63
|
<div className="dash-card flex flex-col gap-8 -mt-4">
|
|
60
64
|
<div className="flex items-center gap-2" style={{ width: '100%', alignSelf: 'stretch' }}>
|
|
61
65
|
<Input
|
|
62
66
|
type="text"
|
|
67
|
+
aria-label="Search tags"
|
|
63
68
|
placeholder="Search tags…"
|
|
64
69
|
value={search}
|
|
65
70
|
onChange={(e) => setSearch(e.target.value)}
|
|
@@ -75,14 +80,16 @@ const TagModeOverlay = () => {
|
|
|
75
80
|
autoFocus
|
|
76
81
|
/>
|
|
77
82
|
{search && (
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
<Tooltip label="Clear search">
|
|
84
|
+
<Button
|
|
85
|
+
variant="outline"
|
|
86
|
+
quiet
|
|
87
|
+
size="sm"
|
|
88
|
+
iconOnly="x"
|
|
89
|
+
onClick={() => setSearch('')}
|
|
90
|
+
aria-label="Clear search"
|
|
91
|
+
/>
|
|
92
|
+
</Tooltip>
|
|
86
93
|
)}
|
|
87
94
|
</div>
|
|
88
95
|
|