@scalewing/react 1.44.0 → 1.45.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,17 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.45.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4170f23: `Article` `contentLang`: the language of the title and the body (a BCP 47 tag such as `es`) when it is not the page's, set as `lang` on the title's heading and the body's box only, never the eyebrow, meta line, actions or cover, which are the app's words (WCAG 3.1.2). The article's name is unchanged. A blank `contentLang` throws a `RangeError`. No new classes, tokens or dependencies. Request: `docs/requests/teisoro-article-content-lang.md`.
8
+ - fada31f: `ListRow` `titleLang`: the title's language (a BCP 47 tag such as `es`) when it is not the page's, set as `lang` on the title's box only (its words and the wordless unread dot), never the detail, value, tag or unread words (WCAG 3.1.2). A pressable row's name and description are unchanged; it works with `truncate`. A blank `titleLang` throws a `RangeError`. No new classes, tokens or dependencies. Request: `docs/requests/teisoro-list-row-title-lang.md`.
9
+ - 8ed264e: `Step` `titleDetail`: muted words after an open step's title, on its line, such as "Optional" or "Wetlands done · Woodlands empty", at the label size and body weight in the muted colour, a space-2 after the title on its baseline or on their own line under it where they do not fit. Drawn while the step is open (current, error, or any step of `Steps open`), outside the heading, so the heading and the region keep the title's name; an open list's region is described by the words. A blank `titleDetail` throws a `RangeError`. New classes `sw-steps-title-line` and `sw-steps-title-detail`. No new tokens, no new dependency. Request: `docs/requests/teisoro-step-title-detail.md`.
10
+
11
+ ### Patch Changes
12
+
13
+ - 54907b3: `Steps`: a folded step's heading reads "The site, Done", not "The site , Done" (checked against Chromium's accessibility tree; the name is one text box, so there are no two boxes to space apart). Its name is now one visually hidden line (the title, a comma and the status word) and the drawn title is `aria-hidden` on its own box; the step's region and its Edit button are still named by the drawn title, and nothing drawn changes. A current step, and a skipped step showing its reason, are unchanged. The heading's text content now holds the title twice (drawn, then in the spoken line), so a test reading a folded heading's `textContent` or finding its title with a pattern (`getByText(/The site/)`) finds both: query headings by role and name (`getByRole('heading', { name: 'The site, Done' })`). Copying a folded heading, or finding its title in the page, meets the words twice too. No new classes, tokens or dependencies. Request: `docs/requests/teisoro-steps-status-words.md`.
14
+
3
15
  ## 1.44.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -78,7 +78,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
78
78
 
79
79
  `AppHeader` is the page's top chrome, sticky by default (`sticky={false}` leaves it in flow). Its default `appearance="glass"` is a rounded frosted card. `appearance="bar"` (`AppHeaderAppearance`, `appHeaderAppearances`) is a full-width band of the solid surface with the border hairline under it: no blur, no rounding, no side or top border, the same under Reduce Transparency, and the hairline kept in forced colors. Set it against the page's edges (no side gutter round it) and put the gutter inside it. An unknown `appearance` throws a `RangeError`.
80
80
 
81
- `Box hideBelow="md"` hides a region under 48rem and `hideFrom="md"` hides it at 48rem and wider, so a product can swap a wide layout for a narrow one without its own stylesheet. They also take `lg` (64rem), for content that fits only from a laptop up, such as a row of labelled destinations that a tablet shows as glyphs (name each glyph-only control, for example with `Tooltip relationship="label"`); the layout props keep `md`. `breakpointQuery('below', 'lg')` returns the query a hide class uses (`not all and (min-width: 64rem)`), for an app whose script must follow the same width. An unknown breakpoint throws a `RangeError`. Hidden regions leave the accessibility tree; do not use this to protect data. `Box as="a"` is a layout link. Use `Button` for press actions. `Box as="a" link="quiet"` (opt-in; `link="accent"`, the default, is the canvas link) is a title that opens something, such as a table row's title: it keeps the surrounding text's colour at weight 600 (`sw-link-quiet`) and is underlined on hover and keyboard focus. Its weight is the non-colour cue WCAG 1.4.1 asks for (axe's `link-in-text-block` passes it on colour, the same colour and background as its text, or on its weight; the weight is what keeps it visibly a link), so use it in text lighter than 600. In text already at 600 the quiet look has no cue: an accent link there needs at least 3:1 contrast against the text. `link` on a box that is not `as="a"` throws a `TypeError`, an unknown value a `RangeError`. `Field` associates a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Steps` is a one-page vertical stepper: an `ol` of `Step` panels in order, numbered by their place, a line joining each marker to the next (in the success color below a done step). A `Step` takes `status` (`'upcoming' | 'current' | 'done' | 'error' | 'skipped'`) and `title` (a heading at the list's `headingLevel`, 2 by default, or the step's own). The current step is open with its `children` and has `aria-current="step"`; an error step stays open with a "!" marker. A done step folds to one line: a check, the title, its `summary` (truncated) and, with `onEdit` and `editLabel`, a ghost small `Button` at the end named by its label and the title ("Edit The site"). A step ahead is greyed with its number in a ring; a skipped step is greyed with its `skippedReason` after the title (pass your own separator, such as "· not needed"). A folded step's heading ends in its status word for assistive technology (visually hidden; English by default, `labels` on `Steps` takes your words). When a step becomes current from a folded state (upcoming, done or skipped), focus moves to its heading, a programmatic focus target; if several do in one update, the first in the list takes it. A first render, a move between current and error, and a step that turns to error leave focus alone: after a refused submit, move focus to the first invalid field yourself, as with `Field`. An error step has no `aria-current`; its heading's status word says it needs attention. Scalewing holds no order rules: the consumer sets each step's status. `Steps open` (off by default) keeps every step open at once, for a form whose sections are all filled on one page: each step shows its `children` whatever its status, inside a `section` named by its title (a region, class `sw-steps-region`), every title at the open size; the marker still follows the status (a number to fill, a check when done, "!" in error, a grey ring and muted title when greyed, such as after a blocking step), no step is `aria-current`, and no step takes focus when its status changes. Pass the `Step`s as direct children (a Fragment around several would share one number). A `Step` outside `Steps`, a Fragment child, a blank title or reason, an unknown status or heading level, `onEdit` without `editLabel`, or a blank status word throws a `RangeError`.
81
+ `Box hideBelow="md"` hides a region under 48rem and `hideFrom="md"` hides it at 48rem and wider, so a product can swap a wide layout for a narrow one without its own stylesheet. They also take `lg` (64rem), for content that fits only from a laptop up, such as a row of labelled destinations that a tablet shows as glyphs (name each glyph-only control, for example with `Tooltip relationship="label"`); the layout props keep `md`. `breakpointQuery('below', 'lg')` returns the query a hide class uses (`not all and (min-width: 64rem)`), for an app whose script must follow the same width. An unknown breakpoint throws a `RangeError`. Hidden regions leave the accessibility tree; do not use this to protect data. `Box as="a"` is a layout link. Use `Button` for press actions. `Box as="a" link="quiet"` (opt-in; `link="accent"`, the default, is the canvas link) is a title that opens something, such as a table row's title: it keeps the surrounding text's colour at weight 600 (`sw-link-quiet`) and is underlined on hover and keyboard focus. Its weight is the non-colour cue WCAG 1.4.1 asks for (axe's `link-in-text-block` passes it on colour, the same colour and background as its text, or on its weight; the weight is what keeps it visibly a link), so use it in text lighter than 600. In text already at 600 the quiet look has no cue: an accent link there needs at least 3:1 contrast against the text. `link` on a box that is not `as="a"` throws a `TypeError`, an unknown value a `RangeError`. `Field` associates a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Steps` is a one-page vertical stepper: an `ol` of `Step` panels in order, numbered by their place, a line joining each marker to the next (in the success color below a done step). A `Step` takes `status` (`'upcoming' | 'current' | 'done' | 'error' | 'skipped'`) and `title` (a heading at the list's `headingLevel`, 2 by default, or the step's own). The current step is open with its `children` and has `aria-current="step"`; an error step stays open with a "!" marker. A done step folds to one line: a check, the title, its `summary` (truncated) and, with `onEdit` and `editLabel`, a ghost small `Button` at the end named by its label and the title ("Edit The site"). A step ahead is greyed with its number in a ring; a skipped step is greyed with its `skippedReason` after the title (pass your own separator, such as "· not needed"). A folded step's heading ends in its status word for assistive technology (visually hidden; English by default, `labels` on `Steps` takes your words): its name is one visually hidden line, the title, a comma and the word ("The site, Done"), and the drawn title is `aria-hidden`, because a browser lays a hidden box out as a block and would put a space before the comma ("The site , Done"). The step's region and its Edit button are still named by the drawn title. A current step, and a skipped step showing its reason, draw and read their words as they are. The heading's text content now holds the title twice (drawn, then in the spoken line), so a test reading a folded heading's `textContent` or finding its title with a pattern (`getByText(/The site/)`) finds both: query headings by role and name (`getByRole('heading', { name: 'The site, Done' })`). Copying a folded heading, or finding its title in the page, meets the words twice too. The comma after the title is Scalewing's, as before; the word is yours. When a step becomes current from a folded state (upcoming, done or skipped), focus moves to its heading, a programmatic focus target; if several do in one update, the first in the list takes it. A first render, a move between current and error, and a step that turns to error leave focus alone: after a refused submit, move focus to the first invalid field yourself, as with `Field`. An error step has no `aria-current`; its heading's status word says it needs attention. Scalewing holds no order rules: the consumer sets each step's status. `Steps open` (off by default) keeps every step open at once, for a form whose sections are all filled on one page: each step shows its `children` whatever its status, inside a `section` named by its title (a region, class `sw-steps-region`), every title at the open size; the marker still follows the status (a number to fill, a check when done, "!" in error, a grey ring and muted title when greyed, such as after a blocking step), no step is `aria-current`, and no step takes focus when its status changes. `titleDetail` (opt-in) is muted words after an open step's title, on its line, such as "Optional" on a step a person may skip, or "Wetlands done · Woodlands empty" on a step with a part per habitat: the label size at the body's weight in the muted colour, a space-2 after the title on its baseline, or on a line of their own under the title where they do not fit beside it (a long title on a phone), never past the panel (a long word breaks). They are drawn while the step is open (current or error, or every step of an open list, with fields or without) and stay as the step turns done in an open list; a folded step draws its summary or reason instead. They sit outside the heading, so the heading's name stays the title (and its status word) and a heading list reads the titles alone; an open list's region keeps the title as its name and is described by the words, read as a person enters it, and in a stepped list they follow the heading in reading order. Write your own separators inside the words; Scalewing draws only the gap after the title. The words share a skipped step's reason's muted colour, not its size: a reason is part of the title, these are a note beside it. Give a step its `titleDetail` from its first render: setting or clearing it later moves the heading into or out of the title line, which remounts it (and drops its focus if it has it). A blank `titleDetail` throws a `RangeError`. Pass the `Step`s as direct children (a Fragment around several would share one number). A `Step` outside `Steps`, a Fragment child, a blank title or reason, an unknown status or heading level, `onEdit` without `editLabel`, or a blank status word throws a `RangeError`.
82
82
 
83
83
  ```tsx
84
84
  <Steps aria-label="Heron survey">
@@ -750,6 +750,8 @@ The consumer needs no PDF.js dependency of its own. With another bundler, build
750
750
 
751
751
  `Article` is a page that reads like a post, on a card (`variant`, as `Card`'s) no wider than the reading width (`--sw-reading-max`, 46rem): an optional `cover` photo (`{ src, alt }`, `alt` required, `''` when it only decorates) (or `null`) edge to edge across the top at 3:1 (2:1 below `md`) with the card's top corners; then, a space-6 from the top and bottom and a space-8 from the sides (space-5 all round below `md`), the `eyebrow` (muted caption type at the label's weight) with your `actions` at its row's end (such as a language switch; under the eyebrow, at its start, when the row is too narrow), the `title` in heading type (`titleAs`: `h1` by default, on the article's own page; `h2` or `h3` under other headings), the muted `meta` line (such as "Manager 1 · Oct 16 · 4 min read"), a hairline, and the body (`children`, your own content, such as paragraphs or Markdown your app draws). It is an `article` landmark named by its title, and its ref is that element; the eyebrow and meta are `div`s, so any content fits. The card sits at the start of a wider column. Empty parts draw nothing. A blank `title`, an unknown `titleAs` or `variant`, or a `cover` with a blank `src` or no `alt` throws a `RangeError`.
752
752
 
753
+ `contentLang` (opt-in) is the language of the title and the body, a BCP 47 tag such as `es`, when it is not the page's, such as a guide shown in Spanish on an English screen because it exists only in Spanish or the reader chose Español (WCAG 3.1.2, Language of Parts). It sets `lang` on the title's heading and on the body's box only; the eyebrow, the meta line, the actions and the cover's `alt` stay in the page's language, since they are the app's words (write the cover's `alt` in the page's language, or mark it yourself). The article's name is unchanged. Pass `lang` on the `Article` instead when every word in it, the app's included, is in that language. A blank `contentLang` throws a `RangeError`.
754
+
753
755
  ```tsx
754
756
  <Article
755
757
  cover={{ src: cover.url, alt: cover.alt }}
@@ -844,6 +846,8 @@ A row can also carry three trailing parts. `unread` marks it new for the person,
844
846
 
845
847
  `truncate` (opt-in) keeps a row's title and its detail line to one line each, cut with "…" where they do not fit, for a long list read at a glance such as a shelf of documents. The title's words sit in their own box, so the unread dot follows the cut words and is never hidden; the whole words stay in the row's name and description and in a `title` tooltip on the words and the detail. The value, its detail, the tag and the action keep their words whole. A tooltip needs a pointer, so on a touch screen or from the keyboard the cut words are whole only in what the row opens and in what assistive technology reads; keep `truncate` for rows that open their whole content, or whose words a person does not need in full to choose.
846
848
 
849
+ `titleLang` (opt-in) is the title's language, a BCP 47 tag such as `es`, when it is not the page's, such as a document's Spanish title in a list on an English page (WCAG 3.1.2, Language of Parts). It sets `lang` on the title's box, which holds the title's words and the wordless unread dot, so a screen reader speaks the title in that language and the browser hyphenates and shapes it as such; the detail, the value and its detail, the tag, the unread words and the action stay in the page's language (give a tag its own `lang` inside it if it needs one). A pressable row's name and description are unchanged: the title is still read first. It works with `truncate` (the words box sits inside the marked title) and on every kind of row. A blank `titleLang` throws a `RangeError`.
850
+
847
851
  ```tsx
848
852
  <ListRow
849
853
  leading={
@@ -26,6 +26,14 @@ export type ArticleProps = Omit<HTMLAttributes<HTMLElement>, 'children' | 'title
26
26
  actions?: ReactNode;
27
27
  /** The card look, as `Card`'s. */
28
28
  variant?: CardVariant;
29
+ /**
30
+ * The language of the title and the body (a BCP 47 tag such as `es`)
31
+ * when it is not the page's, such as a document shown in Spanish on an
32
+ * English screen (WCAG 3.1.2): `lang` on the title's heading and the
33
+ * body only, never the eyebrow, meta line, actions or cover, which are
34
+ * the app's own words.
35
+ */
36
+ contentLang?: string;
29
37
  /** The article's body, under a hairline. */
30
38
  children?: ReactNode;
31
39
  };
@@ -34,8 +42,9 @@ export type ArticleProps = Omit<HTMLAttributes<HTMLElement>, 'children' | 'title
34
42
  * cover photo edge to edge across the top, then the eyebrow (with the
35
43
  * consumer's actions at its row's end), the title in heading type, the
36
44
  * muted meta line, a hairline, and the body. The article is named by its
37
- * title (an `aria-labelledby` you pass replaces that). The ref is the
38
- * `article` element.
45
+ * title (an `aria-labelledby` you pass replaces that). `contentLang` marks
46
+ * the title and the body as another language than the page's. The ref is
47
+ * the `article` element.
39
48
  */
40
49
  export declare const Article: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLElement>, "title" | "children"> & {
41
50
  /** The article's title, in heading type. */
@@ -55,6 +64,14 @@ export declare const Article: import("react").ForwardRefExoticComponent<Omit<HTM
55
64
  actions?: ReactNode;
56
65
  /** The card look, as `Card`'s. */
57
66
  variant?: CardVariant;
67
+ /**
68
+ * The language of the title and the body (a BCP 47 tag such as `es`)
69
+ * when it is not the page's, such as a document shown in Spanish on an
70
+ * English screen (WCAG 3.1.2): `lang` on the title's heading and the
71
+ * body only, never the eyebrow, meta line, actions or cover, which are
72
+ * the app's own words.
73
+ */
74
+ contentLang?: string;
58
75
  /** The article's body, under a hairline. */
59
76
  children?: ReactNode;
60
77
  } & import("react").RefAttributes<HTMLElement>>;
@@ -11,9 +11,12 @@ function hasContent(node) {
11
11
  typeof node !== 'boolean' &&
12
12
  node !== '');
13
13
  }
14
- function assertArticle({ title, titleAs, cover, variant }) {
14
+ function assertArticle({ title, titleAs, cover, variant, contentLang, }) {
15
15
  if (typeof title !== 'string' || title.trim() === '')
16
16
  throw new RangeError('Article title must be non-empty text');
17
+ if (contentLang !== undefined &&
18
+ (typeof contentLang !== 'string' || contentLang.trim() === ''))
19
+ throw new RangeError('Article contentLang must be a non-empty language tag');
17
20
  if (titleAs !== undefined && !articleTitleElements.includes(titleAs))
18
21
  throw new RangeError(`Unknown Article titleAs: ${String(titleAs)}`);
19
22
  if (variant !== undefined && !cardVariants.includes(variant))
@@ -30,13 +33,14 @@ function assertArticle({ title, titleAs, cover, variant }) {
30
33
  * cover photo edge to edge across the top, then the eyebrow (with the
31
34
  * consumer's actions at its row's end), the title in heading type, the
32
35
  * muted meta line, a hairline, and the body. The article is named by its
33
- * title (an `aria-labelledby` you pass replaces that). The ref is the
34
- * `article` element.
36
+ * title (an `aria-labelledby` you pass replaces that). `contentLang` marks
37
+ * the title and the body as another language than the page's. The ref is
38
+ * the `article` element.
35
39
  */
36
40
  export const Article = forwardRef(function Article(props, ref) {
37
41
  assertArticle(props);
38
- const { title, titleAs = 'h1', eyebrow, meta, cover, actions, variant = 'glass', children, className, ...rest } = props;
42
+ const { title, titleAs = 'h1', eyebrow, meta, cover, actions, variant = 'glass', contentLang, children, className, ...rest } = props;
39
43
  const titleId = useId();
40
44
  const top = hasContent(eyebrow) || hasContent(actions);
41
- return (_jsxs("article", { "aria-labelledby": titleId, className: cx('sw-card', `sw-card-${variant}`, 'sw-article', className), ref: ref, ...rest, children: [cover ? (_jsx("img", { alt: cover.alt, className: "sw-article-cover", src: cover.src })) : null, _jsxs("div", { className: "sw-article-body", children: [_jsxs("header", { className: "sw-article-header", children: [top ? (_jsxs("div", { className: "sw-article-top", children: [hasContent(eyebrow) ? (_jsx("div", { className: "sw-article-eyebrow", children: eyebrow })) : null, hasContent(actions) ? (_jsx("div", { className: "sw-article-actions", children: actions })) : null] })) : null, _jsx(Text, { as: titleAs, className: "sw-article-title", id: titleId, variant: "heading", children: title }), hasContent(meta) ? (_jsx("div", { className: "sw-article-meta", children: meta })) : null] }), _jsx(Separator, { decorative: true }), hasContent(children) ? (_jsx("div", { className: "sw-article-content", children: children })) : null] })] }));
45
+ return (_jsxs("article", { "aria-labelledby": titleId, className: cx('sw-card', `sw-card-${variant}`, 'sw-article', className), ref: ref, ...rest, children: [cover ? (_jsx("img", { alt: cover.alt, className: "sw-article-cover", src: cover.src })) : null, _jsxs("div", { className: "sw-article-body", children: [_jsxs("header", { className: "sw-article-header", children: [top ? (_jsxs("div", { className: "sw-article-top", children: [hasContent(eyebrow) ? (_jsx("div", { className: "sw-article-eyebrow", children: eyebrow })) : null, hasContent(actions) ? (_jsx("div", { className: "sw-article-actions", children: actions })) : null] })) : null, _jsx(Text, { as: titleAs, className: "sw-article-title", id: titleId, lang: contentLang, variant: "heading", children: title }), hasContent(meta) ? (_jsx("div", { className: "sw-article-meta", children: meta })) : null] }), _jsx(Separator, { decorative: true }), hasContent(children) ? (_jsx("div", { className: "sw-article-content", lang: contentLang, children: children })) : null] })] }));
42
46
  });
@@ -3,6 +3,13 @@ import { type BadgeTone } from '../css/css-data.js';
3
3
  import { type ListRowTitleAppearance, type ListRowTone, type ListRowValueTone } from '../list-options.js';
4
4
  export type ListRowProps = {
5
5
  title: string;
6
+ /**
7
+ * The title's language (a BCP 47 tag such as `es`) when it is not the
8
+ * page's, such as a document's Spanish title on an English screen (WCAG
9
+ * 3.1.2): `lang` on the title's box only (its words and the wordless
10
+ * unread dot), never the detail, value, tag or unread words.
11
+ */
12
+ titleLang?: string;
6
13
  /** A muted line under the title. */
7
14
  detail?: string;
8
15
  /** A figure or word at the row's end, such as "+$1.48". */
@@ -83,7 +90,8 @@ export type ListRowProps = {
83
90
  * One row of a `ListGroup`: it opens something (`onPress` or `href`, with
84
91
  * a chevron, the whole row pressing), picks one option in place
85
92
  * (`selected`), or only states something (neither). `unread` marks it new,
86
- * `tag` adds a status before its end, `action` a control beside it, and
87
- * `truncate` keeps its title and detail to one line each.
93
+ * `tag` adds a status before its end, `action` a control beside it,
94
+ * `truncate` keeps its title and detail to one line each, and `titleLang`
95
+ * marks a title in another language than the page's.
88
96
  */
89
- export declare function ListRow({ title, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled, tone, titleAppearance, accessory, unread, unreadLabel, tag, action, truncate, 'aria-label': ariaLabel, }: ListRowProps): import("react").JSX.Element;
97
+ export declare function ListRow({ title, titleLang, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled, tone, titleAppearance, accessory, unread, unreadLabel, tag, action, truncate, 'aria-label': ariaLabel, }: ListRowProps): import("react").JSX.Element;
@@ -12,13 +12,15 @@ import { rowNaming } from './list/row-naming.js';
12
12
  * One row of a `ListGroup`: it opens something (`onPress` or `href`, with
13
13
  * a chevron, the whole row pressing), picks one option in place
14
14
  * (`selected`), or only states something (neither). `unread` marks it new,
15
- * `tag` adds a status before its end, `action` a control beside it, and
16
- * `truncate` keeps its title and detail to one line each.
15
+ * `tag` adds a status before its end, `action` a control beside it,
16
+ * `truncate` keeps its title and detail to one line each, and `titleLang`
17
+ * marks a title in another language than the page's.
17
18
  */
18
- export function ListRow({ title, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled = false, tone = 'default', titleAppearance = 'strong', accessory = 'chevron', unread = false, unreadLabel, tag, action, truncate = false, 'aria-label': ariaLabel, }) {
19
+ export function ListRow({ title, titleLang, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled = false, tone = 'default', titleAppearance = 'strong', accessory = 'chevron', unread = false, unreadLabel, tag, action, truncate = false, 'aria-label': ariaLabel, }) {
19
20
  const pressable = onPress !== undefined || href !== undefined;
20
21
  assertListRow({
21
22
  title,
23
+ titleLang,
22
24
  tone,
23
25
  titleAppearance,
24
26
  valueTone,
@@ -33,7 +35,7 @@ export function ListRow({ title, detail, value, valueDetail, valueTone, valueBad
33
35
  });
34
36
  const id = useId();
35
37
  const kind = listRowAccessory({ pressable, selected, accessory });
36
- const content = (_jsx(ListRowContent, { accessory: kind, detail: detail, id: id, leading: leading, tag: tag, title: title, titleAppearance: titleAppearance, tone: tone, truncate: truncate, unreadLabel: unread ? unreadLabel : undefined, value: value, valueBadge: valueBadge, valueDetail: valueDetail, valueTone: valueTone }));
38
+ const content = (_jsx(ListRowContent, { accessory: kind, detail: detail, id: id, leading: leading, tag: tag, title: title, titleAppearance: titleAppearance, titleLang: titleLang, tone: tone, truncate: truncate, unreadLabel: unread ? unreadLabel : undefined, value: value, valueBadge: valueBadge, valueDetail: valueDetail, valueTone: valueTone }));
37
39
  const className = cx('sw-list-row', pressable && 'sw-list-row-pressable', disabled && 'sw-list-row-disabled', truncate && 'sw-list-row-truncate');
38
40
  const naming = rowNaming(id, {
39
41
  'aria-label': ariaLabel,
@@ -5,6 +5,8 @@ export type ListRowContentProps = {
5
5
  /** Prefixes the ids that name and describe a pressable row. */
6
6
  id: string;
7
7
  title: string;
8
+ /** The title's language when it is not the page's; on its words only. */
9
+ titleLang?: string;
8
10
  detail?: string;
9
11
  value?: string;
10
12
  valueDetail?: string;
@@ -25,5 +27,7 @@ export type ListRowContentProps = {
25
27
  * (with the unread dot and its hidden words) over its detail, the value
26
28
  * over its detail, the tag, then the chevron or check. A truncated row
27
29
  * sets its title's words in their own box, so the "…" never hides the dot.
30
+ * `titleLang` goes on the title's box, which holds its words and the
31
+ * wordless dot only; the unread words after it stay in the page's language.
28
32
  */
29
- export declare function ListRowContent({ id, title, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, truncate, }: ListRowContentProps): import("react").JSX.Element;
33
+ export declare function ListRowContent({ id, title, titleLang, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, truncate, }: ListRowContentProps): import("react").JSX.Element;
@@ -8,7 +8,9 @@ import { hasSlot } from '../slot.js';
8
8
  * (with the unread dot and its hidden words) over its detail, the value
9
9
  * over its detail, the tag, then the chevron or check. A truncated row
10
10
  * sets its title's words in their own box, so the "…" never hides the dot.
11
+ * `titleLang` goes on the title's box, which holds its words and the
12
+ * wordless dot only; the unread words after it stay in the page's language.
11
13
  */
12
- export function ListRowContent({ id, title, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, truncate, }) {
13
- return (_jsxs(_Fragment, { children: [!hasSlot(leading) ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-leading", children: leading })), _jsxs("span", { className: "sw-list-row-label", children: [_jsxs("span", { className: cx('sw-list-row-title', titleAppearance === 'plain' && 'sw-list-row-title-plain', tone === 'danger' && 'sw-list-row-title-danger'), id: `${id}-title`, children: [truncate ? (_jsx("span", { className: "sw-list-row-title-text", title: title, children: title })) : (title), unreadLabel === undefined ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-unread" }))] }), unreadLabel === undefined ? null : (_jsx("span", { className: "sw-sr-only", id: `${id}-unread`, children: unreadLabel })), detail ? (_jsx("span", { className: "sw-list-row-detail", id: `${id}-detail`, title: truncate ? detail : undefined, children: detail })) : null] }), value === undefined ? null : (_jsxs("span", { className: "sw-list-row-end", children: [_jsx("span", { className: cx('sw-list-row-value', valueTone && `sw-list-row-value-${valueTone}`), id: `${id}-value`, children: valueBadge ? _jsx(Badge, { tone: valueBadge, children: value }) : value }), valueDetail ? (_jsx("span", { className: "sw-list-row-value-detail", id: `${id}-value-detail`, children: valueDetail })) : null] })), !hasSlot(tag) ? null : (_jsx("span", { className: "sw-list-row-tag", id: `${id}-tag`, children: tag })), _jsx(ListRowAccessoryMark, { kind: accessory })] }));
14
+ export function ListRowContent({ id, title, titleLang, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, truncate, }) {
15
+ return (_jsxs(_Fragment, { children: [!hasSlot(leading) ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-leading", children: leading })), _jsxs("span", { className: "sw-list-row-label", children: [_jsxs("span", { className: cx('sw-list-row-title', titleAppearance === 'plain' && 'sw-list-row-title-plain', tone === 'danger' && 'sw-list-row-title-danger'), id: `${id}-title`, lang: titleLang, children: [truncate ? (_jsx("span", { className: "sw-list-row-title-text", title: title, children: title })) : (title), unreadLabel === undefined ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-unread" }))] }), unreadLabel === undefined ? null : (_jsx("span", { className: "sw-sr-only", id: `${id}-unread`, children: unreadLabel })), detail ? (_jsx("span", { className: "sw-list-row-detail", id: `${id}-detail`, title: truncate ? detail : undefined, children: detail })) : null] }), value === undefined ? null : (_jsxs("span", { className: "sw-list-row-end", children: [_jsx("span", { className: cx('sw-list-row-value', valueTone && `sw-list-row-value-${valueTone}`), id: `${id}-value`, children: valueBadge ? _jsx(Badge, { tone: valueBadge, children: value }) : value }), valueDetail ? (_jsx("span", { className: "sw-list-row-value-detail", id: `${id}-value-detail`, children: valueDetail })) : null] })), !hasSlot(tag) ? null : (_jsx("span", { className: "sw-list-row-tag", id: `${id}-tag`, children: tag })), _jsx(ListRowAccessoryMark, { kind: accessory })] }));
14
16
  }
@@ -11,6 +11,14 @@ export type StepProps = {
11
11
  editLabel?: string;
12
12
  /** Muted words after a skipped step's title, such as "not needed". */
13
13
  skippedReason?: string;
14
+ /**
15
+ * Muted words after an open step's title, on its line, such as
16
+ * "Optional" or "Wetlands done · Woodlands empty". Drawn while the step
17
+ * is open (current or error, or any step of an open list) and outside
18
+ * the heading, so the heading and the region keep the title's name; an
19
+ * open list's region is described by them.
20
+ */
21
+ titleDetail?: string;
14
22
  /** Overrides the list's heading level for this step's title. */
15
23
  headingLevel?: StepHeadingLevel;
16
24
  /** The step's fields, shown while it is current or in error, or always in an open list. */
@@ -21,6 +29,7 @@ export type StepProps = {
21
29
  * and, by status, its fields (current, error) or one folded line (a done
22
30
  * step's summary and Edit; an upcoming or skipped step's title alone). In
23
31
  * an open list every step shows its fields, inside a region named by its
24
- * title.
32
+ * title. An open step's `titleDetail` is muted words beside its title,
33
+ * outside the heading.
25
34
  */
26
35
  export declare function Step(props: StepProps): import("react").JSX.Element;
@@ -1,22 +1,13 @@
1
1
  'use client';
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { Children, useContext, useEffect, useId, useRef, } from 'react';
4
4
  import { cx } from '../../class-names.js';
5
5
  import { Button } from '../Button.js';
6
- import { isOpenStep, stepHeadingLevels, stepStatuses, stepStatusWord, stepTakesFocus, } from './step-status.js';
6
+ import { isOpenStep, stepStatusWord, stepTakesFocus, } from './step-status.js';
7
+ import { assertStep } from './assert-step.js';
7
8
  import { StepPlaceContext } from './steps-context.js';
8
- function assertStep(props, level) {
9
- if (!props.title.trim())
10
- throw new RangeError('Step title must not be empty');
11
- if (!stepStatuses.includes(props.status))
12
- throw new RangeError(`Unknown Step status: ${String(props.status)}`);
13
- if (!stepHeadingLevels.includes(level))
14
- throw new RangeError('Step headingLevel must be 2, 3 or 4');
15
- if (props.onEdit && !props.editLabel?.trim())
16
- throw new RangeError('Step onEdit needs an editLabel');
17
- if (props.skippedReason !== undefined && !props.skippedReason.trim())
18
- throw new RangeError('Step skippedReason must not be empty');
19
- }
9
+ import { StepTitleLine } from './StepTitleLine.js';
10
+ import { StepTitleWords } from './StepTitleWords.js';
20
11
  /**
21
12
  * Moves focus to the heading when the step becomes current from a folded
22
13
  * state, unless an earlier step of the list already took it in this commit
@@ -46,7 +37,8 @@ function useFocusWhenOpened(status, heading, focusClaimRef, open) {
46
37
  * and, by status, its fields (current, error) or one folded line (a done
47
38
  * step's summary and Edit; an upcoming or skipped step's title alone). In
48
39
  * an open list every step shows its fields, inside a region named by its
49
- * title.
40
+ * title. An open step's `titleDetail` is muted words beside its title,
41
+ * outside the heading.
50
42
  */
51
43
  export function Step(props) {
52
44
  const place = useContext(StepPlaceContext);
@@ -54,17 +46,21 @@ export function Step(props) {
54
46
  throw new RangeError('Step must be a child of Steps');
55
47
  const level = props.headingLevel ?? place.headingLevel;
56
48
  assertStep(props, level);
57
- const { status, title, summary, onEdit, editLabel, skippedReason, children } = props;
49
+ const { status, title, summary, onEdit, editLabel, skippedReason, titleDetail, children, } = props;
58
50
  const heading = useRef(null);
59
51
  const titleId = useId();
60
52
  const editId = useId();
53
+ const detailId = useId();
61
54
  useFocusWhenOpened(status, heading, place.focusClaimRef, place.open);
62
55
  const Heading = `h${level}`;
63
56
  const reason = status === 'skipped' ? skippedReason : undefined;
64
57
  const word = stepStatusWord(status, place.labels, reason !== undefined);
65
58
  const done = status === 'done';
66
- const header = (_jsxs("div", { className: "sw-steps-header", children: [_jsx("span", { "aria-hidden": "true", className: "sw-steps-marker", children: status === 'error' ? '!' : done ? null : place.number }), _jsxs(Heading, { ref: heading, className: "sw-steps-title", tabIndex: -1, children: [_jsx("span", { id: titleId, children: title }), reason ? (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-steps-reason", children: reason })] })) : null, word ? _jsxs("span", { className: "sw-sr-only", children: [", ", word] }) : null] }), done && summary ? (_jsx("span", { className: "sw-steps-summary", children: summary })) : null, done && onEdit ? (_jsx(Button, { "aria-labelledby": `${editId} ${titleId}`, className: "sw-steps-edit", id: editId, onPress: onEdit, size: "sm", variant: "ghost", children: editLabel })) : null] }));
67
- const shown = (place.open || isOpenStep(status)) && Children.toArray(children).length > 0;
59
+ const shownOpen = place.open || isOpenStep(status);
60
+ const detail = shownOpen ? titleDetail : undefined;
61
+ const headingElement = (_jsx(Heading, { ref: heading, className: "sw-steps-title", tabIndex: -1, children: word === null ? (_jsx(StepTitleWords, { reason: reason, title: title, titleId: titleId, word: null })) : (_jsx(StepTitleWords, { title: title, titleId: titleId, word: word })) }));
62
+ const header = (_jsxs("div", { className: "sw-steps-header", children: [_jsx("span", { "aria-hidden": "true", className: "sw-steps-marker", children: status === 'error' ? '!' : done ? null : place.number }), _jsx(StepTitleLine, { detail: detail, detailId: detailId, hasDetail: titleDetail !== undefined, children: headingElement }), done && summary ? (_jsx("span", { className: "sw-steps-summary", children: summary })) : null, done && onEdit ? (_jsx(Button, { "aria-labelledby": `${editId} ${titleId}`, className: "sw-steps-edit", id: editId, onPress: onEdit, size: "sm", variant: "ghost", children: editLabel })) : null] }));
63
+ const shown = shownOpen && Children.toArray(children).length > 0;
68
64
  const content = (_jsxs(_Fragment, { children: [header, shown ? _jsx("div", { className: "sw-steps-body", children: children }) : null] }));
69
- return (_jsx("li", { "aria-current": !place.open && status === 'current' ? 'step' : undefined, className: cx('sw-steps-item', `sw-steps-item-${status}`), children: place.open ? (_jsx("section", { "aria-labelledby": titleId, className: "sw-steps-region", children: content })) : (content) }));
65
+ return (_jsx("li", { "aria-current": !place.open && status === 'current' ? 'step' : undefined, className: cx('sw-steps-item', `sw-steps-item-${status}`), children: place.open ? (_jsx("section", { "aria-describedby": detail ? detailId : undefined, "aria-labelledby": titleId, className: "sw-steps-region", children: content })) : (content) }));
70
66
  }
@@ -0,0 +1,19 @@
1
+ import { type ReactNode } from 'react';
2
+ export type StepTitleLineProps = {
3
+ /** The step's heading. */
4
+ children: ReactNode;
5
+ /** Whether the step has `titleDetail` at all, open or folded. */
6
+ hasDetail: boolean;
7
+ /** The muted words to draw now (an open step's), or none. */
8
+ detail?: string;
9
+ /** Their id, which an open list's region is described by. */
10
+ detailId: string;
11
+ };
12
+ /**
13
+ * A step's heading and, after it on a line that wraps, its muted
14
+ * `titleDetail` words, outside the heading. The line is there whenever the
15
+ * step has words, drawn or not, so the heading keeps its element (and its
16
+ * focus) as the step opens and folds; a step without words has the bare
17
+ * heading, as before.
18
+ */
19
+ export declare function StepTitleLine({ children, hasDetail, detail, detailId, }: StepTitleLineProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A step's heading and, after it on a line that wraps, its muted
4
+ * `titleDetail` words, outside the heading. The line is there whenever the
5
+ * step has words, drawn or not, so the heading keeps its element (and its
6
+ * focus) as the step opens and folds; a step without words has the bare
7
+ * heading, as before.
8
+ */
9
+ export function StepTitleLine({ children, hasDetail, detail, detailId, }) {
10
+ if (!hasDetail)
11
+ return children;
12
+ return (_jsxs("div", { className: "sw-steps-title-line", children: [children, detail ? (_jsx("span", { className: "sw-steps-title-detail", id: detailId, children: detail })) : null] }));
13
+ }
@@ -0,0 +1,27 @@
1
+ export type StepTitleWordsProps = {
2
+ /** The id the step's region and its Edit button are named by. */
3
+ titleId: string;
4
+ title: string;
5
+ } & ({
6
+ /** The status word read after the title. */
7
+ word: string;
8
+ reason?: never;
9
+ } | {
10
+ word: null;
11
+ /** A skipped step's muted reason after its title. */
12
+ reason?: string;
13
+ });
14
+ /**
15
+ * The words inside a step's heading. Without a status word they are the
16
+ * title (and a skipped step's reason, which never comes with a word) as
17
+ * drawn. With one, the drawn title is hidden from assistive technology and
18
+ * one visually hidden line holds the whole spoken name ("The site, Done"):
19
+ * a hidden box is laid out as a block, so a browser computing the name
20
+ * puts a space before it ("The site , Done") when the title and the word
21
+ * sit in separate boxes.
22
+ *
23
+ * `aria-hidden` goes on the title's own box, never a wrapper: the region
24
+ * and the Edit button name themselves by its id, and Chrome reads a
25
+ * referenced box inside a hidden wrapper into the heading's name too.
26
+ */
27
+ export declare function StepTitleWords(props: StepTitleWordsProps): import("react").JSX.Element;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The words inside a step's heading. Without a status word they are the
4
+ * title (and a skipped step's reason, which never comes with a word) as
5
+ * drawn. With one, the drawn title is hidden from assistive technology and
6
+ * one visually hidden line holds the whole spoken name ("The site, Done"):
7
+ * a hidden box is laid out as a block, so a browser computing the name
8
+ * puts a space before it ("The site , Done") when the title and the word
9
+ * sit in separate boxes.
10
+ *
11
+ * `aria-hidden` goes on the title's own box, never a wrapper: the region
12
+ * and the Edit button name themselves by its id, and Chrome reads a
13
+ * referenced box inside a hidden wrapper into the heading's name too.
14
+ */
15
+ export function StepTitleWords(props) {
16
+ const { titleId, title } = props;
17
+ if (props.word !== null)
18
+ return (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", id: titleId, children: title }), _jsx("span", { className: "sw-sr-only", children: `${title}, ${props.word}` })] }));
19
+ const { reason } = props;
20
+ return (_jsxs(_Fragment, { children: [_jsx("span", { id: titleId, children: title }), reason ? (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-steps-reason", children: reason })] })) : null] }));
21
+ }
@@ -0,0 +1,7 @@
1
+ import type { StepProps } from './Step.js';
2
+ import { type StepHeadingLevel } from './step-status.js';
3
+ /**
4
+ * Fails closed on a step it cannot draw: a blank title, reason or title
5
+ * detail, an unknown status or heading level, or an Edit with no words.
6
+ */
7
+ export declare function assertStep(props: StepProps, level: StepHeadingLevel): void;
@@ -0,0 +1,19 @@
1
+ import { stepHeadingLevels, stepStatuses, } from './step-status.js';
2
+ /**
3
+ * Fails closed on a step it cannot draw: a blank title, reason or title
4
+ * detail, an unknown status or heading level, or an Edit with no words.
5
+ */
6
+ export function assertStep(props, level) {
7
+ if (!props.title.trim())
8
+ throw new RangeError('Step title must not be empty');
9
+ if (!stepStatuses.includes(props.status))
10
+ throw new RangeError(`Unknown Step status: ${String(props.status)}`);
11
+ if (!stepHeadingLevels.includes(level))
12
+ throw new RangeError('Step headingLevel must be 2, 3 or 4');
13
+ if (props.onEdit && !props.editLabel?.trim())
14
+ throw new RangeError('Step onEdit needs an editLabel');
15
+ if (props.skippedReason !== undefined && !props.skippedReason.trim())
16
+ throw new RangeError('Step skippedReason must not be empty');
17
+ if (props.titleDetail !== undefined && !props.titleDetail.trim())
18
+ throw new RangeError('Step titleDetail must not be empty');
19
+ }
@@ -0,0 +1,2 @@
1
+ export declare function cssStepsTitleDetailClasses(): string;
2
+ export declare function stepsTitleDetailClassCatalog(): string[];
@@ -0,0 +1,36 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ const body = typographyVariants.body;
3
+ const label = typographyVariants.label;
4
+ /*
5
+ * An open step's muted words after its title ("Optional", "Wetlands done ·
6
+ * Woodlands empty"): the title and the words share one line that wraps, the
7
+ * words on the title's baseline a space-2 after it, or on a line of their
8
+ * own under the title when they do not fit beside it. The line gives way
9
+ * to a done step's summary and Edit in an open list, as the title does.
10
+ * The words are the label size at the body's weight in the muted colour,
11
+ * a skipped step's reason's colour beside its title; a long word breaks
12
+ * rather than pass the panel.
13
+ */
14
+ export function cssStepsTitleDetailClasses() {
15
+ return `.sw-steps-title-line {
16
+ align-items: baseline;
17
+ column-gap: var(--sw-space-2);
18
+ display: flex;
19
+ flex: 0 1 auto;
20
+ flex-wrap: wrap;
21
+ min-width: 0;
22
+ }
23
+
24
+ .sw-steps-title-detail {
25
+ color: var(--sw-color-muted);
26
+ font-size: ${label.fontSize}px;
27
+ font-weight: ${body.fontWeight};
28
+ letter-spacing: ${label.letterSpacing}px;
29
+ line-height: ${label.lineHeight}px;
30
+ min-width: 0;
31
+ overflow-wrap: anywhere;
32
+ }`;
33
+ }
34
+ export function stepsTitleDetailClassCatalog() {
35
+ return ['sw-steps-title-line', 'sw-steps-title-detail'];
36
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The one-page vertical stepper: an ordered list of glass panels a small
3
- * gap apart. Each has a header line (marker, title, and on a done step its
4
- * summary and Edit) and, while open (current or error), a body indented to
3
+ * gap apart. Each has a header line (marker, title, an open step's muted
4
+ * words beside it, and on a done step its summary and Edit) and, while open (current or error), a body indented to
5
5
  * the title. An open step's title is the title variant; a folded one's is
6
6
  * body size at label weight, muted ahead of the form or skipped. In an
7
7
  * open list (`sw-steps-open`) every step is open: each panel takes the open
@@ -1,5 +1,6 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { checkStrokeIn } from './chevron.js';
3
+ import { cssStepsTitleDetailClasses, stepsTitleDetailClassCatalog, } from './css-steps-title-detail.js';
3
4
  const body = typographyVariants.body;
4
5
  const caption = typographyVariants.caption;
5
6
  const label = typographyVariants.label;
@@ -69,8 +70,8 @@ function forcedColorRules() {
69
70
  }
70
71
  /**
71
72
  * The one-page vertical stepper: an ordered list of glass panels a small
72
- * gap apart. Each has a header line (marker, title, and on a done step its
73
- * summary and Edit) and, while open (current or error), a body indented to
73
+ * gap apart. Each has a header line (marker, title, an open step's muted
74
+ * words beside it, and on a done step its summary and Edit) and, while open (current or error), a body indented to
74
75
  * the title. An open step's title is the title variant; a folded one's is
75
76
  * body size at label weight, muted ahead of the form or skipped. In an
76
77
  * open list (`sw-steps-open`) every step is open: each panel takes the open
@@ -190,6 +191,8 @@ export function cssStepsClasses() {
190
191
  min-width: 0;
191
192
  }
192
193
 
194
+ ${cssStepsTitleDetailClasses()}
195
+
193
196
  ${markerRules()}
194
197
 
195
198
  ${connectorRules()}
@@ -214,5 +217,6 @@ export function stepsClassCatalog() {
214
217
  'sw-steps-edit',
215
218
  'sw-steps-body',
216
219
  'sw-steps-region',
220
+ ...stepsTitleDetailClassCatalog(),
217
221
  ];
218
222
  }
@@ -41,6 +41,7 @@ export declare function assertListGroup({ label, labelledBy, variant, maxHeight,
41
41
  export declare function listMaxHeightStyle(maxHeight: ListGroupMaxHeight): CSSProperties;
42
42
  export type ListRowInput = {
43
43
  title: string;
44
+ titleLang: string | undefined;
44
45
  tone: unknown;
45
46
  titleAppearance: unknown;
46
47
  valueTone: unknown;
@@ -54,12 +55,12 @@ export type ListRowInput = {
54
55
  unreadLabel: string | undefined;
55
56
  };
56
57
  /**
57
- * Fails closed on a ListRow it cannot draw: an empty title or value, an
58
- * unknown tone, title appearance, value tone, badge tone or accessory, a
59
- * `valueDetail`, `valueTone` or `valueBadge` with no value, a value both
60
- * toned and a badge, `selected` on a row that is not a button or link
61
- * (its check would be a state nothing can read), or an unread row with no
62
- * words for its dot (a colour alone says nothing).
58
+ * Fails closed on a ListRow it cannot draw: an empty title, titleLang or
59
+ * value, an unknown tone, title appearance, value tone, badge tone or
60
+ * accessory, a `valueDetail`, `valueTone` or `valueBadge` with no value, a
61
+ * value both toned and a badge, `selected` on a row that is not a button or
62
+ * link (its check would be a state nothing can read), or an unread row with
63
+ * no words for its dot (a colour alone says nothing).
63
64
  */
64
65
  export declare function assertListRow(input: ListRowInput): void;
65
66
  /**
@@ -38,16 +38,18 @@ export function listMaxHeightStyle(maxHeight) {
38
38
  return { '--sw-list-max-rows': maxHeight.rows };
39
39
  }
40
40
  /**
41
- * Fails closed on a ListRow it cannot draw: an empty title or value, an
42
- * unknown tone, title appearance, value tone, badge tone or accessory, a
43
- * `valueDetail`, `valueTone` or `valueBadge` with no value, a value both
44
- * toned and a badge, `selected` on a row that is not a button or link
45
- * (its check would be a state nothing can read), or an unread row with no
46
- * words for its dot (a colour alone says nothing).
41
+ * Fails closed on a ListRow it cannot draw: an empty title, titleLang or
42
+ * value, an unknown tone, title appearance, value tone, badge tone or
43
+ * accessory, a `valueDetail`, `valueTone` or `valueBadge` with no value, a
44
+ * value both toned and a badge, `selected` on a row that is not a button or
45
+ * link (its check would be a state nothing can read), or an unread row with
46
+ * no words for its dot (a colour alone says nothing).
47
47
  */
48
48
  export function assertListRow(input) {
49
49
  if (isBlank(input.title))
50
50
  throw new RangeError('ListRow title must be non-empty text');
51
+ if (input.titleLang !== undefined && isBlank(input.titleLang))
52
+ throw new RangeError('ListRow titleLang must be a non-empty language tag');
51
53
  if (!isOneOf(listRowTones, input.tone))
52
54
  throw new RangeError(`Unknown ListRow tone: ${String(input.tone)}`);
53
55
  if (!isOneOf(listRowTitleAppearances, input.titleAppearance))
package/dist/styles.css CHANGED
@@ -7303,6 +7303,25 @@ body {
7303
7303
  min-width: 0;
7304
7304
  }
7305
7305
 
7306
+ .sw-steps-title-line {
7307
+ align-items: baseline;
7308
+ column-gap: var(--sw-space-2);
7309
+ display: flex;
7310
+ flex: 0 1 auto;
7311
+ flex-wrap: wrap;
7312
+ min-width: 0;
7313
+ }
7314
+
7315
+ .sw-steps-title-detail {
7316
+ color: var(--sw-color-muted);
7317
+ font-size: 15px;
7318
+ font-weight: 400;
7319
+ letter-spacing: -0.2px;
7320
+ line-height: 20px;
7321
+ min-width: 0;
7322
+ overflow-wrap: anywhere;
7323
+ }
7324
+
7306
7325
  .sw-steps-item-upcoming .sw-steps-marker,
7307
7326
  .sw-steps-item-skipped .sw-steps-marker {
7308
7327
  box-shadow: inset 0 0 0 2px var(--sw-color-border);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.44.0",
3
+ "version": "1.45.0",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",