@scalewing/react 0.2.0 → 0.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +13 -2
  3. package/dist/components/Accordion.d.ts +8 -0
  4. package/dist/components/Accordion.js +14 -0
  5. package/dist/components/AppHeader.d.ts +5 -0
  6. package/dist/components/AppHeader.js +7 -0
  7. package/dist/components/Badge.d.ts +13 -0
  8. package/dist/components/Badge.js +7 -0
  9. package/dist/components/BarChart.d.ts +16 -0
  10. package/dist/components/BarChart.js +13 -0
  11. package/dist/components/Box.d.ts +1 -1
  12. package/dist/components/Button.d.ts +1 -1
  13. package/dist/components/Dialog.d.ts +8 -0
  14. package/dist/components/Dialog.js +36 -0
  15. package/dist/components/Field.d.ts +4 -1
  16. package/dist/components/Field.js +2 -2
  17. package/dist/components/Nav.d.ts +3 -0
  18. package/dist/components/Nav.js +7 -0
  19. package/dist/components/SegmentedControl.d.ts +19 -0
  20. package/dist/components/SegmentedControl.js +28 -0
  21. package/dist/components/Select.d.ts +17 -0
  22. package/dist/components/Select.js +107 -0
  23. package/dist/components/Split.d.ts +8 -0
  24. package/dist/components/Split.js +117 -0
  25. package/dist/components/Table.d.ts +32 -0
  26. package/dist/components/Table.js +24 -0
  27. package/dist/components/Text.d.ts +3 -3
  28. package/dist/components/Text.js +2 -1
  29. package/dist/components/Toast.d.ts +10 -0
  30. package/dist/components/Toast.js +61 -0
  31. package/dist/index.d.ts +17 -1
  32. package/dist/index.js +11 -0
  33. package/dist/palette/amber.css +21 -0
  34. package/dist/palette/capri.css +21 -0
  35. package/dist/palette/cerulean.css +21 -0
  36. package/dist/palette/cornflower.css +21 -0
  37. package/dist/palette/fuchsia.css +21 -0
  38. package/dist/palette/harvest.css +31 -0
  39. package/dist/palette/ink.css +21 -0
  40. package/dist/palette/mocha.css +31 -0
  41. package/dist/palette/navy.css +21 -0
  42. package/dist/palette/postcard.css +31 -0
  43. package/dist/palette/raspberry.css +21 -0
  44. package/dist/palette/sky.css +21 -0
  45. package/dist/palette/soft-blue.css +21 -0
  46. package/dist/palette/sunburst.css +31 -0
  47. package/dist/palette/synthwave.css +26 -0
  48. package/dist/palette/tangerine.css +21 -0
  49. package/dist/select-list.d.ts +7 -0
  50. package/dist/select-list.js +24 -0
  51. package/dist/split-size.d.ts +18 -0
  52. package/dist/split-size.js +94 -0
  53. package/dist/styles.css +1084 -12
  54. package/dist/theme/ThemeProvider.d.ts +4 -3
  55. package/dist/theme/ThemeProvider.js +8 -3
  56. package/dist/toast-travel.d.ts +16 -0
  57. package/dist/toast-travel.js +42 -0
  58. package/package.json +7 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 247f158: Adds Accordion, a web disclosure on native details/summary, so in-flow panels can expand without a modal or an always-open Card.
8
+ - 247f158: Adds web page chrome: `AppHeader` (sticky glass), `Nav` (label-size link cluster, current page via `aria-current`), compact `Field` (`size="xs"`, `labelVisuallyHidden`), native `select` chevrons, and accent `:focus-visible` on text controls so selects do not keep the user-agent blue ring.
9
+ - 247f158: Adds Dialog, a web modal on the native `<dialog>` top layer, so info surfaces can leave nested page flow.
10
+ - 247f158: Adds a horizontal BarChart for labeled factor contributions, compact table density (`density="compact"`) with a selected-row marker, and compact Badge chicklets.
11
+ - 247f158: Default accent is electric indigo, and glass fill is derived from the merged accent so ThemeProvider overlays retint frost.
12
+
13
+ Adds opt-in density (`xs` controls, `data` type, `sw-tabular`) and web `Badge`, `SegmentedControl`, and `Table`. Pin the previous teal with `colors={{ accent: '#0B615E', onAccent: '#FFFFFF' }}` (dark: `#7EDAD6` / `#101214`).
14
+
15
+ - 247f158: Adds a named palette catalog so products pick a reviewed light/dark overlay instead of copying hex. Apply with ThemeProvider `palette`, `data-palette` on the generated canvas, or `import '@scalewing/react/palette/<id>.css'` after styles.css. Default remains indigo. `colors` still wins over a named palette and may be `{ light, dark }` so end users can switch scheme without the product swapping hex.
16
+ - 247f158: Adds Select, a web labeled listbox menu, so a compact choice can open with canvas glass instead of the operating system picker.
17
+ - 247f158: Adds Split, a web start pane with a labeled separator, so a column can be resized up to a max width and hidden when dragged too small.
18
+ - 247f158: Adds Toast, a web auto-dismiss confirmation on the popover layer that does not trap focus. Optional `anchor` and `target` send it from a press to a destination, then it vanishes. Ghost buttons with `aria-pressed="false"` use quiet opacity so a selected press stays full color.
19
+
20
+ ### Patch Changes
21
+
22
+ - 247f158: Adds Select action, a last listbox command that does not become the displayed value.
23
+ - Updated dependencies [247f158]
24
+ - Updated dependencies [247f158]
25
+ - Updated dependencies [247f158]
26
+ - Updated dependencies [247f158]
27
+ - Updated dependencies [247f158]
28
+ - Updated dependencies [247f158]
29
+ - Updated dependencies [247f158]
30
+ - Updated dependencies [247f158]
31
+ - Updated dependencies [247f158]
32
+ - Updated dependencies [247f158]
33
+ - Updated dependencies [247f158]
34
+ - Updated dependencies [247f158]
35
+ - @scalewing/tokens@0.3.0
36
+
3
37
  ## 0.2.0
4
38
 
5
39
  ### Minor Changes
package/README.md CHANGED
@@ -4,23 +4,34 @@ DOM components plus the generated Scalewing stylesheet.
4
4
 
5
5
  ```ts
6
6
  import '@scalewing/react/styles.css';
7
+ import '@scalewing/react/palette/cerulean.css';
7
8
  import {
9
+ Accordion,
10
+ BarChart,
8
11
  Box,
9
12
  Button,
10
13
  Card,
11
14
  Field,
15
+ Select,
16
+ Split,
12
17
  Stack,
13
18
  Text,
14
19
  ThemeProvider,
15
20
  } from '@scalewing/react';
21
+
22
+ <ThemeProvider colorScheme="system" palette="cerulean">
23
+ {children}
24
+ </ThemeProvider>
16
25
  ```
17
26
 
27
+ `colorScheme` is `"light"`, `"dark"`, or `"system"` so product users can switch. Each named palette already has a light pair and a dark pair. Brand overlays may be a flat `colors` map or `{ light, dark }` so both schemes are declared once.
28
+
18
29
  ```html
19
30
  <div class="sw-padding-top-4 sw-padding-x-4"></div>
20
31
  ```
21
32
 
22
- Import the CSS once at the application entry. Do not copy it into your source tree.
33
+ 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.
23
34
 
24
- `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.
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.
25
36
 
26
37
  React is a peer dependency. Rationale: the host app already owns the React runtime.
@@ -0,0 +1,8 @@
1
+ import { type DetailsHTMLAttributes, type ReactNode } from 'react';
2
+ export type AccordionProps = Omit<DetailsHTMLAttributes<HTMLDetailsElement>, 'onToggle' | 'open' | 'title' | 'children'> & {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ title: string;
6
+ children: ReactNode;
7
+ };
8
+ export declare function Accordion({ children, className, onOpenChange, open, title, ...rest }: AccordionProps): import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cx } from '../class-names.js';
4
+ import { spacingClassNames } from '../spacing-classes.js';
5
+ import { Stack } from './Stack.js';
6
+ import { Text } from './Text.js';
7
+ export function Accordion({ children, className, onOpenChange, open, title, ...rest }) {
8
+ return (_jsxs("details", { ...rest, className: cx('sw-accordion', className), open: open, onToggle: (event) => {
9
+ const next = event.currentTarget.open;
10
+ if (next !== open) {
11
+ onOpenChange(next);
12
+ }
13
+ }, children: [_jsx("summary", { className: cx('sw-accordion-summary', ...spacingClassNames({ padding: 4 })), children: _jsx(Text, { as: "span", variant: "title", children: title }) }), _jsx(Stack, { gap: 4, padding: 4, paddingTop: 0, children: children })] }));
14
+ }
@@ -0,0 +1,5 @@
1
+ import { type BoxProps } from './Box.js';
2
+ export type AppHeaderProps = Omit<BoxProps, 'as'> & {
3
+ sticky?: boolean;
4
+ };
5
+ export declare function AppHeader({ className, padding, sticky, ...rest }: AppHeaderProps): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { cx } from '../class-names.js';
4
+ import { Box } from './Box.js';
5
+ export function AppHeader({ className, padding = 4, sticky = true, ...rest }) {
6
+ return (_jsx(Box, { as: "header", className: cx('sw-app-header', sticky && 'sw-app-header-sticky', className), padding: padding, ...rest }));
7
+ }
@@ -0,0 +1,13 @@
1
+ import { type BadgeSize, type BadgeTone } from '@scalewing/tokens';
2
+ import { type HTMLAttributes, type ReactNode } from 'react';
3
+ export type { BadgeSize, BadgeTone };
4
+ export type BadgeProps = HTMLAttributes<HTMLSpanElement> & {
5
+ children: ReactNode;
6
+ size?: BadgeSize;
7
+ tone?: BadgeTone;
8
+ };
9
+ export declare const Badge: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLSpanElement> & {
10
+ children: ReactNode;
11
+ size?: BadgeSize;
12
+ tone?: BadgeTone;
13
+ } & import("react").RefAttributes<HTMLSpanElement>>;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { badgeClassNames, } from '@scalewing/tokens';
3
+ import { forwardRef } from 'react';
4
+ import { cx } from '../class-names.js';
5
+ export const Badge = forwardRef(function Badge({ children, className, size = 'md', tone = 'neutral', ...rest }, ref) {
6
+ return (_jsx("span", { ref: ref, className: cx(...badgeClassNames(tone, size), className), ...rest, children: children }));
7
+ });
@@ -0,0 +1,16 @@
1
+ import { type HTMLAttributes } from 'react';
2
+ export type BarChartItem = {
3
+ label: string;
4
+ value: number;
5
+ valueLabel?: string;
6
+ };
7
+ export type BarChartProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
8
+ 'aria-label': string;
9
+ items: readonly BarChartItem[];
10
+ max?: number;
11
+ };
12
+ export declare const BarChart: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
13
+ 'aria-label': string;
14
+ items: readonly BarChartItem[];
15
+ max?: number;
16
+ } & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { barChartFillRatio, barChartScaleMax, formatBarChartValue, } from '@scalewing/tokens';
3
+ import { forwardRef } from 'react';
4
+ import { cx } from '../class-names.js';
5
+ export const BarChart = forwardRef(function BarChart({ 'aria-label': ariaLabel, className, items, max, style, ...rest }, ref) {
6
+ const scaleMax = barChartScaleMax(items.map((item) => item.value), max);
7
+ return (_jsxs("div", { ref: ref, className: cx('sw-bar-chart', className), style: style, ...rest, children: [_jsx("ul", { "aria-label": ariaLabel, className: "sw-bar-chart-plot", children: items.map((item, index) => {
8
+ const ratio = barChartFillRatio(item.value, scaleMax);
9
+ return (_jsxs("li", { className: "sw-bar-chart-row", children: [_jsx("span", { className: "sw-bar-chart-label", children: item.label }), _jsx("span", { className: "sw-bar-chart-track", children: _jsx("span", { className: cx('sw-bar-chart-fill', item.value < 0 && 'sw-bar-chart-fill-negative'), style: {
10
+ '--sw-bar-fill': ratio,
11
+ } }) }), _jsx("span", { className: "sw-bar-chart-value", children: item.valueLabel ?? formatBarChartValue(item.value) })] }, `${item.label}-${index}`));
12
+ }) }), _jsxs("div", { "aria-hidden": "true", className: "sw-bar-chart-axis", children: [_jsx("span", {}), _jsxs("span", { className: "sw-bar-chart-axis-track", children: [_jsx("span", { children: "0" }), _jsx("span", { children: formatBarChartValue(scaleMax) })] }), _jsx("span", {})] })] }));
13
+ });
@@ -8,7 +8,7 @@ export type BoxProps = SpacingProps & HTMLAttributes<HTMLElement> & Pick<AnchorH
8
8
  radius?: RadiusStep;
9
9
  border?: boolean;
10
10
  };
11
- export declare const Box: import("react").ForwardRefExoticComponent<SpacingProps & HTMLAttributes<HTMLElement> & Pick<AnchorHTMLAttributes<HTMLAnchorElement>, "rel" | "download" | "href" | "target"> & {
11
+ export declare const Box: import("react").ForwardRefExoticComponent<SpacingProps & HTMLAttributes<HTMLElement> & Pick<AnchorHTMLAttributes<HTMLAnchorElement>, "download" | "href" | "rel" | "target"> & {
12
12
  as?: BoxElement;
13
13
  background?: Extract<SemanticColorKey, "background" | "surface">;
14
14
  radius?: RadiusStep;
@@ -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>, "onClick" | "type"> & {
11
+ export declare const Button: import("react").ForwardRefExoticComponent<Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type" | "onClick"> & {
12
12
  children: ReactNode;
13
13
  onPress: () => void;
14
14
  size?: ButtonSize;
@@ -0,0 +1,8 @@
1
+ import { type DialogHTMLAttributes, type ReactNode } from 'react';
2
+ export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose' | 'open' | 'title' | 'children'> & {
3
+ open: boolean;
4
+ onClose: () => void;
5
+ title: string;
6
+ children: ReactNode;
7
+ };
8
+ export declare function Dialog({ children, className, onClose, open, title, ...rest }: DialogProps): import("react").JSX.Element;
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, } from 'react';
4
+ import { cx } from '../class-names.js';
5
+ import { spacingClassNames } from '../spacing-classes.js';
6
+ import { Stack } from './Stack.js';
7
+ import { Text } from './Text.js';
8
+ export function Dialog({ children, className, onClose, open, title, ...rest }) {
9
+ const nodeRef = useRef(null);
10
+ const titleId = useId();
11
+ useEffect(() => {
12
+ const node = nodeRef.current;
13
+ if (!node) {
14
+ return;
15
+ }
16
+ if (open) {
17
+ if (!node.open) {
18
+ node.showModal();
19
+ }
20
+ return;
21
+ }
22
+ if (node.open) {
23
+ node.close();
24
+ }
25
+ }, [open]);
26
+ return (_jsx("dialog", { ...rest, ref: nodeRef, "aria-labelledby": titleId, className: cx('sw-dialog', ...spacingClassNames({ padding: 5 }), className), onClose: onClose, onPointerDown: (event) => {
27
+ const rect = event.currentTarget.getBoundingClientRect();
28
+ const inside = event.clientX >= rect.left &&
29
+ event.clientX <= rect.right &&
30
+ event.clientY >= rect.top &&
31
+ event.clientY <= rect.bottom;
32
+ if (!inside) {
33
+ onClose();
34
+ }
35
+ }, children: _jsxs(Stack, { gap: 4, children: [_jsx(Text, { id: titleId, variant: "title", children: title }), children] }) }));
36
+ }
@@ -1,8 +1,11 @@
1
1
  import { type SpacingStep } from '@scalewing/tokens';
2
2
  import { type ReactNode } from 'react';
3
+ export type FieldSize = 'xs' | 'md';
3
4
  export type FieldProps = {
4
5
  children: ReactNode;
5
6
  gap?: SpacingStep;
6
7
  label: string;
8
+ labelVisuallyHidden?: boolean;
9
+ size?: FieldSize;
7
10
  };
8
- export declare function Field({ children, gap, label }: FieldProps): import("react").JSX.Element;
11
+ export declare function Field({ children, gap, label, labelVisuallyHidden, size, }: FieldProps): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Stack } from './Stack.js';
3
3
  import { Text } from './Text.js';
4
- export function Field({ children, gap = 1, label }) {
5
- return (_jsxs(Stack, { as: "label", gap: gap, children: [_jsx(Text, { as: "span", variant: "label", children: label }), children] }));
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
6
  }
@@ -0,0 +1,3 @@
1
+ import { type BoxProps } from './Box.js';
2
+ export type NavProps = Omit<BoxProps, 'as'>;
3
+ export declare function Nav({ className, ...rest }: NavProps): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { cx } from '../class-names.js';
4
+ import { Box } from './Box.js';
5
+ export function Nav({ className, ...rest }) {
6
+ return _jsx(Box, { as: "nav", className: cx('sw-nav', className), ...rest });
7
+ }
@@ -0,0 +1,19 @@
1
+ import { type ReactNode } from 'react';
2
+ export type SegmentedItem = {
3
+ id: string;
4
+ label: ReactNode;
5
+ };
6
+ type SegmentedLabel = {
7
+ 'aria-label': string;
8
+ 'aria-labelledby'?: never;
9
+ } | {
10
+ 'aria-label'?: never;
11
+ 'aria-labelledby': string;
12
+ };
13
+ export type SegmentedControlProps = SegmentedLabel & {
14
+ items: readonly SegmentedItem[];
15
+ onChange: (id: string) => void;
16
+ value: string;
17
+ };
18
+ export declare const SegmentedControl: import("react").ForwardRefExoticComponent<SegmentedControlProps & import("react").RefAttributes<HTMLDivElement>>;
19
+ export {};
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import { cx } from '../class-names.js';
4
+ export const SegmentedControl = forwardRef(function SegmentedControl({ items, onChange, value, ...labelProps }, ref) {
5
+ function onKeyDown(event) {
6
+ if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') {
7
+ return;
8
+ }
9
+ const index = items.findIndex((item) => item.id === value);
10
+ if (index < 0) {
11
+ return;
12
+ }
13
+ event.preventDefault();
14
+ const delta = event.key === 'ArrowRight' ? 1 : -1;
15
+ const next = items[(index + delta + items.length) % items.length];
16
+ if (next) {
17
+ onChange(next.id);
18
+ }
19
+ }
20
+ return (_jsx("div", { ref: ref, className: "sw-segmented", onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
21
+ const selected = item.id === value;
22
+ return (_jsx("button", { "aria-checked": selected, className: cx('sw-segmented-item', selected && 'sw-segmented-item-selected'), onClick: () => {
23
+ if (!selected) {
24
+ onChange(item.id);
25
+ }
26
+ }, role: "radio", type: "button", children: item.label }, item.id));
27
+ }) }));
28
+ });
@@ -0,0 +1,17 @@
1
+ import { type SelectOption } from '../select-list.js';
2
+ import { type FieldSize } from './Field.js';
3
+ export type { SelectOption };
4
+ export type SelectAction = {
5
+ label: string;
6
+ onPress: () => void;
7
+ };
8
+ export type SelectProps = {
9
+ action?: SelectAction;
10
+ label: string;
11
+ labelVisuallyHidden?: boolean;
12
+ onChange: (value: string) => void;
13
+ options: readonly SelectOption[];
14
+ size?: FieldSize;
15
+ value: string;
16
+ };
17
+ export declare function Select({ action, label, labelVisuallyHidden, onChange, options, size, value, }: SelectProps): import("react").JSX.Element;
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState, } from 'react';
4
+ import { cx } from '../class-names.js';
5
+ import { selectIndexForKey, selectedSelectIndex, } from '../select-list.js';
6
+ import { Stack } from './Stack.js';
7
+ import { Text } from './Text.js';
8
+ function SelectListOption({ action = false, active, children, id, onCommit, onHighlight, selected, }) {
9
+ return (_jsx("div", { "aria-selected": selected, className: cx('sw-select-option', action && 'sw-select-action'), "data-active": active ? 'true' : undefined, id: id, onClick: onCommit, onPointerDown: (event) => {
10
+ event.preventDefault();
11
+ }, onPointerMove: onHighlight, role: "option", children: children }));
12
+ }
13
+ export function Select({ action, label, labelVisuallyHidden = false, onChange, options, size = 'md', value, }) {
14
+ const labelId = useId();
15
+ const triggerId = useId();
16
+ const listId = useId();
17
+ const optionIdPrefix = useId();
18
+ const rootRef = useRef(null);
19
+ const [open, setOpen] = useState(false);
20
+ const [highlight, setHighlight] = useState(0);
21
+ const selectedIndex = selectedSelectIndex(options, value);
22
+ const selected = options[selectedIndex];
23
+ const actionIndex = options.length;
24
+ const itemCount = action ? actionIndex + 1 : actionIndex;
25
+ useEffect(() => {
26
+ if (!open) {
27
+ return;
28
+ }
29
+ function onPointerDown(event) {
30
+ if (!rootRef.current?.contains(event.target)) {
31
+ setOpen(false);
32
+ }
33
+ }
34
+ document.addEventListener('pointerdown', onPointerDown);
35
+ return () => {
36
+ document.removeEventListener('pointerdown', onPointerDown);
37
+ };
38
+ }, [open]);
39
+ function openList() {
40
+ if (itemCount === 0) {
41
+ return;
42
+ }
43
+ setHighlight(selectedIndex);
44
+ setOpen(true);
45
+ }
46
+ function commit(index) {
47
+ if (action && index === actionIndex) {
48
+ action.onPress();
49
+ setOpen(false);
50
+ return;
51
+ }
52
+ const option = options[index];
53
+ if (option && option.value !== value) {
54
+ onChange(option.value);
55
+ }
56
+ setOpen(false);
57
+ }
58
+ function onTriggerKeyDown(event) {
59
+ if (event.key === 'Escape' && open) {
60
+ event.preventDefault();
61
+ event.stopPropagation();
62
+ setOpen(false);
63
+ return;
64
+ }
65
+ if (!open) {
66
+ if (event.key === 'ArrowDown' ||
67
+ event.key === 'ArrowUp' ||
68
+ event.key === 'Enter' ||
69
+ event.key === ' ') {
70
+ event.preventDefault();
71
+ openList();
72
+ }
73
+ return;
74
+ }
75
+ if (event.key === 'Enter' || event.key === ' ') {
76
+ event.preventDefault();
77
+ commit(highlight);
78
+ return;
79
+ }
80
+ const next = selectIndexForKey(event.key, highlight, itemCount);
81
+ if (next !== null) {
82
+ event.preventDefault();
83
+ setHighlight(next);
84
+ }
85
+ }
86
+ return (_jsx("div", { ref: rootRef, className: cx('sw-select', size === 'xs' && 'sw-select-xs'), children: _jsxs(Stack, { gap: labelVisuallyHidden ? 0 : 1, children: [_jsx(Text, { as: "label", className: labelVisuallyHidden ? 'sw-sr-only' : undefined, htmlFor: triggerId, id: labelId, variant: size === 'xs' ? 'caption' : 'label', children: label }), _jsxs("div", { className: "sw-select-control", children: [_jsx("button", { "aria-activedescendant": open ? `${optionIdPrefix}-${highlight}` : undefined, "aria-controls": open ? listId : undefined, "aria-expanded": open, "aria-haspopup": "listbox", "aria-labelledby": labelId, className: "sw-select-trigger", id: triggerId, onBlur: (event) => {
87
+ const next = event.relatedTarget;
88
+ if (next instanceof Node && rootRef.current?.contains(next)) {
89
+ return;
90
+ }
91
+ setOpen(false);
92
+ }, onClick: () => {
93
+ if (open) {
94
+ setOpen(false);
95
+ return;
96
+ }
97
+ openList();
98
+ }, onKeyDown: onTriggerKeyDown, role: "combobox", type: "button", children: selected?.label ?? '' }), open ? (_jsxs("div", { "aria-labelledby": labelId, className: "sw-select-list", id: listId, role: "listbox", children: [options.map((option, index) => (_jsx(SelectListOption, { active: index === highlight, id: `${optionIdPrefix}-${index}`, onCommit: () => {
99
+ commit(index);
100
+ }, onHighlight: () => {
101
+ setHighlight(index);
102
+ }, selected: option.value === value, children: option.label }, option.value))), action ? (_jsx(SelectListOption, { action: true, active: highlight === actionIndex, id: `${optionIdPrefix}-${actionIndex}`, onCommit: () => {
103
+ commit(actionIndex);
104
+ }, onHighlight: () => {
105
+ setHighlight(actionIndex);
106
+ }, selected: false, children: action.label })) : null] })) : null] })] }) }));
107
+ }
@@ -0,0 +1,8 @@
1
+ import { type HTMLAttributes, type ReactNode } from 'react';
2
+ export type SplitProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
3
+ children: ReactNode;
4
+ label: string;
5
+ collapsed?: boolean;
6
+ onCollapsedChange?: (collapsed: boolean) => void;
7
+ };
8
+ export declare function Split({ children, className, collapsed: collapsedProp, label, onCollapsedChange, ...rest }: SplitProps): import("react").JSX.Element;
@@ -0,0 +1,117 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { spacingScale } from '@scalewing/tokens';
4
+ import { useCallback, useId, useLayoutEffect, useRef, useState, } from 'react';
5
+ import { cx } from '../class-names.js';
6
+ import { applySplitDrag, applySplitKey, cssLengthToPx, effectiveSplitMax, readSplitMetrics, splitPercent, } from '../split-size.js';
7
+ function metricsFrom(node) {
8
+ const styles = getComputedStyle(node);
9
+ const remPx = cssLengthToPx(getComputedStyle(document.documentElement).fontSize) || 16;
10
+ return readSplitMetrics((name) => styles.getPropertyValue(name), remPx);
11
+ }
12
+ export function Split({ children, className, collapsed: collapsedProp, label, onCollapsedChange, ...rest }) {
13
+ const paneId = useId();
14
+ const splitRef = useRef(null);
15
+ const handleRef = useRef(null);
16
+ const dragRef = useRef(null);
17
+ const [width, setWidth] = useState(null);
18
+ const [range, setRange] = useState(null);
19
+ const [collapsedUncontrolled, setCollapsedUncontrolled] = useState(false);
20
+ const collapsed = collapsedProp ?? collapsedUncontrolled;
21
+ const setCollapsed = useCallback((next) => {
22
+ onCollapsedChange?.(next);
23
+ if (collapsedProp === undefined) {
24
+ setCollapsedUncontrolled(next);
25
+ }
26
+ }, [collapsedProp, onCollapsedChange]);
27
+ useLayoutEffect(() => {
28
+ const node = splitRef.current;
29
+ if (!node) {
30
+ return;
31
+ }
32
+ const measured = metricsFrom(node);
33
+ setRange({ min: measured.min, max: measured.max });
34
+ setWidth((current) => current ?? measured.defaultWidth);
35
+ }, []);
36
+ function dragMetrics() {
37
+ const split = splitRef.current;
38
+ const handle = handleRef.current;
39
+ if (!split) {
40
+ return { min: 0, max: 0, step: 16, defaultWidth: 0 };
41
+ }
42
+ const measured = metricsFrom(split);
43
+ const parentWidth = split.parentElement?.getBoundingClientRect().width ?? 0;
44
+ const handleWidth = handle?.getBoundingClientRect().width ?? 0;
45
+ return {
46
+ ...measured,
47
+ max: effectiveSplitMax(measured.max, parentWidth, handleWidth, measured.min),
48
+ };
49
+ }
50
+ function currentSnapshot() {
51
+ const measured = dragMetrics();
52
+ return {
53
+ collapsed,
54
+ width: width ?? measured.defaultWidth,
55
+ };
56
+ }
57
+ function applySnapshot(next) {
58
+ setWidth(next.width);
59
+ if (next.collapsed !== collapsed) {
60
+ setCollapsed(next.collapsed);
61
+ }
62
+ }
63
+ function onPointerDown(event) {
64
+ if (event.button !== 0 || !splitRef.current) {
65
+ return;
66
+ }
67
+ event.preventDefault();
68
+ event.currentTarget.focus();
69
+ event.currentTarget.setPointerCapture?.(event.pointerId);
70
+ dragRef.current = {
71
+ pointerId: event.pointerId,
72
+ startX: event.clientX,
73
+ snapshot: currentSnapshot(),
74
+ metrics: dragMetrics(),
75
+ moved: false,
76
+ };
77
+ }
78
+ function onPointerMove(event) {
79
+ const drag = dragRef.current;
80
+ if (!drag || event.pointerId !== drag.pointerId) {
81
+ return;
82
+ }
83
+ const delta = event.clientX - drag.startX;
84
+ if (Math.abs(delta) >= spacingScale[1]) {
85
+ drag.moved = true;
86
+ }
87
+ applySnapshot(applySplitDrag(drag.snapshot, delta, drag.metrics));
88
+ }
89
+ function endDrag(event) {
90
+ const drag = dragRef.current;
91
+ if (!drag || event.pointerId !== drag.pointerId) {
92
+ return;
93
+ }
94
+ dragRef.current = null;
95
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
96
+ if (!drag.moved && drag.snapshot.collapsed) {
97
+ setCollapsed(false);
98
+ }
99
+ }
100
+ function onKeyDown(event) {
101
+ if (!splitRef.current) {
102
+ return;
103
+ }
104
+ const next = applySplitKey(event.key, currentSnapshot(), dragMetrics());
105
+ if (!next) {
106
+ return;
107
+ }
108
+ event.preventDefault();
109
+ applySnapshot(next);
110
+ }
111
+ const valueNow = collapsed
112
+ ? 0
113
+ : range && width !== null
114
+ ? splitPercent(width, range.min, range.max)
115
+ : 0;
116
+ return (_jsxs("div", { ...rest, ref: splitRef, className: cx('sw-split', className), "data-collapsed": collapsed ? '' : undefined, children: [_jsx("div", { className: "sw-split-pane", id: paneId, "aria-hidden": collapsed || undefined, style: width === null ? undefined : { width: `${width}px` }, children: children }), _jsx("div", { ref: handleRef, "aria-controls": paneId, "aria-label": collapsed ? `Show ${label}` : `Resize ${label}`, "aria-orientation": "vertical", "aria-valuemax": 100, "aria-valuemin": 0, "aria-valuenow": valueNow, className: "sw-split-handle", onKeyDown: onKeyDown, onPointerCancel: endDrag, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: endDrag, role: "separator", tabIndex: 0, children: _jsx("span", { "aria-hidden": "true", className: "sw-split-grip" }) })] }));
117
+ }
@@ -0,0 +1,32 @@
1
+ import { type HTMLAttributes, type TdHTMLAttributes, type ThHTMLAttributes } from 'react';
2
+ export type TableDensity = 'comfortable' | 'compact';
3
+ export type TableProps = HTMLAttributes<HTMLTableElement> & {
4
+ density?: TableDensity;
5
+ stickyHeader?: boolean;
6
+ };
7
+ export declare const Table: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLTableElement> & {
8
+ density?: TableDensity;
9
+ stickyHeader?: boolean;
10
+ } & import("react").RefAttributes<HTMLTableElement>>;
11
+ export type TableHeaderProps = HTMLAttributes<HTMLTableSectionElement>;
12
+ export declare const TableHeader: import("react").ForwardRefExoticComponent<TableHeaderProps & import("react").RefAttributes<HTMLTableSectionElement>>;
13
+ export type TableBodyProps = HTMLAttributes<HTMLTableSectionElement>;
14
+ export declare const TableBody: import("react").ForwardRefExoticComponent<TableBodyProps & import("react").RefAttributes<HTMLTableSectionElement>>;
15
+ export type TableRowProps = HTMLAttributes<HTMLTableRowElement> & {
16
+ selected?: boolean;
17
+ };
18
+ export declare const TableRow: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLTableRowElement> & {
19
+ selected?: boolean;
20
+ } & import("react").RefAttributes<HTMLTableRowElement>>;
21
+ type TableCellAlign = 'start' | 'end';
22
+ export type TableCellProps = ((TdHTMLAttributes<HTMLTableCellElement> & {
23
+ as?: 'td';
24
+ }) | (ThHTMLAttributes<HTMLTableCellElement> & {
25
+ as: 'th';
26
+ })) & {
27
+ align?: TableCellAlign;
28
+ numeric?: boolean;
29
+ truncate?: boolean;
30
+ };
31
+ export declare const TableCell: import("react").ForwardRefExoticComponent<TableCellProps & import("react").RefAttributes<HTMLTableCellElement>>;
32
+ export {};
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef, } from 'react';
3
+ import { cx } from '../class-names.js';
4
+ export const Table = forwardRef(function Table({ children, className, density = 'comfortable', stickyHeader = true, ...rest }, ref) {
5
+ return (_jsx("div", { className: "sw-table-wrap", children: _jsx("table", { ref: ref, className: cx('sw-table', density === 'compact' && 'sw-table-compact', stickyHeader && 'sw-table-sticky', className), ...rest, children: children }) }));
6
+ });
7
+ export const TableHeader = forwardRef(function TableHeader({ className, ...rest }, ref) {
8
+ return _jsx("thead", { ref: ref, className: className, ...rest });
9
+ });
10
+ export const TableBody = forwardRef(function TableBody({ className, ...rest }, ref) {
11
+ return _jsx("tbody", { ref: ref, className: className, ...rest });
12
+ });
13
+ export const TableRow = forwardRef(function TableRow({ className, selected = false, ...rest }, ref) {
14
+ return (_jsx("tr", { ref: ref, "aria-selected": selected || undefined, className: cx(selected && 'sw-table-row-selected', className), ...rest }));
15
+ });
16
+ export const TableCell = forwardRef(function TableCell({ align = 'start', as = 'td', className, numeric = false, truncate = false, ...rest }, ref) {
17
+ const Component = as;
18
+ const alignmentClass = numeric
19
+ ? 'sw-table-numeric'
20
+ : align === 'end'
21
+ ? 'sw-table-end'
22
+ : undefined;
23
+ return (_jsx(Component, { ref: ref, className: cx(alignmentClass, truncate && 'sw-table-clip', className), ...rest }));
24
+ });