@scalewing/react 1.52.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 CHANGED
@@ -1,5 +1,19 @@
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
+
3
17
  ## 1.52.0
4
18
 
5
19
  ### 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"
@@ -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
- return (_jsxs("div", { className: "sw-checkbox", children: [_jsxs("label", { className: "sw-checkbox-label", children: [_jsx(CheckboxControl, { "aria-describedby": describedBy || undefined, "aria-invalid": Boolean(error), checked: checked, disabled: disabled, onChange: (event) => {
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(event.currentTarget.checked);
18
- }, required: required }), _jsx("span", { className: "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 })] }));
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 function Dialog({ 'aria-describedby': describedBy, appearance = 'glass', canvas = 'default', children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, sheetGrabber = false, size = 'md', status, title, titleLevel = 3, ...rest }) {
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
+ }
@@ -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, };
@@ -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 function CheckboxControl({ tabIndex, ...props }: CheckboxControlProps): import("react").JSX.Element;
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;
@@ -0,0 +1,123 @@
1
+ export const pastedRowsDelimiters = [
2
+ 'auto',
3
+ 'tab',
4
+ 'comma',
5
+ ];
6
+ function resolveDelimiter(text, delimiter) {
7
+ if (!pastedRowsDelimiters.includes(delimiter))
8
+ throw new RangeError(`Unknown PasteRows delimiter: ${String(delimiter)}`);
9
+ if (delimiter !== 'auto')
10
+ return delimiter;
11
+ return text.includes('\t') ? 'tab' : 'comma';
12
+ }
13
+ /**
14
+ * Reads one quoted cell starting at the opening quote `start`: `""` is
15
+ * one quote, and delimiters and line breaks inside it are its words.
16
+ * Returns the words and the index after the closing quote, or `null` when
17
+ * no lone closing quote follows (the opening quote is then a character).
18
+ */
19
+ function readQuoted(text, start) {
20
+ let words = '';
21
+ let index = start + 1;
22
+ while (index < text.length) {
23
+ const char = text[index];
24
+ if (char === '"') {
25
+ if (text[index + 1] === '"') {
26
+ words += '"';
27
+ index += 2;
28
+ continue;
29
+ }
30
+ return [words, index + 1];
31
+ }
32
+ words += char;
33
+ index += 1;
34
+ }
35
+ return null;
36
+ }
37
+ /**
38
+ * For each index, where a quoted cell's scan starting there would find its
39
+ * lone closing quote (`""` being one escaped quote), or -1 when none
40
+ * follows; one extra entry for the end. Built backwards in one pass, so
41
+ * each opening quote is checked in constant time and the parse stays
42
+ * linear: a run of quotes from i of odd length closes on its last quote,
43
+ * an even run is all escapes and the scan carries on after it.
44
+ */
45
+ function closingQuoteTable(text) {
46
+ const closesFrom = new Int32Array(text.length + 1).fill(-1);
47
+ const runFrom = new Int32Array(text.length + 1);
48
+ for (let index = text.length - 1; index >= 0; index -= 1) {
49
+ if (text[index] !== '"') {
50
+ closesFrom[index] = closesFrom[index + 1];
51
+ continue;
52
+ }
53
+ const run = 1 + runFrom[index + 1];
54
+ runFrom[index] = run;
55
+ closesFrom[index] =
56
+ run % 2 === 1 ? index + run - 1 : closesFrom[index + run];
57
+ }
58
+ return closesFrom;
59
+ }
60
+ /**
61
+ * Splits pasted text into rows of trimmed cells: rows on line breaks
62
+ * (CR, LF or CRLF), cells on the delimiter. A cell that starts with a
63
+ * double quote and has a closing one later is quoted, as spreadsheets and
64
+ * CSV write a cell holding the delimiter, a line break or a quote; any
65
+ * other quote is a plain character. Rows whose cells are all blank are
66
+ * left out. It knows nothing of what the columns mean: the consumer maps
67
+ * and checks them. Linear in the text's length.
68
+ */
69
+ export function parsePastedRows(text, delimiter = 'auto') {
70
+ const used = resolveDelimiter(text, delimiter);
71
+ const separator = used === 'tab' ? '\t' : ',';
72
+ const closesFrom = closingQuoteTable(text);
73
+ const rows = [];
74
+ let row = [];
75
+ let cell = '';
76
+ let cellStart = true;
77
+ const endCell = () => {
78
+ row.push(cell.trim());
79
+ cell = '';
80
+ cellStart = true;
81
+ };
82
+ const endRow = () => {
83
+ endCell();
84
+ if (row.some((value) => value !== ''))
85
+ rows.push(row);
86
+ row = [];
87
+ };
88
+ let index = 0;
89
+ while (index < text.length) {
90
+ const char = text[index];
91
+ const quoted = cellStart && char === '"' && closesFrom[index + 1] >= 0
92
+ ? readQuoted(text, index)
93
+ : null;
94
+ if (quoted) {
95
+ cell += quoted[0];
96
+ index = quoted[1];
97
+ cellStart = false;
98
+ }
99
+ else if (char === separator) {
100
+ endCell();
101
+ index += 1;
102
+ }
103
+ else if (char === '\r' || char === '\n') {
104
+ endRow();
105
+ index += char === '\r' && text[index + 1] === '\n' ? 2 : 1;
106
+ }
107
+ else {
108
+ // Spaces before a quoted cell's opening quote do not unquote it.
109
+ if (!(cellStart && char === ' ')) {
110
+ cell += char;
111
+ cellStart = false;
112
+ }
113
+ index += 1;
114
+ }
115
+ }
116
+ if (cell !== '' || row.length > 0)
117
+ endRow();
118
+ const columnCount = rows.reduce((most, cells) => Math.max(most, cells.length), 0);
119
+ for (const cells of rows)
120
+ while (cells.length < columnCount)
121
+ cells.push('');
122
+ return { rows, columnCount, delimiter: used };
123
+ }
@@ -0,0 +1,26 @@
1
+ export type TableSelectCellProps = {
2
+ /**
3
+ * The checkbox's name, not drawn: "Select Mallard" in a row, "Select
4
+ * all" in the header. Name the row by the words the row shows.
5
+ */
6
+ label: string;
7
+ checked: boolean;
8
+ /** The mixed state, for a header's "Select all" while some rows are chosen. */
9
+ indeterminate?: boolean;
10
+ onCheckedChange: (checked: boolean) => void;
11
+ disabled?: boolean;
12
+ /**
13
+ * `th` in a `TableHeader` row (the column's header cell, `scope="col"`),
14
+ * `td` (the default) in a body row. A fixed table sizes its columns from
15
+ * the header row, so put one in the header too.
16
+ */
17
+ as?: 'td' | 'th';
18
+ };
19
+ /**
20
+ * A table's select column: a cell holding one `Checkbox` whose label is
21
+ * visually hidden. The column is as narrow as the box, which is its own
22
+ * target, at least 44 × 44 px on a coarse pointer. Pair a chosen row with
23
+ * `TableRow selected`; a press on the box is left alone by a
24
+ * `TableRow pressable`.
25
+ */
26
+ export declare const TableSelectCell: import("react").ForwardRefExoticComponent<TableSelectCellProps & import("react").RefAttributes<HTMLTableCellElement>>;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import { tableSelectCellClass } from '../../css/css-table-select.js';
4
+ import { Checkbox } from '../Checkbox.js';
5
+ /**
6
+ * A table's select column: a cell holding one `Checkbox` whose label is
7
+ * visually hidden. The column is as narrow as the box, which is its own
8
+ * target, at least 44 × 44 px on a coarse pointer. Pair a chosen row with
9
+ * `TableRow selected`; a press on the box is left alone by a
10
+ * `TableRow pressable`.
11
+ */
12
+ export const TableSelectCell = forwardRef(function TableSelectCell({ as = 'td', checked, disabled = false, indeterminate = false, label, onCheckedChange, }, ref) {
13
+ if (typeof label !== 'string' || !label.trim())
14
+ throw new RangeError('TableSelectCell label must not be empty');
15
+ if (as !== 'td' && as !== 'th')
16
+ throw new RangeError(`Unknown TableSelectCell as: ${String(as)}`);
17
+ const Component = as;
18
+ return (_jsx(Component, { ref: ref, className: tableSelectCellClass, scope: as === 'th' ? 'col' : undefined, children: _jsx(Checkbox, { checked: checked, disabled: disabled, indeterminate: indeterminate, label: label, labelVisuallyHidden: true, onCheckedChange: onCheckedChange }) }));
19
+ });
@@ -1,5 +1,6 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { checkStrokeIn } from './chevron.js';
3
+ import { coarsePointerQuery, touchTarget } from './touch-target.js';
3
4
  const label = typographyVariants.label;
4
5
  const caption = typographyVariants.caption;
5
6
  export function cssCheckboxClasses() {
@@ -68,6 +69,38 @@ export function cssCheckboxClasses() {
68
69
 
69
70
  .sw-checkbox-input:checked + .sw-checkbox-mark::after { display: block; }
70
71
 
72
+ /* The mixed state: the accent fill with a dash in place of the check. */
73
+ .sw-checkbox-input:indeterminate + .sw-checkbox-mark {
74
+ background: var(--sw-color-accent);
75
+ border-color: var(--sw-color-accent);
76
+ }
77
+
78
+ .sw-checkbox-input:indeterminate + .sw-checkbox-mark::after {
79
+ border-right: 0;
80
+ display: block;
81
+ height: 0;
82
+ inset-block-start: calc(50% - 1px);
83
+ inset-inline-start: calc(50% - var(--sw-space-3) / 2);
84
+ transform: none;
85
+ width: var(--sw-space-3);
86
+ }
87
+
88
+ /* A visually hidden label: the box alone is the target, its own size on a
89
+ fine pointer (so a compact table row keeps its height), the 44 px touch
90
+ target both ways on a coarse one. */
91
+ .sw-checkbox-label-hidden {
92
+ justify-content: center;
93
+ min-height: 0;
94
+ position: relative;
95
+ }
96
+
97
+ @media ${coarsePointerQuery} {
98
+ .sw-checkbox-label-hidden {
99
+ min-height: ${touchTarget};
100
+ min-width: ${touchTarget};
101
+ }
102
+ }
103
+
71
104
  .sw-checkbox-input:focus-visible + .sw-checkbox-mark {
72
105
  outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
73
106
  outline-offset: var(--sw-focus-ring-offset);
@@ -104,13 +137,13 @@ export function cssCheckboxClasses() {
104
137
 
105
138
  @media (prefers-reduced-transparency: reduce) {
106
139
  .sw-checkbox-mark { background: var(--sw-color-surface); }
107
- .sw-checkbox-input:checked + .sw-checkbox-mark { background: var(--sw-color-accent); }
140
+ .sw-checkbox-input:is(:checked, :indeterminate) + .sw-checkbox-mark { background: var(--sw-color-accent); }
108
141
  }
109
142
 
110
143
  @media (forced-colors: active) {
111
144
  .sw-checkbox-mark { background: Canvas; border-color: CanvasText; }
112
- .sw-checkbox-input:checked + .sw-checkbox-mark { background: Highlight; border-color: Highlight; }
113
- .sw-checkbox-input:checked + .sw-checkbox-mark::after { border-color: HighlightText; }
145
+ .sw-checkbox-input:is(:checked, :indeterminate) + .sw-checkbox-mark { background: Highlight; border-color: Highlight; }
146
+ .sw-checkbox-input:is(:checked, :indeterminate) + .sw-checkbox-mark::after { border-color: HighlightText; }
114
147
  .sw-checkbox-input[aria-invalid='true'] + .sw-checkbox-mark { border-color: CanvasText; }
115
148
  }`;
116
149
  }
@@ -118,6 +151,7 @@ export function checkboxClassCatalog() {
118
151
  return [
119
152
  'sw-checkbox',
120
153
  'sw-checkbox-label',
154
+ 'sw-checkbox-label-hidden',
121
155
  'sw-checkbox-control',
122
156
  'sw-checkbox-input',
123
157
  'sw-checkbox-mark',
@@ -35,6 +35,7 @@ import { cssDialogBandedClasses } from './css-dialog-banded.js';
35
35
  import { cssDialogFrameClasses } from './css-dialog-frame.js';
36
36
  import { cssDialogCanvasClasses } from './css-dialog-canvas.js';
37
37
  import { cssDialogGrabberClasses } from './css-dialog-grabber.js';
38
+ import { cssDialogEndClasses } from './css-dialog-end.js';
38
39
  import { cssComboboxClasses } from './css-combobox.js';
39
40
  import { cssSelectClasses } from './css-select.js';
40
41
  import { cssMultiSelectClasses } from './css-multi-select.js';
@@ -224,6 +225,8 @@ ${cssDialogCanvasClasses()}
224
225
 
225
226
  ${cssDialogGrabberClasses()}
226
227
 
228
+ ${cssDialogEndClasses()}
229
+
227
230
  ${cssAccordionClasses()}
228
231
 
229
232
  ${cssSelectClasses()}
@@ -0,0 +1,13 @@
1
+ import { type Breakpoint } from './breakpoints.js';
2
+ /**
3
+ * Where a Dialog sits from the layout breakpoint up: `center` (the
4
+ * default) in the middle of the window, `end` as a side panel docked to
5
+ * the inline end (the right, the left in right-to-left).
6
+ */
7
+ export declare const dialogPlacements: readonly ["center", "end"];
8
+ export type DialogPlacement = (typeof dialogPlacements)[number];
9
+ export declare const dialogEndClass = "sw-dialog-end";
10
+ /** The side panel takes its place from this breakpoint up. */
11
+ export declare const dialogEndFrom: Breakpoint;
12
+ export declare function cssDialogEndClasses(): string;
13
+ export declare function dialogEndClassCatalog(): string[];
@@ -0,0 +1,75 @@
1
+ import { breakpointQuery } from './breakpoints.js';
2
+ /**
3
+ * Where a Dialog sits from the layout breakpoint up: `center` (the
4
+ * default) in the middle of the window, `end` as a side panel docked to
5
+ * the inline end (the right, the left in right-to-left).
6
+ */
7
+ export const dialogPlacements = ['center', 'end'];
8
+ export const dialogEndClass = 'sw-dialog-end';
9
+ /** The side panel takes its place from this breakpoint up. */
10
+ export const dialogEndFrom = 'md';
11
+ const end = `.${dialogEndClass}`;
12
+ /*
13
+ * From md up, `placement="end"` is a side panel: the window's full height
14
+ * at the inline end, square corners, one hairline on its inline-start
15
+ * edge, the size's width (md 32rem, lg 56rem, xl 65rem) with a space-8
16
+ * strip of backdrop always in view. Its column grows to the full height so
17
+ * the pinned footer sits on the bottom edge even when the content is short;
18
+ * longer content scrolls inside the panel under the sticky footer, as in
19
+ * the centred dialog. It slides in from its edge unless reduced motion is
20
+ * asked for. Below md the class draws nothing: the dialog is centred, or a
21
+ * bottom sheet with `sheetBelow`. Emitted after the banded and grabber
22
+ * rules so its edges win at equal specificity.
23
+ */
24
+ export function cssDialogEndClasses() {
25
+ return `@media ${breakpointQuery('from', dialogEndFrom)} {
26
+ ${end} {
27
+ border-color: var(--sw-color-border);
28
+ border-radius: 0;
29
+ border-width: 0;
30
+ border-inline-start-width: 1px;
31
+ height: 100%;
32
+ inset-block: 0;
33
+ margin: 0;
34
+ margin-inline-start: auto;
35
+ max-height: none;
36
+ }
37
+
38
+ ${end}[open] {
39
+ animation: sw-dialog-end-in var(--sw-motion-default) var(--sw-motion-easing);
40
+ display: flex;
41
+ flex-direction: column;
42
+ }
43
+
44
+ ${end}[open]:dir(rtl) {
45
+ animation-name: sw-dialog-end-in-rtl;
46
+ }
47
+
48
+ ${end} > .sw-stack {
49
+ flex: 1 0 auto;
50
+ }
51
+
52
+ ${end} .sw-dialog-footer {
53
+ margin-top: auto;
54
+ }
55
+ }
56
+
57
+ @keyframes sw-dialog-end-in {
58
+ from { translate: 100% 0; }
59
+ }
60
+
61
+ @keyframes sw-dialog-end-in-rtl {
62
+ from { translate: -100% 0; }
63
+ }
64
+
65
+ @media (prefers-reduced-motion: reduce) {
66
+ ${end}[open] { animation: none; }
67
+ }
68
+
69
+ @media (forced-colors: active) {
70
+ ${end} { border-inline-start-color: CanvasText; }
71
+ }`;
72
+ }
73
+ export function dialogEndClassCatalog() {
74
+ return [dialogEndClass];
75
+ }
@@ -2,6 +2,7 @@ import { typographyVariants } from '@scalewing/tokens';
2
2
  import { chevronStroke } from './chevron.js';
3
3
  import { zIndex } from './stacking.js';
4
4
  import { quietFill } from './quiet-fill.js';
5
+ import { coarsePointerQuery, touchTarget } from './touch-target.js';
5
6
  const caption = typographyVariants.caption;
6
7
  const label = typographyVariants.label;
7
8
  export function cssSelectClasses() {
@@ -186,6 +187,15 @@ export function cssSelectClasses() {
186
187
  border-top: 1px solid var(--sw-color-border);
187
188
  color: var(--sw-color-accent);
188
189
  margin-top: var(--sw-space-1);
190
+ }
191
+
192
+ /* The comfortable touch target on a coarse pointer, at every size: the
193
+ trigger and each option are at least 44 px high (the md control height),
194
+ as every Button is. Type and padding keep the size's; the rule follows
195
+ the size rules, which it outranks only by order. */
196
+ @media ${coarsePointerQuery} {
197
+ .sw-select .sw-select-trigger,
198
+ .sw-select .sw-select-option { min-height: ${touchTarget}; }
189
199
  }`;
190
200
  }
191
201
  export function selectClassCatalog() {
@@ -0,0 +1,3 @@
1
+ export declare const tableSelectCellClass = "sw-table-select";
2
+ export declare function cssTableSelectClasses(): string;
3
+ export declare function tableSelectClassCatalog(): string[];
@@ -0,0 +1,24 @@
1
+ import { coarsePointerQuery, touchTarget } from './touch-target.js';
2
+ export const tableSelectCellClass = 'sw-table-select';
3
+ /*
4
+ * TableSelectCell's column: as wide as the checkbox (its padding outside),
5
+ * in an auto or a fixed table alike; the box centred in the cell. On a
6
+ * coarse pointer the column widens to the box's 44 px target
7
+ * (`sw-checkbox-label-hidden`, css-checkbox.ts).
8
+ */
9
+ export function cssTableSelectClasses() {
10
+ return `.sw-table .${tableSelectCellClass} {
11
+ width: var(--sw-space-5);
12
+ }
13
+
14
+ .sw-table .${tableSelectCellClass} .sw-checkbox {
15
+ align-items: center;
16
+ }
17
+
18
+ @media ${coarsePointerQuery} {
19
+ .sw-table .${tableSelectCellClass} { width: ${touchTarget}; }
20
+ }`;
21
+ }
22
+ export function tableSelectClassCatalog() {
23
+ return [tableSelectCellClass];
24
+ }
@@ -7,6 +7,7 @@ import { cssTableLedgerClasses, tableLedgerClassCatalog, } from './css-table-led
7
7
  import { cssTableFooterClasses, tableFooterClassCatalog, } from './css-table-footer.js';
8
8
  import { cssTablePagePinClasses, tablePagePinClassCatalog, } from './css-table-page-pin.js';
9
9
  import { cssTableTotalRowClasses, tableTotalRowClassCatalog, } from './css-table-total-row.js';
10
+ import { cssTableSelectClasses, tableSelectClassCatalog, } from './css-table-select.js';
10
11
  import { cssTablePressClasses, tablePressClassCatalog, } from './css-table-press.js';
11
12
  import { cssTableStickyHeaderClasses, tableStickyHeaderClassCatalog, } from './css-table-sticky-header.js';
12
13
  import { cssTableSortClasses, tableSortClassCatalog, } from './css-table-sort.js';
@@ -103,6 +104,8 @@ ${cssTableBoundedClasses()}
103
104
 
104
105
  ${cssTablePressClasses()}
105
106
 
107
+ ${cssTableSelectClasses()}
108
+
106
109
  ${selectedRowRules()}`;
107
110
  }
108
111
  export function tableClassCatalog() {
@@ -123,5 +126,6 @@ export function tableClassCatalog() {
123
126
  ...tablePagePinClassCatalog(),
124
127
  ...tableBoundedClassCatalog(),
125
128
  ...tablePressClassCatalog(),
129
+ ...tableSelectClassCatalog(),
126
130
  ];
127
131
  }
@@ -68,6 +68,7 @@ import { dialogBandedClassCatalog } from './css-dialog-banded.js';
68
68
  import { dialogFrameClassCatalog } from './css-dialog-frame.js';
69
69
  import { dialogCanvasClassCatalog } from './css-dialog-canvas.js';
70
70
  import { dialogGrabberClassCatalog } from './css-dialog-grabber.js';
71
+ import { dialogEndClassCatalog } from './css-dialog-end.js';
71
72
  import { selectClassCatalog } from './css-select.js';
72
73
  import { multiSelectClassCatalog } from './css-multi-select.js';
73
74
  import { comboboxClassCatalog } from './css-combobox.js';
@@ -113,6 +114,7 @@ export function utilityClassCatalog() {
113
114
  ...dialogBandedClassCatalog(),
114
115
  ...dialogCanvasClassCatalog(),
115
116
  ...dialogGrabberClassCatalog(),
117
+ ...dialogEndClassCatalog(),
116
118
  ...accordionClassCatalog(),
117
119
  ...selectClassCatalog(),
118
120
  ...actionMenuClassCatalog(),
package/dist/index.d.ts CHANGED
@@ -15,6 +15,8 @@ export { ListRow, type ListRowProps } from './components/ListRow.js';
15
15
  export { listGroupVariants, listRowTitleAppearances, listRowTones, listRowValueTones, type ListGroupMaxHeight, type ListGroupVariant, type ListRowTitleAppearance, type ListRowTone, type ListRowValueTone, } from './list-options.js';
16
16
  export { listTileColumnCounts, type ListGroupTileColumns, } from './css/css-list-tiles.js';
17
17
  export { Checkbox, type CheckboxProps } from './components/Checkbox.js';
18
+ export { PasteRows, type PasteRowsColumn, type PasteRowsProps, } from './components/PasteRows.js';
19
+ export { parsePastedRows, type PastedRows, type PastedRowsDelimiter, pastedRowsDelimiters, } from './components/paste-rows/parse-pasted-rows.js';
18
20
  export { Spinner, type SpinnerProps, type SpinnerSize, } from './components/Spinner.js';
19
21
  export { Skeleton, skeletonVariants, type SkeletonProps, type SkeletonVariant, } from './components/Skeleton.js';
20
22
  export { EmptyState, emptyStateTitleElements, type EmptyStateProps, type EmptyStateTitleElement, } from './components/EmptyState.js';
@@ -40,7 +42,7 @@ export { Box, type BoxBorder, type BoxElement, type BoxLink, type BoxProps, } fr
40
42
  export { boxLinkAppearances } from './css/css-box-link.js';
41
43
  export { ButtonGroup, buttonGroupAppearances, type ButtonGroupAppearance, type ButtonGroupJustify, type ButtonGroupProps, } from './components/ButtonGroup.js';
42
44
  export { Card, type CardProps, type CardVariant } from './components/Card.js';
43
- export { Dialog, dialogAppearances, dialogSizes, type DialogAppearance, type DialogProps, type DialogRole, type DialogSize, type DialogTitleLevel, } from './components/Dialog.js';
45
+ export { Dialog, dialogAppearances, dialogPlacements, dialogSizes, type DialogAppearance, type DialogPlacement, type DialogProps, type DialogRole, type DialogSize, type DialogTitleLevel, } from './components/Dialog.js';
44
46
  export { StatTile, type StatTileAppearance, statTileAppearances, type StatTileEmphasis, type StatTileProps, type StatTileTone, statTileTones, } from './components/StatTile.js';
45
47
  export { TabPanel, type TabItem, type TabPanelProps, Tabs, type TabsProps, tabId, tabPanelId, } from './components/Tabs.js';
46
48
  export { Toast, toastPlacements, toastTones, type ToastAction, type ToastPlacement, } from './components/Toast.js';
@@ -69,7 +71,7 @@ export { Select } from './components/Select.js';
69
71
  export type { SelectAction, SelectOption, SelectProps, SelectWidth, } from './components/Select.js';
70
72
  export { MultiSelect, type MultiSelectAlign, type MultiSelectOption, type MultiSelectProps, } from './components/MultiSelect.js';
71
73
  export { Stack, type Align, type Justify, type StackProps, } from './components/Stack.js';
72
- export { Table, TableBody, TableCell, TableFooter, TableHeader, TableRow, tableCellTones, tableRowVariants, type TableBodyProps, type TableCellProps, type TableCellTone, type TableColumnWidth, type TableDensity, type TableFooterProps, type TableHeaderProps, type TableLayout, type TableMaxHeight, type TableProps, type TableRowProps, type TableRowVariant, type TableSort, type TableStickyHeader, type TableVerticalAlign, } from './components/Table.js';
74
+ export { Table, TableBody, TableCell, TableFooter, TableHeader, TableRow, TableSelectCell, tableCellTones, tableRowVariants, type TableBodyProps, type TableCellProps, type TableCellTone, type TableColumnWidth, type TableDensity, type TableFooterProps, type TableHeaderProps, type TableLayout, type TableMaxHeight, type TableProps, type TableRowProps, type TableRowVariant, type TableSelectCellProps, type TableSort, type TableStickyHeader, type TableVerticalAlign, } from './components/Table.js';
73
75
  export { Text, type TextAlign, type TextElement, type TextProps, } from './components/Text.js';
74
76
  export { ThemeProvider, useTheme, type ThemeProviderProps, } from './theme/ThemeProvider.js';
75
77
  export { themeCanvases, type ThemeCanvas } from './css/css-canvas-grouped.js';
package/dist/index.js CHANGED
@@ -14,6 +14,8 @@ export { ListRow } from './components/ListRow.js';
14
14
  export { listGroupVariants, listRowTitleAppearances, listRowTones, listRowValueTones, } from './list-options.js';
15
15
  export { listTileColumnCounts, } from './css/css-list-tiles.js';
16
16
  export { Checkbox } from './components/Checkbox.js';
17
+ export { PasteRows, } from './components/PasteRows.js';
18
+ export { parsePastedRows, pastedRowsDelimiters, } from './components/paste-rows/parse-pasted-rows.js';
17
19
  export { Spinner, } from './components/Spinner.js';
18
20
  export { Skeleton, skeletonVariants, } from './components/Skeleton.js';
19
21
  export { EmptyState, emptyStateTitleElements, } from './components/EmptyState.js';
@@ -39,7 +41,7 @@ export { Box, } from './components/Box.js';
39
41
  export { boxLinkAppearances } from './css/css-box-link.js';
40
42
  export { ButtonGroup, buttonGroupAppearances, } from './components/ButtonGroup.js';
41
43
  export { Card } from './components/Card.js';
42
- export { Dialog, dialogAppearances, dialogSizes, } from './components/Dialog.js';
44
+ export { Dialog, dialogAppearances, dialogPlacements, dialogSizes, } from './components/Dialog.js';
43
45
  export { StatTile, statTileAppearances, statTileTones, } from './components/StatTile.js';
44
46
  export { TabPanel, Tabs, tabId, tabPanelId, } from './components/Tabs.js';
45
47
  export { Toast, toastPlacements, toastTones, } from './components/Toast.js';
@@ -65,7 +67,7 @@ export { Combobox, comboboxSizes, } from './components/Combobox.js';
65
67
  export { Select } from './components/Select.js';
66
68
  export { MultiSelect, } from './components/MultiSelect.js';
67
69
  export { Stack, } from './components/Stack.js';
68
- export { Table, TableBody, TableCell, TableFooter, TableHeader, TableRow, tableCellTones, tableRowVariants, } from './components/Table.js';
70
+ export { Table, TableBody, TableCell, TableFooter, TableHeader, TableRow, TableSelectCell, tableCellTones, tableRowVariants, } from './components/Table.js';
69
71
  export { Text, } from './components/Text.js';
70
72
  export { ThemeProvider, useTheme, } from './theme/ThemeProvider.js';
71
73
  export { themeCanvases } from './css/css-canvas-grouped.js';
package/dist/styles.css CHANGED
@@ -1185,6 +1185,38 @@ body {
1185
1185
 
1186
1186
  .sw-checkbox-input:checked + .sw-checkbox-mark::after { display: block; }
1187
1187
 
1188
+ /* The mixed state: the accent fill with a dash in place of the check. */
1189
+ .sw-checkbox-input:indeterminate + .sw-checkbox-mark {
1190
+ background: var(--sw-color-accent);
1191
+ border-color: var(--sw-color-accent);
1192
+ }
1193
+
1194
+ .sw-checkbox-input:indeterminate + .sw-checkbox-mark::after {
1195
+ border-right: 0;
1196
+ display: block;
1197
+ height: 0;
1198
+ inset-block-start: calc(50% - 1px);
1199
+ inset-inline-start: calc(50% - var(--sw-space-3) / 2);
1200
+ transform: none;
1201
+ width: var(--sw-space-3);
1202
+ }
1203
+
1204
+ /* A visually hidden label: the box alone is the target, its own size on a
1205
+ fine pointer (so a compact table row keeps its height), the 44 px touch
1206
+ target both ways on a coarse one. */
1207
+ .sw-checkbox-label-hidden {
1208
+ justify-content: center;
1209
+ min-height: 0;
1210
+ position: relative;
1211
+ }
1212
+
1213
+ @media (pointer: coarse) {
1214
+ .sw-checkbox-label-hidden {
1215
+ min-height: var(--sw-control-md-min-height);
1216
+ min-width: var(--sw-control-md-min-height);
1217
+ }
1218
+ }
1219
+
1188
1220
  .sw-checkbox-input:focus-visible + .sw-checkbox-mark {
1189
1221
  outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
1190
1222
  outline-offset: var(--sw-focus-ring-offset);
@@ -1221,13 +1253,13 @@ body {
1221
1253
 
1222
1254
  @media (prefers-reduced-transparency: reduce) {
1223
1255
  .sw-checkbox-mark { background: var(--sw-color-surface); }
1224
- .sw-checkbox-input:checked + .sw-checkbox-mark { background: var(--sw-color-accent); }
1256
+ .sw-checkbox-input:is(:checked, :indeterminate) + .sw-checkbox-mark { background: var(--sw-color-accent); }
1225
1257
  }
1226
1258
 
1227
1259
  @media (forced-colors: active) {
1228
1260
  .sw-checkbox-mark { background: Canvas; border-color: CanvasText; }
1229
- .sw-checkbox-input:checked + .sw-checkbox-mark { background: Highlight; border-color: Highlight; }
1230
- .sw-checkbox-input:checked + .sw-checkbox-mark::after { border-color: HighlightText; }
1261
+ .sw-checkbox-input:is(:checked, :indeterminate) + .sw-checkbox-mark { background: Highlight; border-color: Highlight; }
1262
+ .sw-checkbox-input:is(:checked, :indeterminate) + .sw-checkbox-mark::after { border-color: HighlightText; }
1231
1263
  .sw-checkbox-input[aria-invalid='true'] + .sw-checkbox-mark { border-color: CanvasText; }
1232
1264
  }
1233
1265
 
@@ -2174,6 +2206,18 @@ body {
2174
2206
  .sw-table-action { color: ButtonText; }
2175
2207
  }
2176
2208
 
2209
+ .sw-table .sw-table-select {
2210
+ width: var(--sw-space-5);
2211
+ }
2212
+
2213
+ .sw-table .sw-table-select .sw-checkbox {
2214
+ align-items: center;
2215
+ }
2216
+
2217
+ @media (pointer: coarse) {
2218
+ .sw-table .sw-table-select { width: var(--sw-control-md-min-height); }
2219
+ }
2220
+
2177
2221
  .sw-table-row-selected > :first-child {
2178
2222
  position: relative;
2179
2223
  }
@@ -5837,6 +5881,54 @@ html:has(.sw-site-header-sticky) {
5837
5881
  }
5838
5882
  }
5839
5883
 
5884
+ @media (min-width: 48rem) {
5885
+ .sw-dialog-end {
5886
+ border-color: var(--sw-color-border);
5887
+ border-radius: 0;
5888
+ border-width: 0;
5889
+ border-inline-start-width: 1px;
5890
+ height: 100%;
5891
+ inset-block: 0;
5892
+ margin: 0;
5893
+ margin-inline-start: auto;
5894
+ max-height: none;
5895
+ }
5896
+
5897
+ .sw-dialog-end[open] {
5898
+ animation: sw-dialog-end-in var(--sw-motion-default) var(--sw-motion-easing);
5899
+ display: flex;
5900
+ flex-direction: column;
5901
+ }
5902
+
5903
+ .sw-dialog-end[open]:dir(rtl) {
5904
+ animation-name: sw-dialog-end-in-rtl;
5905
+ }
5906
+
5907
+ .sw-dialog-end > .sw-stack {
5908
+ flex: 1 0 auto;
5909
+ }
5910
+
5911
+ .sw-dialog-end .sw-dialog-footer {
5912
+ margin-top: auto;
5913
+ }
5914
+ }
5915
+
5916
+ @keyframes sw-dialog-end-in {
5917
+ from { translate: 100% 0; }
5918
+ }
5919
+
5920
+ @keyframes sw-dialog-end-in-rtl {
5921
+ from { translate: -100% 0; }
5922
+ }
5923
+
5924
+ @media (prefers-reduced-motion: reduce) {
5925
+ .sw-dialog-end[open] { animation: none; }
5926
+ }
5927
+
5928
+ @media (forced-colors: active) {
5929
+ .sw-dialog-end { border-inline-start-color: CanvasText; }
5930
+ }
5931
+
5840
5932
  .sw-accordion {
5841
5933
  background: var(--sw-glass-fill);
5842
5934
  border: 1px solid var(--sw-glass-border);
@@ -6091,6 +6183,15 @@ html:has(.sw-site-header-sticky) {
6091
6183
  margin-top: var(--sw-space-1);
6092
6184
  }
6093
6185
 
6186
+ /* The comfortable touch target on a coarse pointer, at every size: the
6187
+ trigger and each option are at least 44 px high (the md control height),
6188
+ as every Button is. Type and padding keep the size's; the rule follows
6189
+ the size rules, which it outranks only by order. */
6190
+ @media (pointer: coarse) {
6191
+ .sw-select .sw-select-trigger,
6192
+ .sw-select .sw-select-option { min-height: var(--sw-control-md-min-height); }
6193
+ }
6194
+
6094
6195
  .sw-action-menu {
6095
6196
  display: inline-flex;
6096
6197
  position: relative;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.52.0",
3
+ "version": "1.53.0",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",