@scalewing/react 1.46.0 → 1.48.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/CHANGELOG.md +74 -0
- package/README.md +228 -4
- package/dist/app-shell.test-support.d.ts +5 -0
- package/dist/app-shell.test-support.js +45 -0
- package/dist/command-palette.test-support.d.ts +21 -0
- package/dist/command-palette.test-support.js +55 -0
- package/dist/components/AppHeader.d.ts +3 -0
- package/dist/components/AppShell.d.ts +13 -0
- package/dist/components/AppShell.js +65 -0
- package/dist/components/Band.d.ts +30 -0
- package/dist/components/Band.js +23 -0
- package/dist/components/ButtonLink.d.ts +33 -0
- package/dist/components/ButtonLink.js +27 -0
- package/dist/components/CommandPalette.d.ts +12 -0
- package/dist/components/CommandPalette.js +143 -0
- package/dist/components/CommandPaletteTrigger.d.ts +9 -0
- package/dist/components/CommandPaletteTrigger.js +16 -0
- package/dist/components/ContextSwitcher.d.ts +12 -0
- package/dist/components/ContextSwitcher.js +62 -0
- package/dist/components/FixtureGroup.d.ts +9 -0
- package/dist/components/FixtureGroup.js +16 -0
- package/dist/components/FixtureRow.d.ts +10 -0
- package/dist/components/FixtureRow.js +46 -0
- package/dist/components/GlyphTile.d.ts +9 -3
- package/dist/components/GlyphTile.js +15 -4
- package/dist/components/NavCount.d.ts +12 -5
- package/dist/components/NavCount.js +9 -4
- package/dist/components/SectionNav.d.ts +2 -1
- package/dist/components/SectionNav.js +3 -19
- package/dist/components/SiteFooter.d.ts +12 -0
- package/dist/components/SiteFooter.js +23 -0
- package/dist/components/SiteHeader.d.ts +14 -0
- package/dist/components/SiteHeader.js +24 -0
- package/dist/components/Skeleton.d.ts +22 -0
- package/dist/components/Skeleton.js +30 -0
- package/dist/components/app-shell/AppShellBar.d.ts +23 -0
- package/dist/components/app-shell/AppShellBar.js +25 -0
- package/dist/components/app-shell/AppShellDrawer.d.ts +24 -0
- package/dist/components/app-shell/AppShellDrawer.js +17 -0
- package/dist/components/app-shell/AppShellLink.d.ts +14 -0
- package/dist/components/app-shell/AppShellLink.js +27 -0
- package/dist/components/app-shell/AppShellNav.d.ts +15 -0
- package/dist/components/app-shell/AppShellNav.js +29 -0
- package/dist/components/app-shell/AppShellSidebar.d.ts +18 -0
- package/dist/components/app-shell/AppShellSidebar.js +18 -0
- package/dist/components/app-shell/types.d.ts +122 -0
- package/dist/components/app-shell/types.js +1 -0
- package/dist/components/app-shell/validate.d.ts +7 -0
- package/dist/components/app-shell/validate.js +70 -0
- package/dist/components/command-palette/PaletteList.d.ts +20 -0
- package/dist/components/command-palette/PaletteList.js +21 -0
- package/dist/components/command-palette/shortcut.d.ts +19 -0
- package/dist/components/command-palette/shortcut.js +75 -0
- package/dist/components/command-palette/types.d.ts +72 -0
- package/dist/components/command-palette/types.js +2 -0
- package/dist/components/command-palette/validate.d.ts +10 -0
- package/dist/components/command-palette/validate.js +50 -0
- package/dist/components/context-switcher/SwitcherOption.d.ts +17 -0
- package/dist/components/context-switcher/SwitcherOption.js +20 -0
- package/dist/components/context-switcher/SwitcherPopup.d.ts +31 -0
- package/dist/components/context-switcher/SwitcherPopup.js +111 -0
- package/dist/components/context-switcher/options.d.ts +33 -0
- package/dist/components/context-switcher/options.js +60 -0
- package/dist/components/context-switcher/types.d.ts +96 -0
- package/dist/components/context-switcher/types.js +1 -0
- package/dist/components/context-switcher/use-switcher-list.d.ts +29 -0
- package/dist/components/context-switcher/use-switcher-list.js +93 -0
- package/dist/components/context-switcher/validate.d.ts +8 -0
- package/dist/components/context-switcher/validate.js +94 -0
- package/dist/components/fixture/fixture-list-context.d.ts +4 -0
- package/dist/components/fixture/fixture-list-context.js +9 -0
- package/dist/components/fixture/types.d.ts +50 -0
- package/dist/components/fixture/types.js +1 -0
- package/dist/components/fixture/validate.d.ts +8 -0
- package/dist/components/fixture/validate.js +36 -0
- package/dist/components/nav-items.d.ts +21 -0
- package/dist/components/nav-items.js +26 -0
- package/dist/components/site-footer/types.d.ts +31 -0
- package/dist/components/site-footer/types.js +1 -0
- package/dist/components/site-footer/validate.d.ts +7 -0
- package/dist/components/site-footer/validate.js +22 -0
- package/dist/components/site-header/SiteHeaderLinks.d.ts +7 -0
- package/dist/components/site-header/SiteHeaderLinks.js +7 -0
- package/dist/components/site-header/types.d.ts +38 -0
- package/dist/components/site-header/types.js +1 -0
- package/dist/components/site-header/use-menu-dismiss.d.ts +8 -0
- package/dist/components/site-header/use-menu-dismiss.js +62 -0
- package/dist/components/site-header/validate.d.ts +7 -0
- package/dist/components/site-header/validate.js +17 -0
- package/dist/components/use-element-height.d.ts +10 -0
- package/dist/components/use-element-height.js +35 -0
- package/dist/context-switcher.test-support.d.ts +9 -0
- package/dist/context-switcher.test-support.js +41 -0
- package/dist/css/css-app-shell-drawer.d.ts +2 -0
- package/dist/css/css-app-shell-drawer.js +91 -0
- package/dist/css/css-app-shell-nav.d.ts +3 -0
- package/dist/css/css-app-shell-nav.js +167 -0
- package/dist/css/css-app-shell-page-bar.d.ts +3 -0
- package/dist/css/css-app-shell-page-bar.js +81 -0
- package/dist/css/css-app-shell.d.ts +4 -0
- package/dist/css/css-app-shell.js +155 -0
- package/dist/css/css-band.d.ts +10 -0
- package/dist/css/css-band.js +101 -0
- package/dist/css/css-button-link.d.ts +3 -0
- package/dist/css/css-button-link.js +48 -0
- package/dist/css/css-command-palette-key.d.ts +1 -0
- package/dist/css/css-command-palette-key.js +33 -0
- package/dist/css/css-command-palette-trigger.d.ts +2 -0
- package/dist/css/css-command-palette-trigger.js +88 -0
- package/dist/css/css-command-palette.d.ts +2 -0
- package/dist/css/css-command-palette.js +209 -0
- package/dist/css/css-components.js +27 -0
- package/dist/css/css-context-switcher-trigger.d.ts +15 -0
- package/dist/css/css-context-switcher-trigger.js +175 -0
- package/dist/css/css-context-switcher.d.ts +4 -0
- package/dist/css/css-context-switcher.js +219 -0
- package/dist/css/css-count-pill.d.ts +7 -0
- package/dist/css/css-count-pill.js +50 -12
- package/dist/css/css-fixture.d.ts +5 -0
- package/dist/css/css-fixture.js +260 -0
- package/dist/css/css-glyph-tile.d.ts +2 -0
- package/dist/css/css-glyph-tile.js +29 -1
- package/dist/css/css-site-footer.d.ts +2 -0
- package/dist/css/css-site-footer.js +152 -0
- package/dist/css/css-site-header.d.ts +2 -0
- package/dist/css/css-site-header.js +262 -0
- package/dist/css/css-skeleton.d.ts +10 -0
- package/dist/css/css-skeleton.js +110 -0
- package/dist/css/stacking.js +7 -4
- package/dist/css/stylesheet.js +18 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +11 -0
- package/dist/styles.css +2122 -512
- package/package.json +2 -2
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The pill's colours: `danger` for work waiting, `accent` for news,
|
|
3
|
+
* `warning` for work that needs the person but is not urgent.
|
|
4
|
+
*/
|
|
5
|
+
export declare const navCountTones: readonly ["danger", "accent", "warning"];
|
|
3
6
|
export type NavCountTone = (typeof navCountTones)[number];
|
|
4
7
|
export type NavCountProps = {
|
|
5
8
|
/**
|
|
@@ -17,8 +20,11 @@ export type NavCountProps = {
|
|
|
17
20
|
/**
|
|
18
21
|
* The pill's colour. `danger` (the default) is work waiting there, such
|
|
19
22
|
* as tasks to do; `accent` is news, such as what was added since the
|
|
20
|
-
* person last looked, where nothing is overdue
|
|
21
|
-
*
|
|
23
|
+
* person last looked, where nothing is overdue; `warning` is amber, work
|
|
24
|
+
* that needs the person but is not urgent ("needs you"): the warning
|
|
25
|
+
* tint with the warning ink (4.5:1 in every palette) inside a warning
|
|
26
|
+
* edge. Give an accent or warning count a `countLabel` ("3 new", "2 to
|
|
27
|
+
* confirm"), so its words, not its colour, say what it is.
|
|
22
28
|
* Place either tone on a neutral or ghost destination: a filled one of
|
|
23
29
|
* the same colour would hide it.
|
|
24
30
|
*/
|
|
@@ -29,6 +35,7 @@ export type NavCountProps = {
|
|
|
29
35
|
* Button: `<a href="/tasks">Tasks<NavCount count={8} /></a>`. It is the
|
|
30
36
|
* pill SectionNav's item count draws, and it is part of the destination's
|
|
31
37
|
* accessible name, as there. `tone="accent"` draws it in the accent colour,
|
|
32
|
-
* for news rather than work
|
|
38
|
+
* for news rather than work; `tone="warning"` in amber, for work that needs
|
|
39
|
+
* the person but is not urgent.
|
|
33
40
|
*/
|
|
34
41
|
export declare function NavCount({ count, countLabel, tone, }: NavCountProps): import("react").JSX.Element | null;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../class-names.js';
|
|
3
|
+
import { countToneClass } from '../css/css-count-pill.js';
|
|
3
4
|
import { isOneOf } from '../is-one-of.js';
|
|
4
|
-
/**
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The pill's colours: `danger` for work waiting, `accent` for news,
|
|
7
|
+
* `warning` for work that needs the person but is not urgent.
|
|
8
|
+
*/
|
|
9
|
+
export const navCountTones = ['danger', 'accent', 'warning'];
|
|
6
10
|
/**
|
|
7
11
|
* Fails closed on a count that is not a whole number, an empty label or an
|
|
8
12
|
* unknown tone.
|
|
@@ -21,7 +25,8 @@ function assertNavCount({ count, countLabel, tone }) {
|
|
|
21
25
|
* Button: `<a href="/tasks">Tasks<NavCount count={8} /></a>`. It is the
|
|
22
26
|
* pill SectionNav's item count draws, and it is part of the destination's
|
|
23
27
|
* accessible name, as there. `tone="accent"` draws it in the accent colour,
|
|
24
|
-
* for news rather than work
|
|
28
|
+
* for news rather than work; `tone="warning"` in amber, for work that needs
|
|
29
|
+
* the person but is not urgent.
|
|
25
30
|
*/
|
|
26
31
|
export function NavCount({ count, countLabel, tone = 'danger', }) {
|
|
27
32
|
assertNavCount({ count, countLabel, tone });
|
|
@@ -29,5 +34,5 @@ export function NavCount({ count, countLabel, tone = 'danger', }) {
|
|
|
29
34
|
return null;
|
|
30
35
|
// The space is its own text node, so every browser and the name's
|
|
31
36
|
// computation keep it between the words and the count.
|
|
32
|
-
return (_jsxs(_Fragment, { children: [' ', countLabel ? _jsx("span", { className: "sw-sr-only", children: countLabel }) : null, _jsx("span", { "aria-hidden": countLabel ? true : undefined, className: cx('sw-nav-count', tone
|
|
37
|
+
return (_jsxs(_Fragment, { children: [' ', countLabel ? _jsx("span", { className: "sw-sr-only", children: countLabel }) : null, _jsx("span", { "aria-hidden": countLabel ? true : undefined, className: cx('sw-nav-count', tone !== 'danger' && countToneClass('sw-nav-count', tone)), children: count })] }));
|
|
33
38
|
}
|
|
@@ -2,6 +2,7 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
import { type AccessibleName } from '../accessible-name.js';
|
|
3
3
|
import { type Breakpoint } from '../css/breakpoints.js';
|
|
4
4
|
import { type SectionNavAppearance } from '../css/css-section-nav-filled.js';
|
|
5
|
+
import { type NavItemCurrent } from './nav-items.js';
|
|
5
6
|
export type { SectionNavAppearance };
|
|
6
7
|
export { sectionNavAppearances } from '../css/css-section-nav-filled.js';
|
|
7
8
|
export type SectionNavItem = {
|
|
@@ -12,7 +13,7 @@ export type SectionNavItem = {
|
|
|
12
13
|
* `page` when the item is this page, `location` (or `true`) when the page
|
|
13
14
|
* is inside the item's section, such as a detail page under its list.
|
|
14
15
|
*/
|
|
15
|
-
current?:
|
|
16
|
+
current?: NavItemCurrent;
|
|
16
17
|
/** A decorative glyph before the label. */
|
|
17
18
|
icon?: ReactNode;
|
|
18
19
|
/**
|
|
@@ -5,24 +5,8 @@ import { sectionNavVerticalClass } from '../css/css-section-nav.js';
|
|
|
5
5
|
import { assertSectionNavAppearance, } from '../css/css-section-nav-filled.js';
|
|
6
6
|
import { tabStop } from '../tab-stop.js';
|
|
7
7
|
import { plainPress } from './link-press.js';
|
|
8
|
+
import { assertNavItems, navItemAriaCurrent, } from './nav-items.js';
|
|
8
9
|
export { sectionNavAppearances } from '../css/css-section-nav-filled.js';
|
|
9
|
-
function ariaCurrent(current) {
|
|
10
|
-
if (current === true)
|
|
11
|
-
return 'true';
|
|
12
|
-
return current || undefined;
|
|
13
|
-
}
|
|
14
|
-
function assertItems(items) {
|
|
15
|
-
if (items.some((item) => !item.id.trim() || !item.label.trim()))
|
|
16
|
-
throw new RangeError('SectionNav items need nonempty ids and labels');
|
|
17
|
-
if (new Set(items.map((item) => item.id)).size !== items.length)
|
|
18
|
-
throw new RangeError('SectionNav items need unique ids');
|
|
19
|
-
if (items.some((item) => item.count !== undefined &&
|
|
20
|
-
(!Number.isInteger(item.count) || item.count < 0)))
|
|
21
|
-
throw new RangeError('SectionNav item counts are whole numbers from 0');
|
|
22
|
-
if (items.some((item) => item.countLabel !== undefined &&
|
|
23
|
-
(!item.countLabel.trim() || item.count === undefined)))
|
|
24
|
-
throw new RangeError('SectionNav item countLabel is nonempty and comes with a count');
|
|
25
|
-
}
|
|
26
10
|
function assertBreakpoint(verticalFrom) {
|
|
27
11
|
if (verticalFrom !== undefined && !breakpoints.includes(verticalFrom))
|
|
28
12
|
throw new RangeError(`SectionNav verticalFrom must be one of ${breakpoints.join(', ')}`);
|
|
@@ -34,10 +18,10 @@ function assertBreakpoint(verticalFrom) {
|
|
|
34
18
|
* a side list. The current item carries `aria-current`.
|
|
35
19
|
*/
|
|
36
20
|
export function SectionNav({ appearance = 'underline', items, onNavigate, verticalFrom, ...label }) {
|
|
37
|
-
|
|
21
|
+
assertNavItems('SectionNav', items);
|
|
38
22
|
assertBreakpoint(verticalFrom);
|
|
39
23
|
assertSectionNavAppearance(appearance);
|
|
40
|
-
return (_jsx("nav", { className: cx('sw-section-nav', verticalFrom && sectionNavVerticalClass(verticalFrom), appearance === 'filled' && 'sw-section-nav-filled'), ...label, children: _jsx("ul", { className: "sw-section-nav-list", children: items.map((item) => (_jsx("li", { children: _jsxs("a", { "aria-current":
|
|
24
|
+
return (_jsx("nav", { className: cx('sw-section-nav', verticalFrom && sectionNavVerticalClass(verticalFrom), appearance === 'filled' && 'sw-section-nav-filled'), ...label, children: _jsx("ul", { className: "sw-section-nav-list", children: items.map((item) => (_jsx("li", { children: _jsxs("a", { "aria-current": navItemAriaCurrent(item.current), className: "sw-section-nav-link", href: item.href, tabIndex: tabStop,
|
|
41
25
|
// Only a given onNavigate attaches a handler, so the links
|
|
42
26
|
// render from a server component without one.
|
|
43
27
|
onClick: onNavigate
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type SiteFooterProps } from './site-footer/types.js';
|
|
2
|
+
export type { SiteFooterGroup, SiteFooterLink, SiteFooterProps, } from './site-footer/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A public site's footer on the dark inverse band: the brand and a tagline,
|
|
5
|
+
* groups of links (each a `nav` named by its label, its label shown above
|
|
6
|
+
* its links), and a bottom row past a hairline with the legal line at its
|
|
7
|
+
* start and, say, a language switch at its end. Below `md` the brand takes
|
|
8
|
+
* the full width and the groups two columns; from `md` the brand and the
|
|
9
|
+
* groups share one row. A brand moves the band with a theme overlay of
|
|
10
|
+
* `inverse`. Invalid groups or links throw a `RangeError`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function SiteFooter({ brand, end, groups, legal, tagline, }: SiteFooterProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Band } from './Band.js';
|
|
3
|
+
import { assertSiteFooterGroups } from './site-footer/validate.js';
|
|
4
|
+
import { tabStop } from '../tab-stop.js';
|
|
5
|
+
/** Whether an optional slot draws anything: not left out, empty or `false`. */
|
|
6
|
+
function shown(node) {
|
|
7
|
+
return node !== undefined && node !== null && node !== false && node !== '';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A public site's footer on the dark inverse band: the brand and a tagline,
|
|
11
|
+
* groups of links (each a `nav` named by its label, its label shown above
|
|
12
|
+
* its links), and a bottom row past a hairline with the legal line at its
|
|
13
|
+
* start and, say, a language switch at its end. Below `md` the brand takes
|
|
14
|
+
* the full width and the groups two columns; from `md` the brand and the
|
|
15
|
+
* groups share one row. A brand moves the band with a theme overlay of
|
|
16
|
+
* `inverse`. Invalid groups or links throw a `RangeError`.
|
|
17
|
+
*/
|
|
18
|
+
export function SiteFooter({ brand, end, groups, legal, tagline, }) {
|
|
19
|
+
assertSiteFooterGroups(groups);
|
|
20
|
+
const hasLegal = shown(legal);
|
|
21
|
+
const hasEnd = shown(end);
|
|
22
|
+
return (_jsxs(Band, { as: "footer", className: "sw-site-footer", paddingBottom: 6, tone: "inverse", children: [_jsxs("div", { className: "sw-site-footer-top", children: [_jsxs("div", { className: "sw-site-footer-brand", children: [brand, tagline ? _jsx("p", { className: "sw-site-footer-tagline", children: tagline }) : null] }), groups.map((group) => (_jsxs("nav", { "aria-label": group.label, className: "sw-site-footer-group", children: [_jsx("p", { "aria-hidden": "true", className: "sw-site-footer-label", children: group.label }), _jsx("ul", { className: "sw-site-footer-links", children: group.links.map((link) => (_jsx("li", { children: _jsx("a", { "aria-current": link.current ? 'page' : undefined, className: "sw-site-footer-link", href: link.href, hrefLang: link.hrefLang, lang: link.lang, tabIndex: tabStop, children: link.label }) }, link.id))) })] }, group.id)))] }), hasLegal || hasEnd ? (_jsxs("div", { className: "sw-site-footer-bottom", children: [hasLegal ? (_jsx("div", { className: "sw-site-footer-legal", children: legal })) : null, hasEnd ? _jsx("div", { className: "sw-site-footer-end", children: end }) : null] })) : null] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type SiteHeaderProps } from './site-header/types.js';
|
|
2
|
+
export type { SiteHeaderItem, SiteHeaderLabels, SiteHeaderProps, } from './site-header/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A public site's header: the brand at the start; from `lg` the main
|
|
5
|
+
* destinations as a row of links (the current one marked by weight and a
|
|
6
|
+
* bar under it) and the bar's actions at the end; below `lg` the compact
|
|
7
|
+
* actions and a menu button that opens the destinations and the menu's
|
|
8
|
+
* actions over the page under the bar. The menu is a native `details`, so
|
|
9
|
+
* it opens and closes by touch, click and keyboard with no script, as a
|
|
10
|
+
* page rendered on the server needs; script adds Escape (focus back on the
|
|
11
|
+
* button) and closes it when a link is chosen or focus leaves it. It traps
|
|
12
|
+
* no focus. Invalid items or words throw a `RangeError`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function SiteHeader({ actions, brand, closeIcon, compactActions, items, labels, menuActions, menuIcon, navLabel, sticky, }: SiteHeaderProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useRef } from 'react';
|
|
4
|
+
import { cx } from '../class-names.js';
|
|
5
|
+
import { SiteHeaderLinks } from './site-header/SiteHeaderLinks.js';
|
|
6
|
+
import { useMenuDismiss } from './site-header/use-menu-dismiss.js';
|
|
7
|
+
import { assertSiteHeaderProps } from './site-header/validate.js';
|
|
8
|
+
/**
|
|
9
|
+
* A public site's header: the brand at the start; from `lg` the main
|
|
10
|
+
* destinations as a row of links (the current one marked by weight and a
|
|
11
|
+
* bar under it) and the bar's actions at the end; below `lg` the compact
|
|
12
|
+
* actions and a menu button that opens the destinations and the menu's
|
|
13
|
+
* actions over the page under the bar. The menu is a native `details`, so
|
|
14
|
+
* it opens and closes by touch, click and keyboard with no script, as a
|
|
15
|
+
* page rendered on the server needs; script adds Escape (focus back on the
|
|
16
|
+
* button) and closes it when a link is chosen or focus leaves it. It traps
|
|
17
|
+
* no focus. Invalid items or words throw a `RangeError`.
|
|
18
|
+
*/
|
|
19
|
+
export function SiteHeader({ actions, brand, closeIcon, compactActions, items, labels, menuActions, menuIcon, navLabel, sticky = true, }) {
|
|
20
|
+
assertSiteHeaderProps({ items, labels, navLabel });
|
|
21
|
+
const menuRef = useRef(null);
|
|
22
|
+
useMenuDismiss(menuRef);
|
|
23
|
+
return (_jsx("header", { className: cx('sw-site-header', sticky && 'sw-site-header-sticky'), children: _jsxs("div", { className: "sw-site-header-bar", children: [_jsx("div", { className: "sw-site-header-brand", children: brand }), _jsx(SiteHeaderLinks, { className: "sw-site-header-nav", items: items, navLabel: navLabel }), actions ? (_jsx("div", { className: "sw-site-header-actions", children: actions })) : null, _jsxs("div", { className: "sw-site-header-compact", children: [compactActions, _jsxs("details", { className: "sw-site-header-menu", ref: menuRef, children: [_jsxs("summary", { className: "sw-site-header-menu-button", children: [_jsxs("span", { className: "sw-site-header-menu-open", children: [_jsx("span", { "aria-hidden": "true", className: "sw-site-header-glyph", children: menuIcon }), _jsx("span", { className: "sw-sr-only", children: labels.openMenu })] }), _jsxs("span", { className: "sw-site-header-menu-close", children: [_jsx("span", { "aria-hidden": "true", className: "sw-site-header-glyph", children: closeIcon }), _jsx("span", { className: "sw-sr-only", children: labels.closeMenu })] })] }), _jsxs("div", { className: "sw-site-header-panel", children: [_jsx(SiteHeaderLinks, { className: "sw-site-header-menu-nav", items: items, navLabel: navLabel }), menuActions ? (_jsx("div", { className: "sw-site-header-menu-actions", children: menuActions })) : null] })] })] })] }) }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { skeletonVariants, type SkeletonVariant } from '../css/css-skeleton.js';
|
|
2
|
+
export { skeletonVariants, type SkeletonVariant };
|
|
3
|
+
export type SkeletonProps = {
|
|
4
|
+
/**
|
|
5
|
+
* What is loading, in the consumer's words ("Loading this week's
|
|
6
|
+
* games"): the status's words, read once politely; the bars are hidden.
|
|
7
|
+
*/
|
|
8
|
+
label: string;
|
|
9
|
+
/** How many bars (`lines`) or rows (`rows`), 1 to 6; 3 by default. */
|
|
10
|
+
lines?: number;
|
|
11
|
+
/**
|
|
12
|
+
* `lines` (default) is bars of text in a column, the last one shorter;
|
|
13
|
+
* `rows` is list rows (a mark and a bar) with hairlines between them.
|
|
14
|
+
*/
|
|
15
|
+
variant?: SkeletonVariant;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Grey placeholder bars where content is loading, in place of a spinner:
|
|
19
|
+
* a polite status named by `label`, the bars decorative. They pulse
|
|
20
|
+
* gently, and stay still when reduced motion is asked for.
|
|
21
|
+
*/
|
|
22
|
+
export declare function Skeleton({ label, lines, variant, }: SkeletonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
import { cx } from '../class-names.js';
|
|
4
|
+
import { skeletonLineRange, skeletonVariants, } from '../css/css-skeleton.js';
|
|
5
|
+
import { isOneOf } from '../is-one-of.js';
|
|
6
|
+
export { skeletonVariants };
|
|
7
|
+
/** Fails closed on blank words, a count outside 1–6 or an unknown variant. */
|
|
8
|
+
function assertSkeleton({ label, lines, variant }) {
|
|
9
|
+
if (typeof label !== 'string' || !label.trim())
|
|
10
|
+
throw new RangeError('Skeleton label must be nonempty');
|
|
11
|
+
if (!Number.isInteger(lines) ||
|
|
12
|
+
lines < skeletonLineRange.min ||
|
|
13
|
+
lines > skeletonLineRange.max)
|
|
14
|
+
throw new RangeError(`Skeleton lines must be a whole number from ${skeletonLineRange.min} to ${skeletonLineRange.max}: ${String(lines)}`);
|
|
15
|
+
if (!isOneOf(skeletonVariants, variant))
|
|
16
|
+
throw new RangeError(`Skeleton variant must be one of ${skeletonVariants.join(', ')}`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Grey placeholder bars where content is loading, in place of a spinner:
|
|
20
|
+
* a polite status named by `label`, the bars decorative. They pulse
|
|
21
|
+
* gently, and stay still when reduced motion is asked for.
|
|
22
|
+
*/
|
|
23
|
+
export function Skeleton({ label, lines = 3, variant = 'lines', }) {
|
|
24
|
+
assertSkeleton({ label, lines, variant });
|
|
25
|
+
const labelId = useId();
|
|
26
|
+
// The bars and rows are divs and the words a span, so the generated
|
|
27
|
+
// :nth-of-type widths count bars only.
|
|
28
|
+
const places = Array.from({ length: lines }, (_, index) => index);
|
|
29
|
+
return (_jsxs("div", { "aria-labelledby": labelId, className: cx('sw-skeleton', `sw-skeleton-${variant}`), role: "status", children: [_jsx("span", { className: "sw-sr-only", id: labelId, children: label }), places.map((place) => variant === 'rows' ? (_jsxs("div", { "aria-hidden": "true", className: "sw-skeleton-row", children: [_jsx("div", { className: "sw-skeleton-mark" }), _jsx("div", { className: "sw-skeleton-bar" })] }, place)) : (_jsx("div", { "aria-hidden": "true", className: "sw-skeleton-bar" }, place)))] }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReactNode, type Ref } from 'react';
|
|
2
|
+
import { type AppShellProps } from './types.js';
|
|
3
|
+
export type AppShellBarProps = Pick<AppShellProps, 'barEnd' | 'barTitle' | 'menuCount' | 'menuCountLabel' | 'menuCountTone'> & {
|
|
4
|
+
/** The bar itself, measured so a page bar can stick under it. */
|
|
5
|
+
barRef: Ref<HTMLElement>;
|
|
6
|
+
menuRef: Ref<HTMLButtonElement>;
|
|
7
|
+
menuIcon: ReactNode;
|
|
8
|
+
openMenuLabel: string;
|
|
9
|
+
menuOpen: boolean;
|
|
10
|
+
onOpenMenu: () => void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The menu button's name: its label, then the count's words (or the bare
|
|
14
|
+
* number) after a comma while there is a count to read.
|
|
15
|
+
*/
|
|
16
|
+
export declare function menuButtonName(openMenu: string, menuCount: number | undefined, menuCountLabel: string | undefined): string;
|
|
17
|
+
/**
|
|
18
|
+
* The compact top bar below `lg` (an AppHeader `bar`): the icon-only menu
|
|
19
|
+
* button, which may carry the shared count pill on its top end corner, the
|
|
20
|
+
* title and the end controls. The pill is hidden from assistive technology
|
|
21
|
+
* because the button's name already reads it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function AppShellBar({ barEnd, barRef, barTitle, menuCount, menuCountLabel, menuCountTone, menuIcon, menuOpen, menuRef, onOpenMenu, openMenuLabel, }: AppShellBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cx } from '../../class-names.js';
|
|
4
|
+
import { countToneClass } from '../../css/css-count-pill.js';
|
|
5
|
+
import { AppHeader } from '../AppHeader.js';
|
|
6
|
+
import { Button } from '../Button.js';
|
|
7
|
+
/**
|
|
8
|
+
* The menu button's name: its label, then the count's words (or the bare
|
|
9
|
+
* number) after a comma while there is a count to read.
|
|
10
|
+
*/
|
|
11
|
+
export function menuButtonName(openMenu, menuCount, menuCountLabel) {
|
|
12
|
+
if (!menuCount)
|
|
13
|
+
return openMenu;
|
|
14
|
+
return `${openMenu}, ${menuCountLabel ?? String(menuCount)}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The compact top bar below `lg` (an AppHeader `bar`): the icon-only menu
|
|
18
|
+
* button, which may carry the shared count pill on its top end corner, the
|
|
19
|
+
* title and the end controls. The pill is hidden from assistive technology
|
|
20
|
+
* because the button's name already reads it.
|
|
21
|
+
*/
|
|
22
|
+
export function AppShellBar({ barEnd, barRef, barTitle, menuCount, menuCountLabel, menuCountTone = 'danger', menuIcon, menuOpen, menuRef, onOpenMenu, openMenuLabel, }) {
|
|
23
|
+
return (_jsxs(AppHeader, { ref: barRef, appearance: "bar", className: "sw-app-shell-bar", padding: 2, children: [_jsxs(Button, { ref: menuRef, "aria-expanded": menuOpen, "aria-haspopup": "dialog", "aria-label": menuButtonName(openMenuLabel, menuCount, menuCountLabel), className: "sw-app-shell-menu", onPress: onOpenMenu, size: "sm", variant: "ghost", children: [_jsx("span", { "aria-hidden": "true", className: "sw-app-shell-icon", children: menuIcon }), menuCount ? (_jsx("span", { "aria-hidden": "true", className: cx('sw-app-shell-count', menuCountTone !== 'danger' &&
|
|
24
|
+
countToneClass('sw-app-shell-count', menuCountTone)), children: menuCount })) : null] }), _jsx("div", { className: "sw-app-shell-bar-title", children: barTitle }), barEnd ? _jsx("div", { className: "sw-app-shell-bar-end", children: barEnd }) : null] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type AppShellNavGroup, type AppShellNavItem } from './types.js';
|
|
3
|
+
export type AppShellDrawerProps = {
|
|
4
|
+
open: boolean;
|
|
5
|
+
/** Asked on every close request: Escape, a press outside, the close button. */
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
/** Asked after a plain press on a destination, which also closes it. */
|
|
8
|
+
onChoose: () => void;
|
|
9
|
+
closeLabel: string;
|
|
10
|
+
navLabel: string;
|
|
11
|
+
groups: readonly AppShellNavGroup[];
|
|
12
|
+
onNavigate?: (item: AppShellNavItem) => void;
|
|
13
|
+
header?: ReactNode;
|
|
14
|
+
footer?: ReactNode;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The menu below `lg`: a modal `dialog` on the top layer, full height at
|
|
18
|
+
* the inline start, named by the navigation's label. Dialog's close-request
|
|
19
|
+
* mechanics make Escape, a backdrop press and a platform close request one
|
|
20
|
+
* `onClose`, after which focus returns to the menu button. Its close button
|
|
21
|
+
* sits where the menu button was, so it is the first control. The content
|
|
22
|
+
* is drawn only while open, so the page holds one navigation at a time.
|
|
23
|
+
*/
|
|
24
|
+
export declare function AppShellDrawer({ closeLabel, footer, groups, header, navLabel, onChoose, onClose, onNavigate, open, }: AppShellDrawerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from '../Button.js';
|
|
4
|
+
import { useDialogCloseRequests } from '../dialog-close-requests.js';
|
|
5
|
+
import { AppShellNav } from './AppShellNav.js';
|
|
6
|
+
/**
|
|
7
|
+
* The menu below `lg`: a modal `dialog` on the top layer, full height at
|
|
8
|
+
* the inline start, named by the navigation's label. Dialog's close-request
|
|
9
|
+
* mechanics make Escape, a backdrop press and a platform close request one
|
|
10
|
+
* `onClose`, after which focus returns to the menu button. Its close button
|
|
11
|
+
* sits where the menu button was, so it is the first control. The content
|
|
12
|
+
* is drawn only while open, so the page holds one navigation at a time.
|
|
13
|
+
*/
|
|
14
|
+
export function AppShellDrawer({ closeLabel, footer, groups, header, navLabel, onChoose, onClose, onNavigate, open, }) {
|
|
15
|
+
const { nodeRef, handlers } = useDialogCloseRequests({ open, onClose });
|
|
16
|
+
return (_jsx("dialog", { ref: nodeRef, "aria-label": navLabel, className: "sw-app-shell-drawer", ...handlers, children: open ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sw-app-shell-drawer-top", children: [_jsxs(Button, { className: "sw-app-shell-close", onPress: onClose, size: "sm", variant: "ghost", children: [_jsx("span", { className: "sw-sr-only", children: closeLabel }), _jsx("span", { "aria-hidden": "true", className: "sw-dialog-close-glyph" })] }), header ? (_jsx("div", { className: "sw-app-shell-header", children: header })) : null] }), _jsx(AppShellNav, { groups: groups, navLabel: navLabel, onChoose: onChoose, onNavigate: onNavigate }), footer ? _jsx("div", { className: "sw-app-shell-footer", children: footer }) : null] })) : null }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type MouseEvent } from 'react';
|
|
2
|
+
import { type AppShellNavItem } from './types.js';
|
|
3
|
+
export type AppShellLinkProps = {
|
|
4
|
+
item: AppShellNavItem;
|
|
5
|
+
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
6
|
+
/**
|
|
7
|
+
* The sidebar's links sit in a Tooltip with their label, shown only while
|
|
8
|
+
* `tooltip` is true (the rail, where the label is hidden). Left out (the
|
|
9
|
+
* drawer) there is no Tooltip at all.
|
|
10
|
+
*/
|
|
11
|
+
tooltip?: boolean;
|
|
12
|
+
};
|
|
13
|
+
/** One destination in its list item: the glyph, the label and the count. */
|
|
14
|
+
export declare function AppShellLink({ item, onClick, tooltip }: AppShellLinkProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../../class-names.js';
|
|
3
|
+
import { countToneClass } from '../../css/css-count-pill.js';
|
|
4
|
+
import { tabStop } from '../../tab-stop.js';
|
|
5
|
+
import { navItemAriaCurrent } from '../nav-items.js';
|
|
6
|
+
import { Tooltip } from '../Tooltip.js';
|
|
7
|
+
/*
|
|
8
|
+
* The destination's link. A Tooltip around it passes its references in;
|
|
9
|
+
* only `aria-labelledby` is kept. Its `aria-describedby` is left out on
|
|
10
|
+
* purpose: the tooltip's words are the label the link is already named by,
|
|
11
|
+
* so they would be read twice.
|
|
12
|
+
*/
|
|
13
|
+
function AppShellAnchor({ 'aria-labelledby': labelledBy, children, item, onClick, }) {
|
|
14
|
+
return (_jsx("a", { "aria-current": navItemAriaCurrent(item.current), "aria-labelledby": labelledBy, className: "sw-app-shell-link", href: item.href, onClick: onClick, tabIndex: tabStop, children: children }));
|
|
15
|
+
}
|
|
16
|
+
/** The count after the label, read in the link's name as SectionNav's is. */
|
|
17
|
+
function AppShellCount({ count, countLabel, countTone = 'danger', }) {
|
|
18
|
+
if (!count)
|
|
19
|
+
return null;
|
|
20
|
+
return (_jsxs(_Fragment, { children: [countLabel ? (_jsx("span", { className: "sw-sr-only", children: ` ${countLabel}` })) : (' '), _jsx("span", { "aria-hidden": countLabel ? true : undefined, className: cx('sw-app-shell-count', countTone !== 'danger' &&
|
|
21
|
+
countToneClass('sw-app-shell-count', countTone)), children: count })] }));
|
|
22
|
+
}
|
|
23
|
+
/** One destination in its list item: the glyph, the label and the count. */
|
|
24
|
+
export function AppShellLink({ item, onClick, tooltip }) {
|
|
25
|
+
const content = (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: "sw-app-shell-icon", children: item.icon }), _jsx("span", { className: "sw-app-shell-label", children: item.label }), _jsx(AppShellCount, { count: item.count, countLabel: item.countLabel, countTone: item.countTone })] }));
|
|
26
|
+
return (_jsx("li", { className: "sw-app-shell-item", children: tooltip === undefined ? (_jsx(AppShellAnchor, { item: item, onClick: onClick, children: content })) : (_jsx(Tooltip, { content: item.label, disabled: !tooltip, trigger: _jsx(AppShellAnchor, { item: item, onClick: onClick, children: content }) })) }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AppShellNavGroup, type AppShellNavItem } from './types.js';
|
|
2
|
+
export type AppShellNavProps = {
|
|
3
|
+
navLabel: string;
|
|
4
|
+
groups: readonly AppShellNavGroup[];
|
|
5
|
+
onNavigate?: (item: AppShellNavItem) => void;
|
|
6
|
+
/** The drawer's: a plain press on a destination also closes it. */
|
|
7
|
+
onChoose?: () => void;
|
|
8
|
+
/** The sidebar's: whether the rail's tooltips show (see AppShellLink). */
|
|
9
|
+
tooltip?: boolean;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The shell's navigation landmark: each group a list named by its label,
|
|
13
|
+
* the same in the sidebar and the drawer.
|
|
14
|
+
*/
|
|
15
|
+
export declare function AppShellNav({ groups, navLabel, onChoose, onNavigate, tooltip, }: AppShellNavProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
import { plainPress } from '../link-press.js';
|
|
4
|
+
import { AppShellLink } from './AppShellLink.js';
|
|
5
|
+
/**
|
|
6
|
+
* The shell's navigation landmark: each group a list named by its label,
|
|
7
|
+
* the same in the sidebar and the drawer.
|
|
8
|
+
*/
|
|
9
|
+
export function AppShellNav({ groups, navLabel, onChoose, onNavigate, tooltip, }) {
|
|
10
|
+
const idPrefix = useId();
|
|
11
|
+
// Only a given onNavigate or onChoose attaches a handler.
|
|
12
|
+
function pressHandler(item) {
|
|
13
|
+
if (!onNavigate && !onChoose)
|
|
14
|
+
return undefined;
|
|
15
|
+
return (event) => {
|
|
16
|
+
if (!plainPress(event))
|
|
17
|
+
return;
|
|
18
|
+
if (onNavigate) {
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
onNavigate(item);
|
|
21
|
+
}
|
|
22
|
+
onChoose?.();
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return (_jsx("nav", { "aria-label": navLabel, className: "sw-app-shell-nav", children: groups.map((group, index) => {
|
|
26
|
+
const labelId = `${idPrefix}-group-${index}`;
|
|
27
|
+
return (_jsxs("div", { className: "sw-app-shell-group", children: [group.label ? (_jsx("p", { className: "sw-app-shell-group-label", id: labelId, children: group.label })) : null, _jsx("ul", { "aria-labelledby": group.label ? labelId : undefined, className: "sw-app-shell-list", children: group.items.map((item) => (_jsx(AppShellLink, { item: item, onClick: pressHandler(item), tooltip: tooltip }, item.id))) })] }, group.id));
|
|
28
|
+
}) }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type AppShellNavGroup, type AppShellNavItem } from './types.js';
|
|
3
|
+
export type AppShellSidebarProps = {
|
|
4
|
+
navLabel: string;
|
|
5
|
+
groups: readonly AppShellNavGroup[];
|
|
6
|
+
onNavigate?: (item: AppShellNavItem) => void;
|
|
7
|
+
header?: ReactNode;
|
|
8
|
+
footer?: ReactNode;
|
|
9
|
+
railHeader?: ReactNode;
|
|
10
|
+
railFooter?: ReactNode;
|
|
11
|
+
/** Whether the window is in the rail's range, where tooltips show. */
|
|
12
|
+
rail: boolean;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The pinned column from `lg` up: a rail to 75rem, the full sidebar from
|
|
16
|
+
* there. Hidden below `lg`, where the drawer holds the same parts.
|
|
17
|
+
*/
|
|
18
|
+
export declare function AppShellSidebar({ footer, groups, header, navLabel, onNavigate, rail, railFooter, railHeader, }: AppShellSidebarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AppShellNav } from './AppShellNav.js';
|
|
3
|
+
/*
|
|
4
|
+
* A slot drawn in the full sidebar (`sw-app-shell-slot`) or only in the
|
|
5
|
+
* rail (`sw-app-shell-rail-slot`); the generated media queries pick one.
|
|
6
|
+
*/
|
|
7
|
+
function Slot({ children, part, rail, }) {
|
|
8
|
+
if (!children)
|
|
9
|
+
return null;
|
|
10
|
+
return (_jsx("div", { className: `sw-app-shell-${part} ${rail ? 'sw-app-shell-rail-slot' : 'sw-app-shell-slot'}`, children: children }));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The pinned column from `lg` up: a rail to 75rem, the full sidebar from
|
|
14
|
+
* there. Hidden below `lg`, where the drawer holds the same parts.
|
|
15
|
+
*/
|
|
16
|
+
export function AppShellSidebar({ footer, groups, header, navLabel, onNavigate, rail, railFooter, railHeader, }) {
|
|
17
|
+
return (_jsxs("div", { className: "sw-app-shell-sidebar", children: [_jsx(Slot, { part: "header", rail: false, children: header }), _jsx(Slot, { part: "header", rail: true, children: railHeader }), _jsx(AppShellNav, { groups: groups, navLabel: navLabel, onNavigate: onNavigate, tooltip: rail }), _jsx(Slot, { part: "footer", rail: false, children: footer }), _jsx(Slot, { part: "footer", rail: true, children: railFooter })] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type NavCountTone } from '../NavCount.js';
|
|
3
|
+
import { type NavItemCurrent } from '../nav-items.js';
|
|
4
|
+
export type AppShellNavItem = {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
href: string;
|
|
8
|
+
/**
|
|
9
|
+
* The destination's glyph (a consumer Lucide icon or mark), hidden from
|
|
10
|
+
* assistive technology. Required: in the rail it is all the item shows.
|
|
11
|
+
*/
|
|
12
|
+
icon: ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* How many things wait there, such as work to do: the shared count pill
|
|
15
|
+
* (danger) after the label, or on the glyph in the rail, read in the
|
|
16
|
+
* link's name. A whole number; 0 (or none) shows no pill.
|
|
17
|
+
*/
|
|
18
|
+
count?: number;
|
|
19
|
+
/**
|
|
20
|
+
* What the count means, in the consumer's words ("3 to confirm"): read in
|
|
21
|
+
* the name in place of the bare number, the pill then hidden from
|
|
22
|
+
* assistive technology. Needs `count`.
|
|
23
|
+
*/
|
|
24
|
+
countLabel?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The pill's colour, as NavCount's `tone`: `danger` (the default) for
|
|
27
|
+
* work waiting, `warning` (amber) for work that needs the person but is
|
|
28
|
+
* not urgent, `accent` for news. Needs `count`; say what it is in a
|
|
29
|
+
* `countLabel`, never by its colour alone.
|
|
30
|
+
*/
|
|
31
|
+
countTone?: NavCountTone;
|
|
32
|
+
/**
|
|
33
|
+
* `page` when the item is this page, `location` (or `true`) when the page
|
|
34
|
+
* is inside the item's area, such as a detail page under its list.
|
|
35
|
+
*/
|
|
36
|
+
current?: NavItemCurrent;
|
|
37
|
+
};
|
|
38
|
+
export type AppShellNavGroup = {
|
|
39
|
+
id: string;
|
|
40
|
+
/**
|
|
41
|
+
* A small muted caption over the group's destinations that names their
|
|
42
|
+
* list. In the rail it is hidden (still naming the list) and the group
|
|
43
|
+
* starts under a hairline.
|
|
44
|
+
*/
|
|
45
|
+
label?: string;
|
|
46
|
+
items: readonly AppShellNavItem[];
|
|
47
|
+
};
|
|
48
|
+
/** The shell's own words: the menu button's name and the drawer's close. */
|
|
49
|
+
export type AppShellLabels = {
|
|
50
|
+
openMenu: string;
|
|
51
|
+
closeMenu: string;
|
|
52
|
+
};
|
|
53
|
+
export type AppShellProps = {
|
|
54
|
+
/** The navigation landmark's name, also the drawer's. */
|
|
55
|
+
navLabel: string;
|
|
56
|
+
groups: readonly AppShellNavGroup[];
|
|
57
|
+
/**
|
|
58
|
+
* Called for a plain press on a destination (the main button, no
|
|
59
|
+
* modifier key), after the browser's own navigation is prevented, so a
|
|
60
|
+
* client router moves instead. A press with a modifier key, or a middle
|
|
61
|
+
* press, keeps the browser's behaviour. Without it the links navigate.
|
|
62
|
+
*/
|
|
63
|
+
onNavigate?: (item: AppShellNavItem) => void;
|
|
64
|
+
/**
|
|
65
|
+
* The sidebar's top, such as a workspace switcher; also the drawer's.
|
|
66
|
+
* It may hold an interactive trigger (an `ActionMenu`, a `ContextSwitcher`):
|
|
67
|
+
* its popup opens on the top layer, so the sidebar and the drawer never
|
|
68
|
+
* clip it.
|
|
69
|
+
* While the drawer is open it is mounted twice (the sidebar's copy is
|
|
70
|
+
* hidden), so each copy keeps its own state: put no fixed DOM `id` in it
|
|
71
|
+
* (use `useId`), and keep state that must be shared in the consumer.
|
|
72
|
+
* The same holds for `footer`, `railHeader` and `railFooter`.
|
|
73
|
+
*/
|
|
74
|
+
header?: ReactNode;
|
|
75
|
+
/** The sidebar's bottom, such as the account menu; also the drawer's (see `header`). */
|
|
76
|
+
footer?: ReactNode;
|
|
77
|
+
/**
|
|
78
|
+
* A compact `header` for the rail, such as a workspace switcher drawn as
|
|
79
|
+
* its glyph alone (an icon trigger named by its label). Left out, the
|
|
80
|
+
* rail shows none.
|
|
81
|
+
*/
|
|
82
|
+
railHeader?: ReactNode;
|
|
83
|
+
/** A compact `footer` for the rail. Left out, the rail shows none. */
|
|
84
|
+
railFooter?: ReactNode;
|
|
85
|
+
/** The compact top bar's title, below `lg`. */
|
|
86
|
+
barTitle: ReactNode;
|
|
87
|
+
/** Controls at the top bar's end, below `lg`. */
|
|
88
|
+
barEnd?: ReactNode;
|
|
89
|
+
/** The consumer's glyph for the menu button (Scalewing has no icon catalog). */
|
|
90
|
+
menuIcon: ReactNode;
|
|
91
|
+
/**
|
|
92
|
+
* How many things wait behind the menu, such as an inbox: the shared
|
|
93
|
+
* count pill on the menu button's top end corner, below `lg`, read in
|
|
94
|
+
* its name after `labels.openMenu` and a comma ("Open menu, 6"). A whole
|
|
95
|
+
* number; 0 (or none) shows no pill.
|
|
96
|
+
*/
|
|
97
|
+
menuCount?: number;
|
|
98
|
+
/**
|
|
99
|
+
* What the menu's count means, in the consumer's words ("6 need you"):
|
|
100
|
+
* read in the name in place of the bare number ("Open menu, 6 need you").
|
|
101
|
+
* Needs `menuCount`.
|
|
102
|
+
*/
|
|
103
|
+
menuCountLabel?: string;
|
|
104
|
+
/**
|
|
105
|
+
* The menu pill's colour, NavCount's tones: `danger` (the default),
|
|
106
|
+
* `warning` (amber) for work that needs the person but is not urgent,
|
|
107
|
+
* `accent` for news. Needs `menuCount`.
|
|
108
|
+
*/
|
|
109
|
+
menuCountTone?: NavCountTone;
|
|
110
|
+
/**
|
|
111
|
+
* A band at the top of `main` on every layout, such as the page's
|
|
112
|
+
* context pickers and search: the solid surface with a hairline under it
|
|
113
|
+
* and the page's inline padding (space-4 below `lg`, space-6 from it),
|
|
114
|
+
* its children laid out in a wrapping row with space between them. It is
|
|
115
|
+
* sticky: under the compact top bar below `lg`, at the window's top
|
|
116
|
+
* from `lg` up. It scrolls with nothing else; keep it to one row.
|
|
117
|
+
*/
|
|
118
|
+
pageBar?: ReactNode;
|
|
119
|
+
labels: AppShellLabels;
|
|
120
|
+
/** The page, rendered in `main`. */
|
|
121
|
+
children: ReactNode;
|
|
122
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type AppShellProps } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fails closed on the shell's words, the menu's count, its groups and
|
|
4
|
+
* their items (ids unique across every group, each with an icon, which is
|
|
5
|
+
* all the rail shows).
|
|
6
|
+
*/
|
|
7
|
+
export declare function assertAppShellProps({ groups, labels, menuCount, menuCountLabel, menuCountTone, navLabel, }: Pick<AppShellProps, 'groups' | 'labels' | 'menuCount' | 'menuCountLabel' | 'menuCountTone' | 'navLabel'>): void;
|