@kolkrabbi/kol-shell 0.25.0 → 0.27.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.25.0",
3
+ "version": "0.27.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.135.0",
23
+ "@kolkrabbi/kol-component": "^0.136.0",
24
24
  "@kolkrabbi/kol-icons": "^0.25.0",
25
25
  "@kolkrabbi/kol-framework": "^0.35.0",
26
- "@kolkrabbi/kol-theme": "^0.99.0"
26
+ "@kolkrabbi/kol-theme": "^0.106.0"
27
27
  },
28
28
  "files": [
29
29
  "src",
package/src/AppShell.jsx CHANGED
@@ -1,6 +1,7 @@
1
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
  /**
@@ -71,6 +72,14 @@ import TouchDeviceOverlay, { useTouchPrimary } from './TouchDeviceOverlay.jsx'
71
72
  * Bare and with ⌥, ignored while typing in a field. Needs
72
73
  * `settingsPath`; alone it does nothing.
73
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
+
74
83
  export default function AppShell({
75
84
  items,
76
85
  bottomItems,
@@ -117,7 +126,15 @@ export default function AppShell({
117
126
  useEffect(() => {
118
127
  if (!settingsKey || !settingsPath) return undefined
119
128
  const onKey = (e) => {
120
- if (e.key !== settingsKey || e.metaKey || e.ctrlKey) return
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
121
138
  const t = e.target
122
139
  if (t?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t?.tagName)) return
123
140
  e.preventDefault()
@@ -191,6 +208,7 @@ export default function AppShell({
191
208
 
192
209
  return (
193
210
  <NavHiddenContext.Provider value={{ navHidden, setNavHidden }}>
211
+ <SettingsToggleContext.Provider value={toggleSettings}>
194
212
  {/* `kol-app-shell` = the app tier: neutral ::selection (kol-theme).
195
213
  * A hidden rail zeroes the live width token, so the content's own
196
214
  * margin closes with it — one variable, both sides. */}
@@ -218,6 +236,7 @@ export default function AppShell({
218
236
  {children}
219
237
  </div>
220
238
  </div>
239
+ </SettingsToggleContext.Provider>
221
240
  </NavHiddenContext.Provider>
222
241
  )
223
242
  }
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'react'
2
- import { ContentFilters } from '@kolkrabbi/kol-component'
2
+ import { ContentFilters, IconFrame } from '@kolkrabbi/kol-component'
3
3
  import PageShell from './PageShell.jsx'
4
4
  import PageHeader from './PageHeader.jsx'
5
5
 
@@ -54,6 +54,13 @@ import PageHeader from './PageHeader.jsx'
54
54
  * A FUNCTION receives `(tab, setTab)` — for a control that has to
55
55
  * move the page, e.g. an icon pair that jumps back to Settings
56
56
  * @param {string} props.tone forwarded to ContentFilters (`sunken` is fxr's approved page)
57
+ * @param {ReactNode} props.picker the app's own picker for the masthead cluster —
58
+ * fxr opens a chrome, kol-r2b2 a bucket. Optional
59
+ * @param {ReactNode} props.themeToggle the app's ThemeToggle node. A NODE, not rendered
60
+ * here, because it lives in kol-framework and shell
61
+ * dropped that peer in 0.16.0
62
+ * @param {Function} props.onOpenSettings the gear's handler — opens the app's settings
63
+ * drawer. The scaffold draws the control
57
64
  * @param {Object} props.filtersProps escape hatch — anything else the organism takes
58
65
  */
59
66
  export default function SettingsScaffold({
@@ -67,6 +74,9 @@ export default function SettingsScaffold({
67
74
  searchKeys,
68
75
  trailingActions,
69
76
  tone = 'sunken',
77
+ picker,
78
+ themeToggle,
79
+ onOpenSettings,
70
80
  filtersProps,
71
81
  }) {
72
82
  const [tab, setTab] = useState(defaultTab ?? tabs[0]?.value)
@@ -85,13 +95,50 @@ export default function SettingsScaffold({
85
95
  const viewTabs = tabs.filter((t) => t.row !== 'layout')
86
96
  const layoutTabs = tabs.filter((t) => t.row === 'layout')
87
97
 
98
+ /* THE MASTHEAD CLUSTER — picker · theme toggle · gear, in that order, on the
99
+ * subtitle's baseline (user ruling 2026-08-30, off kol-fxr's approved page;
100
+ * kol-r2b2's row 1 is the same shape). It was a raw `header.actions` slot, so
101
+ * fxr and r2b2 each hand-built it and kol-mirror and kol-monitor passed
102
+ * NOTHING — which is the whole of why the three settings pages did not match.
103
+ * Order, gap and tone are the scaffold's now; only the picker's contents are
104
+ * the app's.
105
+ *
106
+ * `themeToggle` is a NODE rather than drawn here: it lives in kol-framework,
107
+ * and shell dropped that peer in 0.16.0. The gear is `IconFrame`, which is a
108
+ * peer, so the DS rules its glyph, tone and size.
109
+ *
110
+ * Pass none of the three and no cluster renders — mirror and monitor are
111
+ * untouched until they opt in. An explicit `header.actions` still wins. */
112
+ const cluster = picker || themeToggle || onOpenSettings ? (
113
+ <div className="flex items-center gap-2">
114
+ {picker}
115
+ {themeToggle}
116
+ {onOpenSettings && (
117
+ <IconFrame
118
+ name="settings-01"
119
+ variant="primary"
120
+ tone={tone}
121
+ size="sm"
122
+ onClick={onOpenSettings}
123
+ title="Display settings"
124
+ aria-label="Display settings"
125
+ />
126
+ )}
127
+ </div>
128
+ ) : null
129
+
88
130
  return (
89
131
  <PageShell mode="fixed">
90
132
  {/* `header` is spread onto the PageHeader (PageHeaderMonoTitle addendum,
91
133
  * kol-fxr 2026-08-27): a consumer could not reach this title at all —
92
134
  * `voice="mono"`, `size`, `titleClass`, `eyebrow` all pass through.
93
135
  * `actions` is where the drawer opener goes. */}
94
- <PageHeader title={active?.title} subtitle={active?.subtitle} {...header} />
136
+ <PageHeader
137
+ title={active?.title}
138
+ subtitle={active?.subtitle}
139
+ actions={cluster}
140
+ {...header}
141
+ />
95
142
  <ContentFilters
96
143
  tone={tone}
97
144
  title={title}
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) ?? (() => {})