@kolkrabbi/kol-workshop 0.37.0 → 0.39.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 +9 -9
- package/src/docs/DocsFrontmatter.jsx +37 -13
- package/src/docs/DocumentationReader.jsx +9 -1
- package/src/docs/render-tokens.jsx +2 -1
- package/src/shell/RailRow.jsx +5 -2
- package/src/shell/RightRail.jsx +3 -3
- package/src/shell/ShellLayout.jsx +27 -1
- package/src/shell/ShellSidebar.jsx +23 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-workshop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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-markdown": "^0.1.
|
|
17
|
-
"@kolkrabbi/kol-
|
|
18
|
-
"@kolkrabbi/kol-
|
|
16
|
+
"@kolkrabbi/kol-markdown": "^0.1.3",
|
|
17
|
+
"@kolkrabbi/kol-search": "^0.3.0",
|
|
18
|
+
"@kolkrabbi/kol-brand": "^0.1.3"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@kolkrabbi/kol-component": ">=0.237.0",
|
|
22
22
|
"@kolkrabbi/kol-framework": ">=0.45.0",
|
|
23
|
-
"@kolkrabbi/kol-theme": ">=0.
|
|
23
|
+
"@kolkrabbi/kol-theme": ">=0.165.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-
|
|
32
|
-
"@kolkrabbi/kol-
|
|
33
|
-
"@kolkrabbi/kol-
|
|
34
|
-
"@kolkrabbi/kol-icons": "^0.33.
|
|
31
|
+
"@kolkrabbi/kol-component": "^0.240.0",
|
|
32
|
+
"@kolkrabbi/kol-framework": "^0.49.0",
|
|
33
|
+
"@kolkrabbi/kol-theme": "^0.166.0",
|
|
34
|
+
"@kolkrabbi/kol-icons": "^0.33.1"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"src",
|
|
@@ -137,17 +137,34 @@ const CASED_VALUE_FIELDS = new Set(['type', 'status'])
|
|
|
137
137
|
/* a doc's lifecycle on Badge's status tones (W17): live reads success, in progress warning, gone error */
|
|
138
138
|
const STATUS_TONE = { active: 'success', canonical: 'info', draft: 'warning', archived: 'error', superseded: 'error', deprecated: 'error' }
|
|
139
139
|
|
|
140
|
-
/** Contract order first, then anything else the doc carries, alphabetically.
|
|
141
|
-
|
|
140
|
+
/** Contract order first, then anything else the doc carries, alphabetically. A consumer's
|
|
141
|
+
* `fields[key].order` places a key at that index of the list; `hidden` drops it. */
|
|
142
|
+
const orderFields = (metadata, fields = {}) => {
|
|
142
143
|
const present = Object.keys(metadata).filter(
|
|
143
|
-
(k) => !HIDDEN.has(k) && metadata[k] != null && metadata[k] !== '' &&
|
|
144
|
+
(k) => !(fields[k]?.hidden ?? HIDDEN.has(k)) && metadata[k] != null && metadata[k] !== '' &&
|
|
144
145
|
!(Array.isArray(metadata[k]) && metadata[k].length === 0)
|
|
145
146
|
)
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
147
|
+
const placed = present.filter((k) => Number.isFinite(fields[k]?.order))
|
|
148
|
+
const known = FIELD_ORDER.filter((k) => present.includes(k) && !placed.includes(k))
|
|
149
|
+
const rest = present.filter((k) => !FIELD_ORDER.includes(k) && !placed.includes(k)).sort()
|
|
150
|
+
const list = [...known, ...rest]
|
|
151
|
+
for (const k of placed.sort((a, b) => fields[a].order - fields[b].order)) list.splice(Math.min(fields[k].order, list.length), 0, k)
|
|
152
|
+
return list
|
|
149
153
|
}
|
|
150
154
|
|
|
155
|
+
/* A VALUE THAT IS A URL IS A LINK (reader-takes-field-config-and-page-actions, kol-website
|
|
156
|
+
* 2026-10-05): `url`/`repo` printed as plain text. Printed without the protocol, in the body's
|
|
157
|
+
* link idiom, opening a new tab as the body's external links do (render-tokens). */
|
|
158
|
+
const URL_RE = /^https?:\/\//
|
|
159
|
+
const LINK_CLS = 'text-emphasis underline decoration-fg-16 underline-offset-2 hover:decoration-current break-all'
|
|
160
|
+
const printValue = (v) => {
|
|
161
|
+
const s = String(v)
|
|
162
|
+
return URL_RE.test(s) ? <a href={s} target="_blank" rel="noreferrer" className={LINK_CLS}>{s.replace(URL_RE, '').replace(/\/$/, '')}</a> : s
|
|
163
|
+
}
|
|
164
|
+
/* …and a key with no icon entry takes this one, so the label column keeps one left edge whatever
|
|
165
|
+
* a document carries (the 2026-08-01 rule above, made true for keys this file has never seen) */
|
|
166
|
+
const DEFAULT_ICON = 'hash-01'
|
|
167
|
+
|
|
151
168
|
/* Every field rendered as a date. `updated`/`created`/`verified` are the
|
|
152
169
|
* kol-docs names; `date`/`modified` are the sample dialect's. */
|
|
153
170
|
const DATE_FIELDS = new Set(['updated', 'created', 'verified', 'date', 'modified'])
|
|
@@ -159,7 +176,13 @@ const formatDate = (dateStr) => {
|
|
|
159
176
|
return d.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' }).replace(/\//g, '.')
|
|
160
177
|
}
|
|
161
178
|
|
|
162
|
-
|
|
179
|
+
/**
|
|
180
|
+
* @param {Object} [fields] per-key config from the consumer, merged over this file's tables:
|
|
181
|
+
* `{ [key]: { label, icon, order, hidden, render(value, metadata) } }`
|
|
182
|
+
* (2026-10-05, kol-website /workshop). No config is the output as before,
|
|
183
|
+
* apart from URL values as links and the default icon.
|
|
184
|
+
*/
|
|
185
|
+
const DocsFrontmatter = ({ metadata, docId, fields: config = {} }) => {
|
|
163
186
|
const { openTagMode } = useTagMode()
|
|
164
187
|
/* THE PANEL COLLAPSES, THE PAGE SCROLLS (user call 2026-08-09). A long
|
|
165
188
|
* frontmatter (Dropdown: classes + reuses + tokens) made the panel the
|
|
@@ -170,7 +193,7 @@ const DocsFrontmatter = ({ metadata, docId }) => {
|
|
|
170
193
|
const [expanded, setExpanded] = useState(false)
|
|
171
194
|
const [overflowing, setOverflowing] = useState(false)
|
|
172
195
|
|
|
173
|
-
const fields = metadata ? orderFields(metadata) : []
|
|
196
|
+
const fields = metadata ? orderFields(metadata, config) : []
|
|
174
197
|
|
|
175
198
|
useLayoutEffect(() => {
|
|
176
199
|
const el = bodyRef.current
|
|
@@ -190,16 +213,17 @@ const DocsFrontmatter = ({ metadata, docId }) => {
|
|
|
190
213
|
>
|
|
191
214
|
{fields.map((key) => {
|
|
192
215
|
const value = metadata[key]
|
|
193
|
-
const
|
|
216
|
+
const cfg = config[key] ?? {}
|
|
217
|
+
const icon = cfg.icon ?? FIELD_ICONS[key] ?? DEFAULT_ICON
|
|
194
218
|
|
|
195
219
|
return (
|
|
196
220
|
<div key={key} className="docs-frontmatter-row">
|
|
197
221
|
<span className="docs-frontmatter-key kol-helper-12 text-meta">
|
|
198
222
|
{icon && <Icon name={icon} size={14} className="text-oq-48" />}
|
|
199
|
-
{FIELD_LABELS[key] ?? humanise(key)}
|
|
223
|
+
{cfg.label ?? FIELD_LABELS[key] ?? humanise(key)}
|
|
200
224
|
</span>
|
|
201
225
|
<span className="docs-frontmatter-value kol-mono-12 text-strong">
|
|
202
|
-
{key === 'tags' && Array.isArray(value) ? (
|
|
226
|
+
{cfg.render ? cfg.render(value, metadata) : key === 'tags' && Array.isArray(value) ? (
|
|
203
227
|
/* `naked`, the SAME Tag rendering the rail's own tag list uses
|
|
204
228
|
* (DocReaderSidebar). This was the default filled variant, so
|
|
205
229
|
* the one concept rendered as a solid pill here and as plain
|
|
@@ -248,11 +272,11 @@ const DocsFrontmatter = ({ metadata, docId }) => {
|
|
|
248
272
|
* and a glyph restating it is decoration. */
|
|
249
273
|
<span className="docs-frontmatter-list">
|
|
250
274
|
{value.map((item, i) => (
|
|
251
|
-
<span key={i} className="break-all">{
|
|
275
|
+
<span key={i} className="break-all">{printValue(item)}</span>
|
|
252
276
|
))}
|
|
253
277
|
</span>
|
|
254
278
|
) : (
|
|
255
|
-
|
|
279
|
+
printValue(value)
|
|
256
280
|
)}
|
|
257
281
|
</span>
|
|
258
282
|
</div>
|
|
@@ -77,6 +77,11 @@ const DocReaderSidebar = ({ toc, allTags, related, docId, docsIndexHref, compone
|
|
|
77
77
|
* (2026-09-30, the names audit: every level has a home).
|
|
78
78
|
* @param {boolean} [props.showFrontmatter=true] the frontmatter panel; a home hides it by
|
|
79
79
|
* default and the consumer toggles it.
|
|
80
|
+
* @param {Object} [props.fields] per-key frontmatter config, handed to DocsFrontmatter:
|
|
81
|
+
* `{ [key]: { label, icon, order, hidden, render } }`
|
|
82
|
+
* @param {node} [props.actions] the page's own actions (a button to the live app),
|
|
83
|
+
* rendered under the title and before the intro; absent,
|
|
84
|
+
* nothing (2026-10-05, kol-website /workshop)
|
|
80
85
|
*/
|
|
81
86
|
const DocumentationReader = ({
|
|
82
87
|
inventory = [],
|
|
@@ -85,6 +90,8 @@ const DocumentationReader = ({
|
|
|
85
90
|
routes = {},
|
|
86
91
|
docId: docIdProp,
|
|
87
92
|
showFrontmatter = true,
|
|
93
|
+
fields,
|
|
94
|
+
actions,
|
|
88
95
|
/* false → render the article only and leave the right rail to the page (a HOME sits on top of
|
|
89
96
|
* a page whose sections the rail must list — 2026-09-30, "This page (0)" on every home) */
|
|
90
97
|
rail = true,
|
|
@@ -365,10 +372,11 @@ const DocumentationReader = ({
|
|
|
365
372
|
|
|
366
373
|
return (
|
|
367
374
|
<DocsArticle>
|
|
368
|
-
{showFrontmatter && <DocsFrontmatter metadata={doc.metadata} docId={docId} />}
|
|
375
|
+
{showFrontmatter && <DocsFrontmatter metadata={doc.metadata} docId={docId} fields={fields} />}
|
|
369
376
|
{docTitle && (
|
|
370
377
|
<h1 id="page-top" className="kol-doc-heading scroll-mt-20">{docTitle}</h1>
|
|
371
378
|
)}
|
|
379
|
+
{actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}
|
|
372
380
|
{/* Render intro blocks (excluding H1 which is docTitle) */}
|
|
373
381
|
{introBlocks.filter(b => b.type !== 'heading1').map((block, index) =>
|
|
374
382
|
renderBlock(block, `intro-${block.type}-${index}`)
|
|
@@ -63,7 +63,8 @@ export const renderInlineTokens = (
|
|
|
63
63
|
return token.text
|
|
64
64
|
}
|
|
65
65
|
return (
|
|
66
|
-
|
|
66
|
+
/* a link that leaves the site opens a new tab (2026-10-02); a relative or `#` link stays */
|
|
67
|
+
<a key={tokenKey} href={token.url} className={linkCls} {...(/^https?:/.test(token.url) ? { target: '_blank', rel: 'noreferrer' } : null)}>
|
|
67
68
|
{token.text}
|
|
68
69
|
</a>
|
|
69
70
|
)
|
package/src/shell/RailRow.jsx
CHANGED
|
@@ -31,7 +31,7 @@ import { Link, NavLink } from 'react-router-dom'
|
|
|
31
31
|
* @param {string} [to] route → NavLink, active resolved by the router. A `to` carrying a `#` is a
|
|
32
32
|
* SECTION of a page (2026-10-01): the router would light every section of
|
|
33
33
|
* the open page at once, so it is a plain Link and `active` decides
|
|
34
|
-
* @param {string} [href] plain anchor (hash links: on-this-page)
|
|
34
|
+
* @param {string} [href] plain anchor (hash links: on-this-page); an `http(s)` href opens a new tab
|
|
35
35
|
* @param {Function} [onClick] no `to`/`href` → a <button>
|
|
36
36
|
* @param {boolean} [active] forced active state (hash links, buttons)
|
|
37
37
|
* @param {node} [icon] leading glyph
|
|
@@ -80,8 +80,11 @@ export default function RailRow({
|
|
|
80
80
|
)
|
|
81
81
|
}
|
|
82
82
|
if (href) {
|
|
83
|
+
/* a link that leaves the site opens a new tab, as the body's do (render-tokens, 2026-10-02);
|
|
84
|
+
hash and same-site links stay in it (2026-10-05, kol-website) */
|
|
85
|
+
const external = /^https?:\/\//.test(href) ? { target: '_blank', rel: 'noreferrer' } : null
|
|
83
86
|
return (
|
|
84
|
-
<a href={href} className={cls(active)} onClick={onNavigate}>
|
|
87
|
+
<a href={href} className={cls(active)} onClick={onNavigate} {...external}>
|
|
85
88
|
{inner}
|
|
86
89
|
</a>
|
|
87
90
|
)
|
package/src/shell/RightRail.jsx
CHANGED
|
@@ -60,7 +60,7 @@ const hueOf = (tag) => { const c = getTagColor(tag); return c === 'dark' ? undef
|
|
|
60
60
|
* the scroll spy; both rails must agree on it)
|
|
61
61
|
* @param {Array} related [{ id, href, label }] — label is the target's
|
|
62
62
|
* own SHORT title, never a wikilink display text
|
|
63
|
-
* @param {Array} actions [{ id, label, icon, to?, onClick? }]
|
|
63
|
+
* @param {Array} actions [{ id, label, icon, to?, href?, onClick? }] — `href` for an action that leaves the site
|
|
64
64
|
* @param {Array} topTags [{ tag, count }] — the way in, not a dump
|
|
65
65
|
* @param {Array} tags [string] — this page's own tags
|
|
66
66
|
* @param {Function} renderTag (tag) => node — the chip renderer, injected so
|
|
@@ -179,7 +179,7 @@ export default function RightRail({
|
|
|
179
179
|
|
|
180
180
|
{group('actions', 'Quick actions', allActions.length,
|
|
181
181
|
allActions.map((a) => (
|
|
182
|
-
<RailRow key={a.id} to={a.to} onClick={a.onClick} icon={a.icon}>
|
|
182
|
+
<RailRow key={a.id} to={a.to} href={a.href} onClick={a.onClick} icon={a.icon}>
|
|
183
183
|
{a.label}
|
|
184
184
|
</RailRow>
|
|
185
185
|
))
|
|
@@ -203,7 +203,7 @@ export default function RightRail({
|
|
|
203
203
|
{tagViews.length > 0 && (
|
|
204
204
|
<nav className="shell-nav-items">
|
|
205
205
|
{tagViews.map((a) => (
|
|
206
|
-
<RailRow key={a.id} to={a.to} onClick={a.onClick} icon={a.icon}>{a.label}</RailRow>
|
|
206
|
+
<RailRow key={a.id} to={a.to} href={a.href} onClick={a.onClick} icon={a.icon}>{a.label}</RailRow>
|
|
207
207
|
))}
|
|
208
208
|
</nav>
|
|
209
209
|
)}
|
|
@@ -2,7 +2,7 @@ import { createContext, useContext, useMemo, useRef, useState, useEffect, Suspen
|
|
|
2
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, ShellRailModeContext } from './ShellSidebar.jsx'
|
|
5
|
-
import { IconFrame, ShellDrawer, ShellSearchOverlay, ShortcutsOverlay, SettingsPanel, SettingsSections, SettingsSwitch, Tooltip, useDragResize } from '@kolkrabbi/kol-component'
|
|
5
|
+
import { FullscreenOverlay, IconFrame, ShellDrawer, ShellSearchOverlay, ShortcutsOverlay, SettingsPanel, SettingsSections, SettingsSwitch, Tooltip, useDragResize } from '@kolkrabbi/kol-component'
|
|
6
6
|
import { createIndex, search } from '@kolkrabbi/kol-search'
|
|
7
7
|
import { useTagMode } from '../tags/TagModeContext.jsx'
|
|
8
8
|
import TagModeOverlay from '../tags/TagModeOverlay.jsx'
|
|
@@ -262,6 +262,7 @@ const ShellLayout = ({ routes = [], basePath = '/', brand: brandProp, brandLogoS
|
|
|
262
262
|
const [isFullHeight, setIsFullHeight] = useState(false)
|
|
263
263
|
const [contentWidth, setContentWidth] = useState('canvas')
|
|
264
264
|
const [isShortcutsOpen, setIsShortcutsOpen] = useState(false)
|
|
265
|
+
const [isTreeOpen, setIsTreeOpen] = useState(false)
|
|
265
266
|
const location = useLocation()
|
|
266
267
|
const navigate = useNavigate()
|
|
267
268
|
|
|
@@ -318,6 +319,7 @@ const ShellLayout = ({ routes = [], basePath = '/', brand: brandProp, brandLogoS
|
|
|
318
319
|
{ id: 'toc', label: 'Right rail', combo: ']' },
|
|
319
320
|
{ id: 'rails', label: 'Both rails', combo: '\\' },
|
|
320
321
|
{ id: 'fold', label: 'Fold / expand all', combo: 'C' },
|
|
322
|
+
{ id: 'tree', label: 'Whole tree', combo: 'T' },
|
|
321
323
|
{ id: 'esc', label: 'Close', combo: 'Esc' },
|
|
322
324
|
] },
|
|
323
325
|
...(shortcuts.length ? [{ section: 'Page', items: shortcuts.map(({ id, label, combo }) => ({ id, label, combo })) }] : []),
|
|
@@ -366,6 +368,11 @@ const ShellLayout = ({ routes = [], basePath = '/', brand: brandProp, brandLogoS
|
|
|
366
368
|
e.preventDefault()
|
|
367
369
|
foldedRef.current = !foldedRef.current
|
|
368
370
|
window.dispatchEvent(new CustomEvent(RAIL_FOLD_EVENT, { detail: { collapsed: foldedRef.current } }))
|
|
371
|
+
} else if (e.key === 't' || e.key === 'T') {
|
|
372
|
+
/* THE WHOLE TREE (user 2026-10-02: *"C … is just about the visual tree per space, I was
|
|
373
|
+
* talking about a shortcut to get the entire tree in any given view"*) */
|
|
374
|
+
e.preventDefault()
|
|
375
|
+
setIsTreeOpen((v) => !v)
|
|
369
376
|
} else if (shortcutsRef.current.some((s) => s.key === e.key)) {
|
|
370
377
|
e.preventDefault()
|
|
371
378
|
shortcutsRef.current.find((s) => s.key === e.key).run()
|
|
@@ -655,6 +662,25 @@ const ShellLayout = ({ routes = [], basePath = '/', brand: brandProp, brandLogoS
|
|
|
655
662
|
|
|
656
663
|
{isShortcutsOpen && <ShortcutsOverlay shortcuts={SHORTCUTS} onClose={() => setIsShortcutsOpen(false)} />}
|
|
657
664
|
|
|
665
|
+
{/* THE WHOLE TREE, FROM ANY PAGE (`T`, 2026-10-02): every space's rail side by side, every
|
|
666
|
+
* fold open, over the page like the search modal. Each column is the consumer's own rail
|
|
667
|
+
* for that space (`renderSidebar`) drawn in the `full` rail mode, so it cannot list
|
|
668
|
+
* anything the rails do not. A row closes the sheet and goes there. */}
|
|
669
|
+
<FullscreenOverlay open={isTreeOpen} onClose={() => setIsTreeOpen(false)} scrim>
|
|
670
|
+
<div className="shell-tree bg-surface-primary">
|
|
671
|
+
{routes.map((route) => (
|
|
672
|
+
<section key={route.id ?? route.path} className="shell-tree-space">
|
|
673
|
+
<Link to={route.path?.startsWith('/') ? route.path : `${basePath}/${route.path ?? ''}`} className="kol-doc-eyebrow text-emphasis" onClick={() => setIsTreeOpen(false)}>{route.label}</Link>
|
|
674
|
+
<ShellRailModeContext.Provider value="full">
|
|
675
|
+
{renderSidebar
|
|
676
|
+
? renderSidebar({ activeRoute: route, onNavigate: () => setIsTreeOpen(false), mode: 'full' })
|
|
677
|
+
: <ShellSidebar routes={route.children ?? []} basePath={basePath} label={null} onNavigate={() => setIsTreeOpen(false)} />}
|
|
678
|
+
</ShellRailModeContext.Provider>
|
|
679
|
+
</section>
|
|
680
|
+
))}
|
|
681
|
+
</div>
|
|
682
|
+
</FullscreenOverlay>
|
|
683
|
+
|
|
658
684
|
{/* THE SHELL'S SETTINGS (2026-09-28, user: "might we also want to utilize a
|
|
659
685
|
* settings page or settings sidebar … to offload some functional
|
|
660
686
|
* settings, search settings, sidenav settings"). A right drawer over
|
|
@@ -30,7 +30,8 @@ export const RAIL_FOLD_EVENT = 'kol-rail-fold'
|
|
|
30
30
|
* we dont use it, I want to see it"*). Beside visible and hidden: `icons` — the rail is a narrow
|
|
31
31
|
* strip, one glyph per group, and opens on hover. The column that holds the rail (`ShellNavColumn`)
|
|
32
32
|
* provides the state; every ShellSidebar inside it reads it, so a consumer's rail takes the state
|
|
33
|
-
* without passing a prop. Outside a provider a rail is `open`, as it always was.
|
|
33
|
+
* without passing a prop. Outside a provider a rail is `open`, as it always was. A third value,
|
|
34
|
+
* `full`, is the whole-tree sheet's: an open rail with every fold open. */
|
|
34
35
|
export const ShellRailModeContext = createContext('open')
|
|
35
36
|
|
|
36
37
|
const ShellSidebar = ({ routes = [], basePath = '/', onNavigate, label = 'Navigation', labelTo, collapsed, onToggle, defaultCollapsed = false }) => {
|
|
@@ -48,8 +49,8 @@ const ShellSidebar = ({ routes = [], basePath = '/', onNavigate, label = 'Naviga
|
|
|
48
49
|
|
|
49
50
|
/* THE RAIL FOLLOWS YOU — INTO A CHILD ONLY (2026-09-30, reversing the 2026-09-28 "open the
|
|
50
51
|
* group you are in" on its landing page too). Landing on a page INSIDE a chapter opens that
|
|
51
|
-
* chapter and folds the rest; a chapter's own home
|
|
52
|
-
* `chainTo`). Every other chapter starts
|
|
52
|
+
* chapter and folds the rest; a chapter's own home leaves that chapter's fold alone (2026-10-02,
|
|
53
|
+
* see `chainTo`). Every other chapter starts
|
|
53
54
|
* folded — including ones this rail has never seen (a Group-by switch hands it new ids, and an
|
|
54
55
|
* unknown id used to read as open, so every chapter sprang open).
|
|
55
56
|
*
|
|
@@ -72,28 +73,32 @@ const ShellSidebar = ({ routes = [], basePath = '/', onNavigate, label = 'Naviga
|
|
|
72
73
|
/* deepest first: `/sets` holds `/sets/app-shell` by prefix, but the set's own row is the place */
|
|
73
74
|
const deeper = chainTo(r.children, [...trail, r])
|
|
74
75
|
if (deeper) return deeper
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
*
|
|
78
|
-
* the 2026-09-30
|
|
79
|
-
* chevron
|
|
80
|
-
|
|
76
|
+
/* A GROUP'S OWN HOME LEAVES ITS FOLD AS IT WAS (user 2026-10-02, said many times: *"just
|
|
77
|
+
* clicking a title opens the home, if I want to expand then I fucking click the expand
|
|
78
|
+
* chevron"*). The 2026-10-01 reading opened the group on its own home, so a title click
|
|
79
|
+
* expanded its children; the 2026-09-30 one folded it. Neither: the way down to it opens,
|
|
80
|
+
* the group itself stays as the chevron left it — open stays open, folded stays folded.
|
|
81
|
+
* Checked BEFORE the children: a vault chapter's home is also its `About` row, and reading
|
|
82
|
+
* that as "a child is here" opened every Docs chapter on a title click.
|
|
83
|
+
* `validate:rail-pages` P5 holds it. */
|
|
84
|
+
if (r.path && getChildPath(r, basePath).replace(/\/$/, '') === normalizedPath) return { open: trail, keep: r.id }
|
|
85
|
+
if (r.children.some(isHere)) return { open: [...trail, r] }
|
|
81
86
|
}
|
|
82
87
|
return null
|
|
83
88
|
}
|
|
84
|
-
const followed = () => {
|
|
85
|
-
const
|
|
86
|
-
if (!
|
|
87
|
-
const open = new Set(
|
|
88
|
-
return Object.fromEntries(groupsOf(routes).map((g) => [g.id, !open.has(g.id)]))
|
|
89
|
+
const followed = (prev = {}) => {
|
|
90
|
+
const at = chainTo(routes)
|
|
91
|
+
if (!at) return null
|
|
92
|
+
const open = new Set(at.open.map((g) => g.id))
|
|
93
|
+
return Object.fromEntries(groupsOf(routes).map((g) => [g.id, g.id === at.keep ? (prev[g.id] ?? true) : !open.has(g.id)]))
|
|
89
94
|
}
|
|
90
95
|
const [collapsedSections, setCollapsedSections] = useState(() => followed() ?? {})
|
|
91
96
|
useEffect(() => {
|
|
92
|
-
|
|
93
|
-
if (next) setCollapsedSections(next)
|
|
97
|
+
setCollapsedSections((prev) => followed(prev) ?? prev)
|
|
94
98
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
95
99
|
}, [normalizedPath, routes, basePath])
|
|
96
|
-
|
|
100
|
+
/* `full` — the whole-tree sheet (`T`, 2026-10-02): every fold open, nothing to follow */
|
|
101
|
+
const isFolded = (route) => (mode === 'full' ? false : collapsedSections[route.id] ?? true)
|
|
97
102
|
|
|
98
103
|
/* FOLD ALL (2026-09-30, the names audit: *"shortcut collapse/expand the categories"*). The
|
|
99
104
|
* shell's `C` key broadcasts one target state; every rail's chapters take it. */
|
|
@@ -210,7 +215,7 @@ const ShellSidebar = ({ routes = [], basePath = '/', onNavigate, label = 'Naviga
|
|
|
210
215
|
label={label}
|
|
211
216
|
to={labelTo}
|
|
212
217
|
count={leafCount}
|
|
213
|
-
collapsed={navCollapsed}
|
|
218
|
+
collapsed={mode === 'full' ? false : navCollapsed}
|
|
214
219
|
onToggle={handleToggle}
|
|
215
220
|
onNavigate={onNavigate}
|
|
216
221
|
icon={Icon}
|