@astryxdesign/lab 0.1.9-canary.fef7dca → 0.2.0-canary.136a00f
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/dist/BottomSheet/BottomSheet.d.ts +95 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheet.js +150 -0
- package/dist/BottomSheet/index.d.ts +9 -0
- package/dist/BottomSheet/index.d.ts.map +1 -0
- package/dist/BottomSheet/index.js +10 -0
- package/dist/BottomSheet/useSheetGestures.d.ts +69 -0
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
- package/dist/BottomSheet/useSheetGestures.js +163 -0
- package/dist/Chart/Chart.d.ts +12 -1
- package/dist/Chart/Chart.d.ts.map +1 -1
- package/dist/Chart/Chart.js +51 -3
- package/dist/CodeEditor/CodeEditor.d.ts +14 -2
- package/dist/CodeEditor/CodeEditor.d.ts.map +1 -1
- package/dist/CodeEditor/CodeEditor.js +110 -9
- package/dist/Radial/RadialChart.d.ts +12 -1
- package/dist/Radial/RadialChart.d.ts.map +1 -1
- package/dist/Radial/RadialChart.js +89 -4
- package/dist/RichTextEditor/RichTextEditor.d.ts +45 -5
- package/dist/RichTextEditor/RichTextEditor.d.ts.map +1 -1
- package/dist/RichTextEditor/RichTextEditor.js +157 -11
- package/dist/RichTextEditor/index.d.ts +1 -1
- package/dist/RichTextEditor/index.d.ts.map +1 -1
- package/dist/Sankey/SankeyChart.d.ts +13 -1
- package/dist/Sankey/SankeyChart.d.ts.map +1 -1
- package/dist/Sankey/SankeyChart.js +53 -5
- package/dist/ThreeD/ThreeDChart.d.ts +16 -3
- package/dist/ThreeD/ThreeDChart.d.ts.map +1 -1
- package/dist/ThreeD/ThreeDChart.js +63 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/lab.css +13 -0
- package/package.json +2 -2
- package/src/BottomSheet/BottomSheet.doc.mjs +105 -0
- package/src/BottomSheet/BottomSheet.test.tsx +233 -0
- package/src/BottomSheet/BottomSheet.tsx +226 -0
- package/src/BottomSheet/index.ts +11 -0
- package/src/BottomSheet/useSheetGestures.doc.mjs +31 -0
- package/src/BottomSheet/useSheetGestures.test.ts +160 -0
- package/src/BottomSheet/useSheetGestures.ts +226 -0
- package/src/Chart/Chart.test.tsx +126 -0
- package/src/Chart/Chart.tsx +59 -0
- package/src/ChatReasoning/ChatReasoning.doc.mjs +49 -2
- package/src/CodeEditor/CodeEditor.doc.mjs +67 -2
- package/src/CodeEditor/CodeEditor.test.tsx +172 -2
- package/src/CodeEditor/CodeEditor.tsx +142 -9
- package/src/LogStream/LogStream.doc.mjs +53 -2
- package/src/Radial/RadialChart.test.tsx +157 -0
- package/src/Radial/RadialChart.tsx +102 -1
- package/src/RichTextEditor/RichTextEditor.doc.mjs +12 -1
- package/src/RichTextEditor/RichTextEditor.test.tsx +213 -3
- package/src/RichTextEditor/RichTextEditor.tsx +261 -30
- package/src/RichTextEditor/index.ts +1 -0
- package/src/Sankey/SankeyChart.test.tsx +95 -0
- package/src/Sankey/SankeyChart.tsx +55 -1
- package/src/Schedule/Schedule.doc.mjs +143 -3
- package/src/Stepper/Stepper.doc.mjs +6 -0
- package/src/ThreeD/ThreeDChart.test.tsx +251 -0
- package/src/ThreeD/ThreeDChart.tsx +84 -6
- package/src/__tests__/labApiContractDrift.test.tsx +57 -0
- 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
|
+
}
|
package/dist/Chart/Chart.d.ts
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/Chart/Chart.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|