@kolkrabbi/kol-framework 0.32.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/kol-framework.css CHANGED
@@ -146,6 +146,21 @@ body {
146
146
  position: relative;
147
147
  padding: 0.5rem 2.5rem 0.5rem 1.5rem;
148
148
  }
149
+ /* THE HEADER SLOT (RailLogomarkAtTop, kol-fxr 2026-08-28 — user, on the
150
+ * raven in the footer: "why did you move the logo from top to bottom?"): the
151
+ * `header` node sits ABOVE the tree, outside the scroll region, in the hop's
152
+ * own horizontal geometry — its `.kol-sidenav-hop-icon` centres on the icon
153
+ * column collapsed and left-aligns to it expanded, its `.kol-sidenav-hop-label`
154
+ * hides with the labels. 1rem above it; the scroll region's own top pad is the
155
+ * gap to the first row, so the first tree glyph's y is one number in every
156
+ * consumer that passes the same header. */
157
+ .kol-sidenav-header {
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 0.75rem;
161
+ width: 100%;
162
+ padding: 1rem 2.5rem 0 1.5rem;
163
+ }
149
164
  /* the caret rides a PLAIN span (Icon stamps inline-flex on its own element,
150
165
  * which would outrank the collapsed display:none) */
151
166
  .kol-sidenav-hop-caret {
@@ -208,6 +223,7 @@ body {
208
223
  :root[data-sidenav="collapsed"] .kol-brand-layout { grid-template-columns: var(--kol-sidenav-w-collapsed) minmax(0, 1fr); }
209
224
  :root[data-sidenav="collapsed"] .kol-sidenav-hops { padding: 0 0 var(--kol-spacing-4); margin-bottom: var(--kol-spacing-4); }
210
225
  :root[data-sidenav="collapsed"] .kol-sidenav-hop { padding: var(--kol-spacing-2) 0; justify-content: center; gap: 0; }
226
+ :root[data-sidenav="collapsed"] .kol-sidenav-header { padding: 1rem 0 0; justify-content: center; gap: 0; }
211
227
  :root[data-sidenav="collapsed"] .kol-sidenav-hop-label { display: none; }
212
228
  :root[data-sidenav="collapsed"] .kol-sidenav-body { display: none; }
213
229
  :root[data-sidenav="collapsed"] .kol-sidenav-hop-count,
@@ -281,6 +297,7 @@ body {
281
297
  .kol-brand-layout { grid-template-columns: var(--kol-sidenav-w-collapsed) minmax(0, 1fr); }
282
298
  .kol-sidenav-hops { padding: 0 0 var(--kol-spacing-4); margin-bottom: var(--kol-spacing-4); }
283
299
  .kol-sidenav-hop { padding: var(--kol-spacing-2) 0; justify-content: center; gap: 0; }
300
+ .kol-sidenav-header { padding: 1rem 0 0; justify-content: center; gap: 0; }
284
301
  .kol-sidenav-hop-label { display: none; }
285
302
  .kol-sidenav-section { display: none; }
286
303
  .kol-sidenav-body { display: none; }
@@ -303,6 +320,7 @@ body {
303
320
  .kol-sidenav.is-drawer-open { transform: translateX(0); }
304
321
  /* Restore expanded labels + body inside the open drawer (override the rail rules above). */
305
322
  .kol-sidenav.is-drawer-open .kol-sidenav-hop { padding: 8px 0; justify-content: flex-start; gap: 12px; }
323
+ .kol-sidenav.is-drawer-open .kol-sidenav-header { padding: 1rem 2.5rem 0 1.5rem; justify-content: flex-start; gap: 12px; }
306
324
  .kol-sidenav.is-drawer-open .kol-sidenav-hop-label { display: inline; }
307
325
  .kol-sidenav.is-drawer-open .kol-sidenav-section { display: block; }
308
326
  .kol-sidenav.is-drawer-open .kol-sidenav-body { display: block; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-framework",
3
- "version": "0.32.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-component": "^0.118.3",
24
- "@kolkrabbi/kol-icons": "^0.24.0"
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'
@@ -71,6 +72,26 @@ import useDragResize from './useDragResize'
71
72
  * pixel (fxr measured 48 vs 56 wide, 20 vs 16px glyphs, 40 vs 38 pitch, first
72
73
  * glyph at y 76 vs 26 between the old NavRail and this rail collapsed). The
73
74
  * seams the shell needed, all opt-in, a tree with none of them renders as before:
75
+ * `header` a node ABOVE the tree, outside the scroll region, in the
76
+ * hop's geometry (`.kol-sidenav-header`, kol-framework.css)
77
+ * — the app's logomark (RailLogomarkAtTop, 2026-08-28 —
78
+ * user: "why did you move the logo from top to bottom?").
79
+ * Give it a `.kol-sidenav-hop-icon` span (20px mark on the
80
+ * icon column) and, optionally, a `.kol-sidenav-hop-label`;
81
+ * both follow the collapse. Same node in every consumer =
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.
74
95
  * `bottomItems` top-level nodes pinned BELOW the theme slot, above the
75
96
  * footer (the shell pins theme → Settings; a Settings row in
76
97
  * the tree landed above the theme instead — "same rail, two
@@ -143,6 +164,7 @@ function SideNavInner({
143
164
  * name; a rail floating over media and a rail without a line are different
144
165
  * choices. */
145
166
  hairline = false,
167
+ header,
146
168
  bottomItems = [],
147
169
  footer,
148
170
  themeToggle = true,
@@ -163,12 +185,14 @@ function SideNavInner({
163
185
  const handleNavigate = (e, to) => {
164
186
  onNavigate?.(e, to)
165
187
  onCloseDrawer?.()
188
+ setOpenPanel(null)
166
189
  if (collapsed && expandOnSelect) toggleCollapsed()
167
190
  }
168
191
  /* an action leaf dispatches — same drawer / rail behaviour as a navigation */
169
192
  const handleSelect = (e, row) => {
170
193
  row.onSelect(e)
171
194
  onCloseDrawer?.()
195
+ setOpenPanel(null)
172
196
  if (collapsed && expandOnSelect) toggleCollapsed()
173
197
  }
174
198
  /* the route leaf: NavLink under a router, `<a href>` on the agnostic seam */
@@ -182,6 +206,9 @@ function SideNavInner({
182
206
  * 2026-08-01) — arriving somewhere must not silently close wherever you
183
207
  * were. Loads closed, in-memory only (elder ruling 2026-08-06). */
184
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)
185
212
  const toggleCat = (id) => setOpenCats((prev) => {
186
213
  const next = new Set(prev)
187
214
  next.has(id) ? next.delete(id) : next.add(id)
@@ -195,6 +222,29 @@ function SideNavInner({
195
222
  const asideRef = useRef(null)
196
223
  const { collapsed, toggleCollapsed, grabProps } = useDragResize(asideRef, { defaultCollapsed })
197
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
+
198
248
  /* Rows under a category: a leaf is a NavLink; a group is a non-routing
199
249
  * header over its own rows, one step further in (recursive). A group lights
200
250
  * up the way a category does — when one of its leaves is the current route. */
@@ -244,11 +294,12 @@ function SideNavInner({
244
294
  const pages = pagesOf(cat)
245
295
  const isOpen = openCats.has(cat.id)
246
296
  const isLink = !pages.length && cat.to
247
- const isAction = !pages.length && !cat.to && cat.onSelect
297
+ const isPanel = !pages.length && !cat.to && !!cat.panel
298
+ const isAction = !pages.length && !cat.to && !isPanel && cat.onSelect
248
299
  /* THE COLLAPSE RULE: no icon, no route, no action = a LABEL over
249
300
  * its rows, not a row — `.kol-sidenav-section` hides on collapse
250
301
  * and the icon rows beneath it stay */
251
- if (!cat.icon && !cat.to && !cat.onSelect) {
302
+ if (!cat.icon && !cat.to && !cat.onSelect && !cat.panel) {
252
303
  return (
253
304
  <li key={cat.id ?? `${cat.label}-${ci}`} className="relative">
254
305
  <div className="kol-sidenav-section kol-eyebrow text-strong">{cat.label}</div>
@@ -273,6 +324,17 @@ function SideNavInner({
273
324
  {glyph}
274
325
  <span className="kol-sidenav-hop-label flex-1 min-w-0">{cat.label}</span>
275
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>
276
338
  ) : isAction ? (
277
339
  <button
278
340
  type="button"
@@ -322,7 +384,7 @@ function SideNavInner({
322
384
  </Tooltip>
323
385
  ) : head}
324
386
 
325
- {!isLink && !isAction && isOpen && (
387
+ {!isLink && !isAction && !isPanel && isOpen && (
326
388
  <ul className="kol-sidenav-list">{renderRows(pages, 0)}</ul>
327
389
  )}
328
390
  </li>
@@ -344,6 +406,8 @@ function SideNavInner({
344
406
  * THE single collapse/resize control — click toggles, drag resizes,
345
407
  * snap-to-default on release, Home/Enter/Space on the keyboard. */}
346
408
 
409
+ {header && <div className="kol-sidenav-header">{header}</div>}
410
+
347
411
  {/* ONLY the tree scrolls (fxr's re-measure, 2026-08-28): the theme slot
348
412
  * and the pinned rows used to sit inside this region, so on a 26-row
349
413
  * collapsed tree they scrolled off below the footer — "pinned" only
@@ -396,6 +460,17 @@ function SideNavInner({
396
460
  </div>
397
461
  )}
398
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
+
399
474
  {/* Grab edge — width/cursor/focus chrome in kol-framework.css
400
475
  * (.kol-sidenav-grab); hidden below lg by the narrow-mode block. */}
401
476
  <div className="kol-sidenav-grab absolute top-0 right-0 bottom-0 z-[1]" {...grabProps} />