@kolkrabbi/kol-framework 0.20.0 → 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 CHANGED
@@ -472,6 +472,15 @@ body {
472
472
  margin-top: 16px;
473
473
  }
474
474
 
475
+ /* Inline — the pair sits in a header row beside the label/counter instead of
476
+ * under the viewport, so it owns neither the top gap nor the end-alignment.
477
+ * kol-website's FeaturedCarousel fork put its nav in the header; that is the
478
+ * one thing the fork did that the position needed a name for (2026-08-15). */
479
+ .kol-embla-controls.is-inline {
480
+ margin-top: 0;
481
+ justify-content: flex-start;
482
+ }
483
+
475
484
  .kol-embla-btn {
476
485
  width: 40px;
477
486
  height: 40px;
@@ -479,6 +488,11 @@ body {
479
488
  background: transparent;
480
489
  cursor: pointer;
481
490
  font-size: 16px;
491
+ /* Centres the chevron glyph. The buttons carried literal ‹ › text until
492
+ * 2026-08-15 and centred on line-height alone; EmblaNav draws real icons. */
493
+ display: inline-flex;
494
+ align-items: center;
495
+ justify-content: center;
482
496
  }
483
497
 
484
498
  .kol-embla-btn:disabled {
@@ -803,6 +817,44 @@ body {
803
817
  }
804
818
  }
805
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
+
806
858
  /* SideNav collapsibleSections chrome — inert unless that mode renders.
807
859
  The forced rail (≤1024px) hides count/toggle with the labels; the open
808
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.20.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.38.0",
32
- "@kolkrabbi/kol-icons": "^0.16.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",
@@ -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 --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. */
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('data-sidenav', 'collapsed')
48
- else root().removeAttribute('data-sidenav')
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('--kol-sidenav-w')
52
- else root().style.setProperty('--kol-sidenav-w', `${px}px`)
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('--kol-sidenav-w'))
81
+ const readBack = (n) => {
82
+ const inline = parseFloat(root().style.getPropertyValue(n.wVar))
56
83
  return {
57
- collapsed: root().getAttribute('data-sidenav') === 'collapsed',
84
+ collapsed: root().getAttribute(n.collapsedAttr) === 'collapsed',
58
85
  widthPx: Number.isNaN(inline) ? null : inline,
59
86
  }
60
87
  }
61
88
 
62
- export default function useDragResize(asideRef) {
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(STATE_KEY, c ? 'collapsed' : 'expanded')
75
- if (w == null) localStorage.removeItem(WIDTH_KEY)
76
- else localStorage.setItem(WIDTH_KEY, String(Math.round(w)))
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('--kol-sidenav-w')
90
- collapsedPx.current = readVarPx('--kol-sidenav-w-collapsed')
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(WIDTH_KEY)) || null
95
- c = localStorage.getItem(STATE_KEY) === 'collapsed'
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('data-sidenav-dragging')
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('--kol-sidenav-snap-default') ?? readVarPx('--kol-sidenav-step') ?? 16
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('--kol-sidenav-snap')
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: asideRef.current?.getBoundingClientRect().width ?? defaultPx.current,
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('data-sidenav-dragging', '')
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('--kol-sidenav-snap')
172
- const stepPx = readVarPx('--kol-sidenav-step')
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
- if (e.key === 'ArrowRight') {
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 === 'ArrowLeft') {
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()