@kolkrabbi/kol-workshop 0.1.11 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-workshop",
3
- "version": "0.1.11",
3
+ "version": "0.2.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,11 +13,11 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "d3": "^7.9.0",
16
+ "@kolkrabbi/kol-component": "0.12.19",
16
17
  "@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"
18
+ "@kolkrabbi/kol-icons": "0.8.10",
19
+ "@kolkrabbi/kol-framework": "0.6.2",
20
+ "@kolkrabbi/kol-theme": "0.12.0"
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,7 +41,7 @@ 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
  )
@@ -50,6 +51,7 @@ const ShellLayout = ({ routes = [], basePath = '/', brandLogoSrc, brandLogoAlt =
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()
@@ -105,25 +107,34 @@ const ShellLayout = ({ routes = [], basePath = '/', brandLogoSrc, brandLogoAlt =
105
107
  </Link>
106
108
  ) : (
107
109
  // 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.
110
+ // nav column at lg+) and links to the SITE home; WORKSHOP falls at the
111
+ // content-column edge and links to the shell root. Both h-6.
109
112
  <>
110
- <Link to={basePath} className="shell-header-logo hidden md:flex shrink-0 items-center text-emphasis lg:w-64">
113
+ <Link to="/" className="shell-header-logo hidden md:flex shrink-0 items-center text-emphasis lg:w-64">
111
114
  <Asset name="kol-wordmark" title="Kolkrabbi" className="inline-flex [&>svg]:h-6 [&>svg]:w-auto" />
112
115
  </Link>
113
- <Asset name="wordmark-workshop" title="Workshop" className="inline-flex text-emphasis [&>svg]:h-6 [&>svg]:w-auto" />
116
+ <Link to={basePath} className="shell-header-logo flex items-center text-emphasis">
117
+ <Asset name="wordmark-workshop" title="Workshop" className="inline-flex [&>svg]:h-6 [&>svg]:w-auto" />
118
+ </Link>
114
119
  </>
115
120
  )
116
121
  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>
122
+ <Tooltip label="Search">
123
+ <Button variant="ghost" quiet iconOnly="search" iconSize={18} onClick={() => setIsSearchOpen(true)} aria-label="Search" />
124
+ </Tooltip>
125
125
  )
126
126
 
127
+ /* searchItems ({ id, label, href, sectionLabel?, tags?, headings?, keywords? })
128
+ * → the overlay's row shape ({ id, label, group?, hint? }), matched by the
129
+ * engine. hint surfaces WHY a row matched when the label didn't. */
130
+ const searchResults = matchSearchItems(searchItems ?? [], searchQuery).map((item, i) => ({
131
+ id: item.id ?? item.href ?? `${item.label}-${i}`,
132
+ label: item.label,
133
+ group: item.sectionLabel ?? item.group,
134
+ hint: item.matchedHeading ?? item.matchedKeyword ?? item.hint,
135
+ href: item.href,
136
+ }))
137
+
127
138
  return (
128
139
  <ShellTocContext.Provider value={setTocContent}>
129
140
  <ShellFullHeightContext.Provider value={setIsFullHeight}>
@@ -137,8 +148,12 @@ const ShellLayout = ({ routes = [], basePath = '/', brandLogoSrc, brandLogoAlt =
137
148
  actions={searchTrigger}
138
149
  onMenuClick={() => {
139
150
  if (window.matchMedia('(min-width: 1024px)').matches) {
140
- setNavCollapsed((p) => !p)
141
- setTocCollapsed((p) => !p)
151
+ /* Both rails to ONE target state — independent `!p` flips made
152
+ * them oppose each other once the dock buttons had diverged
153
+ * them. Collapse if anything is open, else reopen both. */
154
+ const collapse = !navCollapsed || !tocCollapsed
155
+ setNavCollapsed(collapse)
156
+ setTocCollapsed(collapse)
142
157
  } else {
143
158
  setIsNavDrawerOpen(true)
144
159
  }
@@ -178,8 +193,10 @@ const ShellLayout = ({ routes = [], basePath = '/', brandLogoSrc, brandLogoAlt =
178
193
  </div>
179
194
  </div>
180
195
 
196
+ {/* `open`, not `isOpen` — the old prop name silently kept the drawer
197
+ * shut, so there was no navigation at all below lg. */}
181
198
  <ShellDrawer
182
- isOpen={isNavDrawerOpen}
199
+ open={isNavDrawerOpen}
183
200
  onClose={() => setIsNavDrawerOpen(false)}
184
201
  >
185
202
  {renderSidebar
@@ -188,12 +205,22 @@ const ShellLayout = ({ routes = [], basePath = '/', brandLogoSrc, brandLogoAlt =
188
205
  }
189
206
  </ShellDrawer>
190
207
 
208
+ {/* The overlay owns no filtering — the shell holds the query and
209
+ * feeds it results (the pre-0.12 API `isOpen/routes/items` matched
210
+ * nothing, so ⌘K rendered an empty box and searchItems was dead).
211
+ * Selecting routes to item.href; matchSearchItems is the engine's. */}
191
212
  <ShellSearchOverlay
192
- isOpen={isSearchOpen}
193
- onClose={() => setIsSearchOpen(false)}
194
- routes={routes}
195
- basePath={basePath}
196
- items={searchItems}
213
+ open={isSearchOpen}
214
+ onClose={() => { setIsSearchOpen(false); setSearchQuery('') }}
215
+ query={searchQuery}
216
+ onQueryChange={setSearchQuery}
217
+ results={searchResults}
218
+ onSelect={(item) => {
219
+ setIsSearchOpen(false)
220
+ setSearchQuery('')
221
+ if (item?.href) navigate(item.href)
222
+ }}
223
+ placeholder="Search…"
197
224
  />
198
225
  </div>
199
226
  </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