@kolkrabbi/kol-shell 0.56.0 → 0.57.1
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/AppHub.jsx +101 -0
- package/src/AppShell.jsx +4 -1
- package/src/CatalogPage.jsx +2 -2
- package/src/HubHome.jsx +83 -0
- package/src/HubSettings.jsx +207 -0
- package/src/NavRail.jsx +11 -6
- package/src/SettingsScaffold.jsx +6 -3
- package/src/ShortcutsOverlay.jsx +5 -2
- package/src/WalkthroughPanel.jsx +17 -2
- package/src/index.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.1",
|
|
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.226.0",
|
|
24
|
+
"@kolkrabbi/kol-icons": "^0.28.0",
|
|
24
25
|
"@kolkrabbi/kol-framework": "^0.44.0",
|
|
25
|
-
"@kolkrabbi/kol-
|
|
26
|
-
"@kolkrabbi/kol-theme": "^0.142.0"
|
|
26
|
+
"@kolkrabbi/kol-theme": "^0.153.0"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"src",
|
package/src/AppHub.jsx
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import AppShell from './AppShell.jsx'
|
|
3
|
+
import HubHome from './HubHome.jsx'
|
|
4
|
+
import HubSettings from './HubSettings.jsx'
|
|
5
|
+
import ShortcutsOverlay from './ShortcutsOverlay.jsx'
|
|
6
|
+
|
|
7
|
+
/* taxonomy-ok: organism — nests AppShell / HubHome / HubSettings / ShortcutsOverlay (relative) */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* AppHub — Shell + Hub in one call; the app passes its Tool as children (the app anatomy,
|
|
11
|
+
* `docs/documentation/04-compositions/16-app-anatomy.md`, 2026-09-26).
|
|
12
|
+
*
|
|
13
|
+
* monitor, mirror and fxr each assembled this by hand: the same AppShell props, the same
|
|
14
|
+
* Home on CatalogPage, the same Settings on SettingsScaffold, a local ⌥-digit handler
|
|
15
|
+
* apiece and a local shortcuts-sheet key — so their small things drifted. The Hub is
|
|
16
|
+
* those decisions made once. An app describes itself and supplies its tool.
|
|
17
|
+
*
|
|
18
|
+
* ROUTES: `homePath` (default `/`) is Home, `settingsPath` is Settings, every other path renders
|
|
19
|
+
* `children` — the tool. A tool that should be what LOADS moves Home off `/` (media-shell, user
|
|
20
|
+
* 2026-09-26: "the point of this is not to bury media, the column browser is what we want loaded"):
|
|
21
|
+
* `homePath="/library"`, a rail row for it, and the mark still goes to `/` — the tool.
|
|
22
|
+
* Router-agnostic like AppShell: pass `currentPath` + `onNavigate`, and your router's element
|
|
23
|
+
* (`<Outlet/>`) as children.
|
|
24
|
+
*
|
|
25
|
+
* KEYS: `,` toggles Settings · `S` toggles the shortcuts sheet · `\` hides the rail ·
|
|
26
|
+
* ⌥1…9 walks the rail top to bottom, mark first, Settings last. None fire while typing.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} app `{ name, subtitle, logomark (svg url), about (node), links [{label,url}] }`
|
|
29
|
+
* @param {Array} items the tool's rail rows `{ icon, path, label }` — Home is the mark, Settings is pinned
|
|
30
|
+
* @param {string} currentPath · {Function} onNavigate · {ElementType} iconComponent as AppShell
|
|
31
|
+
* @param {string} homePath where Home lives (default `/`); anything else puts the tool at `/`
|
|
32
|
+
* @param {Object} home HubHome's props (`items`, `toCard`, `views`, `actions`, `filterGroups`, …)
|
|
33
|
+
* @param {Array} walkthrough the steps Home's Walkthrough button opens
|
|
34
|
+
* @param {Object} settings HubSettings' props (`content`, `tabs`, `picker`, `onOpenSettings`, `comboLabel`)
|
|
35
|
+
* @param {Array} shortcuts `[{ section, items: [{ id, label, combo }] }]` — the sheet AND Settings
|
|
36
|
+
* @param {ReactNode} themeToggle Settings' masthead toggle (a node — kol-framework is not a peer)
|
|
37
|
+
* @param {string} shortcutsKey default `s`; `null` turns the sheet key off (a tool that owns S)
|
|
38
|
+
* @param {Object} shell any AppShell prop, spread last (`touch`, `pageWash`, `drawerOpenOn`, `railComponent`, …)
|
|
39
|
+
*/
|
|
40
|
+
const SETTINGS_ROW = (path) => [{ icon: 'nav-settings', path, label: 'Settings' }]
|
|
41
|
+
|
|
42
|
+
export default function AppHub({
|
|
43
|
+
app = {},
|
|
44
|
+
items = [],
|
|
45
|
+
currentPath,
|
|
46
|
+
onNavigate,
|
|
47
|
+
iconComponent,
|
|
48
|
+
home,
|
|
49
|
+
walkthrough,
|
|
50
|
+
settings,
|
|
51
|
+
shortcuts = [],
|
|
52
|
+
themeToggle,
|
|
53
|
+
settingsPath = '/settings',
|
|
54
|
+
homePath = '/',
|
|
55
|
+
shortcutsKey = 's',
|
|
56
|
+
shell,
|
|
57
|
+
children,
|
|
58
|
+
}) {
|
|
59
|
+
const [sheet, setSheet] = useState(false)
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!shortcutsKey) return undefined
|
|
63
|
+
const onKey = (e) => {
|
|
64
|
+
if (e.key.toLowerCase() !== shortcutsKey || e.metaKey || e.ctrlKey || e.altKey || e.repeat) return
|
|
65
|
+
const t = e.target
|
|
66
|
+
if (t?.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t?.tagName)) return
|
|
67
|
+
e.preventDefault()
|
|
68
|
+
setSheet((v) => !v)
|
|
69
|
+
}
|
|
70
|
+
window.addEventListener('keydown', onKey)
|
|
71
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
72
|
+
}, [shortcutsKey])
|
|
73
|
+
|
|
74
|
+
const page = currentPath === homePath
|
|
75
|
+
? <HubHome app={app} iconComponent={iconComponent} walkthrough={walkthrough} {...home} />
|
|
76
|
+
: currentPath === settingsPath
|
|
77
|
+
? <HubSettings app={app} shortcuts={shortcuts} themeToggle={themeToggle} {...settings} />
|
|
78
|
+
: children
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<AppShell
|
|
82
|
+
items={items}
|
|
83
|
+
bottomItems={SETTINGS_ROW(settingsPath)}
|
|
84
|
+
logomark={app.logomark ? { svgUrl: app.logomark, title: app.name } : undefined}
|
|
85
|
+
currentPath={currentPath}
|
|
86
|
+
onNavigate={onNavigate}
|
|
87
|
+
iconComponent={iconComponent}
|
|
88
|
+
appName={app.name}
|
|
89
|
+
railToggleKey={'\\'}
|
|
90
|
+
settingsPath={settingsPath}
|
|
91
|
+
settingsKey=","
|
|
92
|
+
navKeys
|
|
93
|
+
touch="drawer"
|
|
94
|
+
pageWash="var(--kol-fg-02)"
|
|
95
|
+
{...shell}
|
|
96
|
+
>
|
|
97
|
+
{page}
|
|
98
|
+
{sheet && <ShortcutsOverlay shortcuts={shortcuts} onClose={() => setSheet(false)} />}
|
|
99
|
+
</AppShell>
|
|
100
|
+
)
|
|
101
|
+
}
|
package/src/AppShell.jsx
CHANGED
|
@@ -272,7 +272,10 @@ export default function AppShell({
|
|
|
272
272
|
* With a `logomark`, the order is the order you see: the mark, then the items.
|
|
273
273
|
* Without one, `items[n - 1]` exactly as before, so an app that renders no
|
|
274
274
|
* mark is untouched. */
|
|
275
|
-
|
|
275
|
+
/* …and the BOTTOM rows after them (the Hub, 2026-09-26): the rail read top to bottom,
|
|
276
|
+
* Settings included. kol-fxr kept a local ⌥-digit handler for exactly this — its ⌥6 is
|
|
277
|
+
* Settings — and monitor and mirror kept theirs; three copies of one key map. */
|
|
278
|
+
const navPaths = (logomark ? ['/'] : []).concat([...(items ?? []), ...(bottomItems ?? [])].map((i) => i?.path)).filter(Boolean)
|
|
276
279
|
useEffect(() => {
|
|
277
280
|
if (!navKeys) return undefined
|
|
278
281
|
const onKey = (e) => {
|
package/src/CatalogPage.jsx
CHANGED
|
@@ -52,7 +52,7 @@ import WalkthroughPanel from './WalkthroughPanel.jsx'
|
|
|
52
52
|
* @param {string} view · onViewChange controlled view; or `defaultView`
|
|
53
53
|
* @param {Array} layouts LIST / GRID (default both)
|
|
54
54
|
* @param {string} defaultLayout 'grid' (default) | 'list'
|
|
55
|
-
* @param {{steps: Array, open: boolean, iconComponent?: ElementType}} walkthrough the panel over the catalog
|
|
55
|
+
* @param {{steps: Array, open: boolean, onClose?: Function, iconComponent?: ElementType}} walkthrough the panel over the catalog; `onClose` draws its X inside the card
|
|
56
56
|
* @param {ReactNode} actions the bottom row's buttons
|
|
57
57
|
* @param {'bleed'|'capped'} width the page's TIER, forwarded to PageShell (two-page-scaffolds-one-job, 2026-09-03):
|
|
58
58
|
* `bleed` (default) fills the window — the app tier; `capped` centres on the
|
|
@@ -209,7 +209,7 @@ export default function CatalogPage({
|
|
|
209
209
|
<PageShell width={width} className={`${PRESETS[preset]?.className ?? ''} ${className}`.trim()} style={{ overflow: 'hidden', '--kol-catalog-n': cols ?? undefined, ...style }}>
|
|
210
210
|
{header && <PageHeader {...header} />}
|
|
211
211
|
<div style={{ flex: 1, position: 'relative' }}>
|
|
212
|
-
{open && <WalkthroughPanel steps={walkthrough.steps} iconComponent={walkthrough.iconComponent} />}
|
|
212
|
+
{open && <WalkthroughPanel steps={walkthrough.steps} iconComponent={walkthrough.iconComponent} onClose={walkthrough.onClose} />}
|
|
213
213
|
<ContentFilters
|
|
214
214
|
items={items}
|
|
215
215
|
title={filtersTitle}
|
package/src/HubHome.jsx
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Button } from '@kolkrabbi/kol-component'
|
|
3
|
+
import CatalogPage from './CatalogPage.jsx'
|
|
4
|
+
|
|
5
|
+
/* taxonomy-ok: organism — nests CatalogPage (relative) + kol-component's Button */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* HubHome — the Hub's Home: a Catalog under the app's masthead (the Hub, 2026-09-26).
|
|
9
|
+
* monitor, mirror and fxr each wrote this page on `CatalogPage` with the same five
|
|
10
|
+
* decisions — the mono masthead, RECENT · SAVED, a walkthrough opened by a button,
|
|
11
|
+
* that button's Close label, a "Get started" last step — and the case of the view
|
|
12
|
+
* labels had already drifted. They are made here once.
|
|
13
|
+
*
|
|
14
|
+
* THE WALKTHROUGH IS OPT-IN (user, 2026-09-26, on media-shell): it never opens by
|
|
15
|
+
* itself. The Walkthrough button at the end of the action row opens it; the X inside
|
|
16
|
+
* the card and the same button close it.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} app `{ name, subtitle }` — the masthead, when Home is the app's front door
|
|
19
|
+
* @param {string} title · subtitle Home's own masthead instead — for a Home that is not the front
|
|
20
|
+
* door (media-shell's Library at `/library`, 2026-09-26: it said "Media")
|
|
21
|
+
* @param {Array|Function} items the catalog's items, or `(view) => items` — RECENT
|
|
22
|
+
* and SAVED are different sets in every app
|
|
23
|
+
* @param {Array} views the view strip (default RECENT · SAVED); rename a set by
|
|
24
|
+
* passing your own, e.g. `[…, { value: 'saved', label: 'FAVOURITES' }]`
|
|
25
|
+
* @param {Array} walkthrough steps (WalkthroughPanel's); a step whose `actions` is a
|
|
26
|
+
* FUNCTION gets `close` — the "Get started" step's buttons
|
|
27
|
+
* @param {ReactNode} actions the app's buttons; the Walkthrough button follows them
|
|
28
|
+
* THE LIST IS THE FILE ROW, one per line (user, 2026-09-26, on apps/shell: CatalogPage's
|
|
29
|
+
* 36px `catalog` row four across "both a bad hover state and using columns wrong"; brand's
|
|
30
|
+
* library row is the reference) — `rowVariant="file"` + `listLayout="stack"`: the ruled
|
|
31
|
+
* 48px thumb row, `date` · `size` · `actions` from `toCard`. Only the Hub's default —
|
|
32
|
+
* CatalogPage's own default does not move, and either prop can be passed back.
|
|
33
|
+
*
|
|
34
|
+
* @param {...*} rest every other CatalogPage prop (`toCard`, `filterGroups`,
|
|
35
|
+
* `filtersTitle`, `filtersProps`, …) — spread through
|
|
36
|
+
*/
|
|
37
|
+
const HUB_VIEWS = [
|
|
38
|
+
{ value: 'recent', label: 'RECENT' },
|
|
39
|
+
{ value: 'saved', label: 'SAVED' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
export default function HubHome({
|
|
43
|
+
app = {},
|
|
44
|
+
title,
|
|
45
|
+
subtitle,
|
|
46
|
+
items = [],
|
|
47
|
+
views = HUB_VIEWS,
|
|
48
|
+
defaultView,
|
|
49
|
+
walkthrough,
|
|
50
|
+
actions,
|
|
51
|
+
iconComponent,
|
|
52
|
+
...rest
|
|
53
|
+
}) {
|
|
54
|
+
const [view, setView] = useState(defaultView ?? views?.[0]?.value)
|
|
55
|
+
const [touring, setTouring] = useState(false)
|
|
56
|
+
const close = () => setTouring(false)
|
|
57
|
+
const steps = (walkthrough ?? []).map((s) => (typeof s.actions === 'function' ? { ...s, actions: s.actions(close) } : s))
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<CatalogPage
|
|
61
|
+
header={{ title: title ?? app.name, subtitle: subtitle ?? app.subtitle, size: 'sm', voice: 'mono' }}
|
|
62
|
+
items={typeof items === 'function' ? items(view) : items}
|
|
63
|
+
views={views}
|
|
64
|
+
view={view}
|
|
65
|
+
onViewChange={setView}
|
|
66
|
+
iconComponent={iconComponent}
|
|
67
|
+
rowVariant="file"
|
|
68
|
+
listLayout="stack"
|
|
69
|
+
walkthrough={steps.length ? { open: touring, steps, onClose: close, iconComponent } : undefined}
|
|
70
|
+
actions={(actions || steps.length) ? (
|
|
71
|
+
<>
|
|
72
|
+
{actions}
|
|
73
|
+
{steps.length > 0 && (
|
|
74
|
+
<Button variant="grey" size="md" onClick={() => setTouring((t) => !t)}>
|
|
75
|
+
{touring ? 'Close' : 'Walkthrough'}
|
|
76
|
+
</Button>
|
|
77
|
+
)}
|
|
78
|
+
</>
|
|
79
|
+
) : undefined}
|
|
80
|
+
{...rest}
|
|
81
|
+
/>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Divider, LabeledControlSection, SettingsFooter, SettingsPanel, SettingsSections, ViewToggle } from '@kolkrabbi/kol-component'
|
|
3
|
+
import SettingsScaffold from './SettingsScaffold.jsx'
|
|
4
|
+
import SettingsShortcuts from './SettingsShortcuts.jsx'
|
|
5
|
+
import SettingsLinks, { SettingsColophon } from './SettingsLinks.jsx'
|
|
6
|
+
|
|
7
|
+
/* taxonomy-ok: organism — nests SettingsScaffold / SettingsShortcuts / SettingsLinks (relative) + kol-component's SettingsPanel / SettingsSections */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* HubSettings — the Hub's Settings page (the Hub, 2026-09-26). monitor, mirror and fxr
|
|
11
|
+
* each put the same page on `SettingsScaffold`: SETTINGS · ABOUT · REPO with the same
|
|
12
|
+
* three subtitles, the same theme toggle in the masthead, About as the app's prose over
|
|
13
|
+
* the colophon, Repo as a link list, and the keyboard shortcuts from the one array the
|
|
14
|
+
* sheet shows. Only the app's own settings differ, so only those are passed in.
|
|
15
|
+
*
|
|
16
|
+
* kol-fxr's page is the richest of the three and is the reference (user, 2026-09-26:
|
|
17
|
+
* "not saying its more true then other versions, just more feature rich, which we want
|
|
18
|
+
* as options, based on what repo has"). Every one of its features is here, and each
|
|
19
|
+
* turns on only when the app supplies what it needs:
|
|
20
|
+
*
|
|
21
|
+
* sections settings rows AS DATA (`SettingsSections`' shape). The page renders
|
|
22
|
+
* them, and the search reads them with the shortcuts, under a Section
|
|
23
|
+
* filter — fxr's page
|
|
24
|
+
* drawer the gear in the masthead opens the SAME sections as a right-hand
|
|
25
|
+
* `SettingsPanel` — one definition, two frames, so they cannot drift.
|
|
26
|
+
* `true`, or `{ title, onReset }` (a reset foot)
|
|
27
|
+
* picker the masthead's dropdown (fxr: open a chrome)
|
|
28
|
+
* splitShortcuts OPTIONS / SHORTCUTS as an icon pair beside the row, instead of the
|
|
29
|
+
* shortcuts stacked under the settings — fxr's page
|
|
30
|
+
* content free-form sections instead of (or above) `sections` — monitor's prose
|
|
31
|
+
* tabs extra tabs after SETTINGS (mirror's PERFORMANCE)
|
|
32
|
+
*
|
|
33
|
+
* ABOUT and REPO sit BELOW the rule (`row: 'layout'`) — fxr's, the page the scaffold
|
|
34
|
+
* was rebuilt from (SettingsScaffoldTabRows).
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} app `{ name, about, links }`
|
|
37
|
+
* @param {Array} sections `[{ label, rows: [{ label, render | value, align? }] }]`
|
|
38
|
+
* @param {boolean|Object} drawer gear + drawer over `sections` — `{ title = 'Display settings', onReset }`
|
|
39
|
+
* @param {ReactNode} content free-form body above the sections
|
|
40
|
+
* @param {Array} tabs `[{ value, label, title, subtitle, content }]`
|
|
41
|
+
* @param {Array} shortcuts `[{ section, items: [{ id, label, combo }] }]`
|
|
42
|
+
* @param {boolean} splitShortcuts the OPTIONS / SHORTCUTS pair (default off — stacked)
|
|
43
|
+
* @param {ReactNode} themeToggle · picker the masthead cluster, as SettingsScaffold
|
|
44
|
+
* @param {'masthead'|'drawer'} themeIn where the theme toggle sits (user, 2026-09-26). `masthead`
|
|
45
|
+
* (default) is the 2026-08-30 ruling (SettingsMastheadCluster) — and the
|
|
46
|
+
* only place on a page with no drawer; `drawer` puts it in the drawer's
|
|
47
|
+
* footer beside reset, apps/media's arrangement. `drawer` with no drawer
|
|
48
|
+
* falls back to the masthead, so the toggle is never lost
|
|
49
|
+
* @param {Function} comboLabel (combo) => string, for SettingsShortcuts
|
|
50
|
+
* @param {string} tone the page's control tone (default `sunken`, SettingsScaffold's): the
|
|
51
|
+
* filter row, the masthead gear AND every control in the settings rows,
|
|
52
|
+
* which inherit it from a `kol-tone-*` wrapper — `tone: 'primary'` in an
|
|
53
|
+
* app's `settings` is the one line that changes all of them
|
|
54
|
+
*/
|
|
55
|
+
const SHORTCUTS = 'Shortcuts'
|
|
56
|
+
/* ONE settings column for every app on the Hub (user, 2026-09-26: "the layout has 2 different
|
|
57
|
+
* settings, thats wrong") — a per-app width was how two pages drifted within a day. 320 = the 160
|
|
58
|
+
* label + a dropdown holding a bucket name ("R2 · kol-media"); fxr's 268 was sized for switches. */
|
|
59
|
+
const COLUMN = 320
|
|
60
|
+
const PAIR = [
|
|
61
|
+
{ value: 'options', label: 'OPTIONS', icon: 'slider-01' },
|
|
62
|
+
{ value: 'shortcuts', label: 'SHORTCUTS', icon: 'view-list' },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
export default function HubSettings({
|
|
66
|
+
app = {},
|
|
67
|
+
sections,
|
|
68
|
+
drawer,
|
|
69
|
+
content,
|
|
70
|
+
tabs = [],
|
|
71
|
+
shortcuts = [],
|
|
72
|
+
splitShortcuts = false,
|
|
73
|
+
themeToggle,
|
|
74
|
+
picker,
|
|
75
|
+
comboLabel,
|
|
76
|
+
tone = 'sunken',
|
|
77
|
+
themeIn = 'masthead',
|
|
78
|
+
}) {
|
|
79
|
+
const [drawerOpen, setDrawerOpen] = useState(false)
|
|
80
|
+
const [mode, setMode] = useState('options')
|
|
81
|
+
const drawerCfg = drawer === true ? {} : drawer
|
|
82
|
+
const hasDrawer = !!drawerCfg && sections?.length > 0
|
|
83
|
+
const themeInDrawer = themeIn === 'drawer' && hasDrawer && !!themeToggle
|
|
84
|
+
|
|
85
|
+
const all = [
|
|
86
|
+
{ value: 'settings', label: 'SETTINGS', title: 'Settings', subtitle: 'Configuration and preferences' },
|
|
87
|
+
...tabs,
|
|
88
|
+
{ value: 'about', label: 'ABOUT', row: 'layout', title: 'About', subtitle: `${app.name ?? ''} by Kolkrabbi`.trim() },
|
|
89
|
+
{ value: 'repo', label: 'REPO', row: 'layout', title: 'Repo', subtitle: 'Source and deployments' },
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
/* ONE FLAT LIST for the filter row: a setting row carries its section, a shortcut its
|
|
93
|
+
* own group under the one Shortcuts section — fxr's page, so the chips stay few */
|
|
94
|
+
const items = [
|
|
95
|
+
...(sections ?? []).flatMap((sec) => sec.rows.map((row) => ({
|
|
96
|
+
id: `${sec.label}:${row.label}`, kind: 'setting', label: row.label, section: sec.label, row,
|
|
97
|
+
}))),
|
|
98
|
+
...shortcuts.flatMap((sec) => (sec.items ?? []).map((k) => ({
|
|
99
|
+
id: `shortcut:${k.id ?? k.label}`, kind: 'shortcut', label: k.label, combo: k.combo, section: SHORTCUTS, group: sec.section, k,
|
|
100
|
+
}))),
|
|
101
|
+
]
|
|
102
|
+
const filterGroups = sections?.length
|
|
103
|
+
? [{ label: 'Section', key: 'section', values: [...sections.map((s) => s.label), ...(shortcuts.length ? [SHORTCUTS] : [])] }]
|
|
104
|
+
: undefined
|
|
105
|
+
|
|
106
|
+
/* regroup what survived in the declared order — a search narrows, it never reorders */
|
|
107
|
+
const liveSections = (rows) => (sections ?? [])
|
|
108
|
+
.map((sec) => ({ ...sec, rows: rows.filter((i) => i.kind === 'setting' && i.section === sec.label).map((i) => i.row) }))
|
|
109
|
+
.filter((sec) => sec.rows.length)
|
|
110
|
+
const liveShortcuts = (rows) => shortcuts
|
|
111
|
+
.map((sec) => ({ section: sec.section, items: rows.filter((i) => i.kind === 'shortcut' && i.group === sec.section).map((i) => i.k) }))
|
|
112
|
+
.filter((sec) => sec.items.length)
|
|
113
|
+
|
|
114
|
+
const renderSettings = (filtered) => {
|
|
115
|
+
const rows = filtered ?? items
|
|
116
|
+
const keys = liveShortcuts(rows)
|
|
117
|
+
/* SPLIT: the page-wide multi-column grid, for a keymap that fills a page (monitor's) */
|
|
118
|
+
if (splitShortcuts && mode === 'shortcuts') return keys.length > 0 && <SettingsShortcuts sections={keys} comboLabel={comboLabel} />
|
|
119
|
+
const live = liveSections(rows)
|
|
120
|
+
/* STACKED: the shortcuts are settings ROWS in the settings column (user, 2026-09-26: "why
|
|
121
|
+
* doesnt items justify space between right side? where toggle and dropdown align?") — the
|
|
122
|
+
* page-wide grid packed them into ~180px tracks, so the combos ended short of the controls
|
|
123
|
+
* above. Same anatomy as the S sheet's rows; the combo is the row's value, right-aligned. */
|
|
124
|
+
const keyRows = keys.map(({ section, items: ks }) => ({
|
|
125
|
+
label: section,
|
|
126
|
+
rows: ks.map((k) => ({
|
|
127
|
+
id: k.id ?? k.label,
|
|
128
|
+
label: k.label,
|
|
129
|
+
value: <span className="text-fg-32 kol-helper-12 whitespace-nowrap">{comboLabel ? comboLabel(k.combo) : k.combo}</span>,
|
|
130
|
+
})),
|
|
131
|
+
}))
|
|
132
|
+
return (
|
|
133
|
+
<div className="flex flex-col gap-8">
|
|
134
|
+
{content}
|
|
135
|
+
{/* the rows are sized for the drawer's 380 (a 160 label + the control); unbounded on a
|
|
136
|
+
* page the control runs the full width, so the column is capped — one column for every app (COLUMN) */}
|
|
137
|
+
{live.length > 0 && <div style={{ maxWidth: COLUMN }}><SettingsSections sections={live} /></div>}
|
|
138
|
+
{!splitShortcuts && keyRows.length > 0 && (
|
|
139
|
+
<LabeledControlSection label="Keyboard Shortcuts">
|
|
140
|
+
<div style={{ maxWidth: COLUMN }}><SettingsSections sections={keyRows} /></div>
|
|
141
|
+
</LabeledControlSection>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const render = (tab, filtered) => {
|
|
148
|
+
if (tab === 'settings') return <div className={`kol-tone-${tone}`}>{renderSettings(filtered)}</div>
|
|
149
|
+
if (tab === 'about') return (
|
|
150
|
+
<div className="flex flex-col gap-8">
|
|
151
|
+
{app.about && (
|
|
152
|
+
<LabeledControlSection label={app.name}>
|
|
153
|
+
<div className="text-fg-48 kol-mono-14" style={{ maxWidth: 640 }}>{app.about}</div>
|
|
154
|
+
</LabeledControlSection>
|
|
155
|
+
)}
|
|
156
|
+
<SettingsColophon />
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
if (tab === 'repo') return (
|
|
160
|
+
<LabeledControlSection label="Links">
|
|
161
|
+
<SettingsLinks links={app.links ?? []} />
|
|
162
|
+
</LabeledControlSection>
|
|
163
|
+
)
|
|
164
|
+
return tabs.find((t) => t.value === tab)?.content ?? null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<>
|
|
169
|
+
<SettingsScaffold
|
|
170
|
+
tabs={all}
|
|
171
|
+
defaultTab="settings"
|
|
172
|
+
header={{ size: 'sm', voice: 'mono' }}
|
|
173
|
+
tone={tone}
|
|
174
|
+
picker={picker}
|
|
175
|
+
themeToggle={themeInDrawer ? undefined : themeToggle}
|
|
176
|
+
onOpenSettings={hasDrawer ? () => setDrawerOpen(true) : undefined}
|
|
177
|
+
items={items}
|
|
178
|
+
filterGroups={filterGroups}
|
|
179
|
+
searchKeys={['label', 'section', 'group', 'combo']}
|
|
180
|
+
/* the pair picks the Settings view, so from ABOUT or REPO it also takes you back to Settings */
|
|
181
|
+
trailingActions={splitShortcuts ? (tab, setTab) => (
|
|
182
|
+
<>
|
|
183
|
+
<ViewToggle variant="icon" size="sm" tone="sunken" options={PAIR} viewMode={mode}
|
|
184
|
+
onViewChange={(v) => { setMode(v); setTab('settings') }} />
|
|
185
|
+
<Divider variant="vertical" />
|
|
186
|
+
</>
|
|
187
|
+
) : undefined}
|
|
188
|
+
renderContent={render}
|
|
189
|
+
/>
|
|
190
|
+
{/* the drawer is the page's other half, a SIBLING of the scaffold — never inside its scroll */}
|
|
191
|
+
{hasDrawer && (
|
|
192
|
+
<SettingsPanel
|
|
193
|
+
open={drawerOpen}
|
|
194
|
+
variant="drawer"
|
|
195
|
+
title={drawerCfg.title ?? 'Display settings'}
|
|
196
|
+
onClose={() => setDrawerOpen(false)}
|
|
197
|
+
/* the footer's children sit before reset in its row — media's theme · reset arrangement */
|
|
198
|
+
footer={(drawerCfg.onReset || themeInDrawer)
|
|
199
|
+
? <SettingsFooter onReset={drawerCfg.onReset}>{themeInDrawer ? themeToggle : null}</SettingsFooter>
|
|
200
|
+
: undefined}
|
|
201
|
+
>
|
|
202
|
+
<SettingsSections sections={sections} divided />
|
|
203
|
+
</SettingsPanel>
|
|
204
|
+
)}
|
|
205
|
+
</>
|
|
206
|
+
)
|
|
207
|
+
}
|
package/src/NavRail.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import gsap from 'gsap'
|
|
3
|
-
import { Button } from '@kolkrabbi/kol-component'
|
|
3
|
+
import { Button, Tooltip } from '@kolkrabbi/kol-component'
|
|
4
4
|
import { GRAB } from '@kolkrabbi/kol-component/utilities/motion'
|
|
5
5
|
/* the grab pill's proximity wake, travel and dwell — ONE implementation, shared
|
|
6
6
|
* with kol-framework's useDragResize (OneGrabGestureBothRails, kol-fxr
|
|
@@ -187,6 +187,9 @@ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconCompone
|
|
|
187
187
|
return (
|
|
188
188
|
<>
|
|
189
189
|
<div className="flex items-center gap-3 w-full overflow-hidden shrink-0">
|
|
190
|
+
{/* the DS tooltip, not the browser's (the native-title sweep, 2026-09-26) — to the RIGHT,
|
|
191
|
+
* out over the page, since the collapsed rail's glyph is all there is to read */}
|
|
192
|
+
<Tooltip label={label} placement="right" triggerClassName="inline-flex shrink-0">
|
|
190
193
|
<Button
|
|
191
194
|
iconOnly={icon}
|
|
192
195
|
iconSize={20}
|
|
@@ -202,9 +205,9 @@ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconCompone
|
|
|
202
205
|
* in the rail") */
|
|
203
206
|
style={{ color: 'var(--kol-oq-96)' }}
|
|
204
207
|
onClick={press}
|
|
205
|
-
title={label}
|
|
206
208
|
aria-label={label}
|
|
207
209
|
/>
|
|
210
|
+
</Tooltip>
|
|
208
211
|
<span
|
|
209
212
|
className="kol-helper-12 uppercase text-oq-96 flex-1 min-w-0 truncate cursor-pointer"
|
|
210
213
|
onClick={press}
|
|
@@ -212,6 +215,7 @@ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconCompone
|
|
|
212
215
|
{label}
|
|
213
216
|
</span>
|
|
214
217
|
{sub?.length > 0 && (
|
|
218
|
+
<Tooltip label={label} placement="right" triggerClassName="inline-flex shrink-0">
|
|
215
219
|
<Button
|
|
216
220
|
iconOnly={open ? 'chevron-down' : 'chevron-right'}
|
|
217
221
|
iconSize={16}
|
|
@@ -222,23 +226,22 @@ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconCompone
|
|
|
222
226
|
style={{ color: 'var(--kol-oq-96)' }}
|
|
223
227
|
aria-expanded={open}
|
|
224
228
|
onClick={() => setOpen((v) => !v)}
|
|
225
|
-
title={label}
|
|
226
229
|
aria-label={`${label} sub categories`}
|
|
227
230
|
/>
|
|
231
|
+
</Tooltip>
|
|
228
232
|
)}
|
|
229
233
|
</div>
|
|
230
234
|
{railOpen && open && sub.map((s) => {
|
|
231
235
|
const on = s.path === '/' ? currentPath === '/' : currentPath.startsWith(s.path)
|
|
232
236
|
return (
|
|
237
|
+
<Tooltip key={s.path} label={s.label} placement="right" asChild>
|
|
233
238
|
<div
|
|
234
|
-
key={s.path}
|
|
235
239
|
/* 18px of gutter puts the 20px box at x 26, so its 12px glyph starts
|
|
236
240
|
* at 30 — one step right of L1's 20-in-32 at 14 (measured in fxr) */
|
|
237
241
|
className="flex items-center gap-3 w-full overflow-hidden shrink-0 cursor-pointer"
|
|
238
242
|
style={{ paddingLeft: 18 }}
|
|
239
243
|
onClick={() => onNavigate?.(s.path)}
|
|
240
244
|
aria-current={on ? 'page' : undefined}
|
|
241
|
-
title={s.label}
|
|
242
245
|
>
|
|
243
246
|
{s.icon && (
|
|
244
247
|
<span
|
|
@@ -256,6 +259,7 @@ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconCompone
|
|
|
256
259
|
{s.label}
|
|
257
260
|
</span>
|
|
258
261
|
</div>
|
|
262
|
+
</Tooltip>
|
|
259
263
|
)
|
|
260
264
|
})}
|
|
261
265
|
</>
|
|
@@ -324,14 +328,15 @@ export default function NavRail({
|
|
|
324
328
|
/* the mark, and the app name beside it when open — uppercase like the
|
|
325
329
|
* rows (user 2026-08-28: "uppercase CONSISTENCY"). The `w-8` centring box
|
|
326
330
|
* puts the 20px mark on the same x as the 20px glyphs below it. */
|
|
331
|
+
<Tooltip label={logomark.title} placement="right" asChild>
|
|
327
332
|
<div
|
|
328
333
|
onClick={() => onNavigate?.('/')}
|
|
329
334
|
className="text-oq-96 cursor-pointer flex items-center gap-3 w-full overflow-hidden shrink-0 pt-1 mb-4"
|
|
330
|
-
title={logomark.title}
|
|
331
335
|
>
|
|
332
336
|
<span className="w-8 flex justify-center shrink-0"><Logomark svgUrl={logomark.svgUrl} size={20} /></span>
|
|
333
337
|
<span className="kol-helper-12 uppercase flex-1 min-w-0 truncate">{logomark.title}</span>
|
|
334
338
|
</div>
|
|
339
|
+
</Tooltip>
|
|
335
340
|
)}
|
|
336
341
|
{items.map(row)}
|
|
337
342
|
{/* the spacer IS the layout */}
|
package/src/SettingsScaffold.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
-
import { ContentFilters, IconFrame } from '@kolkrabbi/kol-component'
|
|
2
|
+
import { ContentFilters, IconFrame, Tooltip } from '@kolkrabbi/kol-component'
|
|
3
3
|
import PageShell from './PageShell.jsx'
|
|
4
4
|
import { PageHeader } from '@kolkrabbi/kol-component'
|
|
5
5
|
|
|
@@ -110,19 +110,22 @@ export default function SettingsScaffold({
|
|
|
110
110
|
* Pass none of the three and no cluster renders — mirror and monitor are
|
|
111
111
|
* untouched until they opt in. An explicit `header.actions` still wins. */
|
|
112
112
|
const cluster = picker || themeToggle || onOpenSettings ? (
|
|
113
|
-
|
|
113
|
+
/* the cluster carries the page's tone as a wrapper (the Hub review, 2026-09-26), so a picker
|
|
114
|
+
* or toggle passed with no tone of its own matches the gear and the filter row */
|
|
115
|
+
<div className={`flex items-center gap-2 kol-tone-${tone}`}>
|
|
114
116
|
{picker}
|
|
115
117
|
{themeToggle}
|
|
116
118
|
{onOpenSettings && (
|
|
119
|
+
<Tooltip label="Display settings">
|
|
117
120
|
<IconFrame
|
|
118
121
|
name="settings-01"
|
|
119
122
|
variant="primary"
|
|
120
123
|
tone={tone}
|
|
121
124
|
size="sm"
|
|
122
125
|
onClick={onOpenSettings}
|
|
123
|
-
title="Display settings"
|
|
124
126
|
aria-label="Display settings"
|
|
125
127
|
/>
|
|
128
|
+
</Tooltip>
|
|
126
129
|
)}
|
|
127
130
|
</div>
|
|
128
131
|
) : null
|
package/src/ShortcutsOverlay.jsx
CHANGED
|
@@ -82,8 +82,11 @@ export default function ShortcutsOverlay({ shortcuts = [], onClose }) {
|
|
|
82
82
|
ground rather than a surface from the page's own ladder — so the
|
|
83
83
|
panel separates from the scrimmed canvas without a stroke. */}
|
|
84
84
|
<div
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
/* THE SHEET FITS THE WINDOW (editor review #4, 2026-09-27): a long keymap — the design
|
|
86
|
+
* editor's is six sections — ran past both edges of the viewport with no way to reach the
|
|
87
|
+
* rest. Capped to the window less a margin, scrolling inside. */
|
|
88
|
+
className="kol-shortcuts-panel text-fg-64 kol-helper-12 bg-oq-04 flex flex-col gap-6 overflow-y-auto"
|
|
89
|
+
style={{ padding: 24, borderRadius: 4, maxHeight: 'calc(100dvh - 48px)' }}
|
|
87
90
|
onClick={(e) => e.stopPropagation()}
|
|
88
91
|
>
|
|
89
92
|
<SettingsSections
|
package/src/WalkthroughPanel.jsx
CHANGED
|
@@ -9,8 +9,12 @@ import { Button } from '@kolkrabbi/kol-component'
|
|
|
9
9
|
* `illustration` is a render slot (monitor globs SVGs, mirror uses a JPEG),
|
|
10
10
|
* a step with `actions` renders that node centred instead of the text/image
|
|
11
11
|
* split (the "Get started" step).
|
|
12
|
+
*
|
|
13
|
+
* `onClose` draws an X INSIDE the card, top-right (user, 2026-09-26, on media-shell:
|
|
14
|
+
* the panel had no close of its own, so every app put one outside the card or on a
|
|
15
|
+
* page button). Unset = no X, exactly as before.
|
|
12
16
|
*/
|
|
13
|
-
export default function WalkthroughPanel({ steps = [], iconComponent }) {
|
|
17
|
+
export default function WalkthroughPanel({ steps = [], iconComponent, onClose }) {
|
|
14
18
|
const [step, setStep] = useState(0)
|
|
15
19
|
const current = steps[step]
|
|
16
20
|
if (!current) return null
|
|
@@ -34,8 +38,19 @@ export default function WalkthroughPanel({ steps = [], iconComponent }) {
|
|
|
34
38
|
|
|
35
39
|
<div
|
|
36
40
|
className="bg-surface-tertiary border border-fg-08"
|
|
37
|
-
style={{ flex: 1, display: 'flex', borderRadius: 4, minHeight: 480, overflow: 'hidden' }}
|
|
41
|
+
style={{ flex: 1, display: 'flex', borderRadius: 4, minHeight: 480, overflow: 'hidden', position: 'relative' }}
|
|
38
42
|
>
|
|
43
|
+
{onClose && (
|
|
44
|
+
<Button
|
|
45
|
+
variant="nav"
|
|
46
|
+
size="sm"
|
|
47
|
+
iconOnly="x"
|
|
48
|
+
iconComponent={iconComponent}
|
|
49
|
+
aria-label="Close walkthrough"
|
|
50
|
+
onClick={onClose}
|
|
51
|
+
style={{ position: 'absolute', top: 8, right: 8 }}
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
39
54
|
{current.actions ? (
|
|
40
55
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12 }}>
|
|
41
56
|
{current.actions}
|
package/src/index.js
CHANGED
|
@@ -31,3 +31,7 @@ export { default as CatalogPage } from './CatalogPage.jsx'
|
|
|
31
31
|
export { default as SettingsShortcuts } from './SettingsShortcuts.jsx'
|
|
32
32
|
export { default as SettingsLinks, SettingsColophon } from './SettingsLinks.jsx'
|
|
33
33
|
export { default as TouchDeviceOverlay, useTouchPrimary } from './TouchDeviceOverlay.jsx'
|
|
34
|
+
// the Hub — Shell + Home + Settings + keys, made once (the app anatomy, 2026-09-26)
|
|
35
|
+
export { default as AppHub } from './AppHub.jsx'
|
|
36
|
+
export { default as HubHome } from './HubHome.jsx'
|
|
37
|
+
export { default as HubSettings } from './HubSettings.jsx'
|