@scalewing/react 1.47.0 → 1.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +171 -4
  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/FixtureGroup.d.ts +9 -0
  19. package/dist/components/FixtureGroup.js +16 -0
  20. package/dist/components/FixtureRow.d.ts +10 -0
  21. package/dist/components/FixtureRow.js +46 -0
  22. package/dist/components/GlyphTile.d.ts +9 -3
  23. package/dist/components/GlyphTile.js +15 -4
  24. package/dist/components/SiteFooter.d.ts +12 -0
  25. package/dist/components/SiteFooter.js +23 -0
  26. package/dist/components/SiteHeader.d.ts +14 -0
  27. package/dist/components/SiteHeader.js +24 -0
  28. package/dist/components/Skeleton.d.ts +22 -0
  29. package/dist/components/Skeleton.js +30 -0
  30. package/dist/components/app-shell/AppShellBar.d.ts +23 -0
  31. package/dist/components/app-shell/AppShellBar.js +25 -0
  32. package/dist/components/app-shell/types.d.ts +36 -1
  33. package/dist/components/app-shell/validate.d.ts +4 -3
  34. package/dist/components/app-shell/validate.js +21 -3
  35. package/dist/components/command-palette/PaletteList.d.ts +20 -0
  36. package/dist/components/command-palette/PaletteList.js +21 -0
  37. package/dist/components/command-palette/shortcut.d.ts +19 -0
  38. package/dist/components/command-palette/shortcut.js +75 -0
  39. package/dist/components/command-palette/types.d.ts +72 -0
  40. package/dist/components/command-palette/types.js +2 -0
  41. package/dist/components/command-palette/validate.d.ts +10 -0
  42. package/dist/components/command-palette/validate.js +50 -0
  43. package/dist/components/context-switcher/SwitcherOption.d.ts +17 -0
  44. package/dist/components/context-switcher/SwitcherOption.js +20 -0
  45. package/dist/components/context-switcher/SwitcherPopup.d.ts +31 -0
  46. package/dist/components/context-switcher/SwitcherPopup.js +111 -0
  47. package/dist/components/context-switcher/options.d.ts +33 -0
  48. package/dist/components/context-switcher/options.js +60 -0
  49. package/dist/components/context-switcher/types.d.ts +96 -0
  50. package/dist/components/context-switcher/types.js +1 -0
  51. package/dist/components/context-switcher/use-switcher-list.d.ts +29 -0
  52. package/dist/components/context-switcher/use-switcher-list.js +93 -0
  53. package/dist/components/context-switcher/validate.d.ts +8 -0
  54. package/dist/components/context-switcher/validate.js +94 -0
  55. package/dist/components/fixture/fixture-list-context.d.ts +4 -0
  56. package/dist/components/fixture/fixture-list-context.js +9 -0
  57. package/dist/components/fixture/types.d.ts +50 -0
  58. package/dist/components/fixture/types.js +1 -0
  59. package/dist/components/fixture/validate.d.ts +8 -0
  60. package/dist/components/fixture/validate.js +36 -0
  61. package/dist/components/site-footer/types.d.ts +31 -0
  62. package/dist/components/site-footer/types.js +1 -0
  63. package/dist/components/site-footer/validate.d.ts +7 -0
  64. package/dist/components/site-footer/validate.js +22 -0
  65. package/dist/components/site-header/SiteHeaderLinks.d.ts +7 -0
  66. package/dist/components/site-header/SiteHeaderLinks.js +7 -0
  67. package/dist/components/site-header/types.d.ts +38 -0
  68. package/dist/components/site-header/types.js +1 -0
  69. package/dist/components/site-header/use-menu-dismiss.d.ts +8 -0
  70. package/dist/components/site-header/use-menu-dismiss.js +62 -0
  71. package/dist/components/site-header/validate.d.ts +7 -0
  72. package/dist/components/site-header/validate.js +17 -0
  73. package/dist/components/use-element-height.d.ts +10 -0
  74. package/dist/components/use-element-height.js +35 -0
  75. package/dist/context-switcher.test-support.d.ts +9 -0
  76. package/dist/context-switcher.test-support.js +41 -0
  77. package/dist/css/css-app-shell-page-bar.d.ts +3 -0
  78. package/dist/css/css-app-shell-page-bar.js +81 -0
  79. package/dist/css/css-app-shell.js +6 -1
  80. package/dist/css/css-band.d.ts +10 -0
  81. package/dist/css/css-band.js +101 -0
  82. package/dist/css/css-button-link.d.ts +3 -0
  83. package/dist/css/css-button-link.js +48 -0
  84. package/dist/css/css-command-palette-key.d.ts +1 -0
  85. package/dist/css/css-command-palette-key.js +33 -0
  86. package/dist/css/css-command-palette-trigger.d.ts +2 -0
  87. package/dist/css/css-command-palette-trigger.js +88 -0
  88. package/dist/css/css-command-palette.d.ts +2 -0
  89. package/dist/css/css-command-palette.js +209 -0
  90. package/dist/css/css-components.js +24 -0
  91. package/dist/css/css-context-switcher-trigger.d.ts +15 -0
  92. package/dist/css/css-context-switcher-trigger.js +175 -0
  93. package/dist/css/css-context-switcher.d.ts +4 -0
  94. package/dist/css/css-context-switcher.js +219 -0
  95. package/dist/css/css-count-pill.d.ts +2 -2
  96. package/dist/css/css-count-pill.js +11 -3
  97. package/dist/css/css-fixture.d.ts +5 -0
  98. package/dist/css/css-fixture.js +260 -0
  99. package/dist/css/css-glyph-tile.d.ts +2 -0
  100. package/dist/css/css-glyph-tile.js +29 -1
  101. package/dist/css/css-site-footer.d.ts +2 -0
  102. package/dist/css/css-site-footer.js +152 -0
  103. package/dist/css/css-site-header.d.ts +2 -0
  104. package/dist/css/css-site-header.js +262 -0
  105. package/dist/css/css-skeleton.d.ts +10 -0
  106. package/dist/css/css-skeleton.js +110 -0
  107. package/dist/css/stacking.js +7 -4
  108. package/dist/css/stylesheet.js +16 -0
  109. package/dist/index.d.ts +13 -0
  110. package/dist/index.js +10 -0
  111. package/dist/styles.css +2008 -719
  112. package/package.json +2 -2
@@ -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,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
+ }
@@ -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
  }
@@ -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;