@scalewing/react 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +11 -1
  3. package/dist/components/DenominationGrid.d.ts +3 -0
  4. package/dist/components/DenominationGrid.js +12 -0
  5. package/dist/components/Dialog.d.ts +7 -1
  6. package/dist/components/Dialog.js +2 -2
  7. package/dist/components/FilterChips.d.ts +1 -0
  8. package/dist/components/FilterChips.js +9 -3
  9. package/dist/components/Grid.d.ts +11 -0
  10. package/dist/components/Grid.js +21 -0
  11. package/dist/components/SegmentedControl.d.ts +8 -0
  12. package/dist/components/SegmentedControl.js +2 -2
  13. package/dist/components/Text.d.ts +3 -0
  14. package/dist/components/Text.js +2 -2
  15. package/dist/components/denomination-grid/DenominationStrip.d.ts +4 -0
  16. package/dist/components/denomination-grid/DenominationStrip.js +10 -0
  17. package/dist/components/denomination-grid/DenominationTiles.d.ts +4 -0
  18. package/dist/components/denomination-grid/DenominationTiles.js +13 -0
  19. package/dist/components/denomination-grid/types.d.ts +25 -0
  20. package/dist/components/denomination-grid/types.js +1 -0
  21. package/dist/components/denomination-grid/validate.d.ts +2 -0
  22. package/dist/components/denomination-grid/validate.js +18 -0
  23. package/dist/css/css-components.js +19 -1
  24. package/dist/css/css-data.js +26 -0
  25. package/dist/css/css-denomination-grid.d.ts +2 -0
  26. package/dist/css/css-denomination-grid.js +213 -0
  27. package/dist/css/css-dialog.js +5 -1
  28. package/dist/css/css-filter-chips.js +24 -0
  29. package/dist/css/css-grid.d.ts +7 -0
  30. package/dist/css/css-grid.js +31 -0
  31. package/dist/css/css-variables.js +1 -0
  32. package/dist/css/stylesheet.js +7 -0
  33. package/dist/denomination-cells.d.ts +12 -0
  34. package/dist/denomination-cells.js +15 -0
  35. package/dist/index.d.ts +7 -4
  36. package/dist/index.js +4 -2
  37. package/dist/styles.css +255 -0
  38. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 829dbdb: Add `Dialog` `size` (`'md' | 'lg'`): `lg` widens the modal to `--sw-dialog-max-lg` (56rem) so a row of six fields or a data grid fits without folding. The default stays `md` (32rem); phones keep the viewport gutter for both.
8
+ - 28b53e0: `Grid` accepts `columns={6}` and `columnsBelow={{ md: 6 }}` (generated `sw-grid-cols-6` and `sw-grid-cols-below-md-6`) so a six-denomination entry row keeps one row on desktop. Five and seven still throw.
9
+ - 9363116: Add `SegmentedControl` `variant` (`'compact' | 'filled'`): `filled` gives every segment the same width and an accent-filled selection (generated `sw-segmented-filled`); it takes the full width in a Stack and its labels' width in an Inline. The default `compact` look is unchanged.
10
+
11
+ ## 1.1.0
12
+
13
+ ### Minor Changes
14
+
15
+ - f45ef5d: Generate compact `display` and `heading` sizes below the `md` breakpoint from the new tokens, so page titles and panel figures stop dominating a phone screen. No product change needed.
16
+ - 01e7d0e: Add `align` (`start`, `center`, `end`) to `Text`, backed by generated `sw-text-align-*` classes, and keep `DenominationGrid` tiles three per row below the `md` breakpoint.
17
+ - f00b3cc: Add `DenominationGrid`, a read-only count-per-unit display across a fixed column set. The `strip` layout is a captioned table with a toned row label (icon slot), muted zero cells, signed delta cells toned by sign, and an optional consumer-formatted total that moves under the row label below the `md` breakpoint; the `tiles` layout stacks the column label, count, and an optional consumer-formatted subtotal per column. Generated `sw-denomination-*` classes; invalid columns, rows, or cells throw.
18
+ - cc9aa28: Add an optional `count` to `FilterChipOption`. It renders as a tabular chicklet after the chip label (generated `sw-filter-chip-count`), joins the option's accessible name, and a zero count quiets the chip (`sw-filter-chip-quiet`) until it is selected. Negative or fractional counts throw.
19
+ - f7f3bf9: Add `Grid`, a Box-based layout that places children in one to four equal-width columns with a token gap step and an optional smaller column count below the `md` breakpoint (`columnsBelow={{ md: 2 }}`), backed by generated `sw-grid-*` classes.
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [f45ef5d]
24
+ - @scalewing/tokens@1.1.0
25
+
3
26
  ## 1.0.0
4
27
 
5
28
  ### Major Changes
package/README.md CHANGED
@@ -18,7 +18,9 @@ import {
18
18
  Progress,
19
19
  Tooltip,
20
20
  Separator,
21
+ DenominationGrid,
21
22
  FilterChips,
23
+ Grid,
22
24
  DateField,
23
25
  Field,
24
26
  Select,
@@ -62,7 +64,15 @@ Import the CSS once at the application entry. Do not copy it into your source tr
62
64
 
63
65
  `Separator` divides sections with a token-colored line. It is horizontal by default; use `orientation="vertical"` in a flex row. Set `decorative` when the line only supports layout so assistive technology ignores it.
64
66
 
65
- `FilterChips` presents a long, wrapping single-choice filter set. Provide a localized group `label`, controlled `value`, `onChange`, and options with stable values and localized labels. Counts belong in those labels. Native radios provide arrow and Space navigation; disabled options are skipped.
67
+ `Text` steps `display` and `heading` down to the compact token sizes below the `md` breakpoint; consumers do not size type per viewport.
68
+
69
+ `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.
70
+
71
+ `Grid` places children in one to four 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.
72
+
73
+ `DenominationGrid` shows integer counts per unit across a fixed set of columns. The `strip` layout is a captioned table: each row has a toned label with an icon slot, muted zero cells, optional signed deltas toned by sign, and an optional consumer-formatted `total` that moves under the label on a phone. The `tiles` layout stacks the column label, the count, and an optional `subtotal` string per column. The primitive does no arithmetic and no currency formatting; tones reuse the Badge vocabulary. Invalid columns, rows, or cells throw.
74
+
75
+ `FilterChips` presents a long, wrapping single-choice filter set. Provide a localized group `label`, controlled `value`, `onChange`, and options with stable values and localized labels. An optional non-negative integer `count` renders as a tabular chicklet after the label and is part of the option's accessible name; a zero count quiets the chip until it is selected. Native radios provide arrow and Space navigation; disabled options are skipped.
66
76
 
67
77
  `DateField` is a controlled native date input. Supply `value`, `min`, and `max` as valid `YYYY-MM-DD` dates; use an empty `value` for a blank field. Its callback returns a date-only string or an empty string, without timezone conversion. Invalid serialized dates and inverted bounds throw; a value outside the bounds remains visible with invalid styling. For a blank required field, supply `error` when form validation runs to show the message and invalid styling. The browser chooses the calendar and localized display.
68
78
 
@@ -0,0 +1,3 @@
1
+ import { type DenominationGridProps } from './denomination-grid/types.js';
2
+ export type { DenominationGridColumn, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridTone, } from './denomination-grid/types.js';
3
+ export declare function DenominationGrid({ label, columns, rows, layout, subtotal, zeroLabel, }: DenominationGridProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { DenominationStrip } from './denomination-grid/DenominationStrip.js';
3
+ import { DenominationTiles } from './denomination-grid/DenominationTiles.js';
4
+ import { assertDenominationGrid } from './denomination-grid/validate.js';
5
+ export function DenominationGrid({ label, columns, rows, layout = 'strip', subtotal, zeroLabel = '—', }) {
6
+ assertDenominationGrid(label, columns, rows);
7
+ if (!zeroLabel)
8
+ throw new RangeError('zeroLabel must not be empty');
9
+ if (layout === 'tiles')
10
+ return (_jsx(DenominationTiles, { columns: columns, label: label, rows: rows, subtotal: subtotal, zeroLabel: zeroLabel }));
11
+ return (_jsx(DenominationStrip, { columns: columns, label: label, rows: rows, zeroLabel: zeroLabel }));
12
+ }
@@ -1,8 +1,14 @@
1
1
  import { type DialogHTMLAttributes, type ReactNode } from 'react';
2
+ /**
3
+ * `md` is the reading width (32rem) for a message or a short form; `lg`
4
+ * (56rem) holds a row of six fields or a data grid without folding it.
5
+ */
6
+ export type DialogSize = 'md' | 'lg';
2
7
  export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose' | 'open' | 'title' | 'children'> & {
3
8
  open: boolean;
4
9
  onClose: () => void;
10
+ size?: DialogSize;
5
11
  title: string;
6
12
  children: ReactNode;
7
13
  };
8
- export declare function Dialog({ children, className, onClose, open, title, ...rest }: DialogProps): import("react").JSX.Element;
14
+ export declare function Dialog({ children, className, onClose, open, size, title, ...rest }: DialogProps): import("react").JSX.Element;
@@ -5,7 +5,7 @@ import { cx } from '../class-names.js';
5
5
  import { spacingClassNames } from '../spacing-classes.js';
6
6
  import { Stack } from './Stack.js';
7
7
  import { Text } from './Text.js';
8
- export function Dialog({ children, className, onClose, open, title, ...rest }) {
8
+ export function Dialog({ children, className, onClose, open, size = 'md', title, ...rest }) {
9
9
  const nodeRef = useRef(null);
10
10
  const titleId = useId();
11
11
  useEffect(() => {
@@ -23,7 +23,7 @@ export function Dialog({ children, className, onClose, open, title, ...rest }) {
23
23
  node.close();
24
24
  }
25
25
  }, [open]);
26
- return (_jsx("dialog", { ...rest, ref: nodeRef, "aria-labelledby": titleId, className: cx('sw-dialog', ...spacingClassNames({ padding: 5 }), className), onClose: onClose, onPointerDown: (event) => {
26
+ return (_jsx("dialog", { ...rest, ref: nodeRef, "aria-labelledby": titleId, className: cx('sw-dialog', size === 'lg' && 'sw-dialog-lg', ...spacingClassNames({ padding: 5 }), className), onClose: onClose, onPointerDown: (event) => {
27
27
  const rect = event.currentTarget.getBoundingClientRect();
28
28
  const inside = event.clientX >= rect.left &&
29
29
  event.clientX <= rect.right &&
@@ -1,6 +1,7 @@
1
1
  export type FilterChipOption = {
2
2
  value: string;
3
3
  label: string;
4
+ count?: number;
4
5
  disabled?: boolean;
5
6
  };
6
7
  export type FilterChipsProps = {
@@ -1,6 +1,10 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useId } from 'react';
4
+ import { cx } from '../class-names.js';
5
+ function isCount(count) {
6
+ return count === undefined || (Number.isInteger(count) && count >= 0);
7
+ }
4
8
  export function FilterChips({ label, value, onChange, options, }) {
5
9
  const name = useId();
6
10
  const values = options.map((option) => option.value);
@@ -13,8 +17,10 @@ export function FilterChips({ label, value, onChange, options, }) {
13
17
  throw new RangeError('options must have unique values');
14
18
  if (!values.includes(value))
15
19
  throw new RangeError('value must match an option');
16
- return (_jsxs("fieldset", { className: "sw-filter-chips", children: [_jsx("legend", { className: "sw-filter-chips-legend", children: label }), _jsx("span", { className: "sw-filter-chips-options", children: options.map((option) => (_jsxs("label", { className: "sw-filter-chip", children: [_jsx("input", { checked: option.value === value, className: "sw-filter-chip-input", disabled: option.disabled, name: name, onChange: (event) => {
20
+ if (options.some((option) => !isCount(option.count)))
21
+ throw new RangeError('count must be a non-negative integer');
22
+ return (_jsxs("fieldset", { className: "sw-filter-chips", children: [_jsx("legend", { className: "sw-filter-chips-legend", children: label }), _jsx("span", { className: "sw-filter-chips-options", children: options.map((option) => (_jsxs("label", { className: cx('sw-filter-chip', option.count === 0 && 'sw-filter-chip-quiet'), children: [_jsx("input", { checked: option.value === value, className: "sw-filter-chip-input", disabled: option.disabled, name: name, onChange: (event) => {
17
23
  if (event.currentTarget.checked && !option.disabled)
18
24
  onChange(option.value);
19
- }, type: "radio", value: option.value }), _jsx("span", { className: "sw-filter-chip-face", children: option.label })] }, option.value))) })] }));
25
+ }, type: "radio", value: option.value }), _jsxs("span", { className: "sw-filter-chip-face", children: [option.label, option.count === undefined ? null : (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-filter-chip-count", children: option.count })] }))] })] }, option.value))) })] }));
20
26
  }
@@ -0,0 +1,11 @@
1
+ import { type SpacingStep } from '@scalewing/tokens';
2
+ import { type Breakpoint } from '../css/breakpoints.js';
3
+ import { type GridColumns } from '../css/css-grid.js';
4
+ import { type BoxProps } from './Box.js';
5
+ export type { GridColumns };
6
+ export type GridProps = BoxProps & {
7
+ columns?: GridColumns;
8
+ columnsBelow?: Partial<Record<Breakpoint, GridColumns>>;
9
+ gap?: SpacingStep;
10
+ };
11
+ export declare function Grid({ className, columns, columnsBelow, gap, ...rest }: GridProps): import("react").JSX.Element;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../class-names.js';
3
+ import { breakpoints } from '../css/breakpoints.js';
4
+ import { gridColumnCounts, gridColumnsBelowClass, gridColumnsClass, } from '../css/css-grid.js';
5
+ import { spacingClass } from '../css/spacing-classes.js';
6
+ import { Box } from './Box.js';
7
+ function assertColumns(columns) {
8
+ if (!gridColumnCounts.includes(columns))
9
+ throw new RangeError(`columns must be one of ${gridColumnCounts.join(', ')}`);
10
+ }
11
+ export function Grid({ className, columns = 1, columnsBelow, gap = 0, ...rest }) {
12
+ assertColumns(columns);
13
+ const belowClasses = breakpoints.flatMap((breakpoint) => {
14
+ const count = columnsBelow?.[breakpoint];
15
+ if (count === undefined)
16
+ return [];
17
+ assertColumns(count);
18
+ return [gridColumnsBelowClass(breakpoint, count)];
19
+ });
20
+ return (_jsx(Box, { className: cx('sw-grid', gridColumnsClass(columns), ...belowClasses, spacingClass('gap', 'all', gap), className), ...rest }));
21
+ }
@@ -10,10 +10,18 @@ type SegmentedLabel = {
10
10
  'aria-label'?: never;
11
11
  'aria-labelledby': string;
12
12
  };
13
+ /**
14
+ * `compact` is the quiet chip track for a section switch; `filled` gives every
15
+ * segment the same width and paints the selected one in the accent, for a
16
+ * choice that decides what a form does. It sizes to its container: the full
17
+ * width in a Stack, the width of its widest label in an Inline.
18
+ */
19
+ export type SegmentedControlVariant = 'compact' | 'filled';
13
20
  export type SegmentedControlProps = SegmentedLabel & {
14
21
  items: readonly SegmentedItem[];
15
22
  onChange: (id: string) => void;
16
23
  value: string;
24
+ variant?: SegmentedControlVariant;
17
25
  };
18
26
  export declare const SegmentedControl: import("react").ForwardRefExoticComponent<SegmentedControlProps & import("react").RefAttributes<HTMLDivElement>>;
19
27
  export {};
@@ -1,7 +1,7 @@
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
- export const SegmentedControl = forwardRef(function SegmentedControl({ items, onChange, value, ...labelProps }, ref) {
4
+ export const SegmentedControl = forwardRef(function SegmentedControl({ items, onChange, value, variant = 'compact', ...labelProps }, ref) {
5
5
  function onKeyDown(event) {
6
6
  if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') {
7
7
  return;
@@ -17,7 +17,7 @@ export const SegmentedControl = forwardRef(function SegmentedControl({ items, on
17
17
  onChange(next.id);
18
18
  }
19
19
  }
20
- return (_jsx("div", { ref: ref, className: "sw-segmented", onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
20
+ return (_jsx("div", { ref: ref, className: cx('sw-segmented', variant === 'filled' && 'sw-segmented-filled'), onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
21
21
  const selected = item.id === value;
22
22
  return (_jsx("button", { "aria-checked": selected, className: cx('sw-segmented-item', selected && 'sw-segmented-item-selected'), onClick: () => {
23
23
  if (!selected) {
@@ -1,13 +1,16 @@
1
1
  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
+ export type TextAlign = 'start' | 'center' | 'end';
4
5
  export type TextProps = HTMLAttributes<HTMLElement> & Pick<LabelHTMLAttributes<HTMLLabelElement>, 'htmlFor'> & {
6
+ align?: TextAlign;
5
7
  as?: TextElement;
6
8
  color?: SemanticColorKey;
7
9
  truncate?: boolean;
8
10
  variant?: TypographyVariant;
9
11
  };
10
12
  export declare const Text: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLElement> & Pick<LabelHTMLAttributes<HTMLLabelElement>, "htmlFor"> & {
13
+ align?: TextAlign;
11
14
  as?: TextElement;
12
15
  color?: SemanticColorKey;
13
16
  truncate?: boolean;
@@ -10,7 +10,7 @@ const defaultElement = {
10
10
  label: 'span',
11
11
  title: 'h3',
12
12
  };
13
- export const Text = forwardRef(function Text({ as, className, color = 'text', style, truncate = false, variant = 'body', ...rest }, ref) {
13
+ export const Text = forwardRef(function Text({ align, as, className, color = 'text', style, truncate = false, variant = 'body', ...rest }, ref) {
14
14
  const Component = as ?? defaultElement[variant];
15
- return (_jsx(Component, { ref: ref, className: cx(`sw-text-${variant}`, truncate && 'sw-truncate', className), style: { color: `var(--sw-color-${color})`, ...style }, ...rest }));
15
+ 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 }));
16
16
  });
@@ -0,0 +1,4 @@
1
+ import { type DenominationGridProps } from './types.js';
2
+ type StripProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel'>>;
3
+ export declare function DenominationStrip({ label, columns, rows, zeroLabel, }: StripProps): import("react").JSX.Element;
4
+ export {};
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../../class-names.js';
3
+ import { denominationCellView } from '../../denomination-cells.js';
4
+ export function DenominationStrip({ label, columns, rows, zeroLabel, }) {
5
+ const hasTotals = rows.some((row) => row.total !== undefined);
6
+ return (_jsxs("table", { className: "sw-denomination-grid sw-denomination-strip", children: [_jsx("caption", { className: "sw-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", { className: "sw-denomination-corner" }), columns.map((column) => (_jsx("th", { className: "sw-denomination-head", scope: "col", children: column.label }, column.key))), hasTotals ? _jsx("td", { className: "sw-denomination-corner" }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`, row.signed && 'sw-denomination-row-signed'), children: [_jsxs("th", { className: "sw-denomination-label", scope: "row", children: [row.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-icon", children: row.icon })) : null, _jsx("span", { className: "sw-denomination-label-text", children: row.label }), row.total !== undefined ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-total-inline", children: row.total })) : null] }), columns.map((column, index) => {
7
+ const view = denominationCellView(row.cells[index] ?? null, row.signed ?? false, zeroLabel);
8
+ return (_jsx("td", { className: `sw-denomination-cell sw-denomination-cell-${view.state}`, children: view.text }, column.key));
9
+ }), hasTotals ? (_jsx("td", { className: "sw-denomination-total", children: row.total ?? '' })) : null] }, row.id))) })] }));
10
+ }
@@ -0,0 +1,4 @@
1
+ import { type DenominationGridProps } from './types.js';
2
+ type TilesProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel'>> & Pick<DenominationGridProps, 'subtotal'>;
3
+ export declare function DenominationTiles({ label, columns, rows, subtotal, zeroLabel, }: TilesProps): import("react").JSX.Element;
4
+ export {};
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../../class-names.js';
3
+ import { denominationCellView } from '../../denomination-cells.js';
4
+ export function DenominationTiles({ label, columns, rows, subtotal, zeroLabel, }) {
5
+ return (_jsx("div", { "aria-label": label, className: "sw-denomination-grid sw-denomination-tiles", role: "group", children: rows.map((row) => (_jsxs("section", { "aria-label": row.label, className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`), children: [rows.length > 1 || row.icon ? (_jsxs("span", { className: "sw-denomination-label", children: [row.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-icon", children: row.icon })) : null, _jsx("span", { className: "sw-denomination-label-text", children: row.label }), row.total !== undefined ? (_jsx("span", { className: "sw-denomination-total", children: row.total })) : null] })) : null, _jsx("ul", { className: "sw-denomination-tile-list", children: columns.map((column, index) => {
6
+ const cell = row.cells[index] ?? null;
7
+ const view = denominationCellView(cell, row.signed ?? false, zeroLabel);
8
+ const amount = subtotal && cell !== null && cell !== 0
9
+ ? subtotal(cell, column)
10
+ : null;
11
+ return (_jsxs("li", { className: "sw-denomination-tile", children: [_jsx("span", { className: "sw-denomination-head", children: column.label }), _jsx("span", { className: `sw-denomination-cell sw-denomination-cell-${view.state}`, children: view.text }), subtotal ? (_jsx("span", { className: "sw-denomination-subtotal", children: amount ?? zeroLabel })) : null] }, column.key));
12
+ }) })] }, row.id))) }));
13
+ }
@@ -0,0 +1,25 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type BadgeTone } from '../../css/css-data.js';
3
+ export type DenominationGridTone = BadgeTone;
4
+ export type DenominationGridLayout = 'strip' | 'tiles';
5
+ export type DenominationGridColumn = {
6
+ key: string;
7
+ label: string;
8
+ };
9
+ export type DenominationGridRow = {
10
+ id: string;
11
+ label: string;
12
+ cells: readonly (number | null)[];
13
+ icon?: ReactNode;
14
+ tone?: DenominationGridTone;
15
+ signed?: boolean;
16
+ total?: string;
17
+ };
18
+ export type DenominationGridProps = {
19
+ label: string;
20
+ columns: readonly DenominationGridColumn[];
21
+ rows: readonly DenominationGridRow[];
22
+ layout?: DenominationGridLayout;
23
+ subtotal?: (count: number, column: DenominationGridColumn) => string;
24
+ zeroLabel?: string;
25
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { type DenominationGridColumn, type DenominationGridRow } from './types.js';
2
+ export declare function assertDenominationGrid(label: string, columns: readonly DenominationGridColumn[], rows: readonly DenominationGridRow[]): void;
@@ -0,0 +1,18 @@
1
+ import { isDenominationCount } from '../../denomination-cells.js';
2
+ export function assertDenominationGrid(label, columns, rows) {
3
+ if (!label.trim())
4
+ throw new RangeError('label must not be empty');
5
+ if (columns.length === 0 ||
6
+ columns.some((column) => !column.key.trim() || !column.label.trim()))
7
+ throw new RangeError('columns must have nonempty keys and labels');
8
+ if (new Set(columns.map((column) => column.key)).size !== columns.length)
9
+ throw new RangeError('columns must have unique keys');
10
+ if (rows.some((row) => !row.id.trim() || !row.label.trim()))
11
+ throw new RangeError('rows must have nonempty ids and labels');
12
+ if (new Set(rows.map((row) => row.id)).size !== rows.length)
13
+ throw new RangeError('rows must have unique ids');
14
+ if (rows.some((row) => row.cells.length !== columns.length))
15
+ throw new RangeError('each row needs one cell per column');
16
+ if (rows.some((row) => row.cells.some((cell) => !isDenominationCount(cell))))
17
+ throw new RangeError('cells must be integers or null');
18
+ }
@@ -1,4 +1,5 @@
1
- import { typographyVariants } from '@scalewing/tokens';
1
+ import { compactTypographyVariants, typographyVariants, } from '@scalewing/tokens';
2
+ import { breakpointQuery } from './breakpoints.js';
2
3
  import { cssActionMenuClasses } from './css-action-menu.js';
3
4
  import { cssAccordionClasses } from './css-accordion.js';
4
5
  import { cssButtonClasses } from './css-button.js';
@@ -18,6 +19,8 @@ import { cssTooltipClasses } from './css-tooltip.js';
18
19
  import { cssSeparatorClasses } from './css-separator.js';
19
20
  import { cssResponsiveClasses } from './css-responsive.js';
20
21
  import { cssFilterChipsClasses } from './css-filter-chips.js';
22
+ import { cssGridClasses } from './css-grid.js';
23
+ import { cssDenominationGridClasses } from './css-denomination-grid.js';
21
24
  import { cssFieldClasses } from './css-field.js';
22
25
  import { cssToastClasses } from './css-toast.js';
23
26
  function textVariantRules() {
@@ -30,6 +33,12 @@ function textVariantRules() {
30
33
  })
31
34
  .join('\n');
32
35
  }
36
+ function compactTextVariantRules() {
37
+ const rules = Object.entries(compactTypographyVariants)
38
+ .map(([name, variant]) => ` .sw-text-${name} { font-size: ${variant.fontSize}px; line-height: ${variant.lineHeight}px; letter-spacing: ${variant.letterSpacing}px; }`)
39
+ .join('\n');
40
+ return `@media ${breakpointQuery('below', 'md')} {\n${rules}\n}`;
41
+ }
33
42
  export function cssComponentClasses() {
34
43
  return `.sw-card {
35
44
  border-radius: var(--sw-radius-lg);
@@ -74,6 +83,10 @@ export function cssComponentClasses() {
74
83
  }
75
84
  }
76
85
 
86
+ .sw-text-align-start { text-align: start; }
87
+ .sw-text-align-center { text-align: center; }
88
+ .sw-text-align-end { text-align: end; }
89
+
77
90
  .sw-truncate {
78
91
  overflow: hidden;
79
92
  text-overflow: ellipsis;
@@ -86,6 +99,8 @@ export function cssComponentClasses() {
86
99
 
87
100
  ${textVariantRules()}
88
101
 
102
+ ${compactTextVariantRules()}
103
+
89
104
  ${cssButtonClasses()}
90
105
 
91
106
  ${cssCheckboxClasses()}
@@ -120,6 +135,9 @@ ${cssSeparatorClasses()}
120
135
 
121
136
  ${cssFilterChipsClasses()}
122
137
 
138
+ ${cssGridClasses()}
139
+ ${cssDenominationGridClasses()}
140
+
123
141
  ${cssFieldClasses()}
124
142
 
125
143
  ${cssSplitClasses()}
@@ -3,6 +3,7 @@ export const badgeTones = ['neutral', 'accent', 'success', 'danger'];
3
3
  export const badgeSizes = ['sm', 'md'];
4
4
  const caption = typographyVariants.caption;
5
5
  const data = typographyVariants.data;
6
+ const body = typographyVariants.body;
6
7
  export function badgeClassNames(tone, size = 'md') {
7
8
  return ['sw-badge', `sw-badge-${tone}`, `sw-badge-${size}`];
8
9
  }
@@ -89,6 +90,30 @@ export function cssDataClasses() {
89
90
  color: var(--sw-color-text);
90
91
  }
91
92
 
93
+ .sw-segmented-filled {
94
+ display: grid;
95
+ grid-auto-columns: minmax(0, 1fr);
96
+ grid-auto-flow: column;
97
+ }
98
+
99
+ .sw-segmented-filled .sw-segmented-item {
100
+ align-items: center;
101
+ color: var(--sw-color-text);
102
+ display: inline-flex;
103
+ font-size: ${body.fontSize}px;
104
+ justify-content: center;
105
+ letter-spacing: ${body.letterSpacing}px;
106
+ line-height: ${body.lineHeight}px;
107
+ min-height: var(--sw-control-md-min-height);
108
+ min-width: 0;
109
+ padding-inline: var(--sw-control-md-padding-inline);
110
+ }
111
+
112
+ .sw-segmented-filled .sw-segmented-item-selected {
113
+ background: var(--sw-color-accent);
114
+ color: var(--sw-color-onAccent);
115
+ }
116
+
92
117
  .sw-table-wrap {
93
118
  overflow: auto;
94
119
  width: 100%;
@@ -177,6 +202,7 @@ export function dataClassCatalog() {
177
202
  'sw-segmented',
178
203
  'sw-segmented-item',
179
204
  'sw-segmented-item-selected',
205
+ 'sw-segmented-filled',
180
206
  'sw-table-wrap',
181
207
  'sw-table',
182
208
  'sw-table-end',
@@ -0,0 +1,2 @@
1
+ export declare function cssDenominationGridClasses(): string;
2
+ export declare function denominationGridClassCatalog(): string[];
@@ -0,0 +1,213 @@
1
+ import { typographyVariants } from '@scalewing/tokens';
2
+ import { breakpointQuery } from './breakpoints.js';
3
+ import { badgeTones } from './css-data.js';
4
+ const caption = typographyVariants.caption;
5
+ const data = typographyVariants.data;
6
+ const label = typographyVariants.label;
7
+ const title = typographyVariants.title;
8
+ function toneRules() {
9
+ return badgeTones
10
+ .map((tone) => `.sw-denomination-row-${tone} { --sw-denomination-tone: var(--sw-color-${tone === 'neutral' ? 'muted' : tone}); }`)
11
+ .join('\n');
12
+ }
13
+ export function cssDenominationGridClasses() {
14
+ return `.sw-denomination-grid {
15
+ color: var(--sw-color-text);
16
+ font-family: var(--sw-font-sans);
17
+ max-width: 100%;
18
+ min-width: 0;
19
+ }
20
+
21
+ ${toneRules()}
22
+
23
+ .sw-denomination-strip {
24
+ border-collapse: collapse;
25
+ width: 100%;
26
+ }
27
+
28
+ .sw-denomination-strip th,
29
+ .sw-denomination-strip td {
30
+ padding: var(--sw-space-1) var(--sw-space-2);
31
+ vertical-align: middle;
32
+ }
33
+
34
+ .sw-denomination-strip tbody tr {
35
+ border-top: 1px solid var(--sw-color-border);
36
+ }
37
+
38
+ .sw-denomination-head {
39
+ color: var(--sw-color-muted);
40
+ font-size: ${caption.fontSize}px;
41
+ font-weight: 600;
42
+ letter-spacing: ${caption.letterSpacing}px;
43
+ line-height: ${caption.lineHeight}px;
44
+ text-align: end;
45
+ }
46
+
47
+ .sw-denomination-corner {
48
+ padding: 0;
49
+ }
50
+
51
+ .sw-denomination-label {
52
+ align-items: center;
53
+ border-inline-start: var(--sw-space-1) solid var(--sw-denomination-tone);
54
+ color: var(--sw-denomination-tone);
55
+ display: flex;
56
+ flex-wrap: wrap;
57
+ font-size: ${label.fontSize}px;
58
+ font-weight: ${label.fontWeight};
59
+ gap: var(--sw-space-2);
60
+ letter-spacing: ${label.letterSpacing}px;
61
+ line-height: ${label.lineHeight}px;
62
+ min-width: 0;
63
+ text-align: start;
64
+ }
65
+
66
+ .sw-denomination-icon {
67
+ display: inline-flex;
68
+ flex: none;
69
+ }
70
+
71
+ .sw-denomination-cell {
72
+ color: var(--sw-color-text);
73
+ font-size: ${data.fontSize}px;
74
+ font-variant-numeric: tabular-nums;
75
+ font-weight: 600;
76
+ letter-spacing: ${data.letterSpacing}px;
77
+ line-height: ${data.lineHeight}px;
78
+ text-align: end;
79
+ }
80
+
81
+ .sw-denomination-cell-zero {
82
+ color: var(--sw-color-muted);
83
+ font-weight: ${data.fontWeight};
84
+ opacity: var(--sw-quiet-opacity);
85
+ }
86
+
87
+ .sw-denomination-row-signed .sw-denomination-cell-positive {
88
+ color: var(--sw-color-success);
89
+ }
90
+
91
+ .sw-denomination-cell-negative {
92
+ color: var(--sw-color-danger);
93
+ }
94
+
95
+ .sw-denomination-total {
96
+ color: var(--sw-denomination-tone);
97
+ font-size: ${data.fontSize}px;
98
+ font-variant-numeric: tabular-nums;
99
+ font-weight: 600;
100
+ letter-spacing: ${data.letterSpacing}px;
101
+ line-height: ${data.lineHeight}px;
102
+ text-align: end;
103
+ white-space: nowrap;
104
+ }
105
+
106
+ .sw-denomination-total-inline {
107
+ display: none;
108
+ flex-basis: 100%;
109
+ font-size: ${caption.fontSize}px;
110
+ font-variant-numeric: tabular-nums;
111
+ font-weight: 600;
112
+ line-height: ${caption.lineHeight}px;
113
+ }
114
+
115
+ .sw-denomination-tiles {
116
+ display: flex;
117
+ flex-direction: column;
118
+ gap: var(--sw-space-3);
119
+ }
120
+
121
+ .sw-denomination-tiles .sw-denomination-label {
122
+ border-inline-start: 0;
123
+ }
124
+
125
+ .sw-denomination-tiles .sw-denomination-row {
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: var(--sw-space-2);
129
+ }
130
+
131
+ .sw-denomination-tile-list {
132
+ display: grid;
133
+ gap: var(--sw-space-2);
134
+ grid-template-columns: repeat(auto-fit, minmax(var(--sw-space-8), 1fr));
135
+ list-style: none;
136
+ margin: 0;
137
+ padding: 0;
138
+ }
139
+
140
+ .sw-denomination-tile {
141
+ align-items: center;
142
+ background: var(--sw-color-surface);
143
+ border: 1px solid var(--sw-color-border);
144
+ border-radius: var(--sw-radius-md);
145
+ display: flex;
146
+ flex-direction: column;
147
+ gap: var(--sw-space-1);
148
+ min-width: 0;
149
+ padding: var(--sw-space-3) var(--sw-space-2);
150
+ }
151
+
152
+ .sw-denomination-tile .sw-denomination-head,
153
+ .sw-denomination-tile .sw-denomination-cell {
154
+ text-align: center;
155
+ }
156
+
157
+ .sw-denomination-tile .sw-denomination-cell {
158
+ font-size: ${title.fontSize}px;
159
+ letter-spacing: ${title.letterSpacing}px;
160
+ line-height: ${title.lineHeight}px;
161
+ }
162
+
163
+ .sw-denomination-subtotal {
164
+ color: var(--sw-color-muted);
165
+ font-size: ${caption.fontSize}px;
166
+ font-variant-numeric: tabular-nums;
167
+ letter-spacing: ${caption.letterSpacing}px;
168
+ line-height: ${caption.lineHeight}px;
169
+ text-align: center;
170
+ }
171
+
172
+ @media ${breakpointQuery('below', 'md')} {
173
+ .sw-denomination-strip .sw-denomination-total { display: none; }
174
+ .sw-denomination-total-inline { display: block; }
175
+ .sw-denomination-strip .sw-denomination-label { flex-direction: column; align-items: flex-start; gap: var(--sw-space-1); }
176
+ .sw-denomination-strip th,
177
+ .sw-denomination-strip td { padding: var(--sw-space-1); }
178
+ .sw-denomination-tile-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
179
+ }
180
+
181
+ @media (forced-colors: active) {
182
+ .sw-denomination-label { border-inline-start-color: CanvasText; color: CanvasText; }
183
+ .sw-denomination-cell-zero { color: GrayText; opacity: 1; }
184
+ .sw-denomination-cell-negative,
185
+ .sw-denomination-row-signed .sw-denomination-cell-positive,
186
+ .sw-denomination-total { color: CanvasText; }
187
+ .sw-denomination-tile { border-color: CanvasText; }
188
+ }`;
189
+ }
190
+ export function denominationGridClassCatalog() {
191
+ return [
192
+ 'sw-denomination-grid',
193
+ ...badgeTones.map((tone) => `sw-denomination-row-${tone}`),
194
+ 'sw-denomination-strip',
195
+ 'sw-denomination-head',
196
+ 'sw-denomination-corner',
197
+ 'sw-denomination-row',
198
+ 'sw-denomination-row-signed',
199
+ 'sw-denomination-label',
200
+ 'sw-denomination-label-text',
201
+ 'sw-denomination-icon',
202
+ 'sw-denomination-cell',
203
+ 'sw-denomination-cell-zero',
204
+ 'sw-denomination-cell-positive',
205
+ 'sw-denomination-cell-negative',
206
+ 'sw-denomination-total',
207
+ 'sw-denomination-total-inline',
208
+ 'sw-denomination-tiles',
209
+ 'sw-denomination-tile-list',
210
+ 'sw-denomination-tile',
211
+ 'sw-denomination-subtotal',
212
+ ];
213
+ }
@@ -15,6 +15,10 @@ export function cssDialogClasses() {
15
15
  width: calc(100% - var(--sw-space-8));
16
16
  }
17
17
 
18
+ .sw-dialog-lg {
19
+ max-width: min(var(--sw-dialog-max-lg), calc(100vw - var(--sw-space-8)));
20
+ }
21
+
18
22
  .sw-dialog::backdrop {
19
23
  background: color-mix(in srgb, var(--sw-color-text) 28%, transparent);
20
24
  }
@@ -25,5 +29,5 @@ export function cssDialogClasses() {
25
29
  }`;
26
30
  }
27
31
  export function dialogClassCatalog() {
28
- return ['sw-dialog'];
32
+ return ['sw-dialog', 'sw-dialog-lg'];
29
33
  }
@@ -1,5 +1,6 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  const label = typographyVariants.label;
3
+ const data = typographyVariants.data;
3
4
  export function cssFilterChipsClasses() {
4
5
  return `.sw-filter-chips {
5
6
  border: 0;
@@ -51,6 +52,7 @@ export function cssFilterChipsClasses() {
51
52
  display: inline-flex;
52
53
  font-family: var(--sw-font-sans);
53
54
  font-size: ${label.fontSize}px;
55
+ gap: var(--sw-space-2);
54
56
  line-height: ${label.lineHeight}px;
55
57
  min-height: var(--sw-control-md-min-height);
56
58
  min-width: 0;
@@ -58,12 +60,30 @@ export function cssFilterChipsClasses() {
58
60
  padding: var(--sw-space-1) var(--sw-space-3);
59
61
  }
60
62
 
63
+ .sw-filter-chip-count {
64
+ color: var(--sw-color-muted);
65
+ flex: none;
66
+ font-size: ${data.fontSize}px;
67
+ font-variant-numeric: tabular-nums;
68
+ font-weight: ${data.fontWeight};
69
+ letter-spacing: ${data.letterSpacing}px;
70
+ line-height: ${data.lineHeight}px;
71
+ }
72
+
73
+ .sw-filter-chip-quiet:not(:has(.sw-filter-chip-input:checked)) .sw-filter-chip-face {
74
+ opacity: var(--sw-quiet-opacity);
75
+ }
76
+
61
77
  .sw-filter-chip-input:checked + .sw-filter-chip-face {
62
78
  background: var(--sw-color-accent);
63
79
  border-color: var(--sw-color-accent);
64
80
  color: var(--sw-color-onAccent);
65
81
  }
66
82
 
83
+ .sw-filter-chip-input:checked + .sw-filter-chip-face .sw-filter-chip-count {
84
+ color: inherit;
85
+ }
86
+
67
87
  .sw-filter-chip-input:focus-visible + .sw-filter-chip-face {
68
88
  outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
69
89
  outline-offset: var(--sw-focus-ring-offset);
@@ -78,6 +98,8 @@ export function cssFilterChipsClasses() {
78
98
  .sw-filter-chip-face { background: Canvas; border-color: CanvasText; color: CanvasText; }
79
99
  .sw-filter-chip-input:checked + .sw-filter-chip-face { background: Highlight; border-color: Highlight; color: HighlightText; }
80
100
  .sw-filter-chip-input:focus-visible + .sw-filter-chip-face { outline-color: Highlight; }
101
+ .sw-filter-chip-count { color: inherit; }
102
+ .sw-filter-chip-quiet:not(:has(.sw-filter-chip-input:checked)) .sw-filter-chip-face { opacity: 1; color: GrayText; border-color: GrayText; }
81
103
  }`;
82
104
  }
83
105
  export function filterChipsClassCatalog() {
@@ -88,5 +110,7 @@ export function filterChipsClassCatalog() {
88
110
  'sw-filter-chip',
89
111
  'sw-filter-chip-input',
90
112
  'sw-filter-chip-face',
113
+ 'sw-filter-chip-count',
114
+ 'sw-filter-chip-quiet',
91
115
  ];
92
116
  }
@@ -0,0 +1,7 @@
1
+ import { type Breakpoint } from './breakpoints.js';
2
+ export declare const gridColumnCounts: readonly [1, 2, 3, 4, 6];
3
+ export type GridColumns = (typeof gridColumnCounts)[number];
4
+ export declare function gridColumnsClass(columns: GridColumns): string;
5
+ export declare function gridColumnsBelowClass(breakpoint: Breakpoint, columns: GridColumns): string;
6
+ export declare function cssGridClasses(): string;
7
+ export declare function gridClassCatalog(): string[];
@@ -0,0 +1,31 @@
1
+ import { breakpointQuery, breakpoints, } from './breakpoints.js';
2
+ export const gridColumnCounts = [1, 2, 3, 4, 6];
3
+ export function gridColumnsClass(columns) {
4
+ return `sw-grid-cols-${columns}`;
5
+ }
6
+ export function gridColumnsBelowClass(breakpoint, columns) {
7
+ return `sw-grid-cols-below-${breakpoint}-${columns}`;
8
+ }
9
+ function columnsRule(className, columns) {
10
+ return `.${className} { grid-template-columns: repeat(${columns}, minmax(0, 1fr)); }`;
11
+ }
12
+ export function cssGridClasses() {
13
+ const base = `.sw-grid {
14
+ display: grid;
15
+ min-width: 0;
16
+ }`;
17
+ const columns = gridColumnCounts.map((count) => columnsRule(gridColumnsClass(count), count));
18
+ const below = breakpoints.map((breakpoint) => `@media ${breakpointQuery('below', breakpoint)} {
19
+ ${gridColumnCounts
20
+ .map((count) => columnsRule(gridColumnsBelowClass(breakpoint, count), count))
21
+ .join('\n')}
22
+ }`);
23
+ return [base, ...columns, ...below].join('\n');
24
+ }
25
+ export function gridClassCatalog() {
26
+ return [
27
+ 'sw-grid',
28
+ ...gridColumnCounts.map((count) => gridColumnsClass(count)),
29
+ ...breakpoints.flatMap((breakpoint) => gridColumnCounts.map((count) => gridColumnsBelowClass(breakpoint, count))),
30
+ ];
31
+ }
@@ -36,6 +36,7 @@ ${control}
36
36
  --sw-focus-ring-offset: ${focusRing.offset}px;
37
37
  --sw-container-max: 72rem;
38
38
  --sw-dialog-max: 32rem;
39
+ --sw-dialog-max-lg: 56rem;
39
40
  --sw-select-max: 16rem;
40
41
  --sw-split-min: ${splitScale.min}rem;
41
42
  --sw-split-size: ${splitScale.size}rem;
@@ -11,6 +11,8 @@ import { tooltipClassCatalog } from './css-tooltip.js';
11
11
  import { separatorClassCatalog } from './css-separator.js';
12
12
  import { responsiveClassCatalog } from './css-responsive.js';
13
13
  import { filterChipsClassCatalog } from './css-filter-chips.js';
14
+ import { gridClassCatalog } from './css-grid.js';
15
+ import { denominationGridClassCatalog } from './css-denomination-grid.js';
14
16
  import { fieldClassCatalog } from './css-field.js';
15
17
  import { cssComponentClasses } from './css-components.js';
16
18
  import { buttonClassCatalog } from './css-button.js';
@@ -34,6 +36,9 @@ export function utilityClassCatalog() {
34
36
  'sw-card',
35
37
  ...cardVariants.map((variant) => `sw-card-${variant}`),
36
38
  'sw-truncate',
39
+ 'sw-text-align-start',
40
+ 'sw-text-align-center',
41
+ 'sw-text-align-end',
37
42
  'sw-tabular',
38
43
  ...Object.keys(typographyVariants).map((name) => `sw-text-${name}`),
39
44
  ...buttonClassCatalog(),
@@ -53,6 +58,8 @@ export function utilityClassCatalog() {
53
58
  ...tooltipClassCatalog(),
54
59
  ...separatorClassCatalog(),
55
60
  ...filterChipsClassCatalog(),
61
+ ...gridClassCatalog(),
62
+ ...denominationGridClassCatalog(),
56
63
  ...fieldClassCatalog(),
57
64
  ...splitClassCatalog(),
58
65
  ...toastClassCatalog(),
@@ -0,0 +1,12 @@
1
+ export type DenominationCellState = 'zero' | 'positive' | 'negative';
2
+ export type DenominationCellView = {
3
+ state: DenominationCellState;
4
+ text: string;
5
+ };
6
+ /**
7
+ * Classifies one count for display. A null or zero count renders the zero
8
+ * label; a signed row prefixes positive counts with "+" so movement reads as
9
+ * a delta. Formatting stays plain digits: the consumer owns locale grouping.
10
+ */
11
+ export declare function denominationCellView(count: number | null, signed: boolean, zeroLabel: string): DenominationCellView;
12
+ export declare function isDenominationCount(count: number | null): boolean;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Classifies one count for display. A null or zero count renders the zero
3
+ * label; a signed row prefixes positive counts with "+" so movement reads as
4
+ * a delta. Formatting stays plain digits: the consumer owns locale grouping.
5
+ */
6
+ export function denominationCellView(count, signed, zeroLabel) {
7
+ if (count === null || count === 0)
8
+ return { state: 'zero', text: zeroLabel };
9
+ if (count < 0)
10
+ return { state: 'negative', text: String(count) };
11
+ return { state: 'positive', text: signed ? `+${count}` : String(count) };
12
+ }
13
+ export function isDenominationCount(count) {
14
+ return count === null || Number.isInteger(count);
15
+ }
package/dist/index.d.ts CHANGED
@@ -14,8 +14,7 @@ export { BarChart, type BarChartItem, type BarChartProps, } from './components/B
14
14
  export { Button, type ButtonProps, type ButtonSize, type ButtonVariant, } from './components/Button.js';
15
15
  export { Box, type BoxElement, type BoxProps } from './components/Box.js';
16
16
  export { Card, type CardProps, type CardVariant } from './components/Card.js';
17
- export { Dialog } from './components/Dialog.js';
18
- export type { DialogProps } from './components/Dialog.js';
17
+ export { Dialog, type DialogProps, type DialogSize, } from './components/Dialog.js';
19
18
  export { Toast } from './components/Toast.js';
20
19
  export type { ToastProps } from './components/Toast.js';
21
20
  export { Field, type FieldProps, type FieldSize } from './components/Field.js';
@@ -23,16 +22,20 @@ export { Inline, type InlineProps } from './components/Inline.js';
23
22
  export { Split } from './components/Split.js';
24
23
  export type { SplitProps } from './components/Split.js';
25
24
  export { Nav, type NavProps } from './components/Nav.js';
26
- export { SegmentedControl, type SegmentedControlProps, type SegmentedItem, } from './components/SegmentedControl.js';
25
+ export { SegmentedControl, type SegmentedControlProps, type SegmentedControlVariant, type SegmentedItem, } from './components/SegmentedControl.js';
27
26
  export { Select } from './components/Select.js';
28
27
  export type { SelectAction, SelectOption, SelectProps, } from './components/Select.js';
29
28
  export { Stack, type Align, type Justify, type StackProps, } from './components/Stack.js';
30
29
  export { Table, TableBody, TableCell, TableHeader, TableRow, type TableBodyProps, type TableCellProps, type TableDensity, type TableHeaderProps, type TableProps, type TableRowProps, } from './components/Table.js';
31
- export { Text, type TextElement, type TextProps } from './components/Text.js';
30
+ export { Text, type TextAlign, type TextElement, type TextProps, } from './components/Text.js';
32
31
  export { ThemeProvider, useTheme, type ThemeProviderProps, } from './theme/ThemeProvider.js';
33
32
  export { utilityClassCatalog } from './css/stylesheet.js';
34
33
  export type { VisibilityProps } from './visibility-classes.js';
35
34
  export { Separator } from './components/Separator.js';
36
35
  export type { SeparatorOrientation, SeparatorProps, } from './components/Separator.js';
36
+ export { Grid } from './components/Grid.js';
37
+ export type { GridColumns, GridProps } from './components/Grid.js';
38
+ export { DenominationGrid } from './components/DenominationGrid.js';
39
+ export type { DenominationGridColumn, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridTone, } from './components/DenominationGrid.js';
37
40
  export { FilterChips } from './components/FilterChips.js';
38
41
  export type { FilterChipOption, FilterChipsProps, } from './components/FilterChips.js';
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ export { BarChart, } from './components/BarChart.js';
13
13
  export { Button, } from './components/Button.js';
14
14
  export { Box } from './components/Box.js';
15
15
  export { Card } from './components/Card.js';
16
- export { Dialog } from './components/Dialog.js';
16
+ export { Dialog, } from './components/Dialog.js';
17
17
  export { Toast } from './components/Toast.js';
18
18
  export { Field } from './components/Field.js';
19
19
  export { Inline } from './components/Inline.js';
@@ -23,8 +23,10 @@ export { SegmentedControl, } from './components/SegmentedControl.js';
23
23
  export { Select } from './components/Select.js';
24
24
  export { Stack, } from './components/Stack.js';
25
25
  export { Table, TableBody, TableCell, TableHeader, TableRow, } from './components/Table.js';
26
- export { Text } from './components/Text.js';
26
+ export { Text, } from './components/Text.js';
27
27
  export { ThemeProvider, useTheme, } from './theme/ThemeProvider.js';
28
28
  export { utilityClassCatalog } from './css/stylesheet.js';
29
29
  export { Separator } from './components/Separator.js';
30
+ export { Grid } from './components/Grid.js';
31
+ export { DenominationGrid } from './components/DenominationGrid.js';
30
32
  export { FilterChips } from './components/FilterChips.js';
package/dist/styles.css CHANGED
@@ -39,6 +39,7 @@
39
39
  --sw-focus-ring-offset: 2px;
40
40
  --sw-container-max: 72rem;
41
41
  --sw-dialog-max: 32rem;
42
+ --sw-dialog-max-lg: 56rem;
42
43
  --sw-select-max: 16rem;
43
44
  --sw-split-min: 14rem;
44
45
  --sw-split-size: 22rem;
@@ -94,6 +95,7 @@
94
95
  --sw-focus-ring-offset: 2px;
95
96
  --sw-container-max: 72rem;
96
97
  --sw-dialog-max: 32rem;
98
+ --sw-dialog-max-lg: 56rem;
97
99
  --sw-select-max: 16rem;
98
100
  --sw-split-min: 14rem;
99
101
  --sw-split-size: 22rem;
@@ -708,6 +710,10 @@ body {
708
710
  }
709
711
  }
710
712
 
713
+ .sw-text-align-start { text-align: start; }
714
+ .sw-text-align-center { text-align: center; }
715
+ .sw-text-align-end { text-align: end; }
716
+
711
717
  .sw-truncate {
712
718
  overflow: hidden;
713
719
  text-overflow: ellipsis;
@@ -726,6 +732,11 @@ body {
726
732
  .sw-text-caption { font-family: var(--sw-font-sans); font-size: 13px; line-height: 18px; font-weight: 400; letter-spacing: -0.08px; }
727
733
  .sw-text-data { font-family: var(--sw-font-sans); font-size: 13px; line-height: 18px; font-weight: 500; letter-spacing: -0.08px; font-variant-numeric: tabular-nums; }
728
734
 
735
+ @media not all and (min-width: 48rem) {
736
+ .sw-text-display { font-size: 32px; line-height: 38px; letter-spacing: -0.6px; }
737
+ .sw-text-heading { font-size: 24px; line-height: 30px; letter-spacing: -0.4px; }
738
+ }
739
+
729
740
  .sw-button {
730
741
  appearance: none;
731
742
  align-items: center;
@@ -1089,6 +1100,30 @@ body {
1089
1100
  color: var(--sw-color-text);
1090
1101
  }
1091
1102
 
1103
+ .sw-segmented-filled {
1104
+ display: grid;
1105
+ grid-auto-columns: minmax(0, 1fr);
1106
+ grid-auto-flow: column;
1107
+ }
1108
+
1109
+ .sw-segmented-filled .sw-segmented-item {
1110
+ align-items: center;
1111
+ color: var(--sw-color-text);
1112
+ display: inline-flex;
1113
+ font-size: 17px;
1114
+ justify-content: center;
1115
+ letter-spacing: -0.2px;
1116
+ line-height: 25px;
1117
+ min-height: var(--sw-control-md-min-height);
1118
+ min-width: 0;
1119
+ padding-inline: var(--sw-control-md-padding-inline);
1120
+ }
1121
+
1122
+ .sw-segmented-filled .sw-segmented-item-selected {
1123
+ background: var(--sw-color-accent);
1124
+ color: var(--sw-color-onAccent);
1125
+ }
1126
+
1092
1127
  .sw-table-wrap {
1093
1128
  overflow: auto;
1094
1129
  width: 100%;
@@ -1380,6 +1415,10 @@ body {
1380
1415
  width: calc(100% - var(--sw-space-8));
1381
1416
  }
1382
1417
 
1418
+ .sw-dialog-lg {
1419
+ max-width: min(var(--sw-dialog-max-lg), calc(100vw - var(--sw-space-8)));
1420
+ }
1421
+
1383
1422
  .sw-dialog::backdrop {
1384
1423
  background: color-mix(in srgb, var(--sw-color-text) 28%, transparent);
1385
1424
  }
@@ -1915,6 +1954,7 @@ body {
1915
1954
  display: inline-flex;
1916
1955
  font-family: var(--sw-font-sans);
1917
1956
  font-size: 15px;
1957
+ gap: var(--sw-space-2);
1918
1958
  line-height: 20px;
1919
1959
  min-height: var(--sw-control-md-min-height);
1920
1960
  min-width: 0;
@@ -1922,12 +1962,30 @@ body {
1922
1962
  padding: var(--sw-space-1) var(--sw-space-3);
1923
1963
  }
1924
1964
 
1965
+ .sw-filter-chip-count {
1966
+ color: var(--sw-color-muted);
1967
+ flex: none;
1968
+ font-size: 13px;
1969
+ font-variant-numeric: tabular-nums;
1970
+ font-weight: 500;
1971
+ letter-spacing: -0.08px;
1972
+ line-height: 18px;
1973
+ }
1974
+
1975
+ .sw-filter-chip-quiet:not(:has(.sw-filter-chip-input:checked)) .sw-filter-chip-face {
1976
+ opacity: var(--sw-quiet-opacity);
1977
+ }
1978
+
1925
1979
  .sw-filter-chip-input:checked + .sw-filter-chip-face {
1926
1980
  background: var(--sw-color-accent);
1927
1981
  border-color: var(--sw-color-accent);
1928
1982
  color: var(--sw-color-onAccent);
1929
1983
  }
1930
1984
 
1985
+ .sw-filter-chip-input:checked + .sw-filter-chip-face .sw-filter-chip-count {
1986
+ color: inherit;
1987
+ }
1988
+
1931
1989
  .sw-filter-chip-input:focus-visible + .sw-filter-chip-face {
1932
1990
  outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
1933
1991
  outline-offset: var(--sw-focus-ring-offset);
@@ -1942,6 +2000,203 @@ body {
1942
2000
  .sw-filter-chip-face { background: Canvas; border-color: CanvasText; color: CanvasText; }
1943
2001
  .sw-filter-chip-input:checked + .sw-filter-chip-face { background: Highlight; border-color: Highlight; color: HighlightText; }
1944
2002
  .sw-filter-chip-input:focus-visible + .sw-filter-chip-face { outline-color: Highlight; }
2003
+ .sw-filter-chip-count { color: inherit; }
2004
+ .sw-filter-chip-quiet:not(:has(.sw-filter-chip-input:checked)) .sw-filter-chip-face { opacity: 1; color: GrayText; border-color: GrayText; }
2005
+ }
2006
+
2007
+ .sw-grid {
2008
+ display: grid;
2009
+ min-width: 0;
2010
+ }
2011
+ .sw-grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
2012
+ .sw-grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2013
+ .sw-grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2014
+ .sw-grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
2015
+ .sw-grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
2016
+ @media not all and (min-width: 48rem) {
2017
+ .sw-grid-cols-below-md-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
2018
+ .sw-grid-cols-below-md-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2019
+ .sw-grid-cols-below-md-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2020
+ .sw-grid-cols-below-md-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
2021
+ .sw-grid-cols-below-md-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
2022
+ }
2023
+ .sw-denomination-grid {
2024
+ color: var(--sw-color-text);
2025
+ font-family: var(--sw-font-sans);
2026
+ max-width: 100%;
2027
+ min-width: 0;
2028
+ }
2029
+
2030
+ .sw-denomination-row-neutral { --sw-denomination-tone: var(--sw-color-muted); }
2031
+ .sw-denomination-row-accent { --sw-denomination-tone: var(--sw-color-accent); }
2032
+ .sw-denomination-row-success { --sw-denomination-tone: var(--sw-color-success); }
2033
+ .sw-denomination-row-danger { --sw-denomination-tone: var(--sw-color-danger); }
2034
+
2035
+ .sw-denomination-strip {
2036
+ border-collapse: collapse;
2037
+ width: 100%;
2038
+ }
2039
+
2040
+ .sw-denomination-strip th,
2041
+ .sw-denomination-strip td {
2042
+ padding: var(--sw-space-1) var(--sw-space-2);
2043
+ vertical-align: middle;
2044
+ }
2045
+
2046
+ .sw-denomination-strip tbody tr {
2047
+ border-top: 1px solid var(--sw-color-border);
2048
+ }
2049
+
2050
+ .sw-denomination-head {
2051
+ color: var(--sw-color-muted);
2052
+ font-size: 13px;
2053
+ font-weight: 600;
2054
+ letter-spacing: -0.08px;
2055
+ line-height: 18px;
2056
+ text-align: end;
2057
+ }
2058
+
2059
+ .sw-denomination-corner {
2060
+ padding: 0;
2061
+ }
2062
+
2063
+ .sw-denomination-label {
2064
+ align-items: center;
2065
+ border-inline-start: var(--sw-space-1) solid var(--sw-denomination-tone);
2066
+ color: var(--sw-denomination-tone);
2067
+ display: flex;
2068
+ flex-wrap: wrap;
2069
+ font-size: 15px;
2070
+ font-weight: 600;
2071
+ gap: var(--sw-space-2);
2072
+ letter-spacing: -0.2px;
2073
+ line-height: 20px;
2074
+ min-width: 0;
2075
+ text-align: start;
2076
+ }
2077
+
2078
+ .sw-denomination-icon {
2079
+ display: inline-flex;
2080
+ flex: none;
2081
+ }
2082
+
2083
+ .sw-denomination-cell {
2084
+ color: var(--sw-color-text);
2085
+ font-size: 13px;
2086
+ font-variant-numeric: tabular-nums;
2087
+ font-weight: 600;
2088
+ letter-spacing: -0.08px;
2089
+ line-height: 18px;
2090
+ text-align: end;
2091
+ }
2092
+
2093
+ .sw-denomination-cell-zero {
2094
+ color: var(--sw-color-muted);
2095
+ font-weight: 500;
2096
+ opacity: var(--sw-quiet-opacity);
2097
+ }
2098
+
2099
+ .sw-denomination-row-signed .sw-denomination-cell-positive {
2100
+ color: var(--sw-color-success);
2101
+ }
2102
+
2103
+ .sw-denomination-cell-negative {
2104
+ color: var(--sw-color-danger);
2105
+ }
2106
+
2107
+ .sw-denomination-total {
2108
+ color: var(--sw-denomination-tone);
2109
+ font-size: 13px;
2110
+ font-variant-numeric: tabular-nums;
2111
+ font-weight: 600;
2112
+ letter-spacing: -0.08px;
2113
+ line-height: 18px;
2114
+ text-align: end;
2115
+ white-space: nowrap;
2116
+ }
2117
+
2118
+ .sw-denomination-total-inline {
2119
+ display: none;
2120
+ flex-basis: 100%;
2121
+ font-size: 13px;
2122
+ font-variant-numeric: tabular-nums;
2123
+ font-weight: 600;
2124
+ line-height: 18px;
2125
+ }
2126
+
2127
+ .sw-denomination-tiles {
2128
+ display: flex;
2129
+ flex-direction: column;
2130
+ gap: var(--sw-space-3);
2131
+ }
2132
+
2133
+ .sw-denomination-tiles .sw-denomination-label {
2134
+ border-inline-start: 0;
2135
+ }
2136
+
2137
+ .sw-denomination-tiles .sw-denomination-row {
2138
+ display: flex;
2139
+ flex-direction: column;
2140
+ gap: var(--sw-space-2);
2141
+ }
2142
+
2143
+ .sw-denomination-tile-list {
2144
+ display: grid;
2145
+ gap: var(--sw-space-2);
2146
+ grid-template-columns: repeat(auto-fit, minmax(var(--sw-space-8), 1fr));
2147
+ list-style: none;
2148
+ margin: 0;
2149
+ padding: 0;
2150
+ }
2151
+
2152
+ .sw-denomination-tile {
2153
+ align-items: center;
2154
+ background: var(--sw-color-surface);
2155
+ border: 1px solid var(--sw-color-border);
2156
+ border-radius: var(--sw-radius-md);
2157
+ display: flex;
2158
+ flex-direction: column;
2159
+ gap: var(--sw-space-1);
2160
+ min-width: 0;
2161
+ padding: var(--sw-space-3) var(--sw-space-2);
2162
+ }
2163
+
2164
+ .sw-denomination-tile .sw-denomination-head,
2165
+ .sw-denomination-tile .sw-denomination-cell {
2166
+ text-align: center;
2167
+ }
2168
+
2169
+ .sw-denomination-tile .sw-denomination-cell {
2170
+ font-size: 20px;
2171
+ letter-spacing: -0.3px;
2172
+ line-height: 28px;
2173
+ }
2174
+
2175
+ .sw-denomination-subtotal {
2176
+ color: var(--sw-color-muted);
2177
+ font-size: 13px;
2178
+ font-variant-numeric: tabular-nums;
2179
+ letter-spacing: -0.08px;
2180
+ line-height: 18px;
2181
+ text-align: center;
2182
+ }
2183
+
2184
+ @media not all and (min-width: 48rem) {
2185
+ .sw-denomination-strip .sw-denomination-total { display: none; }
2186
+ .sw-denomination-total-inline { display: block; }
2187
+ .sw-denomination-strip .sw-denomination-label { flex-direction: column; align-items: flex-start; gap: var(--sw-space-1); }
2188
+ .sw-denomination-strip th,
2189
+ .sw-denomination-strip td { padding: var(--sw-space-1); }
2190
+ .sw-denomination-tile-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2191
+ }
2192
+
2193
+ @media (forced-colors: active) {
2194
+ .sw-denomination-label { border-inline-start-color: CanvasText; color: CanvasText; }
2195
+ .sw-denomination-cell-zero { color: GrayText; opacity: 1; }
2196
+ .sw-denomination-cell-negative,
2197
+ .sw-denomination-row-signed .sw-denomination-cell-positive,
2198
+ .sw-denomination-total { color: CanvasText; }
2199
+ .sw-denomination-tile { border-color: CanvasText; }
1945
2200
  }
1946
2201
 
1947
2202
  .sw-field-required { color: var(--sw-color-danger); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,7 +38,7 @@
38
38
  "README.md"
39
39
  ],
40
40
  "dependencies": {
41
- "@scalewing/tokens": "^1.0.0"
41
+ "@scalewing/tokens": "^1.1.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": "^19.0.0",