@scalewing/react 1.27.0 → 1.29.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +99 -3
  3. package/dist/components/Checkbox.js +5 -4
  4. package/dist/components/DenominationGrid.d.ts +4 -1
  5. package/dist/components/DenominationGrid.js +8 -3
  6. package/dist/components/Dialog.d.ts +28 -4
  7. package/dist/components/Dialog.js +20 -2
  8. package/dist/components/GlyphTile.d.ts +21 -0
  9. package/dist/components/GlyphTile.js +17 -0
  10. package/dist/components/MultiSelect.d.ts +10 -0
  11. package/dist/components/MultiSelect.js +62 -0
  12. package/dist/components/Notice.d.ts +69 -0
  13. package/dist/components/Notice.js +48 -0
  14. package/dist/components/Progress.d.ts +3 -1
  15. package/dist/components/Progress.js +4 -0
  16. package/dist/components/SegmentedControl.d.ts +8 -0
  17. package/dist/components/SegmentedControl.js +4 -3
  18. package/dist/components/Toast.d.ts +20 -4
  19. package/dist/components/Toast.js +44 -21
  20. package/dist/components/checkbox/CheckboxControl.d.ts +9 -0
  21. package/dist/components/checkbox/CheckboxControl.js +9 -0
  22. package/dist/components/denomination-grid/DenominationStrip.d.ts +5 -2
  23. package/dist/components/denomination-grid/DenominationStrip.js +2 -2
  24. package/dist/components/denomination-grid/DenominationTiles.d.ts +5 -2
  25. package/dist/components/denomination-grid/DenominationTiles.js +2 -2
  26. package/dist/components/denomination-grid/types.d.ts +9 -0
  27. package/dist/components/dialog/DialogFooter.d.ts +14 -0
  28. package/dist/components/dialog/DialogFooter.js +11 -0
  29. package/dist/components/dialog/DialogTitle.d.ts +11 -4
  30. package/dist/components/dialog/DialogTitle.js +8 -6
  31. package/dist/components/multi-select/MultiSelectPanel.d.ts +25 -0
  32. package/dist/components/multi-select/MultiSelectPanel.js +41 -0
  33. package/dist/components/multi-select/types.d.ts +35 -0
  34. package/dist/components/multi-select/types.js +1 -0
  35. package/dist/components/multi-select/validate.d.ts +3 -0
  36. package/dist/components/multi-select/validate.js +25 -0
  37. package/dist/components/toast/ToastContent.d.ts +22 -0
  38. package/dist/components/toast/ToastContent.js +21 -0
  39. package/dist/components/toast/use-toast-hold.d.ts +28 -0
  40. package/dist/components/toast/use-toast-hold.js +53 -0
  41. package/dist/components/toast/use-toast-timer.d.ts +15 -0
  42. package/dist/components/toast/use-toast-timer.js +34 -0
  43. package/dist/css/css-components.js +16 -0
  44. package/dist/css/css-denomination-grid.js +4 -0
  45. package/dist/css/css-denomination-label.d.ts +10 -0
  46. package/dist/css/css-denomination-label.js +36 -0
  47. package/dist/css/css-dialog-frame.d.ts +2 -0
  48. package/dist/css/css-dialog-frame.js +83 -0
  49. package/dist/css/css-dialog.js +20 -4
  50. package/dist/css/css-glyph-tile.d.ts +5 -0
  51. package/dist/css/css-glyph-tile.js +57 -0
  52. package/dist/css/css-multi-select.d.ts +2 -0
  53. package/dist/css/css-multi-select.js +156 -0
  54. package/dist/css/css-notice.d.ts +22 -0
  55. package/dist/css/css-notice.js +168 -0
  56. package/dist/css/css-progress.d.ts +3 -0
  57. package/dist/css/css-progress.js +13 -6
  58. package/dist/css/css-segmented.js +58 -0
  59. package/dist/css/css-toast-card.d.ts +5 -0
  60. package/dist/css/css-toast-card.js +122 -0
  61. package/dist/css/css-variables.js +12 -1
  62. package/dist/css/stacking.js +5 -2
  63. package/dist/css/stylesheet.js +10 -0
  64. package/dist/index.d.ts +8 -5
  65. package/dist/index.js +7 -4
  66. package/dist/styles.css +522 -3
  67. package/package.json +2 -2
@@ -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,16 @@ 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';
18
+ import { cssMultiSelectClasses } from './css-multi-select.js';
17
19
  import { cssSplitClasses } from './css-split.js';
18
20
  import { cssSwitchClasses } from './css-switch.js';
19
21
  import { cssTextClasses } from './css-text.js';
20
22
  import { cssSpinnerClasses } from './css-spinner.js';
21
23
  import { cssProgressClasses } from './css-progress.js';
24
+ import { cssNoticeClasses } from './css-notice.js';
25
+ import { cssGlyphTileClasses } from './css-glyph-tile.js';
22
26
  import { cssTooltipClasses } from './css-tooltip.js';
23
27
  import { cssInfoTipClasses } from './css-info-tip.js';
24
28
  import { cssSeparatorClasses } from './css-separator.js';
@@ -34,6 +38,7 @@ import { cssFieldClasses } from './css-field.js';
34
38
  import { cssStepperClasses } from './css-stepper.js';
35
39
  import { cssStepsClasses } from './css-steps.js';
36
40
  import { cssToastClasses } from './css-toast.js';
41
+ import { cssToastCardClasses } from './css-toast-card.js';
37
42
  import { cssBoxBorderClasses } from './css-box-border.js';
38
43
  import { cssPopupHostRules } from './stacking.js';
39
44
  /*
@@ -57,6 +62,7 @@ function reducedTransparencyRules() {
57
62
  .sw-accordion,
58
63
  .sw-select-list,
59
64
  .sw-action-menu-list,
65
+ .sw-multi-select-panel,
60
66
  .sw-date-field-calendar,
61
67
  .sw-toast {
62
68
  background: var(--sw-color-surface);
@@ -133,12 +139,16 @@ ${cssSectionNavClasses()}
133
139
 
134
140
  ${cssDialogClasses()}
135
141
 
142
+ ${cssDialogFrameClasses()}
143
+
136
144
  ${cssAccordionClasses()}
137
145
 
138
146
  ${cssSelectClasses()}
139
147
 
140
148
  ${cssActionMenuClasses()}
141
149
 
150
+ ${cssMultiSelectClasses()}
151
+
142
152
  ${cssActionBarClasses()}
143
153
 
144
154
  ${cssSwitchClasses()}
@@ -147,6 +157,10 @@ ${cssSpinnerClasses()}
147
157
 
148
158
  ${cssProgressClasses()}
149
159
 
160
+ ${cssNoticeClasses()}
161
+
162
+ ${cssGlyphTileClasses()}
163
+
150
164
  ${cssTooltipClasses()}
151
165
 
152
166
  ${cssInfoTipClasses()}
@@ -175,6 +189,8 @@ ${cssSplitClasses()}
175
189
 
176
190
  ${cssToastClasses()}
177
191
 
192
+ ${cssToastCardClasses()}
193
+
178
194
  ${cssResponsiveClasses()}
179
195
 
180
196
  ${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,2 @@
1
+ export declare function cssDialogFrameClasses(): string;
2
+ export declare function dialogFrameClassCatalog(): string[];
@@ -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
+ }
@@ -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. Emitted after
15
- * `sw-dialog-lg` so it wins at equal specificity. No grabber: there is no
16
- * drag gesture.
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 > :first-child {
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,5 @@
1
+ export declare const glyphTileSizes: readonly ["sm", "md", "lg"];
2
+ /** `sm` 24, `md` 28 (the xs control height), `lg` 32. */
3
+ export type GlyphTileSize = (typeof glyphTileSizes)[number];
4
+ export declare function cssGlyphTileClasses(): string;
5
+ export declare function glyphTileClassCatalog(): string[];
@@ -0,0 +1,57 @@
1
+ import { categoricalColorKeys } from '@scalewing/tokens';
2
+ export const glyphTileSizes = ['sm', 'md', 'lg'];
3
+ const tileSide = {
4
+ sm: 'var(--sw-space-5)',
5
+ md: 'var(--sw-control-xs-min-height)',
6
+ lg: 'var(--sw-space-6)',
7
+ };
8
+ function hueRules() {
9
+ return categoricalColorKeys
10
+ .map((key) => `.sw-glyph-tile-${key} { --sw-glyph-tile-fill: var(--sw-categorical-${key}); }`)
11
+ .join('\n');
12
+ }
13
+ function sizeRules() {
14
+ return Object.entries(tileSide)
15
+ .map(([size, side]) => `.sw-glyph-tile-${size} { height: ${side}; width: ${side}; }`)
16
+ .join('\n');
17
+ }
18
+ /*
19
+ * A rounded square in a categorical fill with the consumer's glyph centred
20
+ * in white, sized to spacing step 4 whatever the glyph's own size. It is
21
+ * decoration: the words beside it carry the meaning. Forced colors drop the
22
+ * fill, so a border keeps the tile's shape and the glyph takes the text
23
+ * color.
24
+ */
25
+ export function cssGlyphTileClasses() {
26
+ return `.sw-glyph-tile {
27
+ align-items: center;
28
+ background: var(--sw-glyph-tile-fill);
29
+ border-radius: var(--sw-radius-sm);
30
+ box-sizing: border-box;
31
+ color: var(--sw-categorical-on);
32
+ display: inline-flex;
33
+ flex: none;
34
+ justify-content: center;
35
+ }
36
+
37
+ .sw-glyph-tile > svg {
38
+ flex: none;
39
+ height: var(--sw-space-4);
40
+ width: var(--sw-space-4);
41
+ }
42
+
43
+ ${hueRules()}
44
+
45
+ ${sizeRules()}
46
+
47
+ @media (forced-colors: active) {
48
+ .sw-glyph-tile { border: 1px solid CanvasText; color: CanvasText; }
49
+ }`;
50
+ }
51
+ export function glyphTileClassCatalog() {
52
+ return [
53
+ 'sw-glyph-tile',
54
+ ...categoricalColorKeys.map((key) => `sw-glyph-tile-${key}`),
55
+ ...glyphTileSizes.map((size) => `sw-glyph-tile-${size}`),
56
+ ];
57
+ }
@@ -0,0 +1,2 @@
1
+ export declare function cssMultiSelectClasses(): string;
2
+ export declare function multiSelectClassCatalog(): string[];
@@ -0,0 +1,156 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ import { chevronStroke } from './chevron.js';
3
+ import { zIndex } from './stacking.js';
4
+ const label = typographyVariants.label;
5
+ const body = typographyVariants.body;
6
+ /*
7
+ * The trigger: a glass control at the md height with the muted label, the
8
+ * summary and Select's chevron. The panel: a glass popover beside it (as
9
+ * ActionMenu's list), All and Clear over a hairline, then one checkbox row
10
+ * per option at the md height, the whole row its label; a separated row
11
+ * starts after a hairline. Checkboxes reuse Checkbox's generated control.
12
+ */
13
+ export function cssMultiSelectClasses() {
14
+ return `.sw-multi-select {
15
+ display: inline-flex;
16
+ max-width: 100%;
17
+ }
18
+
19
+ .sw-multi-select-trigger {
20
+ align-items: center;
21
+ appearance: none;
22
+ background-color: var(--sw-glass-fill);
23
+ border: 1px solid var(--sw-color-border);
24
+ border-radius: var(--sw-radius-sm);
25
+ box-sizing: border-box;
26
+ color: var(--sw-color-text);
27
+ column-gap: var(--sw-space-2);
28
+ cursor: pointer;
29
+ display: inline-flex;
30
+ font-family: var(--sw-font-sans);
31
+ font-size: ${label.fontSize}px;
32
+ font-weight: ${label.fontWeight};
33
+ letter-spacing: ${label.letterSpacing}px;
34
+ line-height: ${label.lineHeight}px;
35
+ margin: 0;
36
+ max-width: 100%;
37
+ min-height: var(--sw-control-md-min-height);
38
+ padding-inline: var(--sw-control-md-padding-inline);
39
+ }
40
+
41
+ .sw-multi-select-trigger::after {
42
+ ${chevronStroke}
43
+ content: '';
44
+ flex: none;
45
+ margin-inline-start: var(--sw-space-2);
46
+ transform: translateY(-25%) rotate(45deg);
47
+ }
48
+
49
+ .sw-multi-select-trigger:focus-visible {
50
+ outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
51
+ outline-offset: var(--sw-focus-ring-offset);
52
+ }
53
+
54
+ .sw-multi-select-trigger:disabled {
55
+ cursor: not-allowed;
56
+ opacity: var(--sw-disabled-opacity);
57
+ }
58
+
59
+ .sw-multi-select-label {
60
+ color: var(--sw-color-muted);
61
+ font-weight: 500;
62
+ }
63
+
64
+ .sw-multi-select-summary {
65
+ min-width: 0;
66
+ overflow: hidden;
67
+ text-overflow: ellipsis;
68
+ white-space: nowrap;
69
+ }
70
+
71
+ .sw-multi-select-panel {
72
+ background: var(--sw-glass-fill);
73
+ border: 1px solid var(--sw-glass-border);
74
+ border-radius: var(--sw-radius-sm);
75
+ box-shadow: var(--sw-elevation-sm), inset 0 1px 0 var(--sw-glass-specular);
76
+ backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
77
+ -webkit-backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
78
+ box-sizing: border-box;
79
+ color: var(--sw-color-text);
80
+ display: flex;
81
+ flex-direction: column;
82
+ inset: auto;
83
+ margin: 0;
84
+ /* The viewport, not --sw-select-max: the list is short and should show
85
+ whole rather than scroll inside a 22rem cap. */
86
+ max-height: calc(100% - var(--sw-space-2) - var(--sw-space-2));
87
+ max-width: calc(100% - var(--sw-space-2) - var(--sw-space-2));
88
+ min-width: var(--sw-select-max);
89
+ overflow: auto;
90
+ padding: var(--sw-space-1);
91
+ position: fixed;
92
+ width: max-content;
93
+ ${zIndex('popup')}
94
+ }
95
+
96
+ .sw-multi-select-commands {
97
+ border-bottom: 1px solid var(--sw-color-border);
98
+ display: flex;
99
+ justify-content: space-between;
100
+ margin-bottom: var(--sw-space-1);
101
+ padding-bottom: var(--sw-space-1);
102
+ }
103
+
104
+ .sw-multi-select-row-separated {
105
+ border-top: 1px solid var(--sw-color-border);
106
+ margin-top: var(--sw-space-1);
107
+ padding-top: var(--sw-space-1);
108
+ }
109
+
110
+ .sw-checkbox-label.sw-multi-select-option {
111
+ border-radius: var(--sw-radius-sm);
112
+ box-sizing: border-box;
113
+ font-family: var(--sw-font-sans);
114
+ font-size: ${label.fontSize}px;
115
+ font-weight: ${body.fontWeight};
116
+ letter-spacing: ${label.letterSpacing}px;
117
+ line-height: ${label.lineHeight}px;
118
+ padding-inline: var(--sw-space-2);
119
+ width: 100%;
120
+ }
121
+
122
+ .sw-multi-select-option:hover {
123
+ background: color-mix(in srgb, var(--sw-color-muted) 16%, transparent);
124
+ }
125
+
126
+ .sw-multi-select-icon {
127
+ display: inline-flex;
128
+ flex: none;
129
+ }
130
+
131
+ .sw-multi-select-text {
132
+ min-width: 0;
133
+ }
134
+
135
+ @media (forced-colors: active) {
136
+ .sw-multi-select-trigger::after { border-color: CanvasText; }
137
+ .sw-multi-select-panel { border-color: CanvasText; }
138
+ .sw-multi-select-commands,
139
+ .sw-multi-select-row-separated { border-color: CanvasText; }
140
+ }`;
141
+ }
142
+ export function multiSelectClassCatalog() {
143
+ return [
144
+ 'sw-multi-select',
145
+ 'sw-multi-select-trigger',
146
+ 'sw-multi-select-label',
147
+ 'sw-multi-select-summary',
148
+ 'sw-multi-select-panel',
149
+ 'sw-multi-select-commands',
150
+ 'sw-multi-select-row',
151
+ 'sw-multi-select-row-separated',
152
+ 'sw-multi-select-option',
153
+ 'sw-multi-select-icon',
154
+ 'sw-multi-select-text',
155
+ ];
156
+ }