@scalewing/react 1.60.0 → 1.61.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 (62) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/components/AppShell.d.ts +5 -3
  3. package/dist/components/AppShell.js +9 -4
  4. package/dist/components/DescriptionList.d.ts +11 -3
  5. package/dist/components/DescriptionList.js +5 -4
  6. package/dist/components/FilterChips.d.ts +1 -1
  7. package/dist/components/FilterChips.js +3 -3
  8. package/dist/components/FixtureGroup.d.ts +7 -3
  9. package/dist/components/FixtureGroup.js +11 -4
  10. package/dist/components/Grid.d.ts +11 -3
  11. package/dist/components/Grid.js +24 -6
  12. package/dist/components/PageHeader.d.ts +40 -0
  13. package/dist/components/PageHeader.js +36 -0
  14. package/dist/components/Tabs.d.ts +19 -0
  15. package/dist/components/Tabs.js +13 -3
  16. package/dist/components/Text.d.ts +20 -0
  17. package/dist/components/Text.js +9 -2
  18. package/dist/components/app-shell/types.d.ts +15 -0
  19. package/dist/components/app-shell/validate.d.ts +3 -3
  20. package/dist/components/app-shell/validate.js +9 -3
  21. package/dist/components/description-list-layout.d.ts +9 -0
  22. package/dist/components/description-list-layout.js +15 -0
  23. package/dist/components/filter-chips/options.d.ts +10 -1
  24. package/dist/components/filter-chips/options.js +2 -0
  25. package/dist/components/filter-chips/validate.d.ts +2 -2
  26. package/dist/components/filter-chips/validate.js +6 -3
  27. package/dist/components/fixture/types.d.ts +10 -0
  28. package/dist/css/breakpoints.d.ts +2 -1
  29. package/dist/css/breakpoints.js +2 -1
  30. package/dist/css/css-app-shell.d.ts +6 -0
  31. package/dist/css/css-app-shell.js +20 -1
  32. package/dist/css/css-button.js +9 -11
  33. package/dist/css/css-components.js +6 -0
  34. package/dist/css/css-density.d.ts +9 -0
  35. package/dist/css/css-density.js +95 -0
  36. package/dist/css/css-description-list.js +21 -0
  37. package/dist/css/css-filter-chips.d.ts +8 -0
  38. package/dist/css/css-filter-chips.js +35 -1
  39. package/dist/css/css-fixture-list.d.ts +10 -0
  40. package/dist/css/css-fixture-list.js +38 -0
  41. package/dist/css/css-fixture.js +4 -1
  42. package/dist/css/css-glyph-tile.js +6 -2
  43. package/dist/css/css-grid-span.js +8 -3
  44. package/dist/css/css-grid.d.ts +8 -1
  45. package/dist/css/css-grid.js +21 -2
  46. package/dist/css/css-page-header.d.ts +2 -0
  47. package/dist/css/css-page-header.js +106 -0
  48. package/dist/css/css-table-bounded.js +3 -2
  49. package/dist/css/css-table-metrics.d.ts +5 -0
  50. package/dist/css/css-table-metrics.js +5 -0
  51. package/dist/css/css-tabs.js +8 -0
  52. package/dist/css/css-text.d.ts +5 -0
  53. package/dist/css/css-text.js +26 -1
  54. package/dist/css/stylesheet.js +4 -0
  55. package/dist/css/touch-target.d.ts +1 -0
  56. package/dist/css/touch-target.js +4 -0
  57. package/dist/index.d.ts +12 -9
  58. package/dist/index.js +8 -6
  59. package/dist/styles.css +186 -7
  60. package/dist/theme/ThemeProvider.d.ts +13 -1
  61. package/dist/theme/ThemeProvider.js +7 -2
  62. package/package.json +2 -2
@@ -8,3 +8,12 @@ export declare const descriptionListLayouts: readonly ["columns", "spread"];
8
8
  export type DescriptionListLayout = (typeof descriptionListLayouts)[number];
9
9
  /** Fails closed on a layout the list cannot draw. */
10
10
  export declare function assertDescriptionListLayout(layout: unknown): void;
11
+ /**
12
+ * A fixed term column, so details line up across several lists on a page:
13
+ * `sm` 8rem, `md` 12rem, `lg` 16rem, from `md` up and never more than half
14
+ * the list. Only with the `columns` layout.
15
+ */
16
+ export declare const descriptionListTermWidths: readonly ["sm", "md", "lg"];
17
+ export type DescriptionListTermWidth = (typeof descriptionListTermWidths)[number];
18
+ /** Fails closed on a term width the list cannot draw. */
19
+ export declare function assertDescriptionListTermWidth(termWidth: unknown, layout: DescriptionListLayout): void;
@@ -11,3 +11,18 @@ export function assertDescriptionListLayout(layout) {
11
11
  if (!isOneOf(descriptionListLayouts, layout))
12
12
  throw new RangeError(`Unknown DescriptionList layout: ${String(layout)}`);
13
13
  }
14
+ /**
15
+ * A fixed term column, so details line up across several lists on a page:
16
+ * `sm` 8rem, `md` 12rem, `lg` 16rem, from `md` up and never more than half
17
+ * the list. Only with the `columns` layout.
18
+ */
19
+ export const descriptionListTermWidths = ['sm', 'md', 'lg'];
20
+ /** Fails closed on a term width the list cannot draw. */
21
+ export function assertDescriptionListTermWidth(termWidth, layout) {
22
+ if (termWidth === undefined)
23
+ return;
24
+ if (!isOneOf(descriptionListTermWidths, termWidth))
25
+ throw new RangeError(`Unknown DescriptionList termWidth: ${String(termWidth)}`);
26
+ if (layout !== 'columns')
27
+ throw new RangeError('DescriptionList termWidth needs the columns layout');
28
+ }
@@ -1,4 +1,6 @@
1
+ import { filterChipsSelectedStyles, type FilterChipsSelectedStyle } from '../../css/css-filter-chips.js';
1
2
  import { type RequiredMark } from '../FieldLabelText.js';
3
+ export { filterChipsSelectedStyles, type FilterChipsSelectedStyle };
2
4
  /**
3
5
  * A chip's colour while it is not chosen: `neutral` (the default), the
4
6
  * glass chip; `warning`, the warning tint with its words in the warning
@@ -72,6 +74,14 @@ export type FilterChipsProps = FilterChipsChoice & {
72
74
  requiredMark?: RequiredMark;
73
75
  countAppearance?: FilterChipCountAppearance;
74
76
  overflow?: FilterChipsOverflow;
77
+ /**
78
+ * How a chosen chip looks: `solid` (the default) is the accent fill;
79
+ * `subtle` is the accent's quiet tint with the accent's ink inside an accent
80
+ * edge (4.5:1 in every palette), for a page where a chosen filter should
81
+ * not outweigh the page's primary action. Forced colours draw either in
82
+ * Highlight.
83
+ */
84
+ selectedStyle?: FilterChipsSelectedStyle;
75
85
  /**
76
86
  * A message under the chips, such as "Pick a reason": a polite live
77
87
  * region linked to the group by `aria-describedby`; the group is marked
@@ -82,4 +92,3 @@ export type FilterChipsProps = FilterChipsChoice & {
82
92
  /** More ids that describe the group, such as a hint beside it. */
83
93
  'aria-describedby'?: string;
84
94
  };
85
- export {};
@@ -1,3 +1,5 @@
1
+ import { filterChipsSelectedStyles, } from '../../css/css-filter-chips.js';
2
+ export { filterChipsSelectedStyles };
1
3
  /**
2
4
  * A chip's colour while it is not chosen: `neutral` (the default), the
3
5
  * glass chip; `warning`, the warning tint with its words in the warning
@@ -2,8 +2,8 @@ import { type FilterChipsProps } from './options.js';
2
2
  /**
3
3
  * Fails closed on chips it cannot draw: an empty label, no options, blank
4
4
  * or repeated values, a chosen value that is no option, a count that is
5
- * not a whole number from 0, an unknown chip tone, count appearance or
6
- * overflow, and a multiple choice asked to be required or given a value
5
+ * not a whole number from 0, an unknown chip tone, count appearance,
6
+ * overflow or selected style, and a multiple choice asked to be required or given a value
7
7
  * that is not a list.
8
8
  */
9
9
  export declare function assertFilterChips(props: FilterChipsProps): void;
@@ -1,5 +1,5 @@
1
1
  import { assertRequiredMark } from '../FieldLabelText.js';
2
- import { filterChipCountAppearances, filterChipsOverflows, filterChipTones, } from './options.js';
2
+ import { filterChipCountAppearances, filterChipsOverflows, filterChipsSelectedStyles, filterChipTones, } from './options.js';
3
3
  function isCount(count) {
4
4
  return count === undefined || (Number.isInteger(count) && count >= 0);
5
5
  }
@@ -11,8 +11,8 @@ 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 chip tone, count appearance or
15
- * overflow, and a multiple choice asked to be required or given a value
14
+ * not a whole number from 0, an unknown chip tone, count appearance,
15
+ * overflow or selected style, and a multiple choice asked to be required or given a value
16
16
  * that is not a list.
17
17
  */
18
18
  export function assertFilterChips(props) {
@@ -49,4 +49,7 @@ export function assertFilterChips(props) {
49
49
  const overflow = props.overflow ?? 'wrap';
50
50
  if (!filterChipsOverflows.includes(overflow))
51
51
  throw new RangeError(`Unknown FilterChips overflow: ${String(overflow)}`);
52
+ const selectedStyle = props.selectedStyle ?? 'solid';
53
+ if (!filterChipsSelectedStyles.includes(selectedStyle))
54
+ throw new RangeError(`Unknown FilterChips selectedStyle: ${String(selectedStyle)}`);
52
55
  }
@@ -2,6 +2,7 @@ import { type MouseEvent, type ReactNode, type Ref } from 'react';
2
2
  import { type VisibilityBreakpoint } from '../../css/breakpoints.js';
3
3
  import { type BadgeTone } from '../../css/css-data.js';
4
4
  import { type FixtureCenterKind, type FixtureFlagTone } from '../../css/css-fixture.js';
5
+ import { type FixtureGroupSides } from '../../css/css-fixture-list.js';
5
6
  /** One side of a fixture: its name and an optional decorative mark (a crest). */
6
7
  export type FixtureSide = {
7
8
  name: string;
@@ -146,6 +147,15 @@ export type FixtureGroupProps = {
146
147
  * the game and its day. Default `false`.
147
148
  */
148
149
  titleHidden?: boolean;
150
+ /**
151
+ * `fluid` (the default): the sides share the row, so the centre moves with
152
+ * the run's width. `fixed`: from `md` the sides are at most 16rem each and
153
+ * the end column takes the rest, its details and action at the row's end,
154
+ * so the score sits at the same place in every run of the same width on a
155
+ * page (a run with a `select` starts one box later). Below `md` both are
156
+ * the same.
157
+ */
158
+ sides?: FixtureGroupSides;
149
159
  /** The run's FixtureRows. */
150
160
  children: ReactNode;
151
161
  };
@@ -7,7 +7,8 @@ export declare const breakpoints: Breakpoint[];
7
7
  /**
8
8
  * The breakpoints `hideBelow` and `hideFrom` take: `md`, and `lg` (64rem)
9
9
  * for content that fits only from a laptop up, such as a row of labelled
10
- * destinations a tablet shows as glyphs. The layout props (`columnsBelow`,
10
+ * destinations a tablet shows as glyphs, and `Grid rail` takes them for a rail
11
+ * that needs a laptop's room. The other layout props (`columnsBelow`,
11
12
  * `stickyBelow`, `verticalFrom`, …) keep the one layout breakpoint.
12
13
  */
13
14
  export declare const visibilityBreakpointScale: {
@@ -6,7 +6,8 @@ export const breakpoints = Object.keys(breakpointScale);
6
6
  /**
7
7
  * The breakpoints `hideBelow` and `hideFrom` take: `md`, and `lg` (64rem)
8
8
  * for content that fits only from a laptop up, such as a row of labelled
9
- * destinations a tablet shows as glyphs. The layout props (`columnsBelow`,
9
+ * destinations a tablet shows as glyphs, and `Grid rail` takes them for a rail
10
+ * that needs a laptop's room. The other layout props (`columnsBelow`,
10
11
  * `stickyBelow`, `verticalFrom`, …) keep the one layout breakpoint.
11
12
  */
12
13
  export const visibilityBreakpointScale = {
@@ -1,3 +1,9 @@
1
+ /** `container` centres the page beside the sidebar at the container width. */
2
+ export declare const appShellContentMaxWidths: readonly ["none", "container"];
3
+ export type AppShellContentMaxWidth = (typeof appShellContentMaxWidths)[number];
4
+ /** The sidebar's and the drawer's fill. */
5
+ export declare const appShellSidebarTones: readonly ["surface", "subtle"];
6
+ export type AppShellSidebarTone = (typeof appShellSidebarTones)[number];
1
7
  /** From this width up AppShell draws its full sidebar; script reads it too. */
2
8
  export declare const appShellWideQuery = "(min-width: 75rem)";
3
9
  export declare function cssAppShellClasses(): string;
@@ -4,6 +4,10 @@ import { appShellNavClassCatalog, cssAppShellNavClasses, cssAppShellRailNavRules
4
4
  import { appShellPageBarClassCatalog, cssAppShellPageBarClasses, } from './css-app-shell-page-bar.js';
5
5
  import { appShellSkipLinkClassCatalog, cssAppShellSkipLinkRules, } from './css-app-shell-skip-link.js';
6
6
  import { iconButtonRules } from './css-icon-button.js';
7
+ /** `container` centres the page beside the sidebar at the container width. */
8
+ export const appShellContentMaxWidths = ['none', 'container'];
9
+ /** The sidebar's and the drawer's fill. */
10
+ export const appShellSidebarTones = ['surface', 'subtle'];
7
11
  /*
8
12
  * The width from which AppShell's sidebar is full rather than a rail: an
9
13
  * internal media query of the component, not a public breakpoint (the
@@ -109,6 +113,18 @@ ${cssAppShellRailNavRules(' ')}
109
113
  }
110
114
  }`;
111
115
  }
116
+ /*
117
+ * `sidebarTone="subtle"`: the sidebar and the drawer on the subtle fill, a
118
+ * class more than their own surface rules. The words keep 4.5:1 there in
119
+ * every palette: the text colour, a group's muted caption, and the text
120
+ * colour on the current link's fill over the grey (tested in every theme).
121
+ * Forced colours replace the fill.
122
+ */
123
+ function sidebarToneRules() {
124
+ return `.sw-app-shell-sidebar-subtle :is(.sw-app-shell-sidebar, .sw-app-shell-drawer) {
125
+ background: var(--sw-color-subtle);
126
+ }`;
127
+ }
112
128
  /*
113
129
  * AppShell: a workspace frame with a pinned side navigation (see
114
130
  * components/AppShell.tsx). The links, groups and the rail's nav are
@@ -137,7 +153,9 @@ ${sidebarRules()}
137
153
 
138
154
  ${cssAppShellDrawerClasses()}
139
155
 
140
- ${cssAppShellSkipLinkRules()}`;
156
+ ${cssAppShellSkipLinkRules()}
157
+
158
+ ${sidebarToneRules()}`;
141
159
  }
142
160
  export function appShellClassCatalog() {
143
161
  return [
@@ -148,6 +166,7 @@ export function appShellClassCatalog() {
148
166
  'sw-app-shell-bar-end',
149
167
  'sw-app-shell-main',
150
168
  'sw-app-shell-sidebar',
169
+ 'sw-app-shell-sidebar-subtle',
151
170
  'sw-app-shell-slot',
152
171
  'sw-app-shell-rail-slot',
153
172
  'sw-app-shell-header',
@@ -1,4 +1,4 @@
1
- import { buttonSizes, buttonVariants, controlScale, typographyVariants, } from '@scalewing/tokens';
1
+ import { buttonSizes, buttonVariants, typographyVariants, } from '@scalewing/tokens';
2
2
  import { breakpointQuery } from './breakpoints.js';
3
3
  import { buttonGroupAttachedClassCatalog, cssButtonGroupAttachedClasses, } from './css-button-group-attached.js';
4
4
  import { buttonWidthClassCatalog, cssButtonWidthClasses, } from './css-button-width.js';
@@ -84,19 +84,17 @@ function ariaDisabledForcedRules() {
84
84
  }`;
85
85
  }
86
86
  /*
87
- * Each size's height and side padding for a fine pointer, then the
88
- * comfortable touch target (44 px both ways, the md control height) for
89
- * every size on a coarse pointer, so an xs or sm Button in a table row or a
90
- * checklist is still easy to tap. Its look stays the size's (padding and
91
- * type); only the minimum box grows. The coarse rule follows the sizes,
92
- * which it outranks only by order.
87
+ * Each size's height and side padding for a fine pointer, from the control
88
+ * variables (which the compact density restates), then the comfortable
89
+ * touch target (44 px both ways, the md control height) for every size on a
90
+ * coarse pointer, so an xs or sm Button in a table row or a checklist is
91
+ * still easy to tap. Its look stays the size's (padding and type); only the
92
+ * minimum box grows. The coarse rule follows the sizes, which it outranks
93
+ * only by order.
93
94
  */
94
95
  function sizeRules() {
95
96
  const sizes = buttonSizes
96
- .map((size) => {
97
- const control = controlScale[size];
98
- return `.sw-button-${size} { min-height: ${control.minHeight}px; padding-inline: ${control.paddingInline}px; }`;
99
- })
97
+ .map((size) => `.sw-button-${size} { min-height: var(--sw-control-${size}-min-height); padding-inline: var(--sw-control-${size}-padding-inline); }`)
100
98
  .join('\n');
101
99
  return `${sizes}
102
100
  @media ${coarsePointerQuery} {
@@ -57,6 +57,7 @@ import { cssSeparatorClasses } from './css-separator.js';
57
57
  import { cssBandClasses } from './css-band.js';
58
58
  import { cssScreenPreviewClasses } from './css-screen-preview.js';
59
59
  import { cssHeroClasses } from './css-hero.js';
60
+ import { cssPageHeaderClasses } from './css-page-header.js';
60
61
  import { cssSiteFooterClasses } from './css-site-footer.js';
61
62
  import { cssSiteHeaderClasses } from './css-site-header.js';
62
63
  import { cssStatTileClasses } from './css-stat-tile.js';
@@ -74,6 +75,7 @@ import { cssTaskListClasses } from './css-task-list.js';
74
75
  import { cssToastClasses } from './css-toast.js';
75
76
  import { cssToastCardClasses } from './css-toast-card.js';
76
77
  import { cssBoxBorderClasses } from './css-box-border.js';
78
+ import { cssDensityClasses } from './css-density.js';
77
79
  import { cssPopupHostRules } from './stacking.js';
78
80
  /*
79
81
  * Reduce Transparency swaps every glass surface for the solid surface. It is
@@ -215,6 +217,8 @@ ${cssScreenPreviewClasses()}
215
217
 
216
218
  ${cssHeroClasses()}
217
219
 
220
+ ${cssPageHeaderClasses()}
221
+
218
222
  ${cssSiteFooterClasses()}
219
223
 
220
224
  ${cssSiteHeaderClasses()}
@@ -298,5 +302,7 @@ ${cssResponsiveClasses()}
298
302
 
299
303
  ${cssBoxBorderClasses()}
300
304
 
305
+ ${cssDensityClasses()}
306
+
301
307
  ${reducedTransparencyRules()}`;
302
308
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * How tightly the themed root draws its controls: `comfortable` (the
3
+ * default) is the touch-sized scale; `compact` (opt-in, on a fine pointer
4
+ * only) is a desktop data tool's smaller controls and reading text.
5
+ */
6
+ export declare const themeDensities: readonly ["comfortable", "compact"];
7
+ export type ThemeDensity = (typeof themeDensities)[number];
8
+ export declare function assertThemeDensity(density: unknown): asserts density is ThemeDensity;
9
+ export declare function cssDensityClasses(): string;
@@ -0,0 +1,95 @@
1
+ import { compactControlScale, compactFilterChipMinHeight, typographyVariants, } from '@scalewing/tokens';
2
+ import { tableRowLineVariable } from './css-table-metrics.js';
3
+ import { finePointerQuery } from './touch-target.js';
4
+ /**
5
+ * How tightly the themed root draws its controls: `comfortable` (the
6
+ * default) is the touch-sized scale; `compact` (opt-in, on a fine pointer
7
+ * only) is a desktop data tool's smaller controls and reading text.
8
+ */
9
+ export const themeDensities = ['comfortable', 'compact'];
10
+ export function assertThemeDensity(density) {
11
+ if (!themeDensities.includes(density))
12
+ throw new RangeError(`Unknown ThemeProvider density: ${String(density)}`);
13
+ }
14
+ /**
15
+ * The compact themed root. Two attributes outrank the `[data-theme]` block
16
+ * that declares the comfortable values, in any order.
17
+ */
18
+ const compactRoot = "[data-theme][data-density='compact']";
19
+ /*
20
+ * The controls that state the body size for their own words rather than
21
+ * inherit it, which would otherwise stand 2 px over a compact Field beside
22
+ * them. An xs segmented track states its own caption size and stays.
23
+ */
24
+ const statedBodyControls = [
25
+ '.sw-date-field-input',
26
+ '.sw-fixture-score-box',
27
+ '.sw-segmented-filled .sw-segmented-item',
28
+ '.sw-stepper-input',
29
+ '.sw-stepper-unit',
30
+ ].join(', ');
31
+ function compactControlDeclarations() {
32
+ return Object.entries(compactControlScale)
33
+ .flatMap(([size, value]) => [
34
+ ` --sw-control-${size}-min-height: ${value.minHeight}px;`,
35
+ ` --sw-control-${size}-padding-inline: ${value.paddingInline}px;`,
36
+ ])
37
+ .join('\n');
38
+ }
39
+ function bodySmDeclarations(indent) {
40
+ const { fontSize, letterSpacing, lineHeight } = typographyVariants.bodySm;
41
+ return [
42
+ `font-size: ${fontSize}px;`,
43
+ `letter-spacing: ${letterSpacing}px;`,
44
+ `line-height: ${lineHeight}px;`,
45
+ ]
46
+ .map((line) => `${indent}${line}`)
47
+ .join('\n');
48
+ }
49
+ /*
50
+ * `ThemeProvider density="compact"`, and everything it changes, on a fine
51
+ * pointer only: a touch screen keeps the comfortable scale, its 44 px
52
+ * targets (`touchTarget` reads the md variable) included, and a coarse rule
53
+ * never runs where these do.
54
+ *
55
+ * Every size-aware rule reads the control variables, so restating them here
56
+ * moves them all, and geometry derived from them (the rail's width, a page
57
+ * bar's clearance, a calendar's days) moves in step. The themed root's own
58
+ * type steps to `bodySm`, so what inherits it follows: a table's cells, a
59
+ * field's label and its control's words (the same in a Card and outside
60
+ * one). `Text`'s default `body` variant states its own size, so it follows
61
+ * only where the reading text is asked for: in a Card, a Table and a
62
+ * DescriptionList. A rail's smaller link would bury its glyph under the count
63
+ * pill, so the pill moves out into the gutter (the full sidebar's is in flow,
64
+ * where this has no effect).
65
+ */
66
+ export function cssDensityClasses() {
67
+ return `@media ${finePointerQuery} {
68
+ ${compactRoot} {
69
+ ${compactControlDeclarations()}
70
+ --sw-filter-chip-min-height: ${compactFilterChipMinHeight}px;
71
+ ${bodySmDeclarations(' ')}
72
+ }
73
+
74
+ ${compactRoot} :is(.sw-card, .sw-table, .sw-description-list) .sw-text-body {
75
+ ${bodySmDeclarations(' ')}
76
+ }
77
+
78
+ ${compactRoot} :is(${statedBodyControls}) {
79
+ ${bodySmDeclarations(' ')}
80
+ }
81
+
82
+ ${compactRoot} .sw-table-bounded {
83
+ ${tableRowLineVariable}: ${typographyVariants.bodySm.lineHeight}px;
84
+ }
85
+
86
+ ${compactRoot} .sw-card {
87
+ border-radius: var(--sw-radius-md);
88
+ }
89
+
90
+ ${compactRoot} .sw-app-shell-sidebar .sw-app-shell-count {
91
+ inset-block-start: calc(-1 * var(--sw-space-1));
92
+ inset-inline-end: calc(-1 * var(--sw-space-2));
93
+ }
94
+ }`;
95
+ }
@@ -1,4 +1,22 @@
1
1
  import { breakpointQuery } from './breakpoints.js';
2
+ /** The fixed term columns of `termWidth`, a named geometry of this list. */
3
+ const termWidths = { sm: '8rem', md: '12rem', lg: '16rem' };
4
+ function termWidthClass(width) {
5
+ return `sw-description-list-term-${width}`;
6
+ }
7
+ /*
8
+ * From `md` only, where the list has a term column at all; below it the
9
+ * base rule stacks each term over its detail. The column never takes more
10
+ * than half the list, so a narrow card keeps room for its details.
11
+ */
12
+ function termWidthRules() {
13
+ const widths = Object.entries(termWidths);
14
+ return `@media ${breakpointQuery('from', 'md')} {
15
+ ${widths
16
+ .map(([width, size]) => ` .${termWidthClass(width)} { grid-template-columns: min(${size}, 50%) minmax(0, 1fr); }`)
17
+ .join('\n')}
18
+ }`;
19
+ }
2
20
  /*
3
21
  * A spread list sets each row on its own two columns, at every width: the
4
22
  * term fits its words up to half the row at the start, and the detail
@@ -72,6 +90,8 @@ export function cssDescriptionListClasses() {
72
90
 
73
91
  ${spreadRules()}
74
92
 
93
+ ${termWidthRules()}
94
+
75
95
  @media (forced-colors: active) {
76
96
  .sw-description-item + .sw-description-item { border-top-color: CanvasText; }
77
97
  }`;
@@ -80,6 +100,7 @@ export function descriptionListClassCatalog() {
80
100
  return [
81
101
  'sw-description-list',
82
102
  'sw-description-list-spread',
103
+ ...Object.keys(termWidths).map(termWidthClass),
83
104
  'sw-description-item',
84
105
  'sw-description-term',
85
106
  'sw-description-detail',
@@ -1,2 +1,10 @@
1
+ /**
2
+ * How a chosen chip looks: `solid` (the default) is the accent fill with its
3
+ * on-colour words; `subtle` is the accent's quiet tint with the accent's
4
+ * words inside an accent edge, for a page where the chosen chip should not
5
+ * outweigh the page's primary action.
6
+ */
7
+ export declare const filterChipsSelectedStyles: readonly ["solid", "subtle"];
8
+ export type FilterChipsSelectedStyle = (typeof filterChipsSelectedStyles)[number];
1
9
  export declare function cssFilterChipsClasses(): string;
2
10
  export declare function filterChipsClassCatalog(): string[];
@@ -1,10 +1,38 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
+ import { toneTintFill, toneTintInk } from './tone-tint.js';
2
3
  import { cssFilterChipToneClasses, filterChipToneClassCatalog, } from './css-filter-chip-tones.js';
3
4
  import { cssFilterChipsLayoutClasses, filterChipsLayoutClassCatalog, } from './css-filter-chips-layout.js';
4
5
  import { cssFilterChipsRowClasses, filterChipsRowClassCatalog, } from './css-filter-chips-row.js';
5
6
  const label = typographyVariants.label;
6
7
  const data = typographyVariants.data;
7
8
  const caption = typographyVariants.caption;
9
+ /**
10
+ * How a chosen chip looks: `solid` (the default) is the accent fill with its
11
+ * on-colour words; `subtle` is the accent's quiet tint with the accent's
12
+ * words inside an accent edge, for a page where the chosen chip should not
13
+ * outweigh the page's primary action.
14
+ */
15
+ export const filterChipsSelectedStyles = ['solid', 'subtle'];
16
+ /*
17
+ * The compact density's chip height (`--sw-filter-chip-min-height`, set only
18
+ * there), else the md control's.
19
+ */
20
+ const chipMinHeight = 'var(--sw-filter-chip-min-height, var(--sw-control-md-min-height))';
21
+ /*
22
+ * `selectedStyle="subtle"`. The words are the accent's ink, the accent moved
23
+ * toward the text colour, a Notice's way with a tint: the bare accent falls
24
+ * to 4.24:1 on the tint in harvest dark, and the ink keeps 4.5:1 in every
25
+ * palette. `:where()` holds the rule at the chosen chip's own specificity, so
26
+ * what follows it wins as it does over the accent fill: an error's danger
27
+ * edge and the forced colours' Highlight.
28
+ */
29
+ function selectedSubtleRules() {
30
+ return `:where(.sw-filter-chips-selected-subtle) .sw-filter-chip-input:checked + .sw-filter-chip-face {
31
+ background: ${toneTintFill('accent')};
32
+ border-color: var(--sw-color-accent);
33
+ color: ${toneTintInk('accent')};
34
+ }`;
35
+ }
8
36
  export function cssFilterChipsClasses() {
9
37
  return `.sw-filter-chips {
10
38
  border: 0;
@@ -52,13 +80,14 @@ export function cssFilterChipsClasses() {
52
80
  background: var(--sw-glass-fill);
53
81
  border: 1px solid var(--sw-color-border);
54
82
  border-radius: var(--sw-radius-pill);
83
+ box-sizing: border-box;
55
84
  color: var(--sw-color-text);
56
85
  display: inline-flex;
57
86
  font-family: var(--sw-font-sans);
58
87
  font-size: ${label.fontSize}px;
59
88
  gap: var(--sw-space-2);
60
89
  line-height: ${label.lineHeight}px;
61
- min-height: var(--sw-control-md-min-height);
90
+ min-height: ${chipMinHeight};
62
91
  min-width: 0;
63
92
  overflow-wrap: anywhere;
64
93
  padding: var(--sw-space-1) var(--sw-space-3);
@@ -91,6 +120,8 @@ export function cssFilterChipsClasses() {
91
120
  color: var(--sw-color-onAccent);
92
121
  }
93
122
 
123
+ ${selectedSubtleRules()}
124
+
94
125
  .sw-filter-chip-input:checked + .sw-filter-chip-face .sw-filter-chip-count {
95
126
  color: inherit;
96
127
  }
@@ -153,6 +184,9 @@ export function filterChipsClassCatalog() {
153
184
  'sw-filter-chip-face',
154
185
  'sw-filter-chip-count',
155
186
  'sw-filter-chip-quiet',
187
+ ...filterChipsSelectedStyles
188
+ .filter((style) => style !== 'solid')
189
+ .map((style) => `sw-filter-chips-selected-${style}`),
156
190
  'sw-filter-chips-error',
157
191
  ...filterChipsLayoutClassCatalog(),
158
192
  ...filterChipToneClassCatalog(),
@@ -1 +1,11 @@
1
+ /**
2
+ * How a FixtureGroup's sides take the row: `fluid` (the default) share all
3
+ * of it, so the centre sits in the middle of whatever is left; `fixed` are
4
+ * at most 16rem each, the end column taking the rest, so the centre sits at
5
+ * the same place in every run on a page.
6
+ */
7
+ export declare const fixtureGroupSides: readonly ["fluid", "fixed"];
8
+ export type FixtureGroupSides = (typeof fixtureGroupSides)[number];
1
9
  export declare function cssFixtureListRules(): string;
10
+ /** After the row's narrow and fallback rules, which `fixed` outranks only from md. */
11
+ export declare function cssFixtureListSidesRules(): string;
@@ -1,5 +1,39 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
+ import { breakpointQuery } from './breakpoints.js';
2
3
  const caption = typographyVariants.caption;
4
+ /**
5
+ * How a FixtureGroup's sides take the row: `fluid` (the default) share all
6
+ * of it, so the centre sits in the middle of whatever is left; `fixed` are
7
+ * at most 16rem each, the end column taking the rest, so the centre sits at
8
+ * the same place in every run on a page.
9
+ */
10
+ export const fixtureGroupSides = ['fluid', 'fixed'];
11
+ const fixedSide = 'minmax(0, 16rem)';
12
+ /*
13
+ * The centre's least width in a `fixed` run: an `auto` column alone is as
14
+ * wide as its widest centre, which differs between a run of scores and a run
15
+ * of kickoff times and would move the centres apart. This holds either, and
16
+ * two score boxes, with its margins.
17
+ */
18
+ const fixedCentre = 'minmax(7rem, auto)';
19
+ /*
20
+ * From \`md\` only: below it the narrow rules' columns stand, the end on a
21
+ * second line. Without subgrid each row is its own grid of the same fixed
22
+ * columns, which line up on their own.
23
+ */
24
+ function fixedSidesRules() {
25
+ return `@media ${breakpointQuery('from', 'md')} {
26
+ .sw-fixture-list-sides-fixed {
27
+ grid-template-columns: auto ${fixedSide} ${fixedCentre} ${fixedSide} 1fr auto auto;
28
+ }
29
+
30
+ @supports not (grid-template-columns: subgrid) {
31
+ .sw-fixture-list-sides-fixed .sw-fixture-row {
32
+ grid-template-columns: ${fixedSide} ${fixedCentre} ${fixedSide} 1fr auto;
33
+ }
34
+ }
35
+ }`;
36
+ }
3
37
  /*
4
38
  * The run: one grid whose columns are the select, the home side, the
5
39
  * centre, the away side, the end (details and status), the chevron and the
@@ -48,3 +82,7 @@ export function cssFixtureListRules() {
48
82
  border-top: 1px solid var(--sw-color-border);
49
83
  }`;
50
84
  }
85
+ /** After the row's narrow and fallback rules, which `fixed` outranks only from md. */
86
+ export function cssFixtureListSidesRules() {
87
+ return fixedSidesRules();
88
+ }
@@ -3,7 +3,7 @@ import { breakpointQuery } from './breakpoints.js';
3
3
  import { chevronStrokeIn } from './chevron.js';
4
4
  import { mutedReadsAsText, quietFill } from './quiet-fill.js';
5
5
  import { cssFixtureActionClasses, fixtureActionClassCatalog, } from './css-fixture-action.js';
6
- import { cssFixtureListRules } from './css-fixture-list.js';
6
+ import { cssFixtureListRules, cssFixtureListSidesRules, } from './css-fixture-list.js';
7
7
  import { cssFixtureDetailClasses, fixtureDetailClassCatalog, } from './css-fixture-detail.js';
8
8
  import { cssFixtureFlagClasses, fixtureFlagClassCatalog, } from './css-fixture-flag.js';
9
9
  import { cssFixtureScoreClasses, fixtureScoreClassCatalog, } from './css-fixture-score.js';
@@ -209,6 +209,8 @@ ${narrowRules()}
209
209
 
210
210
  ${fallbackRules()}
211
211
 
212
+ ${cssFixtureListSidesRules()}
213
+
212
214
  ${cssFixtureFlagClasses()}
213
215
 
214
216
  ${cssFixtureSelectClasses()}
@@ -224,6 +226,7 @@ export function fixtureClassCatalog() {
224
226
  'sw-fixture-group',
225
227
  'sw-fixture-group-title',
226
228
  'sw-fixture-list',
229
+ 'sw-fixture-list-sides-fixed',
227
230
  'sw-fixture-item',
228
231
  'sw-fixture-row',
229
232
  'sw-fixture-row-pressable',
@@ -1,4 +1,4 @@
1
- import { categoricalColorKeys } from '@scalewing/tokens';
1
+ import { categoricalColorKeys, controlScale } from '@scalewing/tokens';
2
2
  export const glyphTileSizes = ['sm', 'md', 'lg'];
3
3
  /** The most letters a tile holds, such as a three-letter crest ("NSL"). */
4
4
  export const glyphTileLetterLimit = 3;
@@ -10,9 +10,13 @@ export const glyphTileLetterLimit = 3;
10
10
  * the mix, 5.08:1). A glyph keeps the fill as it is.
11
11
  */
12
12
  export const glyphTileLetterDarken = 0.25;
13
+ /*
14
+ * `md` reads the token, not the variable: a tile holds up to three letters,
15
+ * which a compact density's smaller xs control would crowd.
16
+ */
13
17
  const tileSide = {
14
18
  sm: 'var(--sw-space-5)',
15
- md: 'var(--sw-control-xs-min-height)',
19
+ md: `${controlScale.xs.minHeight}px`,
16
20
  lg: 'var(--sw-space-6)',
17
21
  };
18
22
  function hueRules() {
@@ -1,5 +1,5 @@
1
1
  import { breakpointQuery, breakpoints } from './breakpoints.js';
2
- import { gridColumnCounts, gridColumnsBelowClass, gridColumnsClass, gridFitClass, gridFits, } from './css-grid.js';
2
+ import { gridColumnCounts, gridColumnsBelowClass, gridColumnsClass, gridFitClass, gridFits, gridRailClass, gridRails, } from './css-grid.js';
3
3
  export const gridColumnSpans = gridColumnCounts;
4
4
  export function gridSpanClass(span) {
5
5
  return `sw-grid-span-${span}`;
@@ -22,8 +22,13 @@ export function cssGridSpanClasses() {
22
22
  const spans = gridColumnSpans.map((span) => spanRule(`.${gridSpanClass(span)}`, span));
23
23
  const caps = gridColumnCounts.flatMap((columns) => widerSpans(columns).map((span) => capRule(gridColumnsClass(columns), columns, span)));
24
24
  // A fitted grid's column count follows its width, so a span there would
25
- // add implicit columns: its children each take one column.
26
- const fitted = gridFits.flatMap((fit) => widerSpans(1).map((span) => `:where(.${gridFitClass(fit)}) > .${gridSpanClass(span)} { grid-column: auto; }`));
25
+ // add implicit columns: its children each take one column. A rail grid's
26
+ // two columns are not interchangeable, so the same holds there.
27
+ const oneColumnEach = [
28
+ ...gridFits.map(gridFitClass),
29
+ ...gridRails.map(gridRailClass),
30
+ ];
31
+ const fitted = oneColumnEach.flatMap((gridClass) => widerSpans(1).map((span) => `:where(.${gridClass}) > .${gridSpanClass(span)} { grid-column: auto; }`));
27
32
  // Below a breakpoint every span wider than one is restated for the phone
28
33
  // column count, which also undoes a desktop cap when that count is larger.
29
34
  const below = breakpoints.map((breakpoint) => `@media ${breakpointQuery('below', breakpoint)} {