@kolkrabbi/kol-component 0.326.0 → 0.328.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 +1 -1
- package/src/organisms/NavRail.jsx +65 -17
package/package.json
CHANGED
|
@@ -82,6 +82,7 @@ import NavRailSite from './NavRailSite.jsx'
|
|
|
82
82
|
* EXPANDS (its rows open under it in place) and COLLAPSES. No other words, in code or docs.
|
|
83
83
|
*
|
|
84
84
|
* @param {'app'|'site'|'workshop'} preset which rail (default `app`); `variant="site"` is the old spelling of `preset="site"`
|
|
85
|
+
* @param {'top'} variant `top` — the horizontal top rail (TopRail): logomark · items · `actions` (the right slot)
|
|
85
86
|
* @param {Array} items `{ icon, path, label, sub?: [{ icon?, path, label }] }`
|
|
86
87
|
* a section (`sub`) with `to` is a FOLDER NOTE: its icon and label open `to`, its chevron expands or enters
|
|
87
88
|
* @param {Array} bottomItems pinned below the rule (Settings)
|
|
@@ -122,11 +123,21 @@ const openWidth = () => {
|
|
|
122
123
|
* are now — inside `GRAB.range`, the middle band of the edge, so it never rides
|
|
123
124
|
* up beside the logomark. One window listener, rAF-throttled. */
|
|
124
125
|
|
|
125
|
-
/* THE DRAG
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
126
|
+
/* THE DRAG HOLDS ITS WIDTH (RailDragHoldsWidth, kol-fxr 2026-10-31 — user: *"you should be able to drag it however
|
|
127
|
+
* you want, it will just get reset on rail click"*). Hold the pill and the width follows the pointer between CLOSED
|
|
128
|
+
* and MAX; release leaves it exactly there, no snap. A click (no travel past GRAB.slop) still snaps collapsed ⇄
|
|
129
|
+
* expanded — any width past CLOSED closes, CLOSED opens to the open width — so a click resets a custom width.
|
|
130
|
+
*
|
|
131
|
+
* THE LABEL THRESHOLD (LABELS): below it the rail is icons only — labels, chevrons and the mark's title hide (the
|
|
132
|
+
* `@max-[…]:hidden` container query on the rows, measured on the rail's content box = LABELS − its px-2 gutter), never
|
|
133
|
+
* a label clipped in a narrow strip. `onSnap` reports from the same threshold: crossings during a drag report live,
|
|
134
|
+
* a snap reports its target once at the start of the tween (as before) — the sub rows render only while it is true,
|
|
135
|
+
* because closed they would still take their height and push the rungs below them.
|
|
136
|
+
*
|
|
137
|
+
* ONE AXIS: the gesture reads `pos` and `size` only. A side rail reads x and width; a top rail (variant="top" — built
|
|
138
|
+
* 2026-10-31 as `TopRail`, without a drag) would swap them for y and height — nothing else in here assumes a side. */
|
|
139
|
+
const RAIL_MAX = 560
|
|
140
|
+
const RAIL_LABELS = 160
|
|
130
141
|
function useRailDrag(railRef, grabRef, onSnap, snapRef, enabled = true) {
|
|
131
142
|
useEffect(() => {
|
|
132
143
|
const strip = grabRef.current, rail = railRef.current, root = document.documentElement
|
|
@@ -137,11 +148,15 @@ function useRailDrag(railRef, grabRef, onSnap, snapRef, enabled = true) {
|
|
|
137
148
|
* exactly the inline token a consumer could not reach without `!important`. */
|
|
138
149
|
if (!enabled) return undefined
|
|
139
150
|
if (!strip || !rail) return undefined
|
|
151
|
+
const pos = (e) => e.clientX
|
|
152
|
+
const size = () => rail.offsetWidth
|
|
140
153
|
gsap.set(root, { [RAIL_W]: `${CLOSED}px` })
|
|
141
154
|
let drag = null
|
|
155
|
+
let labelled = false
|
|
156
|
+
const report = (w) => { const on = w >= RAIL_LABELS; if (on !== labelled) { labelled = on; onSnap(on) } }
|
|
142
157
|
const snapTo = (w) => {
|
|
143
158
|
gsap.to(root, { [RAIL_W]: `${w}px`, ...GRAB.snap, overwrite: 'auto' })
|
|
144
|
-
|
|
159
|
+
report(w)
|
|
145
160
|
}
|
|
146
161
|
/* the ONLY way out of this effect (RailSectionPressOpensRail, kol-fxr
|
|
147
162
|
* 2026-08-30). `snapTo` closes over the animation and the CLOSED/open
|
|
@@ -155,20 +170,21 @@ function useRailDrag(railRef, grabRef, onSnap, snapRef, enabled = true) {
|
|
|
155
170
|
const onDown = (e) => {
|
|
156
171
|
strip.setPointerCapture(e.pointerId)
|
|
157
172
|
gsap.killTweensOf(root)
|
|
158
|
-
drag = {
|
|
173
|
+
drag = { p: pos(e), w: size(), moved: false }
|
|
159
174
|
strip.classList.add('is-dragging')
|
|
160
175
|
}
|
|
161
176
|
const onMove = (e) => {
|
|
162
177
|
if (!drag) return
|
|
163
|
-
const
|
|
164
|
-
if (Math.abs(
|
|
165
|
-
|
|
178
|
+
const d = pos(e) - drag.p
|
|
179
|
+
if (Math.abs(d) > GRAB.slop) drag.moved = true
|
|
180
|
+
if (!drag.moved) return
|
|
181
|
+
const w = Math.min(Math.max(drag.w + d, CLOSED), RAIL_MAX)
|
|
182
|
+
root.style.setProperty(RAIL_W, `${w}px`)
|
|
183
|
+
report(w)
|
|
166
184
|
}
|
|
167
185
|
const onUp = () => {
|
|
168
186
|
if (!drag) return
|
|
169
|
-
|
|
170
|
-
const open = drag.open
|
|
171
|
-
snapTo(drag.moved ? (w > (CLOSED + open) / 2 ? open : CLOSED) : (drag.w > CLOSED ? CLOSED : open))
|
|
187
|
+
if (!drag.moved) snapTo(drag.w > CLOSED ? CLOSED : openWidth())
|
|
172
188
|
drag = null
|
|
173
189
|
strip.classList.remove('is-dragging')
|
|
174
190
|
}
|
|
@@ -250,13 +266,13 @@ function RailItem({ icon, path, to, label, sub, currentPath, onNavigate, iconCom
|
|
|
250
266
|
/>
|
|
251
267
|
</Tooltip>
|
|
252
268
|
<span
|
|
253
|
-
className={`${RAIL_ROW.label} text-oq-96 flex-1 min-w-0 truncate cursor-pointer`}
|
|
269
|
+
className={`${RAIL_ROW.label} text-oq-96 flex-1 min-w-0 truncate cursor-pointer @max-[144px]/rail:hidden`}
|
|
254
270
|
onClick={press}
|
|
255
271
|
>
|
|
256
272
|
{label}
|
|
257
273
|
</span>
|
|
258
274
|
{sub?.length > 0 && (
|
|
259
|
-
<Tooltip label={label} placement="right" triggerClassName="inline-flex shrink-0">
|
|
275
|
+
<Tooltip label={label} placement="right" triggerClassName="inline-flex shrink-0 @max-[144px]/rail:hidden">
|
|
260
276
|
<Button
|
|
261
277
|
iconOnly={onEnter ? 'chevron-right' : open ? 'chevron-down' : 'chevron-right'}
|
|
262
278
|
iconSize={16}
|
|
@@ -306,6 +322,7 @@ export const RAIL_PRESETS = {
|
|
|
306
322
|
export function registerRailPreset(name, Component) { RAIL_PRESETS[name] = { ...(RAIL_PRESETS[name] ?? {}), component: Component } }
|
|
307
323
|
|
|
308
324
|
export default function NavRail({ variant, preset, ...props }) {
|
|
325
|
+
if (variant === 'top') return <TopRail {...props} />
|
|
309
326
|
const name = preset ?? (variant === 'site' ? 'site' : 'app')
|
|
310
327
|
if (name === 'app') return <AppRail {...props} sections={props.sections ?? RAIL_PRESETS.app.sections} />
|
|
311
328
|
const Named = RAIL_PRESETS[name]?.component
|
|
@@ -313,6 +330,37 @@ export default function NavRail({ variant, preset, ...props }) {
|
|
|
313
330
|
return <Named {...props} />
|
|
314
331
|
}
|
|
315
332
|
|
|
333
|
+
/* THE TOP RAIL (`variant="top"`, 2026-10-31 — user: *"it would have to be a TopNavRail? and completely rebuilt with Rail
|
|
334
|
+
* Component? so we wouldnt ever import this, but make something in its stead"*). Replaces Atlas's /lab header ("Lab bar"
|
|
335
|
+
* in Figma): the rail's own parts laid out on a row, 48 tall — the mark and the app name (the side rail's logomark row),
|
|
336
|
+
* the items as rail rows (a nav Button rung + the rail label), then `actions` on the right (views / menus — a `Menu` with
|
|
337
|
+
* a `MenuTrigger`). Same type and inks as the side rail; no grab, no drag. A row's `sub` is not drawn here. */
|
|
338
|
+
function TopRail({ items = [], logomark, currentPath = '', onNavigate, iconComponent, actions, hidden = false, className = '' }) {
|
|
339
|
+
if (hidden) return null
|
|
340
|
+
return (
|
|
341
|
+
<header className={`kol-shell-rail kol-shell-rail--top bg-surface-primary sticky top-0 flex h-12 w-full items-center gap-6 px-4 border-b border-fg-08 ${className}`.trim()}>
|
|
342
|
+
{logomark && (
|
|
343
|
+
<div onClick={() => onNavigate?.('/')} className="text-oq-96 cursor-pointer flex items-center gap-3 shrink-0">
|
|
344
|
+
{logomark.svgUrl && <VectorLoader src={logomark.svgUrl} size={20} />}
|
|
345
|
+
<span className="kol-helper-12 uppercase">{logomark.title}</span>
|
|
346
|
+
</div>
|
|
347
|
+
)}
|
|
348
|
+
<nav className="flex min-w-0 flex-1 items-center gap-2 overflow-x-auto">
|
|
349
|
+
{items.map(({ icon, path, label }) => {
|
|
350
|
+
const active = path === '/' ? currentPath === '/' : currentPath.startsWith(path)
|
|
351
|
+
return (
|
|
352
|
+
<div key={path} className="flex items-center gap-2 shrink-0 cursor-pointer" onClick={() => onNavigate?.(path)}>
|
|
353
|
+
{icon && <Button iconOnly={icon} iconSize={RAIL_ROW.glyph} iconComponent={iconComponent} variant="nav" size="md" aria-current={active ? 'page' : undefined} aria-label={label} />}
|
|
354
|
+
<span className={`${RAIL_ROW.label} text-oq-96`}>{label}</span>
|
|
355
|
+
</div>
|
|
356
|
+
)
|
|
357
|
+
})}
|
|
358
|
+
</nav>
|
|
359
|
+
{actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
|
|
360
|
+
</header>
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
316
364
|
function AppRail({
|
|
317
365
|
items = [],
|
|
318
366
|
bottomItems = [],
|
|
@@ -371,7 +419,7 @@ function AppRail({
|
|
|
371
419
|
* trigger travelling to mid-screen, never the navigation's side. The
|
|
372
420
|
* trigger is now pinned top-right in both states, so a left panel has
|
|
373
421
|
* nothing to collide with and the drawer opens where the rail lives. */
|
|
374
|
-
className="kol-shell-rail bg-surface-primary fixed inset-y-0 left-0 flex flex-col items-start pt-4 pb-4 px-2 gap-2 border-r border-fg-08"
|
|
422
|
+
className="@container/rail kol-shell-rail bg-surface-primary fixed inset-y-0 left-0 flex flex-col items-start pt-4 pb-4 px-2 gap-2 border-r border-fg-08"
|
|
375
423
|
/* A drawer takes its own width, NOT the live rail token — that token is
|
|
376
424
|
* zeroed in drawer mode so the content gets the whole viewport back. */
|
|
377
425
|
style={{ width: drawer ? 'var(--kol-shell-drawer-width, 240px)' : `var(${RAIL_W})` }}
|
|
@@ -413,7 +461,7 @@ function AppRail({
|
|
|
413
461
|
className="text-oq-96 cursor-pointer flex items-center gap-3 w-full overflow-hidden shrink-0 pt-1 mb-4"
|
|
414
462
|
>
|
|
415
463
|
<span className="w-8 flex justify-center shrink-0"><VectorLoader src={logomark.svgUrl} size={20} /></span>
|
|
416
|
-
<span className="kol-helper-12 uppercase flex-1 min-w-0 truncate">{logomark.title}</span>
|
|
464
|
+
<span className="kol-helper-12 uppercase flex-1 min-w-0 truncate @max-[144px]/rail:hidden">{logomark.title}</span>
|
|
417
465
|
</div>
|
|
418
466
|
</Tooltip>
|
|
419
467
|
)}
|