@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 +7 -7
- package/src/AppShell.jsx +48 -10
- package/src/NavRail.jsx +37 -59
- package/src/index.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-shell",
|
|
3
|
-
"version": "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.
|
|
16
|
+
"@kolkrabbi/kol-framework": ">=0.31.0",
|
|
17
17
|
"@kolkrabbi/kol-icons": ">=0.16.0",
|
|
18
|
-
"@kolkrabbi/kol-theme": ">=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-
|
|
24
|
-
"@kolkrabbi/kol-
|
|
25
|
-
"@kolkrabbi/kol-
|
|
26
|
-
"@kolkrabbi/kol-
|
|
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:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* THE RAIL IS A SIDENAV (RailSideNavPixelParity, kol-fxr 2026-08-28 — user:
|
|
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 }` —
|
|
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
|
-
|
|
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={{
|
|
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 {
|
|
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 —
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
45
|
-
|
|
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
|
-
<
|
|
49
|
-
{
|
|
50
|
-
|
|
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
|
-
</
|
|
58
|
-
)}
|
|
59
|
-
|
|
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
|
|
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.
|