@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.
- package/CHANGELOG.md +67 -0
- package/README.md +171 -4
- package/dist/command-palette.test-support.d.ts +21 -0
- package/dist/command-palette.test-support.js +55 -0
- package/dist/components/AppHeader.d.ts +3 -0
- package/dist/components/AppShell.d.ts +4 -3
- package/dist/components/AppShell.js +24 -7
- package/dist/components/Band.d.ts +30 -0
- package/dist/components/Band.js +23 -0
- package/dist/components/ButtonLink.d.ts +33 -0
- package/dist/components/ButtonLink.js +27 -0
- package/dist/components/CommandPalette.d.ts +12 -0
- package/dist/components/CommandPalette.js +143 -0
- package/dist/components/CommandPaletteTrigger.d.ts +9 -0
- package/dist/components/CommandPaletteTrigger.js +16 -0
- package/dist/components/ContextSwitcher.d.ts +12 -0
- package/dist/components/ContextSwitcher.js +62 -0
- package/dist/components/FixtureGroup.d.ts +9 -0
- package/dist/components/FixtureGroup.js +16 -0
- package/dist/components/FixtureRow.d.ts +10 -0
- package/dist/components/FixtureRow.js +46 -0
- package/dist/components/GlyphTile.d.ts +9 -3
- package/dist/components/GlyphTile.js +15 -4
- package/dist/components/SiteFooter.d.ts +12 -0
- package/dist/components/SiteFooter.js +23 -0
- package/dist/components/SiteHeader.d.ts +14 -0
- package/dist/components/SiteHeader.js +24 -0
- package/dist/components/Skeleton.d.ts +22 -0
- package/dist/components/Skeleton.js +30 -0
- package/dist/components/app-shell/AppShellBar.d.ts +23 -0
- package/dist/components/app-shell/AppShellBar.js +25 -0
- package/dist/components/app-shell/types.d.ts +36 -1
- package/dist/components/app-shell/validate.d.ts +4 -3
- package/dist/components/app-shell/validate.js +21 -3
- package/dist/components/command-palette/PaletteList.d.ts +20 -0
- package/dist/components/command-palette/PaletteList.js +21 -0
- package/dist/components/command-palette/shortcut.d.ts +19 -0
- package/dist/components/command-palette/shortcut.js +75 -0
- package/dist/components/command-palette/types.d.ts +72 -0
- package/dist/components/command-palette/types.js +2 -0
- package/dist/components/command-palette/validate.d.ts +10 -0
- package/dist/components/command-palette/validate.js +50 -0
- package/dist/components/context-switcher/SwitcherOption.d.ts +17 -0
- package/dist/components/context-switcher/SwitcherOption.js +20 -0
- package/dist/components/context-switcher/SwitcherPopup.d.ts +31 -0
- package/dist/components/context-switcher/SwitcherPopup.js +111 -0
- package/dist/components/context-switcher/options.d.ts +33 -0
- package/dist/components/context-switcher/options.js +60 -0
- package/dist/components/context-switcher/types.d.ts +96 -0
- package/dist/components/context-switcher/types.js +1 -0
- package/dist/components/context-switcher/use-switcher-list.d.ts +29 -0
- package/dist/components/context-switcher/use-switcher-list.js +93 -0
- package/dist/components/context-switcher/validate.d.ts +8 -0
- package/dist/components/context-switcher/validate.js +94 -0
- package/dist/components/fixture/fixture-list-context.d.ts +4 -0
- package/dist/components/fixture/fixture-list-context.js +9 -0
- package/dist/components/fixture/types.d.ts +50 -0
- package/dist/components/fixture/types.js +1 -0
- package/dist/components/fixture/validate.d.ts +8 -0
- package/dist/components/fixture/validate.js +36 -0
- package/dist/components/site-footer/types.d.ts +31 -0
- package/dist/components/site-footer/types.js +1 -0
- package/dist/components/site-footer/validate.d.ts +7 -0
- package/dist/components/site-footer/validate.js +22 -0
- package/dist/components/site-header/SiteHeaderLinks.d.ts +7 -0
- package/dist/components/site-header/SiteHeaderLinks.js +7 -0
- package/dist/components/site-header/types.d.ts +38 -0
- package/dist/components/site-header/types.js +1 -0
- package/dist/components/site-header/use-menu-dismiss.d.ts +8 -0
- package/dist/components/site-header/use-menu-dismiss.js +62 -0
- package/dist/components/site-header/validate.d.ts +7 -0
- package/dist/components/site-header/validate.js +17 -0
- package/dist/components/use-element-height.d.ts +10 -0
- package/dist/components/use-element-height.js +35 -0
- package/dist/context-switcher.test-support.d.ts +9 -0
- package/dist/context-switcher.test-support.js +41 -0
- package/dist/css/css-app-shell-page-bar.d.ts +3 -0
- package/dist/css/css-app-shell-page-bar.js +81 -0
- package/dist/css/css-app-shell.js +6 -1
- package/dist/css/css-band.d.ts +10 -0
- package/dist/css/css-band.js +101 -0
- package/dist/css/css-button-link.d.ts +3 -0
- package/dist/css/css-button-link.js +48 -0
- package/dist/css/css-command-palette-key.d.ts +1 -0
- package/dist/css/css-command-palette-key.js +33 -0
- package/dist/css/css-command-palette-trigger.d.ts +2 -0
- package/dist/css/css-command-palette-trigger.js +88 -0
- package/dist/css/css-command-palette.d.ts +2 -0
- package/dist/css/css-command-palette.js +209 -0
- package/dist/css/css-components.js +24 -0
- package/dist/css/css-context-switcher-trigger.d.ts +15 -0
- package/dist/css/css-context-switcher-trigger.js +175 -0
- package/dist/css/css-context-switcher.d.ts +4 -0
- package/dist/css/css-context-switcher.js +219 -0
- package/dist/css/css-count-pill.d.ts +2 -2
- package/dist/css/css-count-pill.js +11 -3
- package/dist/css/css-fixture.d.ts +5 -0
- package/dist/css/css-fixture.js +260 -0
- package/dist/css/css-glyph-tile.d.ts +2 -0
- package/dist/css/css-glyph-tile.js +29 -1
- package/dist/css/css-site-footer.d.ts +2 -0
- package/dist/css/css-site-footer.js +152 -0
- package/dist/css/css-site-header.d.ts +2 -0
- package/dist/css/css-site-header.js +262 -0
- package/dist/css/css-skeleton.d.ts +10 -0
- package/dist/css/css-skeleton.js +110 -0
- package/dist/css/stacking.js +7 -4
- package/dist/css/stylesheet.js +16 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/dist/styles.css +2008 -719
- package/package.json +2 -2
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReactNode, type Ref } from 'react';
|
|
2
|
+
import { type AppShellProps } from './types.js';
|
|
3
|
+
export type AppShellBarProps = Pick<AppShellProps, 'barEnd' | 'barTitle' | 'menuCount' | 'menuCountLabel' | 'menuCountTone'> & {
|
|
4
|
+
/** The bar itself, measured so a page bar can stick under it. */
|
|
5
|
+
barRef: Ref<HTMLElement>;
|
|
6
|
+
menuRef: Ref<HTMLButtonElement>;
|
|
7
|
+
menuIcon: ReactNode;
|
|
8
|
+
openMenuLabel: string;
|
|
9
|
+
menuOpen: boolean;
|
|
10
|
+
onOpenMenu: () => void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The menu button's name: its label, then the count's words (or the bare
|
|
14
|
+
* number) after a comma while there is a count to read.
|
|
15
|
+
*/
|
|
16
|
+
export declare function menuButtonName(openMenu: string, menuCount: number | undefined, menuCountLabel: string | undefined): string;
|
|
17
|
+
/**
|
|
18
|
+
* The compact top bar below `lg` (an AppHeader `bar`): the icon-only menu
|
|
19
|
+
* button, which may carry the shared count pill on its top end corner, the
|
|
20
|
+
* title and the end controls. The pill is hidden from assistive technology
|
|
21
|
+
* because the button's name already reads it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function AppShellBar({ barEnd, barRef, barTitle, menuCount, menuCountLabel, menuCountTone, menuIcon, menuOpen, menuRef, onOpenMenu, openMenuLabel, }: AppShellBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cx } from '../../class-names.js';
|
|
4
|
+
import { countToneClass } from '../../css/css-count-pill.js';
|
|
5
|
+
import { AppHeader } from '../AppHeader.js';
|
|
6
|
+
import { Button } from '../Button.js';
|
|
7
|
+
/**
|
|
8
|
+
* The menu button's name: its label, then the count's words (or the bare
|
|
9
|
+
* number) after a comma while there is a count to read.
|
|
10
|
+
*/
|
|
11
|
+
export function menuButtonName(openMenu, menuCount, menuCountLabel) {
|
|
12
|
+
if (!menuCount)
|
|
13
|
+
return openMenu;
|
|
14
|
+
return `${openMenu}, ${menuCountLabel ?? String(menuCount)}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The compact top bar below `lg` (an AppHeader `bar`): the icon-only menu
|
|
18
|
+
* button, which may carry the shared count pill on its top end corner, the
|
|
19
|
+
* title and the end controls. The pill is hidden from assistive technology
|
|
20
|
+
* because the button's name already reads it.
|
|
21
|
+
*/
|
|
22
|
+
export function AppShellBar({ barEnd, barRef, barTitle, menuCount, menuCountLabel, menuCountTone = 'danger', menuIcon, menuOpen, menuRef, onOpenMenu, openMenuLabel, }) {
|
|
23
|
+
return (_jsxs(AppHeader, { ref: barRef, appearance: "bar", className: "sw-app-shell-bar", padding: 2, children: [_jsxs(Button, { ref: menuRef, "aria-expanded": menuOpen, "aria-haspopup": "dialog", "aria-label": menuButtonName(openMenuLabel, menuCount, menuCountLabel), className: "sw-app-shell-menu", onPress: onOpenMenu, size: "sm", variant: "ghost", children: [_jsx("span", { "aria-hidden": "true", className: "sw-app-shell-icon", children: menuIcon }), menuCount ? (_jsx("span", { "aria-hidden": "true", className: cx('sw-app-shell-count', menuCountTone !== 'danger' &&
|
|
24
|
+
countToneClass('sw-app-shell-count', menuCountTone)), children: menuCount })) : null] }), _jsx("div", { className: "sw-app-shell-bar-title", children: barTitle }), barEnd ? _jsx("div", { className: "sw-app-shell-bar-end", children: barEnd }) : null] }));
|
|
25
|
+
}
|
|
@@ -63,6 +63,9 @@ export type AppShellProps = {
|
|
|
63
63
|
onNavigate?: (item: AppShellNavItem) => void;
|
|
64
64
|
/**
|
|
65
65
|
* The sidebar's top, such as a workspace switcher; also the drawer's.
|
|
66
|
+
* It may hold an interactive trigger (an `ActionMenu`, a `ContextSwitcher`):
|
|
67
|
+
* its popup opens on the top layer, so the sidebar and the drawer never
|
|
68
|
+
* clip it.
|
|
66
69
|
* While the drawer is open it is mounted twice (the sidebar's copy is
|
|
67
70
|
* hidden), so each copy keeps its own state: put no fixed DOM `id` in it
|
|
68
71
|
* (use `useId`), and keep state that must be shared in the consumer.
|
|
@@ -71,7 +74,11 @@ export type AppShellProps = {
|
|
|
71
74
|
header?: ReactNode;
|
|
72
75
|
/** The sidebar's bottom, such as the account menu; also the drawer's (see `header`). */
|
|
73
76
|
footer?: ReactNode;
|
|
74
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* A compact `header` for the rail, such as a workspace switcher drawn as
|
|
79
|
+
* its glyph alone (an icon trigger named by its label). Left out, the
|
|
80
|
+
* rail shows none.
|
|
81
|
+
*/
|
|
75
82
|
railHeader?: ReactNode;
|
|
76
83
|
/** A compact `footer` for the rail. Left out, the rail shows none. */
|
|
77
84
|
railFooter?: ReactNode;
|
|
@@ -81,6 +88,34 @@ export type AppShellProps = {
|
|
|
81
88
|
barEnd?: ReactNode;
|
|
82
89
|
/** The consumer's glyph for the menu button (Scalewing has no icon catalog). */
|
|
83
90
|
menuIcon: ReactNode;
|
|
91
|
+
/**
|
|
92
|
+
* How many things wait behind the menu, such as an inbox: the shared
|
|
93
|
+
* count pill on the menu button's top end corner, below `lg`, read in
|
|
94
|
+
* its name after `labels.openMenu` and a comma ("Open menu, 6"). A whole
|
|
95
|
+
* number; 0 (or none) shows no pill.
|
|
96
|
+
*/
|
|
97
|
+
menuCount?: number;
|
|
98
|
+
/**
|
|
99
|
+
* What the menu's count means, in the consumer's words ("6 need you"):
|
|
100
|
+
* read in the name in place of the bare number ("Open menu, 6 need you").
|
|
101
|
+
* Needs `menuCount`.
|
|
102
|
+
*/
|
|
103
|
+
menuCountLabel?: string;
|
|
104
|
+
/**
|
|
105
|
+
* The menu pill's colour, NavCount's tones: `danger` (the default),
|
|
106
|
+
* `warning` (amber) for work that needs the person but is not urgent,
|
|
107
|
+
* `accent` for news. Needs `menuCount`.
|
|
108
|
+
*/
|
|
109
|
+
menuCountTone?: NavCountTone;
|
|
110
|
+
/**
|
|
111
|
+
* A band at the top of `main` on every layout, such as the page's
|
|
112
|
+
* context pickers and search: the solid surface with a hairline under it
|
|
113
|
+
* and the page's inline padding (space-4 below `lg`, space-6 from it),
|
|
114
|
+
* its children laid out in a wrapping row with space between them. It is
|
|
115
|
+
* sticky: under the compact top bar below `lg`, at the window's top
|
|
116
|
+
* from `lg` up. It scrolls with nothing else; keep it to one row.
|
|
117
|
+
*/
|
|
118
|
+
pageBar?: ReactNode;
|
|
84
119
|
labels: AppShellLabels;
|
|
85
120
|
/** The page, rendered in `main`. */
|
|
86
121
|
children: ReactNode;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AppShellProps } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Fails closed on the shell's words, its groups and
|
|
4
|
-
* across every group, each with an icon, which is
|
|
3
|
+
* Fails closed on the shell's words, the menu's count, its groups and
|
|
4
|
+
* their items (ids unique across every group, each with an icon, which is
|
|
5
|
+
* all the rail shows).
|
|
5
6
|
*/
|
|
6
|
-
export declare function assertAppShellProps({ groups, labels, navLabel, }: Pick<AppShellProps, 'groups' | 'labels' | 'navLabel'>): void;
|
|
7
|
+
export declare function assertAppShellProps({ groups, labels, menuCount, menuCountLabel, menuCountTone, navLabel, }: Pick<AppShellProps, 'groups' | 'labels' | 'menuCount' | 'menuCountLabel' | 'menuCountTone' | 'navLabel'>): void;
|
|
@@ -28,14 +28,32 @@ function assertGroups(groups) {
|
|
|
28
28
|
throw new RangeError('AppShell groups need at least one item');
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Fails closed on the
|
|
32
|
-
*
|
|
31
|
+
* Fails closed on the menu button's count: a whole number from 0, a
|
|
32
|
+
* nonempty label and a known tone, each of the last two with a count.
|
|
33
33
|
*/
|
|
34
|
-
|
|
34
|
+
function assertMenuCount({ menuCount, menuCountLabel, menuCountTone, }) {
|
|
35
|
+
if (menuCount !== undefined &&
|
|
36
|
+
(!Number.isInteger(menuCount) || menuCount < 0))
|
|
37
|
+
throw new RangeError(`AppShell menuCount must be a whole number from 0: ${String(menuCount)}`);
|
|
38
|
+
if (menuCountLabel !== undefined && isBlank(menuCountLabel))
|
|
39
|
+
throw new RangeError('AppShell menuCountLabel must be nonempty');
|
|
40
|
+
if (menuCountTone !== undefined && !isOneOf(navCountTones, menuCountTone))
|
|
41
|
+
throw new RangeError(`AppShell menuCountTone must be one of ${navCountTones.join(', ')}`);
|
|
42
|
+
if (menuCount === undefined &&
|
|
43
|
+
(menuCountLabel !== undefined || menuCountTone !== undefined))
|
|
44
|
+
throw new RangeError('AppShell menuCountLabel and menuCountTone come with a menuCount');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fails closed on the shell's words, the menu's count, its groups and
|
|
48
|
+
* their items (ids unique across every group, each with an icon, which is
|
|
49
|
+
* all the rail shows).
|
|
50
|
+
*/
|
|
51
|
+
export function assertAppShellProps({ groups, labels, menuCount, menuCountLabel, menuCountTone, navLabel, }) {
|
|
35
52
|
if (isBlank(navLabel))
|
|
36
53
|
throw new RangeError('AppShell navLabel must be nonempty');
|
|
37
54
|
if (isBlank(labels?.openMenu) || isBlank(labels?.closeMenu))
|
|
38
55
|
throw new RangeError('AppShell labels openMenu and closeMenu are nonempty');
|
|
56
|
+
assertMenuCount({ menuCount, menuCountLabel, menuCountTone });
|
|
39
57
|
assertGroups(groups);
|
|
40
58
|
const items = groups.flatMap((group) => group.items);
|
|
41
59
|
assertNavItems('AppShell', items);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type PointerEvent } from 'react';
|
|
2
|
+
import { type CommandPaletteGroup, type CommandPaletteItem } from './types.js';
|
|
3
|
+
type PaletteListProps = {
|
|
4
|
+
listId: string;
|
|
5
|
+
label: string;
|
|
6
|
+
groups: readonly CommandPaletteGroup[];
|
|
7
|
+
activeId: string | undefined;
|
|
8
|
+
/** The row's DOM id from the item's id. */
|
|
9
|
+
rowId: (item: CommandPaletteItem) => string;
|
|
10
|
+
onActivate: (item: CommandPaletteItem, event: PointerEvent) => void;
|
|
11
|
+
onChoose: (item: CommandPaletteItem) => void;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The always-open results: a listbox named by the palette's label, each
|
|
15
|
+
* group a `group` under its caption, each item its mark (hidden), label and
|
|
16
|
+
* detail, and the active row's Enter key hint (hidden: Enter is how a
|
|
17
|
+
* listbox row is chosen).
|
|
18
|
+
*/
|
|
19
|
+
export declare function PaletteList({ activeId, groups, label, listId, onActivate, onChoose, rowId, }: PaletteListProps): import("react").JSX.Element;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The always-open results: a listbox named by the palette's label, each
|
|
4
|
+
* group a `group` under its caption, each item its mark (hidden), label and
|
|
5
|
+
* detail, and the active row's Enter key hint (hidden: Enter is how a
|
|
6
|
+
* listbox row is chosen).
|
|
7
|
+
*/
|
|
8
|
+
export function PaletteList({ activeId, groups, label, listId, onActivate, onChoose, rowId, }) {
|
|
9
|
+
return (_jsx("div", { "aria-label": label, className: "sw-command-palette-list", hidden: !groups.some((group) => group.items.length > 0), id: listId, role: "listbox", children: groups.map((group, index) => {
|
|
10
|
+
if (group.items.length === 0)
|
|
11
|
+
return null;
|
|
12
|
+
const headingId = `${listId}-group-${index}`;
|
|
13
|
+
return (_jsxs("div", { "aria-labelledby": headingId, role: "group", children: [_jsx("div", { className: "sw-command-palette-heading", id: headingId, role: "presentation", children: group.label }), group.items.map((item) => {
|
|
14
|
+
const active = item.id === activeId;
|
|
15
|
+
return (_jsxs("div", { "aria-selected": active, className: "sw-command-palette-item", id: rowId(item), onClick: () => onChoose(item), onPointerMove: (event) => {
|
|
16
|
+
if (!active)
|
|
17
|
+
onActivate(item, event);
|
|
18
|
+
}, role: "option", children: [item.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-command-palette-icon", children: item.icon })) : null, _jsxs("span", { className: "sw-command-palette-words", children: [_jsx("span", { className: "sw-command-palette-label", children: item.label }), item.detail ? (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-command-palette-detail", children: item.detail })] })) : null] }), active ? (_jsx("span", { "aria-hidden": "true", className: "sw-command-palette-key", children: "\u21B5" })) : null] }, item.id));
|
|
19
|
+
})] }, group.id));
|
|
20
|
+
}) }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CommandPaletteShortcut } from './types.js';
|
|
2
|
+
/** Whether the system's command key is ⌘ (Apple) rather than Ctrl. */
|
|
3
|
+
export declare function usesCommandKey(platform: string): boolean;
|
|
4
|
+
/** An element a person types into: a text field, a select or editable text. */
|
|
5
|
+
export declare function isEditable(target: EventTarget | null): boolean;
|
|
6
|
+
type ShortcutKey = Pick<KeyboardEvent, 'key' | 'code' | 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey' | 'repeat' | 'isComposing'>;
|
|
7
|
+
/**
|
|
8
|
+
* ⌘K on an Apple system or Ctrl+K elsewhere, with no other modifier: the K
|
|
9
|
+
* the layout types, or the key in K's place (`KeyK`) on a layout that types
|
|
10
|
+
* another letter there, such as Cyrillic or Greek.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isModK(event: ShortcutKey, commandKey: boolean): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Listens on the document for the palette's shortcut while `shortcut` is
|
|
15
|
+
* set: it calls `onToggle` (and stops the browser's own use of the keys)
|
|
16
|
+
* unless focus is in another text field than `ownField`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function usePaletteShortcut(shortcut: CommandPaletteShortcut | null | undefined, ownField: () => HTMLElement | null, onToggle: () => void): void;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
/** Whether the system's command key is ⌘ (Apple) rather than Ctrl. */
|
|
4
|
+
export function usesCommandKey(platform) {
|
|
5
|
+
return /mac|iphone|ipad|ipod/i.test(platform);
|
|
6
|
+
}
|
|
7
|
+
function systemPlatform() {
|
|
8
|
+
const nav = navigator;
|
|
9
|
+
return nav.userAgentData?.platform || nav.platform || nav.userAgent || '';
|
|
10
|
+
}
|
|
11
|
+
/** An element a person types into: a text field, a select or editable text. */
|
|
12
|
+
export function isEditable(target) {
|
|
13
|
+
if (!(target instanceof HTMLElement))
|
|
14
|
+
return false;
|
|
15
|
+
if (target.isContentEditable)
|
|
16
|
+
return true;
|
|
17
|
+
if (target instanceof HTMLTextAreaElement ||
|
|
18
|
+
target instanceof HTMLSelectElement)
|
|
19
|
+
return true;
|
|
20
|
+
if (!(target instanceof HTMLInputElement))
|
|
21
|
+
return false;
|
|
22
|
+
return ![
|
|
23
|
+
'button',
|
|
24
|
+
'checkbox',
|
|
25
|
+
'radio',
|
|
26
|
+
'range',
|
|
27
|
+
'color',
|
|
28
|
+
'file',
|
|
29
|
+
'image',
|
|
30
|
+
'reset',
|
|
31
|
+
'submit',
|
|
32
|
+
].includes(target.type);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* ⌘K on an Apple system or Ctrl+K elsewhere, with no other modifier: the K
|
|
36
|
+
* the layout types, or the key in K's place (`KeyK`) on a layout that types
|
|
37
|
+
* another letter there, such as Cyrillic or Greek.
|
|
38
|
+
*/
|
|
39
|
+
export function isModK(event, commandKey) {
|
|
40
|
+
if (event.repeat || event.isComposing)
|
|
41
|
+
return false;
|
|
42
|
+
const isK = event.key.toLowerCase() === 'k' || event.code === 'KeyK';
|
|
43
|
+
if (!isK || event.altKey || event.shiftKey)
|
|
44
|
+
return false;
|
|
45
|
+
return commandKey
|
|
46
|
+
? event.metaKey && !event.ctrlKey
|
|
47
|
+
: event.ctrlKey && !event.metaKey;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Listens on the document for the palette's shortcut while `shortcut` is
|
|
51
|
+
* set: it calls `onToggle` (and stops the browser's own use of the keys)
|
|
52
|
+
* unless focus is in another text field than `ownField`.
|
|
53
|
+
*/
|
|
54
|
+
export function usePaletteShortcut(shortcut, ownField, onToggle) {
|
|
55
|
+
const latest = useRef({ ownField, onToggle });
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
latest.current = { ownField, onToggle };
|
|
58
|
+
});
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (shortcut !== 'mod+k')
|
|
61
|
+
return;
|
|
62
|
+
const commandKey = usesCommandKey(systemPlatform());
|
|
63
|
+
function onKeyDown(event) {
|
|
64
|
+
if (event.defaultPrevented || !isModK(event, commandKey))
|
|
65
|
+
return;
|
|
66
|
+
const target = event.target;
|
|
67
|
+
if (isEditable(target) && target !== latest.current.ownField())
|
|
68
|
+
return;
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
latest.current.onToggle();
|
|
71
|
+
}
|
|
72
|
+
document.addEventListener('keydown', onKeyDown);
|
|
73
|
+
return () => document.removeEventListener('keydown', onKeyDown);
|
|
74
|
+
}, [shortcut]);
|
|
75
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/** One result or command in the palette. */
|
|
3
|
+
export type CommandPaletteItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** A muted line under the label, such as where a result lives. */
|
|
7
|
+
detail?: string;
|
|
8
|
+
/** A decorative mark before the label (a consumer glyph or crest). */
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* Runs once the palette has closed after it was chosen (the consumer set
|
|
12
|
+
* `open` to false from `onOpenChange(false)`), so it can move focus to
|
|
13
|
+
* the page; without that, focus returns to where it was.
|
|
14
|
+
*/
|
|
15
|
+
onSelect: () => void;
|
|
16
|
+
};
|
|
17
|
+
/** A run of items under a caption, such as "Teams" or "Actions". */
|
|
18
|
+
export type CommandPaletteGroup = {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
items: readonly CommandPaletteItem[];
|
|
22
|
+
};
|
|
23
|
+
/** The global shortcut that opens the palette: ⌘K on Apple systems, Ctrl+K elsewhere. */
|
|
24
|
+
export declare const commandPaletteShortcuts: readonly ["mod+k"];
|
|
25
|
+
export type CommandPaletteShortcut = (typeof commandPaletteShortcuts)[number];
|
|
26
|
+
export type CommandPaletteProps = {
|
|
27
|
+
/** Whether the palette is shown. */
|
|
28
|
+
open: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Asked to open (the shortcut) or close (Escape, a press outside, the
|
|
31
|
+
* shortcut again, or a chosen item).
|
|
32
|
+
*/
|
|
33
|
+
onOpenChange: (open: boolean) => void;
|
|
34
|
+
/** The palette's name and its field's, such as "Search". */
|
|
35
|
+
label: string;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
/** The field's words; the consumer filters `groups` from them. */
|
|
38
|
+
query: string;
|
|
39
|
+
onQueryChange: (query: string) => void;
|
|
40
|
+
/** The results in groups; a group with no items is not drawn. */
|
|
41
|
+
groups: readonly CommandPaletteGroup[];
|
|
42
|
+
/** Shown, and announced, when no group has an item (and not loading). */
|
|
43
|
+
emptyText: string;
|
|
44
|
+
/** The words announced for the number of items, such as `(n) => \`${n} results\``. */
|
|
45
|
+
countText?: (count: number) => string;
|
|
46
|
+
/** A muted line in the footer, such as how to use the keys. */
|
|
47
|
+
hint?: string;
|
|
48
|
+
/** While true, `loadingText` is shown and announced in place of the empty words. */
|
|
49
|
+
loading?: boolean;
|
|
50
|
+
loadingText?: string;
|
|
51
|
+
/**
|
|
52
|
+
* `'mod+k'` listens on the whole document for ⌘K (Apple) or Ctrl+K
|
|
53
|
+
* (elsewhere) to open the palette, or close it while open; it is left
|
|
54
|
+
* alone while focus is in another text field. `null` or nothing: no
|
|
55
|
+
* shortcut. Give it to one palette per page: two palettes listening for
|
|
56
|
+
* it would both open.
|
|
57
|
+
*/
|
|
58
|
+
shortcut?: CommandPaletteShortcut | null;
|
|
59
|
+
};
|
|
60
|
+
export type CommandPaletteTriggerProps = {
|
|
61
|
+
/** The button's name, such as "Search". Start `placeholder` with its words. */
|
|
62
|
+
label: string;
|
|
63
|
+
/** The words drawn in the search-looking button, such as "Search teams and games". */
|
|
64
|
+
placeholder: string;
|
|
65
|
+
/** The key hint drawn at the end, such as "⌘K" or "Ctrl K"; hidden from assistive technology. */
|
|
66
|
+
shortcutLabel?: string;
|
|
67
|
+
/** The consumer's search glyph (Scalewing has no icon catalog). */
|
|
68
|
+
icon: ReactNode;
|
|
69
|
+
/** An icon-only button, the glyph alone, for a narrow bar. */
|
|
70
|
+
compact?: boolean;
|
|
71
|
+
onPress: () => void;
|
|
72
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CommandPaletteProps, type CommandPaletteTriggerProps } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fails closed on a palette it cannot name or draw: blank words, a group
|
|
4
|
+
* without a caption, empty or duplicate group or item ids (items across
|
|
5
|
+
* groups), a blank item label or detail, loading without its words, or an
|
|
6
|
+
* unknown shortcut.
|
|
7
|
+
*/
|
|
8
|
+
export declare function assertCommandPalette({ emptyText, groups, hint, label, loading, loadingText, placeholder, shortcut, }: CommandPaletteProps): void;
|
|
9
|
+
/** Fails closed on a trigger with blank words or a glyph that draws nothing. */
|
|
10
|
+
export declare function assertCommandPaletteTrigger({ icon, label, placeholder, shortcutLabel, }: CommandPaletteTriggerProps): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isOneOf } from '../../is-one-of.js';
|
|
2
|
+
import { hasSlot } from '../slot.js';
|
|
3
|
+
import { commandPaletteShortcuts, } from './types.js';
|
|
4
|
+
function isBlank(words) {
|
|
5
|
+
return typeof words !== 'string' || !words.trim();
|
|
6
|
+
}
|
|
7
|
+
function blankWhenGiven(words) {
|
|
8
|
+
return words !== undefined && isBlank(words);
|
|
9
|
+
}
|
|
10
|
+
function hasDuplicates(ids) {
|
|
11
|
+
return new Set(ids).size !== ids.length;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Fails closed on a palette it cannot name or draw: blank words, a group
|
|
15
|
+
* without a caption, empty or duplicate group or item ids (items across
|
|
16
|
+
* groups), a blank item label or detail, loading without its words, or an
|
|
17
|
+
* unknown shortcut.
|
|
18
|
+
*/
|
|
19
|
+
export function assertCommandPalette({ emptyText, groups, hint, label, loading, loadingText, placeholder, shortcut, }) {
|
|
20
|
+
const name = 'CommandPalette';
|
|
21
|
+
if (isBlank(label) || isBlank(emptyText))
|
|
22
|
+
throw new RangeError(`${name} label and emptyText must be nonempty`);
|
|
23
|
+
if (blankWhenGiven(placeholder) || blankWhenGiven(hint))
|
|
24
|
+
throw new RangeError(`${name} placeholder and hint are nonempty when given`);
|
|
25
|
+
if (blankWhenGiven(loadingText) || (loading && isBlank(loadingText)))
|
|
26
|
+
throw new RangeError(`${name} loading needs a nonempty loadingText`);
|
|
27
|
+
if (shortcut != null && !isOneOf(commandPaletteShortcuts, shortcut))
|
|
28
|
+
throw new RangeError(`${name} shortcut must be one of ${commandPaletteShortcuts.join(', ')} or null`);
|
|
29
|
+
if (groups.some((group) => isBlank(group.id) || isBlank(group.label)))
|
|
30
|
+
throw new RangeError(`${name} groups need nonempty ids and labels`);
|
|
31
|
+
if (hasDuplicates(groups.map((group) => group.id)))
|
|
32
|
+
throw new RangeError(`${name} groups need unique ids`);
|
|
33
|
+
const items = groups.flatMap((group) => group.items);
|
|
34
|
+
if (items.some((item) => isBlank(item.id) || isBlank(item.label)))
|
|
35
|
+
throw new RangeError(`${name} items need nonempty ids and labels`);
|
|
36
|
+
if (items.some((item) => blankWhenGiven(item.detail)))
|
|
37
|
+
throw new RangeError(`${name} item details are nonempty when given`);
|
|
38
|
+
if (hasDuplicates(items.map((item) => item.id)))
|
|
39
|
+
throw new RangeError(`${name} items need unique ids`);
|
|
40
|
+
}
|
|
41
|
+
/** Fails closed on a trigger with blank words or a glyph that draws nothing. */
|
|
42
|
+
export function assertCommandPaletteTrigger({ icon, label, placeholder, shortcutLabel, }) {
|
|
43
|
+
const name = 'CommandPaletteTrigger';
|
|
44
|
+
if (isBlank(label) || isBlank(placeholder))
|
|
45
|
+
throw new RangeError(`${name} label and placeholder must be nonempty`);
|
|
46
|
+
if (blankWhenGiven(shortcutLabel))
|
|
47
|
+
throw new RangeError(`${name} shortcutLabel is nonempty when given`);
|
|
48
|
+
if (!hasSlot(icon) || (typeof icon === 'string' && !icon.trim()))
|
|
49
|
+
throw new RangeError(`${name} needs its search glyph`);
|
|
50
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type PointerEvent } from 'react';
|
|
2
|
+
import { type SwitcherEntry } from './options.js';
|
|
3
|
+
type SwitcherOptionProps = {
|
|
4
|
+
entry: SwitcherEntry;
|
|
5
|
+
active: boolean;
|
|
6
|
+
current: boolean;
|
|
7
|
+
/** A pointer moved over the row; the popup decides whether it really moved. */
|
|
8
|
+
onActivate: (event: PointerEvent) => void;
|
|
9
|
+
onChoose: () => void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* One option: its mark (hidden), its label over its detail, its badge and
|
|
13
|
+
* count (both read in its name, a `countLabel` in place of the number) and
|
|
14
|
+
* the current one's check (hidden; `aria-selected` says it).
|
|
15
|
+
*/
|
|
16
|
+
export declare function SwitcherOption({ active, current, entry, onActivate, onChoose, }: SwitcherOptionProps): import("react").JSX.Element;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../../class-names.js';
|
|
3
|
+
import { countToneClass } from '../../css/css-count-pill.js';
|
|
4
|
+
import { Badge } from '../Badge.js';
|
|
5
|
+
/**
|
|
6
|
+
* One option: its mark (hidden), its label over its detail, its badge and
|
|
7
|
+
* count (both read in its name, a `countLabel` in place of the number) and
|
|
8
|
+
* the current one's check (hidden; `aria-selected` says it).
|
|
9
|
+
*/
|
|
10
|
+
export function SwitcherOption({ active, current, entry, onActivate, onChoose, }) {
|
|
11
|
+
const { option } = entry;
|
|
12
|
+
const tone = option.countTone ?? 'danger';
|
|
13
|
+
return (_jsxs("div", { "aria-selected": current, className: "sw-context-switcher-option", "data-active": active ? 'true' : undefined, id: entry.domId, onClick: onChoose,
|
|
14
|
+
// The pointer moves the active row, as the arrow keys do.
|
|
15
|
+
onPointerMove: (event) => {
|
|
16
|
+
if (!active)
|
|
17
|
+
onActivate(event);
|
|
18
|
+
}, role: "option", children: [option.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-context-switcher-icon", children: option.icon })) : null, _jsxs("span", { className: "sw-context-switcher-words", children: [_jsx("span", { className: "sw-context-switcher-label", children: option.label }), option.detail ? (_jsxs("span", { className: "sw-context-switcher-detail", children: [" ", option.detail] })) : null] }), option.badge ? (_jsx(Badge, { size: "sm", tone: option.badge.tone, children: option.badge.label })) : null, option.count ? (_jsxs(_Fragment, { children: [option.countLabel ? (_jsx("span", { className: "sw-sr-only", children: ` ${option.countLabel}` })) : null, _jsx("span", { "aria-hidden": option.countLabel ? true : undefined, className: cx('sw-context-switcher-count', tone !== 'danger' &&
|
|
19
|
+
countToneClass('sw-context-switcher-count', tone)), children: option.count })] })) : null, current ? (_jsx("span", { "aria-hidden": "true", className: "sw-context-switcher-check" })) : null] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { type InlineAlign } from '../../anchored-position.js';
|
|
3
|
+
import { type SwitcherEntry } from './options.js';
|
|
4
|
+
import { type ContextSwitcherAction, type ContextSwitcherFilter, type ContextSwitcherGroup } from './types.js';
|
|
5
|
+
type SwitcherPopupProps = {
|
|
6
|
+
label: string;
|
|
7
|
+
groups: readonly ContextSwitcherGroup[];
|
|
8
|
+
currentId: string | undefined;
|
|
9
|
+
/** The filter, when it is shown (enough options). */
|
|
10
|
+
filter: ContextSwitcherFilter | undefined;
|
|
11
|
+
actions: readonly ContextSwitcherAction[];
|
|
12
|
+
align: InlineAlign;
|
|
13
|
+
triggerRef: RefObject<HTMLButtonElement | null>;
|
|
14
|
+
/** Whether a pointer is pressing the trigger (its click toggles the popup). */
|
|
15
|
+
triggerPressed: () => boolean;
|
|
16
|
+
/** Closes the popup; `refocus` hands focus back to the trigger. */
|
|
17
|
+
onClose: (refocus: boolean) => void;
|
|
18
|
+
onChoose: (entry: SwitcherEntry) => void;
|
|
19
|
+
onAction: (action: ContextSwitcherAction) => void;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The open popup: a non-modal `dialog` named by the switcher's label on the
|
|
23
|
+
* top layer beside the trigger. Focus moves into it, to the filter field
|
|
24
|
+
* (an ARIA combobox over the listbox) or else to the listbox itself; both
|
|
25
|
+
* point at the active row with `aria-activedescendant`. Escape closes it
|
|
26
|
+
* and returns focus to the trigger; a press outside or focus leaving it
|
|
27
|
+
* (Tab past its end, Shift+Tab back to the trigger) closes it where focus
|
|
28
|
+
* went.
|
|
29
|
+
*/
|
|
30
|
+
export declare function SwitcherPopup({ actions, align, currentId, filter, groups, label, onAction, onChoose, onClose, triggerPressed, triggerRef, }: SwitcherPopupProps): import("react").JSX.Element;
|
|
31
|
+
export {};
|