@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
@@ -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
- /** The consumer's glyph (Lucide), drawn in white at spacing step 4. */
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: a
18
- * kind's mark beside its name, such as a service in a list. Decorative
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: a
8
- * kind's mark beside its name, such as a service in a list. Decorative
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
- return (_jsx("span", { ...rest, "aria-hidden": "true", className: cx('sw-glyph-tile', `sw-glyph-tile-${hue}`, `sw-glyph-tile-${size}`, className), children: children }));
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 { GridColumns };
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;
@@ -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 function Grid({ align, className, columns = 1, columnsBelow, gap = 0, ...rest }) {
9
- assertGridCount('columns', columns);
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(columns), ...belowClasses, spacingClass('gap', 'all', gap), align && `sw-align-${align}`, className), ...rest }));
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
- /** Says the count in words for assistive technology (`aria-valuetext`), such as "12 straps". */
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
- .filter((id) => id !== null)
81
- .join(' ') || undefined, "aria-invalid": invalid || undefined, "aria-valuemax": Number.isFinite(max) ? max : undefined, "aria-valuemin": min, "aria-valuenow": value ?? undefined, "aria-valuetext": valueText, autoComplete: "off", className: "sw-stepper-input", disabled: disabled, id: inputId,
82
- // iOS's numeric pad has no minus key, so a range below zero takes
83
- // the text keyboard.
84
- 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 })] }));
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
  });
@@ -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
- /** A compact `header` for the rail. Left out, the rail shows none. */
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 their items (ids unique
4
- * across every group, each with an icon, which is all the rail shows).
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 shell's words, its groups and their items (ids unique
32
- * across every group, each with an icon, which is all the rail shows).
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
- export function assertAppShellProps({ groups, labels, navLabel, }) {
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 {};