@astryxdesign/lab 0.1.9-canary.fef7dca → 0.2.0-canary.210e195

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 (62) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts +95 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
  3. package/dist/BottomSheet/BottomSheet.js +150 -0
  4. package/dist/BottomSheet/index.d.ts +9 -0
  5. package/dist/BottomSheet/index.d.ts.map +1 -0
  6. package/dist/BottomSheet/index.js +10 -0
  7. package/dist/BottomSheet/useSheetGestures.d.ts +69 -0
  8. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
  9. package/dist/BottomSheet/useSheetGestures.js +163 -0
  10. package/dist/Chart/Chart.d.ts +12 -1
  11. package/dist/Chart/Chart.d.ts.map +1 -1
  12. package/dist/Chart/Chart.js +51 -3
  13. package/dist/CodeEditor/CodeEditor.d.ts +14 -2
  14. package/dist/CodeEditor/CodeEditor.d.ts.map +1 -1
  15. package/dist/CodeEditor/CodeEditor.js +110 -9
  16. package/dist/Radial/RadialChart.d.ts +12 -1
  17. package/dist/Radial/RadialChart.d.ts.map +1 -1
  18. package/dist/Radial/RadialChart.js +89 -4
  19. package/dist/RichTextEditor/RichTextEditor.d.ts +45 -5
  20. package/dist/RichTextEditor/RichTextEditor.d.ts.map +1 -1
  21. package/dist/RichTextEditor/RichTextEditor.js +157 -11
  22. package/dist/RichTextEditor/index.d.ts +1 -1
  23. package/dist/RichTextEditor/index.d.ts.map +1 -1
  24. package/dist/Sankey/SankeyChart.d.ts +13 -1
  25. package/dist/Sankey/SankeyChart.d.ts.map +1 -1
  26. package/dist/Sankey/SankeyChart.js +53 -5
  27. package/dist/ThreeD/ThreeDChart.d.ts +16 -3
  28. package/dist/ThreeD/ThreeDChart.d.ts.map +1 -1
  29. package/dist/ThreeD/ThreeDChart.js +63 -5
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +3 -0
  33. package/dist/lab.css +13 -0
  34. package/package.json +2 -2
  35. package/src/BottomSheet/BottomSheet.doc.mjs +105 -0
  36. package/src/BottomSheet/BottomSheet.test.tsx +233 -0
  37. package/src/BottomSheet/BottomSheet.tsx +226 -0
  38. package/src/BottomSheet/index.ts +11 -0
  39. package/src/BottomSheet/useSheetGestures.doc.mjs +31 -0
  40. package/src/BottomSheet/useSheetGestures.test.ts +160 -0
  41. package/src/BottomSheet/useSheetGestures.ts +226 -0
  42. package/src/Chart/Chart.test.tsx +126 -0
  43. package/src/Chart/Chart.tsx +59 -0
  44. package/src/ChatReasoning/ChatReasoning.doc.mjs +49 -2
  45. package/src/CodeEditor/CodeEditor.doc.mjs +67 -2
  46. package/src/CodeEditor/CodeEditor.test.tsx +172 -2
  47. package/src/CodeEditor/CodeEditor.tsx +142 -9
  48. package/src/LogStream/LogStream.doc.mjs +53 -2
  49. package/src/Radial/RadialChart.test.tsx +157 -0
  50. package/src/Radial/RadialChart.tsx +102 -1
  51. package/src/RichTextEditor/RichTextEditor.doc.mjs +12 -1
  52. package/src/RichTextEditor/RichTextEditor.test.tsx +213 -3
  53. package/src/RichTextEditor/RichTextEditor.tsx +261 -30
  54. package/src/RichTextEditor/index.ts +1 -0
  55. package/src/Sankey/SankeyChart.test.tsx +95 -0
  56. package/src/Sankey/SankeyChart.tsx +55 -1
  57. package/src/Schedule/Schedule.doc.mjs +143 -3
  58. package/src/Stepper/Stepper.doc.mjs +6 -0
  59. package/src/ThreeD/ThreeDChart.test.tsx +251 -0
  60. package/src/ThreeD/ThreeDChart.tsx +84 -6
  61. package/src/__tests__/labApiContractDrift.test.tsx +57 -0
  62. package/src/index.ts +4 -0
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @file BottomSheet.tsx
3
+ * @input Uses React, StyleX, theme tokens, Drawer, useSheetGestures, themeProps
4
+ * @output Exports BottomSheet component and BottomSheetProps
5
+ * @position Lab implementation; consumed by index.ts, tested by BottomSheet.test.tsx, demonstrated in Storybook
6
+ *
7
+ * A mobile touch surface that rises from the bottom edge with a grab handle
8
+ * and swipe-to-dismiss. Built ON TOP OF the lab Drawer's native `<dialog>`
9
+ * engine (`side="bottom"`) rather than forking it — Drawer owns
10
+ * showModal/show, the open/close slide animation, focus trap + restore,
11
+ * scroll lock, the Escape/scrim contract, and the LIFO overlay registry.
12
+ * BottomSheet layers the grab handle and the drag-to-dismiss gesture on top,
13
+ * inside the dialog.
14
+ *
15
+ * The gesture translate is applied to an inner sheet wrapper, not the
16
+ * `<dialog>` itself, so Drawer owns the open/close slide and the drag
17
+ * translates within it without the two transforms fighting.
18
+ *
19
+ * SYNC: When modified, update these files to stay in sync:
20
+ * - /packages/lab/src/BottomSheet/BottomSheet.doc.mjs (props table, features, usage)
21
+ * - /packages/lab/src/BottomSheet/BottomSheet.test.tsx (tests for new/changed behavior)
22
+ * - /packages/lab/src/BottomSheet/index.ts (exports if types change)
23
+ * - /apps/storybook/stories/BottomSheet.stories.tsx (examples and visual coverage)
24
+ */
25
+ import { type ReactNode } from 'react';
26
+ import type { BaseProps } from '@astryxdesign/core';
27
+ /**
28
+ * Height budget for each named size. `medium`/`tall` track the viewport so
29
+ * the sheet scales with the device; `short` is a fixed peek height. `auto`
30
+ * is handled separately (fits content, capped at the `tall` budget).
31
+ */
32
+ declare const HEIGHT_BUDGETS: {
33
+ readonly short: "240px";
34
+ readonly medium: "50dvh";
35
+ readonly tall: "90dvh";
36
+ readonly auto: "90dvh";
37
+ };
38
+ export type BottomSheetHeight = keyof typeof HEIGHT_BUDGETS;
39
+ export interface BottomSheetProps extends BaseProps<HTMLDialogElement> {
40
+ /** Ref forwarded to the underlying <dialog> element. */
41
+ ref?: React.Ref<HTMLDialogElement>;
42
+ /** Whether the sheet is open. Fully controlled — pair with `onOpenChange`. */
43
+ isOpen: boolean;
44
+ /**
45
+ * Called when the sheet opens or closes. The boolean is the requested
46
+ * next state (`false` on Escape, scrim click, or a swipe past the dismiss
47
+ * threshold). The caller owns the open state.
48
+ */
49
+ onOpenChange: (isOpen: boolean) => void;
50
+ /**
51
+ * Accessible label for the sheet (required — the sheet has no built-in
52
+ * heading to derive a name from).
53
+ */
54
+ label: string;
55
+ /** Sheet content, rendered below the grab handle in a scrollable area. */
56
+ children: ReactNode;
57
+ /**
58
+ * How tall the sheet is:
59
+ * - `'short'` — a fixed peek height
60
+ * - `'medium'` — half the viewport
61
+ * - `'tall'` — nearly the full viewport
62
+ * - `'auto'` — fits its content, capped at the `'tall'` budget
63
+ *
64
+ * On viewports shorter than the budget the sheet fills the available
65
+ * height.
66
+ * @default 'medium'
67
+ */
68
+ height?: BottomSheetHeight;
69
+ /** Test ID for the root element. */
70
+ 'data-testid'?: string;
71
+ }
72
+ /**
73
+ * A mobile touch sheet that rises from the bottom edge, with a grab handle
74
+ * and swipe-to-dismiss. Built on the lab Drawer's `<dialog>` engine
75
+ * (`side="bottom"`): Drawer owns the open/close slide, focus trap + restore,
76
+ * scroll lock, and the Escape/scrim contract, while BottomSheet layers the
77
+ * grab handle and drag-to-dismiss on top.
78
+ *
79
+ * @example
80
+ * ```
81
+ * const [isOpen, setIsOpen] = useState(false);
82
+ * <BottomSheet
83
+ * isOpen={isOpen}
84
+ * onOpenChange={setIsOpen}
85
+ * label="Filters">
86
+ * <FilterControls />
87
+ * </BottomSheet>
88
+ * ```
89
+ */
90
+ export declare function BottomSheet({ isOpen, onOpenChange, label, children, height, xstyle, ...props }: BottomSheetProps): import("react").JSX.Element;
91
+ export declare namespace BottomSheet {
92
+ var displayName: string;
93
+ }
94
+ export {};
95
+ //# sourceMappingURL=BottomSheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BottomSheet.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/BottomSheet.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAErC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAUlD;;;;GAIG;AACH,QAAA,MAAM,cAAc;;;;;CAKV,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,cAAc,CAAC;AAoE5D,MAAM,WAAW,gBAAiB,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IACpE,wDAAwD;IACxD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAEnC,8EAA8E;IAC9E,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;OAIG;IACH,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAExC;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd,0EAA0E;IAC1E,QAAQ,EAAE,SAAS,CAAC;IAEpB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAE3B,oCAAoC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,MAAiB,EACjB,MAAM,EACN,GAAG,KAAK,EACT,EAAE,gBAAgB,+BAoClB;yBA5Ce,WAAW"}
@@ -0,0 +1,150 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file BottomSheet.tsx
7
+ * @input Uses React, StyleX, theme tokens, Drawer, useSheetGestures, themeProps
8
+ * @output Exports BottomSheet component and BottomSheetProps
9
+ * @position Lab implementation; consumed by index.ts, tested by BottomSheet.test.tsx, demonstrated in Storybook
10
+ *
11
+ * A mobile touch surface that rises from the bottom edge with a grab handle
12
+ * and swipe-to-dismiss. Built ON TOP OF the lab Drawer's native `<dialog>`
13
+ * engine (`side="bottom"`) rather than forking it — Drawer owns
14
+ * showModal/show, the open/close slide animation, focus trap + restore,
15
+ * scroll lock, the Escape/scrim contract, and the LIFO overlay registry.
16
+ * BottomSheet layers the grab handle and the drag-to-dismiss gesture on top,
17
+ * inside the dialog.
18
+ *
19
+ * The gesture translate is applied to an inner sheet wrapper, not the
20
+ * `<dialog>` itself, so Drawer owns the open/close slide and the drag
21
+ * translates within it without the two transforms fighting.
22
+ *
23
+ * SYNC: When modified, update these files to stay in sync:
24
+ * - /packages/lab/src/BottomSheet/BottomSheet.doc.mjs (props table, features, usage)
25
+ * - /packages/lab/src/BottomSheet/BottomSheet.test.tsx (tests for new/changed behavior)
26
+ * - /packages/lab/src/BottomSheet/index.ts (exports if types change)
27
+ * - /apps/storybook/stories/BottomSheet.stories.tsx (examples and visual coverage)
28
+ */
29
+ import * as stylex from '@stylexjs/stylex';
30
+ import '@astryxdesign/core/theme/tokens.stylex';
31
+ import { colorVars, radiusVars, spacingVars } from '@astryxdesign/core/theme/tokens.stylex';
32
+ import { mergeProps, themeProps } from '@astryxdesign/core/utils';
33
+ import { Drawer } from "../Drawer/index.js";
34
+ import { useSheetGestures } from "./useSheetGestures.js";
35
+
36
+ /**
37
+ * Height budget for each named size. `medium`/`tall` track the viewport so
38
+ * the sheet scales with the device; `short` is a fixed peek height. `auto`
39
+ * is handled separately (fits content, capped at the `tall` budget).
40
+ */
41
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
42
+ const HEIGHT_BUDGETS = {
43
+ short: '240px',
44
+ medium: '50dvh',
45
+ tall: '90dvh',
46
+ auto: '90dvh'
47
+ };
48
+ // Sheets on wide touch devices (tablets) shouldn't stretch edge to edge;
49
+ // cap and center them. Matches the `sm` layout breakpoint. On phones the
50
+ // viewport is narrower than this, so the sheet stays full-width.
51
+ const MAX_SHEET_WIDTH = 640;
52
+ const styles = {
53
+ sheet: {
54
+ k1xSpc: "x78zum5",
55
+ kXwgrk: "xdt5ytf",
56
+ kAzted: "x2lwn1j",
57
+ kZKoxP: "x5yr21d",
58
+ kzqmXN: "xh8yej3",
59
+ ks0D6T: "x11gisft",
60
+ kUOVxO: "xvueqy4",
61
+ kWkggS: "x10xzikg",
62
+ krdFHd: "x183tx6i",
63
+ kfmiAY: "x663v1",
64
+ kVQacm: "xb3r6kr",
65
+ kGO01o: "x1a148e8",
66
+ k6sLGO: "x1so62im",
67
+ $$css: true
68
+ },
69
+ dialogSurface: {
70
+ kWkggS: "xjbqb8w",
71
+ kGVxlE: "x1gnnqk1",
72
+ kEafiO: "x972fbf",
73
+ $$css: true
74
+ },
75
+ autoHeight: {
76
+ kZKoxP: "xg7h5cd",
77
+ kskxy: "x1ismmbu",
78
+ $$css: true
79
+ }
80
+ };
81
+ /**
82
+ * A mobile touch sheet that rises from the bottom edge, with a grab handle
83
+ * and swipe-to-dismiss. Built on the lab Drawer's `<dialog>` engine
84
+ * (`side="bottom"`): Drawer owns the open/close slide, focus trap + restore,
85
+ * scroll lock, and the Escape/scrim contract, while BottomSheet layers the
86
+ * grab handle and drag-to-dismiss on top.
87
+ *
88
+ * @example
89
+ * ```
90
+ * const [isOpen, setIsOpen] = useState(false);
91
+ * <BottomSheet
92
+ * isOpen={isOpen}
93
+ * onOpenChange={setIsOpen}
94
+ * label="Filters">
95
+ * <FilterControls />
96
+ * </BottomSheet>
97
+ * ```
98
+ */
99
+ export function BottomSheet({
100
+ isOpen,
101
+ onOpenChange,
102
+ label,
103
+ children,
104
+ height = 'medium',
105
+ xstyle,
106
+ ...props
107
+ }) {
108
+ const close = () => onOpenChange(false);
109
+ const {
110
+ contentProps,
111
+ handleProps
112
+ } = useSheetGestures({
113
+ isOpen,
114
+ onDismiss: close
115
+ });
116
+ return /*#__PURE__*/_jsx(Drawer, {
117
+ isOpen: isOpen,
118
+ onClose: close,
119
+ side: "bottom",
120
+ size: HEIGHT_BUDGETS[height],
121
+ label: label,
122
+ hasScrim: true,
123
+ hasCloseButton: false,
124
+ xstyle: [styles.dialogSurface, height === 'auto' && styles.autoHeight],
125
+ ...props,
126
+ children: /*#__PURE__*/_jsxs("div", {
127
+ "data-astryx-sheet": "",
128
+ ...mergeProps(themeProps('bottom-sheet'), stylex.props(styles.sheet, xstyle), contentProps.style),
129
+ children: [/*#__PURE__*/_jsx("div", {
130
+ "data-astryx-sheet-handle": "",
131
+ ...{
132
+ className: "x2lah0s x78zum5 x6s0dn4 xl56j7k xce4md1 x5ve5x3 x1jm3nie"
133
+ },
134
+ ...handleProps,
135
+ "aria-hidden": "true",
136
+ children: /*#__PURE__*/_jsx("div", {
137
+ ...{
138
+ className: "x14qfxbe xqu0tyb xjspbzw x1m4xfpy"
139
+ }
140
+ })
141
+ }), /*#__PURE__*/_jsx("div", {
142
+ ...{
143
+ className: "x1iyjqo2 x2lwn1j x1odjw0f xish69e"
144
+ },
145
+ children: children
146
+ })]
147
+ })
148
+ });
149
+ }
150
+ BottomSheet.displayName = 'BottomSheet';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @file index.ts
3
+ * @input BottomSheet.tsx
4
+ * @output Re-exports BottomSheet component and BottomSheetProps type
5
+ * @position Lab entry point for the BottomSheet directory
6
+ */
7
+ export { BottomSheet } from './BottomSheet';
8
+ export type { BottomSheetProps } from './BottomSheet';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/index.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAC1C,YAAY,EAAC,gBAAgB,EAAC,MAAM,eAAe,CAAC"}
@@ -0,0 +1,10 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file index.ts
5
+ * @input BottomSheet.tsx
6
+ * @output Re-exports BottomSheet component and BottomSheetProps type
7
+ * @position Lab entry point for the BottomSheet directory
8
+ */
9
+
10
+ export { BottomSheet } from "./BottomSheet.js";
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @file useSheetGestures.ts
3
+ * @input Uses React (useCallback, useEffect, useMemo, useRef, useState)
4
+ * @output Exports useSheetGestures hook and its option/result types
5
+ * @position Internal to BottomSheet; not exported from the lab entry point
6
+ *
7
+ * Drag-to-dismiss machinery for the bottom sheet. Tracks a pointer drag
8
+ * down the block axis, translates the sliding surface live, and on release
9
+ * either dismisses (past a distance or velocity threshold) or springs back
10
+ * to fully open. Pointer-events based so one code path covers mouse + touch.
11
+ *
12
+ * Kept private to BottomSheet: the behavior is sheet-specific (a dismiss
13
+ * edge on a bottom-anchored surface). It is not a general primitive and is
14
+ * intentionally not exported.
15
+ *
16
+ * SSR-safe: no window/document access at module scope; all measurement
17
+ * happens inside handlers. Respects prefers-reduced-motion by skipping the
18
+ * settle transition.
19
+ *
20
+ * SYNC: When modified, update these files to stay in sync:
21
+ * - /packages/lab/src/BottomSheet/useSheetGestures.doc.mjs
22
+ * - /packages/lab/src/BottomSheet/useSheetGestures.test.ts
23
+ */
24
+ import { type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react';
25
+ export interface UseSheetGesturesOptions {
26
+ /** Whether the owning sheet is open. Drag state resets when it closes. */
27
+ isOpen: boolean;
28
+ /** Called when a drag crosses the dismiss threshold (distance or flick). */
29
+ onDismiss: () => void;
30
+ }
31
+ export interface SheetContentProps {
32
+ style: CSSProperties;
33
+ }
34
+ export interface SheetHandleProps {
35
+ style: CSSProperties;
36
+ onPointerDown: (event: ReactPointerEvent) => void;
37
+ onPointerMove: (event: ReactPointerEvent) => void;
38
+ onPointerUp: (event: ReactPointerEvent) => void;
39
+ onPointerCancel: (event: ReactPointerEvent) => void;
40
+ }
41
+ export interface UseSheetGesturesResult {
42
+ /** Spread on the sliding surface: live translate + touch-action guard. */
43
+ contentProps: SheetContentProps;
44
+ /** Spread on the grab-handle element: pointer drag handlers. */
45
+ handleProps: SheetHandleProps;
46
+ /** Current live drag offset in px (positive = dragged toward the edge). */
47
+ dragOffset: number;
48
+ /** Whether a drag is currently in progress. */
49
+ isDragging: boolean;
50
+ }
51
+ /**
52
+ * Drag-to-dismiss machinery for the bottom sheet. Returns props to spread on
53
+ * the grab handle and the sliding surface, plus live drag state for callers
54
+ * that want to drive their own animation.
55
+ *
56
+ * @example
57
+ * ```
58
+ * const {contentProps, handleProps} = useSheetGestures({
59
+ * isOpen,
60
+ * onDismiss: () => onOpenChange(false),
61
+ * });
62
+ * <div {...contentProps}>
63
+ * <div {...handleProps} />
64
+ * {children}
65
+ * </div>
66
+ * ```
67
+ */
68
+ export declare function useSheetGestures({ isOpen, onDismiss, }: UseSheetGesturesOptions): UseSheetGesturesResult;
69
+ //# sourceMappingURL=useSheetGestures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSheetGestures.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/useSheetGestures.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,YAAY,IAAI,iBAAiB,EACvC,MAAM,OAAO,CAAC;AAQf,MAAM,WAAW,uBAAuB;IACtC,0EAA0E;IAC1E,MAAM,EAAE,OAAO,CAAC;IAChB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,aAAa,CAAC;IACrB,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAClD,aAAa,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAClD,WAAW,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAChD,eAAe,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,YAAY,EAAE,iBAAiB,CAAC;IAChC,gEAAgE;IAChE,WAAW,EAAE,gBAAgB,CAAC;IAC9B,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,UAAU,EAAE,OAAO,CAAC;CACrB;AASD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,SAAS,GACV,EAAE,uBAAuB,GAAG,sBAAsB,CA4HlD"}
@@ -0,0 +1,163 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file useSheetGestures.ts
7
+ * @input Uses React (useCallback, useEffect, useMemo, useRef, useState)
8
+ * @output Exports useSheetGestures hook and its option/result types
9
+ * @position Internal to BottomSheet; not exported from the lab entry point
10
+ *
11
+ * Drag-to-dismiss machinery for the bottom sheet. Tracks a pointer drag
12
+ * down the block axis, translates the sliding surface live, and on release
13
+ * either dismisses (past a distance or velocity threshold) or springs back
14
+ * to fully open. Pointer-events based so one code path covers mouse + touch.
15
+ *
16
+ * Kept private to BottomSheet: the behavior is sheet-specific (a dismiss
17
+ * edge on a bottom-anchored surface). It is not a general primitive and is
18
+ * intentionally not exported.
19
+ *
20
+ * SSR-safe: no window/document access at module scope; all measurement
21
+ * happens inside handlers. Respects prefers-reduced-motion by skipping the
22
+ * settle transition.
23
+ *
24
+ * SYNC: When modified, update these files to stay in sync:
25
+ * - /packages/lab/src/BottomSheet/useSheetGestures.doc.mjs
26
+ * - /packages/lab/src/BottomSheet/useSheetGestures.test.ts
27
+ */
28
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
29
+
30
+ // Fraction of the measured sheet height a drag must cross (with low
31
+ // velocity) before release dismisses instead of springing back.
32
+ const DISMISS_DISTANCE_RATIO = 0.25;
33
+ // A quick flick past this speed (px/ms) dismisses regardless of distance.
34
+ const DISMISS_VELOCITY = 0.5;
35
+ function prefersReducedMotion() {
36
+ if (typeof window === 'undefined' || !window.matchMedia) {
37
+ return false;
38
+ }
39
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
40
+ }
41
+
42
+ /**
43
+ * Drag-to-dismiss machinery for the bottom sheet. Returns props to spread on
44
+ * the grab handle and the sliding surface, plus live drag state for callers
45
+ * that want to drive their own animation.
46
+ *
47
+ * @example
48
+ * ```
49
+ * const {contentProps, handleProps} = useSheetGestures({
50
+ * isOpen,
51
+ * onDismiss: () => onOpenChange(false),
52
+ * });
53
+ * <div {...contentProps}>
54
+ * <div {...handleProps} />
55
+ * {children}
56
+ * </div>
57
+ * ```
58
+ */
59
+ export function useSheetGestures({
60
+ isOpen,
61
+ onDismiss
62
+ }) {
63
+ const [dragOffset, setDragOffset] = useState(0);
64
+ const [isDragging, setIsDragging] = useState(false);
65
+
66
+ // Latest callback ref so pointer handlers stay stable across renders.
67
+ const onDismissRef = useRef(onDismiss);
68
+ useEffect(() => {
69
+ onDismissRef.current = onDismiss;
70
+ });
71
+
72
+ // Live drag bookkeeping (refs so pointermove doesn't churn renders).
73
+ const dragStateRef = useRef(null);
74
+
75
+ // Reset drag state each time the sheet re-opens.
76
+ useEffect(() => {
77
+ if (isOpen) {
78
+ setDragOffset(0);
79
+ setIsDragging(false);
80
+ }
81
+ }, [isOpen]);
82
+ const measureHeight = useCallback(el => {
83
+ const sheet = el?.closest('[data-astryx-sheet]') ?? el;
84
+ return sheet?.getBoundingClientRect().height ?? 0;
85
+ }, []);
86
+ const settleFromDrag = useCallback((offset, velocity, height) => {
87
+ const dismiss = offset > height * DISMISS_DISTANCE_RATIO || velocity > DISMISS_VELOCITY;
88
+ if (dismiss) {
89
+ onDismissRef.current();
90
+ return;
91
+ }
92
+ // Below threshold: spring back to fully open.
93
+ setDragOffset(0);
94
+ }, []);
95
+ const handlePointerDown = useCallback(event => {
96
+ const target = event.currentTarget;
97
+ target.setPointerCapture?.(event.pointerId);
98
+ dragStateRef.current = {
99
+ pointerId: event.pointerId,
100
+ startCoord: event.clientY,
101
+ lastCoord: event.clientY,
102
+ lastTime: event.timeStamp,
103
+ velocity: 0,
104
+ height: measureHeight(target)
105
+ };
106
+ setIsDragging(true);
107
+ }, [measureHeight]);
108
+ const handlePointerMove = useCallback(event => {
109
+ const state = dragStateRef.current;
110
+ if (!state || state.pointerId !== event.pointerId) {
111
+ return;
112
+ }
113
+ // Dragging DOWN (increasing Y) collapses/dismisses. Only allow drag in
114
+ // the closing direction past the fully-open position (no overscroll).
115
+ const delta = event.clientY - state.startCoord;
116
+ const dt = event.timeStamp - state.lastTime;
117
+ if (dt > 0) {
118
+ state.velocity = (event.clientY - state.lastCoord) / dt;
119
+ state.lastCoord = event.clientY;
120
+ state.lastTime = event.timeStamp;
121
+ }
122
+ setDragOffset(Math.max(0, delta));
123
+ }, []);
124
+ const endDrag = useCallback(event => {
125
+ const state = dragStateRef.current;
126
+ if (!state || state.pointerId !== event.pointerId) {
127
+ return;
128
+ }
129
+ const target = event.currentTarget;
130
+ target.releasePointerCapture?.(event.pointerId);
131
+ const offset = Math.max(0, event.clientY - state.startCoord);
132
+ dragStateRef.current = null;
133
+ setIsDragging(false);
134
+ settleFromDrag(offset, state.velocity, state.height || 1);
135
+ }, [settleFromDrag]);
136
+
137
+ // Reduced motion skips the settle transition (snaps instantly).
138
+ const reducedMotion = useMemo(prefersReducedMotion, [isOpen]);
139
+ const contentProps = useMemo(() => ({
140
+ style: {
141
+ transform: dragOffset !== 0 ? `translateY(${dragOffset}px)` : undefined,
142
+ transition: isDragging || reducedMotion ? 'none' : undefined,
143
+ touchAction: 'none',
144
+ overscrollBehavior: 'contain'
145
+ }
146
+ }), [dragOffset, isDragging, reducedMotion]);
147
+ const handleProps = useMemo(() => ({
148
+ style: {
149
+ touchAction: 'none',
150
+ cursor: 'grab'
151
+ },
152
+ onPointerDown: handlePointerDown,
153
+ onPointerMove: handlePointerMove,
154
+ onPointerUp: endDrag,
155
+ onPointerCancel: endDrag
156
+ }), [handlePointerDown, handlePointerMove, endDrag]);
157
+ return {
158
+ contentProps,
159
+ handleProps,
160
+ dragOffset,
161
+ isDragging
162
+ };
163
+ }
@@ -5,6 +5,10 @@
5
5
  *
6
6
  * Handles the d3 margin convention: outer SVG at full size, inner <g> translated
7
7
  * by margins. Children receive scales and dimensions via context.
8
+ *
9
+ * Accessibility: the svg is exposed as a named image (role="img" + aria-label,
10
+ * overridable via `label`), and small datasets are mirrored in a visually
11
+ * hidden data table so screen reader users can read the underlying values.
8
12
  */
9
13
  import { type ReactNode } from 'react';
10
14
  import type { ChartMargin } from './types';
@@ -58,6 +62,13 @@ export interface ChartProps {
58
62
  * interference on mobile.
59
63
  */
60
64
  interactive?: boolean;
65
+ /**
66
+ * Accessible name for the chart, announced by screen readers (the svg is
67
+ * exposed as `role="img"`). Defaults to an English description derived from
68
+ * the keys (e.g. "Chart of revenue by month") — pass a localized string to
69
+ * override.
70
+ */
71
+ label?: string;
61
72
  /** Chart contents — axes, marks, tooltips */
62
73
  children: ReactNode;
63
74
  }
@@ -73,5 +84,5 @@ export interface ChartProps {
73
84
  * </Chart>
74
85
  * ```
75
86
  */
76
- export declare function Chart({ data, xKey, yKeys, height, margin: marginOverride, yBaseline, yDomain: yDomainProp, xDomain: xDomainProp, interactive, children, }: ChartProps): import("react").JSX.Element;
87
+ export declare function Chart({ data, xKey, yKeys, height, margin: marginOverride, yBaseline, yDomain: yDomainProp, xDomain: xDomainProp, interactive, label, children, }: ChartProps): import("react").JSX.Element;
77
88
  //# sourceMappingURL=Chart.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Chart.d.ts","sourceRoot":"","sources":["../../src/Chart/Chart.tsx"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,EAEL,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAKf,OAAO,KAAK,EAAC,WAAW,EAAa,MAAM,SAAS,CAAC;AAErD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjD,MAAM,WAAW,UAAU;IACzB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,6CAA6C;IAC7C,QAAQ,EAAE,SAAS,CAAC;CACrB;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,EACpB,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,MAAY,EACZ,MAAM,EAAE,cAAc,EACtB,SAAkB,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,WAAW,EACpB,WAAmB,EACnB,QAAQ,GACT,EAAE,UAAU,+BA6MZ"}
1
+ {"version":3,"file":"Chart.d.ts","sourceRoot":"","sources":["../../src/Chart/Chart.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,OAAO,EAEL,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAMf,OAAO,KAAK,EAAC,WAAW,EAAa,MAAM,SAAS,CAAC;AAErD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjD,MAAM,WAAW,UAAU;IACzB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,QAAQ,EAAE,SAAS,CAAC;CACrB;AAgBD;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,EACpB,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,MAAY,EACZ,MAAM,EAAE,cAAc,EACtB,SAAkB,EAClB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,WAAW,EACpB,WAAmB,EACnB,KAAK,EACL,QAAQ,GACT,EAAE,UAAU,+BA+OZ"}
@@ -7,10 +7,15 @@
7
7
  *
8
8
  * Handles the d3 margin convention: outer SVG at full size, inner <g> translated
9
9
  * by margins. Children receive scales and dimensions via context.
10
+ *
11
+ * Accessibility: the svg is exposed as a named image (role="img" + aria-label,
12
+ * overridable via `label`), and small datasets are mirrored in a visually
13
+ * hidden data table so screen reader users can read the underlying values.
10
14
  */
11
15
 
12
16
  import { useMemo, useRef, useState, useCallback, useLayoutEffect } from 'react';
13
17
  import { scaleLinear, scaleBand } from 'd3-scale';
18
+ import { VisuallyHidden } from '@astryxdesign/core/VisuallyHidden';
14
19
  import { isBandScale } from "./utils.js";
15
20
  import { ChartProvider } from "./ChartContext.js";
16
21
 
@@ -28,6 +33,18 @@ const DEFAULT_MARGIN = {
28
33
  left: 48
29
34
  };
30
35
 
36
+ /**
37
+ * Maximum number of data points (rows × series) for which the visually hidden
38
+ * data-table fallback is rendered. Beyond this a table is more noise than
39
+ * signal for screen reader users, so larger charts are name-only.
40
+ */
41
+ const MAX_TABLE_POINTS = 100;
42
+
43
+ /** Stringify a cell value for the hidden data table. */
44
+ function tableCell(value) {
45
+ return value == null ? '' : String(value);
46
+ }
47
+
31
48
  /**
32
49
  * Root chart container. Computes scales from data and provides them to children.
33
50
  *
@@ -50,6 +67,7 @@ export function Chart({
50
67
  yDomain: yDomainProp,
51
68
  xDomain: xDomainProp,
52
69
  interactive = false,
70
+ label,
53
71
  children
54
72
  }) {
55
73
  const containerRef = useRef(null);
@@ -215,11 +233,15 @@ export function Chart({
215
233
  touchAction: interactive ? 'none' : undefined,
216
234
  userSelect: interactive ? 'none' : undefined
217
235
  };
218
- return /*#__PURE__*/_jsx("div", {
236
+ const accessibleLabel = label ?? `Chart of ${yKeys.join(', ')} by ${xKey}`;
237
+ const showDataTable = data.length > 0 && data.length * Math.max(yKeys.length, 1) <= MAX_TABLE_POINTS;
238
+ return /*#__PURE__*/_jsxs("div", {
219
239
  ref: containerRef,
220
240
  style: containerStyle,
221
- children: containerWidth > 0 && /*#__PURE__*/_jsxs("svg", {
241
+ children: [containerWidth > 0 && /*#__PURE__*/_jsxs("svg", {
222
242
  ref: svgRef,
243
+ role: "img",
244
+ "aria-label": accessibleLabel,
223
245
  width: containerWidth,
224
246
  height: height,
225
247
  style: interactive ? {
@@ -242,6 +264,32 @@ export function Chart({
242
264
  children: children
243
265
  })
244
266
  })]
245
- })
267
+ }), showDataTable && /*#__PURE__*/_jsx(VisuallyHidden, {
268
+ as: "div",
269
+ children: /*#__PURE__*/_jsxs("table", {
270
+ children: [/*#__PURE__*/_jsx("caption", {
271
+ children: `${accessibleLabel} data`
272
+ }), /*#__PURE__*/_jsx("thead", {
273
+ children: /*#__PURE__*/_jsxs("tr", {
274
+ children: [/*#__PURE__*/_jsx("th", {
275
+ scope: "col",
276
+ children: xKey
277
+ }), yKeys.map(key => /*#__PURE__*/_jsx("th", {
278
+ scope: "col",
279
+ children: key
280
+ }, key))]
281
+ })
282
+ }), /*#__PURE__*/_jsx("tbody", {
283
+ children: data.map((d, i) => /*#__PURE__*/_jsxs("tr", {
284
+ children: [/*#__PURE__*/_jsx("th", {
285
+ scope: "row",
286
+ children: tableCell(d[xKey])
287
+ }), yKeys.map(key => /*#__PURE__*/_jsx("td", {
288
+ children: tableCell(d[key])
289
+ }, key))]
290
+ }, i))
291
+ })]
292
+ })
293
+ })]
246
294
  });
247
295
  }