@scalewing/react 1.51.0 → 1.53.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 +20 -0
- package/README.md +14 -2
- package/dist/components/Checkbox.d.ts +18 -1
- package/dist/components/Checkbox.js +15 -5
- package/dist/components/Dialog.d.ts +14 -1
- package/dist/components/Dialog.js +5 -2
- package/dist/components/PasteRows.d.ts +55 -0
- package/dist/components/PasteRows.js +55 -0
- package/dist/components/SiteHeader.d.ts +6 -3
- package/dist/components/SiteHeader.js +8 -4
- package/dist/components/Table.d.ts +1 -0
- package/dist/components/Table.js +1 -0
- package/dist/components/checkbox/CheckboxControl.d.ts +1 -1
- package/dist/components/checkbox/CheckboxControl.js +4 -3
- package/dist/components/dialog/validate.d.ts +3 -1
- package/dist/components/dialog/validate.js +3 -1
- package/dist/components/paste-rows/parse-pasted-rows.d.ts +26 -0
- package/dist/components/paste-rows/parse-pasted-rows.js +123 -0
- package/dist/components/site-header/types.d.ts +16 -1
- package/dist/components/site-header/validate.d.ts +4 -3
- package/dist/components/site-header/validate.js +12 -3
- package/dist/components/table/TableSelectCell.d.ts +26 -0
- package/dist/components/table/TableSelectCell.js +19 -0
- package/dist/css/css-checkbox.js +37 -3
- package/dist/css/css-components.js +3 -0
- package/dist/css/css-dialog-end.d.ts +13 -0
- package/dist/css/css-dialog-end.js +75 -0
- package/dist/css/css-select.js +10 -0
- package/dist/css/css-site-footer.js +11 -1
- package/dist/css/css-site-header-anchors.d.ts +1 -0
- package/dist/css/css-site-header-anchors.js +22 -0
- package/dist/css/css-site-header-skip-link.d.ts +1 -0
- package/dist/css/css-site-header-skip-link.js +49 -0
- package/dist/css/css-site-header.js +26 -11
- package/dist/css/css-table-select.d.ts +3 -0
- package/dist/css/css-table-select.js +24 -0
- package/dist/css/css-table.js +4 -0
- package/dist/css/site-header-geometry.d.ts +21 -0
- package/dist/css/site-header-geometry.js +28 -0
- package/dist/css/stylesheet.js +2 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/styles.css +165 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.53.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2d5e8ef: `Checkbox` `labelVisuallyHidden` (the label stays the name, not drawn; the box is its own target, 44 × 44 px on a coarse pointer) and `indeterminate` (the mixed state: a dash on the accent fill, the native `indeterminate` property and `aria-checked="mixed"`). `TableSelectCell` (`label`, `checked`, `indeterminate?`, `onCheckedChange`, `disabled?`, `as?: 'td' | 'th'`): a table's select column holding that Checkbox, as narrow as the box. New classes `sw-checkbox-label-hidden`, `sw-table-select`. Request: `docs/requests/futmas-table-row-selection.md`.
|
|
8
|
+
|
|
9
|
+
`Dialog` `placement` (`'center'`, the default, or `'end'`): from md up a full-height side panel at the inline end with Dialog's title row, close button, scrolling content and pinned footer; below md the centred dialog or the bottom sheet with `sheetBelow`. Exports `dialogPlacements`, `DialogPlacement`. New class `sw-dialog-end`. Request: `docs/requests/futmas-dialog-side-panel.md`.
|
|
10
|
+
|
|
11
|
+
`PasteRows`: a text area for rows pasted from a spreadsheet, a polite count of the columns found and one `Select size="xs"` per column choosing its meaning (`label`, `description?`, `value`, `onValueChange`, `columns`, `mapping`, `onMappingChange`, `countLabel`, `columnLabel?`, `delimiter?`, `rows?`, `placeholder?`, `error?`, `disabled?`, `required?`), with the pure `parsePastedRows(text, delimiter?)` (tab or comma cells, quoted cells, blank rows dropped). Exports `PasteRows`, `PasteRowsProps`, `PasteRowsColumn`, `parsePastedRows`, `PastedRows`, `PastedRowsDelimiter`, `pastedRowsDelimiters`. Request: `docs/requests/futmas-paste-rows.md`.
|
|
12
|
+
|
|
13
|
+
`Select`: on a coarse pointer the trigger and each option are at least 44 px high at every size, as every `Button` is since 1.51.0; a fine pointer keeps `xs` at 28 px, and an `xs` Select's open list is taller on touch. Request: `docs/requests/futmas-select-touch-target.md`.
|
|
14
|
+
|
|
15
|
+
No new tokens or dependencies.
|
|
16
|
+
|
|
17
|
+
## 1.52.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 03f2e78: Site chrome follow-ups from Teisoro's marketing-site UX review. `SiteHeader` takes an optional `skipLink` (`{ label, href }`, exported as `SiteHeaderSkipLink`): a "skip to content" link that is the header's first focusable element, visually hidden until it has focus, then a pill on the surface with the accent ring laid over the bar's start (`sw-site-header-skip-link`); a blank label or an `href` that is not a `#fragment` throws a `RangeError`. While a sticky `SiteHeader` is on the page, the document's `scroll-padding-top` clears the bar (56 px, 72 px from `lg`), its hairline and a small gap, so `#fragment` links, `:target` and `scrollIntoView` land below the bar; a header with `sticky={false}` changes nothing. Below `lg`, the bar's `compactActions` (now in a `sw-site-header-compact-actions` box with `display: contents`) step aside while the menu is open, since the menu holds its own `menuActions`, leaving the brand and the close button. On a coarse pointer, a plain link that is all of a `SiteFooter` bottom-row slot (`legal` or `end`) gets the 44 px touch target, as the group links have. No new tokens or dependencies. Request: `docs/requests/teisoro-site-chrome-follow-ups.md`.
|
|
22
|
+
|
|
3
23
|
## 1.51.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
DateRangeButton,
|
|
26
26
|
Card,
|
|
27
27
|
Checkbox,
|
|
28
|
+
PasteRows,
|
|
28
29
|
RadioGroup,
|
|
29
30
|
SectionNav,
|
|
30
31
|
Skeleton,
|
|
@@ -271,6 +272,8 @@ A chosen photo can show itself: `value.preview` is the address of a small image
|
|
|
271
272
|
|
|
272
273
|
`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.
|
|
273
274
|
|
|
275
|
+
On a coarse pointer every `Select` size's trigger and each option in its open list are at least 44 px high (the md control height), as every `Button` is; type and padding keep the size's, so an `xs` Select in a toolbar or a table row is still easy to tap. A fine pointer keeps `xs` at 28 px.
|
|
276
|
+
|
|
274
277
|
`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.
|
|
275
278
|
|
|
276
279
|
```tsx
|
|
@@ -475,6 +478,8 @@ A `Button` that toggles something passes `aria-pressed`. The pressed button gets
|
|
|
475
478
|
|
|
476
479
|
`Dialog` is a modal `<dialog>` controlled by `open`. Escape, a backdrop press, a platform close request, and a `<form method="dialog">` submit (or a submitter with `formmethod="dialog"`) are each prevented and call `onClose`; none of them closes the dialog itself. A consumer `onKeyDown`, `onCancel`, `onSubmit` or `onPointerDown` that prevents the event vetoes that request. Escape in a search field that holds text clears the field first, and a descendant's own `cancel` (a dismissed file picker) is not a close request. Set `open` to false to close it, or keep it true (for example while a form is saving) and it stays shown. `onClose` is not called when `open` turns false, and it is required: a dialog that must not be dismissed passes a callback that keeps `open` true. The `title` is an `h3` in the title style by default; `titleLevel={2}` renders it as an `h2` in the same style, for a dialog whose own section labels are `h3` headings, so they read as parts of the dialog and not as its peers. The default stays `3` so existing dialogs keep their outline; any value other than `2` renders an `h3`. `sheetBelow="md"` makes the dialog a bottom sheet below `md`: docked to the bottom edge at the full viewport width, top corners rounded and bottom corners square, a `space-8` strip of backdrop above it, and bottom padding that clears `env(safe-area-inset-bottom)` (with `viewport-fit=cover`); it slides up on open unless reduced motion is asked for, and has no grabber, since there is no drag. From `md` up it is the centered dialog at its `size`. `closeLabel` (such as "Close") adds an icon-only ghost close button at the end of the title row, named by the label, which calls `onClose` like Escape; the title still names the dialog. The button is the dialog's first control, so the browser focuses it on open; give a field `autoFocus` to start there instead. Without `closeLabel` the dialog has no close button and its markup is unchanged.
|
|
477
480
|
|
|
481
|
+
`Dialog` `placement="end"` (opt-in; `center` is the default) is a side panel from md up: the window's full height at the inline end (the right, the left in right-to-left), its `size`'s width (md 32rem, lg 56rem, xl 65rem, always leaving a space-8 strip of backdrop), square corners and one hairline on its inner edge, sliding in from its edge unless reduced motion is asked for. Its title row, `closeLabel` button, `description`, scrolling content and `footer` (pinned to the bottom edge even when the content is short) are Dialog's, as are its focus trap (the native modal), Escape and backdrop requests to `onClose`, and focus returning to the opener. Below md it is the centred dialog, or the bottom sheet with `sheetBelow="md"` (the usual pairing). `appearance="banded"` gives it the solid surface. An unknown placement throws a `RangeError`.
|
|
482
|
+
|
|
478
483
|
A sheet over a page takes a fuller frame. `icon` is a consumer glyph or tile before the title (hidden from assistive technology) and `description` a muted line under the title that also describes the dialog (unless you pass `aria-describedby`). `footer` holds the actions, pinned to the dialog's bottom edge under a hairline while the content scrolls, and `summary` (ActionBar's `{ label, value, detail?, tone? }`) puts a labelled figure at the footer's start in a polite live region. `size="xl"` (65rem) holds a six-column count grid with its labels and total. `role="alertdialog"` is for a warning that interrupts, such as an idle sign-out: give its safe action `autoFocus` (the browser otherwise focuses the first footer button), and point `aria-describedby` at the warning in its body when that is not the `description`. `appearance="banded"` (default `"glass"`) draws the dialog on the solid surface with the medium elevation instead of the frosted panel: its title row is a full-width band over a hairline (a lone title too), the footer the band it already is, and the close button a circle on the quiet muted fill in the text color, keeping its touch target; forced colors outline the dialog and keep the hairlines. `status` is one short line in the footer after the summary and before the actions, in the same polite live region: muted caption words, or a `Badge` and a short line in one `span` (a status alone makes a footer; blank words or `false` leave it out). `canvas="grouped"` (default `"default"`) fills the dialog with the grouped canvas grey that `ThemeProvider canvas="grouped"` paints, solid with no blur, so `Card`s inside stand apart; a banded title row and the footer keep the surface. `sheetGrabber` (with `sheetBelow`, which it needs) draws a grabber, a short rounded bar centered at the top of the bottom sheet, hidden from assistive technology; it stays at the top while the content scrolls, does not drag, and is not drawn from the breakpoint up. An unknown `size`, `role`, `appearance` or `canvas`, a blank `description`, an invalid `summary`, or `sheetGrabber` without `sheetBelow` throws a `RangeError`.
|
|
479
484
|
|
|
480
485
|
```tsx
|
|
@@ -557,6 +562,8 @@ A toast can also be a corner card that confirms a save: `placement="top-end"` pu
|
|
|
557
562
|
|
|
558
563
|
`Checkbox` is a controlled native checkbox for one independent form or confirmation choice. `checked` and `onCheckedChange` own its state. The consumer supplies `error` after validation to describe and style an invalid choice (associated by `aria-describedby` with `aria-invalid` and announced from a polite live region, as `Field`'s error is, never an alert); `required` retains native form semantics.
|
|
559
564
|
|
|
565
|
+
`Checkbox` `labelVisuallyHidden` (opt-in) keeps `label` as the checkbox's name but does not draw it, for a box that stands alone, such as a table's select column ("Select Mallard"); the box is then its own target, its drawn size (24 px) on a fine pointer and at least 44 × 44 px on a coarse one. `indeterminate` (opt-in) is the mixed state, such as "Select all" while only some rows are chosen: a dash on the accent fill (Highlight in forced colours), the native `indeterminate` property and `aria-checked="mixed"`, so Playwright's `toBeChecked({ indeterminate: true })` and `getByRole('checkbox', { checked: 'mixed' })` find it (Testing Library's `checked` query ignores the mixed state of a native checkbox; assert `aria-checked="mixed"` or the `indeterminate` property there). It wins over `checked` in the drawing and the state read out. A press reports `!checked` (with `checked` false: `true`, choose all); the box stays mixed until the props change.
|
|
566
|
+
|
|
560
567
|
`RadioGroup` is a controlled fieldset of native radio choices. Supply a unique nonempty `value` for each option, one selected `value` or an empty value for no selection, a `legend`, and `onChange`. Long labels wrap in a vertical group. The consumer provides localized option labels and validation `error`; `required` keeps native form semantics. An option's optional `icon` (a `ReactNode`, such as a Lucide glyph) sits between the radio and the label, in the text color and the option's gap; it is `aria-hidden`, so the radio's accessible name stays the `label` text, and a press on it chooses the option as the label does.
|
|
561
568
|
|
|
562
569
|
```tsx
|
|
@@ -754,6 +761,8 @@ The layout follows the window, with generated media queries only. From 75rem (12
|
|
|
754
761
|
|
|
755
762
|
`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`.
|
|
756
763
|
|
|
764
|
+
`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`.
|
|
765
|
+
|
|
757
766
|
```tsx
|
|
758
767
|
<TableRow pressable>
|
|
759
768
|
<TableCell onPress={() => pick('Red fox')}>Red fox</TableCell>
|
|
@@ -965,6 +974,8 @@ The consumer needs no PDF.js dependency of its own. With another bundler, build
|
|
|
965
974
|
|
|
966
975
|
`MultiSelect` is several choices from a short list behind one trigger, such as which kinds a list shows. The trigger reads the muted `label` and then `summary` (your words for what is chosen, such as "All entries" or "2 services") with a chevron; it opens a panel named by the label with `allLabel` and `clearLabel` commands over one checkbox per option, each with an optional `icon` (use a small `GlyphTile`, never a bare dot) and a hairline before an option with `separatorBefore`. `onChange` reports the checked values in the options' order. It is a disclosure of native checkboxes, not a listbox: the first checkbox takes focus as it opens, Escape closes it and returns focus to the trigger, focus or a press outside closes it, and disabling it while open closes it. Blank words, no or duplicate options, a value that is not an option, or an unknown `align` throw a `RangeError`.
|
|
967
976
|
|
|
977
|
+
`PasteRows` is rows pasted from a spreadsheet: a `Field` text area (`label`, `description`, `error`, `required`, `disabled`, `placeholder`, `rows` default 6, spell check off) for the controlled `value` (`onValueChange`), and, once something is pasted, a polite status line (`countLabel(n)`, such as "Found 2 columns") followed by one `Select` `size="xs"` per found column, its label visually hidden (`columnLabel(position)`, default "Column 1", a 1-based position), offering `columns` (`{ id, label }[]`, such as Team, Division, Don’t use). `mapping` holds one column id per found column, in order; a change calls `onMappingChange` with the whole new array (a missing entry, or an id not in `columns`, counts as the first choice). It parses rows and cells only, with the exported pure `parsePastedRows(text, delimiter?)` (`{ rows, columnCount, delimiter }`): rows on CR, LF or CRLF, cells on tabs or commas (`delimiter`: `auto`, tab when the text holds a tab, else comma; `tab`; `comma`; pass `tab` when one column may hold commas), quoted cells as spreadsheets write them (a cell that starts with a quote runs to the next closing quote, across tabs and line breaks), cells trimmed, blank rows dropped, short rows padded with `''`. Call it on the same `value` to derive the default mapping and the preview; what the columns mean, headers, checks and the preview are the consumer's. Query the text area by its label (role `textbox`), each column by `getByRole('combobox', { name: 'Column 1' })`, and the count by role `status`. While `disabled` the text area is disabled and the column Selects are left out (the count line stays). A blank label, no columns, a blank or repeated column id or label, or `rows` under 1 throws a `RangeError`.
|
|
978
|
+
|
|
968
979
|
```tsx
|
|
969
980
|
<MultiSelect
|
|
970
981
|
label="Show"
|
|
@@ -1079,10 +1090,11 @@ const { headings, omissions } = inspectMarkdown(page.body, {
|
|
|
1079
1090
|
</Band>
|
|
1080
1091
|
```
|
|
1081
1092
|
|
|
1082
|
-
`SiteHeader` is a public site's header. Supply `brand` (such as a logo linked to the home page), `navLabel` and `items` (`{ id, label, href, current? }`, `current` as SectionNav's: `page`, `location` or `true` sets `aria-current`), `labels` (`openMenu`, `closeMenu`) and the decorative `menuIcon` and `closeIcon` glyphs. From `lg` the bar shows the brand, the destinations as a row of links (the current one at label weight over a bar in the words' colour, a thick underline in forced colours) and `actions` at its end, such as a language switch, Sign in (`Box as="a" link="quiet"`) and a `ButtonLink`. Below `lg` it shows the brand, `compactActions` and a menu button that opens, under the bar over the rest of the window, the destinations as large rows (the current one in the accent) and `menuActions` at the panel's foot, such as full-width `ButtonLink`s. The menu is a native `details`, so it opens and closes by touch, click and keyboard with no script, as a page rendered on the server needs; its `summary` is the button, named `openMenu` or `closeMenu` by its state. Script adds Escape (focus back on the button), and closes it when a link in it is chosen, focus leaves it, a press lands outside it, or the window widens to `lg`; nothing traps focus. A page rendered on the server has the no-script menu until it hydrates; a site that never hydrates keeps it, which still opens and closes. The bar is 56 px tall below `lg` and 72 px from it, on the solid surface with a hairline under it; `sticky` (default `true`) keeps it at the window's top. No items, blank or repeated ids, blank labels or hrefs,
|
|
1093
|
+
`SiteHeader` is a public site's header. Supply `brand` (such as a logo linked to the home page), `navLabel` and `items` (`{ id, label, href, current? }`, `current` as SectionNav's: `page`, `location` or `true` sets `aria-current`), `labels` (`openMenu`, `closeMenu`) and the decorative `menuIcon` and `closeIcon` glyphs. From `lg` the bar shows the brand, the destinations as a row of links (the current one at label weight over a bar in the words' colour, a thick underline in forced colours) and `actions` at its end, such as a language switch, Sign in (`Box as="a" link="quiet"`) and a `ButtonLink`. Below `lg` it shows the brand, `compactActions` and a menu button that opens, under the bar over the rest of the window, the destinations as large rows (the current one in the accent) and `menuActions` at the panel's foot, such as full-width `ButtonLink`s. The menu is a native `details`, so it opens and closes by touch, click and keyboard with no script, as a page rendered on the server needs; its `summary` is the button, named `openMenu` or `closeMenu` by its state. Script adds Escape (focus back on the button), and closes it when a link in it is chosen, focus leaves it, a press lands outside it, or the window widens to `lg`; nothing traps focus. A page rendered on the server has the no-script menu until it hydrates; a site that never hydrates keeps it, which still opens and closes. While the menu is open the bar's `compactActions` step aside (the menu holds its own `menuActions`), so the bar shows only the brand and the close button. The bar is 56 px tall below `lg` and 72 px from it, on the solid surface with a hairline under it; `sticky` (default `true`) keeps it at the window's top. While a sticky header is on the page the document's `scroll-padding-top` clears the bar, its hairline and a small gap, so a `#fragment` link, `:target` and `scrollIntoView` land below the bar instead of under it; a header with `sticky={false}` leaves the scroll padding alone. An optional `skipLink` (`{ label, href }`, such as `{ label: 'Skip to content', href: '#main' }`) is the header's first focusable element: visually hidden until it has focus, then a pill on the surface with the focus ring over the bar's start. Give the target (usually `main`) the matching `id`, and `tabIndex={-1}` so every browser moves focus there with the jump. No items, blank or repeated ids, blank labels or hrefs, a blank `navLabel` or menu label, or a skip link with a blank label or an `href` that is not a `#fragment` throw a `RangeError`.
|
|
1083
1094
|
|
|
1084
1095
|
```tsx
|
|
1085
1096
|
<SiteHeader
|
|
1097
|
+
skipLink={{ label: 'Skip to content', href: '#main' }}
|
|
1086
1098
|
brand={<a href="/">Wetland Trust</a>}
|
|
1087
1099
|
navLabel="Main"
|
|
1088
1100
|
items={[{ id: 'marsh', label: 'Marsh', href: '/marsh', current: 'page' }]}
|
|
@@ -1132,7 +1144,7 @@ const { headings, omissions } = inspectMarkdown(page.body, {
|
|
|
1132
1144
|
</ScreenPreview>
|
|
1133
1145
|
```
|
|
1134
1146
|
|
|
1135
|
-
`SiteFooter` is a public site's footer on the dark inverse band (`Band tone="inverse"`, so a brand moves it with a theme overlay of `inverse`). Supply `brand` (such as a logo for a dark background), an optional `tagline`, and `groups`: each `{ id, label, links }` is a `nav` named by its label, which shows above its links (hidden from assistive technology, since the `nav` already carries it). Each link is `{ id, label, href, current?, lang?, hrefLang? }`; `lang` and `hrefLang` pass through to the link unchecked; `current` sets `aria-current="page"` and draws it in the band's words at label weight, the others in its muted words, underlined on hover and focus. `legal` and `end` fill the bottom row past a hairline, such as the copyright and a language switch. Below `md` the brand takes the full width and the groups two columns; from `md` the brand and the groups share one spread row. Links get the 44 px target on a coarse pointer. Put it last in the page's `body` (outside a `section` or `article`) so it is the page's `contentinfo` landmark. No groups, a group with no links, blank or repeated ids, blank labels or a blank `href` throw a `RangeError`.
|
|
1147
|
+
`SiteFooter` is a public site's footer on the dark inverse band (`Band tone="inverse"`, so a brand moves it with a theme overlay of `inverse`). Supply `brand` (such as a logo for a dark background), an optional `tagline`, and `groups`: each `{ id, label, links }` is a `nav` named by its label, which shows above its links (hidden from assistive technology, since the `nav` already carries it). Each link is `{ id, label, href, current?, lang?, hrefLang? }`; `lang` and `hrefLang` pass through to the link unchecked; `current` sets `aria-current="page"` and draws it in the band's words at label weight, the others in its muted words, underlined on hover and focus. `legal` and `end` fill the bottom row past a hairline, such as the copyright and a language switch; a plain link that is all of `legal` or `end` gets the 44 px target on a coarse pointer too (a link inside running legal words stays inline). Below `md` the brand takes the full width and the groups two columns; from `md` the brand and the groups share one spread row. Links get the 44 px target on a coarse pointer. Put it last in the page's `body` (outside a `section` or `article`) so it is the page's `contentinfo` landmark. No groups, a group with no links, blank or repeated ids, blank labels or a blank `href` throw a `RangeError`.
|
|
1136
1148
|
|
|
1137
1149
|
```tsx
|
|
1138
1150
|
<SiteFooter
|
|
@@ -6,5 +6,22 @@ export type CheckboxProps = {
|
|
|
6
6
|
required?: boolean;
|
|
7
7
|
description?: string;
|
|
8
8
|
error?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Keeps `label` as the checkbox's name but does not draw it, so the box
|
|
11
|
+
* stands alone, as in a table's select column ("Select Mallard"). The
|
|
12
|
+
* box is then its own target: its size on a fine pointer, at least
|
|
13
|
+
* 44 × 44 px on a coarse one. Say what the box picks in words the page
|
|
14
|
+
* draws nearby (the row's name), not by position alone.
|
|
15
|
+
*/
|
|
16
|
+
labelVisuallyHidden?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The mixed state, such as "Select all" while only some rows are
|
|
19
|
+
* chosen: a dash in place of the check, the native `indeterminate`
|
|
20
|
+
* property and `aria-checked="mixed"`. It wins over `checked` in the
|
|
21
|
+
* drawing and the state read out. A press reports `!checked` (so with
|
|
22
|
+
* `checked` false, `true`: choose all) and the consumer decides what
|
|
23
|
+
* stays mixed.
|
|
24
|
+
*/
|
|
25
|
+
indeterminate?: boolean;
|
|
9
26
|
};
|
|
10
|
-
export declare function Checkbox({ label, checked, onCheckedChange, disabled, required, description, error, }: CheckboxProps): import("react").JSX.Element;
|
|
27
|
+
export declare function Checkbox({ label, checked, onCheckedChange, disabled, required, description, error, labelVisuallyHidden, indeterminate, }: CheckboxProps): import("react").JSX.Element;
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useId } from 'react';
|
|
3
|
+
import { useId, useLayoutEffect, useRef } from 'react';
|
|
4
|
+
import { cx } from '../class-names.js';
|
|
4
5
|
import { CheckboxControl } from './checkbox/CheckboxControl.js';
|
|
5
6
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
6
|
-
export function Checkbox({ label, checked, onCheckedChange, disabled = false, required = false, description, error, }) {
|
|
7
|
+
export function Checkbox({ label, checked, onCheckedChange, disabled = false, required = false, description, error, labelVisuallyHidden = false, indeterminate = false, }) {
|
|
7
8
|
const descriptionId = useId();
|
|
8
9
|
const errorId = useId();
|
|
10
|
+
const inputRef = useRef(null);
|
|
9
11
|
const describedBy = [
|
|
10
12
|
description ? descriptionId : null,
|
|
11
13
|
error ? errorId : null,
|
|
12
14
|
]
|
|
13
15
|
.filter((id) => id !== null)
|
|
14
16
|
.join(' ');
|
|
15
|
-
|
|
17
|
+
// A press clears the native property, so set it again after every render.
|
|
18
|
+
useLayoutEffect(() => {
|
|
19
|
+
if (inputRef.current)
|
|
20
|
+
inputRef.current.indeterminate = indeterminate;
|
|
21
|
+
});
|
|
22
|
+
return (_jsxs("div", { className: "sw-checkbox", children: [_jsxs("label", { className: cx('sw-checkbox-label', labelVisuallyHidden && 'sw-checkbox-label-hidden'), children: [_jsx(CheckboxControl, { ref: inputRef, "aria-checked": indeterminate ? 'mixed' : undefined, "aria-describedby": describedBy || undefined, "aria-invalid": Boolean(error), checked: checked, disabled: disabled, onChange: (event) => {
|
|
23
|
+
const input = event.currentTarget;
|
|
24
|
+
// The press cleared the mixed state; keep it until the props say.
|
|
25
|
+
input.indeterminate = indeterminate;
|
|
16
26
|
if (!disabled)
|
|
17
|
-
onCheckedChange(
|
|
18
|
-
}, required: required }), _jsx("span", { className:
|
|
27
|
+
onCheckedChange(input.checked);
|
|
28
|
+
}, required: required }), _jsx("span", { className: labelVisuallyHidden ? 'sw-sr-only' : 'sw-checkbox-text', children: label })] }), description ? (_jsx("span", { className: "sw-checkbox-description", id: descriptionId, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-checkbox-error", id: errorId, message: error })] }));
|
|
19
29
|
}
|
|
@@ -2,10 +2,12 @@ import { type DialogHTMLAttributes, type ReactNode } from 'react';
|
|
|
2
2
|
import { type Breakpoint } from '../css/breakpoints.js';
|
|
3
3
|
import { type DialogAppearance, dialogAppearances } from '../css/css-dialog-banded.js';
|
|
4
4
|
import { type ThemeCanvas } from '../css/css-canvas-grouped.js';
|
|
5
|
+
import { type DialogPlacement, dialogPlacements } from '../css/css-dialog-end.js';
|
|
5
6
|
import { type ActionBarSummary } from './action-bar/ActionBarSummary.js';
|
|
6
7
|
import { type DialogRole, type DialogSize, dialogSizes } from './dialog/validate.js';
|
|
7
8
|
export { dialogAppearances, type DialogAppearance };
|
|
8
9
|
export { dialogSizes, type DialogRole, type DialogSize };
|
|
10
|
+
export { dialogPlacements, type DialogPlacement };
|
|
9
11
|
/**
|
|
10
12
|
* The heading level of the dialog's title. It changes only the element (and
|
|
11
13
|
* so the outline a screen reader reads), never the title's look.
|
|
@@ -98,6 +100,17 @@ export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose
|
|
|
98
100
|
* footer stay on the surface. `default` keeps the appearance's fill.
|
|
99
101
|
*/
|
|
100
102
|
canvas?: ThemeCanvas;
|
|
103
|
+
/**
|
|
104
|
+
* `end` makes the dialog a side panel from md up: the window's full
|
|
105
|
+
* height at the inline end (the right, the left in right-to-left), its
|
|
106
|
+
* `size`'s width, square corners and a hairline on its inner edge, with
|
|
107
|
+
* the same title row, close button, scrolling content and footer pinned
|
|
108
|
+
* to the bottom edge. Below md it is the centred dialog, or the bottom
|
|
109
|
+
* sheet with `sheetBelow="md"`. It is the same modal: focus stays inside,
|
|
110
|
+
* Escape and a backdrop press ask `onClose`, and focus returns to the
|
|
111
|
+
* opener on close. `center` (the default) is unchanged.
|
|
112
|
+
*/
|
|
113
|
+
placement?: DialogPlacement;
|
|
101
114
|
children: ReactNode;
|
|
102
115
|
};
|
|
103
|
-
export declare function Dialog({ 'aria-describedby': describedBy, appearance, canvas, children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, sheetGrabber, size, status, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
|
|
116
|
+
export declare function Dialog({ 'aria-describedby': describedBy, appearance, canvas, children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, placement, sheetBelow, sheetGrabber, size, status, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
|
|
@@ -5,6 +5,7 @@ import { cx } from '../class-names.js';
|
|
|
5
5
|
import { dialogSheetClass } from '../css/css-dialog.js';
|
|
6
6
|
import { dialogAppearances, dialogBandedClass, } from '../css/css-dialog-banded.js';
|
|
7
7
|
import { dialogCanvasClass } from '../css/css-dialog-canvas.js';
|
|
8
|
+
import { dialogEndClass, dialogPlacements, } from '../css/css-dialog-end.js';
|
|
8
9
|
import { dialogGrabberClass, dialogHasGrabberClass, } from '../css/css-dialog-grabber.js';
|
|
9
10
|
import { spacingClassNames } from '../spacing-classes.js';
|
|
10
11
|
import { useDialogCloseRequests } from './dialog-close-requests.js';
|
|
@@ -14,11 +15,13 @@ import { assertDialogProps, dialogSizes, } from './dialog/validate.js';
|
|
|
14
15
|
import { Stack } from './Stack.js';
|
|
15
16
|
export { dialogAppearances };
|
|
16
17
|
export { dialogSizes };
|
|
17
|
-
export
|
|
18
|
+
export { dialogPlacements };
|
|
19
|
+
export function Dialog({ 'aria-describedby': describedBy, appearance = 'glass', canvas = 'default', children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, placement = 'center', sheetBelow, sheetGrabber = false, size = 'md', status, title, titleLevel = 3, ...rest }) {
|
|
18
20
|
assertDialogProps({
|
|
19
21
|
appearance,
|
|
20
22
|
canvas,
|
|
21
23
|
description,
|
|
24
|
+
placement,
|
|
22
25
|
role,
|
|
23
26
|
sheetBelow,
|
|
24
27
|
sheetGrabber,
|
|
@@ -41,5 +44,5 @@ export function Dialog({ 'aria-describedby': describedBy, appearance = 'glass',
|
|
|
41
44
|
onPointerDown,
|
|
42
45
|
onSubmit,
|
|
43
46
|
});
|
|
44
|
-
return (_jsxs("dialog", { ...rest, ref: nodeRef, "aria-describedby": describedBy ?? (description ? descriptionId : undefined), "aria-labelledby": titleId, className: cx('sw-dialog', size !== 'md' && `sw-dialog-${size}`, sheetBelow && dialogSheetClass(sheetBelow), appearance === 'banded' && dialogBandedClass, canvas === 'grouped' && dialogCanvasClass, sheetGrabber && dialogHasGrabberClass, ...spacingClassNames({ padding: 5 }), className), role: role === 'alertdialog' ? 'alertdialog' : undefined, ...handlers, children: [sheetGrabber ? (_jsx("span", { "aria-hidden": "true", className: dialogGrabberClass })) : null, _jsxs(Stack, { gap: 4, children: [_jsx(DialogTitle, { banded: appearance === 'banded', closeLabel: closeLabel, description: description, descriptionId: descriptionId, icon: icon, id: titleId, level: titleLevel === 2 ? 2 : 3, onClose: onClose, title: title }), children, hasFooter ? (_jsx(DialogFooter, { status: hasStatus ? status : undefined, summary: summary, children: footer })) : null] })] }));
|
|
47
|
+
return (_jsxs("dialog", { ...rest, ref: nodeRef, "aria-describedby": describedBy ?? (description ? descriptionId : undefined), "aria-labelledby": titleId, className: cx('sw-dialog', size !== 'md' && `sw-dialog-${size}`, sheetBelow && dialogSheetClass(sheetBelow), appearance === 'banded' && dialogBandedClass, canvas === 'grouped' && dialogCanvasClass, sheetGrabber && dialogHasGrabberClass, placement === 'end' && dialogEndClass, ...spacingClassNames({ padding: 5 }), className), role: role === 'alertdialog' ? 'alertdialog' : undefined, ...handlers, children: [sheetGrabber ? (_jsx("span", { "aria-hidden": "true", className: dialogGrabberClass })) : null, _jsxs(Stack, { gap: 4, children: [_jsx(DialogTitle, { banded: appearance === 'banded', closeLabel: closeLabel, description: description, descriptionId: descriptionId, icon: icon, id: titleId, level: titleLevel === 2 ? 2 : 3, onClose: onClose, title: title }), children, hasFooter ? (_jsx(DialogFooter, { status: hasStatus ? status : undefined, summary: summary, children: footer })) : null] })] }));
|
|
45
48
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type PastedRowsDelimiter } from './paste-rows/parse-pasted-rows.js';
|
|
2
|
+
export type PasteRowsColumn = {
|
|
3
|
+
/** What `mapping` holds for a found column given this meaning. */
|
|
4
|
+
id: string;
|
|
5
|
+
/** The choice's words in each column's Select, such as "Team" or "Don’t use". */
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
export type PasteRowsProps = {
|
|
9
|
+
/** The text area's label, such as "Paste here". */
|
|
10
|
+
label: string;
|
|
11
|
+
/** A muted line under the label, read as the text area's description. */
|
|
12
|
+
description?: string;
|
|
13
|
+
/** The pasted text, as typed. */
|
|
14
|
+
value: string;
|
|
15
|
+
onValueChange: (value: string) => void;
|
|
16
|
+
/** The meanings each found column can be given, in menu order. */
|
|
17
|
+
columns: readonly PasteRowsColumn[];
|
|
18
|
+
/**
|
|
19
|
+
* One `columns` id per found column, in order. Derive it from the same
|
|
20
|
+
* `value` and `delimiter` with `parsePastedRows` (its `columnCount`), so a new column
|
|
21
|
+
* gets its default in the same render. A missing entry, or an id not
|
|
22
|
+
* in `columns`, counts as the first choice: it is drawn so and filled in
|
|
23
|
+
* on the next change.
|
|
24
|
+
*/
|
|
25
|
+
mapping: readonly string[];
|
|
26
|
+
/** Called with the whole new mapping when one column's Select changes. */
|
|
27
|
+
onMappingChange: (mapping: string[]) => void;
|
|
28
|
+
/** The line before the Selects, such as `(n) => \`Found ${n} columns\``. */
|
|
29
|
+
countLabel: (count: number) => string;
|
|
30
|
+
/**
|
|
31
|
+
* The name of the Select for the column at a 1-based position, not
|
|
32
|
+
* drawn: "Column 1" (the default, English).
|
|
33
|
+
*/
|
|
34
|
+
columnLabel?: (position: number) => string;
|
|
35
|
+
/** How cells are split (`parsePastedRows`); default `auto`. */
|
|
36
|
+
delimiter?: PastedRowsDelimiter;
|
|
37
|
+
/** The text area's visible rows; default 6. */
|
|
38
|
+
rows?: number;
|
|
39
|
+
placeholder?: string;
|
|
40
|
+
error?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Disables the text area and leaves the column Selects out (the count
|
|
43
|
+
* line stays), so nothing changes while the consumer saves.
|
|
44
|
+
*/
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
required?: boolean;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* A text area for rows pasted from a spreadsheet (cells split on tabs or
|
|
50
|
+
* commas) and, once something is pasted, a polite count line and one
|
|
51
|
+
* small Select per found column choosing what that column means. It parses
|
|
52
|
+
* only rows and cells (`parsePastedRows`); the consumer gives the
|
|
53
|
+
* meanings, keeps the mapping, checks the values and draws the preview.
|
|
54
|
+
*/
|
|
55
|
+
export declare function PasteRows({ columnLabel, columns, countLabel, delimiter, description, disabled, error, label, mapping, onMappingChange, onValueChange, placeholder, required, rows, value, }: PasteRowsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Field } from './Field.js';
|
|
4
|
+
import { Inline } from './Inline.js';
|
|
5
|
+
import { parsePastedRows, } from './paste-rows/parse-pasted-rows.js';
|
|
6
|
+
import { Select } from './Select.js';
|
|
7
|
+
import { Stack } from './Stack.js';
|
|
8
|
+
import { Text } from './Text.js';
|
|
9
|
+
const defaultColumnLabel = (position) => `Column ${position}`;
|
|
10
|
+
function assertPasteRowsProps({ columns, label, rows, }) {
|
|
11
|
+
if (typeof label !== 'string' || !label.trim())
|
|
12
|
+
throw new RangeError('PasteRows label must not be empty');
|
|
13
|
+
if (columns.length === 0)
|
|
14
|
+
throw new RangeError('PasteRows needs at least one column');
|
|
15
|
+
const ids = new Set();
|
|
16
|
+
const labels = new Set();
|
|
17
|
+
for (const column of columns) {
|
|
18
|
+
if (!column.id.trim() || !column.label.trim())
|
|
19
|
+
throw new RangeError('PasteRows column id and label must not be empty');
|
|
20
|
+
if (ids.has(column.id))
|
|
21
|
+
throw new RangeError(`Repeated PasteRows column id: ${column.id}`);
|
|
22
|
+
if (labels.has(column.label))
|
|
23
|
+
throw new RangeError(`Repeated PasteRows column label: ${column.label}`);
|
|
24
|
+
ids.add(column.id);
|
|
25
|
+
labels.add(column.label);
|
|
26
|
+
}
|
|
27
|
+
if (rows !== undefined && (!Number.isInteger(rows) || rows < 1))
|
|
28
|
+
throw new RangeError('PasteRows rows must be a whole number from 1');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A text area for rows pasted from a spreadsheet (cells split on tabs or
|
|
32
|
+
* commas) and, once something is pasted, a polite count line and one
|
|
33
|
+
* small Select per found column choosing what that column means. It parses
|
|
34
|
+
* only rows and cells (`parsePastedRows`); the consumer gives the
|
|
35
|
+
* meanings, keeps the mapping, checks the values and draws the preview.
|
|
36
|
+
*/
|
|
37
|
+
export function PasteRows({ columnLabel = defaultColumnLabel, columns, countLabel, delimiter = 'auto', description, disabled = false, error, label, mapping, onMappingChange, onValueChange, placeholder, required = false, rows = 6, value, }) {
|
|
38
|
+
assertPasteRowsProps({ columns, label, rows });
|
|
39
|
+
const { columnCount } = parsePastedRows(value, delimiter);
|
|
40
|
+
const options = columns.map((column) => ({
|
|
41
|
+
value: column.id,
|
|
42
|
+
label: column.label,
|
|
43
|
+
}));
|
|
44
|
+
// A missing entry, or an id not in `columns`, counts as the first choice.
|
|
45
|
+
const chosenId = (position) => {
|
|
46
|
+
const id = mapping[position];
|
|
47
|
+
return columns.some((column) => column.id === id) ? id : columns[0].id;
|
|
48
|
+
};
|
|
49
|
+
const choose = (index, id) => {
|
|
50
|
+
const next = Array.from({ length: columnCount }, (_, position) => chosenId(position));
|
|
51
|
+
next[index] = id;
|
|
52
|
+
onMappingChange(next);
|
|
53
|
+
};
|
|
54
|
+
return (_jsxs(Stack, { gap: 2, children: [_jsx(Field, { description: description, error: error, label: label, required: required, children: _jsx("textarea", { disabled: disabled, onChange: (event) => onValueChange(event.currentTarget.value), placeholder: placeholder, required: required, rows: rows, spellCheck: false, value: value }) }), _jsxs(Inline, { align: "center", gap: 2, wrap: true, children: [_jsx(Text, { color: "muted", role: "status", variant: "caption", children: columnCount > 0 ? countLabel(columnCount) : '' }), (disabled ? [] : Array.from({ length: columnCount })).map((_, index) => (_jsx(Select, { label: columnLabel(index + 1), labelVisuallyHidden: true, onChange: (id) => choose(index, id), options: options, size: "xs", value: chosenId(index) }, index)))] })] }));
|
|
55
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SiteHeaderProps } from './site-header/types.js';
|
|
2
|
-
export type { SiteHeaderItem, SiteHeaderLabels, SiteHeaderProps, } from './site-header/types.js';
|
|
2
|
+
export type { SiteHeaderItem, SiteHeaderLabels, SiteHeaderProps, SiteHeaderSkipLink, } from './site-header/types.js';
|
|
3
3
|
/**
|
|
4
4
|
* A public site's header: the brand at the start; from `lg` the main
|
|
5
5
|
* destinations as a row of links (the current one marked by weight and a
|
|
@@ -9,6 +9,9 @@ export type { SiteHeaderItem, SiteHeaderLabels, SiteHeaderProps, } from './site-
|
|
|
9
9
|
* it opens and closes by touch, click and keyboard with no script, as a
|
|
10
10
|
* page rendered on the server needs; script adds Escape (focus back on the
|
|
11
11
|
* button) and closes it when a link is chosen or focus leaves it. It traps
|
|
12
|
-
* no focus.
|
|
12
|
+
* no focus. While the menu is open the compact actions step aside, so the
|
|
13
|
+
* bar shows only the brand and the close button. An optional skip link
|
|
14
|
+
* comes first, hidden until it has focus. Invalid items or words throw a
|
|
15
|
+
* `RangeError`.
|
|
13
16
|
*/
|
|
14
|
-
export declare function SiteHeader({ actions, brand, closeIcon, compactActions, items, labels, menuActions, menuIcon, navLabel, sticky, }: SiteHeaderProps): import("react").JSX.Element;
|
|
17
|
+
export declare function SiteHeader({ actions, brand, closeIcon, compactActions, items, labels, menuActions, menuIcon, navLabel, skipLink, sticky, }: SiteHeaderProps): import("react").JSX.Element;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useRef } from 'react';
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
|
+
import { tabStop } from '../tab-stop.js';
|
|
5
6
|
import { SiteHeaderLinks } from './site-header/SiteHeaderLinks.js';
|
|
6
7
|
import { useMenuDismiss } from './site-header/use-menu-dismiss.js';
|
|
7
8
|
import { assertSiteHeaderProps } from './site-header/validate.js';
|
|
@@ -14,11 +15,14 @@ import { assertSiteHeaderProps } from './site-header/validate.js';
|
|
|
14
15
|
* it opens and closes by touch, click and keyboard with no script, as a
|
|
15
16
|
* page rendered on the server needs; script adds Escape (focus back on the
|
|
16
17
|
* button) and closes it when a link is chosen or focus leaves it. It traps
|
|
17
|
-
* no focus.
|
|
18
|
+
* no focus. While the menu is open the compact actions step aside, so the
|
|
19
|
+
* bar shows only the brand and the close button. An optional skip link
|
|
20
|
+
* comes first, hidden until it has focus. Invalid items or words throw a
|
|
21
|
+
* `RangeError`.
|
|
18
22
|
*/
|
|
19
|
-
export function SiteHeader({ actions, brand, closeIcon, compactActions, items, labels, menuActions, menuIcon, navLabel, sticky = true, }) {
|
|
20
|
-
assertSiteHeaderProps({ items, labels, navLabel });
|
|
23
|
+
export function SiteHeader({ actions, brand, closeIcon, compactActions, items, labels, menuActions, menuIcon, navLabel, skipLink, sticky = true, }) {
|
|
24
|
+
assertSiteHeaderProps({ items, labels, navLabel, skipLink });
|
|
21
25
|
const menuRef = useRef(null);
|
|
22
26
|
useMenuDismiss(menuRef);
|
|
23
|
-
return (
|
|
27
|
+
return (_jsxs("header", { className: cx('sw-site-header', sticky && 'sw-site-header-sticky'), children: [skipLink ? (_jsx("a", { className: "sw-site-header-skip-link", href: skipLink.href, tabIndex: tabStop, children: skipLink.label })) : null, _jsxs("div", { className: "sw-site-header-bar", children: [_jsx("div", { className: "sw-site-header-brand", children: brand }), _jsx(SiteHeaderLinks, { className: "sw-site-header-nav", items: items, navLabel: navLabel }), actions ? (_jsx("div", { className: "sw-site-header-actions", children: actions })) : null, _jsxs("div", { className: "sw-site-header-compact", children: [compactActions ? (_jsx("div", { className: "sw-site-header-compact-actions", children: compactActions })) : null, _jsxs("details", { className: "sw-site-header-menu", ref: menuRef, children: [_jsxs("summary", { className: "sw-site-header-menu-button", children: [_jsxs("span", { className: "sw-site-header-menu-open", children: [_jsx("span", { "aria-hidden": "true", className: "sw-site-header-glyph", children: menuIcon }), _jsx("span", { className: "sw-sr-only", children: labels.openMenu })] }), _jsxs("span", { className: "sw-site-header-menu-close", children: [_jsx("span", { "aria-hidden": "true", className: "sw-site-header-glyph", children: closeIcon }), _jsx("span", { className: "sw-sr-only", children: labels.closeMenu })] })] }), _jsxs("div", { className: "sw-site-header-panel", children: [_jsx(SiteHeaderLinks, { className: "sw-site-header-menu-nav", items: items, navLabel: navLabel }), menuActions ? (_jsx("div", { className: "sw-site-header-menu-actions", children: menuActions })) : null] })] })] })] })] }));
|
|
24
28
|
}
|
|
@@ -3,6 +3,7 @@ import { type TableLayout, type TableStickyHeader, type TableVerticalAlign } fro
|
|
|
3
3
|
import { type TableMaxHeight } from './table/table-max-height.js';
|
|
4
4
|
export { TableCell, type TableCellProps, type TableColumnWidth, type TableSort, } from './table/TableCell.js';
|
|
5
5
|
export { TableRow, type TableRowProps, type TableRowVariant, } from './table/TableRow.js';
|
|
6
|
+
export { TableSelectCell, type TableSelectCellProps, } from './table/TableSelectCell.js';
|
|
6
7
|
export type { TableCellTone } from '../css/css-table-ledger.js';
|
|
7
8
|
export { tableCellTones, tableRowVariants } from '../css/css-table-ledger.js';
|
|
8
9
|
export type { TableLayout, TableMaxHeight, TableStickyHeader, TableVerticalAlign, };
|
package/dist/components/Table.js
CHANGED
|
@@ -6,6 +6,7 @@ import { assertTableLayout, assertTableStickyHeader, assertTableVerticalAlign, }
|
|
|
6
6
|
import { assertTableMaxHeight, tableMaxHeightStyle, } from './table/table-max-height.js';
|
|
7
7
|
export { TableCell, } from './table/TableCell.js';
|
|
8
8
|
export { TableRow, } from './table/TableRow.js';
|
|
9
|
+
export { TableSelectCell, } from './table/TableSelectCell.js';
|
|
9
10
|
export { tableCellTones, tableRowVariants } from '../css/css-table-ledger.js';
|
|
10
11
|
/**
|
|
11
12
|
* A wide table scrolls inside its own `ScrollRegion`, named after the
|
|
@@ -5,5 +5,5 @@ type CheckboxControlProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'classNa
|
|
|
5
5
|
* generated rules style (`input + .sw-checkbox-mark`). Shared by Checkbox
|
|
6
6
|
* and MultiSelect's rows so the two cannot drift apart.
|
|
7
7
|
*/
|
|
8
|
-
export declare
|
|
8
|
+
export declare const CheckboxControl: import("react").ForwardRefExoticComponent<CheckboxControlProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
9
9
|
export {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
2
3
|
import { tabStop } from '../../tab-stop.js';
|
|
3
4
|
/**
|
|
4
5
|
* The native checkbox and its drawn mark, the one markup Checkbox's
|
|
5
6
|
* generated rules style (`input + .sw-checkbox-mark`). Shared by Checkbox
|
|
6
7
|
* and MultiSelect's rows so the two cannot drift apart.
|
|
7
8
|
*/
|
|
8
|
-
export function CheckboxControl({ tabIndex, ...props }) {
|
|
9
|
-
return (_jsxs("span", { className: "sw-checkbox-control", children: [_jsx("input", { tabIndex: tabIndex ?? tabStop, ...props, className: "sw-checkbox-input", type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-checkbox-mark" })] }));
|
|
10
|
-
}
|
|
9
|
+
export const CheckboxControl = forwardRef(function CheckboxControl({ tabIndex, ...props }, ref) {
|
|
10
|
+
return (_jsxs("span", { className: "sw-checkbox-control", children: [_jsx("input", { ref: ref, tabIndex: tabIndex ?? tabStop, ...props, className: "sw-checkbox-input", type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-checkbox-mark" })] }));
|
|
11
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Breakpoint } from '../../css/breakpoints.js';
|
|
2
2
|
import { type DialogAppearance } from '../../css/css-dialog-banded.js';
|
|
3
|
+
import { type DialogPlacement } from '../../css/css-dialog-end.js';
|
|
3
4
|
import { type ThemeCanvas } from '../../css/css-canvas-grouped.js';
|
|
4
5
|
import { type ActionBarSummary } from '../action-bar/ActionBarSummary.js';
|
|
5
6
|
export declare const dialogSizes: readonly ["md", "lg", "xl"];
|
|
@@ -20,5 +21,6 @@ export type DialogChecks = {
|
|
|
20
21
|
canvas: ThemeCanvas;
|
|
21
22
|
sheetBelow: Breakpoint | undefined;
|
|
22
23
|
sheetGrabber: boolean;
|
|
24
|
+
placement: DialogPlacement;
|
|
23
25
|
};
|
|
24
|
-
export declare function assertDialogProps({ appearance, canvas, description, role, sheetBelow, sheetGrabber, size, summary, }: DialogChecks): void;
|
|
26
|
+
export declare function assertDialogProps({ appearance, canvas, description, placement, role, sheetBelow, sheetGrabber, size, summary, }: DialogChecks): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dialogAppearances, } from '../../css/css-dialog-banded.js';
|
|
2
|
+
import { dialogPlacements, } from '../../css/css-dialog-end.js';
|
|
2
3
|
import { themeCanvases, } from '../../css/css-canvas-grouped.js';
|
|
3
4
|
import { assertActionBarSummary, } from '../action-bar/ActionBarSummary.js';
|
|
4
5
|
export const dialogSizes = ['md', 'lg', 'xl'];
|
|
@@ -7,10 +8,11 @@ function oneOf(values, value, prop) {
|
|
|
7
8
|
if (!values.includes(value))
|
|
8
9
|
throw new RangeError(`Unknown Dialog ${prop}: ${String(value)}`);
|
|
9
10
|
}
|
|
10
|
-
export function assertDialogProps({ appearance, canvas, description, role, sheetBelow, sheetGrabber, size, summary, }) {
|
|
11
|
+
export function assertDialogProps({ appearance, canvas, description, placement, role, sheetBelow, sheetGrabber, size, summary, }) {
|
|
11
12
|
oneOf(dialogSizes, size, 'size');
|
|
12
13
|
oneOf(dialogAppearances, appearance, 'appearance');
|
|
13
14
|
oneOf(themeCanvases, canvas, 'canvas');
|
|
15
|
+
oneOf(dialogPlacements, placement, 'placement');
|
|
14
16
|
if (role !== undefined)
|
|
15
17
|
oneOf(dialogRoles, role, 'role');
|
|
16
18
|
if (description !== undefined &&
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How pasted cells are split: `tab` (what a spreadsheet copies), `comma`
|
|
3
|
+
* (a CSV line), or `auto`, tab when the text holds a tab and comma
|
|
4
|
+
* otherwise. A list whose one column may hold commas ("Smith, Jones") is
|
|
5
|
+
* parsed with `tab`, so a comma never splits it.
|
|
6
|
+
*/
|
|
7
|
+
export type PastedRowsDelimiter = 'auto' | 'tab' | 'comma';
|
|
8
|
+
export declare const pastedRowsDelimiters: readonly PastedRowsDelimiter[];
|
|
9
|
+
export type PastedRows = {
|
|
10
|
+
/** The rows in order, each padded with `''` to `columnCount` cells. */
|
|
11
|
+
rows: string[][];
|
|
12
|
+
/** The most cells any row has; 0 when nothing but blank lines was pasted. */
|
|
13
|
+
columnCount: number;
|
|
14
|
+
/** The delimiter used, `auto` resolved. */
|
|
15
|
+
delimiter: 'tab' | 'comma';
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Splits pasted text into rows of trimmed cells: rows on line breaks
|
|
19
|
+
* (CR, LF or CRLF), cells on the delimiter. A cell that starts with a
|
|
20
|
+
* double quote and has a closing one later is quoted, as spreadsheets and
|
|
21
|
+
* CSV write a cell holding the delimiter, a line break or a quote; any
|
|
22
|
+
* other quote is a plain character. Rows whose cells are all blank are
|
|
23
|
+
* left out. It knows nothing of what the columns mean: the consumer maps
|
|
24
|
+
* and checks them. Linear in the text's length.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parsePastedRows(text: string, delimiter?: PastedRowsDelimiter): PastedRows;
|