@kolkrabbi/kol-framework 0.30.0 → 0.31.1
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 +2 -2
- package/src/SideNav.jsx +97 -46
- package/src/useDragResize.js +8 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.1",
|
|
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,7 +20,7 @@
|
|
|
20
20
|
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@kolkrabbi/kol-component": "^0.118.
|
|
23
|
+
"@kolkrabbi/kol-component": "^0.118.3",
|
|
24
24
|
"@kolkrabbi/kol-icons": "^0.24.0"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
package/src/SideNav.jsx
CHANGED
|
@@ -64,6 +64,27 @@ import useDragResize from './useDragResize'
|
|
|
64
64
|
* the `kol-sidenav` storage schema, owned by useDragResize. Collapsed rows
|
|
65
65
|
* get a DS Tooltip — the glyph is otherwise the only thing naming them.
|
|
66
66
|
*
|
|
67
|
+
* THE SHELL RAIL IS THIS COMPONENT (RailSideNavPixelParity, kol-fxr 2026-08-28
|
|
68
|
+
* — user, on the brand sidebar's drag: "same component both states super
|
|
69
|
+
* nice"): kol-shell's `AppShell` mounts a SideNav — collapsed by default, the
|
|
70
|
+
* drag opens it — so an app rail and the brand sidebar are one geometry to the
|
|
71
|
+
* pixel (fxr measured 48 vs 56 wide, 20 vs 16px glyphs, 40 vs 38 pitch, first
|
|
72
|
+
* glyph at y 76 vs 26 between the old NavRail and this rail collapsed). The
|
|
73
|
+
* seams the shell needed, all opt-in, a tree with none of them renders as before:
|
|
74
|
+
* `bottomItems` top-level nodes pinned BELOW the theme slot, above the
|
|
75
|
+
* footer (the shell pins theme → Settings; a Settings row in
|
|
76
|
+
* the tree landed above the theme instead — "same rail, two
|
|
77
|
+
* bottoms")
|
|
78
|
+
* `footer` the footer box's content: undefined = the Kolkrabbi link
|
|
79
|
+
* (K / wordmark, as ever); `false` = no footer; a node =
|
|
80
|
+
* that node (the shell's logomark → '/')
|
|
81
|
+
* `themeToggle` `false` drops the theme slot (default `true`)
|
|
82
|
+
* `iconComponent` the hop glyph's renderer, kol-shell's seam — `(props:
|
|
83
|
+
* { name, size }) => element`; default the DS `Icon`
|
|
84
|
+
* `expandOnSelect` `false` keeps the rail collapsed when a row navigates or
|
|
85
|
+
* dispatches — an app rail stays a rail. Default `true`, the
|
|
86
|
+
* brand ruling ("pressing any icon would expand the sidebar")
|
|
87
|
+
* `defaultCollapsed` boots collapsed when nothing is stored (useDragResize)
|
|
67
88
|
* `background` (2026-08-09, prop): `true` (default) paints the rail surface;
|
|
68
89
|
* `false` renders chromeless so the rail can float over a consumer's media
|
|
69
90
|
* (the brand-hero model — the consumer owns the plane). `hairline` (2026-08-26,
|
|
@@ -122,7 +143,14 @@ function SideNavInner({
|
|
|
122
143
|
* name; a rail floating over media and a rail without a line are different
|
|
123
144
|
* choices. */
|
|
124
145
|
hairline = false,
|
|
146
|
+
bottomItems = [],
|
|
147
|
+
footer,
|
|
148
|
+
themeToggle = true,
|
|
149
|
+
iconComponent,
|
|
150
|
+
expandOnSelect = true,
|
|
151
|
+
defaultCollapsed = false,
|
|
125
152
|
}) {
|
|
153
|
+
const Glyph = iconComponent ?? Icon
|
|
126
154
|
const isPageActive = (to) => (isActive ? isActive(to) : pathname === to)
|
|
127
155
|
/* a leaf is lit by its route, or — an action leaf — by its own `active` */
|
|
128
156
|
const isLeafActive = (leaf) => (leaf.onSelect ? !!leaf.active : isPageActive(leaf.to))
|
|
@@ -135,13 +163,13 @@ function SideNavInner({
|
|
|
135
163
|
const handleNavigate = (e, to) => {
|
|
136
164
|
onNavigate?.(e, to)
|
|
137
165
|
onCloseDrawer?.()
|
|
138
|
-
if (collapsed) toggleCollapsed()
|
|
166
|
+
if (collapsed && expandOnSelect) toggleCollapsed()
|
|
139
167
|
}
|
|
140
168
|
/* an action leaf dispatches — same drawer / rail behaviour as a navigation */
|
|
141
169
|
const handleSelect = (e, row) => {
|
|
142
170
|
row.onSelect(e)
|
|
143
171
|
onCloseDrawer?.()
|
|
144
|
-
if (collapsed) toggleCollapsed()
|
|
172
|
+
if (collapsed && expandOnSelect) toggleCollapsed()
|
|
145
173
|
}
|
|
146
174
|
/* the route leaf: NavLink under a router, `<a href>` on the agnostic seam */
|
|
147
175
|
const RouteLeaf = ({ to, className, style, children }) => router ? (
|
|
@@ -165,7 +193,7 @@ function SideNavInner({
|
|
|
165
193
|
* storage schema and the state; the chip Button and the grab edge both
|
|
166
194
|
* drive it. */
|
|
167
195
|
const asideRef = useRef(null)
|
|
168
|
-
const { collapsed, toggleCollapsed, grabProps } = useDragResize(asideRef)
|
|
196
|
+
const { collapsed, toggleCollapsed, grabProps } = useDragResize(asideRef, { defaultCollapsed })
|
|
169
197
|
|
|
170
198
|
/* Rows under a category: a leaf is a NavLink; a group is a non-routing
|
|
171
199
|
* header over its own rows, one step further in (recursive). A group lights
|
|
@@ -211,25 +239,8 @@ function SideNavInner({
|
|
|
211
239
|
)
|
|
212
240
|
})
|
|
213
241
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
ref={asideRef}
|
|
217
|
-
/* Box utilities deliberately absent (SideNavMobilePosition, 2026-08-25):
|
|
218
|
-
* the aside's position/height/stack live in .kol-sidenav
|
|
219
|
-
* (kol-framework.css). `sticky … h-dvh z-20` here outranked the 767px
|
|
220
|
-
* drawer rule's `position: fixed`, so every consumer page opened one
|
|
221
|
-
* viewport down on phones. Only colour utilities ride the element. */
|
|
222
|
-
className={`kol-sidenav${background ? ' bg-surface-primary' : ''}${hairline ? ' border-r border-fg-08' : ''}${collapsed ? ' is-collapsed' : ''}${drawerOpen ? ' is-drawer-open' : ''}`}
|
|
223
|
-
>
|
|
224
|
-
{/* NO chip Button in either state (user build order 2026-08-09 — "MAKE
|
|
225
|
-
* IT"; supersedes the elder A/B): the pill-marked grab edge below is
|
|
226
|
-
* THE single collapse/resize control — click toggles, drag resizes,
|
|
227
|
-
* snap-to-default on release, Home/Enter/Space on the keyboard. */}
|
|
228
|
-
|
|
229
|
-
<div className="kol-sidenav-scroll flex-1 flex flex-col justify-between overflow-y-auto pt-4 pb-4 [scrollbar-width:thin]">
|
|
230
|
-
<nav aria-label="Sections">
|
|
231
|
-
<ul className="kol-sidenav-tree flex flex-col gap-[2px]">
|
|
232
|
-
{navTree.map((cat, ci) => {
|
|
242
|
+
/* one renderer for the tree and the pinned rows (RailSideNavPixelParity) */
|
|
243
|
+
const renderCat = (cat, ci) => {
|
|
233
244
|
const pages = pagesOf(cat)
|
|
234
245
|
const isOpen = openCats.has(cat.id)
|
|
235
246
|
const isLink = !pages.length && cat.to
|
|
@@ -251,7 +262,7 @@ function SideNavInner({
|
|
|
251
262
|
const rowCls = 'kol-sidenav-hop kol-helper-12 text-emphasis no-underline'
|
|
252
263
|
const glyph = (
|
|
253
264
|
<span className="kol-sidenav-hop-icon inline-flex items-center justify-center w-5 h-5 shrink-0" aria-hidden="true">
|
|
254
|
-
<
|
|
265
|
+
<Glyph name={cat.icon} size={16} />
|
|
255
266
|
</span>
|
|
256
267
|
)
|
|
257
268
|
/* A category is a <button>, not a link — it has no route by
|
|
@@ -316,35 +327,75 @@ function SideNavInner({
|
|
|
316
327
|
)}
|
|
317
328
|
</li>
|
|
318
329
|
)
|
|
319
|
-
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<aside
|
|
334
|
+
ref={asideRef}
|
|
335
|
+
/* Box utilities deliberately absent (SideNavMobilePosition, 2026-08-25):
|
|
336
|
+
* the aside's position/height/stack live in .kol-sidenav
|
|
337
|
+
* (kol-framework.css). `sticky … h-dvh z-20` here outranked the 767px
|
|
338
|
+
* drawer rule's `position: fixed`, so every consumer page opened one
|
|
339
|
+
* viewport down on phones. Only colour utilities ride the element. */
|
|
340
|
+
className={`kol-sidenav${background ? ' bg-surface-primary' : ''}${hairline ? ' border-r border-fg-08' : ''}${collapsed ? ' is-collapsed' : ''}${drawerOpen ? ' is-drawer-open' : ''}`}
|
|
341
|
+
>
|
|
342
|
+
{/* NO chip Button in either state (user build order 2026-08-09 — "MAKE
|
|
343
|
+
* IT"; supersedes the elder A/B): the pill-marked grab edge below is
|
|
344
|
+
* THE single collapse/resize control — click toggles, drag resizes,
|
|
345
|
+
* snap-to-default on release, Home/Enter/Space on the keyboard. */}
|
|
346
|
+
|
|
347
|
+
{/* ONLY the tree scrolls (fxr's re-measure, 2026-08-28): the theme slot
|
|
348
|
+
* and the pinned rows used to sit inside this region, so on a 26-row
|
|
349
|
+
* collapsed tree they scrolled off below the footer — "pinned" only
|
|
350
|
+
* held while the tree was shorter than the rail. They are the block
|
|
351
|
+
* after it now, above the footer, the tree scrolling underneath. */}
|
|
352
|
+
<div className="kol-sidenav-scroll flex-1 overflow-y-auto pt-4 pb-4 [scrollbar-width:thin]">
|
|
353
|
+
<nav aria-label="Sections">
|
|
354
|
+
<ul className="kol-sidenav-tree flex flex-col gap-[2px]">
|
|
355
|
+
{navTree.map(renderCat)}
|
|
320
356
|
</ul>
|
|
321
357
|
</nav>
|
|
322
|
-
|
|
323
|
-
{/* hop-bare carries its own gutter padding; collapsed falls back to
|
|
324
|
-
`icon` — a full-width label can't sit in the collapsed rail. */}
|
|
325
|
-
<div className="kol-sidenav-theme-slot flex">
|
|
326
|
-
<ThemeToggle variant={collapsed ? 'icon' : 'hop-bare'} size="md" />
|
|
327
|
-
</div>
|
|
328
358
|
</div>
|
|
329
359
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
</a>
|
|
360
|
+
{/* hop-bare carries its own gutter padding; collapsed falls back to
|
|
361
|
+
`icon` — a full-width label can't sit in the collapsed rail. */}
|
|
362
|
+
<div className="pb-4">
|
|
363
|
+
{themeToggle && (
|
|
364
|
+
<div className="kol-sidenav-theme-slot flex">
|
|
365
|
+
<ThemeToggle variant={collapsed ? 'icon' : 'hop-bare'} size="md" />
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
{/* pinned rows — BELOW the theme, above the footer: the shell's bottom
|
|
369
|
+
* pair (theme → Settings), specified once */}
|
|
370
|
+
{bottomItems.length > 0 && (
|
|
371
|
+
<nav aria-label="Pinned">
|
|
372
|
+
<ul className="kol-sidenav-tree flex flex-col gap-[2px]">{bottomItems.map(renderCat)}</ul>
|
|
373
|
+
</nav>
|
|
374
|
+
)}
|
|
346
375
|
</div>
|
|
347
376
|
|
|
377
|
+
{footer !== false && footer !== null && (
|
|
378
|
+
<div className="kol-sidenav-footer flex items-center h-14 border-t border-fg-08 min-w-0">
|
|
379
|
+
{footer !== undefined ? footer : (
|
|
380
|
+
<a
|
|
381
|
+
href="https://kolkrabbi.io"
|
|
382
|
+
target="_blank"
|
|
383
|
+
rel="noopener"
|
|
384
|
+
className="kol-helper-10 !font-normal no-underline group whitespace-nowrap overflow-hidden text-ellipsis min-w-0"
|
|
385
|
+
>
|
|
386
|
+
{/* Collapsed rail shows the initial alone; the full wordmark
|
|
387
|
+
overflows it. Both live in one <a> so the link survives either
|
|
388
|
+
state (CSS swaps them on the data-sidenav stamp). */}
|
|
389
|
+
<span className="kol-sidenav-footer-mark text-body group-hover:text-emphasis">K</span>
|
|
390
|
+
<span className="kol-sidenav-footer-full">
|
|
391
|
+
<span className="text-body group-hover:text-emphasis">Kolkrabbi Vinnustofa</span>
|
|
392
|
+
<span className="text-meta group-hover:text-emphasis"> · {new Date().getFullYear()}</span>
|
|
393
|
+
</span>
|
|
394
|
+
</a>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
|
|
348
399
|
{/* Grab edge — width/cursor/focus chrome in kol-framework.css
|
|
349
400
|
* (.kol-sidenav-grab); hidden below lg by the narrow-mode block. */}
|
|
350
401
|
<div className="kol-sidenav-grab absolute top-0 right-0 bottom-0 z-[1]" {...grabProps} />
|
package/src/useDragResize.js
CHANGED
|
@@ -96,7 +96,7 @@ const readBack = (n) => {
|
|
|
96
96
|
* rightward drag = wider. 'right' inverts both the pointer sign and
|
|
97
97
|
* the arrow keys. */
|
|
98
98
|
export default function useDragResize(ref, options = {}) {
|
|
99
|
-
const { token = 'kol-sidenav', side = 'left' } = options
|
|
99
|
+
const { token = 'kol-sidenav', side = 'left', defaultCollapsed = false } = options
|
|
100
100
|
/* -1 on a right-hand rail: the same rightward pointer travel that widens a
|
|
101
101
|
* left rail must NARROW a right one, because its handle faces the canvas. */
|
|
102
102
|
const dir = side === 'right' ? -1 : 1
|
|
@@ -131,14 +131,18 @@ export default function useDragResize(ref, options = {}) {
|
|
|
131
131
|
defaultPx.current = readVarPx(names.wVar)
|
|
132
132
|
collapsedPx.current = readVarPx(names.collapsedVar)
|
|
133
133
|
let w = null
|
|
134
|
-
let
|
|
134
|
+
let stored = null
|
|
135
135
|
try {
|
|
136
136
|
w = parseFloat(localStorage.getItem(names.widthKey)) || null
|
|
137
|
-
|
|
137
|
+
stored = localStorage.getItem(names.stateKey)
|
|
138
138
|
} catch { /* storage blocked */ }
|
|
139
|
+
/* nothing stored → the consumer's boot state (RailSideNavPixelParity,
|
|
140
|
+
* 2026-08-28): an app rail boots collapsed, the brand sidebar boots open;
|
|
141
|
+
* the first drag or click persists and the default never speaks again */
|
|
142
|
+
const c = stored ? stored === 'collapsed' : !!defaultCollapsed
|
|
139
143
|
if (w) { writeWidth(names, w); setWidthPx(w) }
|
|
140
144
|
if (c) { stampCollapsed(names, true); setCollapsed(true) }
|
|
141
|
-
}, [names])
|
|
145
|
+
}, [names, defaultCollapsed])
|
|
142
146
|
|
|
143
147
|
useEffect(() => {
|
|
144
148
|
const onMove = (e) => {
|