@kolkrabbi/kol-shell 0.61.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.61.0",
3
+ "version": "0.62.0",
4
4
  "private": false,
5
5
  "description": "The application shell",
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.239.0",
24
- "@kolkrabbi/kol-framework": "^0.49.0",
25
- "@kolkrabbi/kol-icons": "^0.33.0",
26
- "@kolkrabbi/kol-theme": "^0.165.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
@@ -43,6 +43,9 @@ import { TABBAR_H, MastheadContext } from '@kolkrabbi/kol-component'
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
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)
46
49
  * @param {string} props.railToggleKey a key that toggles the rail (e.g. '\\') — ignored while typing in a field;
47
50
  * the rail comes back on every `currentPath` change (ShellHomeSystem, 2026-08-27)
48
51
  * @param {'shell'|'bare'|'overlay'|'drawer'|'bar'} props.touch the touch-primary policy (default 'shell' = the rail regardless):
@@ -132,6 +135,7 @@ export default function AppShell({
132
135
  iconComponent,
133
136
  railComponent: Rail = NavRail,
134
137
  railFade = true,
138
+ railSections = 'fold',
135
139
  railToggleKey,
136
140
  touch = 'shell',
137
141
  appName,
@@ -373,6 +377,7 @@ export default function AppShell({
373
377
  iconComponent={iconComponent}
374
378
  drawer={drawer}
375
379
  fade={railFade}
380
+ sections={railSections}
376
381
  />
377
382
  )}
378
383
  {/* THE BACK OF THE BACK — surface-primary, always, in every app; the
package/src/NavRail.jsx CHANGED
@@ -167,7 +167,7 @@ function IconAt({ name, size, component }) {
167
167
  return <Cmp name={name} size={size} />
168
168
  }
169
169
 
170
- function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconComponent, railOpen, onOpenRail }) {
170
+ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconComponent, railOpen, onOpenRail, onEnter }) {
171
171
  const [open, setOpen] = useState(false)
172
172
  /* A SECTION ROW IS NOT A DESTINATION (RailSectionPressOpensRail, kol-fxr
173
173
  * 2026-08-30 — user: *"if I press effects from collapsed nav it should maybe
@@ -183,8 +183,13 @@ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconCompone
183
183
  * "nothing auto-expands" — that rule is about ARRIVING on a route; this is an
184
184
  * explicit press. */
185
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. */
186
190
  const press = () => {
187
- 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 }
188
193
  onNavigate?.(path)
189
194
  }
190
195
  const active = path === '/' ? currentPath === '/' : currentPath.startsWith(path)
@@ -221,7 +226,7 @@ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconCompone
221
226
  {sub?.length > 0 && (
222
227
  <Tooltip label={label} placement="right" triggerClassName="inline-flex shrink-0">
223
228
  <Button
224
- iconOnly={open ? 'chevron-down' : 'chevron-right'}
229
+ iconOnly={onEnter ? 'chevron-right' : open ? 'chevron-down' : 'chevron-right'}
225
230
  iconSize={16}
226
231
  iconComponent={iconComponent}
227
232
  variant="nav"
@@ -230,14 +235,14 @@ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconCompone
230
235
  size="md"
231
236
  className="shrink-0"
232
237
  style={{ color: 'var(--kol-oq-96)' }}
233
- aria-expanded={open}
234
- onClick={() => setOpen((v) => !v)}
238
+ aria-expanded={onEnter ? undefined : open}
239
+ onClick={onEnter ? press : () => setOpen((v) => !v)}
235
240
  aria-label={`${label} sub categories`}
236
241
  />
237
242
  </Tooltip>
238
243
  )}
239
244
  </div>
240
- {railOpen && open && sub.map((s) => {
245
+ {railOpen && open && !onEnter && sub.map((s) => {
241
246
  const on = s.path === '/' ? currentPath === '/' : currentPath.startsWith(s.path)
242
247
  return (
243
248
  <Tooltip key={s.path} label={s.label} placement="right" asChild>
@@ -282,18 +287,28 @@ export default function NavRail({
282
287
  hidden = false,
283
288
  drawer = false,
284
289
  fade = true,
290
+ sections = 'fold',
285
291
  }) {
286
292
  const railRef = useRef(null)
287
293
  const grabRef = useRef(null)
288
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)
289
301
  const snapOpenRef = useRef(null)
290
302
  /* the grab strip is not rendered in drawer mode, so the hook's ref stays null
291
303
  * and it no-ops on its own — no second argument needed */
292
304
  useGrabEdge(grabRef)
293
305
  useRailDrag(railRef, grabRef, setRailOpen, snapOpenRef, !drawer)
294
306
  if (hidden) return null
307
+ const enterMode = sections === 'enter'
308
+ const section = enterMode && entered ? items.find((i) => i.path === entered) : null
295
309
  const row = (item) => (
296
- <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} />
297
312
  )
298
313
  return (
299
314
  <div
@@ -349,7 +364,12 @@ export default function NavRail({
349
364
  * bottom rows — what the bare spacer used to hold — scrolls when the rows outrun the window,
350
365
  * and fades at its foot unless `fade={false}`. Same column, same gap, as the rail's own. */}
351
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()}>
352
- {items.map(row)}
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)}
353
373
  </div>
354
374
  {/* the rule runs the full rail width, out past the px-2 */}
355
375
  {bottomItems.length > 0 && <div className="self-stretch -mx-2 border-t border-fg-08" />}