@kolkrabbi/kol-shell 0.24.0 → 0.26.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 +4 -4
- package/src/AppShell.jsx +79 -4
- package/src/index.js +1 -0
- package/src/settingsToggle.js +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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.
|
|
24
|
-
"@kolkrabbi/kol-icons": "^0.25.0",
|
|
23
|
+
"@kolkrabbi/kol-component": "^0.136.0",
|
|
25
24
|
"@kolkrabbi/kol-framework": "^0.35.0",
|
|
26
|
-
"@kolkrabbi/kol-
|
|
25
|
+
"@kolkrabbi/kol-icons": "^0.25.0",
|
|
26
|
+
"@kolkrabbi/kol-theme": "^0.100.0"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"src",
|
package/src/AppShell.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
|
+
import { SettingsToggleContext } from './settingsToggle.js'
|
|
4
5
|
import TouchDeviceOverlay, { useTouchPrimary } from './TouchDeviceOverlay.jsx'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -62,7 +63,23 @@ import TouchDeviceOverlay, { useTouchPrimary } from './TouchDeviceOverlay.jsx'
|
|
|
62
63
|
* content wrapper, so a page root that is not `PageShell` reads it too.
|
|
63
64
|
* Default none — unset renders exactly as before. A prop, not a token
|
|
64
65
|
* an app binds, because fxr's stylesheet is imports-only by rule.
|
|
66
|
+
* @param {string} props.settingsPath a destination the shell TOGGLES rather than navigates to
|
|
67
|
+
* (SettingsToggleGesture, user 2026-08-30): pressing the key or
|
|
68
|
+
* picking its rail row again returns you where you were, instead of
|
|
69
|
+
* stranding you on the page. Three repos had built this each for
|
|
70
|
+
* themselves. Unset = every destination behaves exactly as before.
|
|
71
|
+
* @param {string} props.settingsKey the key that toggles `settingsPath` — `','` in the apps that asked.
|
|
72
|
+
* Bare and with ⌥, ignored while typing in a field. Needs
|
|
73
|
+
* `settingsPath`; alone it does nothing.
|
|
65
74
|
*/
|
|
75
|
+
/* the physical-key name for a bound character. Only the keys people actually
|
|
76
|
+
* bind — a full layout table would be a lie about coverage. */
|
|
77
|
+
const CODE_FOR_KEY = {
|
|
78
|
+
',': 'Comma', '.': 'Period', '/': 'Slash', ';': 'Semicolon', "'": 'Quote',
|
|
79
|
+
'[': 'BracketLeft', ']': 'BracketRight', '\\': 'Backslash', '`': 'Backquote',
|
|
80
|
+
'-': 'Minus', '=': 'Equal',
|
|
81
|
+
}
|
|
82
|
+
|
|
66
83
|
export default function AppShell({
|
|
67
84
|
items,
|
|
68
85
|
bottomItems,
|
|
@@ -76,11 +93,66 @@ export default function AppShell({
|
|
|
76
93
|
appName,
|
|
77
94
|
pageWash,
|
|
78
95
|
navKeys = false,
|
|
96
|
+
settingsPath,
|
|
97
|
+
settingsKey,
|
|
79
98
|
children,
|
|
80
99
|
}) {
|
|
81
100
|
const [navHidden, setNavHidden] = useState(false)
|
|
82
101
|
const coarse = useTouchPrimary()
|
|
83
102
|
|
|
103
|
+
/* TOGGLING A DESTINATION (user 2026-08-30: "comma opens and closes the
|
|
104
|
+
* settings page, and clicking the icon in sidebar opens and clicking again
|
|
105
|
+
* closes"). The shell already owned two keyboard behaviours; this is a third
|
|
106
|
+
* of the same kind, and it was about to be written three times — fxr, mirror
|
|
107
|
+
* and monitor all render this page.
|
|
108
|
+
*
|
|
109
|
+
* The only new state is WHERE YOU CAME FROM. Navigation stays the consumer's
|
|
110
|
+
* (`onNavigate`); the shell just decides which path to hand back. `useRef`,
|
|
111
|
+
* not state: the return path must not re-render anything when it changes. */
|
|
112
|
+
const returnPath = useRef(null)
|
|
113
|
+
const toggleSettings = useCallback(() => {
|
|
114
|
+
if (!settingsPath) return
|
|
115
|
+
if (currentPath === settingsPath) {
|
|
116
|
+
/* nothing remembered (deep link straight onto /settings) → the mark, which
|
|
117
|
+
* is where the rail's first row goes anyway. Never a dead key. */
|
|
118
|
+
onNavigate?.(returnPath.current ?? '/')
|
|
119
|
+
returnPath.current = null
|
|
120
|
+
} else {
|
|
121
|
+
returnPath.current = currentPath
|
|
122
|
+
onNavigate?.(settingsPath)
|
|
123
|
+
}
|
|
124
|
+
}, [settingsPath, currentPath, onNavigate])
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!settingsKey || !settingsPath) return undefined
|
|
128
|
+
const onKey = (e) => {
|
|
129
|
+
/* MATCH THE PHYSICAL KEY (SettingsToggleGestureConsumerSeam, kol-fxr
|
|
130
|
+
* 2026-08-30). Option rewrites `e.key` on macOS — **the chord for `,` is
|
|
131
|
+
* `≤`** — so an `e.key` comparison silently drops it while the bare key
|
|
132
|
+
* works, which is the worst way to fail. `e.code` is the same physical key
|
|
133
|
+
* either way; it is why the Option-digit handler above reads `Digit1…`
|
|
134
|
+
* rather than `¡ ™ £`. `e.key` still matches too, so a character with no
|
|
135
|
+
* entry in the table below is unaffected. */
|
|
136
|
+
const wanted = CODE_FOR_KEY[settingsKey]
|
|
137
|
+
if (!(e.key === settingsKey || (wanted && e.code === wanted)) || e.metaKey || e.ctrlKey) return
|
|
138
|
+
const t = e.target
|
|
139
|
+
if (t?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t?.tagName)) return
|
|
140
|
+
e.preventDefault()
|
|
141
|
+
toggleSettings()
|
|
142
|
+
}
|
|
143
|
+
window.addEventListener('keydown', onKey)
|
|
144
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
145
|
+
}, [settingsKey, settingsPath, toggleSettings])
|
|
146
|
+
|
|
147
|
+
/* the rail row for that path toggles too — one gesture, two ways to reach it */
|
|
148
|
+
const navigate = useCallback(
|
|
149
|
+
(path, ...rest) => {
|
|
150
|
+
if (settingsPath && path === settingsPath) return toggleSettings()
|
|
151
|
+
return onNavigate?.(path, ...rest)
|
|
152
|
+
},
|
|
153
|
+
[settingsPath, toggleSettings, onNavigate],
|
|
154
|
+
)
|
|
155
|
+
|
|
84
156
|
/* the rail comes back on every route change */
|
|
85
157
|
useEffect(() => { setNavHidden(false) }, [currentPath])
|
|
86
158
|
/* one key toggles the rail — never while typing in a field */
|
|
@@ -122,11 +194,11 @@ export default function AppShell({
|
|
|
122
194
|
const path = navPaths[Number(m[1]) - 1]
|
|
123
195
|
if (!path) return
|
|
124
196
|
e.preventDefault()
|
|
125
|
-
|
|
197
|
+
navigate(path)
|
|
126
198
|
}
|
|
127
199
|
window.addEventListener('keydown', onKey)
|
|
128
200
|
return () => window.removeEventListener('keydown', onKey)
|
|
129
|
-
}, [navKeys, navPaths.join('\u0000'),
|
|
201
|
+
}, [navKeys, navPaths.join('\u0000'), navigate]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
130
202
|
|
|
131
203
|
let wantsDesktop = false
|
|
132
204
|
try { wantsDesktop = typeof localStorage !== 'undefined' && localStorage.getItem('kol-desktop') === '1' } catch { /* storage blocked */ }
|
|
@@ -136,6 +208,7 @@ export default function AppShell({
|
|
|
136
208
|
|
|
137
209
|
return (
|
|
138
210
|
<NavHiddenContext.Provider value={{ navHidden, setNavHidden }}>
|
|
211
|
+
<SettingsToggleContext.Provider value={toggleSettings}>
|
|
139
212
|
{/* `kol-app-shell` = the app tier: neutral ::selection (kol-theme).
|
|
140
213
|
* A hidden rail zeroes the live width token, so the content's own
|
|
141
214
|
* margin closes with it — one variable, both sides. */}
|
|
@@ -150,7 +223,8 @@ export default function AppShell({
|
|
|
150
223
|
bottomItems={bottomItems}
|
|
151
224
|
logomark={logomark}
|
|
152
225
|
currentPath={currentPath}
|
|
153
|
-
|
|
226
|
+
/* the rail routes through `navigate`, so its settings row toggles like the key */
|
|
227
|
+
onNavigate={navigate}
|
|
154
228
|
iconComponent={iconComponent}
|
|
155
229
|
/>
|
|
156
230
|
)}
|
|
@@ -162,6 +236,7 @@ export default function AppShell({
|
|
|
162
236
|
{children}
|
|
163
237
|
</div>
|
|
164
238
|
</div>
|
|
239
|
+
</SettingsToggleContext.Provider>
|
|
165
240
|
</NavHiddenContext.Provider>
|
|
166
241
|
)
|
|
167
242
|
}
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export { default as AppShell } from './AppShell.jsx'
|
|
14
14
|
export { NavHiddenContext, useNavHidden } from './navHidden.js'
|
|
15
|
+
export { SettingsToggleContext, useSettingsToggle } from './settingsToggle.js'
|
|
15
16
|
export { default as NavRail } from './NavRail.jsx'
|
|
16
17
|
export { default as PageShell, PageBleed } from './PageShell.jsx'
|
|
17
18
|
export { default as PageHeader } from './PageHeader.jsx'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
/* Settings-toggle context — own file so AppShell exports only components
|
|
4
|
+
* (the react-refresh constraint `navHidden.js` was split out for).
|
|
5
|
+
*
|
|
6
|
+
* WHY A HOOK AND NOT JUST THE PROPS (SettingsToggleGestureConsumerSeam, kol-fxr
|
|
7
|
+
* 2026-08-30). `settingsKey` navigates unconditionally, and that is wrong for an
|
|
8
|
+
* app whose settings are sometimes a DRAWER: on kol-fxr's `/editor`, `,` opens
|
|
9
|
+
* the panel in place and must not leave the canvas. Its rule is "open whatever
|
|
10
|
+
* settings is available", which only the app can know.
|
|
11
|
+
*
|
|
12
|
+
* So the shell keeps what is genuinely shared — the return path, and the rail
|
|
13
|
+
* row toggling — and hands out the toggle for a consumer that owns the gesture.
|
|
14
|
+
* Without this, fxr had to keep its whole local copy (a `lastPage` ref and a
|
|
15
|
+
* branch in `onNavigate`) to keep one line of app-specific routing, which is
|
|
16
|
+
* the duplication `settingsPath` exists to end.
|
|
17
|
+
*
|
|
18
|
+
* No-op when `settingsPath` is unset, and safe outside an AppShell — a hook
|
|
19
|
+
* that throws on a missing provider would make it unusable in exactly the
|
|
20
|
+
* conditional places it is for. */
|
|
21
|
+
export const SettingsToggleContext = createContext(null)
|
|
22
|
+
export const useSettingsToggle = () => useContext(SettingsToggleContext) ?? (() => {})
|