@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
|
@@ -6,7 +6,8 @@ export type { AppShellLabels, AppShellNavGroup, AppShellNavItem, AppShellProps,
|
|
|
6
6
|
* From 75rem the sidebar is full (glyphs, labels, counts and group
|
|
7
7
|
* captions); from `lg` to 75rem it is a rail of glyphs with tooltips; below
|
|
8
8
|
* `lg` a sticky top bar's menu button opens the same navigation in a modal
|
|
9
|
-
* drawer from the inline start
|
|
10
|
-
* `
|
|
9
|
+
* drawer from the inline start, its button optionally carrying a count.
|
|
10
|
+
* `pageBar` is a sticky band at the top of `main` on every layout. Invalid
|
|
11
|
+
* groups, items, counts or words throw a `RangeError`.
|
|
11
12
|
*/
|
|
12
|
-
export declare function AppShell({ barEnd, barTitle, children, footer, groups, header, labels, menuIcon, navLabel, onNavigate, railFooter, railHeader, }: AppShellProps): import("react").JSX.Element;
|
|
13
|
+
export declare function AppShell({ barEnd, barTitle, children, footer, groups, header, labels, menuCount, menuCountLabel, menuCountTone, menuIcon, navLabel, onNavigate, pageBar, railFooter, railHeader, }: AppShellProps): import("react").JSX.Element;
|
|
@@ -3,11 +3,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { breakpointQuery } from '../css/breakpoints.js';
|
|
5
5
|
import { appShellWideQuery } from '../css/css-app-shell.js';
|
|
6
|
-
import {
|
|
6
|
+
import { appShellBarHeightVariable } from '../css/css-app-shell-page-bar.js';
|
|
7
|
+
import { AppShellBar } from './app-shell/AppShellBar.js';
|
|
7
8
|
import { AppShellDrawer } from './app-shell/AppShellDrawer.js';
|
|
8
9
|
import { AppShellSidebar } from './app-shell/AppShellSidebar.js';
|
|
9
10
|
import { assertAppShellProps } from './app-shell/validate.js';
|
|
10
|
-
import {
|
|
11
|
+
import { useElementHeight } from './use-element-height.js';
|
|
11
12
|
import { useMediaQuery } from './use-media-query.js';
|
|
12
13
|
const fromLg = breakpointQuery('from', 'lg');
|
|
13
14
|
/**
|
|
@@ -16,18 +17,34 @@ const fromLg = breakpointQuery('from', 'lg');
|
|
|
16
17
|
* From 75rem the sidebar is full (glyphs, labels, counts and group
|
|
17
18
|
* captions); from `lg` to 75rem it is a rail of glyphs with tooltips; below
|
|
18
19
|
* `lg` a sticky top bar's menu button opens the same navigation in a modal
|
|
19
|
-
* drawer from the inline start
|
|
20
|
-
* `
|
|
20
|
+
* drawer from the inline start, its button optionally carrying a count.
|
|
21
|
+
* `pageBar` is a sticky band at the top of `main` on every layout. Invalid
|
|
22
|
+
* groups, items, counts or words throw a `RangeError`.
|
|
21
23
|
*/
|
|
22
|
-
export function AppShell({ barEnd, barTitle, children, footer, groups, header, labels, menuIcon, navLabel, onNavigate, railFooter, railHeader, }) {
|
|
23
|
-
assertAppShellProps({
|
|
24
|
+
export function AppShell({ barEnd, barTitle, children, footer, groups, header, labels, menuCount, menuCountLabel, menuCountTone, menuIcon, navLabel, onNavigate, pageBar, railFooter, railHeader, }) {
|
|
25
|
+
assertAppShellProps({
|
|
26
|
+
groups,
|
|
27
|
+
labels,
|
|
28
|
+
menuCount,
|
|
29
|
+
menuCountLabel,
|
|
30
|
+
menuCountTone,
|
|
31
|
+
navLabel,
|
|
32
|
+
});
|
|
24
33
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
25
34
|
const menuRef = useRef(null);
|
|
35
|
+
const barRef = useRef(null);
|
|
26
36
|
// Set by a dismissal (Escape, a press outside, the close button): only
|
|
27
37
|
// that hands focus back to the menu button.
|
|
28
38
|
const refocusRef = useRef(false);
|
|
29
39
|
const sidebarShown = useMediaQuery(fromLg, false);
|
|
30
40
|
const wide = useMediaQuery(appShellWideQuery, false);
|
|
41
|
+
// A page bar sticks under the compact bar, however tall its end controls
|
|
42
|
+
// make it; the generated CSS falls back to the bar's usual height.
|
|
43
|
+
const hasPageBar = pageBar !== undefined && pageBar !== null && pageBar !== false;
|
|
44
|
+
const barHeight = useElementHeight(barRef, hasPageBar);
|
|
45
|
+
const shellStyle = barHeight === null
|
|
46
|
+
? undefined
|
|
47
|
+
: { [appShellBarHeightVariable]: `${barHeight}px` };
|
|
31
48
|
// The drawer belongs to the bar, which is gone from `lg` up: a window
|
|
32
49
|
// widened while it is open closes it.
|
|
33
50
|
if (sidebarShown && menuOpen)
|
|
@@ -41,7 +58,7 @@ export function AppShell({ barEnd, barTitle, children, footer, groups, header, l
|
|
|
41
58
|
refocusRef.current = false;
|
|
42
59
|
menuRef.current?.focus();
|
|
43
60
|
}, [menuOpen]);
|
|
44
|
-
return (_jsxs("div", { className: "sw-app-shell",
|
|
61
|
+
return (_jsxs("div", { className: "sw-app-shell", style: shellStyle, children: [_jsx(AppShellBar, { barEnd: barEnd, barRef: barRef, barTitle: barTitle, menuCount: menuCount, menuCountLabel: menuCountLabel, menuCountTone: menuCountTone, menuIcon: menuIcon, menuOpen: menuOpen, menuRef: menuRef, onOpenMenu: () => setMenuOpen(true), openMenuLabel: labels.openMenu }), _jsx(AppShellSidebar, { footer: footer, groups: groups, header: header, navLabel: navLabel, onNavigate: onNavigate, rail: sidebarShown && !wide, railFooter: railFooter, railHeader: railHeader }), _jsxs("main", { className: "sw-app-shell-main", children: [hasPageBar ? (_jsx("div", { className: "sw-app-shell-page-bar", children: pageBar })) : null, children] }), _jsx(AppShellDrawer, { closeLabel: labels.closeMenu, footer: footer, groups: groups, header: header, navLabel: navLabel, onChoose: () => setMenuOpen(false), onClose: () => {
|
|
45
62
|
refocusRef.current = true;
|
|
46
63
|
setMenuOpen(false);
|
|
47
64
|
}, onNavigate: onNavigate, open: menuOpen })] }));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
import { type BandTone, bandTones, type BandWidth, bandWidths } from '../css/css-band.js';
|
|
3
|
+
import { type SpacingProps } from '../spacing-classes.js';
|
|
4
|
+
export { bandTones, bandWidths, type BandTone, type BandWidth };
|
|
5
|
+
export declare const bandElements: readonly ["section", "div", "aside", "footer"];
|
|
6
|
+
export type BandElement = (typeof bandElements)[number];
|
|
7
|
+
export type BandProps = Omit<HTMLAttributes<HTMLElement>, 'style'> & Pick<SpacingProps, 'paddingY' | 'paddingTop' | 'paddingBottom'> & {
|
|
8
|
+
/**
|
|
9
|
+
* The fill: `surface`, `subtle` (the quiet grey), or `inverse`, the dark
|
|
10
|
+
* band (`--sw-color-inverse`), inside which words, muted words,
|
|
11
|
+
* hairlines, links, the accent and a primary button read in
|
|
12
|
+
* `onInverse`, so its content needs no band-aware variant.
|
|
13
|
+
*/
|
|
14
|
+
tone: BandTone;
|
|
15
|
+
/** The element: `section` (default), `div`, `aside` or `footer`. */
|
|
16
|
+
as?: BandElement;
|
|
17
|
+
/**
|
|
18
|
+
* The content column: `container` (default) is the page's width,
|
|
19
|
+
* `reading` the narrower reading width; either keeps the page gutter.
|
|
20
|
+
*/
|
|
21
|
+
width?: BandWidth;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A full-width stretch of the page in one fill, its content in a centred
|
|
26
|
+
* column with the page gutter: a site's footer, one dark closing section,
|
|
27
|
+
* a grey strip between white ones. Vertical padding is a spacing step
|
|
28
|
+
* (default 8). An unknown tone, width or element throws a `RangeError`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function Band({ as, children, className, paddingBottom, paddingTop, paddingY, tone, width, ...rest }: BandProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../class-names.js';
|
|
3
|
+
import { bandToneClass, bandTones, bandWidthClass, bandWidths, } from '../css/css-band.js';
|
|
4
|
+
import { isOneOf } from '../is-one-of.js';
|
|
5
|
+
import { spacingClassNames } from '../spacing-classes.js';
|
|
6
|
+
export { bandTones, bandWidths };
|
|
7
|
+
export const bandElements = ['section', 'div', 'aside', 'footer'];
|
|
8
|
+
/**
|
|
9
|
+
* A full-width stretch of the page in one fill, its content in a centred
|
|
10
|
+
* column with the page gutter: a site's footer, one dark closing section,
|
|
11
|
+
* a grey strip between white ones. Vertical padding is a spacing step
|
|
12
|
+
* (default 8). An unknown tone, width or element throws a `RangeError`.
|
|
13
|
+
*/
|
|
14
|
+
export function Band({ as = 'section', children, className, paddingBottom, paddingTop, paddingY = 8, tone, width = 'container', ...rest }) {
|
|
15
|
+
if (!isOneOf(bandTones, tone))
|
|
16
|
+
throw new RangeError(`Unknown Band tone: ${String(tone)}`);
|
|
17
|
+
if (!isOneOf(bandWidths, width))
|
|
18
|
+
throw new RangeError(`Unknown Band width: ${String(width)}`);
|
|
19
|
+
if (!isOneOf(bandElements, as))
|
|
20
|
+
throw new RangeError(`Unknown Band element: ${String(as)}`);
|
|
21
|
+
const Element = as;
|
|
22
|
+
return (_jsx(Element, { className: cx('sw-band', bandToneClass(tone), ...spacingClassNames({ paddingY, paddingTop, paddingBottom }), className), ...rest, children: _jsx("div", { className: cx('sw-band-inner', bandWidthClass(width)), children: children }) }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ButtonSize } from '@scalewing/tokens';
|
|
2
|
+
import { type AnchorHTMLAttributes, type ReactNode } from 'react';
|
|
3
|
+
import { type WebButtonVariant } from '../css/css-button.js';
|
|
4
|
+
import { type ButtonWidth } from '../css/css-button-width.js';
|
|
5
|
+
export type ButtonLinkProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'style'> & {
|
|
6
|
+
/** Where the link goes. */
|
|
7
|
+
href: string;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
size?: ButtonSize;
|
|
10
|
+
/** The Button's variants: `primary` (default), `secondary`, `tertiary`, `ghost`, `danger`, `neutral`. */
|
|
11
|
+
variant?: WebButtonVariant;
|
|
12
|
+
/** `content` (default) is as wide as the words; `full` fills the container. */
|
|
13
|
+
width?: ButtonWidth;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A link drawn as a Button, for a destination that reads as an action, such
|
|
17
|
+
* as a site's "Contact us" or "Sign in": an `<a href>` with the Button's
|
|
18
|
+
* variants, sizes and width, so it opens like any link (a new tab, a
|
|
19
|
+
* server-rendered page with no script). `aria-current="page"` marks the
|
|
20
|
+
* one that is the current page with the pressed ring. A link has no
|
|
21
|
+
* disabled state; render no link instead. An unknown variant, size or
|
|
22
|
+
* width throws a `RangeError`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const ButtonLink: import("react").ForwardRefExoticComponent<Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "style" | "href"> & {
|
|
25
|
+
/** Where the link goes. */
|
|
26
|
+
href: string;
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
size?: ButtonSize;
|
|
29
|
+
/** The Button's variants: `primary` (default), `secondary`, `tertiary`, `ghost`, `danger`, `neutral`. */
|
|
30
|
+
variant?: WebButtonVariant;
|
|
31
|
+
/** `content` (default) is as wide as the words; `full` fills the container. */
|
|
32
|
+
width?: ButtonWidth;
|
|
33
|
+
} & import("react").RefAttributes<HTMLAnchorElement>>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { buttonSizes } from '@scalewing/tokens';
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { cx } from '../class-names.js';
|
|
5
|
+
import { buttonClassNames, webButtonVariants, } from '../css/css-button.js';
|
|
6
|
+
import { buttonWidthClass, buttonWidths, } from '../css/css-button-width.js';
|
|
7
|
+
import { buttonLinkClass } from '../css/css-button-link.js';
|
|
8
|
+
import { isOneOf } from '../is-one-of.js';
|
|
9
|
+
import { tabStop } from '../tab-stop.js';
|
|
10
|
+
/**
|
|
11
|
+
* A link drawn as a Button, for a destination that reads as an action, such
|
|
12
|
+
* as a site's "Contact us" or "Sign in": an `<a href>` with the Button's
|
|
13
|
+
* variants, sizes and width, so it opens like any link (a new tab, a
|
|
14
|
+
* server-rendered page with no script). `aria-current="page"` marks the
|
|
15
|
+
* one that is the current page with the pressed ring. A link has no
|
|
16
|
+
* disabled state; render no link instead. An unknown variant, size or
|
|
17
|
+
* width throws a `RangeError`.
|
|
18
|
+
*/
|
|
19
|
+
export const ButtonLink = forwardRef(function ButtonLink({ children, className, href, size = 'md', tabIndex, variant = 'primary', width = 'content', ...rest }, ref) {
|
|
20
|
+
if (!isOneOf(webButtonVariants, variant))
|
|
21
|
+
throw new RangeError(`Unknown ButtonLink variant: ${String(variant)}`);
|
|
22
|
+
if (!isOneOf(buttonSizes, size))
|
|
23
|
+
throw new RangeError(`Unknown ButtonLink size: ${String(size)}`);
|
|
24
|
+
if (!isOneOf(buttonWidths, width))
|
|
25
|
+
throw new RangeError(`Unknown ButtonLink width: ${String(width)}`);
|
|
26
|
+
return (_jsx("a", { ref: ref, className: cx(...buttonClassNames({ size, variant }), buttonLinkClass, buttonWidthClass(width), className), href: href, tabIndex: tabIndex ?? tabStop, ...rest, children: children }));
|
|
27
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CommandPaletteProps } from './command-palette/types.js';
|
|
2
|
+
export { commandPaletteShortcuts, type CommandPaletteGroup, type CommandPaletteItem, type CommandPaletteProps, type CommandPaletteShortcut, } from './command-palette/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A search over the product, on the top layer: a modal dialog named by
|
|
5
|
+
* `label` holding a field (an ARIA combobox) over an always-open listbox of
|
|
6
|
+
* grouped results. ↑ and ↓ move the active row (wrapping), Enter chooses
|
|
7
|
+
* it; Escape, a press outside or the shortcut again close it, and focus
|
|
8
|
+
* returns to where it was. With `shortcut="mod+k"` ⌘K or Ctrl+K opens it
|
|
9
|
+
* from anywhere but another text field. Invalid words or groups throw a
|
|
10
|
+
* `RangeError`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function CommandPalette(props: CommandPaletteProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useLayoutEffect, useRef, useState, } from 'react';
|
|
4
|
+
import { useDialogCloseRequests } from './dialog-close-requests.js';
|
|
5
|
+
import { PaletteList } from './command-palette/PaletteList.js';
|
|
6
|
+
import { usePaletteShortcut } from './command-palette/shortcut.js';
|
|
7
|
+
import { assertCommandPalette } from './command-palette/validate.js';
|
|
8
|
+
export { commandPaletteShortcuts, } from './command-palette/types.js';
|
|
9
|
+
/** The hex of an item's id, so any id makes a valid DOM id part. */
|
|
10
|
+
function idPart(id) {
|
|
11
|
+
return Array.from(new TextEncoder().encode(id), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
12
|
+
}
|
|
13
|
+
/** The words the status says: loading, nothing found, or the count. */
|
|
14
|
+
function statusWords({ countText, emptyText, loading, loadingText }, count) {
|
|
15
|
+
if (loading)
|
|
16
|
+
return loadingText ?? '';
|
|
17
|
+
if (count === 0)
|
|
18
|
+
return emptyText;
|
|
19
|
+
return countText?.(count) ?? '';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A search over the product, on the top layer: a modal dialog named by
|
|
23
|
+
* `label` holding a field (an ARIA combobox) over an always-open listbox of
|
|
24
|
+
* grouped results. ↑ and ↓ move the active row (wrapping), Enter chooses
|
|
25
|
+
* it; Escape, a press outside or the shortcut again close it, and focus
|
|
26
|
+
* returns to where it was. With `shortcut="mod+k"` ⌘K or Ctrl+K opens it
|
|
27
|
+
* from anywhere but another text field. Invalid words or groups throw a
|
|
28
|
+
* `RangeError`.
|
|
29
|
+
*/
|
|
30
|
+
export function CommandPalette(props) {
|
|
31
|
+
assertCommandPalette(props);
|
|
32
|
+
const { groups, hint, label, loading = false, loadingText, onOpenChange, onQueryChange, open, placeholder, query, shortcut, } = props;
|
|
33
|
+
const listId = useId();
|
|
34
|
+
const inputRef = useRef(null);
|
|
35
|
+
const openerRef = useRef(null);
|
|
36
|
+
const pointerAt = useRef(null);
|
|
37
|
+
const [activeId, setActiveId] = useState();
|
|
38
|
+
// The status starts empty each time the palette opens and is filled just
|
|
39
|
+
// after, so its first words are announced as a change.
|
|
40
|
+
const [announcing, setAnnouncing] = useState(false);
|
|
41
|
+
// Each opening and closing starts the active row over at the first item.
|
|
42
|
+
const [shownOpen, setShownOpen] = useState(open);
|
|
43
|
+
if (shownOpen !== open) {
|
|
44
|
+
setShownOpen(open);
|
|
45
|
+
setActiveId(undefined);
|
|
46
|
+
setAnnouncing(false);
|
|
47
|
+
}
|
|
48
|
+
// A chosen item's action, run once the dialog has really closed, so it
|
|
49
|
+
// can move focus to the page.
|
|
50
|
+
const pendingRef = useRef(null);
|
|
51
|
+
const items = groups.flatMap((group) => group.items);
|
|
52
|
+
const active = items.find((item) => item.id === activeId) ?? items[0];
|
|
53
|
+
const rowId = (item) => `${listId}-${idPart(item.id)}`;
|
|
54
|
+
const { nodeRef, handlers } = useDialogCloseRequests({
|
|
55
|
+
open,
|
|
56
|
+
onClose: () => onOpenChange(false),
|
|
57
|
+
});
|
|
58
|
+
usePaletteShortcut(shortcut, () => inputRef.current, () => onOpenChange(!open));
|
|
59
|
+
// Opening remembers where focus was (before the dialog takes it), then,
|
|
60
|
+
// once the dialog is shown, selects the field's words so typing replaces
|
|
61
|
+
// them. Closing hands focus back unless a chosen item moved it elsewhere.
|
|
62
|
+
useLayoutEffect(() => {
|
|
63
|
+
if (open)
|
|
64
|
+
openerRef.current = document.activeElement;
|
|
65
|
+
}, [open]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (open) {
|
|
68
|
+
pointerAt.current = null;
|
|
69
|
+
inputRef.current?.focus();
|
|
70
|
+
inputRef.current?.select();
|
|
71
|
+
const timer = setTimeout(() => setAnnouncing(true), 0);
|
|
72
|
+
return () => clearTimeout(timer);
|
|
73
|
+
}
|
|
74
|
+
const pending = pendingRef.current;
|
|
75
|
+
pendingRef.current = null;
|
|
76
|
+
pending?.();
|
|
77
|
+
const opener = openerRef.current;
|
|
78
|
+
openerRef.current = null;
|
|
79
|
+
const focus = document.activeElement;
|
|
80
|
+
const lost = !focus ||
|
|
81
|
+
focus === document.body ||
|
|
82
|
+
(nodeRef.current?.contains(focus) ?? false);
|
|
83
|
+
if (lost && opener instanceof HTMLElement && opener.isConnected)
|
|
84
|
+
opener.focus();
|
|
85
|
+
}, [open, nodeRef]);
|
|
86
|
+
const activeRow = active ? rowId(active) : undefined;
|
|
87
|
+
const firstActive = active !== undefined && active === items[0];
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!activeRow)
|
|
90
|
+
return;
|
|
91
|
+
// The first row brings its group's caption back into view too.
|
|
92
|
+
const list = document.getElementById(listId);
|
|
93
|
+
if (firstActive && list)
|
|
94
|
+
list.scrollTop = 0;
|
|
95
|
+
else
|
|
96
|
+
document
|
|
97
|
+
.getElementById(activeRow)
|
|
98
|
+
?.scrollIntoView?.({ block: 'nearest' });
|
|
99
|
+
}, [activeRow, firstActive, listId]);
|
|
100
|
+
function choose(item) {
|
|
101
|
+
pendingRef.current = item.onSelect;
|
|
102
|
+
onOpenChange(false);
|
|
103
|
+
}
|
|
104
|
+
function move(step) {
|
|
105
|
+
if (items.length === 0)
|
|
106
|
+
return;
|
|
107
|
+
const index = active ? items.indexOf(active) : -1;
|
|
108
|
+
const next = items[(index + step + items.length) % items.length];
|
|
109
|
+
setActiveId(next?.id);
|
|
110
|
+
}
|
|
111
|
+
function onFieldKeyDown(event) {
|
|
112
|
+
if (event.nativeEvent.isComposing || event.keyCode === 229)
|
|
113
|
+
return;
|
|
114
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
move(event.key === 'ArrowDown' ? 1 : -1);
|
|
117
|
+
}
|
|
118
|
+
else if (event.key === 'Enter') {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
if (active)
|
|
121
|
+
choose(active);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function onActivate(item, event) {
|
|
125
|
+
// A list scrolled under a still pointer reports a move with no movement.
|
|
126
|
+
const last = pointerAt.current;
|
|
127
|
+
pointerAt.current = { x: event.clientX, y: event.clientY };
|
|
128
|
+
if (last && last.x === event.clientX && last.y === event.clientY)
|
|
129
|
+
return;
|
|
130
|
+
setActiveId(item.id);
|
|
131
|
+
}
|
|
132
|
+
// A press anywhere but the field keeps focus in it, so the keys work on.
|
|
133
|
+
function keepFocus(event) {
|
|
134
|
+
if (event.target !== inputRef.current)
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
}
|
|
137
|
+
const status = statusWords(props, items.length);
|
|
138
|
+
const showEmpty = !loading && items.length === 0;
|
|
139
|
+
return (_jsx("dialog", { ref: nodeRef, "aria-label": label, className: "sw-command-palette", ...handlers, children: open ? (_jsxs("div", { className: "sw-command-palette-body", onMouseDown: keepFocus, children: [_jsx("input", { "aria-activedescendant": activeRow, "aria-autocomplete": "list", "aria-controls": listId, "aria-expanded": items.length > 0, "aria-label": label, autoComplete: "off", className: "sw-command-palette-field", onChange: (event) => {
|
|
140
|
+
setActiveId(undefined);
|
|
141
|
+
onQueryChange(event.target.value);
|
|
142
|
+
}, onKeyDown: onFieldKeyDown, placeholder: placeholder, ref: inputRef, role: "combobox", spellCheck: false, type: "text", value: query }), _jsx(PaletteList, { activeId: active?.id, groups: groups, label: label, listId: listId, onActivate: onActivate, onChoose: choose, rowId: rowId }), loading || showEmpty ? (_jsx("p", { "aria-hidden": "true", className: "sw-command-palette-message", children: loading ? loadingText : props.emptyText })) : null, _jsx("div", { className: "sw-sr-only", role: "status", children: announcing ? status : '' }), hint ? _jsx("p", { className: "sw-command-palette-hint", children: hint }) : null] })) : null }));
|
|
143
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CommandPaletteTriggerProps } from './command-palette/types.js';
|
|
2
|
+
export type { CommandPaletteTriggerProps } from './command-palette/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The button that opens a CommandPalette: a search-looking pill holding the
|
|
5
|
+
* glyph, the placeholder words and a key hint, or with `compact` the glyph
|
|
6
|
+
* alone in a round quiet button. Named by `label`; the hint is hidden from
|
|
7
|
+
* assistive technology. Blank words or a missing glyph throw a `RangeError`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function CommandPaletteTrigger(props: CommandPaletteTriggerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cx } from '../class-names.js';
|
|
4
|
+
import { tabStop } from '../tab-stop.js';
|
|
5
|
+
import { assertCommandPaletteTrigger } from './command-palette/validate.js';
|
|
6
|
+
/**
|
|
7
|
+
* The button that opens a CommandPalette: a search-looking pill holding the
|
|
8
|
+
* glyph, the placeholder words and a key hint, or with `compact` the glyph
|
|
9
|
+
* alone in a round quiet button. Named by `label`; the hint is hidden from
|
|
10
|
+
* assistive technology. Blank words or a missing glyph throw a `RangeError`.
|
|
11
|
+
*/
|
|
12
|
+
export function CommandPaletteTrigger(props) {
|
|
13
|
+
assertCommandPaletteTrigger(props);
|
|
14
|
+
const { compact = false, icon, label, onPress, placeholder, shortcutLabel, } = props;
|
|
15
|
+
return (_jsxs("button", { "aria-haspopup": "dialog", "aria-label": label, className: cx('sw-command-palette-trigger', compact && 'sw-command-palette-trigger-compact'), onClick: onPress, tabIndex: tabStop, type: "button", children: [_jsx("span", { "aria-hidden": "true", className: "sw-command-palette-trigger-icon", children: icon }), compact ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: "sw-command-palette-trigger-text", children: placeholder }), shortcutLabel ? (_jsx("span", { "aria-hidden": "true", className: "sw-command-palette-key", children: shortcutLabel })) : null] }))] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { contextSwitcherTriggerAppearances, type ContextSwitcherTriggerAppearance, contextSwitcherWidths, type ContextSwitcherWidth } from '../css/css-context-switcher-trigger.js';
|
|
2
|
+
import { type ContextSwitcherProps } from './context-switcher/types.js';
|
|
3
|
+
export type { ContextSwitcherAction, ContextSwitcherBadge, ContextSwitcherFilter, ContextSwitcherGroup, ContextSwitcherOption, ContextSwitcherProps, } from './context-switcher/types.js';
|
|
4
|
+
export { contextSwitcherTriggerAppearances, contextSwitcherWidths, type ContextSwitcherTriggerAppearance, type ContextSwitcherWidth, };
|
|
5
|
+
/**
|
|
6
|
+
* A button showing the context in use (a workspace, a competition, a
|
|
7
|
+
* season) that opens a popup to switch it: an optional filter field, the
|
|
8
|
+
* options in groups as a listbox (the current one checked), and commands
|
|
9
|
+
* in a footer. Invalid groups, options, words or props throw a
|
|
10
|
+
* `RangeError`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ContextSwitcher(props: ContextSwitcherProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useRef, useState } from 'react';
|
|
4
|
+
import { cx } from '../class-names.js';
|
|
5
|
+
import { contextSwitcherTriggerAppearances, contextSwitcherTriggerClass, contextSwitcherWidths, } from '../css/css-context-switcher-trigger.js';
|
|
6
|
+
import { tabStop } from '../tab-stop.js';
|
|
7
|
+
import { optionCount } from './context-switcher/options.js';
|
|
8
|
+
import { SwitcherPopup } from './context-switcher/SwitcherPopup.js';
|
|
9
|
+
import { assertContextSwitcher } from './context-switcher/validate.js';
|
|
10
|
+
export { contextSwitcherTriggerAppearances, contextSwitcherWidths, };
|
|
11
|
+
/**
|
|
12
|
+
* A button showing the context in use (a workspace, a competition, a
|
|
13
|
+
* season) that opens a popup to switch it: an optional filter field, the
|
|
14
|
+
* options in groups as a listbox (the current one checked), and commands
|
|
15
|
+
* in a footer. Invalid groups, options, words or props throw a
|
|
16
|
+
* `RangeError`.
|
|
17
|
+
*/
|
|
18
|
+
export function ContextSwitcher(props) {
|
|
19
|
+
assertContextSwitcher(props);
|
|
20
|
+
const { actions = [], align = 'start', currentId, filter, groups, label, onSelect, trigger, triggerAppearance = 'framed', width = 'content', } = props;
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
22
|
+
const triggerRef = useRef(null);
|
|
23
|
+
const pressedRef = useRef(false);
|
|
24
|
+
const labelId = useId();
|
|
25
|
+
const contentId = useId();
|
|
26
|
+
const current = groups
|
|
27
|
+
.flatMap((group) => group.options)
|
|
28
|
+
.find((option) => option.id === currentId);
|
|
29
|
+
const showFilter = filter !== undefined && optionCount(groups) >= (filter.minOptions ?? 0);
|
|
30
|
+
function close(refocus) {
|
|
31
|
+
setOpen(false);
|
|
32
|
+
if (refocus)
|
|
33
|
+
triggerRef.current?.focus();
|
|
34
|
+
}
|
|
35
|
+
function onTriggerKeyDown(event) {
|
|
36
|
+
if (!open && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
setOpen(true);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function runAction(action) {
|
|
42
|
+
// Focus is back on the trigger first, so a dialog the command opens
|
|
43
|
+
// returns focus there when it closes.
|
|
44
|
+
close(true);
|
|
45
|
+
action.onSelect();
|
|
46
|
+
}
|
|
47
|
+
const icon = triggerAppearance === 'icon';
|
|
48
|
+
// The name is what is switched, then what the trigger shows (for a glyph
|
|
49
|
+
// alone, the current option's label).
|
|
50
|
+
const labelledBy = icon && !current ? labelId : `${labelId} ${contentId}`;
|
|
51
|
+
return (_jsxs("div", { className: cx('sw-context-switcher', width === 'full' && 'sw-context-switcher-full'), children: [_jsxs("button", { "aria-expanded": open, "aria-haspopup": "dialog", "aria-labelledby": labelledBy, className: cx('sw-context-switcher-trigger', contextSwitcherTriggerClass(triggerAppearance)), onClick: () => (open ? close(true) : setOpen(true)), onKeyDown: onTriggerKeyDown, onPointerDown: () => {
|
|
52
|
+
pressedRef.current = true;
|
|
53
|
+
}, onPointerUp: () => {
|
|
54
|
+
pressedRef.current = false;
|
|
55
|
+
}, onPointerLeave: () => {
|
|
56
|
+
pressedRef.current = false;
|
|
57
|
+
}, ref: triggerRef, tabIndex: tabStop, type: "button", children: [_jsx("span", { className: "sw-sr-only", id: labelId, children: label }), _jsx("span", { "aria-hidden": icon ? true : undefined, className: "sw-context-switcher-trigger-content", id: icon ? undefined : contentId, children: trigger }), icon && current ? (_jsx("span", { className: "sw-sr-only", id: contentId, children: current.label })) : null] }), open ? (_jsx(SwitcherPopup, { actions: actions, align: align, currentId: currentId, filter: showFilter ? filter : undefined, groups: groups, label: label, onAction: runAction, onChoose: (entry) => {
|
|
58
|
+
close(true);
|
|
59
|
+
if (entry.option.id !== currentId)
|
|
60
|
+
onSelect(entry.option);
|
|
61
|
+
}, onClose: close, triggerPressed: () => pressedRef.current, triggerRef: triggerRef })) : null] }));
|
|
62
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export declare const emptyStateTitleElements: readonly ["h2", "h3"];
|
|
3
|
+
/** The title's heading level: `h2` (the default) in a page, `h3` in a card under its own heading. */
|
|
4
|
+
export type EmptyStateTitleElement = (typeof emptyStateTitleElements)[number];
|
|
5
|
+
export type EmptyStateProps = {
|
|
6
|
+
/** What is empty, in the consumer's words ("No teams in Spring 2027 yet"). */
|
|
7
|
+
title: string;
|
|
8
|
+
/** `h2` (the default) in a page column; `h3` inside a card under its own heading. */
|
|
9
|
+
titleAs?: EmptyStateTitleElement;
|
|
10
|
+
/** A muted line under the title: what to do next. Not blank. */
|
|
11
|
+
description?: string;
|
|
12
|
+
/** A decorative glyph over the title (Lucide in the consumer), hidden from assistive technology. */
|
|
13
|
+
icon?: ReactNode;
|
|
14
|
+
/** The ways out, such as Buttons, in a centred row that wraps. */
|
|
15
|
+
actions?: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Where a list or a page has nothing yet: a centred panel on the surface
|
|
19
|
+
* with a dashed hairline edge, an optional decorative glyph, a title, a
|
|
20
|
+
* muted line and the actions that fill it. It fills its column, so it fits
|
|
21
|
+
* a page column or a Card.
|
|
22
|
+
*/
|
|
23
|
+
export declare function EmptyState({ title, titleAs, description, icon, actions, }: EmptyStateProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Children, Fragment, isValidElement } from 'react';
|
|
3
|
+
import { isOneOf } from '../is-one-of.js';
|
|
4
|
+
import { Text } from './Text.js';
|
|
5
|
+
export const emptyStateTitleElements = ['h2', 'h3'];
|
|
6
|
+
/** Fails closed on blank words or an unknown heading level. */
|
|
7
|
+
function assertEmptyState({ title, titleAs, description }) {
|
|
8
|
+
if (typeof title !== 'string' || !title.trim())
|
|
9
|
+
throw new RangeError('EmptyState title must be nonempty');
|
|
10
|
+
if (titleAs !== undefined && !isOneOf(emptyStateTitleElements, titleAs))
|
|
11
|
+
throw new RangeError(`Unknown EmptyState titleAs: ${String(titleAs)}`);
|
|
12
|
+
if (description !== undefined &&
|
|
13
|
+
(typeof description !== 'string' || !description.trim()))
|
|
14
|
+
throw new RangeError('EmptyState description must be nonempty');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Whether a slot draws anything: nothing, a boolean, blank words, an empty
|
|
18
|
+
* array or a Fragment holding only those is no content, so the slot's box
|
|
19
|
+
* (and its gap) is left out.
|
|
20
|
+
*/
|
|
21
|
+
function hasContent(node) {
|
|
22
|
+
return Children.toArray(node).some((child) => {
|
|
23
|
+
if (typeof child === 'string')
|
|
24
|
+
return child.trim() !== '';
|
|
25
|
+
if (isValidElement(child) &&
|
|
26
|
+
child.type === Fragment)
|
|
27
|
+
return hasContent(child.props.children);
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Where a list or a page has nothing yet: a centred panel on the surface
|
|
33
|
+
* with a dashed hairline edge, an optional decorative glyph, a title, a
|
|
34
|
+
* muted line and the actions that fill it. It fills its column, so it fits
|
|
35
|
+
* a page column or a Card.
|
|
36
|
+
*/
|
|
37
|
+
export function EmptyState({ title, titleAs = 'h2', description, icon, actions, }) {
|
|
38
|
+
assertEmptyState({ title, titleAs, description });
|
|
39
|
+
return (_jsxs("div", { className: "sw-empty-state", children: [hasContent(icon) ? (_jsx("span", { "aria-hidden": "true", className: "sw-empty-state-icon", children: icon })) : null, _jsx(Text, { as: titleAs, className: "sw-empty-state-title", variant: "title", children: title }), description !== undefined ? (_jsx(Text, { as: "p", className: "sw-empty-state-description", color: "muted", children: description })) : null, hasContent(actions) ? (_jsx("div", { className: "sw-empty-state-actions", children: actions })) : null] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type FixtureGroupProps } from './fixture/types.js';
|
|
2
|
+
export type { FixtureGroupProps } from './fixture/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A run of fixtures under a caption, such as a match day at a venue: the
|
|
5
|
+
* caption names a list of FixtureRows, whose columns line up across the
|
|
6
|
+
* run (one grid, each row a subgrid of it). A blank title throws a
|
|
7
|
+
* `RangeError`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function FixtureGroup({ children, title }: FixtureGroupProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from 'react';
|
|
4
|
+
import { FixtureListContext } from './fixture/fixture-list-context.js';
|
|
5
|
+
/**
|
|
6
|
+
* A run of fixtures under a caption, such as a match day at a venue: the
|
|
7
|
+
* caption names a list of FixtureRows, whose columns line up across the
|
|
8
|
+
* run (one grid, each row a subgrid of it). A blank title throws a
|
|
9
|
+
* `RangeError`.
|
|
10
|
+
*/
|
|
11
|
+
export function FixtureGroup({ children, title }) {
|
|
12
|
+
if (typeof title !== 'string' || !title.trim())
|
|
13
|
+
throw new RangeError('FixtureGroup title must be nonempty');
|
|
14
|
+
const titleId = useId();
|
|
15
|
+
return (_jsxs("div", { className: "sw-fixture-group", children: [_jsx("div", { className: "sw-fixture-group-title", id: titleId, children: title }), _jsx("ul", { "aria-labelledby": titleId, className: "sw-fixture-list", role: "list", children: _jsx(FixtureListContext.Provider, { value: true, children: children }) })] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type FixtureRowProps } from './fixture/types.js';
|
|
2
|
+
export type { FixtureRowProps, FixtureSide, FixtureStatus, } from './fixture/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* One fixture in a FixtureGroup: the home side ending at the centre, the
|
|
5
|
+
* kickoff time or score in the middle, the away side starting from it, then
|
|
6
|
+
* the details and a status at the end (under the sides below `md`). With
|
|
7
|
+
* `href` or `onPress` the whole row is a link or a button with a chevron.
|
|
8
|
+
* Invalid words or props throw a `RangeError`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function FixtureRow(props: FixtureRowProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { cx } from '../class-names.js';
|
|
4
|
+
import { hideClass } from '../css/breakpoints.js';
|
|
5
|
+
import { tabStop } from '../tab-stop.js';
|
|
6
|
+
import { Badge } from './Badge.js';
|
|
7
|
+
import { useFixtureList } from './fixture/fixture-list-context.js';
|
|
8
|
+
import { assertFixtureRow } from './fixture/validate.js';
|
|
9
|
+
import { plainPress } from './link-press.js';
|
|
10
|
+
import { hasSlot } from './slot.js';
|
|
11
|
+
/** One side: its name, and its mark on the centre's side (hidden). */
|
|
12
|
+
function Side({ side, place }) {
|
|
13
|
+
const mark = hasSlot(side.mark) ? (_jsx("span", { "aria-hidden": "true", className: "sw-fixture-mark", children: side.mark })) : null;
|
|
14
|
+
return (_jsxs("span", { className: `sw-fixture-team sw-fixture-team-${place}`, children: [place === 'away' ? mark : null, _jsx("span", { className: "sw-fixture-name", children: side.name }), place === 'home' ? mark : null] }));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* One fixture in a FixtureGroup: the home side ending at the centre, the
|
|
18
|
+
* kickoff time or score in the middle, the away side starting from it, then
|
|
19
|
+
* the details and a status at the end (under the sides below `md`). With
|
|
20
|
+
* `href` or `onPress` the whole row is a link or a button with a chevron.
|
|
21
|
+
* Invalid words or props throw a `RangeError`.
|
|
22
|
+
*/
|
|
23
|
+
export function FixtureRow(props) {
|
|
24
|
+
useFixtureList();
|
|
25
|
+
assertFixtureRow(props);
|
|
26
|
+
const { away, center, centerKind = 'time', home, href, meta, metaHideBelow, onPress, status, 'aria-label': ariaLabel, } = props;
|
|
27
|
+
const hasMeta = hasSlot(meta);
|
|
28
|
+
const pressable = href !== undefined || onPress !== undefined;
|
|
29
|
+
// Spaces as their own text nodes keep the parts apart in the name.
|
|
30
|
+
const parts = (_jsxs(_Fragment, { children: [_jsx(Side, { place: "home", side: home }), ' ', _jsx("span", { className: `sw-fixture-center sw-fixture-center-${centerKind}`, children: center }), ' ', _jsx(Side, { place: "away", side: away }), hasMeta || status ? (_jsxs(_Fragment, { children: [' ', _jsxs("span", { className: "sw-fixture-end", children: [hasMeta ? (_jsx("span", { className: cx('sw-fixture-meta', metaHideBelow && hideClass('below', metaHideBelow)), children: meta })) : null, hasMeta && status ? ' ' : null, status ? (_jsx(Badge, { size: "sm", tone: status.tone, children: status.label })) : null] })] })) : null, pressable ? (_jsx("span", { "aria-hidden": "true", className: "sw-fixture-chevron" })) : null] }));
|
|
31
|
+
let row;
|
|
32
|
+
if (href !== undefined)
|
|
33
|
+
row = (_jsx("a", { "aria-label": ariaLabel, className: "sw-fixture-row sw-fixture-row-pressable", href: href, onClick: onPress
|
|
34
|
+
? (event) => {
|
|
35
|
+
if (!plainPress(event))
|
|
36
|
+
return;
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
onPress(event);
|
|
39
|
+
}
|
|
40
|
+
: undefined, tabIndex: tabStop, children: parts }));
|
|
41
|
+
else if (onPress !== undefined)
|
|
42
|
+
row = (_jsx("button", { "aria-label": ariaLabel, className: "sw-fixture-row sw-fixture-row-pressable", onClick: onPress, tabIndex: tabStop, type: "button", children: parts }));
|
|
43
|
+
else
|
|
44
|
+
row = _jsx("div", { className: "sw-fixture-row", children: parts });
|
|
45
|
+
return _jsx("li", { className: "sw-fixture-item", children: row });
|
|
46
|
+
}
|