@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-workshop",
3
- "version": "0.33.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.228.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.30.0",
31
- "@kolkrabbi/kol-framework": "^0.48.0",
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-component": "^0.230.0"
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'
@@ -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
- * @deprecated — use kol-component's `ContentRow` (variant `article`, `media={false}`). ResultRow was
12
- * minted 2026-09-30 for the search page while the design system already shipped its row; it
13
- * forwards to ContentRow and drops at 30 days on the retirement ledger. `variant` is ignored.
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
- export default function ResultRow({ to, title, meta, description }) {
16
- const navigate = useNavigate()
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
- <ContentRow
19
- variant="article"
20
- media={false}
21
- href={to}
22
- onNavigate={(e) => { e.preventDefault(); navigate(to) }}
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 { useNavigate, useSearchParams } from 'react-router-dom'
3
- import { Button, ContentRow, EmptyState, Input, Pill } from '@kolkrabbi/kol-component'
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
- {/* THE SHIPPED ROW (2026-09-30): kol-component's ContentRow, the row form of the
129
- * content-card system — not a row minted here. Article form, no cover. */}
130
- <ContentRow
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
- body={r.item.description}
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
  ))}
@@ -1 +1,2 @@
1
1
  export { default as SearchPage } from './SearchPage.jsx'
2
+ export { default as ResultRow } from './ResultRow.jsx'
@@ -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
- const labelNode = to && level === 2 ? (
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
- const [collapsedSections, setCollapsedSections] = useState(() => {
41
- const initial = {}
42
- routes.forEach((route) => {
43
- const sectionPath = getSectionRootPath(route, basePath)
44
- const isActive =
45
- sectionPath === basePath
46
- ? normalizedPath === basePath
47
- : normalizedPath === sectionPath || normalizedPath.startsWith(sectionPath + '/')
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
- /* THE RAIL FOLLOWS YOU (2026-09-28, user: "the sidebar doesnt highlight that
54
- * part, which it probably should by closing/collapsing other categories").
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 active = routes.filter(isRouteActive)
69
- if (!active.length) return
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={hasChildren ? route.children.length : undefined}
120
- /* THE HEADER ALWAYS LINKS (2026-08-02). It used to link only
121
- * when the group had NO children, so a chapter with pages had
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
- collapsible={hasChildren}
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
- {(route.children ?? []).map((child) => (
128
+ {route.children.map((child) => (
135
129
  <RailRow
136
130
  key={child.id}
137
131
  to={getChildPath(child, basePath)}