@scalewing/react 1.58.0 → 1.59.1
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 +12 -0
- package/README.md +31 -1
- package/dist/components/AppShell.d.ts +5 -4
- package/dist/components/AppShell.js +10 -5
- package/dist/components/LiveRegion.d.ts +44 -0
- package/dist/components/LiveRegion.js +54 -0
- package/dist/components/Select.d.ts +8 -1
- package/dist/components/Select.js +5 -2
- package/dist/components/app-shell/SkipToMain.d.ts +14 -0
- package/dist/components/app-shell/SkipToMain.js +14 -0
- package/dist/components/app-shell/types.d.ts +17 -0
- package/dist/components/app-shell/validate.d.ts +4 -4
- package/dist/components/app-shell/validate.js +6 -4
- package/dist/css/css-app-shell-skip-link.d.ts +2 -0
- package/dist/css/css-app-shell-skip-link.js +26 -0
- package/dist/css/css-app-shell.js +7 -2
- package/dist/css/css-chrome.js +21 -0
- package/dist/css/css-fixture-action.js +5 -2
- package/dist/css/css-fixture.js +5 -2
- package/dist/css/css-list-row.js +7 -2
- package/dist/css/css-select.js +11 -0
- package/dist/css/css-site-header-skip-link.js +10 -36
- package/dist/css/css-table-press.js +21 -6
- package/dist/css/quiet-fill.d.ts +11 -0
- package/dist/css/quiet-fill.js +15 -0
- package/dist/css/skip-link.d.ts +1 -0
- package/dist/css/skip-link.js +39 -0
- package/dist/css/stacking.d.ts +1 -0
- package/dist/css/stacking.js +4 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/styles.css +109 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.59.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3f78164: An xs `Field`'s own text input (`input` of a typed kind, or `textarea`) now draws at the xs size: the document canvas's `[data-theme]` typed-input rule outranked `.sw-field-xs`, so the control kept the md min-height (44 px) and the inherited font size beside a 28 px xs `Select`. The xs rule now matches it under `[data-theme]` (caption type, xs min-height and padding), and on a coarse pointer the control is the 44 px touch target. Adorned and search inputs keep their frames' rules. No API change.
|
|
8
|
+
|
|
9
|
+
## 1.59.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 9ca5393: Muted words on a row's hover fill now keep 4.5:1: while a pressable `FixtureRow`, `ListRow` or `TableRow` shows the quiet fill (hovered, a ListRow or TableRow pressed, a TableRow's action focused), the parts on it read `--sw-color-muted` as the text colour, including the meta, details, a muted `TableCell`, the chevron and a consumer's `Text color="muted"` (muted was 4.14:1 on the fill in `ink` light, 3.47:1 in `harvest` dark). No prop, nothing changes at rest, toned words keep their tone, forced colours unchanged. `LiveRegion` (new, `LiveRegionProps`, `LiveRegionPoliteness`, `liveRegionPolitenesses`): a status line whose changing words are announced, an always-mounted region that starts empty (`role="status"` polite by default, or `role="alert"` with `politeness="assertive"`, `aria-atomic`), its string `children` swapped in as a new node on each change or `announceKey` change (the first words once it has been mounted 100 ms), drawn inline in `Text`'s look (`variant`, `color`) or `visuallyHidden`; an unknown politeness throws a `RangeError`, a look with `visuallyHidden` a `TypeError`. `AppShell` `skipLink` (opt-in, `AppShellSkipLink`: `{ label }`): a skip link first in the shell, hidden until focused, then fixed at the window's top start; a press moves focus to `main`, which gets an id and `tabindex="-1"` only with it, without changing the address; a blank label throws a `RangeError`. `Select` `disabled` (opt-in): a native disabled trigger, out of the Tab order and faded (`GrayText` in forced colours), its list never opens and `onChange` never runs. New class `sw-app-shell-skip-link` and stacking layer `skipLink` (7). SiteHeader's skip link now shares its look's rules (`css/skip-link.ts`); its output is unchanged apart from rule order.
|
|
14
|
+
|
|
3
15
|
## 1.58.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
DateToggleCalendar,
|
|
53
53
|
ListGroup,
|
|
54
54
|
ListRow,
|
|
55
|
+
LiveRegion,
|
|
55
56
|
MarkdownView,
|
|
56
57
|
DenominationGrid,
|
|
57
58
|
FilterChips,
|
|
@@ -319,6 +320,8 @@ On a coarse pointer every `Select` size's trigger and each option in its open li
|
|
|
319
320
|
/>
|
|
320
321
|
```
|
|
321
322
|
|
|
323
|
+
`Select` `disabled` (opt-in, default `false`) turns the control off, as `Button`'s `disabled` does: the trigger is a native disabled button, so it leaves the Tab order, is read as dimmed and fades to the disabled opacity (its frame, words and chevron `GrayText` in forced colours); its list cannot open, an open list closes when it is turned off, and `onChange` is never called. Say why it is off in the page's words, such as a polite `LiveRegion` beside it ("Locked while the census uploads").
|
|
324
|
+
|
|
322
325
|
`Select` `width` is `'content'` (the default: the field is as wide as its longest option, as above) or `'full'`: the field and its trigger fill the container's inline size in a `Stack`, a `Grid` cell or any narrow column, such as a filter above full-width cards on a phone. In an `Inline` row it takes the space its siblings leave, so a `Button` beside it keeps its label on one line. The value still takes the free space and ellipsizes past it, the chevron stays at the inline end (the left, right to left), and the open list is exactly the trigger's width, so it never runs past the screen's edge; a long option wraps inside it, even a single long word.
|
|
323
326
|
|
|
324
327
|
```tsx
|
|
@@ -755,6 +758,14 @@ The layout follows the window, with generated media queries only. From 75rem (12
|
|
|
755
758
|
</AppShell>
|
|
756
759
|
```
|
|
757
760
|
|
|
761
|
+
`AppShell` `skipLink` (opt-in, `AppShellSkipLink`: `{ label }`) puts a "skip to main content" link first in the shell, before the top bar and the sidebar, in the consumer's words (`label`, such as "Skip to main content" or "Saltar al contenido principal"), so a keyboard user passes every destination on each page (WCAG 2.4.1). It is visually hidden until it has focus, then drawn as `SiteHeader`'s skip link (a pill on the surface in the text colour with the accent ring, the control height) fixed at the window's top start, over the sidebar, the compact bar and a page bar, however far the page has scrolled. The shell gives its `main` an id and `tabindex="-1"` only when the link is there, so no `href` is needed: a press (Enter included) moves focus to `main` without following the fragment, so a hash router's address and the history are unchanged, and the next Tab reaches the page's first control. `main` shows no ring while it holds that focus (it is not a control). Forced colours draw the link as a link on the system canvas. A blank `label` throws a `RangeError`.
|
|
762
|
+
|
|
763
|
+
```tsx
|
|
764
|
+
<AppShell skipLink={{ label: t('skipToMain') }} {...shellProps}>
|
|
765
|
+
{page}
|
|
766
|
+
</AppShell>
|
|
767
|
+
```
|
|
768
|
+
|
|
758
769
|
```tsx
|
|
759
770
|
<Stack gap={3}>
|
|
760
771
|
<Box paddingX={4}>
|
|
@@ -778,7 +789,7 @@ The layout follows the window, with generated media queries only. From 75rem (12
|
|
|
778
789
|
|
|
779
790
|
`Table` aligns data in rows inside its own keyboard-reachable scroll region, named after the table. While the table is wider than the region, each inline edge with columns scrolled out past it draws a soft inset shade (`sw-scroll-more-start`, `sw-scroll-more-end`, mirrored right to left), so a phone, whose scrollbars stay hidden, still shows that the table goes on; a table that fits draws none, and in forced colors the system scrollbar is the cue. `TableCell` takes `numeric` (tabular numerals, end aligned), `align` and `truncate`; `density="compact"` densifies cells. `TableRow selected` sets `aria-selected` and marks the row with a 4 px accent bar at its inline start (3:1 or more against the surface and the page), the right edge in a right-to-left table. The bar is drawn inside the first cell's padding and takes no layout space, so no column moves when a row is picked, and the row takes no fill, so text, muted and accent text keep their contrast; forced colors keep the bar in the system highlight.
|
|
780
791
|
|
|
781
|
-
`TableRow pressable` (opt-in) makes the whole row the pointer target of its one action, such as a search result that is picked: a plain primary press anywhere on it calls `click()` on the row's first link or button, so the action's own handler runs. Left alone: a modifier key or non-primary button, a press already handled, a press on another control, a press that ends a text selection (text stays selectable), and a disabled or missing action. Keep one control in the row: a control placed before the action (an icon button, say) would take the row's press. The row is not a control (no role, no tab stop), so keyboard and screen reader users use the action itself. It takes the pointer cursor and the quiet fill while hovered, pressed (a brief flash on touch) or while its action has keyboard focus (a `Highlight` outline in forced colours), and underlines its action on hover (`sw-table-row-press`). A consumer `onClick` runs once per press. Each click of a double click presses the action, as a native button's would, so keep it safe to repeat. `TableCell onPress` (a body cell or a row header in `TableBody`) makes the cell's children that action: a button in the cell's own text style at weight 600, underlined on hover and focus, with the accent focus ring (`sw-table-action`); a non-function value or one on a sortable header throws a `TypeError`.
|
|
792
|
+
`TableRow pressable` (opt-in) makes the whole row the pointer target of its one action, such as a search result that is picked: a plain primary press anywhere on it calls `click()` on the row's first link or button, so the action's own handler runs. Left alone: a modifier key or non-primary button, a press already handled, a press on another control, a press that ends a text selection (text stays selectable), and a disabled or missing action. Keep one control in the row: a control placed before the action (an icon button, say) would take the row's press. The row is not a control (no role, no tab stop), so keyboard and screen reader users use the action itself. It takes the pointer cursor and the quiet fill while hovered, pressed (a brief flash on touch) or while its action has keyboard focus (a `Highlight` outline in forced colours), and underlines its action on hover (`sw-table-row-press`). On that fill a muted cell and muted words in the cells take the text colour (see "Muted words on a row's quiet fill" under `FixtureRow`). A consumer `onClick` runs once per press. Each click of a double click presses the action, as a native button's would, so keep it safe to repeat. `TableCell onPress` (a body cell or a row header in `TableBody`) makes the cell's children that action: a button in the cell's own text style at weight 600, underlined on hover and focus, with the accent focus ring (`sw-table-action`); a non-function value or one on a sortable header throws a `TypeError`.
|
|
782
793
|
|
|
783
794
|
`TableSelectCell` is a table's select column: a cell (`as="td"`, the default, in a body row, or `as="th"` in a `TableHeader` row, which gets `scope="col"`) holding one `Checkbox` with `labelVisuallyHidden`. Its props are `label` (the box's name, not drawn: "Select all", "Select Mallard"), `checked`, `indeterminate`, `onCheckedChange` and `disabled`. The column is as wide as the box (`sw-table-select`), and 44 px wide on a coarse pointer, where the box is the 44 × 44 px target; a fixed table sizes columns from its header row, so put one in the header too. Mark a chosen row with `TableRow selected`; a press on the box is left alone by `TableRow pressable`. The consumer keeps the chosen set and works out "Select all"'s `checked` and `indeterminate`. A blank label or an unknown `as` throws a `RangeError`.
|
|
784
795
|
|
|
@@ -935,6 +946,23 @@ The consumer needs no PDF.js dependency of its own. With another bundler, build
|
|
|
935
946
|
<Skeleton label="Loading rangers" variant="rows" lines={4} />
|
|
936
947
|
```
|
|
937
948
|
|
|
949
|
+
`LiveRegion` is a status line whose words change and are read out. It renders a region that is always mounted and starts empty (`role="status"` with `aria-live="polite"`, or `role="alert"` with `aria-live="assertive"`, both `aria-atomic="true"`), and swaps its words in as a new keyed child each time `children` (a string, the words in the consumer's language) or `announceKey` changes, so new words are read, and the same words again when only the key changes (a second "Copied"). Empty `children` clears it and announces nothing. Screen readers miss words added the moment a region appears, so its first words wait until it has been mounted for 100 ms; later changes show at once (a re-render with the same words and key changes nothing, so nothing is re-read). `politeness` (`liveRegionPolitenesses`) is `'polite'` (default), read when the reader is idle, or `'assertive'`, read at once: keep that for an error the person must hear before going on. By default the words are drawn inline in a `span` with `Text`'s look (`variant`, default `body`, and `color`, default `text`); `visuallyHidden` keeps them for assistive technology only (`sw-sr-only`), for a change the page already shows another way, such as a button's label turning to "Copied". `id` names the region, for example for a control's `aria-describedby`. An unknown politeness throws a `RangeError`; `variant` or `color` with `visuallyHidden` throws a `TypeError`.
|
|
950
|
+
|
|
951
|
+
Use `LiveRegion` rather than your own `role="status"` or `aria-live` whenever words change on a region already in the page: a save line ("Saving…", "Saved"), a count after a filter, a copy confirmation, a step's result. A `role="status"` element mounted with its words already inside is often not read, text replaced in place is read inconsistently across screen readers, and the same words twice are not read again; `LiveRegion` handles all three. Put it on the status line itself, never on a heading or a whole card, and keep one per message. Components that own their announcements keep them: `Toast` (`status` / `alert`), `Notice`, `Skeleton`, `Field`'s and `Select`'s error region, `Pagination`'s range, `ActionBar`'s and `Dialog`'s status lines, `ScheduleGrid`'s steps; do not wrap them in another region.
|
|
952
|
+
|
|
953
|
+
```tsx
|
|
954
|
+
<LiveRegion color="muted" variant="caption">
|
|
955
|
+
{saving ? 'Saving…' : 'Saved'}
|
|
956
|
+
</LiveRegion>
|
|
957
|
+
|
|
958
|
+
<Button onPress={copy}>{copied ? 'Copied' : 'Copy code'}</Button>
|
|
959
|
+
<LiveRegion announceKey={copies} visuallyHidden>
|
|
960
|
+
{copies > 0 ? 'Code copied' : ''}
|
|
961
|
+
</LiveRegion>
|
|
962
|
+
|
|
963
|
+
<LiveRegion color="danger" politeness="assertive">{error}</LiveRegion>
|
|
964
|
+
```
|
|
965
|
+
|
|
938
966
|
`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`.
|
|
939
967
|
|
|
940
968
|
```tsx
|
|
@@ -1265,6 +1293,8 @@ const { headings, omissions } = inspectMarkdown(page.body, {
|
|
|
1265
1293
|
</FixtureGroup>
|
|
1266
1294
|
```
|
|
1267
1295
|
|
|
1296
|
+
Muted words on a row's quiet fill: a pressable `FixtureRow` (and a detail under one with an `action` or `select`, which the fill spans), a pressable `ListRow` (hovered or pressed) and a `TableRow pressable` (hovered, pressed or with its action focused) show the quiet grey fill, on which the muted colour falls under 4.5:1 in most palettes (4.14:1 in `ink` light, 3.47:1 in `harvest` dark). While the fill shows, the parts on it read the muted token as the text colour, which keeps 4.5:1 there in every palette and scheme: the details, the meta, a muted `TableCell`, the chevron, and a consumer's `Text color="muted"` inside the row, with no prop and no change at rest. Toned words (a value's or a cell's success, danger or accent, a danger title, a Badge) keep their tone. Forced colours replace both colours and outline the hovered row in `Highlight`, as before.
|
|
1297
|
+
|
|
1268
1298
|
`FixtureRow` `flag` (opt-in, `FixtureFlag`: `{ label, tone? }`) marks an issue at the row's start, such as a clash: a round mark a space-4 across with a "!", in the tone's tint and ink (`warning`, amber, the default, for something that needs the person; `danger`, red), its edge keeping 3:1 on the surface in every palette and the text colour in forced colors. The mark is `aria-hidden` with `label` as its `title` tooltip, and `label` is read first in the row from a visually hidden span ("Can't-play clash Heron Marsh 8:00 Otter Pool"); with `aria-label` it is put before your words with a comma ("Can't-play clash, Heron Marsh vs Otter Pool, 8:00"), so do not repeat it there. From `md` the mark sits on the first line at the row's start, the home side kept clear of it; below `md` it leads the second line, before the details, so it never squeezes the home name. A row with nothing on that line below `md` (no `status`, and no `meta` or one hidden below `md` by `metaHideBelow`) keeps the mark inline on its first line instead, as from `md` (class `sw-fixture-row-flag-inline`), rather than gaining a line that holds only the flag. A blank `label` or an unknown `tone` throws a `RangeError`. `action` (opt-in) is a control after the row, such as a small neutral `Button` ("Move", named in your words: `aria-label="Move Heron Marsh vs Otter Pool"`): it is the row's sibling in its list item, outside a link or button row's press area and after it in the Tab order, a press on it never opens the row, and it stays centred on the row's height in the run's last column at every width (from `md` the row's one line, below `md` both lines), so every row's action lines up. Where `:has` is supported the hover fill spans the row and its action. An empty slot draws nothing. The run has a column for it at the end, and one for `select` at the start (`sw-fixture-list` is `auto minmax(0, 1fr) auto minmax(0, 1fr) auto auto auto`, six below `md`); a row spans every column but those two (`grid-column: 2 / -2`), and an `auto` column no row uses takes no space. New classes `sw-fixture-row-flagged`, `sw-fixture-flag`, `sw-fixture-flag-mark`, `sw-fixture-flag-warning`, `sw-fixture-flag-danger`, `sw-fixture-item-action` and `sw-fixture-action`.
|
|
1269
1299
|
|
|
1270
1300
|
```tsx
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AppShellProps } from './app-shell/types.js';
|
|
2
|
-
export type { AppShellLabels, AppShellNavGroup, AppShellNavItem, AppShellProps, } from './app-shell/types.js';
|
|
2
|
+
export type { AppShellLabels, AppShellNavGroup, AppShellNavItem, AppShellProps, AppShellSkipLink, } from './app-shell/types.js';
|
|
3
3
|
/**
|
|
4
4
|
* A web workspace's frame: a pinned side navigation of grouped
|
|
5
5
|
* destinations beside the page (`main`), which scrolls with the document.
|
|
@@ -7,7 +7,8 @@ export type { AppShellLabels, AppShellNavGroup, AppShellNavItem, AppShellProps,
|
|
|
7
7
|
* captions); from `lg` to 75rem it is a rail of glyphs with tooltips; below
|
|
8
8
|
* `lg` a sticky top bar's menu button opens the same navigation in a modal
|
|
9
9
|
* drawer from the inline start, its button optionally carrying a count.
|
|
10
|
-
* `pageBar` is a sticky band at the top of `main` on every layout
|
|
11
|
-
*
|
|
10
|
+
* `pageBar` is a sticky band at the top of `main` on every layout, and
|
|
11
|
+
* `skipLink` a first link that moves focus to `main`. Invalid groups,
|
|
12
|
+
* items, counts or words throw a `RangeError`.
|
|
12
13
|
*/
|
|
13
|
-
export declare function AppShell({ barEnd, barTitle, children, footer, groups, header, labels, menuCount, menuCountLabel, menuCountTone, menuIcon, navLabel, onNavigate, pageBar, railFooter, railHeader, }: AppShellProps): import("react").JSX.Element;
|
|
14
|
+
export declare function AppShell({ barEnd, barTitle, children, footer, groups, header, labels, menuCount, menuCountLabel, menuCountTone, menuIcon, navLabel, onNavigate, pageBar, railFooter, railHeader, skipLink, }: AppShellProps): import("react").JSX.Element;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
4
4
|
import { breakpointQuery } from '../css/breakpoints.js';
|
|
5
5
|
import { appShellWideQuery } from '../css/css-app-shell.js';
|
|
6
6
|
import { appShellBarHeightVariable } from '../css/css-app-shell-page-bar.js';
|
|
7
7
|
import { AppShellBar } from './app-shell/AppShellBar.js';
|
|
8
8
|
import { AppShellDrawer } from './app-shell/AppShellDrawer.js';
|
|
9
9
|
import { AppShellSidebar } from './app-shell/AppShellSidebar.js';
|
|
10
|
+
import { SkipToMain } from './app-shell/SkipToMain.js';
|
|
10
11
|
import { assertAppShellProps } from './app-shell/validate.js';
|
|
11
12
|
import { useElementHeight } from './use-element-height.js';
|
|
12
13
|
import { useMediaQuery } from './use-media-query.js';
|
|
@@ -18,10 +19,11 @@ const fromLg = breakpointQuery('from', 'lg');
|
|
|
18
19
|
* captions); from `lg` to 75rem it is a rail of glyphs with tooltips; below
|
|
19
20
|
* `lg` a sticky top bar's menu button opens the same navigation in a modal
|
|
20
21
|
* drawer from the inline start, its button optionally carrying a count.
|
|
21
|
-
* `pageBar` is a sticky band at the top of `main` on every layout
|
|
22
|
-
*
|
|
22
|
+
* `pageBar` is a sticky band at the top of `main` on every layout, and
|
|
23
|
+
* `skipLink` a first link that moves focus to `main`. Invalid groups,
|
|
24
|
+
* items, counts or words throw a `RangeError`.
|
|
23
25
|
*/
|
|
24
|
-
export function AppShell({ barEnd, barTitle, children, footer, groups, header, labels, menuCount, menuCountLabel, menuCountTone, menuIcon, navLabel, onNavigate, pageBar, railFooter, railHeader, }) {
|
|
26
|
+
export function AppShell({ barEnd, barTitle, children, footer, groups, header, labels, menuCount, menuCountLabel, menuCountTone, menuIcon, navLabel, onNavigate, pageBar, railFooter, railHeader, skipLink, }) {
|
|
25
27
|
assertAppShellProps({
|
|
26
28
|
groups,
|
|
27
29
|
labels,
|
|
@@ -29,10 +31,13 @@ export function AppShell({ barEnd, barTitle, children, footer, groups, header, l
|
|
|
29
31
|
menuCountLabel,
|
|
30
32
|
menuCountTone,
|
|
31
33
|
navLabel,
|
|
34
|
+
skipLink,
|
|
32
35
|
});
|
|
33
36
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
34
37
|
const menuRef = useRef(null);
|
|
35
38
|
const barRef = useRef(null);
|
|
39
|
+
const mainRef = useRef(null);
|
|
40
|
+
const mainId = useId();
|
|
36
41
|
// Set by a dismissal (Escape, a press outside, the close button): only
|
|
37
42
|
// that hands focus back to the menu button.
|
|
38
43
|
const refocusRef = useRef(false);
|
|
@@ -58,7 +63,7 @@ export function AppShell({ barEnd, barTitle, children, footer, groups, header, l
|
|
|
58
63
|
refocusRef.current = false;
|
|
59
64
|
menuRef.current?.focus();
|
|
60
65
|
}, [menuOpen]);
|
|
61
|
-
return (_jsxs("div", { className: "sw-app-shell", style: shellStyle, children: [_jsx(AppShellBar, { barEnd: barEnd, barRef: barRef, barTitle: barTitle, menuCount: menuCount, menuCountLabel: menuCountLabel, menuCountTone: menuCountTone, menuIcon: menuIcon, menuOpen: menuOpen, menuRef: menuRef, onOpenMenu: () => setMenuOpen(true), openMenuLabel: labels.openMenu }), _jsx(AppShellSidebar, { footer: footer, groups: groups, header: header, navLabel: navLabel, onNavigate: onNavigate, rail: sidebarShown && !wide, railFooter: railFooter, railHeader: railHeader }), _jsxs("main", { className: "sw-app-shell-main", children: [hasPageBar ? (_jsx("div", { className: "sw-app-shell-page-bar", children: pageBar })) : null, children] }), _jsx(AppShellDrawer, { closeLabel: labels.closeMenu, footer: footer, groups: groups, header: header, navLabel: navLabel, onChoose: () => setMenuOpen(false), onClose: () => {
|
|
66
|
+
return (_jsxs("div", { className: "sw-app-shell", style: shellStyle, children: [skipLink ? (_jsx(SkipToMain, { label: skipLink.label, mainId: mainId, mainRef: mainRef })) : null, _jsx(AppShellBar, { barEnd: barEnd, barRef: barRef, barTitle: barTitle, menuCount: menuCount, menuCountLabel: menuCountLabel, menuCountTone: menuCountTone, menuIcon: menuIcon, menuOpen: menuOpen, menuRef: menuRef, onOpenMenu: () => setMenuOpen(true), openMenuLabel: labels.openMenu }), _jsx(AppShellSidebar, { footer: footer, groups: groups, header: header, navLabel: navLabel, onNavigate: onNavigate, rail: sidebarShown && !wide, railFooter: railFooter, railHeader: railHeader }), _jsxs("main", { ref: mainRef, className: "sw-app-shell-main", id: skipLink ? mainId : undefined, tabIndex: skipLink ? -1 : undefined, children: [hasPageBar ? (_jsx("div", { className: "sw-app-shell-page-bar", children: pageBar })) : null, children] }), _jsx(AppShellDrawer, { closeLabel: labels.closeMenu, footer: footer, groups: groups, header: header, navLabel: navLabel, onChoose: () => setMenuOpen(false), onClose: () => {
|
|
62
67
|
refocusRef.current = true;
|
|
63
68
|
setMenuOpen(false);
|
|
64
69
|
}, onNavigate: onNavigate, open: menuOpen })] }));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type SemanticColorKey, type TypographyVariant } from '@scalewing/tokens';
|
|
2
|
+
/** `polite` waits for the reader to finish; `assertive` interrupts it. */
|
|
3
|
+
export declare const liveRegionPolitenesses: readonly ["polite", "assertive"];
|
|
4
|
+
export type LiveRegionPoliteness = (typeof liveRegionPolitenesses)[number];
|
|
5
|
+
export type LiveRegionProps = {
|
|
6
|
+
/**
|
|
7
|
+
* The words to announce now, in the consumer's language. Empty (or none)
|
|
8
|
+
* clears the region and announces nothing.
|
|
9
|
+
*/
|
|
10
|
+
children?: string;
|
|
11
|
+
/**
|
|
12
|
+
* `polite` (the default) is a `status`, read when the reader is idle;
|
|
13
|
+
* `assertive` is an `alert`, read at once: keep it for an error the
|
|
14
|
+
* person must hear before they go on.
|
|
15
|
+
*/
|
|
16
|
+
politeness?: LiveRegionPoliteness;
|
|
17
|
+
/**
|
|
18
|
+
* Changed to announce the same words again, such as a second "Copied".
|
|
19
|
+
* New words are announced without it.
|
|
20
|
+
*/
|
|
21
|
+
announceKey?: string | number;
|
|
22
|
+
/**
|
|
23
|
+
* Keeps the words for assistive technology only (`sw-sr-only`), for a
|
|
24
|
+
* change the page already shows another way, such as a button's label
|
|
25
|
+
* turning to "Copied". Default `false`: the words are drawn inline as
|
|
26
|
+
* `Text` draws them.
|
|
27
|
+
*/
|
|
28
|
+
visuallyHidden?: boolean;
|
|
29
|
+
/** The drawn words' type, as `Text`'s (default `body`). Not with `visuallyHidden`. */
|
|
30
|
+
variant?: TypographyVariant;
|
|
31
|
+
/** The drawn words' colour, as `Text`'s (default `text`). Not with `visuallyHidden`. */
|
|
32
|
+
color?: SemanticColorKey;
|
|
33
|
+
/** The region's id, such as for a control's `aria-describedby`. */
|
|
34
|
+
id?: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* A status line whose words change and are announced: a region that is
|
|
38
|
+
* always mounted and starts empty (`role="status"`, or `"alert"` when
|
|
39
|
+
* assertive, with `aria-live` and `aria-atomic`), its words a keyed child
|
|
40
|
+
* swapped in on each change of `children` or `announceKey`, so new words,
|
|
41
|
+
* and the same words again, are read. Drawn inline in `Text`'s look, or
|
|
42
|
+
* visually hidden.
|
|
43
|
+
*/
|
|
44
|
+
export declare function LiveRegion({ announceKey, children, color, id, politeness, variant, visuallyHidden, }: LiveRegionProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { Text } from './Text.js';
|
|
5
|
+
/** `polite` waits for the reader to finish; `assertive` interrupts it. */
|
|
6
|
+
export const liveRegionPolitenesses = ['polite', 'assertive'];
|
|
7
|
+
/*
|
|
8
|
+
* Screen readers miss words put into a live region in the same moment the
|
|
9
|
+
* region itself appears, so the first words wait until it has been in the
|
|
10
|
+
* page this long, as common announcers do. Later changes show at once.
|
|
11
|
+
*/
|
|
12
|
+
const regionSettleMs = 100;
|
|
13
|
+
function assertLiveRegionProps({ color, politeness, variant, visuallyHidden, }) {
|
|
14
|
+
if (!liveRegionPolitenesses.includes(politeness))
|
|
15
|
+
throw new RangeError(`Unknown LiveRegion politeness: ${String(politeness)}`);
|
|
16
|
+
if (visuallyHidden && (variant !== undefined || color !== undefined))
|
|
17
|
+
throw new TypeError('LiveRegion variant and color draw words; a visuallyHidden region draws none');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A status line whose words change and are announced: a region that is
|
|
21
|
+
* always mounted and starts empty (`role="status"`, or `"alert"` when
|
|
22
|
+
* assertive, with `aria-live` and `aria-atomic`), its words a keyed child
|
|
23
|
+
* swapped in on each change of `children` or `announceKey`, so new words,
|
|
24
|
+
* and the same words again, are read. Drawn inline in `Text`'s look, or
|
|
25
|
+
* visually hidden.
|
|
26
|
+
*/
|
|
27
|
+
export function LiveRegion({ announceKey, children = '', color, id, politeness = 'polite', variant, visuallyHidden = false, }) {
|
|
28
|
+
assertLiveRegionProps({ color, politeness, variant, visuallyHidden });
|
|
29
|
+
const [settled, setSettled] = useState(false);
|
|
30
|
+
const [shown, setShown] = useState({
|
|
31
|
+
words: '',
|
|
32
|
+
announceKey,
|
|
33
|
+
key: 0,
|
|
34
|
+
});
|
|
35
|
+
// Words or a key that changed since the last swap are swapped in while
|
|
36
|
+
// rendering (React's state from the previous render), once settled.
|
|
37
|
+
if (settled &&
|
|
38
|
+
(shown.words !== children || shown.announceKey !== announceKey))
|
|
39
|
+
setShown({ words: children, announceKey, key: shown.key + 1 });
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const timer = window.setTimeout(() => setSettled(true), regionSettleMs);
|
|
42
|
+
return () => window.clearTimeout(timer);
|
|
43
|
+
}, []);
|
|
44
|
+
const region = {
|
|
45
|
+
'aria-atomic': true,
|
|
46
|
+
'aria-live': politeness,
|
|
47
|
+
id,
|
|
48
|
+
role: politeness === 'assertive' ? 'alert' : 'status',
|
|
49
|
+
};
|
|
50
|
+
const words = shown.words === '' ? null : _jsx("span", { children: shown.words }, shown.key);
|
|
51
|
+
if (visuallyHidden)
|
|
52
|
+
return (_jsx("span", { className: "sw-sr-only", ...region, children: words }));
|
|
53
|
+
return (_jsx(Text, { as: "span", color: color, variant: variant, ...region, children: words }));
|
|
54
|
+
}
|
|
@@ -39,6 +39,13 @@ export type SelectProps = SelectNaming & {
|
|
|
39
39
|
requiredMark?: RequiredMark;
|
|
40
40
|
/** A validation message under the control, wired as Field's error is. */
|
|
41
41
|
error?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Turns the control off, as `Button`'s `disabled`: the trigger is a
|
|
44
|
+
* native disabled button (out of the Tab order, read as dimmed, faded),
|
|
45
|
+
* its list cannot open (an open one closes) and `onChange` is never
|
|
46
|
+
* called. Say why it is off in the page's words. Default `false`.
|
|
47
|
+
*/
|
|
48
|
+
disabled?: boolean;
|
|
42
49
|
/**
|
|
43
50
|
* `full` makes the field, its trigger and its open list fill the
|
|
44
51
|
* container's inline size; the chosen label still ellipsizes past it.
|
|
@@ -46,4 +53,4 @@ export type SelectProps = SelectNaming & {
|
|
|
46
53
|
*/
|
|
47
54
|
width?: SelectWidth;
|
|
48
55
|
};
|
|
49
|
-
export declare function Select({ action, label, labelVisuallyHidden, 'aria-labelledby': labelledBy, onChange, options, size, value, placeholder, required, requiredMark, error, width, }: SelectProps): import("react").JSX.Element;
|
|
56
|
+
export declare function Select({ action, label, labelVisuallyHidden, 'aria-labelledby': labelledBy, disabled, onChange, options, size, value, placeholder, required, requiredMark, error, width, }: SelectProps): import("react").JSX.Element;
|
|
@@ -11,7 +11,7 @@ import { SelectValue } from './select/SelectValue.js';
|
|
|
11
11
|
import { useSelectList } from './select/use-select-list.js';
|
|
12
12
|
import { Stack } from './Stack.js';
|
|
13
13
|
import { tabStop } from '../tab-stop.js';
|
|
14
|
-
export function Select({ action, label, labelVisuallyHidden, 'aria-labelledby': labelledBy, onChange, options, size = 'md', value, placeholder, required = false, requiredMark, error, width = 'content', }) {
|
|
14
|
+
export function Select({ action, label, labelVisuallyHidden, 'aria-labelledby': labelledBy, disabled = false, onChange, options, size = 'md', value, placeholder, required = false, requiredMark, error, width = 'content', }) {
|
|
15
15
|
assertSelectNaming({ label, labelVisuallyHidden, labelledBy, requiredMark });
|
|
16
16
|
assertRequiredMark(requiredMark ?? 'visible');
|
|
17
17
|
if (placeholder !== undefined && !placeholder.trim())
|
|
@@ -27,7 +27,10 @@ export function Select({ action, label, labelVisuallyHidden, 'aria-labelledby':
|
|
|
27
27
|
const showsPlaceholder = placeholder !== undefined &&
|
|
28
28
|
!options.some((option) => option.value === value);
|
|
29
29
|
const { commit, highlight, onTriggerKeyDown, open, openList, setHighlight, setOpen, } = useSelectList({ action, onChange, options, rootRef, value });
|
|
30
|
-
|
|
30
|
+
// A control turned off while its list is open closes it.
|
|
31
|
+
if (disabled && open)
|
|
32
|
+
setOpen(false);
|
|
33
|
+
return (_jsx("div", { ref: rootRef, className: cx('sw-select', size === 'xs' && 'sw-select-xs', width === 'full' && 'sw-select-full', error && 'sw-select-invalid'), children: _jsxs(Stack, { gap: label === undefined || labelVisuallyHidden ? 0 : 1, children: [label === undefined ? null : (_jsx("label", { htmlFor: triggerId, id: labelId, children: _jsx(FieldLabelText, { label: label, required: required, requiredMark: requiredMark ?? 'visible', size: size, visuallyHidden: labelVisuallyHidden ?? false }) })), _jsxs("div", { className: "sw-select-control", children: [_jsx("button", { "aria-activedescendant": open ? `${optionIdPrefix}-${highlight}` : undefined, "aria-controls": open ? listId : undefined, "aria-describedby": error ? errorId : undefined, "aria-expanded": open, "aria-haspopup": "listbox", "aria-invalid": error ? true : undefined, "aria-labelledby": labelId, "aria-required": required || undefined, className: "sw-select-trigger", disabled: disabled, id: triggerId, onBlur: (event) => {
|
|
31
34
|
const next = event.relatedTarget;
|
|
32
35
|
if (next instanceof Node && rootRef.current?.contains(next)) {
|
|
33
36
|
return;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
type SkipToMainProps = {
|
|
3
|
+
label: string;
|
|
4
|
+
mainId: string;
|
|
5
|
+
mainRef: RefObject<HTMLElement | null>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The shell's first focusable element. A press (Enter included) moves
|
|
9
|
+
* focus to `main` itself rather than following the fragment, so a hash
|
|
10
|
+
* router's address and the history stay as they were; the `href` keeps it
|
|
11
|
+
* a real link to the same place.
|
|
12
|
+
*/
|
|
13
|
+
export declare function SkipToMain({ label, mainId, mainRef }: SkipToMainProps): import("react").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { tabStop } from '../../tab-stop.js';
|
|
3
|
+
/**
|
|
4
|
+
* The shell's first focusable element. A press (Enter included) moves
|
|
5
|
+
* focus to `main` itself rather than following the fragment, so a hash
|
|
6
|
+
* router's address and the history stay as they were; the `href` keeps it
|
|
7
|
+
* a real link to the same place.
|
|
8
|
+
*/
|
|
9
|
+
export function SkipToMain({ label, mainId, mainRef }) {
|
|
10
|
+
return (_jsx("a", { className: "sw-app-shell-skip-link", href: `#${mainId}`, onClick: (event) => {
|
|
11
|
+
event.preventDefault();
|
|
12
|
+
mainRef.current?.focus();
|
|
13
|
+
}, tabIndex: tabStop, children: label }));
|
|
14
|
+
}
|
|
@@ -50,6 +50,15 @@ export type AppShellLabels = {
|
|
|
50
50
|
openMenu: string;
|
|
51
51
|
closeMenu: string;
|
|
52
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* A "skip to main content" link, the shell's first focusable element:
|
|
55
|
+
* hidden until it has focus, it moves focus to the shell's `main` (which
|
|
56
|
+
* the shell names with its own id), past the navigation.
|
|
57
|
+
*/
|
|
58
|
+
export type AppShellSkipLink = {
|
|
59
|
+
/** Its words, in the page's language ("Skip to main content"). */
|
|
60
|
+
label: string;
|
|
61
|
+
};
|
|
53
62
|
export type AppShellProps = {
|
|
54
63
|
/** The navigation landmark's name, also the drawer's. */
|
|
55
64
|
navLabel: string;
|
|
@@ -117,6 +126,14 @@ export type AppShellProps = {
|
|
|
117
126
|
*/
|
|
118
127
|
pageBar?: ReactNode;
|
|
119
128
|
labels: AppShellLabels;
|
|
129
|
+
/**
|
|
130
|
+
* A skip link before everything else in the shell: shown at the window's
|
|
131
|
+
* top start while it has focus, a press moves focus to `main` (given an
|
|
132
|
+
* id and `tabindex="-1"`, without changing the address), so a keyboard
|
|
133
|
+
* user passes the navigation's destinations (WCAG 2.4.1). Left out, the
|
|
134
|
+
* shell draws none.
|
|
135
|
+
*/
|
|
136
|
+
skipLink?: AppShellSkipLink;
|
|
120
137
|
/** The page, rendered in `main`. */
|
|
121
138
|
children: ReactNode;
|
|
122
139
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AppShellProps } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Fails closed on the shell's words, the menu's count,
|
|
4
|
-
* their items (ids unique across every group, each with an
|
|
5
|
-
* all the rail shows).
|
|
3
|
+
* Fails closed on the shell's words (a skip link's too), the menu's count,
|
|
4
|
+
* its groups and their items (ids unique across every group, each with an
|
|
5
|
+
* icon, which is all the rail shows).
|
|
6
6
|
*/
|
|
7
|
-
export declare function assertAppShellProps({ groups, labels, menuCount, menuCountLabel, menuCountTone, navLabel, }: Pick<AppShellProps, 'groups' | 'labels' | 'menuCount' | 'menuCountLabel' | 'menuCountTone' | 'navLabel'>): void;
|
|
7
|
+
export declare function assertAppShellProps({ groups, labels, menuCount, menuCountLabel, menuCountTone, navLabel, skipLink, }: Pick<AppShellProps, 'groups' | 'labels' | 'menuCount' | 'menuCountLabel' | 'menuCountTone' | 'navLabel' | 'skipLink'>): void;
|
|
@@ -44,15 +44,17 @@ function assertMenuCount({ menuCount, menuCountLabel, menuCountTone, }) {
|
|
|
44
44
|
throw new RangeError('AppShell menuCountLabel and menuCountTone come with a menuCount');
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
* Fails closed on the shell's words, the menu's count,
|
|
48
|
-
* their items (ids unique across every group, each with an
|
|
49
|
-
* all the rail shows).
|
|
47
|
+
* Fails closed on the shell's words (a skip link's too), the menu's count,
|
|
48
|
+
* its groups and their items (ids unique across every group, each with an
|
|
49
|
+
* icon, which is all the rail shows).
|
|
50
50
|
*/
|
|
51
|
-
export function assertAppShellProps({ groups, labels, menuCount, menuCountLabel, menuCountTone, navLabel, }) {
|
|
51
|
+
export function assertAppShellProps({ groups, labels, menuCount, menuCountLabel, menuCountTone, navLabel, skipLink, }) {
|
|
52
52
|
if (isBlank(navLabel))
|
|
53
53
|
throw new RangeError('AppShell navLabel must be nonempty');
|
|
54
54
|
if (isBlank(labels?.openMenu) || isBlank(labels?.closeMenu))
|
|
55
55
|
throw new RangeError('AppShell labels openMenu and closeMenu are nonempty');
|
|
56
|
+
if (skipLink != null && isBlank(skipLink.label))
|
|
57
|
+
throw new RangeError('AppShell skipLink needs a nonempty label');
|
|
56
58
|
assertMenuCount({ menuCount, menuCountLabel, menuCountTone });
|
|
57
59
|
assertGroups(groups);
|
|
58
60
|
const items = groups.flatMap((group) => group.items);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { skipLinkRules } from './skip-link.js';
|
|
2
|
+
import { zIndex } from './stacking.js';
|
|
3
|
+
const link = '.sw-app-shell-skip-link';
|
|
4
|
+
/*
|
|
5
|
+
* The shell's skip link, its first focusable element, in the shared skip
|
|
6
|
+
* link look: while it has focus it is fixed at the window's top start,
|
|
7
|
+
* over the sidebar, the compact bar and a page bar on its own layer, so it
|
|
8
|
+
* shows however far the page has scrolled. Once used, `main` holds focus
|
|
9
|
+
* (`tabindex="-1"`) without a ring: it is not a control, and the next Tab
|
|
10
|
+
* reaches the page's first one.
|
|
11
|
+
*/
|
|
12
|
+
export function cssAppShellSkipLinkRules() {
|
|
13
|
+
return `${skipLinkRules(link)}
|
|
14
|
+
|
|
15
|
+
${link} {
|
|
16
|
+
inset-block-start: var(--sw-space-2);
|
|
17
|
+
inset-inline-start: var(--sw-space-2);
|
|
18
|
+
position: fixed;
|
|
19
|
+
${zIndex('skipLink')}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.sw-app-shell-main:focus { outline: none; }`;
|
|
23
|
+
}
|
|
24
|
+
export function appShellSkipLinkClassCatalog() {
|
|
25
|
+
return ['sw-app-shell-skip-link'];
|
|
26
|
+
}
|
|
@@ -2,6 +2,7 @@ import { breakpointQuery } from './breakpoints.js';
|
|
|
2
2
|
import { appShellDrawerClassCatalog, cssAppShellDrawerClasses, } from './css-app-shell-drawer.js';
|
|
3
3
|
import { appShellNavClassCatalog, cssAppShellNavClasses, cssAppShellRailNavRules, } from './css-app-shell-nav.js';
|
|
4
4
|
import { appShellPageBarClassCatalog, cssAppShellPageBarClasses, } from './css-app-shell-page-bar.js';
|
|
5
|
+
import { appShellSkipLinkClassCatalog, cssAppShellSkipLinkRules, } from './css-app-shell-skip-link.js';
|
|
5
6
|
import { iconButtonRules } from './css-icon-button.js';
|
|
6
7
|
/*
|
|
7
8
|
* The width from which AppShell's sidebar is full rather than a rail: an
|
|
@@ -112,7 +113,8 @@ ${cssAppShellRailNavRules(' ')}
|
|
|
112
113
|
* AppShell: a workspace frame with a pinned side navigation (see
|
|
113
114
|
* components/AppShell.tsx). The links, groups and the rail's nav are
|
|
114
115
|
* `css-app-shell-nav.ts`; the page bar and the menu's count are
|
|
115
|
-
* `css-app-shell-page-bar.ts`; the menu drawer is `css-app-shell-drawer.ts
|
|
116
|
+
* `css-app-shell-page-bar.ts`; the menu drawer is `css-app-shell-drawer.ts`;
|
|
117
|
+
* the skip link is `css-app-shell-skip-link.ts`.
|
|
116
118
|
*/
|
|
117
119
|
export function cssAppShellClasses() {
|
|
118
120
|
return `${barRules()}
|
|
@@ -133,7 +135,9 @@ ${cssAppShellPageBarClasses()}
|
|
|
133
135
|
|
|
134
136
|
${sidebarRules()}
|
|
135
137
|
|
|
136
|
-
${cssAppShellDrawerClasses()}
|
|
138
|
+
${cssAppShellDrawerClasses()}
|
|
139
|
+
|
|
140
|
+
${cssAppShellSkipLinkRules()}`;
|
|
137
141
|
}
|
|
138
142
|
export function appShellClassCatalog() {
|
|
139
143
|
return [
|
|
@@ -151,5 +155,6 @@ export function appShellClassCatalog() {
|
|
|
151
155
|
...appShellNavClassCatalog(),
|
|
152
156
|
...appShellPageBarClassCatalog(),
|
|
153
157
|
...appShellDrawerClassCatalog(),
|
|
158
|
+
...appShellSkipLinkClassCatalog(),
|
|
154
159
|
];
|
|
155
160
|
}
|
package/dist/css/css-chrome.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
2
|
import { appHeaderBarClassCatalog, cssAppHeaderBarClasses, } from './css-app-header-bar.js';
|
|
3
|
+
import { typedInputs } from './css-document.js';
|
|
3
4
|
import { cssNavWordsClasses, navWordsClassCatalog } from './css-nav-words.js';
|
|
4
5
|
import { zIndex } from './stacking.js';
|
|
5
6
|
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
@@ -71,6 +72,26 @@ ${cssNavWordsClasses()}
|
|
|
71
72
|
padding-inline-end: calc(
|
|
72
73
|
var(--sw-control-xs-padding-inline) + var(--sw-space-5)
|
|
73
74
|
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
* The document canvas sizes every typed input under [data-theme] (0,2,1),
|
|
79
|
+
* which outranks the xs rule above, so an xs field's own text control
|
|
80
|
+
* matches it there. Adorned and search inputs sit one level deeper and keep
|
|
81
|
+
* their frames' rules. On a coarse pointer it is the 44 px touch target.
|
|
82
|
+
*/
|
|
83
|
+
[data-theme] .sw-field-xs > :is(${typedInputs}) {
|
|
84
|
+
font-size: ${caption.fontSize}px;
|
|
85
|
+
letter-spacing: ${caption.letterSpacing}px;
|
|
86
|
+
line-height: ${caption.lineHeight}px;
|
|
87
|
+
min-height: var(--sw-control-xs-min-height);
|
|
88
|
+
padding-inline: var(--sw-control-xs-padding-inline);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@media ${coarsePointerQuery} {
|
|
92
|
+
[data-theme] .sw-field-xs > :is(${typedInputs}) {
|
|
93
|
+
min-height: ${touchTarget};
|
|
94
|
+
}
|
|
74
95
|
}`;
|
|
75
96
|
}
|
|
76
97
|
export function chromeClassCatalog() {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { quietFill } from './quiet-fill.js';
|
|
1
|
+
import { mutedReadsAsText, quietFill } from './quiet-fill.js';
|
|
2
2
|
/*
|
|
3
3
|
* The action sits after the row in its own column, centred on the row's
|
|
4
4
|
* height, outside the row's press area, keeping the row's end padding
|
|
5
5
|
* after it. Where :has() is supported the hover fill spans the whole item,
|
|
6
|
-
* so the row, its select and its action read as one line
|
|
6
|
+
* so the row, its select and its action read as one line; a detail under
|
|
7
|
+
* the row is on that fill too, so its muted words take the text colour.
|
|
7
8
|
*/
|
|
8
9
|
function actionRules() {
|
|
9
10
|
return `.sw-fixture-action {
|
|
@@ -24,6 +25,8 @@ function actionRules() {
|
|
|
24
25
|
:is(.sw-fixture-item-action, .sw-fixture-item-select) > .sw-fixture-row-pressable:hover {
|
|
25
26
|
background: transparent;
|
|
26
27
|
}
|
|
28
|
+
|
|
29
|
+
${mutedReadsAsText([':is(.sw-fixture-item-action, .sw-fixture-item-select):has(> .sw-fixture-row-pressable:hover) > .sw-fixture-detail'])}
|
|
27
30
|
}
|
|
28
31
|
}`;
|
|
29
32
|
}
|
package/dist/css/css-fixture.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
2
|
import { breakpointQuery } from './breakpoints.js';
|
|
3
3
|
import { chevronStrokeIn } from './chevron.js';
|
|
4
|
-
import { quietFill } from './quiet-fill.js';
|
|
4
|
+
import { mutedReadsAsText, quietFill } from './quiet-fill.js';
|
|
5
5
|
import { cssFixtureActionClasses, fixtureActionClassCatalog, } from './css-fixture-action.js';
|
|
6
6
|
import { cssFixtureListRules } from './css-fixture-list.js';
|
|
7
7
|
import { cssFixtureDetailClasses, fixtureDetailClassCatalog, } from './css-fixture-detail.js';
|
|
@@ -20,7 +20,8 @@ const belowMd = breakpointQuery('below', 'md');
|
|
|
20
20
|
* colour, the quiet fill on hover and an inset accent ring, with the
|
|
21
21
|
* shared chevron at the end. The sides' names are label type at weight 500
|
|
22
22
|
* and wrap; the home side ends at the centre (its mark nearest it), the
|
|
23
|
-
* away side starts from it, mirrored in right-to-left.
|
|
23
|
+
* away side starts from it, mirrored in right-to-left. On the hover fill
|
|
24
|
+
* its muted parts take the text colour (`mutedReadsAsText`).
|
|
24
25
|
*/
|
|
25
26
|
function rowRules() {
|
|
26
27
|
return `.sw-fixture-row {
|
|
@@ -47,6 +48,8 @@ function rowRules() {
|
|
|
47
48
|
|
|
48
49
|
@media (hover: hover) {
|
|
49
50
|
.sw-fixture-row-pressable:hover { background: ${quietFill}; }
|
|
51
|
+
|
|
52
|
+
${mutedReadsAsText(['.sw-fixture-row-pressable:hover *'])}
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
.sw-fixture-row-pressable:focus { outline: none; }
|
package/dist/css/css-list-row.js
CHANGED
|
@@ -2,7 +2,7 @@ import { typographyVariants } from '@scalewing/tokens';
|
|
|
2
2
|
import { checkStrokeIn, chevronStrokeIn } from './chevron.js';
|
|
3
3
|
import { chartColorMix, chartInkMixes } from './chart-tones.js';
|
|
4
4
|
import { tableCellTones } from './css-table-ledger.js';
|
|
5
|
-
import { quietFill } from './quiet-fill.js';
|
|
5
|
+
import { mutedReadsAsText, quietFill } from './quiet-fill.js';
|
|
6
6
|
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
7
7
|
const label = typographyVariants.label;
|
|
8
8
|
const body = typographyVariants.body;
|
|
@@ -16,7 +16,8 @@ function onTitleLine(size) {
|
|
|
16
16
|
* detail at the end, then the chevron or check, each lined up with the
|
|
17
17
|
* title's first line. A pressable row is the whole button or link: a
|
|
18
18
|
* quiet fill under the pointer, the focus ring inside its edge, 44 px on a
|
|
19
|
-
* coarse pointer. Plain rows (inside a Card) have no inline padding.
|
|
19
|
+
* coarse pointer. Plain rows (inside a Card) have no inline padding. On
|
|
20
|
+
* the fill its muted parts take the text colour (`mutedReadsAsText`).
|
|
20
21
|
*/
|
|
21
22
|
function rowRules() {
|
|
22
23
|
return `.sw-list-row {
|
|
@@ -44,10 +45,14 @@ function rowRules() {
|
|
|
44
45
|
|
|
45
46
|
@media (hover: hover) {
|
|
46
47
|
.sw-list-row-pressable:hover:not(.sw-list-row-disabled) { background: ${quietFill}; }
|
|
48
|
+
|
|
49
|
+
${mutedReadsAsText(['.sw-list-row-pressable:hover:not(.sw-list-row-disabled) *'])}
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
.sw-list-row-pressable:active:not(.sw-list-row-disabled) { background: ${quietFill}; }
|
|
50
53
|
|
|
54
|
+
${mutedReadsAsText(['.sw-list-row-pressable:active:not(.sw-list-row-disabled) *'])}
|
|
55
|
+
|
|
51
56
|
.sw-list-row-pressable:focus { outline: none; }
|
|
52
57
|
|
|
53
58
|
.sw-list-row-pressable:focus-visible {
|
package/dist/css/css-select.js
CHANGED
|
@@ -125,6 +125,17 @@ export function cssSelectClasses() {
|
|
|
125
125
|
.sw-select-invalid .sw-select-trigger { border-color: Mark; }
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/* Off: faded as a disabled Button is; forced colours grey its frame, words and chevron. */
|
|
129
|
+
.sw-select-trigger:disabled {
|
|
130
|
+
cursor: not-allowed;
|
|
131
|
+
opacity: var(--sw-disabled-opacity);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@media (forced-colors: active) {
|
|
135
|
+
.sw-select-trigger:disabled { border-color: GrayText; color: GrayText; }
|
|
136
|
+
.sw-select-trigger:disabled::after { border-color: GrayText; }
|
|
137
|
+
}
|
|
138
|
+
|
|
128
139
|
.sw-select-list {
|
|
129
140
|
background: var(--sw-glass-fill);
|
|
130
141
|
border: 1px solid var(--sw-glass-border);
|
|
@@ -1,49 +1,23 @@
|
|
|
1
|
-
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
-
import { visuallyHiddenDeclarations } from './css-utilities.js';
|
|
3
1
|
import { siteHeaderContentStart, siteHeaderGutter, siteHeaderGutterQuery, } from './site-header-geometry.js';
|
|
4
|
-
|
|
2
|
+
import { skipLinkRules } from './skip-link.js';
|
|
3
|
+
const link = '.sw-site-header-skip-link';
|
|
5
4
|
/*
|
|
6
|
-
* The header's skip link, its first focusable element
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
9
|
-
*
|
|
10
|
-
* it paints over the bar's content, which is not, so it needs no layer of
|
|
11
|
-
* its own.
|
|
5
|
+
* The header's skip link, its first focusable element, in the shared skip
|
|
6
|
+
* link look, laid over the bar's start (centred in its height and lined up
|
|
7
|
+
* with the brand) while it has focus. Positioned, it paints over the bar's
|
|
8
|
+
* content, which is not, so it needs no layer of its own.
|
|
12
9
|
*/
|
|
13
10
|
export function cssSiteHeaderSkipLinkRules() {
|
|
14
|
-
return
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
border-radius: var(--sw-radius-pill);
|
|
18
|
-
box-sizing: border-box;
|
|
19
|
-
display: inline-flex;
|
|
20
|
-
font-family: var(--sw-font-sans);
|
|
21
|
-
font-size: ${label.fontSize}px;
|
|
22
|
-
font-weight: ${label.fontWeight};
|
|
11
|
+
return `${skipLinkRules(link)}
|
|
12
|
+
|
|
13
|
+
${link} {
|
|
23
14
|
inset-block-start: 50%;
|
|
24
15
|
inset-inline-start: ${siteHeaderContentStart(siteHeaderGutter.compact)};
|
|
25
|
-
letter-spacing: ${label.letterSpacing}px;
|
|
26
|
-
line-height: ${label.lineHeight}px;
|
|
27
|
-
padding-inline: var(--sw-space-4);
|
|
28
16
|
position: absolute;
|
|
29
17
|
transform: translateY(-50%);
|
|
30
|
-
white-space: nowrap;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
[data-theme] .sw-site-header-skip-link {
|
|
34
|
-
color: var(--sw-color-text);
|
|
35
|
-
text-decoration: none;
|
|
36
18
|
}
|
|
37
19
|
|
|
38
|
-
[data-theme] .sw-site-header-skip-link:focus {
|
|
39
|
-
min-height: var(--sw-control-md-min-height);
|
|
40
|
-
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
41
|
-
outline-offset: calc(-1 * var(--sw-focus-ring-width));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.sw-site-header-skip-link:not(:focus) { ${visuallyHiddenDeclarations} }
|
|
45
|
-
|
|
46
20
|
@media ${siteHeaderGutterQuery} {
|
|
47
|
-
|
|
21
|
+
${link} { inset-inline-start: ${siteHeaderContentStart(siteHeaderGutter.wide)}; }
|
|
48
22
|
}`;
|
|
49
23
|
}
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
-
import { quietFill } from './quiet-fill.js';
|
|
2
|
+
import { mutedReadsAsText, quietFill } from './quiet-fill.js';
|
|
3
3
|
const row = '.sw-table-row-press';
|
|
4
4
|
const action = '.sw-table-action';
|
|
5
5
|
/*
|
|
6
6
|
* A pressable row: the pointer cursor over all of it, and the quiet fill
|
|
7
7
|
* on its cells while it is hovered (on a pointer that hovers), pressed, or
|
|
8
8
|
* while its action has keyboard focus, so a keyboard user sees the row the
|
|
9
|
-
* focused name belongs to.
|
|
10
|
-
* muted
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* focused name belongs to. Muted words fall under 4.5:1 on that fill, so
|
|
10
|
+
* a muted cell and the muted parts inside every cell take the text colour
|
|
11
|
+
* while it shows (`mutedReadsAsText`; the cell paints the fill, so its own
|
|
12
|
+
* colour is set, not the token). Its action is underlined with it, so the
|
|
13
|
+
* row reads as that one action. Forced colours drop fills, so the row is
|
|
14
|
+
* outlined in the system highlight instead.
|
|
14
15
|
*/
|
|
16
|
+
const hovered = `${row}:hover`;
|
|
17
|
+
const held = [`${row}:active`, `${row}:has(:focus-visible)`];
|
|
18
|
+
/* A filled row's muted cells and the muted parts inside its cells read as text. */
|
|
19
|
+
function mutedCellRules(rows) {
|
|
20
|
+
return `${mutedReadsAsText(rows.map((filled) => `${filled} > :is(td, th) *`))}
|
|
21
|
+
|
|
22
|
+
${rows.map((filled) => `${filled} > .sw-table-tone-muted`).join(',\n')} {
|
|
23
|
+
color: var(--sw-color-text);
|
|
24
|
+
}`;
|
|
25
|
+
}
|
|
15
26
|
function pressableRowRules() {
|
|
16
27
|
return `${row} {
|
|
17
28
|
cursor: pointer;
|
|
@@ -25,6 +36,8 @@ function pressableRowRules() {
|
|
|
25
36
|
${row}:hover ${action} {
|
|
26
37
|
text-decoration-line: underline;
|
|
27
38
|
}
|
|
39
|
+
|
|
40
|
+
${mutedCellRules([hovered])}
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
${row}:active > :is(td, th),
|
|
@@ -32,6 +45,8 @@ ${row}:has(:focus-visible) > :is(td, th) {
|
|
|
32
45
|
background: ${quietFill};
|
|
33
46
|
}
|
|
34
47
|
|
|
48
|
+
${mutedCellRules(held)}
|
|
49
|
+
|
|
35
50
|
@media (forced-colors: active) {
|
|
36
51
|
${row}:hover,
|
|
37
52
|
${row}:has(:focus-visible) {
|
package/dist/css/quiet-fill.d.ts
CHANGED
|
@@ -5,3 +5,14 @@
|
|
|
5
5
|
* button all take it, so they read as one grey.
|
|
6
6
|
*/
|
|
7
7
|
export declare const quietFill = "color-mix(in srgb, var(--sw-color-muted) 16%, transparent)";
|
|
8
|
+
/**
|
|
9
|
+
* Muted words keep 4.5:1 on the surface but not on the quiet fill (4.14:1
|
|
10
|
+
* in most light palettes, 3.47:1 in harvest dark), while the text colour
|
|
11
|
+
* keeps it in every palette and scheme. So while a row shows the fill, its
|
|
12
|
+
* parts read the muted token as the text colour: the details, a consumer's
|
|
13
|
+
* `Text color="muted"`, the chevron. `scopes` are selectors for what sits
|
|
14
|
+
* on the fill (`<row>:hover *`), never the element that paints it, whose
|
|
15
|
+
* fill reads the muted token too. Toned words keep their tone. Forced
|
|
16
|
+
* colours replace both colours, so this changes nothing there.
|
|
17
|
+
*/
|
|
18
|
+
export declare function mutedReadsAsText(scopes: readonly string[]): string;
|
package/dist/css/quiet-fill.js
CHANGED
|
@@ -5,3 +5,18 @@
|
|
|
5
5
|
* button all take it, so they read as one grey.
|
|
6
6
|
*/
|
|
7
7
|
export const quietFill = 'color-mix(in srgb, var(--sw-color-muted) 16%, transparent)';
|
|
8
|
+
/**
|
|
9
|
+
* Muted words keep 4.5:1 on the surface but not on the quiet fill (4.14:1
|
|
10
|
+
* in most light palettes, 3.47:1 in harvest dark), while the text colour
|
|
11
|
+
* keeps it in every palette and scheme. So while a row shows the fill, its
|
|
12
|
+
* parts read the muted token as the text colour: the details, a consumer's
|
|
13
|
+
* `Text color="muted"`, the chevron. `scopes` are selectors for what sits
|
|
14
|
+
* on the fill (`<row>:hover *`), never the element that paints it, whose
|
|
15
|
+
* fill reads the muted token too. Toned words keep their tone. Forced
|
|
16
|
+
* colours replace both colours, so this changes nothing there.
|
|
17
|
+
*/
|
|
18
|
+
export function mutedReadsAsText(scopes) {
|
|
19
|
+
return `${scopes.join(',\n')} {
|
|
20
|
+
--sw-color-muted: var(--sw-color-text);
|
|
21
|
+
}`;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function skipLinkRules(selector: string): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { visuallyHiddenDeclarations } from './css-utilities.js';
|
|
3
|
+
const label = typographyVariants.label;
|
|
4
|
+
/*
|
|
5
|
+
* A skip link's look, shared by SiteHeader's and AppShell's: visually
|
|
6
|
+
* hidden until it has focus, then a pill on the surface in the text colour
|
|
7
|
+
* with the accent ring inside its edge, the control height tall. Each
|
|
8
|
+
* component positions its own; forced colours draw it as a link on the
|
|
9
|
+
* system canvas with the ring in the system colour.
|
|
10
|
+
*/
|
|
11
|
+
export function skipLinkRules(selector) {
|
|
12
|
+
return `${selector} {
|
|
13
|
+
align-items: center;
|
|
14
|
+
background: var(--sw-color-surface);
|
|
15
|
+
border-radius: var(--sw-radius-pill);
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
display: inline-flex;
|
|
18
|
+
font-family: var(--sw-font-sans);
|
|
19
|
+
font-size: ${label.fontSize}px;
|
|
20
|
+
font-weight: ${label.fontWeight};
|
|
21
|
+
letter-spacing: ${label.letterSpacing}px;
|
|
22
|
+
line-height: ${label.lineHeight}px;
|
|
23
|
+
padding-inline: var(--sw-space-4);
|
|
24
|
+
white-space: nowrap;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
[data-theme] ${selector} {
|
|
28
|
+
color: var(--sw-color-text);
|
|
29
|
+
text-decoration: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[data-theme] ${selector}:focus {
|
|
33
|
+
min-height: var(--sw-control-md-min-height);
|
|
34
|
+
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
35
|
+
outline-offset: calc(-1 * var(--sw-focus-ring-width));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
${selector}:not(:focus) { ${visuallyHiddenDeclarations} }`;
|
|
39
|
+
}
|
package/dist/css/stacking.d.ts
CHANGED
package/dist/css/stacking.js
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* strip its way between sections, and a popup opens downward).
|
|
20
20
|
* 6. `sheetGrabber`: a bottom sheet's grabber, inside its dialog, over the
|
|
21
21
|
* banded title band that sticks under it.
|
|
22
|
-
* 7. `
|
|
22
|
+
* 7. `skipLink`: AppShell's skip link while it has focus, fixed over the
|
|
23
|
+
* top chrome and the sidebar (a popup is never open while it has focus).
|
|
24
|
+
* 8. `popup`: a popup outside any lifted surface (Select list, ActionMenu
|
|
23
25
|
* list, ContextSwitcher popup, MultiSelect panel, Combobox list, Tooltip,
|
|
24
26
|
* the DateField and CalendarButton calendar) over everything in the page.
|
|
25
27
|
*
|
|
@@ -32,6 +34,7 @@ export const stackingOrder = {
|
|
|
32
34
|
popupHost: 4,
|
|
33
35
|
topChrome: 5,
|
|
34
36
|
sheetGrabber: 6,
|
|
37
|
+
skipLink: 7,
|
|
35
38
|
popup: 10,
|
|
36
39
|
};
|
|
37
40
|
/*
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { Checkbox, type CheckboxProps } from './components/Checkbox.js';
|
|
|
18
18
|
export { PasteRows, type PasteRowsColumn, type PasteRowsProps, } from './components/PasteRows.js';
|
|
19
19
|
export { parsePastedRows, type PastedRows, type PastedRowsDelimiter, pastedRowsDelimiters, } from './components/paste-rows/parse-pasted-rows.js';
|
|
20
20
|
export { Spinner, type SpinnerProps, type SpinnerSize, } from './components/Spinner.js';
|
|
21
|
+
export { LiveRegion, liveRegionPolitenesses, type LiveRegionPoliteness, type LiveRegionProps, } from './components/LiveRegion.js';
|
|
21
22
|
export { Skeleton, skeletonVariants, type SkeletonProps, type SkeletonVariant, } from './components/Skeleton.js';
|
|
22
23
|
export { EmptyState, emptyStateTitleElements, type EmptyStateProps, type EmptyStateTitleElement, } from './components/EmptyState.js';
|
|
23
24
|
export { GlyphTile, glyphTileSizes, type GlyphTileHue, type GlyphTileProps, type GlyphTileSize, } from './components/GlyphTile.js';
|
|
@@ -65,7 +66,7 @@ export { DateToggleCalendar, type DateToggleCalendarProps, } from './components/
|
|
|
65
66
|
export { CommandPalette, commandPaletteShortcuts, type CommandPaletteGroup, type CommandPaletteItem, type CommandPaletteProps, type CommandPaletteShortcut, } from './components/CommandPalette.js';
|
|
66
67
|
export { CommandPaletteTrigger, type CommandPaletteTriggerProps, } from './components/CommandPaletteTrigger.js';
|
|
67
68
|
export { ContextSwitcher, contextSwitcherTriggerAppearances, contextSwitcherWidths, type ContextSwitcherAction, type ContextSwitcherBadge, type ContextSwitcherFilter, type ContextSwitcherGroup, type ContextSwitcherOption, type ContextSwitcherProps, type ContextSwitcherTriggerAppearance, type ContextSwitcherWidth, } from './components/ContextSwitcher.js';
|
|
68
|
-
export { AppShell, type AppShellLabels, type AppShellNavGroup, type AppShellNavItem, type AppShellProps, } from './components/AppShell.js';
|
|
69
|
+
export { AppShell, type AppShellLabels, type AppShellNavGroup, type AppShellNavItem, type AppShellProps, type AppShellSkipLink, } from './components/AppShell.js';
|
|
69
70
|
export { SectionNav, sectionNavAppearances, type SectionNavAppearance, type SectionNavItem, type SectionNavProps, } from './components/SectionNav.js';
|
|
70
71
|
export { SegmentedControl, type SegmentedControlProps, type SegmentedControlVariant, segmentedSizes, type SegmentedSize, type SegmentedItem, } from './components/SegmentedControl.js';
|
|
71
72
|
export { Combobox, comboboxSizes, type ComboboxAction, type ComboboxBadge, type ComboboxGroup, type ComboboxOption, type ComboboxProps, type ComboboxSize, } from './components/Combobox.js';
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export { Checkbox } from './components/Checkbox.js';
|
|
|
17
17
|
export { PasteRows, } from './components/PasteRows.js';
|
|
18
18
|
export { parsePastedRows, pastedRowsDelimiters, } from './components/paste-rows/parse-pasted-rows.js';
|
|
19
19
|
export { Spinner, } from './components/Spinner.js';
|
|
20
|
+
export { LiveRegion, liveRegionPolitenesses, } from './components/LiveRegion.js';
|
|
20
21
|
export { Skeleton, skeletonVariants, } from './components/Skeleton.js';
|
|
21
22
|
export { EmptyState, emptyStateTitleElements, } from './components/EmptyState.js';
|
|
22
23
|
export { GlyphTile, glyphTileSizes, } from './components/GlyphTile.js';
|
package/dist/styles.css
CHANGED
|
@@ -2194,6 +2194,14 @@ body {
|
|
|
2194
2194
|
.sw-table-row-press:hover .sw-table-action {
|
|
2195
2195
|
text-decoration-line: underline;
|
|
2196
2196
|
}
|
|
2197
|
+
|
|
2198
|
+
.sw-table-row-press:hover > :is(td, th) * {
|
|
2199
|
+
--sw-color-muted: var(--sw-color-text);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
.sw-table-row-press:hover > .sw-table-tone-muted {
|
|
2203
|
+
color: var(--sw-color-text);
|
|
2204
|
+
}
|
|
2197
2205
|
}
|
|
2198
2206
|
|
|
2199
2207
|
.sw-table-row-press:active > :is(td, th),
|
|
@@ -2201,6 +2209,16 @@ body {
|
|
|
2201
2209
|
background: color-mix(in srgb, var(--sw-color-muted) 16%, transparent);
|
|
2202
2210
|
}
|
|
2203
2211
|
|
|
2212
|
+
.sw-table-row-press:active > :is(td, th) *,
|
|
2213
|
+
.sw-table-row-press:has(:focus-visible) > :is(td, th) * {
|
|
2214
|
+
--sw-color-muted: var(--sw-color-text);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
.sw-table-row-press:active > .sw-table-tone-muted,
|
|
2218
|
+
.sw-table-row-press:has(:focus-visible) > .sw-table-tone-muted {
|
|
2219
|
+
color: var(--sw-color-text);
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2204
2222
|
@media (forced-colors: active) {
|
|
2205
2223
|
.sw-table-row-press:hover,
|
|
2206
2224
|
.sw-table-row-press:has(:focus-visible) {
|
|
@@ -3077,10 +3095,18 @@ button.sw-date-toggle-day[aria-disabled='true'] {
|
|
|
3077
3095
|
|
|
3078
3096
|
@media (hover: hover) {
|
|
3079
3097
|
.sw-list-row-pressable:hover:not(.sw-list-row-disabled) { background: color-mix(in srgb, var(--sw-color-muted) 16%, transparent); }
|
|
3098
|
+
|
|
3099
|
+
.sw-list-row-pressable:hover:not(.sw-list-row-disabled) * {
|
|
3100
|
+
--sw-color-muted: var(--sw-color-text);
|
|
3101
|
+
}
|
|
3080
3102
|
}
|
|
3081
3103
|
|
|
3082
3104
|
.sw-list-row-pressable:active:not(.sw-list-row-disabled) { background: color-mix(in srgb, var(--sw-color-muted) 16%, transparent); }
|
|
3083
3105
|
|
|
3106
|
+
.sw-list-row-pressable:active:not(.sw-list-row-disabled) * {
|
|
3107
|
+
--sw-color-muted: var(--sw-color-text);
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3084
3110
|
.sw-list-row-pressable:focus { outline: none; }
|
|
3085
3111
|
|
|
3086
3112
|
.sw-list-row-pressable:focus-visible {
|
|
@@ -3355,6 +3381,10 @@ button.sw-date-toggle-day[aria-disabled='true'] {
|
|
|
3355
3381
|
|
|
3356
3382
|
@media (hover: hover) {
|
|
3357
3383
|
.sw-fixture-row-pressable:hover { background: color-mix(in srgb, var(--sw-color-muted) 16%, transparent); }
|
|
3384
|
+
|
|
3385
|
+
.sw-fixture-row-pressable:hover * {
|
|
3386
|
+
--sw-color-muted: var(--sw-color-text);
|
|
3387
|
+
}
|
|
3358
3388
|
}
|
|
3359
3389
|
|
|
3360
3390
|
.sw-fixture-row-pressable:focus { outline: none; }
|
|
@@ -3598,6 +3628,10 @@ button.sw-date-toggle-day[aria-disabled='true'] {
|
|
|
3598
3628
|
:is(.sw-fixture-item-action, .sw-fixture-item-select) > .sw-fixture-row-pressable:hover {
|
|
3599
3629
|
background: transparent;
|
|
3600
3630
|
}
|
|
3631
|
+
|
|
3632
|
+
:is(.sw-fixture-item-action, .sw-fixture-item-select):has(> .sw-fixture-row-pressable:hover) > .sw-fixture-detail {
|
|
3633
|
+
--sw-color-muted: var(--sw-color-text);
|
|
3634
|
+
}
|
|
3601
3635
|
}
|
|
3602
3636
|
}
|
|
3603
3637
|
|
|
@@ -5011,6 +5045,26 @@ button.sw-date-toggle-day[aria-disabled='true'] {
|
|
|
5011
5045
|
);
|
|
5012
5046
|
}
|
|
5013
5047
|
|
|
5048
|
+
/*
|
|
5049
|
+
* The document canvas sizes every typed input under [data-theme] (0,2,1),
|
|
5050
|
+
* which outranks the xs rule above, so an xs field's own text control
|
|
5051
|
+
* matches it there. Adorned and search inputs sit one level deeper and keep
|
|
5052
|
+
* their frames' rules. On a coarse pointer it is the 44 px touch target.
|
|
5053
|
+
*/
|
|
5054
|
+
[data-theme] .sw-field-xs > :is(input[type='text'], input[type='email'], input[type='number'], input[type='search'], input[type='url'], input[type='password'], input:not([type]), textarea) {
|
|
5055
|
+
font-size: 13px;
|
|
5056
|
+
letter-spacing: -0.08px;
|
|
5057
|
+
line-height: 18px;
|
|
5058
|
+
min-height: var(--sw-control-xs-min-height);
|
|
5059
|
+
padding-inline: var(--sw-control-xs-padding-inline);
|
|
5060
|
+
}
|
|
5061
|
+
|
|
5062
|
+
@media (pointer: coarse) {
|
|
5063
|
+
[data-theme] .sw-field-xs > :is(input[type='text'], input[type='email'], input[type='number'], input[type='search'], input[type='url'], input[type='password'], input:not([type]), textarea) {
|
|
5064
|
+
min-height: var(--sw-control-md-min-height);
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5014
5068
|
.sw-section-nav-list {
|
|
5015
5069
|
border-bottom: 1px solid var(--sw-color-border);
|
|
5016
5070
|
display: flex;
|
|
@@ -5724,13 +5778,9 @@ button.sw-date-toggle-day[aria-disabled='true'] {
|
|
|
5724
5778
|
font-family: var(--sw-font-sans);
|
|
5725
5779
|
font-size: 15px;
|
|
5726
5780
|
font-weight: 600;
|
|
5727
|
-
inset-block-start: 50%;
|
|
5728
|
-
inset-inline-start: calc(max(0px, (100% - calc(var(--sw-container-max) + 2 * var(--sw-space-5))) / 2) + var(--sw-space-4));
|
|
5729
5781
|
letter-spacing: -0.2px;
|
|
5730
5782
|
line-height: 20px;
|
|
5731
5783
|
padding-inline: var(--sw-space-4);
|
|
5732
|
-
position: absolute;
|
|
5733
|
-
transform: translateY(-50%);
|
|
5734
5784
|
white-space: nowrap;
|
|
5735
5785
|
}
|
|
5736
5786
|
|
|
@@ -5747,6 +5797,13 @@ button.sw-date-toggle-day[aria-disabled='true'] {
|
|
|
5747
5797
|
|
|
5748
5798
|
.sw-site-header-skip-link:not(:focus) { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
5749
5799
|
|
|
5800
|
+
.sw-site-header-skip-link {
|
|
5801
|
+
inset-block-start: 50%;
|
|
5802
|
+
inset-inline-start: calc(max(0px, (100% - calc(var(--sw-container-max) + 2 * var(--sw-space-5))) / 2) + var(--sw-space-4));
|
|
5803
|
+
position: absolute;
|
|
5804
|
+
transform: translateY(-50%);
|
|
5805
|
+
}
|
|
5806
|
+
|
|
5750
5807
|
@media (min-width: 48rem) {
|
|
5751
5808
|
.sw-site-header-skip-link { inset-inline-start: calc(max(0px, (100% - calc(var(--sw-container-max) + 2 * var(--sw-space-5))) / 2) + var(--sw-space-5)); }
|
|
5752
5809
|
}
|
|
@@ -6132,6 +6189,43 @@ html:has(.sw-site-header-sticky) {
|
|
|
6132
6189
|
.sw-app-shell-drawer { border-inline-end-color: CanvasText; }
|
|
6133
6190
|
}
|
|
6134
6191
|
|
|
6192
|
+
.sw-app-shell-skip-link {
|
|
6193
|
+
align-items: center;
|
|
6194
|
+
background: var(--sw-color-surface);
|
|
6195
|
+
border-radius: var(--sw-radius-pill);
|
|
6196
|
+
box-sizing: border-box;
|
|
6197
|
+
display: inline-flex;
|
|
6198
|
+
font-family: var(--sw-font-sans);
|
|
6199
|
+
font-size: 15px;
|
|
6200
|
+
font-weight: 600;
|
|
6201
|
+
letter-spacing: -0.2px;
|
|
6202
|
+
line-height: 20px;
|
|
6203
|
+
padding-inline: var(--sw-space-4);
|
|
6204
|
+
white-space: nowrap;
|
|
6205
|
+
}
|
|
6206
|
+
|
|
6207
|
+
[data-theme] .sw-app-shell-skip-link {
|
|
6208
|
+
color: var(--sw-color-text);
|
|
6209
|
+
text-decoration: none;
|
|
6210
|
+
}
|
|
6211
|
+
|
|
6212
|
+
[data-theme] .sw-app-shell-skip-link:focus {
|
|
6213
|
+
min-height: var(--sw-control-md-min-height);
|
|
6214
|
+
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
6215
|
+
outline-offset: calc(-1 * var(--sw-focus-ring-width));
|
|
6216
|
+
}
|
|
6217
|
+
|
|
6218
|
+
.sw-app-shell-skip-link:not(:focus) { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
6219
|
+
|
|
6220
|
+
.sw-app-shell-skip-link {
|
|
6221
|
+
inset-block-start: var(--sw-space-2);
|
|
6222
|
+
inset-inline-start: var(--sw-space-2);
|
|
6223
|
+
position: fixed;
|
|
6224
|
+
z-index: 7;
|
|
6225
|
+
}
|
|
6226
|
+
|
|
6227
|
+
.sw-app-shell-main:focus { outline: none; }
|
|
6228
|
+
|
|
6135
6229
|
.sw-dialog {
|
|
6136
6230
|
background: var(--sw-glass-fill);
|
|
6137
6231
|
border: 1px solid var(--sw-glass-border);
|
|
@@ -6637,6 +6731,17 @@ html:has(.sw-site-header-sticky) {
|
|
|
6637
6731
|
.sw-select-invalid .sw-select-trigger { border-color: Mark; }
|
|
6638
6732
|
}
|
|
6639
6733
|
|
|
6734
|
+
/* Off: faded as a disabled Button is; forced colours grey its frame, words and chevron. */
|
|
6735
|
+
.sw-select-trigger:disabled {
|
|
6736
|
+
cursor: not-allowed;
|
|
6737
|
+
opacity: var(--sw-disabled-opacity);
|
|
6738
|
+
}
|
|
6739
|
+
|
|
6740
|
+
@media (forced-colors: active) {
|
|
6741
|
+
.sw-select-trigger:disabled { border-color: GrayText; color: GrayText; }
|
|
6742
|
+
.sw-select-trigger:disabled::after { border-color: GrayText; }
|
|
6743
|
+
}
|
|
6744
|
+
|
|
6640
6745
|
.sw-select-list {
|
|
6641
6746
|
background: var(--sw-glass-fill);
|
|
6642
6747
|
border: 1px solid var(--sw-glass-border);
|