@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.
Files changed (135) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +215 -6
  3. package/dist/command-palette.test-support.d.ts +21 -0
  4. package/dist/command-palette.test-support.js +55 -0
  5. package/dist/components/AppHeader.d.ts +3 -0
  6. package/dist/components/AppShell.d.ts +4 -3
  7. package/dist/components/AppShell.js +24 -7
  8. package/dist/components/Band.d.ts +30 -0
  9. package/dist/components/Band.js +23 -0
  10. package/dist/components/ButtonLink.d.ts +33 -0
  11. package/dist/components/ButtonLink.js +27 -0
  12. package/dist/components/CommandPalette.d.ts +12 -0
  13. package/dist/components/CommandPalette.js +143 -0
  14. package/dist/components/CommandPaletteTrigger.d.ts +9 -0
  15. package/dist/components/CommandPaletteTrigger.js +16 -0
  16. package/dist/components/ContextSwitcher.d.ts +12 -0
  17. package/dist/components/ContextSwitcher.js +62 -0
  18. package/dist/components/EmptyState.d.ts +23 -0
  19. package/dist/components/EmptyState.js +40 -0
  20. package/dist/components/FixtureGroup.d.ts +9 -0
  21. package/dist/components/FixtureGroup.js +16 -0
  22. package/dist/components/FixtureRow.d.ts +10 -0
  23. package/dist/components/FixtureRow.js +46 -0
  24. package/dist/components/GlyphTile.d.ts +9 -3
  25. package/dist/components/GlyphTile.js +15 -4
  26. package/dist/components/Grid.d.ts +11 -3
  27. package/dist/components/Grid.js +13 -4
  28. package/dist/components/Hero.d.ts +26 -0
  29. package/dist/components/Hero.js +23 -0
  30. package/dist/components/SiteFooter.d.ts +12 -0
  31. package/dist/components/SiteFooter.js +23 -0
  32. package/dist/components/SiteHeader.d.ts +14 -0
  33. package/dist/components/SiteHeader.js +24 -0
  34. package/dist/components/Skeleton.d.ts +22 -0
  35. package/dist/components/Skeleton.js +30 -0
  36. package/dist/components/Stepper.d.ts +11 -1
  37. package/dist/components/Stepper.js +10 -8
  38. package/dist/components/Text.js +3 -0
  39. package/dist/components/app-shell/AppShellBar.d.ts +23 -0
  40. package/dist/components/app-shell/AppShellBar.js +25 -0
  41. package/dist/components/app-shell/types.d.ts +36 -1
  42. package/dist/components/app-shell/validate.d.ts +4 -3
  43. package/dist/components/app-shell/validate.js +21 -3
  44. package/dist/components/command-palette/PaletteList.d.ts +20 -0
  45. package/dist/components/command-palette/PaletteList.js +21 -0
  46. package/dist/components/command-palette/shortcut.d.ts +19 -0
  47. package/dist/components/command-palette/shortcut.js +75 -0
  48. package/dist/components/command-palette/types.d.ts +72 -0
  49. package/dist/components/command-palette/types.js +2 -0
  50. package/dist/components/command-palette/validate.d.ts +10 -0
  51. package/dist/components/command-palette/validate.js +50 -0
  52. package/dist/components/context-switcher/SwitcherOption.d.ts +17 -0
  53. package/dist/components/context-switcher/SwitcherOption.js +20 -0
  54. package/dist/components/context-switcher/SwitcherPopup.d.ts +31 -0
  55. package/dist/components/context-switcher/SwitcherPopup.js +111 -0
  56. package/dist/components/context-switcher/options.d.ts +33 -0
  57. package/dist/components/context-switcher/options.js +60 -0
  58. package/dist/components/context-switcher/types.d.ts +96 -0
  59. package/dist/components/context-switcher/types.js +1 -0
  60. package/dist/components/context-switcher/use-switcher-list.d.ts +29 -0
  61. package/dist/components/context-switcher/use-switcher-list.js +93 -0
  62. package/dist/components/context-switcher/validate.d.ts +8 -0
  63. package/dist/components/context-switcher/validate.js +94 -0
  64. package/dist/components/fixture/fixture-list-context.d.ts +4 -0
  65. package/dist/components/fixture/fixture-list-context.js +9 -0
  66. package/dist/components/fixture/types.d.ts +50 -0
  67. package/dist/components/fixture/types.js +1 -0
  68. package/dist/components/fixture/validate.d.ts +8 -0
  69. package/dist/components/fixture/validate.js +36 -0
  70. package/dist/components/site-footer/types.d.ts +31 -0
  71. package/dist/components/site-footer/types.js +1 -0
  72. package/dist/components/site-footer/validate.d.ts +7 -0
  73. package/dist/components/site-footer/validate.js +22 -0
  74. package/dist/components/site-header/SiteHeaderLinks.d.ts +7 -0
  75. package/dist/components/site-header/SiteHeaderLinks.js +7 -0
  76. package/dist/components/site-header/types.d.ts +38 -0
  77. package/dist/components/site-header/types.js +1 -0
  78. package/dist/components/site-header/use-menu-dismiss.d.ts +8 -0
  79. package/dist/components/site-header/use-menu-dismiss.js +62 -0
  80. package/dist/components/site-header/validate.d.ts +7 -0
  81. package/dist/components/site-header/validate.js +17 -0
  82. package/dist/components/stepper/StepperCountFrame.d.ts +15 -0
  83. package/dist/components/stepper/StepperCountFrame.js +25 -0
  84. package/dist/components/use-element-height.d.ts +10 -0
  85. package/dist/components/use-element-height.js +35 -0
  86. package/dist/context-switcher.test-support.d.ts +9 -0
  87. package/dist/context-switcher.test-support.js +41 -0
  88. package/dist/css/css-app-shell-page-bar.d.ts +3 -0
  89. package/dist/css/css-app-shell-page-bar.js +81 -0
  90. package/dist/css/css-app-shell.js +6 -1
  91. package/dist/css/css-band.d.ts +10 -0
  92. package/dist/css/css-band.js +101 -0
  93. package/dist/css/css-button-link.d.ts +3 -0
  94. package/dist/css/css-button-link.js +48 -0
  95. package/dist/css/css-command-palette-key.d.ts +1 -0
  96. package/dist/css/css-command-palette-key.js +33 -0
  97. package/dist/css/css-command-palette-trigger.d.ts +2 -0
  98. package/dist/css/css-command-palette-trigger.js +88 -0
  99. package/dist/css/css-command-palette.d.ts +2 -0
  100. package/dist/css/css-command-palette.js +209 -0
  101. package/dist/css/css-components.js +30 -0
  102. package/dist/css/css-context-switcher-trigger.d.ts +15 -0
  103. package/dist/css/css-context-switcher-trigger.js +175 -0
  104. package/dist/css/css-context-switcher.d.ts +4 -0
  105. package/dist/css/css-context-switcher.js +219 -0
  106. package/dist/css/css-count-pill.d.ts +2 -2
  107. package/dist/css/css-count-pill.js +11 -3
  108. package/dist/css/css-empty-state.d.ts +2 -0
  109. package/dist/css/css-empty-state.js +77 -0
  110. package/dist/css/css-fixture.d.ts +5 -0
  111. package/dist/css/css-fixture.js +260 -0
  112. package/dist/css/css-glyph-tile.d.ts +2 -0
  113. package/dist/css/css-glyph-tile.js +29 -1
  114. package/dist/css/css-grid-span.js +5 -2
  115. package/dist/css/css-grid.d.ts +13 -0
  116. package/dist/css/css-grid.js +17 -1
  117. package/dist/css/css-hero.d.ts +2 -0
  118. package/dist/css/css-hero.js +62 -0
  119. package/dist/css/css-site-footer.d.ts +2 -0
  120. package/dist/css/css-site-footer.js +152 -0
  121. package/dist/css/css-site-header.d.ts +2 -0
  122. package/dist/css/css-site-header.js +262 -0
  123. package/dist/css/css-skeleton.d.ts +10 -0
  124. package/dist/css/css-skeleton.js +110 -0
  125. package/dist/css/css-stepper-unit.d.ts +9 -0
  126. package/dist/css/css-stepper-unit.js +102 -0
  127. package/dist/css/css-stepper.js +5 -1
  128. package/dist/css/stacking.js +7 -4
  129. package/dist/css/stylesheet.js +20 -0
  130. package/dist/index.d.ts +18 -2
  131. package/dist/index.js +13 -1
  132. package/dist/stepper-count.d.ts +8 -0
  133. package/dist/stepper-count.js +17 -0
  134. package/dist/styles.css +2202 -702
  135. 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. Invalid groups, items or words throw a
10
- * `RangeError`.
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 { AppHeader } from './AppHeader.js';
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 { Button } from './Button.js';
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. Invalid groups, items or words throw a
20
- * `RangeError`.
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({ groups, labels, navLabel });
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", children: [_jsxs(AppHeader, { appearance: "bar", className: "sw-app-shell-bar", padding: 2, children: [_jsx(Button, { ref: menuRef, "aria-expanded": menuOpen, "aria-haspopup": "dialog", "aria-label": labels.openMenu, className: "sw-app-shell-menu", onPress: () => setMenuOpen(true), size: "sm", variant: "ghost", children: _jsx("span", { "aria-hidden": "true", className: "sw-app-shell-icon", children: menuIcon }) }), _jsx("div", { className: "sw-app-shell-bar-title", children: barTitle }), barEnd ? _jsx("div", { className: "sw-app-shell-bar-end", children: barEnd }) : null] }), _jsx(AppShellSidebar, { footer: footer, groups: groups, header: header, navLabel: navLabel, onNavigate: onNavigate, rail: sidebarShown && !wide, railFooter: railFooter, railHeader: railHeader }), _jsx("main", { className: "sw-app-shell-main", children: children }), _jsx(AppShellDrawer, { closeLabel: labels.closeMenu, footer: footer, groups: groups, header: header, navLabel: navLabel, onChoose: () => setMenuOpen(false), onClose: () => {
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
+ }