@kolkrabbi/kol-shell 0.11.0 → 0.13.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.11.0",
3
+ "version": "0.13.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",
@@ -13,17 +13,17 @@
13
13
  },
14
14
  "peerDependencies": {
15
15
  "@kolkrabbi/kol-component": ">=0.108.0",
16
- "@kolkrabbi/kol-framework": ">=0.20.0",
16
+ "@kolkrabbi/kol-framework": ">=0.31.0",
17
17
  "@kolkrabbi/kol-icons": ">=0.16.0",
18
- "@kolkrabbi/kol-theme": ">=0.68.0",
18
+ "@kolkrabbi/kol-theme": ">=0.80.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-icons": "^0.22.0",
24
- "@kolkrabbi/kol-component": "^0.110.0",
25
- "@kolkrabbi/kol-theme": "^0.72.0",
26
- "@kolkrabbi/kol-framework": "^0.28.0"
23
+ "@kolkrabbi/kol-component": "^0.118.3",
24
+ "@kolkrabbi/kol-framework": "^0.31.0",
25
+ "@kolkrabbi/kol-icons": "^0.24.0",
26
+ "@kolkrabbi/kol-theme": "^0.80.0"
27
27
  },
28
28
  "files": [
29
29
  "src",
package/src/AppShell.jsx CHANGED
@@ -4,17 +4,25 @@ import { NavHiddenContext } from './navHidden.js'
4
4
  import TouchDeviceOverlay, { useTouchPrimary } from './TouchDeviceOverlay.jsx'
5
5
 
6
6
  /**
7
- * AppShell — layout root: fixed NavRail + content offset by the rail width.
8
- * No header, no footer. Router-agnostic: render your router's element (e.g.
9
- * `<Outlet/>`) as children and wire `currentPath`/`onNavigate` from your
10
- * router at the call site.
7
+ * AppShell — layout root: the rail + the content column. No header, no
8
+ * footer. Router-agnostic: render your router's element (e.g. `<Outlet/>`) as
9
+ * children and wire `currentPath`/`onNavigate` from your router at the call
10
+ * site.
11
11
  *
12
- * Rail width is `--kol-shell-rail-width` (kol-theme) read by both the rail
13
- * and the offset so they can never disagree.
12
+ * THE RAIL IS A SIDENAV (RailSideNavPixelParity, kol-fxr 2026-08-28user:
13
+ * "same component both states super nice"): since 0.13.0 the root is the
14
+ * brand grid, `.kol-brand-layout` (kol-framework.css — `--kol-sidenav-w` ×
15
+ * `minmax(0, 1fr)`, the collapsed column on the `data-sidenav` stamp, the
16
+ * ≤1024 forced rail and the ≤767 drawer), and `NavRail` mounts kol-framework's
17
+ * `SideNav` collapsed in it. The grab edge opens it to the brand sidebar and
18
+ * the drag is the transition; the content column follows the grid, so the old
19
+ * fixed rail + `margin-left` offset is gone. `--kol-shell-rail-width`
20
+ * (kol-theme ≥0.80.0) is LIVE — the sidenav's width in its current state —
21
+ * for consumers that offset a fixed element by it; hidden rail = `0px`.
14
22
  *
15
23
  * @param {Array} props.items nav items `{ icon, path, label }`
16
24
  * @param {Array} props.bottomItems items pinned below the theme toggle
17
- * @param {Object} props.logomark `{ svgUrl, title }` — top mark, navigates to '/'
25
+ * @param {Object} props.logomark `{ svgUrl, title }` — the rail's footer mark, navigates to '/'
18
26
  * @param {string} props.currentPath the router's current pathname
19
27
  * @param {Function} props.onNavigate `(path) => void`
20
28
  * @param {ElementType} props.iconComponent icon renderer seam (see Button)
@@ -26,6 +34,13 @@ import TouchDeviceOverlay, { useTouchPrimary } from './TouchDeviceOverlay.jsx'
26
34
  * localStorage `kol-desktop` is '1' (fxr's gate); `overlay` keeps the shell and
27
35
  * mounts TouchDeviceOverlay once (monitor's)
28
36
  * @param {string} props.appName TouchDeviceOverlay's subject
37
+ * @param {boolean} props.navKeys Option+1…9 navigates to `items[n-1].path` through `onNavigate` — the
38
+ * rail-order counterpart of `railToggleKey` (AppShellNavKeys, kol-monitor
39
+ * 2026-08-28 — user: "make command or alt 1234 go from home 1 2 3 4 in the
40
+ * sidenav without clicking"). Option, not Command: ⌘1–9 is the browser's
41
+ * tab switch. Matched on `e.code` (`Digit1`…) because Opt+digit yields
42
+ * `¡ ™ £ ¢` as `e.key` on macOS; ignored while typing in a field; only
43
+ * digits with an item; `preventDefault` on a match. Default off.
29
44
  * @param {string} props.pageWash a CSS colour painted by `PageShell` OVER the shell's primary back
30
45
  * (ShellPageWash, kol-monitor 2026-08-27 — user: "the back of the back
31
46
  * should be primary — then you can just add transparent on top to step
@@ -47,6 +62,7 @@ export default function AppShell({
47
62
  touch = 'shell',
48
63
  appName,
49
64
  pageWash,
65
+ navKeys = false,
50
66
  children,
51
67
  }) {
52
68
  const [navHidden, setNavHidden] = useState(false)
@@ -68,6 +84,23 @@ export default function AppShell({
68
84
  return () => window.removeEventListener('keydown', onKey)
69
85
  }, [railToggleKey])
70
86
 
87
+ /* Option+digit → the rail item in that position (never while typing) */
88
+ useEffect(() => {
89
+ if (!navKeys) return undefined
90
+ const onKey = (e) => {
91
+ const m = /^Digit([1-9])$/.exec(e.code)
92
+ if (!m || !e.altKey || e.metaKey || e.ctrlKey) return
93
+ const t = e.target
94
+ if (t?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t?.tagName)) return
95
+ const item = items?.[Number(m[1]) - 1]
96
+ if (!item?.path) return
97
+ e.preventDefault()
98
+ onNavigate?.(item.path)
99
+ }
100
+ window.addEventListener('keydown', onKey)
101
+ return () => window.removeEventListener('keydown', onKey)
102
+ }, [navKeys, items, onNavigate])
103
+
71
104
  let wantsDesktop = false
72
105
  try { wantsDesktop = typeof localStorage !== 'undefined' && localStorage.getItem('kol-desktop') === '1' } catch { /* storage blocked */ }
73
106
  /* the wash variable rides the bare wrapper too (custom properties inherit
@@ -76,8 +109,13 @@ export default function AppShell({
76
109
 
77
110
  return (
78
111
  <NavHiddenContext.Provider value={{ navHidden, setNavHidden }}>
79
- {/* `kol-app-shell` = the app tier: neutral ::selection (kol-theme) */}
80
- <div className="kol-app-shell contents">
112
+ {/* `kol-app-shell` = the app tier: neutral ::selection (kol-theme).
113
+ * `kol-brand-layout` = the sidenav grid; a hidden rail collapses the
114
+ * grid to the content column and zeroes the live width token. */}
115
+ <div
116
+ className="kol-app-shell kol-brand-layout min-h-dvh bg-surface-primary"
117
+ style={navHidden ? { gridTemplateColumns: 'minmax(0, 1fr)', '--kol-shell-rail-width': '0px' } : undefined}
118
+ >
81
119
  {touch === 'overlay' && <TouchDeviceOverlay appName={appName} />}
82
120
  {!navHidden && (
83
121
  <NavRail
@@ -92,7 +130,7 @@ export default function AppShell({
92
130
  )}
93
131
  {/* THE BACK OF THE BACK — surface-primary, always, in every app; the
94
132
  * page paints its wash over it (ShellPageWash). */}
95
- <div className="bg-surface-primary" style={{ marginLeft: navHidden ? 0 : 'var(--kol-shell-rail-width)', '--kol-shell-page-wash': pageWash }}>
133
+ <div className="bg-surface-primary min-w-0" style={{ '--kol-shell-page-wash': pageWash }}>
96
134
  {children}
97
135
  </div>
98
136
  </div>
package/src/NavRail.jsx CHANGED
@@ -1,35 +1,23 @@
1
- import { Button } from '@kolkrabbi/kol-component'
2
- import { ThemeToggle } from '@kolkrabbi/kol-framework'
1
+ import { SideNav } from '@kolkrabbi/kol-framework'
3
2
  import Logomark from './Logomark.jsx'
4
3
 
5
4
  /**
6
- * NavRail — the fixed 48px icon rail. Geometry/stacking live in kol-theme's
7
- * `.kol-shell-rail`; surface/border ride the theme utility classes.
5
+ * NavRail — AppShell's rail, which IS kol-framework's `SideNav`
6
+ * (RailSideNavPixelParity, kol-fxr 2026-08-28 user, on the brand sidebar's
7
+ * drag: "same component both states super nice"). Until 0.13.0 this was its
8
+ * own fixed 48px icon rail, and the first app to run it beside a collapsed
9
+ * SideNav on adjacent routes measured every icon moving on the route change
10
+ * (48 vs 56 wide, 20 vs 16px glyphs, 40 vs 38 pitch, first glyph at y 76 vs
11
+ * 26). There is no second component now: this file maps the shell's flat
12
+ * `{ icon, path, label }` items onto the sidenav's tree and mounts it
13
+ * collapsed — the pill-marked grab edge opens it to the brand sidebar, the
14
+ * drag is the transition, and it stays a rail while it navigates
15
+ * (`expandOnSelect={false}`).
8
16
  *
9
- * Items are flat `{ icon, path, label }` deliberately NOT kol-framework
10
- * SideNav's two-level navTree; different component, not a variant.
11
- *
12
- * Active state (user ruling 2026-08-12, carried natively in kol-theme):
13
- * ink `--kol-oq-96` in every state, hover = the `--kol-oq-04` wash, active
14
- * route = that wash held on via `aria-current="page"` — never
15
- * `selected`/`pressed`; navigation is location, not a toggled tool.
17
+ * The bottom, specified once: theme slot `bottomItems` (pinned rows)
18
+ * footer. The `logomark` moved from the top of the rail to the footer — the
19
+ * brand sidebar's mark sits there, and the first glyph's y is the sidenav's.
16
20
  */
17
-
18
- function RailItem({ icon, path, label, active, onNavigate, iconComponent }) {
19
- return (
20
- <Button
21
- iconOnly={icon}
22
- iconSize={20}
23
- variant="nav"
24
- size="md"
25
- aria-current={active ? 'page' : undefined}
26
- onClick={() => onNavigate?.(path)}
27
- title={label}
28
- iconComponent={iconComponent}
29
- />
30
- )
31
- }
32
-
33
21
  export default function NavRail({
34
22
  items = [],
35
23
  bottomItems = [],
@@ -41,42 +29,32 @@ export default function NavRail({
41
29
  hidden = false,
42
30
  }) {
43
31
  if (hidden) return null
44
- const isActive = (path) =>
45
- path === '/' ? currentPath === '/' : currentPath.startsWith(path)
46
-
32
+ const node = (item) => ({ id: item.path, label: item.label, icon: item.icon, to: item.path })
33
+ /* the shell's route match: '/' exact, anything else by prefix */
34
+ const isActive = (to) => (to === '/' ? currentPath === '/' : currentPath.startsWith(to))
47
35
  return (
48
- <div className="kol-shell-rail bg-surface-tertiary border-r border-fg-04">
49
- {logomark && (
50
- <div
36
+ <SideNav
37
+ navTree={items.map(node)}
38
+ bottomItems={bottomItems.map(node)}
39
+ currentPath={currentPath}
40
+ isActive={isActive}
41
+ onNavigate={(e, to) => onNavigate?.(to)}
42
+ iconComponent={iconComponent}
43
+ themeToggle={themeToggle}
44
+ expandOnSelect={false}
45
+ defaultCollapsed
46
+ hairline
47
+ footer={logomark ? (
48
+ <button
49
+ type="button"
50
+ className="inline-flex items-center bg-transparent border-0 p-0 cursor-pointer text-body hover:text-emphasis"
51
51
  onClick={() => onNavigate?.('/')}
52
- className="text-oq-96"
53
- style={{ cursor: 'pointer', marginBottom: 16, paddingTop: 4 }}
54
52
  title={logomark.title}
53
+ aria-label={logomark.title}
55
54
  >
56
55
  <Logomark svgUrl={logomark.svgUrl} size={20} />
57
- </div>
58
- )}
59
- {items.map((item) => (
60
- <RailItem
61
- key={item.path}
62
- {...item}
63
- active={isActive(item.path)}
64
- onNavigate={onNavigate}
65
- iconComponent={iconComponent}
66
- />
67
- ))}
68
- <div style={{ flex: 1 }} />
69
- {themeToggle && <ThemeToggle label={false} style={{ color: 'var(--kol-oq-96)' }} />}
70
- {bottomItems.map((item) => (
71
- <RailItem
72
- key={item.path}
73
- {...item}
74
- active={isActive(item.path)}
75
- onNavigate={onNavigate}
76
- iconComponent={iconComponent}
77
- />
78
- ))}
79
- <div style={{ height: 8 }} />
80
- </div>
56
+ </button>
57
+ ) : false}
58
+ />
81
59
  )
82
60
  }
package/src/index.js CHANGED
@@ -4,7 +4,8 @@
4
4
  * Lifted 2026-08-14 from the hand-copied twins in kol-monitor ("Monitor") and
5
5
  * kol-mirror ("Hall of Mirrors") — AppShellSet lobby brief. App chrome, not
6
6
  * site chrome: kol-framework's SideNav/footer/heroes are the site register;
7
- * this is the fixed 48px rail + page scaffolds an application is built from.
7
+ * this is the rail (a collapsed kol-framework SideNav since 0.13.0
8
+ * RailSideNavPixelParity) + the page scaffolds an application is built from.
8
9
  *
9
10
  * Router-agnostic: no react-router dependency — pass `currentPath` +
10
11
  * `onNavigate` and render your router's element as children.