@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,79 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.48.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a4b524c: `AppShell` gains `pageBar`, a band at the top of `main` on every layout (the solid surface with a hairline under it and the page's inline padding, its children a wrapping row with space between them), sticky under the compact top bar below `lg` (the shell measures the bar and sets `--sw-app-shell-bar-height`, falling back to its usual height) and at the window's top from `lg`; and `menuCount`, `menuCountLabel` and `menuCountTone`, the shared count pill on the compact bar's menu button, read in its name after a comma ("Open menu, 6 need you"). `header` and `railHeader` are documented as hosts for an interactive switcher trigger, whose popup opens on the top layer. `AppHeader` accepts a `ref` for its `header`. New class `sw-app-shell-page-bar`; an invalid menu count, a blank label, an unknown tone or a label or tone without a count throws a `RangeError`. No new tokens or dependencies. Request: `docs/requests/futmas-app-shell-page-bar.md`.
8
+ - 0d789a6: `Band`: a full-width stretch of the page in one fill, its content in a
9
+ centred column with the page gutter (16 px, 24 px from `md`), for a site's
10
+ footer, one dark closing section or a grey strip.
11
+
12
+ - `tone`: `surface` (the cards' fill), `subtle` or `inverse`, the dark band
13
+ (`--sw-color-inverse`, which a brand moves with a theme overlay). Inside
14
+ the inverse band the theme's words, muted words, hairlines, subtle fill,
15
+ accent and `onAccent` read on the dark fill, so `Text`, a filled `Card`,
16
+ a primary `Button` and the focus ring need no variant; every plain link
17
+ takes the band's words with a half-strength underline.
18
+ - `width` `container` or `reading`; `as` `section`, `div`, `aside` or
19
+ `footer`; `paddingY` (default 8), `paddingTop`, `paddingBottom`.
20
+ - Forced colours outline the inverse band.
21
+
22
+ New classes `sw-band`, `sw-band-inner`, `sw-band-<tone>`,
23
+ `sw-band-inner-<width>`; an unknown tone, width or element throws a
24
+ `RangeError`. Needs `@scalewing/tokens` with `inverse` and `onInverse`. No
25
+ new dependencies. Request: `docs/requests/teisoro-band.md`.
26
+
27
+ - 54adbbf: `ButtonLink`: a link drawn as a `Button`, for a destination that reads as
28
+ an action (a site's Contact us or Sign in). An `<a href>` with the
29
+ Button's `variant`, `size` and `width`, so it opens like any link and works
30
+ on a page rendered with no script; it keeps its variant's words over the
31
+ canvas link colour, hover included. `aria-current="page"` marks the
32
+ current page with the pressed button's ring (a Highlight border in forced
33
+ colours). New class `sw-button-link`; an unknown variant, size or width
34
+ throws a `RangeError`. No new tokens or dependencies. Request:
35
+ `docs/requests/teisoro-button-link.md`.
36
+ - a4bb2f5: `CommandPalette` and `CommandPaletteTrigger`. CommandPalette is a search over the product on the top layer: a modal dialog near the window's top holding a field (an ARIA combobox) over an always-open listbox of grouped results the consumer filters from `query`; ↑/↓ move the active row (wrapping, the Enter key cap on it), Enter or a press asks to close and then runs the item; Escape, a press outside or the shortcut again close it and focus returns to where it was. A polite status says `countText`, `emptyText` or `loadingText`; `hint` is a footer line; `shortcut="mod+k"` listens for ⌘K (Apple) or Ctrl+K, left alone in another text field. CommandPaletteTrigger is the search-looking pill with a key hint that opens it, or its glyph alone with `compact`. 44 px targets on a coarse pointer and forced colours covered. New classes `sw-command-palette*`; invalid words, groups or items throw a `RangeError`. No new tokens or dependencies. Request: `docs/requests/futmas-command-palette.md`.
37
+ - ff103a2: `ContextSwitcher`: a button showing the context in use (a workspace, a competition, a season) that opens a non-modal popup on the top layer to switch it: an optional filter field (an ARIA combobox that narrows the options by label or detail, case and accents ignored, its empty words shown and announced), the options in groups as a listbox (each with a mark, a detail, a status `Badge` and the shared count pill, all read in its name; the current one checked and `aria-selected`), and footer commands. Arrow keys, Enter, Home/End and typeahead in the listbox; Escape closes it with focus back on the trigger; a press outside or focus leaving closes it. `triggerAppearance` `framed` (default), `text`, `pill` or `icon` (a glyph alone, named by the label and the current option); `width="full"` fills a sidebar's top; `align` start or end. 44 px targets on a coarse pointer, right-to-left and forced colours covered. New classes `sw-context-switcher*` (its count joins the shared pill as `sw-context-switcher-count`); invalid groups, options, words or props throw a `RangeError`. No new tokens or dependencies. Request: `docs/requests/futmas-context-switcher.md`.
38
+ - 82fe15a: `FixtureRow` and `FixtureGroup`: one pairing of two sides with a kickoff time (a grey chip) or a score (bold figures) between them, the home side ending at the centre and the away side starting from it, marks on the centre side, meta and a status `Badge` at the end (under the sides below `md`, `metaHideBelow` leaving the meta out below `md` or `lg`); with `href` or `onPress` the whole row is a link or a button with a chevron. `FixtureGroup` is a caption naming the run's list, one grid with each row a subgrid, so the centres line up down the run. 44 px rows, right-to-left and forced colours covered. New classes `sw-fixture-*`; invalid words or props, or a row outside a group, throw a `RangeError`. No new tokens or dependencies. Request: `docs/requests/futmas-fixture-row.md`.
39
+ - 22f1769: Behaviour change: `GlyphTile` children that are a string or a number are now letters. An empty or whitespace-only string (which used to render an empty tile) and four or more letters (which used to render, clipped) now throw a `RangeError`; a number counts by its digits, so `{12345}` throws. The letters are sized from the tile and ignore the surrounding font size.
40
+
41
+ `GlyphTile` holds up to three letters in place of a glyph, such as initials ("NSL"): drawn in one line at weight 600, one letter at half the tile's side, two at 0.42, three at a third, so every count fits every size. A glyph child is unchanged. New classes `sw-glyph-tile-text`, `sw-glyph-tile-text-1`/`-2`/`-3`. No new tokens or dependencies. Request: `docs/requests/futmas-glyph-tile-three-letters.md`.
42
+
43
+ - 1321a50: `SiteFooter`: a public site's footer on the dark inverse band. `brand`,
44
+ an optional `tagline`, `groups` of links (each a `nav` named by its label,
45
+ the label shown above its links; a link's `current`, `lang` and
46
+ `hrefLang`), and a bottom row past a hairline with `legal` at its start and
47
+ `end` (such as a language switch) at its end. Links are the band's muted
48
+ words, the current one its words at label weight, underlined on hover and
49
+ focus, 44 px on a coarse pointer. Two columns of groups below `md`, one
50
+ spread row from `md`. New classes `sw-site-footer*`; no groups, an empty
51
+ group, blank or repeated ids, blank labels or a blank `href` throw a
52
+ `RangeError`. No new tokens or dependencies. Request:
53
+ `docs/requests/teisoro-site-footer.md`.
54
+ - 987e0ef: `SiteHeader`: a public site's header. The brand; from `lg` the main
55
+ destinations as a row of links (the current one at label weight over a
56
+ bar) and `actions` at the bar's end; below `lg` `compactActions` and a menu
57
+ button that opens the destinations as large rows and `menuActions` under
58
+ the bar over the window. The menu is a native `details`, so it opens and
59
+ closes by touch, click and keyboard with no script (a server-rendered
60
+ page); script adds Escape (focus back on the button) and closes it when a
61
+ link is chosen, focus leaves it, a press lands outside it or the window
62
+ widens to `lg`. Forced colours draw the current bar as a thick underline. Nothing traps focus. `labels`
63
+ (`openMenu`, `closeMenu`) name the button by its state; `menuIcon` and
64
+ `closeIcon` are its glyph slots; `sticky` (default `true`). New classes
65
+ `sw-site-header*`; no items, blank or repeated ids, blank labels or hrefs,
66
+ or blank names throw a `RangeError`. No new tokens or dependencies.
67
+ Request: `docs/requests/teisoro-site-header.md`.
68
+ - e82df4e: `Skeleton`: grey placeholder bars where content is loading, in place of a page spinner. A polite status named by `label` (read once), the bars hidden; `variant="lines"` (default) is 1–6 bars of text with ragged ends, the last shorter, and `variant="rows"` is list rows (a mark and a bar, 44 px, hairlines between). The bars pulse gently and stay still under reduced motion; forced colours keep their edges. New classes `sw-skeleton*`; blank words, `lines` outside 1–6 or an unknown variant throw a `RangeError`. No new tokens or dependencies. Request: `docs/requests/futmas-skeleton.md`.
69
+
70
+ ## 1.47.0
71
+
72
+ ### Minor Changes
73
+
74
+ - 26fe8e5: `AppShell`: a web workspace's frame with a pinned side navigation of grouped destinations beside the page in `main`, which scrolls with the document. From 75rem a full sidebar (header, groups with optional captions, footer; glyph, label and count per destination, the current one on a quiet fill), sticky at the viewport's height with only its nav scrolling; from `lg` to 75rem an icon rail whose labels stay in each link's name and show in a Tooltip, the count a pill on the glyph, with `railHeader`/`railFooter`; below `lg` a sticky AppHeader `bar` whose menu button opens the same navigation in a modal drawer from the inline start (Escape, a press outside, its close button or choosing a destination close it, focus returning to the menu button). Items follow `SectionNav`'s rules plus a required `icon`; `onNavigate` takes a plain press for a client router; invalid groups, items or words throw a `RangeError`. New classes `sw-app-shell*` (the count joins the shared count pill as `sw-app-shell-count`); the 75rem width is internal, not a public breakpoint. `SectionNav` and `AppShell` now share one item validation (`nav-items.ts`); `SectionNav`'s behaviour and messages are unchanged. No new tokens or dependencies. Request: `docs/requests/futmas-app-shell.md`.
75
+ - 26fe8e5: `NavCount` `tone="warning"`: an amber count for work that needs the person but is not urgent, the warning tint with the warning ink (a warning Notice's, 4.5:1 in every palette) inside a warning edge; `navCountTones` is now `['danger', 'accent', 'warning']`. `AppShell` items take `countTone` (NavCount's tones, `danger` by default; needs a `count`). New classes `sw-nav-count-warning`, `sw-app-shell-count-accent` and `sw-app-shell-count-warning`. The default stays danger. No new tokens or dependencies. Request: `docs/requests/futmas-nav-count-warning.md`.
76
+
3
77
  ## 1.46.0
4
78
 
5
79
  ### Minor Changes
package/README.md CHANGED
@@ -9,6 +9,10 @@ import {
9
9
  Accordion,
10
10
  ActionBar,
11
11
  ActionMenu,
12
+ CommandPalette,
13
+ CommandPaletteTrigger,
14
+ ContextSwitcher,
15
+ AppShell,
12
16
  BarChart,
13
17
  ChartLegend,
14
18
  ColumnChart,
@@ -16,20 +20,27 @@ import {
16
20
  DataBar,
17
21
  Box,
18
22
  Button,
23
+ ButtonLink,
19
24
  CalendarButton,
20
25
  DateRangeButton,
21
26
  Card,
22
27
  Checkbox,
23
28
  RadioGroup,
24
29
  SectionNav,
30
+ Skeleton,
25
31
  Spinner,
26
32
  Pagination,
27
33
  Progress,
28
34
  Tooltip,
29
35
  InfoTip,
30
36
  Separator,
37
+ Band,
38
+ SiteFooter,
39
+ SiteHeader,
31
40
  DescriptionList,
32
41
  DescriptionItem,
42
+ FixtureGroup,
43
+ FixtureRow,
33
44
  ListGroup,
34
45
  ListRow,
35
46
  MarkdownView,
@@ -76,7 +87,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
76
87
 
77
88
  `Box` `border` draws a hairline in the `border` color token: `border` (or `border={true}`) is solid, and `border="dashed"` is dashed (the generated `sw-border` and `sw-border-dashed` classes; a `style` you pass still wins), for a space to fill in by hand such as a blank on a printed form. `border="bottom"` draws only the hairline under the box (`sw-border-bottom`, solid), for a full-width bar such as a sub-navigation strip under the header; give it `background="surface"` to read as a band. It does not make the box look or act like an input. Every `Box`-based component takes it.
78
89
 
79
- `AppHeader` is the page's top chrome, sticky by default (`sticky={false}` leaves it in flow). Its default `appearance="glass"` is a rounded frosted card. `appearance="bar"` (`AppHeaderAppearance`, `appHeaderAppearances`) is a full-width band of the solid surface with the border hairline under it: no blur, no rounding, no side or top border, the same under Reduce Transparency, and the hairline kept in forced colors. Set it against the page's edges (no side gutter round it) and put the gutter inside it. An unknown `appearance` throws a `RangeError`.
90
+ `AppHeader` is the page's top chrome, sticky by default (`sticky={false}` leaves it in flow). Its default `appearance="glass"` is a rounded frosted card. `appearance="bar"` (`AppHeaderAppearance`, `appHeaderAppearances`) is a full-width band of the solid surface with the border hairline under it: no blur, no rounding, no side or top border, the same under Reduce Transparency, and the hairline kept in forced colors. Set it against the page's edges (no side gutter round it) and put the gutter inside it. A `ref` reaches its `header` element. An unknown `appearance` throws a `RangeError`.
80
91
 
81
92
  `Box hideBelow="md"` hides a region under 48rem and `hideFrom="md"` hides it at 48rem and wider, so a product can swap a wide layout for a narrow one without its own stylesheet. They also take `lg` (64rem), for content that fits only from a laptop up, such as a row of labelled destinations that a tablet shows as glyphs (name each glyph-only control, for example with `Tooltip relationship="label"`); the layout props keep `md`. `breakpointQuery('below', 'lg')` returns the query a hide class uses (`not all and (min-width: 64rem)`), for an app whose script must follow the same width. An unknown breakpoint throws a `RangeError`. Hidden regions leave the accessibility tree; do not use this to protect data. `Box as="a"` is a layout link. Use `Button` for press actions. `Box as="a" link="quiet"` (opt-in; `link="accent"`, the default, is the canvas link) is a title that opens something, such as a table row's title: it keeps the surrounding text's colour at weight 600 (`sw-link-quiet`) and is underlined on hover and keyboard focus. Its weight is the non-colour cue WCAG 1.4.1 asks for (axe's `link-in-text-block` passes it on colour, the same colour and background as its text, or on its weight; the weight is what keeps it visibly a link), so use it in text lighter than 600. In text already at 600 the quiet look has no cue: an accent link there needs at least 3:1 contrast against the text. `link` on a box that is not `as="a"` throws a `TypeError`, an unknown value a `RangeError`. `Field` associates a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Steps` is a one-page vertical stepper: an `ol` of `Step` panels in order, numbered by their place, a line joining each marker to the next (in the success color below a done step). A `Step` takes `status` (`'upcoming' | 'current' | 'done' | 'error' | 'skipped'`) and `title` (a heading at the list's `headingLevel`, 2 by default, or the step's own). The current step is open with its `children` and has `aria-current="step"`; an error step stays open with a "!" marker. A done step folds to one line: a check, the title, its `summary` (truncated) and, with `onEdit` and `editLabel`, a ghost small `Button` at the end named by its label and the title ("Edit The site"). A step ahead is greyed with its number in a ring; a skipped step is greyed with its `skippedReason` after the title (pass your own separator, such as "· not needed"). A folded step's heading ends in its status word for assistive technology (visually hidden; English by default, `labels` on `Steps` takes your words): its name is one visually hidden line, the title, a comma and the word ("The site, Done"), and the drawn title is `aria-hidden`, because a browser lays a hidden box out as a block and would put a space before the comma ("The site , Done"). The step's region and its Edit button are still named by the drawn title. A current step, and a skipped step showing its reason, draw and read their words as they are. The heading's text content now holds the title twice (drawn, then in the spoken line), so a test reading a folded heading's `textContent` or finding its title with a pattern (`getByText(/The site/)`) finds both: query headings by role and name (`getByRole('heading', { name: 'The site, Done' })`). Copying a folded heading, or finding its title in the page, meets the words twice too. The comma after the title is Scalewing's, as before; the word is yours. When a step becomes current from a folded state (upcoming, done or skipped), focus moves to its heading, a programmatic focus target; if several do in one update, the first in the list takes it. A first render, a move between current and error, and a step that turns to error leave focus alone: after a refused submit, move focus to the first invalid field yourself, as with `Field`. An error step has no `aria-current`; its heading's status word says it needs attention. Scalewing holds no order rules: the consumer sets each step's status. `Steps open` (off by default) keeps every step open at once, for a form whose sections are all filled on one page: each step shows its `children` whatever its status, inside a `section` named by its title (a region, class `sw-steps-region`), every title at the open size; the marker still follows the status (a number to fill, a check when done, "!" in error, a grey ring and muted title when greyed, such as after a blocking step), no step is `aria-current`, and no step takes focus when its status changes. `titleDetail` (opt-in) is muted words after an open step's title, on its line, such as "Optional" on a step a person may skip, or "Wetlands done · Woodlands empty" on a step with a part per habitat: the label size at the body's weight in the muted colour, a space-2 after the title on its baseline, or on a line of their own under the title where they do not fit beside it (a long title on a phone), never past the panel (a long word breaks). They are drawn while the step is open (current or error, or every step of an open list, with fields or without) and stay as the step turns done in an open list; a folded step draws its summary or reason instead. They sit outside the heading, so the heading's name stays the title (and its status word) and a heading list reads the titles alone; an open list's region keeps the title as its name and is described by the words, read as a person enters it, and in a stepped list they follow the heading in reading order. Write your own separators inside the words; Scalewing draws only the gap after the title. The words share a skipped step's reason's muted colour, not its size: a reason is part of the title, these are a note beside it. Give a step its `titleDetail` from its first render: setting or clearing it later moves the heading into or out of the title line, which remounts it (and drops its focus if it has it). A blank `titleDetail` throws a `RangeError`. Pass the `Step`s as direct children (a Fragment around several would share one number). A `Step` outside `Steps`, a Fragment child, a blank title or reason, an unknown status or heading level, `onEdit` without `editLabel`, or a blank status word throws a `RangeError`.
82
93
 
@@ -110,7 +121,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
110
121
  </Field>
111
122
  ```
112
123
 
113
- Every press control and link takes an explicit `tabindex="0"`: `Button` (and the controls built on it, `CalendarButton` and `InfoTip`), `Box as="a"` with an `href`, `SectionNav` links, the `Select`, `MultiSelect` and `ActionMenu` triggers, `SegmentedControl` items (every item, as outside Safari: its arrow keys change the value, not the focus, so it is not a roving group), `DateField`'s calendar button and month steps, a search `Field`'s clear button, a `Table`'s sort and `TableCell onPress` buttons, `Checkbox`, `Switch`, and the checked radio of a `RadioGroup` or `FilterChips` (the first enabled one while none is; the others keep no `tabindex`, so each browser's arrow-key group rule stands). Safari's default (its "Press Tab to highlight each item" setting off) leaves a button, a link, a checkbox and a radio out of the Tab order unless it carries one, so a Safari user would Tab past them; the explicit `0` gives them the order every other browser already does. A `tabIndex` you pass wins, and controls that are deliberately no tab stop keep their `-1`: a `Stepper`'s − and +, and a roving item (a `Tabs` tab, a menu item, a calendar day) other than the current one. A `<summary>` is already in Safari's order and takes none.
124
+ Every press control and link takes an explicit `tabindex="0"`: `Button` (and the controls built on it, `CalendarButton` and `InfoTip`), `CommandPaletteTrigger`, `Box as="a"` with an `href`, `SectionNav` links, a link or button `FixtureRow`, `AppShell` links and its menu and close buttons, the `Select`, `MultiSelect`, `ActionMenu` and `ContextSwitcher` triggers, `CommandPaletteTrigger` (and a `ContextSwitcher`'s listbox while it holds focus), `SegmentedControl` items (every item, as outside Safari: its arrow keys change the value, not the focus, so it is not a roving group), `DateField`'s calendar button and month steps, a search `Field`'s clear button, a `Table`'s sort and `TableCell onPress` buttons, `Checkbox`, `Switch`, and the checked radio of a `RadioGroup` or `FilterChips` (the first enabled one while none is; the others keep no `tabindex`, so each browser's arrow-key group rule stands). Safari's default (its "Press Tab to highlight each item" setting off) leaves a button, a link, a checkbox and a radio out of the Tab order unless it carries one, so a Safari user would Tab past them; the explicit `0` gives them the order every other browser already does. A `tabIndex` you pass wins, and controls that are deliberately no tab stop keep their `-1`: a `Stepper`'s − and +, and a roving item (a `Tabs` tab, a menu item, a calendar day) other than the current one. A `<summary>` is already in Safari's order and takes none.
114
125
 
115
126
  An element a script focuses, such as a notice or a card announced after a save, takes the same accent focus ring as the canvas's links and native controls, 2 px past its edge (`--sw-focus-ring-width` and `--sw-focus-ring-offset`), instead of the browser's outline. Give it `tabIndex={-1}`, so it takes focus from a script but is no tab stop, and put the focus on the whole card rather than its text, so the ring goes round it clear of the words; a `Box` round the card with the card's `radius` makes the ring follow its corners. As with the browser's own outline, the ring shows only when focus is visible (`:focus-visible`): after a keyboard press, not after a mouse press. The rule has zero specificity, so a component's own ring always wins.
116
127
 
@@ -300,6 +311,15 @@ On the web, `variant="neutral"` is a quiet grey fill (the muted color at 16 %, t
300
311
 
301
312
  `Button` `width` is `'content'` (the default: as wide as its words) or `'full'`: the button fills its container's inline size in a `Stack`, a `Grid` cell or any column, and in an `Inline` row takes the space its siblings leave, as a full `Select` does, so an Edit beside a ••• `ActionMenu` fills the row and the menu keeps its own square at the end. The row rule holds for a `Button` that is a direct child of the `Inline`; wrapped in another box it fills that box instead. In a wrapping `Inline` (`wrap`) a full button grows from its words' width and moves to a line of its own when the row runs out, rather than shrinking past its words. Inside a `ButtonGroup` it is not needed: the group already stacks its buttons full width below `md`. The words stay centred. An unknown width throws a `RangeError`. The class is `sw-button-full`.
302
313
 
314
+ `ButtonLink` is a link drawn as a `Button`, for a destination that reads as an action, such as a site's Contact us or Sign in: an `<a href>` with the Button's `variant` (default `primary`), `size` (default `md`) and `width`, so it opens like any link (a new tab, a page rendered with no script) and keeps its variant's words over the canvas link colour, hover included. `aria-current="page"` marks the one that is the current page with the pressed button's ring (a Highlight border in forced colours). A link has no disabled state: render none instead. Use `Button` for a press that does something on the page. An unknown variant, size or width throws a `RangeError`; the class is `sw-button-link` beside the Button's own.
315
+
316
+ ```tsx
317
+ <ButtonLink href="/contact">Contact the rangers</ButtonLink>
318
+ <ButtonLink href="/sign-in" variant="neutral">
319
+ Sign in
320
+ </ButtonLink>
321
+ ```
322
+
303
323
  ```tsx
304
324
  <Inline gap={2}>
305
325
  <Button variant="neutral" width="full" onPress={edit}>
@@ -332,6 +352,39 @@ A `Button` that toggles something passes `aria-pressed`. The pressed button gets
332
352
 
333
353
  `ActionMenu` `triggerAppearance="text"` (opt-in; default `"framed"`, `actionMenuTriggerAppearances`) draws the trigger as its words (`trigger`, or `label`) in label type at medium weight in the text color, with the shared muted chevron pointing down after them and no fill or outline; the quiet grey fill shows on hover and while the menu is open. It is the sm control height (the 44 px target on a coarse pointer). Name the trigger with `label` as before, and include the words shown in it (WCAG 2.5.3, so voice control can say them): a person's name that opens their account menu takes a `label` such as "Account, Ada Okafor". The trigger's words are one flex item (mixed inline content keeps its spaces), then the chevron. Forced colors draw its border in the system's border color, the chevron in CanvasText, and an open trigger's border in Highlight. An unknown value throws a `RangeError`.
334
354
 
355
+ `ContextSwitcher` is a button showing the context in use, such as a workspace, a competition or a season, that opens a popup to switch it. `label` says what is switched ("Season"); `trigger` is what the button shows, usually the current context (words, or a mark and words on two lines); the button is named by both ("Season Fall 2026", the visible words in the name). `groups` is a list of `{ id, label?, options }`, each option `{ id, label, detail?, icon?, badge?: { label, tone }, count?, countLabel?, countTone? }`: a decorative mark, the label over a muted detail, a status `Badge`, and the shared count pill (`NavCount`'s tones), all read in the option's name, a `countLabel` in place of the number; a `count` of 0 shows no pill, as `NavCount`'s. `currentId` is the option in use, checked and `aria-selected`. The popup is a non-modal dialog named by `label` on the top layer, placed as `ActionMenu`'s list is (`align` `"start"` or `"end"`). Focus moves into it: to the `filter` field when there is one (`{ label, placeholder?, emptyText, minOptions? }`, a combobox that narrows the options by label or detail, case and accents ignored, shows and announces `emptyText` when nothing is left, and appears only from `minOptions` options, 0 by default), else to the listbox. ↓ and ↑ move the active row and Enter chooses it; in the listbox Home and End go to the ends, letters jump by typeahead and Space chooses. Choosing closes the popup, puts focus back on the trigger, then calls `onSelect(option)`; the current option only closes it. `actions` (`{ id, label, icon?, onSelect }`) are commands in a footer under a hairline, reached with Tab, that close the popup and return focus to the trigger before they run. Escape closes it with focus on the trigger (and stops there, so an `AppShell` drawer round it stays open); a press outside or Tab out of it (or Shift+Tab back to the trigger) closes it where focus went. Focus leaving the window (another app, a browser tab) leaves it open until the next press. A press on the popup's caption, padding or a row keeps focus in the field or the listbox, so the keys keep working; Enter while an input method composes is the method's. The popup is at least 15rem and never narrower than the trigger, capped by the viewport, and follows the trigger's width while open. `triggerAppearance` is `"framed"` (default, the outlined control), `"text"` (words and a chevron, no fill until hovered or open), `"pill"` (a rounded outlined pill at label weight) or `"icon"` (a glyph alone in a round quiet button, no chevron, named by `label` and the current option's label, for a rail); `width="full"` fills the column, such as a sidebar's top. The trigger, options and commands are 44 px on a coarse pointer; forced colours draw the active row in `Highlight`. Keep state shared by several copies (an `AppShell` header and its rail, for instance) in your own component. Blank words, empty or duplicate groups, options or actions, a bad count, badge or tone, a `currentId` that names no option, a trigger that draws nothing, and an unknown `triggerAppearance`, `width` or `align` throw a `RangeError`.
356
+
357
+ ```tsx
358
+ <ContextSwitcher
359
+ label="Season"
360
+ trigger={season.label}
361
+ triggerAppearance="pill"
362
+ groups={[
363
+ {
364
+ id: 'seasons',
365
+ options: [
366
+ {
367
+ id: 'spring-27',
368
+ label: 'Spring 2027',
369
+ badge: { label: 'Draft', tone: 'neutral' },
370
+ },
371
+ {
372
+ id: 'autumn-26',
373
+ label: 'Autumn 2026',
374
+ detail: 'Current',
375
+ badge: { label: 'Live', tone: 'success' },
376
+ },
377
+ ],
378
+ },
379
+ ]}
380
+ currentId={season.id}
381
+ onSelect={(option) => setSeason(option)}
382
+ actions={[
383
+ { id: 'new', label: 'Start a new season', onSelect: openNewSeason },
384
+ ]}
385
+ />
386
+ ```
387
+
335
388
  `triggerAppearance="icon"` (opt-in) draws the `trigger` glyph alone in a round quiet button, as wide as it is tall (the sm control height, 44 px on a coarse pointer), in the muted colour with no fill, outline or chevron; it turns to the text colour on the quiet fill on hover and while the menu is open. Use it for a row's own ••• menu, named by `label` ("More for Heron calls"); the glyph says nothing on its own. An icon trigger without a `trigger` glyph throws a `RangeError`. Forced colors draw it in ButtonText with its kept border, Highlight while open.
336
389
 
337
390
  ```tsx
@@ -517,7 +570,7 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
517
570
 
518
571
  `appearance="words"` (opt-in; default `"default"`, `navAppearances`) draws every destination, a link or a `Button` of any variant, as plain words: no fill, border or underline, label type, the others in the muted color at medium weight and the current one (`aria-current`, not `"false"`) in the text color at label weight. A Button keeps its size, padding, touch target and focus ring; hover lifts a destination to the text color where the pointer can hover, never a disabled one. The current mark is color and weight, so each destination's words must be visible: a glyph-only destination has no weight to change (keep `default` for a glyph rail). Forced colors draw the current one's border in Highlight and underline its words. An unknown value throws a `RangeError`.
519
572
 
520
- `NavCount` puts a count after a destination's words, such as the tasks waiting in an area: place it inside the link or `Button`, after its words (`<a href="/tasks">Tasks<NavCount count={8} /></a>`). It is the pill `SectionNav`'s item `count` draws (the danger color with its on-color figures, an edge in forced colors), a space-2 after the words in a link or a Button, the link keeping its siblings' height, and it is read as part of the destination's name ("Tasks 8"). `count` is a whole number; 0 shows nothing. `countLabel` says what it means in your words ("8 tasks to do"): it is read in place of the bare number ("Tasks 8 tasks to do") and the pill is hidden from assistive technology. `tone="accent"` (default `"danger"`, `navCountTones`) draws the pill in the accent color with its on-color figures, for a count that is news rather than work waiting, such as what was added since the person last looked; its size, spacing and name are the same. Give an accent count a `countLabel` ("3 new") so its words, not its color, say it is news, and place either tone on a neutral or ghost destination, never a filled one of the same color. A count that is not a whole number from 0, a blank `countLabel` or an unknown `tone` throws a `RangeError`.
573
+ `NavCount` puts a count after a destination's words, such as the tasks waiting in an area: place it inside the link or `Button`, after its words (`<a href="/tasks">Tasks<NavCount count={8} /></a>`). It is the pill `SectionNav`'s item `count` draws (the danger color with its on-color figures, an edge in forced colors), a space-2 after the words in a link or a Button, the link keeping its siblings' height, and it is read as part of the destination's name ("Tasks 8"). `count` is a whole number; 0 shows nothing. `countLabel` says what it means in your words ("8 tasks to do"): it is read in place of the bare number ("Tasks 8 tasks to do") and the pill is hidden from assistive technology. `tone="accent"` (default `"danger"`, `navCountTones`) draws the pill in the accent color with its on-color figures, for a count that is news rather than work waiting, such as what was added since the person last looked; its size, spacing and name are the same. `tone="warning"` draws it in amber, for work that needs the person but is not urgent ("needs you"): the warning tint with the warning ink (a warning `Notice`'s, 4.5:1 in every palette) inside a warning edge. Give an accent or warning count a `countLabel` ("3 new", "2 to confirm") so its words, not its color, say what it is, and place either tone on a neutral or ghost destination, never a filled one of the same color. A count that is not a whole number from 0, a blank `countLabel` or an unknown `tone` throws a `RangeError`.
521
574
 
522
575
  ```tsx
523
576
  <Nav appearance="words" aria-label="Areas">
@@ -549,6 +602,66 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
549
602
  />
550
603
  ```
551
604
 
605
+ `AppShell` is a web workspace's frame: a pinned side navigation of grouped destinations beside the page, which it renders in `main` and which scrolls with the document (`main` is never its own scroll box). `groups` is a list of `{ id, label?, items }`; each item is `{ id, label, href, icon, count?, countLabel?, countTone?, current? }` with `SectionNav`'s rules for `current`, `count` and `countLabel` (the same shared count pill, read in the link's name), `countTone` one of `NavCount`'s tones (`danger` by default, `warning` for amber, `accent`; it needs a `count`), and `icon`, the consumer's decorative glyph, is required because the rail shows nothing else. `navLabel` names the navigation (and the drawer). `onNavigate` takes a plain press for a client router, as `SectionNav`'s does: the item, after the browser's navigation is prevented; a modifier or middle press keeps the browser's behaviour.
606
+
607
+ The layout follows the window, with generated media queries only. From 75rem (1200 px, an internal width of the component, not a public breakpoint) the sidebar is 15rem on the solid surface with a hairline at its end, sticky at the top at the viewport's height: `header` (a workspace switcher such as an `ActionMenu`), the groups (a group's `label` is a small muted caption naming its list), `footer` (the account menu) under a hairline; only the nav scrolls, and only when it is taller than the window. Each destination shows its glyph, label and count; the current one takes `SectionNav filled`'s quiet fill at the label weight; a coarse pointer gets 44 px rows. From `lg` (64rem) to 75rem it is a rail of glyphs, the 44 px target wide: labels are visually hidden but stay in each link's name and show in a `Tooltip` on hover or keyboard focus, a count is the pill on the glyph's corner, group captions become hairlines, and `railHeader`/`railFooter` replace `header`/`footer` (none is drawn without them). Below `lg` there is no sidebar: a sticky `AppHeader appearance="bar"` holds an icon-only ghost menu button (`menuIcon`, named `labels.openMenu`), `barTitle` and `barEnd`. The menu opens a modal drawer on the top layer from the inline start (the end in right-to-left), sliding in unless reduced motion is asked for, with a close button named `labels.closeMenu` where the menu button was, then `header`, the nav and `footer`. Escape, a press outside, the close button or a plain press on a destination close it; after the first three focus returns to the menu button, while a chosen destination leaves focus to the page it moves to. A window widened past `lg` closes it too. `menuCount` (a whole number) puts the shared count pill on the menu button's top end corner, read in its name after `labels.openMenu` and a comma ("Open menu, 6 need you", with `menuCountLabel` in place of the bare number; the pill is hidden from assistive technology); 0 shows none and leaves the plain name, `menuCountLabel` and `menuCountTone` then having no effect; `menuCountTone` takes `NavCount`'s tones. The pill shows the number as given, with no cap such as 99+: cap it in your own words if a count can grow that large. `pageBar` is a band at the top of `main` on every layout, such as a page's context pickers and its search: the solid surface with a hairline under it, padded space-2 by the page's inline padding (space-4 below `lg`, space-6 from it; pad your page to match), its children a wrapping row with space between them. It is sticky: from `lg` at the window's top, below `lg` right under the top bar, whose height the shell measures (it falls back to the bar's usual height before that). `header` and `railHeader` may hold an interactive trigger, such as an `ActionMenu` or a switcher drawn as its glyph alone in the rail: its popup opens on the top layer, so neither the sidebar nor the drawer clips it. `header` and `footer` render in the sidebar and again in the open drawer (and `railHeader`/`railFooter` beside them in the hidden sidebar), so a slot's content can be mounted twice at once with separate state: give it no fixed DOM `id` (use `useId`), and keep state that must survive opening the menu, such as a selection, in your own component. Forced colours draw the links as `LinkText`, the current one with a `Highlight` edge. A blank `navLabel` or menu label, no groups, an empty or duplicate group id, a blank group label, an empty group, an empty or duplicate item id (across groups), an empty label, a missing icon (or one that draws nothing: a boolean, `''` or `0`), a `count` that is not a whole number from 0, a blank `countLabel` or one without a `count`, an unknown `countTone` or one without a `count`, a `menuCount` that is not a whole number from 0, a blank `menuCountLabel`, an unknown `menuCountTone`, and a `menuCountLabel` or `menuCountTone` without a `menuCount` throw a `RangeError`.
608
+
609
+ ```tsx
610
+ <AppShell
611
+ navLabel="Field station"
612
+ groups={[
613
+ {
614
+ id: 'field',
615
+ items: [
616
+ {
617
+ id: 'today',
618
+ label: 'Today',
619
+ href: '/today',
620
+ icon: <CalendarDays />,
621
+ current: 'page',
622
+ },
623
+ ],
624
+ },
625
+ {
626
+ id: 'reserves',
627
+ label: 'Reserves',
628
+ items: [
629
+ {
630
+ id: 'wetlands',
631
+ label: 'Wetlands',
632
+ href: '/wetlands',
633
+ icon: <Waves />,
634
+ count: 3,
635
+ countLabel: '3 sightings to check',
636
+ },
637
+ {
638
+ id: 'forests',
639
+ label: 'Forests',
640
+ href: '/forests',
641
+ icon: <Trees />,
642
+ count: 2,
643
+ countLabel: '2 to confirm',
644
+ countTone: 'warning',
645
+ },
646
+ ],
647
+ },
648
+ ]}
649
+ onNavigate={(item) => navigate(item.href)}
650
+ header={<StationMenu />}
651
+ railHeader={<StationTile />}
652
+ footer={<AccountMenu />}
653
+ pageBar={<CensusBar />}
654
+ barTitle={<Text variant="label">Heron Marsh</Text>}
655
+ menuIcon={<Menu />}
656
+ menuCount={5}
657
+ menuCountLabel="5 need you"
658
+ menuCountTone="warning"
659
+ labels={{ openMenu: 'Open menu', closeMenu: 'Close menu' }}
660
+ >
661
+ <Box padding={5}>{page}</Box>
662
+ </AppShell>
663
+ ```
664
+
552
665
  ```tsx
553
666
  <Stack gap={3}>
554
667
  <Box paddingX={4}>
@@ -720,7 +833,14 @@ The consumer needs no PDF.js dependency of its own. With another bundler, build
720
833
 
721
834
  `Spinner` shows indeterminate loading in small, medium, or large sizes. Supply localized `label` for the one announced status in a loading region. Use `decorative` on additional indicators beside that status so screen readers do not hear the same message repeatedly. Reduced motion leaves a static accented ring.
722
835
 
723
- `GlyphTile` is a small rounded square in a categorical colour with the consumer's glyph in white: the mark of a kind beside its name, such as a service in a list. `hue` is one of the `@scalewing/tokens` categorical colours (`green`, `blue`, `orange`, `purple`, `teal`, `rose`, or `neutral`); which kind takes which hue is yours. `size` is `sm` (24), `md` (28, the default) or `lg` (32); the glyph is drawn at spacing step 4 in white, which keeps 3:1 on every fill; pass the SVG (a Lucide component) as the tile's direct child, since only a direct-child SVG is sized. It is always `aria-hidden`, so put the kind's name in visible text beside it. An unknown `hue` or `size` throws a `RangeError`.
836
+ `Skeleton` is grey placeholder bars where content is loading, in place of a spinner for a card or a list that is coming: a polite status named by `label` ("Loading this week's games"), read once, its bars hidden from assistive technology. `variant="lines"` (default, `skeletonVariants`) draws `lines` bars of text (1 to 6, 3 by default) with ragged ends, the last of several shorter; `variant="rows"` draws `lines` list rows, a mark and a bar at the 44 px row height with hairlines between. The bars are the quiet grey fill and pulse gently, still under reduced motion; forced colours draw their edges. Put one Skeleton where each loading block will be, at its width. Each Skeleton announces its label once as it appears, so several at once are several announcements: keep one named status per loading region (other placeholders in that region can wait for it), or accept that each is read. Removing a Skeleton announces nothing, so say "loaded" another way if it matters (a heading the content brings, or a status of your own). Blank words, `lines` outside 1–6 or an unknown `variant` throw a `RangeError`.
837
+
838
+ ```tsx
839
+ <Skeleton label="Loading this week's sightings" />
840
+ <Skeleton label="Loading rangers" variant="rows" lines={4} />
841
+ ```
842
+
843
+ `GlyphTile` is a small rounded square in a categorical colour with the consumer's glyph in white: the mark of a kind beside its name, such as a service in a list. `hue` is one of the `@scalewing/tokens` categorical colours (`green`, `blue`, `orange`, `purple`, `teal`, `rose`, or `neutral`); which kind takes which hue is yours. `size` is `sm` (24), `md` (28, the default) or `lg` (32); the glyph is drawn at spacing step 4 in white, which keeps 3:1 on every fill; pass the SVG (a Lucide component) as the tile's direct child, since only a direct-child SVG is sized. It is always `aria-hidden`, so put the kind's name in visible text beside it. An unknown `hue` or `size` throws a `RangeError`. It may hold up to three letters in place of the glyph, such as initials (`<GlyphTile hue="neutral" size="sm">NSL</GlyphTile>`): words given as its children are drawn in one line at weight 600 and sized from the tile, not the text round it (one letter at half its side, two at 0.42, three at a third), so they fit every size; blank words or more than three letters throw a `RangeError`.
724
844
 
725
845
  ```tsx
726
846
  <Inline gap={2} align="center">
@@ -733,6 +853,31 @@ The consumer needs no PDF.js dependency of its own. With another bundler, build
733
853
 
734
854
  `Combobox` is a text input with a list of suggestions, an ARIA 1.2 combobox with list autocomplete. Pass the `label` (drawn above the input unless `labelVisuallyHidden`), the controlled `inputValue` and `onInputChange`, and `groups` you filter from the text yourself: `{ label?, options }`, each option `{ value, label, description?, badge?: { label, tone? }, disabled? }`. Typing, a press on the input or ArrowDown opens the list on the popover layer under the input, at least as wide; each group with a `label` is a `role="group"` under its heading. ArrowDown and ArrowUp move the active option (named by the input's `aria-activedescendant`; focus stays in the input) and wrap; Enter or a press chooses it and calls `onSelect(value)` (set `inputValue` to its label yourself), and a disabled option is shown and read as unavailable but never chosen. `action` (`{ label, onPress, badge? }`) ends the list as one more option, such as adding a new entry. `emptyText` shows when no group has an option, and a polite live region, always in the page, announces it (and, with `countText: (count) => string`, the number of options); `note` is a muted line under the list that describes the input while open. `listLabel` names the list when it differs from the input's label. The active option is held by its value, so it stays put when your results change under it. Escape closes the list and a second Escape empties the input; Alt+ArrowUp, Tab, a blur or a press outside closes it. The input is a `Field` input, so `description`, `error`, `required`, `disabled`, `placeholder` and `size` (`md` or `xs`; `comboboxSizes`) work as on any field. Blank words, duplicate option values, an unknown `size` or badge tone throw a `RangeError`, and an `action` without `onPress` a `TypeError`.
735
855
 
856
+ `CommandPalette` is a search over the product on the top layer: a modal dialog named by `label`, near the window's top, holding a field (an ARIA combobox, `placeholder`) over an always-open listbox of grouped results. Keep `query` and `onQueryChange` yourself and filter `groups` from the words: `{ id, label, items }`, each item `{ id, label, detail?, icon?, onSelect }`; a group with no items is not drawn. The field takes focus as it opens, its words selected. ↓ and ↑ move the active row, wrapping (`aria-selected`, the quiet fill and the Enter key cap at its end); Enter or a press chooses it: it calls `onOpenChange(false)`, and once the palette has closed the item's `onSelect` runs, so it can move focus to the page. Each opening starts on the first row. With no items the field is `aria-expanded="false"`; the status starts empty as the palette opens and is filled just after, so its first words are announced. Escape, a press outside or the shortcut again call `onOpenChange(false)`, and focus returns to where it was before the palette opened (unless a chosen item moved it). A polite status says `loadingText` while `loading`, `emptyText` when nothing is left, else `countText(n)`; the first two are also drawn in place of the results. `hint` is a muted footer line ("↑↓ to move · Enter to open · Esc to close"). `shortcut="mod+k"` listens on the document for ⌘K on Apple systems and Ctrl+K elsewhere (the key in K's place too, on a layout that types another letter there) and calls `onOpenChange(!open)`; it is left alone while focus is in another text field, and works from the palette's own field. Give it to one palette per page: two listening palettes would both open. Rows are 44 px on a coarse pointer; forced colours draw the active row in `Highlight`. `CommandPaletteTrigger` is the button that opens it: a search pill with the consumer's `icon`, the `placeholder` words and a `shortcutLabel` key cap ("⌘K" or "Ctrl K", hidden from assistive technology), named by `label` (start `placeholder` with its words), or with `compact` the glyph alone in a round quiet button; the sm height, 44 px on a coarse pointer. Blank words, a group without a caption, empty or duplicate ids, `loading` without `loadingText`, an unknown `shortcut` or a trigger glyph that draws nothing throw a `RangeError`.
857
+
858
+ ```tsx
859
+ <CommandPaletteTrigger
860
+ label="Search"
861
+ placeholder="Search species and sites"
862
+ shortcutLabel="⌘K"
863
+ icon={<Search />}
864
+ onPress={() => setOpen(true)}
865
+ />
866
+ <CommandPalette
867
+ open={open}
868
+ onOpenChange={setOpen}
869
+ label="Search"
870
+ placeholder="Search species and sites"
871
+ query={query}
872
+ onQueryChange={setQuery}
873
+ groups={resultsFor(query)}
874
+ emptyText="No species or sites match"
875
+ countText={(n) => `${n} results`}
876
+ hint="↑↓ to move · Enter to open · Esc to close"
877
+ shortcut="mod+k"
878
+ />
879
+ ```
880
+
736
881
  `MultiSelect` is several choices from a short list behind one trigger, such as which kinds a list shows. The trigger reads the muted `label` and then `summary` (your words for what is chosen, such as "All entries" or "2 services") with a chevron; it opens a panel named by the label with `allLabel` and `clearLabel` commands over one checkbox per option, each with an optional `icon` (use a small `GlyphTile`, never a bare dot) and a hairline before an option with `separatorBefore`. `onChange` reports the checked values in the options' order. It is a disclosure of native checkboxes, not a listbox: the first checkbox takes focus as it opens, Escape closes it and returns focus to the trigger, focus or a press outside closes it, and disabling it while open closes it. Blank words, no or duplicate options, a value that is not an option, or an unknown `align` throw a `RangeError`.
737
882
 
738
883
  ```tsx
@@ -839,6 +984,62 @@ const { headings, omissions } = inspectMarkdown(page.body, {
839
984
 
840
985
  `Separator` divides sections with a token-colored line. It is horizontal by default; use `orientation="vertical"` in a flex row. Set `decorative` when the line only supports layout so assistive technology ignores it.
841
986
 
987
+ `Band` is a full-width stretch of the page in one fill, its content in a centred column with the page gutter (16 px, 24 px from `md`): a site's footer, one dark closing section, a grey strip between white ones. `tone` is `surface`, `subtle` or `inverse`, the dark band (`--sw-color-inverse`, which a brand moves with a theme overlay such as `colors: { inverse: '#0B2038' }`). Inside the inverse band the theme's words, muted words, hairlines, subtle fill, accent and `onAccent` read on the dark fill, so `Text`, a filled `Card`, a primary `Button` and the focus ring need no band-aware variant; every plain link, a quiet one included, takes the band's words with a half-strength underline. The `surface` band is the cards' fill (white in light), so it stands apart from a grouped or dark page. `width` is `container` (default) or `reading`; `as` is `section` (default), `div`, `aside` or `footer`; `paddingY` (default 8), `paddingTop` and `paddingBottom` take spacing steps. Name a `section` band with `aria-labelledby` on its heading. An unknown tone, width or element throws a `RangeError`.
988
+
989
+ ```tsx
990
+ <Band tone="inverse" aria-labelledby="closing-title">
991
+ <Text id="closing-title" variant="title">
992
+ Count with us
993
+ </Text>
994
+ </Band>
995
+ ```
996
+
997
+ `SiteHeader` is a public site's header. Supply `brand` (such as a logo linked to the home page), `navLabel` and `items` (`{ id, label, href, current? }`, `current` as SectionNav's: `page`, `location` or `true` sets `aria-current`), `labels` (`openMenu`, `closeMenu`) and the decorative `menuIcon` and `closeIcon` glyphs. From `lg` the bar shows the brand, the destinations as a row of links (the current one at label weight over a bar in the words' colour, a thick underline in forced colours) and `actions` at its end, such as a language switch, Sign in (`Box as="a" link="quiet"`) and a `ButtonLink`. Below `lg` it shows the brand, `compactActions` and a menu button that opens, under the bar over the rest of the window, the destinations as large rows (the current one in the accent) and `menuActions` at the panel's foot, such as full-width `ButtonLink`s. The menu is a native `details`, so it opens and closes by touch, click and keyboard with no script, as a page rendered on the server needs; its `summary` is the button, named `openMenu` or `closeMenu` by its state. Script adds Escape (focus back on the button), and closes it when a link in it is chosen, focus leaves it, a press lands outside it, or the window widens to `lg`; nothing traps focus. A page rendered on the server has the no-script menu until it hydrates; a site that never hydrates keeps it, which still opens and closes. The bar is 56 px tall below `lg` and 72 px from it, on the solid surface with a hairline under it; `sticky` (default `true`) keeps it at the window's top. No items, blank or repeated ids, blank labels or hrefs, or a blank `navLabel` or menu label throw a `RangeError`.
998
+
999
+ ```tsx
1000
+ <SiteHeader
1001
+ brand={<a href="/">Wetland Trust</a>}
1002
+ navLabel="Main"
1003
+ items={[{ id: 'marsh', label: 'Marsh', href: '/marsh', current: 'page' }]}
1004
+ actions={<ButtonLink href="/contact">Contact the rangers</ButtonLink>}
1005
+ compactActions={
1006
+ <a href="/es" lang="es" hrefLang="es">
1007
+ Español
1008
+ </a>
1009
+ }
1010
+ menuActions={
1011
+ <ButtonLink href="/contact" width="full">
1012
+ Contact the rangers
1013
+ </ButtonLink>
1014
+ }
1015
+ labels={{ openMenu: 'Open the menu', closeMenu: 'Close the menu' }}
1016
+ menuIcon={<MenuIcon />}
1017
+ closeIcon={<CloseIcon />}
1018
+ />
1019
+ ```
1020
+
1021
+ `SiteFooter` is a public site's footer on the dark inverse band (`Band tone="inverse"`, so a brand moves it with a theme overlay of `inverse`). Supply `brand` (such as a logo for a dark background), an optional `tagline`, and `groups`: each `{ id, label, links }` is a `nav` named by its label, which shows above its links (hidden from assistive technology, since the `nav` already carries it). Each link is `{ id, label, href, current?, lang?, hrefLang? }`; `lang` and `hrefLang` pass through to the link unchecked; `current` sets `aria-current="page"` and draws it in the band's words at label weight, the others in its muted words, underlined on hover and focus. `legal` and `end` fill the bottom row past a hairline, such as the copyright and a language switch. Below `md` the brand takes the full width and the groups two columns; from `md` the brand and the groups share one spread row. Links get the 44 px target on a coarse pointer. Put it last in the page's `body` (outside a `section` or `article`) so it is the page's `contentinfo` landmark. No groups, a group with no links, blank or repeated ids, blank labels or a blank `href` throw a `RangeError`.
1022
+
1023
+ ```tsx
1024
+ <SiteFooter
1025
+ brand={<img alt="Wetland Trust" src="/logo-white.svg" />}
1026
+ tagline="Counting birds since 1990."
1027
+ groups={[
1028
+ {
1029
+ id: 'visit',
1030
+ label: 'Visit',
1031
+ links: [{ id: 'marsh', label: 'North marsh', href: '/marsh' }],
1032
+ },
1033
+ ]}
1034
+ legal="© Wetland Trust"
1035
+ end={
1036
+ <a href="/es" lang="es" hrefLang="es">
1037
+ Español
1038
+ </a>
1039
+ }
1040
+ />
1041
+ ```
1042
+
842
1043
  `DescriptionList` pairs terms with what each says, one `DescriptionItem` per row: a `dl` whose items each group a `dt` (`term`) with its `dd` (`children`). From `md` up the term sits in a start column as wide as the widest term, up to 40% of the list, and the detail beside it; every row aligns to its top, so a term and its detail's first line start level, and a hairline divides the rows, with no space above the first row or below the last. Below `md` each term sits over its detail. The list sets only the layout: pass your own `Text` for the term and the detail. Each child of a term or a detail is its own line with its own line height (a column), as wide as its content unless it sets its own width (a `Progress` fills the detail; a `Field` keeps its input's width): wrap inline content, such as words and a link on one line, in one element such as an `Inline`. Long unbreakable words wrap inside their column. A `dl` has no ARIA role, so name a list with a visible heading, and `aria-labelledby` on the `section` around it, rather than an `aria-label` on the list.
843
1044
 
844
1045
  `layout="spread"` (opt-in; the default is `columns`) sets each row on its own two columns at every width, for a short box of facts such as a side panel's details: the term at the row's start, as wide as its words up to half the row, and the detail at the row's end, its lines end-aligned. The hairlines, padding and top alignment are the same, and a phone keeps each term beside its detail. An unknown `layout` throws a `RangeError`; `descriptionListLayouts` lists the layouts.
@@ -887,6 +1088,29 @@ const { headings, omissions } = inspectMarkdown(page.body, {
887
1088
 
888
1089
  `ListGroup` is rows in one list with a hairline between each pair, the web twin of react-native's: a `ul` (its list role kept) named by `aria-label` or `aria-labelledby`. `variant="panel"` (the default) is one bordered panel; `plain` draws no panel and a hairline above the first row too, for rows inside a `Card`. `variant="tiles"` keeps one list (its name, its list items, a screen reader's count of them) and lays its rows out as tiles: each row its own bordered panel on the surface with no hairline between them, in `tileColumns` equal columns (`2`, `3` the default, or `4`; `listTileColumnCounts`, `ListGroupTileColumns`) a spacing step 3 apart from `md` up, one a row below it, such as a strip of new arrivals under its heading. A row's press fill and inset focus ring stay inside its tile; `truncate` keeps a long title to one line in a narrow tile. Tiles take no `maxHeight` or `footer`, and `tileColumns` needs `tiles`; either throws a `RangeError`, as does a count other than 2, 3 or 4. Each `ListRow` has a `title` (weight 600, or the body weight with `titleAppearance="plain"`; `tone="danger"` for a row that ends something), an optional muted `detail` line, an optional `value` at its end (weight 600, tabular figures; `valueTone` `success`, `danger`, `muted`, `accent` or `warning`, the warning colour's ink moved halfway to the text colour so it keeps 4.5:1 on the surface in every palette; `valueBadge` draws the value as a `Badge` in that tone instead, such as a status, still read in the row's name, never with `valueTone`; `valueDetail` a muted line under it) and an optional `leading` mark such as a status dot, hidden from assistive technology, so the words say what it means. With `onPress` the whole row is a button, and with `href` a link (`onPress` then takes a plain press for a client router); either adds a chevron, a quiet fill under the pointer and 44 px on a coarse pointer, and the row is named by its title and value and described by its detail lines (`aria-label` replaces the name). `selected` makes a pressable row a choice: a check while true, `aria-pressed` on a button or `aria-current` on a link (on a row with neither `onPress` nor `href` it throws, since nothing could read the state). `accessory="none"` hides the chevron of a row that acts in place; `disabled` blocks presses. A row with neither `onPress` nor `href` only states something. `maxHeight={{ rows }}` bounds the list to that many rows of a title and one detail line, the rest scrolling inside its own named region, which shades an edge with more rows past it; a bounded list must be named. `footer` stays pinned under the rows, outside the scroll, such as a `ListRow` with the count and the total (a row there is not a list item). An empty title or value, an unknown variant, tone or badge tone, a `valueDetail`, `valueTone` or `valueBadge` without a value, a value both toned and a badge, or a bad row count throws a `RangeError`.
889
1090
 
1091
+ `FixtureRow` is one pairing of two sides, such as a game: `home` and `away` (`{ name, mark? }`, the mark a hidden crest beside the name on the centre's side), `center` (a kickoff time, `centerKind="time"`, the default, in a small grey chip, or a score, `"score"`, in bold figures; both tabular), then `meta` (muted, such as the field) and `status` (`{ label, tone }`, a small `Badge`) at the end. The home side ends at the centre and the away side starts from it, mirrored in right-to-left; names wrap. Rows live in a `FixtureGroup` (`{ title, children }`), a caption that names their list (a date and a venue): the list is one grid and each row a subgrid of it, so the centres and ends line up down the run whatever the names (a browser without subgrid, before Chrome 117 or Safari 16, draws each row on its own grid, unaligned). Below `md` the end moves under the sides; `metaHideBelow` (`md` or `lg`) leaves the meta out below it. With `href` the whole row is a link and with `onPress` alone a button (with both, `onPress` takes a plain press on the link for a client router), with a chevron, a hover fill, an inset focus ring and 44 px; `aria-label` names it in your words ("Heron Marsh with Otter Pool, 8:00"), otherwise it is named by what it shows, in order. A blank title, a row outside a group, a side without a name, a blank centre, an unknown `centerKind` or `metaHideBelow`, a bad status, a blank `href`, or an `aria-label` that is blank or on a row with neither `href` nor `onPress` throws a `RangeError`.
1092
+
1093
+ ```tsx
1094
+ <FixtureGroup title="Sunday 25 October · North meadow hides">
1095
+ <FixtureRow
1096
+ home={{ name: 'Heron Marsh', mark: <Crest /> }}
1097
+ away={{ name: 'Otter Pool', mark: <Crest /> }}
1098
+ center="8:00"
1099
+ meta="Hide 1"
1100
+ metaHideBelow="md"
1101
+ href="/counts/1"
1102
+ aria-label="Heron Marsh with Otter Pool, 8:00, Hide 1"
1103
+ />
1104
+ <FixtureRow
1105
+ home={{ name: 'Pine Ridge' }}
1106
+ away={{ name: 'Bittern Fen' }}
1107
+ center="14–9"
1108
+ centerKind="score"
1109
+ status={{ label: 'Counted', tone: 'success' }}
1110
+ />
1111
+ </FixtureGroup>
1112
+ ```
1113
+
890
1114
  A row can also carry three trailing parts. `unread` marks it new for the person, such as a document they have not opened: an accent dot after the title's last word (drawn as a border, so forced colors keep it in the text color), hidden from assistive technology, and `unreadLabel` (required while `unread`, such as "New") is read after the title, in a pressable row's name ("Heron calls New"). `tag` is a short status before the row's end, such as a `Badge` ("Only in English"), centred on the row and never wrapped; a pressable row is described by it after its detail lines. `action` is a control after the row and outside its press area, such as an `ActionMenu` of the row's own commands named by the consumer ("More for Heron calls"): it is the row's sibling in the list item, centred on the row, after it in the Tab order, and a press on it never opens the row; it keeps the row's end inset (a space-4 in a panel or outside a ListGroup, none in a plain list), and where `:has` is supported the hover fill spans the row and its control. An empty slot (`undefined`, `null`, `false` or `''`, as conditional JSX gives) draws nothing, for `leading` too. Pair `action` with `accessory="none"` when the row's own chevron would sit beside it. An unread row without `unreadLabel`, or a blank one, throws a `RangeError`. An `aria-label` replaces the whole name, unread words included.
891
1115
 
892
1116
  `truncate` (opt-in) keeps a row's title and its detail line to one line each, cut with "…" where they do not fit, for a long list read at a glance such as a shelf of documents. The title's words sit in their own box, so the unread dot follows the cut words and is never hidden; the whole words stay in the row's name and description and in a `title` tooltip on the words and the detail. The value, its detail, the tag and the action keep their words whole. A tooltip needs a pointer, so on a touch screen or from the keyboard the cut words are whole only in what the row opens and in what assistive technology reads; keep `truncate` for rows that open their whole content, or whose words a person does not need in full to choose.
@@ -0,0 +1,5 @@
1
+ import { type AppShellNavGroup, type AppShellProps } from './components/AppShell.js';
2
+ /** A field station's destinations: one bare group and one labelled. */
3
+ export declare const habitats: AppShellNavGroup[];
4
+ /** The shell with its required words, a page, and any prop overridden. */
5
+ export declare function renderShell(props?: Partial<AppShellProps>): import("@testing-library/react").RenderResult<typeof import("@testing-library/dom/types/queries.js"), HTMLElement, HTMLElement>;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from '@testing-library/react';
3
+ import { AppShell, } from './components/AppShell.js';
4
+ /** A field station's destinations: one bare group and one labelled. */
5
+ export const habitats = [
6
+ {
7
+ id: 'field',
8
+ items: [
9
+ {
10
+ id: 'today',
11
+ label: 'Today',
12
+ href: '/today',
13
+ icon: _jsx("svg", { "data-glyph": "today" }),
14
+ current: 'page',
15
+ },
16
+ ],
17
+ },
18
+ {
19
+ id: 'reserves',
20
+ label: 'Reserves',
21
+ items: [
22
+ {
23
+ id: 'wetlands',
24
+ label: 'Wetlands',
25
+ href: '/wetlands',
26
+ icon: _jsx("svg", {}),
27
+ count: 3,
28
+ countLabel: '3 sightings to check',
29
+ },
30
+ {
31
+ id: 'forests',
32
+ label: 'Forests',
33
+ href: '/forests',
34
+ icon: _jsx("svg", {}),
35
+ count: 2,
36
+ countTone: 'warning',
37
+ },
38
+ { id: 'dunes', label: 'Dunes', href: '/dunes', icon: _jsx("svg", {}), count: 0 },
39
+ ],
40
+ },
41
+ ];
42
+ /** The shell with its required words, a page, and any prop overridden. */
43
+ export function renderShell(props = {}) {
44
+ return render(_jsx(AppShell, { barTitle: "Heron Marsh", groups: habitats, labels: { openMenu: 'Open menu', closeMenu: 'Close menu' }, menuIcon: _jsx("svg", { "data-glyph": "menu" }), navLabel: "Field station", ...props, children: _jsx("p", { children: "Otters seen at dawn." }) }));
45
+ }
@@ -0,0 +1,21 @@
1
+ import { type RenderResult } from '@testing-library/react';
2
+ import { type Mock } from 'vitest';
3
+ import { type CommandPaletteGroup, type CommandPaletteProps } from './components/CommandPalette.js';
4
+ /** jsdom has no modal dialogs: open and close by the attribute. */
5
+ export declare function stubDialogs(): void;
6
+ export declare const otters: Mock<import("@vitest/spy").Procedure>;
7
+ export declare const herons: Mock<import("@vitest/spy").Procedure>;
8
+ export declare const report: Mock<import("@vitest/spy").Procedure>;
9
+ /** Field results: two groups, the second with a detail. */
10
+ export declare const results: CommandPaletteGroup[];
11
+ type Harness = Partial<CommandPaletteProps> & {
12
+ initiallyOpen?: boolean;
13
+ };
14
+ /**
15
+ * The palette in a page with a button before it, owning `open` and
16
+ * `query` as a consumer would; `onOpenChange` is also a spy.
17
+ */
18
+ export declare function renderPalette({ initiallyOpen, ...props }?: Harness): RenderResult & {
19
+ onOpenChange: Mock;
20
+ };
21
+ export {};
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { render } from '@testing-library/react';
3
+ import { useState } from 'react';
4
+ import { vi } from 'vitest';
5
+ import { CommandPalette, } from './components/CommandPalette.js';
6
+ /** jsdom has no modal dialogs: open and close by the attribute. */
7
+ export function stubDialogs() {
8
+ HTMLDialogElement.prototype.showModal = function showModal() {
9
+ this.setAttribute('open', '');
10
+ };
11
+ HTMLDialogElement.prototype.close = function close() {
12
+ this.removeAttribute('open');
13
+ this.dispatchEvent(new Event('close'));
14
+ };
15
+ }
16
+ export const otters = vi.fn();
17
+ export const herons = vi.fn();
18
+ export const report = vi.fn();
19
+ /** Field results: two groups, the second with a detail. */
20
+ export const results = [
21
+ {
22
+ id: 'species',
23
+ label: 'Species',
24
+ items: [
25
+ { id: 'otter', label: 'Eurasian otter', icon: _jsx("svg", {}), onSelect: otters },
26
+ {
27
+ id: 'heron',
28
+ label: 'Grey heron',
29
+ detail: 'Heron Marsh',
30
+ onSelect: herons,
31
+ },
32
+ ],
33
+ },
34
+ {
35
+ id: 'actions',
36
+ label: 'Actions',
37
+ items: [{ id: 'report', label: 'Report a sighting', onSelect: report }],
38
+ },
39
+ ];
40
+ /**
41
+ * The palette in a page with a button before it, owning `open` and
42
+ * `query` as a consumer would; `onOpenChange` is also a spy.
43
+ */
44
+ export function renderPalette({ initiallyOpen = true, ...props } = {}) {
45
+ const onOpenChange = vi.fn();
46
+ function Page() {
47
+ const [open, setOpen] = useState(initiallyOpen);
48
+ const [query, setQuery] = useState('');
49
+ return (_jsxs(_Fragment, { children: [_jsx("button", { onClick: () => setOpen(true), type: "button", children: "Opener" }), _jsx("input", { "aria-label": "Notes" }), _jsx(CommandPalette, { emptyText: "Nothing matches", groups: results, label: "Search", onOpenChange: (next) => {
50
+ onOpenChange(next);
51
+ setOpen(next);
52
+ }, onQueryChange: setQuery, open: open, placeholder: "Search species", query: query, ...props })] }));
53
+ }
54
+ return { ...render(_jsx(Page, {})), onOpenChange };
55
+ }
@@ -1,7 +1,10 @@
1
+ import { type Ref } from 'react';
1
2
  import { type AppHeaderAppearance, appHeaderAppearances } from '../css/css-app-header-bar.js';
2
3
  import { type BoxProps } from './Box.js';
3
4
  export { appHeaderAppearances, type AppHeaderAppearance };
4
5
  export type AppHeaderProps = Omit<BoxProps, 'as'> & {
6
+ /** The `header` element, such as for measuring the bar. */
7
+ ref?: Ref<HTMLElement>;
5
8
  sticky?: boolean;
6
9
  /**
7
10
  * `bar` draws the header as a full-width band of the solid surface with