@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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. 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.24.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.133.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.97.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
- onNavigate?.(path)
180
+ navigate(path)
126
181
  }
127
182
  window.addEventListener('keydown', onKey)
128
183
  return () => window.removeEventListener('keydown', onKey)
129
- }, [navKeys, navPaths.join('\u0000'), onNavigate]) // eslint-disable-line react-hooks/exhaustive-deps
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
- onNavigate={onNavigate}
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
  )}