@kolkrabbi/kol-framework 0.20.1 → 0.21.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 +38 -0
- package/package.json +3 -3
- package/src/useDragResize.js +99 -52
package/kol-framework.css
CHANGED
|
@@ -817,6 +817,44 @@ body {
|
|
|
817
817
|
}
|
|
818
818
|
}
|
|
819
819
|
|
|
820
|
+
/* ───── Inspector rail — the OTHER third column (ThreeColumnEditorShell, kol-fxr 2026-08-15) ───── */
|
|
821
|
+
|
|
822
|
+
/* --kol-toc-w is a 160px docs table-of-contents and was the only third column
|
|
823
|
+
the shipped grid knew, so every editor-shaped app restated the whole
|
|
824
|
+
template locally to get an inspector. This is that column, sized and named
|
|
825
|
+
as what it is. Deliberately NOT behind the TOC's min-width:1280px gate — an
|
|
826
|
+
editor's inspector is the surface, not an enhancement that may drop away.
|
|
827
|
+
|
|
828
|
+
NOTE ON THE ATTRIBUTE, because the word does double duty exactly as
|
|
829
|
+
`sidenav` already does: `data-rail="true"` on .kol-brand-layout is the
|
|
830
|
+
layout OPT-IN, while `data-rail="collapsed"` on :root is the GESTURE STATE
|
|
831
|
+
written by useDragResize({ token: 'kol-rail' }). Different elements, same
|
|
832
|
+
precedent as data-toc / data-sidenav above. */
|
|
833
|
+
:root {
|
|
834
|
+
--kol-rail-w: 16rem; /* the same 256px standard as --kol-sidenav-w */
|
|
835
|
+
--kol-rail-snap: 12rem; /* drag under this snaps to -w-collapsed */
|
|
836
|
+
--kol-rail-step: var(--kol-spacing-4); /* keyboard resize increment */
|
|
837
|
+
--kol-rail-snap-default: var(--kol-rail-step); /* release within this of the default clears the override */
|
|
838
|
+
--kol-rail-w-collapsed: 56px;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/* Each rail's track is its own private var, so the two collapse INDEPENDENTLY
|
|
842
|
+
off one rule each — not the four rules a 2×2 of collapsed states would cost. */
|
|
843
|
+
.kol-brand-layout[data-rail="true"] {
|
|
844
|
+
--_kol-nav-track: var(--kol-sidenav-w);
|
|
845
|
+
--_kol-rail-track: var(--kol-rail-w);
|
|
846
|
+
grid-template-columns: var(--_kol-nav-track) minmax(0, 1fr) var(--_kol-rail-track);
|
|
847
|
+
}
|
|
848
|
+
:root[data-sidenav="collapsed"] .kol-brand-layout[data-rail="true"] {
|
|
849
|
+
--_kol-nav-track: var(--kol-sidenav-w-collapsed);
|
|
850
|
+
}
|
|
851
|
+
:root[data-rail="collapsed"] .kol-brand-layout[data-rail="true"] {
|
|
852
|
+
--_kol-rail-track: var(--kol-rail-w-collapsed);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/* Same reason as the sidenav's: the grid ease would trail the pointer. */
|
|
856
|
+
:root[data-rail-dragging] .kol-brand-layout { transition: none; }
|
|
857
|
+
|
|
820
858
|
/* SideNav collapsibleSections chrome — inert unless that mode renders.
|
|
821
859
|
The forced rail (≤1024px) hides count/toggle with the labels; the open
|
|
822
860
|
≤767px drawer restores them, mirroring -hop-label. (Manual .is-collapsed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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.46.0",
|
|
32
|
+
"@kolkrabbi/kol-icons": "^0.17.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": "^18.3.0 || ^19.0.0",
|
package/src/useDragResize.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
/* Grab-edge resize + collapse for SideNav — THE single control since 0.17.0
|
|
4
4
|
* (user build order 2026-08-09, completing the SideNavGrabResize brief: the
|
|
@@ -23,16 +23,43 @@ import { useEffect, useRef, useState } from 'react'
|
|
|
23
23
|
* - Width + state survive reload ('kol-sidenav' keeps the consumers'
|
|
24
24
|
* existing 'collapsed'|'expanded' schema; width under its own key).
|
|
25
25
|
*
|
|
26
|
-
* Every value the gesture needs is a
|
|
27
|
-
*
|
|
28
|
-
* gesture constant, not chrome. Missing tokens leave the gesture inert.
|
|
26
|
+
* Every value the gesture needs is a --<token>-* custom property in the
|
|
27
|
+
* consumer's CSS — no literals here except the click slop, which is a
|
|
28
|
+
* gesture constant, not chrome. Missing tokens leave the gesture inert.
|
|
29
|
+
*
|
|
30
|
+
* SIDE-AGNOSTIC since ThreeColumnEditorShell (kol-fxr, 2026-08-15): the name
|
|
31
|
+
* family and the drag direction are both arguments now, so a right-hand
|
|
32
|
+
* inspector rail reuses this gesture — pointer, keyboard, snap, collapse and
|
|
33
|
+
* persistence — instead of reimplementing it. The bullets above describe the
|
|
34
|
+
* DEFAULT ('kol-sidenav', side 'left'), which is byte-identical to 0.17.0. */
|
|
29
35
|
|
|
30
|
-
const STATE_KEY = 'kol-sidenav'
|
|
31
|
-
const WIDTH_KEY = 'kol-sidenav-w'
|
|
32
36
|
const CLICK_SLOP_PX = 3
|
|
33
37
|
|
|
34
38
|
const root = () => document.documentElement
|
|
35
39
|
|
|
40
|
+
/* Every name the gesture touches, derived from ONE token (ThreeColumnEditorShell,
|
|
41
|
+
* filed from kol-fxr 2026-08-15). The default token reproduces the hardcoded
|
|
42
|
+
* 0.17.0 names EXACTLY — 'kol-sidenav' → data-sidenav, --kol-sidenav-w,
|
|
43
|
+
* storage 'kol-sidenav'/'kol-sidenav-w' — so SideNav and every existing caller
|
|
44
|
+
* are untouched by this generalisation.
|
|
45
|
+
*
|
|
46
|
+
* The data-attribute drops the `kol-` prefix because that is what the shipped
|
|
47
|
+
* CSS already selects (`:root[data-sidenav="collapsed"]`), not a new scheme. */
|
|
48
|
+
export function buildNames(token) {
|
|
49
|
+
const base = token.replace(/^kol-/, '')
|
|
50
|
+
return {
|
|
51
|
+
stateKey: token,
|
|
52
|
+
widthKey: `${token}-w`,
|
|
53
|
+
collapsedAttr: `data-${base}`,
|
|
54
|
+
draggingAttr: `data-${base}-dragging`,
|
|
55
|
+
wVar: `--${token}-w`,
|
|
56
|
+
collapsedVar: `--${token}-w-collapsed`,
|
|
57
|
+
snapVar: `--${token}-snap`,
|
|
58
|
+
stepVar: `--${token}-step`,
|
|
59
|
+
snapDefaultVar: `--${token}-snap-default`,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
36
63
|
/* Resolve a length token to px, or null when it is absent/unparsable. */
|
|
37
64
|
function readVarPx(name) {
|
|
38
65
|
const raw = getComputedStyle(root()).getPropertyValue(name).trim()
|
|
@@ -43,23 +70,38 @@ function readVarPx(name) {
|
|
|
43
70
|
|
|
44
71
|
/* Imperative DOM writes — pointermove must never re-render the nav tree.
|
|
45
72
|
* React state syncs from the DOM at rest (release / key press / reset). */
|
|
46
|
-
const stampCollapsed = (on) => {
|
|
47
|
-
if (on) root().setAttribute(
|
|
48
|
-
else root().removeAttribute(
|
|
73
|
+
const stampCollapsed = (n, on) => {
|
|
74
|
+
if (on) root().setAttribute(n.collapsedAttr, 'collapsed')
|
|
75
|
+
else root().removeAttribute(n.collapsedAttr)
|
|
49
76
|
}
|
|
50
|
-
const writeWidth = (px) => {
|
|
51
|
-
if (px == null) root().style.removeProperty(
|
|
52
|
-
else root().style.setProperty(
|
|
77
|
+
const writeWidth = (n, px) => {
|
|
78
|
+
if (px == null) root().style.removeProperty(n.wVar)
|
|
79
|
+
else root().style.setProperty(n.wVar, `${px}px`)
|
|
53
80
|
}
|
|
54
|
-
const readBack = () => {
|
|
55
|
-
const inline = parseFloat(root().style.getPropertyValue(
|
|
81
|
+
const readBack = (n) => {
|
|
82
|
+
const inline = parseFloat(root().style.getPropertyValue(n.wVar))
|
|
56
83
|
return {
|
|
57
|
-
collapsed: root().getAttribute(
|
|
84
|
+
collapsed: root().getAttribute(n.collapsedAttr) === 'collapsed',
|
|
58
85
|
widthPx: Number.isNaN(inline) ? null : inline,
|
|
59
86
|
}
|
|
60
87
|
}
|
|
61
88
|
|
|
62
|
-
|
|
89
|
+
/* @param ref the panel being resized — its measured width seeds the drag
|
|
90
|
+
* @param options { token, side }
|
|
91
|
+
* token — the CSS/storage name family. Default 'kol-sidenav' (SideNav).
|
|
92
|
+
* A right-hand inspector passes its own, e.g. 'kol-rail', so the two
|
|
93
|
+
* rails never share one :root variable and drag together.
|
|
94
|
+
* side — which EDGE the grab handle sits on. 'left' (default) is a rail on
|
|
95
|
+
* the left of the viewport whose handle is on its right edge, so
|
|
96
|
+
* rightward drag = wider. 'right' inverts both the pointer sign and
|
|
97
|
+
* the arrow keys. */
|
|
98
|
+
export default function useDragResize(ref, options = {}) {
|
|
99
|
+
const { token = 'kol-sidenav', side = 'left' } = options
|
|
100
|
+
/* -1 on a right-hand rail: the same rightward pointer travel that widens a
|
|
101
|
+
* left rail must NARROW a right one, because its handle faces the canvas. */
|
|
102
|
+
const dir = side === 'right' ? -1 : 1
|
|
103
|
+
const names = useMemo(() => buildNames(token), [token])
|
|
104
|
+
|
|
63
105
|
const drag = useRef(null) // { startX, startW, snapPx, maxPx, moved } during a drag
|
|
64
106
|
const defaultPx = useRef(null)
|
|
65
107
|
const collapsedPx = useRef(null)
|
|
@@ -67,36 +109,36 @@ export default function useDragResize(asideRef) {
|
|
|
67
109
|
const [widthPx, setWidthPx] = useState(null) // null = stylesheet default
|
|
68
110
|
|
|
69
111
|
const syncAndPersist = () => {
|
|
70
|
-
const { collapsed: c, widthPx: w } = readBack()
|
|
112
|
+
const { collapsed: c, widthPx: w } = readBack(names)
|
|
71
113
|
setCollapsed(c)
|
|
72
114
|
setWidthPx(w)
|
|
73
115
|
try {
|
|
74
|
-
localStorage.setItem(
|
|
75
|
-
if (w == null) localStorage.removeItem(
|
|
76
|
-
else localStorage.setItem(
|
|
116
|
+
localStorage.setItem(names.stateKey, c ? 'collapsed' : 'expanded')
|
|
117
|
+
if (w == null) localStorage.removeItem(names.widthKey)
|
|
118
|
+
else localStorage.setItem(names.widthKey, String(Math.round(w)))
|
|
77
119
|
} catch { /* storage blocked */ }
|
|
78
120
|
}
|
|
79
121
|
|
|
80
122
|
const toggleCollapsed = () => {
|
|
81
|
-
const { collapsed: c } = readBack()
|
|
82
|
-
stampCollapsed(!c)
|
|
123
|
+
const { collapsed: c } = readBack(names)
|
|
124
|
+
stampCollapsed(names, !c)
|
|
83
125
|
syncAndPersist()
|
|
84
126
|
}
|
|
85
127
|
|
|
86
128
|
/* Boot: capture the stylesheet defaults BEFORE any inline override lands,
|
|
87
129
|
* then restore the persisted width/state. */
|
|
88
130
|
useEffect(() => {
|
|
89
|
-
defaultPx.current = readVarPx(
|
|
90
|
-
collapsedPx.current = readVarPx(
|
|
131
|
+
defaultPx.current = readVarPx(names.wVar)
|
|
132
|
+
collapsedPx.current = readVarPx(names.collapsedVar)
|
|
91
133
|
let w = null
|
|
92
134
|
let c = false
|
|
93
135
|
try {
|
|
94
|
-
w = parseFloat(localStorage.getItem(
|
|
95
|
-
c = localStorage.getItem(
|
|
136
|
+
w = parseFloat(localStorage.getItem(names.widthKey)) || null
|
|
137
|
+
c = localStorage.getItem(names.stateKey) === 'collapsed'
|
|
96
138
|
} catch { /* storage blocked */ }
|
|
97
|
-
if (w) { writeWidth(w); setWidthPx(w) }
|
|
98
|
-
if (c) { stampCollapsed(true); setCollapsed(true) }
|
|
99
|
-
}, [])
|
|
139
|
+
if (w) { writeWidth(names, w); setWidthPx(w) }
|
|
140
|
+
if (c) { stampCollapsed(names, true); setCollapsed(true) }
|
|
141
|
+
}, [names])
|
|
100
142
|
|
|
101
143
|
useEffect(() => {
|
|
102
144
|
const onMove = (e) => {
|
|
@@ -109,28 +151,28 @@ export default function useDragResize(asideRef) {
|
|
|
109
151
|
if (Math.abs(dx) < CLICK_SLOP_PX) return
|
|
110
152
|
d.moved = true
|
|
111
153
|
}
|
|
112
|
-
const next = d.startW + dx
|
|
154
|
+
const next = d.startW + dx * dir
|
|
113
155
|
if (next < d.snapPx) {
|
|
114
|
-
stampCollapsed(true)
|
|
156
|
+
stampCollapsed(names, true)
|
|
115
157
|
} else {
|
|
116
|
-
stampCollapsed(false)
|
|
117
|
-
writeWidth(Math.min(next, d.maxPx))
|
|
158
|
+
stampCollapsed(names, false)
|
|
159
|
+
writeWidth(names, Math.min(next, d.maxPx))
|
|
118
160
|
}
|
|
119
161
|
}
|
|
120
162
|
const onUp = () => {
|
|
121
163
|
if (!drag.current) return
|
|
122
164
|
const { moved } = drag.current
|
|
123
165
|
drag.current = null
|
|
124
|
-
root().removeAttribute(
|
|
166
|
+
root().removeAttribute(names.draggingAttr)
|
|
125
167
|
document.body.style.cursor = ''
|
|
126
168
|
document.body.style.userSelect = ''
|
|
127
169
|
if (!moved) { toggleCollapsed(); return } // a click, not a drag
|
|
128
170
|
/* Snap-to-default: release near the stylesheet default clears the
|
|
129
171
|
* override entirely. */
|
|
130
|
-
const { collapsed: c, widthPx: w } = readBack()
|
|
131
|
-
const band = readVarPx(
|
|
172
|
+
const { collapsed: c, widthPx: w } = readBack(names)
|
|
173
|
+
const band = readVarPx(names.snapDefaultVar) ?? readVarPx(names.stepVar) ?? 16
|
|
132
174
|
if (!c && w != null && defaultPx.current != null && Math.abs(w - defaultPx.current) <= band) {
|
|
133
|
-
writeWidth(null)
|
|
175
|
+
writeWidth(names, null)
|
|
134
176
|
}
|
|
135
177
|
syncAndPersist()
|
|
136
178
|
}
|
|
@@ -140,15 +182,15 @@ export default function useDragResize(asideRef) {
|
|
|
140
182
|
window.removeEventListener('pointermove', onMove)
|
|
141
183
|
window.removeEventListener('pointerup', onUp)
|
|
142
184
|
}
|
|
143
|
-
}, [])
|
|
185
|
+
}, [names, dir])
|
|
144
186
|
|
|
145
187
|
const onPointerDown = (e) => {
|
|
146
|
-
const snapPx = readVarPx(
|
|
188
|
+
const snapPx = readVarPx(names.snapVar)
|
|
147
189
|
if (defaultPx.current == null || snapPx == null) return // tokens absent → inert
|
|
148
190
|
e.preventDefault()
|
|
149
191
|
drag.current = {
|
|
150
192
|
startX: e.clientX,
|
|
151
|
-
startW:
|
|
193
|
+
startW: ref.current?.getBoundingClientRect().width ?? defaultPx.current,
|
|
152
194
|
snapPx,
|
|
153
195
|
/* mirror's ceiling (default × 3), resolved from the token not hardcoded */
|
|
154
196
|
maxPx: defaultPx.current * 3,
|
|
@@ -156,34 +198,39 @@ export default function useDragResize(asideRef) {
|
|
|
156
198
|
}
|
|
157
199
|
/* the grid's grid-template-columns ease would trail the pointer —
|
|
158
200
|
* kol-framework.css suspends it while this attribute is stamped */
|
|
159
|
-
root().setAttribute(
|
|
201
|
+
root().setAttribute(names.draggingAttr, '')
|
|
160
202
|
document.body.style.cursor = 'col-resize'
|
|
161
203
|
document.body.style.userSelect = 'none'
|
|
162
204
|
}
|
|
163
205
|
|
|
164
206
|
const resetToDefault = () => {
|
|
165
|
-
stampCollapsed(false)
|
|
166
|
-
writeWidth(null)
|
|
207
|
+
stampCollapsed(names, false)
|
|
208
|
+
writeWidth(names, null)
|
|
167
209
|
syncAndPersist()
|
|
168
210
|
}
|
|
169
211
|
|
|
170
212
|
const onKeyDown = (e) => {
|
|
171
|
-
const snapPx = readVarPx(
|
|
172
|
-
const stepPx = readVarPx(
|
|
213
|
+
const snapPx = readVarPx(names.snapVar)
|
|
214
|
+
const stepPx = readVarPx(names.stepVar)
|
|
173
215
|
if (defaultPx.current == null || snapPx == null || stepPx == null) return
|
|
174
|
-
const { collapsed: c, widthPx: w } = readBack()
|
|
216
|
+
const { collapsed: c, widthPx: w } = readBack(names)
|
|
175
217
|
const current = w ?? defaultPx.current
|
|
176
|
-
|
|
218
|
+
/* The arrow that GROWS is the one pointing away from the rail's own edge —
|
|
219
|
+
* ArrowRight on a left rail, ArrowLeft on a right one. Same inversion the
|
|
220
|
+
* pointer gets, so keyboard and drag never disagree. */
|
|
221
|
+
const growKey = dir === 1 ? 'ArrowRight' : 'ArrowLeft'
|
|
222
|
+
const shrinkKey = dir === 1 ? 'ArrowLeft' : 'ArrowRight'
|
|
223
|
+
if (e.key === growKey) {
|
|
177
224
|
e.preventDefault()
|
|
178
|
-
if (c) stampCollapsed(false)
|
|
179
|
-
else writeWidth(Math.min(current + stepPx, defaultPx.current * 3))
|
|
225
|
+
if (c) stampCollapsed(names, false)
|
|
226
|
+
else writeWidth(names, Math.min(current + stepPx, defaultPx.current * 3))
|
|
180
227
|
syncAndPersist()
|
|
181
|
-
} else if (e.key ===
|
|
228
|
+
} else if (e.key === shrinkKey) {
|
|
182
229
|
e.preventDefault()
|
|
183
230
|
if (c) return
|
|
184
231
|
const next = current - stepPx
|
|
185
|
-
if (next < snapPx) stampCollapsed(true)
|
|
186
|
-
else writeWidth(next)
|
|
232
|
+
if (next < snapPx) stampCollapsed(names, true)
|
|
233
|
+
else writeWidth(names, next)
|
|
187
234
|
syncAndPersist()
|
|
188
235
|
} else if (e.key === 'Home') {
|
|
189
236
|
e.preventDefault()
|