@kolkrabbi/kol-framework 0.14.0 → 0.15.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 +11 -0
- package/package.json +2 -2
- package/src/AppShell.jsx +5 -2
- package/src/SideNav.jsx +164 -232
- package/src/index.js +2 -2
- package/src/useDragResize.js +10 -0
package/kol-framework.css
CHANGED
|
@@ -124,6 +124,17 @@ body {
|
|
|
124
124
|
:root[data-sidenav="collapsed"] .kol-sidenav-body { display: none; }
|
|
125
125
|
:root[data-sidenav="collapsed"] .kol-sidenav-hop-count,
|
|
126
126
|
:root[data-sidenav="collapsed"] .kol-sidenav-section-toggle { display: none; }
|
|
127
|
+
/* Folded back from the elder's brand-local sidenav-collapse.css (2026-08-09
|
|
128
|
+
* port): the disclosure caret and the disclosed list are noise in a rail with
|
|
129
|
+
* no labels; the theme slot and footer centre; the footer wordmark swaps for
|
|
130
|
+
* its initial — both spans live in one <a>, so the link survives either state. */
|
|
131
|
+
:root[data-sidenav="collapsed"] .kol-sidenav-hop-caret,
|
|
132
|
+
:root[data-sidenav="collapsed"] .kol-sidenav-list { display: none; }
|
|
133
|
+
:root[data-sidenav="collapsed"] .kol-sidenav-theme-slot { padding-inline: 0; justify-content: center; }
|
|
134
|
+
:root[data-sidenav="collapsed"] .kol-sidenav-footer { padding-inline: 0; justify-content: center; }
|
|
135
|
+
.kol-sidenav-footer-mark { display: none; }
|
|
136
|
+
:root[data-sidenav="collapsed"] .kol-sidenav-footer-mark { display: inline; }
|
|
137
|
+
:root[data-sidenav="collapsed"] .kol-sidenav-footer-full { display: none; }
|
|
127
138
|
|
|
128
139
|
/* The grab edge itself — invisible by spec (col-resize cursor is the pointer
|
|
129
140
|
* affordance, the focus ring the keyboard one — recipe from
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"app-shell"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@kolkrabbi/kol-component": "0.
|
|
30
|
+
"@kolkrabbi/kol-component": "0.27.0",
|
|
31
31
|
"@kolkrabbi/kol-icons": "0.10.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
package/src/AppShell.jsx
CHANGED
|
@@ -15,7 +15,10 @@ export const ShellTocContext = createContext(null)
|
|
|
15
15
|
* useLayoutEffect(() => { setTocCollapsed(true); return () => setTocCollapsed(false) }, [setTocCollapsed]) */
|
|
16
16
|
export const ShellTocCollapsedContext = createContext(null)
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/* `getActivePage` — ACCEPTED-but-inert since the 2026-08-09 SideNav port (the
|
|
19
|
+
* two-level elder model derives the active category from the route); kept so
|
|
20
|
+
* no call site breaks. */
|
|
21
|
+
export default function AppShell({ navTree = [], getActivePage: _getActivePage, header, footer, defaultTocContent }) {
|
|
19
22
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
|
20
23
|
const [tocContent, setTocContent] = useState(null)
|
|
21
24
|
const [tocCollapsed, setTocCollapsed] = useState(false)
|
|
@@ -58,7 +61,7 @@ export default function AppShell({ navTree = [], getActivePage, header, footer,
|
|
|
58
61
|
aria-hidden="true"
|
|
59
62
|
/>
|
|
60
63
|
|
|
61
|
-
<SideNav navTree={navTree}
|
|
64
|
+
<SideNav navTree={navTree} drawerOpen={drawerOpen} onCloseDrawer={() => setDrawerOpen(false)} />
|
|
62
65
|
<div className="min-w-0">
|
|
63
66
|
{header}
|
|
64
67
|
<Outlet />
|
package/src/SideNav.jsx
CHANGED
|
@@ -1,256 +1,182 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { NavLink, useLocation } from 'react-router-dom'
|
|
2
|
+
import { useRef, useState } from 'react'
|
|
3
3
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
4
|
+
import { Button, Tooltip } from '@kolkrabbi/kol-component'
|
|
4
5
|
import ThemeToggle from './ThemeToggle'
|
|
5
6
|
import useDragResize from './useDragResize'
|
|
6
|
-
import { useScrollSpy } from '@kolkrabbi/kol-component'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
'
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
</li>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function RouteLeaf({ leaf, indent, onNavigate, isActiveFn }) {
|
|
57
|
-
return (
|
|
58
|
-
<li>
|
|
59
|
-
<NavLink
|
|
60
|
-
to={leaf.to}
|
|
61
|
-
end
|
|
62
|
-
className={({ isActive }) => ((isActiveFn ? isActiveFn(leaf.to) : isActive) ? linkActiveCls : linkCls)}
|
|
63
|
-
style={leafStyle(indent)}
|
|
64
|
-
onClick={onNavigate}
|
|
65
|
-
>
|
|
66
|
-
{leaf.label}
|
|
67
|
-
</NavLink>
|
|
68
|
-
</li>
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function GroupNode({ group, basePath, activeSectionId, indent, onNavigate, isActiveFn }) {
|
|
73
|
-
const isAncestor = hasActiveDescendant(group.children, activeSectionId)
|
|
74
|
-
return (
|
|
75
|
-
<li>
|
|
76
|
-
<div
|
|
77
|
-
className={`kol-sidenav-group kol-helper-10 ${isAncestor ? 'text-emphasis' : 'text-subtle'}`}
|
|
78
|
-
style={{ paddingLeft: indent }}
|
|
79
|
-
>
|
|
80
|
-
{group.label}
|
|
81
|
-
</div>
|
|
82
|
-
<ul className="kol-sidenav-list">
|
|
83
|
-
{group.children.map((child, i) => (
|
|
84
|
-
<ChildNode
|
|
85
|
-
key={child.id ?? child.to ?? `g-${i}`}
|
|
86
|
-
child={child}
|
|
87
|
-
basePath={basePath}
|
|
88
|
-
activeSectionId={activeSectionId}
|
|
89
|
-
indent={indent + 12}
|
|
90
|
-
onNavigate={onNavigate}
|
|
91
|
-
isActiveFn={isActiveFn}
|
|
92
|
-
/>
|
|
93
|
-
))}
|
|
94
|
-
</ul>
|
|
95
|
-
</li>
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function ChildNode({ child, basePath, activeSectionId, indent, onNavigate, isActiveFn }) {
|
|
100
|
-
if (child.children) {
|
|
101
|
-
return (
|
|
102
|
-
<GroupNode
|
|
103
|
-
group={child}
|
|
104
|
-
basePath={basePath}
|
|
105
|
-
activeSectionId={activeSectionId}
|
|
106
|
-
indent={indent}
|
|
107
|
-
onNavigate={onNavigate}
|
|
108
|
-
isActiveFn={isActiveFn}
|
|
109
|
-
/>
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
if (child.to) {
|
|
113
|
-
return <RouteLeaf leaf={child} indent={indent} onNavigate={onNavigate} isActiveFn={isActiveFn} />
|
|
114
|
-
}
|
|
115
|
-
if (child.id) {
|
|
116
|
-
return (
|
|
117
|
-
<SectionLeaf
|
|
118
|
-
leaf={child}
|
|
119
|
-
basePath={basePath}
|
|
120
|
-
isActive={activeSectionId === child.id}
|
|
121
|
-
indent={indent}
|
|
122
|
-
onNavigate={onNavigate}
|
|
123
|
-
/>
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
return null
|
|
127
|
-
}
|
|
8
|
+
/**
|
|
9
|
+
* SideNav — the grouped navigation rail.
|
|
10
|
+
*
|
|
11
|
+
* REPLICATED from the elder reference (kol-website/apps/brand
|
|
12
|
+
* src/components/framework/SideNav.jsx, 2026-08-09 user mandate: "there is a
|
|
13
|
+
* better version in kol-website apps:brand") — the brand app's evolved fork
|
|
14
|
+
* folded back into the package, generic seams kept.
|
|
15
|
+
*
|
|
16
|
+
* TWO LEVELS ONLY (elder ruling 2026-08-01). A category is a grouping LABEL —
|
|
17
|
+
* a disclosure <button> with a trailing caret, no route. A page is a route.
|
|
18
|
+
* The `#anchor` scroll-spied section layer is gone, and with it the
|
|
19
|
+
* group/section tree walkers. The ONE exception: a category with `to` and no
|
|
20
|
+
* `pages` (Home) renders as a plain link row — it has nothing to disclose.
|
|
21
|
+
*
|
|
22
|
+
* navTree: [{ id, label, icon, to?, pages?: [{ label, to }] }]
|
|
23
|
+
* (legacy `children` route leaves are read as `pages`; section anchors are not.)
|
|
24
|
+
*
|
|
25
|
+
* Collapse: BOTH affordances share one contract —
|
|
26
|
+
* `:root[data-sidenav="collapsed"]` + the `kol-sidenav` storage schema, owned
|
|
27
|
+
* by useDragResize. The round chip Button (elder A/B, closed 2026-08-01) is
|
|
28
|
+
* the one-click toggle; the grab edge (lobby: SideNavGrabResize, 2026-08-09)
|
|
29
|
+
* resizes, snap-collapses, and carries the keyboard path. Collapsed rows get
|
|
30
|
+
* a DS Tooltip — the glyph is otherwise the only thing naming them.
|
|
31
|
+
*
|
|
32
|
+
* `background` (2026-08-09, prop): `true` (default) paints the rail surface +
|
|
33
|
+
* right hairline; `false` renders chromeless so the rail can float over a
|
|
34
|
+
* consumer's media (the brand-hero model — the consumer owns the plane).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const pageBase =
|
|
38
|
+
'kol-sidenav-link kol-helper-10 block relative py-[4px] pl-14 pr-6 no-underline transition-colors duration-150'
|
|
39
|
+
|
|
40
|
+
/* The active-dot pseudo-element is positioned by `--kol-sidenav-dot-left`
|
|
41
|
+
* (kol-components-atoms.css). The page row indents 3.5rem (pl-14); the dot
|
|
42
|
+
* sits 0.875rem inside that, in the gutter the icon column leaves. */
|
|
43
|
+
const pageStyle = { '--kol-sidenav-dot-left': '2.625rem' }
|
|
44
|
+
/* Category and page must not share a stop: category `emphasis`, page `strong`
|
|
45
|
+
* — two rungs of one ladder, so the tree reads as hierarchy (elder ruling). */
|
|
46
|
+
const pageCls = `${pageBase} text-strong hover:text-emphasis`
|
|
47
|
+
const pageActiveCls = `${pageBase} is-active`
|
|
48
|
+
|
|
49
|
+
/* Legacy DS trees carried `children` (groups + `#anchor` leaves). Route
|
|
50
|
+
* leaves survive as pages; anchor sections died with the two-level ruling. */
|
|
51
|
+
const pagesOf = (cat) => cat.pages ?? cat.children?.filter((c) => c.to) ?? []
|
|
128
52
|
|
|
129
53
|
export default function SideNav({
|
|
130
54
|
drawerOpen = false,
|
|
131
55
|
onCloseDrawer,
|
|
132
56
|
navTree = [],
|
|
133
|
-
getActivePage,
|
|
134
57
|
onNavigate,
|
|
135
|
-
/* INERT since 0.9.2 — accepted so no call site breaks, but nothing reads
|
|
136
|
-
* them; the collapse feature they drove has no CSS. See below. */
|
|
137
|
-
collapsed: _collapsed,
|
|
138
|
-
onToggle: _onToggle,
|
|
139
|
-
collapsibleSections = false,
|
|
140
58
|
isActive,
|
|
59
|
+
background = true,
|
|
141
60
|
}) {
|
|
142
61
|
const { pathname } = useLocation()
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
62
|
+
const isPageActive = (to) => (isActive ? isActive(to) : pathname === to)
|
|
63
|
+
const isActiveCat = (cat) => pagesOf(cat).some((p) => isPageActive(p.to))
|
|
64
|
+
|
|
65
|
+
/* Which categories are disclosed. INDEPENDENT per category (elder ruling
|
|
66
|
+
* 2026-08-01) — arriving somewhere must not silently close wherever you
|
|
67
|
+
* were. Loads closed, in-memory only (elder ruling 2026-08-06). */
|
|
68
|
+
const [openCats, setOpenCats] = useState(() => new Set())
|
|
69
|
+
const toggleCat = (id) => setOpenCats((prev) => {
|
|
70
|
+
const next = new Set(prev)
|
|
71
|
+
next.has(id) ? next.delete(id) : next.add(id)
|
|
72
|
+
return next
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
/* One collapse contract, two gestures — the hook owns the stamp, the
|
|
76
|
+
* storage schema and the state; the chip Button and the grab edge both
|
|
77
|
+
* drive it. */
|
|
154
78
|
const asideRef = useRef(null)
|
|
155
|
-
const { grabProps } = useDragResize(asideRef)
|
|
156
|
-
|
|
157
|
-
/* Per-page section expand/collapse (opt-in via collapsibleSections).
|
|
158
|
-
* The section containing the active route auto-expands on every
|
|
159
|
-
* navigation; manual toggles persist for the session. */
|
|
160
|
-
const [expandedSections, setExpandedSections] = useState(() =>
|
|
161
|
-
collapsibleSections && activePage?.id ? { [activePage.id]: true } : {}
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
if (!collapsibleSections || !activePage?.id) return
|
|
166
|
-
setExpandedSections((prev) => (prev[activePage.id] ? prev : { ...prev, [activePage.id]: true }))
|
|
167
|
-
}, [collapsibleSections, activePage?.id, pathname])
|
|
168
|
-
|
|
169
|
-
const toggleSection = (pageId) =>
|
|
170
|
-
setExpandedSections((prev) => ({ ...prev, [pageId]: !prev[pageId] }))
|
|
79
|
+
const { collapsed, toggleCollapsed, grabProps } = useDragResize(asideRef)
|
|
171
80
|
|
|
172
81
|
return (
|
|
173
82
|
<aside
|
|
174
83
|
ref={asideRef}
|
|
175
|
-
className={`kol-sidenav sticky top-0 self-start h-dvh flex flex-col border-r border-fg-08
|
|
84
|
+
className={`kol-sidenav sticky top-0 self-start h-dvh flex flex-col z-20${background ? ' bg-surface-primary border-r border-fg-08' : ''}${collapsed ? ' is-collapsed' : ''}${drawerOpen ? ' is-drawer-open' : ''}`}
|
|
176
85
|
>
|
|
177
|
-
{/* The
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
|
|
181
|
-
|
|
86
|
+
{/* The elder A/B (closed 2026-08-01): the DS Button IS the collapse
|
|
87
|
+
* control. Offset is half the pinned sm square, centring it on the
|
|
88
|
+
* rail's edge — the DS hardcodes that square and exposes no token. */}
|
|
89
|
+
<Button
|
|
90
|
+
variant="primary"
|
|
91
|
+
size="sm"
|
|
92
|
+
radius="full"
|
|
93
|
+
iconOnly={collapsed ? 'chevron-right' : 'chevron-left'}
|
|
94
|
+
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
95
|
+
aria-expanded={!collapsed}
|
|
96
|
+
title={collapsed ? 'Expand' : 'Collapse'}
|
|
97
|
+
onClick={toggleCollapsed}
|
|
98
|
+
className="kol-sidenav-toggle absolute top-5 right-[-0.875rem] z-[2]"
|
|
99
|
+
/>
|
|
182
100
|
|
|
183
101
|
<div className="kol-sidenav-scroll flex-1 flex flex-col justify-between overflow-y-auto pt-4 pb-4 [scrollbar-width:thin]">
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
end={page.to === '/'}
|
|
193
|
-
className={({ isActive: navActive }) =>
|
|
194
|
-
`kol-sidenav-hop kol-helper-12 relative flex items-center gap-3 py-2 pr-10 pl-6 no-underline${(isActive ? isActive(page.to) : navActive) ? ' is-active' : ''}`
|
|
195
|
-
}
|
|
196
|
-
onClick={onNavigate}
|
|
197
|
-
>
|
|
102
|
+
<nav aria-label="Sections">
|
|
103
|
+
<ul className="kol-sidenav-tree flex flex-col gap-[2px]">
|
|
104
|
+
{navTree.map((cat) => {
|
|
105
|
+
const pages = pagesOf(cat)
|
|
106
|
+
const isOpen = openCats.has(cat.id)
|
|
107
|
+
const isLink = !pages.length && cat.to
|
|
108
|
+
const rowCls = 'kol-sidenav-hop kol-helper-12 text-emphasis w-full relative flex items-center gap-3 py-2 pr-10 pl-6 no-underline'
|
|
109
|
+
const glyph = (
|
|
198
110
|
<span className="kol-sidenav-hop-icon inline-flex items-center justify-center w-5 h-5 shrink-0" aria-hidden="true">
|
|
199
|
-
<Icon name={
|
|
111
|
+
<Icon name={cat.icon} size={16} />
|
|
200
112
|
</span>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
113
|
+
)
|
|
114
|
+
/* A category is a <button>, not a link — it has no route by
|
|
115
|
+
definition, and an <a href> would promise a page that does
|
|
116
|
+
not exist. Clicking it discloses its pages. */
|
|
117
|
+
const head = isLink ? (
|
|
118
|
+
<NavLink
|
|
119
|
+
to={cat.to}
|
|
120
|
+
end
|
|
121
|
+
className={({ isActive: navActive }) => `${rowCls}${(isActive ? isActive(cat.to) : navActive) ? ' is-active' : ''}`}
|
|
122
|
+
onClick={onNavigate}
|
|
123
|
+
>
|
|
124
|
+
{glyph}
|
|
125
|
+
<span className="kol-sidenav-hop-label flex-1 min-w-0">{cat.label}</span>
|
|
126
|
+
</NavLink>
|
|
127
|
+
) : (
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
className={`${rowCls} bg-transparent border-0 cursor-pointer text-left${isActiveCat(cat) ? ' is-active' : ''}`}
|
|
131
|
+
aria-expanded={isOpen}
|
|
132
|
+
onClick={() => toggleCat(cat.id)}
|
|
133
|
+
>
|
|
134
|
+
{glyph}
|
|
135
|
+
<span className="kol-sidenav-hop-label flex-1 min-w-0">{cat.label}</span>
|
|
136
|
+
<Icon
|
|
137
|
+
name="chevron-down"
|
|
138
|
+
size={12}
|
|
139
|
+
className={`kol-sidenav-hop-caret absolute top-3 right-3 transition-transform duration-150${isOpen ? '' : ' -rotate-90'}`}
|
|
140
|
+
/>
|
|
141
|
+
</button>
|
|
142
|
+
)
|
|
143
|
+
return (
|
|
144
|
+
<li key={cat.id} className="relative">
|
|
145
|
+
{/* Collapsed rail hides the label — the tooltip is the only
|
|
146
|
+
thing naming the row. Portalled, so overflow can't clip it. */}
|
|
147
|
+
{collapsed ? (
|
|
148
|
+
<Tooltip label={cat.label} placement="right" triggerClassName="block">
|
|
149
|
+
{head}
|
|
150
|
+
</Tooltip>
|
|
151
|
+
) : head}
|
|
152
|
+
|
|
153
|
+
{!isLink && isOpen && (
|
|
154
|
+
<ul className="kol-sidenav-list mb-2 flex flex-col gap-[2px]">
|
|
155
|
+
{pages.map((page) => (
|
|
156
|
+
<li key={page.to}>
|
|
157
|
+
<NavLink
|
|
158
|
+
to={page.to}
|
|
159
|
+
end
|
|
160
|
+
style={pageStyle}
|
|
161
|
+
className={({ isActive: navActive }) => ((isActive ? isActive(page.to) : navActive) ? pageActiveCls : pageCls)}
|
|
162
|
+
onClick={onNavigate}
|
|
163
|
+
>
|
|
164
|
+
{page.label}
|
|
165
|
+
</NavLink>
|
|
166
|
+
</li>
|
|
167
|
+
))}
|
|
168
|
+
</ul>
|
|
169
|
+
)}
|
|
170
|
+
</li>
|
|
171
|
+
)
|
|
172
|
+
})}
|
|
173
|
+
</ul>
|
|
174
|
+
</nav>
|
|
175
|
+
|
|
176
|
+
{/* hop-bare carries its own gutter padding; collapsed falls back to
|
|
177
|
+
`icon` — a full-width label can't sit in the collapsed rail. */}
|
|
178
|
+
<div className="kol-sidenav-theme-slot flex">
|
|
179
|
+
<ThemeToggle variant={collapsed ? 'icon' : 'hop-bare'} size="md" />
|
|
254
180
|
</div>
|
|
255
181
|
</div>
|
|
256
182
|
|
|
@@ -261,8 +187,14 @@ export default function SideNav({
|
|
|
261
187
|
rel="noopener"
|
|
262
188
|
className="kol-helper-10 !font-normal no-underline group whitespace-nowrap overflow-hidden text-ellipsis min-w-0"
|
|
263
189
|
>
|
|
264
|
-
|
|
265
|
-
|
|
190
|
+
{/* Collapsed rail shows the initial alone; the full wordmark
|
|
191
|
+
overflows it. Both live in one <a> so the link survives either
|
|
192
|
+
state (CSS swaps them on the data-sidenav stamp). */}
|
|
193
|
+
<span className="kol-sidenav-footer-mark text-body group-hover:text-emphasis">K</span>
|
|
194
|
+
<span className="kol-sidenav-footer-full">
|
|
195
|
+
<span className="text-body group-hover:text-emphasis">Kolkrabbi Vinnustofa</span>
|
|
196
|
+
<span className="text-meta group-hover:text-emphasis"> · {new Date().getFullYear()}</span>
|
|
197
|
+
</span>
|
|
266
198
|
</a>
|
|
267
199
|
</div>
|
|
268
200
|
|
package/src/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @kol/framework — shared KOL app shell.
|
|
3
3
|
*
|
|
4
4
|
* Site chrome (sidenav, layout, theme toggle, footer, heroes) consumed across
|
|
5
|
-
* apps. SideNav takes its nav data as props (navTree
|
|
6
|
-
*
|
|
5
|
+
* apps. SideNav takes its nav data as props (navTree) so the tree stays
|
|
6
|
+
* app-local. CSS lives in src/styles: kol-framework.css
|
|
7
7
|
* and kol-brand-color.css.
|
|
8
8
|
*/
|
|
9
9
|
|
package/src/useDragResize.js
CHANGED
|
@@ -134,6 +134,15 @@ export default function useDragResize(asideRef) {
|
|
|
134
134
|
syncAndPersist()
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/* One-click toggle for the chip Button (elder A/B, folded back 2026-08-09)
|
|
138
|
+
* — same stamp, same storage schema, so the chip and the grab edge can
|
|
139
|
+
* never disagree about the state. */
|
|
140
|
+
const toggleCollapsed = () => {
|
|
141
|
+
const { collapsed: c } = readBack()
|
|
142
|
+
stampCollapsed(!c)
|
|
143
|
+
syncAndPersist()
|
|
144
|
+
}
|
|
145
|
+
|
|
137
146
|
const onKeyDown = (e) => {
|
|
138
147
|
const snapPx = readVarPx('--kol-sidenav-snap')
|
|
139
148
|
const stepPx = readVarPx('--kol-sidenav-step')
|
|
@@ -160,6 +169,7 @@ export default function useDragResize(asideRef) {
|
|
|
160
169
|
|
|
161
170
|
return {
|
|
162
171
|
collapsed,
|
|
172
|
+
toggleCollapsed,
|
|
163
173
|
grabProps: {
|
|
164
174
|
role: 'separator',
|
|
165
175
|
'aria-orientation': 'vertical',
|