@scalewing/react 1.45.0 → 1.47.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 +16 -0
- package/README.md +105 -3
- package/dist/app-shell.test-support.d.ts +5 -0
- package/dist/app-shell.test-support.js +45 -0
- package/dist/components/AppShell.d.ts +12 -0
- package/dist/components/AppShell.js +48 -0
- package/dist/components/Button.d.ts +14 -1
- package/dist/components/Button.js +5 -2
- package/dist/components/FileField.d.ts +18 -2
- package/dist/components/FileField.js +26 -22
- package/dist/components/ListGroup.d.ts +8 -3
- package/dist/components/ListGroup.js +13 -5
- package/dist/components/NavCount.d.ts +12 -5
- package/dist/components/NavCount.js +9 -4
- package/dist/components/SectionNav.d.ts +2 -1
- package/dist/components/SectionNav.js +3 -19
- package/dist/components/Select.d.ts +8 -5
- package/dist/components/Select.js +7 -4
- package/dist/components/app-shell/AppShellDrawer.d.ts +24 -0
- package/dist/components/app-shell/AppShellDrawer.js +17 -0
- package/dist/components/app-shell/AppShellLink.d.ts +14 -0
- package/dist/components/app-shell/AppShellLink.js +27 -0
- package/dist/components/app-shell/AppShellNav.d.ts +15 -0
- package/dist/components/app-shell/AppShellNav.js +29 -0
- package/dist/components/app-shell/AppShellSidebar.d.ts +18 -0
- package/dist/components/app-shell/AppShellSidebar.js +18 -0
- package/dist/components/app-shell/types.d.ts +87 -0
- package/dist/components/app-shell/types.js +1 -0
- package/dist/components/app-shell/validate.d.ts +6 -0
- package/dist/components/app-shell/validate.js +52 -0
- package/dist/components/file-field/ChosenFile.d.ts +45 -0
- package/dist/components/file-field/ChosenFile.js +48 -0
- package/dist/components/file-field/file-field-checks.d.ts +5 -2
- package/dist/components/file-field/file-field-checks.js +7 -2
- package/dist/components/nav-items.d.ts +21 -0
- package/dist/components/nav-items.js +26 -0
- package/dist/components/select/select-naming.d.ts +34 -0
- package/dist/components/select/select-naming.js +22 -0
- package/dist/css/css-app-shell-drawer.d.ts +2 -0
- package/dist/css/css-app-shell-drawer.js +91 -0
- package/dist/css/css-app-shell-nav.d.ts +3 -0
- package/dist/css/css-app-shell-nav.js +167 -0
- package/dist/css/css-app-shell.d.ts +4 -0
- package/dist/css/css-app-shell.js +150 -0
- package/dist/css/css-button-width.d.ts +11 -0
- package/dist/css/css-button-width.js +36 -0
- package/dist/css/css-button.js +3 -0
- package/dist/css/css-components.js +3 -0
- package/dist/css/css-count-pill.d.ts +7 -0
- package/dist/css/css-count-pill.js +42 -12
- package/dist/css/css-file-field-compact.d.ts +2 -0
- package/dist/css/css-file-field-compact.js +106 -0
- package/dist/css/css-file-field.js +4 -0
- package/dist/css/css-list-tiles.d.ts +6 -0
- package/dist/css/css-list-tiles.js +50 -0
- package/dist/css/css-list.js +5 -1
- package/dist/css/stylesheet.js +2 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/list-options.d.ts +12 -5
- package/dist/list-options.js +19 -5
- package/dist/styles.css +443 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.47.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 26fe8e5: `AppShell`: a web workspace's frame with a pinned side navigation of grouped destinations beside the page in `main`, which scrolls with the document. From 75rem a full sidebar (header, groups with optional captions, footer; glyph, label and count per destination, the current one on a quiet fill), sticky at the viewport's height with only its nav scrolling; from `lg` to 75rem an icon rail whose labels stay in each link's name and show in a Tooltip, the count a pill on the glyph, with `railHeader`/`railFooter`; below `lg` a sticky AppHeader `bar` whose menu button opens the same navigation in a modal drawer from the inline start (Escape, a press outside, its close button or choosing a destination close it, focus returning to the menu button). Items follow `SectionNav`'s rules plus a required `icon`; `onNavigate` takes a plain press for a client router; invalid groups, items or words throw a `RangeError`. New classes `sw-app-shell*` (the count joins the shared count pill as `sw-app-shell-count`); the 75rem width is internal, not a public breakpoint. `SectionNav` and `AppShell` now share one item validation (`nav-items.ts`); `SectionNav`'s behaviour and messages are unchanged. No new tokens or dependencies. Request: `docs/requests/futmas-app-shell.md`.
|
|
8
|
+
- 26fe8e5: `NavCount` `tone="warning"`: an amber count for work that needs the person but is not urgent, the warning tint with the warning ink (a warning Notice's, 4.5:1 in every palette) inside a warning edge; `navCountTones` is now `['danger', 'accent', 'warning']`. `AppShell` items take `countTone` (NavCount's tones, `danger` by default; needs a `count`). New classes `sw-nav-count-warning`, `sw-app-shell-count-accent` and `sw-app-shell-count-warning`. The default stays danger. No new tokens or dependencies. Request: `docs/requests/futmas-nav-count-warning.md`.
|
|
9
|
+
|
|
10
|
+
## 1.46.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- f93ec57: `Button` `width`: `'content'` (the default, as before) or `'full'`, which fills the container's inline size in a `Stack`, a `Grid` cell or a column, and in an `Inline` row takes the space its siblings leave (as a full `Select` does; in a wrapping row it grows from its words and wraps), such as an Edit beside a ••• `ActionMenu` that keeps its own square at the row's end. The words stay centred. An unknown width throws a `RangeError`. New class `sw-button-full`; `ButtonWidth` and `buttonWidths` are exported. No new tokens or dependencies. Request: `docs/requests/teisoro-button-full-width.md`.
|
|
15
|
+
- 58bcede: `FileField` `onRemove` and `removeLabel`: Remove beside a chosen file, a neutral button after the change button in the full field, named by its words and the label ("Remove Cover photo"); the consumer clears `value` and focus moves to the empty field. `FileField` `compact`: the field at a text field's height for a form row; chosen, the thumbnail (or mark) and the name cut to one line are the change button, named by the label and `changeLabel` and described by the name and detail (read, not drawn), with Remove as a quiet button at the row's end. Set `onRemove` and `removeLabel` together (a `RangeError` otherwise, and for blank words). New classes `sw-file-field-compact`, `sw-file-field-pick`, `sw-file-field-remove` and `sw-file-field-actions`; disabled, the compact row and its Remove fade alike. A field without them renders as before. No new tokens or dependencies. Request: `docs/requests/teisoro-file-field-compact.md`.
|
|
16
|
+
- 0441a50: `ListGroup` `variant="tiles"`: the same one list (its name, its items and their count) with its rows laid out as tiles, each row its own bordered panel on the surface and no hairline between them, in `tileColumns` equal columns (2, 3 by default, or 4) a space-3 apart from `md` up, and one a row below it whatever `tileColumns` says, such as a strip of new arrivals. A row's press fill and inset focus ring stay inside its tile. Tiles take no `maxHeight` or `footer`, and `tileColumns` needs `tiles` (a `RangeError`, as for a count other than 2, 3 or 4). New classes `sw-list-group-tiles` and `sw-list-group-tiles-2`, `-3`, `-4`; `listTileColumnCounts` and `ListGroupTileColumns` are exported. No new tokens or dependencies. Request: `docs/requests/teisoro-list-group-tiles.md`.
|
|
17
|
+
- ccc2f79: `Select` `aria-labelledby`: in place of `label`, the id of words the page already draws, such as a label above a choice that is a `SegmentedControl` on a wide screen and a `Select` on a phone. The select then draws no label of its own, so a screen reader meets the words once instead of the page's words followed by the select's hidden copy; the trigger and its open list are named by them. `required` still sets `aria-required` and draws no mark. `label` and `aria-labelledby` are exclusive (a type union, and a `RangeError` for neither, a blank id, both, or `requiredMark` with `aria-labelledby`). A select with `label` renders exactly as before. **Types:** `SelectProps` is now a union (`SelectNaming`), so `label` is optional on the type. Code that reads `props.label` from a `SelectProps` narrows it first, for example a wrapper typed `SelectProps & { label: string }`, or a check `if (props.label !== undefined)`. No new classes, tokens or dependencies. Request: `docs/requests/teisoro-select-labelledby.md`.
|
|
18
|
+
|
|
3
19
|
## 1.45.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Accordion,
|
|
10
10
|
ActionBar,
|
|
11
11
|
ActionMenu,
|
|
12
|
+
AppShell,
|
|
12
13
|
BarChart,
|
|
13
14
|
ChartLegend,
|
|
14
15
|
ColumnChart,
|
|
@@ -110,7 +111,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
|
|
|
110
111
|
</Field>
|
|
111
112
|
```
|
|
112
113
|
|
|
113
|
-
Every press control and link takes an explicit `tabindex="0"`: `Button` (and the controls built on it, `CalendarButton` and `InfoTip`), `Box as="a"` with an `href`, `SectionNav` links, the `Select`, `MultiSelect` and `ActionMenu` triggers, `SegmentedControl` items (every item, as outside Safari: its arrow keys change the value, not the focus, so it is not a roving group), `DateField`'s calendar button and month steps, a search `Field`'s clear button, a `Table`'s sort and `TableCell onPress` buttons, `Checkbox`, `Switch`, and the checked radio of a `RadioGroup` or `FilterChips` (the first enabled one while none is; the others keep no `tabindex`, so each browser's arrow-key group rule stands). Safari's default (its "Press Tab to highlight each item" setting off) leaves a button, a link, a checkbox and a radio out of the Tab order unless it carries one, so a Safari user would Tab past them; the explicit `0` gives them the order every other browser already does. A `tabIndex` you pass wins, and controls that are deliberately no tab stop keep their `-1`: a `Stepper`'s − and +, and a roving item (a `Tabs` tab, a menu item, a calendar day) other than the current one. A `<summary>` is already in Safari's order and takes none.
|
|
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 `ActionMenu` triggers, `SegmentedControl` items (every item, as outside Safari: its arrow keys change the value, not the focus, so it is not a roving group), `DateField`'s calendar button and month steps, a search `Field`'s clear button, a `Table`'s sort and `TableCell onPress` buttons, `Checkbox`, `Switch`, and the checked radio of a `RadioGroup` or `FilterChips` (the first enabled one while none is; the others keep no `tabindex`, so each browser's arrow-key group rule stands). Safari's default (its "Press Tab to highlight each item" setting off) leaves a button, a link, a checkbox and a radio out of the Tab order unless it carries one, so a Safari user would Tab past them; the explicit `0` gives them the order every other browser already does. A `tabIndex` you pass wins, and controls that are deliberately no tab stop keep their `-1`: a `Stepper`'s − and +, and a roving item (a `Tabs` tab, a menu item, a calendar day) other than the current one. A `<summary>` is already in Safari's order and takes none.
|
|
114
115
|
|
|
115
116
|
An element a script focuses, such as a notice or a card announced after a save, takes the same accent focus ring as the canvas's links and native controls, 2 px past its edge (`--sw-focus-ring-width` and `--sw-focus-ring-offset`), instead of the browser's outline. Give it `tabIndex={-1}`, so it takes focus from a script but is no tab stop, and put the focus on the whole card rather than its text, so the ring goes round it clear of the words; a `Box` round the card with the card's `radius` makes the ring follow its corners. As with the browser's own outline, the ring shows only when focus is visible (`:focus-visible`): after a keyboard press, not after a mouse press. The rule has zero specificity, so a component's own ring always wins.
|
|
116
117
|
|
|
@@ -144,6 +145,23 @@ A disabled native text control (`<input>`, `<select>` or `<textarea>` on the can
|
|
|
144
145
|
|
|
145
146
|
`FileField` is one file chosen with the system picker or dropped on the field, with `Field`'s `label` (above it, or hidden with `labelVisuallyHidden`), `description` and `error` (a polite live region in place of the hint, the frame's edge in the danger color, `Mark` in forced colors). Empty, it is one wide zone on the subtle fill, a button named by the label and `chooseLabel` ("Field guide Choose a PDF"), with `dropLabel` ("or drop it here") muted after it where the pointer can drag (left out on a coarse pointer). Chosen (`value={{ name, detail? }}`), it shows the consumer's `icon` (hidden from assistive technology), the file's name over the muted `detail` and a neutral button named by the label and `changeLabel`, described by the name and detail. Either button opens the picker (the native input is hidden and out of the Tab order, its `accept` the list the picker offers); `onChange` receives the `File` chosen or dropped, the first of several, and when a choice or a drop swaps the zone and the chosen row while focus is in the field, focus moves to the button now there (focus the person moved elsewhere is never taken back). A file dragged over the field draws the accent edge and tint, its words in the text color. It never reads, checks or uploads the file, and a dropped file is not filtered by `accept`: check what you receive (type, size, its header) and say why in `error`, keeping the earlier `value` or none. `disabled` blocks the picker, refuses a dropped file and dashes the frame. `id` prefixes the ids of the field's parts. An empty `label`, `chooseLabel` or `changeLabel`, a blank `dropLabel`, or a `value` with no name or a blank detail throws a `RangeError`.
|
|
146
147
|
|
|
148
|
+
`FileField` `onRemove` with `removeLabel` (set both or neither) adds Remove beside a chosen file: a neutral button after the change button in the full field (the two together at the row's end), named by its words and the label ("Remove Cover photo"), so two fields' Removes are told apart. It calls `onRemove`, and the consumer clears `value`; focus then moves to the empty field's button. Nothing is removed while the field is empty, and `disabled` disables it too. A lone `onRemove` or `removeLabel`, or blank `removeLabel` words, throws a `RangeError`.
|
|
149
|
+
|
|
150
|
+
`FileField` `compact` draws the field at a text field's height (the md control, 44 px) for a form row beside other fields, such as a cover photo beside a title. Empty, the zone holds the choose words at its start. Chosen, the row is one button, the thumbnail (or the mark) and the file's name in the body weight cut to one line with an ellipsis, named by the label and `changeLabel` ("Cover photo Choose another") and described by the name and `detail` (read, not drawn); Remove, a quiet ghost button, ends the row. The label, description and error are as in the full field.
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
<FileField
|
|
154
|
+
label="Cover photo"
|
|
155
|
+
compact
|
|
156
|
+
chooseLabel="Choose a photo"
|
|
157
|
+
changeLabel="Choose another"
|
|
158
|
+
removeLabel="Remove"
|
|
159
|
+
value={cover}
|
|
160
|
+
onChange={choose}
|
|
161
|
+
onRemove={() => setCover(null)}
|
|
162
|
+
/>
|
|
163
|
+
```
|
|
164
|
+
|
|
147
165
|
A chosen photo can show itself: `value.preview` is the address of a small image of it, such as an object URL the consumer made from the `File` (and revokes when another is chosen), drawn as a 3:2 thumbnail (a space-8 wide, a space-6 tall, cropped to fill, at the small radius) before the name in place of `icon`. It is decorative (`alt=""`): the name beside it says which file it is. A blank `preview` throws a `RangeError`.
|
|
148
166
|
|
|
149
167
|
```tsx
|
|
@@ -178,6 +196,18 @@ A chosen photo can show itself: `value.preview` is the address of a small image
|
|
|
178
196
|
|
|
179
197
|
`Select` is a labeled listbox menu: supply `label`, `options` (`value` and `label`), a controlled `value` and `onChange`; `size="xs"` and `labelVisuallyHidden` are for toolbar chrome, and `action` is a last listbox command that never becomes the value. The closed trigger matches `Field`: its text is centred in the control, it ends in the `Accordion` chevron, and it is as wide as its longest option (as a native select is), so it keeps its width when the value changes; past the available width the label ellipsizes. The option labels that size it are drawn by CSS from `data-label`, so the trigger's text content and accessible value are only the current label. Its label is drawn as `Field`'s (the same label words and mark inside a `<label>` that keeps the canvas type), so a `Select` beside a `Field` lines up at the label and the control.
|
|
180
198
|
|
|
199
|
+
`Select` `aria-labelledby`, in place of `label`, names the select by words the page already draws, such as a label above a choice that is a `SegmentedControl` on a wide screen and a `Select` on a phone. The select then draws no label of its own, hidden or shown, so a screen reader meets the words once (as the page's text, then as the combobox's name) instead of the page's words followed by the select's hidden copy of them; the open list is named by the same words. `required` still sets `aria-required` but draws no mark, since the page owns the words. `label` and `aria-labelledby` are exclusive: a blank `label` with no `aria-labelledby`, a blank `aria-labelledby`, or `aria-labelledby` with `label`, `labelVisuallyHidden` or `requiredMark` throws a `RangeError`. The id must name an element in the page while the select is shown: a missing one leaves the select unnamed, which neither the types nor the select can check.
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
<Text as="span" id="den-habitat" variant="label">Den habitat</Text>
|
|
203
|
+
<Select
|
|
204
|
+
aria-labelledby="den-habitat"
|
|
205
|
+
value={habitat}
|
|
206
|
+
onChange={setHabitat}
|
|
207
|
+
options={habitats}
|
|
208
|
+
/>
|
|
209
|
+
```
|
|
210
|
+
|
|
181
211
|
`Select` `placeholder` shows in the closed trigger, in the muted color, while `value` matches no option (such as `''`); it is not an option, is never selected in the list and never becomes the value, and it counts toward the trigger's width. `required` marks the label with `Field`'s `aria-hidden` asterisk and sets `aria-required` on the trigger. `error` renders a message under the control (`sw-field-error`), links it by `aria-describedby`, sets `aria-invalid` and the danger border; like `Field`'s it is announced politely from a live region that is always there, never as an alert, and an empty string is no error. A blank `placeholder` throws a `RangeError`.
|
|
182
212
|
|
|
183
213
|
```tsx
|
|
@@ -269,6 +299,22 @@ A `Button`'s own children are laid out in a row one token gap apart (spacing ste
|
|
|
269
299
|
|
|
270
300
|
On the web, `variant="neutral"` is a quiet grey fill (the muted color at 16 %, the fill of an open menu row) with its words in the text color and no border: an action beside the primary that should not draw the eye, such as a sheet's Cancel. It is web only (`webButtonVariants`; the shared `buttonVariants` in `@scalewing/tokens` and React Native do not have it). `disabledAppearance="neutral"` (default `"faded"`) draws a disabled button on that grey with its words in the muted color at 48 % (about #AEAEAE on the grey) and full opacity, whatever its variant, instead of fading its own fill (a disabled accent reads as a paler accent); a `ghost` button, which has no fill, still fades. The button is still a disabled `button`; forced colors draw it as any disabled button. An unknown `disabledAppearance` throws a `RangeError`.
|
|
271
301
|
|
|
302
|
+
`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
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
<Inline gap={2}>
|
|
306
|
+
<Button variant="neutral" width="full" onPress={edit}>
|
|
307
|
+
Edit
|
|
308
|
+
</Button>
|
|
309
|
+
<ActionMenu
|
|
310
|
+
label="More for Heron log"
|
|
311
|
+
items={items}
|
|
312
|
+
trigger={<Dots />}
|
|
313
|
+
triggerAppearance="icon"
|
|
314
|
+
/>
|
|
315
|
+
</Inline>
|
|
316
|
+
```
|
|
317
|
+
|
|
272
318
|
A `Button` with `aria-disabled="true"` (not `disabled`) looks disabled, faded or, with `disabledAppearance="neutral"`, on the neutral grey, with `GrayText` in forced colors, yet stays a focusable, pressable `button`: its `onPress` still runs, so a submit pressed while off can take the person to what is missing. Its look alone does not say what is missing: a press while off should show the message of the first thing to fix. Use `disabled` when a press should do nothing.
|
|
273
319
|
|
|
274
320
|
A `Button` that toggles something passes `aria-pressed`. The pressed button gets a 2 px accent ring outside its fill, past a 2 px gap in `--sw-color-background`, so the ring meets only the page and keeps the accent's contrast on the canvas (4.5:1 or more in every palette, at least the 3:1 a state indicator needs) whatever its `variant` or fill. A focused pressed button moves its focus outline out past the ring. In forced colors, which drop shadows, the pressed button keeps the forced button colors, so its label and any glyph or badge inside it stay readable, and draws the same ring and its own border in the system highlight (`Highlight`); nothing opts out of forced colors. An unpressed button is drawn at full strength, so its label keeps its contrast. To make the choice stand out further, give the pressed button `primary` and the others `secondary`.
|
|
@@ -472,7 +518,7 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
|
|
|
472
518
|
|
|
473
519
|
`appearance="words"` (opt-in; default `"default"`, `navAppearances`) draws every destination, a link or a `Button` of any variant, as plain words: no fill, border or underline, label type, the others in the muted color at medium weight and the current one (`aria-current`, not `"false"`) in the text color at label weight. A Button keeps its size, padding, touch target and focus ring; hover lifts a destination to the text color where the pointer can hover, never a disabled one. The current mark is color and weight, so each destination's words must be visible: a glyph-only destination has no weight to change (keep `default` for a glyph rail). Forced colors draw the current one's border in Highlight and underline its words. An unknown value throws a `RangeError`.
|
|
474
520
|
|
|
475
|
-
`NavCount` puts a count after a destination's words, such as the tasks waiting in an area: place it inside the link or `Button`, after its words (`<a href="/tasks">Tasks<NavCount count={8} /></a>`). It is the pill `SectionNav`'s item `count` draws (the danger color with its on-color figures, an edge in forced colors), a space-2 after the words in a link or a Button, the link keeping its siblings' height, and it is read as part of the destination's name ("Tasks 8"). `count` is a whole number; 0 shows nothing. `countLabel` says what it means in your words ("8 tasks to do"): it is read in place of the bare number ("Tasks 8 tasks to do") and the pill is hidden from assistive technology. `tone="accent"` (default `"danger"`, `navCountTones`) draws the pill in the accent color with its on-color figures, for a count that is news rather than work waiting, such as what was added since the person last looked; its size, spacing and name are the same. Give an accent count a `countLabel` ("3 new") so its words, not its color, say it is
|
|
521
|
+
`NavCount` puts a count after a destination's words, such as the tasks waiting in an area: place it inside the link or `Button`, after its words (`<a href="/tasks">Tasks<NavCount count={8} /></a>`). It is the pill `SectionNav`'s item `count` draws (the danger color with its on-color figures, an edge in forced colors), a space-2 after the words in a link or a Button, the link keeping its siblings' height, and it is read as part of the destination's name ("Tasks 8"). `count` is a whole number; 0 shows nothing. `countLabel` says what it means in your words ("8 tasks to do"): it is read in place of the bare number ("Tasks 8 tasks to do") and the pill is hidden from assistive technology. `tone="accent"` (default `"danger"`, `navCountTones`) draws the pill in the accent color with its on-color figures, for a count that is news rather than work waiting, such as what was added since the person last looked; its size, spacing and name are the same. `tone="warning"` draws it in amber, for work that needs the person but is not urgent ("needs you"): the warning tint with the warning ink (a warning `Notice`'s, 4.5:1 in every palette) inside a warning edge. Give an accent or warning count a `countLabel` ("3 new", "2 to confirm") so its words, not its color, say what it is, and place either tone on a neutral or ghost destination, never a filled one of the same color. A count that is not a whole number from 0, a blank `countLabel` or an unknown `tone` throws a `RangeError`.
|
|
476
522
|
|
|
477
523
|
```tsx
|
|
478
524
|
<Nav appearance="words" aria-label="Areas">
|
|
@@ -504,6 +550,62 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
|
|
|
504
550
|
/>
|
|
505
551
|
```
|
|
506
552
|
|
|
553
|
+
`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
|
+
|
|
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`, and an unknown `countTone` or one without a `count` throw a `RangeError`.
|
|
556
|
+
|
|
557
|
+
```tsx
|
|
558
|
+
<AppShell
|
|
559
|
+
navLabel="Field station"
|
|
560
|
+
groups={[
|
|
561
|
+
{
|
|
562
|
+
id: 'field',
|
|
563
|
+
items: [
|
|
564
|
+
{
|
|
565
|
+
id: 'today',
|
|
566
|
+
label: 'Today',
|
|
567
|
+
href: '/today',
|
|
568
|
+
icon: <CalendarDays />,
|
|
569
|
+
current: 'page',
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
id: 'reserves',
|
|
575
|
+
label: 'Reserves',
|
|
576
|
+
items: [
|
|
577
|
+
{
|
|
578
|
+
id: 'wetlands',
|
|
579
|
+
label: 'Wetlands',
|
|
580
|
+
href: '/wetlands',
|
|
581
|
+
icon: <Waves />,
|
|
582
|
+
count: 3,
|
|
583
|
+
countLabel: '3 sightings to check',
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
id: 'forests',
|
|
587
|
+
label: 'Forests',
|
|
588
|
+
href: '/forests',
|
|
589
|
+
icon: <Trees />,
|
|
590
|
+
count: 2,
|
|
591
|
+
countLabel: '2 to confirm',
|
|
592
|
+
countTone: 'warning',
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
]}
|
|
597
|
+
onNavigate={(item) => navigate(item.href)}
|
|
598
|
+
header={<StationMenu />}
|
|
599
|
+
railHeader={<StationTile />}
|
|
600
|
+
footer={<AccountMenu />}
|
|
601
|
+
barTitle={<Text variant="label">Heron Marsh</Text>}
|
|
602
|
+
menuIcon={<Menu />}
|
|
603
|
+
labels={{ openMenu: 'Open menu', closeMenu: 'Close menu' }}
|
|
604
|
+
>
|
|
605
|
+
<Box padding={5}>{page}</Box>
|
|
606
|
+
</AppShell>
|
|
607
|
+
```
|
|
608
|
+
|
|
507
609
|
```tsx
|
|
508
610
|
<Stack gap={3}>
|
|
509
611
|
<Box paddingX={4}>
|
|
@@ -840,7 +942,7 @@ const { headings, omissions } = inspectMarkdown(page.body, {
|
|
|
840
942
|
</Card>
|
|
841
943
|
```
|
|
842
944
|
|
|
843
|
-
`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`. 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`.
|
|
945
|
+
`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`.
|
|
844
946
|
|
|
845
947
|
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.
|
|
846
948
|
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type AppShellNavGroup, type AppShellProps } from './components/AppShell.js';
|
|
2
|
+
/** A field station's destinations: one bare group and one labelled. */
|
|
3
|
+
export declare const habitats: AppShellNavGroup[];
|
|
4
|
+
/** The shell with its required words, a page, and any prop overridden. */
|
|
5
|
+
export declare function renderShell(props?: Partial<AppShellProps>): import("@testing-library/react").RenderResult<typeof import("@testing-library/dom/types/queries.js"), HTMLElement, HTMLElement>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { AppShell, } from './components/AppShell.js';
|
|
4
|
+
/** A field station's destinations: one bare group and one labelled. */
|
|
5
|
+
export const habitats = [
|
|
6
|
+
{
|
|
7
|
+
id: 'field',
|
|
8
|
+
items: [
|
|
9
|
+
{
|
|
10
|
+
id: 'today',
|
|
11
|
+
label: 'Today',
|
|
12
|
+
href: '/today',
|
|
13
|
+
icon: _jsx("svg", { "data-glyph": "today" }),
|
|
14
|
+
current: 'page',
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'reserves',
|
|
20
|
+
label: 'Reserves',
|
|
21
|
+
items: [
|
|
22
|
+
{
|
|
23
|
+
id: 'wetlands',
|
|
24
|
+
label: 'Wetlands',
|
|
25
|
+
href: '/wetlands',
|
|
26
|
+
icon: _jsx("svg", {}),
|
|
27
|
+
count: 3,
|
|
28
|
+
countLabel: '3 sightings to check',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'forests',
|
|
32
|
+
label: 'Forests',
|
|
33
|
+
href: '/forests',
|
|
34
|
+
icon: _jsx("svg", {}),
|
|
35
|
+
count: 2,
|
|
36
|
+
countTone: 'warning',
|
|
37
|
+
},
|
|
38
|
+
{ id: 'dunes', label: 'Dunes', href: '/dunes', icon: _jsx("svg", {}), count: 0 },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
/** The shell with its required words, a page, and any prop overridden. */
|
|
43
|
+
export function renderShell(props = {}) {
|
|
44
|
+
return render(_jsx(AppShell, { barTitle: "Heron Marsh", groups: habitats, labels: { openMenu: 'Open menu', closeMenu: 'Close menu' }, menuIcon: _jsx("svg", { "data-glyph": "menu" }), navLabel: "Field station", ...props, children: _jsx("p", { children: "Otters seen at dawn." }) }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type AppShellProps } from './app-shell/types.js';
|
|
2
|
+
export type { AppShellLabels, AppShellNavGroup, AppShellNavItem, AppShellProps, } from './app-shell/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A web workspace's frame: a pinned side navigation of grouped
|
|
5
|
+
* destinations beside the page (`main`), which scrolls with the document.
|
|
6
|
+
* From 75rem the sidebar is full (glyphs, labels, counts and group
|
|
7
|
+
* captions); from `lg` to 75rem it is a rail of glyphs with tooltips; below
|
|
8
|
+
* `lg` a sticky top bar's menu button opens the same navigation in a modal
|
|
9
|
+
* drawer from the inline start. Invalid groups, items or words throw a
|
|
10
|
+
* `RangeError`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function AppShell({ barEnd, barTitle, children, footer, groups, header, labels, menuIcon, navLabel, onNavigate, railFooter, railHeader, }: AppShellProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { breakpointQuery } from '../css/breakpoints.js';
|
|
5
|
+
import { appShellWideQuery } from '../css/css-app-shell.js';
|
|
6
|
+
import { AppHeader } from './AppHeader.js';
|
|
7
|
+
import { AppShellDrawer } from './app-shell/AppShellDrawer.js';
|
|
8
|
+
import { AppShellSidebar } from './app-shell/AppShellSidebar.js';
|
|
9
|
+
import { assertAppShellProps } from './app-shell/validate.js';
|
|
10
|
+
import { Button } from './Button.js';
|
|
11
|
+
import { useMediaQuery } from './use-media-query.js';
|
|
12
|
+
const fromLg = breakpointQuery('from', 'lg');
|
|
13
|
+
/**
|
|
14
|
+
* A web workspace's frame: a pinned side navigation of grouped
|
|
15
|
+
* destinations beside the page (`main`), which scrolls with the document.
|
|
16
|
+
* From 75rem the sidebar is full (glyphs, labels, counts and group
|
|
17
|
+
* captions); from `lg` to 75rem it is a rail of glyphs with tooltips; below
|
|
18
|
+
* `lg` a sticky top bar's menu button opens the same navigation in a modal
|
|
19
|
+
* drawer from the inline start. Invalid groups, items or words throw a
|
|
20
|
+
* `RangeError`.
|
|
21
|
+
*/
|
|
22
|
+
export function AppShell({ barEnd, barTitle, children, footer, groups, header, labels, menuIcon, navLabel, onNavigate, railFooter, railHeader, }) {
|
|
23
|
+
assertAppShellProps({ groups, labels, navLabel });
|
|
24
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
25
|
+
const menuRef = useRef(null);
|
|
26
|
+
// Set by a dismissal (Escape, a press outside, the close button): only
|
|
27
|
+
// that hands focus back to the menu button.
|
|
28
|
+
const refocusRef = useRef(false);
|
|
29
|
+
const sidebarShown = useMediaQuery(fromLg, false);
|
|
30
|
+
const wide = useMediaQuery(appShellWideQuery, false);
|
|
31
|
+
// The drawer belongs to the bar, which is gone from `lg` up: a window
|
|
32
|
+
// widened while it is open closes it.
|
|
33
|
+
if (sidebarShown && menuOpen)
|
|
34
|
+
setMenuOpen(false);
|
|
35
|
+
// After a dismissal (the drawer's own effect closes it first), focus
|
|
36
|
+
// returns to the menu button. A chosen destination leaves focus to the
|
|
37
|
+
// page it moves to, and a widened window has no menu button to focus.
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (menuOpen || !refocusRef.current)
|
|
40
|
+
return;
|
|
41
|
+
refocusRef.current = false;
|
|
42
|
+
menuRef.current?.focus();
|
|
43
|
+
}, [menuOpen]);
|
|
44
|
+
return (_jsxs("div", { className: "sw-app-shell", children: [_jsxs(AppHeader, { appearance: "bar", className: "sw-app-shell-bar", padding: 2, children: [_jsx(Button, { ref: menuRef, "aria-expanded": menuOpen, "aria-haspopup": "dialog", "aria-label": labels.openMenu, className: "sw-app-shell-menu", onPress: () => setMenuOpen(true), size: "sm", variant: "ghost", children: _jsx("span", { "aria-hidden": "true", className: "sw-app-shell-icon", children: menuIcon }) }), _jsx("div", { className: "sw-app-shell-bar-title", children: barTitle }), barEnd ? _jsx("div", { className: "sw-app-shell-bar-end", children: barEnd }) : null] }), _jsx(AppShellSidebar, { footer: footer, groups: groups, header: header, navLabel: navLabel, onNavigate: onNavigate, rail: sidebarShown && !wide, railFooter: railFooter, railHeader: railHeader }), _jsx("main", { className: "sw-app-shell-main", children: children }), _jsx(AppShellDrawer, { closeLabel: labels.closeMenu, footer: footer, groups: groups, header: header, navLabel: navLabel, onChoose: () => setMenuOpen(false), onClose: () => {
|
|
45
|
+
refocusRef.current = true;
|
|
46
|
+
setMenuOpen(false);
|
|
47
|
+
}, onNavigate: onNavigate, open: menuOpen })] }));
|
|
48
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type ButtonSize } from '@scalewing/tokens';
|
|
2
2
|
import { type ButtonDisabledAppearance, type WebButtonVariant } from '../css/css-button.js';
|
|
3
|
+
import { type ButtonWidth } from '../css/css-button-width.js';
|
|
3
4
|
import { type ButtonHTMLAttributes, type ReactNode } from 'react';
|
|
4
5
|
/** The web variants: the shared tiers plus `neutral`. */
|
|
5
6
|
export type ButtonVariant = WebButtonVariant;
|
|
6
|
-
export type { ButtonDisabledAppearance, ButtonSize };
|
|
7
|
+
export type { ButtonDisabledAppearance, ButtonSize, ButtonWidth };
|
|
7
8
|
export type ButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'type'> & {
|
|
8
9
|
children: ReactNode;
|
|
9
10
|
onPress: () => void;
|
|
@@ -21,6 +22,12 @@ export type ButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick
|
|
|
21
22
|
* whatever its variant (a ghost button still fades).
|
|
22
23
|
*/
|
|
23
24
|
disabledAppearance?: ButtonDisabledAppearance;
|
|
25
|
+
/**
|
|
26
|
+
* `content` (default) is as wide as the words; `full` fills the
|
|
27
|
+
* container, and in an Inline row the space its siblings leave, such as
|
|
28
|
+
* an Edit beside a ••• menu.
|
|
29
|
+
*/
|
|
30
|
+
width?: ButtonWidth;
|
|
24
31
|
};
|
|
25
32
|
export declare const Button: import("react").ForwardRefExoticComponent<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "type"> & {
|
|
26
33
|
children: ReactNode;
|
|
@@ -39,4 +46,10 @@ export declare const Button: import("react").ForwardRefExoticComponent<Omit<Butt
|
|
|
39
46
|
* whatever its variant (a ghost button still fades).
|
|
40
47
|
*/
|
|
41
48
|
disabledAppearance?: ButtonDisabledAppearance;
|
|
49
|
+
/**
|
|
50
|
+
* `content` (default) is as wide as the words; `full` fills the
|
|
51
|
+
* container, and in an Inline row the space its siblings leave, such as
|
|
52
|
+
* an Edit beside a ••• menu.
|
|
53
|
+
*/
|
|
54
|
+
width?: ButtonWidth;
|
|
42
55
|
} & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { buttonClassNames, buttonDisabledAppearances, } from '../css/css-button.js';
|
|
3
|
+
import { buttonWidthClass, buttonWidths, } from '../css/css-button-width.js';
|
|
3
4
|
import { forwardRef } from 'react';
|
|
4
5
|
import { cx } from '../class-names.js';
|
|
5
6
|
import { tabStop } from '../tab-stop.js';
|
|
6
|
-
export const Button = forwardRef(function Button({ children, className, disabled = false, disabledAppearance = 'faded', onPress, size = 'md', tabIndex, type = 'button', variant = 'primary', ...rest }, ref) {
|
|
7
|
+
export const Button = forwardRef(function Button({ children, className, disabled = false, disabledAppearance = 'faded', onPress, size = 'md', tabIndex, type = 'button', variant = 'primary', width = 'content', ...rest }, ref) {
|
|
7
8
|
if (!buttonDisabledAppearances.includes(disabledAppearance))
|
|
8
9
|
throw new RangeError(`Unknown Button disabledAppearance: ${String(disabledAppearance)}`);
|
|
9
|
-
|
|
10
|
+
if (!buttonWidths.includes(width))
|
|
11
|
+
throw new RangeError(`Unknown Button width: ${String(width)}`);
|
|
12
|
+
return (_jsx("button", { ref: ref, className: cx(...buttonClassNames({ size, variant, disabledAppearance }), buttonWidthClass(width), className), disabled: disabled, tabIndex: tabIndex ?? tabStop, type: type, ...rest, onClick: onPress, children: children }));
|
|
10
13
|
});
|
|
@@ -42,6 +42,21 @@ export type FileFieldProps = {
|
|
|
42
42
|
*/
|
|
43
43
|
icon?: ReactNode;
|
|
44
44
|
disabled?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The field at a text field's height, for a form row beside other
|
|
47
|
+
* fields: empty, one button with `chooseLabel`; chosen, the file's mark
|
|
48
|
+
* and its name cut to one line, pressed to choose another (named by
|
|
49
|
+
* `label` and `changeLabel`), the detail read but not drawn. Default
|
|
50
|
+
* false: the wide zone, and the chosen row with its change button.
|
|
51
|
+
*/
|
|
52
|
+
compact?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Called by Remove, drawn beside a chosen file when set with
|
|
55
|
+
* `removeLabel`: the consumer clears `value`. Set both or neither.
|
|
56
|
+
*/
|
|
57
|
+
onRemove?: () => void;
|
|
58
|
+
/** Remove's words, such as "Remove"; named with `label` ("Remove Cover photo"). */
|
|
59
|
+
removeLabel?: string;
|
|
45
60
|
/** Prefixes the ids the field gives its parts (its button is `${id}-button`). */
|
|
46
61
|
id?: string;
|
|
47
62
|
};
|
|
@@ -49,7 +64,8 @@ export type FileFieldProps = {
|
|
|
49
64
|
* One file chosen by a press or dropped on the field: empty, a wide zone
|
|
50
65
|
* that opens the system picker; chosen, the file's name and detail (after
|
|
51
66
|
* a thumbnail for a photo with a `preview`) with a button to choose
|
|
52
|
-
* another
|
|
67
|
+
* another, and Remove with `onRemove`. `compact` draws both at a text
|
|
68
|
+
* field's height. It hands the `File` to the consumer and shows what the
|
|
53
69
|
* consumer passes back as `value`; it never reads or uploads it.
|
|
54
70
|
*/
|
|
55
|
-
export declare function FileField({ label, labelVisuallyHidden, description, error, accept, value, onChange, chooseLabel, changeLabel, dropLabel, icon, disabled, id, }: FileFieldProps): import("react").JSX.Element;
|
|
71
|
+
export declare function FileField({ label, labelVisuallyHidden, description, error, accept, value, onChange, chooseLabel, changeLabel, dropLabel, icon, disabled, compact, onRemove, removeLabel, id, }: FileFieldProps): import("react").JSX.Element;
|
|
@@ -1,39 +1,33 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useId, useRef } from 'react';
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
5
|
import { tabStop } from '../tab-stop.js';
|
|
6
|
-
import { Button } from './Button.js';
|
|
7
6
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
8
7
|
import { FieldLabelText } from './FieldLabelText.js';
|
|
8
|
+
import { ChosenFileCompact, ChosenFileRow } from './file-field/ChosenFile.js';
|
|
9
9
|
import { assertFileField } from './file-field/file-field-checks.js';
|
|
10
10
|
import { useFileDrop } from './file-field/use-file-drop.js';
|
|
11
11
|
import { useSwapFocus } from './file-field/use-swap-focus.js';
|
|
12
12
|
import { Stack } from './Stack.js';
|
|
13
|
-
/** Whether the consumer passed a mark to draw (not empty JSX). */
|
|
14
|
-
function hasMark(icon) {
|
|
15
|
-
return (icon !== undefined &&
|
|
16
|
-
icon !== null &&
|
|
17
|
-
icon !== '' &&
|
|
18
|
-
typeof icon !== 'boolean');
|
|
19
|
-
}
|
|
20
|
-
/** Before a chosen file's name: its photo's thumbnail, else the consumer's mark. */
|
|
21
|
-
function ChosenMark({ icon, preview }) {
|
|
22
|
-
if (preview !== undefined)
|
|
23
|
-
return _jsx("img", { alt: "", className: "sw-file-field-preview", src: preview });
|
|
24
|
-
if (!hasMark(icon))
|
|
25
|
-
return null;
|
|
26
|
-
return (_jsx("span", { "aria-hidden": "true", className: "sw-file-field-icon", children: icon }));
|
|
27
|
-
}
|
|
28
13
|
/**
|
|
29
14
|
* One file chosen by a press or dropped on the field: empty, a wide zone
|
|
30
15
|
* that opens the system picker; chosen, the file's name and detail (after
|
|
31
16
|
* a thumbnail for a photo with a `preview`) with a button to choose
|
|
32
|
-
* another
|
|
17
|
+
* another, and Remove with `onRemove`. `compact` draws both at a text
|
|
18
|
+
* field's height. It hands the `File` to the consumer and shows what the
|
|
33
19
|
* consumer passes back as `value`; it never reads or uploads it.
|
|
34
20
|
*/
|
|
35
|
-
export function FileField({ label, labelVisuallyHidden = false, description, error, accept, value = null, onChange, chooseLabel, changeLabel, dropLabel, icon, disabled = false, id, }) {
|
|
36
|
-
assertFileField({
|
|
21
|
+
export function FileField({ label, labelVisuallyHidden = false, description, error, accept, value = null, onChange, chooseLabel, changeLabel, dropLabel, icon, disabled = false, compact = false, onRemove, removeLabel, id, }) {
|
|
22
|
+
assertFileField({
|
|
23
|
+
label,
|
|
24
|
+
chooseLabel,
|
|
25
|
+
changeLabel,
|
|
26
|
+
dropLabel,
|
|
27
|
+
value,
|
|
28
|
+
onRemove,
|
|
29
|
+
removeLabel,
|
|
30
|
+
});
|
|
37
31
|
const ownId = useId();
|
|
38
32
|
const baseId = id ?? ownId;
|
|
39
33
|
const inputRef = useRef(null);
|
|
@@ -55,11 +49,17 @@ export function FileField({ label, labelVisuallyHidden = false, description, err
|
|
|
55
49
|
detail: `${baseId}-detail`,
|
|
56
50
|
hint: `${baseId}-hint`,
|
|
57
51
|
error: `${baseId}-error`,
|
|
52
|
+
remove: `${baseId}-remove`,
|
|
58
53
|
};
|
|
54
|
+
const compactChosen = compact && chosen;
|
|
59
55
|
// The error is in the button's description and the polite region; the
|
|
60
56
|
// button role takes no aria-invalid.
|
|
57
|
+
// Compact and chosen, the button holds the file's name: it is named by
|
|
58
|
+
// the label and its hidden change words, the name only describing it.
|
|
61
59
|
const naming = {
|
|
62
|
-
'aria-labelledby':
|
|
60
|
+
'aria-labelledby': compactChosen
|
|
61
|
+
? `${ids.label} ${ids.button}-words`
|
|
62
|
+
: `${ids.label} ${ids.button}`,
|
|
63
63
|
'aria-describedby': [
|
|
64
64
|
chosen && ids.name,
|
|
65
65
|
chosen && value.detail && ids.detail,
|
|
@@ -71,5 +71,9 @@ export function FileField({ label, labelVisuallyHidden = false, description, err
|
|
|
71
71
|
id: ids.button,
|
|
72
72
|
};
|
|
73
73
|
const open = () => inputRef.current?.click();
|
|
74
|
-
|
|
74
|
+
const remove = onRemove !== undefined && removeLabel !== undefined
|
|
75
|
+
? { label: removeLabel, onRemove }
|
|
76
|
+
: null;
|
|
77
|
+
const ChosenFile = compact ? ChosenFileCompact : ChosenFileRow;
|
|
78
|
+
return (_jsxs(Stack, { className: cx('sw-field', 'sw-file-field', error && 'sw-field-invalid'), gap: labelVisuallyHidden ? 0 : 1, children: [_jsx("span", { id: ids.label, children: _jsx(FieldLabelText, { label: label, visuallyHidden: labelVisuallyHidden }) }), _jsxs("div", { className: cx('sw-file-field-frame', chosen ? 'sw-file-field-chosen' : 'sw-file-field-empty', compact && 'sw-file-field-compact', drop.dragging && 'sw-file-field-dragging', disabled && 'sw-file-field-disabled'), ...drop.handlers, ...focusHandlers, children: [_jsx("input", { accept: accept, "aria-label": label, disabled: disabled, hidden: true, onChange: onInput, ref: inputRef, tabIndex: -1, type: "file" }), chosen ? (_jsx(ChosenFile, { buttonRef: buttonRef, changeLabel: changeLabel, disabled: disabled, icon: icon, ids: ids, naming: naming, onChange: open, remove: remove, value: value })) : (_jsxs("button", { ...naming, className: "sw-file-field-zone", disabled: disabled, onClick: open, ref: buttonRef, tabIndex: tabStop, type: "button", children: [_jsx("span", { className: "sw-file-field-choose", children: chooseLabel }), dropLabel ? ' ' : null, dropLabel ? (_jsx("span", { className: "sw-file-field-drop", children: dropLabel })) : null] }))] }), description && !error ? (_jsx("span", { className: "sw-field-description", id: ids.hint, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-field-error", id: ids.error, message: error })] }));
|
|
75
79
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
+
import { type ListGroupTileColumns } from '../css/css-list-tiles.js';
|
|
2
3
|
import { type ListGroupMaxHeight, type ListGroupVariant } from '../list-options.js';
|
|
3
4
|
export type ListGroupProps = {
|
|
4
5
|
/** `ListRow` elements; the group draws a hairline between each pair. */
|
|
@@ -8,9 +9,13 @@ export type ListGroupProps = {
|
|
|
8
9
|
'aria-labelledby'?: string;
|
|
9
10
|
/**
|
|
10
11
|
* `panel` (the default) is one bordered panel; `plain` draws no panel
|
|
11
|
-
* and a hairline above the first row too, for rows inside a Card
|
|
12
|
+
* and a hairline above the first row too, for rows inside a Card;
|
|
13
|
+
* `tiles` sets each row on a panel of its own, in `tileColumns` equal
|
|
14
|
+
* columns from `md` up and one a row below it, still one list.
|
|
12
15
|
*/
|
|
13
16
|
variant?: ListGroupVariant;
|
|
17
|
+
/** How many tiles share a row from `md` up (`tiles` only): 2, 3 (default) or 4. */
|
|
18
|
+
tileColumns?: ListGroupTileColumns;
|
|
14
19
|
/**
|
|
15
20
|
* Bounds the list to a row count of its own region, `{ rows }` rows of a
|
|
16
21
|
* title and one detail line, where the rest scroll; the region is a named
|
|
@@ -27,7 +32,7 @@ export type ListGroupProps = {
|
|
|
27
32
|
};
|
|
28
33
|
/**
|
|
29
34
|
* Rows in one list (`ul`, its list role kept) with hairlines between them:
|
|
30
|
-
* a panel of its own, or plain rows inside a surface
|
|
31
|
-
* outer box.
|
|
35
|
+
* a panel of its own, or plain rows inside a surface; or the same list's
|
|
36
|
+
* rows as tiles, each a panel. The ref reaches the outer box.
|
|
32
37
|
*/
|
|
33
38
|
export declare const ListGroup: import("react").ForwardRefExoticComponent<ListGroupProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -2,18 +2,26 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { forwardRef } from 'react';
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
|
+
import { listTileColumnsClass, } from '../css/css-list-tiles.js';
|
|
5
6
|
import { assertListGroup, listMaxHeightStyle, } from '../list-options.js';
|
|
6
7
|
import { ListRowContainer } from './list/list-row-container.js';
|
|
7
8
|
import { ScrollRegion } from './ScrollRegion.js';
|
|
8
9
|
/**
|
|
9
10
|
* Rows in one list (`ul`, its list role kept) with hairlines between them:
|
|
10
|
-
* a panel of its own, or plain rows inside a surface
|
|
11
|
-
* outer box.
|
|
11
|
+
* a panel of its own, or plain rows inside a surface; or the same list's
|
|
12
|
+
* rows as tiles, each a panel. The ref reaches the outer box.
|
|
12
13
|
*/
|
|
13
|
-
export const ListGroup = forwardRef(function ListGroup({ children, 'aria-label': label, 'aria-labelledby': labelledBy, variant = 'panel', maxHeight, footer, className, id, }, ref) {
|
|
14
|
-
assertListGroup({
|
|
14
|
+
export const ListGroup = forwardRef(function ListGroup({ children, 'aria-label': label, 'aria-labelledby': labelledBy, variant = 'panel', tileColumns, maxHeight, footer, className, id, }, ref) {
|
|
15
|
+
assertListGroup({
|
|
16
|
+
label,
|
|
17
|
+
labelledBy,
|
|
18
|
+
variant,
|
|
19
|
+
maxHeight,
|
|
20
|
+
tileColumns,
|
|
21
|
+
hasFooter: footer !== undefined,
|
|
22
|
+
});
|
|
15
23
|
const list = (_jsx("ul", { "aria-label": label, "aria-labelledby": labelledBy, className: "sw-list",
|
|
16
24
|
// A list styled without bullets loses its role in Safari.
|
|
17
25
|
role: "list", children: _jsx(ListRowContainer.Provider, { value: "li", children: children }) }));
|
|
18
|
-
return (_jsxs("div", { className: cx('sw-list-group', `sw-list-group-${variant}`, className), id: id, ref: ref, children: [maxHeight === undefined ? (list) : (_jsx(ScrollRegion, { "aria-label": label, "aria-labelledby": labelledBy, className: "sw-list-group-scroll", style: listMaxHeightStyle(maxHeight), children: list })), footer === undefined ? null : (_jsx("div", { className: "sw-list-group-footer", children: _jsx(ListRowContainer.Provider, { value: "div", children: footer }) }))] }));
|
|
26
|
+
return (_jsxs("div", { className: cx('sw-list-group', `sw-list-group-${variant}`, variant === 'tiles' && listTileColumnsClass(tileColumns ?? 3), className), id: id, ref: ref, children: [maxHeight === undefined ? (list) : (_jsx(ScrollRegion, { "aria-label": label, "aria-labelledby": labelledBy, className: "sw-list-group-scroll", style: listMaxHeightStyle(maxHeight), children: list })), footer === undefined ? null : (_jsx("div", { className: "sw-list-group-footer", children: _jsx(ListRowContainer.Provider, { value: "div", children: footer }) }))] }));
|
|
19
27
|
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The pill's colours: `danger` for work waiting, `accent` for news,
|
|
3
|
+
* `warning` for work that needs the person but is not urgent.
|
|
4
|
+
*/
|
|
5
|
+
export declare const navCountTones: readonly ["danger", "accent", "warning"];
|
|
3
6
|
export type NavCountTone = (typeof navCountTones)[number];
|
|
4
7
|
export type NavCountProps = {
|
|
5
8
|
/**
|
|
@@ -17,8 +20,11 @@ export type NavCountProps = {
|
|
|
17
20
|
/**
|
|
18
21
|
* The pill's colour. `danger` (the default) is work waiting there, such
|
|
19
22
|
* as tasks to do; `accent` is news, such as what was added since the
|
|
20
|
-
* person last looked, where nothing is overdue
|
|
21
|
-
*
|
|
23
|
+
* person last looked, where nothing is overdue; `warning` is amber, work
|
|
24
|
+
* that needs the person but is not urgent ("needs you"): the warning
|
|
25
|
+
* tint with the warning ink (4.5:1 in every palette) inside a warning
|
|
26
|
+
* edge. Give an accent or warning count a `countLabel` ("3 new", "2 to
|
|
27
|
+
* confirm"), so its words, not its colour, say what it is.
|
|
22
28
|
* Place either tone on a neutral or ghost destination: a filled one of
|
|
23
29
|
* the same colour would hide it.
|
|
24
30
|
*/
|
|
@@ -29,6 +35,7 @@ export type NavCountProps = {
|
|
|
29
35
|
* Button: `<a href="/tasks">Tasks<NavCount count={8} /></a>`. It is the
|
|
30
36
|
* pill SectionNav's item count draws, and it is part of the destination's
|
|
31
37
|
* accessible name, as there. `tone="accent"` draws it in the accent colour,
|
|
32
|
-
* for news rather than work
|
|
38
|
+
* for news rather than work; `tone="warning"` in amber, for work that needs
|
|
39
|
+
* the person but is not urgent.
|
|
33
40
|
*/
|
|
34
41
|
export declare function NavCount({ count, countLabel, tone, }: NavCountProps): import("react").JSX.Element | null;
|