@scalewing/react 1.47.0 → 1.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/README.md +215 -6
- package/dist/command-palette.test-support.d.ts +21 -0
- package/dist/command-palette.test-support.js +55 -0
- package/dist/components/AppHeader.d.ts +3 -0
- package/dist/components/AppShell.d.ts +4 -3
- package/dist/components/AppShell.js +24 -7
- package/dist/components/Band.d.ts +30 -0
- package/dist/components/Band.js +23 -0
- package/dist/components/ButtonLink.d.ts +33 -0
- package/dist/components/ButtonLink.js +27 -0
- package/dist/components/CommandPalette.d.ts +12 -0
- package/dist/components/CommandPalette.js +143 -0
- package/dist/components/CommandPaletteTrigger.d.ts +9 -0
- package/dist/components/CommandPaletteTrigger.js +16 -0
- package/dist/components/ContextSwitcher.d.ts +12 -0
- package/dist/components/ContextSwitcher.js +62 -0
- package/dist/components/EmptyState.d.ts +23 -0
- package/dist/components/EmptyState.js +40 -0
- package/dist/components/FixtureGroup.d.ts +9 -0
- package/dist/components/FixtureGroup.js +16 -0
- package/dist/components/FixtureRow.d.ts +10 -0
- package/dist/components/FixtureRow.js +46 -0
- package/dist/components/GlyphTile.d.ts +9 -3
- package/dist/components/GlyphTile.js +15 -4
- package/dist/components/Grid.d.ts +11 -3
- package/dist/components/Grid.js +13 -4
- package/dist/components/Hero.d.ts +26 -0
- package/dist/components/Hero.js +23 -0
- package/dist/components/SiteFooter.d.ts +12 -0
- package/dist/components/SiteFooter.js +23 -0
- package/dist/components/SiteHeader.d.ts +14 -0
- package/dist/components/SiteHeader.js +24 -0
- package/dist/components/Skeleton.d.ts +22 -0
- package/dist/components/Skeleton.js +30 -0
- package/dist/components/Stepper.d.ts +11 -1
- package/dist/components/Stepper.js +10 -8
- package/dist/components/Text.js +3 -0
- package/dist/components/app-shell/AppShellBar.d.ts +23 -0
- package/dist/components/app-shell/AppShellBar.js +25 -0
- package/dist/components/app-shell/types.d.ts +36 -1
- package/dist/components/app-shell/validate.d.ts +4 -3
- package/dist/components/app-shell/validate.js +21 -3
- package/dist/components/command-palette/PaletteList.d.ts +20 -0
- package/dist/components/command-palette/PaletteList.js +21 -0
- package/dist/components/command-palette/shortcut.d.ts +19 -0
- package/dist/components/command-palette/shortcut.js +75 -0
- package/dist/components/command-palette/types.d.ts +72 -0
- package/dist/components/command-palette/types.js +2 -0
- package/dist/components/command-palette/validate.d.ts +10 -0
- package/dist/components/command-palette/validate.js +50 -0
- package/dist/components/context-switcher/SwitcherOption.d.ts +17 -0
- package/dist/components/context-switcher/SwitcherOption.js +20 -0
- package/dist/components/context-switcher/SwitcherPopup.d.ts +31 -0
- package/dist/components/context-switcher/SwitcherPopup.js +111 -0
- package/dist/components/context-switcher/options.d.ts +33 -0
- package/dist/components/context-switcher/options.js +60 -0
- package/dist/components/context-switcher/types.d.ts +96 -0
- package/dist/components/context-switcher/types.js +1 -0
- package/dist/components/context-switcher/use-switcher-list.d.ts +29 -0
- package/dist/components/context-switcher/use-switcher-list.js +93 -0
- package/dist/components/context-switcher/validate.d.ts +8 -0
- package/dist/components/context-switcher/validate.js +94 -0
- package/dist/components/fixture/fixture-list-context.d.ts +4 -0
- package/dist/components/fixture/fixture-list-context.js +9 -0
- package/dist/components/fixture/types.d.ts +50 -0
- package/dist/components/fixture/types.js +1 -0
- package/dist/components/fixture/validate.d.ts +8 -0
- package/dist/components/fixture/validate.js +36 -0
- package/dist/components/site-footer/types.d.ts +31 -0
- package/dist/components/site-footer/types.js +1 -0
- package/dist/components/site-footer/validate.d.ts +7 -0
- package/dist/components/site-footer/validate.js +22 -0
- package/dist/components/site-header/SiteHeaderLinks.d.ts +7 -0
- package/dist/components/site-header/SiteHeaderLinks.js +7 -0
- package/dist/components/site-header/types.d.ts +38 -0
- package/dist/components/site-header/types.js +1 -0
- package/dist/components/site-header/use-menu-dismiss.d.ts +8 -0
- package/dist/components/site-header/use-menu-dismiss.js +62 -0
- package/dist/components/site-header/validate.d.ts +7 -0
- package/dist/components/site-header/validate.js +17 -0
- package/dist/components/stepper/StepperCountFrame.d.ts +15 -0
- package/dist/components/stepper/StepperCountFrame.js +25 -0
- package/dist/components/use-element-height.d.ts +10 -0
- package/dist/components/use-element-height.js +35 -0
- package/dist/context-switcher.test-support.d.ts +9 -0
- package/dist/context-switcher.test-support.js +41 -0
- package/dist/css/css-app-shell-page-bar.d.ts +3 -0
- package/dist/css/css-app-shell-page-bar.js +81 -0
- package/dist/css/css-app-shell.js +6 -1
- package/dist/css/css-band.d.ts +10 -0
- package/dist/css/css-band.js +101 -0
- package/dist/css/css-button-link.d.ts +3 -0
- package/dist/css/css-button-link.js +48 -0
- package/dist/css/css-command-palette-key.d.ts +1 -0
- package/dist/css/css-command-palette-key.js +33 -0
- package/dist/css/css-command-palette-trigger.d.ts +2 -0
- package/dist/css/css-command-palette-trigger.js +88 -0
- package/dist/css/css-command-palette.d.ts +2 -0
- package/dist/css/css-command-palette.js +209 -0
- package/dist/css/css-components.js +30 -0
- package/dist/css/css-context-switcher-trigger.d.ts +15 -0
- package/dist/css/css-context-switcher-trigger.js +175 -0
- package/dist/css/css-context-switcher.d.ts +4 -0
- package/dist/css/css-context-switcher.js +219 -0
- package/dist/css/css-count-pill.d.ts +2 -2
- package/dist/css/css-count-pill.js +11 -3
- package/dist/css/css-empty-state.d.ts +2 -0
- package/dist/css/css-empty-state.js +77 -0
- package/dist/css/css-fixture.d.ts +5 -0
- package/dist/css/css-fixture.js +260 -0
- package/dist/css/css-glyph-tile.d.ts +2 -0
- package/dist/css/css-glyph-tile.js +29 -1
- package/dist/css/css-grid-span.js +5 -2
- package/dist/css/css-grid.d.ts +13 -0
- package/dist/css/css-grid.js +17 -1
- package/dist/css/css-hero.d.ts +2 -0
- package/dist/css/css-hero.js +62 -0
- package/dist/css/css-site-footer.d.ts +2 -0
- package/dist/css/css-site-footer.js +152 -0
- package/dist/css/css-site-header.d.ts +2 -0
- package/dist/css/css-site-header.js +262 -0
- package/dist/css/css-skeleton.d.ts +10 -0
- package/dist/css/css-skeleton.js +110 -0
- package/dist/css/css-stepper-unit.d.ts +9 -0
- package/dist/css/css-stepper-unit.js +102 -0
- package/dist/css/css-stepper.js +5 -1
- package/dist/css/stacking.js +7 -4
- package/dist/css/stylesheet.js +20 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.js +13 -1
- package/dist/stepper-count.d.ts +8 -0
- package/dist/stepper-count.js +17 -0
- package/dist/styles.css +2202 -702
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.49.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2ae2974: `Hero`: a public page's opening, a `section` named by its title. An optional `eyebrow` (muted label words), the `title` (the page's `h1` in the `hero` size), an optional `lead` and `actions`, and an optional `media` picture that keeps its own width beside the words from `md` and sits under them below it, where the actions stack to the full width. With no picture the words keep the reading width. New classes `sw-hero*`; a blank `title` throws a `RangeError`. No new tokens or dependencies. Request: `docs/requests/teisoro-hero.md`.
|
|
8
|
+
- dc0c896: `EmptyState` (new): where a list or a page has nothing yet, a centred panel on the surface with a dashed hairline edge that fills its column (a page column or a Card). `title` is a heading at `titleAs` (`h2` by default, or `h3`; `emptyStateTitleElements`), `description` a muted line, `icon` a decorative glyph in a quiet square, `actions` a centred row that wraps (44 px Buttons on a coarse pointer). Forced colours keep the dashed edge. A blank `title` or `description` or an unknown `titleAs` throws a `RangeError`. New classes `sw-empty-state`, `sw-empty-state-icon`, `sw-empty-state-title`, `sw-empty-state-description`, `sw-empty-state-actions`. No new tokens or dependencies. Request: `docs/requests/futmas-empty-state.md`.
|
|
9
|
+
- 6d3cbe8: `Grid` takes `fit` (`narrow` 15rem, `medium` 16.25rem, `wide` 18.75rem): as many columns as the grid's width holds at that minimum, sharing the row, one when the grid is narrower, so a row of cards reflows with no breakpoint. Not with `columns` or `columnsBelow`; either, or an unknown `fit`, throws a `RangeError`. New classes `sw-grid-fit-*` and the `gridFits` list and `GridFit` type. No new tokens or dependencies. Request: `docs/requests/teisoro-grid-fit.md`.
|
|
10
|
+
- b466bbb: `Text` takes the tokens' new variants `hero` (an `h1` by default), `headline` (an `h2`) and `lead` (a `p`), each stepping down to its compact size below `md` like `display` and `heading`. New classes `sw-text-hero`, `sw-text-headline`, `sw-text-lead`, generated from the tokens. Needs `@scalewing/tokens` 1.7.0. Request: `docs/requests/teisoro-showcase-type.md`.
|
|
11
|
+
- 7b57979: `Stepper` takes `unit` (opt-in), a short muted word after the count inside its frame, such as "min" or "players". The count is read with it (`aria-valuetext` "90 min") unless `valueText` says it in words; the unit itself is hidden from assistive technology. The frame draws the control surface, focus ring and the invalid, changed and disabled states; the count keeps its few-digit width, so the frame never follows it. A blank `unit` throws a `RangeError`. New classes `sw-stepper-count`, `sw-stepper-unit`. No new tokens or dependencies. Request: `docs/requests/futmas-stepper-unit.md`.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [b466bbb]
|
|
16
|
+
- @scalewing/tokens@1.7.0
|
|
17
|
+
|
|
18
|
+
## 1.48.0
|
|
19
|
+
|
|
20
|
+
### Minor Changes
|
|
21
|
+
|
|
22
|
+
- 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`.
|
|
23
|
+
- 0d789a6: `Band`: a full-width stretch of the page in one fill, its content in a
|
|
24
|
+
centred column with the page gutter (16 px, 24 px from `md`), for a site's
|
|
25
|
+
footer, one dark closing section or a grey strip.
|
|
26
|
+
|
|
27
|
+
- `tone`: `surface` (the cards' fill), `subtle` or `inverse`, the dark band
|
|
28
|
+
(`--sw-color-inverse`, which a brand moves with a theme overlay). Inside
|
|
29
|
+
the inverse band the theme's words, muted words, hairlines, subtle fill,
|
|
30
|
+
accent and `onAccent` read on the dark fill, so `Text`, a filled `Card`,
|
|
31
|
+
a primary `Button` and the focus ring need no variant; every plain link
|
|
32
|
+
takes the band's words with a half-strength underline.
|
|
33
|
+
- `width` `container` or `reading`; `as` `section`, `div`, `aside` or
|
|
34
|
+
`footer`; `paddingY` (default 8), `paddingTop`, `paddingBottom`.
|
|
35
|
+
- Forced colours outline the inverse band.
|
|
36
|
+
|
|
37
|
+
New classes `sw-band`, `sw-band-inner`, `sw-band-<tone>`,
|
|
38
|
+
`sw-band-inner-<width>`; an unknown tone, width or element throws a
|
|
39
|
+
`RangeError`. Needs `@scalewing/tokens` with `inverse` and `onInverse`. No
|
|
40
|
+
new dependencies. Request: `docs/requests/teisoro-band.md`.
|
|
41
|
+
|
|
42
|
+
- 54adbbf: `ButtonLink`: a link drawn as a `Button`, for a destination that reads as
|
|
43
|
+
an action (a site's Contact us or Sign in). An `<a href>` with the
|
|
44
|
+
Button's `variant`, `size` and `width`, so it opens like any link and works
|
|
45
|
+
on a page rendered with no script; it keeps its variant's words over the
|
|
46
|
+
canvas link colour, hover included. `aria-current="page"` marks the
|
|
47
|
+
current page with the pressed button's ring (a Highlight border in forced
|
|
48
|
+
colours). New class `sw-button-link`; an unknown variant, size or width
|
|
49
|
+
throws a `RangeError`. No new tokens or dependencies. Request:
|
|
50
|
+
`docs/requests/teisoro-button-link.md`.
|
|
51
|
+
- 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`.
|
|
52
|
+
- 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`.
|
|
53
|
+
- 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`.
|
|
54
|
+
- 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.
|
|
55
|
+
|
|
56
|
+
`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`.
|
|
57
|
+
|
|
58
|
+
- 1321a50: `SiteFooter`: a public site's footer on the dark inverse band. `brand`,
|
|
59
|
+
an optional `tagline`, `groups` of links (each a `nav` named by its label,
|
|
60
|
+
the label shown above its links; a link's `current`, `lang` and
|
|
61
|
+
`hrefLang`), and a bottom row past a hairline with `legal` at its start and
|
|
62
|
+
`end` (such as a language switch) at its end. Links are the band's muted
|
|
63
|
+
words, the current one its words at label weight, underlined on hover and
|
|
64
|
+
focus, 44 px on a coarse pointer. Two columns of groups below `md`, one
|
|
65
|
+
spread row from `md`. New classes `sw-site-footer*`; no groups, an empty
|
|
66
|
+
group, blank or repeated ids, blank labels or a blank `href` throw a
|
|
67
|
+
`RangeError`. No new tokens or dependencies. Request:
|
|
68
|
+
`docs/requests/teisoro-site-footer.md`.
|
|
69
|
+
- 987e0ef: `SiteHeader`: a public site's header. The brand; from `lg` the main
|
|
70
|
+
destinations as a row of links (the current one at label weight over a
|
|
71
|
+
bar) and `actions` at the bar's end; below `lg` `compactActions` and a menu
|
|
72
|
+
button that opens the destinations as large rows and `menuActions` under
|
|
73
|
+
the bar over the window. The menu is a native `details`, so it opens and
|
|
74
|
+
closes by touch, click and keyboard with no script (a server-rendered
|
|
75
|
+
page); script adds Escape (focus back on the button) and closes it when a
|
|
76
|
+
link is chosen, focus leaves it, a press lands outside it or the window
|
|
77
|
+
widens to `lg`. Forced colours draw the current bar as a thick underline. Nothing traps focus. `labels`
|
|
78
|
+
(`openMenu`, `closeMenu`) name the button by its state; `menuIcon` and
|
|
79
|
+
`closeIcon` are its glyph slots; `sticky` (default `true`). New classes
|
|
80
|
+
`sw-site-header*`; no items, blank or repeated ids, blank labels or hrefs,
|
|
81
|
+
or blank names throw a `RangeError`. No new tokens or dependencies.
|
|
82
|
+
Request: `docs/requests/teisoro-site-header.md`.
|
|
83
|
+
- 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`.
|
|
84
|
+
|
|
3
85
|
## 1.47.0
|
|
4
86
|
|
|
5
87
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
Accordion,
|
|
10
10
|
ActionBar,
|
|
11
11
|
ActionMenu,
|
|
12
|
+
CommandPalette,
|
|
13
|
+
CommandPaletteTrigger,
|
|
14
|
+
ContextSwitcher,
|
|
12
15
|
AppShell,
|
|
13
16
|
BarChart,
|
|
14
17
|
ChartLegend,
|
|
@@ -17,20 +20,29 @@ import {
|
|
|
17
20
|
DataBar,
|
|
18
21
|
Box,
|
|
19
22
|
Button,
|
|
23
|
+
ButtonLink,
|
|
20
24
|
CalendarButton,
|
|
21
25
|
DateRangeButton,
|
|
22
26
|
Card,
|
|
23
27
|
Checkbox,
|
|
24
28
|
RadioGroup,
|
|
25
29
|
SectionNav,
|
|
30
|
+
Skeleton,
|
|
26
31
|
Spinner,
|
|
27
32
|
Pagination,
|
|
28
33
|
Progress,
|
|
29
34
|
Tooltip,
|
|
30
35
|
InfoTip,
|
|
31
36
|
Separator,
|
|
37
|
+
Band,
|
|
38
|
+
Hero,
|
|
39
|
+
SiteFooter,
|
|
40
|
+
SiteHeader,
|
|
32
41
|
DescriptionList,
|
|
33
42
|
DescriptionItem,
|
|
43
|
+
EmptyState,
|
|
44
|
+
FixtureGroup,
|
|
45
|
+
FixtureRow,
|
|
34
46
|
ListGroup,
|
|
35
47
|
ListRow,
|
|
36
48
|
MarkdownView,
|
|
@@ -77,7 +89,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
|
|
|
77
89
|
|
|
78
90
|
`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.
|
|
79
91
|
|
|
80
|
-
`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`.
|
|
92
|
+
`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`.
|
|
81
93
|
|
|
82
94
|
`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`.
|
|
83
95
|
|
|
@@ -111,7 +123,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
|
|
|
111
123
|
</Field>
|
|
112
124
|
```
|
|
113
125
|
|
|
114
|
-
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, `AppShell` links and its menu and close buttons, the `Select`, `MultiSelect` and `
|
|
126
|
+
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.
|
|
115
127
|
|
|
116
128
|
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.
|
|
117
129
|
|
|
@@ -179,7 +191,7 @@ A chosen photo can show itself: `value.preview` is the address of a small image
|
|
|
179
191
|
/>
|
|
180
192
|
```
|
|
181
193
|
|
|
182
|
-
`Stepper` is a whole-number count changed with − and + or typed. The count is a native text input with `role="spinbutton"` and `inputmode="numeric"`, named by `label` (visible above it, or `labelVisuallyHidden` where table headers say it) and the one tab stop: ArrowUp and ArrowDown step it by `step` (default 1), Home goes to `min` and End to a finite `max` (a range below zero takes `inputmode="text"`, since a phone's numeric pad has no minus key). The − and + buttons are pointer targets outside the tab order, named by `decrementLabel` and `incrementLabel` (your copy), and a press on one leaves focus where it was; the count and both buttons sit in a `group` named by the label. − is disabled at `min` (default 0) and + at `max` (no upper bound when left out). `value` is a count or `null` for an empty box, which the buttons treat as `min` and `placeholder` can fill. Typed text is refused unless it is digits (with a leading minus when `min` is below zero), so `onChange` only receives a whole number or `null`; a typed count is reported as typed even outside the bounds, so say why in `error` (a polite message, `aria-invalid` and the danger border, as `Field`'s), and the buttons then move it back inside them. `aria-describedby` takes ids of elements that describe the count, such as the value it adds up to, read before an `error`'s message. `changed` takes `Field`'s changed look; `readOnly` keeps the count focusable and hides the buttons in their own room; `disabled` stops all three. `limitAppearance="plain"` (default `"faded"`) keeps − at `min` and + at `max` in their usual look instead of faded, for a count sheet where a faded − reads as broken; they are still disabled, a disabled or read-only Stepper is unchanged, forced colors still grey them, and an unknown value throws a `RangeError`. The count is a few digits wide and keeps that width (`width="fixed"`), or fills its container (`width="full"`, for a table cell). Bounds that are not whole numbers, `min` above `max`, or a `step` that is not above zero throw a `RangeError`.
|
|
194
|
+
`Stepper` is a whole-number count changed with − and + or typed. The count is a native text input with `role="spinbutton"` and `inputmode="numeric"`, named by `label` (visible above it, or `labelVisuallyHidden` where table headers say it) and the one tab stop: ArrowUp and ArrowDown step it by `step` (default 1), Home goes to `min` and End to a finite `max` (a range below zero takes `inputmode="text"`, since a phone's numeric pad has no minus key). The − and + buttons are pointer targets outside the tab order, named by `decrementLabel` and `incrementLabel` (your copy), and a press on one leaves focus where it was; the count and both buttons sit in a `group` named by the label. − is disabled at `min` (default 0) and + at `max` (no upper bound when left out). `value` is a count or `null` for an empty box, which the buttons treat as `min` and `placeholder` can fill. Typed text is refused unless it is digits (with a leading minus when `min` is below zero), so `onChange` only receives a whole number or `null`; a typed count is reported as typed even outside the bounds, so say why in `error` (a polite message, `aria-invalid` and the danger border, as `Field`'s), and the buttons then move it back inside them. `aria-describedby` takes ids of elements that describe the count, such as the value it adds up to, read before an `error`'s message. `changed` takes `Field`'s changed look; `readOnly` keeps the count focusable and hides the buttons in their own room; `disabled` stops all three. `limitAppearance="plain"` (default `"faded"`) keeps − at `min` and + at `max` in their usual look instead of faded, for a count sheet where a faded − reads as broken; they are still disabled, a disabled or read-only Stepper is unchanged, forced colors still grey them, and an unknown value throws a `RangeError`. The count is a few digits wide and keeps that width (`width="fixed"`), or fills its container (`width="full"`, for a table cell). `unit` (opt-in) is a short muted word after the count inside its frame, such as "min" or "players": the frame (`sw-stepper-count`) draws the control surface, focus ring and the invalid, changed and disabled states, a press on the word focuses the count, and the count keeps its few-digit width with its digits against the word, so the frame never follows it. The word (`sw-stepper-unit`) is hidden from assistive technology; the count is read with it (`aria-valuetext` "90 min", none while the box is empty), and a `valueText` you pass wins. The default joins the count and the word as given, so a word that changes with the count ("players") reads "1 players": pass `valueText` in your language's right form ("1 player", "11 players"). A blank `unit` throws a `RangeError`. Bounds that are not whole numbers, `min` above `max`, or a `step` that is not above zero throw a `RangeError`.
|
|
183
195
|
|
|
184
196
|
```tsx
|
|
185
197
|
<Stepper
|
|
@@ -301,6 +313,15 @@ On the web, `variant="neutral"` is a quiet grey fill (the muted color at 16 %, t
|
|
|
301
313
|
|
|
302
314
|
`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`.
|
|
303
315
|
|
|
316
|
+
`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.
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
<ButtonLink href="/contact">Contact the rangers</ButtonLink>
|
|
320
|
+
<ButtonLink href="/sign-in" variant="neutral">
|
|
321
|
+
Sign in
|
|
322
|
+
</ButtonLink>
|
|
323
|
+
```
|
|
324
|
+
|
|
304
325
|
```tsx
|
|
305
326
|
<Inline gap={2}>
|
|
306
327
|
<Button variant="neutral" width="full" onPress={edit}>
|
|
@@ -333,6 +354,39 @@ A `Button` that toggles something passes `aria-pressed`. The pressed button gets
|
|
|
333
354
|
|
|
334
355
|
`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`.
|
|
335
356
|
|
|
357
|
+
`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`.
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
<ContextSwitcher
|
|
361
|
+
label="Season"
|
|
362
|
+
trigger={season.label}
|
|
363
|
+
triggerAppearance="pill"
|
|
364
|
+
groups={[
|
|
365
|
+
{
|
|
366
|
+
id: 'seasons',
|
|
367
|
+
options: [
|
|
368
|
+
{
|
|
369
|
+
id: 'spring-27',
|
|
370
|
+
label: 'Spring 2027',
|
|
371
|
+
badge: { label: 'Draft', tone: 'neutral' },
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
id: 'autumn-26',
|
|
375
|
+
label: 'Autumn 2026',
|
|
376
|
+
detail: 'Current',
|
|
377
|
+
badge: { label: 'Live', tone: 'success' },
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
},
|
|
381
|
+
]}
|
|
382
|
+
currentId={season.id}
|
|
383
|
+
onSelect={(option) => setSeason(option)}
|
|
384
|
+
actions={[
|
|
385
|
+
{ id: 'new', label: 'Start a new season', onSelect: openNewSeason },
|
|
386
|
+
]}
|
|
387
|
+
/>
|
|
388
|
+
```
|
|
389
|
+
|
|
336
390
|
`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.
|
|
337
391
|
|
|
338
392
|
```tsx
|
|
@@ -552,7 +606,7 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
|
|
|
552
606
|
|
|
553
607
|
`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.
|
|
554
608
|
|
|
555
|
-
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. `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`,
|
|
609
|
+
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`.
|
|
556
610
|
|
|
557
611
|
```tsx
|
|
558
612
|
<AppShell
|
|
@@ -598,8 +652,12 @@ The layout follows the window, with generated media queries only. From 75rem (12
|
|
|
598
652
|
header={<StationMenu />}
|
|
599
653
|
railHeader={<StationTile />}
|
|
600
654
|
footer={<AccountMenu />}
|
|
655
|
+
pageBar={<CensusBar />}
|
|
601
656
|
barTitle={<Text variant="label">Heron Marsh</Text>}
|
|
602
657
|
menuIcon={<Menu />}
|
|
658
|
+
menuCount={5}
|
|
659
|
+
menuCountLabel="5 need you"
|
|
660
|
+
menuCountTone="warning"
|
|
603
661
|
labels={{ openMenu: 'Open menu', closeMenu: 'Close menu' }}
|
|
604
662
|
>
|
|
605
663
|
<Box padding={5}>{page}</Box>
|
|
@@ -777,7 +835,32 @@ The consumer needs no PDF.js dependency of its own. With another bundler, build
|
|
|
777
835
|
|
|
778
836
|
`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.
|
|
779
837
|
|
|
780
|
-
`
|
|
838
|
+
`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`.
|
|
839
|
+
|
|
840
|
+
```tsx
|
|
841
|
+
<Skeleton label="Loading this week's sightings" />
|
|
842
|
+
<Skeleton label="Loading rangers" variant="rows" lines={4} />
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
`EmptyState` is where a list or a page has nothing yet: a centred panel on the surface with a dashed hairline edge (the md radius) that fills its column, so it sits in a page column or inside a `Card`. `title` (required) says what is empty ("No teams in Spring 2027 yet"), a heading at `titleAs` (`emptyStateTitleElements`: `"h2"` by default in a page, `"h3"` inside a card under its own heading) in title type. `description` is a muted line under it, usually what to do next. `icon` is a decorative glyph (Lucide in your app) in a quiet subtle-fill square over the title, hidden from assistive technology. `actions` are the ways out, such as a primary and a neutral `Button`, in a centred row that wraps (a 44 px target on a touch screen). The panel is a plain `div`, not a landmark or a live region: if it replaces a list after a person's action, say so yourself. An empty slot (nothing, an empty Fragment or array, or blank words) draws no box. Inside a bordered `Card` (the default glass or `outlined`) the dashed edge sits just inside the card's own hairline and reads as a double edge: where the empty panel is the card's whole content, use it in place of the card, or in an `elevated` or `filled` Card, which draw no hairline. Forced colours keep the dashed edge. A blank `title` or `description` or an unknown `titleAs` throws a `RangeError`.
|
|
846
|
+
|
|
847
|
+
```tsx
|
|
848
|
+
<EmptyState
|
|
849
|
+
icon={<Users />}
|
|
850
|
+
title="No nests counted yet"
|
|
851
|
+
description="Bring back last spring's nests, or paste a list."
|
|
852
|
+
actions={
|
|
853
|
+
<>
|
|
854
|
+
<Button onPress={copy}>Copy last spring's 12 nests</Button>
|
|
855
|
+
<Button variant="neutral" onPress={paste}>
|
|
856
|
+
Paste a list
|
|
857
|
+
</Button>
|
|
858
|
+
</>
|
|
859
|
+
}
|
|
860
|
+
/>
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
`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`.
|
|
781
864
|
|
|
782
865
|
```tsx
|
|
783
866
|
<Inline gap={2} align="center">
|
|
@@ -790,6 +873,31 @@ The consumer needs no PDF.js dependency of its own. With another bundler, build
|
|
|
790
873
|
|
|
791
874
|
`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`.
|
|
792
875
|
|
|
876
|
+
`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`.
|
|
877
|
+
|
|
878
|
+
```tsx
|
|
879
|
+
<CommandPaletteTrigger
|
|
880
|
+
label="Search"
|
|
881
|
+
placeholder="Search species and sites"
|
|
882
|
+
shortcutLabel="⌘K"
|
|
883
|
+
icon={<Search />}
|
|
884
|
+
onPress={() => setOpen(true)}
|
|
885
|
+
/>
|
|
886
|
+
<CommandPalette
|
|
887
|
+
open={open}
|
|
888
|
+
onOpenChange={setOpen}
|
|
889
|
+
label="Search"
|
|
890
|
+
placeholder="Search species and sites"
|
|
891
|
+
query={query}
|
|
892
|
+
onQueryChange={setQuery}
|
|
893
|
+
groups={resultsFor(query)}
|
|
894
|
+
emptyText="No species or sites match"
|
|
895
|
+
countText={(n) => `${n} results`}
|
|
896
|
+
hint="↑↓ to move · Enter to open · Esc to close"
|
|
897
|
+
shortcut="mod+k"
|
|
898
|
+
/>
|
|
899
|
+
```
|
|
900
|
+
|
|
793
901
|
`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`.
|
|
794
902
|
|
|
795
903
|
```tsx
|
|
@@ -896,6 +1004,74 @@ const { headings, omissions } = inspectMarkdown(page.body, {
|
|
|
896
1004
|
|
|
897
1005
|
`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.
|
|
898
1006
|
|
|
1007
|
+
`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`.
|
|
1008
|
+
|
|
1009
|
+
```tsx
|
|
1010
|
+
<Band tone="inverse" aria-labelledby="closing-title">
|
|
1011
|
+
<Text id="closing-title" variant="title">
|
|
1012
|
+
Count with us
|
|
1013
|
+
</Text>
|
|
1014
|
+
</Band>
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
`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`.
|
|
1018
|
+
|
|
1019
|
+
```tsx
|
|
1020
|
+
<SiteHeader
|
|
1021
|
+
brand={<a href="/">Wetland Trust</a>}
|
|
1022
|
+
navLabel="Main"
|
|
1023
|
+
items={[{ id: 'marsh', label: 'Marsh', href: '/marsh', current: 'page' }]}
|
|
1024
|
+
actions={<ButtonLink href="/contact">Contact the rangers</ButtonLink>}
|
|
1025
|
+
compactActions={
|
|
1026
|
+
<a href="/es" lang="es" hrefLang="es">
|
|
1027
|
+
Español
|
|
1028
|
+
</a>
|
|
1029
|
+
}
|
|
1030
|
+
menuActions={
|
|
1031
|
+
<ButtonLink href="/contact" width="full">
|
|
1032
|
+
Contact the rangers
|
|
1033
|
+
</ButtonLink>
|
|
1034
|
+
}
|
|
1035
|
+
labels={{ openMenu: 'Open the menu', closeMenu: 'Close the menu' }}
|
|
1036
|
+
menuIcon={<MenuIcon />}
|
|
1037
|
+
closeIcon={<CloseIcon />}
|
|
1038
|
+
/>
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
`Hero` is a public page's opening, a `section` named by its title: an optional `eyebrow` (muted label words), the `title` (the page's one `h1`, in the `hero` size), an optional `lead` (the `lead` size) and `actions` (links or buttons in a wrapping row, stacked to the full width below `md`), with an optional `media` picture. From `md` the picture keeps its own width at the row's end and the words take the rest; below `md` it sits centred under the words. With no picture the words keep the reading width (`--sw-reading-max`). Put it in the page's column (`sw-container`); it sets no gutter or fill of its own. The title always names the section (`aria-labelledby` is not a prop). Leave out `actions` or `media` rather than passing an empty fragment, which keeps its row or column. A blank `title` throws a `RangeError`.
|
|
1042
|
+
|
|
1043
|
+
```tsx
|
|
1044
|
+
<Hero
|
|
1045
|
+
eyebrow="For birdwatchers"
|
|
1046
|
+
title="Every heron counted."
|
|
1047
|
+
lead="The north marsh in every season, from the hide to the shore."
|
|
1048
|
+
actions={<ButtonLink href="/visit">Plan a visit</ButtonLink>}
|
|
1049
|
+
media={<img alt="The heron hide at dawn" src="/hide.jpg" />}
|
|
1050
|
+
/>
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
`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`.
|
|
1054
|
+
|
|
1055
|
+
```tsx
|
|
1056
|
+
<SiteFooter
|
|
1057
|
+
brand={<img alt="Wetland Trust" src="/logo-white.svg" />}
|
|
1058
|
+
tagline="Counting birds since 1990."
|
|
1059
|
+
groups={[
|
|
1060
|
+
{
|
|
1061
|
+
id: 'visit',
|
|
1062
|
+
label: 'Visit',
|
|
1063
|
+
links: [{ id: 'marsh', label: 'North marsh', href: '/marsh' }],
|
|
1064
|
+
},
|
|
1065
|
+
]}
|
|
1066
|
+
legal="© Wetland Trust"
|
|
1067
|
+
end={
|
|
1068
|
+
<a href="/es" lang="es" hrefLang="es">
|
|
1069
|
+
Español
|
|
1070
|
+
</a>
|
|
1071
|
+
}
|
|
1072
|
+
/>
|
|
1073
|
+
```
|
|
1074
|
+
|
|
899
1075
|
`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.
|
|
900
1076
|
|
|
901
1077
|
`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.
|
|
@@ -944,6 +1120,29 @@ const { headings, omissions } = inspectMarkdown(page.body, {
|
|
|
944
1120
|
|
|
945
1121
|
`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`.
|
|
946
1122
|
|
|
1123
|
+
`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`.
|
|
1124
|
+
|
|
1125
|
+
```tsx
|
|
1126
|
+
<FixtureGroup title="Sunday 25 October · North meadow hides">
|
|
1127
|
+
<FixtureRow
|
|
1128
|
+
home={{ name: 'Heron Marsh', mark: <Crest /> }}
|
|
1129
|
+
away={{ name: 'Otter Pool', mark: <Crest /> }}
|
|
1130
|
+
center="8:00"
|
|
1131
|
+
meta="Hide 1"
|
|
1132
|
+
metaHideBelow="md"
|
|
1133
|
+
href="/counts/1"
|
|
1134
|
+
aria-label="Heron Marsh with Otter Pool, 8:00, Hide 1"
|
|
1135
|
+
/>
|
|
1136
|
+
<FixtureRow
|
|
1137
|
+
home={{ name: 'Pine Ridge' }}
|
|
1138
|
+
away={{ name: 'Bittern Fen' }}
|
|
1139
|
+
center="14–9"
|
|
1140
|
+
centerKind="score"
|
|
1141
|
+
status={{ label: 'Counted', tone: 'success' }}
|
|
1142
|
+
/>
|
|
1143
|
+
</FixtureGroup>
|
|
1144
|
+
```
|
|
1145
|
+
|
|
947
1146
|
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.
|
|
948
1147
|
|
|
949
1148
|
`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.
|
|
@@ -1014,7 +1213,7 @@ A row can also carry three trailing parts. `unread` marks it new for the person,
|
|
|
1014
1213
|
|
|
1015
1214
|
`--sw-color-accentSubtle` is the accent tint from `@scalewing/tokens`: a quiet fill behind accent text or glyphs that keeps 4.5:1. The stylesheet and every palette file set it, and a palette that moves the accent or surface retints it.
|
|
1016
1215
|
|
|
1017
|
-
`Text` steps `display` and `
|
|
1216
|
+
`Text` steps `hero`, `display`, `headline`, `heading` and `lead` down to the compact token sizes below the `md` breakpoint; consumers do not size type per viewport. `hero` (an `h1`), `headline` (an `h2`) and `lead` (a `p`) are a public page's sizes: its one opening title, its section titles and the sentence under them; an app's screens keep `display` and `heading`.
|
|
1018
1217
|
|
|
1019
1218
|
`sw-container-reading` centres a column at the reading width (`--sw-reading-max`, 46rem, `Article`'s), as `sw-container` centres a page's column at the container width (72rem): `width: 100%`, the max width and auto inline margins, full width below it. Use it for a form or text that reads better narrower than the page, such as an open form of one column. As with `sw-container`, the width is the content box's, so put padding or a border on a child.
|
|
1020
1219
|
|
|
@@ -1026,6 +1225,16 @@ A row can also carry three trailing parts. `unread` marks it new for the person,
|
|
|
1026
1225
|
|
|
1027
1226
|
`Grid` places children in one to four (or six) equal-width columns with a token `gap` step. `columnsBelow={{ md: 2 }}` drops to fewer columns below the `md` breakpoint so tiles and stat cards stay readable on a phone. It accepts every `Box` prop.
|
|
1028
1227
|
|
|
1228
|
+
`fit` (`narrow` 15rem, `medium` 16.25rem, `wide` 18.75rem) fits the columns to the grid's width instead of a fixed count: as many columns as fit at that minimum, sharing the row, and one column when the grid is narrower than it, so a row of cards reflows on a tablet and stacks on a phone with no breakpoint. A fitted grid takes no `columns` or `columnsBelow` (either throws a `RangeError`, as an unknown `fit` does); keep `columnSpan` for fixed-column grids. `gridFits` lists the names.
|
|
1229
|
+
|
|
1230
|
+
```tsx
|
|
1231
|
+
<Grid fit="narrow" gap={4}>
|
|
1232
|
+
{habitats.map((habitat) => (
|
|
1233
|
+
<Card key={habitat.id}>…</Card>
|
|
1234
|
+
))}
|
|
1235
|
+
</Grid>
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1029
1238
|
Every `Box`-based component (`Box`, `Stack`, `Inline`, `Card`, `Grid`, …) takes `columnSpan` (1, 2, 3, 4 or 6) to cover several columns of the `Grid` it sits in directly, through the generated `sw-grid-span-*` classes. A span is capped at the columns the grid has at the current width, so it never adds an implicit column, and `columnsBelow` still decides the phone layout. A form about twice the width of a side panel, stacking below `md`:
|
|
1030
1239
|
|
|
1031
1240
|
```tsx
|
|
@@ -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
|