@scalewing/react 1.42.0 → 1.44.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 (60) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +60 -0
  3. package/dist/components/ActionMenu.d.ts +4 -2
  4. package/dist/components/ActionMenu.js +4 -8
  5. package/dist/components/DescriptionList.d.ts +12 -2
  6. package/dist/components/DescriptionList.js +7 -3
  7. package/dist/components/FileField.d.ts +15 -4
  8. package/dist/components/FileField.js +13 -4
  9. package/dist/components/FilterChips.d.ts +1 -1
  10. package/dist/components/FilterChips.js +1 -1
  11. package/dist/components/FilterChipsRow.d.ts +24 -0
  12. package/dist/components/FilterChipsRow.js +14 -0
  13. package/dist/components/ListRow.d.ts +10 -2
  14. package/dist/components/ListRow.js +5 -4
  15. package/dist/components/action-menu/assert-action-menu.d.ts +13 -0
  16. package/dist/components/action-menu/assert-action-menu.js +21 -0
  17. package/dist/components/description-list-layout.d.ts +10 -0
  18. package/dist/components/description-list-layout.js +13 -0
  19. package/dist/components/file-field/file-field-checks.d.ts +2 -2
  20. package/dist/components/file-field/file-field-checks.js +4 -2
  21. package/dist/components/filter-chips/FilterChip.js +1 -1
  22. package/dist/components/filter-chips/options.d.ts +10 -0
  23. package/dist/components/filter-chips/options.js +8 -0
  24. package/dist/components/filter-chips/validate.d.ts +3 -3
  25. package/dist/components/filter-chips/validate.js +8 -4
  26. package/dist/components/list/ListRowContent.d.ts +5 -2
  27. package/dist/components/list/ListRowContent.js +5 -4
  28. package/dist/components/list/ListRowItem.js +1 -1
  29. package/dist/components/list/row-naming.js +1 -1
  30. package/dist/components/slot.d.ts +9 -0
  31. package/dist/components/slot.js +13 -0
  32. package/dist/css/css-action-menu-trigger.d.ts +6 -2
  33. package/dist/css/css-action-menu-trigger.js +50 -2
  34. package/dist/css/css-action-menu.js +4 -2
  35. package/dist/css/css-components.js +3 -0
  36. package/dist/css/css-description-list.d.ts +1 -0
  37. package/dist/css/css-description-list.js +24 -0
  38. package/dist/css/css-file-field.js +15 -1
  39. package/dist/css/css-filter-chip-tones.d.ts +3 -0
  40. package/dist/css/css-filter-chip-tones.js +28 -0
  41. package/dist/css/css-filter-chips-layout.d.ts +1 -0
  42. package/dist/css/css-filter-chips-layout.js +1 -1
  43. package/dist/css/css-filter-chips-row.d.ts +2 -0
  44. package/dist/css/css-filter-chips-row.js +79 -0
  45. package/dist/css/css-filter-chips.js +8 -0
  46. package/dist/css/css-font-mono.d.ts +3 -0
  47. package/dist/css/css-font-mono.js +23 -0
  48. package/dist/css/css-list-row-truncate.d.ts +2 -0
  49. package/dist/css/css-list-row-truncate.js +29 -0
  50. package/dist/css/css-list.js +5 -1
  51. package/dist/css/css-utilities.js +3 -0
  52. package/dist/css/spacing-classes.d.ts +1 -1
  53. package/dist/css/spacing-classes.js +1 -0
  54. package/dist/css/stylesheet.js +2 -0
  55. package/dist/index.d.ts +6 -4
  56. package/dist/index.js +3 -2
  57. package/dist/styles.css +145 -0
  58. package/package.json +1 -1
  59. package/dist/components/list/row-slot.d.ts +0 -8
  60. package/dist/components/list/row-slot.js +0 -12
@@ -1,5 +1,5 @@
1
1
  import { assertRequiredMark } from '../FieldLabelText.js';
2
- import { filterChipCountAppearances, filterChipsOverflows, } from './options.js';
2
+ import { filterChipCountAppearances, filterChipsOverflows, filterChipTones, } from './options.js';
3
3
  function isCount(count) {
4
4
  return count === undefined || (Number.isInteger(count) && count >= 0);
5
5
  }
@@ -11,9 +11,9 @@ function chosenValues(props) {
11
11
  /**
12
12
  * Fails closed on chips it cannot draw: an empty label, no options, blank
13
13
  * or repeated values, a chosen value that is no option, a count that is
14
- * not a whole number from 0, an unknown count appearance or overflow, and
15
- * a multiple choice asked to be required or given a value that is not a
16
- * list.
14
+ * not a whole number from 0, an unknown chip tone, count appearance or
15
+ * overflow, and a multiple choice asked to be required or given a value
16
+ * that is not a list.
17
17
  */
18
18
  export function assertFilterChips(props) {
19
19
  const { label, options } = props;
@@ -39,6 +39,10 @@ export function assertFilterChips(props) {
39
39
  : 'value must match an option or be empty');
40
40
  if (options.some((option) => !isCount(option.count)))
41
41
  throw new RangeError('count must be a non-negative integer');
42
+ const unknownTone = options.find((option) => option.tone !== undefined &&
43
+ !filterChipTones.includes(option.tone));
44
+ if (unknownTone)
45
+ throw new RangeError(`Unknown FilterChips option tone: ${String(unknownTone.tone)}`);
42
46
  const countAppearance = props.countAppearance ?? 'figure';
43
47
  if (!filterChipCountAppearances.includes(countAppearance))
44
48
  throw new RangeError(`Unknown FilterChips countAppearance: ${String(countAppearance)}`);
@@ -17,10 +17,13 @@ export type ListRowContentProps = {
17
17
  /** The unread words; the dot shows while they are given. */
18
18
  unreadLabel?: string;
19
19
  tag?: ReactNode;
20
+ /** One line each for the title and detail, cut with "…", the words in a tooltip. */
21
+ truncate: boolean;
20
22
  };
21
23
  /**
22
24
  * A list row's parts in reading order: the hidden leading mark, the title
23
25
  * (with the unread dot and its hidden words) over its detail, the value
24
- * over its detail, the tag, then the chevron or check.
26
+ * over its detail, the tag, then the chevron or check. A truncated row
27
+ * sets its title's words in their own box, so the "…" never hides the dot.
25
28
  */
26
- export declare function ListRowContent({ id, title, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, }: ListRowContentProps): import("react").JSX.Element;
29
+ export declare function ListRowContent({ id, title, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, truncate, }: ListRowContentProps): import("react").JSX.Element;
@@ -2,12 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { cx } from '../../class-names.js';
3
3
  import { Badge } from '../Badge.js';
4
4
  import { ListRowAccessoryMark } from './ListRowAccessoryMark.js';
5
- import { hasSlot } from './row-slot.js';
5
+ import { hasSlot } from '../slot.js';
6
6
  /**
7
7
  * A list row's parts in reading order: the hidden leading mark, the title
8
8
  * (with the unread dot and its hidden words) over its detail, the value
9
- * over its detail, the tag, then the chevron or check.
9
+ * over its detail, the tag, then the chevron or check. A truncated row
10
+ * sets its title's words in their own box, so the "…" never hides the dot.
10
11
  */
11
- export function ListRowContent({ id, title, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, }) {
12
- return (_jsxs(_Fragment, { children: [!hasSlot(leading) ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-leading", children: leading })), _jsxs("span", { className: "sw-list-row-label", children: [_jsxs("span", { className: cx('sw-list-row-title', titleAppearance === 'plain' && 'sw-list-row-title-plain', tone === 'danger' && 'sw-list-row-title-danger'), id: `${id}-title`, children: [title, unreadLabel === undefined ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-unread" }))] }), unreadLabel === undefined ? null : (_jsx("span", { className: "sw-sr-only", id: `${id}-unread`, children: unreadLabel })), detail ? (_jsx("span", { className: "sw-list-row-detail", id: `${id}-detail`, children: detail })) : null] }), value === undefined ? null : (_jsxs("span", { className: "sw-list-row-end", children: [_jsx("span", { className: cx('sw-list-row-value', valueTone && `sw-list-row-value-${valueTone}`), id: `${id}-value`, children: valueBadge ? _jsx(Badge, { tone: valueBadge, children: value }) : value }), valueDetail ? (_jsx("span", { className: "sw-list-row-value-detail", id: `${id}-value-detail`, children: valueDetail })) : null] })), !hasSlot(tag) ? null : (_jsx("span", { className: "sw-list-row-tag", id: `${id}-tag`, children: tag })), _jsx(ListRowAccessoryMark, { kind: accessory })] }));
12
+ export function ListRowContent({ id, title, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, truncate, }) {
13
+ return (_jsxs(_Fragment, { children: [!hasSlot(leading) ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-leading", children: leading })), _jsxs("span", { className: "sw-list-row-label", children: [_jsxs("span", { className: cx('sw-list-row-title', titleAppearance === 'plain' && 'sw-list-row-title-plain', tone === 'danger' && 'sw-list-row-title-danger'), id: `${id}-title`, children: [truncate ? (_jsx("span", { className: "sw-list-row-title-text", title: title, children: title })) : (title), unreadLabel === undefined ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-unread" }))] }), unreadLabel === undefined ? null : (_jsx("span", { className: "sw-sr-only", id: `${id}-unread`, children: unreadLabel })), detail ? (_jsx("span", { className: "sw-list-row-detail", id: `${id}-detail`, title: truncate ? detail : undefined, children: detail })) : null] }), value === undefined ? null : (_jsxs("span", { className: "sw-list-row-end", children: [_jsx("span", { className: cx('sw-list-row-value', valueTone && `sw-list-row-value-${valueTone}`), id: `${id}-value`, children: valueBadge ? _jsx(Badge, { tone: valueBadge, children: value }) : value }), valueDetail ? (_jsx("span", { className: "sw-list-row-value-detail", id: `${id}-value-detail`, children: valueDetail })) : null] })), !hasSlot(tag) ? null : (_jsx("span", { className: "sw-list-row-tag", id: `${id}-tag`, children: tag })), _jsx(ListRowAccessoryMark, { kind: accessory })] }));
13
14
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { useContext } from 'react';
3
3
  import { cx } from '../../class-names.js';
4
4
  import { ListRowContainer } from './list-row-container.js';
5
- import { hasSlot } from './row-slot.js';
5
+ import { hasSlot } from '../slot.js';
6
6
  /**
7
7
  * A row's list item: an `li` in a ListGroup's list, a `div` elsewhere. A
8
8
  * row with an `action` holds that control after the row, a sibling of the
@@ -1,4 +1,4 @@
1
- import { hasSlot } from './row-slot.js';
1
+ import { hasSlot } from '../slot.js';
2
2
  function ids(...parts) {
3
3
  const joined = parts.filter(Boolean).join(' ');
4
4
  return joined === '' ? undefined : joined;
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * Whether a component's slot (a row's `leading`, `tag` or `action`, a
4
+ * chips row's `end`) holds something to draw. React renders nothing for
5
+ * `undefined`, `null`, a boolean (the `false` of
6
+ * `canArchive && <ActionMenu …/>`) or an empty string, so those are an
7
+ * empty slot and add no box and no gap.
8
+ */
9
+ export declare function hasSlot(node: ReactNode): boolean;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Whether a component's slot (a row's `leading`, `tag` or `action`, a
3
+ * chips row's `end`) holds something to draw. React renders nothing for
4
+ * `undefined`, `null`, a boolean (the `false` of
5
+ * `canArchive && <ActionMenu …/>`) or an empty string, so those are an
6
+ * empty slot and add no box and no gap.
7
+ */
8
+ export function hasSlot(node) {
9
+ return (node !== undefined &&
10
+ node !== null &&
11
+ node !== '' &&
12
+ typeof node !== 'boolean');
13
+ }
@@ -1,11 +1,15 @@
1
- export declare const actionMenuTriggerAppearances: readonly ["framed", "text"];
1
+ export declare const actionMenuTriggerAppearances: readonly ["framed", "text", "icon"];
2
2
  /**
3
3
  * How an ActionMenu's trigger looks: `framed` (the default) is a small
4
4
  * outlined control; `text` is its words in the text color with a chevron
5
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.
6
+ * person's name that opens their account menu in a header; `icon` is the
7
+ * consumer's glyph alone in a round quiet button, no chevron and no
8
+ * outline until hovered or open, such as a row's ••• menu.
7
9
  */
8
10
  export type ActionMenuTriggerAppearance = (typeof actionMenuTriggerAppearances)[number];
9
11
  export declare const actionMenuTriggerTextClass = "sw-action-menu-trigger-text";
12
+ export declare const actionMenuTriggerIconClass = "sw-action-menu-trigger-icon";
10
13
  export declare function cssActionMenuTriggerClasses(): string;
14
+ export declare function cssActionMenuTriggerIconClasses(): string;
11
15
  export declare function actionMenuTriggerClassCatalog(): string[];
@@ -3,9 +3,11 @@ import { chevronStroke } from './chevron.js';
3
3
  import { quietFill } from './quiet-fill.js';
4
4
  import { coarsePointerQuery, touchTarget } from './touch-target.js';
5
5
  const label = typographyVariants.label;
6
- export const actionMenuTriggerAppearances = ['framed', 'text'];
6
+ export const actionMenuTriggerAppearances = ['framed', 'text', 'icon'];
7
7
  export const actionMenuTriggerTextClass = 'sw-action-menu-trigger-text';
8
+ export const actionMenuTriggerIconClass = 'sw-action-menu-trigger-icon';
8
9
  const text = `.sw-action-menu-trigger.${actionMenuTriggerTextClass}`;
10
+ const icon = `.sw-action-menu-trigger.${actionMenuTriggerIconClass}`;
9
11
  /*
10
12
  * A text trigger: no fill and a transparent border (kept, so forced colors
11
13
  * can draw one), its words in label type at medium weight in the text
@@ -61,6 +63,52 @@ ${text}[aria-expanded='true'] {
61
63
  ${text}[aria-expanded='true'] { border-color: Highlight; }
62
64
  }`;
63
65
  }
66
+ /*
67
+ * An icon trigger: the consumer's glyph alone, centred in a round button
68
+ * as wide as it is tall (the sm control height, the 44 px target on a
69
+ * coarse pointer), in the muted colour, with no fill, no chevron and a
70
+ * transparent border (kept for forced colors). Like the text trigger it
71
+ * takes the quiet fill while open and under a hovering pointer. Its name
72
+ * is the menu's label, so the glyph says nothing on its own.
73
+ */
74
+ export function cssActionMenuTriggerIconClasses() {
75
+ return `${icon} {
76
+ align-items: center;
77
+ background: transparent;
78
+ border-color: transparent;
79
+ border-radius: var(--sw-radius-pill);
80
+ color: var(--sw-color-muted);
81
+ display: inline-flex;
82
+ justify-content: center;
83
+ min-height: var(--sw-control-sm-min-height);
84
+ min-width: var(--sw-control-sm-min-height);
85
+ padding: 0;
86
+ }
87
+
88
+ ${icon}[aria-expanded='true'] {
89
+ background: ${quietFill};
90
+ color: var(--sw-color-text);
91
+ }
92
+
93
+ @media (hover: hover) {
94
+ ${icon}:hover:not(:disabled) {
95
+ background: ${quietFill};
96
+ color: var(--sw-color-text);
97
+ }
98
+ }
99
+
100
+ @media ${coarsePointerQuery} {
101
+ ${icon} {
102
+ min-height: ${touchTarget};
103
+ min-width: ${touchTarget};
104
+ }
105
+ }
106
+
107
+ @media (forced-colors: active) {
108
+ ${icon} { color: ButtonText; }
109
+ ${icon}[aria-expanded='true'] { border-color: Highlight; }
110
+ }`;
111
+ }
64
112
  export function actionMenuTriggerClassCatalog() {
65
- return [actionMenuTriggerTextClass];
113
+ return [actionMenuTriggerTextClass, actionMenuTriggerIconClass];
66
114
  }
@@ -1,6 +1,6 @@
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
+ import { actionMenuTriggerClassCatalog, cssActionMenuTriggerClasses, cssActionMenuTriggerIconClasses, } from './css-action-menu-trigger.js';
4
4
  import { zIndex } from './stacking.js';
5
5
  import { coarsePointerQuery, touchTarget } from './touch-target.js';
6
6
  import { quietFill } from './quiet-fill.js';
@@ -166,7 +166,9 @@ export function cssActionMenuClasses() {
166
166
  }
167
167
  }
168
168
 
169
- ${cssActionMenuTriggerClasses()}`;
169
+ ${cssActionMenuTriggerClasses()}
170
+
171
+ ${cssActionMenuTriggerIconClasses()}`;
170
172
  }
171
173
  export function actionMenuClassCatalog() {
172
174
  return [
@@ -36,6 +36,7 @@ import { cssMultiSelectClasses } from './css-multi-select.js';
36
36
  import { cssSplitClasses } from './css-split.js';
37
37
  import { cssSwitchClasses } from './css-switch.js';
38
38
  import { cssTextClasses } from './css-text.js';
39
+ import { cssFontMonoClasses } from './css-font-mono.js';
39
40
  import { cssSpinnerClasses } from './css-spinner.js';
40
41
  import { cssProgressClasses } from './css-progress.js';
41
42
  import { cssNoticeClasses } from './css-notice.js';
@@ -134,6 +135,8 @@ ${cssPopupHostRules()}
134
135
 
135
136
  ${cssTextClasses()}
136
137
 
138
+ ${cssFontMonoClasses()}
139
+
137
140
  ${cssButtonClasses()}
138
141
 
139
142
  ${cssCheckboxClasses()}
@@ -7,6 +7,7 @@
7
7
  * a term sits over its detail. A term and a detail are columns of lines:
8
8
  * each child is its own line with its own line height, so a caption term
9
9
  * starts level with a caption detail instead of on the list's taller line.
10
+ * `spread` puts each detail at its row's end, beside its term at every width.
10
11
  */
11
12
  export declare function cssDescriptionListClasses(): string;
12
13
  export declare function descriptionListClassCatalog(): string[];
@@ -1,4 +1,24 @@
1
1
  import { breakpointQuery } from './breakpoints.js';
2
+ /*
3
+ * A spread list sets each row on its own two columns, at every width: the
4
+ * term fits its words up to half the row at the start, and the detail
5
+ * takes the rest, its lines end-aligned at the row's end, a space-4 apart
6
+ * (the list's column gap, which a row's own grid does not inherit). The
7
+ * rows still share the hairlines, the padding and the top alignment.
8
+ */
9
+ function spreadRules() {
10
+ return `.sw-description-list-spread { grid-template-columns: minmax(0, 1fr); }
11
+
12
+ .sw-description-list-spread > .sw-description-item {
13
+ column-gap: var(--sw-space-4);
14
+ grid-template-columns: fit-content(50%) minmax(0, 1fr);
15
+ }
16
+
17
+ .sw-description-list-spread .sw-description-detail {
18
+ align-items: flex-end;
19
+ text-align: end;
20
+ }`;
21
+ }
2
22
  /**
3
23
  * The description list: a two-column grid whose items share its columns
4
24
  * (`subgrid`), so every term starts the same column and every detail the
@@ -8,6 +28,7 @@ import { breakpointQuery } from './breakpoints.js';
8
28
  * a term sits over its detail. A term and a detail are columns of lines:
9
29
  * each child is its own line with its own line height, so a caption term
10
30
  * starts level with a caption detail instead of on the list's taller line.
31
+ * `spread` puts each detail at its row's end, beside its term at every width.
11
32
  */
12
33
  export function cssDescriptionListClasses() {
13
34
  return `.sw-description-list {
@@ -49,6 +70,8 @@ export function cssDescriptionListClasses() {
49
70
  .sw-description-list { grid-template-columns: minmax(0, 1fr); }
50
71
  }
51
72
 
73
+ ${spreadRules()}
74
+
52
75
  @media (forced-colors: active) {
53
76
  .sw-description-item + .sw-description-item { border-top-color: CanvasText; }
54
77
  }`;
@@ -56,6 +79,7 @@ export function cssDescriptionListClasses() {
56
79
  export function descriptionListClassCatalog() {
57
80
  return [
58
81
  'sw-description-list',
82
+ 'sw-description-list-spread',
59
83
  'sw-description-item',
60
84
  'sw-description-term',
61
85
  'sw-description-detail',
@@ -106,7 +106,10 @@ function zoneRules() {
106
106
  }
107
107
  /*
108
108
  * The chosen file: the mark, then the name over its detail (at least four
109
- * space-8 wide before the button wraps under it), then the button.
109
+ * space-8 wide before the button wraps under it), then the button. A
110
+ * photo's preview takes the mark's place: a 3:2 thumbnail a space-8 wide
111
+ * and a space-6 tall, cropped to fill, at the small radius, on the subtle
112
+ * fill while it loads.
110
113
  */
111
114
  function fileRules() {
112
115
  return `.sw-file-field-icon {
@@ -116,6 +119,16 @@ function fileRules() {
116
119
  flex: none;
117
120
  }
118
121
 
122
+ .sw-file-field-preview {
123
+ background: var(--sw-color-subtle);
124
+ border-radius: var(--sw-radius-sm);
125
+ display: block;
126
+ flex: none;
127
+ height: var(--sw-space-6);
128
+ object-fit: cover;
129
+ width: var(--sw-space-8);
130
+ }
131
+
119
132
  .sw-file-field-file {
120
133
  display: flex;
121
134
  flex: 1 1 calc(4 * var(--sw-space-8));
@@ -173,6 +186,7 @@ export function fileFieldClassCatalog() {
173
186
  'sw-file-field-choose',
174
187
  'sw-file-field-drop',
175
188
  'sw-file-field-icon',
189
+ 'sw-file-field-preview',
176
190
  'sw-file-field-file',
177
191
  'sw-file-field-name',
178
192
  'sw-file-field-detail',
@@ -0,0 +1,3 @@
1
+ export declare const warningChipUnchosen = ".sw-filter-chip-warning:not(.sw-filter-chip-quiet) .sw-filter-chip-input:not(:checked) + .sw-filter-chip-face";
2
+ export declare function cssFilterChipToneClasses(): string;
3
+ export declare function filterChipToneClassCatalog(): string[];
@@ -0,0 +1,28 @@
1
+ import { toneTintFill, toneTintInk } from './tone-tint.js';
2
+ /*
3
+ * A warning chip, while it is not chosen, is the warning tint with its
4
+ * words and count in the warning ink, as a warning Notice's (4.5:1 on the
5
+ * tint in every palette). It keeps the chips' hairline, so an error's
6
+ * danger edge still shows on it. Chosen, it is the accent like every chip;
7
+ * with nothing to count it is quiet like every chip. Forced colours draw
8
+ * it as every chip, so its words say why it matters. The focus ring sits
9
+ * outside the face (the chips' ring offset), on the page, not on the tint.
10
+ */
11
+ export const warningChipUnchosen = '.sw-filter-chip-warning:not(.sw-filter-chip-quiet) .sw-filter-chip-input:not(:checked) + .sw-filter-chip-face';
12
+ export function cssFilterChipToneClasses() {
13
+ return `${warningChipUnchosen} {
14
+ background: ${toneTintFill('warning')};
15
+ color: ${toneTintInk('warning')};
16
+ }
17
+
18
+ ${warningChipUnchosen} .sw-filter-chip-count {
19
+ color: inherit;
20
+ }
21
+
22
+ @media (forced-colors: active) {
23
+ ${warningChipUnchosen} { background: Canvas; color: CanvasText; }
24
+ }`;
25
+ }
26
+ export function filterChipToneClassCatalog() {
27
+ return ['sw-filter-chip-warning'];
28
+ }
@@ -1,2 +1,3 @@
1
+ export declare const ringReach = "calc(var(--sw-focus-ring-width) + var(--sw-focus-ring-offset))";
1
2
  export declare function cssFilterChipsLayoutClasses(): string;
2
3
  export declare function filterChipsLayoutClassCatalog(): string[];
@@ -13,7 +13,7 @@
13
13
  * scrolled into view by the browser. The scrollbar stays, thin, so a
14
14
  * pointer without a sideways wheel can still reach the last chip.
15
15
  */
16
- const ringReach = 'calc(var(--sw-focus-ring-width) + var(--sw-focus-ring-offset))';
16
+ export const ringReach = 'calc(var(--sw-focus-ring-width) + var(--sw-focus-ring-offset))';
17
17
  export function cssFilterChipsLayoutClasses() {
18
18
  return `.sw-filter-chip-count-inline {
19
19
  font-variant-numeric: tabular-nums;
@@ -0,0 +1,2 @@
1
+ export declare function cssFilterChipsRowClasses(): string;
2
+ export declare function filterChipsRowClassCatalog(): string[];
@@ -0,0 +1,79 @@
1
+ import { ringReach } from './css-filter-chips-layout.js';
2
+ /*
3
+ * A chips row lays several FilterChips groups on one line, a space-2 apart
4
+ * as chips are, their chips level along the bottom (a group may draw its
5
+ * label above; an error under one lifts its chips above the others'). The
6
+ * `end` group sits at the row's end, or at its own line's end when the row
7
+ * wraps.
8
+ *
9
+ * Wrapping, a group is as wide as its chips and wraps them itself when it
10
+ * is wider than the row. Scrolling, every group's chips stay on one line
11
+ * and the row is the one box that scrolls sideways (never the page),
12
+ * padded by the focus ring's reach and pulled back as a scrolling group
13
+ * is; a group's own scroll gives way, so its chips scroll with the rest.
14
+ * A scrolling group is as wide as its chips (min-content: the chips do not
15
+ * wrap), so a drawn label or an error wraps under them instead of widening
16
+ * the group. The scrolling row is positioned, so a group's visually hidden
17
+ * legend (absolutely placed where it would sit, past the edge) stays inside
18
+ * the box it scrolls in rather than widening the page. These rules follow
19
+ * the groups' own layout rules, which they override.
20
+ */
21
+ export function cssFilterChipsRowClasses() {
22
+ const scroll = '.sw-filter-chips-row-scroll';
23
+ return `.sw-filter-chips-row {
24
+ align-items: flex-end;
25
+ display: flex;
26
+ flex-wrap: wrap;
27
+ gap: var(--sw-space-2);
28
+ max-width: 100%;
29
+ min-width: 0;
30
+ }
31
+
32
+ .sw-filter-chips-row-end {
33
+ display: flex;
34
+ margin-inline-start: auto;
35
+ max-width: 100%;
36
+ min-width: 0;
37
+ }
38
+
39
+ ${scroll} {
40
+ flex-wrap: nowrap;
41
+ margin: calc(-1 * ${ringReach});
42
+ overflow-x: auto;
43
+ overscroll-behavior-x: contain;
44
+ padding: ${ringReach};
45
+ position: relative;
46
+ scrollbar-width: thin;
47
+ }
48
+
49
+ ${scroll} .sw-filter-chips,
50
+ ${scroll} > .sw-filter-chips-row-end {
51
+ flex: none;
52
+ max-width: none;
53
+ }
54
+
55
+ ${scroll} .sw-filter-chips { width: min-content; }
56
+
57
+ ${scroll} .sw-filter-chips-options {
58
+ flex-wrap: nowrap;
59
+ margin: 0;
60
+ overflow: visible;
61
+ padding: 0;
62
+ }
63
+
64
+ ${scroll} .sw-filter-chip {
65
+ flex: none;
66
+ max-width: none;
67
+ }
68
+
69
+ ${scroll} .sw-filter-chip-face {
70
+ white-space: nowrap;
71
+ }`;
72
+ }
73
+ export function filterChipsRowClassCatalog() {
74
+ return [
75
+ 'sw-filter-chips-row',
76
+ 'sw-filter-chips-row-end',
77
+ 'sw-filter-chips-row-scroll',
78
+ ];
79
+ }
@@ -1,5 +1,7 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
+ import { cssFilterChipToneClasses, filterChipToneClassCatalog, } from './css-filter-chip-tones.js';
2
3
  import { cssFilterChipsLayoutClasses, filterChipsLayoutClassCatalog, } from './css-filter-chips-layout.js';
4
+ import { cssFilterChipsRowClasses, filterChipsRowClassCatalog, } from './css-filter-chips-row.js';
3
5
  const label = typographyVariants.label;
4
6
  const data = typographyVariants.data;
5
7
  const caption = typographyVariants.caption;
@@ -126,6 +128,10 @@ export function cssFilterChipsClasses() {
126
128
 
127
129
  ${cssFilterChipsLayoutClasses()}
128
130
 
131
+ ${cssFilterChipToneClasses()}
132
+
133
+ ${cssFilterChipsRowClasses()}
134
+
129
135
  @media (forced-colors: active) {
130
136
  .sw-filter-chip-face { background: Canvas; border-color: CanvasText; color: CanvasText; }
131
137
  .sw-filter-chips-error { color: Mark; }
@@ -148,5 +154,7 @@ export function filterChipsClassCatalog() {
148
154
  'sw-filter-chip-quiet',
149
155
  'sw-filter-chips-error',
150
156
  ...filterChipsLayoutClassCatalog(),
157
+ ...filterChipToneClassCatalog(),
158
+ ...filterChipsRowClassCatalog(),
151
159
  ];
152
160
  }
@@ -0,0 +1,3 @@
1
+ export declare const monospaceFamilies = "ui-monospace, monospace";
2
+ export declare function cssFontMonoClasses(): string;
3
+ export declare function fontMonoClassCatalog(): string[];
@@ -0,0 +1,23 @@
1
+ import { typedInputs } from './css-document.js';
2
+ /*
3
+ * The platform's monospace face by its generic CSS names only, as the
4
+ * forced-colour rules name system colours: no face is chosen here, so
5
+ * there is no type value for the tokens to own. Each platform draws its
6
+ * own (SF Mono, Cascadia, Menlo …).
7
+ */
8
+ export const monospaceFamilies = 'ui-monospace, monospace';
9
+ /*
10
+ * Monospace text, for words read as typed code or marks (a Markdown source
11
+ * being written), at the size the text already has. It outranks the
12
+ * canvas's text controls, which take the page's family, so a `textarea` or
13
+ * a text input can take it too.
14
+ */
15
+ export function cssFontMonoClasses() {
16
+ return `[data-theme] .sw-font-mono,
17
+ [data-theme] :is(${typedInputs}).sw-font-mono {
18
+ font-family: ${monospaceFamilies};
19
+ }`;
20
+ }
21
+ export function fontMonoClassCatalog() {
22
+ return ['sw-font-mono'];
23
+ }
@@ -0,0 +1,2 @@
1
+ export declare function cssListRowTruncateClasses(): string;
2
+ export declare function listRowTruncateClassCatalog(): string[];
@@ -0,0 +1,29 @@
1
+ /*
2
+ * A truncated row keeps its title and its detail to one line each, cut
3
+ * with "…" where they do not fit. The title's words sit in their own box
4
+ * (`sw-list-row-title-text`) beside the unread dot, so the cut never hides
5
+ * the dot; the title is a one-line flex row centring them. The label
6
+ * column already shrinks (`min-width: 0`), so the value, the tag and the
7
+ * action keep their place and their words whole.
8
+ */
9
+ export function cssListRowTruncateClasses() {
10
+ return `.sw-list-row-truncate .sw-list-row-title {
11
+ align-items: center;
12
+ display: flex;
13
+ min-width: 0;
14
+ }
15
+
16
+ .sw-list-row-truncate .sw-list-row-unread { flex: none; }
17
+
18
+ .sw-list-row-truncate .sw-list-row-title-text,
19
+ .sw-list-row-truncate .sw-list-row-detail {
20
+ min-width: 0;
21
+ overflow: hidden;
22
+ overflow-wrap: normal;
23
+ text-overflow: ellipsis;
24
+ white-space: nowrap;
25
+ }`;
26
+ }
27
+ export function listRowTruncateClassCatalog() {
28
+ return ['sw-list-row-truncate', 'sw-list-row-title-text'];
29
+ }
@@ -1,6 +1,7 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { cssListRowClasses, listRowClassCatalog } from './css-list-row.js';
3
3
  import { cssListRowTrailingClasses, listRowTrailingClassCatalog, } from './css-list-row-trailing.js';
4
+ import { cssListRowTruncateClasses, listRowTruncateClassCatalog, } from './css-list-row-truncate.js';
4
5
  import { blockEdgeShadeRules, scrollRegionRules } from './css-scroll-region.js';
5
6
  const label = typographyVariants.label;
6
7
  const caption = typographyVariants.caption;
@@ -72,7 +73,9 @@ ${boundedRules()}
72
73
 
73
74
  ${cssListRowClasses()}
74
75
 
75
- ${cssListRowTrailingClasses()}`;
76
+ ${cssListRowTrailingClasses()}
77
+
78
+ ${cssListRowTruncateClasses()}`;
76
79
  }
77
80
  export function listClassCatalog() {
78
81
  return [
@@ -85,5 +88,6 @@ export function listClassCatalog() {
85
88
  'sw-list-group-footer',
86
89
  ...listRowClassCatalog(),
87
90
  ...listRowTrailingClassCatalog(),
91
+ ...listRowTruncateClassCatalog(),
88
92
  ];
89
93
  }
@@ -41,6 +41,9 @@ const layoutRules = {
41
41
  'sw-grow': '.sw-grow { flex-grow: 1; }',
42
42
  'sw-full-width': '.sw-full-width { width: 100%; }',
43
43
  'sw-container': '.sw-container { width: 100%; max-width: var(--sw-container-max); margin-inline: auto; }',
44
+ // A narrower centred column at the reading width (Article's), for a
45
+ // form or text that reads better than it would across the container.
46
+ 'sw-container-reading': '.sw-container-reading { width: 100%; max-width: var(--sw-reading-max); margin-inline: auto; }',
44
47
  'sw-sr-only': `.sw-sr-only { ${visuallyHiddenDeclarations} }`,
45
48
  'sw-align-start': '.sw-align-start { align-items: flex-start; }',
46
49
  'sw-align-center': '.sw-align-center { align-items: center; }',
@@ -7,4 +7,4 @@ export type SpacingKind = 'padding' | 'gap';
7
7
  export declare function spacingClass(kind: SpacingKind, axis: PaddingAxis | GapAxis, step: SpacingStep): string;
8
8
  export declare function paddingClassNames(): string[];
9
9
  export declare function gapClassNames(): string[];
10
- export declare const layoutClassNames: readonly ["sw-stack", "sw-inline", "sw-wrap", "sw-grow", "sw-full-width", "sw-container", "sw-sr-only", "sw-align-start", "sw-align-center", "sw-align-end", "sw-align-stretch", "sw-justify-start", "sw-justify-center", "sw-justify-end", "sw-justify-between"];
10
+ export declare const layoutClassNames: readonly ["sw-stack", "sw-inline", "sw-wrap", "sw-grow", "sw-full-width", "sw-container", "sw-container-reading", "sw-sr-only", "sw-align-start", "sw-align-center", "sw-align-end", "sw-align-stretch", "sw-justify-start", "sw-justify-center", "sw-justify-end", "sw-justify-between"];
@@ -28,6 +28,7 @@ export const layoutClassNames = [
28
28
  'sw-grow',
29
29
  'sw-full-width',
30
30
  'sw-container',
31
+ 'sw-container-reading',
31
32
  'sw-sr-only',
32
33
  'sw-align-start',
33
34
  'sw-align-center',
@@ -27,6 +27,7 @@ import { descriptionListClassCatalog } from './css-description-list.js';
27
27
  import { tabsClassCatalog } from './css-tabs.js';
28
28
  import { responsiveClassCatalog } from './css-responsive.js';
29
29
  import { filterChipsClassCatalog } from './css-filter-chips.js';
30
+ import { fontMonoClassCatalog } from './css-font-mono.js';
30
31
  import { gridClassCatalog } from './css-grid.js';
31
32
  import { gridSpanClassCatalog } from './css-grid-span.js';
32
33
  import { denominationGridClassCatalog } from './css-denomination-grid.js';
@@ -126,6 +127,7 @@ export function utilityClassCatalog() {
126
127
  ...descriptionListClassCatalog(),
127
128
  ...tabsClassCatalog(),
128
129
  ...filterChipsClassCatalog(),
130
+ ...fontMonoClassCatalog(),
129
131
  ...gridClassCatalog(),
130
132
  ...gridSpanClassCatalog(),
131
133
  ...denominationGridClassCatalog(),