@scalewing/react 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3ce39ea: Add `ButtonGroup`, the action row of a form or dialog: buttons on one line from the `md` breakpoint up, aligned `start`, `end` (default) or `between`; below `md` they stack full width in source order (generated `sw-button-group` and `sw-button-group-*`).
8
+ - 3e20a6e: Add `SegmentedControl` `disabled`: the group reports `aria-disabled`, every segment is a disabled button, arrow keys and clicks are ignored, and the track fades to `--sw-disabled-opacity` (generated `sw-segmented-disabled`). The recorded choice stays visible, for an identity that can no longer change; a disabled control may omit `onChange`.
9
+
10
+ ## 1.2.0
11
+
12
+ ### Minor Changes
13
+
14
+ - 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.
15
+ - 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.
16
+ - 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.
17
+
3
18
  ## 1.1.0
4
19
 
5
20
  ### Minor Changes
@@ -0,0 +1,14 @@
1
+ import { type BoxProps } from './Box.js';
2
+ export type ButtonGroupJustify = 'start' | 'end' | 'between';
3
+ export type ButtonGroupProps = BoxProps & {
4
+ /** Where the row sits from the `md` breakpoint up; below it every button stacks full width in source order. */
5
+ justify?: ButtonGroupJustify;
6
+ };
7
+ /**
8
+ * The action row of a form or dialog: buttons on one line, wrapping when
9
+ * they must, aligned to the end by default. On a phone the same buttons
10
+ * stack full width so each one is a whole-row target, in source order
11
+ * (put the primary action last, as the row already does). It is a `group`,
12
+ * so an `aria-label` names the row for assistive tech.
13
+ */
14
+ export declare function ButtonGroup({ className, justify, ...rest }: ButtonGroupProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../class-names.js';
3
+ import { Box } from './Box.js';
4
+ /**
5
+ * The action row of a form or dialog: buttons on one line, wrapping when
6
+ * they must, aligned to the end by default. On a phone the same buttons
7
+ * stack full width so each one is a whole-row target, in source order
8
+ * (put the primary action last, as the row already does). It is a `group`,
9
+ * so an `aria-label` names the row for assistive tech.
10
+ */
11
+ export function ButtonGroup({ className, justify = 'end', ...rest }) {
12
+ return (_jsx(Box, { className: cx('sw-button-group', `sw-button-group-${justify}`, className), role: "group", ...rest }));
13
+ }
@@ -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 &&
@@ -10,10 +10,29 @@ type SegmentedLabel = {
10
10
  'aria-label'?: never;
11
11
  'aria-labelledby': string;
12
12
  };
13
- export type SegmentedControlProps = SegmentedLabel & {
14
- items: readonly SegmentedItem[];
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';
20
+ /**
21
+ * A disabled control can never report a change, so it needs no `onChange`;
22
+ * a live one must have it.
23
+ */
24
+ type SegmentedChange = {
25
+ disabled?: false;
15
26
  onChange: (id: string) => void;
27
+ } | {
28
+ /** Keeps the current choice visible but inert, for an identity that can no longer change. */
29
+ disabled: true;
30
+ onChange?: (id: string) => void;
31
+ };
32
+ export type SegmentedControlProps = SegmentedLabel & SegmentedChange & {
33
+ items: readonly SegmentedItem[];
16
34
  value: string;
35
+ variant?: SegmentedControlVariant;
17
36
  };
18
37
  export declare const SegmentedControl: import("react").ForwardRefExoticComponent<SegmentedControlProps & import("react").RefAttributes<HTMLDivElement>>;
19
38
  export {};
@@ -1,9 +1,9 @@
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({ disabled = false, items, onChange, value, variant = 'compact', ...labelProps }, ref) {
5
5
  function onKeyDown(event) {
6
- if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') {
6
+ if (disabled || (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft')) {
7
7
  return;
8
8
  }
9
9
  const index = items.findIndex((item) => item.id === value);
@@ -14,14 +14,14 @@ export const SegmentedControl = forwardRef(function SegmentedControl({ items, on
14
14
  const delta = event.key === 'ArrowRight' ? 1 : -1;
15
15
  const next = items[(index + delta + items.length) % items.length];
16
16
  if (next) {
17
- onChange(next.id);
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, "aria-disabled": disabled || undefined, className: cx('sw-segmented', variant === 'filled' && 'sw-segmented-filled', disabled && 'sw-segmented-disabled'), onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
21
21
  const selected = item.id === value;
22
- return (_jsx("button", { "aria-checked": selected, className: cx('sw-segmented-item', selected && 'sw-segmented-item-selected'), onClick: () => {
22
+ return (_jsx("button", { "aria-checked": selected, className: cx('sw-segmented-item', selected && 'sw-segmented-item-selected'), disabled: disabled, onClick: () => {
23
23
  if (!selected) {
24
- onChange(item.id);
24
+ onChange?.(item.id);
25
25
  }
26
26
  }, role: "radio", type: "button", children: item.label }, item.id));
27
27
  }) }));
@@ -4,4 +4,5 @@ export declare function buttonClassNames(options: {
4
4
  variant: ButtonVariant;
5
5
  }): string[];
6
6
  export declare function cssButtonClasses(): string;
7
+ export declare const buttonGroupJustifies: readonly ["start", "end", "between"];
7
8
  export declare function buttonClassCatalog(): string[];
@@ -1,4 +1,5 @@
1
1
  import { buttonSizes, buttonVariants, controlScale, typographyVariants, } from '@scalewing/tokens';
2
+ import { breakpointQuery } from './breakpoints.js';
2
3
  const label = typographyVariants.label;
3
4
  export function buttonClassNames(options) {
4
5
  return [
@@ -57,12 +58,32 @@ export function cssButtonClasses() {
57
58
  }
58
59
 
59
60
  ${variantRules()}
60
- ${sizeRules()}`;
61
+ ${sizeRules()}
62
+ ${groupRules()}`;
63
+ }
64
+ export const buttonGroupJustifies = ['start', 'end', 'between'];
65
+ /** The action row: one line from md up, full-width stacked buttons below it. */
66
+ function groupRules() {
67
+ const justify = {
68
+ start: 'flex-start',
69
+ end: 'flex-end',
70
+ between: 'space-between',
71
+ };
72
+ return [
73
+ `.sw-button-group { align-items: center; display: flex; flex-wrap: wrap; gap: var(--sw-space-2); min-width: 0; }`,
74
+ ...buttonGroupJustifies.map((name) => `.sw-button-group-${name} { justify-content: ${justify[name]}; }`),
75
+ `@media ${breakpointQuery('below', 'md')} {
76
+ .sw-button-group { align-items: stretch; flex-direction: column; }
77
+ .sw-button-group > * { width: 100%; }
78
+ }`,
79
+ ].join('\n');
61
80
  }
62
81
  export function buttonClassCatalog() {
63
82
  return [
64
83
  'sw-button',
65
84
  ...buttonVariants.map((variant) => `sw-button-${variant}`),
66
85
  ...buttonSizes.map((size) => `sw-button-${size}`),
86
+ 'sw-button-group',
87
+ ...buttonGroupJustifies.map((name) => `sw-button-group-${name}`),
67
88
  ];
68
89
  }
@@ -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,38 @@ 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
+
117
+ .sw-segmented-disabled {
118
+ opacity: var(--sw-disabled-opacity);
119
+ }
120
+
121
+ .sw-segmented-item:disabled {
122
+ cursor: not-allowed;
123
+ }
124
+
92
125
  .sw-table-wrap {
93
126
  overflow: auto;
94
127
  width: 100%;
@@ -177,6 +210,8 @@ export function dataClassCatalog() {
177
210
  'sw-segmented',
178
211
  'sw-segmented-item',
179
212
  'sw-segmented-item-selected',
213
+ 'sw-segmented-filled',
214
+ 'sw-segmented-disabled',
180
215
  'sw-table-wrap',
181
216
  'sw-table',
182
217
  'sw-table-end',
@@ -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,5 @@
1
1
  import { type Breakpoint } from './breakpoints.js';
2
- export declare const gridColumnCounts: readonly [1, 2, 3, 4];
2
+ export declare const gridColumnCounts: readonly [1, 2, 3, 4, 6];
3
3
  export type GridColumns = (typeof gridColumnCounts)[number];
4
4
  export declare function gridColumnsClass(columns: GridColumns): string;
5
5
  export declare function gridColumnsBelowClass(breakpoint: Breakpoint, columns: GridColumns): string;
@@ -1,5 +1,5 @@
1
1
  import { breakpointQuery, breakpoints, } from './breakpoints.js';
2
- export const gridColumnCounts = [1, 2, 3, 4];
2
+ export const gridColumnCounts = [1, 2, 3, 4, 6];
3
3
  export function gridColumnsClass(columns) {
4
4
  return `sw-grid-cols-${columns}`;
5
5
  }
@@ -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;
package/dist/index.d.ts CHANGED
@@ -13,9 +13,9 @@ export { Badge, type BadgeProps, type BadgeSize, type BadgeTone, badgeSizes, bad
13
13
  export { BarChart, type BarChartItem, type BarChartProps, } from './components/BarChart.js';
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
+ export { ButtonGroup, type ButtonGroupJustify, type ButtonGroupProps, } from './components/ButtonGroup.js';
16
17
  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';
18
+ export { Dialog, type DialogProps, type DialogSize, } from './components/Dialog.js';
19
19
  export { Toast } from './components/Toast.js';
20
20
  export type { ToastProps } from './components/Toast.js';
21
21
  export { Field, type FieldProps, type FieldSize } from './components/Field.js';
@@ -23,7 +23,7 @@ export { Inline, type InlineProps } from './components/Inline.js';
23
23
  export { Split } from './components/Split.js';
24
24
  export type { SplitProps } from './components/Split.js';
25
25
  export { Nav, type NavProps } from './components/Nav.js';
26
- export { SegmentedControl, type SegmentedControlProps, type SegmentedItem, } from './components/SegmentedControl.js';
26
+ export { SegmentedControl, type SegmentedControlProps, type SegmentedControlVariant, type SegmentedItem, } from './components/SegmentedControl.js';
27
27
  export { Select } from './components/Select.js';
28
28
  export type { SelectAction, SelectOption, SelectProps, } from './components/Select.js';
29
29
  export { Stack, type Align, type Justify, type StackProps, } from './components/Stack.js';
package/dist/index.js CHANGED
@@ -12,8 +12,9 @@ export { Badge, badgeSizes, badgeTones, } from './components/Badge.js';
12
12
  export { BarChart, } from './components/BarChart.js';
13
13
  export { Button, } from './components/Button.js';
14
14
  export { Box } from './components/Box.js';
15
+ export { ButtonGroup, } from './components/ButtonGroup.js';
15
16
  export { Card } from './components/Card.js';
16
- export { Dialog } from './components/Dialog.js';
17
+ export { Dialog, } from './components/Dialog.js';
17
18
  export { Toast } from './components/Toast.js';
18
19
  export { Field } from './components/Field.js';
19
20
  export { Inline } from './components/Inline.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;
@@ -774,6 +776,14 @@ body {
774
776
  .sw-button-xs { min-height: 28px; padding-inline: 10px; }
775
777
  .sw-button-sm { min-height: 32px; padding-inline: 12px; }
776
778
  .sw-button-md { min-height: 44px; padding-inline: 16px; }
779
+ .sw-button-group { align-items: center; display: flex; flex-wrap: wrap; gap: var(--sw-space-2); min-width: 0; }
780
+ .sw-button-group-start { justify-content: flex-start; }
781
+ .sw-button-group-end { justify-content: flex-end; }
782
+ .sw-button-group-between { justify-content: space-between; }
783
+ @media not all and (min-width: 48rem) {
784
+ .sw-button-group { align-items: stretch; flex-direction: column; }
785
+ .sw-button-group > * { width: 100%; }
786
+ }
777
787
 
778
788
  .sw-checkbox {
779
789
  display: flex;
@@ -1098,6 +1108,38 @@ body {
1098
1108
  color: var(--sw-color-text);
1099
1109
  }
1100
1110
 
1111
+ .sw-segmented-filled {
1112
+ display: grid;
1113
+ grid-auto-columns: minmax(0, 1fr);
1114
+ grid-auto-flow: column;
1115
+ }
1116
+
1117
+ .sw-segmented-filled .sw-segmented-item {
1118
+ align-items: center;
1119
+ color: var(--sw-color-text);
1120
+ display: inline-flex;
1121
+ font-size: 17px;
1122
+ justify-content: center;
1123
+ letter-spacing: -0.2px;
1124
+ line-height: 25px;
1125
+ min-height: var(--sw-control-md-min-height);
1126
+ min-width: 0;
1127
+ padding-inline: var(--sw-control-md-padding-inline);
1128
+ }
1129
+
1130
+ .sw-segmented-filled .sw-segmented-item-selected {
1131
+ background: var(--sw-color-accent);
1132
+ color: var(--sw-color-onAccent);
1133
+ }
1134
+
1135
+ .sw-segmented-disabled {
1136
+ opacity: var(--sw-disabled-opacity);
1137
+ }
1138
+
1139
+ .sw-segmented-item:disabled {
1140
+ cursor: not-allowed;
1141
+ }
1142
+
1101
1143
  .sw-table-wrap {
1102
1144
  overflow: auto;
1103
1145
  width: 100%;
@@ -1389,6 +1431,10 @@ body {
1389
1431
  width: calc(100% - var(--sw-space-8));
1390
1432
  }
1391
1433
 
1434
+ .sw-dialog-lg {
1435
+ max-width: min(var(--sw-dialog-max-lg), calc(100vw - var(--sw-space-8)));
1436
+ }
1437
+
1392
1438
  .sw-dialog::backdrop {
1393
1439
  background: color-mix(in srgb, var(--sw-color-text) 28%, transparent);
1394
1440
  }
@@ -1982,11 +2028,13 @@ body {
1982
2028
  .sw-grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1983
2029
  .sw-grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
1984
2030
  .sw-grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
2031
+ .sw-grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
1985
2032
  @media not all and (min-width: 48rem) {
1986
2033
  .sw-grid-cols-below-md-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
1987
2034
  .sw-grid-cols-below-md-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1988
2035
  .sw-grid-cols-below-md-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
1989
2036
  .sw-grid-cols-below-md-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
2037
+ .sw-grid-cols-below-md-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
1990
2038
  }
1991
2039
  .sw-denomination-grid {
1992
2040
  color: var(--sw-color-text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",