@kolkrabbi/kol-shell 0.60.0 → 0.62.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-shell",
3
- "version": "0.60.0",
3
+ "version": "0.62.0",
4
4
  "private": false,
5
5
  "description": "The application shell",
6
6
  "license": "MIT",
@@ -14,16 +14,16 @@
14
14
  "peerDependencies": {
15
15
  "@kolkrabbi/kol-component": ">=0.229.0",
16
16
  "@kolkrabbi/kol-icons": ">=0.16.0",
17
- "@kolkrabbi/kol-theme": ">=0.156.0",
17
+ "@kolkrabbi/kol-theme": ">=0.165.0",
18
18
  "gsap": "^3.13.0",
19
19
  "react": "^18.3.0 || ^19.0.0",
20
20
  "react-dom": "^18.3.0 || ^19.0.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@kolkrabbi/kol-component": "^0.237.0",
24
- "@kolkrabbi/kol-icons": "^0.33.0",
25
- "@kolkrabbi/kol-framework": "^0.48.0",
26
- "@kolkrabbi/kol-theme": "^0.163.0"
23
+ "@kolkrabbi/kol-component": "^0.240.0",
24
+ "@kolkrabbi/kol-icons": "^0.33.1",
25
+ "@kolkrabbi/kol-theme": "^0.166.0",
26
+ "@kolkrabbi/kol-framework": "^0.49.0"
27
27
  },
28
28
  "files": [
29
29
  "src",
package/src/AppShell.jsx CHANGED
@@ -42,6 +42,10 @@ import { TABBAR_H, MastheadContext } from '@kolkrabbi/kol-component'
42
42
  * open/close is the consumer's `onNavigate` (kol-mirror's model: navigating to
43
43
  * `/settings` while already there returns to the last page), and the theme
44
44
  * toggle lives on the settings page, not in the rail (user, 2026-08-28).
45
+ * @param {boolean} [props.railFade=true] the rail's list fades out at its foot (`NavRail fade`); `false` opts out
46
+ * @param {'fold'|'enter'} [props.railSections='fold'] how a section row opens: `fold` unfolds it in place (the
47
+ * accordion), `enter` replaces the list with its rows under a Back row
48
+ * (`NavRail sections`, 2026-10-06 — labs' catalog)
45
49
  * @param {string} props.railToggleKey a key that toggles the rail (e.g. '\\') — ignored while typing in a field;
46
50
  * the rail comes back on every `currentPath` change (ShellHomeSystem, 2026-08-27)
47
51
  * @param {'shell'|'bare'|'overlay'|'drawer'|'bar'} props.touch the touch-primary policy (default 'shell' = the rail regardless):
@@ -130,6 +134,8 @@ export default function AppShell({
130
134
  onNavigate,
131
135
  iconComponent,
132
136
  railComponent: Rail = NavRail,
137
+ railFade = true,
138
+ railSections = 'fold',
133
139
  railToggleKey,
134
140
  touch = 'shell',
135
141
  appName,
@@ -370,6 +376,8 @@ export default function AppShell({
370
376
  onNavigate={navigate}
371
377
  iconComponent={iconComponent}
372
378
  drawer={drawer}
379
+ fade={railFade}
380
+ sections={railSections}
373
381
  />
374
382
  )}
375
383
  {/* THE BACK OF THE BACK — surface-primary, always, in every app; the
package/src/NavRail.jsx CHANGED
@@ -70,6 +70,10 @@ import Logomark from './Logomark.jsx'
70
70
  * @param {boolean} drawer off-canvas mode: no grab, no drag, no token writes, rows always
71
71
  * labelled, and the rail sizes itself from `--kol-shell-drawer-width` (default 240px).
72
72
  * AppShell sets this from `touch="drawer"` — see ShellRailNoDrawerOnMobile, 2026-08-31.
73
+ * @param {boolean} [fade=true] the list fades out at its foot — the workshop rail's fade
74
+ * (2026-10-01), here too on the user's word (2026-10-02). The rows sit in their own scroll
75
+ * region between the mark and the pinned bottom rows, so a list longer than the window scrolls
76
+ * and runs out under the fade instead of being cut by the edge. `false` draws the list plain.
73
77
  */
74
78
  const RAIL_W = '--kol-shell-rail-width'
75
79
  const CLOSED = 48
@@ -163,7 +167,7 @@ function IconAt({ name, size, component }) {
163
167
  return <Cmp name={name} size={size} />
164
168
  }
165
169
 
166
- function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconComponent, railOpen, onOpenRail }) {
170
+ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconComponent, railOpen, onOpenRail, onEnter }) {
167
171
  const [open, setOpen] = useState(false)
168
172
  /* A SECTION ROW IS NOT A DESTINATION (RailSectionPressOpensRail, kol-fxr
169
173
  * 2026-08-30 — user: *"if I press effects from collapsed nav it should maybe
@@ -179,8 +183,13 @@ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconCompone
179
183
  * "nothing auto-expands" — that rule is about ARRIVING on a route; this is an
180
184
  * explicit press. */
181
185
  const isSection = sub?.length > 0
186
+ /* …AND IN AN OPEN RAIL THE SAME PRESS FOLDS AND UNFOLDS IT (2026-10-05, labs on a phone). Open,
187
+ * it still fell through to `onNavigate` with that path no consumer dispatches: dead at a desk,
188
+ * and worse on a touch drawer, where AppShell closes the drawer on every navigate — pressing a
189
+ * section's name shut the nav and loaded nothing. */
182
190
  const press = () => {
183
- if (isSection && !railOpen) { onOpenRail?.(); setOpen(true); return }
191
+ if (isSection && onEnter) { if (!railOpen) onOpenRail?.(); onEnter(); return }
192
+ if (isSection) { if (!railOpen) onOpenRail?.(); setOpen((v) => !railOpen || !v); return }
184
193
  onNavigate?.(path)
185
194
  }
186
195
  const active = path === '/' ? currentPath === '/' : currentPath.startsWith(path)
@@ -217,7 +226,7 @@ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconCompone
217
226
  {sub?.length > 0 && (
218
227
  <Tooltip label={label} placement="right" triggerClassName="inline-flex shrink-0">
219
228
  <Button
220
- iconOnly={open ? 'chevron-down' : 'chevron-right'}
229
+ iconOnly={onEnter ? 'chevron-right' : open ? 'chevron-down' : 'chevron-right'}
221
230
  iconSize={16}
222
231
  iconComponent={iconComponent}
223
232
  variant="nav"
@@ -226,14 +235,14 @@ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconCompone
226
235
  size="md"
227
236
  className="shrink-0"
228
237
  style={{ color: 'var(--kol-oq-96)' }}
229
- aria-expanded={open}
230
- onClick={() => setOpen((v) => !v)}
238
+ aria-expanded={onEnter ? undefined : open}
239
+ onClick={onEnter ? press : () => setOpen((v) => !v)}
231
240
  aria-label={`${label} sub categories`}
232
241
  />
233
242
  </Tooltip>
234
243
  )}
235
244
  </div>
236
- {railOpen && open && sub.map((s) => {
245
+ {railOpen && open && !onEnter && sub.map((s) => {
237
246
  const on = s.path === '/' ? currentPath === '/' : currentPath.startsWith(s.path)
238
247
  return (
239
248
  <Tooltip key={s.path} label={s.label} placement="right" asChild>
@@ -277,18 +286,29 @@ export default function NavRail({
277
286
  iconComponent,
278
287
  hidden = false,
279
288
  drawer = false,
289
+ fade = true,
290
+ sections = 'fold',
280
291
  }) {
281
292
  const railRef = useRef(null)
282
293
  const grabRef = useRef(null)
283
294
  const [railOpen, setRailOpen] = useState(false)
295
+ /* ENTER MODE (`sections="enter"`, 2026-10-06 — the user, on labs: "having a 'enter' mode in the
296
+ * left sidebar instead of collapse folder?"): pressing a section replaces the list with that
297
+ * section's rows under a Back row, instead of unfolding it in place. `fold` is the accordion
298
+ * every rail had and keeps. Per consumer, not a law: labs' catalog is five sections of up to
299
+ * ten rows each, which is where an accordion stops reading. */
300
+ const [entered, setEntered] = useState(null)
284
301
  const snapOpenRef = useRef(null)
285
302
  /* the grab strip is not rendered in drawer mode, so the hook's ref stays null
286
303
  * and it no-ops on its own — no second argument needed */
287
304
  useGrabEdge(grabRef)
288
305
  useRailDrag(railRef, grabRef, setRailOpen, snapOpenRef, !drawer)
289
306
  if (hidden) return null
307
+ const enterMode = sections === 'enter'
308
+ const section = enterMode && entered ? items.find((i) => i.path === entered) : null
290
309
  const row = (item) => (
291
- <RailItem key={item.path} {...item} currentPath={currentPath} onNavigate={onNavigate} iconComponent={iconComponent} railOpen={drawer || railOpen} onOpenRail={() => snapOpenRef.current?.()} />
310
+ <RailItem key={item.path} {...item} currentPath={currentPath} onNavigate={onNavigate} iconComponent={iconComponent} railOpen={drawer || railOpen} onOpenRail={() => snapOpenRef.current?.()}
311
+ onEnter={enterMode ? () => setEntered(item.path) : undefined} />
292
312
  )
293
313
  return (
294
314
  <div
@@ -340,9 +360,17 @@ export default function NavRail({
340
360
  </div>
341
361
  </Tooltip>
342
362
  )}
343
- {items.map(row)}
344
- {/* the spacer IS the layout */}
345
- <div className="flex-1" />
363
+ {/* THE LIST IS ITS OWN REGION (2026-10-02): it takes the room between the mark and the pinned
364
+ * bottom rows — what the bare spacer used to hold — scrolls when the rows outrun the window,
365
+ * and fades at its foot unless `fade={false}`. Same column, same gap, as the rail's own. */}
366
+ <div className={`kol-shell-rail-list flex min-h-0 w-full flex-1 flex-col items-start gap-2 overflow-y-auto overflow-x-hidden ${fade ? 'kol-shell-rail-list--fade' : ''}`.trim()}>
367
+ {section ? (
368
+ <>
369
+ <RailItem icon="arrow-left" path={section.path} label={section.label} currentPath="" onNavigate={() => setEntered(null)} iconComponent={iconComponent} railOpen={drawer || railOpen} />
370
+ {section.sub.map((s) => <RailItem key={s.path} {...s} currentPath={currentPath} onNavigate={onNavigate} iconComponent={iconComponent} railOpen={drawer || railOpen} />)}
371
+ </>
372
+ ) : items.map(row)}
373
+ </div>
346
374
  {/* the rule runs the full rail width, out past the px-2 */}
347
375
  {bottomItems.length > 0 && <div className="self-stretch -mx-2 border-t border-fg-08" />}
348
376
  {bottomItems.map(row)}