@scalewing/react 1.46.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 (134) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +228 -4
  3. package/dist/app-shell.test-support.d.ts +5 -0
  4. package/dist/app-shell.test-support.js +45 -0
  5. package/dist/command-palette.test-support.d.ts +21 -0
  6. package/dist/command-palette.test-support.js +55 -0
  7. package/dist/components/AppHeader.d.ts +3 -0
  8. package/dist/components/AppShell.d.ts +13 -0
  9. package/dist/components/AppShell.js +65 -0
  10. package/dist/components/Band.d.ts +30 -0
  11. package/dist/components/Band.js +23 -0
  12. package/dist/components/ButtonLink.d.ts +33 -0
  13. package/dist/components/ButtonLink.js +27 -0
  14. package/dist/components/CommandPalette.d.ts +12 -0
  15. package/dist/components/CommandPalette.js +143 -0
  16. package/dist/components/CommandPaletteTrigger.d.ts +9 -0
  17. package/dist/components/CommandPaletteTrigger.js +16 -0
  18. package/dist/components/ContextSwitcher.d.ts +12 -0
  19. package/dist/components/ContextSwitcher.js +62 -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/NavCount.d.ts +12 -5
  27. package/dist/components/NavCount.js +9 -4
  28. package/dist/components/SectionNav.d.ts +2 -1
  29. package/dist/components/SectionNav.js +3 -19
  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/app-shell/AppShellBar.d.ts +23 -0
  37. package/dist/components/app-shell/AppShellBar.js +25 -0
  38. package/dist/components/app-shell/AppShellDrawer.d.ts +24 -0
  39. package/dist/components/app-shell/AppShellDrawer.js +17 -0
  40. package/dist/components/app-shell/AppShellLink.d.ts +14 -0
  41. package/dist/components/app-shell/AppShellLink.js +27 -0
  42. package/dist/components/app-shell/AppShellNav.d.ts +15 -0
  43. package/dist/components/app-shell/AppShellNav.js +29 -0
  44. package/dist/components/app-shell/AppShellSidebar.d.ts +18 -0
  45. package/dist/components/app-shell/AppShellSidebar.js +18 -0
  46. package/dist/components/app-shell/types.d.ts +122 -0
  47. package/dist/components/app-shell/types.js +1 -0
  48. package/dist/components/app-shell/validate.d.ts +7 -0
  49. package/dist/components/app-shell/validate.js +70 -0
  50. package/dist/components/command-palette/PaletteList.d.ts +20 -0
  51. package/dist/components/command-palette/PaletteList.js +21 -0
  52. package/dist/components/command-palette/shortcut.d.ts +19 -0
  53. package/dist/components/command-palette/shortcut.js +75 -0
  54. package/dist/components/command-palette/types.d.ts +72 -0
  55. package/dist/components/command-palette/types.js +2 -0
  56. package/dist/components/command-palette/validate.d.ts +10 -0
  57. package/dist/components/command-palette/validate.js +50 -0
  58. package/dist/components/context-switcher/SwitcherOption.d.ts +17 -0
  59. package/dist/components/context-switcher/SwitcherOption.js +20 -0
  60. package/dist/components/context-switcher/SwitcherPopup.d.ts +31 -0
  61. package/dist/components/context-switcher/SwitcherPopup.js +111 -0
  62. package/dist/components/context-switcher/options.d.ts +33 -0
  63. package/dist/components/context-switcher/options.js +60 -0
  64. package/dist/components/context-switcher/types.d.ts +96 -0
  65. package/dist/components/context-switcher/types.js +1 -0
  66. package/dist/components/context-switcher/use-switcher-list.d.ts +29 -0
  67. package/dist/components/context-switcher/use-switcher-list.js +93 -0
  68. package/dist/components/context-switcher/validate.d.ts +8 -0
  69. package/dist/components/context-switcher/validate.js +94 -0
  70. package/dist/components/fixture/fixture-list-context.d.ts +4 -0
  71. package/dist/components/fixture/fixture-list-context.js +9 -0
  72. package/dist/components/fixture/types.d.ts +50 -0
  73. package/dist/components/fixture/types.js +1 -0
  74. package/dist/components/fixture/validate.d.ts +8 -0
  75. package/dist/components/fixture/validate.js +36 -0
  76. package/dist/components/nav-items.d.ts +21 -0
  77. package/dist/components/nav-items.js +26 -0
  78. package/dist/components/site-footer/types.d.ts +31 -0
  79. package/dist/components/site-footer/types.js +1 -0
  80. package/dist/components/site-footer/validate.d.ts +7 -0
  81. package/dist/components/site-footer/validate.js +22 -0
  82. package/dist/components/site-header/SiteHeaderLinks.d.ts +7 -0
  83. package/dist/components/site-header/SiteHeaderLinks.js +7 -0
  84. package/dist/components/site-header/types.d.ts +38 -0
  85. package/dist/components/site-header/types.js +1 -0
  86. package/dist/components/site-header/use-menu-dismiss.d.ts +8 -0
  87. package/dist/components/site-header/use-menu-dismiss.js +62 -0
  88. package/dist/components/site-header/validate.d.ts +7 -0
  89. package/dist/components/site-header/validate.js +17 -0
  90. package/dist/components/use-element-height.d.ts +10 -0
  91. package/dist/components/use-element-height.js +35 -0
  92. package/dist/context-switcher.test-support.d.ts +9 -0
  93. package/dist/context-switcher.test-support.js +41 -0
  94. package/dist/css/css-app-shell-drawer.d.ts +2 -0
  95. package/dist/css/css-app-shell-drawer.js +91 -0
  96. package/dist/css/css-app-shell-nav.d.ts +3 -0
  97. package/dist/css/css-app-shell-nav.js +167 -0
  98. package/dist/css/css-app-shell-page-bar.d.ts +3 -0
  99. package/dist/css/css-app-shell-page-bar.js +81 -0
  100. package/dist/css/css-app-shell.d.ts +4 -0
  101. package/dist/css/css-app-shell.js +155 -0
  102. package/dist/css/css-band.d.ts +10 -0
  103. package/dist/css/css-band.js +101 -0
  104. package/dist/css/css-button-link.d.ts +3 -0
  105. package/dist/css/css-button-link.js +48 -0
  106. package/dist/css/css-command-palette-key.d.ts +1 -0
  107. package/dist/css/css-command-palette-key.js +33 -0
  108. package/dist/css/css-command-palette-trigger.d.ts +2 -0
  109. package/dist/css/css-command-palette-trigger.js +88 -0
  110. package/dist/css/css-command-palette.d.ts +2 -0
  111. package/dist/css/css-command-palette.js +209 -0
  112. package/dist/css/css-components.js +27 -0
  113. package/dist/css/css-context-switcher-trigger.d.ts +15 -0
  114. package/dist/css/css-context-switcher-trigger.js +175 -0
  115. package/dist/css/css-context-switcher.d.ts +4 -0
  116. package/dist/css/css-context-switcher.js +219 -0
  117. package/dist/css/css-count-pill.d.ts +7 -0
  118. package/dist/css/css-count-pill.js +50 -12
  119. package/dist/css/css-fixture.d.ts +5 -0
  120. package/dist/css/css-fixture.js +260 -0
  121. package/dist/css/css-glyph-tile.d.ts +2 -0
  122. package/dist/css/css-glyph-tile.js +29 -1
  123. package/dist/css/css-site-footer.d.ts +2 -0
  124. package/dist/css/css-site-footer.js +152 -0
  125. package/dist/css/css-site-header.d.ts +2 -0
  126. package/dist/css/css-site-header.js +262 -0
  127. package/dist/css/css-skeleton.d.ts +10 -0
  128. package/dist/css/css-skeleton.js +110 -0
  129. package/dist/css/stacking.js +7 -4
  130. package/dist/css/stylesheet.js +18 -0
  131. package/dist/index.d.ts +14 -0
  132. package/dist/index.js +11 -0
  133. package/dist/styles.css +2122 -512
  134. package/package.json +2 -2
@@ -1,5 +1,8 @@
1
- /** The pill's colours: `danger` for work waiting, `accent` for news. */
2
- export declare const navCountTones: readonly ["danger", "accent"];
1
+ /**
2
+ * The pill's colours: `danger` for work waiting, `accent` for news,
3
+ * `warning` for work that needs the person but is not urgent.
4
+ */
5
+ export declare const navCountTones: readonly ["danger", "accent", "warning"];
3
6
  export type NavCountTone = (typeof navCountTones)[number];
4
7
  export type NavCountProps = {
5
8
  /**
@@ -17,8 +20,11 @@ export type NavCountProps = {
17
20
  /**
18
21
  * The pill's colour. `danger` (the default) is work waiting there, such
19
22
  * as tasks to do; `accent` is news, such as what was added since the
20
- * person last looked, where nothing is overdue. Give an accent count a
21
- * `countLabel` ("3 new"), so its words, not its colour, say it is news.
23
+ * person last looked, where nothing is overdue; `warning` is amber, work
24
+ * that needs the person but is not urgent ("needs you"): the warning
25
+ * tint with the warning ink (4.5:1 in every palette) inside a warning
26
+ * edge. Give an accent or warning count a `countLabel` ("3 new", "2 to
27
+ * confirm"), so its words, not its colour, say what it is.
22
28
  * Place either tone on a neutral or ghost destination: a filled one of
23
29
  * the same colour would hide it.
24
30
  */
@@ -29,6 +35,7 @@ export type NavCountProps = {
29
35
  * Button: `<a href="/tasks">Tasks<NavCount count={8} /></a>`. It is the
30
36
  * pill SectionNav's item count draws, and it is part of the destination's
31
37
  * accessible name, as there. `tone="accent"` draws it in the accent colour,
32
- * for news rather than work.
38
+ * for news rather than work; `tone="warning"` in amber, for work that needs
39
+ * the person but is not urgent.
33
40
  */
34
41
  export declare function NavCount({ count, countLabel, tone, }: NavCountProps): import("react").JSX.Element | null;
@@ -1,8 +1,12 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../class-names.js';
3
+ import { countToneClass } from '../css/css-count-pill.js';
3
4
  import { isOneOf } from '../is-one-of.js';
4
- /** The pill's colours: `danger` for work waiting, `accent` for news. */
5
- export const navCountTones = ['danger', 'accent'];
5
+ /**
6
+ * The pill's colours: `danger` for work waiting, `accent` for news,
7
+ * `warning` for work that needs the person but is not urgent.
8
+ */
9
+ export const navCountTones = ['danger', 'accent', 'warning'];
6
10
  /**
7
11
  * Fails closed on a count that is not a whole number, an empty label or an
8
12
  * unknown tone.
@@ -21,7 +25,8 @@ function assertNavCount({ count, countLabel, tone }) {
21
25
  * Button: `<a href="/tasks">Tasks<NavCount count={8} /></a>`. It is the
22
26
  * pill SectionNav's item count draws, and it is part of the destination's
23
27
  * accessible name, as there. `tone="accent"` draws it in the accent colour,
24
- * for news rather than work.
28
+ * for news rather than work; `tone="warning"` in amber, for work that needs
29
+ * the person but is not urgent.
25
30
  */
26
31
  export function NavCount({ count, countLabel, tone = 'danger', }) {
27
32
  assertNavCount({ count, countLabel, tone });
@@ -29,5 +34,5 @@ export function NavCount({ count, countLabel, tone = 'danger', }) {
29
34
  return null;
30
35
  // The space is its own text node, so every browser and the name's
31
36
  // computation keep it between the words and the count.
32
- return (_jsxs(_Fragment, { children: [' ', countLabel ? _jsx("span", { className: "sw-sr-only", children: countLabel }) : null, _jsx("span", { "aria-hidden": countLabel ? true : undefined, className: cx('sw-nav-count', tone === 'accent' && 'sw-nav-count-accent'), children: count })] }));
37
+ return (_jsxs(_Fragment, { children: [' ', countLabel ? _jsx("span", { className: "sw-sr-only", children: countLabel }) : null, _jsx("span", { "aria-hidden": countLabel ? true : undefined, className: cx('sw-nav-count', tone !== 'danger' && countToneClass('sw-nav-count', tone)), children: count })] }));
33
38
  }
@@ -2,6 +2,7 @@ import { type ReactNode } from 'react';
2
2
  import { type AccessibleName } from '../accessible-name.js';
3
3
  import { type Breakpoint } from '../css/breakpoints.js';
4
4
  import { type SectionNavAppearance } from '../css/css-section-nav-filled.js';
5
+ import { type NavItemCurrent } from './nav-items.js';
5
6
  export type { SectionNavAppearance };
6
7
  export { sectionNavAppearances } from '../css/css-section-nav-filled.js';
7
8
  export type SectionNavItem = {
@@ -12,7 +13,7 @@ export type SectionNavItem = {
12
13
  * `page` when the item is this page, `location` (or `true`) when the page
13
14
  * is inside the item's section, such as a detail page under its list.
14
15
  */
15
- current?: 'page' | 'location' | boolean;
16
+ current?: NavItemCurrent;
16
17
  /** A decorative glyph before the label. */
17
18
  icon?: ReactNode;
18
19
  /**
@@ -5,24 +5,8 @@ import { sectionNavVerticalClass } from '../css/css-section-nav.js';
5
5
  import { assertSectionNavAppearance, } from '../css/css-section-nav-filled.js';
6
6
  import { tabStop } from '../tab-stop.js';
7
7
  import { plainPress } from './link-press.js';
8
+ import { assertNavItems, navItemAriaCurrent, } from './nav-items.js';
8
9
  export { sectionNavAppearances } from '../css/css-section-nav-filled.js';
9
- function ariaCurrent(current) {
10
- if (current === true)
11
- return 'true';
12
- return current || undefined;
13
- }
14
- function assertItems(items) {
15
- if (items.some((item) => !item.id.trim() || !item.label.trim()))
16
- throw new RangeError('SectionNav items need nonempty ids and labels');
17
- if (new Set(items.map((item) => item.id)).size !== items.length)
18
- throw new RangeError('SectionNav items need unique ids');
19
- if (items.some((item) => item.count !== undefined &&
20
- (!Number.isInteger(item.count) || item.count < 0)))
21
- throw new RangeError('SectionNav item counts are whole numbers from 0');
22
- if (items.some((item) => item.countLabel !== undefined &&
23
- (!item.countLabel.trim() || item.count === undefined)))
24
- throw new RangeError('SectionNav item countLabel is nonempty and comes with a count');
25
- }
26
10
  function assertBreakpoint(verticalFrom) {
27
11
  if (verticalFrom !== undefined && !breakpoints.includes(verticalFrom))
28
12
  throw new RangeError(`SectionNav verticalFrom must be one of ${breakpoints.join(', ')}`);
@@ -34,10 +18,10 @@ function assertBreakpoint(verticalFrom) {
34
18
  * a side list. The current item carries `aria-current`.
35
19
  */
36
20
  export function SectionNav({ appearance = 'underline', items, onNavigate, verticalFrom, ...label }) {
37
- assertItems(items);
21
+ assertNavItems('SectionNav', items);
38
22
  assertBreakpoint(verticalFrom);
39
23
  assertSectionNavAppearance(appearance);
40
- return (_jsx("nav", { className: cx('sw-section-nav', verticalFrom && sectionNavVerticalClass(verticalFrom), appearance === 'filled' && 'sw-section-nav-filled'), ...label, children: _jsx("ul", { className: "sw-section-nav-list", children: items.map((item) => (_jsx("li", { children: _jsxs("a", { "aria-current": ariaCurrent(item.current), className: "sw-section-nav-link", href: item.href, tabIndex: tabStop,
24
+ return (_jsx("nav", { className: cx('sw-section-nav', verticalFrom && sectionNavVerticalClass(verticalFrom), appearance === 'filled' && 'sw-section-nav-filled'), ...label, children: _jsx("ul", { className: "sw-section-nav-list", children: items.map((item) => (_jsx("li", { children: _jsxs("a", { "aria-current": navItemAriaCurrent(item.current), className: "sw-section-nav-link", href: item.href, tabIndex: tabStop,
41
25
  // Only a given onNavigate attaches a handler, so the links
42
26
  // render from a server component without one.
43
27
  onClick: onNavigate
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,24 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type AppShellNavGroup, type AppShellNavItem } from './types.js';
3
+ export type AppShellDrawerProps = {
4
+ open: boolean;
5
+ /** Asked on every close request: Escape, a press outside, the close button. */
6
+ onClose: () => void;
7
+ /** Asked after a plain press on a destination, which also closes it. */
8
+ onChoose: () => void;
9
+ closeLabel: string;
10
+ navLabel: string;
11
+ groups: readonly AppShellNavGroup[];
12
+ onNavigate?: (item: AppShellNavItem) => void;
13
+ header?: ReactNode;
14
+ footer?: ReactNode;
15
+ };
16
+ /**
17
+ * The menu below `lg`: a modal `dialog` on the top layer, full height at
18
+ * the inline start, named by the navigation's label. Dialog's close-request
19
+ * mechanics make Escape, a backdrop press and a platform close request one
20
+ * `onClose`, after which focus returns to the menu button. Its close button
21
+ * sits where the menu button was, so it is the first control. The content
22
+ * is drawn only while open, so the page holds one navigation at a time.
23
+ */
24
+ export declare function AppShellDrawer({ closeLabel, footer, groups, header, navLabel, onChoose, onClose, onNavigate, open, }: AppShellDrawerProps): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Button } from '../Button.js';
4
+ import { useDialogCloseRequests } from '../dialog-close-requests.js';
5
+ import { AppShellNav } from './AppShellNav.js';
6
+ /**
7
+ * The menu below `lg`: a modal `dialog` on the top layer, full height at
8
+ * the inline start, named by the navigation's label. Dialog's close-request
9
+ * mechanics make Escape, a backdrop press and a platform close request one
10
+ * `onClose`, after which focus returns to the menu button. Its close button
11
+ * sits where the menu button was, so it is the first control. The content
12
+ * is drawn only while open, so the page holds one navigation at a time.
13
+ */
14
+ export function AppShellDrawer({ closeLabel, footer, groups, header, navLabel, onChoose, onClose, onNavigate, open, }) {
15
+ const { nodeRef, handlers } = useDialogCloseRequests({ open, onClose });
16
+ return (_jsx("dialog", { ref: nodeRef, "aria-label": navLabel, className: "sw-app-shell-drawer", ...handlers, children: open ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sw-app-shell-drawer-top", children: [_jsxs(Button, { className: "sw-app-shell-close", onPress: onClose, size: "sm", variant: "ghost", children: [_jsx("span", { className: "sw-sr-only", children: closeLabel }), _jsx("span", { "aria-hidden": "true", className: "sw-dialog-close-glyph" })] }), header ? (_jsx("div", { className: "sw-app-shell-header", children: header })) : null] }), _jsx(AppShellNav, { groups: groups, navLabel: navLabel, onChoose: onChoose, onNavigate: onNavigate }), footer ? _jsx("div", { className: "sw-app-shell-footer", children: footer }) : null] })) : null }));
17
+ }
@@ -0,0 +1,14 @@
1
+ import { type MouseEvent } from 'react';
2
+ import { type AppShellNavItem } from './types.js';
3
+ export type AppShellLinkProps = {
4
+ item: AppShellNavItem;
5
+ onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
6
+ /**
7
+ * The sidebar's links sit in a Tooltip with their label, shown only while
8
+ * `tooltip` is true (the rail, where the label is hidden). Left out (the
9
+ * drawer) there is no Tooltip at all.
10
+ */
11
+ tooltip?: boolean;
12
+ };
13
+ /** One destination in its list item: the glyph, the label and the count. */
14
+ export declare function AppShellLink({ item, onClick, tooltip }: AppShellLinkProps): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../../class-names.js';
3
+ import { countToneClass } from '../../css/css-count-pill.js';
4
+ import { tabStop } from '../../tab-stop.js';
5
+ import { navItemAriaCurrent } from '../nav-items.js';
6
+ import { Tooltip } from '../Tooltip.js';
7
+ /*
8
+ * The destination's link. A Tooltip around it passes its references in;
9
+ * only `aria-labelledby` is kept. Its `aria-describedby` is left out on
10
+ * purpose: the tooltip's words are the label the link is already named by,
11
+ * so they would be read twice.
12
+ */
13
+ function AppShellAnchor({ 'aria-labelledby': labelledBy, children, item, onClick, }) {
14
+ return (_jsx("a", { "aria-current": navItemAriaCurrent(item.current), "aria-labelledby": labelledBy, className: "sw-app-shell-link", href: item.href, onClick: onClick, tabIndex: tabStop, children: children }));
15
+ }
16
+ /** The count after the label, read in the link's name as SectionNav's is. */
17
+ function AppShellCount({ count, countLabel, countTone = 'danger', }) {
18
+ if (!count)
19
+ return null;
20
+ return (_jsxs(_Fragment, { children: [countLabel ? (_jsx("span", { className: "sw-sr-only", children: ` ${countLabel}` })) : (' '), _jsx("span", { "aria-hidden": countLabel ? true : undefined, className: cx('sw-app-shell-count', countTone !== 'danger' &&
21
+ countToneClass('sw-app-shell-count', countTone)), children: count })] }));
22
+ }
23
+ /** One destination in its list item: the glyph, the label and the count. */
24
+ export function AppShellLink({ item, onClick, tooltip }) {
25
+ const content = (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: "sw-app-shell-icon", children: item.icon }), _jsx("span", { className: "sw-app-shell-label", children: item.label }), _jsx(AppShellCount, { count: item.count, countLabel: item.countLabel, countTone: item.countTone })] }));
26
+ return (_jsx("li", { className: "sw-app-shell-item", children: tooltip === undefined ? (_jsx(AppShellAnchor, { item: item, onClick: onClick, children: content })) : (_jsx(Tooltip, { content: item.label, disabled: !tooltip, trigger: _jsx(AppShellAnchor, { item: item, onClick: onClick, children: content }) })) }));
27
+ }
@@ -0,0 +1,15 @@
1
+ import { type AppShellNavGroup, type AppShellNavItem } from './types.js';
2
+ export type AppShellNavProps = {
3
+ navLabel: string;
4
+ groups: readonly AppShellNavGroup[];
5
+ onNavigate?: (item: AppShellNavItem) => void;
6
+ /** The drawer's: a plain press on a destination also closes it. */
7
+ onChoose?: () => void;
8
+ /** The sidebar's: whether the rail's tooltips show (see AppShellLink). */
9
+ tooltip?: boolean;
10
+ };
11
+ /**
12
+ * The shell's navigation landmark: each group a list named by its label,
13
+ * the same in the sidebar and the drawer.
14
+ */
15
+ export declare function AppShellNav({ groups, navLabel, onChoose, onNavigate, tooltip, }: AppShellNavProps): import("react").JSX.Element;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from 'react';
3
+ import { plainPress } from '../link-press.js';
4
+ import { AppShellLink } from './AppShellLink.js';
5
+ /**
6
+ * The shell's navigation landmark: each group a list named by its label,
7
+ * the same in the sidebar and the drawer.
8
+ */
9
+ export function AppShellNav({ groups, navLabel, onChoose, onNavigate, tooltip, }) {
10
+ const idPrefix = useId();
11
+ // Only a given onNavigate or onChoose attaches a handler.
12
+ function pressHandler(item) {
13
+ if (!onNavigate && !onChoose)
14
+ return undefined;
15
+ return (event) => {
16
+ if (!plainPress(event))
17
+ return;
18
+ if (onNavigate) {
19
+ event.preventDefault();
20
+ onNavigate(item);
21
+ }
22
+ onChoose?.();
23
+ };
24
+ }
25
+ return (_jsx("nav", { "aria-label": navLabel, className: "sw-app-shell-nav", children: groups.map((group, index) => {
26
+ const labelId = `${idPrefix}-group-${index}`;
27
+ return (_jsxs("div", { className: "sw-app-shell-group", children: [group.label ? (_jsx("p", { className: "sw-app-shell-group-label", id: labelId, children: group.label })) : null, _jsx("ul", { "aria-labelledby": group.label ? labelId : undefined, className: "sw-app-shell-list", children: group.items.map((item) => (_jsx(AppShellLink, { item: item, onClick: pressHandler(item), tooltip: tooltip }, item.id))) })] }, group.id));
28
+ }) }));
29
+ }
@@ -0,0 +1,18 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type AppShellNavGroup, type AppShellNavItem } from './types.js';
3
+ export type AppShellSidebarProps = {
4
+ navLabel: string;
5
+ groups: readonly AppShellNavGroup[];
6
+ onNavigate?: (item: AppShellNavItem) => void;
7
+ header?: ReactNode;
8
+ footer?: ReactNode;
9
+ railHeader?: ReactNode;
10
+ railFooter?: ReactNode;
11
+ /** Whether the window is in the rail's range, where tooltips show. */
12
+ rail: boolean;
13
+ };
14
+ /**
15
+ * The pinned column from `lg` up: a rail to 75rem, the full sidebar from
16
+ * there. Hidden below `lg`, where the drawer holds the same parts.
17
+ */
18
+ export declare function AppShellSidebar({ footer, groups, header, navLabel, onNavigate, rail, railFooter, railHeader, }: AppShellSidebarProps): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AppShellNav } from './AppShellNav.js';
3
+ /*
4
+ * A slot drawn in the full sidebar (`sw-app-shell-slot`) or only in the
5
+ * rail (`sw-app-shell-rail-slot`); the generated media queries pick one.
6
+ */
7
+ function Slot({ children, part, rail, }) {
8
+ if (!children)
9
+ return null;
10
+ return (_jsx("div", { className: `sw-app-shell-${part} ${rail ? 'sw-app-shell-rail-slot' : 'sw-app-shell-slot'}`, children: children }));
11
+ }
12
+ /**
13
+ * The pinned column from `lg` up: a rail to 75rem, the full sidebar from
14
+ * there. Hidden below `lg`, where the drawer holds the same parts.
15
+ */
16
+ export function AppShellSidebar({ footer, groups, header, navLabel, onNavigate, rail, railFooter, railHeader, }) {
17
+ return (_jsxs("div", { className: "sw-app-shell-sidebar", children: [_jsx(Slot, { part: "header", rail: false, children: header }), _jsx(Slot, { part: "header", rail: true, children: railHeader }), _jsx(AppShellNav, { groups: groups, navLabel: navLabel, onNavigate: onNavigate, tooltip: rail }), _jsx(Slot, { part: "footer", rail: false, children: footer }), _jsx(Slot, { part: "footer", rail: true, children: railFooter })] }));
18
+ }
@@ -0,0 +1,122 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type NavCountTone } from '../NavCount.js';
3
+ import { type NavItemCurrent } from '../nav-items.js';
4
+ export type AppShellNavItem = {
5
+ id: string;
6
+ label: string;
7
+ href: string;
8
+ /**
9
+ * The destination's glyph (a consumer Lucide icon or mark), hidden from
10
+ * assistive technology. Required: in the rail it is all the item shows.
11
+ */
12
+ icon: ReactNode;
13
+ /**
14
+ * How many things wait there, such as work to do: the shared count pill
15
+ * (danger) after the label, or on the glyph in the rail, read in the
16
+ * link's name. A whole number; 0 (or none) shows no pill.
17
+ */
18
+ count?: number;
19
+ /**
20
+ * What the count means, in the consumer's words ("3 to confirm"): read in
21
+ * the name in place of the bare number, the pill then hidden from
22
+ * assistive technology. Needs `count`.
23
+ */
24
+ countLabel?: string;
25
+ /**
26
+ * The pill's colour, as NavCount's `tone`: `danger` (the default) for
27
+ * work waiting, `warning` (amber) for work that needs the person but is
28
+ * not urgent, `accent` for news. Needs `count`; say what it is in a
29
+ * `countLabel`, never by its colour alone.
30
+ */
31
+ countTone?: NavCountTone;
32
+ /**
33
+ * `page` when the item is this page, `location` (or `true`) when the page
34
+ * is inside the item's area, such as a detail page under its list.
35
+ */
36
+ current?: NavItemCurrent;
37
+ };
38
+ export type AppShellNavGroup = {
39
+ id: string;
40
+ /**
41
+ * A small muted caption over the group's destinations that names their
42
+ * list. In the rail it is hidden (still naming the list) and the group
43
+ * starts under a hairline.
44
+ */
45
+ label?: string;
46
+ items: readonly AppShellNavItem[];
47
+ };
48
+ /** The shell's own words: the menu button's name and the drawer's close. */
49
+ export type AppShellLabels = {
50
+ openMenu: string;
51
+ closeMenu: string;
52
+ };
53
+ export type AppShellProps = {
54
+ /** The navigation landmark's name, also the drawer's. */
55
+ navLabel: string;
56
+ groups: readonly AppShellNavGroup[];
57
+ /**
58
+ * Called for a plain press on a destination (the main button, no
59
+ * modifier key), after the browser's own navigation is prevented, so a
60
+ * client router moves instead. A press with a modifier key, or a middle
61
+ * press, keeps the browser's behaviour. Without it the links navigate.
62
+ */
63
+ onNavigate?: (item: AppShellNavItem) => void;
64
+ /**
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.
69
+ * While the drawer is open it is mounted twice (the sidebar's copy is
70
+ * hidden), so each copy keeps its own state: put no fixed DOM `id` in it
71
+ * (use `useId`), and keep state that must be shared in the consumer.
72
+ * The same holds for `footer`, `railHeader` and `railFooter`.
73
+ */
74
+ header?: ReactNode;
75
+ /** The sidebar's bottom, such as the account menu; also the drawer's (see `header`). */
76
+ footer?: ReactNode;
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
+ */
82
+ railHeader?: ReactNode;
83
+ /** A compact `footer` for the rail. Left out, the rail shows none. */
84
+ railFooter?: ReactNode;
85
+ /** The compact top bar's title, below `lg`. */
86
+ barTitle: ReactNode;
87
+ /** Controls at the top bar's end, below `lg`. */
88
+ barEnd?: ReactNode;
89
+ /** The consumer's glyph for the menu button (Scalewing has no icon catalog). */
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;
119
+ labels: AppShellLabels;
120
+ /** The page, rendered in `main`. */
121
+ children: ReactNode;
122
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import { type AppShellProps } from './types.js';
2
+ /**
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).
6
+ */
7
+ export declare function assertAppShellProps({ groups, labels, menuCount, menuCountLabel, menuCountTone, navLabel, }: Pick<AppShellProps, 'groups' | 'labels' | 'menuCount' | 'menuCountLabel' | 'menuCountTone' | 'navLabel'>): void;