@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.
- package/package.json +3 -3
- 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.
|
|
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.
|
|
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={
|
|
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={
|
|
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
|