@scalewing/react 1.17.0 → 1.18.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 +8 -0
- package/README.md +25 -2
- package/dist/components/ActionBar.d.ts +16 -6
- package/dist/components/ActionBar.js +1 -1
- package/dist/components/Dialog.d.ts +20 -1
- package/dist/components/Dialog.js +4 -3
- package/dist/components/Tabs.d.ts +22 -2
- package/dist/components/Tabs.js +4 -3
- package/dist/components/dialog/DialogTitle.d.ts +15 -0
- package/dist/components/dialog/DialogTitle.js +14 -0
- package/dist/css/css-action-bar.js +7 -0
- package/dist/css/css-dialog.d.ts +2 -0
- package/dist/css/css-dialog.js +125 -3
- package/dist/css/css-tabs.d.ts +1 -0
- package/dist/css/css-tabs.js +42 -1
- package/dist/css/stacking.js +3 -2
- package/dist/styles.css +104 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.18.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ea2fdf0: `ActionBar` `status` takes a node (`docs/requests/fantasy-football-action-bar-status.md`, the fantasy-football companion's trade verdict): `status?: ReactNode`, widened from `string`, so a `Badge` and a short line can share the status. The status element is now a `div` (was a `p`) that keeps `className="sw-action-bar-status"` and `role="status"`, is still rendered while empty, and still takes no room when empty. It lays its children on one wrapping row a `space-2` gap apart (`space-1` between wrapped rows), so a line that does not fit beside its badge wraps under it at phone width. A string status renders as before; a phrase that mixes text and elements belongs in one `span`. Consumers whose tests looked for a `P` status element need to update that query. No new classes, tokens or dependencies.
|
|
8
|
+
- d02521e: `Dialog` `sheetBelow` and `closeLabel` (`docs/requests/fantasy-football-dialog-sheet.md`, the fantasy-football companion's phone review dialogs). `sheetBelow?: Breakpoint` (`'md'`): below that breakpoint the dialog is a bottom sheet, docked to the bottom edge at the full viewport width, top corners at `--sw-radius-lg` and bottom corners square, a `space-8` strip of backdrop left above it (the same dvh-aware cap), and bottom padding that clears `env(safe-area-inset-bottom)`; it slides up on open with `--sw-motion-default`, not under `prefers-reduced-motion`, and draws no grabber. From the breakpoint up it is the centered dialog at its `size`. `closeLabel?: string`: an icon-only ghost close `Button` ends the title row, named by the label, and calls `onClose`; the title still names the dialog and `titleLevel` still applies. The button is the first control, so it takes the focus on open unless content has `autoFocus`. Its X is private control chrome drawn in CSS (no icon dependency). Without `closeLabel` the markup is unchanged. New generated classes `sw-dialog-sheet-below-md`, `sw-dialog-header`, `sw-dialog-close`, `sw-dialog-close-glyph`. No new tokens and no new dependencies.
|
|
9
|
+
- c5fd0a9: `Tabs` `sticky` (`docs/requests/fantasy-football-tabs-sticky.md`, the fantasy-football companion's section tabs): `sticky?: boolean`, default `false`, keeps the strip at the top of the viewport (`top: env(safe-area-inset-top, 0px)`) while a long panel scrolls under it, on the sticky `AppHeader`'s `topChrome` layer. The stuck strip is a full-bleed band of the page canvas, not a glass card: `--sw-color-background` at 90 % over the glass blur and saturate tokens, with the strip's existing hairline, solid canvas under `prefers-reduced-transparency` and `Canvas` in forced colors. A sticky element only sticks within its parent, so make the strip a direct child of the page container that holds the panels; each tab's md inline padding (16 px, spacing step 4) lines the first label up with a `space-4` page gutter. New generated class `sw-tabs-sticky`. No new tokens and no new dependencies.
|
|
10
|
+
|
|
3
11
|
## 1.17.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ A disabled native text control (`<input>`, `<select>` or `<textarea>` on the can
|
|
|
126
126
|
</Accordion>
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
`ActionBar` keeps the actions of a long page on a glass bar stuck to the bottom of the viewport, with an optional
|
|
129
|
+
`ActionBar` keeps the actions of a long page on a glass bar stuck to the bottom of the viewport, with an optional short `status` (such as when the work was last saved): a string, or a node such as a `Badge` and then a short line. Put it last in the content it acts on: it stays stuck while that content scrolls by and then rests in its own place at the end. `stickyBelow="md"` sticks only on a phone and leaves the bar in page flow from `md` up. The bar clears `env(safe-area-inset-bottom)` (set `viewport-fit=cover` in the page's viewport meta for the inset to apply). Its children are the actions: on a phone they share one row under the status. The status is a polite live region (`role="status"`, kept in the page even while empty), so a new status such as "Draft saved at 5:00 PM" is announced; do not announce the same save a second time with your own notice. The status is a `div` whose children sit on one row a `space-2` gap apart and wrap under one another when the row is full, so a line that does not fit beside its badge drops under it on a phone; put a phrase that mixes text and elements in one `span` so it wraps as a sentence. `undefined`, `null` or `false` leaves the status empty. An open `Select`, `ActionMenu` or `Tooltip` paints over the bar, including inside a glass `Card` or `Accordion`.
|
|
130
130
|
|
|
131
131
|
```tsx
|
|
132
132
|
<Stack gap={4}>
|
|
@@ -155,7 +155,7 @@ A `Button` that toggles something passes `aria-pressed`. The pressed button gets
|
|
|
155
155
|
|
|
156
156
|
`ActionMenu` opens independent commands from a labelled button. Provide localized command labels and callbacks; use `disabled` for unavailable commands and `destructive` for a dangerous command's presentation. Escape and choosing a command both return focus to the trigger (before the command runs, so a `Dialog` it opens hands focus back to the trigger on close), and outside interaction dismisses the menu. The open menu sits on the popover layer a `space-1` gap below its trigger (above it when it only fits there), at least `space-2` from every viewport edge (a classic scrollbar is not part of the viewport), and aligned to the trigger's inline start (its right edge in right-to-left), or to its other edge when the trigger ends a row and the menu would not fit from its start. `align="end"` (default `"start"`) lines the menu up with the trigger's inline end even where the start would fit, for a trigger that ends a card or a list row, so the menu stays over that card instead of hanging past it; it takes the start when the end would leave the screen. The menu is as wide as its longest command up to the viewport less both insets, and a longer command wraps. The menu follows its trigger through scroll, window resize, and a change in the menu's or the trigger's size. The menu is in the DOM only while it is open. The trigger and its commands use the compact xs control height for a fine pointer; on a coarse pointer the trigger is at least 44 × 44 px (the md control height, as on `CalendarButton`) and every command is at least 44 px tall.
|
|
157
157
|
|
|
158
|
-
`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`.
|
|
158
|
+
`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.
|
|
159
159
|
|
|
160
160
|
`Toast` is an auto-dismiss confirmation on the popover layer, controlled by `open` and `onOpenChange`; it does not trap focus and closes after `timeoutMs` (default 800, or 6000 for a `warning` or `danger` toast, which must be read). `anchor` and `target` send it from a press to a destination. `tone` (`neutral` by default, `success`, `warning`, `danger`) tints its border and an optional consumer `icon` (a Lucide glyph, hidden from assistive technology); the message keeps the text color, so its words carry the meaning. A `danger` toast is announced as `role="alert"`, the others as `role="status"`, so do not wrap the message in another live region. A toast still dismisses itself: an error the person must act on belongs in the page. A toast is as wide as its message, up to the page's gutter (spacing step 4) on each side, so on a phone a long message wraps inside the gutter instead of running to the screen's edges.
|
|
161
161
|
|
|
@@ -218,6 +218,29 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
|
|
|
218
218
|
/>
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
+
`Tabs` is the strip for the sections of one page (`role="tablist"`, one `role="tab"` button per item; arrow keys, Home and End move and select), and `TabPanel` is each section's content, hidden while another tab is current. `sticky` (default `false`) keeps the strip at the top of the viewport, under `env(safe-area-inset-top)`, while a long panel scrolls under it, on the same layer as a sticky `AppHeader` (so a page uses one or the other at the top edge). The stuck strip is a full-bleed band of the page canvas, not a glass card: the canvas color at nine parts in ten over the glass blur, with the strip's hairline under it, solid canvas under Reduce Transparency and in forced colors. A sticky element only sticks within its parent: make the strip a direct child of the long page container that also holds the panels, not of a padded `Box` round the strip alone, and keep `overflow` visible on its ancestors. Put the page's side gutter on the title and the panels instead: each tab's inline padding is the md control's 16 px (spacing step 4), so the first label lines up with a `space-4` gutter when the strip runs edge to edge.
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
<Stack gap={3}>
|
|
225
|
+
<Box paddingX={4}>
|
|
226
|
+
<Text variant="title">Wetland reserve</Text>
|
|
227
|
+
</Box>
|
|
228
|
+
<Tabs
|
|
229
|
+
aria-label="Reserve log"
|
|
230
|
+
id="reserve"
|
|
231
|
+
items={groups}
|
|
232
|
+
onChange={setGroup}
|
|
233
|
+
sticky
|
|
234
|
+
value={group}
|
|
235
|
+
/>
|
|
236
|
+
<Box paddingX={4}>
|
|
237
|
+
<TabPanel id="birds" tabsId="reserve" value={group}>
|
|
238
|
+
{birdLog}
|
|
239
|
+
</TabPanel>
|
|
240
|
+
</Box>
|
|
241
|
+
</Stack>
|
|
242
|
+
```
|
|
243
|
+
|
|
221
244
|
`Table` aligns data in rows inside its own keyboard-reachable scroll region, named after the table. While the table is wider than the region, each inline edge with columns scrolled out past it draws a soft inset shade (`sw-scroll-more-start`, `sw-scroll-more-end`, mirrored right to left), so a phone, whose scrollbars stay hidden, still shows that the table goes on; a table that fits draws none, and in forced colors the system scrollbar is the cue. `TableCell` takes `numeric` (tabular numerals, end aligned), `align` and `truncate`; `density="compact"` densifies cells. `TableRow selected` sets `aria-selected` and marks the row with a 4 px accent bar at its inline start (3:1 or more against the surface and the page), the right edge in a right-to-left table. The bar is drawn inside the first cell's padding and takes no layout space, so no column moves when a row is picked, and the row takes no fill, so text, muted and accent text keep their contrast; forced colors keep the bar in the system highlight.
|
|
222
245
|
|
|
223
246
|
`Spinner` shows indeterminate loading in small, medium, or large sizes. Supply localized `label` for the one announced status in a loading region. Use `decorative` on additional indicators beside that status so screen readers do not hear the same message repeatedly. Reduced motion leaves a static accented ring.
|
|
@@ -4,10 +4,15 @@ export type ActionBarProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> &
|
|
|
4
4
|
/** The actions, usually two or three `Button`s. */
|
|
5
5
|
children: ReactNode;
|
|
6
6
|
/**
|
|
7
|
-
* One short line about the work, such as when it was last saved
|
|
8
|
-
*
|
|
7
|
+
* One short line about the work, such as when it was last saved, or a
|
|
8
|
+
* `Badge` and then a short line ("Ready" and what is ready). It is a
|
|
9
|
+
* polite live region, so a new status is announced without moving focus;
|
|
10
|
+
* nothing (`undefined`, `null`, `false`) leaves it empty and taking no
|
|
11
|
+
* room. Its direct children sit on one row a small gap apart and wrap
|
|
12
|
+
* under one another when the row is full, so put a phrase that mixes text
|
|
13
|
+
* and elements in one `span` to keep it wrapping as a sentence.
|
|
9
14
|
*/
|
|
10
|
-
status?:
|
|
15
|
+
status?: ReactNode;
|
|
11
16
|
/** Sticky only below this breakpoint; from it up the bar sits in page flow. */
|
|
12
17
|
stickyBelow?: Breakpoint;
|
|
13
18
|
};
|
|
@@ -21,10 +26,15 @@ export declare const ActionBar: import("react").ForwardRefExoticComponent<Omit<H
|
|
|
21
26
|
/** The actions, usually two or three `Button`s. */
|
|
22
27
|
children: ReactNode;
|
|
23
28
|
/**
|
|
24
|
-
* One short line about the work, such as when it was last saved
|
|
25
|
-
*
|
|
29
|
+
* One short line about the work, such as when it was last saved, or a
|
|
30
|
+
* `Badge` and then a short line ("Ready" and what is ready). It is a
|
|
31
|
+
* polite live region, so a new status is announced without moving focus;
|
|
32
|
+
* nothing (`undefined`, `null`, `false`) leaves it empty and taking no
|
|
33
|
+
* room. Its direct children sit on one row a small gap apart and wrap
|
|
34
|
+
* under one another when the row is full, so put a phrase that mixes text
|
|
35
|
+
* and elements in one `span` to keep it wrapping as a sentence.
|
|
26
36
|
*/
|
|
27
|
-
status?:
|
|
37
|
+
status?: ReactNode;
|
|
28
38
|
/** Sticky only below this breakpoint; from it up the bar sits in page flow. */
|
|
29
39
|
stickyBelow?: Breakpoint;
|
|
30
40
|
} & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -9,5 +9,5 @@ import { actionBarStickyClass } from '../css/css-action-bar.js';
|
|
|
9
9
|
* in its own place at the end.
|
|
10
10
|
*/
|
|
11
11
|
export const ActionBar = forwardRef(function ActionBar({ children, className, status, stickyBelow, ...rest }, ref) {
|
|
12
|
-
return (_jsxs("div", { ref: ref, className: cx('sw-action-bar', actionBarStickyClass(stickyBelow), className), ...rest, children: [_jsx("
|
|
12
|
+
return (_jsxs("div", { ref: ref, className: cx('sw-action-bar', actionBarStickyClass(stickyBelow), className), ...rest, children: [_jsx("div", { className: "sw-action-bar-status", role: "status", children: status }), _jsx("div", { className: "sw-action-bar-actions", children: children })] }));
|
|
13
13
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type DialogHTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
import { type Breakpoint } from '../css/breakpoints.js';
|
|
2
3
|
/**
|
|
3
4
|
* `md` is the reading width (32rem) for a message or a short form; `lg`
|
|
4
5
|
* (56rem) holds a row of six fields or a data grid without folding it.
|
|
@@ -31,6 +32,24 @@ export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose
|
|
|
31
32
|
* Any value other than 2 (from an untyped caller) renders an `h3`.
|
|
32
33
|
*/
|
|
33
34
|
titleLevel?: DialogTitleLevel;
|
|
35
|
+
/**
|
|
36
|
+
* Below this breakpoint the dialog is a bottom sheet: docked to the
|
|
37
|
+
* bottom edge at the full viewport width, its top corners rounded and its
|
|
38
|
+
* bottom corners square, a strip of backdrop left in view above it, and
|
|
39
|
+
* its bottom padding clearing `env(safe-area-inset-bottom)`. It slides up
|
|
40
|
+
* on open unless reduced motion is asked for. There is no drag grabber
|
|
41
|
+
* (and no drag): it closes as any dialog does. From the breakpoint up it
|
|
42
|
+
* is the centered dialog, at its `size`.
|
|
43
|
+
*/
|
|
44
|
+
sheetBelow?: Breakpoint;
|
|
45
|
+
/**
|
|
46
|
+
* The accessible name of a close button, such as "Close". When given, an
|
|
47
|
+
* icon-only ghost button ends the title row and asks `onClose`, like
|
|
48
|
+
* Escape. It is the first control in the dialog, so it takes the focus on
|
|
49
|
+
* open unless a control in the content has `autoFocus`. Left out, the
|
|
50
|
+
* dialog has no close button and its markup is unchanged.
|
|
51
|
+
*/
|
|
52
|
+
closeLabel?: string;
|
|
34
53
|
children: ReactNode;
|
|
35
54
|
};
|
|
36
|
-
export declare function Dialog({ children, className, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, size, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
|
|
55
|
+
export declare function Dialog({ children, className, closeLabel, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useId } from 'react';
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
|
+
import { dialogSheetClass } from '../css/css-dialog.js';
|
|
5
6
|
import { spacingClassNames } from '../spacing-classes.js';
|
|
6
7
|
import { useDialogCloseRequests } from './dialog-close-requests.js';
|
|
8
|
+
import { DialogTitle } from './dialog/DialogTitle.js';
|
|
7
9
|
import { Stack } from './Stack.js';
|
|
8
|
-
|
|
9
|
-
export function Dialog({ children, className, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, size = 'md', title, titleLevel = 3, ...rest }) {
|
|
10
|
+
export function Dialog({ children, className, closeLabel, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size = 'md', title, titleLevel = 3, ...rest }) {
|
|
10
11
|
const titleId = useId();
|
|
11
12
|
const { nodeRef, handlers } = useDialogCloseRequests({
|
|
12
13
|
open,
|
|
@@ -16,5 +17,5 @@ export function Dialog({ children, className, onCancel, onClose, onKeyDown, onPo
|
|
|
16
17
|
onPointerDown,
|
|
17
18
|
onSubmit,
|
|
18
19
|
});
|
|
19
|
-
return (_jsx("dialog", { ...rest, ref: nodeRef, "aria-labelledby": titleId, className: cx('sw-dialog', size === 'lg' && 'sw-dialog-lg', ...spacingClassNames({ padding: 5 }), className), ...handlers, children: _jsxs(Stack, { gap: 4, children: [_jsx(
|
|
20
|
+
return (_jsx("dialog", { ...rest, ref: nodeRef, "aria-labelledby": titleId, className: cx('sw-dialog', size === 'lg' && 'sw-dialog-lg', sheetBelow && dialogSheetClass(sheetBelow), ...spacingClassNames({ padding: 5 }), className), ...handlers, children: _jsxs(Stack, { gap: 4, children: [_jsx(DialogTitle, { closeLabel: closeLabel, id: titleId, level: titleLevel === 2 ? 2 : 3, onClose: onClose, title: title }), children] }) }));
|
|
20
21
|
}
|
|
@@ -16,15 +16,35 @@ export type TabsProps = TabsLabel & {
|
|
|
16
16
|
items: readonly TabItem[];
|
|
17
17
|
value: string;
|
|
18
18
|
onChange: (id: string) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Keep the strip at the top of the viewport, under the top safe area,
|
|
21
|
+
* while a long panel scrolls under it. Off by default. The stuck strip is
|
|
22
|
+
* a full-bleed band of the page canvas, not a glass card: the canvas
|
|
23
|
+
* color, a little see-through over the glass blur, with its hairline
|
|
24
|
+
* underneath; solid under Reduce Transparency and in forced colors. It
|
|
25
|
+
* shares the sticky `AppHeader`'s layer, so a page uses one or the other
|
|
26
|
+
* at the top edge.
|
|
27
|
+
*
|
|
28
|
+
* A sticky element only sticks within its parent. Make the strip a direct
|
|
29
|
+
* child of the long page container that also holds the panels, not of a
|
|
30
|
+
* padded `Box` round the strip alone; an ancestor whose `overflow` is not
|
|
31
|
+
* `visible` becomes the box it sticks to. Put the page's side gutter on
|
|
32
|
+
* the title and the panels rather than on that container: each tab
|
|
33
|
+
* already has the md control's inline padding (16 px, spacing step 4), so
|
|
34
|
+
* the first label lines up with a `space-4` page gutter when the strip
|
|
35
|
+
* runs edge to edge.
|
|
36
|
+
*/
|
|
37
|
+
sticky?: boolean;
|
|
19
38
|
};
|
|
20
39
|
export declare function tabId(tabsId: string, id: string): string;
|
|
21
40
|
export declare function tabPanelId(tabsId: string, id: string): string;
|
|
22
41
|
/**
|
|
23
42
|
* A tab strip with `tablist` semantics for the sections of one page. Arrow
|
|
24
43
|
* keys, Home and End move the focus and select at once; the strip scrolls
|
|
25
|
-
* sideways when it overflows. Pair each item with a `TabPanel`.
|
|
44
|
+
* sideways when it overflows. Pair each item with a `TabPanel`. `sticky`
|
|
45
|
+
* keeps it at the top of the viewport over a long panel.
|
|
26
46
|
*/
|
|
27
|
-
export declare function Tabs({ id, items, onChange, value, ...labelProps }: TabsProps): import("react").JSX.Element;
|
|
47
|
+
export declare function Tabs({ id, items, onChange, sticky, value, ...labelProps }: TabsProps): import("react").JSX.Element;
|
|
28
48
|
export type TabPanelProps = Omit<HTMLAttributes<HTMLDivElement>, 'id'> & {
|
|
29
49
|
/** The `id` of the `Tabs` this panel belongs to. */
|
|
30
50
|
tabsId: string;
|
package/dist/components/Tabs.js
CHANGED
|
@@ -10,9 +10,10 @@ export function tabPanelId(tabsId, id) {
|
|
|
10
10
|
/**
|
|
11
11
|
* A tab strip with `tablist` semantics for the sections of one page. Arrow
|
|
12
12
|
* keys, Home and End move the focus and select at once; the strip scrolls
|
|
13
|
-
* sideways when it overflows. Pair each item with a `TabPanel`.
|
|
13
|
+
* sideways when it overflows. Pair each item with a `TabPanel`. `sticky`
|
|
14
|
+
* keeps it at the top of the viewport over a long panel.
|
|
14
15
|
*/
|
|
15
|
-
export function Tabs({ id, items, onChange, value, ...labelProps }) {
|
|
16
|
+
export function Tabs({ id, items, onChange, sticky = false, value, ...labelProps }) {
|
|
16
17
|
const tabs = useRef(new Map());
|
|
17
18
|
function select(next) {
|
|
18
19
|
if (!next || next.id === value) {
|
|
@@ -42,7 +43,7 @@ export function Tabs({ id, items, onChange, value, ...labelProps }) {
|
|
|
42
43
|
event.preventDefault();
|
|
43
44
|
select(target);
|
|
44
45
|
}
|
|
45
|
-
return (_jsx("div", { className:
|
|
46
|
+
return (_jsx("div", { className: cx('sw-tabs', sticky && 'sw-tabs-sticky'), onKeyDown: onKeyDown, role: "tablist", ...labelProps, children: items.map((item) => {
|
|
46
47
|
const selected = item.id === value;
|
|
47
48
|
return (_jsx("button", { "aria-controls": tabPanelId(id, item.id), "aria-selected": selected, className: cx('sw-tab', selected && 'sw-tab-selected'), id: tabId(id, item.id), onClick: () => select(item), ref: (node) => {
|
|
48
49
|
if (node) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type DialogTitleProps = {
|
|
2
|
+
id: string;
|
|
3
|
+
level: 2 | 3;
|
|
4
|
+
title: string;
|
|
5
|
+
/** The close button's accessible name; without it there is no button. */
|
|
6
|
+
closeLabel?: string;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The dialog's title, which names it through `aria-labelledby`. With a
|
|
11
|
+
* `closeLabel` it shares a row with an icon-only ghost close button at its
|
|
12
|
+
* end; without one it is the title alone, as it always was.
|
|
13
|
+
*/
|
|
14
|
+
export declare function DialogTitle({ closeLabel, id, level, onClose, title, }: DialogTitleProps): import("react").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from '../Button.js';
|
|
3
|
+
import { Text } from '../Text.js';
|
|
4
|
+
/**
|
|
5
|
+
* The dialog's title, which names it through `aria-labelledby`. With a
|
|
6
|
+
* `closeLabel` it shares a row with an icon-only ghost close button at its
|
|
7
|
+
* end; without one it is the title alone, as it always was.
|
|
8
|
+
*/
|
|
9
|
+
export function DialogTitle({ closeLabel, id, level, onClose, title, }) {
|
|
10
|
+
const heading = (_jsx(Text, { as: level === 2 ? 'h2' : 'h3', id: id, variant: "title", children: title }));
|
|
11
|
+
if (!closeLabel)
|
|
12
|
+
return heading;
|
|
13
|
+
return (_jsxs("div", { className: "sw-dialog-header", children: [heading, _jsxs(Button, { className: "sw-dialog-close", onPress: onClose, size: "sm", variant: "ghost", children: [_jsx("span", { className: "sw-sr-only", children: closeLabel }), _jsx("span", { "aria-hidden": "true", className: "sw-dialog-close-glyph" })] })] }));
|
|
14
|
+
}
|
|
@@ -50,14 +50,21 @@ export function cssActionBarClasses() {
|
|
|
50
50
|
padding-right: max(var(--sw-space-4), env(safe-area-inset-right, 0px));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/* A Badge and a short line sit on one row a small gap apart; a line that
|
|
54
|
+
does not fit beside the badge wraps under it. */
|
|
53
55
|
.sw-action-bar-status {
|
|
56
|
+
align-items: center;
|
|
54
57
|
color: var(--sw-color-muted);
|
|
58
|
+
column-gap: var(--sw-space-2);
|
|
59
|
+
display: flex;
|
|
55
60
|
flex: 1 1 auto;
|
|
61
|
+
flex-wrap: wrap;
|
|
56
62
|
font-family: var(--sw-font-sans);
|
|
57
63
|
font-size: ${caption.fontSize}px;
|
|
58
64
|
line-height: ${caption.lineHeight}px;
|
|
59
65
|
margin: 0;
|
|
60
66
|
min-width: 0;
|
|
67
|
+
row-gap: var(--sw-space-1);
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
/* With no status the live region stays in the page, so a first status is
|
package/dist/css/css-dialog.d.ts
CHANGED
package/dist/css/css-dialog.js
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { breakpointQuery, breakpoints, } from './breakpoints.js';
|
|
3
|
+
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
4
|
+
const title = typographyVariants.title;
|
|
5
|
+
/* The modal's height cap: a space-8 strip of backdrop stays in view. */
|
|
6
|
+
const maxHeight = 'min(100vh - var(--sw-space-8), 100dvh - var(--sw-space-8))';
|
|
7
|
+
export function dialogSheetClass(sheetBelow) {
|
|
8
|
+
return `sw-dialog-sheet-below-${sheetBelow}`;
|
|
9
|
+
}
|
|
10
|
+
/*
|
|
11
|
+
* Below the breakpoint the dialog is a bottom sheet: docked to the bottom
|
|
12
|
+
* edge at the full viewport width, the top corners rounded and the bottom
|
|
13
|
+
* ones square, the same height cap (so the whole space-8 strip of backdrop
|
|
14
|
+
* shows above it), and padding that clears the safe areas. Emitted after
|
|
15
|
+
* `sw-dialog-lg` so it wins at equal specificity. No grabber: there is no
|
|
16
|
+
* drag gesture.
|
|
17
|
+
*/
|
|
18
|
+
function sheetRules() {
|
|
19
|
+
return breakpoints
|
|
20
|
+
.map((breakpoint) => `@media ${breakpointQuery('below', breakpoint)} {
|
|
21
|
+
.${dialogSheetClass(breakpoint)} {
|
|
22
|
+
border-radius: var(--sw-radius-lg) var(--sw-radius-lg) 0 0;
|
|
23
|
+
border-width: 1px 0 0;
|
|
24
|
+
margin: auto 0 0;
|
|
25
|
+
max-height: ${maxHeight};
|
|
26
|
+
max-width: none;
|
|
27
|
+
padding-bottom: calc(var(--sw-space-5) + env(safe-area-inset-bottom, 0px));
|
|
28
|
+
padding-left: max(var(--sw-space-5), env(safe-area-inset-left, 0px));
|
|
29
|
+
padding-right: max(var(--sw-space-5), env(safe-area-inset-right, 0px));
|
|
30
|
+
width: 100%;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.${dialogSheetClass(breakpoint)}[open] {
|
|
34
|
+
animation: sw-dialog-sheet-in var(--sw-motion-default) var(--sw-motion-easing);
|
|
35
|
+
}
|
|
36
|
+
}`)
|
|
37
|
+
.join('\n\n');
|
|
38
|
+
}
|
|
39
|
+
function sheetMotionRules() {
|
|
40
|
+
const still = breakpoints
|
|
41
|
+
.map((breakpoint) => ` .${dialogSheetClass(breakpoint)}[open] { animation: none; }`)
|
|
42
|
+
.join('\n');
|
|
43
|
+
return `@keyframes sw-dialog-sheet-in {
|
|
44
|
+
from { translate: 0 100%; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@media (prefers-reduced-motion: reduce) {
|
|
48
|
+
${still}
|
|
49
|
+
}`;
|
|
50
|
+
}
|
|
51
|
+
/*
|
|
52
|
+
* The title row when the dialog has a close button: the title takes the
|
|
53
|
+
* row and the button ends it. The button's negative margins keep the row
|
|
54
|
+
* as tall as one title line and put the glyph's end on the padding edge,
|
|
55
|
+
* at the sm control size and at the 44 px touch target alike.
|
|
56
|
+
*/
|
|
57
|
+
function closeButtonRules() {
|
|
58
|
+
const square = (size) => `min-width: ${size};
|
|
59
|
+
margin-block: calc((${title.lineHeight}px - ${size}) / 2);
|
|
60
|
+
margin-inline-end: calc((var(--sw-space-4) - ${size}) / 2);`;
|
|
61
|
+
return `.sw-dialog-header {
|
|
62
|
+
align-items: flex-start;
|
|
63
|
+
display: flex;
|
|
64
|
+
gap: var(--sw-space-2);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.sw-dialog-header > :first-child {
|
|
68
|
+
flex: 1 1 auto;
|
|
69
|
+
min-width: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.sw-button.sw-dialog-close {
|
|
73
|
+
flex: none;
|
|
74
|
+
padding-inline: 0;
|
|
75
|
+
${square('var(--sw-control-sm-min-height)')}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@media ${coarsePointerQuery} {
|
|
79
|
+
.sw-button.sw-dialog-close {
|
|
80
|
+
min-height: ${touchTarget};
|
|
81
|
+
${square(touchTarget)}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Private control chrome: two strokes crossed in a space-4 square, in the
|
|
86
|
+
button's color; borders, so forced colors keep them. */
|
|
87
|
+
.sw-dialog-close-glyph {
|
|
88
|
+
flex: none;
|
|
89
|
+
height: var(--sw-space-4);
|
|
90
|
+
position: relative;
|
|
91
|
+
width: var(--sw-space-4);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.sw-dialog-close-glyph::before,
|
|
95
|
+
.sw-dialog-close-glyph::after {
|
|
96
|
+
border-top: 2px solid currentColor;
|
|
97
|
+
border-radius: 1px;
|
|
98
|
+
box-sizing: border-box;
|
|
99
|
+
content: '';
|
|
100
|
+
left: 0;
|
|
101
|
+
margin-top: -1px;
|
|
102
|
+
position: absolute;
|
|
103
|
+
top: 50%;
|
|
104
|
+
width: 100%;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.sw-dialog-close-glyph::before { transform: rotate(45deg); }
|
|
108
|
+
.sw-dialog-close-glyph::after { transform: rotate(-45deg); }`;
|
|
109
|
+
}
|
|
1
110
|
export function cssDialogClasses() {
|
|
2
111
|
return `.sw-dialog {
|
|
3
112
|
background: var(--sw-glass-fill);
|
|
@@ -9,7 +118,7 @@ export function cssDialogClasses() {
|
|
|
9
118
|
box-sizing: border-box;
|
|
10
119
|
color: var(--sw-color-text);
|
|
11
120
|
margin: auto;
|
|
12
|
-
max-height:
|
|
121
|
+
max-height: ${maxHeight};
|
|
13
122
|
max-width: min(var(--sw-dialog-max), calc(100vw - var(--sw-space-8)));
|
|
14
123
|
overflow: auto;
|
|
15
124
|
width: calc(100% - var(--sw-space-8));
|
|
@@ -19,6 +128,10 @@ export function cssDialogClasses() {
|
|
|
19
128
|
max-width: min(var(--sw-dialog-max-lg), calc(100vw - var(--sw-space-8)));
|
|
20
129
|
}
|
|
21
130
|
|
|
131
|
+
${sheetRules()}
|
|
132
|
+
|
|
133
|
+
${sheetMotionRules()}
|
|
134
|
+
|
|
22
135
|
.sw-dialog::backdrop {
|
|
23
136
|
background: color-mix(in srgb, var(--sw-color-text) 28%, transparent);
|
|
24
137
|
}
|
|
@@ -26,8 +139,17 @@ export function cssDialogClasses() {
|
|
|
26
139
|
.sw-dialog:focus-visible {
|
|
27
140
|
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
28
141
|
outline-offset: var(--sw-focus-ring-offset);
|
|
29
|
-
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
${closeButtonRules()}`;
|
|
30
145
|
}
|
|
31
146
|
export function dialogClassCatalog() {
|
|
32
|
-
return [
|
|
147
|
+
return [
|
|
148
|
+
'sw-dialog',
|
|
149
|
+
'sw-dialog-lg',
|
|
150
|
+
...breakpoints.map((breakpoint) => dialogSheetClass(breakpoint)),
|
|
151
|
+
'sw-dialog-header',
|
|
152
|
+
'sw-dialog-close',
|
|
153
|
+
'sw-dialog-close-glyph',
|
|
154
|
+
];
|
|
33
155
|
}
|
package/dist/css/css-tabs.d.ts
CHANGED
package/dist/css/css-tabs.js
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { zIndex } from './stacking.js';
|
|
2
3
|
const label = typographyVariants.label;
|
|
4
|
+
/*
|
|
5
|
+
* The stuck strip is a full-bleed band of the page canvas, not a glass card:
|
|
6
|
+
* nine parts canvas color to one part see-through, with the glass blur
|
|
7
|
+
* behind it, so a panel scrolling under it shows as a faint smear, never as
|
|
8
|
+
* words beside the labels. At nine parts the muted labels keep at least
|
|
9
|
+
* 4.1:1, and the accent label 3.6:1, in every palette even over a solid
|
|
10
|
+
* block of the text, accent or danger color; over the canvas and ordinary
|
|
11
|
+
* type, which the blur spreads, they keep their canvas contrast. The first
|
|
12
|
+
* tab's own inline padding (the md control's, spacing step 4) lines its
|
|
13
|
+
* label up with a space-4 page gutter, so the strip adds no padding of its
|
|
14
|
+
* own. It rides the AppHeader's layer.
|
|
15
|
+
*/
|
|
16
|
+
export const stuckCanvasShare = 0.9;
|
|
17
|
+
function stickyRules() {
|
|
18
|
+
return `.sw-tabs-sticky {
|
|
19
|
+
background: color-mix(in srgb, var(--sw-color-background) ${stuckCanvasShare * 100}%, transparent);
|
|
20
|
+
backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
|
|
21
|
+
-webkit-backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
|
|
22
|
+
position: sticky;
|
|
23
|
+
top: env(safe-area-inset-top, 0px);
|
|
24
|
+
${zIndex('topChrome')}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
28
|
+
.sw-tabs-sticky {
|
|
29
|
+
background: var(--sw-color-background);
|
|
30
|
+
backdrop-filter: none;
|
|
31
|
+
-webkit-backdrop-filter: none;
|
|
32
|
+
}
|
|
33
|
+
}`;
|
|
34
|
+
}
|
|
3
35
|
/** The tab strip: a scrollable row of tabs with an accent underline under the current one. */
|
|
4
36
|
export function cssTabsClasses() {
|
|
5
37
|
return `.sw-tabs {
|
|
@@ -55,10 +87,19 @@ export function cssTabsClasses() {
|
|
|
55
87
|
outline-offset: var(--sw-focus-ring-offset);
|
|
56
88
|
}
|
|
57
89
|
|
|
90
|
+
${stickyRules()}
|
|
91
|
+
|
|
58
92
|
@media (forced-colors: active) {
|
|
59
93
|
.sw-tab-selected { border-bottom-color: Highlight; color: Highlight; }
|
|
94
|
+
.sw-tabs-sticky { background: Canvas; backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
60
95
|
}`;
|
|
61
96
|
}
|
|
62
97
|
export function tabsClassCatalog() {
|
|
63
|
-
return [
|
|
98
|
+
return [
|
|
99
|
+
'sw-tabs',
|
|
100
|
+
'sw-tabs-sticky',
|
|
101
|
+
'sw-tab',
|
|
102
|
+
'sw-tab-selected',
|
|
103
|
+
'sw-tab-panel',
|
|
104
|
+
];
|
|
64
105
|
}
|
package/dist/css/stacking.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* its popup can only paint as high as the surface does; lifting the surface
|
|
11
11
|
* over the ActionBar keeps an open list tappable where the bar would cover
|
|
12
12
|
* it, and over the next surface in the flow.
|
|
13
|
-
* 4. `topChrome`: the sticky AppHeader, over a lifted
|
|
14
|
-
* under it (the header is the page's way back,
|
|
13
|
+
* 4. `topChrome`: the sticky AppHeader or sticky Tabs strip, over a lifted
|
|
14
|
+
* surface that scrolls up under it (the header is the page's way back, the
|
|
15
|
+
* strip its way between sections, and a popup opens downward).
|
|
15
16
|
* 5. `popup`: a popup outside any lifted surface (Select list, ActionMenu
|
|
16
17
|
* list, Tooltip, the DateField and CalendarButton calendar) over
|
|
17
18
|
* everything in the page.
|
package/dist/styles.css
CHANGED
|
@@ -1890,6 +1890,32 @@ body {
|
|
|
1890
1890
|
max-width: min(var(--sw-dialog-max-lg), calc(100vw - var(--sw-space-8)));
|
|
1891
1891
|
}
|
|
1892
1892
|
|
|
1893
|
+
@media not all and (min-width: 48rem) {
|
|
1894
|
+
.sw-dialog-sheet-below-md {
|
|
1895
|
+
border-radius: var(--sw-radius-lg) var(--sw-radius-lg) 0 0;
|
|
1896
|
+
border-width: 1px 0 0;
|
|
1897
|
+
margin: auto 0 0;
|
|
1898
|
+
max-height: min(100vh - var(--sw-space-8), 100dvh - var(--sw-space-8));
|
|
1899
|
+
max-width: none;
|
|
1900
|
+
padding-bottom: calc(var(--sw-space-5) + env(safe-area-inset-bottom, 0px));
|
|
1901
|
+
padding-left: max(var(--sw-space-5), env(safe-area-inset-left, 0px));
|
|
1902
|
+
padding-right: max(var(--sw-space-5), env(safe-area-inset-right, 0px));
|
|
1903
|
+
width: 100%;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
.sw-dialog-sheet-below-md[open] {
|
|
1907
|
+
animation: sw-dialog-sheet-in var(--sw-motion-default) var(--sw-motion-easing);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
@keyframes sw-dialog-sheet-in {
|
|
1912
|
+
from { translate: 0 100%; }
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1916
|
+
.sw-dialog-sheet-below-md[open] { animation: none; }
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1893
1919
|
.sw-dialog::backdrop {
|
|
1894
1920
|
background: color-mix(in srgb, var(--sw-color-text) 28%, transparent);
|
|
1895
1921
|
}
|
|
@@ -1899,6 +1925,59 @@ body {
|
|
|
1899
1925
|
outline-offset: var(--sw-focus-ring-offset);
|
|
1900
1926
|
}
|
|
1901
1927
|
|
|
1928
|
+
.sw-dialog-header {
|
|
1929
|
+
align-items: flex-start;
|
|
1930
|
+
display: flex;
|
|
1931
|
+
gap: var(--sw-space-2);
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
.sw-dialog-header > :first-child {
|
|
1935
|
+
flex: 1 1 auto;
|
|
1936
|
+
min-width: 0;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
.sw-button.sw-dialog-close {
|
|
1940
|
+
flex: none;
|
|
1941
|
+
padding-inline: 0;
|
|
1942
|
+
min-width: var(--sw-control-sm-min-height);
|
|
1943
|
+
margin-block: calc((28px - var(--sw-control-sm-min-height)) / 2);
|
|
1944
|
+
margin-inline-end: calc((var(--sw-space-4) - var(--sw-control-sm-min-height)) / 2);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
@media (pointer: coarse) {
|
|
1948
|
+
.sw-button.sw-dialog-close {
|
|
1949
|
+
min-height: var(--sw-control-md-min-height);
|
|
1950
|
+
min-width: var(--sw-control-md-min-height);
|
|
1951
|
+
margin-block: calc((28px - var(--sw-control-md-min-height)) / 2);
|
|
1952
|
+
margin-inline-end: calc((var(--sw-space-4) - var(--sw-control-md-min-height)) / 2);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/* Private control chrome: two strokes crossed in a space-4 square, in the
|
|
1957
|
+
button's color; borders, so forced colors keep them. */
|
|
1958
|
+
.sw-dialog-close-glyph {
|
|
1959
|
+
flex: none;
|
|
1960
|
+
height: var(--sw-space-4);
|
|
1961
|
+
position: relative;
|
|
1962
|
+
width: var(--sw-space-4);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
.sw-dialog-close-glyph::before,
|
|
1966
|
+
.sw-dialog-close-glyph::after {
|
|
1967
|
+
border-top: 2px solid currentColor;
|
|
1968
|
+
border-radius: 1px;
|
|
1969
|
+
box-sizing: border-box;
|
|
1970
|
+
content: '';
|
|
1971
|
+
left: 0;
|
|
1972
|
+
margin-top: -1px;
|
|
1973
|
+
position: absolute;
|
|
1974
|
+
top: 50%;
|
|
1975
|
+
width: 100%;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
.sw-dialog-close-glyph::before { transform: rotate(45deg); }
|
|
1979
|
+
.sw-dialog-close-glyph::after { transform: rotate(-45deg); }
|
|
1980
|
+
|
|
1902
1981
|
.sw-accordion {
|
|
1903
1982
|
background: var(--sw-glass-fill);
|
|
1904
1983
|
border: 1px solid var(--sw-glass-border);
|
|
@@ -2272,14 +2351,21 @@ body {
|
|
|
2272
2351
|
padding-right: max(var(--sw-space-4), env(safe-area-inset-right, 0px));
|
|
2273
2352
|
}
|
|
2274
2353
|
|
|
2354
|
+
/* A Badge and a short line sit on one row a small gap apart; a line that
|
|
2355
|
+
does not fit beside the badge wraps under it. */
|
|
2275
2356
|
.sw-action-bar-status {
|
|
2357
|
+
align-items: center;
|
|
2276
2358
|
color: var(--sw-color-muted);
|
|
2359
|
+
column-gap: var(--sw-space-2);
|
|
2360
|
+
display: flex;
|
|
2277
2361
|
flex: 1 1 auto;
|
|
2362
|
+
flex-wrap: wrap;
|
|
2278
2363
|
font-family: var(--sw-font-sans);
|
|
2279
2364
|
font-size: 13px;
|
|
2280
2365
|
line-height: 18px;
|
|
2281
2366
|
margin: 0;
|
|
2282
2367
|
min-width: 0;
|
|
2368
|
+
row-gap: var(--sw-space-1);
|
|
2283
2369
|
}
|
|
2284
2370
|
|
|
2285
2371
|
/* With no status the live region stays in the page, so a first status is
|
|
@@ -2674,8 +2760,26 @@ body {
|
|
|
2674
2760
|
outline-offset: var(--sw-focus-ring-offset);
|
|
2675
2761
|
}
|
|
2676
2762
|
|
|
2763
|
+
.sw-tabs-sticky {
|
|
2764
|
+
background: color-mix(in srgb, var(--sw-color-background) 90%, transparent);
|
|
2765
|
+
backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
|
|
2766
|
+
-webkit-backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
|
|
2767
|
+
position: sticky;
|
|
2768
|
+
top: env(safe-area-inset-top, 0px);
|
|
2769
|
+
z-index: 4;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
2773
|
+
.sw-tabs-sticky {
|
|
2774
|
+
background: var(--sw-color-background);
|
|
2775
|
+
backdrop-filter: none;
|
|
2776
|
+
-webkit-backdrop-filter: none;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2677
2780
|
@media (forced-colors: active) {
|
|
2678
2781
|
.sw-tab-selected { border-bottom-color: Highlight; color: Highlight; }
|
|
2782
|
+
.sw-tabs-sticky { background: Canvas; backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
2679
2783
|
}
|
|
2680
2784
|
|
|
2681
2785
|
.sw-filter-chips {
|