@kolkrabbi/kol-workshop 0.35.0 → 0.36.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.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Docs viewer, shell and tag system",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"d3": "^7.9.0",
|
|
16
|
-
"@kolkrabbi/kol-brand": "^0.1.3",
|
|
17
16
|
"@kolkrabbi/kol-markdown": "^0.1.2",
|
|
18
|
-
"@kolkrabbi/kol-search": "^0.2.0"
|
|
17
|
+
"@kolkrabbi/kol-search": "^0.2.0",
|
|
18
|
+
"@kolkrabbi/kol-brand": "^0.1.3"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@kolkrabbi/kol-component": ">=0.
|
|
21
|
+
"@kolkrabbi/kol-component": ">=0.235.0",
|
|
22
22
|
"@kolkrabbi/kol-framework": ">=0.45.0",
|
|
23
|
-
"@kolkrabbi/kol-theme": ">=0.
|
|
23
|
+
"@kolkrabbi/kol-theme": ">=0.162.0",
|
|
24
24
|
"@kolkrabbi/kol-icons": ">=0.18.0",
|
|
25
25
|
"gsap": "^3.13.0",
|
|
26
26
|
"react": "^18.3.0 || ^19.0.0",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@kolkrabbi/kol-component": "^0.
|
|
32
|
-
"@kolkrabbi/kol-
|
|
31
|
+
"@kolkrabbi/kol-component": "^0.235.0",
|
|
32
|
+
"@kolkrabbi/kol-theme": "^0.162.0",
|
|
33
33
|
"@kolkrabbi/kol-framework": "^0.48.0",
|
|
34
|
-
"@kolkrabbi/kol-
|
|
34
|
+
"@kolkrabbi/kol-icons": "^0.32.0"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"src",
|
|
@@ -331,7 +331,9 @@ const DocumentationReader = ({
|
|
|
331
331
|
setTocContent(
|
|
332
332
|
<DocReaderSidebar
|
|
333
333
|
key={docId}
|
|
334
|
-
|
|
334
|
+
/* THE TITLE LEADS THE OUTLINE (2026-10-01 — user: "something is always on this page"): a
|
|
335
|
+
* chapter index with no h2 used to hand the rail an empty list */
|
|
336
|
+
toc={docTitle ? [{ id: 'page-top', label: docTitle }, ...toc] : toc}
|
|
335
337
|
allTags={allTags}
|
|
336
338
|
topTags={topTags}
|
|
337
339
|
related={related}
|
|
@@ -342,7 +344,7 @@ const DocumentationReader = ({
|
|
|
342
344
|
/>
|
|
343
345
|
)
|
|
344
346
|
return () => setTocContent(null)
|
|
345
|
-
}, [rail, setTocContent, docId, toc, allTags, topTags, related, docsIndex, components, docFilePath])
|
|
347
|
+
}, [rail, setTocContent, docId, docTitle, toc, allTags, topTags, related, docsIndex, components, docFilePath])
|
|
346
348
|
|
|
347
349
|
if (!doc) {
|
|
348
350
|
return (
|
|
@@ -365,7 +367,7 @@ const DocumentationReader = ({
|
|
|
365
367
|
<DocsArticle>
|
|
366
368
|
{showFrontmatter && <DocsFrontmatter metadata={doc.metadata} docId={docId} />}
|
|
367
369
|
{docTitle && (
|
|
368
|
-
<h1 className="kol-doc-heading">{docTitle}</h1>
|
|
370
|
+
<h1 id="page-top" className="kol-doc-heading scroll-mt-20">{docTitle}</h1>
|
|
369
371
|
)}
|
|
370
372
|
{/* Render intro blocks (excluding H1 which is docTitle) */}
|
|
371
373
|
{introBlocks.filter(b => b.type !== 'heading1').map((block, index) =>
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { Fragment, useMemo, useState } from 'react'
|
|
1
|
+
import { Fragment, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { getTagColor } from '@kolkrabbi/kol-markdown'
|
|
3
|
-
import { useSearchParams } from 'react-router-dom'
|
|
4
|
-
import { EmptyState, Input,
|
|
3
|
+
import { useNavigate, useSearchParams } from 'react-router-dom'
|
|
4
|
+
import { EmptyState, Input, Tag } from '@kolkrabbi/kol-component'
|
|
5
5
|
import ResultRow from './ResultRow.jsx'
|
|
6
6
|
import { createIndex, search, parseQuery } from '@kolkrabbi/kol-search'
|
|
7
|
-
import { DocHeader, DocSection } from '../docs/DocKit.jsx'
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* SearchPage — The search results page. THE results page (2026-09-28). Enter in the shell's palette
|
|
@@ -20,8 +19,10 @@ import { DocHeader, DocSection } from '../docs/DocKit.jsx'
|
|
|
20
19
|
*
|
|
21
20
|
* @param {Array} items kol-search items: { id, title, href, kind?, space?, category?, tags?, headings?, keywords?, description?, date? }
|
|
22
21
|
* @param {Array} [spaces] the scope row, `[{ value, label }]` in header order — `value` is an item's `space`
|
|
23
|
-
* @param {Array} [facets] `[[field, label]]`
|
|
22
|
+
* @param {Array} [facets] `[[field, label]]` — the FIRST is the visible filter row, the rest fold behind `+`
|
|
23
|
+
* with the scope and the read-as line (default kind · category · tags)
|
|
24
24
|
* @param {number} [limit=60]
|
|
25
|
+
* @param {string} [resultsPath] the route that shows results, when this box can also sit on another page
|
|
25
26
|
*/
|
|
26
27
|
const DEFAULT_FACETS = [['kind', 'Kind'], ['category', 'Category'], ['tags', 'Tags']]
|
|
27
28
|
const PREFIX = { space: 'in', kind: 'kind', category: 'cat', tags: 'tag' }
|
|
@@ -46,13 +47,18 @@ const readToken = (t) => (t.kind === 'term'
|
|
|
46
47
|
? `${t.field} ${t.value}`
|
|
47
48
|
: `${t.negate ? 'not ' : ''}${t.field === 'tags' ? 'tag' : t.field}: ${t.value}${t.via === 'smart' ? ' (from the word)' : ''}`)
|
|
48
49
|
|
|
49
|
-
export default function SearchPage({ items = [], spaces = [], facets = DEFAULT_FACETS, limit = 60, rowVariant = 'underline' }) {
|
|
50
|
+
export default function SearchPage({ items = [], spaces = [], facets = DEFAULT_FACETS, limit = 60, rowVariant = 'underline', resultsPath }) {
|
|
50
51
|
const [params, setParams] = useSearchParams()
|
|
52
|
+
const navigate = useNavigate()
|
|
51
53
|
const q = params.get('q') ?? ''
|
|
52
54
|
const index = useMemo(() => createIndex(items), [items])
|
|
53
55
|
const out = useMemo(() => search(index, q, { limit }), [index, q, limit])
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
/* `resultsPath`: the query is written to THAT route — a box on a search home sends its first
|
|
58
|
+
* keystroke straight to the results page instead of filtering in place */
|
|
59
|
+
const setQ = (next) => (resultsPath
|
|
60
|
+
? navigate(next.trim() ? `${resultsPath}?q=${encodeURIComponent(next)}` : resultsPath, { replace: true })
|
|
61
|
+
: setParams(next.trim() ? { q: next } : {}, { replace: true }))
|
|
56
62
|
/* a pick adds its token; picking it again takes the token back out */
|
|
57
63
|
const toggleToken = (field, value) => {
|
|
58
64
|
const tok = token(field, value)
|
|
@@ -61,29 +67,48 @@ export default function SearchPage({ items = [], spaces = [], facets = DEFAULT_F
|
|
|
61
67
|
setQ((has ? parts.filter((p) => p !== tok) : [...parts, tok]).join(' '))
|
|
62
68
|
}
|
|
63
69
|
const removeToken = (raw) => setQ(q.split(/\s+/).filter((p) => p && p !== raw).join(' '))
|
|
70
|
+
/* THE BOX OWNS ITS TEXT (2026-10-01). The URL is still the state, but a navigation lands a beat
|
|
71
|
+
* after the keystroke, and an input controlled by the URL alone dropped the letter typed in
|
|
72
|
+
* between ("tag" arrived as "tg"). The box shows what was typed; the URL follows; a query that
|
|
73
|
+
* changes from OUTSIDE (a chip, Back, a link) is read back into the box. */
|
|
74
|
+
const [text, setText] = useState(q)
|
|
75
|
+
const pushed = useRef(q)
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (q !== pushed.current) { pushed.current = q; setText(q) }
|
|
78
|
+
}, [q])
|
|
79
|
+
const type = (next) => { pushed.current = next.trim() ? next : ''; setText(next); setQ(next) }
|
|
64
80
|
const inSpace = parseQuery(q).filters.space ?? []
|
|
65
81
|
|
|
66
|
-
/*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* the
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
82
|
+
/* ONE FILTER ROW (2026-10-01 — user: "why are there so many categories and options, is it relevant
|
|
83
|
+
* to the results we are seeking as a 'searcher'"; ruled: kind stays, the rest folds behind a `+`).
|
|
84
|
+
* The page was a scope row, a "read as" row, a tab strip and its chips before the first result.
|
|
85
|
+
* Now: the box, one row of kind chips, the results. Scope · read-as · category · tags are the
|
|
86
|
+
* same controls, behind the `+` — and open by themselves while one of them is narrowing the list,
|
|
87
|
+
* so an active filter is never hidden. Reverses W11's tab strip (2026-09-30). */
|
|
88
|
+
const [first, ...rest] = facets.filter(([field]) => (out.facets[field] ?? []).length)
|
|
89
|
+
const tokens = out.query.tokens
|
|
90
|
+
const narrowed = inSpace.length > 0 || tokens.some((t) => t.kind !== 'term' && t.field !== first?.[0])
|
|
91
|
+
const [more, setMore] = useState(false)
|
|
92
|
+
const open = more || narrowed
|
|
93
|
+
const chips = ([field]) => (out.facets[field] ?? []).slice(0, 24).map((f) => (
|
|
94
|
+
<Tag key={f.value} hash={field === 'tags'} color={field === 'tags' ? getTagColor(f.value) : undefined} active={f.selected} onClick={() => toggleToken(field, f.value)}>
|
|
95
|
+
{f.value}<span className="ml-1.5 text-subtle">{f.count}</span>
|
|
96
|
+
</Tag>
|
|
97
|
+
))
|
|
73
98
|
|
|
74
99
|
return (
|
|
75
|
-
<div className="flex flex-col gap-
|
|
76
|
-
|
|
77
|
-
eyebrow
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
100
|
+
<div className="flex flex-col gap-6">
|
|
101
|
+
{/* ONE LINE, AND ONLY ONCE THERE IS A QUERY (2026-10-01 — user: "section is search and the title
|
|
102
|
+
* is search, seems like redundant noise"): the eyebrow, the title and the count were three
|
|
103
|
+
* lines, and the count was printed again on the results heading. Without a query the page is
|
|
104
|
+
* its home's (the consumer renders it above) and this is just the box. */}
|
|
105
|
+
{q && <h1 className="kol-doc-section-title">{`${out.total} result${out.total === 1 ? '' : 's'} for “${q}”.`}</h1>}
|
|
81
106
|
|
|
82
|
-
<div className="flex flex-col gap-
|
|
107
|
+
<div className="flex flex-col gap-3">
|
|
83
108
|
{/* md, the ladder's default — lg towered over the page (W11) */}
|
|
84
109
|
<Input
|
|
85
|
-
value={
|
|
86
|
-
onChange={(e) =>
|
|
110
|
+
value={text}
|
|
111
|
+
onChange={(e) => type(e.target.value)}
|
|
87
112
|
placeholder="Search — try atom, tag:pattern/input, in:docs, -legacy"
|
|
88
113
|
iconLeft="search"
|
|
89
114
|
size="md"
|
|
@@ -91,57 +116,63 @@ export default function SearchPage({ items = [], spaces = [], facets = DEFAULT_F
|
|
|
91
116
|
autoFocus
|
|
92
117
|
/>
|
|
93
118
|
{/* every chip on the page is THE Tag, one size (W11 — they were ghost Buttons and md Pills) */}
|
|
94
|
-
{spaces.length > 0 && (
|
|
95
|
-
<div className="flex flex-wrap items-center gap-2" role="group" aria-label=
|
|
96
|
-
|
|
97
|
-
{
|
|
98
|
-
<Tag key={sp.value} hash={false} active={inSpace.includes(sp.value)} onClick={() => toggleToken('space', sp.value)}>{sp.label}</Tag>
|
|
99
|
-
))}
|
|
119
|
+
{q && (first || spaces.length > 0) && (
|
|
120
|
+
<div className="flex flex-wrap items-center gap-2" role="group" aria-label={first?.[1] ?? 'Filters'}>
|
|
121
|
+
{first && chips(first)}
|
|
122
|
+
<Tag hash={false} variant="secondary" active={open} onClick={() => setMore((m) => !m)} aria-expanded={open} aria-label="More filters">{open ? '−' : '+'}</Tag>
|
|
100
123
|
</div>
|
|
101
124
|
)}
|
|
102
|
-
{
|
|
103
|
-
<div className="flex flex-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
{q && open && (
|
|
126
|
+
<div className="flex flex-col gap-3">
|
|
127
|
+
{spaces.length > 0 && (
|
|
128
|
+
<div className="flex flex-wrap items-center gap-2" role="group" aria-label="Scope">
|
|
129
|
+
<span className="kol-doc-eyebrow">Scope</span>
|
|
130
|
+
<Tag hash={false} active={!inSpace.length} onClick={() => setQ(q.split(/\s+/).filter((p) => p && !/^in:/.test(p)).join(' '))}>Everything</Tag>
|
|
131
|
+
{spaces.map((sp) => (
|
|
132
|
+
<Tag key={sp.value} hash={false} active={inSpace.includes(sp.value)} onClick={() => toggleToken('space', sp.value)}>{sp.label}</Tag>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
{rest.map((facet) => (
|
|
137
|
+
<div key={facet[0]} className="flex flex-wrap items-center gap-2" role="group" aria-label={facet[1]}>
|
|
138
|
+
<span className="kol-doc-eyebrow">{facet[1]}</span>
|
|
139
|
+
{chips(facet)}
|
|
140
|
+
</div>
|
|
107
141
|
))}
|
|
142
|
+
{tokens.length > 0 && (
|
|
143
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
144
|
+
<span className="kol-doc-eyebrow">Read as</span>
|
|
145
|
+
{tokens.map((t, i) => (
|
|
146
|
+
<Tag key={`${t.raw}-${i}`} hash={false} variant="secondary" onRemove={() => removeToken(t.raw)} aria-label={`Remove ${t.raw}`}>{readToken(t)}</Tag>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
108
150
|
</div>
|
|
109
151
|
)}
|
|
110
152
|
</div>
|
|
111
153
|
|
|
112
|
-
{
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
154
|
+
{q && (
|
|
155
|
+
<section id="results" className="flex flex-col gap-4 scroll-mt-20">
|
|
156
|
+
{out.results.length === 0 ? (
|
|
157
|
+
<EmptyState eyebrow="No results" title="Nothing matches." body="Remove a token above, or try fewer words." />
|
|
158
|
+
) : (
|
|
159
|
+
<ol className="flex flex-col border-t border-fg-08">
|
|
160
|
+
{out.results.map((r) => (
|
|
161
|
+
<li key={r.item.id} className="border-t border-fg-08 first:border-t-0">
|
|
162
|
+
<ResultRow
|
|
163
|
+
to={r.item.href}
|
|
164
|
+
variant={rowVariant}
|
|
165
|
+
title={<Highlight text={r.item.title} ranges={r.highlights.title} />}
|
|
166
|
+
meta={[r.item.kind, r.item.category, r.item.space, r.item.date].filter(Boolean).join(' · ')}
|
|
167
|
+
description={r.item.description}
|
|
168
|
+
/>
|
|
169
|
+
</li>
|
|
170
|
+
))}
|
|
171
|
+
</ol>
|
|
172
|
+
)}
|
|
173
|
+
{out.total > out.results.length && <p className="kol-helper-12 text-meta">{out.total - out.results.length} more — narrow the query.</p>}
|
|
174
|
+
</section>
|
|
123
175
|
)}
|
|
124
|
-
|
|
125
|
-
<DocSection id="results" title={`Results (${out.total})`}>
|
|
126
|
-
{out.results.length === 0 ? (
|
|
127
|
-
<EmptyState eyebrow={q ? 'No results' : 'Search'} title={q ? 'Nothing matches.' : 'Type to search.'} body={q ? 'Remove a token above, or try fewer words.' : undefined} />
|
|
128
|
-
) : (
|
|
129
|
-
<ol className="flex flex-col">
|
|
130
|
-
{out.results.map((r) => (
|
|
131
|
-
<li key={r.item.id} className="border-t border-fg-08 first:border-t-0">
|
|
132
|
-
<ResultRow
|
|
133
|
-
to={r.item.href}
|
|
134
|
-
variant={rowVariant}
|
|
135
|
-
title={<Highlight text={r.item.title} ranges={r.highlights.title} />}
|
|
136
|
-
meta={[r.item.kind, r.item.category, r.item.space, r.item.date].filter(Boolean).join(' · ')}
|
|
137
|
-
description={r.item.description}
|
|
138
|
-
/>
|
|
139
|
-
</li>
|
|
140
|
-
))}
|
|
141
|
-
</ol>
|
|
142
|
-
)}
|
|
143
|
-
{out.total > out.results.length && <p className="kol-helper-12 text-meta">{out.total - out.results.length} more — narrow the query.</p>}
|
|
144
|
-
</DocSection>
|
|
145
176
|
</div>
|
|
146
177
|
)
|
|
147
178
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext, useMemo, useRef, useState, useEffect, Suspense } from 'react'
|
|
2
|
-
import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom'
|
|
2
|
+
import { Outlet, Link, useLocation, useNavigate, useNavigationType } from 'react-router-dom'
|
|
3
3
|
import { ShellHeader } from '@kolkrabbi/kol-framework'
|
|
4
4
|
import ShellSidebar, { RAIL_FOLD_EVENT } from './ShellSidebar.jsx'
|
|
5
5
|
import { IconFrame, ShellDrawer, ShellSearchOverlay, ShortcutsOverlay, SettingsPanel, SettingsSections, SettingsSwitch, Tooltip, useDragResize } from '@kolkrabbi/kol-component'
|
|
@@ -99,7 +99,7 @@ const useRailGrab = (token, side, hide) => {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
const NavColumn = ({ children, grip, railRef }) => (
|
|
102
|
-
<aside ref={railRef} aria-label="Navigation" className="shell-rail shell-rail--nav shell-sidebar-sticky hidden lg:block pt-6 md:pt-6 lg:pt-8 pb-
|
|
102
|
+
<aside ref={railRef} aria-label="Navigation" className="shell-rail shell-rail--nav shell-sidebar-sticky hidden lg:block pt-6 md:pt-6 lg:pt-8 pb-14">
|
|
103
103
|
{children}
|
|
104
104
|
{grip}
|
|
105
105
|
</aside>
|
|
@@ -139,7 +139,7 @@ const MainColumn = ({ children, fullHeight, width = 'canvas', padStart, padEnd }
|
|
|
139
139
|
* below). The breakpoint here and the one in gridCols are one decision and
|
|
140
140
|
* must not be stated twice differently. */
|
|
141
141
|
const TocColumn = ({ children, grip, railRef }) => (
|
|
142
|
-
<aside ref={railRef} aria-label="Table of contents" className="shell-rail shell-rail--toc shell-sidebar-sticky hidden xl:block pt-6 md:pt-6 lg:pt-8 pb-
|
|
142
|
+
<aside ref={railRef} aria-label="Table of contents" className="shell-rail shell-rail--toc shell-sidebar-sticky hidden xl:block pt-6 md:pt-6 lg:pt-8 pb-14">
|
|
143
143
|
{grip}
|
|
144
144
|
{/* The width is the grid track (--kol-shell-toc-w). An empty rail still
|
|
145
145
|
* holds its column (user ruling 2026-08-01): a rail that disappears
|
|
@@ -231,10 +231,37 @@ const ShellLayout = ({ routes = [], basePath = '/', brand: brandProp, brandLogoS
|
|
|
231
231
|
|
|
232
232
|
/* a new page starts at the top of the one scroll region; a hash is the
|
|
233
233
|
* browser's to resolve */
|
|
234
|
+
/* BACK RETURNS TO WHERE YOU WERE (2026-10-01 — user: "'back' loses the place vertically in the
|
|
235
|
+
* scroll … now I have to scroll and find my place"). Each history entry remembers its own
|
|
236
|
+
* scroll offset; Back/Forward (POP) restores it, a fresh navigation still starts at the top.
|
|
237
|
+
* The page may still be laying out when the entry returns (lazy walls, async demos), so the
|
|
238
|
+
* restore is retried for a moment until the region is tall enough to reach the offset. */
|
|
239
|
+
const navigationType = useNavigationType()
|
|
240
|
+
const scrollMemory = useRef(new Map())
|
|
234
241
|
useEffect(() => {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
242
|
+
const el = document.getElementById('shell-scroll')
|
|
243
|
+
if (!el) return undefined
|
|
244
|
+
const key = location.key
|
|
245
|
+
const save = () => scrollMemory.current.set(key, el.scrollTop)
|
|
246
|
+
el.addEventListener('scroll', save, { passive: true })
|
|
247
|
+
return () => el.removeEventListener('scroll', save)
|
|
248
|
+
}, [location.key])
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
if (location.hash) return undefined
|
|
251
|
+
const el = document.getElementById('shell-scroll')
|
|
252
|
+
if (!el) return undefined
|
|
253
|
+
const saved = navigationType === 'POP' ? scrollMemory.current.get(location.key) : 0
|
|
254
|
+
if (!saved) { el.scrollTo(0, 0); return undefined }
|
|
255
|
+
let frame
|
|
256
|
+
let tries = 60
|
|
257
|
+
const restore = () => {
|
|
258
|
+
el.scrollTo(0, saved)
|
|
259
|
+
if (Math.abs(el.scrollTop - saved) > 1 && tries-- > 0) frame = requestAnimationFrame(restore)
|
|
260
|
+
}
|
|
261
|
+
restore()
|
|
262
|
+
return () => cancelAnimationFrame(frame)
|
|
263
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
264
|
+
}, [location.pathname, location.hash, location.key])
|
|
238
265
|
|
|
239
266
|
/* THE shortcut map. One list, rendered by the `S` sheet AND bound by the
|
|
240
267
|
* handler below — a shortcut that isn't in this array doesn't exist, so the
|
|
@@ -560,6 +587,10 @@ const ShellLayout = ({ routes = [], basePath = '/', brand: brandProp, brandLogoS
|
|
|
560
587
|
* it. Without a `searchPath` Enter keeps the older in-place browser. */
|
|
561
588
|
onExpand={searchPath ? openResultsPage : () => tagMode.setExpanded?.(true)}
|
|
562
589
|
enterLabel={searchPath ? `All results for “${searchQuery.trim()}”` : undefined}
|
|
590
|
+
/* ⌘ENTER AND THE FOOTER LINK open the results page (2026-10-01); plain Enter opens
|
|
591
|
+
* the highlighted row and only falls through to the page when nothing matched. */
|
|
592
|
+
onOpenResults={searchPath ? openResultsPage : undefined}
|
|
593
|
+
resultsLabel={`All results for “${searchQuery.trim()}”`}
|
|
563
594
|
chips={tagMode.isProvided ? tagMode.activeTags : []}
|
|
564
595
|
onRemoveChip={tagMode.removeTag}
|
|
565
596
|
placeholder="Search…"
|
|
@@ -39,7 +39,8 @@ const ShellSidebar = ({ routes = [], basePath = '/', onNavigate, label = 'Naviga
|
|
|
39
39
|
|
|
40
40
|
/* THE RAIL FOLLOWS YOU — INTO A CHILD ONLY (2026-09-30, reversing the 2026-09-28 "open the
|
|
41
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
|
|
42
|
+
* chapter and folds the rest; a chapter's own home keeps that chapter open (2026-10-01, see
|
|
43
|
+
* `chainTo`). Every other chapter starts
|
|
43
44
|
* folded — including ones this rail has never seen (a Group-by switch hands it new ids, and an
|
|
44
45
|
* unknown id used to read as open, so every chapter sprang open).
|
|
45
46
|
*
|
|
@@ -63,6 +64,11 @@ const ShellSidebar = ({ routes = [], basePath = '/', onNavigate, label = 'Naviga
|
|
|
63
64
|
const deeper = chainTo(r.children, [...trail, r])
|
|
64
65
|
if (deeper) return deeper
|
|
65
66
|
if (r.children.some(isHere)) return [...trail, r]
|
|
67
|
+
/* A GROUP'S OWN HOME KEEPS IT OPEN (user 2026-10-01: *"I dont want to collapse the list
|
|
68
|
+
* like that when I just want to go to atoms home and still see the atoms list"* — reverses
|
|
69
|
+
* the 2026-09-30 "a chapter's own home opens nothing"). The label is a link; only the
|
|
70
|
+
* chevron folds. */
|
|
71
|
+
if (r.path && getChildPath(r, basePath).replace(/\/$/, '') === normalizedPath) return [...trail, r]
|
|
66
72
|
}
|
|
67
73
|
return null
|
|
68
74
|
}
|