@kolkrabbi/kol-shell 0.28.0 → 0.30.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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/NavRail.jsx +38 -37
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-shell",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "private": false,
5
5
  "description": "KOL application shell — fixed 48px NavRail + AppShell layout root, PageShell/PageHeader scaffolds, ContentFilters catalog organism, GridCard, SettingsScaffold, WalkthroughPanel, ShortcutsOverlay. App chrome (kol-framework owns site chrome). Nav items, content, shortcuts and settings are consumer-injected. Sits above @kolkrabbi/kol-{theme,component,framework}.",
6
6
  "license": "MIT",
@@ -20,10 +20,10 @@
20
20
  "react-dom": "^18.3.0 || ^19.0.0"
21
21
  },
22
22
  "devDependencies": {
23
+ "@kolkrabbi/kol-component": "^0.142.0",
23
24
  "@kolkrabbi/kol-icons": "^0.25.0",
24
- "@kolkrabbi/kol-component": "^0.137.0",
25
25
  "@kolkrabbi/kol-framework": "^0.35.0",
26
- "@kolkrabbi/kol-theme": "^0.108.0"
26
+ "@kolkrabbi/kol-theme": "^0.111.0"
27
27
  },
28
28
  "files": [
29
29
  "src",
package/src/NavRail.jsx CHANGED
@@ -2,6 +2,11 @@ import { useEffect, useRef, useState } from 'react'
2
2
  import gsap from 'gsap'
3
3
  import { Button } from '@kolkrabbi/kol-component'
4
4
  import { GRAB } from '@kolkrabbi/kol-component/utilities/motion'
5
+ /* the grab pill's proximity wake, travel and dwell — ONE implementation, shared
6
+ * with kol-framework's useDragResize (OneGrabGestureBothRails, kol-fxr
7
+ * 2026-08-30: two rails on one screen felt different because each package had
8
+ * built the gesture itself). It lived here until then. */
9
+ import { useGrabEdge } from '@kolkrabbi/kol-component'
5
10
  import { Icon } from '@kolkrabbi/kol-icons'
6
11
  import Logomark from './Logomark.jsx'
7
12
 
@@ -83,43 +88,13 @@ const openWidth = () => {
83
88
  * lands on you, holds while you move inside the radius, then travels to where you
84
89
  * are now — inside `GRAB.range`, the middle band of the edge, so it never rides
85
90
  * up beside the logomark. One window listener, rAF-throttled. */
86
- function useGrabEdge(ref) {
87
- useEffect(() => {
88
- let raf = 0
89
- const onMove = ({ clientX, clientY }) => {
90
- if (raf) return
91
- raf = requestAnimationFrame(() => {
92
- raf = 0
93
- const h = ref.current
94
- if (!h) return
95
- const r = h.getBoundingClientRect()
96
- const dist = Math.abs(clientX - (r.left + r.width / 2))
97
- const near = dist <= GRAB.near || (h.classList.contains('is-near') && dist <= GRAB.sleep)
98
- h.classList.toggle('is-near', near)
99
- if (!near) return
100
- /* the travel band: bipolar from the middle, GRAB.range of the height */
101
- const edge = (r.height * (1 - GRAB.range)) / 2
102
- const along = Math.min(Math.max(clientY - r.top, edge), r.height - edge)
103
- if (h.dataset.grabSeeded && Math.abs(along - Number(h.dataset.grabTarget)) < GRAB.stick) return
104
- h.dataset.grabTarget = String(along)
105
- const vars = { '--kol-rail-grab-y': `${along}px` }
106
- /* the CSS fallback is 50%, a percentage — nothing to tween from, so the
107
- * first sighting sets and every move after tweens */
108
- if (h.dataset.grabSeeded) gsap.to(h, { ...vars, ...GRAB.travel, overwrite: 'auto' })
109
- else { gsap.set(h, vars); h.dataset.grabSeeded = '1' }
110
- })
111
- }
112
- window.addEventListener('pointermove', onMove)
113
- return () => { window.removeEventListener('pointermove', onMove); cancelAnimationFrame(raf) }
114
- }, [ref])
115
- }
116
91
 
117
92
  /* THE DRAG — hold the pill and the width follows the pointer between closed and
118
93
  * open; release snaps to the nearer state, a click (no travel past GRAB.slop)
119
94
  * toggles. `onSnap` reports the resting state once — the sub rows render only
120
95
  * while open, because closed they would still take their height and push the
121
96
  * rungs below them. */
122
- function useRailDrag(railRef, grabRef, onSnap) {
97
+ function useRailDrag(railRef, grabRef, onSnap, snapRef) {
123
98
  useEffect(() => {
124
99
  const strip = grabRef.current, rail = railRef.current, root = document.documentElement
125
100
  if (!strip || !rail) return undefined
@@ -129,6 +104,13 @@ function useRailDrag(railRef, grabRef, onSnap) {
129
104
  gsap.to(root, { [RAIL_W]: `${w}px`, ...GRAB.snap, overwrite: 'auto' })
130
105
  onSnap(w !== CLOSED)
131
106
  }
107
+ /* the ONLY way out of this effect (RailSectionPressOpensRail, kol-fxr
108
+ * 2026-08-30). `snapTo` closes over the animation and the CLOSED/open
109
+ * ladder, so a section press has to reach it rather than reimplement it —
110
+ * a consumer setting `--kol-shell-rail-width` from outside widens the rail
111
+ * but leaves `railOpen` false, and the L2 rows render behind it: a wide,
112
+ * EMPTY rail, worse than the dead press this fixes. */
113
+ if (snapRef) snapRef.current = () => snapTo(openWidth())
132
114
  const onDown = (e) => {
133
115
  strip.setPointerCapture(e.pointerId)
134
116
  gsap.killTweensOf(root)
@@ -159,7 +141,7 @@ function useRailDrag(railRef, grabRef, onSnap) {
159
141
  strip.removeEventListener('pointerup', onUp)
160
142
  strip.removeEventListener('pointercancel', onUp)
161
143
  }
162
- }, [railRef, grabRef, onSnap])
144
+ }, [railRef, grabRef, onSnap, snapRef])
163
145
  }
164
146
 
165
147
  /* one row: the rung exactly where the closed rail had it, then the label and
@@ -170,8 +152,26 @@ function IconAt({ name, size, component }) {
170
152
  return <Cmp name={name} size={size} />
171
153
  }
172
154
 
173
- function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconComponent, railOpen }) {
155
+ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconComponent, railOpen, onOpenRail }) {
174
156
  const [open, setOpen] = useState(false)
157
+ /* A SECTION ROW IS NOT A DESTINATION (RailSectionPressOpensRail, kol-fxr
158
+ * 2026-08-30 — user: *"if I press effects from collapsed nav it should maybe
159
+ * open? currently pressing it does nothing"*).
160
+ *
161
+ * Closed, the row clips to 48px and the disclosure caret sits OUTSIDE that
162
+ * clip, so the only reachable control was the icon — which called
163
+ * `onNavigate` with a path no consumer dispatches, because a section has no
164
+ * action. A guaranteed dead press, in the rail's default state.
165
+ *
166
+ * Pressing it now opens the rail and expands that section. This is the
167
+ * behaviour `SideNav` had before the flat-rail reversal. It does not fight
168
+ * "nothing auto-expands" — that rule is about ARRIVING on a route; this is an
169
+ * explicit press. */
170
+ const isSection = sub?.length > 0
171
+ const press = () => {
172
+ if (isSection && !railOpen) { onOpenRail?.(); setOpen(true); return }
173
+ onNavigate?.(path)
174
+ }
175
175
  const active = path === '/' ? currentPath === '/' : currentPath.startsWith(path)
176
176
  return (
177
177
  <>
@@ -190,13 +190,13 @@ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconCompone
190
190
  * and too dim on an icon rail (user: "make the color .96 on every icon
191
191
  * in the rail") */
192
192
  style={{ color: 'var(--kol-oq-96)' }}
193
- onClick={() => onNavigate?.(path)}
193
+ onClick={press}
194
194
  title={label}
195
195
  aria-label={label}
196
196
  />
197
197
  <span
198
198
  className="kol-helper-12 uppercase text-oq-96 flex-1 min-w-0 truncate cursor-pointer"
199
- onClick={() => onNavigate?.(path)}
199
+ onClick={press}
200
200
  >
201
201
  {label}
202
202
  </span>
@@ -263,11 +263,12 @@ export default function NavRail({
263
263
  const railRef = useRef(null)
264
264
  const grabRef = useRef(null)
265
265
  const [railOpen, setRailOpen] = useState(false)
266
+ const snapOpenRef = useRef(null)
266
267
  useGrabEdge(grabRef)
267
- useRailDrag(railRef, grabRef, setRailOpen)
268
+ useRailDrag(railRef, grabRef, setRailOpen, snapOpenRef)
268
269
  if (hidden) return null
269
270
  const row = (item) => (
270
- <RailItem key={item.path} {...item} currentPath={currentPath} onNavigate={onNavigate} iconComponent={iconComponent} railOpen={railOpen} />
271
+ <RailItem key={item.path} {...item} currentPath={currentPath} onNavigate={onNavigate} iconComponent={iconComponent} railOpen={railOpen} onOpenRail={() => snapOpenRef.current?.()} />
271
272
  )
272
273
  return (
273
274
  <div