@kolkrabbi/kol-shell 0.63.0 → 0.65.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.63.0",
3
+ "version": "0.65.0",
4
4
  "private": false,
5
5
  "description": "The application shell",
6
6
  "license": "MIT",
@@ -12,7 +12,7 @@
12
12
  "./src/*": "./src/*"
13
13
  },
14
14
  "peerDependencies": {
15
- "@kolkrabbi/kol-component": ">=0.246.0",
15
+ "@kolkrabbi/kol-component": ">=0.265.0",
16
16
  "@kolkrabbi/kol-icons": ">=0.16.0",
17
17
  "@kolkrabbi/kol-theme": ">=0.165.0",
18
18
  "gsap": "^3.13.0",
@@ -20,10 +20,10 @@
20
20
  "react-dom": "^18.3.0 || ^19.0.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@kolkrabbi/kol-component": "^0.246.0",
24
- "@kolkrabbi/kol-icons": "^0.33.1",
23
+ "@kolkrabbi/kol-component": "^0.265.0",
24
+ "@kolkrabbi/kol-icons": "^0.39.0",
25
25
  "@kolkrabbi/kol-framework": "^0.49.0",
26
- "@kolkrabbi/kol-theme": "^0.170.0"
26
+ "@kolkrabbi/kol-theme": "^0.174.0"
27
27
  },
28
28
  "files": [
29
29
  "src",
package/src/AppHub.jsx CHANGED
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
2
2
  import AppShell from './AppShell.jsx'
3
3
  import HubHome from './HubHome.jsx'
4
4
  import HubSettings from './HubSettings.jsx'
5
+ import HubDev from './HubDev.jsx'
5
6
  import { ShortcutsOverlay } from '@kolkrabbi/kol-component'
6
7
 
7
8
  /* taxonomy-ok: organism — nests AppShell / HubHome / HubSettings / ShortcutsOverlay (relative) */
@@ -37,6 +38,9 @@ import { ShortcutsOverlay } from '@kolkrabbi/kol-component'
37
38
  * OPT-IN like AppShell's `bottomItems` (apps review 2026-09-29: *"not sure it makes sense
38
39
  * to neccessarily have settings manditory part of the rail"*) — no `settings`, no rail
39
40
  * row, no route, no `,` key. It was pinned whether the app had settings or not.
41
+ * @param {Object} dev HubDev's props (`tabs`, `title`, `subtitle`) + `path` (default `/dev`). OPT-IN, and only on
42
+ * a dev server (`import.meta.env.DEV`; `always: true` overrides) — a `code` row above
43
+ * Settings (CompareGridDevTab, kol-monitor 2026-10-17)
40
44
  * @param {Array} shortcuts `[{ section, items: [{ id, label, combo }] }]` — the sheet AND Settings
41
45
  * @param {ReactNode} themeToggle Settings' masthead toggle (a node — kol-framework is not a peer)
42
46
  * @param {string} shortcutsKey default `s`; `null` turns the sheet key off (a tool that owns S)
@@ -55,6 +59,7 @@ export default function AppHub({
55
59
  home,
56
60
  walkthrough,
57
61
  settings,
62
+ dev,
58
63
  shortcuts = [],
59
64
  themeToggle,
60
65
  settingsPath = '/settings',
@@ -79,12 +84,18 @@ export default function AppHub({
79
84
  return () => window.removeEventListener('keydown', onKey)
80
85
  }, [shortcutsKey])
81
86
 
87
+ /* the dev page is a dev server's only — a production build never routes it or draws its row */
88
+ const devOn = !!dev && (dev.always || !!import.meta.env?.DEV)
89
+ const devPath = dev?.path ?? '/dev'
90
+ const { path: _dp, always: _da, ...devProps } = dev ?? {}
82
91
  const page = home && currentPath === homePath
83
92
  ? <HubHome app={app} iconComponent={iconComponent} walkthrough={walkthrough} {...home} />
84
93
  : settings && currentPath === settingsPath
85
94
  ? <HubSettings app={app} shortcuts={shortcuts} themeToggle={themeToggle} {...settings} />
86
- : children
87
- const bottom = settings ? SETTINGS_ROW(settingsPath) : []
95
+ : devOn && (currentPath === devPath || currentPath?.startsWith(`${devPath}/`))
96
+ ? <HubDev storageKey={`kol-dev-tab:${app.name ?? 'app'}`} {...devProps} />
97
+ : children
98
+ const bottom = [...(devOn ? [{ icon: 'code', path: devPath, label: 'Dev' }] : []), ...(settings ? SETTINGS_ROW(settingsPath) : [])]
88
99
  /* the phone bar lists Home as a tab — the rail draws it as the mark, the bar has no mark */
89
100
  const barItems = [...(home ? [{ icon: 'home-01', path: homePath, label: 'Home' }] : []), ...items, ...bottom]
90
101
 
@@ -111,7 +122,7 @@ export default function AppHub({
111
122
  {...shell}
112
123
  >
113
124
  {page}
114
- {sheet && <ShortcutsOverlay shortcuts={shortcuts} onClose={() => setSheet(false)} />}
125
+ {sheet && <ShortcutsOverlay shortcuts={shortcuts} currentPath={currentPath} onClose={() => setSheet(false)} />}
115
126
  </AppShell>
116
127
  )
117
128
  }
package/src/AppStudio.jsx CHANGED
@@ -28,11 +28,16 @@ import PageShell from './PageShell.jsx'
28
28
  *
29
29
  * @param {Object} app AppHub's `{ name, subtitle, logomark, about, links }`
30
30
  * @param {Object} home HubHome's props — the landing Catalog (required: a Studio opens on Home)
31
- * @param {Object} [library] CatalogPage's props, plus `{ path, label, icon }`
32
- * @param {Object} [create] `{ header (PageHeader props), children, path, label, icon }`
31
+ * @param {Object} [library] CatalogPage's props — or `children` (a router, for pages under the
32
+ * slot: `/library/:id`), which renders in place of the CatalogPage —
33
+ * plus `{ path, label, icon }`
34
+ * @param {Object} [create] `{ header (PageHeader props), children, path, label, icon }`;
35
+ * `shell: false` renders `children` bare — for a Create that owns its
36
+ * PageShell (a fixed canvas beside a scrolling list)
33
37
  * @param {Object} [use] `{ children, path, label, icon }` — the tool
34
38
  * @param {Array} [pages] `[{ path, icon, label, children }]` — opt-in pages, after Use
35
39
  * @param {Object} [settings] HubSettings' props
40
+ * @param {Object} [dev] HubDev's props — AppHub's `dev`: a dev server's `code` page at `/dev`
36
41
  * @param {'mono'|'display'} [masthead='mono']
37
42
  * @param {Object} [shell] any AppShell prop, spread last
38
43
  */
@@ -47,6 +52,7 @@ export default function AppStudio({
47
52
  use,
48
53
  pages = [],
49
54
  settings,
55
+ dev,
50
56
  walkthrough,
51
57
  shortcuts,
52
58
  themeToggle,
@@ -63,10 +69,10 @@ export default function AppStudio({
63
69
 
64
70
  let page = null
65
71
  if (lib && at(lib.path)) {
66
- const { path: _p, label: _l, icon: _i, ...props } = lib
67
- page = <CatalogPage {...props} />
72
+ const { path: _p, label: _l, icon: _i, children, ...props } = lib
73
+ page = children ?? <CatalogPage {...props} />
68
74
  } else if (make && at(make.path)) {
69
- page = (
75
+ page = make.shell === false ? make.children : (
70
76
  <PageShell>
71
77
  {make.header && <PageHeader size="sm" {...make.header} />}
72
78
  {make.children}
@@ -88,6 +94,7 @@ export default function AppStudio({
88
94
  home={home}
89
95
  walkthrough={walkthrough}
90
96
  settings={settings}
97
+ dev={dev}
91
98
  shortcuts={shortcuts}
92
99
  themeToggle={themeToggle}
93
100
  shortcutsKey={shortcutsKey}
package/src/HubDev.jsx ADDED
@@ -0,0 +1,46 @@
1
+ import { useState } from 'react'
2
+ import { PageHeader } from '@kolkrabbi/kol-component'
3
+ import PageShell from './PageShell.jsx'
4
+ import TabStrip from './TabStrip.jsx'
5
+
6
+ /* taxonomy-ok: organism — nests PageShell / TabStrip (relative) + kol-component's PageHeader */
7
+
8
+ const read = (key, fallback) => { try { return localStorage.getItem(key) ?? fallback } catch { return fallback } }
9
+ const write = (key, value) => { try { localStorage.setItem(key, value) } catch { /* private mode: the tab just resets */ } }
10
+
11
+ /**
12
+ * HubDev — A hub's development page. The one rail row for everything that is not the app yet: experiments,
13
+ * references, comparisons (CompareGridDevTab, kol-monitor 2026-10-17 — user: *"I would also maybe add
14
+ * instructions for making the dev tab to the hub … localhost only using the </> icon"*). Each app had built
15
+ * this page by hand and each read differently; this is the page made once, for reading.
16
+ *
17
+ * The shape: a small mono masthead with one line saying what the page is, the tabs on their own row under
18
+ * it, the active tab's one-line description, then the tab's content at full width. Detail belongs in the
19
+ * content's own toggles, not in more text here. The open tab is remembered per app.
20
+ *
21
+ * Rendered by `AppHub` / `AppStudio` from their `dev` prop, only on a dev server (`import.meta.env.DEV`),
22
+ * at `/dev`, behind the `code` rail row.
23
+ *
24
+ * @param {Array} tabs `[{ value, label, description?, content }]`
25
+ * @param {string} [title='Dev']
26
+ * @param {string} [subtitle] one line: what the page is
27
+ * @param {string} [storageKey] where the open tab is remembered
28
+ */
29
+ export default function HubDev({ tabs = [], title = 'Dev', subtitle = 'Experiments and reference, not the app', storageKey = 'kol-dev-tab' }) {
30
+ const [stored, setStored] = useState(() => read(storageKey, tabs[0]?.value))
31
+ const active = tabs.find((t) => t.value === stored) ?? tabs[0]
32
+ const pick = (v) => { setStored(v); write(storageKey, v) }
33
+
34
+ return (
35
+ <PageShell className="gap-6">
36
+ <PageHeader size="sm" voice="mono" title={title} subtitle={subtitle} />
37
+ {tabs.length > 1 && (
38
+ <div className="flex flex-col gap-2 border-b border-fg-08 pb-4">
39
+ <TabStrip options={tabs.map(({ value, label }) => ({ value, label }))} value={active?.value} onChange={pick} tracked />
40
+ {active?.description && <p className="kol-helper-12 text-meta">{active.description}</p>}
41
+ </div>
42
+ )}
43
+ {active && <div className="min-w-0">{active.content}</div>}
44
+ </PageShell>
45
+ )
46
+ }
package/src/NavRail.jsx CHANGED
@@ -9,6 +9,7 @@ import { GRAB } from '@kolkrabbi/kol-component/utilities/motion'
9
9
  import { useGrabEdge } from '@kolkrabbi/kol-component'
10
10
  import { Icon } from '@kolkrabbi/kol-icons'
11
11
  import Logomark from './Logomark.jsx'
12
+ import { useRailSlot } from './railSlot.jsx'
12
13
 
13
14
  /**
14
15
  * NavRail — the flat rail. ONE fixed column, every child a direct child of it
@@ -70,6 +71,11 @@ import Logomark from './Logomark.jsx'
70
71
  * @param {boolean} drawer off-canvas mode: no grab, no drag, no token writes, rows always
71
72
  * labelled, and the rail sizes itself from `--kol-shell-drawer-width` (default 240px).
72
73
  * AppShell sets this from `touch="drawer"` — see ShellRailNoDrawerOnMobile, 2026-08-31.
74
+ * Under a `RailSlotProvider` the opened width also carries a CONSUMER SLOT below the rows
75
+ * (NavRailConsumerSlot, kol-monitor 2026-10-15) — content that is not a row list (monitor's module
76
+ * catalog: rows that insert, not paths). Always mounted so portalled content keeps its state across
77
+ * open/close; zero tall and clipped while closed. No provider → no slot, the render unchanged.
78
+ *
73
79
  * @param {boolean} [fade=true] the list fades out at its foot — the workshop rail's fade
74
80
  * (2026-10-01), here too on the user's word (2026-10-02). The rows sit in their own scroll
75
81
  * region between the mark and the pinned bottom rows, so a list longer than the window scrolls
@@ -299,6 +305,13 @@ export default function NavRail({
299
305
  * ten rows each, which is where an accordion stops reading. */
300
306
  const [entered, setEntered] = useState(null)
301
307
  const snapOpenRef = useRef(null)
308
+ const slotRef = useRef(null)
309
+ const ctx = useRailSlot()
310
+ const setSlot = ctx?.setSlot
311
+ const setCtxOpen = ctx?.setRailOpen
312
+ const slotOpen = drawer || railOpen
313
+ useEffect(() => { setSlot?.(slotRef.current); return () => setSlot?.(null) }, [setSlot, hidden])
314
+ useEffect(() => { setCtxOpen?.(slotOpen) }, [setCtxOpen, slotOpen])
302
315
  /* the grab strip is not rendered in drawer mode, so the hook's ref stays null
303
316
  * and it no-ops on its own — no second argument needed */
304
317
  useGrabEdge(grabRef)
@@ -363,7 +376,7 @@ export default function NavRail({
363
376
  {/* THE LIST IS ITS OWN REGION (2026-10-02): it takes the room between the mark and the pinned
364
377
  * bottom rows — what the bare spacer used to hold — scrolls when the rows outrun the window,
365
378
  * 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()}>
379
+ <div style={ctx && slotOpen ? { flex: '0 1 auto' } : undefined} 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
380
  {section ? (
368
381
  <>
369
382
  <RailItem icon="arrow-left" path={section.path} label={section.label} currentPath="" onNavigate={() => setEntered(null)} iconComponent={iconComponent} railOpen={drawer || railOpen} />
@@ -371,6 +384,13 @@ export default function NavRail({
371
384
  </>
372
385
  ) : items.map(row)}
373
386
  </div>
387
+ {ctx && (
388
+ <div
389
+ ref={slotRef}
390
+ className="kol-shell-rail-slot w-full min-h-0 overflow-x-hidden"
391
+ style={{ flex: slotOpen ? '1 1 auto' : '0 0 0px', overflowY: slotOpen ? 'auto' : 'hidden' }}
392
+ />
393
+ )}
374
394
  {/* the rule runs the full rail width, out past the px-2 */}
375
395
  {bottomItems.length > 0 && <div className="self-stretch -mx-2 border-t border-fg-08" />}
376
396
  {bottomItems.map(row)}
package/src/index.js CHANGED
@@ -26,7 +26,7 @@ export { default as TabStrip } from './TabStrip.jsx'
26
26
  export { default as SettingsScaffold } from './SettingsScaffold.jsx'
27
27
  export { default as WalkthroughPanel } from './WalkthroughPanel.jsx'
28
28
  /* lives in kol-component since 0.228.0 — re-exported so every kol-shell import keeps resolving */
29
- export { ShortcutsOverlay } from '@kolkrabbi/kol-component'
29
+ export { ShortcutsOverlay, shortcutInScope } from '@kolkrabbi/kol-component'
30
30
  export { default as Logomark } from './Logomark.jsx'
31
31
  // the app tier, shipped once (ShellHomeSystem, kol-fxr 2026-08-27)
32
32
  export { default as CatalogPage } from './CatalogPage.jsx'
@@ -37,5 +37,8 @@ export { default as TouchDeviceOverlay, useTouchPrimary } from './TouchDeviceOve
37
37
  export { default as AppHub } from './AppHub.jsx'
38
38
  export { default as HubHome } from './HubHome.jsx'
39
39
  export { default as HubSettings } from './HubSettings.jsx'
40
+ export { default as HubDev } from './HubDev.jsx'
40
41
  // the Studio — the Hub + Home · Library · Create · Use · pages · Settings, the workstation (apps review §6c, 2026-09-29)
41
42
  export { default as AppStudio } from './AppStudio.jsx'
43
+ // a consumer slot in the opened rail — the page portals into it (NavRailConsumerSlot, kol-monitor 2026-10-15)
44
+ export { RailSlotProvider, useRailSlot } from './railSlot.jsx'
@@ -0,0 +1,28 @@
1
+ import { createContext, useContext, useMemo, useState } from 'react'
2
+
3
+ /**
4
+ * RailSlotProvider / useRailSlot — a consumer slot in NavRail's opened width (NavRailConsumerSlot,
5
+ * kol-monitor 2026-10-15). The rail renders in AppShell's own tree, outside the page's providers,
6
+ * so it cannot read page state and the page cannot render into it. The rail publishes its slot's
7
+ * DOM node here instead, and the page portals into it with its own state:
8
+ *
9
+ * <RailSlotProvider><AppStudio … /></RailSlotProvider>
10
+ * const rail = useRailSlot(); rail?.slot && createPortal(<Panel />, rail.slot)
11
+ *
12
+ * The provider goes ABOVE the shell so the rail and the page both see it. No provider → no slot,
13
+ * and the rail renders as it did. `railOpen` mirrors the rail's open state for the page.
14
+ *
15
+ * Monitor's shape (`src/hooks/useRailSlot.jsx`), shipped so its NavRail fork can retire.
16
+ */
17
+ const RailSlotContext = createContext(null)
18
+
19
+ export function useRailSlot() {
20
+ return useContext(RailSlotContext)
21
+ }
22
+
23
+ export function RailSlotProvider({ children }) {
24
+ const [slot, setSlot] = useState(null)
25
+ const [railOpen, setRailOpen] = useState(false)
26
+ const value = useMemo(() => ({ slot, setSlot, railOpen, setRailOpen }), [slot, railOpen])
27
+ return <RailSlotContext.Provider value={value}>{children}</RailSlotContext.Provider>
28
+ }