@kolkrabbi/kol-shell 0.13.2 → 0.15.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.13.2",
3
+ "version": "0.15.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",
@@ -13,17 +13,17 @@
13
13
  },
14
14
  "peerDependencies": {
15
15
  "@kolkrabbi/kol-component": ">=0.108.0",
16
- "@kolkrabbi/kol-framework": ">=0.33.0",
16
+ "@kolkrabbi/kol-framework": ">=0.34.0",
17
17
  "@kolkrabbi/kol-icons": ">=0.16.0",
18
18
  "@kolkrabbi/kol-theme": ">=0.80.0",
19
19
  "react": "^18.3.0 || ^19.0.0",
20
20
  "react-dom": "^18.3.0 || ^19.0.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@kolkrabbi/kol-component": "^0.118.3",
23
+ "@kolkrabbi/kol-component": "^0.121.1",
24
24
  "@kolkrabbi/kol-icons": "^0.24.0",
25
- "@kolkrabbi/kol-framework": "^0.33.0",
26
- "@kolkrabbi/kol-theme": "^0.80.0"
25
+ "@kolkrabbi/kol-framework": "^0.35.0",
26
+ "@kolkrabbi/kol-theme": "^0.83.0"
27
27
  },
28
28
  "files": [
29
29
  "src",
package/src/AppShell.jsx CHANGED
@@ -31,7 +31,11 @@ import TouchDeviceOverlay, { useTouchPrimary } from './TouchDeviceOverlay.jsx'
31
31
  * @param {string} props.currentPath the router's current pathname
32
32
  * @param {Function} props.onNavigate `(path) => void`
33
33
  * @param {ElementType} props.iconComponent icon renderer seam (see Button)
34
- * @param {boolean} props.themeToggle render the ThemeToggle above bottomItems (default true)
34
+ * @param {Object} props.settings `{ icon, path, label }` Settings as a DISCLOSURE in the pinned bottom
35
+ * row (RailSettingsDisclosure, 2026-08-28): click opens a panel above the
36
+ * row with the theme toggle and the settings page as a row; click again,
37
+ * Escape or any navigation closes it. Turns the rail's theme slot off.
38
+ * @param {boolean} props.themeToggle render the ThemeToggle above bottomItems (default true; false when `settings` is given)
35
39
  * @param {string} props.railToggleKey a key that toggles the rail (e.g. '\\') — ignored while typing in a field;
36
40
  * the rail comes back on every `currentPath` change (ShellHomeSystem, 2026-08-27)
37
41
  * @param {'shell'|'bare'|'overlay'} props.touch the touch-primary policy (default 'shell' = the rail regardless):
@@ -62,6 +66,7 @@ export default function AppShell({
62
66
  currentPath,
63
67
  onNavigate,
64
68
  iconComponent,
69
+ settings,
65
70
  themeToggle,
66
71
  railToggleKey,
67
72
  touch = 'shell',
@@ -130,7 +135,8 @@ export default function AppShell({
130
135
  currentPath={currentPath}
131
136
  onNavigate={onNavigate}
132
137
  iconComponent={iconComponent}
133
- themeToggle={themeToggle}
138
+ settings={settings}
139
+ themeToggle={themeToggle ?? !settings}
134
140
  />
135
141
  )}
136
142
  {/* THE BACK OF THE BACK — surface-primary, always, in every app; the
package/src/NavRail.jsx CHANGED
@@ -1,4 +1,5 @@
1
- import { SideNav } from '@kolkrabbi/kol-framework'
1
+ import { SideNav, ThemeToggle } from '@kolkrabbi/kol-framework'
2
+ import { Icon } from '@kolkrabbi/kol-icons'
2
3
  import Logomark from './Logomark.jsx'
3
4
 
4
5
  /**
@@ -21,6 +22,17 @@ import Logomark from './Logomark.jsx'
21
22
  * 0.13.0's footer placement: "why did you move the logo from top to bottom?
22
23
  * … I've never seen that before so you are the first"). Its `title` is the
23
24
  * label beside the mark in the open rail; collapsed, the mark alone.
25
+ *
26
+ * `settings` — SETTINGS IS A DISCLOSURE, NOT A ROUTE (RailSettingsDisclosure,
27
+ * kol-fxr 2026-08-28 — user: "put the settings where the logo is, and logo
28
+ * back on top" · "sidebar setting should open close on click and click again"
29
+ * · "put the theme toggle in the settings and out of the sidebar" · "we make
30
+ * it work here then ship it"): `{ icon, path, label }` renders the gear as the
31
+ * pinned bottom row — a SideNav panel leaf, same pixel as the route row it
32
+ * replaces — whose panel holds the theme toggle (`icon` collapsed /
33
+ * `hop-bare` expanded, the slot's two variants) and the Settings page as a
34
+ * row. The rail's standing theme slot is off when `settings` is given; the
35
+ * toggle exists in the disclosure and on the settings page only.
24
36
  */
25
37
  export default function NavRail({
26
38
  items = [],
@@ -29,17 +41,43 @@ export default function NavRail({
29
41
  currentPath = '',
30
42
  onNavigate,
31
43
  iconComponent,
32
- themeToggle = true,
44
+ settings,
45
+ themeToggle = !settings,
33
46
  hidden = false,
34
47
  }) {
35
48
  if (hidden) return null
36
49
  const node = (item) => ({ id: item.path, label: item.label, icon: item.icon, to: item.path })
50
+ const Glyph = iconComponent ?? Icon
51
+ const settingsLeaf = settings && {
52
+ id: settings.path,
53
+ label: settings.label,
54
+ icon: settings.icon,
55
+ panel: ({ collapsed, close }) => (
56
+ <>
57
+ <div className="kol-sidenav-theme-slot flex">
58
+ <ThemeToggle variant={collapsed ? 'icon' : 'hop-bare'} size="md" />
59
+ </div>
60
+ <button
61
+ type="button"
62
+ className="kol-sidenav-hop kol-helper-12 text-strong hover:text-emphasis bg-transparent border-0 cursor-pointer text-left"
63
+ onClick={() => { close(); onNavigate?.(settings.path) }}
64
+ title={settings.label}
65
+ aria-label={settings.label}
66
+ >
67
+ <span className="kol-sidenav-hop-icon inline-flex items-center justify-center w-5 h-5 shrink-0" aria-hidden="true">
68
+ <Glyph name="arrow-right" size={16} />
69
+ </span>
70
+ <span className="kol-sidenav-hop-label flex-1 min-w-0 truncate">{settings.label}</span>
71
+ </button>
72
+ </>
73
+ ),
74
+ }
37
75
  /* the shell's route match: '/' exact, anything else by prefix */
38
76
  const isActive = (to) => (to === '/' ? currentPath === '/' : currentPath.startsWith(to))
39
77
  return (
40
78
  <SideNav
41
79
  navTree={items.map(node)}
42
- bottomItems={bottomItems.map(node)}
80
+ bottomItems={[...bottomItems.map(node), ...(settingsLeaf ? [settingsLeaf] : [])]}
43
81
  currentPath={currentPath}
44
82
  isActive={isActive}
45
83
  onNavigate={(e, to) => onNavigate?.(to)}
@@ -29,18 +29,34 @@
29
29
  * kol-mono-heading-03 · kol-mono-display-03 · kol-mono-display-02
30
30
  * (kol-theme ≥0.67.0). The subtitle stays kol-mono-14.
31
31
  * @param {string} titleClass replaces the title role whole (the ContentText seam)
32
+ * @param {ReactNode} actions a control cluster on the SUBTITLE's first baseline — on the title's
33
+ * when there is no subtitle (PageHeaderTrailingSlot, kol-website 2026-08-28;
34
+ * kol-r2b2's header is the reference: wordmark left, controls right, on the
35
+ * line). Without it the consumer wrapped the header in a flex row and got the
36
+ * h1's baseline, or re-rendered the subtitle as a bare <p> off copied classes
37
+ * with an `!important` on the margin — a DS text role re-implemented outside.
38
+ * @param {string} subtitleMaxWidth the lede's measure (e.g. '800px' or '60ch'), a prop
39
+ * instead of a consumer selector reaching inside
40
+ * The bottom rhythm is `--kol-page-header-mb` (default 40px): inline, as before, but through a
41
+ * variable a consumer can re-point where an inline literal could only be `!important`-ed.
32
42
  */
33
43
  const TITLE = {
34
44
  sans: { sm: 'kol-sans-heading-03', md: 'kol-sans-display-03', lg: 'kol-sans-display-02' },
35
45
  mono: { sm: 'kol-mono-heading-03', md: 'kol-mono-display-03', lg: 'kol-mono-display-02' },
36
46
  }
37
47
 
38
- export default function PageHeader({ eyebrow, title, subtitle, size = 'md', voice = 'sans', titleClass, className = '' }) {
48
+ export default function PageHeader({ eyebrow, title, subtitle, actions, subtitleMaxWidth, size = 'md', voice = 'sans', titleClass, className = '' }) {
39
49
  const roles = TITLE[voice] ?? TITLE.sans
50
+ const h1 = <h1 className={`text-fg-96 ${titleClass ?? roles[size] ?? roles.md}`}>{title}</h1>
51
+ const lede = subtitle && <p className="text-oq-64 kol-mono-14 min-w-0" style={{ marginTop: actions ? undefined : 12, maxWidth: subtitleMaxWidth }}>{subtitle}</p>
52
+ /* the cluster shares a baseline row with the lede (or the title): flexbox
53
+ exposes a flex item's FIRST baseline, so putting them in one row is the
54
+ one way to land on the subtitle's line rather than the h1's */
55
+ const cluster = actions && <div className="flex items-center gap-4 shrink-0">{actions}</div>
40
56
  return (
41
57
  /* the block owns its own rhythm — margins inline, never in a shared type
42
58
  class, which leaks estate-wide (ShellHeaderFilterRefinements, 2026-08-15) */
43
- <header className={`flex flex-col ${className}`.trim()} style={{ marginBottom: 40 }}>
59
+ <header className={`flex flex-col ${className}`.trim()} style={{ marginBottom: 'var(--kol-page-header-mb, 40px)' }}>
44
60
  {eyebrow && (
45
61
  /* HELPER, not mono (user ruling 2026-08-15). An eyebrow is single-line
46
62
  chrome — that is the whole definition of the `kol-helper-*` ramp, and
@@ -54,9 +70,24 @@ export default function PageHeader({ eyebrow, title, subtitle, size = 'md', voic
54
70
  {eyebrow}
55
71
  </p>
56
72
  )}
57
- <h1 className={`text-fg-96 ${titleClass ?? roles[size] ?? roles.md}`}>{title}</h1>
58
- {subtitle && (
59
- <p className="text-oq-64 kol-mono-14" style={{ marginTop: 12 }}>{subtitle}</p>
73
+ {actions && subtitle ? (
74
+ <>
75
+ {h1}
76
+ <div className="flex items-baseline justify-between gap-6" style={{ marginTop: 12 }}>
77
+ {lede}
78
+ {cluster}
79
+ </div>
80
+ </>
81
+ ) : actions ? (
82
+ <div className="flex items-baseline justify-between gap-6">
83
+ {h1}
84
+ {cluster}
85
+ </div>
86
+ ) : (
87
+ <>
88
+ {h1}
89
+ {lede}
90
+ </>
60
91
  )}
61
92
  </header>
62
93
  )