@scalewing/react 1.26.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +147 -3
  3. package/dist/components/ActionBar.d.ts +14 -0
  4. package/dist/components/ActionBar.js +5 -2
  5. package/dist/components/DenominationGrid.d.ts +5 -2
  6. package/dist/components/DenominationGrid.js +11 -3
  7. package/dist/components/Dialog.d.ts +28 -4
  8. package/dist/components/Dialog.js +20 -2
  9. package/dist/components/Notice.d.ts +69 -0
  10. package/dist/components/Notice.js +48 -0
  11. package/dist/components/Progress.d.ts +3 -1
  12. package/dist/components/Progress.js +4 -0
  13. package/dist/components/Stepper.d.ts +44 -0
  14. package/dist/components/Stepper.js +77 -0
  15. package/dist/components/Steps.d.ts +18 -0
  16. package/dist/components/Steps.js +34 -0
  17. package/dist/components/Toast.d.ts +20 -4
  18. package/dist/components/Toast.js +44 -21
  19. package/dist/components/action-bar/ActionBarSummary.d.ts +22 -0
  20. package/dist/components/action-bar/ActionBarSummary.js +23 -0
  21. package/dist/components/denomination-grid/ColumnHeadText.d.ts +7 -0
  22. package/dist/components/denomination-grid/ColumnHeadText.js +13 -0
  23. package/dist/components/denomination-grid/DenominationStrip.d.ts +5 -2
  24. package/dist/components/denomination-grid/DenominationStrip.js +4 -8
  25. package/dist/components/denomination-grid/DenominationTiles.d.ts +5 -2
  26. package/dist/components/denomination-grid/DenominationTiles.js +4 -3
  27. package/dist/components/denomination-grid/EntryCell.d.ts +15 -0
  28. package/dist/components/denomination-grid/EntryCell.js +16 -0
  29. package/dist/components/denomination-grid/RowLabelLine.d.ts +3 -2
  30. package/dist/components/denomination-grid/RowLabelLine.js +5 -3
  31. package/dist/components/denomination-grid/StripRow.d.ts +13 -0
  32. package/dist/components/denomination-grid/StripRow.js +19 -0
  33. package/dist/components/denomination-grid/types.d.ts +53 -1
  34. package/dist/components/denomination-grid/validate-entry.d.ts +15 -0
  35. package/dist/components/denomination-grid/validate-entry.js +78 -0
  36. package/dist/components/dialog/DialogFooter.d.ts +14 -0
  37. package/dist/components/dialog/DialogFooter.js +11 -0
  38. package/dist/components/dialog/DialogTitle.d.ts +11 -4
  39. package/dist/components/dialog/DialogTitle.js +8 -6
  40. package/dist/components/steps/Step.d.ts +24 -0
  41. package/dist/components/steps/Step.js +62 -0
  42. package/dist/components/steps/step-status.d.ts +26 -0
  43. package/dist/components/steps/step-status.js +37 -0
  44. package/dist/components/steps/steps-context.d.ts +16 -0
  45. package/dist/components/steps/steps-context.js +3 -0
  46. package/dist/components/toast/ToastContent.d.ts +22 -0
  47. package/dist/components/toast/ToastContent.js +21 -0
  48. package/dist/components/toast/use-toast-hold.d.ts +28 -0
  49. package/dist/components/toast/use-toast-hold.js +53 -0
  50. package/dist/components/toast/use-toast-timer.d.ts +15 -0
  51. package/dist/components/toast/use-toast-timer.js +34 -0
  52. package/dist/css/css-action-bar-summary.d.ts +2 -0
  53. package/dist/css/css-action-bar-summary.js +48 -0
  54. package/dist/css/css-action-bar.js +4 -0
  55. package/dist/css/css-components.js +16 -0
  56. package/dist/css/css-denomination-entry.d.ts +2 -0
  57. package/dist/css/css-denomination-entry.js +78 -0
  58. package/dist/css/css-denomination-grid.js +8 -0
  59. package/dist/css/css-denomination-label.d.ts +10 -0
  60. package/dist/css/css-denomination-label.js +36 -0
  61. package/dist/css/css-dialog-frame.d.ts +2 -0
  62. package/dist/css/css-dialog-frame.js +83 -0
  63. package/dist/css/css-dialog.js +20 -4
  64. package/dist/css/css-notice.d.ts +22 -0
  65. package/dist/css/css-notice.js +168 -0
  66. package/dist/css/css-progress.d.ts +3 -0
  67. package/dist/css/css-progress.js +13 -6
  68. package/dist/css/css-stepper.d.ts +10 -0
  69. package/dist/css/css-stepper.js +126 -0
  70. package/dist/css/css-steps.d.ts +9 -0
  71. package/dist/css/css-steps.js +203 -0
  72. package/dist/css/css-toast-card.d.ts +5 -0
  73. package/dist/css/css-toast-card.js +122 -0
  74. package/dist/css/css-variables.js +1 -0
  75. package/dist/css/stylesheet.js +10 -0
  76. package/dist/index.d.ts +9 -6
  77. package/dist/index.js +8 -5
  78. package/dist/stepper-count.d.ts +37 -0
  79. package/dist/stepper-count.js +50 -0
  80. package/dist/styles.css +623 -1
  81. package/package.json +1 -1
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export declare function cssActionBarSummaryClasses(): string;
2
+ export declare function actionBarSummaryClassCatalog(): string[];
@@ -0,0 +1,48 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ const label = typographyVariants.label;
3
+ /*
4
+ * The bar's figure: label, value and detail on one baseline, a small gap
5
+ * apart, wrapping as one phrase. The value takes the heading variant and
6
+ * tabular figures from the text classes it carries (so it steps down below
7
+ * md with every heading); the label and detail are muted label-size words
8
+ * at regular weight. A tone colors the value only. Forced colors keep the
9
+ * system text color.
10
+ */
11
+ export function cssActionBarSummaryClasses() {
12
+ return `.sw-action-bar-summary {
13
+ align-items: baseline;
14
+ column-gap: var(--sw-space-2);
15
+ display: inline-flex;
16
+ flex-wrap: wrap;
17
+ min-width: 0;
18
+ }
19
+
20
+ .sw-action-bar-summary-label,
21
+ .sw-action-bar-summary-detail {
22
+ color: var(--sw-color-muted);
23
+ font-size: ${label.fontSize}px;
24
+ letter-spacing: ${label.letterSpacing}px;
25
+ line-height: ${label.lineHeight}px;
26
+ }
27
+
28
+ .sw-action-bar-summary-value {
29
+ color: var(--sw-color-text);
30
+ }
31
+
32
+ .sw-action-bar-summary-danger .sw-action-bar-summary-value { color: var(--sw-color-danger); }
33
+ .sw-action-bar-summary-success .sw-action-bar-summary-value { color: var(--sw-color-success); }
34
+
35
+ @media (forced-colors: active) {
36
+ .sw-action-bar-summary-value { color: CanvasText; }
37
+ }`;
38
+ }
39
+ export function actionBarSummaryClassCatalog() {
40
+ return [
41
+ 'sw-action-bar-summary',
42
+ 'sw-action-bar-summary-label',
43
+ 'sw-action-bar-summary-value',
44
+ 'sw-action-bar-summary-detail',
45
+ 'sw-action-bar-summary-danger',
46
+ 'sw-action-bar-summary-success',
47
+ ];
48
+ }
@@ -1,5 +1,6 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { breakpointQuery, breakpoints, } from './breakpoints.js';
3
+ import { actionBarSummaryClassCatalog, cssActionBarSummaryClasses, } from './css-action-bar-summary.js';
3
4
  import { zIndex } from './stacking.js';
4
5
  const caption = typographyVariants.caption;
5
6
  /* The bar floats a step above the bottom edge and clears the home indicator. */
@@ -93,6 +94,8 @@ export function cssActionBarClasses() {
93
94
  .sw-action-bar-actions > * { flex: 1 1 auto; }
94
95
  }
95
96
 
97
+ ${cssActionBarSummaryClasses()}
98
+
96
99
  ${stickyRules()}`;
97
100
  }
98
101
  export function actionBarClassCatalog() {
@@ -101,6 +104,7 @@ export function actionBarClassCatalog() {
101
104
  'sw-action-bar-status',
102
105
  'sw-action-bar-actions',
103
106
  'sw-action-bar-sticky',
107
+ ...actionBarSummaryClassCatalog(),
104
108
  ...breakpoints.map((breakpoint) => actionBarStickyClass(breakpoint)),
105
109
  ];
106
110
  }
@@ -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';
@@ -31,7 +33,10 @@ import { cssGridClasses } from './css-grid.js';
31
33
  import { cssGridSpanClasses } from './css-grid-span.js';
32
34
  import { cssDenominationGridClasses } from './css-denomination-grid.js';
33
35
  import { cssFieldClasses } from './css-field.js';
36
+ import { cssStepperClasses } from './css-stepper.js';
37
+ import { cssStepsClasses } from './css-steps.js';
34
38
  import { cssToastClasses } from './css-toast.js';
39
+ import { cssToastCardClasses } from './css-toast-card.js';
35
40
  import { cssBoxBorderClasses } from './css-box-border.js';
36
41
  import { cssPopupHostRules } from './stacking.js';
37
42
  /*
@@ -44,6 +49,7 @@ function reducedTransparencyRules() {
44
49
  .sw-card-glass,
45
50
  .sw-app-header,
46
51
  .sw-action-bar,
52
+ .sw-steps-item,
47
53
  .sw-badge-neutral,
48
54
  .sw-segmented,
49
55
  .sw-table-sticky thead th,
@@ -130,6 +136,8 @@ ${cssSectionNavClasses()}
130
136
 
131
137
  ${cssDialogClasses()}
132
138
 
139
+ ${cssDialogFrameClasses()}
140
+
133
141
  ${cssAccordionClasses()}
134
142
 
135
143
  ${cssSelectClasses()}
@@ -144,6 +152,8 @@ ${cssSpinnerClasses()}
144
152
 
145
153
  ${cssProgressClasses()}
146
154
 
155
+ ${cssNoticeClasses()}
156
+
147
157
  ${cssTooltipClasses()}
148
158
 
149
159
  ${cssInfoTipClasses()}
@@ -164,10 +174,16 @@ ${cssDenominationGridClasses()}
164
174
 
165
175
  ${cssFieldClasses()}
166
176
 
177
+ ${cssStepperClasses()}
178
+
179
+ ${cssStepsClasses()}
180
+
167
181
  ${cssSplitClasses()}
168
182
 
169
183
  ${cssToastClasses()}
170
184
 
185
+ ${cssToastCardClasses()}
186
+
171
187
  ${cssResponsiveClasses()}
172
188
 
173
189
  ${cssBoxBorderClasses()}
@@ -0,0 +1,2 @@
1
+ export declare function cssDenominationEntryClasses(): string;
2
+ export declare function denominationEntryClassCatalog(): string[];
@@ -0,0 +1,78 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ const caption = typographyVariants.caption;
3
+ /** An enabled entry count, unless its Stepper shows an error (whose danger border wins). */
4
+ const tonedEntryInput = '[data-theme] .sw-denomination-cell-toned .sw-stepper:not(.sw-stepper-invalid) input.sw-stepper-input:not(:disabled)';
5
+ /*
6
+ * Count entry and the labels around it. An entry cell holds a full-width
7
+ * Stepper; its count keeps spacing step 8 (room for three digits), so a narrow strip scrolls
8
+ * in its region instead of crushing the counts. A cell tone draws the
9
+ * count's border in the tone, a hairline thicker through an inset shadow so
10
+ * nothing moves. A caption is a muted caption-size line: under a row label,
11
+ * or after a column label. A quiet row sits on the subtle fill, its pinned
12
+ * label too. A disabled row fades its label, counts and total (its entry is
13
+ * disabled by its Stepper); a pinned label fades its contents, not its
14
+ * fill, so counts never show through it.
15
+ */
16
+ export function cssDenominationEntryClasses() {
17
+ return `.sw-denomination-strip td.sw-denomination-entry {
18
+ padding: var(--sw-space-1);
19
+ }
20
+
21
+ .sw-denomination-entry .sw-stepper-full .sw-stepper-track {
22
+ grid-template-columns: var(--sw-space-6) minmax(var(--sw-space-8), 1fr) var(--sw-space-6);
23
+ }
24
+
25
+ /* A strip with entry rows centres its heads and read-only counts over the steppers. */
26
+ .sw-denomination-strip-entry .sw-denomination-head:not(.sw-denomination-total-head),
27
+ .sw-denomination-strip-entry .sw-denomination-cell {
28
+ text-align: center;
29
+ }
30
+
31
+ ${tonedEntryInput} {
32
+ border-color: var(--sw-denomination-cell-tone);
33
+ box-shadow: inset 0 0 0 1px var(--sw-denomination-cell-tone);
34
+ }
35
+
36
+ .sw-denomination-label-words {
37
+ display: flex;
38
+ flex-direction: column;
39
+ min-width: 0;
40
+ }
41
+
42
+ .sw-denomination-label-caption,
43
+ .sw-denomination-head-caption {
44
+ color: var(--sw-color-muted);
45
+ font-size: ${caption.fontSize}px;
46
+ font-weight: ${caption.fontWeight};
47
+ letter-spacing: ${caption.letterSpacing}px;
48
+ line-height: ${caption.lineHeight}px;
49
+ }
50
+
51
+ .sw-denomination-row-quiet,
52
+ .sw-denomination-strip .sw-denomination-row-quiet .sw-denomination-label {
53
+ background: var(--sw-color-subtle);
54
+ }
55
+
56
+ .sw-denomination-strip .sw-denomination-row-disabled .sw-denomination-label-body,
57
+ .sw-denomination-strip .sw-denomination-row-disabled .sw-denomination-cell,
58
+ .sw-denomination-strip .sw-denomination-row-disabled .sw-denomination-total {
59
+ opacity: var(--sw-disabled-opacity);
60
+ }
61
+
62
+ @media (forced-colors: active) {
63
+ ${tonedEntryInput} { border-color: Highlight; box-shadow: inset 0 0 0 1px Highlight; }
64
+ .sw-denomination-label-caption,
65
+ .sw-denomination-head-caption { color: CanvasText; }
66
+ }`;
67
+ }
68
+ export function denominationEntryClassCatalog() {
69
+ return [
70
+ 'sw-denomination-entry',
71
+ 'sw-denomination-strip-entry',
72
+ 'sw-denomination-label-words',
73
+ 'sw-denomination-label-caption',
74
+ 'sw-denomination-head-caption',
75
+ 'sw-denomination-row-quiet',
76
+ 'sw-denomination-row-disabled',
77
+ ];
78
+ }
@@ -1,7 +1,9 @@
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';
6
+ import { cssDenominationEntryClasses, denominationEntryClassCatalog, } from './css-denomination-entry.js';
5
7
  import { cssDenominationCellToneClasses, denominationCellToneClassCatalog, } from './css-denomination-cell-tones.js';
6
8
  import { pinnedStartShadeRules, scrollRegionRules, } from './css-scroll-region.js';
7
9
  import { visuallyHiddenDeclarations } from './css-utilities.js';
@@ -249,8 +251,12 @@ ${pinnedStartShadeRules('.sw-denomination-scroll', [
249
251
 
250
252
  ${cssDenominationCellToneClasses()}
251
253
 
254
+ ${cssDenominationEntryClasses()}
255
+
252
256
  ${cssDenominationAlignedClasses()}
253
257
 
258
+ ${cssDenominationLabelClasses()}
259
+
254
260
  @media (forced-colors: active) {
255
261
  .sw-denomination-label { border-inline-start-color: CanvasText; color: CanvasText; }
256
262
  .sw-denomination-strip .sw-denomination-label::before { background: CanvasText; forced-color-adjust: none; }
@@ -290,6 +296,8 @@ export function denominationGridClassCatalog() {
290
296
  'sw-denomination-tile',
291
297
  'sw-denomination-subtotal',
292
298
  ...denominationCellToneClassCatalog(),
299
+ ...denominationEntryClassCatalog(),
293
300
  ...denominationAlignedClassCatalog(),
301
+ ...denominationLabelClassCatalog(),
294
302
  ];
295
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,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[];
@@ -0,0 +1,168 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ export const noticeTones = ['info', 'success', 'warning', 'danger'];
3
+ export const noticeVariants = ['default', 'band'];
4
+ /**
5
+ * How much of a tone's color the notice's fill mixes into the surface. Info
6
+ * uses the derived accent tint instead (ADR 0011), which already keeps the
7
+ * accent readable on every palette.
8
+ */
9
+ export const noticeTintShare = 0.1;
10
+ /**
11
+ * How far the notice's words move from the tone's color toward the text
12
+ * color, so they read darker than the tone on a light fill (lighter on a
13
+ * dark one) and keep 4.5:1 on the tint (`notice.test.tsx`).
14
+ */
15
+ export const noticeInkTextShare = 0.3;
16
+ /** The semantic color a tone is drawn from. */
17
+ export function noticeToneColor(tone) {
18
+ return tone === 'info' ? 'accent' : tone;
19
+ }
20
+ function toneFill(tone) {
21
+ if (tone === 'info')
22
+ return 'var(--sw-color-accentSubtle)';
23
+ return `color-mix(in srgb, var(--sw-color-${tone}) ${noticeTintShare * 100}%, var(--sw-color-surface))`;
24
+ }
25
+ function toneInk(tone) {
26
+ const toneShare = Math.round((1 - noticeInkTextShare) * 100);
27
+ return `color-mix(in srgb, var(--sw-color-${noticeToneColor(tone)}) ${toneShare}%, var(--sw-color-text))`;
28
+ }
29
+ function toneRules() {
30
+ return noticeTones
31
+ .map((tone) => `.sw-notice-${tone} { --sw-notice-fill: ${toneFill(tone)}; --sw-notice-ink: ${toneInk(tone)}; }`)
32
+ .join('\n');
33
+ }
34
+ const label = typographyVariants.label;
35
+ const body = typographyVariants.body;
36
+ const caption = typographyVariants.caption;
37
+ /*
38
+ * A tinted block in the page: the tone's fill, its words in the tone's ink,
39
+ * label-size type. An optional glyph leads; the title (a bold lead) and a
40
+ * text message share a row while they fit and wrap under one another when
41
+ * they do not; a message of elements (a field, a checkbox) is stacked under
42
+ * the title. An action ends the row (a small Button
43
+ * overhangs the padding rather than growing it) and drops under the text on
44
+ * a narrow column. Controls inside keep their own colors. Forced
45
+ * colors draw a border instead of the fill.
46
+ */
47
+ export function cssNoticeClasses() {
48
+ return `${toneRules()}
49
+
50
+ .sw-notice {
51
+ align-items: flex-start;
52
+ background: var(--sw-notice-fill);
53
+ border-radius: var(--sw-radius-sm);
54
+ box-sizing: border-box;
55
+ color: var(--sw-notice-ink);
56
+ column-gap: var(--sw-space-2);
57
+ display: flex;
58
+ flex-wrap: wrap;
59
+ font-family: var(--sw-font-sans);
60
+ font-size: ${label.fontSize}px;
61
+ letter-spacing: ${label.letterSpacing}px;
62
+ line-height: ${label.lineHeight}px;
63
+ min-width: 0;
64
+ padding: var(--sw-space-2) var(--sw-space-3);
65
+ row-gap: var(--sw-space-2);
66
+ /* Muted words of a control inside (a field's or a checkbox's
67
+ description) take the ink, which keeps 4.5:1 on the fill. */
68
+ --sw-color-muted: var(--sw-notice-ink);
69
+ }
70
+
71
+ .sw-notice-icon {
72
+ display: inline-flex;
73
+ flex: none;
74
+ min-height: ${label.lineHeight}px;
75
+ align-items: center;
76
+ }
77
+
78
+ .sw-notice-content {
79
+ align-items: baseline;
80
+ column-gap: var(--sw-space-2);
81
+ display: flex;
82
+ flex: 1 1 16rem;
83
+ flex-wrap: wrap;
84
+ min-width: 0;
85
+ row-gap: var(--sw-space-1);
86
+ }
87
+
88
+ .sw-notice-stacked {
89
+ align-items: stretch;
90
+ flex-direction: column;
91
+ row-gap: var(--sw-space-2);
92
+ }
93
+
94
+ .sw-notice-title {
95
+ font-weight: ${label.fontWeight};
96
+ min-width: 0;
97
+ }
98
+
99
+ .sw-notice-body {
100
+ flex: 1 1 auto;
101
+ font-weight: ${body.fontWeight};
102
+ min-width: 0;
103
+ }
104
+
105
+ .sw-notice-action {
106
+ align-items: center;
107
+ align-self: flex-start;
108
+ display: inline-flex;
109
+ flex: none;
110
+ min-height: ${label.lineHeight}px;
111
+ }
112
+
113
+ /* A small Button keeps the note one text line tall: it overhangs the
114
+ padding evenly instead of growing the row. */
115
+ .sw-notice-action > .sw-button {
116
+ margin-block: calc((${label.lineHeight}px - var(--sw-control-sm-min-height)) / 2);
117
+ }
118
+
119
+ /* A band across the page's full width, such as an environment strip
120
+ above the app header: square, the xs control height, caption type,
121
+ its words centered on one line that wraps on a phone. A glyph or an
122
+ action stays on the words' row (no wrap): the words wrap instead. */
123
+ .sw-notice-band {
124
+ align-items: center;
125
+ border-radius: 0;
126
+ flex-wrap: nowrap;
127
+ font-size: ${caption.fontSize}px;
128
+ justify-content: center;
129
+ letter-spacing: ${caption.letterSpacing}px;
130
+ line-height: ${caption.lineHeight}px;
131
+ min-height: var(--sw-control-xs-min-height);
132
+ padding: var(--sw-space-1) var(--sw-space-4);
133
+ text-align: center;
134
+ width: 100%;
135
+ }
136
+
137
+ .sw-notice-band .sw-notice-content {
138
+ flex: 0 1 auto;
139
+ justify-content: center;
140
+ }
141
+
142
+ .sw-notice-band .sw-notice-icon,
143
+ .sw-notice-band .sw-notice-action { min-height: ${caption.lineHeight}px; }
144
+
145
+ .sw-notice-band .sw-notice-action { align-self: center; }
146
+
147
+ .sw-notice-band .sw-notice-action > .sw-button {
148
+ margin-block: calc((${caption.lineHeight}px - var(--sw-control-sm-min-height)) / 2);
149
+ }
150
+
151
+ @media (forced-colors: active) {
152
+ .sw-notice { border: 1px solid CanvasText; }
153
+ .sw-notice-band { border-width: 0 0 1px; }
154
+ }`;
155
+ }
156
+ export function noticeClassCatalog() {
157
+ return [
158
+ 'sw-notice',
159
+ ...noticeTones.map((tone) => `sw-notice-${tone}`),
160
+ 'sw-notice-icon',
161
+ 'sw-notice-content',
162
+ 'sw-notice-stacked',
163
+ 'sw-notice-title',
164
+ 'sw-notice-body',
165
+ 'sw-notice-action',
166
+ 'sw-notice-band',
167
+ ];
168
+ }