@scalewing/react 1.27.0 → 1.28.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 +41 -0
- package/README.md +60 -2
- package/dist/components/DenominationGrid.d.ts +4 -1
- package/dist/components/DenominationGrid.js +8 -3
- package/dist/components/Dialog.d.ts +28 -4
- package/dist/components/Dialog.js +20 -2
- package/dist/components/Notice.d.ts +69 -0
- package/dist/components/Notice.js +48 -0
- package/dist/components/Progress.d.ts +3 -1
- package/dist/components/Progress.js +4 -0
- package/dist/components/Toast.d.ts +20 -4
- package/dist/components/Toast.js +44 -21
- package/dist/components/denomination-grid/DenominationStrip.d.ts +5 -2
- package/dist/components/denomination-grid/DenominationStrip.js +2 -2
- package/dist/components/denomination-grid/DenominationTiles.d.ts +5 -2
- package/dist/components/denomination-grid/DenominationTiles.js +2 -2
- package/dist/components/denomination-grid/types.d.ts +9 -0
- package/dist/components/dialog/DialogFooter.d.ts +14 -0
- package/dist/components/dialog/DialogFooter.js +11 -0
- package/dist/components/dialog/DialogTitle.d.ts +11 -4
- package/dist/components/dialog/DialogTitle.js +8 -6
- package/dist/components/toast/ToastContent.d.ts +22 -0
- package/dist/components/toast/ToastContent.js +21 -0
- package/dist/components/toast/use-toast-hold.d.ts +28 -0
- package/dist/components/toast/use-toast-hold.js +53 -0
- package/dist/components/toast/use-toast-timer.d.ts +15 -0
- package/dist/components/toast/use-toast-timer.js +34 -0
- package/dist/css/css-components.js +9 -0
- package/dist/css/css-denomination-grid.js +4 -0
- package/dist/css/css-denomination-label.d.ts +10 -0
- package/dist/css/css-denomination-label.js +36 -0
- package/dist/css/css-dialog-frame.d.ts +2 -0
- package/dist/css/css-dialog-frame.js +83 -0
- package/dist/css/css-dialog.js +20 -4
- package/dist/css/css-notice.d.ts +22 -0
- package/dist/css/css-notice.js +168 -0
- package/dist/css/css-progress.d.ts +3 -0
- package/dist/css/css-progress.js +13 -6
- package/dist/css/css-toast-card.d.ts +5 -0
- package/dist/css/css-toast-card.js +122 -0
- package/dist/css/css-variables.js +1 -0
- package/dist/css/stylesheet.js +6 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +5 -4
- package/dist/styles.css +299 -1
- package/package.json +1 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { type DenominationGridProps } from './types.js';
|
|
2
|
-
type TilesProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel' | 'rowRole'>> & Pick<DenominationGridProps, 'subtotal'
|
|
3
|
-
|
|
2
|
+
type TilesProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel' | 'rowRole'>> & Pick<DenominationGridProps, 'subtotal'> & {
|
|
3
|
+
/** The row labels' appearance class, if any. */
|
|
4
|
+
className?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function DenominationTiles({ className, label, columns, rows, rowRole, subtotal, zeroLabel, }: TilesProps): import("react").JSX.Element;
|
|
4
7
|
export {};
|
|
@@ -21,8 +21,8 @@ function showsRowLabel(rowCount, row) {
|
|
|
21
21
|
function namesRow(rowCount, row, gridLabel) {
|
|
22
22
|
return showsRowLabel(rowCount, row) || row.label !== gridLabel;
|
|
23
23
|
}
|
|
24
|
-
export function DenominationTiles({ label, columns, rows, rowRole, subtotal, zeroLabel, }) {
|
|
25
|
-
return (_jsx("div", { "aria-label": label, className:
|
|
24
|
+
export function DenominationTiles({ className, label, columns, rows, rowRole, subtotal, zeroLabel, }) {
|
|
25
|
+
return (_jsx("div", { "aria-label": label, className: cx('sw-denomination-grid sw-denomination-tiles', className), role: "group", children: rows.map((row) => {
|
|
26
26
|
const labelled = showsRowLabel(rows.length, row);
|
|
27
27
|
const named = namesRow(rows.length, row, label);
|
|
28
28
|
// A named section is a region; role="group" keeps the name without
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import { type BadgeTone } from '../../css/css-data.js';
|
|
3
|
+
import { type DenominationLabelAppearance } from '../../css/css-denomination-label.js';
|
|
3
4
|
import { type DenominationLabelWidth } from '../../css/css-denomination-aligned.js';
|
|
4
5
|
export type { DenominationLabelWidth };
|
|
5
6
|
export type DenominationGridTone = BadgeTone;
|
|
@@ -102,4 +103,12 @@ export type DenominationGridProps = {
|
|
|
102
103
|
* strip has a label column.
|
|
103
104
|
*/
|
|
104
105
|
labelWidth?: DenominationLabelWidth;
|
|
106
|
+
/**
|
|
107
|
+
* `tone` (the default) marks each row label with its row's tone: a
|
|
108
|
+
* stripe at its start and the words in the tone. `plain` sets every row
|
|
109
|
+
* label in the text color at label weight with no stripe, for a grid
|
|
110
|
+
* whose rows need no color to tell apart; a row's tone still colors its
|
|
111
|
+
* total.
|
|
112
|
+
*/
|
|
113
|
+
labelAppearance?: DenominationLabelAppearance;
|
|
105
114
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type ActionBarSummary } from '../action-bar/ActionBarSummary.js';
|
|
3
|
+
type DialogFooterProps = {
|
|
4
|
+
summary?: ActionBarSummary;
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The dialog's pinned footer: the summary figure (in a polite live region,
|
|
9
|
+
* as in ActionBar) at the start, the actions at the end. Rendered only when
|
|
10
|
+
* the dialog has a footer or a summary, and then always with its status
|
|
11
|
+
* region, so a new figure is announced.
|
|
12
|
+
*/
|
|
13
|
+
export declare function DialogFooter({ children, summary }: DialogFooterProps): import("react").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ActionBarSummaryLine, } from '../action-bar/ActionBarSummary.js';
|
|
3
|
+
/**
|
|
4
|
+
* The dialog's pinned footer: the summary figure (in a polite live region,
|
|
5
|
+
* as in ActionBar) at the start, the actions at the end. Rendered only when
|
|
6
|
+
* the dialog has a footer or a summary, and then always with its status
|
|
7
|
+
* region, so a new figure is announced.
|
|
8
|
+
*/
|
|
9
|
+
export function DialogFooter({ children, summary }) {
|
|
10
|
+
return (_jsxs("div", { className: "sw-dialog-footer", children: [_jsx("div", { className: "sw-dialog-footer-status", role: "status", children: summary ? _jsx(ActionBarSummaryLine, { summary: summary }) : null }), children ? (_jsx("div", { className: "sw-dialog-footer-actions", children: children })) : null] }));
|
|
11
|
+
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
1
2
|
type DialogTitleProps = {
|
|
2
3
|
id: string;
|
|
3
4
|
level: 2 | 3;
|
|
4
5
|
title: string;
|
|
6
|
+
/** A muted line under the title, which describes the dialog. */
|
|
7
|
+
description?: string;
|
|
8
|
+
descriptionId?: string;
|
|
9
|
+
/** A consumer glyph or tile before the title, hidden from assistive technology. */
|
|
10
|
+
icon?: ReactNode;
|
|
5
11
|
/** The close button's accessible name; without it there is no button. */
|
|
6
12
|
closeLabel?: string;
|
|
7
13
|
onClose: () => void;
|
|
8
14
|
};
|
|
9
15
|
/**
|
|
10
|
-
* The dialog's title, which names it through `aria-labelledby`.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
16
|
+
* The dialog's title, which names it through `aria-labelledby`. Alone it is
|
|
17
|
+
* the heading, as it always was. With an icon, a description or a
|
|
18
|
+
* `closeLabel` it is a row: the icon, the title over its description, and
|
|
19
|
+
* an icon-only ghost close button at the end.
|
|
13
20
|
*/
|
|
14
|
-
export declare function DialogTitle({ closeLabel, id, level, onClose, title, }: DialogTitleProps): import("react").JSX.Element;
|
|
21
|
+
export declare function DialogTitle({ closeLabel, description, descriptionId, icon, id, level, onClose, title, }: DialogTitleProps): import("react").JSX.Element;
|
|
15
22
|
export {};
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../../class-names.js';
|
|
2
3
|
import { Button } from '../Button.js';
|
|
3
4
|
import { Text } from '../Text.js';
|
|
4
5
|
/**
|
|
5
|
-
* The dialog's title, which names it through `aria-labelledby`.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* The dialog's title, which names it through `aria-labelledby`. Alone it is
|
|
7
|
+
* the heading, as it always was. With an icon, a description or a
|
|
8
|
+
* `closeLabel` it is a row: the icon, the title over its description, and
|
|
9
|
+
* an icon-only ghost close button at the end.
|
|
8
10
|
*/
|
|
9
|
-
export function DialogTitle({ closeLabel, id, level, onClose, title, }) {
|
|
11
|
+
export function DialogTitle({ closeLabel, description, descriptionId, icon, id, level, onClose, title, }) {
|
|
10
12
|
const heading = (_jsx(Text, { as: level === 2 ? 'h2' : 'h3', id: id, variant: "title", children: title }));
|
|
11
|
-
if (!closeLabel)
|
|
13
|
+
if (!closeLabel && !icon && !description)
|
|
12
14
|
return heading;
|
|
13
|
-
return (_jsxs("div", { className:
|
|
15
|
+
return (_jsxs("div", { className: cx('sw-dialog-header', Boolean(icon || description) && 'sw-dialog-header-framed'), children: [icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-dialog-icon", children: icon })) : null, description ? (_jsxs("div", { className: "sw-dialog-titles", children: [heading, _jsx(Text, { as: "p", className: "sw-dialog-description", color: "muted", id: descriptionId, variant: "caption", children: description })] })) : (heading), closeLabel ? (_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" })] })) : null] }));
|
|
14
16
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/** One press in the toast, such as "Open" for what was just saved. */
|
|
3
|
+
export type ToastAction = {
|
|
4
|
+
label: string;
|
|
5
|
+
onPress: () => void;
|
|
6
|
+
};
|
|
7
|
+
type ToastContentProps = {
|
|
8
|
+
title?: string;
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
action?: ToastAction;
|
|
11
|
+
/** Runs the action, then closes the toast. */
|
|
12
|
+
onAction: () => void;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* What the toast says. Children alone render as they always have; with a
|
|
17
|
+
* title, an icon or an action they go in a row: the icon, the bold title
|
|
18
|
+
* over the children as a muted detail, and the action at the end.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ToastContent({ action, children, icon, onAction, title, }: ToastContentProps): import("react").JSX.Element;
|
|
21
|
+
export declare function assertToastContent(title: string | undefined, action: ToastAction | undefined): void;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from '../Button.js';
|
|
3
|
+
/**
|
|
4
|
+
* What the toast says. Children alone render as they always have; with a
|
|
5
|
+
* title, an icon or an action they go in a row: the icon, the bold title
|
|
6
|
+
* over the children as a muted detail, and the action at the end.
|
|
7
|
+
*/
|
|
8
|
+
export function ToastContent({ action, children, icon, onAction, title, }) {
|
|
9
|
+
if (!icon && !title && !action)
|
|
10
|
+
return _jsx(_Fragment, { children: children });
|
|
11
|
+
return (_jsxs("div", { className: "sw-toast-row", children: [icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-toast-icon", children: icon })) : null, _jsxs("div", { className: "sw-toast-body", children: [title ? _jsx("strong", { className: "sw-toast-title", children: title }) : null, title ? _jsx("div", { className: "sw-toast-detail", children: children }) : children] }), action ? (_jsx(Button, { className: "sw-toast-action", onPress: onAction, size: "sm", variant: "ghost", children: action.label })) : null] }));
|
|
12
|
+
}
|
|
13
|
+
export function assertToastContent(title, action) {
|
|
14
|
+
if (title !== undefined && (typeof title !== 'string' || !title.trim()))
|
|
15
|
+
throw new RangeError('Toast title must not be empty');
|
|
16
|
+
if (action !== undefined &&
|
|
17
|
+
(typeof action.label !== 'string' ||
|
|
18
|
+
!action.label.trim() ||
|
|
19
|
+
typeof action.onPress !== 'function'))
|
|
20
|
+
throw new RangeError('Toast action needs a label and an onPress');
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type FocusEvent, type PointerEvent, type FocusEventHandler, type PointerEventHandler } from 'react';
|
|
2
|
+
type ToastHoldOptions = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
/** Only a toast that takes the pointer (an action or the line) holds. */
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
onBlur?: FocusEventHandler<HTMLDivElement>;
|
|
7
|
+
onFocus?: FocusEventHandler<HTMLDivElement>;
|
|
8
|
+
onPointerEnter?: PointerEventHandler<HTMLDivElement>;
|
|
9
|
+
onPointerLeave?: PointerEventHandler<HTMLDivElement>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Whether the toast's time is held: the pointer is over it or focus is
|
|
13
|
+
* inside it. Each time it opens or closes it starts unheld (a toast that
|
|
14
|
+
* closes under the pointer gets no pointerleave). It also remembers where
|
|
15
|
+
* focus came from, so `returnFocus` can give it back when the toast closes
|
|
16
|
+
* with focus inside (its own action, or the consumer closing it).
|
|
17
|
+
*/
|
|
18
|
+
export declare function useToastHold({ enabled, onBlur, onFocus, onPointerEnter, onPointerLeave, open, }: ToastHoldOptions): {
|
|
19
|
+
paused: boolean;
|
|
20
|
+
returnFocus: (node: HTMLElement) => void;
|
|
21
|
+
handlers: {
|
|
22
|
+
onBlur: (event: FocusEvent<HTMLDivElement>) => void;
|
|
23
|
+
onFocus: (event: FocusEvent<HTMLDivElement>) => void;
|
|
24
|
+
onPointerEnter: (event: PointerEvent<HTMLDivElement>) => void;
|
|
25
|
+
onPointerLeave: (event: PointerEvent<HTMLDivElement>) => void;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useCallback, useRef, useState, } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Whether the toast's time is held: the pointer is over it or focus is
|
|
4
|
+
* inside it. Each time it opens or closes it starts unheld (a toast that
|
|
5
|
+
* closes under the pointer gets no pointerleave). It also remembers where
|
|
6
|
+
* focus came from, so `returnFocus` can give it back when the toast closes
|
|
7
|
+
* with focus inside (its own action, or the consumer closing it).
|
|
8
|
+
*/
|
|
9
|
+
export function useToastHold({ enabled, onBlur, onFocus, onPointerEnter, onPointerLeave, open, }) {
|
|
10
|
+
const [hovered, setHovered] = useState(false);
|
|
11
|
+
const [focused, setFocused] = useState(false);
|
|
12
|
+
const [wasOpen, setWasOpen] = useState(open);
|
|
13
|
+
const cameFromRef = useRef(null);
|
|
14
|
+
if (wasOpen !== open) {
|
|
15
|
+
setWasOpen(open);
|
|
16
|
+
setHovered(false);
|
|
17
|
+
setFocused(false);
|
|
18
|
+
}
|
|
19
|
+
const returnFocus = useCallback((node) => {
|
|
20
|
+
const cameFrom = cameFromRef.current;
|
|
21
|
+
cameFromRef.current = null;
|
|
22
|
+
if (!node.contains(node.ownerDocument.activeElement))
|
|
23
|
+
return;
|
|
24
|
+
if (cameFrom?.isConnected)
|
|
25
|
+
cameFrom.focus();
|
|
26
|
+
}, []);
|
|
27
|
+
return {
|
|
28
|
+
paused: enabled && (hovered || focused),
|
|
29
|
+
returnFocus,
|
|
30
|
+
handlers: {
|
|
31
|
+
onBlur: (event) => {
|
|
32
|
+
onBlur?.(event);
|
|
33
|
+
if (!event.currentTarget.contains(event.relatedTarget))
|
|
34
|
+
setFocused(false);
|
|
35
|
+
},
|
|
36
|
+
onFocus: (event) => {
|
|
37
|
+
onFocus?.(event);
|
|
38
|
+
const from = event.relatedTarget;
|
|
39
|
+
if (from instanceof HTMLElement && !event.currentTarget.contains(from))
|
|
40
|
+
cameFromRef.current = from;
|
|
41
|
+
setFocused(true);
|
|
42
|
+
},
|
|
43
|
+
onPointerEnter: (event) => {
|
|
44
|
+
onPointerEnter?.(event);
|
|
45
|
+
setHovered(true);
|
|
46
|
+
},
|
|
47
|
+
onPointerLeave: (event) => {
|
|
48
|
+
onPointerLeave?.(event);
|
|
49
|
+
setHovered(false);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type ToastTimerOptions = {
|
|
2
|
+
/** The timer runs only while the toast is open and does not travel. */
|
|
3
|
+
running: boolean;
|
|
4
|
+
/** While true, the time left is held, as on hover or focus. */
|
|
5
|
+
paused: boolean;
|
|
6
|
+
/** The whole time the toast stays, in ms. */
|
|
7
|
+
duration: number;
|
|
8
|
+
onExpire: () => void;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Closes the toast once `duration` ms have run, counting only the time it is
|
|
12
|
+
* not paused. Opening it again (or a new duration) starts the count over.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useToastTimer({ duration, onExpire, paused, running, }: ToastTimerOptions): void;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Closes the toast once `duration` ms have run, counting only the time it is
|
|
4
|
+
* not paused. Opening it again (or a new duration) starts the count over.
|
|
5
|
+
*/
|
|
6
|
+
export function useToastTimer({ duration, onExpire, paused, running, }) {
|
|
7
|
+
const leftRef = useRef(duration);
|
|
8
|
+
const expireRef = useRef(onExpire);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
expireRef.current = onExpire;
|
|
11
|
+
}, [onExpire]);
|
|
12
|
+
// A fresh count each time the toast opens or its duration changes.
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
leftRef.current = duration;
|
|
15
|
+
}, [duration, running]);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!running || paused)
|
|
18
|
+
return;
|
|
19
|
+
const started = Date.now();
|
|
20
|
+
let fired = false;
|
|
21
|
+
const timer = window.setTimeout(() => {
|
|
22
|
+
fired = true;
|
|
23
|
+
leftRef.current = duration;
|
|
24
|
+
expireRef.current();
|
|
25
|
+
}, leftRef.current);
|
|
26
|
+
return () => {
|
|
27
|
+
window.clearTimeout(timer);
|
|
28
|
+
// After it fired the count is whole again (a consumer that keeps the
|
|
29
|
+
// toast open gets the full time on its next run, not none).
|
|
30
|
+
if (!fired)
|
|
31
|
+
leftRef.current = Math.max(0, leftRef.current - (Date.now() - started));
|
|
32
|
+
};
|
|
33
|
+
}, [duration, paused, running]);
|
|
34
|
+
}
|
|
@@ -13,12 +13,14 @@ import { cssSegmentedClasses } from './css-segmented.js';
|
|
|
13
13
|
import { cssTableClasses } from './css-table.js';
|
|
14
14
|
import { cssDateFieldClasses } from './css-date-field.js';
|
|
15
15
|
import { cssDialogClasses } from './css-dialog.js';
|
|
16
|
+
import { cssDialogFrameClasses } from './css-dialog-frame.js';
|
|
16
17
|
import { cssSelectClasses } from './css-select.js';
|
|
17
18
|
import { cssSplitClasses } from './css-split.js';
|
|
18
19
|
import { cssSwitchClasses } from './css-switch.js';
|
|
19
20
|
import { cssTextClasses } from './css-text.js';
|
|
20
21
|
import { cssSpinnerClasses } from './css-spinner.js';
|
|
21
22
|
import { cssProgressClasses } from './css-progress.js';
|
|
23
|
+
import { cssNoticeClasses } from './css-notice.js';
|
|
22
24
|
import { cssTooltipClasses } from './css-tooltip.js';
|
|
23
25
|
import { cssInfoTipClasses } from './css-info-tip.js';
|
|
24
26
|
import { cssSeparatorClasses } from './css-separator.js';
|
|
@@ -34,6 +36,7 @@ import { cssFieldClasses } from './css-field.js';
|
|
|
34
36
|
import { cssStepperClasses } from './css-stepper.js';
|
|
35
37
|
import { cssStepsClasses } from './css-steps.js';
|
|
36
38
|
import { cssToastClasses } from './css-toast.js';
|
|
39
|
+
import { cssToastCardClasses } from './css-toast-card.js';
|
|
37
40
|
import { cssBoxBorderClasses } from './css-box-border.js';
|
|
38
41
|
import { cssPopupHostRules } from './stacking.js';
|
|
39
42
|
/*
|
|
@@ -133,6 +136,8 @@ ${cssSectionNavClasses()}
|
|
|
133
136
|
|
|
134
137
|
${cssDialogClasses()}
|
|
135
138
|
|
|
139
|
+
${cssDialogFrameClasses()}
|
|
140
|
+
|
|
136
141
|
${cssAccordionClasses()}
|
|
137
142
|
|
|
138
143
|
${cssSelectClasses()}
|
|
@@ -147,6 +152,8 @@ ${cssSpinnerClasses()}
|
|
|
147
152
|
|
|
148
153
|
${cssProgressClasses()}
|
|
149
154
|
|
|
155
|
+
${cssNoticeClasses()}
|
|
156
|
+
|
|
150
157
|
${cssTooltipClasses()}
|
|
151
158
|
|
|
152
159
|
${cssInfoTipClasses()}
|
|
@@ -175,6 +182,8 @@ ${cssSplitClasses()}
|
|
|
175
182
|
|
|
176
183
|
${cssToastClasses()}
|
|
177
184
|
|
|
185
|
+
${cssToastCardClasses()}
|
|
186
|
+
|
|
178
187
|
${cssResponsiveClasses()}
|
|
179
188
|
|
|
180
189
|
${cssBoxBorderClasses()}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
2
|
import { breakpointQuery } from './breakpoints.js';
|
|
3
3
|
import { badgeTones } from './css-data.js';
|
|
4
|
+
import { cssDenominationLabelClasses, denominationLabelClassCatalog, } from './css-denomination-label.js';
|
|
4
5
|
import { cssDenominationAlignedClasses, denominationAlignedClassCatalog, } from './css-denomination-aligned.js';
|
|
5
6
|
import { cssDenominationEntryClasses, denominationEntryClassCatalog, } from './css-denomination-entry.js';
|
|
6
7
|
import { cssDenominationCellToneClasses, denominationCellToneClassCatalog, } from './css-denomination-cell-tones.js';
|
|
@@ -254,6 +255,8 @@ ${cssDenominationEntryClasses()}
|
|
|
254
255
|
|
|
255
256
|
${cssDenominationAlignedClasses()}
|
|
256
257
|
|
|
258
|
+
${cssDenominationLabelClasses()}
|
|
259
|
+
|
|
257
260
|
@media (forced-colors: active) {
|
|
258
261
|
.sw-denomination-label { border-inline-start-color: CanvasText; color: CanvasText; }
|
|
259
262
|
.sw-denomination-strip .sw-denomination-label::before { background: CanvasText; forced-color-adjust: none; }
|
|
@@ -295,5 +298,6 @@ export function denominationGridClassCatalog() {
|
|
|
295
298
|
...denominationCellToneClassCatalog(),
|
|
296
299
|
...denominationEntryClassCatalog(),
|
|
297
300
|
...denominationAlignedClassCatalog(),
|
|
301
|
+
...denominationLabelClassCatalog(),
|
|
298
302
|
];
|
|
299
303
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const denominationLabelAppearances: readonly ["tone", "plain"];
|
|
2
|
+
/**
|
|
3
|
+
* How a grid's row labels look: `tone` (the default) marks each label with
|
|
4
|
+
* its row's tone, a stripe at its start and the words in the tone; `plain`
|
|
5
|
+
* sets the words in the text color at label weight, with no stripe.
|
|
6
|
+
*/
|
|
7
|
+
export type DenominationLabelAppearance = (typeof denominationLabelAppearances)[number];
|
|
8
|
+
export declare const denominationLabelsPlainClass = "sw-denomination-labels-plain";
|
|
9
|
+
export declare function cssDenominationLabelClasses(): string;
|
|
10
|
+
export declare function denominationLabelClassCatalog(): string[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { breakpointQuery } from './breakpoints.js';
|
|
2
|
+
export const denominationLabelAppearances = ['tone', 'plain'];
|
|
3
|
+
export const denominationLabelsPlainClass = 'sw-denomination-labels-plain';
|
|
4
|
+
const plain = `.${denominationLabelsPlainClass}`;
|
|
5
|
+
/*
|
|
6
|
+
* Plain row labels: the text color, no stripe, and the strip's label cell
|
|
7
|
+
* back to its column's own padding. The row's tone still colors its total.
|
|
8
|
+
* Emitted after the grid's rules (and its narrow-screen padding), and one
|
|
9
|
+
* class more specific, so it wins at every width.
|
|
10
|
+
*/
|
|
11
|
+
export function cssDenominationLabelClasses() {
|
|
12
|
+
return `${plain} .sw-denomination-label {
|
|
13
|
+
border-inline-start: 0;
|
|
14
|
+
color: var(--sw-color-text);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* The total under a phone's label keeps its row's tone. */
|
|
18
|
+
${plain} .sw-denomination-total-inline { color: var(--sw-denomination-tone); }
|
|
19
|
+
|
|
20
|
+
.sw-denomination-strip${plain} .sw-denomination-label {
|
|
21
|
+
padding-inline-start: var(--sw-space-2);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.sw-denomination-strip${plain} .sw-denomination-label::before { content: none; }
|
|
25
|
+
|
|
26
|
+
@media ${breakpointQuery('below', 'md')} {
|
|
27
|
+
.sw-denomination-strip${plain} .sw-denomination-label { padding-inline-start: var(--sw-space-1); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@media (forced-colors: active) {
|
|
31
|
+
${plain} .sw-denomination-label { color: CanvasText; }
|
|
32
|
+
}`;
|
|
33
|
+
}
|
|
34
|
+
export function denominationLabelClassCatalog() {
|
|
35
|
+
return [denominationLabelsPlainClass];
|
|
36
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The dialog's frame beyond its title: a consumer icon before the title, a
|
|
3
|
+
* muted description under it, and a footer pinned to the bottom edge.
|
|
4
|
+
*
|
|
5
|
+
* With an icon or a description the header row centers its parts (the
|
|
6
|
+
* icon, the title over the description, the close button). The footer is
|
|
7
|
+
* sticky to the bottom of the dialog's own scrollport, bleeds to its
|
|
8
|
+
* edges under a hairline, and sits on the solid surface so content
|
|
9
|
+
* scrolling under it never shows through: the summary figure first, the
|
|
10
|
+
* actions at the end, wrapping under one another on a narrow sheet. In a
|
|
11
|
+
* bottom sheet its surface reaches the sheet's bottom edge and its own
|
|
12
|
+
* padding clears the home indicator (see the sheet rules in css-dialog.ts).
|
|
13
|
+
*/
|
|
14
|
+
export function cssDialogFrameClasses() {
|
|
15
|
+
return `.sw-dialog-header-framed {
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: var(--sw-space-3);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.sw-dialog-icon {
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
flex: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.sw-dialog-description { margin: 0; }
|
|
26
|
+
|
|
27
|
+
.sw-dialog-footer {
|
|
28
|
+
align-items: center;
|
|
29
|
+
background: var(--sw-color-surface);
|
|
30
|
+
border-top: 1px solid var(--sw-color-border);
|
|
31
|
+
/* Sticky insets count from the scrollport's padding edge: step past the
|
|
32
|
+
dialog's own padding to sit on its bottom edge. */
|
|
33
|
+
bottom: calc(-1 * var(--sw-space-5));
|
|
34
|
+
box-sizing: border-box;
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-wrap: wrap;
|
|
37
|
+
gap: var(--sw-space-2) var(--sw-space-4);
|
|
38
|
+
margin: 0 calc(-1 * var(--sw-space-5)) calc(-1 * var(--sw-space-5));
|
|
39
|
+
padding: var(--sw-space-3) var(--sw-space-5);
|
|
40
|
+
position: sticky;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.sw-dialog-footer-status {
|
|
44
|
+
align-items: center;
|
|
45
|
+
display: flex;
|
|
46
|
+
flex: 1 1 auto;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* With no summary the live region stays in the dialog, so a first figure
|
|
51
|
+
is still announced, but it takes no room in the footer. */
|
|
52
|
+
.sw-dialog-footer-status:empty {
|
|
53
|
+
clip-path: inset(50%);
|
|
54
|
+
height: 1px;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
position: absolute;
|
|
57
|
+
width: 1px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.sw-dialog-footer-actions {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex: 0 1 auto;
|
|
63
|
+
flex-wrap: wrap;
|
|
64
|
+
gap: var(--sw-space-2);
|
|
65
|
+
justify-content: flex-end;
|
|
66
|
+
margin-inline-start: auto;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@media (forced-colors: active) {
|
|
70
|
+
.sw-dialog-footer { border-top-color: CanvasText; }
|
|
71
|
+
}`;
|
|
72
|
+
}
|
|
73
|
+
export function dialogFrameClassCatalog() {
|
|
74
|
+
return [
|
|
75
|
+
'sw-dialog-header-framed',
|
|
76
|
+
'sw-dialog-icon',
|
|
77
|
+
'sw-dialog-titles',
|
|
78
|
+
'sw-dialog-description',
|
|
79
|
+
'sw-dialog-footer',
|
|
80
|
+
'sw-dialog-footer-status',
|
|
81
|
+
'sw-dialog-footer-actions',
|
|
82
|
+
];
|
|
83
|
+
}
|
package/dist/css/css-dialog.js
CHANGED
|
@@ -4,6 +4,8 @@ import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
|
4
4
|
const title = typographyVariants.title;
|
|
5
5
|
/* The modal's height cap: a space-8 strip of backdrop stays in view. */
|
|
6
6
|
const maxHeight = 'min(100vh - var(--sw-space-8), 100dvh - var(--sw-space-8))';
|
|
7
|
+
/* How far a sheet's pinned footer steps past the sheet's bottom padding. */
|
|
8
|
+
const sheetFooterOffset = 'calc(-1 * (var(--sw-space-5) + env(safe-area-inset-bottom, 0px)))';
|
|
7
9
|
export function dialogSheetClass(sheetBelow) {
|
|
8
10
|
return `sw-dialog-sheet-below-${sheetBelow}`;
|
|
9
11
|
}
|
|
@@ -11,9 +13,11 @@ export function dialogSheetClass(sheetBelow) {
|
|
|
11
13
|
* Below the breakpoint the dialog is a bottom sheet: docked to the bottom
|
|
12
14
|
* edge at the full viewport width, the top corners rounded and the bottom
|
|
13
15
|
* 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.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* shows above it), and padding that clears the safe areas. A pinned
|
|
17
|
+
* footer steps past that padding so its surface reaches the bottom edge,
|
|
18
|
+
* and pads itself clear of the home indicator. Emitted after
|
|
19
|
+
* `sw-dialog-lg` and `sw-dialog-xl` so it wins at equal specificity. No
|
|
20
|
+
* grabber: there is no drag gesture.
|
|
17
21
|
*/
|
|
18
22
|
function sheetRules() {
|
|
19
23
|
return breakpoints
|
|
@@ -33,6 +37,12 @@ function sheetRules() {
|
|
|
33
37
|
.${dialogSheetClass(breakpoint)}[open] {
|
|
34
38
|
animation: sw-dialog-sheet-in var(--sw-motion-default) var(--sw-motion-easing);
|
|
35
39
|
}
|
|
40
|
+
|
|
41
|
+
.${dialogSheetClass(breakpoint)} .sw-dialog-footer {
|
|
42
|
+
bottom: ${sheetFooterOffset};
|
|
43
|
+
margin-bottom: ${sheetFooterOffset};
|
|
44
|
+
padding-bottom: calc(var(--sw-space-3) + env(safe-area-inset-bottom, 0px));
|
|
45
|
+
}
|
|
36
46
|
}`)
|
|
37
47
|
.join('\n\n');
|
|
38
48
|
}
|
|
@@ -64,7 +74,8 @@ function closeButtonRules() {
|
|
|
64
74
|
gap: var(--sw-space-2);
|
|
65
75
|
}
|
|
66
76
|
|
|
67
|
-
.sw-dialog-header > :
|
|
77
|
+
.sw-dialog-header > :is(h2, h3),
|
|
78
|
+
.sw-dialog-titles {
|
|
68
79
|
flex: 1 1 auto;
|
|
69
80
|
min-width: 0;
|
|
70
81
|
}
|
|
@@ -128,6 +139,10 @@ export function cssDialogClasses() {
|
|
|
128
139
|
max-width: min(var(--sw-dialog-max-lg), calc(100vw - var(--sw-space-8)));
|
|
129
140
|
}
|
|
130
141
|
|
|
142
|
+
.sw-dialog-xl {
|
|
143
|
+
max-width: min(var(--sw-dialog-max-xl), calc(100vw - var(--sw-space-8)));
|
|
144
|
+
}
|
|
145
|
+
|
|
131
146
|
${sheetRules()}
|
|
132
147
|
|
|
133
148
|
${sheetMotionRules()}
|
|
@@ -147,6 +162,7 @@ export function dialogClassCatalog() {
|
|
|
147
162
|
return [
|
|
148
163
|
'sw-dialog',
|
|
149
164
|
'sw-dialog-lg',
|
|
165
|
+
'sw-dialog-xl',
|
|
150
166
|
...breakpoints.map((breakpoint) => dialogSheetClass(breakpoint)),
|
|
151
167
|
'sw-dialog-header',
|
|
152
168
|
'sw-dialog-close',
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const noticeTones: readonly ["info", "success", "warning", "danger"];
|
|
2
|
+
export declare const noticeVariants: readonly ["default", "band"];
|
|
3
|
+
/** `default` is a rounded note in the flow; `band` a full-width strip of page chrome. */
|
|
4
|
+
export type NoticeVariant = (typeof noticeVariants)[number];
|
|
5
|
+
/** The notice's meaning: information, a good outcome, a caution, a refusal. */
|
|
6
|
+
export type NoticeTone = (typeof noticeTones)[number];
|
|
7
|
+
/**
|
|
8
|
+
* How much of a tone's color the notice's fill mixes into the surface. Info
|
|
9
|
+
* uses the derived accent tint instead (ADR 0011), which already keeps the
|
|
10
|
+
* accent readable on every palette.
|
|
11
|
+
*/
|
|
12
|
+
export declare const noticeTintShare = 0.1;
|
|
13
|
+
/**
|
|
14
|
+
* How far the notice's words move from the tone's color toward the text
|
|
15
|
+
* color, so they read darker than the tone on a light fill (lighter on a
|
|
16
|
+
* dark one) and keep 4.5:1 on the tint (`notice.test.tsx`).
|
|
17
|
+
*/
|
|
18
|
+
export declare const noticeInkTextShare = 0.3;
|
|
19
|
+
/** The semantic color a tone is drawn from. */
|
|
20
|
+
export declare function noticeToneColor(tone: NoticeTone): string;
|
|
21
|
+
export declare function cssNoticeClasses(): string;
|
|
22
|
+
export declare function noticeClassCatalog(): string[];
|