@kolkrabbi/kol-framework 0.53.0 → 0.54.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 +53 -21
- package/package.json +5 -5
- package/src/CategoryPage.jsx +45 -0
- package/src/PageSection.jsx +1 -1
- package/src/SideNav.jsx +5 -2
- package/src/{PageLayout.jsx → SiteLayout.jsx} +52 -24
- package/src/ThemeToggle.jsx +27 -0
- package/src/index.js +5 -2
package/kol-framework.css
CHANGED
|
@@ -160,7 +160,20 @@ body {
|
|
|
160
160
|
gap: 0.75rem;
|
|
161
161
|
width: 100%;
|
|
162
162
|
position: relative;
|
|
163
|
-
|
|
163
|
+
/* the gutter is THE CHROME INSET (`--kol-pad-chrome-x`, 05-layout-systems: 20 on a phone, 24 from 768) — the rail
|
|
164
|
+
* carried a literal 1.5rem of its own until 2026-10-24, right on the desk and wrong in the phone drawer */
|
|
165
|
+
padding: 0.5rem 2.5rem 0.5rem var(--kol-pad-chrome-x);
|
|
166
|
+
/* THE CLIP MODEL (user 2026-10-24: "some shifting and not very smooth animation between collapsed and
|
|
167
|
+
* expanded"): the collapsed row is the same row with its padding and gap eased on the grid's own 180ms
|
|
168
|
+
* clock, the label fading under the rail's edge — not a second layout that snaps in while the column is
|
|
169
|
+
* still moving. The glyph slides from the gutter to the narrow column's centre; nothing jumps. */
|
|
170
|
+
white-space: nowrap;
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
transition: padding 180ms ease, gap 180ms ease;
|
|
173
|
+
}
|
|
174
|
+
.kol-sidenav-hop-label {
|
|
175
|
+
overflow: hidden;
|
|
176
|
+
transition: opacity 120ms ease;
|
|
164
177
|
}
|
|
165
178
|
/* THE HEADER SLOT (RailLogomarkAtTop, kol-fxr 2026-08-28 — user, on the
|
|
166
179
|
* raven in the footer: "why did you move the logo from top to bottom?"): the
|
|
@@ -175,14 +188,25 @@ body {
|
|
|
175
188
|
align-items: center;
|
|
176
189
|
gap: 0.75rem;
|
|
177
190
|
width: 100%;
|
|
178
|
-
padding: 1rem 2.5rem 0
|
|
191
|
+
padding: 1rem 2.5rem 0 var(--kol-pad-chrome-x);
|
|
192
|
+
white-space: nowrap;
|
|
193
|
+
overflow: hidden;
|
|
194
|
+
transition: padding 180ms ease, gap 180ms ease;
|
|
179
195
|
}
|
|
180
196
|
/* the caret rides a PLAIN span (Icon stamps inline-flex on its own element,
|
|
181
197
|
* which would outrank the collapsed display:none) */
|
|
198
|
+
/* the caret's box lives HERE, not as a utility on the element (2026-10-24: the foldernote's caret is a <button>, and
|
|
199
|
+
* an `inline-flex` utility on it outranked the collapsed rule's display:none, so the chevron showed on the narrow
|
|
200
|
+
* rail) — a 12px line box for the span and the button alike */
|
|
182
201
|
.kol-sidenav-hop-caret {
|
|
183
202
|
position: absolute;
|
|
184
203
|
top: 0.75rem;
|
|
185
204
|
right: 0.75rem;
|
|
205
|
+
display: inline-flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: center;
|
|
208
|
+
line-height: 1;
|
|
209
|
+
transition: transform 150ms ease, opacity 120ms ease, visibility 0s linear 120ms;
|
|
186
210
|
}
|
|
187
211
|
.kol-sidenav-list {
|
|
188
212
|
display: flex;
|
|
@@ -194,11 +218,13 @@ body {
|
|
|
194
218
|
* 2026-08-26): a non-routing label sitting where a leaf sits, its rows one
|
|
195
219
|
* step further in. Type and color ride the element as utilities
|
|
196
220
|
* (kol-helper-10; text-subtle, text-emphasis when a leaf beneath it is the
|
|
197
|
-
* route); the box lives here.
|
|
198
|
-
*
|
|
221
|
+
* route); the box lives here. The `uppercase` rides the element as a utility
|
|
222
|
+
* too (user ruling 2026-10-24 on the another-creation reference, reversing the
|
|
223
|
+
* 2026-08-26 line that kept it out). */
|
|
199
224
|
.kol-sidenav-group {
|
|
200
225
|
display: block;
|
|
201
|
-
|
|
226
|
+
/* one step in from the gutter: the inset + the icon column (20) + the gap (12) */
|
|
227
|
+
padding: 0.25rem var(--kol-pad-chrome-x) 0.25rem calc(var(--kol-pad-chrome-x) + 2rem);
|
|
202
228
|
}
|
|
203
229
|
/* THE COLLAPSE RULE (WorkspaceSidebarGeometry, kol-fxr 2026-08-27 — user: "I
|
|
204
230
|
* would rather all use the same geometry layout"): a row that can survive
|
|
@@ -208,9 +234,9 @@ body {
|
|
|
208
234
|
* thing narrowing (the brand sidebar's model) instead of rows vanishing. */
|
|
209
235
|
.kol-sidenav-section {
|
|
210
236
|
display: block;
|
|
211
|
-
padding: 0.5rem
|
|
237
|
+
padding: 0.5rem var(--kol-pad-chrome-x) 0.25rem var(--kol-pad-chrome-x);
|
|
212
238
|
}
|
|
213
|
-
.kol-sidenav-footer { padding-inline:
|
|
239
|
+
.kol-sidenav-footer { padding-inline: var(--kol-pad-chrome-x) 1rem; }
|
|
214
240
|
|
|
215
241
|
/* The aside's OWN box (SideNavMobilePosition, kol-website 2026-08-25) — the
|
|
216
242
|
* hop's law above, one element up. `sticky top-0 self-start h-dvh flex
|
|
@@ -238,9 +264,11 @@ body {
|
|
|
238
264
|
* rail — one look, two triggers. */
|
|
239
265
|
:root[data-sidenav="collapsed"] .kol-brand-layout { grid-template-columns: var(--kol-sidenav-w-collapsed) minmax(0, 1fr); }
|
|
240
266
|
:root[data-sidenav="collapsed"] .kol-sidenav-hops { padding: 0 0 var(--kol-spacing-4); margin-bottom: var(--kol-spacing-4); }
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
:root[data-sidenav="collapsed"] .kol-sidenav-hop
|
|
267
|
+
/* the narrow row is the wide row with its gutter eased to the column's centre — (56 − 20) / 2 — and its label
|
|
268
|
+
* faded, so the toggle animates as one motion (the clip model, above) */
|
|
269
|
+
:root[data-sidenav="collapsed"] .kol-sidenav-hop { padding: var(--kol-spacing-2) 0 var(--kol-spacing-2) calc((var(--kol-sidenav-w-collapsed) - 1.25rem) / 2); gap: 0; }
|
|
270
|
+
:root[data-sidenav="collapsed"] .kol-sidenav-header { padding: 1rem 0 0 calc((var(--kol-sidenav-w-collapsed) - 1.25rem) / 2); gap: 0; }
|
|
271
|
+
:root[data-sidenav="collapsed"] .kol-sidenav-hop-label { opacity: 0; }
|
|
244
272
|
:root[data-sidenav="collapsed"] .kol-sidenav-body { display: none; }
|
|
245
273
|
:root[data-sidenav="collapsed"] .kol-sidenav-hop-count,
|
|
246
274
|
:root[data-sidenav="collapsed"] .kol-sidenav-section-toggle { display: none; }
|
|
@@ -248,9 +276,12 @@ body {
|
|
|
248
276
|
* port): the disclosure caret and the disclosed list are noise in a rail with
|
|
249
277
|
* no labels; the theme slot and footer centre; the footer wordmark swaps for
|
|
250
278
|
* its initial — both spans live in one <a>, so the link survives either state. */
|
|
251
|
-
:root[data-sidenav="collapsed"] .kol-sidenav-hop-caret
|
|
279
|
+
:root[data-sidenav="collapsed"] .kol-sidenav-hop-caret { opacity: 0; visibility: hidden; pointer-events: none; }
|
|
252
280
|
:root[data-sidenav="collapsed"] .kol-sidenav-section,
|
|
253
281
|
:root[data-sidenav="collapsed"] .kol-sidenav-list { display: none; }
|
|
282
|
+
@media (prefers-reduced-motion: reduce) {
|
|
283
|
+
.kol-brand-layout, .kol-sidenav-hop, .kol-sidenav-header, .kol-sidenav-hop-label, .kol-sidenav-hop-caret { transition: none; }
|
|
284
|
+
}
|
|
254
285
|
:root[data-sidenav="collapsed"] .kol-sidenav-theme-slot { padding-inline: 0; justify-content: center; }
|
|
255
286
|
:root[data-sidenav="collapsed"] .kol-sidenav-footer { padding-inline: 0; justify-content: center; }
|
|
256
287
|
.kol-sidenav-footer-mark { display: none; }
|
|
@@ -322,28 +353,29 @@ body {
|
|
|
322
353
|
}
|
|
323
354
|
|
|
324
355
|
@media (max-width: 767px) {
|
|
325
|
-
/*
|
|
326
|
-
|
|
327
|
-
|
|
356
|
+
/* THE PHONE MODE IS THE APP RAIL'S DRAWER (user 2026-10-24): the rail leaves the grid and slides in from
|
|
357
|
+
the left on the shell's curve; SiteLayout stamps `data-rail-drawer` on the root and renders the shell's
|
|
358
|
+
trigger (`.kol-shell-drawer-trigger`, top-right, hamburger/×) and scrim (`.kol-shell-drawer-scrim`) —
|
|
359
|
+
both kol-theme's, shared with AppShell. The round top-left hamburger and the opacity backdrop are in
|
|
360
|
+
`_tmp/2026-10-24-site-drawer/`. */
|
|
328
361
|
.kol-brand-layout { grid-template-columns: minmax(0, 1fr); }
|
|
329
362
|
.kol-sidenav {
|
|
330
363
|
position: fixed;
|
|
331
364
|
inset: 0 auto 0 0;
|
|
332
365
|
width: min(80vw, 280px);
|
|
333
366
|
transform: translateX(-100%);
|
|
334
|
-
transition: transform 220ms
|
|
367
|
+
transition: transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
335
368
|
}
|
|
336
369
|
.kol-sidenav.is-drawer-open { transform: translateX(0); }
|
|
370
|
+
@media (prefers-reduced-motion: reduce) { .kol-sidenav { transition: none; } }
|
|
337
371
|
/* Restore expanded labels + body inside the open drawer (override the rail rules above). */
|
|
338
|
-
|
|
339
|
-
|
|
372
|
+
/* the open drawer's row is the WIDE row — the gutter at 1.5rem as on the desk (user 2026-10-24: "mobile is missing
|
|
373
|
+
padding left?"; this line restored `padding: 8px 0`, the narrow row's zero gutter, so the rows hugged the edge) */
|
|
374
|
+
.kol-sidenav.is-drawer-open .kol-sidenav-hop { padding: 0.5rem 2.5rem 0.5rem var(--kol-pad-chrome-x); justify-content: flex-start; gap: 0.75rem; }
|
|
375
|
+
.kol-sidenav.is-drawer-open .kol-sidenav-header { padding: 1rem 2.5rem 0 var(--kol-pad-chrome-x); justify-content: flex-start; gap: 0.75rem; }
|
|
340
376
|
.kol-sidenav.is-drawer-open .kol-sidenav-hop-label { display: inline; }
|
|
341
377
|
.kol-sidenav.is-drawer-open .kol-sidenav-section { display: block; }
|
|
342
378
|
.kol-sidenav.is-drawer-open .kol-sidenav-body { display: block; }
|
|
343
|
-
.kol-brand-layout[data-drawer-open="true"] .kol-sidenav-backdrop {
|
|
344
|
-
opacity: 1;
|
|
345
|
-
pointer-events: auto;
|
|
346
|
-
}
|
|
347
379
|
}
|
|
348
380
|
|
|
349
381
|
/* SideNav primitives (.kol-sidenav-hop, -hop-icon, -hop-label, -link,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"description": "Site shell and page layout",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,16 +14,16 @@
|
|
|
14
14
|
"./kol-brand-color.css": "./kol-brand-color.css"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"@kolkrabbi/kol-component": ">=0.
|
|
17
|
+
"@kolkrabbi/kol-component": ">=0.298.0",
|
|
18
18
|
"@kolkrabbi/kol-icons": ">=0.18.0",
|
|
19
|
-
"@kolkrabbi/kol-theme": ">=0.
|
|
19
|
+
"@kolkrabbi/kol-theme": ">=0.186.0",
|
|
20
20
|
"react": "^18.3.0 || ^19.0.0",
|
|
21
21
|
"react-dom": "^18.3.0 || ^19.0.0",
|
|
22
22
|
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@kolkrabbi/kol-component": "^0.
|
|
26
|
-
"@kolkrabbi/kol-icons": "^0.
|
|
25
|
+
"@kolkrabbi/kol-component": "^0.298.0",
|
|
26
|
+
"@kolkrabbi/kol-icons": "^0.45.0"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"src",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useNavigate } from 'react-router-dom'
|
|
2
|
+
import { ContentCard, ContentCollection } from '@kolkrabbi/kol-component'
|
|
3
|
+
import PageHero from './PageHero.jsx'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CategoryPage — A category's overview, from the nav tree. the FOLDERNOTE: the page a rail category opens
|
|
7
|
+
* on — its label and lede over its pages as cards — read from the same tree the rail reads, so nothing is
|
|
8
|
+
* listed by hand and a page added to the rail is on the overview the same moment (site-hub W4, user
|
|
9
|
+
* 2026-10-24: kol-branding's three categories each hand-wrote this page).
|
|
10
|
+
*
|
|
11
|
+
* The tree is `SideNav`'s: `{ id, label, icon, lede?, pages: [{ to, label, card? }] }`. A page with a `card`
|
|
12
|
+
* is on the overview — `{ eyebrow, body, media, meta }`, the article card's fields (`media` a node or an
|
|
13
|
+
* image src); a page without one (the overview's own row, a plain link) is not. The card opens its page.
|
|
14
|
+
*
|
|
15
|
+
* @param {Array} navTree the sidenav's tree
|
|
16
|
+
* @param {string} id the category's id
|
|
17
|
+
* @param {string} [min] the collection's column minimum (ContentCollection `min`, default 280px)
|
|
18
|
+
* @param {string} [ratio] the card media's ratio (default 16 / 9)
|
|
19
|
+
*/
|
|
20
|
+
export default function CategoryPage({ navTree = [], id, min = '280px', ratio = '16 / 9' }) {
|
|
21
|
+
const navigate = useNavigate()
|
|
22
|
+
const cat = navTree.find((c) => c.id === id)
|
|
23
|
+
if (!cat) return null
|
|
24
|
+
const pages = (cat.pages ?? []).filter((p) => p.card)
|
|
25
|
+
return (
|
|
26
|
+
<div className="kol-page flex flex-col gap-10">
|
|
27
|
+
<PageHero label={cat.eyebrow} title={cat.label} lede={cat.lede} />
|
|
28
|
+
<ContentCollection min={min}>
|
|
29
|
+
{pages.map((p) => (
|
|
30
|
+
<ContentCard
|
|
31
|
+
key={p.to}
|
|
32
|
+
variant="article"
|
|
33
|
+
ratio={ratio}
|
|
34
|
+
media={typeof p.card.media === 'string' ? <img src={p.card.media} alt="" className="size-full object-cover" /> : p.card.media}
|
|
35
|
+
eyebrow={p.card.eyebrow}
|
|
36
|
+
title={p.label}
|
|
37
|
+
body={p.card.body}
|
|
38
|
+
date={p.card.meta}
|
|
39
|
+
onClick={() => navigate(p.to)}
|
|
40
|
+
/>
|
|
41
|
+
))}
|
|
42
|
+
</ContentCollection>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
package/src/PageSection.jsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Divider, SectionText } from '@kolkrabbi/kol-component'
|
|
2
2
|
|
|
3
3
|
/* The chapter section of the brand-book page kit — `PageHero` · `PageSection`
|
|
4
|
-
* · `
|
|
4
|
+
* · `SiteLayout`. Its head COMPOSES `SectionText` (page-family-is-not-a-set,
|
|
5
5
|
* 2026-09-03) instead of hand-stacking label / title / lede: the voices are the
|
|
6
6
|
* same `.kol-prose-*` trio as before, passed as the base's seams, and each class
|
|
7
7
|
* carries its own bottom margin, so the block's gap is 0 and nothing moved. */
|
package/src/SideNav.jsx
CHANGED
|
@@ -3,7 +3,7 @@ import { NavRail } from '@kolkrabbi/kol-component'
|
|
|
3
3
|
import ThemeToggle from './ThemeToggle'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* SideNav — `NavRail variant="site"` under its old name (2026-10-22, the one nav rail).
|
|
6
|
+
* SideNav — The grouped navigation rail. `NavRail variant="site"` under its old name (2026-10-22, the one nav rail).
|
|
7
7
|
*
|
|
8
8
|
* @deprecated — use `NavRail variant="site"` from `@kolkrabbi/kol-component`. This name is on the
|
|
9
9
|
* retirement ledger; it stays so every site keeps resolving.
|
|
@@ -40,6 +40,9 @@ function RouterSideNav({ onNavigate, ...props }) {
|
|
|
40
40
|
)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/* the theme is a TREE ROW (user 2026-10-24, site-hub): `ThemeToggle variant="row"` wears the rail's hop
|
|
44
|
+
* box and follows the collapse as every row does, so there is no `icon` fallback; collapsed it carries the
|
|
45
|
+
* toggle's own tooltip, as the rows carry theirs */
|
|
43
46
|
const themeSlot = (on = true) => (on === false ? false : ({ collapsed }) => (
|
|
44
|
-
<ThemeToggle variant={collapsed
|
|
47
|
+
<ThemeToggle variant="row" tooltip={collapsed} />
|
|
45
48
|
))
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createContext, useEffect, useState } from 'react'
|
|
2
2
|
import { Outlet, useLocation } from 'react-router-dom'
|
|
3
3
|
import SideNav from './SideNav.jsx'
|
|
4
|
-
import {
|
|
5
|
-
import { ModalProvider } from '@kolkrabbi/kol-component'
|
|
4
|
+
import { Button, ModalProvider } from '@kolkrabbi/kol-component'
|
|
6
5
|
|
|
7
6
|
/* Pages can register right-rail TOC content via this context.
|
|
8
7
|
* Usage: const setTocContent = useContext(ShellTocContext)
|
|
@@ -24,11 +23,23 @@ export const ShellTocCollapsedContext = createContext(null)
|
|
|
24
23
|
* (sidenav-rail-hairline, 2026-08-26): `background`, `hairline`, `isActive`,
|
|
25
24
|
* `onNavigate` … the next rail option needs no AppShell release. */
|
|
26
25
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
26
|
+
* SiteLayout — The site frame. the frame every site on the site rail stands in: the `.kol-brand-layout`
|
|
27
|
+
* grid with `NavRail variant="site"` (through `SideNav`) in its first track, the page plane in the
|
|
28
|
+
* second, the phone drawer, the modal provider and the optional TOC rail. A site is this plus a nav
|
|
29
|
+
* tree and routes — `apps/site-hub` is the reference.
|
|
30
30
|
*
|
|
31
|
-
* WAS `
|
|
31
|
+
* WAS `PageLayout` (user 2026-10-24: "why not SiteLayout instead? to at least name it with variant
|
|
32
|
+
* 'site'") — the frame is named for the rail variant it frames, as `AppShell` is for the app rail.
|
|
33
|
+
* `PageLayout` stays an alias on the retirements ledger, as `AppShell` already was.
|
|
34
|
+
*
|
|
35
|
+
* THE PHONE MODE IS THE APP RAIL'S DRAWER (user 2026-10-24: "I think we should use that hamburger
|
|
36
|
+
* siderail"): below 768 the rail goes off-canvas and slides in from the left on the shell's 220ms
|
|
37
|
+
* curve, a 32px nav Button pinned top-right swaps hamburger for × (`.kol-shell-drawer-trigger`,
|
|
38
|
+
* kol-theme), a scrim BUTTON closes on tap (`.kol-shell-drawer-scrim`; iOS Safari does not bubble
|
|
39
|
+
* taps from a div), Escape closes, and a tap on a destination closes. The round top-left hamburger
|
|
40
|
+
* and the opacity backdrop this frame carried are in `_tmp/2026-10-24-site-drawer/`.
|
|
41
|
+
*
|
|
42
|
+
* BEFORE THAT it was `AppShell` (page-family-is-not-a-set, kol-client-olina 2026-09-03). The
|
|
32
43
|
* ticket asked for a `PageLayout` to ship beside the fifteen `.kol-brand-layout`
|
|
33
44
|
* rules kol-framework already owned — every consumer `BrandLayout.jsx` in the
|
|
34
45
|
* estate is a copy of this file's markup. The component existed; the name did
|
|
@@ -58,8 +69,20 @@ export const ShellTocCollapsedContext = createContext(null)
|
|
|
58
69
|
* @param {string} pageWash the plane's wash, a CSS color (e.g. `'var(--kol-fg-02)'`)
|
|
59
70
|
* @param {boolean} bare plane + outlet only (default false)
|
|
60
71
|
*/
|
|
61
|
-
|
|
72
|
+
const NARROW = '(max-width: 767px)'
|
|
73
|
+
|
|
74
|
+
export default function SiteLayout({ navTree = [], header, footer, defaultTocContent, sideNav, pageWash, bare = false }) {
|
|
62
75
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
|
76
|
+
/* DRAWER MODE below 768 — a width query, as AppShell's `drawerBelow`; JS owns the breakpoint and
|
|
77
|
+
* stamps `data-rail-drawer` on the root, the CSS only paints */
|
|
78
|
+
const [narrow, setNarrow] = useState(() => typeof window !== 'undefined' && window.matchMedia(NARROW).matches)
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const mq = window.matchMedia(NARROW)
|
|
81
|
+
const on = () => setNarrow(mq.matches)
|
|
82
|
+
mq.addEventListener('change', on)
|
|
83
|
+
return () => mq.removeEventListener('change', on)
|
|
84
|
+
}, [])
|
|
85
|
+
useEffect(() => { if (!narrow) setDrawerOpen(false) }, [narrow])
|
|
63
86
|
const [tocContent, setTocContent] = useState(null)
|
|
64
87
|
const [tocCollapsed, setTocCollapsed] = useState(false)
|
|
65
88
|
const { pathname } = useLocation()
|
|
@@ -114,25 +137,30 @@ export default function PageLayout({ navTree = [], header, footer, defaultTocCon
|
|
|
114
137
|
* and the PLANE wears `pageWash` over it (BrandLayout's line,
|
|
115
138
|
* 2026-08-24 → 08-27). */
|
|
116
139
|
className="kol-brand-layout bg-surface-primary min-h-dvh"
|
|
117
|
-
data-drawer
|
|
140
|
+
data-rail-drawer={narrow ? (drawerOpen ? 'open' : 'closed') : undefined}
|
|
118
141
|
data-toc={showToc ? 'true' : undefined}
|
|
119
142
|
>
|
|
120
|
-
{/*
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
{/* THE TRIGGER SHIPS HERE, as the app rail's does in AppShell — hamburger closed, × open; 32px clears the
|
|
144
|
+
* 24px touch floor; pinned top-right so it never meets the left-hand panel */}
|
|
145
|
+
{narrow && (
|
|
146
|
+
<Button
|
|
147
|
+
variant="nav"
|
|
148
|
+
iconOnly={drawerOpen ? 'x' : 'hamburger'}
|
|
149
|
+
aria-label={drawerOpen ? 'Close navigation' : 'Open navigation'}
|
|
150
|
+
aria-expanded={drawerOpen}
|
|
151
|
+
className="kol-shell-drawer-trigger"
|
|
152
|
+
onClick={() => setDrawerOpen((o) => !o)}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
{narrow && drawerOpen && (
|
|
156
|
+
/* A BUTTON, not a div: iOS Safari does not bubble tap-clicks from non-interactive elements */
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
aria-label="Close navigation"
|
|
160
|
+
className="kol-shell-drawer-scrim"
|
|
161
|
+
onClick={() => setDrawerOpen(false)}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
136
164
|
|
|
137
165
|
<SideNav navTree={navTree} drawerOpen={drawerOpen} onCloseDrawer={() => setDrawerOpen(false)} {...sideNav} />
|
|
138
166
|
<div className="min-w-0" style={plane}>
|
package/src/ThemeToggle.jsx
CHANGED
|
@@ -37,6 +37,14 @@ import { useTheme } from './theme.js'
|
|
|
37
37
|
* same prop list minus `fill`; no square pinning (there is no box);
|
|
38
38
|
* size scales text + glyph only.
|
|
39
39
|
*
|
|
40
|
+
* variant "row" — the sidenav's TREE ROW (user 2026-10-24, site-hub: "this theme toggle is not
|
|
41
|
+
* the same kind of row item as the rest … just the rolling icon and pair it with a text in the
|
|
42
|
+
* same as in the rows"): `.kol-sidenav-hop` with the roll at 16 in the 20px icon column and
|
|
43
|
+
* the target's words in `.kol-sidenav-hop-label`, the rows' voice (kol-helper-12). The box is
|
|
44
|
+
* the rail's rule, so it collapses as a row does — no `icon` fallback. Takes no size, fill,
|
|
45
|
+
* label or width: a row is the rail's geometry, not the toggle's. kol-framework's SideNav
|
|
46
|
+
* passes it to the rail's theme slot.
|
|
47
|
+
*
|
|
40
48
|
* Glyph law: text-adjacent 14/16/18 (stays inside the rung line box);
|
|
41
49
|
* solo 16/20/24 (the pinned-square pairing).
|
|
42
50
|
*
|
|
@@ -162,6 +170,25 @@ export default function ThemeToggle({
|
|
|
162
170
|
const tip = 'Theme'
|
|
163
171
|
const wrap = (node) => (tooltip ? <Tooltip label={tip}>{node}</Tooltip> : node)
|
|
164
172
|
|
|
173
|
+
/* ── THE ROW — the rail's tree row verbatim (NavRailSite's `rowCls` + glyph + label; the box lives in
|
|
174
|
+
* .kol-sidenav-hop, kol-framework.css, never a utility here — the 2026-08-09 defect) ── */
|
|
175
|
+
if (variant === 'row') {
|
|
176
|
+
/* the row's tooltip is the RAIL's: to the right, on a block trigger, present in both states and merely
|
|
177
|
+
* disabled when the labels show (user 2026-10-24: collapsed, the toggle sat half off the rail — the default
|
|
178
|
+
* inline-flex trigger sized itself to the hidden label and centred wide; and a trigger that came and went
|
|
179
|
+
* remounted the row on every toggle) */
|
|
180
|
+
return (
|
|
181
|
+
<Tooltip label={tip} placement="right" triggerClassName="block w-full" disabled={!tooltip}>
|
|
182
|
+
<button {...shared} className={`kol-sidenav-hop kol-helper-12 text-emphasis bg-transparent border-0 cursor-pointer text-left ${extra}`.trim()}>
|
|
183
|
+
<span className="kol-sidenav-hop-icon inline-flex items-center justify-center w-5 h-5 shrink-0" aria-hidden="true">
|
|
184
|
+
{iconSwap(16)}
|
|
185
|
+
</span>
|
|
186
|
+
<span className="kol-sidenav-hop-label flex-1 min-w-0">{MODE_LABEL[next]}</span>
|
|
187
|
+
</button>
|
|
188
|
+
</Tooltip>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
165
192
|
/* ── DEPRECATED aliases — old chrome verbatim (0.6.x) ── */
|
|
166
193
|
if (variant === 'icon') {
|
|
167
194
|
return wrap(
|
package/src/index.js
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* and kol-brand-color.css.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/* the site frame — `SiteLayout` (2026-10-24, named for the rail variant it frames); `PageLayout` and `AppShell` are
|
|
11
|
+
* its earlier names, aliases on the retirements ledger */
|
|
12
|
+
export { default as SiteLayout, default as PageLayout, default as AppShell } from './SiteLayout.jsx'
|
|
13
|
+
export { ShellTocContext, ShellTocCollapsedContext } from './SiteLayout.jsx'
|
|
12
14
|
export { default as SideNav } from './SideNav.jsx'
|
|
13
15
|
/* MOVED to kol-component 2026-09-03 (editor-set-is-behind-its-source): its
|
|
14
16
|
* EditorShell needs the same rails and cannot import this package. Re-exported
|
|
@@ -24,3 +26,4 @@ export { default as PageSection } from './PageSection.jsx'
|
|
|
24
26
|
export { default as PortalFooter } from './PortalFooter.jsx'
|
|
25
27
|
export { default as ScrollToTop } from './ScrollToTop.jsx'
|
|
26
28
|
export { default as PageHero, default as BrandHero, default as SubPageHero } from './PageHero.jsx'
|
|
29
|
+
export { default as CategoryPage } from './CategoryPage.jsx'
|