@kolkrabbi/kol-framework 0.43.0 → 0.45.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/kol-framework.css CHANGED
@@ -63,16 +63,17 @@ body {
63
63
  --kol-sidenav-step: var(--kol-spacing-4); /* keyboard resize increment */
64
64
  --kol-sidenav-snap-default: var(--kol-sidenav-step); /* release within this of the default clears the override */
65
65
  --kol-sidenav-w-collapsed: 56px;
66
- /* The workshop shell's RIGHT rail. EQUAL TO THE LEFT (user ruling
67
- 2026-08-01) — the two rails frame the same content and a lopsided frame is
68
- a frame nobody drew on purpose; it read 14rem against the left's 16rem.
69
- Kept as its own token rather than aliased: the right rail can be ruled
70
- different later without touching the sidenav.
71
- The track is RESERVED whether or not the rail has anything in it — a rail
72
- that vanishes on a heading-less page re-flows main, so the same shell
73
- renders at two widths. Distinct from --kol-toc-w further down, which is
74
- the BRAND layout's rail: same concept, two shells, two numbers. */
75
- --kol-shell-toc-w: 16rem;
66
+ /* THE WORKSHOP SHELL'S RAILS — their own pair, 256px each (showcase
67
+ refinement 2026-09-28, user: "should they both be the same width? layout
68
+ feels asymmetrical"). The left rail used to read --kol-sidenav-w, which is
69
+ the DRAGGABLE app sidenav's ladder (264, 320 from 1536 — useDragResize,
70
+ NavRail, the labs rail, the deck editor), so the docs shell rendered
71
+ 320 | main | 256: one token, two shells. The workshop rails are not
72
+ draggable and frame the same content, so they are equal and fixed, in px
73
+ like every other layout width. Distinct from --kol-toc-w further down,
74
+ which is the BRAND layout's rail. */
75
+ --kol-shell-nav-w: 256px;
76
+ --kol-shell-toc-w: 256px;
76
77
  --kol-container-max: 100%;
77
78
 
78
79
  /* Opacity tokens (--kol-fg-*) + text-role tokens (--kol-text-*) live in
@@ -736,7 +737,7 @@ body {
736
737
  .kol-portal-footer a { text-decoration: none; transition: opacity 150ms ease; }
737
738
  .kol-portal-footer a:hover { opacity: 0.8; }
738
739
 
739
- /* ───── Back link (structural only; use kol-helper-xs uppercase tracking-widest text-fg-64 on JSX) ───── */
740
+ /* ───── Back link (structural only; PageHero sets its type: kol-helper-12 tracking-widest text-body) ───── */
740
741
 
741
742
  .kol-back-link {
742
743
  display: inline-flex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-framework",
3
- "version": "0.43.0",
3
+ "version": "0.45.0",
4
4
  "description": "KOL app shell — sidenav, layout, theme toggle, footer, heroes + brand color layer. SideNav takes nav data as props so the tree stays app-local.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,8 +21,8 @@
21
21
  "react-router-dom": "^6.0.0 || ^7.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@kolkrabbi/kol-component": "^0.181.0",
25
- "@kolkrabbi/kol-icons": "^0.25.0"
24
+ "@kolkrabbi/kol-component": "^0.228.0",
25
+ "@kolkrabbi/kol-icons": "^0.29.0"
26
26
  },
27
27
  "files": [
28
28
  "src",
@@ -117,6 +117,7 @@ export default function PageLayout({ navTree = [], header, footer, defaultTocCon
117
117
  data-drawer-open={drawerOpen ? 'true' : undefined}
118
118
  data-toc={showToc ? 'true' : undefined}
119
119
  >
120
+ {/* tip-ok: phone-only (md:hidden) — a hover tip never shows on touch; the aria-label names it */}
120
121
  <button
121
122
  type="button"
122
123
  className="kol-sidenav-hamburger md:hidden fixed top-3 left-3 z-30 w-10 h-10 inline-flex items-center justify-center rounded-full bg-surface-primary border border-fg-08 text-emphasis"
@@ -128,12 +128,12 @@ export default function ShellHeader({
128
128
  <div className="flex shrink-0 items-center gap-1">
129
129
  {actions}
130
130
  {showThemeToggle && (
131
- <Tooltip label="Toggle theme">
131
+ <>
132
132
  {/* md at every width (user re-rule 2026-08-09: the whole
133
133
  * header glyph row rides md, full ink — the earlier lg-at-lg+
134
134
  * split is repealed, which also collapsed the two spans). */}
135
135
  <ThemeToggle fill="none" label={false} size="md" />
136
- </Tooltip>
136
+ </>
137
137
  )}
138
138
  {onMenuClick && (
139
139
  <Tooltip label="Open navigation menu">
package/src/SideNav.jsx CHANGED
@@ -4,7 +4,7 @@ import { createPortal } from 'react-dom'
4
4
  import { Icon } from '@kolkrabbi/kol-icons'
5
5
  import { Tooltip } from '@kolkrabbi/kol-component'
6
6
  import ThemeToggle from './ThemeToggle'
7
- import useDragResize from './useDragResize'
7
+ import { useDragResize } from '@kolkrabbi/kol-component'
8
8
 
9
9
  /**
10
10
  * SideNav — the grouped navigation rail.
@@ -1,4 +1,5 @@
1
1
  import { Icon } from '@kolkrabbi/kol-icons'
2
+ import { Tooltip } from '@kolkrabbi/kol-component'
2
3
  import { useTheme } from './theme.js'
3
4
 
4
5
  /**
@@ -88,6 +89,12 @@ export default function ThemeToggle({
88
89
  * ink, so the toggle beside a sunken Dropdown matches it; `inverse` aliased. Rules in kol-theme. */
89
90
  tone = 'default',
90
91
  className = '',
92
+ /* THE TOGGLE CARRIES ITS OWN TOOLTIP (2026-09-28). It set a native `title`
93
+ * AND the header wrapped it in a KOL Tooltip, so hovering showed both — the
94
+ * browser's "Switch to light mode · following your system" under the DS's
95
+ * "Toggle theme". One tip now, the DS one, with the richer text; `false`
96
+ * leaves the button bare for a caller that wraps it itself. */
97
+ tooltip = true,
91
98
  }) {
92
99
  const { theme, mode, cycle, clear } = useTheme()
93
100
  /* six tones (tone-is-the-ground-axis, 2026-09-03); unset inherits the wrapper's */
@@ -149,12 +156,13 @@ export default function ThemeToggle({
149
156
  type: 'button',
150
157
  onClick,
151
158
  'aria-label': `Switch to ${next} mode`,
152
- title: `Switch to ${next} mode${resetHint}`,
153
159
  }
160
+ const tip = `Switch to ${next} mode${resetHint}`
161
+ const wrap = (node) => (tooltip ? <Tooltip label={tip}>{node}</Tooltip> : node)
154
162
 
155
163
  /* ── DEPRECATED aliases — old chrome verbatim (0.6.x) ── */
156
164
  if (variant === 'icon') {
157
- return (
165
+ return wrap(
158
166
  <button {...shared} className={`kol-theme-toggle kol-theme-toggle-none kol-btn-${size} kol-btn-icon ${extra}`.trim()}>
159
167
  {iconSwap(glyphSolo)}
160
168
  </button>
@@ -176,7 +184,7 @@ export default function ThemeToggle({
176
184
  const chromeCls = bare
177
185
  ? `w-full inline-flex items-center justify-start gap-2 ${barePadY} px-6 ${mono} bg-transparent text-emphasis`
178
186
  : 'kol-theme-toggle kol-theme-toggle-subtle kol-btn-md kol-mono-14 w-full justify-start gap-2'
179
- return (
187
+ return wrap(
180
188
  <button {...shared} className={`${chromeCls} ${extra}`.trim()}>
181
189
  <span className="inline-flex items-center justify-center shrink-0" aria-hidden="true">
182
190
  {iconSwap(bare ? glyphWithText : glyphSolo)}
@@ -194,7 +202,7 @@ export default function ThemeToggle({
194
202
  if (variant === 'flush') {
195
203
  const glyph = label ? glyphWithText : glyphSolo
196
204
  const cls = `kol-theme-toggle kol-theme-toggle-flush gap-2 ${mono} ${width} ${extra}`
197
- return (
205
+ return wrap(
198
206
  <button {...shared} className={cls.replace(/\s+/g, ' ').trim()}>
199
207
  {!iconRight && iconSwap(glyph)}
200
208
  {label && <span className={fullWidth ? 'flex-1 min-w-0 text-left' : ''}>{MODE_LABEL[next]}</span>}
@@ -209,13 +217,13 @@ export default function ThemeToggle({
209
217
  const fillCls = fill === 'none' ? 'kol-theme-toggle-none' : 'kol-theme-toggle-subtle'
210
218
  if (!label) {
211
219
  // icon-only pins the square box per rung — geometry condition, not a variant
212
- return (
220
+ return wrap(
213
221
  <button {...shared} className={`kol-theme-toggle ${fillCls} kol-btn-${size} kol-btn-icon ${width} ${extra}`.replace(/\s+/g, ' ').trim()}>
214
222
  {iconSwap(glyphSolo)}
215
223
  </button>
216
224
  )
217
225
  }
218
- return (
226
+ return wrap(
219
227
  <button {...shared} className={`kol-theme-toggle ${fillCls} kol-btn-${size} ${mono} gap-2 ${width} ${extra}`.replace(/\s+/g, ' ').trim()}>
220
228
  {!iconRight && iconSwap(glyphWithText)}
221
229
  <span className={fullWidth ? 'flex-1 min-w-0 text-left' : ''}>{MODE_LABEL[next]}</span>
package/src/index.js CHANGED
@@ -10,7 +10,10 @@
10
10
  export { default as PageLayout, default as AppShell } from './PageLayout.jsx'
11
11
  export { ShellTocContext, ShellTocCollapsedContext } from './PageLayout.jsx'
12
12
  export { default as SideNav } from './SideNav.jsx'
13
- export { default as useDragResize } from './useDragResize.js'
13
+ /* MOVED to kol-component 2026-09-03 (editor-set-is-behind-its-source): its
14
+ * EditorShell needs the same rails and cannot import this package. Re-exported
15
+ * here under the name it has always had — this specifier stays valid. */
16
+ export { useDragResize } from '@kolkrabbi/kol-component'
14
17
  export { default as ShellHeader, HEADER_ICON } from './ShellHeader.jsx'
15
18
  export { default as ThemeToggle } from './ThemeToggle.jsx'
16
19
  export { useTheme, applyTheme, getInitialTheme, THEME_STORAGE_KEY, THEME_BOOT_SCRIPT } from './theme.js'
@@ -1,311 +0,0 @@
1
- import { useEffect, useMemo, useRef, useState } from 'react'
2
- /* THE SAME GRAB AS THE SHELL RAIL (OneGrabGestureBothRails, kol-fxr 2026-08-30 —
3
- * user: *"why dont we use the grab animation and other sidenav settings to be
4
- * consistent?"*). fxr's /labs shows both rails at once and only the left one
5
- * woke as the cursor neared it, because each package had built the gesture
6
- * itself. The hook lives in kol-component, the one package both can reach —
7
- * kol-shell dropped its framework peer in 0.16.0, so neither could import the
8
- * other's. */
9
- import { useGrabEdge } from '@kolkrabbi/kol-component'
10
-
11
- /* Grab-edge resize + collapse for SideNav — THE single control since 0.17.0
12
- * (user build order 2026-08-09, completing the SideNavGrabResize brief: the
13
- * chip Button is gone in both states; the pill-marked edge does everything).
14
- * Logic lifted from the hand-tested brand proto (kol-website _tmp
15
- * useDragResizeProto.js), which forked this hook's 0.16.0 form.
16
- *
17
- * - CLICK TOGGLES expand↔collapse: pointerup with < 3px of travel is a
18
- * click, never a resize — drags only start past the slop, so toggle
19
- * presses can't jitter the rail.
20
- * - DRAG resizes; live width goes to --kol-sidenav-w on :root so the grid,
21
- * shell-header brand block and aside follow one number. Dragging under
22
- * --kol-sidenav-snap stamps :root[data-sidenav="collapsed"].
23
- * - SNAP-TO-DEFAULT: releasing within --kol-sidenav-snap-default of the
24
- * stylesheet default clears the override — you land exactly on default,
25
- * never 249px or 263px.
26
- * - DOUBLE-CLICK RESET IS GONE — it cannot coexist with click-toggle (two
27
- * clicks would toggle-toggle-reset). Home keeps the reset; the snap band
28
- * covers pointer users.
29
- * - Keyboard on the focused separator: arrows resize by --kol-sidenav-step
30
- * (ArrowLeft past the snap collapses), Home resets, Enter/Space toggle.
31
- * - Width + state survive reload ('kol-sidenav' keeps the consumers'
32
- * existing 'collapsed'|'expanded' schema; width under its own key).
33
- *
34
- * Every value the gesture needs is a --<token>-* custom property in the
35
- * consumer's CSS — no literals here except the click slop, which is a
36
- * gesture constant, not chrome. Missing tokens leave the gesture inert.
37
- *
38
- * SIDE-AGNOSTIC since ThreeColumnEditorShell (kol-fxr, 2026-08-15): the name
39
- * family and the drag direction are both arguments now, so a right-hand
40
- * inspector rail reuses this gesture — pointer, keyboard, snap, collapse and
41
- * persistence — instead of reimplementing it. The bullets above describe the
42
- * DEFAULT ('kol-sidenav', side 'left'), which is byte-identical to 0.17.0. */
43
-
44
- const CLICK_SLOP_PX = 3
45
-
46
- const root = () => document.documentElement
47
-
48
- /* Every name the gesture touches, derived from ONE token (ThreeColumnEditorShell,
49
- * filed from kol-fxr 2026-08-15). The default token reproduces the hardcoded
50
- * 0.17.0 names EXACTLY — 'kol-sidenav' → data-sidenav, --kol-sidenav-w,
51
- * storage 'kol-sidenav'/'kol-sidenav-w' — so SideNav and every existing caller
52
- * are untouched by this generalisation.
53
- *
54
- * The data-attribute drops the `kol-` prefix because that is what the shipped
55
- * CSS already selects (`:root[data-sidenav="collapsed"]`), not a new scheme. */
56
- export function buildNames(token) {
57
- const base = token.replace(/^kol-/, '')
58
- return {
59
- stateKey: token,
60
- widthKey: `${token}-w`,
61
- collapsedAttr: `data-${base}`,
62
- draggingAttr: `data-${base}-dragging`,
63
- wVar: `--${token}-w`,
64
- collapsedVar: `--${token}-w-collapsed`,
65
- snapVar: `--${token}-snap`,
66
- stepVar: `--${token}-step`,
67
- snapDefaultVar: `--${token}-snap-default`,
68
- }
69
- }
70
-
71
- /* Resolve a length token to px, or null when it is absent/unparsable. */
72
- function readVarPx(name) {
73
- const raw = getComputedStyle(root()).getPropertyValue(name).trim()
74
- const n = parseFloat(raw)
75
- if (!raw || Number.isNaN(n)) return null
76
- return raw.endsWith('rem') ? n * parseFloat(getComputedStyle(root()).fontSize) : n
77
- }
78
-
79
- /* Imperative DOM writes — pointermove must never re-render the nav tree.
80
- * React state syncs from the DOM at rest (release / key press / reset). */
81
- const stampCollapsed = (n, on) => {
82
- if (on) root().setAttribute(n.collapsedAttr, 'collapsed')
83
- else root().removeAttribute(n.collapsedAttr)
84
- }
85
- const writeWidth = (n, px) => {
86
- if (px == null) root().style.removeProperty(n.wVar)
87
- else root().style.setProperty(n.wVar, `${px}px`)
88
- }
89
- const readBack = (n) => {
90
- const inline = parseFloat(root().style.getPropertyValue(n.wVar))
91
- return {
92
- collapsed: root().getAttribute(n.collapsedAttr) === 'collapsed',
93
- widthPx: Number.isNaN(inline) ? null : inline,
94
- }
95
- }
96
-
97
- /* @param ref the panel being resized — its measured width seeds the drag
98
- * @param options { token, side }
99
- * token — the CSS/storage name family. Default 'kol-sidenav' (SideNav).
100
- * A right-hand inspector passes its own, e.g. 'kol-rail', so the two
101
- * rails never share one :root variable and drag together.
102
- * side — which EDGE the grab handle sits on. 'left' (default) is a rail on
103
- * the left of the viewport whose handle is on its right edge, so
104
- * rightward drag = wider. 'right' inverts both the pointer sign and
105
- * the arrow keys.
106
- * persistWidth — remember a dragged WIDTH across sessions. **Default false**
107
- * (sidenav-drag-width-outranks-breakpoints, kol-client-olina
108
- * 2026-09-03; user: *"why would you want that in persistent memory?
109
- * and even if you did, shouldnt it be an opt in via prop? off by
110
- * default"*).
111
- *
112
- * It used to be unconditional, and the width was replayed on boot as
113
- * an INLINE custom property on `:root` — which outranks every
114
- * stylesheet rule, so one drag on a laptop killed both shipped rungs
115
- * (`--kol-sidenav-w: 264px` and the 320px `min-width:1536px` rule)
116
- * permanently, on every machine, with no UI saying so and no selector
117
- * a consumer could beat. Measured at 1600×900: a stored 210 rendered
118
- * 210 and the 1536 rung did nothing. A one-off gesture had become a
119
- * permanent global.
120
- *
121
- * Off, a drag still works and lasts the session; the rail follows the
122
- * stylesheet and its breakpoints on the next boot. On, the old
123
- * behaviour returns for an app that genuinely wants the rail
124
- * remembered — and it is still an inline stamp, so an app opting in is
125
- * choosing to outrank its own breakpoints. That precedence is worth
126
- * inverting, but it is a bigger change than this ticket asked for.
127
- *
128
- * STATE is untouched: collapsed/expanded still persists via
129
- * `stateKey`, unconditionally, as it always did. The ticket says the
130
- * two are separate questions and only asks about the width. */
131
- export default function useDragResize(ref, options = {}) {
132
- const { token = 'kol-sidenav', side = 'left', defaultCollapsed = false, persistWidth = false } = options
133
- /* -1 on a right-hand rail: the same rightward pointer travel that widens a
134
- * left rail must NARROW a right one, because its handle faces the canvas. */
135
- const dir = side === 'right' ? -1 : 1
136
- const names = useMemo(() => buildNames(token), [token])
137
-
138
- /* the handle's own ref, so the shared gesture can find it. Returned in
139
- * `grabProps`, so a consumer already spreading those gets the wake, the
140
- * travel and the dwell with no change. */
141
- const grabRef = useRef(null)
142
- useGrabEdge(grabRef)
143
- const drag = useRef(null) // { startX, startW, snapPx, maxPx, moved } during a drag
144
- const defaultPx = useRef(null)
145
- const collapsedPx = useRef(null)
146
- const [collapsed, setCollapsed] = useState(false)
147
- const [widthPx, setWidthPx] = useState(null) // null = stylesheet default
148
-
149
- const syncAndPersist = () => {
150
- const { collapsed: c, widthPx: w } = readBack(names)
151
- setCollapsed(c)
152
- setWidthPx(w)
153
- try {
154
- localStorage.setItem(names.stateKey, c ? 'collapsed' : 'expanded')
155
- /* width only when asked for; off, the key is not written AND any key a
156
- * previous version left behind is cleared, so a consumer that bumps stops
157
- * replaying a width it never opted into */
158
- if (!persistWidth || w == null) localStorage.removeItem(names.widthKey)
159
- else localStorage.setItem(names.widthKey, String(Math.round(w)))
160
- } catch { /* storage blocked */ }
161
- }
162
-
163
- const toggleCollapsed = () => {
164
- const { collapsed: c } = readBack(names)
165
- stampCollapsed(names, !c)
166
- syncAndPersist()
167
- }
168
-
169
- /* Boot: capture the stylesheet defaults BEFORE any inline override lands,
170
- * then restore the persisted width/state. */
171
- useEffect(() => {
172
- defaultPx.current = readVarPx(names.wVar)
173
- collapsedPx.current = readVarPx(names.collapsedVar)
174
- let w = null
175
- let stored = null
176
- try {
177
- /* off → never read it, and drop a key an earlier version wrote */
178
- if (persistWidth) w = parseFloat(localStorage.getItem(names.widthKey)) || null
179
- else localStorage.removeItem(names.widthKey)
180
- stored = localStorage.getItem(names.stateKey)
181
- } catch { /* storage blocked */ }
182
- /* nothing stored → the consumer's boot state (RailSideNavPixelParity,
183
- * 2026-08-28): an app rail boots collapsed, the brand sidebar boots open;
184
- * the first drag or click persists and the default never speaks again */
185
- const c = stored ? stored === 'collapsed' : !!defaultCollapsed
186
- if (w) { writeWidth(names, w); setWidthPx(w) }
187
- if (c) { stampCollapsed(names, true); setCollapsed(true) }
188
- }, [names, defaultCollapsed, persistWidth])
189
-
190
- useEffect(() => {
191
- const onMove = (e) => {
192
- if (!drag.current) return
193
- const d = drag.current
194
- const dx = e.clientX - d.startX
195
- /* Below the slop the pointer is still a CLICK — resizing from the
196
- * first pixel would jitter the rail on every toggle press. */
197
- if (!d.moved) {
198
- if (Math.abs(dx) < CLICK_SLOP_PX) return
199
- d.moved = true
200
- }
201
- const next = d.startW + dx * dir
202
- if (next < d.snapPx) {
203
- stampCollapsed(names, true)
204
- } else {
205
- stampCollapsed(names, false)
206
- writeWidth(names, Math.min(next, d.maxPx))
207
- }
208
- }
209
- const onUp = () => {
210
- if (!drag.current) return
211
- const { moved } = drag.current
212
- drag.current = null
213
- root().removeAttribute(names.draggingAttr)
214
- document.body.style.cursor = ''
215
- document.body.style.userSelect = ''
216
- if (!moved) { toggleCollapsed(); return } // a click, not a drag
217
- /* Snap-to-default: release near the stylesheet default clears the
218
- * override entirely. */
219
- const { collapsed: c, widthPx: w } = readBack(names)
220
- const band = readVarPx(names.snapDefaultVar) ?? readVarPx(names.stepVar) ?? 16
221
- if (!c && w != null && defaultPx.current != null && Math.abs(w - defaultPx.current) <= band) {
222
- writeWidth(names, null)
223
- }
224
- syncAndPersist()
225
- }
226
- window.addEventListener('pointermove', onMove)
227
- window.addEventListener('pointerup', onUp)
228
- return () => {
229
- window.removeEventListener('pointermove', onMove)
230
- window.removeEventListener('pointerup', onUp)
231
- }
232
- }, [names, dir])
233
-
234
- const onPointerDown = (e) => {
235
- const snapPx = readVarPx(names.snapVar)
236
- if (defaultPx.current == null || snapPx == null) return // tokens absent → inert
237
- e.preventDefault()
238
- drag.current = {
239
- startX: e.clientX,
240
- startW: ref.current?.getBoundingClientRect().width ?? defaultPx.current,
241
- snapPx,
242
- /* mirror's ceiling (default × 3), resolved from the token not hardcoded */
243
- maxPx: defaultPx.current * 3,
244
- moved: false,
245
- }
246
- /* the grid's grid-template-columns ease would trail the pointer —
247
- * kol-framework.css suspends it while this attribute is stamped */
248
- root().setAttribute(names.draggingAttr, '')
249
- document.body.style.cursor = 'col-resize'
250
- document.body.style.userSelect = 'none'
251
- }
252
-
253
- const resetToDefault = () => {
254
- stampCollapsed(names, false)
255
- writeWidth(names, null)
256
- syncAndPersist()
257
- }
258
-
259
- const onKeyDown = (e) => {
260
- const snapPx = readVarPx(names.snapVar)
261
- const stepPx = readVarPx(names.stepVar)
262
- if (defaultPx.current == null || snapPx == null || stepPx == null) return
263
- const { collapsed: c, widthPx: w } = readBack(names)
264
- const current = w ?? defaultPx.current
265
- /* The arrow that GROWS is the one pointing away from the rail's own edge —
266
- * ArrowRight on a left rail, ArrowLeft on a right one. Same inversion the
267
- * pointer gets, so keyboard and drag never disagree. */
268
- const growKey = dir === 1 ? 'ArrowRight' : 'ArrowLeft'
269
- const shrinkKey = dir === 1 ? 'ArrowLeft' : 'ArrowRight'
270
- if (e.key === growKey) {
271
- e.preventDefault()
272
- if (c) stampCollapsed(names, false)
273
- else writeWidth(names, Math.min(current + stepPx, defaultPx.current * 3))
274
- syncAndPersist()
275
- } else if (e.key === shrinkKey) {
276
- e.preventDefault()
277
- if (c) return
278
- const next = current - stepPx
279
- if (next < snapPx) stampCollapsed(names, true)
280
- else writeWidth(names, next)
281
- syncAndPersist()
282
- } else if (e.key === 'Home') {
283
- e.preventDefault()
284
- resetToDefault()
285
- } else if (e.key === 'Enter' || e.key === ' ') {
286
- e.preventDefault()
287
- toggleCollapsed()
288
- }
289
- }
290
-
291
- return {
292
- collapsed,
293
- toggleCollapsed,
294
- grabProps: {
295
- ref: grabRef,
296
- /* the pill is drawn by `.kol-rail-grab` (kol-animation.css) — the hook
297
- * only supplies `is-near` and `--kol-rail-grab-y`. A consumer's own
298
- * className, spread after this, still wins. */
299
- className: 'kol-rail-grab',
300
- role: 'separator',
301
- 'aria-orientation': 'vertical',
302
- 'aria-label': 'Resize navigation',
303
- 'aria-valuenow': Math.round(collapsed ? collapsedPx.current : (widthPx ?? defaultPx.current)) || undefined,
304
- 'aria-valuemin': collapsedPx.current == null ? undefined : Math.round(collapsedPx.current),
305
- 'aria-valuemax': defaultPx.current == null ? undefined : Math.round(defaultPx.current * 3),
306
- tabIndex: 0,
307
- onPointerDown,
308
- onKeyDown,
309
- },
310
- }
311
- }