@kolkrabbi/kol-shell 0.24.0 → 0.25.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 +3 -3
- package/src/AppShell.jsx +60 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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",
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"react-dom": "^18.3.0 || ^19.0.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@kolkrabbi/kol-component": "^0.
|
|
23
|
+
"@kolkrabbi/kol-component": "^0.135.0",
|
|
24
24
|
"@kolkrabbi/kol-icons": "^0.25.0",
|
|
25
25
|
"@kolkrabbi/kol-framework": "^0.35.0",
|
|
26
|
-
"@kolkrabbi/kol-theme": "^0.
|
|
26
|
+
"@kolkrabbi/kol-theme": "^0.99.0"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"src",
|
package/src/AppShell.jsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import NavRail from './NavRail.jsx'
|
|
3
3
|
import { NavHiddenContext } from './navHidden.js'
|
|
4
4
|
import TouchDeviceOverlay, { useTouchPrimary } from './TouchDeviceOverlay.jsx'
|
|
@@ -62,6 +62,14 @@ import TouchDeviceOverlay, { useTouchPrimary } from './TouchDeviceOverlay.jsx'
|
|
|
62
62
|
* content wrapper, so a page root that is not `PageShell` reads it too.
|
|
63
63
|
* Default none — unset renders exactly as before. A prop, not a token
|
|
64
64
|
* an app binds, because fxr's stylesheet is imports-only by rule.
|
|
65
|
+
* @param {string} props.settingsPath a destination the shell TOGGLES rather than navigates to
|
|
66
|
+
* (SettingsToggleGesture, user 2026-08-30): pressing the key or
|
|
67
|
+
* picking its rail row again returns you where you were, instead of
|
|
68
|
+
* stranding you on the page. Three repos had built this each for
|
|
69
|
+
* themselves. Unset = every destination behaves exactly as before.
|
|
70
|
+
* @param {string} props.settingsKey the key that toggles `settingsPath` — `','` in the apps that asked.
|
|
71
|
+
* Bare and with ⌥, ignored while typing in a field. Needs
|
|
72
|
+
* `settingsPath`; alone it does nothing.
|
|
65
73
|
*/
|
|
66
74
|
export default function AppShell({
|
|
67
75
|
items,
|
|
@@ -76,11 +84,58 @@ export default function AppShell({
|
|
|
76
84
|
appName,
|
|
77
85
|
pageWash,
|
|
78
86
|
navKeys = false,
|
|
87
|
+
settingsPath,
|
|
88
|
+
settingsKey,
|
|
79
89
|
children,
|
|
80
90
|
}) {
|
|
81
91
|
const [navHidden, setNavHidden] = useState(false)
|
|
82
92
|
const coarse = useTouchPrimary()
|
|
83
93
|
|
|
94
|
+
/* TOGGLING A DESTINATION (user 2026-08-30: "comma opens and closes the
|
|
95
|
+
* settings page, and clicking the icon in sidebar opens and clicking again
|
|
96
|
+
* closes"). The shell already owned two keyboard behaviours; this is a third
|
|
97
|
+
* of the same kind, and it was about to be written three times — fxr, mirror
|
|
98
|
+
* and monitor all render this page.
|
|
99
|
+
*
|
|
100
|
+
* The only new state is WHERE YOU CAME FROM. Navigation stays the consumer's
|
|
101
|
+
* (`onNavigate`); the shell just decides which path to hand back. `useRef`,
|
|
102
|
+
* not state: the return path must not re-render anything when it changes. */
|
|
103
|
+
const returnPath = useRef(null)
|
|
104
|
+
const toggleSettings = useCallback(() => {
|
|
105
|
+
if (!settingsPath) return
|
|
106
|
+
if (currentPath === settingsPath) {
|
|
107
|
+
/* nothing remembered (deep link straight onto /settings) → the mark, which
|
|
108
|
+
* is where the rail's first row goes anyway. Never a dead key. */
|
|
109
|
+
onNavigate?.(returnPath.current ?? '/')
|
|
110
|
+
returnPath.current = null
|
|
111
|
+
} else {
|
|
112
|
+
returnPath.current = currentPath
|
|
113
|
+
onNavigate?.(settingsPath)
|
|
114
|
+
}
|
|
115
|
+
}, [settingsPath, currentPath, onNavigate])
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!settingsKey || !settingsPath) return undefined
|
|
119
|
+
const onKey = (e) => {
|
|
120
|
+
if (e.key !== settingsKey || e.metaKey || e.ctrlKey) return
|
|
121
|
+
const t = e.target
|
|
122
|
+
if (t?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t?.tagName)) return
|
|
123
|
+
e.preventDefault()
|
|
124
|
+
toggleSettings()
|
|
125
|
+
}
|
|
126
|
+
window.addEventListener('keydown', onKey)
|
|
127
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
128
|
+
}, [settingsKey, settingsPath, toggleSettings])
|
|
129
|
+
|
|
130
|
+
/* the rail row for that path toggles too — one gesture, two ways to reach it */
|
|
131
|
+
const navigate = useCallback(
|
|
132
|
+
(path, ...rest) => {
|
|
133
|
+
if (settingsPath && path === settingsPath) return toggleSettings()
|
|
134
|
+
return onNavigate?.(path, ...rest)
|
|
135
|
+
},
|
|
136
|
+
[settingsPath, toggleSettings, onNavigate],
|
|
137
|
+
)
|
|
138
|
+
|
|
84
139
|
/* the rail comes back on every route change */
|
|
85
140
|
useEffect(() => { setNavHidden(false) }, [currentPath])
|
|
86
141
|
/* one key toggles the rail — never while typing in a field */
|
|
@@ -122,11 +177,11 @@ export default function AppShell({
|
|
|
122
177
|
const path = navPaths[Number(m[1]) - 1]
|
|
123
178
|
if (!path) return
|
|
124
179
|
e.preventDefault()
|
|
125
|
-
|
|
180
|
+
navigate(path)
|
|
126
181
|
}
|
|
127
182
|
window.addEventListener('keydown', onKey)
|
|
128
183
|
return () => window.removeEventListener('keydown', onKey)
|
|
129
|
-
}, [navKeys, navPaths.join('\u0000'),
|
|
184
|
+
}, [navKeys, navPaths.join('\u0000'), navigate]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
130
185
|
|
|
131
186
|
let wantsDesktop = false
|
|
132
187
|
try { wantsDesktop = typeof localStorage !== 'undefined' && localStorage.getItem('kol-desktop') === '1' } catch { /* storage blocked */ }
|
|
@@ -150,7 +205,8 @@ export default function AppShell({
|
|
|
150
205
|
bottomItems={bottomItems}
|
|
151
206
|
logomark={logomark}
|
|
152
207
|
currentPath={currentPath}
|
|
153
|
-
|
|
208
|
+
/* the rail routes through `navigate`, so its settings row toggles like the key */
|
|
209
|
+
onNavigate={navigate}
|
|
154
210
|
iconComponent={iconComponent}
|
|
155
211
|
/>
|
|
156
212
|
)}
|