@scalewing/react 1.31.0 → 1.33.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 (92) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +20 -6
  3. package/dist/components/ActionMenu.d.ts +10 -1
  4. package/dist/components/ActionMenu.js +7 -2
  5. package/dist/components/AppHeader.d.ts +9 -1
  6. package/dist/components/AppHeader.js +6 -2
  7. package/dist/components/Box.d.ts +28 -2
  8. package/dist/components/Box.js +4 -2
  9. package/dist/components/Button.d.ts +27 -2
  10. package/dist/components/Button.js +5 -3
  11. package/dist/components/ButtonGroup.d.ts +9 -1
  12. package/dist/components/ButtonGroup.js +8 -2
  13. package/dist/components/Combobox.js +2 -2
  14. package/dist/components/DateField.d.ts +9 -1
  15. package/dist/components/DateField.js +4 -3
  16. package/dist/components/DenominationGrid.d.ts +5 -3
  17. package/dist/components/DenominationGrid.js +21 -11
  18. package/dist/components/Dialog.d.ts +9 -1
  19. package/dist/components/Dialog.js +8 -4
  20. package/dist/components/Field.d.ts +8 -0
  21. package/dist/components/Field.js +4 -3
  22. package/dist/components/FieldLabelText.d.ts +21 -3
  23. package/dist/components/FieldLabelText.js +16 -4
  24. package/dist/components/FilterChips.d.ts +9 -1
  25. package/dist/components/FilterChips.js +4 -2
  26. package/dist/components/Nav.d.ts +14 -2
  27. package/dist/components/Nav.js +6 -2
  28. package/dist/components/SegmentedControl.d.ts +5 -2
  29. package/dist/components/SegmentedControl.js +2 -1
  30. package/dist/components/Select.d.ts +9 -1
  31. package/dist/components/Select.js +4 -3
  32. package/dist/components/Stepper.d.ts +13 -0
  33. package/dist/components/Stepper.js +8 -2
  34. package/dist/components/combobox/types.d.ts +8 -0
  35. package/dist/components/denomination-grid/DenominationStrip.d.ts +4 -2
  36. package/dist/components/denomination-grid/DenominationStrip.js +3 -2
  37. package/dist/components/denomination-grid/DenominationTiles.d.ts +1 -1
  38. package/dist/components/denomination-grid/EntryCell.js +1 -1
  39. package/dist/components/denomination-grid/StripRow.js +5 -2
  40. package/dist/components/denomination-grid/types.d.ts +35 -1
  41. package/dist/components/dialog/DialogTitle.d.ts +5 -3
  42. package/dist/components/dialog/DialogTitle.js +4 -4
  43. package/dist/css/css-action-menu-trigger.d.ts +11 -0
  44. package/dist/css/css-action-menu-trigger.js +66 -0
  45. package/dist/css/css-action-menu.js +7 -2
  46. package/dist/css/css-app-header-bar.d.ts +10 -0
  47. package/dist/css/css-app-header-bar.js +26 -0
  48. package/dist/css/css-box-border.d.ts +6 -1
  49. package/dist/css/css-box-border.js +8 -4
  50. package/dist/css/css-box-link.d.ts +13 -0
  51. package/dist/css/css-box-link.js +50 -0
  52. package/dist/css/css-button-group-attached.js +5 -2
  53. package/dist/css/css-button-group-track.d.ts +11 -0
  54. package/dist/css/css-button-group-track.js +48 -0
  55. package/dist/css/css-button.d.ts +19 -2
  56. package/dist/css/css-button.js +31 -1
  57. package/dist/css/css-chrome.js +14 -1
  58. package/dist/css/css-combobox.js +2 -1
  59. package/dist/css/css-components.js +3 -0
  60. package/dist/css/css-data.js +3 -2
  61. package/dist/css/css-date-field.js +2 -1
  62. package/dist/css/css-denomination-appearance.d.ts +21 -0
  63. package/dist/css/css-denomination-appearance.js +114 -0
  64. package/dist/css/css-denomination-cell-tones.d.ts +8 -0
  65. package/dist/css/css-denomination-cell-tones.js +20 -1
  66. package/dist/css/css-denomination-grid.js +4 -0
  67. package/dist/css/css-dialog-banded.d.ts +11 -0
  68. package/dist/css/css-dialog-banded.js +84 -0
  69. package/dist/css/css-document.js +3 -0
  70. package/dist/css/css-multi-select.js +2 -1
  71. package/dist/css/css-nav-words.d.ts +13 -0
  72. package/dist/css/css-nav-words.js +51 -0
  73. package/dist/css/css-notice.d.ts +4 -11
  74. package/dist/css/css-notice.js +6 -22
  75. package/dist/css/css-scroll-region.d.ts +7 -3
  76. package/dist/css/css-scroll-region.js +8 -3
  77. package/dist/css/css-segmented-raised.d.ts +3 -0
  78. package/dist/css/css-segmented-raised.js +72 -0
  79. package/dist/css/css-segmented.js +9 -3
  80. package/dist/css/css-select.js +2 -1
  81. package/dist/css/css-split.js +2 -1
  82. package/dist/css/css-stepper.js +7 -0
  83. package/dist/css/quiet-fill.d.ts +7 -0
  84. package/dist/css/quiet-fill.js +7 -0
  85. package/dist/css/stacking.js +2 -1
  86. package/dist/css/stylesheet.js +4 -0
  87. package/dist/css/tone-tint.d.ts +21 -0
  88. package/dist/css/tone-tint.js +23 -0
  89. package/dist/index.d.ts +13 -10
  90. package/dist/index.js +10 -7
  91. package/dist/styles.css +313 -10
  92. package/package.json +1 -1
@@ -1,8 +1,11 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type BadgeTone } from '../../css/css-data.js';
3
+ import { type DenominationGridFrame, type DenominationTotalEmphasis } from '../../css/css-denomination-appearance.js';
4
+ import { type DenominationCellToneAppearance } from '../../css/css-denomination-cell-tones.js';
3
5
  import { type DenominationLabelAppearance } from '../../css/css-denomination-label.js';
4
6
  import { type DenominationLabelWidth } from '../../css/css-denomination-aligned.js';
5
- export type { DenominationLabelWidth };
7
+ import { type StepperLimitAppearance } from '../Stepper.js';
8
+ export type { DenominationCellToneAppearance, DenominationGridFrame, DenominationLabelWidth, DenominationTotalEmphasis, };
6
9
  export type DenominationGridTone = BadgeTone;
7
10
  export type DenominationGridLayout = 'strip' | 'tiles';
8
11
  /**
@@ -36,6 +39,11 @@ export type DenominationGridEntry = {
36
39
  errors?: readonly (string | null)[];
37
40
  /** What an empty box shows, such as "0". */
38
41
  placeholder?: string;
42
+ /**
43
+ * The Steppers' `limitAppearance`: `plain` keeps − at 0 (and + at a cap)
44
+ * in its usual look instead of faded; it is still disabled.
45
+ */
46
+ limitAppearance?: StepperLimitAppearance;
39
47
  };
40
48
  export type DenominationGridRow = {
41
49
  id: string;
@@ -79,6 +87,11 @@ export type DenominationGridProps = {
79
87
  rows: readonly DenominationGridRow[];
80
88
  layout?: DenominationGridLayout;
81
89
  subtotal?: (count: number, column: DenominationGridColumn) => string;
90
+ /**
91
+ * What a zero or empty read-only count shows, `—` by default. An empty
92
+ * string leaves those counts blank, so only the counts that are there
93
+ * stand out (the column header still names each cell).
94
+ */
82
95
  zeroLabel?: string;
83
96
  /**
84
97
  * Names the strip's total column, such as "Total": a column header over
@@ -111,4 +124,25 @@ export type DenominationGridProps = {
111
124
  * total.
112
125
  */
113
126
  labelAppearance?: DenominationLabelAppearance;
127
+ /**
128
+ * `table` frames the strip: its surface inside a hairline with rounded
129
+ * corners, the heading row on the subtle fill, a rule between columns,
130
+ * and the counts at label size, centred under their heads. `none` (the
131
+ * default) leaves open rows on the page's surface. Only the strip has a
132
+ * frame.
133
+ */
134
+ frame?: DenominationGridFrame;
135
+ /**
136
+ * `strong` sets the row totals in bold label type, a neutral row's in the
137
+ * text color and a toned row's in its tone; `default` keeps them in data
138
+ * type in the row's tone (a neutral row's muted).
139
+ */
140
+ totalEmphasis?: DenominationTotalEmphasis;
141
+ /**
142
+ * How a `cellTones` tone shows on a strip's read-only count: `text` (the
143
+ * default) sets the count in the tone; `pill` sets it on a rounded tint of
144
+ * the tone in the tone's ink, such as a difference in danger or a
145
+ * changed count in accent. Entry counts and tiles keep their tone border.
146
+ */
147
+ cellToneAppearance?: DenominationCellToneAppearance;
114
148
  };
@@ -11,12 +11,14 @@ type DialogTitleProps = {
11
11
  /** The close button's accessible name; without it there is no button. */
12
12
  closeLabel?: string;
13
13
  onClose: () => void;
14
+ /** A banded dialog's title is always a row, which draws the band. */
15
+ banded: boolean;
14
16
  };
15
17
  /**
16
18
  * The dialog's title, which names it through `aria-labelledby`. Alone it is
17
19
  * 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.
20
+ * `closeLabel`, or in a banded dialog, it is a row: the icon, the title over
21
+ * its description, and an icon-only ghost close button at the end.
20
22
  */
21
- export declare function DialogTitle({ closeLabel, description, descriptionId, icon, id, level, onClose, title, }: DialogTitleProps): import("react").JSX.Element;
23
+ export declare function DialogTitle({ banded, closeLabel, description, descriptionId, icon, id, level, onClose, title, }: DialogTitleProps): import("react").JSX.Element;
22
24
  export {};
@@ -5,12 +5,12 @@ import { Text } from '../Text.js';
5
5
  /**
6
6
  * The dialog's title, which names it through `aria-labelledby`. Alone it is
7
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
+ * `closeLabel`, or in a banded dialog, it is a row: the icon, the title over
9
+ * its description, and an icon-only ghost close button at the end.
10
10
  */
11
- export function DialogTitle({ closeLabel, description, descriptionId, icon, id, level, onClose, title, }) {
11
+ export function DialogTitle({ banded, closeLabel, description, descriptionId, icon, id, level, onClose, title, }) {
12
12
  const heading = (_jsx(Text, { as: level === 2 ? 'h2' : 'h3', id: id, variant: "title", children: title }));
13
- if (!closeLabel && !icon && !description)
13
+ if (!banded && !closeLabel && !icon && !description)
14
14
  return heading;
15
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] }));
16
16
  }
@@ -0,0 +1,11 @@
1
+ export declare const actionMenuTriggerAppearances: readonly ["framed", "text"];
2
+ /**
3
+ * How an ActionMenu's trigger looks: `framed` (the default) is a small
4
+ * outlined control; `text` is its words in the text color with a chevron
5
+ * after them and no fill or outline until hovered or open, such as a
6
+ * person's name that opens their account menu in a header.
7
+ */
8
+ export type ActionMenuTriggerAppearance = (typeof actionMenuTriggerAppearances)[number];
9
+ export declare const actionMenuTriggerTextClass = "sw-action-menu-trigger-text";
10
+ export declare function cssActionMenuTriggerClasses(): string;
11
+ export declare function actionMenuTriggerClassCatalog(): string[];
@@ -0,0 +1,66 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ import { chevronStroke } from './chevron.js';
3
+ import { quietFill } from './quiet-fill.js';
4
+ import { coarsePointerQuery, touchTarget } from './touch-target.js';
5
+ const label = typographyVariants.label;
6
+ export const actionMenuTriggerAppearances = ['framed', 'text'];
7
+ export const actionMenuTriggerTextClass = 'sw-action-menu-trigger-text';
8
+ const text = `.sw-action-menu-trigger.${actionMenuTriggerTextClass}`;
9
+ /*
10
+ * A text trigger: no fill and a transparent border (kept, so forced colors
11
+ * can draw one), its words in label type at medium weight in the text
12
+ * color, the sm control height (the 44 px target on a coarse pointer), and
13
+ * the shared muted chevron pointing down after them, a space-2 gap apart.
14
+ * The quiet fill shows while the menu is open, so the open state is seen
15
+ * as well as announced, and on hover where the pointer can hover (a tap
16
+ * leaves no fill behind). Two classes, so it outranks the framed
17
+ * trigger's rules; the coarse-pointer target is restated after. Forced
18
+ * colors draw the kept border in the system's border color, the chevron
19
+ * in CanvasText as every chevron, and the open trigger's border in
20
+ * Highlight.
21
+ */
22
+ export function cssActionMenuTriggerClasses() {
23
+ return `${text} {
24
+ align-items: center;
25
+ background: transparent;
26
+ border-color: transparent;
27
+ display: inline-flex;
28
+ font-family: var(--sw-font-sans);
29
+ font-size: ${label.fontSize}px;
30
+ font-weight: 500;
31
+ gap: var(--sw-space-2);
32
+ letter-spacing: ${label.letterSpacing}px;
33
+ line-height: ${label.lineHeight}px;
34
+ min-height: var(--sw-control-sm-min-height);
35
+ padding-inline: var(--sw-space-3) var(--sw-space-2);
36
+ }
37
+
38
+ ${text}::after {
39
+ ${chevronStroke}
40
+ content: '';
41
+ flex: none;
42
+ transform: translateY(-25%) rotate(45deg);
43
+ }
44
+
45
+ ${text}[aria-expanded='true'] {
46
+ background: ${quietFill};
47
+ }
48
+
49
+ @media (hover: hover) {
50
+ ${text}:hover:not(:disabled) {
51
+ background: ${quietFill};
52
+ }
53
+ }
54
+
55
+ @media ${coarsePointerQuery} {
56
+ ${text} { min-height: ${touchTarget}; }
57
+ }
58
+
59
+ @media (forced-colors: active) {
60
+ ${text}::after { border-color: CanvasText; }
61
+ ${text}[aria-expanded='true'] { border-color: Highlight; }
62
+ }`;
63
+ }
64
+ export function actionMenuTriggerClassCatalog() {
65
+ return [actionMenuTriggerTextClass];
66
+ }
@@ -1,7 +1,9 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { chevronStroke } from './chevron.js';
3
+ import { actionMenuTriggerClassCatalog, cssActionMenuTriggerClasses, } from './css-action-menu-trigger.js';
3
4
  import { zIndex } from './stacking.js';
4
5
  import { coarsePointerQuery, touchTarget } from './touch-target.js';
6
+ import { quietFill } from './quiet-fill.js';
5
7
  const caption = typographyVariants.caption;
6
8
  const label = typographyVariants.label;
7
9
  export function cssActionMenuClasses() {
@@ -103,7 +105,7 @@ export function cssActionMenuClasses() {
103
105
 
104
106
  .sw-action-menu-item:hover,
105
107
  .sw-action-menu-item:focus-visible {
106
- background: color-mix(in srgb, var(--sw-color-muted) 16%, transparent);
108
+ background: ${quietFill};
107
109
  }
108
110
 
109
111
  .sw-action-menu-item:disabled {
@@ -162,7 +164,9 @@ export function cssActionMenuClasses() {
162
164
  .sw-action-menu-item {
163
165
  min-height: ${touchTarget};
164
166
  }
165
- }`;
167
+ }
168
+
169
+ ${cssActionMenuTriggerClasses()}`;
166
170
  }
167
171
  export function actionMenuClassCatalog() {
168
172
  return [
@@ -176,5 +180,6 @@ export function actionMenuClassCatalog() {
176
180
  'sw-action-menu-detail',
177
181
  'sw-action-menu-chevron',
178
182
  'sw-action-menu-separator',
183
+ ...actionMenuTriggerClassCatalog(),
179
184
  ];
180
185
  }
@@ -0,0 +1,10 @@
1
+ export declare const appHeaderAppearances: readonly ["glass", "bar"];
2
+ /**
3
+ * How an AppHeader is drawn: `glass` (the default) is a rounded frosted
4
+ * card; `bar` is a full-width band of the solid surface with a hairline
5
+ * under it and no rounding, set against the page's edges.
6
+ */
7
+ export type AppHeaderAppearance = (typeof appHeaderAppearances)[number];
8
+ export declare const appHeaderBarClass = "sw-app-header-bar";
9
+ export declare function cssAppHeaderBarClasses(): string;
10
+ export declare function appHeaderBarClassCatalog(): string[];
@@ -0,0 +1,26 @@
1
+ export const appHeaderAppearances = ['glass', 'bar'];
2
+ export const appHeaderBarClass = 'sw-app-header-bar';
3
+ const bar = `.${appHeaderBarClass}`;
4
+ /*
5
+ * A bar header: the surface with no blur or specular line, no radius, and
6
+ * the border hairline under it only. One class, emitted after the glass
7
+ * header, so it wins by order; the Box border classes are emitted later
8
+ * still, so a Box `border` prop on the header wins over it, as on any
9
+ * Box-based frame. Reduce Transparency sets the same surface. Forced
10
+ * colors keep the hairline.
11
+ */
12
+ export function cssAppHeaderBarClasses() {
13
+ return `${bar} {
14
+ backdrop-filter: none;
15
+ -webkit-backdrop-filter: none;
16
+ background: var(--sw-color-surface);
17
+ border-color: var(--sw-color-border);
18
+ border-radius: 0;
19
+ border-style: solid;
20
+ border-width: 0 0 1px;
21
+ box-shadow: none;
22
+ }`;
23
+ }
24
+ export function appHeaderBarClassCatalog() {
25
+ return [appHeaderBarClass];
26
+ }
@@ -1,4 +1,9 @@
1
- export type BoxBorderValue = boolean | 'dashed';
1
+ /**
2
+ * `true` (solid) or `'dashed'` draws the hairline all round; `'bottom'`
3
+ * draws it under the box only, for a full-width bar such as a header or a
4
+ * section's sub-navigation.
5
+ */
6
+ export type BoxBorderValue = boolean | 'dashed' | 'bottom';
2
7
  /** The generated class for a Box `border` prop, or none for `false`. */
3
8
  export declare function boxBorderClass(border: BoxBorderValue | undefined): string | undefined;
4
9
  export declare function cssBoxBorderClasses(): string;
@@ -6,8 +6,11 @@ function borderClass(style) {
6
6
  export function boxBorderClass(border) {
7
7
  if (!border)
8
8
  return undefined;
9
+ if (border === 'bottom')
10
+ return bottomBorderClass;
9
11
  return borderClass(border === 'dashed' ? 'dashed' : 'solid');
10
12
  }
13
+ const bottomBorderClass = 'sw-border-bottom';
11
14
  /*
12
15
  * A Box `border` is a token hairline. These rules are emitted after every
13
16
  * component rule so that, at equal specificity, the prop still wins over a
@@ -15,10 +18,11 @@ export function boxBorderClass(border) {
15
18
  * forced colors the system border color replaces the token and the style stays.
16
19
  */
17
20
  export function cssBoxBorderClasses() {
18
- return borderStyles
19
- .map((style) => `.${borderClass(style)} { border: 1px ${style} var(--sw-color-border); }`)
20
- .join('\n');
21
+ return [
22
+ ...borderStyles.map((style) => `.${borderClass(style)} { border: 1px ${style} var(--sw-color-border); }`),
23
+ `.${bottomBorderClass} { border-width: 0 0 1px; border-style: solid; border-color: var(--sw-color-border); }`,
24
+ ].join('\n');
21
25
  }
22
26
  export function boxBorderClassCatalog() {
23
- return borderStyles.map(borderClass);
27
+ return [...borderStyles.map(borderClass), bottomBorderClass];
24
28
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * How a `Box as="a"` reads as a link. `accent` (the default) is the
3
+ * canvas link: the accent colour, no underline. `quiet` is a title that
4
+ * opens something, such as the title of a table row: the surrounding
5
+ * text's colour at weight 600, underlined while hovered or focused.
6
+ */
7
+ export declare const boxLinkAppearances: readonly ["accent", "quiet"];
8
+ export type BoxLinkAppearance = (typeof boxLinkAppearances)[number];
9
+ export declare function assertBoxLink(link: unknown, as: string): void;
10
+ /** The generated class for a Box `link` prop, or none for the canvas link. */
11
+ export declare function boxLinkClass(link: BoxLinkAppearance | undefined): string | undefined;
12
+ export declare function cssBoxLinkClasses(): string;
13
+ export declare function boxLinkClassCatalog(): string[];
@@ -0,0 +1,50 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ /**
3
+ * How a `Box as="a"` reads as a link. `accent` (the default) is the
4
+ * canvas link: the accent colour, no underline. `quiet` is a title that
5
+ * opens something, such as the title of a table row: the surrounding
6
+ * text's colour at weight 600, underlined while hovered or focused.
7
+ */
8
+ export const boxLinkAppearances = ['accent', 'quiet'];
9
+ const quietLinkClass = 'sw-link-quiet';
10
+ export function assertBoxLink(link, as) {
11
+ if (link === undefined)
12
+ return;
13
+ if (!boxLinkAppearances.includes(link))
14
+ throw new RangeError(`Unknown Box link: ${String(link)}`);
15
+ if (as !== 'a')
16
+ throw new TypeError('Box link is for a link (as="a")');
17
+ }
18
+ /** The generated class for a Box `link` prop, or none for the canvas link. */
19
+ export function boxLinkClass(link) {
20
+ return link === 'quiet' ? quietLinkClass : undefined;
21
+ }
22
+ /*
23
+ * A quiet link keeps the colour of the text around it, so colour cannot
24
+ * tell it apart. Its weight does: 600, the label weight, in body-weight
25
+ * text, the non-colour cue WCAG 1.4.1 asks for, and one that forced colours
26
+ * keep. (axe's link-in-text-block passes it on colour, the same colour
27
+ * and background as its text, or on its weight; the weight is what keeps it
28
+ * a visible link.) Use it in text lighter
29
+ * than 600, such as a body row's title. In text already at 600 the quiet
30
+ * look has no cue; an accent link there needs at least 3:1 against the text.
31
+ * Hover and keyboard focus underline it; focus keeps the canvas focus ring.
32
+ * The rules outrank the canvas `[data-theme] a` rules, hover included.
33
+ * Forced colours paint it in the system link colour, as any link.
34
+ */
35
+ export function cssBoxLinkClasses() {
36
+ const quiet = `[data-theme] a.${quietLinkClass}`;
37
+ return `${quiet} {
38
+ color: inherit;
39
+ font-weight: ${typographyVariants.label.fontWeight};
40
+ }
41
+
42
+ ${quiet}:hover,
43
+ ${quiet}:focus-visible {
44
+ color: inherit;
45
+ text-decoration-line: underline;
46
+ }`;
47
+ }
48
+ export function boxLinkClassCatalog() {
49
+ return [quietLinkClass];
50
+ }
@@ -1,4 +1,5 @@
1
1
  import { breakpointQuery } from './breakpoints.js';
2
+ import { buttonGroupTrackClassCatalog, cssButtonGroupTrackClasses, } from './css-button-group-track.js';
2
3
  import { joinedZIndex } from './stacking.js';
3
4
  /*
4
5
  * An attached ButtonGroup is one joined control, such as Previous, Today
@@ -63,8 +64,10 @@ ${group} > .sw-button:focus-visible {
63
64
  @media ${breakpointQuery('below', 'md')} {
64
65
  ${group} { align-items: stretch; flex-direction: row; }
65
66
  ${group} > * { width: auto; }
66
- }`;
67
+ }
68
+
69
+ ${cssButtonGroupTrackClasses()}`;
67
70
  }
68
71
  export function buttonGroupAttachedClassCatalog() {
69
- return ['sw-button-group-attached'];
72
+ return ['sw-button-group-attached', ...buttonGroupTrackClassCatalog()];
70
73
  }
@@ -0,0 +1,11 @@
1
+ export declare const buttonGroupAppearances: readonly ["default", "track"];
2
+ /**
3
+ * How an attached ButtonGroup is drawn: `default` keeps each button's own
4
+ * fill and outline, joined; `track` sets the buttons as words on one
5
+ * quiet grey track, a hairline between them, such as Previous, Today and
6
+ * Next above a day.
7
+ */
8
+ export type ButtonGroupAppearance = (typeof buttonGroupAppearances)[number];
9
+ export declare const buttonGroupTrackClass = "sw-button-group-track";
10
+ export declare function cssButtonGroupTrackClasses(): string;
11
+ export declare function buttonGroupTrackClassCatalog(): string[];
@@ -0,0 +1,48 @@
1
+ import { quietFill } from './quiet-fill.js';
2
+ export const buttonGroupAppearances = ['default', 'track'];
3
+ export const buttonGroupTrackClass = 'sw-button-group-track';
4
+ const track = `.sw-button-group.sw-button-group-attached.${buttonGroupTrackClass}`;
5
+ /*
6
+ * The track: the attached group takes the quiet fill and the sm radius
7
+ * (its buttons' outer corners already are), and every button in it loses
8
+ * its own fill and outline to take its words in the text color. The
9
+ * divider between two buttons is a border hairline at full height, after
10
+ * the attached group's tinted divider (one more class, so it wins at the
11
+ * same place); it is the second button's start border, so the one before
12
+ * a disabled button fades with it. The divider is the canvas hairline
13
+ * (about 1.2:1 on the track, as the board draws it; fainter on a dark
14
+ * card). A pressed button keeps its ring and a focused one its outline,
15
+ * over the track. A disabled button fades, `disabledAppearance="neutral"`
16
+ * included (its muted words would read as enabled ones here). Forced
17
+ * colors outline the track, keep every border and draw a pressed button's
18
+ * border in Highlight. A track is for equal-weight navigation: every
19
+ * variant's meaning goes, so keep a primary or destructive action out of it.
20
+ */
21
+ export function cssButtonGroupTrackClasses() {
22
+ return `${track} {
23
+ background: ${quietFill};
24
+ border-radius: var(--sw-radius-sm);
25
+ }
26
+
27
+ ${track} > .sw-button {
28
+ background: transparent;
29
+ border-color: transparent;
30
+ color: var(--sw-color-text);
31
+ }
32
+
33
+ ${track} > .sw-button + .sw-button {
34
+ border-inline-start-color: var(--sw-color-border);
35
+ }
36
+
37
+ ${track} > .sw-button:disabled {
38
+ opacity: var(--sw-disabled-opacity);
39
+ }
40
+
41
+ @media (forced-colors: active) {
42
+ ${track} { outline: 1px solid CanvasText; }
43
+ ${track} > .sw-button[aria-pressed='true'] { border-color: Highlight; }
44
+ }`;
45
+ }
46
+ export function buttonGroupTrackClassCatalog() {
47
+ return [buttonGroupTrackClass];
48
+ }
@@ -1,7 +1,24 @@
1
- import { type ButtonSize, type ButtonVariant } from '@scalewing/tokens';
1
+ import { type ButtonSize } from '@scalewing/tokens';
2
+ /**
3
+ * The web Button's variants: the shared tiers from `@scalewing/tokens`, plus
4
+ * `neutral`, a quiet grey fill in the text color for an action beside the
5
+ * primary that should not draw the eye (a Cancel on a sheet). Native has no
6
+ * `neutral` yet.
7
+ */
8
+ export declare const webButtonVariants: readonly ["primary", "secondary", "tertiary", "ghost", "danger", "neutral"];
9
+ export type WebButtonVariant = (typeof webButtonVariants)[number];
10
+ export declare const buttonDisabledAppearances: readonly ["faded", "neutral"];
11
+ /**
12
+ * How a disabled button looks: `faded` (the default) fades the variant as
13
+ * any disabled control; `neutral` draws a filled button on the quiet grey
14
+ * fill with muted words, whatever its variant (a ghost button, which has
15
+ * no fill, still fades).
16
+ */
17
+ export type ButtonDisabledAppearance = (typeof buttonDisabledAppearances)[number];
2
18
  export declare function buttonClassNames(options: {
3
19
  size: ButtonSize;
4
- variant: ButtonVariant;
20
+ variant: WebButtonVariant;
21
+ disabledAppearance?: ButtonDisabledAppearance;
5
22
  }): string[];
6
23
  export declare const pressedRingGap = 2;
7
24
  export declare const pressedRingWidth = 4;
@@ -1,12 +1,24 @@
1
1
  import { buttonSizes, buttonVariants, controlScale, typographyVariants, } from '@scalewing/tokens';
2
2
  import { breakpointQuery } from './breakpoints.js';
3
3
  import { buttonGroupAttachedClassCatalog, cssButtonGroupAttachedClasses, } from './css-button-group-attached.js';
4
+ import { quietFill } from './quiet-fill.js';
4
5
  const label = typographyVariants.label;
6
+ /**
7
+ * The web Button's variants: the shared tiers from `@scalewing/tokens`, plus
8
+ * `neutral`, a quiet grey fill in the text color for an action beside the
9
+ * primary that should not draw the eye (a Cancel on a sheet). Native has no
10
+ * `neutral` yet.
11
+ */
12
+ export const webButtonVariants = [...buttonVariants, 'neutral'];
13
+ export const buttonDisabledAppearances = ['faded', 'neutral'];
5
14
  export function buttonClassNames(options) {
6
15
  return [
7
16
  'sw-button',
8
17
  `sw-button-${options.variant}`,
9
18
  `sw-button-${options.size}`,
19
+ ...(options.disabledAppearance === 'neutral'
20
+ ? ['sw-button-disabled-neutral']
21
+ : []),
10
22
  ];
11
23
  }
12
24
  function variantRules() {
@@ -16,8 +28,24 @@ function variantRules() {
16
28
  `.sw-button-tertiary { background: var(--sw-color-tertiary); border-color: transparent; color: var(--sw-color-onTertiary); }`,
17
29
  `.sw-button-ghost { background: transparent; border-color: transparent; color: var(--sw-color-accent); }`,
18
30
  `.sw-button-danger { background: var(--sw-color-danger); border-color: transparent; color: var(--sw-color-onDanger); }`,
31
+ `.sw-button-neutral { background: ${quietFill}; border-color: transparent; color: var(--sw-color-text); }`,
19
32
  ].join('\n');
20
33
  }
34
+ /*
35
+ * A neutral disabled button: the quiet fill with its words in the muted
36
+ * color at 48 %, about #AEAEAE on the grey (a disabled control needs no
37
+ * contrast, and the faded words say it cannot be pressed), at full
38
+ * opacity so no accent shows through.
39
+ * Forced colors draw it as any disabled button.
40
+ */
41
+ function disabledNeutralRules() {
42
+ return `.sw-button-disabled-neutral:disabled:not(.sw-button-ghost) {
43
+ background: ${quietFill};
44
+ border-color: transparent;
45
+ color: color-mix(in srgb, var(--sw-color-muted) 48%, transparent);
46
+ opacity: 1;
47
+ }`;
48
+ }
21
49
  /*
22
50
  * A toggle button's pressed state is an accent ring drawn outside the fill,
23
51
  * past a 2 px gap in the page background: the ring meets the background on
@@ -92,6 +120,7 @@ export function cssButtonClasses() {
92
120
  }
93
121
 
94
122
  ${variantRules()}
123
+ ${disabledNeutralRules()}
95
124
  ${pressedRules()}
96
125
  ${sizeRules()}
97
126
  ${groupRules()}
@@ -118,7 +147,8 @@ function groupRules() {
118
147
  export function buttonClassCatalog() {
119
148
  return [
120
149
  'sw-button',
121
- ...buttonVariants.map((variant) => `sw-button-${variant}`),
150
+ ...webButtonVariants.map((variant) => `sw-button-${variant}`),
151
+ 'sw-button-disabled-neutral',
122
152
  ...buttonSizes.map((size) => `sw-button-${size}`),
123
153
  'sw-button-group',
124
154
  ...buttonGroupJustifies.map((name) => `sw-button-group-${name}`),
@@ -1,4 +1,6 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
+ import { appHeaderBarClassCatalog, cssAppHeaderBarClasses, } from './css-app-header-bar.js';
3
+ import { cssNavWordsClasses, navWordsClassCatalog } from './css-nav-words.js';
2
4
  import { zIndex } from './stacking.js';
3
5
  import { coarsePointerQuery, touchTarget } from './touch-target.js';
4
6
  const caption = typographyVariants.caption;
@@ -13,6 +15,8 @@ export function cssChromeClasses() {
13
15
  -webkit-backdrop-filter: blur(var(--sw-glass-blur)) saturate(var(--sw-glass-saturate));
14
16
  }
15
17
 
18
+ ${cssAppHeaderBarClasses()}
19
+
16
20
  .sw-app-header-sticky {
17
21
  position: sticky;
18
22
  top: 0;
@@ -49,6 +53,8 @@ export function cssChromeClasses() {
49
53
  }
50
54
  }
51
55
 
56
+ ${cssNavWordsClasses()}
57
+
52
58
  .sw-field-xs :is(select, input, textarea) {
53
59
  font-size: ${caption.fontSize}px;
54
60
  letter-spacing: ${caption.letterSpacing}px;
@@ -68,5 +74,12 @@ export function cssChromeClasses() {
68
74
  }`;
69
75
  }
70
76
  export function chromeClassCatalog() {
71
- return ['sw-app-header', 'sw-app-header-sticky', 'sw-nav', 'sw-field-xs'];
77
+ return [
78
+ 'sw-app-header',
79
+ 'sw-app-header-sticky',
80
+ ...appHeaderBarClassCatalog(),
81
+ 'sw-nav',
82
+ ...navWordsClassCatalog(),
83
+ 'sw-field-xs',
84
+ ];
72
85
  }
@@ -1,5 +1,6 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { zIndex } from './stacking.js';
3
+ import { quietFill } from './quiet-fill.js';
3
4
  const caption = typographyVariants.caption;
4
5
  const label = typographyVariants.label;
5
6
  /*
@@ -81,7 +82,7 @@ export function cssComboboxClasses() {
81
82
  }
82
83
 
83
84
  .sw-combobox-option[aria-selected='true'] {
84
- background: color-mix(in srgb, var(--sw-color-muted) 16%, transparent);
85
+ background: ${quietFill};
85
86
  }
86
87
 
87
88
  .sw-combobox-option[aria-disabled='true'] {
@@ -13,6 +13,7 @@ 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 { cssDialogBandedClasses } from './css-dialog-banded.js';
16
17
  import { cssDialogFrameClasses } from './css-dialog-frame.js';
17
18
  import { cssComboboxClasses } from './css-combobox.js';
18
19
  import { cssSelectClasses } from './css-select.js';
@@ -144,6 +145,8 @@ ${cssDialogClasses()}
144
145
 
145
146
  ${cssDialogFrameClasses()}
146
147
 
148
+ ${cssDialogBandedClasses()}
149
+
147
150
  ${cssAccordionClasses()}
148
151
 
149
152
  ${cssSelectClasses()}
@@ -1,4 +1,5 @@
1
- import { buttonVariants, typographyVariants } from '@scalewing/tokens';
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ import { webButtonVariants } from './css-button.js';
2
3
  export const badgeTones = [
3
4
  'neutral',
4
5
  'accent',
@@ -20,7 +21,7 @@ export function badgeClassNames(tone, size = 'md') {
20
21
  * button has no fill.
21
22
  */
22
23
  function badgeOnFilledButtonRules() {
23
- const filled = buttonVariants
24
+ const filled = webButtonVariants
24
25
  .filter((variant) => variant !== 'ghost')
25
26
  .map((variant) => `.sw-button-${variant}`)
26
27
  .join(', ');
@@ -1,6 +1,7 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { disabledControlForcedColors, disabledControlSurface, } from './css-document.js';
3
3
  import { zIndex } from './stacking.js';
4
+ import { quietFill } from './quiet-fill.js';
4
5
  const body = typographyVariants.body;
5
6
  const label = typographyVariants.label;
6
7
  const caption = typographyVariants.caption;
@@ -226,7 +227,7 @@ function calendarRules() {
226
227
 
227
228
  .sw-date-field-step:hover:not([aria-disabled='true']),
228
229
  .sw-date-field-day:hover:not([aria-disabled='true']) {
229
- background: color-mix(in srgb, var(--sw-color-muted) 16%, transparent);
230
+ background: ${quietFill};
230
231
  }
231
232
 
232
233
  /*