@kolkrabbi/kol-framework 0.16.1 → 0.18.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 +21 -1
- package/package.json +3 -3
- package/src/ShellHeader.jsx +7 -10
- package/src/SideNav.jsx +12 -26
- package/src/useDragResize.js +62 -36
package/kol-framework.css
CHANGED
|
@@ -40,9 +40,10 @@ body {
|
|
|
40
40
|
-dot-left shipped). kol-mirror carried them as bare literals, the
|
|
41
41
|
consumer-app slack the brief bans copying; both widths reference the
|
|
42
42
|
spacing ladder (the mirror strip's width lands on the ladder's first rung). */
|
|
43
|
-
--kol-sidenav-grab-w: var(--kol-spacing-
|
|
43
|
+
--kol-sidenav-grab-w: var(--kol-spacing-2); /* the strip's hit width (doubled 2026-08-09 — the pill made it a click target) */
|
|
44
44
|
--kol-sidenav-snap: 12rem; /* drag under this snaps to -w-collapsed */
|
|
45
45
|
--kol-sidenav-step: var(--kol-spacing-4); /* keyboard resize increment */
|
|
46
|
+
--kol-sidenav-snap-default: var(--kol-sidenav-step); /* release within this of the default clears the override */
|
|
46
47
|
--kol-sidenav-w-collapsed: 56px;
|
|
47
48
|
/* The workshop shell's RIGHT rail. EQUAL TO THE LEFT (user ruling
|
|
48
49
|
2026-08-01) — the two rails frame the same content and a lopsided frame is
|
|
@@ -175,6 +176,25 @@ body {
|
|
|
175
176
|
width: var(--kol-sidenav-grab-w);
|
|
176
177
|
cursor: col-resize;
|
|
177
178
|
}
|
|
179
|
+
/* THE PILL (user build order 2026-08-09): the edge's rest-visible indicator —
|
|
180
|
+
* a vertical rounded bar centred mid-height, dim at rest, full ink on
|
|
181
|
+
* hover/focus. Same law as the record grip: affordances visible at rest. */
|
|
182
|
+
.kol-sidenav-grab::before {
|
|
183
|
+
content: '';
|
|
184
|
+
position: absolute;
|
|
185
|
+
top: 50%;
|
|
186
|
+
left: 50%;
|
|
187
|
+
translate: -50% -50%;
|
|
188
|
+
width: 0.1875rem;
|
|
189
|
+
height: 2rem;
|
|
190
|
+
border-radius: var(--kol-radius-full);
|
|
191
|
+
background: var(--kol-fg-24);
|
|
192
|
+
transition: background-color 150ms ease;
|
|
193
|
+
}
|
|
194
|
+
.kol-sidenav-grab:hover::before,
|
|
195
|
+
.kol-sidenav-grab:focus-visible::before {
|
|
196
|
+
background: var(--kol-fg-64);
|
|
197
|
+
}
|
|
178
198
|
.kol-sidenav-grab:focus-visible {
|
|
179
199
|
outline: 2px solid var(--kol-focus-ring);
|
|
180
200
|
outline-offset: -2px;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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,8 +27,8 @@
|
|
|
27
27
|
"app-shell"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@kolkrabbi/kol-component": "^0.
|
|
31
|
-
"@kolkrabbi/kol-icons": "^0.
|
|
30
|
+
"@kolkrabbi/kol-component": "^0.34.0",
|
|
31
|
+
"@kolkrabbi/kol-icons": "^0.14.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"react": "^18.3.0 || ^19.0.0",
|
package/src/ShellHeader.jsx
CHANGED
|
@@ -108,12 +108,10 @@ export default function ShellHeader({
|
|
|
108
108
|
{actions}
|
|
109
109
|
{showThemeToggle && (
|
|
110
110
|
<Tooltip label="Toggle theme">
|
|
111
|
-
{/*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
|
|
115
|
-
<span className="hidden lg:inline-flex"><ThemeToggle fill="none" label={false} size="lg" /></span>
|
|
116
|
-
<span className="lg:hidden"><ThemeToggle fill="none" label={false} size="lg" /></span>
|
|
111
|
+
{/* md at every width (user re-rule 2026-08-09: the whole
|
|
112
|
+
* header glyph row rides md, full ink — the earlier lg-at-lg+
|
|
113
|
+
* split is repealed, which also collapsed the two spans). */}
|
|
114
|
+
<ThemeToggle fill="none" label={false} size="md" />
|
|
117
115
|
</Tooltip>
|
|
118
116
|
)}
|
|
119
117
|
{onMenuClick && (
|
|
@@ -121,13 +119,12 @@ export default function ShellHeader({
|
|
|
121
119
|
{/* IconFrame, not Button (user ruling 2026-08-01). Header
|
|
122
120
|
* chrome takes a click but must not light up; the frame has
|
|
123
121
|
* no state rules at all, which is the property Button can
|
|
124
|
-
* only approximate by dropping its own. `nav` rests at
|
|
125
|
-
*
|
|
126
|
-
* months. */}
|
|
122
|
+
* only approximate by dropping its own. `nav` rests at FULL
|
|
123
|
+
* ink since the 2026-08-09 re-rule (was oq-64). */}
|
|
127
124
|
<IconFrame
|
|
128
125
|
name="hamburger"
|
|
129
126
|
variant="nav"
|
|
130
|
-
size="
|
|
127
|
+
size="md"
|
|
131
128
|
onClick={onMenuClick}
|
|
132
129
|
aria-label="Open navigation menu"
|
|
133
130
|
/>
|
package/src/SideNav.jsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NavLink, useLocation } from 'react-router-dom'
|
|
2
2
|
import { useRef, useState } from 'react'
|
|
3
3
|
import { Icon } from '@kolkrabbi/kol-icons'
|
|
4
|
-
import {
|
|
4
|
+
import { Tooltip } from '@kolkrabbi/kol-component'
|
|
5
5
|
import ThemeToggle from './ThemeToggle'
|
|
6
6
|
import useDragResize from './useDragResize'
|
|
7
7
|
|
|
@@ -22,12 +22,13 @@ import useDragResize from './useDragResize'
|
|
|
22
22
|
* navTree: [{ id, label, icon, to?, pages?: [{ label, to }] }]
|
|
23
23
|
* (legacy `children` route leaves are read as `pages`; section anchors are not.)
|
|
24
24
|
*
|
|
25
|
-
* Collapse:
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
25
|
+
* Collapse: the pill-marked grab edge is THE single control (user build
|
|
26
|
+
* order 2026-08-09, completing SideNavGrabResize — the chip Button is gone
|
|
27
|
+
* in both states). Click toggles expand↔collapse, drag resizes with
|
|
28
|
+
* snap-collapse and snap-to-default, keyboard on the separator: arrows /
|
|
29
|
+
* Home / Enter / Space. One contract: `:root[data-sidenav="collapsed"]` +
|
|
30
|
+
* the `kol-sidenav` storage schema, owned by useDragResize. Collapsed rows
|
|
31
|
+
* get a DS Tooltip — the glyph is otherwise the only thing naming them.
|
|
31
32
|
*
|
|
32
33
|
* `background` (2026-08-09, prop): `true` (default) paints the rail surface +
|
|
33
34
|
* right hairline; `false` renders chromeless so the rail can float over a
|
|
@@ -94,25 +95,10 @@ export default function SideNav({
|
|
|
94
95
|
ref={asideRef}
|
|
95
96
|
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' : ''}`}
|
|
96
97
|
>
|
|
97
|
-
{/*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* is the rows themselves. Offset is half the pinned sm square,
|
|
102
|
-
* centring it on the rail's edge. */}
|
|
103
|
-
{!collapsed && (
|
|
104
|
-
<Button
|
|
105
|
-
variant="primary"
|
|
106
|
-
size="sm"
|
|
107
|
-
radius="full"
|
|
108
|
-
iconOnly="chevron-left"
|
|
109
|
-
aria-label="Collapse sidebar"
|
|
110
|
-
aria-expanded
|
|
111
|
-
title="Collapse"
|
|
112
|
-
onClick={toggleCollapsed}
|
|
113
|
-
className="kol-sidenav-toggle absolute top-5 right-[-0.875rem] z-[2]"
|
|
114
|
-
/>
|
|
115
|
-
)}
|
|
98
|
+
{/* NO chip Button in either state (user build order 2026-08-09 — "MAKE
|
|
99
|
+
* IT"; supersedes the elder A/B): the pill-marked grab edge below is
|
|
100
|
+
* THE single collapse/resize control — click toggles, drag resizes,
|
|
101
|
+
* snap-to-default on release, Home/Enter/Space on the keyboard. */}
|
|
116
102
|
|
|
117
103
|
<div className="kol-sidenav-scroll flex-1 flex flex-col justify-between overflow-y-auto pt-4 pb-4 [scrollbar-width:thin]">
|
|
118
104
|
<nav aria-label="Sections">
|
package/src/useDragResize.js
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
|
|
3
|
-
/* Grab-edge resize +
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
3
|
+
/* Grab-edge resize + collapse for SideNav — THE single control since 0.17.0
|
|
4
|
+
* (user build order 2026-08-09, completing the SideNavGrabResize brief: the
|
|
5
|
+
* chip Button is gone in both states; the pill-marked edge does everything).
|
|
6
|
+
* Logic lifted from the hand-tested brand proto (kol-website _tmp
|
|
7
|
+
* useDragResizeProto.js), which forked this hook's 0.16.0 form.
|
|
8
|
+
*
|
|
9
|
+
* - CLICK TOGGLES expand↔collapse: pointerup with < 3px of travel is a
|
|
10
|
+
* click, never a resize — drags only start past the slop, so toggle
|
|
11
|
+
* presses can't jitter the rail.
|
|
12
|
+
* - DRAG resizes; live width goes to --kol-sidenav-w on :root so the grid,
|
|
13
|
+
* shell-header brand block and aside follow one number. Dragging under
|
|
14
|
+
* --kol-sidenav-snap stamps :root[data-sidenav="collapsed"].
|
|
15
|
+
* - SNAP-TO-DEFAULT: releasing within --kol-sidenav-snap-default of the
|
|
16
|
+
* stylesheet default clears the override — you land exactly on default,
|
|
17
|
+
* never 249px or 263px.
|
|
18
|
+
* - DOUBLE-CLICK RESET IS GONE — it cannot coexist with click-toggle (two
|
|
19
|
+
* clicks would toggle-toggle-reset). Home keeps the reset; the snap band
|
|
20
|
+
* covers pointer users.
|
|
21
|
+
* - Keyboard on the focused separator: arrows resize by --kol-sidenav-step
|
|
22
|
+
* (ArrowLeft past the snap collapses), Home resets, Enter/Space toggle.
|
|
23
|
+
* - Width + state survive reload ('kol-sidenav' keeps the consumers'
|
|
24
|
+
* existing 'collapsed'|'expanded' schema; width under its own key).
|
|
25
|
+
*
|
|
26
|
+
* Every value the gesture needs is a --kol-sidenav-* token in
|
|
27
|
+
* kol-framework.css — no literals here except the click slop, which is a
|
|
28
|
+
* gesture constant, not chrome. Missing tokens leave the gesture inert. */
|
|
20
29
|
|
|
21
30
|
const STATE_KEY = 'kol-sidenav'
|
|
22
31
|
const WIDTH_KEY = 'kol-sidenav-w'
|
|
32
|
+
const CLICK_SLOP_PX = 3
|
|
23
33
|
|
|
24
34
|
const root = () => document.documentElement
|
|
25
35
|
|
|
@@ -32,7 +42,7 @@ function readVarPx(name) {
|
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
/* Imperative DOM writes — pointermove must never re-render the nav tree.
|
|
35
|
-
* React state syncs from the DOM at rest (
|
|
45
|
+
* React state syncs from the DOM at rest (release / key press / reset). */
|
|
36
46
|
const stampCollapsed = (on) => {
|
|
37
47
|
if (on) root().setAttribute('data-sidenav', 'collapsed')
|
|
38
48
|
else root().removeAttribute('data-sidenav')
|
|
@@ -50,7 +60,7 @@ const readBack = () => {
|
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
export default function useDragResize(asideRef) {
|
|
53
|
-
const drag = useRef(null) // { startX, startW, snapPx, maxPx } during a drag
|
|
63
|
+
const drag = useRef(null) // { startX, startW, snapPx, maxPx, moved } during a drag
|
|
54
64
|
const defaultPx = useRef(null)
|
|
55
65
|
const collapsedPx = useRef(null)
|
|
56
66
|
const [collapsed, setCollapsed] = useState(false)
|
|
@@ -67,6 +77,12 @@ export default function useDragResize(asideRef) {
|
|
|
67
77
|
} catch { /* storage blocked */ }
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
const toggleCollapsed = () => {
|
|
81
|
+
const { collapsed: c } = readBack()
|
|
82
|
+
stampCollapsed(!c)
|
|
83
|
+
syncAndPersist()
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
/* Boot: capture the stylesheet defaults BEFORE any inline override lands,
|
|
71
87
|
* then restore the persisted width/state. */
|
|
72
88
|
useEffect(() => {
|
|
@@ -85,21 +101,37 @@ export default function useDragResize(asideRef) {
|
|
|
85
101
|
useEffect(() => {
|
|
86
102
|
const onMove = (e) => {
|
|
87
103
|
if (!drag.current) return
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
104
|
+
const d = drag.current
|
|
105
|
+
const dx = e.clientX - d.startX
|
|
106
|
+
/* Below the slop the pointer is still a CLICK — resizing from the
|
|
107
|
+
* first pixel would jitter the rail on every toggle press. */
|
|
108
|
+
if (!d.moved) {
|
|
109
|
+
if (Math.abs(dx) < CLICK_SLOP_PX) return
|
|
110
|
+
d.moved = true
|
|
111
|
+
}
|
|
112
|
+
const next = d.startW + dx
|
|
113
|
+
if (next < d.snapPx) {
|
|
91
114
|
stampCollapsed(true)
|
|
92
115
|
} else {
|
|
93
116
|
stampCollapsed(false)
|
|
94
|
-
writeWidth(Math.min(next, maxPx))
|
|
117
|
+
writeWidth(Math.min(next, d.maxPx))
|
|
95
118
|
}
|
|
96
119
|
}
|
|
97
120
|
const onUp = () => {
|
|
98
121
|
if (!drag.current) return
|
|
122
|
+
const { moved } = drag.current
|
|
99
123
|
drag.current = null
|
|
100
124
|
root().removeAttribute('data-sidenav-dragging')
|
|
101
125
|
document.body.style.cursor = ''
|
|
102
126
|
document.body.style.userSelect = ''
|
|
127
|
+
if (!moved) { toggleCollapsed(); return } // a click, not a drag
|
|
128
|
+
/* Snap-to-default: release near the stylesheet default clears the
|
|
129
|
+
* override entirely. */
|
|
130
|
+
const { collapsed: c, widthPx: w } = readBack()
|
|
131
|
+
const band = readVarPx('--kol-sidenav-snap-default') ?? readVarPx('--kol-sidenav-step') ?? 16
|
|
132
|
+
if (!c && w != null && defaultPx.current != null && Math.abs(w - defaultPx.current) <= band) {
|
|
133
|
+
writeWidth(null)
|
|
134
|
+
}
|
|
103
135
|
syncAndPersist()
|
|
104
136
|
}
|
|
105
137
|
window.addEventListener('pointermove', onMove)
|
|
@@ -120,29 +152,21 @@ export default function useDragResize(asideRef) {
|
|
|
120
152
|
snapPx,
|
|
121
153
|
/* mirror's ceiling (default × 3), resolved from the token not hardcoded */
|
|
122
154
|
maxPx: defaultPx.current * 3,
|
|
155
|
+
moved: false,
|
|
123
156
|
}
|
|
124
|
-
/* the grid's
|
|
157
|
+
/* the grid's grid-template-columns ease would trail the pointer —
|
|
125
158
|
* kol-framework.css suspends it while this attribute is stamped */
|
|
126
159
|
root().setAttribute('data-sidenav-dragging', '')
|
|
127
160
|
document.body.style.cursor = 'col-resize'
|
|
128
161
|
document.body.style.userSelect = 'none'
|
|
129
162
|
}
|
|
130
163
|
|
|
131
|
-
const
|
|
164
|
+
const resetToDefault = () => {
|
|
132
165
|
stampCollapsed(false)
|
|
133
166
|
writeWidth(null)
|
|
134
167
|
syncAndPersist()
|
|
135
168
|
}
|
|
136
169
|
|
|
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
|
-
|
|
146
170
|
const onKeyDown = (e) => {
|
|
147
171
|
const snapPx = readVarPx('--kol-sidenav-snap')
|
|
148
172
|
const stepPx = readVarPx('--kol-sidenav-step')
|
|
@@ -163,7 +187,10 @@ export default function useDragResize(asideRef) {
|
|
|
163
187
|
syncAndPersist()
|
|
164
188
|
} else if (e.key === 'Home') {
|
|
165
189
|
e.preventDefault()
|
|
166
|
-
|
|
190
|
+
resetToDefault()
|
|
191
|
+
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
192
|
+
e.preventDefault()
|
|
193
|
+
toggleCollapsed()
|
|
167
194
|
}
|
|
168
195
|
}
|
|
169
196
|
|
|
@@ -179,7 +206,6 @@ export default function useDragResize(asideRef) {
|
|
|
179
206
|
'aria-valuemax': defaultPx.current == null ? undefined : Math.round(defaultPx.current * 3),
|
|
180
207
|
tabIndex: 0,
|
|
181
208
|
onPointerDown,
|
|
182
|
-
onDoubleClick,
|
|
183
209
|
onKeyDown,
|
|
184
210
|
},
|
|
185
211
|
}
|