@kolkrabbi/kol-framework 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 +3 -3
- package/src/SideNav.jsx +68 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "KOL app shell — sidenav, layout, theme toggle, footer, heroes + brand color layer. SideNav takes nav data as props so the tree stays app-local.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@kolkrabbi/kol-
|
|
24
|
-
"@kolkrabbi/kol-
|
|
23
|
+
"@kolkrabbi/kol-icons": "^0.24.0",
|
|
24
|
+
"@kolkrabbi/kol-component": "^0.119.0"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"src",
|
package/src/SideNav.jsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NavLink, useLocation } from 'react-router-dom'
|
|
2
|
-
import { useRef, useState } from 'react'
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
3
|
+
import { createPortal } from 'react-dom'
|
|
3
4
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
4
5
|
import { Tooltip } from '@kolkrabbi/kol-component'
|
|
5
6
|
import ThemeToggle from './ThemeToggle'
|
|
@@ -79,6 +80,18 @@ import useDragResize from './useDragResize'
|
|
|
79
80
|
* icon column) and, optionally, a `.kol-sidenav-hop-label`;
|
|
80
81
|
* both follow the collapse. Same node in every consumer =
|
|
81
82
|
* the first tree glyph at one y — the parity contract
|
|
83
|
+
* PANEL LEAF `{ id, label, icon, panel }` — a top-level node (tree or
|
|
84
|
+
* `bottomItems`) whose row is a DISCLOSURE: click opens,
|
|
85
|
+
* click again closes, lit while open (RailSettingsDisclosure,
|
|
86
|
+
* kol-fxr 2026-08-28 — user: "sidebar setting should open
|
|
87
|
+
* close on click and click again" · "put the theme toggle in
|
|
88
|
+
* the settings and out of the sidebar"). `panel` is a node or
|
|
89
|
+
* `({ collapsed, close }) => node`, PORTALLED to <body>
|
|
90
|
+
* directly above the row at the rail's own width (left/width
|
|
91
|
+
* from the aside, bottom = viewport − the row's top,
|
|
92
|
+
* `--kol-z-tooltip`), so it works collapsed (icon column)
|
|
93
|
+
* and expanded (rows) alike. Escape closes; any navigation
|
|
94
|
+
* or dispatch closes. fxr's RailSettings.jsx, promoted.
|
|
82
95
|
* `bottomItems` top-level nodes pinned BELOW the theme slot, above the
|
|
83
96
|
* footer (the shell pins theme → Settings; a Settings row in
|
|
84
97
|
* the tree landed above the theme instead — "same rail, two
|
|
@@ -172,12 +185,14 @@ function SideNavInner({
|
|
|
172
185
|
const handleNavigate = (e, to) => {
|
|
173
186
|
onNavigate?.(e, to)
|
|
174
187
|
onCloseDrawer?.()
|
|
188
|
+
setOpenPanel(null)
|
|
175
189
|
if (collapsed && expandOnSelect) toggleCollapsed()
|
|
176
190
|
}
|
|
177
191
|
/* an action leaf dispatches — same drawer / rail behaviour as a navigation */
|
|
178
192
|
const handleSelect = (e, row) => {
|
|
179
193
|
row.onSelect(e)
|
|
180
194
|
onCloseDrawer?.()
|
|
195
|
+
setOpenPanel(null)
|
|
181
196
|
if (collapsed && expandOnSelect) toggleCollapsed()
|
|
182
197
|
}
|
|
183
198
|
/* the route leaf: NavLink under a router, `<a href>` on the agnostic seam */
|
|
@@ -191,6 +206,9 @@ function SideNavInner({
|
|
|
191
206
|
* 2026-08-01) — arriving somewhere must not silently close wherever you
|
|
192
207
|
* were. Loads closed, in-memory only (elder ruling 2026-08-06). */
|
|
193
208
|
const [openCats, setOpenCats] = useState(() => new Set())
|
|
209
|
+
/* the ONE open panel leaf (by id) and where its panel sits */
|
|
210
|
+
const [openPanel, setOpenPanel] = useState(null)
|
|
211
|
+
const [panelBox, setPanelBox] = useState(null)
|
|
194
212
|
const toggleCat = (id) => setOpenCats((prev) => {
|
|
195
213
|
const next = new Set(prev)
|
|
196
214
|
next.has(id) ? next.delete(id) : next.add(id)
|
|
@@ -204,6 +222,29 @@ function SideNavInner({
|
|
|
204
222
|
const asideRef = useRef(null)
|
|
205
223
|
const { collapsed, toggleCollapsed, grabProps } = useDragResize(asideRef, { defaultCollapsed })
|
|
206
224
|
|
|
225
|
+
/* the panel's anchor: its row — the panel's bottom edge meets the row's top
|
|
226
|
+
* edge, its width is the rail's; re-placed on resize and on collapse */
|
|
227
|
+
useLayoutEffect(() => {
|
|
228
|
+
if (!openPanel) { setPanelBox(null); return undefined }
|
|
229
|
+
const place = () => {
|
|
230
|
+
const rail = asideRef.current
|
|
231
|
+
const row = rail?.querySelector(`[data-panel-trigger="${CSS.escape(String(openPanel))}"]`)
|
|
232
|
+
if (!rail || !row) return
|
|
233
|
+
const rr = rail.getBoundingClientRect(), tr = row.getBoundingClientRect()
|
|
234
|
+
setPanelBox({ left: rr.left, width: rr.width, bottom: window.innerHeight - tr.top })
|
|
235
|
+
}
|
|
236
|
+
place()
|
|
237
|
+
window.addEventListener('resize', place)
|
|
238
|
+
return () => window.removeEventListener('resize', place)
|
|
239
|
+
}, [openPanel, collapsed])
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!openPanel) return undefined
|
|
242
|
+
const onKey = (e) => { if (e.key === 'Escape') setOpenPanel(null) }
|
|
243
|
+
window.addEventListener('keydown', onKey)
|
|
244
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
245
|
+
}, [openPanel])
|
|
246
|
+
const panelNode = openPanel != null ? [...navTree, ...bottomItems].find((c) => c.id === openPanel) : null
|
|
247
|
+
|
|
207
248
|
/* Rows under a category: a leaf is a NavLink; a group is a non-routing
|
|
208
249
|
* header over its own rows, one step further in (recursive). A group lights
|
|
209
250
|
* up the way a category does — when one of its leaves is the current route. */
|
|
@@ -253,11 +294,12 @@ function SideNavInner({
|
|
|
253
294
|
const pages = pagesOf(cat)
|
|
254
295
|
const isOpen = openCats.has(cat.id)
|
|
255
296
|
const isLink = !pages.length && cat.to
|
|
256
|
-
const
|
|
297
|
+
const isPanel = !pages.length && !cat.to && !!cat.panel
|
|
298
|
+
const isAction = !pages.length && !cat.to && !isPanel && cat.onSelect
|
|
257
299
|
/* THE COLLAPSE RULE: no icon, no route, no action = a LABEL over
|
|
258
300
|
* its rows, not a row — `.kol-sidenav-section` hides on collapse
|
|
259
301
|
* and the icon rows beneath it stay */
|
|
260
|
-
if (!cat.icon && !cat.to && !cat.onSelect) {
|
|
302
|
+
if (!cat.icon && !cat.to && !cat.onSelect && !cat.panel) {
|
|
261
303
|
return (
|
|
262
304
|
<li key={cat.id ?? `${cat.label}-${ci}`} className="relative">
|
|
263
305
|
<div className="kol-sidenav-section kol-eyebrow text-strong">{cat.label}</div>
|
|
@@ -282,6 +324,17 @@ function SideNavInner({
|
|
|
282
324
|
{glyph}
|
|
283
325
|
<span className="kol-sidenav-hop-label flex-1 min-w-0">{cat.label}</span>
|
|
284
326
|
</RouteLeaf>
|
|
327
|
+
) : isPanel ? (
|
|
328
|
+
<button
|
|
329
|
+
type="button"
|
|
330
|
+
data-panel-trigger={cat.id}
|
|
331
|
+
className={`${rowCls} bg-transparent border-0 cursor-pointer text-left${openPanel === cat.id ? ' is-active' : ''}`}
|
|
332
|
+
aria-expanded={openPanel === cat.id}
|
|
333
|
+
onClick={() => setOpenPanel((o) => (o === cat.id ? null : cat.id))}
|
|
334
|
+
>
|
|
335
|
+
{glyph}
|
|
336
|
+
<span className="kol-sidenav-hop-label flex-1 min-w-0">{cat.label}</span>
|
|
337
|
+
</button>
|
|
285
338
|
) : isAction ? (
|
|
286
339
|
<button
|
|
287
340
|
type="button"
|
|
@@ -331,7 +384,7 @@ function SideNavInner({
|
|
|
331
384
|
</Tooltip>
|
|
332
385
|
) : head}
|
|
333
386
|
|
|
334
|
-
{!isLink && !isAction && isOpen && (
|
|
387
|
+
{!isLink && !isAction && !isPanel && isOpen && (
|
|
335
388
|
<ul className="kol-sidenav-list">{renderRows(pages, 0)}</ul>
|
|
336
389
|
)}
|
|
337
390
|
</li>
|
|
@@ -407,6 +460,17 @@ function SideNavInner({
|
|
|
407
460
|
</div>
|
|
408
461
|
)}
|
|
409
462
|
|
|
463
|
+
{/* the open panel leaf's panel — a portal above its row, the rail's width */}
|
|
464
|
+
{panelNode && panelBox && createPortal(
|
|
465
|
+
<div
|
|
466
|
+
className="kol-sidenav-panel fixed flex flex-col gap-[2px] py-2 bg-surface-primary border-t border-r border-fg-08"
|
|
467
|
+
style={{ left: panelBox.left, width: panelBox.width, bottom: panelBox.bottom, zIndex: 'var(--kol-z-tooltip)' }}
|
|
468
|
+
>
|
|
469
|
+
{typeof panelNode.panel === 'function' ? panelNode.panel({ collapsed, close: () => setOpenPanel(null) }) : panelNode.panel}
|
|
470
|
+
</div>,
|
|
471
|
+
document.body,
|
|
472
|
+
)}
|
|
473
|
+
|
|
410
474
|
{/* Grab edge — width/cursor/focus chrome in kol-framework.css
|
|
411
475
|
* (.kol-sidenav-grab); hidden below lg by the narrow-mode block. */}
|
|
412
476
|
<div className="kol-sidenav-grab absolute top-0 right-0 bottom-0 z-[1]" {...grabProps} />
|