@kolkrabbi/kol-framework 0.35.0 → 0.37.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-framework",
3
- "version": "0.35.0",
3
+ "version": "0.37.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",
@@ -20,8 +20,8 @@
20
20
  "react-router-dom": "^6.0.0 || ^7.0.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@kolkrabbi/kol-component": "^0.120.0",
24
- "@kolkrabbi/kol-icons": "^0.24.0"
23
+ "@kolkrabbi/kol-component": "^0.173.0",
24
+ "@kolkrabbi/kol-icons": "^0.25.0"
25
25
  },
26
26
  "files": [
27
27
  "src",
@@ -27,7 +27,7 @@ import { useTheme } from './theme.js'
27
27
  * that — a sidebar row is not a button.
28
28
  * iconRight bool glyph right of the text (left default)
29
29
  * label bool text on/off — off pins the square box
30
- * per rung (28/32/36; icon-only is a
30
+ * per size (22/26/32/40; icon-only is a
31
31
  * geometry condition, not a variant)
32
32
  * fullWidth bool stretches to a sidenav row
33
33
  * size 'sm' | 'md' | 'lg' moves pad + text + glyph together
@@ -1,4 +1,12 @@
1
1
  import { useEffect, useMemo, useRef, useState } from 'react'
2
+ /* THE SAME GRAB AS THE SHELL RAIL (OneGrabGestureBothRails, kol-fxr 2026-08-30 —
3
+ * user: *"why dont we use the grab animation and other sidenav settings to be
4
+ * consistent?"*). fxr's /labs shows both rails at once and only the left one
5
+ * woke as the cursor neared it, because each package had built the gesture
6
+ * itself. The hook lives in kol-component, the one package both can reach —
7
+ * kol-shell dropped its framework peer in 0.16.0, so neither could import the
8
+ * other's. */
9
+ import { useGrabEdge } from '@kolkrabbi/kol-component'
2
10
 
3
11
  /* Grab-edge resize + collapse for SideNav — THE single control since 0.17.0
4
12
  * (user build order 2026-08-09, completing the SideNavGrabResize brief: the
@@ -94,14 +102,44 @@ const readBack = (n) => {
94
102
  * side — which EDGE the grab handle sits on. 'left' (default) is a rail on
95
103
  * the left of the viewport whose handle is on its right edge, so
96
104
  * rightward drag = wider. 'right' inverts both the pointer sign and
97
- * the arrow keys. */
105
+ * the arrow keys.
106
+ * persistWidth — remember a dragged WIDTH across sessions. **Default false**
107
+ * (sidenav-drag-width-outranks-breakpoints, kol-client-olina
108
+ * 2026-09-03; user: *"why would you want that in persistent memory?
109
+ * and even if you did, shouldnt it be an opt in via prop? off by
110
+ * default"*).
111
+ *
112
+ * It used to be unconditional, and the width was replayed on boot as
113
+ * an INLINE custom property on `:root` — which outranks every
114
+ * stylesheet rule, so one drag on a laptop killed both shipped rungs
115
+ * (`--kol-sidenav-w: 264px` and the 320px `min-width:1536px` rule)
116
+ * permanently, on every machine, with no UI saying so and no selector
117
+ * a consumer could beat. Measured at 1600×900: a stored 210 rendered
118
+ * 210 and the 1536 rung did nothing. A one-off gesture had become a
119
+ * permanent global.
120
+ *
121
+ * Off, a drag still works and lasts the session; the rail follows the
122
+ * stylesheet and its breakpoints on the next boot. On, the old
123
+ * behaviour returns for an app that genuinely wants the rail
124
+ * remembered — and it is still an inline stamp, so an app opting in is
125
+ * choosing to outrank its own breakpoints. That precedence is worth
126
+ * inverting, but it is a bigger change than this ticket asked for.
127
+ *
128
+ * STATE is untouched: collapsed/expanded still persists via
129
+ * `stateKey`, unconditionally, as it always did. The ticket says the
130
+ * two are separate questions and only asks about the width. */
98
131
  export default function useDragResize(ref, options = {}) {
99
- const { token = 'kol-sidenav', side = 'left', defaultCollapsed = false } = options
132
+ const { token = 'kol-sidenav', side = 'left', defaultCollapsed = false, persistWidth = false } = options
100
133
  /* -1 on a right-hand rail: the same rightward pointer travel that widens a
101
134
  * left rail must NARROW a right one, because its handle faces the canvas. */
102
135
  const dir = side === 'right' ? -1 : 1
103
136
  const names = useMemo(() => buildNames(token), [token])
104
137
 
138
+ /* the handle's own ref, so the shared gesture can find it. Returned in
139
+ * `grabProps`, so a consumer already spreading those gets the wake, the
140
+ * travel and the dwell with no change. */
141
+ const grabRef = useRef(null)
142
+ useGrabEdge(grabRef)
105
143
  const drag = useRef(null) // { startX, startW, snapPx, maxPx, moved } during a drag
106
144
  const defaultPx = useRef(null)
107
145
  const collapsedPx = useRef(null)
@@ -114,7 +152,10 @@ export default function useDragResize(ref, options = {}) {
114
152
  setWidthPx(w)
115
153
  try {
116
154
  localStorage.setItem(names.stateKey, c ? 'collapsed' : 'expanded')
117
- if (w == null) localStorage.removeItem(names.widthKey)
155
+ /* width only when asked for; off, the key is not written AND any key a
156
+ * previous version left behind is cleared, so a consumer that bumps stops
157
+ * replaying a width it never opted into */
158
+ if (!persistWidth || w == null) localStorage.removeItem(names.widthKey)
118
159
  else localStorage.setItem(names.widthKey, String(Math.round(w)))
119
160
  } catch { /* storage blocked */ }
120
161
  }
@@ -133,7 +174,9 @@ export default function useDragResize(ref, options = {}) {
133
174
  let w = null
134
175
  let stored = null
135
176
  try {
136
- w = parseFloat(localStorage.getItem(names.widthKey)) || null
177
+ /* off → never read it, and drop a key an earlier version wrote */
178
+ if (persistWidth) w = parseFloat(localStorage.getItem(names.widthKey)) || null
179
+ else localStorage.removeItem(names.widthKey)
137
180
  stored = localStorage.getItem(names.stateKey)
138
181
  } catch { /* storage blocked */ }
139
182
  /* nothing stored → the consumer's boot state (RailSideNavPixelParity,
@@ -142,7 +185,7 @@ export default function useDragResize(ref, options = {}) {
142
185
  const c = stored ? stored === 'collapsed' : !!defaultCollapsed
143
186
  if (w) { writeWidth(names, w); setWidthPx(w) }
144
187
  if (c) { stampCollapsed(names, true); setCollapsed(true) }
145
- }, [names, defaultCollapsed])
188
+ }, [names, defaultCollapsed, persistWidth])
146
189
 
147
190
  useEffect(() => {
148
191
  const onMove = (e) => {
@@ -249,6 +292,11 @@ export default function useDragResize(ref, options = {}) {
249
292
  collapsed,
250
293
  toggleCollapsed,
251
294
  grabProps: {
295
+ ref: grabRef,
296
+ /* the pill is drawn by `.kol-rail-grab` (kol-animation.css) — the hook
297
+ * only supplies `is-near` and `--kol-rail-grab-y`. A consumer's own
298
+ * className, spread after this, still wins. */
299
+ className: 'kol-rail-grab',
252
300
  role: 'separator',
253
301
  'aria-orientation': 'vertical',
254
302
  'aria-label': 'Resize navigation',