@scalewing/react 1.57.0 → 1.58.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,11 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.58.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cf53284: `Field` `descriptionTone` (opt-in, `FieldDescriptionTone`, `fieldDescriptionTones`: `'muted'` default, `'warning'`): a warning description in the warning colour after a small `aria-hidden` alert glyph, for a hint that needs the person but is not an error; it stays in the control's `aria-describedby`, sets no `aria-invalid` and no danger border, gives way to `error`, and takes `CanvasText` in forced colors. `FixtureGroup` `titleHidden` (opt-in): the title still names the list (`aria-labelledby`) but is not drawn, for a group whose heading is already on screen. `Text` `font` (opt-in, `TextFont`, `textFonts`: `'sans'` default, `'mono'`): the platform's monospace face through the generated `sw-font-mono` class at the variant's size, weight and colour, for a code read character by character. Unknown tones and fonts throw a `RangeError`. New classes `sw-field-description-warning`, `sw-field-description-glyph`. `FieldProps` now lives in `components/field/field-props.ts` and is still exported from the package root.
8
+
3
9
  ## 1.57.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -189,6 +189,21 @@ Import the CSS once at the application entry. Do not copy it into your source tr
189
189
  </Field>
190
190
  ```
191
191
 
192
+ `Field` `descriptionTone="warning"` (default `"muted"`; `fieldDescriptionTones`, `FieldDescriptionTone`) draws the `description` in the warning colour (`--sw-color-warning`, 4.5:1 on the canvas in both schemes) after a small alert glyph a space-4 square (private chrome, `aria-hidden`, centred on its first line), for a hint that needs the person but is not an error, such as a number another row already has. It stays the control's description (`aria-describedby`, after any ids the control already lists) and sets no invalid state: no `aria-invalid`, no danger border, nothing announced. `error` still replaces it on screen, and is the only way to mark the field invalid. In forced colors the words and glyph take `CanvasText` (an error takes `Mark`), so the glyph is the cue. It works at every `size`, including `xs` in a table cell. An unknown tone throws a `RangeError`. New classes `sw-field-description-warning`, `sw-field-description-glyph`.
193
+
194
+ ```tsx
195
+ <Field
196
+ description={twin ? `${twin} also wears ${band}` : undefined}
197
+ descriptionTone="warning"
198
+ label={`Band number for ${bird}`}
199
+ labelVisuallyHidden
200
+ size="xs"
201
+ width="xs"
202
+ >
203
+ <input inputMode="numeric" value={band} onChange={setBand} />
204
+ </Field>
205
+ ```
206
+
192
207
  Every press control and link takes an explicit `tabindex="0"`: `Button` (and the controls built on it, `CalendarButton` and `InfoTip`), `CommandPaletteTrigger`, `Box as="a"` with an `href`, `SectionNav` links, a link or button `FixtureRow`, `AppShell` links and its menu and close buttons, the `Select`, `MultiSelect`, `ActionMenu` and `ContextSwitcher` triggers, `CommandPaletteTrigger` (and a `ContextSwitcher`'s listbox while it holds focus), `SegmentedControl` items (every item, as outside Safari: its arrow keys change the value, not the focus, so it is not a roving group), `DateField`'s calendar button and month steps, a search `Field`'s clear button, a `Table`'s sort and `TableCell onPress` buttons, `Checkbox`, `Switch`, and the checked radio of a `RadioGroup` or `FilterChips` (the first enabled one while none is; the others keep no `tabindex`, so each browser's arrow-key group rule stands). Safari's default (its "Press Tab to highlight each item" setting off) leaves a button, a link, a checkbox and a radio out of the Tab order unless it carries one, so a Safari user would Tab past them; the explicit `0` gives them the order every other browser already does. A `tabIndex` you pass wins, and controls that are deliberately no tab stop keep their `-1`: a `Stepper`'s − and +, and a roving item (a `Tabs` tab, a menu item, a calendar day, a `DateToggleCalendar` day, a `ScheduleGrid` cell) other than the current one. A `<summary>` is already in Safari's order and takes none.
193
208
 
194
209
  An element a script focuses, such as a notice or a card announced after a save, takes the same accent focus ring as the canvas's links and native controls, 2 px past its edge (`--sw-focus-ring-width` and `--sw-focus-ring-offset`), instead of the browser's outline. Give it `tabIndex={-1}`, so it takes focus from a script but is no tab stop, and put the focus on the whole card rather than its text, so the ring goes round it clear of the words; a `Box` round the card with the card's `radius` makes the ring follow its corners. As with the browser's own outline, the ring shows only when focus is visible (`:focus-visible`): after a keyboard press, not after a mouse press. The rule has zero specificity, so a component's own ring always wins.
@@ -1241,6 +1256,15 @@ const { headings, omissions } = inspectMarkdown(page.body, {
1241
1256
  </FixtureGroup>
1242
1257
  ```
1243
1258
 
1259
+ `FixtureGroup` `titleHidden` (opt-in, default `false`) keeps `title` as the list's name (`aria-labelledby`) but does not draw the caption: it moves to a visually hidden element (`sw-sr-only`) out of the layout, so the first row starts where the caption would. Use it when the heading above already says what the caption would, such as one result in a sheet titled with the pairing and the day; the title is still required and still must be nonblank.
1260
+
1261
+ ```tsx
1262
+ <Text as="h3" variant="title">Heron Marsh vs Otter Pool</Text>
1263
+ <FixtureGroup title="Heron Marsh vs Otter Pool, Sunday 25 October" titleHidden>
1264
+ <FixtureRow home={{ name: 'Heron Marsh' }} away={{ name: 'Otter Pool' }} center="14–9" centerKind="score" />
1265
+ </FixtureGroup>
1266
+ ```
1267
+
1244
1268
  `FixtureRow` `flag` (opt-in, `FixtureFlag`: `{ label, tone? }`) marks an issue at the row's start, such as a clash: a round mark a space-4 across with a "!", in the tone's tint and ink (`warning`, amber, the default, for something that needs the person; `danger`, red), its edge keeping 3:1 on the surface in every palette and the text colour in forced colors. The mark is `aria-hidden` with `label` as its `title` tooltip, and `label` is read first in the row from a visually hidden span ("Can't-play clash Heron Marsh 8:00 Otter Pool"); with `aria-label` it is put before your words with a comma ("Can't-play clash, Heron Marsh vs Otter Pool, 8:00"), so do not repeat it there. From `md` the mark sits on the first line at the row's start, the home side kept clear of it; below `md` it leads the second line, before the details, so it never squeezes the home name. A row with nothing on that line below `md` (no `status`, and no `meta` or one hidden below `md` by `metaHideBelow`) keeps the mark inline on its first line instead, as from `md` (class `sw-fixture-row-flag-inline`), rather than gaining a line that holds only the flag. A blank `label` or an unknown `tone` throws a `RangeError`. `action` (opt-in) is a control after the row, such as a small neutral `Button` ("Move", named in your words: `aria-label="Move Heron Marsh vs Otter Pool"`): it is the row's sibling in its list item, outside a link or button row's press area and after it in the Tab order, a press on it never opens the row, and it stays centred on the row's height in the run's last column at every width (from `md` the row's one line, below `md` both lines), so every row's action lines up. Where `:has` is supported the hover fill spans the row and its action. An empty slot draws nothing. The run has a column for it at the end, and one for `select` at the start (`sw-fixture-list` is `auto minmax(0, 1fr) auto minmax(0, 1fr) auto auto auto`, six below `md`); a row spans every column but those two (`grid-column: 2 / -2`), and an `auto` column no row uses takes no space. New classes `sw-fixture-row-flagged`, `sw-fixture-flag`, `sw-fixture-flag-mark`, `sw-fixture-flag-warning`, `sw-fixture-flag-danger`, `sw-fixture-item-action` and `sw-fixture-action`.
1245
1269
 
1246
1270
  ```tsx
@@ -1461,6 +1485,8 @@ A row can also carry three trailing parts. `unread` marks it new for the person,
1461
1485
 
1462
1486
  `Text` takes an optional `align` (`start`, `center`, `end`) mapped to generated `sw-text-align-*` classes, for a heading that must stay centered when it wraps.
1463
1487
 
1488
+ `Text` `font="mono"` (default `"sans"`; `textFonts`, `TextFont`) sets the words in the platform's monospace face through the generated `sw-font-mono` class (`ui-monospace, monospace`, the generic names only, so each platform draws its own), keeping the variant's size, weight, letter spacing and `color`, for a code read or typed character by character, such as a join code: `<Text variant="headline" font="mono">R8M 3TW</Text>`. The face is fixed-width, so figures and letters line up; the text stays selectable. An unknown font throws a `RangeError`.
1489
+
1464
1490
  `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.
1465
1491
 
1466
1492
  `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.
@@ -1,75 +1,6 @@
1
- import { type SpacingStep } from '@scalewing/tokens';
2
- import { type ReactNode } from 'react';
3
- import { type RequiredMark } from './FieldLabelText.js';
4
- import { type FieldWidth } from '../css/css-field-width.js';
1
+ import { type FieldProps } from './field/field-props.js';
5
2
  import { fieldVariants, type FieldVariant } from './field/field-checks.js';
6
- export type FieldSize = 'xs' | 'md';
7
3
  export { fieldVariants, type FieldVariant };
8
- export type FieldProps = {
9
- children: ReactNode;
10
- gap?: SpacingStep;
11
- label: string;
12
- labelVisuallyHidden?: boolean;
13
- size?: FieldSize;
14
- description?: string;
15
- error?: string;
16
- /**
17
- * Marks the control invalid (`aria-invalid` and the danger border) without
18
- * a message of its own, for a field whose error is shown elsewhere, such
19
- * as one message under a group of fields. Point the control's
20
- * `aria-describedby` at that message. `error` implies it. Passing
21
- * `invalid`, even `false` but not `undefined`, requires one native control
22
- * child from the first render.
23
- */
24
- invalid?: boolean;
25
- /**
26
- * Marks a value the person changed from a saved one, such as a field in a
27
- * correction: the control's border (or an adorned frame's) in the accent
28
- * color, thicker than the plain hairline, and its fill tinted with 12 % of
29
- * the accent, so a changed field reads apart from the focused one (focus
30
- * draws a ring and never tints). The value keeps 4.5:1 on the tint; while
31
- * a placeholder shows, the fill stays plain. It is a cue, not the record:
32
- * say what it was in `description` ("Was 25"). `invalid` and `error`
33
- * win. Like `invalid`, passing it, even `false` but not `undefined`,
34
- * requires one native control child from the first render.
35
- */
36
- changed?: boolean;
37
- required?: boolean;
38
- /**
39
- * `hidden` leaves out the label's " *" while the control stays required
40
- * (and announced so); `visible` (default) draws it.
41
- * When hidden, say in visible text which fields must be filled; a
42
- * placeholder alone is not enough.
43
- */
44
- requiredMark?: RequiredMark;
45
- /** Short text inside the control frame before the value, such as a currency sign. Not part of the value. */
46
- prefix?: string;
47
- /** Short text inside the control frame after the value, such as a unit. Not part of the value. */
48
- suffix?: string;
49
- /**
50
- * `outlined` (default) is the labelled form field. `search` is a search
51
- * box: the label is not drawn (it stays the input's name), a `leading`
52
- * glyph sits at the frame's start, the input is `type="search"`, and a
53
- * clear button named `clearLabel` shows while the input holds text and
54
- * is enabled; it calls `onClear` (set the value to `''`) and puts focus
55
- * back in the input. Needs one native `input` child with a `value`.
56
- */
57
- variant?: FieldVariant;
58
- /** A search field's glyph at its start, such as a magnifier; hidden from assistive technology. */
59
- leading?: ReactNode;
60
- /** A search field's clear button name, such as "Clear search". */
61
- clearLabel?: string;
62
- /** Called by a search field's clear button; the consumer empties the value. */
63
- onClear?: () => void;
64
- /**
65
- * Caps the control's width at a Table column size (`xs` 4rem, `sm` 6rem,
66
- * `md` 8rem, `lg` 12rem, `xl` 16rem), at the start of the column and
67
- * never wider than it, so a short value such as a rate or a count does
68
- * not stretch across a form. The label and messages keep the column's
69
- * width. Left out, the control fills the column. It needs one native
70
- * text input, select or textarea child (a `TypeError` otherwise); an
71
- * unknown width throws a `RangeError`.
72
- */
73
- width?: FieldWidth;
74
- };
4
+ export type { FieldProps, FieldSize } from './field/field-props.js';
5
+ export { fieldDescriptionTones, type FieldDescriptionTone, } from './field/FieldDescription.js';
75
6
  export declare function Field(props: FieldProps): import("react").JSX.Element;
@@ -1,16 +1,18 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { cloneElement, isValidElement, useId, } from 'react';
3
+ import { cloneElement, isValidElement, useId } from 'react';
4
4
  import { adornedControlAria, FieldAdornment, } from './FieldAdornment.js';
5
5
  import { FieldErrorRegion } from './FieldErrorRegion.js';
6
- import { assertRequiredMark, FieldLabelText, } from './FieldLabelText.js';
6
+ import { assertRequiredMark, FieldLabelText } from './FieldLabelText.js';
7
7
  import { fieldWidthClass } from '../css/css-field-width.js';
8
+ import { FieldDescription } from './field/FieldDescription.js';
8
9
  import { SearchFrame } from './field/SearchFrame.js';
9
10
  import { assertFieldProps, fieldVariants, passesInvalid, } from './field/field-checks.js';
10
11
  import { Stack } from './Stack.js';
11
12
  export { fieldVariants };
13
+ export { fieldDescriptionTones, } from './field/FieldDescription.js';
12
14
  export function Field(props) {
13
- const { children, gap = 1, label, labelVisuallyHidden = false, size = 'md', description, error, invalid = false, changed = false, required = false, requiredMark = 'visible', prefix, suffix, variant = 'outlined', leading, clearLabel = '', onClear, width, } = props;
15
+ const { children, gap = 1, label, labelVisuallyHidden = false, size = 'md', description, descriptionTone = 'muted', error, invalid = false, changed = false, required = false, requiredMark = 'visible', prefix, suffix, variant = 'outlined', leading, clearLabel = '', onClear, width, } = props;
14
16
  assertFieldProps(props, children);
15
17
  const search = variant === 'search';
16
18
  const controlId = useId();
@@ -90,5 +92,5 @@ export function Field(props) {
90
92
  });
91
93
  const value = children.props.value;
92
94
  const hasText = value !== undefined && value !== null && String(value) !== '';
93
- return (_jsxs(Stack, { as: "div", className: className, gap: labelHidden ? 0 : gap, children: [_jsx("label", { className: search ? 'sw-sr-only' : undefined, htmlFor: children.props.id || controlId, id: labelId, children: labelText }), search ? (_jsx(SearchFrame, { clearLabel: clearLabel, leading: leading, onClear: onClear, showClear: hasText && !children.props.disabled && !children.props.readOnly, children: control })) : adorned ? (_jsx(FieldAdornment, { prefix: prefix, prefixId: prefixId, suffix: suffix, suffixId: suffixId, children: control })) : (control), description && !error ? (_jsx("span", { className: "sw-field-description", id: descriptionId, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
95
+ return (_jsxs(Stack, { as: "div", className: className, gap: labelHidden ? 0 : gap, children: [_jsx("label", { className: search ? 'sw-sr-only' : undefined, htmlFor: children.props.id || controlId, id: labelId, children: labelText }), search ? (_jsx(SearchFrame, { clearLabel: clearLabel, leading: leading, onClear: onClear, showClear: hasText && !children.props.disabled && !children.props.readOnly, children: control })) : adorned ? (_jsx(FieldAdornment, { prefix: prefix, prefixId: prefixId, suffix: suffix, suffixId: suffixId, children: control })) : (control), description && !error ? (_jsx(FieldDescription, { id: descriptionId, tone: descriptionTone, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
94
96
  }
@@ -3,7 +3,8 @@ export type { FixtureGroupProps } from './fixture/types.js';
3
3
  /**
4
4
  * A run of fixtures under a caption, such as a match day at a venue: the
5
5
  * caption names a list of FixtureRows, whose columns line up across the
6
- * run (one grid, each row a subgrid of it). A blank title throws a
6
+ * run (one grid, each row a subgrid of it). `titleHidden` keeps the
7
+ * caption as the list's name without drawing it. A blank title throws a
7
8
  * `RangeError`.
8
9
  */
9
- export declare function FixtureGroup({ children, title }: FixtureGroupProps): import("react").JSX.Element;
10
+ export declare function FixtureGroup({ children, title, titleHidden, }: FixtureGroupProps): import("react").JSX.Element;
@@ -5,12 +5,13 @@ import { FixtureListContext } from './fixture/fixture-list-context.js';
5
5
  /**
6
6
  * A run of fixtures under a caption, such as a match day at a venue: the
7
7
  * caption names a list of FixtureRows, whose columns line up across the
8
- * run (one grid, each row a subgrid of it). A blank title throws a
8
+ * run (one grid, each row a subgrid of it). `titleHidden` keeps the
9
+ * caption as the list's name without drawing it. A blank title throws a
9
10
  * `RangeError`.
10
11
  */
11
- export function FixtureGroup({ children, title }) {
12
+ export function FixtureGroup({ children, title, titleHidden = false, }) {
12
13
  if (typeof title !== 'string' || !title.trim())
13
14
  throw new RangeError('FixtureGroup title must be nonempty');
14
15
  const titleId = useId();
15
- return (_jsxs("div", { className: "sw-fixture-group", children: [_jsx("div", { className: "sw-fixture-group-title", id: titleId, children: title }), _jsx("ul", { "aria-labelledby": titleId, className: "sw-fixture-list", role: "list", children: _jsx(FixtureListContext.Provider, { value: true, children: children }) })] }));
16
+ return (_jsxs("div", { className: "sw-fixture-group", children: [_jsx("div", { className: titleHidden ? 'sw-sr-only' : 'sw-fixture-group-title', id: titleId, children: title }), _jsx("ul", { "aria-labelledby": titleId, className: "sw-fixture-list", role: "list", children: _jsx(FixtureListContext.Provider, { value: true, children: children }) })] }));
16
17
  }
@@ -2,10 +2,21 @@ import { type SemanticColorKey, type TypographyVariant } from '@scalewing/tokens
2
2
  import { type HTMLAttributes, type LabelHTMLAttributes } from 'react';
3
3
  export type TextElement = 'p' | 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'label' | 'strong';
4
4
  export type TextAlign = 'start' | 'center' | 'end';
5
+ /** The faces Text sets its words in: the theme's sans or the platform's monospace. */
6
+ export declare const textFonts: readonly ["sans", "mono"];
7
+ export type TextFont = (typeof textFonts)[number];
5
8
  export type TextProps = HTMLAttributes<HTMLElement> & Pick<LabelHTMLAttributes<HTMLLabelElement>, 'htmlFor'> & {
6
9
  align?: TextAlign;
7
10
  as?: TextElement;
8
11
  color?: SemanticColorKey;
12
+ /**
13
+ * `mono` sets the words in the platform's monospace face (the generated
14
+ * `sw-font-mono` class) at the variant's size, weight and colour, for a
15
+ * code read or typed character by character, such as a join code. The
16
+ * face is fixed-width, so figures line up. Default `sans`; an unknown
17
+ * font throws a `RangeError`.
18
+ */
19
+ font?: TextFont;
9
20
  truncate?: boolean;
10
21
  variant?: TypographyVariant;
11
22
  };
@@ -13,6 +24,14 @@ export declare const Text: import("react").ForwardRefExoticComponent<HTMLAttribu
13
24
  align?: TextAlign;
14
25
  as?: TextElement;
15
26
  color?: SemanticColorKey;
27
+ /**
28
+ * `mono` sets the words in the platform's monospace face (the generated
29
+ * `sw-font-mono` class) at the variant's size, weight and colour, for a
30
+ * code read or typed character by character, such as a join code. The
31
+ * face is fixed-width, so figures line up. Default `sans`; an unknown
32
+ * font throws a `RangeError`.
33
+ */
34
+ font?: TextFont;
16
35
  truncate?: boolean;
17
36
  variant?: TypographyVariant;
18
37
  } & import("react").RefAttributes<HTMLElement>>;
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef, } from 'react';
3
3
  import { cx } from '../class-names.js';
4
+ /** The faces Text sets its words in: the theme's sans or the platform's monospace. */
5
+ export const textFonts = ['sans', 'mono'];
4
6
  const defaultElement = {
5
7
  body: 'p',
6
8
  caption: 'span',
@@ -13,7 +15,9 @@ const defaultElement = {
13
15
  lead: 'p',
14
16
  title: 'h3',
15
17
  };
16
- export const Text = forwardRef(function Text({ align, as, className, color = 'text', style, truncate = false, variant = 'body', ...rest }, ref) {
18
+ export const Text = forwardRef(function Text({ align, as, className, color = 'text', font = 'sans', style, truncate = false, variant = 'body', ...rest }, ref) {
19
+ if (!textFonts.includes(font))
20
+ throw new RangeError(`Unknown Text font: ${String(font)}`);
17
21
  const Component = as ?? defaultElement[variant];
18
- return (_jsx(Component, { ref: ref, className: cx(`sw-text-${variant}`, align && `sw-text-align-${align}`, truncate && 'sw-truncate', className), style: { color: `var(--sw-color-${color})`, ...style }, ...rest }));
22
+ return (_jsx(Component, { ref: ref, className: cx(`sw-text-${variant}`, align && `sw-text-align-${align}`, font === 'mono' && 'sw-font-mono', truncate && 'sw-truncate', className), style: { color: `var(--sw-color-${color})`, ...style }, ...rest }));
19
23
  });
@@ -0,0 +1,15 @@
1
+ /** How a Field's description reads: the quiet hint or a warning that is not an error. */
2
+ export declare const fieldDescriptionTones: readonly ["muted", "warning"];
3
+ export type FieldDescriptionTone = (typeof fieldDescriptionTones)[number];
4
+ export declare function assertFieldDescriptionTone(tone: unknown): void;
5
+ /**
6
+ * A Field's description under its control, the control's
7
+ * `aria-describedby` target. A warning one leads with a private alert
8
+ * glyph, hidden from assistive technology, so it reads apart from a muted
9
+ * hint without colour (and in forced colours, where both take CanvasText).
10
+ */
11
+ export declare function FieldDescription({ children, id, tone, }: {
12
+ children: string;
13
+ id: string;
14
+ tone: FieldDescriptionTone;
15
+ }): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** How a Field's description reads: the quiet hint or a warning that is not an error. */
3
+ export const fieldDescriptionTones = ['muted', 'warning'];
4
+ export function assertFieldDescriptionTone(tone) {
5
+ if (!fieldDescriptionTones.includes(tone))
6
+ throw new RangeError(`Unknown Field descriptionTone: ${String(tone)}`);
7
+ }
8
+ /** A triangle with an exclamation mark, drawn in the description's colour. */
9
+ const alertPath = 'M12 3.5 21.5 20h-19zM12 9.75v4.5M12 17.25v.01';
10
+ /**
11
+ * A Field's description under its control, the control's
12
+ * `aria-describedby` target. A warning one leads with a private alert
13
+ * glyph, hidden from assistive technology, so it reads apart from a muted
14
+ * hint without colour (and in forced colours, where both take CanvasText).
15
+ */
16
+ export function FieldDescription({ children, id, tone, }) {
17
+ if (tone === 'muted')
18
+ return (_jsx("span", { className: "sw-field-description", id: id, children: children }));
19
+ return (_jsxs("span", { className: "sw-field-description sw-field-description-warning", id: id, children: [_jsx("svg", { "aria-hidden": "true", className: "sw-field-description-glyph", focusable: "false", viewBox: "0 0 24 24", children: _jsx("path", { d: alertPath }) }), _jsx("span", { children: children })] }));
20
+ }
@@ -1,5 +1,5 @@
1
1
  import { type KeyboardEvent } from 'react';
2
- import type { FieldProps } from '../Field.js';
2
+ import type { FieldProps } from './field-props.js';
3
3
  export declare const fieldVariants: readonly ["outlined", "search"];
4
4
  export type FieldVariant = (typeof fieldVariants)[number];
5
5
  /** The props Field reads from, and sets on, its native control child. */
@@ -18,9 +18,9 @@ export type FieldControlProps = {
18
18
  onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
19
19
  };
20
20
  /**
21
- * Fails closed on a Field it cannot draw: an unknown variant or width,
22
- * search props on an outlined field, and a search field without its clear
23
- * button, its handler or one controlled native input.
21
+ * Fails closed on a Field it cannot draw: an unknown variant, width or
22
+ * description tone, search props on an outlined field, and a search field
23
+ * without its clear button, its handler or one controlled native input.
24
24
  */
25
25
  export declare function assertFieldProps(props: FieldProps, child: unknown): void;
26
26
  /**
@@ -1,14 +1,16 @@
1
1
  import { isValidElement } from 'react';
2
2
  import { assertFieldWidth } from '../../css/css-field-width.js';
3
+ import { assertFieldDescriptionTone } from './FieldDescription.js';
3
4
  export const fieldVariants = ['outlined', 'search'];
4
5
  /**
5
- * Fails closed on a Field it cannot draw: an unknown variant or width,
6
- * search props on an outlined field, and a search field without its clear
7
- * button, its handler or one controlled native input.
6
+ * Fails closed on a Field it cannot draw: an unknown variant, width or
7
+ * description tone, search props on an outlined field, and a search field
8
+ * without its clear button, its handler or one controlled native input.
8
9
  */
9
10
  export function assertFieldProps(props, child) {
10
- const { variant = 'outlined' } = props;
11
+ const { variant = 'outlined', descriptionTone = 'muted' } = props;
11
12
  assertFieldWidth(props.width);
13
+ assertFieldDescriptionTone(descriptionTone);
12
14
  if (!fieldVariants.includes(variant))
13
15
  throw new RangeError(`Unknown Field variant: ${String(variant)}`);
14
16
  if (variant !== 'search') {
@@ -0,0 +1,82 @@
1
+ import { type SpacingStep } from '@scalewing/tokens';
2
+ import { type ReactNode } from 'react';
3
+ import { type FieldWidth } from '../../css/css-field-width.js';
4
+ import { type RequiredMark } from '../FieldLabelText.js';
5
+ import { type FieldDescriptionTone } from './FieldDescription.js';
6
+ import { type FieldVariant } from './field-checks.js';
7
+ export type FieldSize = 'xs' | 'md';
8
+ export type FieldProps = {
9
+ children: ReactNode;
10
+ gap?: SpacingStep;
11
+ label: string;
12
+ labelVisuallyHidden?: boolean;
13
+ size?: FieldSize;
14
+ description?: string;
15
+ /**
16
+ * `warning` draws `description` in the warning colour after a small alert
17
+ * glyph, for a hint that needs the person but is not an error, such as a
18
+ * value another row already has. It stays the control's description and
19
+ * sets no invalid state; `error` still replaces it. Default `muted`; an
20
+ * unknown tone throws a `RangeError`.
21
+ */
22
+ descriptionTone?: FieldDescriptionTone;
23
+ error?: string;
24
+ /**
25
+ * Marks the control invalid (`aria-invalid` and the danger border) without
26
+ * a message of its own, for a field whose error is shown elsewhere, such
27
+ * as one message under a group of fields. Point the control's
28
+ * `aria-describedby` at that message. `error` implies it. Passing
29
+ * `invalid`, even `false` but not `undefined`, requires one native control
30
+ * child from the first render.
31
+ */
32
+ invalid?: boolean;
33
+ /**
34
+ * Marks a value the person changed from a saved one, such as a field in a
35
+ * correction: the control's border (or an adorned frame's) in the accent
36
+ * color, thicker than the plain hairline, and its fill tinted with 12 % of
37
+ * the accent, so a changed field reads apart from the focused one (focus
38
+ * draws a ring and never tints). The value keeps 4.5:1 on the tint; while
39
+ * a placeholder shows, the fill stays plain. It is a cue, not the record:
40
+ * say what it was in `description` ("Was 25"). `invalid` and `error`
41
+ * win. Like `invalid`, passing it, even `false` but not `undefined`,
42
+ * requires one native control child from the first render.
43
+ */
44
+ changed?: boolean;
45
+ required?: boolean;
46
+ /**
47
+ * `hidden` leaves out the label's " *" while the control stays required
48
+ * (and announced so); `visible` (default) draws it.
49
+ * When hidden, say in visible text which fields must be filled; a
50
+ * placeholder alone is not enough.
51
+ */
52
+ requiredMark?: RequiredMark;
53
+ /** Short text inside the control frame before the value, such as a currency sign. Not part of the value. */
54
+ prefix?: string;
55
+ /** Short text inside the control frame after the value, such as a unit. Not part of the value. */
56
+ suffix?: string;
57
+ /**
58
+ * `outlined` (default) is the labelled form field. `search` is a search
59
+ * box: the label is not drawn (it stays the input's name), a `leading`
60
+ * glyph sits at the frame's start, the input is `type="search"`, and a
61
+ * clear button named `clearLabel` shows while the input holds text and
62
+ * is enabled; it calls `onClear` (set the value to `''`) and puts focus
63
+ * back in the input. Needs one native `input` child with a `value`.
64
+ */
65
+ variant?: FieldVariant;
66
+ /** A search field's glyph at its start, such as a magnifier; hidden from assistive technology. */
67
+ leading?: ReactNode;
68
+ /** A search field's clear button name, such as "Clear search". */
69
+ clearLabel?: string;
70
+ /** Called by a search field's clear button; the consumer empties the value. */
71
+ onClear?: () => void;
72
+ /**
73
+ * Caps the control's width at a Table column size (`xs` 4rem, `sm` 6rem,
74
+ * `md` 8rem, `lg` 12rem, `xl` 16rem), at the start of the column and
75
+ * never wider than it, so a short value such as a rate or a count does
76
+ * not stretch across a form. The label and messages keep the column's
77
+ * width. Left out, the control fills the column. It needs one native
78
+ * text input, select or textarea child (a `TypeError` otherwise); an
79
+ * unknown width throws a `RangeError`.
80
+ */
81
+ width?: FieldWidth;
82
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -140,6 +140,12 @@ export type FixtureRowProps = {
140
140
  export type FixtureGroupProps = {
141
141
  /** The run's caption, such as a date and a venue; it names the list. */
142
142
  title: string;
143
+ /**
144
+ * Keeps `title` as the list's name but does not draw it, for a run whose
145
+ * heading is already on screen, such as one row in a sheet titled with
146
+ * the game and its day. Default `false`.
147
+ */
148
+ titleHidden?: boolean;
143
149
  /** The run's FixtureRows. */
144
150
  children: ReactNode;
145
151
  };
@@ -0,0 +1,2 @@
1
+ export declare function cssFieldDescriptionClasses(): string;
2
+ export declare function fieldDescriptionClassCatalog(): string[];
@@ -0,0 +1,39 @@
1
+ import { spacingScale, typographyVariants } from '@scalewing/tokens';
2
+ const caption = typographyVariants.caption;
3
+ const glyphStep = 4;
4
+ /*
5
+ * A warning description (`descriptionTone="warning"`): the description's
6
+ * caption type in the warning colour (4.5:1 on the canvas in both schemes),
7
+ * led by a space-4 alert glyph centred on its first line, so the hint reads
8
+ * apart from a muted one without colour too. It never touches the control:
9
+ * no invalid border, no ARIA state. In forced colours the words and the
10
+ * glyph take CanvasText, apart from an error's Mark.
11
+ */
12
+ export function cssFieldDescriptionClasses() {
13
+ const glyphInset = (caption.lineHeight - spacingScale[glyphStep]) / 2;
14
+ return `.sw-field-description-warning {
15
+ align-items: flex-start;
16
+ color: var(--sw-color-warning);
17
+ display: flex;
18
+ gap: var(--sw-space-1);
19
+ }
20
+
21
+ .sw-field-description-glyph {
22
+ fill: none;
23
+ flex: none;
24
+ height: var(--sw-space-${glyphStep});
25
+ margin-block: ${glyphInset}px;
26
+ stroke: currentColor;
27
+ stroke-linecap: round;
28
+ stroke-linejoin: round;
29
+ stroke-width: 2;
30
+ width: var(--sw-space-${glyphStep});
31
+ }
32
+
33
+ @media (forced-colors: active) {
34
+ .sw-field-description-warning { color: CanvasText; }
35
+ }`;
36
+ }
37
+ export function fieldDescriptionClassCatalog() {
38
+ return ['sw-field-description-warning', 'sw-field-description-glyph'];
39
+ }
@@ -1,6 +1,7 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { controlSurface, disabledControlForcedColors, disabledControlSurface, } from './css-document.js';
3
3
  import { cssFieldChangedClasses } from './css-field-changed.js';
4
+ import { cssFieldDescriptionClasses, fieldDescriptionClassCatalog, } from './css-field-description.js';
4
5
  import { cssFieldWidthClasses, fieldWidthClassCatalog, } from './css-field-width.js';
5
6
  import { cssFieldSearchClasses, fieldSearchClassCatalog, } from './css-field-search.js';
6
7
  const caption = typographyVariants.caption;
@@ -76,6 +77,8 @@ export function cssFieldClasses() {
76
77
  .sw-field-description { color: var(--sw-color-muted); }
77
78
  .sw-field-error { color: var(--sw-color-danger); }
78
79
 
80
+ ${cssFieldDescriptionClasses()}
81
+
79
82
  /* An error region is always rendered as a live region; empty, it leaves the layout (and its gap) but stays in the accessibility tree. */
80
83
  .sw-field-error:empty { position: absolute; }
81
84
 
@@ -106,6 +109,7 @@ export function fieldClassCatalog() {
106
109
  'sw-field',
107
110
  'sw-field-required',
108
111
  'sw-field-description',
112
+ ...fieldDescriptionClassCatalog(),
109
113
  'sw-field-error',
110
114
  'sw-field-invalid',
111
115
  'sw-field-changed',
@@ -8,7 +8,8 @@ import { typedInputs } from './css-document.js';
8
8
  export const monospaceFamilies = 'ui-monospace, monospace';
9
9
  /*
10
10
  * Monospace text, for words read as typed code or marks (a Markdown source
11
- * being written), at the size the text already has. It outranks the
11
+ * being written, or a code set with `Text font="mono"`), at the size the
12
+ * text already has; it outranks a Text variant's sans family. It outranks the
12
13
  * canvas's text controls, which take the page's family, so a `textarea` or
13
14
  * a text input can take it too.
14
15
  */
package/dist/index.d.ts CHANGED
@@ -47,7 +47,7 @@ export { StatTile, type StatTileAppearance, statTileAppearances, type StatTileEm
47
47
  export { TabPanel, type TabItem, type TabPanelProps, Tabs, type TabsProps, tabId, tabPanelId, } from './components/Tabs.js';
48
48
  export { Toast, toastPlacements, toastTones, type ToastAction, type ToastPlacement, } from './components/Toast.js';
49
49
  export type { ToastProps, ToastTone } from './components/Toast.js';
50
- export { Field, fieldVariants, type FieldProps, type FieldSize, type FieldVariant, } from './components/Field.js';
50
+ export { Field, fieldDescriptionTones, fieldVariants, type FieldDescriptionTone, type FieldProps, type FieldSize, type FieldVariant, } from './components/Field.js';
51
51
  export { fieldWidths, type FieldWidth } from './css/css-field-width.js';
52
52
  export { requiredMarks, type RequiredMark, } from './components/FieldLabelText.js';
53
53
  export { Step, type StepHeadingLevel, type StepProps, Steps, type StepsLabels, type StepsProps, type StepStatus, } from './components/Steps.js';
@@ -74,7 +74,7 @@ export type { SelectAction, SelectOption, SelectProps, SelectWidth, } from './co
74
74
  export { MultiSelect, type MultiSelectAlign, type MultiSelectOption, type MultiSelectProps, } from './components/MultiSelect.js';
75
75
  export { Stack, type Align, type Justify, type StackProps, } from './components/Stack.js';
76
76
  export { Table, TableBody, TableCell, TableFooter, TableHeader, TableRow, TableSelectCell, tableCellTones, tableRowRules, tableRowVariants, type TableBodyProps, type TableCellProps, type TableCellTone, type TableColumnWidth, type TableDensity, type TableFooterProps, type TableHeaderProps, type TableLayout, type TableMaxHeight, type TableProps, type TableRowProps, type TableRowRule, type TableRowVariant, type TableSelectCellProps, type TableSort, type TableStickyHeader, type TableVerticalAlign, } from './components/Table.js';
77
- export { Text, type TextAlign, type TextElement, type TextProps, } from './components/Text.js';
77
+ export { Text, textFonts, type TextAlign, type TextElement, type TextFont, type TextProps, } from './components/Text.js';
78
78
  export { ThemeProvider, useTheme, type ThemeProviderProps, } from './theme/ThemeProvider.js';
79
79
  export { themeCanvases, type ThemeCanvas } from './css/css-canvas-grouped.js';
80
80
  export { themeFrames, type ThemeFrame } from './css/css-theme-frame.js';
package/dist/index.js CHANGED
@@ -45,7 +45,7 @@ export { Dialog, dialogAppearances, dialogPlacements, dialogSizes, } from './com
45
45
  export { StatTile, statTileAppearances, statTileTones, } from './components/StatTile.js';
46
46
  export { TabPanel, Tabs, tabId, tabPanelId, } from './components/Tabs.js';
47
47
  export { Toast, toastPlacements, toastTones, } from './components/Toast.js';
48
- export { Field, fieldVariants, } from './components/Field.js';
48
+ export { Field, fieldDescriptionTones, fieldVariants, } from './components/Field.js';
49
49
  export { fieldWidths } from './css/css-field-width.js';
50
50
  export { requiredMarks, } from './components/FieldLabelText.js';
51
51
  export { Step, Steps, } from './components/Steps.js';
@@ -70,7 +70,7 @@ export { Select } from './components/Select.js';
70
70
  export { MultiSelect, } from './components/MultiSelect.js';
71
71
  export { Stack, } from './components/Stack.js';
72
72
  export { Table, TableBody, TableCell, TableFooter, TableHeader, TableRow, TableSelectCell, tableCellTones, tableRowRules, tableRowVariants, } from './components/Table.js';
73
- export { Text, } from './components/Text.js';
73
+ export { Text, textFonts, } from './components/Text.js';
74
74
  export { ThemeProvider, useTheme, } from './theme/ThemeProvider.js';
75
75
  export { themeCanvases } from './css/css-canvas-grouped.js';
76
76
  export { themeFrames } from './css/css-theme-frame.js';
package/dist/styles.css CHANGED
@@ -9563,6 +9563,29 @@ html:has(.sw-site-header-sticky) {
9563
9563
  .sw-field-description { color: var(--sw-color-muted); }
9564
9564
  .sw-field-error { color: var(--sw-color-danger); }
9565
9565
 
9566
+ .sw-field-description-warning {
9567
+ align-items: flex-start;
9568
+ color: var(--sw-color-warning);
9569
+ display: flex;
9570
+ gap: var(--sw-space-1);
9571
+ }
9572
+
9573
+ .sw-field-description-glyph {
9574
+ fill: none;
9575
+ flex: none;
9576
+ height: var(--sw-space-4);
9577
+ margin-block: 1px;
9578
+ stroke: currentColor;
9579
+ stroke-linecap: round;
9580
+ stroke-linejoin: round;
9581
+ stroke-width: 2;
9582
+ width: var(--sw-space-4);
9583
+ }
9584
+
9585
+ @media (forced-colors: active) {
9586
+ .sw-field-description-warning { color: CanvasText; }
9587
+ }
9588
+
9566
9589
  /* An error region is always rendered as a live region; empty, it leaves the layout (and its gap) but stays in the accessibility tree. */
9567
9590
  .sw-field-error:empty { position: absolute; }
9568
9591
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.57.0",
3
+ "version": "1.58.0",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",