@bitbaum/sitekit 0.5.0 → 0.6.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/README.md +17 -0
- package/dist/react/SiteChrome.d.ts +4 -1
- package/dist/react/SiteChrome.js +2 -2
- package/dist/react/SiteNav.d.ts +16 -1
- package/dist/react/SiteNav.js +6 -10
- package/dist/react/index.d.ts +1 -1
- package/package.json +3 -1
- package/styles.css +129 -0
package/README.md
CHANGED
|
@@ -73,6 +73,23 @@ design tokens. Existing document sites keep their utility-class appearance.
|
|
|
73
73
|
links={[{ href: '/roadmap', label: 'Roadmap' }]} extra="Beta" />
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
For a grouped application menu, pass `navGroups` alongside `navItems`. Each
|
|
77
|
+
group has a `label` and `items` containing `path`, `label` and an optional
|
|
78
|
+
`description`. Import `SiteNavGroup` from `@bitbaum/sitekit/react` for typed
|
|
79
|
+
configuration. Nonempty groups replace the flat application navigation with
|
|
80
|
+
one native disclosure and a responsive panel; omitted or empty groups retain
|
|
81
|
+
the flat API. Group labels, destinations and descriptions belong to the host.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
navGroups={[{ label: 'Create', items: [
|
|
85
|
+
{ path: 'write', label: 'Write', description: 'Start an article.' }
|
|
86
|
+
] }]}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Run `pnpm exec playwright install --with-deps chromium` before `pnpm run verify`.
|
|
90
|
+
An existing Chromium can be selected with `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`.
|
|
91
|
+
The browser regression uses the public renderer and stylesheet at four widths.
|
|
92
|
+
|
|
76
93
|
The package owns the responsive menu, its native no-JavaScript fallback,
|
|
77
94
|
Escape/outside/route dismissal, skip link, active states, touch targets and
|
|
78
95
|
footer layout. Products supply labels, navigation data, identity and account
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { SiteChrome as SiteChromeSpec, SiteNavItem } from '../schema.js';
|
|
3
3
|
import { type LinkLike } from './link.js';
|
|
4
|
+
import { type SiteNavGroup } from './SiteNav.js';
|
|
4
5
|
interface Props {
|
|
5
6
|
chrome: SiteChromeSpec;
|
|
6
7
|
navItems: SiteNavItem[];
|
|
8
|
+
/** Descriptive application navigation; product-owned group configuration. */
|
|
9
|
+
navGroups?: SiteNavGroup[];
|
|
7
10
|
currentPath: string;
|
|
8
11
|
/** Framework link component (e.g. next/link). Defaults to a plain anchor. */
|
|
9
12
|
Link?: LinkLike;
|
|
@@ -16,7 +19,7 @@ interface Props {
|
|
|
16
19
|
navLabel?: string;
|
|
17
20
|
menuLabel?: string;
|
|
18
21
|
}
|
|
19
|
-
export declare function SiteMasthead({ chrome, navItems, currentPath, Link, appearance, brand, actions, mainId, skipLabel, navLabel, menuLabel, }: Props): import("react").JSX.Element;
|
|
22
|
+
export declare function SiteMasthead({ chrome, navItems, navGroups, currentPath, Link, appearance, brand, actions, mainId, skipLabel, navLabel, menuLabel, }: Props): import("react").JSX.Element;
|
|
20
23
|
export declare function SiteFooter({ chrome, appearance, links, groups, Link, label, extra, }: {
|
|
21
24
|
chrome: SiteChromeSpec;
|
|
22
25
|
appearance?: 'application';
|
package/dist/react/SiteChrome.js
CHANGED
|
@@ -13,9 +13,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
13
13
|
import { href } from '../model.js';
|
|
14
14
|
import { DefaultLink } from './link.js';
|
|
15
15
|
import { SiteNav } from './SiteNav.js';
|
|
16
|
-
export function SiteMasthead({ chrome, navItems, currentPath, Link = DefaultLink, appearance, brand, actions, mainId, skipLabel = 'Skip to content', navLabel = 'Main navigation', menuLabel = 'Menu', }) {
|
|
16
|
+
export function SiteMasthead({ chrome, navItems, navGroups, currentPath, Link = DefaultLink, appearance, brand, actions, mainId, skipLabel = 'Skip to content', navLabel = 'Main navigation', menuLabel = 'Menu', }) {
|
|
17
17
|
if (appearance === 'application')
|
|
18
|
-
return (_jsxs(_Fragment, { children: [mainId && (_jsx("a", { className: "sk-skip", href: '#' + mainId, children: skipLabel })), _jsx("header", { className: "sk-masthead", children: _jsxs("div", { className: "sk-shell-frame sk-masthead-row", children: [_jsx(Link, { href: "/", className: "sk-brand", children: brand ?? chrome.name }), _jsx(SiteNav, { items: navItems, currentPath: currentPath, Link: Link, appearance: "application", label: navLabel, menuLabel: menuLabel }), actions && _jsx("div", { className: "sk-account-actions", children: actions })] }) })] }));
|
|
18
|
+
return (_jsxs(_Fragment, { children: [mainId && (_jsx("a", { className: "sk-skip", href: '#' + mainId, children: skipLabel })), _jsx("header", { className: "sk-masthead", children: _jsxs("div", { className: "sk-shell-frame sk-masthead-row", children: [_jsx(Link, { href: "/", className: "sk-brand", children: brand ?? chrome.name }), _jsx(SiteNav, { items: navItems, groups: navGroups, currentPath: currentPath, Link: Link, appearance: "application", label: navLabel, menuLabel: menuLabel, menuActions: actions }), actions && _jsx("div", { className: "sk-account-actions", children: actions })] }) })] }));
|
|
19
19
|
return (_jsx("header", { className: "sticky top-0 z-30 border-b border-subtle bg-surface-page/85 backdrop-blur", children: _jsx("div", { className: "mx-auto max-w-shell px-4 sm:px-6 lg:px-8", children: _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-x-6 py-2", children: [_jsx(Link, { href: href(), className: "inline-flex min-h-11 shrink-0 items-center rounded focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent", children: _jsx("span", { className: "font-heading text-lg font-semibold tracking-display text-fg-primary", children: chrome.name }) }), _jsx(SiteNav, { items: navItems, currentPath: currentPath, Link: Link })] }) }) }));
|
|
20
20
|
}
|
|
21
21
|
export function SiteFooter({ chrome, appearance, links = [], groups, Link = DefaultLink, label = 'Footer navigation', extra, }) {
|
package/dist/react/SiteNav.d.ts
CHANGED
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
* client behaviour — scrolling the active item into view — lives in
|
|
7
7
|
* `NavAutoScroll`, which takes only a string.
|
|
8
8
|
*/
|
|
9
|
+
import type { ReactNode } from 'react';
|
|
9
10
|
import type { SiteNavItem } from '../schema.js';
|
|
10
11
|
import { type LinkLike } from './link.js';
|
|
12
|
+
export interface SiteNavGroup {
|
|
13
|
+
label: string;
|
|
14
|
+
items: Array<SiteNavItem & {
|
|
15
|
+
description?: string;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
11
18
|
interface Props {
|
|
12
19
|
/**
|
|
13
20
|
* Deliberately NOT `SitePage[]`. Everything the nav receives ends up in the
|
|
@@ -15,11 +22,19 @@ interface Props {
|
|
|
15
22
|
*/
|
|
16
23
|
items: SiteNavItem[];
|
|
17
24
|
currentPath: string;
|
|
25
|
+
/** Opt-in grouped application menu. Flat items remain unchanged when omitted. */
|
|
26
|
+
groups?: SiteNavGroup[];
|
|
18
27
|
/** Framework link component (e.g. next/link). Defaults to a plain anchor. */
|
|
19
28
|
Link?: LinkLike;
|
|
20
29
|
appearance?: 'application';
|
|
21
30
|
label?: string;
|
|
22
31
|
menuLabel?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The masthead's account actions, repeated at the foot of the mobile menu.
|
|
34
|
+
* On the narrowest phones the row has no room for them beside the brand and
|
|
35
|
+
* the menu button, so styles.css shows them here instead (one tap away).
|
|
36
|
+
*/
|
|
37
|
+
menuActions?: ReactNode;
|
|
23
38
|
}
|
|
24
|
-
export declare function SiteNav({ items, currentPath, Link, appearance, label, menuLabel, }: Props): import("react").JSX.Element | null;
|
|
39
|
+
export declare function SiteNav({ items, groups, currentPath, Link, appearance, label, menuLabel, menuActions, }: Props): import("react").JSX.Element | null;
|
|
25
40
|
export {};
|
package/dist/react/SiteNav.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* The masthead's section nav.
|
|
4
|
-
*
|
|
5
|
-
* A server component, deliberately: the injected `Link` is a component
|
|
6
|
-
* function, and functions cannot cross into a 'use client' boundary. The one
|
|
7
|
-
* client behaviour — scrolling the active item into view — lives in
|
|
8
|
-
* `NavAutoScroll`, which takes only a string.
|
|
9
|
-
*/
|
|
10
2
|
import { href } from '../model.js';
|
|
11
3
|
import { DefaultLink } from './link.js';
|
|
12
4
|
import { MenuDismiss } from './MenuDismiss.js';
|
|
13
5
|
import { NavAutoScroll } from './NavAutoScroll.js';
|
|
14
|
-
export function SiteNav({ items, currentPath, Link = DefaultLink, appearance, label = 'Sections', menuLabel = 'Menu', }) {
|
|
6
|
+
export function SiteNav({ items, groups, currentPath, Link = DefaultLink, appearance, label = 'Sections', menuLabel = 'Menu', menuActions, }) {
|
|
15
7
|
if (appearance === 'application') {
|
|
8
|
+
const populatedGroups = groups?.filter((group) => group.items.length > 0);
|
|
9
|
+
if (populatedGroups?.length) {
|
|
10
|
+
return (_jsxs(_Fragment, { children: [_jsxs("details", { className: "sk-grouped-menu", "data-sitekit-menu": true, children: [_jsxs("summary", { children: [menuLabel, _jsx("span", { "aria-hidden": "true", children: "\u2630" })] }), _jsxs("nav", { className: "sk-grouped-panel", "aria-label": label, children: [_jsx("div", { className: "sk-nav-groups", children: populatedGroups.map((group) => (_jsxs("section", { className: "sk-nav-group", "aria-label": group.label, children: [_jsx("h2", { children: group.label }), group.items.map((item) => (_jsxs(Link, { href: href(item.path), "aria-current": item.path === currentPath ? 'page' : undefined, children: [_jsx("span", { className: "sk-nav-link-label", children: item.label }), item.description && (_jsx("span", { className: "sk-nav-description", children: item.description }))] }, item.path)))] }, group.label))) }), menuActions && _jsx("div", { className: "sk-grouped-actions", children: menuActions })] })] }), _jsx(MenuDismiss, { currentPath: currentPath })] }));
|
|
11
|
+
}
|
|
16
12
|
if (items.length === 0)
|
|
17
13
|
return null;
|
|
18
14
|
const links = () => items.map((item) => (_jsx(Link, { href: href(item.path), "aria-current": item.path === currentPath ? 'page' : undefined, children: item.label }, item.path)));
|
|
19
|
-
return (_jsxs(_Fragment, { children: [_jsx("nav", { className: "sk-desktop-nav", "aria-label": label, "data-sitekit-nav": true, children: links() }), _jsxs("details", { className: "sk-mobile-menu", "data-sitekit-menu": true, children: [_jsxs("summary", { children: [menuLabel, _jsx("span", { "aria-hidden": "true", children: "\u2630" })] }),
|
|
15
|
+
return (_jsxs(_Fragment, { children: [_jsx("nav", { className: "sk-desktop-nav", "aria-label": label, "data-sitekit-nav": true, children: links() }), _jsxs("details", { className: "sk-mobile-menu", "data-sitekit-menu": true, children: [_jsxs("summary", { children: [menuLabel, _jsx("span", { "aria-hidden": "true", children: "\u2630" })] }), _jsxs("nav", { "aria-label": label, children: [links(), menuActions && _jsx("div", { className: "sk-menu-actions", children: menuActions })] })] }), _jsx(MenuDismiss, { currentPath: currentPath })] }));
|
|
20
16
|
}
|
|
21
17
|
return (_jsxs("nav", { "data-sitekit-nav": true, "aria-label": label, className: "scrollbar-hide -mx-1 flex max-w-full flex-nowrap items-center gap-x-1 overflow-x-auto", children: [items.map((item) => {
|
|
22
18
|
const isCurrent = item.path === currentPath;
|
package/dist/react/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { SiteSections } from './SiteSections.js';
|
|
10
10
|
export { SiteMasthead, SiteFooter } from './SiteChrome.js';
|
|
11
|
-
export { SiteNav } from './SiteNav.js';
|
|
11
|
+
export { SiteNav, type SiteNavGroup } from './SiteNav.js';
|
|
12
12
|
export { SiteTabBar, type SiteTabItem } from './SiteTabBar.js';
|
|
13
13
|
export { NavAutoScroll } from './NavAutoScroll.js';
|
|
14
14
|
export { DefaultLink, type LinkLike, type LinkProps } from './link.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitbaum/sitekit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "A website as data: a closed union of section shapes validated at runtime, one set of React renderers, and per-field provenance so a generated site can prove it fabricated nothing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Mao Nakamoto",
|
|
@@ -67,8 +67,10 @@
|
|
|
67
67
|
"@eslint/js": "^10.0.1",
|
|
68
68
|
"@types/node": "^26.4.1",
|
|
69
69
|
"@types/react": "^19.0.0",
|
|
70
|
+
"esbuild": "^0.28.2",
|
|
70
71
|
"eslint": "^10.9.1",
|
|
71
72
|
"globals": "^17.12.0",
|
|
73
|
+
"playwright": "^1.64.1",
|
|
72
74
|
"prettier": "3.9.10",
|
|
73
75
|
"react": "^19.0.0",
|
|
74
76
|
"react-dom": "^19.0.0",
|
package/styles.css
CHANGED
|
@@ -207,6 +207,39 @@
|
|
|
207
207
|
margin-inline-end: auto;
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
+
.sk-menu-actions {
|
|
211
|
+
display: none;
|
|
212
|
+
}
|
|
213
|
+
/* The narrowest phones (up to 380px, which takes in 360px Android): brand, an
|
|
214
|
+
account action and the menu button do not fit in one row (AOZ at 320px
|
|
215
|
+
needed 369px and scrolled sideways by 49px), so the action moves to the foot
|
|
216
|
+
of the menu, and only when there is a menu to hold it. */
|
|
217
|
+
@media (max-width: 380px) {
|
|
218
|
+
.sk-masthead-row:has(.sk-mobile-menu) > .sk-account-actions {
|
|
219
|
+
display: none;
|
|
220
|
+
}
|
|
221
|
+
.sk-menu-actions {
|
|
222
|
+
display: flex;
|
|
223
|
+
flex-direction: column;
|
|
224
|
+
gap: 0.5rem;
|
|
225
|
+
margin-top: 0.5rem;
|
|
226
|
+
padding-top: 0.5rem;
|
|
227
|
+
border-top: 1px solid var(--sk-line, #d6d6d6);
|
|
228
|
+
}
|
|
229
|
+
.sk-menu-actions > a {
|
|
230
|
+
display: flex;
|
|
231
|
+
align-items: center;
|
|
232
|
+
justify-content: center;
|
|
233
|
+
min-height: 44px;
|
|
234
|
+
padding: 0.65rem 1rem;
|
|
235
|
+
color: var(--sk-fg, CanvasText);
|
|
236
|
+
border: 1px solid var(--sk-line, #d6d6d6);
|
|
237
|
+
border-radius: var(--sk-radius, 0.5rem);
|
|
238
|
+
text-decoration: none;
|
|
239
|
+
font-weight: 600;
|
|
240
|
+
font-size: 0.875rem;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
210
243
|
@media (max-width: 600px) {
|
|
211
244
|
.sk-footer-top {
|
|
212
245
|
grid-template-columns: 1fr;
|
|
@@ -360,3 +393,99 @@
|
|
|
360
393
|
display: none;
|
|
361
394
|
}
|
|
362
395
|
}
|
|
396
|
+
|
|
397
|
+
/* One native disclosure and one link tree for grouped application navigation. */
|
|
398
|
+
.sk-grouped-menu {
|
|
399
|
+
margin-inline-start: auto;
|
|
400
|
+
}
|
|
401
|
+
.sk-grouped-menu summary {
|
|
402
|
+
display: flex;
|
|
403
|
+
align-items: center;
|
|
404
|
+
justify-content: center;
|
|
405
|
+
gap: 0.5rem;
|
|
406
|
+
min-height: 44px;
|
|
407
|
+
padding: 0.65rem 0.85rem;
|
|
408
|
+
list-style: none;
|
|
409
|
+
cursor: pointer;
|
|
410
|
+
border: 1px solid var(--sk-line, #d6d6d6);
|
|
411
|
+
border-radius: var(--sk-radius, 0.5rem);
|
|
412
|
+
font-size: 0.875rem;
|
|
413
|
+
font-weight: 600;
|
|
414
|
+
}
|
|
415
|
+
.sk-grouped-menu summary::-webkit-details-marker {
|
|
416
|
+
display: none;
|
|
417
|
+
}
|
|
418
|
+
.sk-grouped-panel {
|
|
419
|
+
position: absolute;
|
|
420
|
+
inset-block-start: 100%;
|
|
421
|
+
inset-inline-end: var(--sk-gutter, 1rem);
|
|
422
|
+
width: min(54rem, calc(100% - 2 * var(--sk-gutter, 1rem)));
|
|
423
|
+
box-sizing: border-box;
|
|
424
|
+
max-height: 75dvh;
|
|
425
|
+
overflow-y: auto;
|
|
426
|
+
padding: 1rem;
|
|
427
|
+
background: var(--sk-bg, Canvas);
|
|
428
|
+
border: 1px solid var(--sk-line, #d6d6d6);
|
|
429
|
+
border-radius: var(--sk-radius, 0.5rem);
|
|
430
|
+
box-shadow: 0 18px 40px #00000014;
|
|
431
|
+
}
|
|
432
|
+
.sk-nav-groups {
|
|
433
|
+
display: grid;
|
|
434
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
|
|
435
|
+
gap: 1rem;
|
|
436
|
+
}
|
|
437
|
+
.sk-nav-group {
|
|
438
|
+
min-width: 0;
|
|
439
|
+
}
|
|
440
|
+
.sk-nav-group h2 {
|
|
441
|
+
margin: 0 0 0.5rem;
|
|
442
|
+
font-size: 0.875rem;
|
|
443
|
+
color: var(--sk-muted, #555);
|
|
444
|
+
}
|
|
445
|
+
.sk-grouped-actions a,
|
|
446
|
+
.sk-nav-group a {
|
|
447
|
+
display: flex;
|
|
448
|
+
flex-direction: column;
|
|
449
|
+
justify-content: center;
|
|
450
|
+
box-sizing: border-box;
|
|
451
|
+
min-height: 44px;
|
|
452
|
+
padding: 0.75rem;
|
|
453
|
+
border-radius: var(--sk-radius, 0.5rem);
|
|
454
|
+
color: var(--sk-fg, CanvasText);
|
|
455
|
+
text-decoration: none;
|
|
456
|
+
overflow-wrap: anywhere;
|
|
457
|
+
}
|
|
458
|
+
.sk-nav-group a:hover,
|
|
459
|
+
.sk-nav-group a[aria-current='page'] {
|
|
460
|
+
background: var(--sk-raised, #f0f0f0);
|
|
461
|
+
}
|
|
462
|
+
.sk-nav-link-label {
|
|
463
|
+
font-weight: 600;
|
|
464
|
+
}
|
|
465
|
+
.sk-nav-description {
|
|
466
|
+
margin-top: 0.25rem;
|
|
467
|
+
font-size: 0.875rem;
|
|
468
|
+
line-height: 1.5;
|
|
469
|
+
color: var(--sk-muted, #555);
|
|
470
|
+
}
|
|
471
|
+
.sk-grouped-actions {
|
|
472
|
+
display: none;
|
|
473
|
+
}
|
|
474
|
+
@media (max-width: 600px) {
|
|
475
|
+
.sk-nav-groups {
|
|
476
|
+
grid-template-columns: minmax(0, 1fr);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
@media (max-width: 380px) {
|
|
480
|
+
.sk-masthead-row:has(.sk-grouped-menu) > .sk-account-actions {
|
|
481
|
+
display: none;
|
|
482
|
+
}
|
|
483
|
+
.sk-grouped-actions {
|
|
484
|
+
display: flex;
|
|
485
|
+
flex-direction: column;
|
|
486
|
+
gap: 0.5rem;
|
|
487
|
+
border-top: 1px solid var(--sk-line, #d6d6d6);
|
|
488
|
+
padding-top: 0.75rem;
|
|
489
|
+
margin-top: 0.75rem;
|
|
490
|
+
}
|
|
491
|
+
}
|