@bitbaum/sitekit 0.4.2 → 0.5.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 +6 -0
- package/dist/react/SiteChrome.js +1 -1
- package/dist/react/SiteNav.d.ts +8 -1
- package/dist/react/SiteNav.js +2 -10
- package/dist/react/SiteTabBar.d.ts +25 -0
- package/dist/react/SiteTabBar.js +16 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +1 -0
- package/package.json +1 -1
- package/styles.css +87 -0
package/README.md
CHANGED
|
@@ -160,3 +160,9 @@ semantic navigation groups. Existing flat `links` remain supported. Consumers
|
|
|
160
160
|
can configure `--sk-header-height`, `--sk-mobile-header-height`,
|
|
161
161
|
`--sk-display-weight`, `--sk-display-leading`, `--sk-display-tracking` and
|
|
162
162
|
`--sk-footer-gap` through their theme adapter, without replacing shared markup.
|
|
163
|
+
|
|
164
|
+
## Mobile tab bar
|
|
165
|
+
|
|
166
|
+
`SiteTabBar` renders a fixed bottom navigation for small screens (hidden from
|
|
167
|
+
48rem up, where the masthead nav takes over). Pass 3–5 `{ path, label, icon }`
|
|
168
|
+
items and the current path; it renders its own spacer so content is not hidden.
|
package/dist/react/SiteChrome.js
CHANGED
|
@@ -15,7 +15,7 @@ import { DefaultLink } from './link.js';
|
|
|
15
15
|
import { SiteNav } from './SiteNav.js';
|
|
16
16
|
export function SiteMasthead({ chrome, navItems, 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, 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,6 +6,7 @@
|
|
|
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';
|
|
11
12
|
interface Props {
|
|
@@ -20,6 +21,12 @@ interface Props {
|
|
|
20
21
|
appearance?: 'application';
|
|
21
22
|
label?: string;
|
|
22
23
|
menuLabel?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The masthead's account actions, repeated at the foot of the mobile menu.
|
|
26
|
+
* On the narrowest phones the row has no room for them beside the brand and
|
|
27
|
+
* the menu button, so styles.css shows them here instead (one tap away).
|
|
28
|
+
*/
|
|
29
|
+
menuActions?: ReactNode;
|
|
23
30
|
}
|
|
24
|
-
export declare function SiteNav({ items, currentPath, Link, appearance, label, menuLabel, }: Props): import("react").JSX.Element | null;
|
|
31
|
+
export declare function SiteNav({ items, currentPath, Link, appearance, label, menuLabel, menuActions, }: Props): import("react").JSX.Element | null;
|
|
25
32
|
export {};
|
package/dist/react/SiteNav.js
CHANGED
|
@@ -1,22 +1,14 @@
|
|
|
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, currentPath, Link = DefaultLink, appearance, label = 'Sections', menuLabel = 'Menu', menuActions, }) {
|
|
15
7
|
if (appearance === 'application') {
|
|
16
8
|
if (items.length === 0)
|
|
17
9
|
return null;
|
|
18
10
|
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" })] }),
|
|
11
|
+
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
12
|
}
|
|
21
13
|
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
14
|
const isCurrent = item.path === currentPath;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mobile bottom tab bar: three to five destinations within thumb reach.
|
|
3
|
+
*
|
|
4
|
+
* A server component like SiteNav (the injected Link is a function). It is
|
|
5
|
+
* hidden from the desktop breakpoint up by styles.css (`--sk-tabbar-hide`,
|
|
6
|
+
* default 48rem) so the masthead nav stays the one desktop navigation.
|
|
7
|
+
* Consumers add `padding-bottom: var(--sk-tabbar-height)` to their page body
|
|
8
|
+
* on small screens, or use the `.sk-tabbar-spacer` the component renders.
|
|
9
|
+
*/
|
|
10
|
+
import type { ReactNode } from 'react';
|
|
11
|
+
import { type LinkLike } from './link.js';
|
|
12
|
+
export interface SiteTabItem {
|
|
13
|
+
path: string;
|
|
14
|
+
label: string;
|
|
15
|
+
/** Decorative glyph or SVG; hidden from assistive tech. */
|
|
16
|
+
icon: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
interface Props {
|
|
19
|
+
items: SiteTabItem[];
|
|
20
|
+
currentPath: string;
|
|
21
|
+
Link?: LinkLike;
|
|
22
|
+
label?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function SiteTabBar({ items, currentPath, Link, label }: Props): import("react").JSX.Element | null;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { href } from '../model.js';
|
|
3
|
+
import { DefaultLink } from './link.js';
|
|
4
|
+
const isCurrent = (path, current) => {
|
|
5
|
+
const a = href(path);
|
|
6
|
+
const b = href(current);
|
|
7
|
+
return a === '/' ? b === '/' : b === a || b.startsWith(a + '/');
|
|
8
|
+
};
|
|
9
|
+
export function SiteTabBar({ items, currentPath, Link = DefaultLink, label = 'Main' }) {
|
|
10
|
+
if (items.length === 0)
|
|
11
|
+
return null;
|
|
12
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sk-tabbar-spacer", "aria-hidden": "true" }), _jsx("nav", { className: "sk-tabbar", "aria-label": label, children: items.slice(0, 5).map((item) => {
|
|
13
|
+
const current = isCurrent(item.path, currentPath);
|
|
14
|
+
return (_jsxs(Link, { href: href(item.path), "aria-current": current ? 'page' : undefined, className: "sk-tab", children: [_jsx("span", { className: "sk-tab-icon", "aria-hidden": "true", children: item.icon }), _jsx("span", { className: "sk-tab-label", children: item.label })] }, item.path));
|
|
15
|
+
}) })] }));
|
|
16
|
+
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
export { SiteSections } from './SiteSections.js';
|
|
10
10
|
export { SiteMasthead, SiteFooter } from './SiteChrome.js';
|
|
11
11
|
export { SiteNav } from './SiteNav.js';
|
|
12
|
+
export { SiteTabBar, type SiteTabItem } from './SiteTabBar.js';
|
|
12
13
|
export { NavAutoScroll } from './NavAutoScroll.js';
|
|
13
14
|
export { DefaultLink, type LinkLike, type LinkProps } from './link.js';
|
|
14
15
|
export { SectionHeading, Blurb, SectionBody } from './sections/Primitives.js';
|
package/dist/react/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
export { SiteSections } from './SiteSections.js';
|
|
10
10
|
export { SiteMasthead, SiteFooter } from './SiteChrome.js';
|
|
11
11
|
export { SiteNav } from './SiteNav.js';
|
|
12
|
+
export { SiteTabBar } from './SiteTabBar.js';
|
|
12
13
|
export { NavAutoScroll } from './NavAutoScroll.js';
|
|
13
14
|
export { DefaultLink } from './link.js';
|
|
14
15
|
export { SectionHeading, Blurb, SectionBody } from './sections/Primitives.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitbaum/sitekit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
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;
|
|
@@ -306,3 +339,57 @@
|
|
|
306
339
|
gap: 1.5rem;
|
|
307
340
|
}
|
|
308
341
|
}
|
|
342
|
+
/* Mobile bottom tab bar (SiteTabBar). Hidden from --sk-tabbar-breakpoint up. */
|
|
343
|
+
.sk-tabbar {
|
|
344
|
+
position: fixed;
|
|
345
|
+
inset: auto 0 0 0;
|
|
346
|
+
z-index: 45;
|
|
347
|
+
display: flex;
|
|
348
|
+
justify-content: space-around;
|
|
349
|
+
gap: 2px;
|
|
350
|
+
padding: 6px 6px calc(6px + env(safe-area-inset-bottom));
|
|
351
|
+
background: var(--sk-bg, Canvas);
|
|
352
|
+
border-top: 1px solid var(--sk-line, #d6d6d6);
|
|
353
|
+
font-family: var(--sk-font, system-ui, sans-serif);
|
|
354
|
+
}
|
|
355
|
+
.sk-tab {
|
|
356
|
+
flex: 1;
|
|
357
|
+
min-width: 0;
|
|
358
|
+
display: flex;
|
|
359
|
+
flex-direction: column;
|
|
360
|
+
align-items: center;
|
|
361
|
+
gap: 2px;
|
|
362
|
+
padding: 6px 2px;
|
|
363
|
+
border-radius: 10px;
|
|
364
|
+
color: var(--sk-muted, #555);
|
|
365
|
+
font-size: 11px;
|
|
366
|
+
font-weight: 600;
|
|
367
|
+
text-decoration: none;
|
|
368
|
+
}
|
|
369
|
+
.sk-tab-icon {
|
|
370
|
+
font-size: 20px;
|
|
371
|
+
line-height: 1;
|
|
372
|
+
}
|
|
373
|
+
.sk-tab-label {
|
|
374
|
+
white-space: nowrap;
|
|
375
|
+
overflow: hidden;
|
|
376
|
+
text-overflow: ellipsis;
|
|
377
|
+
max-width: 100%;
|
|
378
|
+
}
|
|
379
|
+
.sk-tab[aria-current='page'] {
|
|
380
|
+
color: var(--sk-fg, CanvasText);
|
|
381
|
+
background: var(--sk-tab-active, color-mix(in srgb, currentColor 8%, transparent));
|
|
382
|
+
}
|
|
383
|
+
.sk-tab:focus-visible {
|
|
384
|
+
outline: 2px solid var(--sk-focus, Highlight);
|
|
385
|
+
outline-offset: 2px;
|
|
386
|
+
}
|
|
387
|
+
.sk-tabbar-spacer {
|
|
388
|
+
height: var(--sk-tabbar-height, 68px);
|
|
389
|
+
}
|
|
390
|
+
@media (min-width: 48rem) {
|
|
391
|
+
.sk-tabbar,
|
|
392
|
+
.sk-tabbar-spacer {
|
|
393
|
+
display: none;
|
|
394
|
+
}
|
|
395
|
+
}
|