@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.11",
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.4",
18
- "@kolkrabbi/kol-component": "0.12.5",
19
- "@kolkrabbi/kol-framework": "0.5.8",
20
- "@kolkrabbi/kol-theme": "0.11.21"
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
- <button
73
- type="button"
74
- onClick={onToggle}
75
- aria-label={collapsed ? 'Expand Documentation' : 'Collapse Documentation'}
76
- className="flex items-center justify-center"
77
- style={{ height: '16.5px', marginBottom: '8px' }}
78
- >
79
- <Icon
80
- name="chevron-down"
81
- size={10}
82
- className={`stroke-[2.5] transition-transform ${collapsed ? '' : 'rotate-180'}`}
83
- />
84
- </button>
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]+-index$/)
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
- const isActive = (href) => location.pathname === href || location.pathname.startsWith(`${href}/`)
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+), WORKSHOP falls at the content-column edge. Both h-6.
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={basePath} className="shell-header-logo hidden md:flex shrink-0 items-center text-emphasis lg:w-64">
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
- <Asset name="wordmark-workshop" title="Workshop" className="inline-flex text-emphasis [&>svg]:h-6 [&>svg]:w-auto" />
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
- <button
118
- type="button"
119
- onClick={() => setIsSearchOpen(true)}
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
- setNavCollapsed((p) => !p)
141
- setTocCollapsed((p) => !p)
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
- isOpen={isNavDrawerOpen}
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
- isOpen={isSearchOpen}
193
- onClose={() => setIsSearchOpen(false)}
194
- routes={routes}
195
- basePath={basePath}
196
- items={searchItems}
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
- <button
81
- type="button"
82
- onClick={handleToggle}
83
- aria-label={navCollapsed ? `Expand ${label}` : `Collapse ${label}`}
84
- className="flex items-center justify-center"
85
- style={{ height: '16.5px', marginBottom: '8px' }}
86
- >
87
- <Icon
88
- name="chevron-down"
89
- size={10}
90
- className={`stroke-[2.5] transition-transform ${navCollapsed ? '' : 'rotate-180'}`}
91
- />
92
- </button>
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={viewMode === 'graph' ? 'view-list' : 'polygon'}
45
- onClick={() => setViewMode(viewMode === 'graph' ? 'list' : 'graph')}
46
- aria-label={viewMode === 'graph' ? 'List view' : 'Graph view'}
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
- <Button
79
- variant="outline"
80
- quiet
81
- size="sm"
82
- iconOnly="x"
83
- onClick={() => setSearch('')}
84
- aria-label="Clear search"
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