@kolkrabbi/kol-shell 0.16.1 → 0.17.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-shell",
3
- "version": "0.16.1",
3
+ "version": "0.17.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",
@@ -12,7 +12,7 @@
12
12
  "./src/*": "./src/*"
13
13
  },
14
14
  "peerDependencies": {
15
- "@kolkrabbi/kol-component": ">=0.126.0",
15
+ "@kolkrabbi/kol-component": ">=0.127.0",
16
16
  "@kolkrabbi/kol-icons": ">=0.16.0",
17
17
  "@kolkrabbi/kol-theme": ">=0.84.0",
18
18
  "gsap": "^3.13.0",
@@ -20,9 +20,9 @@
20
20
  "react-dom": "^18.3.0 || ^19.0.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@kolkrabbi/kol-component": "^0.126.0",
24
- "@kolkrabbi/kol-icons": "^0.24.0",
25
23
  "@kolkrabbi/kol-framework": "^0.35.0",
24
+ "@kolkrabbi/kol-component": "^0.127.0",
25
+ "@kolkrabbi/kol-icons": "^0.24.0",
26
26
  "@kolkrabbi/kol-theme": "^0.88.0"
27
27
  },
28
28
  "files": [
package/src/AppShell.jsx CHANGED
@@ -21,6 +21,12 @@ import TouchDeviceOverlay, { useTouchPrimary } from './TouchDeviceOverlay.jsx'
21
21
  * `kol-framework.css` is no longer required by the shell (it was, for one
22
22
  * grid rule, 0.13–0.15).
23
23
  *
24
+ * `railComponent` (RailTwoLevelSections, kol-fxr 2026-08-28) — the rail is
25
+ * rendered directly here, so proving a rail change in a consumer meant forking
26
+ * BOTH this file and `NavRail`. Pass a component with `NavRail`'s props to
27
+ * render your own; default is `NavRail`. A seam for experiments, not an
28
+ * invitation to keep a fork.
29
+ *
24
30
  * @param {Array} props.items nav items `{ icon, path, label }`
25
31
  * @param {Array} props.bottomItems items pinned below the theme toggle
26
32
  * @param {Object} props.logomark `{ svgUrl, title }` — the rail's header mark, at the top in both states, navigates to '/'
@@ -62,6 +68,7 @@ export default function AppShell({
62
68
  currentPath,
63
69
  onNavigate,
64
70
  iconComponent,
71
+ railComponent: Rail = NavRail,
65
72
  railToggleKey,
66
73
  touch = 'shell',
67
74
  appName,
@@ -122,7 +129,7 @@ export default function AppShell({
122
129
  >
123
130
  {touch === 'overlay' && <TouchDeviceOverlay appName={appName} />}
124
131
  {!navHidden && (
125
- <NavRail
132
+ <Rail
126
133
  items={items}
127
134
  bottomItems={bottomItems}
128
135
  logomark={logomark}
package/src/NavRail.jsx CHANGED
@@ -34,7 +34,30 @@ import Logomark from './Logomark.jsx'
34
34
  * has animation and gsap") — `.kol-rail-grab` in kol-theme's kol-animation.css,
35
35
  * its pointer numbers in kol-component's `utilities/motion` (`GRAB`).
36
36
  *
37
- * @param {Array} items `{ icon, path, label, sub?: [{ path, label }] }`
37
+ * TWO LEVELS (RailTwoLevelSections, kol-fxr 2026-08-28 user: "the NAV has 20px
38
+ * icons in 32px containers, maybe level below has 12px icons in 20px container
39
+ * aligned to right? … you expand you see that they have main 56 items each, which
40
+ * you won't be aware of until you actually open labs, which should not happen
41
+ * automatically"). An item with `sub` is a SECTION: the same L1 row, plus a caret.
42
+ * Its rows are L2 — a 12px glyph in a 20px box, indented so its glyph column sits
43
+ * to the RIGHT of L1's (glyph x 14 → 30), resting a rung quieter at `oq-64` and
44
+ * going `oq-96` when it is the route. fxr's labs chrome is ~44 rows across four
45
+ * method sections; on one level they flatten into an icon column built for five
46
+ * destinations and the section names have nowhere to render at all.
47
+ *
48
+ * L2 is NOT a `Button`: the icon-button ladder is sm 28 / md 32 / lg 36 and this
49
+ * rung is 20 — there is nothing below sm, and adding one is a change to that
50
+ * ladder's law, which is the user's call and not this ticket's. The row is
51
+ * written directly and keys `aria-current`, so kol-theme's
52
+ * `.kol-shell-rail .kol-btn-nav` active rules still reach it. A sub row with no
53
+ * `icon` renders label-only, as it did before this level existed.
54
+ *
55
+ * NOTHING AUTO-EXPANDS. An L2 row is behind BOTH the rail's clipped width and its
56
+ * section's own disclosure, and `open` starts `false` with no prop to get it
57
+ * wrong — arriving on a route reveals nothing, by construction rather than by a
58
+ * flag a consumer has to remember.
59
+ *
60
+ * @param {Array} items `{ icon, path, label, sub?: [{ icon?, path, label }] }`
38
61
  * @param {Array} bottomItems pinned below the rule (Settings)
39
62
  * @param {Object} logomark `{ svgUrl, title }` — the mark, and the app name beside it when open
40
63
  * @param {string} currentPath active match: '/' exact, else prefix → aria-current="page"
@@ -58,7 +81,8 @@ const openWidth = () => {
58
81
  * it lands, so it's not constantly jerking"): it targets the pointer's own
59
82
  * position and re-targets only once the pointer is `GRAB.stick` px away, so it
60
83
  * lands on you, holds while you move inside the radius, then travels to where you
61
- * are now. One window listener, rAF-throttled. */
84
+ * are now — inside `GRAB.range`, the middle band of the edge, so it never rides
85
+ * up beside the logomark. One window listener, rAF-throttled. */
62
86
  function useGrabEdge(ref) {
63
87
  useEffect(() => {
64
88
  let raf = 0
@@ -73,7 +97,9 @@ function useGrabEdge(ref) {
73
97
  const near = dist <= GRAB.near || (h.classList.contains('is-near') && dist <= GRAB.sleep)
74
98
  h.classList.toggle('is-near', near)
75
99
  if (!near) return
76
- const along = Math.min(Math.max(clientY - r.top, 0), r.height)
100
+ /* the travel band: bipolar from the middle, GRAB.range of the height */
101
+ const edge = (r.height * (1 - GRAB.range)) / 2
102
+ const along = Math.min(Math.max(clientY - r.top, edge), r.height - edge)
77
103
  if (h.dataset.grabSeeded && Math.abs(along - Number(h.dataset.grabTarget)) < GRAB.stick) return
78
104
  h.dataset.grabTarget = String(along)
79
105
  const vars = { '--kol-rail-grab-y': `${along}px` }
@@ -138,6 +164,12 @@ function useRailDrag(railRef, grabRef, onSnap) {
138
164
 
139
165
  /* one row: the rung exactly where the closed rail had it, then the label and
140
166
  * (with `sub`) the chevron — the row clips to the rail's width */
167
+ /* the L2 glyph goes through the same injection seam Button gives L1 */
168
+ function IconAt({ name, size, component }) {
169
+ const Cmp = component || Icon
170
+ return <Cmp name={name} size={size} />
171
+ }
172
+
141
173
  function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconComponent, railOpen }) {
142
174
  const [open, setOpen] = useState(false)
143
175
  const active = path === '/' ? currentPath === '/' : currentPath.startsWith(path)
@@ -184,16 +216,37 @@ function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconCompone
184
216
  />
185
217
  )}
186
218
  </div>
187
- {railOpen && open && sub.map((s) => (
188
- <div key={s.path} className="flex items-center w-full overflow-hidden shrink-0 pl-11">
189
- <span
190
- className="kol-helper-12 uppercase text-oq-64 hover:text-oq-96 truncate cursor-pointer"
219
+ {railOpen && open && sub.map((s) => {
220
+ const on = s.path === '/' ? currentPath === '/' : currentPath.startsWith(s.path)
221
+ return (
222
+ <div
223
+ key={s.path}
224
+ /* 18px of gutter puts the 20px box at x 26, so its 12px glyph starts
225
+ * at 30 — one step right of L1's 20-in-32 at 14 (measured in fxr) */
226
+ className="flex items-center gap-3 w-full overflow-hidden shrink-0 cursor-pointer"
227
+ style={{ paddingLeft: 18 }}
191
228
  onClick={() => onNavigate?.(s.path)}
229
+ aria-current={on ? 'page' : undefined}
230
+ title={s.label}
192
231
  >
193
- {s.label}
194
- </span>
195
- </div>
196
- ))}
232
+ {s.icon && (
233
+ <span
234
+ className="w-5 h-5 shrink-0 inline-flex items-center justify-center"
235
+ style={{ color: on ? 'var(--kol-oq-96)' : 'var(--kol-oq-64)' }}
236
+ aria-hidden="true"
237
+ >
238
+ <IconAt name={s.icon} size={12} component={iconComponent} />
239
+ </span>
240
+ )}
241
+ <span
242
+ className="kol-helper-12 uppercase flex-1 min-w-0 truncate"
243
+ style={{ color: on ? 'var(--kol-oq-96)' : 'var(--kol-oq-64)' }}
244
+ >
245
+ {s.label}
246
+ </span>
247
+ </div>
248
+ )
249
+ })}
197
250
  </>
198
251
  )
199
252
  }