@scalewing/react 1.48.0 → 1.49.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.49.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2ae2974: `Hero`: a public page's opening, a `section` named by its title. An optional `eyebrow` (muted label words), the `title` (the page's `h1` in the `hero` size), an optional `lead` and `actions`, and an optional `media` picture that keeps its own width beside the words from `md` and sits under them below it, where the actions stack to the full width. With no picture the words keep the reading width. New classes `sw-hero*`; a blank `title` throws a `RangeError`. No new tokens or dependencies. Request: `docs/requests/teisoro-hero.md`.
8
+ - dc0c896: `EmptyState` (new): where a list or a page has nothing yet, a centred panel on the surface with a dashed hairline edge that fills its column (a page column or a Card). `title` is a heading at `titleAs` (`h2` by default, or `h3`; `emptyStateTitleElements`), `description` a muted line, `icon` a decorative glyph in a quiet square, `actions` a centred row that wraps (44 px Buttons on a coarse pointer). Forced colours keep the dashed edge. A blank `title` or `description` or an unknown `titleAs` throws a `RangeError`. New classes `sw-empty-state`, `sw-empty-state-icon`, `sw-empty-state-title`, `sw-empty-state-description`, `sw-empty-state-actions`. No new tokens or dependencies. Request: `docs/requests/futmas-empty-state.md`.
9
+ - 6d3cbe8: `Grid` takes `fit` (`narrow` 15rem, `medium` 16.25rem, `wide` 18.75rem): as many columns as the grid's width holds at that minimum, sharing the row, one when the grid is narrower, so a row of cards reflows with no breakpoint. Not with `columns` or `columnsBelow`; either, or an unknown `fit`, throws a `RangeError`. New classes `sw-grid-fit-*` and the `gridFits` list and `GridFit` type. No new tokens or dependencies. Request: `docs/requests/teisoro-grid-fit.md`.
10
+ - b466bbb: `Text` takes the tokens' new variants `hero` (an `h1` by default), `headline` (an `h2`) and `lead` (a `p`), each stepping down to its compact size below `md` like `display` and `heading`. New classes `sw-text-hero`, `sw-text-headline`, `sw-text-lead`, generated from the tokens. Needs `@scalewing/tokens` 1.7.0. Request: `docs/requests/teisoro-showcase-type.md`.
11
+ - 7b57979: `Stepper` takes `unit` (opt-in), a short muted word after the count inside its frame, such as "min" or "players". The count is read with it (`aria-valuetext` "90 min") unless `valueText` says it in words; the unit itself is hidden from assistive technology. The frame draws the control surface, focus ring and the invalid, changed and disabled states; the count keeps its few-digit width, so the frame never follows it. A blank `unit` throws a `RangeError`. New classes `sw-stepper-count`, `sw-stepper-unit`. No new tokens or dependencies. Request: `docs/requests/futmas-stepper-unit.md`.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [b466bbb]
16
+ - @scalewing/tokens@1.7.0
17
+
3
18
  ## 1.48.0
4
19
 
5
20
  ### Minor Changes
package/README.md CHANGED
@@ -35,10 +35,12 @@ import {
35
35
  InfoTip,
36
36
  Separator,
37
37
  Band,
38
+ Hero,
38
39
  SiteFooter,
39
40
  SiteHeader,
40
41
  DescriptionList,
41
42
  DescriptionItem,
43
+ EmptyState,
42
44
  FixtureGroup,
43
45
  FixtureRow,
44
46
  ListGroup,
@@ -189,7 +191,7 @@ A chosen photo can show itself: `value.preview` is the address of a small image
189
191
  />
190
192
  ```
191
193
 
192
- `Stepper` is a whole-number count changed with − and + or typed. The count is a native text input with `role="spinbutton"` and `inputmode="numeric"`, named by `label` (visible above it, or `labelVisuallyHidden` where table headers say it) and the one tab stop: ArrowUp and ArrowDown step it by `step` (default 1), Home goes to `min` and End to a finite `max` (a range below zero takes `inputmode="text"`, since a phone's numeric pad has no minus key). The − and + buttons are pointer targets outside the tab order, named by `decrementLabel` and `incrementLabel` (your copy), and a press on one leaves focus where it was; the count and both buttons sit in a `group` named by the label. − is disabled at `min` (default 0) and + at `max` (no upper bound when left out). `value` is a count or `null` for an empty box, which the buttons treat as `min` and `placeholder` can fill. Typed text is refused unless it is digits (with a leading minus when `min` is below zero), so `onChange` only receives a whole number or `null`; a typed count is reported as typed even outside the bounds, so say why in `error` (a polite message, `aria-invalid` and the danger border, as `Field`'s), and the buttons then move it back inside them. `aria-describedby` takes ids of elements that describe the count, such as the value it adds up to, read before an `error`'s message. `changed` takes `Field`'s changed look; `readOnly` keeps the count focusable and hides the buttons in their own room; `disabled` stops all three. `limitAppearance="plain"` (default `"faded"`) keeps − at `min` and + at `max` in their usual look instead of faded, for a count sheet where a faded − reads as broken; they are still disabled, a disabled or read-only Stepper is unchanged, forced colors still grey them, and an unknown value throws a `RangeError`. The count is a few digits wide and keeps that width (`width="fixed"`), or fills its container (`width="full"`, for a table cell). Bounds that are not whole numbers, `min` above `max`, or a `step` that is not above zero throw a `RangeError`.
194
+ `Stepper` is a whole-number count changed with − and + or typed. The count is a native text input with `role="spinbutton"` and `inputmode="numeric"`, named by `label` (visible above it, or `labelVisuallyHidden` where table headers say it) and the one tab stop: ArrowUp and ArrowDown step it by `step` (default 1), Home goes to `min` and End to a finite `max` (a range below zero takes `inputmode="text"`, since a phone's numeric pad has no minus key). The − and + buttons are pointer targets outside the tab order, named by `decrementLabel` and `incrementLabel` (your copy), and a press on one leaves focus where it was; the count and both buttons sit in a `group` named by the label. − is disabled at `min` (default 0) and + at `max` (no upper bound when left out). `value` is a count or `null` for an empty box, which the buttons treat as `min` and `placeholder` can fill. Typed text is refused unless it is digits (with a leading minus when `min` is below zero), so `onChange` only receives a whole number or `null`; a typed count is reported as typed even outside the bounds, so say why in `error` (a polite message, `aria-invalid` and the danger border, as `Field`'s), and the buttons then move it back inside them. `aria-describedby` takes ids of elements that describe the count, such as the value it adds up to, read before an `error`'s message. `changed` takes `Field`'s changed look; `readOnly` keeps the count focusable and hides the buttons in their own room; `disabled` stops all three. `limitAppearance="plain"` (default `"faded"`) keeps − at `min` and + at `max` in their usual look instead of faded, for a count sheet where a faded − reads as broken; they are still disabled, a disabled or read-only Stepper is unchanged, forced colors still grey them, and an unknown value throws a `RangeError`. The count is a few digits wide and keeps that width (`width="fixed"`), or fills its container (`width="full"`, for a table cell). `unit` (opt-in) is a short muted word after the count inside its frame, such as "min" or "players": the frame (`sw-stepper-count`) draws the control surface, focus ring and the invalid, changed and disabled states, a press on the word focuses the count, and the count keeps its few-digit width with its digits against the word, so the frame never follows it. The word (`sw-stepper-unit`) is hidden from assistive technology; the count is read with it (`aria-valuetext` "90 min", none while the box is empty), and a `valueText` you pass wins. The default joins the count and the word as given, so a word that changes with the count ("players") reads "1 players": pass `valueText` in your language's right form ("1 player", "11 players"). A blank `unit` throws a `RangeError`. Bounds that are not whole numbers, `min` above `max`, or a `step` that is not above zero throw a `RangeError`.
193
195
 
194
196
  ```tsx
195
197
  <Stepper
@@ -840,6 +842,24 @@ The consumer needs no PDF.js dependency of its own. With another bundler, build
840
842
  <Skeleton label="Loading rangers" variant="rows" lines={4} />
841
843
  ```
842
844
 
845
+ `EmptyState` is where a list or a page has nothing yet: a centred panel on the surface with a dashed hairline edge (the md radius) that fills its column, so it sits in a page column or inside a `Card`. `title` (required) says what is empty ("No teams in Spring 2027 yet"), a heading at `titleAs` (`emptyStateTitleElements`: `"h2"` by default in a page, `"h3"` inside a card under its own heading) in title type. `description` is a muted line under it, usually what to do next. `icon` is a decorative glyph (Lucide in your app) in a quiet subtle-fill square over the title, hidden from assistive technology. `actions` are the ways out, such as a primary and a neutral `Button`, in a centred row that wraps (a 44 px target on a touch screen). The panel is a plain `div`, not a landmark or a live region: if it replaces a list after a person's action, say so yourself. An empty slot (nothing, an empty Fragment or array, or blank words) draws no box. Inside a bordered `Card` (the default glass or `outlined`) the dashed edge sits just inside the card's own hairline and reads as a double edge: where the empty panel is the card's whole content, use it in place of the card, or in an `elevated` or `filled` Card, which draw no hairline. Forced colours keep the dashed edge. A blank `title` or `description` or an unknown `titleAs` throws a `RangeError`.
846
+
847
+ ```tsx
848
+ <EmptyState
849
+ icon={<Users />}
850
+ title="No nests counted yet"
851
+ description="Bring back last spring's nests, or paste a list."
852
+ actions={
853
+ <>
854
+ <Button onPress={copy}>Copy last spring's 12 nests</Button>
855
+ <Button variant="neutral" onPress={paste}>
856
+ Paste a list
857
+ </Button>
858
+ </>
859
+ }
860
+ />
861
+ ```
862
+
843
863
  `GlyphTile` is a small rounded square in a categorical colour with the consumer's glyph in white: the mark of a kind beside its name, such as a service in a list. `hue` is one of the `@scalewing/tokens` categorical colours (`green`, `blue`, `orange`, `purple`, `teal`, `rose`, or `neutral`); which kind takes which hue is yours. `size` is `sm` (24), `md` (28, the default) or `lg` (32); the glyph is drawn at spacing step 4 in white, which keeps 3:1 on every fill; pass the SVG (a Lucide component) as the tile's direct child, since only a direct-child SVG is sized. It is always `aria-hidden`, so put the kind's name in visible text beside it. An unknown `hue` or `size` throws a `RangeError`. It may hold up to three letters in place of the glyph, such as initials (`<GlyphTile hue="neutral" size="sm">NSL</GlyphTile>`): words given as its children are drawn in one line at weight 600 and sized from the tile, not the text round it (one letter at half its side, two at 0.42, three at a third), so they fit every size; blank words or more than three letters throw a `RangeError`.
844
864
 
845
865
  ```tsx
@@ -1018,6 +1038,18 @@ const { headings, omissions } = inspectMarkdown(page.body, {
1018
1038
  />
1019
1039
  ```
1020
1040
 
1041
+ `Hero` is a public page's opening, a `section` named by its title: an optional `eyebrow` (muted label words), the `title` (the page's one `h1`, in the `hero` size), an optional `lead` (the `lead` size) and `actions` (links or buttons in a wrapping row, stacked to the full width below `md`), with an optional `media` picture. From `md` the picture keeps its own width at the row's end and the words take the rest; below `md` it sits centred under the words. With no picture the words keep the reading width (`--sw-reading-max`). Put it in the page's column (`sw-container`); it sets no gutter or fill of its own. The title always names the section (`aria-labelledby` is not a prop). Leave out `actions` or `media` rather than passing an empty fragment, which keeps its row or column. A blank `title` throws a `RangeError`.
1042
+
1043
+ ```tsx
1044
+ <Hero
1045
+ eyebrow="For birdwatchers"
1046
+ title="Every heron counted."
1047
+ lead="The north marsh in every season, from the hide to the shore."
1048
+ actions={<ButtonLink href="/visit">Plan a visit</ButtonLink>}
1049
+ media={<img alt="The heron hide at dawn" src="/hide.jpg" />}
1050
+ />
1051
+ ```
1052
+
1021
1053
  `SiteFooter` is a public site's footer on the dark inverse band (`Band tone="inverse"`, so a brand moves it with a theme overlay of `inverse`). Supply `brand` (such as a logo for a dark background), an optional `tagline`, and `groups`: each `{ id, label, links }` is a `nav` named by its label, which shows above its links (hidden from assistive technology, since the `nav` already carries it). Each link is `{ id, label, href, current?, lang?, hrefLang? }`; `lang` and `hrefLang` pass through to the link unchecked; `current` sets `aria-current="page"` and draws it in the band's words at label weight, the others in its muted words, underlined on hover and focus. `legal` and `end` fill the bottom row past a hairline, such as the copyright and a language switch. Below `md` the brand takes the full width and the groups two columns; from `md` the brand and the groups share one spread row. Links get the 44 px target on a coarse pointer. Put it last in the page's `body` (outside a `section` or `article`) so it is the page's `contentinfo` landmark. No groups, a group with no links, blank or repeated ids, blank labels or a blank `href` throw a `RangeError`.
1022
1054
 
1023
1055
  ```tsx
@@ -1181,7 +1213,7 @@ A row can also carry three trailing parts. `unread` marks it new for the person,
1181
1213
 
1182
1214
  `--sw-color-accentSubtle` is the accent tint from `@scalewing/tokens`: a quiet fill behind accent text or glyphs that keeps 4.5:1. The stylesheet and every palette file set it, and a palette that moves the accent or surface retints it.
1183
1215
 
1184
- `Text` steps `display` and `heading` down to the compact token sizes below the `md` breakpoint; consumers do not size type per viewport.
1216
+ `Text` steps `hero`, `display`, `headline`, `heading` and `lead` down to the compact token sizes below the `md` breakpoint; consumers do not size type per viewport. `hero` (an `h1`), `headline` (an `h2`) and `lead` (a `p`) are a public page's sizes: its one opening title, its section titles and the sentence under them; an app's screens keep `display` and `heading`.
1185
1217
 
1186
1218
  `sw-container-reading` centres a column at the reading width (`--sw-reading-max`, 46rem, `Article`'s), as `sw-container` centres a page's column at the container width (72rem): `width: 100%`, the max width and auto inline margins, full width below it. Use it for a form or text that reads better narrower than the page, such as an open form of one column. As with `sw-container`, the width is the content box's, so put padding or a border on a child.
1187
1219
 
@@ -1193,6 +1225,16 @@ A row can also carry three trailing parts. `unread` marks it new for the person,
1193
1225
 
1194
1226
  `Grid` places children in one to four (or six) equal-width columns with a token `gap` step. `columnsBelow={{ md: 2 }}` drops to fewer columns below the `md` breakpoint so tiles and stat cards stay readable on a phone. It accepts every `Box` prop.
1195
1227
 
1228
+ `fit` (`narrow` 15rem, `medium` 16.25rem, `wide` 18.75rem) fits the columns to the grid's width instead of a fixed count: as many columns as fit at that minimum, sharing the row, and one column when the grid is narrower than it, so a row of cards reflows on a tablet and stacks on a phone with no breakpoint. A fitted grid takes no `columns` or `columnsBelow` (either throws a `RangeError`, as an unknown `fit` does); keep `columnSpan` for fixed-column grids. `gridFits` lists the names.
1229
+
1230
+ ```tsx
1231
+ <Grid fit="narrow" gap={4}>
1232
+ {habitats.map((habitat) => (
1233
+ <Card key={habitat.id}>…</Card>
1234
+ ))}
1235
+ </Grid>
1236
+ ```
1237
+
1196
1238
  Every `Box`-based component (`Box`, `Stack`, `Inline`, `Card`, `Grid`, …) takes `columnSpan` (1, 2, 3, 4 or 6) to cover several columns of the `Grid` it sits in directly, through the generated `sw-grid-span-*` classes. A span is capped at the columns the grid has at the current width, so it never adds an implicit column, and `columnsBelow` still decides the phone layout. A form about twice the width of a side panel, stacking below `md`:
1197
1239
 
1198
1240
  ```tsx
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from 'react';
2
+ export declare const emptyStateTitleElements: readonly ["h2", "h3"];
3
+ /** The title's heading level: `h2` (the default) in a page, `h3` in a card under its own heading. */
4
+ export type EmptyStateTitleElement = (typeof emptyStateTitleElements)[number];
5
+ export type EmptyStateProps = {
6
+ /** What is empty, in the consumer's words ("No teams in Spring 2027 yet"). */
7
+ title: string;
8
+ /** `h2` (the default) in a page column; `h3` inside a card under its own heading. */
9
+ titleAs?: EmptyStateTitleElement;
10
+ /** A muted line under the title: what to do next. Not blank. */
11
+ description?: string;
12
+ /** A decorative glyph over the title (Lucide in the consumer), hidden from assistive technology. */
13
+ icon?: ReactNode;
14
+ /** The ways out, such as Buttons, in a centred row that wraps. */
15
+ actions?: ReactNode;
16
+ };
17
+ /**
18
+ * Where a list or a page has nothing yet: a centred panel on the surface
19
+ * with a dashed hairline edge, an optional decorative glyph, a title, a
20
+ * muted line and the actions that fill it. It fills its column, so it fits
21
+ * a page column or a Card.
22
+ */
23
+ export declare function EmptyState({ title, titleAs, description, icon, actions, }: EmptyStateProps): import("react").JSX.Element;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, Fragment, isValidElement } from 'react';
3
+ import { isOneOf } from '../is-one-of.js';
4
+ import { Text } from './Text.js';
5
+ export const emptyStateTitleElements = ['h2', 'h3'];
6
+ /** Fails closed on blank words or an unknown heading level. */
7
+ function assertEmptyState({ title, titleAs, description }) {
8
+ if (typeof title !== 'string' || !title.trim())
9
+ throw new RangeError('EmptyState title must be nonempty');
10
+ if (titleAs !== undefined && !isOneOf(emptyStateTitleElements, titleAs))
11
+ throw new RangeError(`Unknown EmptyState titleAs: ${String(titleAs)}`);
12
+ if (description !== undefined &&
13
+ (typeof description !== 'string' || !description.trim()))
14
+ throw new RangeError('EmptyState description must be nonempty');
15
+ }
16
+ /**
17
+ * Whether a slot draws anything: nothing, a boolean, blank words, an empty
18
+ * array or a Fragment holding only those is no content, so the slot's box
19
+ * (and its gap) is left out.
20
+ */
21
+ function hasContent(node) {
22
+ return Children.toArray(node).some((child) => {
23
+ if (typeof child === 'string')
24
+ return child.trim() !== '';
25
+ if (isValidElement(child) &&
26
+ child.type === Fragment)
27
+ return hasContent(child.props.children);
28
+ return true;
29
+ });
30
+ }
31
+ /**
32
+ * Where a list or a page has nothing yet: a centred panel on the surface
33
+ * with a dashed hairline edge, an optional decorative glyph, a title, a
34
+ * muted line and the actions that fill it. It fills its column, so it fits
35
+ * a page column or a Card.
36
+ */
37
+ export function EmptyState({ title, titleAs = 'h2', description, icon, actions, }) {
38
+ assertEmptyState({ title, titleAs, description });
39
+ return (_jsxs("div", { className: "sw-empty-state", children: [hasContent(icon) ? (_jsx("span", { "aria-hidden": "true", className: "sw-empty-state-icon", children: icon })) : null, _jsx(Text, { as: titleAs, className: "sw-empty-state-title", variant: "title", children: title }), description !== undefined ? (_jsx(Text, { as: "p", className: "sw-empty-state-description", color: "muted", children: description })) : null, hasContent(actions) ? (_jsx("div", { className: "sw-empty-state-actions", children: actions })) : null] }));
40
+ }
@@ -1,14 +1,22 @@
1
1
  import { type SpacingStep } from '@scalewing/tokens';
2
2
  import { type Breakpoint } from '../css/breakpoints.js';
3
- import { type GridColumns } from '../css/css-grid.js';
3
+ import { type GridColumns, type GridFit, gridFits } from '../css/css-grid.js';
4
4
  import { type BoxProps } from './Box.js';
5
5
  import { type Align } from './Stack.js';
6
- export type { GridColumns };
6
+ export { gridFits, type GridColumns, type GridFit };
7
7
  export type GridProps = BoxProps & {
8
8
  /** Where each child sits in the height of its row; unset, children stretch. */
9
9
  align?: Align;
10
10
  columns?: GridColumns;
11
11
  columnsBelow?: Partial<Record<Breakpoint, GridColumns>>;
12
+ /**
13
+ * Fits the columns to the grid's width instead of a fixed count: as many
14
+ * as fit at the named minimum (`narrow` 15rem, `medium` 16.25rem, `wide`
15
+ * 18.75rem), sharing the row, one column when narrower. Not with
16
+ * `columns` or `columnsBelow`; a child's `columnSpan` is ignored in a
17
+ * fitted grid, where every child takes one column.
18
+ */
19
+ fit?: GridFit;
12
20
  gap?: SpacingStep;
13
21
  };
14
- export declare function Grid({ align, className, columns, columnsBelow, gap, ...rest }: GridProps): import("react").JSX.Element;
22
+ export declare function Grid({ align, className, columns, columnsBelow, fit, gap, ...rest }: GridProps): import("react").JSX.Element;
@@ -1,12 +1,21 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../class-names.js';
3
3
  import { breakpoints } from '../css/breakpoints.js';
4
- import { gridColumnsBelowClass, gridColumnsClass, } from '../css/css-grid.js';
4
+ import { gridColumnsBelowClass, gridColumnsClass, gridFitClass, gridFits, } from '../css/css-grid.js';
5
5
  import { spacingClass } from '../css/spacing-classes.js';
6
6
  import { assertGridCount } from '../grid-counts.js';
7
+ import { isOneOf } from '../is-one-of.js';
7
8
  import { Box } from './Box.js';
8
- export function Grid({ align, className, columns = 1, columnsBelow, gap = 0, ...rest }) {
9
- assertGridCount('columns', columns);
9
+ export { gridFits };
10
+ export function Grid({ align, className, columns, columnsBelow, fit, gap = 0, ...rest }) {
11
+ if (fit !== undefined) {
12
+ if (!isOneOf(gridFits, fit))
13
+ throw new RangeError(`Unknown Grid fit: ${String(fit)}`);
14
+ if (columns !== undefined || columnsBelow !== undefined)
15
+ throw new RangeError('A fitted Grid takes no columns or columnsBelow; fit sets them');
16
+ }
17
+ const fixedColumns = columns ?? 1;
18
+ assertGridCount('columns', fixedColumns);
10
19
  const belowClasses = breakpoints.flatMap((breakpoint) => {
11
20
  const count = columnsBelow?.[breakpoint];
12
21
  if (count === undefined)
@@ -14,5 +23,5 @@ export function Grid({ align, className, columns = 1, columnsBelow, gap = 0, ...
14
23
  assertGridCount('columnsBelow', count);
15
24
  return [gridColumnsBelowClass(breakpoint, count)];
16
25
  });
17
- return (_jsx(Box, { className: cx('sw-grid', gridColumnsClass(columns), ...belowClasses, spacingClass('gap', 'all', gap), align && `sw-align-${align}`, className), ...rest }));
26
+ return (_jsx(Box, { className: cx('sw-grid', fit ? gridFitClass(fit) : gridColumnsClass(fixedColumns), ...belowClasses, spacingClass('gap', 'all', gap), align && `sw-align-${align}`, className), ...rest }));
18
27
  }
@@ -0,0 +1,26 @@
1
+ import { type HTMLAttributes, type ReactNode } from 'react';
2
+ export type HeroProps = Omit<HTMLAttributes<HTMLElement>, 'aria-labelledby' | 'children' | 'style' | 'title'> & {
3
+ /** The page's title, its one `h1`, in the `hero` size. */
4
+ title: ReactNode;
5
+ /** A short line above the title saying who or what the page is for. */
6
+ eyebrow?: ReactNode;
7
+ /** The sentence under the title, in the `lead` size. */
8
+ lead?: ReactNode;
9
+ /**
10
+ * Links or buttons in a row, stacked to the full width below `md`. Leave
11
+ * it out rather than passing an empty fragment, which keeps the row.
12
+ */
13
+ actions?: ReactNode;
14
+ /**
15
+ * A picture beside the words from `md`, under them below it. Leave it out
16
+ * rather than passing an empty fragment, which keeps its column.
17
+ */
18
+ media?: ReactNode;
19
+ };
20
+ /**
21
+ * A public page's opening, as a `section` named by its title: an optional
22
+ * eyebrow, the title (`h1`, `hero` size), an optional lead (`lead` size)
23
+ * and actions, with an optional picture beside them from `md`. The title
24
+ * always names the section. A blank title throws a `RangeError`.
25
+ */
26
+ export declare function Hero({ actions, className, eyebrow, lead, media, title, ...rest }: HeroProps): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from 'react';
3
+ import { cx } from '../class-names.js';
4
+ import { Text } from './Text.js';
5
+ function isBlank(node) {
6
+ return (node === undefined ||
7
+ node === null ||
8
+ node === false ||
9
+ (typeof node === 'string' && node.trim() === ''));
10
+ }
11
+ /**
12
+ * A public page's opening, as a `section` named by its title: an optional
13
+ * eyebrow, the title (`h1`, `hero` size), an optional lead (`lead` size)
14
+ * and actions, with an optional picture beside them from `md`. The title
15
+ * always names the section. A blank title throws a `RangeError`.
16
+ */
17
+ export function Hero({ actions, className, eyebrow, lead, media, title, ...rest }) {
18
+ const titleId = useId();
19
+ if (isBlank(title))
20
+ throw new RangeError('A Hero needs a title');
21
+ const withMedia = !isBlank(media);
22
+ return (_jsxs("section", { ...rest, "aria-labelledby": titleId, className: cx('sw-hero', withMedia && 'sw-hero-with-media', className), children: [_jsxs("div", { className: "sw-hero-text", children: [isBlank(eyebrow) ? null : (_jsx(Text, { color: "muted", as: "p", variant: "label", children: eyebrow })), _jsx(Text, { id: titleId, variant: "hero", children: title }), isBlank(lead) ? null : _jsx(Text, { variant: "lead", children: lead }), isBlank(actions) ? null : (_jsx("div", { className: "sw-hero-actions", children: actions }))] }), withMedia ? _jsx("div", { className: "sw-hero-media", children: media }) : null] }));
23
+ }
@@ -36,8 +36,18 @@ export type StepperProps = {
36
36
  disabled?: boolean;
37
37
  /** Readable and focusable, never changed: the buttons are hidden and keep their room. */
38
38
  readOnly?: boolean;
39
- /** Says the count in words for assistive technology (`aria-valuetext`), such as "12 straps". */
39
+ /**
40
+ * Says the count in words for assistive technology (`aria-valuetext`),
41
+ * such as "12 straps". It wins over the count and `unit` read together.
42
+ */
40
43
  valueText?: string;
44
+ /**
45
+ * A short muted word after the count, inside its frame, such as "min".
46
+ * Without `valueText` the count is read with it ("90 min"), word for
47
+ * word: a unit that changes with the count ("players") reads "1 players",
48
+ * so pass `valueText` with the right form ("1 player"). Not blank.
49
+ */
50
+ unit?: string;
41
51
  /**
42
52
  * Ids of elements that describe the count, such as the value it adds up
43
53
  * to; read before an `error`'s message.
@@ -2,9 +2,10 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { forwardRef, useId, useState, } from 'react';
4
4
  import { cx } from '../class-names.js';
5
- import { assertStepperBounds, readStepperEntry, stepperMoves, stepperText, } from '../stepper-count.js';
5
+ import { assertStepperBounds, assertStepperUnit, readStepperEntry, stepperMoves, stepperText, stepperValueText, } from '../stepper-count.js';
6
6
  import { FieldErrorRegion } from './FieldErrorRegion.js';
7
7
  import { FieldLabelText } from './FieldLabelText.js';
8
+ import { StepperCountFrame } from './stepper/StepperCountFrame.js';
8
9
  export const stepperLimitAppearances = ['faded', 'plain'];
9
10
  /** Keeps focus where it was (the count, or nowhere) when a button is pressed. */
10
11
  function keepFocus(event) {
@@ -15,9 +16,10 @@ function keepFocus(event) {
15
16
  * tab stop (the count, a spin button that ArrowUp and ArrowDown step), with
16
17
  * the buttons as pointer targets beside it. − stops at `min`, + at `max`.
17
18
  */
18
- export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden = false, value, onChange, min = 0, max = Number.POSITIVE_INFINITY, step = 1, decrementLabel, incrementLabel, error, changed = false, disabled = false, readOnly = false, valueText, 'aria-describedby': describedBy, placeholder, name, width = 'fixed', limitAppearance = 'faded', }, ref) {
19
+ export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden = false, value, onChange, min = 0, max = Number.POSITIVE_INFINITY, step = 1, decrementLabel, incrementLabel, error, changed = false, disabled = false, readOnly = false, valueText, unit, 'aria-describedby': describedBy, placeholder, name, width = 'fixed', limitAppearance = 'faded', }, ref) {
19
20
  const bounds = { min, max, step };
20
21
  assertStepperBounds(bounds);
22
+ assertStepperUnit(unit);
21
23
  if (!stepperLimitAppearances.includes(limitAppearance))
22
24
  throw new RangeError(`Unknown Stepper limitAppearance: ${String(limitAppearance)}`);
23
25
  if (value !== null && !Number.isSafeInteger(value))
@@ -76,10 +78,10 @@ export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden
76
78
  return (_jsxs("div", { className: cx('sw-stepper', width === 'full' && 'sw-stepper-full', invalid && 'sw-stepper-invalid', changed && !invalid && 'sw-stepper-changed', readOnly && 'sw-stepper-readonly', limitAppearance === 'plain' &&
77
79
  !disabled &&
78
80
  !readOnly &&
79
- 'sw-stepper-limit-plain'), children: [_jsx("label", { className: labelVisuallyHidden ? 'sw-sr-only' : undefined, htmlFor: inputId, id: labelId, children: _jsx(FieldLabelText, { label: label }) }), _jsxs("div", { "aria-labelledby": labelId, className: "sw-stepper-track", role: "group", children: [_jsx("button", { "aria-controls": inputId, "aria-label": decrementLabel, className: "sw-stepper-button", disabled: moves.down === null, onClick: () => move(moves.down), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: '−' }) }), _jsx("input", { ref: ref, "aria-describedby": [describedBy?.trim() || null, invalid ? errorId : null]
80
- .filter((id) => id !== null)
81
- .join(' ') || undefined, "aria-invalid": invalid || undefined, "aria-valuemax": Number.isFinite(max) ? max : undefined, "aria-valuemin": min, "aria-valuenow": value ?? undefined, "aria-valuetext": valueText, autoComplete: "off", className: "sw-stepper-input", disabled: disabled, id: inputId,
82
- // iOS's numeric pad has no minus key, so a range below zero takes
83
- // the text keyboard.
84
- inputMode: allowsNegative ? 'text' : 'numeric', name: name, onBlur: () => setDraft(null), onChange: onType, onKeyDown: onKeyDown, placeholder: placeholder, readOnly: readOnly, role: "spinbutton", type: "text", value: text }), _jsx("button", { "aria-controls": inputId, "aria-label": incrementLabel, className: "sw-stepper-button", disabled: moves.up === null, onClick: () => move(moves.up), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: "+" }) })] }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
81
+ 'sw-stepper-limit-plain'), children: [_jsx("label", { className: labelVisuallyHidden ? 'sw-sr-only' : undefined, htmlFor: inputId, id: labelId, children: _jsx(FieldLabelText, { label: label }) }), _jsxs("div", { "aria-labelledby": labelId, className: "sw-stepper-track", role: "group", children: [_jsx("button", { "aria-controls": inputId, "aria-label": decrementLabel, className: "sw-stepper-button", disabled: moves.down === null, onClick: () => move(moves.down), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: '−' }) }), _jsx(StepperCountFrame, { unit: unit, children: _jsx("input", { ref: ref, "aria-describedby": [describedBy?.trim() || null, invalid ? errorId : null]
82
+ .filter((id) => id !== null)
83
+ .join(' ') || undefined, "aria-invalid": invalid || undefined, "aria-valuemax": Number.isFinite(max) ? max : undefined, "aria-valuemin": min, "aria-valuenow": value ?? undefined, "aria-valuetext": stepperValueText(value, unit, valueText), autoComplete: "off", className: "sw-stepper-input", disabled: disabled, id: inputId,
84
+ // iOS's numeric pad has no minus key, so a range below zero takes
85
+ // the text keyboard.
86
+ inputMode: allowsNegative ? 'text' : 'numeric', name: name, onBlur: () => setDraft(null), onChange: onType, onKeyDown: onKeyDown, placeholder: placeholder, readOnly: readOnly, role: "spinbutton", type: "text", value: text }) }), _jsx("button", { "aria-controls": inputId, "aria-label": incrementLabel, className: "sw-stepper-button", disabled: moves.up === null, onClick: () => move(moves.up), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: "+" }) })] }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
85
87
  });
@@ -7,7 +7,10 @@ const defaultElement = {
7
7
  data: 'span',
8
8
  display: 'h1',
9
9
  heading: 'h2',
10
+ headline: 'h2',
11
+ hero: 'h1',
10
12
  label: 'span',
13
+ lead: 'p',
11
14
  title: 'h3',
12
15
  };
13
16
  export const Text = forwardRef(function Text({ align, as, className, color = 'text', style, truncate = false, variant = 'body', ...rest }, ref) {
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from 'react';
2
+ export type StepperCountFrameProps = {
3
+ /** The count's input. */
4
+ children: ReactNode;
5
+ /** A short word after the count; no frame is drawn without one. */
6
+ unit?: string;
7
+ };
8
+ /**
9
+ * The count with its unit inside one control frame ("90 min"), as Field's
10
+ * suffix: the frame draws the control surface and focus ring, and the input
11
+ * inside it drops its own. The unit is hidden from assistive technology,
12
+ * which hears it in the count's `aria-valuetext` instead. Without a unit the
13
+ * input is the frame, unchanged.
14
+ */
15
+ export declare function StepperCountFrame({ children, unit }: StepperCountFrameProps): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /*
3
+ * A press on the frame's own chrome (the unit or the padding) focuses the
4
+ * count, as the text cursor over it promises. A press on the input itself
5
+ * keeps the browser's own caret placement and selection.
6
+ */
7
+ function focusCount(event) {
8
+ const input = event.currentTarget.querySelector('input');
9
+ if (!input || input.disabled || event.target === input)
10
+ return;
11
+ event.preventDefault();
12
+ input.focus();
13
+ }
14
+ /**
15
+ * The count with its unit inside one control frame ("90 min"), as Field's
16
+ * suffix: the frame draws the control surface and focus ring, and the input
17
+ * inside it drops its own. The unit is hidden from assistive technology,
18
+ * which hears it in the count's `aria-valuetext` instead. Without a unit the
19
+ * input is the frame, unchanged.
20
+ */
21
+ export function StepperCountFrame({ children, unit }) {
22
+ if (unit === undefined)
23
+ return _jsx(_Fragment, { children: children });
24
+ return (_jsxs("span", { className: "sw-stepper-count", onMouseDown: focusCount, children: [children, _jsx("span", { "aria-hidden": "true", className: "sw-stepper-unit", children: unit.trim() })] }));
25
+ }
@@ -43,6 +43,7 @@ import { cssSwitchClasses } from './css-switch.js';
43
43
  import { cssTextClasses } from './css-text.js';
44
44
  import { cssFontMonoClasses } from './css-font-mono.js';
45
45
  import { cssSkeletonClasses } from './css-skeleton.js';
46
+ import { cssEmptyStateClasses } from './css-empty-state.js';
46
47
  import { cssSpinnerClasses } from './css-spinner.js';
47
48
  import { cssProgressClasses } from './css-progress.js';
48
49
  import { cssNoticeClasses } from './css-notice.js';
@@ -51,6 +52,7 @@ import { cssTooltipClasses } from './css-tooltip.js';
51
52
  import { cssInfoTipClasses } from './css-info-tip.js';
52
53
  import { cssSeparatorClasses } from './css-separator.js';
53
54
  import { cssBandClasses } from './css-band.js';
55
+ import { cssHeroClasses } from './css-hero.js';
54
56
  import { cssSiteFooterClasses } from './css-site-footer.js';
55
57
  import { cssSiteHeaderClasses } from './css-site-header.js';
56
58
  import { cssStatTileClasses } from './css-stat-tile.js';
@@ -200,6 +202,8 @@ ${cssSectionNavClasses()}
200
202
 
201
203
  ${cssBandClasses()}
202
204
 
205
+ ${cssHeroClasses()}
206
+
203
207
  ${cssSiteFooterClasses()}
204
208
 
205
209
  ${cssSiteHeaderClasses()}
@@ -237,6 +241,8 @@ ${cssSpinnerClasses()}
237
241
 
238
242
  ${cssSkeletonClasses()}
239
243
 
244
+ ${cssEmptyStateClasses()}
245
+
240
246
  ${cssProgressClasses()}
241
247
 
242
248
  ${cssNoticeClasses()}
@@ -0,0 +1,2 @@
1
+ export declare function cssEmptyStateClasses(): string;
2
+ export declare function emptyStateClassCatalog(): string[];
@@ -0,0 +1,77 @@
1
+ import { coarsePointerQuery, touchTarget } from './touch-target.js';
2
+ /*
3
+ * EmptyState: a centred column on the surface inside a dashed hairline
4
+ * (the md radius), spacing step 8 above and below and step 4 at the sides,
5
+ * its parts a step 3 apart. The glyph sits in a decorative step-8 square of
6
+ * the subtle fill in the muted colour, a direct-child SVG drawn at step 5.
7
+ * The title and line keep a readable measure and wrap; a long word breaks.
8
+ * The actions are a centred row that wraps, a step 2 apart, each Button a
9
+ * 44 px target on a coarse pointer. Forced colours keep the dashed edge in
10
+ * the system's border colour and draw the glyph in CanvasText.
11
+ */
12
+ export function cssEmptyStateClasses() {
13
+ return `.sw-empty-state {
14
+ align-items: center;
15
+ background: var(--sw-color-surface);
16
+ border: 1px dashed var(--sw-color-border);
17
+ border-radius: var(--sw-radius-md);
18
+ box-sizing: border-box;
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: var(--sw-space-3);
22
+ min-width: 0;
23
+ overflow-wrap: anywhere;
24
+ padding: var(--sw-space-8) var(--sw-space-4);
25
+ text-align: center;
26
+ width: 100%;
27
+ }
28
+
29
+ .sw-empty-state-icon {
30
+ align-items: center;
31
+ background: var(--sw-color-subtle);
32
+ border-radius: var(--sw-radius-md);
33
+ color: var(--sw-color-muted);
34
+ display: inline-flex;
35
+ flex: none;
36
+ height: var(--sw-space-8);
37
+ justify-content: center;
38
+ width: var(--sw-space-8);
39
+ }
40
+
41
+ .sw-empty-state-icon > svg {
42
+ height: var(--sw-space-5);
43
+ width: var(--sw-space-5);
44
+ }
45
+
46
+ .sw-empty-state-title,
47
+ .sw-empty-state-description {
48
+ margin: 0;
49
+ max-width: var(--sw-reading-max);
50
+ }
51
+
52
+ .sw-empty-state-actions {
53
+ display: flex;
54
+ flex-wrap: wrap;
55
+ gap: var(--sw-space-2);
56
+ justify-content: center;
57
+ margin-top: var(--sw-space-1);
58
+ }
59
+
60
+ @media ${coarsePointerQuery} {
61
+ .sw-empty-state-actions .sw-button { min-height: ${touchTarget}; }
62
+ }
63
+
64
+ @media (forced-colors: active) {
65
+ .sw-empty-state { border-color: CanvasText; }
66
+ .sw-empty-state-icon { color: CanvasText; }
67
+ }`;
68
+ }
69
+ export function emptyStateClassCatalog() {
70
+ return [
71
+ 'sw-empty-state',
72
+ 'sw-empty-state-icon',
73
+ 'sw-empty-state-title',
74
+ 'sw-empty-state-description',
75
+ 'sw-empty-state-actions',
76
+ ];
77
+ }
@@ -1,5 +1,5 @@
1
1
  import { breakpointQuery, breakpoints } from './breakpoints.js';
2
- import { gridColumnCounts, gridColumnsBelowClass, gridColumnsClass, } from './css-grid.js';
2
+ import { gridColumnCounts, gridColumnsBelowClass, gridColumnsClass, gridFitClass, gridFits, } from './css-grid.js';
3
3
  export const gridColumnSpans = gridColumnCounts;
4
4
  export function gridSpanClass(span) {
5
5
  return `sw-grid-span-${span}`;
@@ -21,6 +21,9 @@ function widerSpans(columns) {
21
21
  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
+ // 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; }`));
24
27
  // Below a breakpoint every span wider than one is restated for the phone
25
28
  // column count, which also undoes a desktop cap when that count is larger.
26
29
  const below = breakpoints.map((breakpoint) => `@media ${breakpointQuery('below', breakpoint)} {
@@ -28,7 +31,7 @@ ${gridColumnCounts
28
31
  .flatMap((columns) => widerSpans(1).map((span) => ` ${capRule(gridColumnsBelowClass(breakpoint, columns), columns, span)}`))
29
32
  .join('\n')}
30
33
  }`);
31
- return [...spans, ...caps, ...below].join('\n');
34
+ return [...spans, ...caps, ...fitted, ...below].join('\n');
32
35
  }
33
36
  export function gridSpanClassCatalog() {
34
37
  return gridColumnSpans.map((span) => gridSpanClass(span));
@@ -1,6 +1,19 @@
1
1
  import { type Breakpoint } from './breakpoints.js';
2
2
  export declare const gridColumnCounts: readonly [1, 2, 3, 4, 6];
3
3
  export type GridColumns = (typeof gridColumnCounts)[number];
4
+ /**
5
+ * A fitted grid's narrowest column: as many columns as fit the grid's width
6
+ * at this minimum, each stretched to share the row, and one column when
7
+ * the grid is narrower than the minimum.
8
+ */
9
+ export declare const gridFitWidths: {
10
+ readonly narrow: "15rem";
11
+ readonly medium: "16.25rem";
12
+ readonly wide: "18.75rem";
13
+ };
14
+ export type GridFit = keyof typeof gridFitWidths;
15
+ export declare const gridFits: GridFit[];
16
+ export declare function gridFitClass(fit: GridFit): string;
4
17
  export declare function gridColumnsClass(columns: GridColumns): string;
5
18
  export declare function gridColumnsBelowClass(breakpoint: Breakpoint, columns: GridColumns): string;
6
19
  export declare function cssGridClasses(): string;
@@ -1,5 +1,19 @@
1
1
  import { breakpointQuery, breakpoints, } from './breakpoints.js';
2
2
  export const gridColumnCounts = [1, 2, 3, 4, 6];
3
+ /**
4
+ * A fitted grid's narrowest column: as many columns as fit the grid's width
5
+ * at this minimum, each stretched to share the row, and one column when
6
+ * the grid is narrower than the minimum.
7
+ */
8
+ export const gridFitWidths = {
9
+ narrow: '15rem',
10
+ medium: '16.25rem',
11
+ wide: '18.75rem',
12
+ };
13
+ export const gridFits = Object.keys(gridFitWidths);
14
+ export function gridFitClass(fit) {
15
+ return `sw-grid-fit-${fit}`;
16
+ }
3
17
  export function gridColumnsClass(columns) {
4
18
  return `sw-grid-cols-${columns}`;
5
19
  }
@@ -20,12 +34,14 @@ ${gridColumnCounts
20
34
  .map((count) => columnsRule(gridColumnsBelowClass(breakpoint, count), count))
21
35
  .join('\n')}
22
36
  }`);
23
- return [base, ...columns, ...below].join('\n');
37
+ const fitted = gridFits.map((fit) => `.${gridFitClass(fit)} { grid-template-columns: repeat(auto-fit, minmax(min(100%, ${gridFitWidths[fit]}), 1fr)); }`);
38
+ return [base, ...columns, ...below, ...fitted].join('\n');
24
39
  }
25
40
  export function gridClassCatalog() {
26
41
  return [
27
42
  'sw-grid',
28
43
  ...gridColumnCounts.map((count) => gridColumnsClass(count)),
29
44
  ...breakpoints.flatMap((breakpoint) => gridColumnCounts.map((count) => gridColumnsBelowClass(breakpoint, count))),
45
+ ...gridFits.map(gridFitClass),
30
46
  ];
31
47
  }
@@ -0,0 +1,2 @@
1
+ export declare function cssHeroClasses(): string;
2
+ export declare function heroClassCatalog(): string[];
@@ -0,0 +1,62 @@
1
+ import { breakpointQuery } from './breakpoints.js';
2
+ /*
3
+ * A public page's opening: the words in one column (an eyebrow, the hero
4
+ * title, the lead and the actions) and an optional picture beside them.
5
+ * From `md` the picture keeps its own width at the row's end and the words
6
+ * take the rest; below `md` the picture sits centred under the words and
7
+ * the actions stack to the full width, as a phone's thumb expects. With no
8
+ * picture the words keep the reading width, so a long title or lead does
9
+ * not run the page's full width. The actions sit one spacing step further
10
+ * from the lead than the text's gap, so the buttons read as their own row.
11
+ */
12
+ export function cssHeroClasses() {
13
+ return `.sw-hero {
14
+ align-items: center;
15
+ display: grid;
16
+ gap: var(--sw-space-8);
17
+ min-width: 0;
18
+ }
19
+
20
+ .sw-hero-text {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: var(--sw-space-4);
24
+ min-width: 0;
25
+ }
26
+
27
+ .sw-hero:not(.sw-hero-with-media) .sw-hero-text {
28
+ max-width: var(--sw-reading-max);
29
+ }
30
+
31
+ .sw-hero-actions {
32
+ display: flex;
33
+ flex-wrap: wrap;
34
+ gap: var(--sw-space-3);
35
+ padding-top: var(--sw-space-1);
36
+ }
37
+
38
+ .sw-hero-media {
39
+ justify-self: center;
40
+ max-width: 100%;
41
+ min-width: 0;
42
+ }
43
+
44
+ @media ${breakpointQuery('from', 'md')} {
45
+ .sw-hero-text { gap: var(--sw-space-5); }
46
+ .sw-hero-with-media { grid-template-columns: minmax(0, 1fr) auto; }
47
+ }
48
+
49
+ @media ${breakpointQuery('below', 'md')} {
50
+ .sw-hero { gap: var(--sw-space-6); }
51
+ .sw-hero-actions { align-items: stretch; flex-direction: column; }
52
+ }`;
53
+ }
54
+ export function heroClassCatalog() {
55
+ return [
56
+ 'sw-hero',
57
+ 'sw-hero-with-media',
58
+ 'sw-hero-text',
59
+ 'sw-hero-actions',
60
+ 'sw-hero-media',
61
+ ];
62
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A Stepper with a `unit`: the count and its muted word ("90 min") inside
3
+ * one control frame, as Field's suffix. The frame draws the canvas control
4
+ * surface, the focus ring and the states; the input inside drops its own and
5
+ * keeps its few-digit width, its digits against the unit, so the frame never
6
+ * follows the count. The pair is centred, also in a full-width stepper.
7
+ */
8
+ export declare function cssStepperUnitClasses(): string;
9
+ export declare function stepperUnitClassCatalog(): string[];
@@ -0,0 +1,102 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ import { controlSurface, disabledControlForcedColors, disabledControlSurface, } from './css-document.js';
3
+ import { changedTintLayer } from './css-field-changed.js';
4
+ const body = typographyVariants.body;
5
+ /*
6
+ * Outranks the stepper's own input rules and state rules (the invalid
7
+ * border, the changed shadow and tint), which the frame draws instead.
8
+ */
9
+ const framedInput = '[data-theme] .sw-stepper .sw-stepper-count > input.sw-stepper-input';
10
+ const tintedFrame = '.sw-stepper-changed .sw-stepper-count:not(:has(> input:placeholder-shown))';
11
+ /**
12
+ * A Stepper with a `unit`: the count and its muted word ("90 min") inside
13
+ * one control frame, as Field's suffix. The frame draws the canvas control
14
+ * surface, the focus ring and the states; the input inside drops its own and
15
+ * keeps its few-digit width, its digits against the unit, so the frame never
16
+ * follows the count. The pair is centred, also in a full-width stepper.
17
+ */
18
+ export function cssStepperUnitClasses() {
19
+ return `.sw-stepper-count {
20
+ ${controlSurface}
21
+ align-items: center;
22
+ box-sizing: border-box;
23
+ cursor: text;
24
+ display: inline-flex;
25
+ gap: var(--sw-space-1);
26
+ justify-content: center;
27
+ min-height: var(--sw-control-md-min-height);
28
+ min-width: 0;
29
+ padding-inline: var(--sw-space-2);
30
+ }
31
+
32
+ .sw-stepper-full .sw-stepper-count { display: flex; }
33
+
34
+ ${framedInput} {
35
+ align-self: stretch;
36
+ background: transparent;
37
+ background-image: none;
38
+ border: 0;
39
+ border-radius: 0;
40
+ box-shadow: none;
41
+ inline-size: 5ch;
42
+ min-height: 0;
43
+ padding-inline: 0;
44
+ text-align: end;
45
+ }
46
+
47
+ ${framedInput}:focus-visible { outline: none; }
48
+
49
+ .sw-stepper-count:focus-within {
50
+ outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
51
+ outline-offset: var(--sw-focus-ring-offset);
52
+ }
53
+
54
+ .sw-stepper-unit {
55
+ color: var(--sw-color-muted);
56
+ flex: none;
57
+ font-size: ${body.fontSize}px;
58
+ line-height: ${body.lineHeight}px;
59
+ user-select: none;
60
+ white-space: nowrap;
61
+ }
62
+
63
+ .sw-stepper-invalid .sw-stepper-count { border-color: var(--sw-color-danger); }
64
+
65
+ .sw-stepper-changed .sw-stepper-count {
66
+ border-color: var(--sw-color-accent);
67
+ box-shadow: inset 0 0 0 1px var(--sw-color-accent);
68
+ }
69
+
70
+ ${tintedFrame} { background-image: ${changedTintLayer}; }
71
+
72
+ ${tintedFrame} > .sw-stepper-unit { color: var(--sw-color-text); }
73
+
74
+ /*
75
+ * The frame draws the surface, so it carries the disabled look for its count,
76
+ * and only that look: a locked count drops the invalid and changed edges and
77
+ * tint, which name nothing a person can act on.
78
+ */
79
+ .sw-stepper .sw-stepper-count:has(> input:disabled) {
80
+ ${disabledControlSurface}
81
+ background-image: none;
82
+ border-color: var(--sw-color-border);
83
+ box-shadow: none;
84
+ }
85
+
86
+ .sw-stepper .sw-stepper-count:has(> input:disabled) > .sw-stepper-unit {
87
+ color: var(--sw-color-muted);
88
+ }
89
+
90
+ @media (forced-colors: active) {
91
+ .sw-stepper-unit { color: CanvasText; }
92
+ .sw-stepper-invalid .sw-stepper-count { border-color: Mark; }
93
+ .sw-stepper-changed .sw-stepper-count { border-color: Highlight; box-shadow: inset 0 0 0 1px Highlight; background-image: none; }
94
+ .sw-stepper .sw-stepper-count:has(> input:disabled) {
95
+ ${disabledControlForcedColors}
96
+ box-shadow: none;
97
+ }
98
+ }`;
99
+ }
100
+ export function stepperUnitClassCatalog() {
101
+ return ['sw-stepper-count', 'sw-stepper-unit'];
102
+ }
@@ -1,5 +1,6 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { changedTintLayer } from './css-field-changed.js';
3
+ import { cssStepperUnitClasses, stepperUnitClassCatalog, } from './css-stepper-unit.js';
3
4
  const body = typographyVariants.body;
4
5
  const label = typographyVariants.label;
5
6
  const title = typographyVariants.title;
@@ -116,7 +117,9 @@ ${input} {
116
117
 
117
118
  .sw-stepper-full input.sw-stepper-input { inline-size: 100%; }
118
119
 
119
- ${stateRules()}`;
120
+ ${stateRules()}
121
+
122
+ ${cssStepperUnitClasses()}`;
120
123
  }
121
124
  export function stepperClassCatalog() {
122
125
  return [
@@ -129,5 +132,6 @@ export function stepperClassCatalog() {
129
132
  'sw-stepper-changed',
130
133
  'sw-stepper-readonly',
131
134
  'sw-stepper-limit-plain',
135
+ ...stepperUnitClassCatalog(),
132
136
  ];
133
137
  }
@@ -19,6 +19,7 @@ import { countPillClassCatalog } from './css-count-pill.js';
19
19
  import { checkboxClassCatalog } from './css-checkbox.js';
20
20
  import { radioGroupClassCatalog } from './css-radio-group.js';
21
21
  import { skeletonClassCatalog } from './css-skeleton.js';
22
+ import { emptyStateClassCatalog } from './css-empty-state.js';
22
23
  import { spinnerClassCatalog } from './css-spinner.js';
23
24
  import { progressClassCatalog } from './css-progress.js';
24
25
  import { noticeClassCatalog } from './css-notice.js';
@@ -27,6 +28,7 @@ import { tooltipClassCatalog } from './css-tooltip.js';
27
28
  import { infoTipClassCatalog } from './css-info-tip.js';
28
29
  import { separatorClassCatalog } from './css-separator.js';
29
30
  import { bandClassCatalog } from './css-band.js';
31
+ import { heroClassCatalog } from './css-hero.js';
30
32
  import { siteFooterClassCatalog } from './css-site-footer.js';
31
33
  import { siteHeaderClassCatalog } from './css-site-header.js';
32
34
  import { statTileClassCatalog } from './css-stat-tile.js';
@@ -132,6 +134,7 @@ export function utilityClassCatalog() {
132
134
  ...radioGroupClassCatalog(),
133
135
  ...spinnerClassCatalog(),
134
136
  ...skeletonClassCatalog(),
137
+ ...emptyStateClassCatalog(),
135
138
  ...progressClassCatalog(),
136
139
  ...noticeClassCatalog(),
137
140
  ...glyphTileClassCatalog(),
@@ -139,6 +142,7 @@ export function utilityClassCatalog() {
139
142
  ...infoTipClassCatalog(),
140
143
  ...separatorClassCatalog(),
141
144
  ...bandClassCatalog(),
145
+ ...heroClassCatalog(),
142
146
  ...siteFooterClassCatalog(),
143
147
  ...siteHeaderClassCatalog(),
144
148
  ...statTileClassCatalog(),
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export { listTileColumnCounts, type ListGroupTileColumns, } from './css/css-list
17
17
  export { Checkbox, type CheckboxProps } from './components/Checkbox.js';
18
18
  export { Spinner, type SpinnerProps, type SpinnerSize, } from './components/Spinner.js';
19
19
  export { Skeleton, skeletonVariants, type SkeletonProps, type SkeletonVariant, } from './components/Skeleton.js';
20
+ export { EmptyState, emptyStateTitleElements, type EmptyStateProps, type EmptyStateTitleElement, } from './components/EmptyState.js';
20
21
  export { GlyphTile, glyphTileSizes, type GlyphTileHue, type GlyphTileProps, type GlyphTileSize, } from './components/GlyphTile.js';
21
22
  export { Notice, noticeTones, noticeVariants, type NoticeProps, type NoticeRole, type NoticeTone, type NoticeVariant, } from './components/Notice.js';
22
23
  export { Progress, progressTones, type ProgressProps, type ProgressTone, } from './components/Progress.js';
@@ -79,14 +80,16 @@ export { Band, bandElements, bandTones, bandWidths, } from './components/Band.js
79
80
  export type { BandElement, BandProps, BandTone, BandWidth, } from './components/Band.js';
80
81
  export { SiteHeader } from './components/SiteHeader.js';
81
82
  export type { SiteHeaderItem, SiteHeaderLabels, SiteHeaderProps, } from './components/SiteHeader.js';
83
+ export { Hero } from './components/Hero.js';
84
+ export type { HeroProps } from './components/Hero.js';
82
85
  export { SiteFooter } from './components/SiteFooter.js';
83
86
  export type { SiteFooterGroup, SiteFooterLink, SiteFooterProps, } from './components/SiteFooter.js';
84
87
  export { Separator } from './components/Separator.js';
85
88
  export type { SeparatorOrientation, SeparatorProps, } from './components/Separator.js';
86
89
  export { DescriptionItem, DescriptionList, descriptionListLayouts, } from './components/DescriptionList.js';
87
90
  export type { DescriptionItemProps, DescriptionListLayout, DescriptionListProps, } from './components/DescriptionList.js';
88
- export { Grid } from './components/Grid.js';
89
- export type { GridColumns, GridProps } from './components/Grid.js';
91
+ export { Grid, gridFits } from './components/Grid.js';
92
+ export type { GridColumns, GridFit, GridProps } from './components/Grid.js';
90
93
  export type { GridColumnSpan } from './css/css-grid-span.js';
91
94
  export type { ColumnSpanProps } from './column-span-classes.js';
92
95
  export { DenominationGrid, denominationCellToneAppearances, denominationGridFrames, denominationLabelAppearances, denominationNegativeAppearances, denominationTotalEmphases, denominationTransposeBreakpoints, } from './components/DenominationGrid.js';
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ export { listTileColumnCounts, } from './css/css-list-tiles.js';
16
16
  export { Checkbox } from './components/Checkbox.js';
17
17
  export { Spinner, } from './components/Spinner.js';
18
18
  export { Skeleton, skeletonVariants, } from './components/Skeleton.js';
19
+ export { EmptyState, emptyStateTitleElements, } from './components/EmptyState.js';
19
20
  export { GlyphTile, glyphTileSizes, } from './components/GlyphTile.js';
20
21
  export { Notice, noticeTones, noticeVariants, } from './components/Notice.js';
21
22
  export { Progress, progressTones, } from './components/Progress.js';
@@ -71,10 +72,11 @@ export { utilityClassCatalog } from './css/stylesheet.js';
71
72
  export { breakpointQuery } from './css/breakpoints.js';
72
73
  export { Band, bandElements, bandTones, bandWidths, } from './components/Band.js';
73
74
  export { SiteHeader } from './components/SiteHeader.js';
75
+ export { Hero } from './components/Hero.js';
74
76
  export { SiteFooter } from './components/SiteFooter.js';
75
77
  export { Separator } from './components/Separator.js';
76
78
  export { DescriptionItem, DescriptionList, descriptionListLayouts, } from './components/DescriptionList.js';
77
- export { Grid } from './components/Grid.js';
79
+ export { Grid, gridFits } from './components/Grid.js';
78
80
  export { DenominationGrid, denominationCellToneAppearances, denominationGridFrames, denominationLabelAppearances, denominationNegativeAppearances, denominationTotalEmphases, denominationTransposeBreakpoints, } from './components/DenominationGrid.js';
79
81
  export { FilterChips, filterChipCountAppearances, filterChipsOverflows, filterChipTones, } from './components/FilterChips.js';
80
82
  export { FilterChipsRow } from './components/FilterChipsRow.js';
@@ -35,3 +35,11 @@ export declare function stepperMoves(value: number | null, bounds: StepperBounds
35
35
  export declare function readStepperEntry(text: string, allowsNegative: boolean): StepperEntry;
36
36
  /** The box's text for a count: the plain number, or empty for none. */
37
37
  export declare function stepperText(value: number | null): string;
38
+ /** A consumer bug: a unit, when given, is a word to show after the count. */
39
+ export declare function assertStepperUnit(unit: string | undefined): void;
40
+ /**
41
+ * The count in words for assistive technology: the consumer's own words
42
+ * when given, otherwise the count and its unit ("90 min"), or nothing extra
43
+ * (the bare count is `aria-valuenow`) when there is no unit or no count.
44
+ */
45
+ export declare function stepperValueText(value: number | null, unit: string | undefined, valueText: string | undefined): string | undefined;
@@ -48,3 +48,20 @@ export function readStepperEntry(text, allowsNegative) {
48
48
  export function stepperText(value) {
49
49
  return value === null ? '' : String(value);
50
50
  }
51
+ /** A consumer bug: a unit, when given, is a word to show after the count. */
52
+ export function assertStepperUnit(unit) {
53
+ if (unit !== undefined && unit.trim() === '')
54
+ throw new RangeError('Stepper unit must not be blank');
55
+ }
56
+ /**
57
+ * The count in words for assistive technology: the consumer's own words
58
+ * when given, otherwise the count and its unit ("90 min"), or nothing extra
59
+ * (the bare count is `aria-valuenow`) when there is no unit or no count.
60
+ */
61
+ export function stepperValueText(value, unit, valueText) {
62
+ if (valueText !== undefined)
63
+ return valueText;
64
+ if (unit === undefined || value === null)
65
+ return undefined;
66
+ return `${value} ${unit.trim()}`;
67
+ }
package/dist/styles.css CHANGED
@@ -894,19 +894,25 @@ body {
894
894
  .sw-text-align-center { text-align: center; }
895
895
  .sw-text-align-end { text-align: end; }
896
896
 
897
- :where(.sw-text-display, .sw-text-heading, .sw-text-title, .sw-text-body, .sw-text-label, .sw-text-caption, .sw-text-data) { margin: 0; }
897
+ :where(.sw-text-hero, .sw-text-display, .sw-text-headline, .sw-text-heading, .sw-text-title, .sw-text-lead, .sw-text-body, .sw-text-label, .sw-text-caption, .sw-text-data) { margin: 0; }
898
898
 
899
+ .sw-text-hero { font-family: var(--sw-font-sans); font-size: 56px; line-height: 60px; font-weight: 700; letter-spacing: -1.7px; }
899
900
  .sw-text-display { font-family: var(--sw-font-sans); font-size: 40px; line-height: 48px; font-weight: 600; letter-spacing: -0.8px; }
901
+ .sw-text-headline { font-family: var(--sw-font-sans); font-size: 36px; line-height: 40px; font-weight: 700; letter-spacing: -0.7px; }
900
902
  .sw-text-heading { font-family: var(--sw-font-sans); font-size: 28px; line-height: 34px; font-weight: 600; letter-spacing: -0.45px; }
901
903
  .sw-text-title { font-family: var(--sw-font-sans); font-size: 20px; line-height: 28px; font-weight: 600; letter-spacing: -0.3px; }
904
+ .sw-text-lead { font-family: var(--sw-font-sans); font-size: 20px; line-height: 29px; font-weight: 400; letter-spacing: -0.3px; }
902
905
  .sw-text-body { font-family: var(--sw-font-sans); font-size: 17px; line-height: 25px; font-weight: 400; letter-spacing: -0.2px; }
903
906
  .sw-text-label { font-family: var(--sw-font-sans); font-size: 15px; line-height: 20px; font-weight: 600; letter-spacing: -0.2px; }
904
907
  .sw-text-caption { font-family: var(--sw-font-sans); font-size: 13px; line-height: 18px; font-weight: 400; letter-spacing: -0.08px; }
905
908
  .sw-text-data { font-family: var(--sw-font-sans); font-size: 13px; line-height: 18px; font-weight: 500; letter-spacing: -0.08px; font-variant-numeric: tabular-nums; }
906
909
 
907
910
  @media not all and (min-width: 48rem) {
911
+ .sw-text-hero { font-size: 36px; line-height: 40px; letter-spacing: -0.7px; }
908
912
  .sw-text-display { font-size: 32px; line-height: 38px; letter-spacing: -0.6px; }
913
+ .sw-text-headline { font-size: 26px; line-height: 30px; letter-spacing: -0.4px; }
909
914
  .sw-text-heading { font-size: 24px; line-height: 30px; letter-spacing: -0.4px; }
915
+ .sw-text-lead { font-size: 17px; line-height: 25px; letter-spacing: -0.2px; }
910
916
  }
911
917
 
912
918
  [data-theme] .sw-font-mono,
@@ -4600,6 +4606,47 @@ body {
4600
4606
  .sw-band-inverse { border-block: 1px solid CanvasText; }
4601
4607
  }
4602
4608
 
4609
+ .sw-hero {
4610
+ align-items: center;
4611
+ display: grid;
4612
+ gap: var(--sw-space-8);
4613
+ min-width: 0;
4614
+ }
4615
+
4616
+ .sw-hero-text {
4617
+ display: flex;
4618
+ flex-direction: column;
4619
+ gap: var(--sw-space-4);
4620
+ min-width: 0;
4621
+ }
4622
+
4623
+ .sw-hero:not(.sw-hero-with-media) .sw-hero-text {
4624
+ max-width: var(--sw-reading-max);
4625
+ }
4626
+
4627
+ .sw-hero-actions {
4628
+ display: flex;
4629
+ flex-wrap: wrap;
4630
+ gap: var(--sw-space-3);
4631
+ padding-top: var(--sw-space-1);
4632
+ }
4633
+
4634
+ .sw-hero-media {
4635
+ justify-self: center;
4636
+ max-width: 100%;
4637
+ min-width: 0;
4638
+ }
4639
+
4640
+ @media (min-width: 48rem) {
4641
+ .sw-hero-text { gap: var(--sw-space-5); }
4642
+ .sw-hero-with-media { grid-template-columns: minmax(0, 1fr) auto; }
4643
+ }
4644
+
4645
+ @media not all and (min-width: 48rem) {
4646
+ .sw-hero { gap: var(--sw-space-6); }
4647
+ .sw-hero-actions { align-items: stretch; flex-direction: column; }
4648
+ }
4649
+
4603
4650
  .sw-site-footer-top {
4604
4651
  display: grid;
4605
4652
  gap: var(--sw-space-6) var(--sw-space-5);
@@ -7141,6 +7188,62 @@ body {
7141
7188
  .sw-skeleton-row + .sw-skeleton-row { border-color: GrayText; }
7142
7189
  }
7143
7190
 
7191
+ .sw-empty-state {
7192
+ align-items: center;
7193
+ background: var(--sw-color-surface);
7194
+ border: 1px dashed var(--sw-color-border);
7195
+ border-radius: var(--sw-radius-md);
7196
+ box-sizing: border-box;
7197
+ display: flex;
7198
+ flex-direction: column;
7199
+ gap: var(--sw-space-3);
7200
+ min-width: 0;
7201
+ overflow-wrap: anywhere;
7202
+ padding: var(--sw-space-8) var(--sw-space-4);
7203
+ text-align: center;
7204
+ width: 100%;
7205
+ }
7206
+
7207
+ .sw-empty-state-icon {
7208
+ align-items: center;
7209
+ background: var(--sw-color-subtle);
7210
+ border-radius: var(--sw-radius-md);
7211
+ color: var(--sw-color-muted);
7212
+ display: inline-flex;
7213
+ flex: none;
7214
+ height: var(--sw-space-8);
7215
+ justify-content: center;
7216
+ width: var(--sw-space-8);
7217
+ }
7218
+
7219
+ .sw-empty-state-icon > svg {
7220
+ height: var(--sw-space-5);
7221
+ width: var(--sw-space-5);
7222
+ }
7223
+
7224
+ .sw-empty-state-title,
7225
+ .sw-empty-state-description {
7226
+ margin: 0;
7227
+ max-width: var(--sw-reading-max);
7228
+ }
7229
+
7230
+ .sw-empty-state-actions {
7231
+ display: flex;
7232
+ flex-wrap: wrap;
7233
+ gap: var(--sw-space-2);
7234
+ justify-content: center;
7235
+ margin-top: var(--sw-space-1);
7236
+ }
7237
+
7238
+ @media (pointer: coarse) {
7239
+ .sw-empty-state-actions .sw-button { min-height: var(--sw-control-md-min-height); }
7240
+ }
7241
+
7242
+ @media (forced-colors: active) {
7243
+ .sw-empty-state { border-color: CanvasText; }
7244
+ .sw-empty-state-icon { color: CanvasText; }
7245
+ }
7246
+
7144
7247
  .sw-progress {
7145
7248
  display: flex;
7146
7249
  flex-direction: column;
@@ -7868,6 +7971,9 @@ body {
7868
7971
  .sw-grid-cols-below-md-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
7869
7972
  .sw-grid-cols-below-md-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
7870
7973
  }
7974
+ .sw-grid-fit-narrow { grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); }
7975
+ .sw-grid-fit-medium { grid-template-columns: repeat(auto-fit, minmax(min(100%, 16.25rem), 1fr)); }
7976
+ .sw-grid-fit-wide { grid-template-columns: repeat(auto-fit, minmax(min(100%, 18.75rem), 1fr)); }
7871
7977
  .sw-grid-span-1 { grid-column: span 1; }
7872
7978
  .sw-grid-span-2 { grid-column: span 2; }
7873
7979
  .sw-grid-span-3 { grid-column: span 3; }
@@ -7883,6 +7989,18 @@ body {
7883
7989
  :where(.sw-grid-cols-3) > .sw-grid-span-4 { grid-column: span 3; }
7884
7990
  :where(.sw-grid-cols-3) > .sw-grid-span-6 { grid-column: span 3; }
7885
7991
  :where(.sw-grid-cols-4) > .sw-grid-span-6 { grid-column: span 4; }
7992
+ :where(.sw-grid-fit-narrow) > .sw-grid-span-2 { grid-column: auto; }
7993
+ :where(.sw-grid-fit-narrow) > .sw-grid-span-3 { grid-column: auto; }
7994
+ :where(.sw-grid-fit-narrow) > .sw-grid-span-4 { grid-column: auto; }
7995
+ :where(.sw-grid-fit-narrow) > .sw-grid-span-6 { grid-column: auto; }
7996
+ :where(.sw-grid-fit-medium) > .sw-grid-span-2 { grid-column: auto; }
7997
+ :where(.sw-grid-fit-medium) > .sw-grid-span-3 { grid-column: auto; }
7998
+ :where(.sw-grid-fit-medium) > .sw-grid-span-4 { grid-column: auto; }
7999
+ :where(.sw-grid-fit-medium) > .sw-grid-span-6 { grid-column: auto; }
8000
+ :where(.sw-grid-fit-wide) > .sw-grid-span-2 { grid-column: auto; }
8001
+ :where(.sw-grid-fit-wide) > .sw-grid-span-3 { grid-column: auto; }
8002
+ :where(.sw-grid-fit-wide) > .sw-grid-span-4 { grid-column: auto; }
8003
+ :where(.sw-grid-fit-wide) > .sw-grid-span-6 { grid-column: auto; }
7886
8004
  @media not all and (min-width: 48rem) {
7887
8005
  :where(.sw-grid-cols-below-md-1) > .sw-grid-span-2 { grid-column: span 1; }
7888
8006
  :where(.sw-grid-cols-below-md-1) > .sw-grid-span-3 { grid-column: span 1; }
@@ -8915,6 +9033,99 @@ body {
8915
9033
  [data-theme] .sw-stepper-changed input.sw-stepper-input { border-color: Highlight; box-shadow: inset 0 0 0 1px Highlight; background-image: none; }
8916
9034
  }
8917
9035
 
9036
+ .sw-stepper-count {
9037
+ background-color: var(--sw-glass-fill);
9038
+ border: 1px solid var(--sw-color-border);
9039
+ border-radius: var(--sw-radius-sm);
9040
+ color: inherit;
9041
+ font-family: inherit;
9042
+ font-size: inherit;
9043
+ padding-inline: var(--sw-control-md-padding-inline);
9044
+ align-items: center;
9045
+ box-sizing: border-box;
9046
+ cursor: text;
9047
+ display: inline-flex;
9048
+ gap: var(--sw-space-1);
9049
+ justify-content: center;
9050
+ min-height: var(--sw-control-md-min-height);
9051
+ min-width: 0;
9052
+ padding-inline: var(--sw-space-2);
9053
+ }
9054
+
9055
+ .sw-stepper-full .sw-stepper-count { display: flex; }
9056
+
9057
+ [data-theme] .sw-stepper .sw-stepper-count > input.sw-stepper-input {
9058
+ align-self: stretch;
9059
+ background: transparent;
9060
+ background-image: none;
9061
+ border: 0;
9062
+ border-radius: 0;
9063
+ box-shadow: none;
9064
+ inline-size: 5ch;
9065
+ min-height: 0;
9066
+ padding-inline: 0;
9067
+ text-align: end;
9068
+ }
9069
+
9070
+ [data-theme] .sw-stepper .sw-stepper-count > input.sw-stepper-input:focus-visible { outline: none; }
9071
+
9072
+ .sw-stepper-count:focus-within {
9073
+ outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
9074
+ outline-offset: var(--sw-focus-ring-offset);
9075
+ }
9076
+
9077
+ .sw-stepper-unit {
9078
+ color: var(--sw-color-muted);
9079
+ flex: none;
9080
+ font-size: 17px;
9081
+ line-height: 25px;
9082
+ user-select: none;
9083
+ white-space: nowrap;
9084
+ }
9085
+
9086
+ .sw-stepper-invalid .sw-stepper-count { border-color: var(--sw-color-danger); }
9087
+
9088
+ .sw-stepper-changed .sw-stepper-count {
9089
+ border-color: var(--sw-color-accent);
9090
+ box-shadow: inset 0 0 0 1px var(--sw-color-accent);
9091
+ }
9092
+
9093
+ .sw-stepper-changed .sw-stepper-count:not(:has(> input:placeholder-shown)) { background-image: linear-gradient(color-mix(in srgb, var(--sw-color-accent) 12%, transparent), color-mix(in srgb, var(--sw-color-accent) 12%, transparent)); }
9094
+
9095
+ .sw-stepper-changed .sw-stepper-count:not(:has(> input:placeholder-shown)) > .sw-stepper-unit { color: var(--sw-color-text); }
9096
+
9097
+ /*
9098
+ * The frame draws the surface, so it carries the disabled look for its count,
9099
+ * and only that look: a locked count drops the invalid and changed edges and
9100
+ * tint, which name nothing a person can act on.
9101
+ */
9102
+ .sw-stepper .sw-stepper-count:has(> input:disabled) {
9103
+ background-color: var(--sw-color-subtle);
9104
+ border-style: dashed;
9105
+ cursor: not-allowed;
9106
+ opacity: 1;
9107
+ -webkit-text-fill-color: currentColor;
9108
+ background-image: none;
9109
+ border-color: var(--sw-color-border);
9110
+ box-shadow: none;
9111
+ }
9112
+
9113
+ .sw-stepper .sw-stepper-count:has(> input:disabled) > .sw-stepper-unit {
9114
+ color: var(--sw-color-muted);
9115
+ }
9116
+
9117
+ @media (forced-colors: active) {
9118
+ .sw-stepper-unit { color: CanvasText; }
9119
+ .sw-stepper-invalid .sw-stepper-count { border-color: Mark; }
9120
+ .sw-stepper-changed .sw-stepper-count { border-color: Highlight; box-shadow: inset 0 0 0 1px Highlight; background-image: none; }
9121
+ .sw-stepper .sw-stepper-count:has(> input:disabled) {
9122
+ border-color: GrayText;
9123
+ color: GrayText;
9124
+ -webkit-text-fill-color: GrayText;
9125
+ box-shadow: none;
9126
+ }
9127
+ }
9128
+
8918
9129
  .sw-steps {
8919
9130
  display: flex;
8920
9131
  flex-direction: column;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.48.0",
3
+ "version": "1.49.0",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "dependencies": {
46
46
  "markdown-it": "14.3.2",
47
47
  "pdfjs-dist": "6.4.299",
48
- "@scalewing/tokens": "^1.6.0"
48
+ "@scalewing/tokens": "^1.7.0"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "react": "^19.0.0",