@scalewing/react 1.47.0 → 1.49.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 +82 -0
- package/README.md +215 -6
- 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 +4 -3
- package/dist/components/AppShell.js +24 -7
- 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/EmptyState.d.ts +23 -0
- package/dist/components/EmptyState.js +40 -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/Grid.d.ts +11 -3
- package/dist/components/Grid.js +13 -4
- package/dist/components/Hero.d.ts +26 -0
- package/dist/components/Hero.js +23 -0
- 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/Stepper.d.ts +11 -1
- package/dist/components/Stepper.js +10 -8
- package/dist/components/Text.js +3 -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/types.d.ts +36 -1
- package/dist/components/app-shell/validate.d.ts +4 -3
- package/dist/components/app-shell/validate.js +21 -3
- 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/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/stepper/StepperCountFrame.d.ts +15 -0
- package/dist/components/stepper/StepperCountFrame.js +25 -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-page-bar.d.ts +3 -0
- package/dist/css/css-app-shell-page-bar.js +81 -0
- package/dist/css/css-app-shell.js +6 -1
- 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 +30 -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 +2 -2
- package/dist/css/css-count-pill.js +11 -3
- package/dist/css/css-empty-state.d.ts +2 -0
- package/dist/css/css-empty-state.js +77 -0
- 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-grid-span.js +5 -2
- package/dist/css/css-grid.d.ts +13 -0
- package/dist/css/css-grid.js +17 -1
- package/dist/css/css-hero.d.ts +2 -0
- package/dist/css/css-hero.js +62 -0
- 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/css-stepper-unit.d.ts +9 -0
- package/dist/css/css-stepper-unit.js +102 -0
- package/dist/css/css-stepper.js +5 -1
- package/dist/css/stacking.js +7 -4
- package/dist/css/stylesheet.js +20 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.js +13 -1
- package/dist/stepper-count.d.ts +8 -0
- package/dist/stepper-count.js +17 -0
- package/dist/styles.css +2202 -702
- package/package.json +2 -2
|
@@ -10,12 +10,18 @@ export type GlyphTileProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' |
|
|
|
10
10
|
hue: GlyphTileHue;
|
|
11
11
|
/** `sm` 24, `md` 28 (default), `lg` 32. */
|
|
12
12
|
size?: GlyphTileSize;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The consumer's glyph (Lucide), drawn in white at spacing step 4; or, as
|
|
15
|
+
* a single string or number child, up to three letters, such as a team's
|
|
16
|
+
* initials ("NSL"), sized from the tile (smaller for more letters) so
|
|
17
|
+
* they fit every size.
|
|
18
|
+
*/
|
|
14
19
|
children: ReactNode;
|
|
15
20
|
};
|
|
16
21
|
/**
|
|
17
|
-
* A small rounded square in a categorical colour holding a white glyph
|
|
18
|
-
* kind's mark beside its name, such as a service in
|
|
22
|
+
* A small rounded square in a categorical colour holding a white glyph (or
|
|
23
|
+
* up to three letters): a kind's mark beside its name, such as a service in
|
|
24
|
+
* a list. Decorative
|
|
19
25
|
* (`aria-hidden`): the visible label beside it carries the meaning.
|
|
20
26
|
*/
|
|
21
27
|
export declare function GlyphTile({ children, className, hue, size, ...rest }: GlyphTileProps): import("react").JSX.Element;
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { isCategoricalColor } from '@scalewing/tokens';
|
|
3
3
|
import { cx } from '../class-names.js';
|
|
4
|
-
import { glyphTileSizes } from '../css/css-glyph-tile.js';
|
|
4
|
+
import { glyphTileLetterLimit, glyphTileSizes, } from '../css/css-glyph-tile.js';
|
|
5
5
|
export { glyphTileSizes };
|
|
6
|
+
/** The letters of a single string or number child, or null for a glyph. */
|
|
7
|
+
function tileLetters(children) {
|
|
8
|
+
if (typeof children !== 'string' && typeof children !== 'number')
|
|
9
|
+
return null;
|
|
10
|
+
return [...String(children).trim()];
|
|
11
|
+
}
|
|
6
12
|
/**
|
|
7
|
-
* A small rounded square in a categorical colour holding a white glyph
|
|
8
|
-
* kind's mark beside its name, such as a service in
|
|
13
|
+
* A small rounded square in a categorical colour holding a white glyph (or
|
|
14
|
+
* up to three letters): a kind's mark beside its name, such as a service in
|
|
15
|
+
* a list. Decorative
|
|
9
16
|
* (`aria-hidden`): the visible label beside it carries the meaning.
|
|
10
17
|
*/
|
|
11
18
|
export function GlyphTile({ children, className, hue, size = 'md', ...rest }) {
|
|
@@ -13,5 +20,9 @@ export function GlyphTile({ children, className, hue, size = 'md', ...rest }) {
|
|
|
13
20
|
throw new RangeError(`Unknown GlyphTile hue: ${String(hue)}`);
|
|
14
21
|
if (!glyphTileSizes.includes(size))
|
|
15
22
|
throw new RangeError(`Unknown GlyphTile size: ${String(size)}`);
|
|
16
|
-
|
|
23
|
+
const letters = tileLetters(children);
|
|
24
|
+
if (letters &&
|
|
25
|
+
(letters.length === 0 || letters.length > glyphTileLetterLimit))
|
|
26
|
+
throw new RangeError(`GlyphTile letters are 1 to ${glyphTileLetterLimit} characters: ${String(children)}`);
|
|
27
|
+
return (_jsx("span", { ...rest, "aria-hidden": "true", className: cx('sw-glyph-tile', `sw-glyph-tile-${hue}`, `sw-glyph-tile-${size}`, className), children: letters ? (_jsx("span", { className: `sw-glyph-tile-text sw-glyph-tile-text-${letters.length}`, children: letters.join('') })) : (children) }));
|
|
17
28
|
}
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { type SpacingStep } from '@scalewing/tokens';
|
|
2
2
|
import { type Breakpoint } from '../css/breakpoints.js';
|
|
3
|
-
import { type GridColumns } from '../css/css-grid.js';
|
|
3
|
+
import { type GridColumns, type GridFit, gridFits } from '../css/css-grid.js';
|
|
4
4
|
import { type BoxProps } from './Box.js';
|
|
5
5
|
import { type Align } from './Stack.js';
|
|
6
|
-
export type
|
|
6
|
+
export { gridFits, type GridColumns, type GridFit };
|
|
7
7
|
export type GridProps = BoxProps & {
|
|
8
8
|
/** Where each child sits in the height of its row; unset, children stretch. */
|
|
9
9
|
align?: Align;
|
|
10
10
|
columns?: GridColumns;
|
|
11
11
|
columnsBelow?: Partial<Record<Breakpoint, GridColumns>>;
|
|
12
|
+
/**
|
|
13
|
+
* Fits the columns to the grid's width instead of a fixed count: as many
|
|
14
|
+
* as fit at the named minimum (`narrow` 15rem, `medium` 16.25rem, `wide`
|
|
15
|
+
* 18.75rem), sharing the row, one column when narrower. Not with
|
|
16
|
+
* `columns` or `columnsBelow`; a child's `columnSpan` is ignored in a
|
|
17
|
+
* fitted grid, where every child takes one column.
|
|
18
|
+
*/
|
|
19
|
+
fit?: GridFit;
|
|
12
20
|
gap?: SpacingStep;
|
|
13
21
|
};
|
|
14
|
-
export declare function Grid({ align, className, columns, columnsBelow, gap, ...rest }: GridProps): import("react").JSX.Element;
|
|
22
|
+
export declare function Grid({ align, className, columns, columnsBelow, fit, gap, ...rest }: GridProps): import("react").JSX.Element;
|
package/dist/components/Grid.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../class-names.js';
|
|
3
3
|
import { breakpoints } from '../css/breakpoints.js';
|
|
4
|
-
import { gridColumnsBelowClass, gridColumnsClass, } from '../css/css-grid.js';
|
|
4
|
+
import { gridColumnsBelowClass, gridColumnsClass, gridFitClass, gridFits, } from '../css/css-grid.js';
|
|
5
5
|
import { spacingClass } from '../css/spacing-classes.js';
|
|
6
6
|
import { assertGridCount } from '../grid-counts.js';
|
|
7
|
+
import { isOneOf } from '../is-one-of.js';
|
|
7
8
|
import { Box } from './Box.js';
|
|
8
|
-
export
|
|
9
|
-
|
|
9
|
+
export { gridFits };
|
|
10
|
+
export function Grid({ align, className, columns, columnsBelow, fit, gap = 0, ...rest }) {
|
|
11
|
+
if (fit !== undefined) {
|
|
12
|
+
if (!isOneOf(gridFits, fit))
|
|
13
|
+
throw new RangeError(`Unknown Grid fit: ${String(fit)}`);
|
|
14
|
+
if (columns !== undefined || columnsBelow !== undefined)
|
|
15
|
+
throw new RangeError('A fitted Grid takes no columns or columnsBelow; fit sets them');
|
|
16
|
+
}
|
|
17
|
+
const fixedColumns = columns ?? 1;
|
|
18
|
+
assertGridCount('columns', fixedColumns);
|
|
10
19
|
const belowClasses = breakpoints.flatMap((breakpoint) => {
|
|
11
20
|
const count = columnsBelow?.[breakpoint];
|
|
12
21
|
if (count === undefined)
|
|
@@ -14,5 +23,5 @@ export function Grid({ align, className, columns = 1, columnsBelow, gap = 0, ...
|
|
|
14
23
|
assertGridCount('columnsBelow', count);
|
|
15
24
|
return [gridColumnsBelowClass(breakpoint, count)];
|
|
16
25
|
});
|
|
17
|
-
return (_jsx(Box, { className: cx('sw-grid', gridColumnsClass(
|
|
26
|
+
return (_jsx(Box, { className: cx('sw-grid', fit ? gridFitClass(fit) : gridColumnsClass(fixedColumns), ...belowClasses, spacingClass('gap', 'all', gap), align && `sw-align-${align}`, className), ...rest }));
|
|
18
27
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
export type HeroProps = Omit<HTMLAttributes<HTMLElement>, 'aria-labelledby' | 'children' | 'style' | 'title'> & {
|
|
3
|
+
/** The page's title, its one `h1`, in the `hero` size. */
|
|
4
|
+
title: ReactNode;
|
|
5
|
+
/** A short line above the title saying who or what the page is for. */
|
|
6
|
+
eyebrow?: ReactNode;
|
|
7
|
+
/** The sentence under the title, in the `lead` size. */
|
|
8
|
+
lead?: ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* Links or buttons in a row, stacked to the full width below `md`. Leave
|
|
11
|
+
* it out rather than passing an empty fragment, which keeps the row.
|
|
12
|
+
*/
|
|
13
|
+
actions?: ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* A picture beside the words from `md`, under them below it. Leave it out
|
|
16
|
+
* rather than passing an empty fragment, which keeps its column.
|
|
17
|
+
*/
|
|
18
|
+
media?: ReactNode;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A public page's opening, as a `section` named by its title: an optional
|
|
22
|
+
* eyebrow, the title (`h1`, `hero` size), an optional lead (`lead` size)
|
|
23
|
+
* and actions, with an optional picture beside them from `md`. The title
|
|
24
|
+
* always names the section. A blank title throws a `RangeError`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function Hero({ actions, className, eyebrow, lead, media, title, ...rest }: HeroProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
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 { Text } from './Text.js';
|
|
5
|
+
function isBlank(node) {
|
|
6
|
+
return (node === undefined ||
|
|
7
|
+
node === null ||
|
|
8
|
+
node === false ||
|
|
9
|
+
(typeof node === 'string' && node.trim() === ''));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A public page's opening, as a `section` named by its title: an optional
|
|
13
|
+
* eyebrow, the title (`h1`, `hero` size), an optional lead (`lead` size)
|
|
14
|
+
* and actions, with an optional picture beside them from `md`. The title
|
|
15
|
+
* always names the section. A blank title throws a `RangeError`.
|
|
16
|
+
*/
|
|
17
|
+
export function Hero({ actions, className, eyebrow, lead, media, title, ...rest }) {
|
|
18
|
+
const titleId = useId();
|
|
19
|
+
if (isBlank(title))
|
|
20
|
+
throw new RangeError('A Hero needs a title');
|
|
21
|
+
const withMedia = !isBlank(media);
|
|
22
|
+
return (_jsxs("section", { ...rest, "aria-labelledby": titleId, className: cx('sw-hero', withMedia && 'sw-hero-with-media', className), children: [_jsxs("div", { className: "sw-hero-text", children: [isBlank(eyebrow) ? null : (_jsx(Text, { color: "muted", as: "p", variant: "label", children: eyebrow })), _jsx(Text, { id: titleId, variant: "hero", children: title }), isBlank(lead) ? null : _jsx(Text, { variant: "lead", children: lead }), isBlank(actions) ? null : (_jsx("div", { className: "sw-hero-actions", children: actions }))] }), withMedia ? _jsx("div", { className: "sw-hero-media", children: media }) : null] }));
|
|
23
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -36,8 +36,18 @@ export type StepperProps = {
|
|
|
36
36
|
disabled?: boolean;
|
|
37
37
|
/** Readable and focusable, never changed: the buttons are hidden and keep their room. */
|
|
38
38
|
readOnly?: boolean;
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Says the count in words for assistive technology (`aria-valuetext`),
|
|
41
|
+
* such as "12 straps". It wins over the count and `unit` read together.
|
|
42
|
+
*/
|
|
40
43
|
valueText?: string;
|
|
44
|
+
/**
|
|
45
|
+
* A short muted word after the count, inside its frame, such as "min".
|
|
46
|
+
* Without `valueText` the count is read with it ("90 min"), word for
|
|
47
|
+
* word: a unit that changes with the count ("players") reads "1 players",
|
|
48
|
+
* so pass `valueText` with the right form ("1 player"). Not blank.
|
|
49
|
+
*/
|
|
50
|
+
unit?: string;
|
|
41
51
|
/**
|
|
42
52
|
* Ids of elements that describe the count, such as the value it adds up
|
|
43
53
|
* to; read before an `error`'s message.
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { forwardRef, useId, useState, } from 'react';
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
|
-
import { assertStepperBounds, readStepperEntry, stepperMoves, stepperText, } from '../stepper-count.js';
|
|
5
|
+
import { assertStepperBounds, assertStepperUnit, readStepperEntry, stepperMoves, stepperText, stepperValueText, } from '../stepper-count.js';
|
|
6
6
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
7
7
|
import { FieldLabelText } from './FieldLabelText.js';
|
|
8
|
+
import { StepperCountFrame } from './stepper/StepperCountFrame.js';
|
|
8
9
|
export const stepperLimitAppearances = ['faded', 'plain'];
|
|
9
10
|
/** Keeps focus where it was (the count, or nowhere) when a button is pressed. */
|
|
10
11
|
function keepFocus(event) {
|
|
@@ -15,9 +16,10 @@ function keepFocus(event) {
|
|
|
15
16
|
* tab stop (the count, a spin button that ArrowUp and ArrowDown step), with
|
|
16
17
|
* the buttons as pointer targets beside it. − stops at `min`, + at `max`.
|
|
17
18
|
*/
|
|
18
|
-
export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden = false, value, onChange, min = 0, max = Number.POSITIVE_INFINITY, step = 1, decrementLabel, incrementLabel, error, changed = false, disabled = false, readOnly = false, valueText, 'aria-describedby': describedBy, placeholder, name, width = 'fixed', limitAppearance = 'faded', }, ref) {
|
|
19
|
+
export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden = false, value, onChange, min = 0, max = Number.POSITIVE_INFINITY, step = 1, decrementLabel, incrementLabel, error, changed = false, disabled = false, readOnly = false, valueText, unit, 'aria-describedby': describedBy, placeholder, name, width = 'fixed', limitAppearance = 'faded', }, ref) {
|
|
19
20
|
const bounds = { min, max, step };
|
|
20
21
|
assertStepperBounds(bounds);
|
|
22
|
+
assertStepperUnit(unit);
|
|
21
23
|
if (!stepperLimitAppearances.includes(limitAppearance))
|
|
22
24
|
throw new RangeError(`Unknown Stepper limitAppearance: ${String(limitAppearance)}`);
|
|
23
25
|
if (value !== null && !Number.isSafeInteger(value))
|
|
@@ -76,10 +78,10 @@ export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden
|
|
|
76
78
|
return (_jsxs("div", { className: cx('sw-stepper', width === 'full' && 'sw-stepper-full', invalid && 'sw-stepper-invalid', changed && !invalid && 'sw-stepper-changed', readOnly && 'sw-stepper-readonly', limitAppearance === 'plain' &&
|
|
77
79
|
!disabled &&
|
|
78
80
|
!readOnly &&
|
|
79
|
-
'sw-stepper-limit-plain'), children: [_jsx("label", { className: labelVisuallyHidden ? 'sw-sr-only' : undefined, htmlFor: inputId, id: labelId, children: _jsx(FieldLabelText, { label: label }) }), _jsxs("div", { "aria-labelledby": labelId, className: "sw-stepper-track", role: "group", children: [_jsx("button", { "aria-controls": inputId, "aria-label": decrementLabel, className: "sw-stepper-button", disabled: moves.down === null, onClick: () => move(moves.down), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: '−' }) }), _jsx("input", { ref: ref, "aria-describedby": [describedBy?.trim() || null, invalid ? errorId : null]
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
'sw-stepper-limit-plain'), children: [_jsx("label", { className: labelVisuallyHidden ? 'sw-sr-only' : undefined, htmlFor: inputId, id: labelId, children: _jsx(FieldLabelText, { label: label }) }), _jsxs("div", { "aria-labelledby": labelId, className: "sw-stepper-track", role: "group", children: [_jsx("button", { "aria-controls": inputId, "aria-label": decrementLabel, className: "sw-stepper-button", disabled: moves.down === null, onClick: () => move(moves.down), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: '−' }) }), _jsx(StepperCountFrame, { unit: unit, children: _jsx("input", { ref: ref, "aria-describedby": [describedBy?.trim() || null, invalid ? errorId : null]
|
|
82
|
+
.filter((id) => id !== null)
|
|
83
|
+
.join(' ') || undefined, "aria-invalid": invalid || undefined, "aria-valuemax": Number.isFinite(max) ? max : undefined, "aria-valuemin": min, "aria-valuenow": value ?? undefined, "aria-valuetext": stepperValueText(value, unit, valueText), autoComplete: "off", className: "sw-stepper-input", disabled: disabled, id: inputId,
|
|
84
|
+
// iOS's numeric pad has no minus key, so a range below zero takes
|
|
85
|
+
// the text keyboard.
|
|
86
|
+
inputMode: allowsNegative ? 'text' : 'numeric', name: name, onBlur: () => setDraft(null), onChange: onType, onKeyDown: onKeyDown, placeholder: placeholder, readOnly: readOnly, role: "spinbutton", type: "text", value: text }) }), _jsx("button", { "aria-controls": inputId, "aria-label": incrementLabel, className: "sw-stepper-button", disabled: moves.up === null, onClick: () => move(moves.up), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: "+" }) })] }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
|
|
85
87
|
});
|
package/dist/components/Text.js
CHANGED
|
@@ -7,7 +7,10 @@ const defaultElement = {
|
|
|
7
7
|
data: 'span',
|
|
8
8
|
display: 'h1',
|
|
9
9
|
heading: 'h2',
|
|
10
|
+
headline: 'h2',
|
|
11
|
+
hero: 'h1',
|
|
10
12
|
label: 'span',
|
|
13
|
+
lead: 'p',
|
|
11
14
|
title: 'h3',
|
|
12
15
|
};
|
|
13
16
|
export const Text = forwardRef(function Text({ align, as, className, color = 'text', style, truncate = false, variant = 'body', ...rest }, ref) {
|
|
@@ -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
|
+
}
|
|
@@ -63,6 +63,9 @@ export type AppShellProps = {
|
|
|
63
63
|
onNavigate?: (item: AppShellNavItem) => void;
|
|
64
64
|
/**
|
|
65
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.
|
|
66
69
|
* While the drawer is open it is mounted twice (the sidebar's copy is
|
|
67
70
|
* hidden), so each copy keeps its own state: put no fixed DOM `id` in it
|
|
68
71
|
* (use `useId`), and keep state that must be shared in the consumer.
|
|
@@ -71,7 +74,11 @@ export type AppShellProps = {
|
|
|
71
74
|
header?: ReactNode;
|
|
72
75
|
/** The sidebar's bottom, such as the account menu; also the drawer's (see `header`). */
|
|
73
76
|
footer?: ReactNode;
|
|
74
|
-
/**
|
|
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
|
+
*/
|
|
75
82
|
railHeader?: ReactNode;
|
|
76
83
|
/** A compact `footer` for the rail. Left out, the rail shows none. */
|
|
77
84
|
railFooter?: ReactNode;
|
|
@@ -81,6 +88,34 @@ export type AppShellProps = {
|
|
|
81
88
|
barEnd?: ReactNode;
|
|
82
89
|
/** The consumer's glyph for the menu button (Scalewing has no icon catalog). */
|
|
83
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;
|
|
84
119
|
labels: AppShellLabels;
|
|
85
120
|
/** The page, rendered in `main`. */
|
|
86
121
|
children: ReactNode;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AppShellProps } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Fails closed on the shell's words, its groups and
|
|
4
|
-
* across every group, each with an icon, which is
|
|
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).
|
|
5
6
|
*/
|
|
6
|
-
export declare function assertAppShellProps({ groups, labels, navLabel, }: Pick<AppShellProps, 'groups' | 'labels' | 'navLabel'>): void;
|
|
7
|
+
export declare function assertAppShellProps({ groups, labels, menuCount, menuCountLabel, menuCountTone, navLabel, }: Pick<AppShellProps, 'groups' | 'labels' | 'menuCount' | 'menuCountLabel' | 'menuCountTone' | 'navLabel'>): void;
|
|
@@ -28,14 +28,32 @@ function assertGroups(groups) {
|
|
|
28
28
|
throw new RangeError('AppShell groups need at least one item');
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Fails closed on the
|
|
32
|
-
*
|
|
31
|
+
* Fails closed on the menu button's count: a whole number from 0, a
|
|
32
|
+
* nonempty label and a known tone, each of the last two with a count.
|
|
33
33
|
*/
|
|
34
|
-
|
|
34
|
+
function assertMenuCount({ menuCount, menuCountLabel, menuCountTone, }) {
|
|
35
|
+
if (menuCount !== undefined &&
|
|
36
|
+
(!Number.isInteger(menuCount) || menuCount < 0))
|
|
37
|
+
throw new RangeError(`AppShell menuCount must be a whole number from 0: ${String(menuCount)}`);
|
|
38
|
+
if (menuCountLabel !== undefined && isBlank(menuCountLabel))
|
|
39
|
+
throw new RangeError('AppShell menuCountLabel must be nonempty');
|
|
40
|
+
if (menuCountTone !== undefined && !isOneOf(navCountTones, menuCountTone))
|
|
41
|
+
throw new RangeError(`AppShell menuCountTone must be one of ${navCountTones.join(', ')}`);
|
|
42
|
+
if (menuCount === undefined &&
|
|
43
|
+
(menuCountLabel !== undefined || menuCountTone !== undefined))
|
|
44
|
+
throw new RangeError('AppShell menuCountLabel and menuCountTone come with a menuCount');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fails closed on the shell's words, the menu's count, its groups and
|
|
48
|
+
* their items (ids unique across every group, each with an icon, which is
|
|
49
|
+
* all the rail shows).
|
|
50
|
+
*/
|
|
51
|
+
export function assertAppShellProps({ groups, labels, menuCount, menuCountLabel, menuCountTone, navLabel, }) {
|
|
35
52
|
if (isBlank(navLabel))
|
|
36
53
|
throw new RangeError('AppShell navLabel must be nonempty');
|
|
37
54
|
if (isBlank(labels?.openMenu) || isBlank(labels?.closeMenu))
|
|
38
55
|
throw new RangeError('AppShell labels openMenu and closeMenu are nonempty');
|
|
56
|
+
assertMenuCount({ menuCount, menuCountLabel, menuCountTone });
|
|
39
57
|
assertGroups(groups);
|
|
40
58
|
const items = groups.flatMap((group) => group.items);
|
|
41
59
|
assertNavItems('AppShell', items);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type PointerEvent } from 'react';
|
|
2
|
+
import { type CommandPaletteGroup, type CommandPaletteItem } from './types.js';
|
|
3
|
+
type PaletteListProps = {
|
|
4
|
+
listId: string;
|
|
5
|
+
label: string;
|
|
6
|
+
groups: readonly CommandPaletteGroup[];
|
|
7
|
+
activeId: string | undefined;
|
|
8
|
+
/** The row's DOM id from the item's id. */
|
|
9
|
+
rowId: (item: CommandPaletteItem) => string;
|
|
10
|
+
onActivate: (item: CommandPaletteItem, event: PointerEvent) => void;
|
|
11
|
+
onChoose: (item: CommandPaletteItem) => void;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The always-open results: a listbox named by the palette's label, each
|
|
15
|
+
* group a `group` under its caption, each item its mark (hidden), label and
|
|
16
|
+
* detail, and the active row's Enter key hint (hidden: Enter is how a
|
|
17
|
+
* listbox row is chosen).
|
|
18
|
+
*/
|
|
19
|
+
export declare function PaletteList({ activeId, groups, label, listId, onActivate, onChoose, rowId, }: PaletteListProps): import("react").JSX.Element;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The always-open results: a listbox named by the palette's label, each
|
|
4
|
+
* group a `group` under its caption, each item its mark (hidden), label and
|
|
5
|
+
* detail, and the active row's Enter key hint (hidden: Enter is how a
|
|
6
|
+
* listbox row is chosen).
|
|
7
|
+
*/
|
|
8
|
+
export function PaletteList({ activeId, groups, label, listId, onActivate, onChoose, rowId, }) {
|
|
9
|
+
return (_jsx("div", { "aria-label": label, className: "sw-command-palette-list", hidden: !groups.some((group) => group.items.length > 0), id: listId, role: "listbox", children: groups.map((group, index) => {
|
|
10
|
+
if (group.items.length === 0)
|
|
11
|
+
return null;
|
|
12
|
+
const headingId = `${listId}-group-${index}`;
|
|
13
|
+
return (_jsxs("div", { "aria-labelledby": headingId, role: "group", children: [_jsx("div", { className: "sw-command-palette-heading", id: headingId, role: "presentation", children: group.label }), group.items.map((item) => {
|
|
14
|
+
const active = item.id === activeId;
|
|
15
|
+
return (_jsxs("div", { "aria-selected": active, className: "sw-command-palette-item", id: rowId(item), onClick: () => onChoose(item), onPointerMove: (event) => {
|
|
16
|
+
if (!active)
|
|
17
|
+
onActivate(item, event);
|
|
18
|
+
}, role: "option", children: [item.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-command-palette-icon", children: item.icon })) : null, _jsxs("span", { className: "sw-command-palette-words", children: [_jsx("span", { className: "sw-command-palette-label", children: item.label }), item.detail ? (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-command-palette-detail", children: item.detail })] })) : null] }), active ? (_jsx("span", { "aria-hidden": "true", className: "sw-command-palette-key", children: "\u21B5" })) : null] }, item.id));
|
|
19
|
+
})] }, group.id));
|
|
20
|
+
}) }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CommandPaletteShortcut } from './types.js';
|
|
2
|
+
/** Whether the system's command key is ⌘ (Apple) rather than Ctrl. */
|
|
3
|
+
export declare function usesCommandKey(platform: string): boolean;
|
|
4
|
+
/** An element a person types into: a text field, a select or editable text. */
|
|
5
|
+
export declare function isEditable(target: EventTarget | null): boolean;
|
|
6
|
+
type ShortcutKey = Pick<KeyboardEvent, 'key' | 'code' | 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey' | 'repeat' | 'isComposing'>;
|
|
7
|
+
/**
|
|
8
|
+
* ⌘K on an Apple system or Ctrl+K elsewhere, with no other modifier: the K
|
|
9
|
+
* the layout types, or the key in K's place (`KeyK`) on a layout that types
|
|
10
|
+
* another letter there, such as Cyrillic or Greek.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isModK(event: ShortcutKey, commandKey: boolean): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Listens on the document for the palette's shortcut while `shortcut` is
|
|
15
|
+
* set: it calls `onToggle` (and stops the browser's own use of the keys)
|
|
16
|
+
* unless focus is in another text field than `ownField`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function usePaletteShortcut(shortcut: CommandPaletteShortcut | null | undefined, ownField: () => HTMLElement | null, onToggle: () => void): void;
|
|
19
|
+
export {};
|