@kolkrabbi/kol-framework 0.22.0 → 0.24.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 +40 -1
- package/package.json +3 -3
- package/src/ShellHeader.jsx +22 -1
- package/src/SideNav.jsx +72 -27
package/kol-framework.css
CHANGED
|
@@ -150,8 +150,37 @@ body {
|
|
|
150
150
|
gap: 0.125rem;
|
|
151
151
|
margin-bottom: 0.5rem;
|
|
152
152
|
}
|
|
153
|
+
/* A nested route GROUP's header (sidenav-nested-groups, kol-studio
|
|
154
|
+
* 2026-08-26): a non-routing label sitting where a leaf sits, its rows one
|
|
155
|
+
* step further in. Type and colour ride the element as utilities
|
|
156
|
+
* (kol-helper-10; text-subtle, text-emphasis when a leaf beneath it is the
|
|
157
|
+
* route); the box lives here. No text-transform — the retired fork's
|
|
158
|
+
* `uppercase` does not return (type law: only the display voice carries one). */
|
|
159
|
+
.kol-sidenav-group {
|
|
160
|
+
display: block;
|
|
161
|
+
padding: 0.25rem 1.5rem 0.25rem 3.5rem;
|
|
162
|
+
}
|
|
153
163
|
.kol-sidenav-footer { padding-inline: 1.5rem 1rem; }
|
|
154
164
|
|
|
165
|
+
/* The aside's OWN box (SideNavMobilePosition, kol-website 2026-08-25) — the
|
|
166
|
+
* hop's law above, one element up. `sticky top-0 self-start h-dvh flex
|
|
167
|
+
* flex-col z-20` rode the className, and a utility outranks every rule in
|
|
168
|
+
* this layer — so the 767px drawer block below (`position: fixed`) never won:
|
|
169
|
+
* the aside stayed IN FLOW, the one-column grid handed it an 852px row, and
|
|
170
|
+
* every consumer page opened one viewport down on phones (probe: aside
|
|
171
|
+
* `sticky`, content top=852 at 393; the consumer's stopgap was one media rule
|
|
172
|
+
* re-stating `fixed`). Declared here, the drawer rule beats it by source
|
|
173
|
+
* order — which is how it was written to work. */
|
|
174
|
+
.kol-sidenav {
|
|
175
|
+
position: sticky;
|
|
176
|
+
top: 0;
|
|
177
|
+
align-self: start;
|
|
178
|
+
height: 100dvh;
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
z-index: var(--kol-z-sticky);
|
|
182
|
+
}
|
|
183
|
+
|
|
155
184
|
/* :root[data-sidenav="collapsed"] — the manual collapse, rebuilt DELIBERATELY
|
|
156
185
|
* 2026-08-09 (lobby: SideNavGrabResize) after the 2026-07-29 removal of the
|
|
157
186
|
* renderer-less stamp. SideNav's grab edge (useDragResize.js) owns the state
|
|
@@ -894,7 +923,15 @@ body {
|
|
|
894
923
|
}
|
|
895
924
|
}
|
|
896
925
|
|
|
897
|
-
/* Tab strip — horizontally scrollable, scrollbar hidden.
|
|
926
|
+
/* Tab strip — horizontally scrollable, scrollbar hidden.
|
|
927
|
+
* Snaps to a tab's start on a flick (WorkshopShellMobile, kol-website
|
|
928
|
+
* 2026-08-25): the strip measured 613px of tabs in a 345px viewport and did
|
|
929
|
+
* scroll, but a swipe could settle on half a label. `proximity`, not
|
|
930
|
+
* `mandatory` — the last tabs can never reach a start position, and mandatory
|
|
931
|
+
* would refuse to show them. ShellHeader.jsx scrolls the active tab into view
|
|
932
|
+
* on load, to the same start edge, so the two never fight. NO edge fade: the
|
|
933
|
+
* 2026-07-28 ruling bans scroll-edge paint (the table wrapper's cover
|
|
934
|
+
* gradients died for it 2026-08-09). */
|
|
898
935
|
.kol-shell-header-tabs {
|
|
899
936
|
display: flex;
|
|
900
937
|
flex-wrap: nowrap;
|
|
@@ -903,6 +940,7 @@ body {
|
|
|
903
940
|
padding-top: 8px;
|
|
904
941
|
scrollbar-width: none;
|
|
905
942
|
-ms-overflow-style: none;
|
|
943
|
+
scroll-snap-type: x proximity;
|
|
906
944
|
}
|
|
907
945
|
|
|
908
946
|
.kol-shell-header-tabs::-webkit-scrollbar {
|
|
@@ -931,6 +969,7 @@ body {
|
|
|
931
969
|
border-bottom: 2px solid transparent;
|
|
932
970
|
transition: color 0.2s ease, border-color 0.2s ease;
|
|
933
971
|
text-decoration: none;
|
|
972
|
+
scroll-snap-align: start;
|
|
934
973
|
}
|
|
935
974
|
|
|
936
975
|
.kol-shell-header-tab:hover {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"app-shell"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@kolkrabbi/kol-component": "^0.
|
|
32
|
-
"@kolkrabbi/kol-icons": "^0.
|
|
31
|
+
"@kolkrabbi/kol-component": "^0.68.1",
|
|
32
|
+
"@kolkrabbi/kol-icons": "^0.18.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": "^18.3.0 || ^19.0.0",
|
package/src/ShellHeader.jsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from 'react'
|
|
1
2
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
2
3
|
import { IconFrame, SearchInput, Tooltip, glyphSize } from '@kolkrabbi/kol-component'
|
|
3
4
|
import ThemeToggle from './ThemeToggle.jsx'
|
|
@@ -89,6 +90,26 @@ export default function ShellHeader({
|
|
|
89
90
|
}) {
|
|
90
91
|
const hasTabRow = nav.length > 0 || Boolean(search) || Boolean(onNavToggle) || Boolean(onTocToggle)
|
|
91
92
|
|
|
93
|
+
/* The current tab must be ON SCREEN (WorkshopShellMobile, kol-website
|
|
94
|
+
* 2026-08-25). The strip scrolls (.kol-shell-header-tabs) — but on a phone
|
|
95
|
+
* the active tab measured x=347–445 in a 345px strip: "Dashboard" was the
|
|
96
|
+
* page you were on and the one tab you could not see. Scrolled to the
|
|
97
|
+
* strip's start edge (the snap position), and ONLY when it is out of view,
|
|
98
|
+
* so a wide viewport never moves. Keyed on the active href, not `nav` —
|
|
99
|
+
* consumers rebuild that array every render. Layout effect: before paint,
|
|
100
|
+
* so a route change never flashes the wrong tab. */
|
|
101
|
+
const tabsRef = useRef(null)
|
|
102
|
+
const activeHref = nav.find((item) => (isActive ? isActive(item.href) : false))?.href
|
|
103
|
+
useLayoutEffect(() => {
|
|
104
|
+
const strip = tabsRef.current
|
|
105
|
+
const tab = strip?.querySelector('[aria-current="page"]')
|
|
106
|
+
if (!strip || !tab) return
|
|
107
|
+
const s = strip.getBoundingClientRect()
|
|
108
|
+
const t = tab.getBoundingClientRect()
|
|
109
|
+
if (t.left >= s.left && t.right <= s.right) return
|
|
110
|
+
strip.scrollLeft += t.left - s.left
|
|
111
|
+
}, [activeHref])
|
|
112
|
+
|
|
92
113
|
return (
|
|
93
114
|
<header className={`kol-shell-header sticky top-0 z-50 shrink-0 bg-surface-primary ${className}`.trim()}>
|
|
94
115
|
{/* Row 1: brand block + controls */}
|
|
@@ -139,7 +160,7 @@ export default function ShellHeader({
|
|
|
139
160
|
{hasTabRow && (
|
|
140
161
|
<div className="border-b border-fg-08">
|
|
141
162
|
<div className="w-full" style={{ paddingInline: 'var(--kol-pad-chrome-x)' }}>
|
|
142
|
-
<div className="kol-shell-header-tabs">
|
|
163
|
+
<div className="kol-shell-header-tabs" ref={tabsRef}>
|
|
143
164
|
<nav className="flex flex-1 gap-6" aria-label="Sections">
|
|
144
165
|
{nav.map((item) => {
|
|
145
166
|
const active = isActive ? isActive(item.href) : false
|
package/src/SideNav.jsx
CHANGED
|
@@ -13,14 +13,26 @@ import useDragResize from './useDragResize'
|
|
|
13
13
|
* better version in kol-website apps:brand") — the brand app's evolved fork
|
|
14
14
|
* folded back into the package, generic seams kept.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `pages` (Home) renders as a plain link row — it has nothing to disclose.
|
|
16
|
+
* A category is a grouping LABEL — a disclosure <button> with a trailing
|
|
17
|
+
* caret, no route. A page is a route. The ONE exception: a category with `to`
|
|
18
|
+
* and no `pages` (Home) renders as a plain link row — it has nothing to
|
|
19
|
+
* disclose.
|
|
21
20
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* The 2026-08-01 "TWO LEVELS ONLY" ruling was the BRAND APP's, about its
|
|
22
|
+
* scroll-spied `#anchor` section layer — that layer stays dead (an `{ id }`
|
|
23
|
+
* leaf is dropped). It was inscribed here as a law for every consumer, and
|
|
24
|
+
* kol-studio lost two groups and six routes from its rail on adopting 0.23.0.
|
|
25
|
+
* User, 2026-08-26, verbatim: "ok that is such a problem, sometimes Im
|
|
26
|
+
* talking about a specific thing and it gets applied as a literal fit-all
|
|
27
|
+
* rule, which isnt the case. its very hard to work in such uncertainty."
|
|
28
|
+
* Re-scoped (sidenav-nested-groups, 0.24.0): nested ROUTE groups are
|
|
29
|
+
* supported. A `{ label, children }` node inside a category renders as a
|
|
30
|
+
* non-routing group header with its rows indented one step under it,
|
|
31
|
+
* recursively. The tree shape is the opt-in — there is no prop — and a tree
|
|
32
|
+
* with no group nodes renders byte-for-byte as before.
|
|
33
|
+
*
|
|
34
|
+
* navTree: [{ id, label, icon, to?, pages?: [{ label, to } | { label, children }] }]
|
|
35
|
+
* (legacy `children` on a category is read as `pages`; section anchors are not.)
|
|
24
36
|
*
|
|
25
37
|
* Collapse: the pill-marked grab edge is THE single control (user build
|
|
26
38
|
* order 2026-08-09, completing SideNavGrabResize — the chip Button is gone
|
|
@@ -47,9 +59,17 @@ const pageStyle = { '--kol-sidenav-dot-left': '2.625rem' }
|
|
|
47
59
|
const pageCls = `${pageBase} text-strong hover:text-emphasis`
|
|
48
60
|
const pageActiveCls = `${pageBase} is-active`
|
|
49
61
|
|
|
50
|
-
/*
|
|
51
|
-
* leaves
|
|
52
|
-
|
|
62
|
+
/* A category's rows: route leaves (`to`) and route groups (`children`).
|
|
63
|
+
* `#anchor` leaves (`id` only) are dropped — that layer died with the brand
|
|
64
|
+
* app's 2026-08-01 ruling and is not reopened here. */
|
|
65
|
+
const rowsOf = (nodes) => nodes.filter((c) => c.to || c.children)
|
|
66
|
+
const pagesOf = (cat) => rowsOf(cat.pages ?? cat.children ?? [])
|
|
67
|
+
/* Every route leaf under a set of rows, through any depth of groups. */
|
|
68
|
+
const leavesOf = (rows) => rows.flatMap((r) => (r.children ? leavesOf(rowsOf(r.children)) : [r]))
|
|
69
|
+
/* A nested row sits one --kol-spacing-3 step further in per depth. Inline,
|
|
70
|
+
* not a rule: `pl-14` rides the leaf as a utility and outranks anything in
|
|
71
|
+
* this layer. Depth 0 carries no indent so a flat tree renders unchanged. */
|
|
72
|
+
const indentOf = (depth) => `calc(3.5rem + ${depth} * var(--kol-spacing-3))`
|
|
53
73
|
|
|
54
74
|
export default function SideNav({
|
|
55
75
|
drawerOpen = false,
|
|
@@ -61,7 +81,7 @@ export default function SideNav({
|
|
|
61
81
|
}) {
|
|
62
82
|
const { pathname } = useLocation()
|
|
63
83
|
const isPageActive = (to) => (isActive ? isActive(to) : pathname === to)
|
|
64
|
-
const isActiveCat = (cat) => pagesOf(cat).some((p) => isPageActive(p.to))
|
|
84
|
+
const isActiveCat = (cat) => leavesOf(pagesOf(cat)).some((p) => isPageActive(p.to))
|
|
65
85
|
|
|
66
86
|
/* Every navigation closes the mobile drawer — the prop sat accepted-but-
|
|
67
87
|
* unread until the kol-website session flagged it (2026-08-09) — and, from
|
|
@@ -90,10 +110,49 @@ export default function SideNav({
|
|
|
90
110
|
const asideRef = useRef(null)
|
|
91
111
|
const { collapsed, toggleCollapsed, grabProps } = useDragResize(asideRef)
|
|
92
112
|
|
|
113
|
+
/* Rows under a category: a leaf is a NavLink; a group is a non-routing
|
|
114
|
+
* header over its own rows, one step further in (recursive). A group lights
|
|
115
|
+
* up the way a category does — when one of its leaves is the current route. */
|
|
116
|
+
const renderRows = (rows, depth) => rows.map((row, i) => {
|
|
117
|
+
if (row.children) {
|
|
118
|
+
const lit = leavesOf(rowsOf(row.children)).some((l) => isPageActive(l.to))
|
|
119
|
+
return (
|
|
120
|
+
<li key={row.id ?? `${row.label}-${i}`}>
|
|
121
|
+
<div
|
|
122
|
+
className={`kol-sidenav-group kol-helper-10${lit ? ' text-emphasis' : ' text-subtle'}`}
|
|
123
|
+
style={depth ? { paddingLeft: indentOf(depth) } : undefined}
|
|
124
|
+
>
|
|
125
|
+
{row.label}
|
|
126
|
+
</div>
|
|
127
|
+
<ul className="kol-sidenav-list">{renderRows(rowsOf(row.children), depth + 1)}</ul>
|
|
128
|
+
</li>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
return (
|
|
132
|
+
<li key={row.to}>
|
|
133
|
+
<NavLink
|
|
134
|
+
to={row.to}
|
|
135
|
+
end
|
|
136
|
+
/* the dot keeps its 0.875rem lead on the text at every depth */
|
|
137
|
+
style={depth ? { paddingLeft: indentOf(depth), '--kol-sidenav-dot-left': `calc(2.625rem + ${depth} * var(--kol-spacing-3))` } : pageStyle}
|
|
138
|
+
className={({ isActive: navActive }) => ((isActive ? isActive(row.to) : navActive) ? pageActiveCls : pageCls)}
|
|
139
|
+
onClick={handleNavigate}
|
|
140
|
+
>
|
|
141
|
+
{row.label}
|
|
142
|
+
</NavLink>
|
|
143
|
+
</li>
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
|
|
93
147
|
return (
|
|
94
148
|
<aside
|
|
95
149
|
ref={asideRef}
|
|
96
|
-
|
|
150
|
+
/* Box utilities deliberately absent (SideNavMobilePosition, 2026-08-25):
|
|
151
|
+
* the aside's position/height/stack live in .kol-sidenav
|
|
152
|
+
* (kol-framework.css). `sticky … h-dvh z-20` here outranked the 767px
|
|
153
|
+
* drawer rule's `position: fixed`, so every consumer page opened one
|
|
154
|
+
* viewport down on phones. Only colour utilities ride the element. */
|
|
155
|
+
className={`kol-sidenav${background ? ' bg-surface-primary border-r border-fg-08' : ''}${collapsed ? ' is-collapsed' : ''}${drawerOpen ? ' is-drawer-open' : ''}`}
|
|
97
156
|
>
|
|
98
157
|
{/* NO chip Button in either state (user build order 2026-08-09 — "MAKE
|
|
99
158
|
* IT"; supersedes the elder A/B): the pill-marked grab edge below is
|
|
@@ -169,21 +228,7 @@ export default function SideNav({
|
|
|
169
228
|
) : head}
|
|
170
229
|
|
|
171
230
|
{!isLink && isOpen && (
|
|
172
|
-
<ul className="kol-sidenav-list">
|
|
173
|
-
{pages.map((page) => (
|
|
174
|
-
<li key={page.to}>
|
|
175
|
-
<NavLink
|
|
176
|
-
to={page.to}
|
|
177
|
-
end
|
|
178
|
-
style={pageStyle}
|
|
179
|
-
className={({ isActive: navActive }) => ((isActive ? isActive(page.to) : navActive) ? pageActiveCls : pageCls)}
|
|
180
|
-
onClick={handleNavigate}
|
|
181
|
-
>
|
|
182
|
-
{page.label}
|
|
183
|
-
</NavLink>
|
|
184
|
-
</li>
|
|
185
|
-
))}
|
|
186
|
-
</ul>
|
|
231
|
+
<ul className="kol-sidenav-list">{renderRows(pages, 0)}</ul>
|
|
187
232
|
)}
|
|
188
233
|
</li>
|
|
189
234
|
)
|