@scalewing/react 0.6.0 → 0.7.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,45 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 630bed9: Add wrapping, accessible FilterChips for long single-choice filter sets.
8
+ - fad8f45: Add an accessible React ActionMenu with generated token styling for independent commands.
9
+ - 1d69e34: Add a controlled date-only DateField with native calendar behavior and generated styling.
10
+ - 05e23f5: Add stable hint, error, and required semantics to Field with generated invalid styling.
11
+ - b718519: Add supplementary Tooltip help for labelled triggers with keyboard, pointer, and touch access.
12
+ - 9e47362: Add a controlled native Checkbox with generated token styling, validation copy, and accessible form semantics.
13
+ - 6dac120: Add a controlled React Switch with native checkbox semantics and generated token styling.
14
+ - 39a85a4: Add an accessible indeterminate Spinner with generated animation, size variants, and reduced-motion styling.
15
+ - de943eb: Add measured Progress with native progressbar semantics and generated tone styling.
16
+ - b0ce196: Add a controlled native RadioGroup with token-generated styling and long-label form choices.
17
+ - 944f7ef: Add semantic and decorative horizontal or vertical separators with generated token styling.
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies [630bed9]
22
+ - Updated dependencies [fad8f45]
23
+ - Updated dependencies [1d69e34]
24
+ - Updated dependencies [05e23f5]
25
+ - Updated dependencies [b718519]
26
+ - Updated dependencies [9e47362]
27
+ - Updated dependencies [6dac120]
28
+ - Updated dependencies [39a85a4]
29
+ - Updated dependencies [de943eb]
30
+ - Updated dependencies [b0ce196]
31
+ - Updated dependencies [944f7ef]
32
+ - @scalewing/tokens@0.7.0
33
+
34
+ ## 0.6.1
35
+
36
+ ### Patch Changes
37
+
38
+ - Keep public package versions aligned with the native TabBar accessibility
39
+ correction. No DOM renderer API changed.
40
+ - Updated dependencies
41
+ - @scalewing/tokens@0.6.1
42
+
3
43
  ## 0.6.0
4
44
 
5
45
  ### Minor Changes
package/README.md CHANGED
@@ -7,12 +7,22 @@ import '@scalewing/react/styles.css';
7
7
  import '@scalewing/react/palette/cerulean.css';
8
8
  import {
9
9
  Accordion,
10
+ ActionMenu,
10
11
  BarChart,
11
12
  Box,
12
13
  Button,
13
14
  Card,
15
+ Checkbox,
16
+ RadioGroup,
17
+ Spinner,
18
+ Progress,
19
+ Tooltip,
20
+ Separator,
21
+ FilterChips,
22
+ DateField,
14
23
  Field,
15
24
  Select,
25
+ Switch,
16
26
  Split,
17
27
  Stack,
18
28
  Text,
@@ -32,6 +42,57 @@ import {
32
42
 
33
43
  Import the CSS once at the application entry. Do not copy it into your source tree. Optional: import one `@scalewing/react/palette/<id>.css` file after it, or set `data-palette` on the `data-theme` node. React apps can set `palette` on `ThemeProvider` instead.
34
44
 
35
- `Box as="a"` is a layout link. Use `Button` for press actions. `Field` wraps a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Select` is a labeled listbox menu when the open list must match the canvas.
45
+ `Box as="a"` is a layout link. Use `Button` for press actions. `Field` associates a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Select` is a labeled listbox menu when the open list must match the canvas.
46
+
47
+ `Field` supports optional `description`, `error`, and `required` on one native input, select, or textarea child. It preserves an existing `aria-describedby`, appends a stable message ID, and replaces the hint with an alert when an error is supplied. `error` sets `aria-invalid` and token-owned invalid styling. The consumer owns validation and localized messages.
48
+
49
+ `ActionMenu` opens independent commands from a labelled button. Provide localized command labels and callbacks; use `disabled` for unavailable commands and `destructive` for a dangerous command's presentation. Escape returns focus to the trigger, and outside interaction dismisses the menu.
50
+
51
+ `Switch` is a controlled on/off input. It keeps native checkbox keyboard behavior and exposes switch semantics. Provide the current `checked` value and update it in `onCheckedChange`.
52
+
53
+ `Checkbox` is a controlled native checkbox for one independent form or confirmation choice. `checked` and `onCheckedChange` own its state. The consumer supplies `error` after validation to announce and style an invalid choice; `required` retains native form semantics.
54
+
55
+ `RadioGroup` is a controlled fieldset of native radio choices. Supply a unique nonempty `value` for each option, one selected `value` or an empty value for no selection, a `legend`, and `onChange`. Long labels wrap in a vertical group. The consumer provides localized option labels and validation `error`; `required` keeps native form semantics.
56
+
57
+ `Spinner` shows indeterminate loading in small, medium, or large sizes. Supply localized `label` for the one announced status in a loading region. Use `decorative` on additional indicators beside that status so screen readers do not hear the same message repeatedly. Reduced motion leaves a static accented ring.
58
+
59
+ `Progress` shows a known value between zero and a positive maximum. It uses native progressbar semantics and displays the value and maximum beside the localized label. Invalid bounds throw instead of silently clamping. Optional `tone` is `accent`, `success`, or `danger`.
60
+
61
+ `Tooltip` adds supplementary plain-text help to one labelled, focusable trigger. Supply localized `content` and an existing trigger element with its own accessible name. It opens on hover or focus, closes on pointer leave, blur, Escape, or outside touch, and toggles on touch. Keep required instructions visible outside the tooltip.
62
+
63
+ `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
+
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.
66
+
67
+ `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
+
69
+ ```tsx
70
+ <DateField
71
+ label="Sighting date"
72
+ value={sightingDate}
73
+ onChange={setSightingDate}
74
+ min="2024-01-01"
75
+ max="2024-12-31"
76
+ />
77
+ ```
78
+
79
+ ```tsx
80
+ <Switch
81
+ label="Active habitats only"
82
+ checked={activeOnly}
83
+ onCheckedChange={setActiveOnly}
84
+ description="Hide archived habitats"
85
+ />
86
+ ```
87
+
88
+ ```tsx
89
+ <ActionMenu
90
+ label="Sighting actions"
91
+ items={[
92
+ { id: 'share', label: 'Share', onSelect: shareSighting },
93
+ { id: 'archive', label: 'Archive', onSelect: archiveSighting },
94
+ ]}
95
+ />
96
+ ```
36
97
 
37
98
  React is a peer dependency. Rationale: the host app already owns the React runtime.
@@ -0,0 +1,16 @@
1
+ import { type ReactNode } from 'react';
2
+ export type ActionMenuItem = {
3
+ id: string;
4
+ label: string;
5
+ onSelect: () => void;
6
+ icon?: ReactNode;
7
+ disabled?: boolean;
8
+ destructive?: boolean;
9
+ };
10
+ export type ActionMenuProps = {
11
+ label: string;
12
+ items: readonly ActionMenuItem[];
13
+ trigger?: ReactNode;
14
+ disabled?: boolean;
15
+ };
16
+ export declare function ActionMenu({ label, items, trigger, disabled, }: ActionMenuProps): import("react").JSX.Element;
@@ -0,0 +1,142 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId, useLayoutEffect, useRef, useState, } from 'react';
4
+ import { cx } from '../class-names.js';
5
+ function nextEnabled(items, current, direction) {
6
+ if (items.length === 0)
7
+ return -1;
8
+ for (let step = 1; step <= items.length; step += 1) {
9
+ const index = (current + direction * step + items.length) % items.length;
10
+ if (!items[index]?.disabled)
11
+ return index;
12
+ }
13
+ return -1;
14
+ }
15
+ export function ActionMenu({ label, items, trigger, disabled = false, }) {
16
+ const menuId = useId();
17
+ const rootRef = useRef(null);
18
+ const triggerRef = useRef(null);
19
+ const menuRef = useRef(null);
20
+ const openRef = useRef(false);
21
+ const [open, setOpen] = useState(false);
22
+ const [focusIndex, setFocusIndex] = useState(-1);
23
+ const enabled = !disabled && items.some((item) => !item.disabled);
24
+ useLayoutEffect(() => {
25
+ if (!enabled && open)
26
+ close();
27
+ }, [enabled, open]);
28
+ function close(restoreFocus = false) {
29
+ openRef.current = false;
30
+ setOpen(false);
31
+ if (restoreFocus)
32
+ triggerRef.current?.focus();
33
+ }
34
+ function openMenu(last = false) {
35
+ if (!enabled)
36
+ return;
37
+ const index = last
38
+ ? nextEnabled(items, 0, -1)
39
+ : nextEnabled(items, items.length - 1, 1);
40
+ openRef.current = true;
41
+ setFocusIndex(index);
42
+ setOpen(true);
43
+ }
44
+ function positionMenu() {
45
+ const triggerNode = triggerRef.current;
46
+ const menuNode = menuRef.current;
47
+ if (!triggerNode || !menuNode)
48
+ return;
49
+ const rect = triggerNode.getBoundingClientRect();
50
+ const width = menuNode.offsetWidth;
51
+ const height = menuNode.offsetHeight;
52
+ menuNode.style.left = `${Math.max(0, Math.min(rect.left, window.innerWidth - width))}px`;
53
+ menuNode.style.top = `${Math.max(0, rect.bottom + height <= window.innerHeight ? rect.bottom : rect.top - height)}px`;
54
+ }
55
+ useLayoutEffect(() => {
56
+ const menuNode = menuRef.current;
57
+ if (!menuNode)
58
+ return;
59
+ const supportsPopover = typeof menuNode.showPopover === 'function';
60
+ if (supportsPopover)
61
+ menuNode.setAttribute('popover', 'manual');
62
+ if (!open) {
63
+ if (supportsPopover && menuNode.matches(':popover-open'))
64
+ menuNode.hidePopover();
65
+ return;
66
+ }
67
+ if (supportsPopover && !menuNode.matches(':popover-open'))
68
+ menuNode.showPopover();
69
+ positionMenu();
70
+ const commands = menuNode.querySelectorAll('[role="menuitem"]');
71
+ commands[focusIndex]?.focus();
72
+ }, [focusIndex, open]);
73
+ useLayoutEffect(() => {
74
+ if (!open)
75
+ return;
76
+ function onPointerDown(event) {
77
+ const target = event.target;
78
+ if (target instanceof Node && !rootRef.current?.contains(target))
79
+ close();
80
+ }
81
+ function onMove() {
82
+ positionMenu();
83
+ }
84
+ document.addEventListener('pointerdown', onPointerDown);
85
+ window.addEventListener('resize', onMove);
86
+ window.addEventListener('scroll', onMove, true);
87
+ return () => {
88
+ document.removeEventListener('pointerdown', onPointerDown);
89
+ window.removeEventListener('resize', onMove);
90
+ window.removeEventListener('scroll', onMove, true);
91
+ };
92
+ }, [open]);
93
+ function onTriggerKeyDown(event) {
94
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
95
+ event.preventDefault();
96
+ openMenu(event.key === 'ArrowUp');
97
+ }
98
+ else if (event.key === 'Enter' || event.key === ' ') {
99
+ event.preventDefault();
100
+ if (open)
101
+ close(true);
102
+ else
103
+ openMenu();
104
+ }
105
+ }
106
+ function onMenuKeyDown(event) {
107
+ if (event.key === 'Escape') {
108
+ event.preventDefault();
109
+ close(true);
110
+ return;
111
+ }
112
+ let next = -1;
113
+ if (event.key === 'ArrowDown')
114
+ next = nextEnabled(items, focusIndex, 1);
115
+ else if (event.key === 'ArrowUp')
116
+ next = nextEnabled(items, focusIndex, -1);
117
+ else if (event.key === 'Home')
118
+ next = nextEnabled(items, items.length - 1, 1);
119
+ else if (event.key === 'End')
120
+ next = nextEnabled(items, 0, -1);
121
+ else if (event.key === 'Tab') {
122
+ close();
123
+ return;
124
+ }
125
+ if (next >= 0) {
126
+ event.preventDefault();
127
+ setFocusIndex(next);
128
+ }
129
+ }
130
+ function select(item) {
131
+ if (!openRef.current || !enabled || item.disabled)
132
+ return;
133
+ close();
134
+ item.onSelect();
135
+ }
136
+ return (_jsxs("div", { className: "sw-action-menu", ref: rootRef, children: [_jsx("button", { "aria-controls": open ? menuId : undefined, "aria-expanded": open, "aria-haspopup": "menu", "aria-label": label, className: "sw-action-menu-trigger", disabled: !enabled, onClick: () => (open ? close(true) : openMenu()), onKeyDown: onTriggerKeyDown, ref: triggerRef, type: "button", children: trigger ?? label }), _jsx("div", { "aria-label": label, className: "sw-action-menu-list", hidden: !open, id: menuId, onKeyDown: onMenuKeyDown, ref: menuRef, role: "menu", children: items.map((item, index) => (_jsxs("button", { className: cx('sw-action-menu-item', item.destructive && 'sw-action-menu-item-danger'), disabled: item.disabled, onClick: () => select(item), onFocus: () => setFocusIndex(index), onKeyDown: (event) => {
137
+ if (event.key === 'Enter' || event.key === ' ') {
138
+ event.preventDefault();
139
+ select(item);
140
+ }
141
+ }, role: "menuitem", tabIndex: index === focusIndex ? 0 : -1, type: "button", children: [item.icon ? _jsx("span", { "aria-hidden": "true", children: item.icon }) : null, _jsx("span", { children: item.label })] }, item.id))) })] }));
142
+ }
@@ -8,7 +8,7 @@ export type ButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick
8
8
  type?: 'button' | 'submit' | 'reset';
9
9
  variant?: ButtonVariant;
10
10
  };
11
- export declare const Button: import("react").ForwardRefExoticComponent<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type" | "onClick"> & {
11
+ export declare const Button: import("react").ForwardRefExoticComponent<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "type"> & {
12
12
  children: ReactNode;
13
13
  onPress: () => void;
14
14
  size?: ButtonSize;
@@ -0,0 +1,10 @@
1
+ export type CheckboxProps = {
2
+ label: string;
3
+ checked: boolean;
4
+ onCheckedChange: (checked: boolean) => void;
5
+ disabled?: boolean;
6
+ required?: boolean;
7
+ description?: string;
8
+ error?: string;
9
+ };
10
+ export declare function Checkbox({ label, checked, onCheckedChange, disabled, required, description, error, }: CheckboxProps): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId } from 'react';
4
+ export function Checkbox({ label, checked, onCheckedChange, disabled = false, required = false, description, error, }) {
5
+ const descriptionId = useId();
6
+ const errorId = useId();
7
+ const describedBy = [
8
+ description ? descriptionId : null,
9
+ error ? errorId : null,
10
+ ]
11
+ .filter((id) => id !== null)
12
+ .join(' ');
13
+ return (_jsxs("div", { className: "sw-checkbox", children: [_jsxs("label", { className: "sw-checkbox-label", children: [_jsxs("span", { className: "sw-checkbox-control", children: [_jsx("input", { "aria-describedby": describedBy || undefined, "aria-invalid": Boolean(error), checked: checked, className: "sw-checkbox-input", disabled: disabled, onChange: (event) => {
14
+ if (!disabled)
15
+ onCheckedChange(event.currentTarget.checked);
16
+ }, required: required, type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-checkbox-mark" })] }), _jsx("span", { className: "sw-checkbox-text", children: label })] }), description ? (_jsx("span", { className: "sw-checkbox-description", id: descriptionId, children: description })) : null, error ? (_jsx("span", { className: "sw-checkbox-error", id: errorId, children: error })) : null] }));
17
+ }
@@ -0,0 +1,12 @@
1
+ export type DateFieldProps = {
2
+ label: string;
3
+ value: string;
4
+ onChange: (value: string) => void;
5
+ min?: string;
6
+ max?: string;
7
+ disabled?: boolean;
8
+ required?: boolean;
9
+ description?: string;
10
+ error?: string;
11
+ };
12
+ export declare function DateField({ label, value, onChange, min, max, disabled, required, description, error, }: DateFieldProps): import("react").JSX.Element;
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId } from 'react';
4
+ import { assertDateOnly, isDateOnly, isOutsideDateRange, } from '../date-only.js';
5
+ export function DateField({ label, value, onChange, min, max, disabled = false, required = false, description, error, }) {
6
+ assertDateOnly('value', value, true);
7
+ if (min !== undefined)
8
+ assertDateOnly('min', min);
9
+ if (max !== undefined)
10
+ assertDateOnly('max', max);
11
+ if (min !== undefined && max !== undefined && min > max)
12
+ throw new RangeError('min must not be after max');
13
+ const inputId = useId();
14
+ const descriptionId = useId();
15
+ const errorId = useId();
16
+ const describedBy = [
17
+ description ? descriptionId : null,
18
+ error ? errorId : null,
19
+ ]
20
+ .filter((id) => id !== null)
21
+ .join(' ');
22
+ const invalid = Boolean(error) || isOutsideDateRange(value, min, max);
23
+ return (_jsxs("div", { className: "sw-date-field", children: [_jsx("label", { className: "sw-date-field-label", htmlFor: inputId, children: label }), _jsx("input", { "aria-describedby": describedBy || undefined, "aria-invalid": invalid, className: "sw-date-field-input", disabled: disabled, id: inputId, max: max, min: min, onChange: (event) => {
24
+ const next = event.currentTarget.value;
25
+ if (!disabled &&
26
+ !event.currentTarget.validity.badInput &&
27
+ (next === '' || isDateOnly(next)))
28
+ onChange(next);
29
+ }, required: required, type: "date", value: value }), description ? (_jsx("span", { className: "sw-date-field-description", id: descriptionId, children: description })) : null, error ? (_jsx("span", { className: "sw-date-field-error", id: errorId, children: error })) : null] }));
30
+ }
@@ -7,5 +7,8 @@ export type FieldProps = {
7
7
  label: string;
8
8
  labelVisuallyHidden?: boolean;
9
9
  size?: FieldSize;
10
+ description?: string;
11
+ error?: string;
12
+ required?: boolean;
10
13
  };
11
- export declare function Field({ children, gap, label, labelVisuallyHidden, size, }: FieldProps): import("react").JSX.Element;
14
+ export declare function Field({ children, gap, label, labelVisuallyHidden, size, description, error, required, }: FieldProps): import("react").JSX.Element;
@@ -1,6 +1,39 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ 'use client';
2
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
+ import { cloneElement, isValidElement, useId } from 'react';
2
4
  import { Stack } from './Stack.js';
3
5
  import { Text } from './Text.js';
4
- export function Field({ children, gap = 1, label, labelVisuallyHidden = false, size = 'md', }) {
5
- return (_jsxs(Stack, { as: "label", className: size === 'xs' ? 'sw-field-xs' : undefined, gap: labelVisuallyHidden ? 0 : gap, children: [_jsx(Text, { as: "span", className: labelVisuallyHidden ? 'sw-sr-only' : undefined, variant: size === 'xs' ? 'caption' : 'label', children: label }), children] }));
6
+ export function Field({ children, gap = 1, label, labelVisuallyHidden = false, size = 'md', description, error, required = false, }) {
7
+ const controlId = useId();
8
+ const messageId = useId();
9
+ const message = error || description;
10
+ const needsControl = Boolean(message || required);
11
+ const className = [
12
+ 'sw-field',
13
+ size === 'xs' && 'sw-field-xs',
14
+ error && 'sw-field-invalid',
15
+ ]
16
+ .filter(Boolean)
17
+ .join(' ');
18
+ const labelText = (_jsxs(Text, { as: "span", className: labelVisuallyHidden ? 'sw-sr-only' : undefined, variant: size === 'xs' ? 'caption' : 'label', children: [label, required ? (_jsxs("span", { "aria-hidden": "true", className: "sw-field-required", children: [' ', "*"] })) : null] }));
19
+ if (!isValidElement(children) ||
20
+ typeof children.type !== 'string' ||
21
+ !['input', 'select', 'textarea'].includes(children.type)) {
22
+ if (needsControl)
23
+ throw new TypeError('Field validation requires one native input, select, or textarea child');
24
+ return (_jsxs(Stack, { as: "label", className: className, gap: labelVisuallyHidden ? 0 : gap, children: [labelText, children] }));
25
+ }
26
+ const describedBy = [
27
+ children.props['aria-describedby'],
28
+ message ? messageId : null,
29
+ ]
30
+ .filter(Boolean)
31
+ .join(' ');
32
+ const control = cloneElement(children, {
33
+ id: children.props.id || controlId,
34
+ 'aria-describedby': describedBy || undefined,
35
+ 'aria-invalid': error ? true : children.props['aria-invalid'],
36
+ required: required || children.props.required,
37
+ });
38
+ return (_jsxs(Stack, { as: "div", className: className, gap: labelVisuallyHidden ? 0 : gap, children: [_jsx("label", { htmlFor: children.props.id || controlId, children: labelText }), control, message ? (_jsx("span", { className: error ? 'sw-field-error' : 'sw-field-description', id: messageId, role: error ? 'alert' : undefined, children: message })) : null] }));
6
39
  }
@@ -0,0 +1,12 @@
1
+ export type FilterChipOption = {
2
+ value: string;
3
+ label: string;
4
+ disabled?: boolean;
5
+ };
6
+ export type FilterChipsProps = {
7
+ label: string;
8
+ value: string;
9
+ onChange: (value: string) => void;
10
+ options: readonly FilterChipOption[];
11
+ };
12
+ export declare function FilterChips({ label, value, onChange, options, }: FilterChipsProps): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId } from 'react';
4
+ export function FilterChips({ label, value, onChange, options, }) {
5
+ const name = useId();
6
+ const values = options.map((option) => option.value);
7
+ if (!label.trim())
8
+ throw new RangeError('label must not be empty');
9
+ if (options.length === 0 ||
10
+ options.some((option) => !option.value.trim() || !option.label.trim()))
11
+ throw new RangeError('options must have nonempty values and labels');
12
+ if (new Set(values).size !== values.length)
13
+ throw new RangeError('options must have unique values');
14
+ if (!values.includes(value))
15
+ 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) => {
17
+ if (event.currentTarget.checked && !option.disabled)
18
+ onChange(option.value);
19
+ }, type: "radio", value: option.value }), _jsx("span", { className: "sw-filter-chip-face", children: option.label })] }, option.value))) })] }));
20
+ }
@@ -0,0 +1,8 @@
1
+ export type ProgressTone = 'accent' | 'success' | 'danger';
2
+ export type ProgressProps = {
3
+ label: string;
4
+ value: number;
5
+ max: number;
6
+ tone?: ProgressTone;
7
+ };
8
+ export declare function Progress({ label, value, max, tone, }: ProgressProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId } from 'react';
4
+ export function Progress({ label, value, max, tone = 'accent', }) {
5
+ if (!label.trim())
6
+ throw new RangeError('label must not be empty');
7
+ if (!Number.isFinite(max) || max <= 0)
8
+ throw new RangeError('max must be a positive finite number');
9
+ if (!Number.isFinite(value) || value < 0 || value > max)
10
+ throw new RangeError('value must be finite and within 0..max');
11
+ const labelId = useId();
12
+ return (_jsxs("div", { className: `sw-progress sw-progress-${tone}`, children: [_jsxs("div", { className: "sw-progress-heading", children: [_jsx("span", { className: "sw-progress-label", id: labelId, children: label }), _jsxs("span", { "aria-hidden": "true", className: "sw-progress-count", children: [value, " / ", max] })] }), _jsx("progress", { "aria-labelledby": labelId, className: "sw-progress-bar", max: max, value: value })] }));
13
+ }
@@ -0,0 +1,16 @@
1
+ export type RadioGroupOption = {
2
+ value: string;
3
+ label: string;
4
+ disabled?: boolean;
5
+ };
6
+ export type RadioGroupProps = {
7
+ legend: string;
8
+ value: string;
9
+ onChange: (value: string) => void;
10
+ options: readonly RadioGroupOption[];
11
+ required?: boolean;
12
+ disabled?: boolean;
13
+ description?: string;
14
+ error?: string;
15
+ };
16
+ export declare function RadioGroup({ legend, value, onChange, options, required, disabled, description, error, }: RadioGroupProps): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId } from 'react';
4
+ export function RadioGroup({ legend, value, onChange, options, required = false, disabled = false, description, error, }) {
5
+ const name = useId();
6
+ const descriptionId = useId();
7
+ const errorId = useId();
8
+ const describedBy = [
9
+ description ? descriptionId : null,
10
+ error ? errorId : null,
11
+ ]
12
+ .filter((id) => id !== null)
13
+ .join(' ');
14
+ const values = options.map((option) => option.value);
15
+ if (options.length === 0 || values.some((option) => option === ''))
16
+ throw new RangeError('options must have nonempty values');
17
+ if (new Set(values).size !== values.length)
18
+ throw new RangeError('options must have unique values');
19
+ if (value !== '' && !values.includes(value))
20
+ throw new RangeError('value must match an option or be empty');
21
+ return (_jsxs("fieldset", { "aria-describedby": describedBy || undefined, "aria-invalid": Boolean(error), className: "sw-radio-group", disabled: disabled, children: [_jsx("legend", { className: "sw-radio-group-legend", children: legend }), description ? (_jsx("span", { className: "sw-radio-group-description", id: descriptionId, children: description })) : null, _jsx("span", { className: "sw-radio-group-options", children: options.map((option) => (_jsxs("label", { className: "sw-radio-group-option", children: [_jsxs("span", { className: "sw-radio-group-control", children: [_jsx("input", { checked: value === option.value, className: "sw-radio-group-input", disabled: option.disabled, name: name, onChange: (event) => {
22
+ if (!disabled &&
23
+ !option.disabled &&
24
+ event.currentTarget.checked)
25
+ onChange(option.value);
26
+ }, required: required, type: "radio", value: option.value }), _jsx("span", { "aria-hidden": "true", className: "sw-radio-group-mark" })] }), _jsx("span", { className: "sw-radio-group-text", children: option.label })] }, option.value))) }), error ? (_jsx("span", { className: "sw-radio-group-error", id: errorId, children: error })) : null] }));
27
+ }
@@ -0,0 +1,6 @@
1
+ export type SeparatorOrientation = 'horizontal' | 'vertical';
2
+ export type SeparatorProps = {
3
+ orientation?: SeparatorOrientation;
4
+ decorative?: boolean;
5
+ };
6
+ export declare function Separator({ orientation, decorative, }: SeparatorProps): import("react").JSX.Element;
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function Separator({ orientation = 'horizontal', decorative = false, }) {
3
+ return (_jsx("div", { "aria-hidden": decorative ? 'true' : undefined, "aria-orientation": decorative ? undefined : orientation, className: `sw-separator sw-separator-${orientation}`, role: decorative ? 'none' : 'separator' }));
4
+ }
@@ -0,0 +1,11 @@
1
+ export type SpinnerSize = 'sm' | 'md' | 'lg';
2
+ export type SpinnerProps = {
3
+ label: string;
4
+ size?: SpinnerSize;
5
+ decorative?: false;
6
+ } | {
7
+ label?: never;
8
+ size?: SpinnerSize;
9
+ decorative: true;
10
+ };
11
+ export declare function Spinner({ label, size, decorative, }: SpinnerProps): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ export function Spinner({ label, size = 'md', decorative = false, }) {
4
+ const className = `sw-spinner sw-spinner-${size}`;
5
+ if (decorative)
6
+ return (_jsx("span", { "aria-hidden": "true", className: className, children: _jsx("span", { className: "sw-spinner-icon" }) }));
7
+ if (!label?.trim())
8
+ throw new RangeError('label must not be empty');
9
+ return (_jsxs("span", { className: className, role: "status", children: [_jsx("span", { "aria-hidden": "true", className: "sw-spinner-icon" }), _jsx("span", { className: "sw-sr-only", children: label })] }));
10
+ }
@@ -0,0 +1,8 @@
1
+ export type SwitchProps = {
2
+ label: string;
3
+ checked: boolean;
4
+ onCheckedChange: (checked: boolean) => void;
5
+ disabled?: boolean;
6
+ description?: string;
7
+ };
8
+ export declare function Switch({ label, checked, onCheckedChange, disabled, description, }: SwitchProps): import("react").JSX.Element;
@@ -0,0 +1,11 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId } from 'react';
4
+ export function Switch({ label, checked, onCheckedChange, disabled = false, description, }) {
5
+ const labelId = useId();
6
+ const descriptionId = useId();
7
+ return (_jsxs("label", { className: "sw-switch", children: [_jsxs("span", { className: "sw-switch-control", children: [_jsx("input", { "aria-checked": checked, "aria-describedby": description ? descriptionId : undefined, "aria-labelledby": labelId, checked: checked, className: "sw-switch-input", disabled: disabled, onChange: (event) => {
8
+ if (!disabled)
9
+ onCheckedChange(event.currentTarget.checked);
10
+ }, role: "switch", type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-switch-track", children: _jsx("span", { className: "sw-switch-thumb" }) })] }), _jsxs("span", { className: "sw-switch-copy", children: [_jsx("span", { className: "sw-switch-label", id: labelId, children: label }), description ? (_jsx("span", { className: "sw-switch-description", id: descriptionId, children: description })) : null] })] }));
11
+ }
@@ -0,0 +1,8 @@
1
+ import { type ReactElement } from 'react';
2
+ export type TooltipProps = {
3
+ content: string;
4
+ trigger: ReactElement<{
5
+ 'aria-describedby'?: string;
6
+ }>;
7
+ };
8
+ export declare function Tooltip({ content, trigger }: TooltipProps): import("react").JSX.Element;