@astryxdesign/lab 0.1.9-canary.fef7dca → 0.2.0-canary.0e01213

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 (76) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts +101 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
  3. package/dist/BottomSheet/BottomSheet.js +297 -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/snapOffsets.d.ts +54 -0
  8. package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
  9. package/dist/BottomSheet/snapOffsets.js +101 -0
  10. package/dist/BottomSheet/useSheetGestures.d.ts +112 -0
  11. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
  12. package/dist/BottomSheet/useSheetGestures.js +506 -0
  13. package/dist/Chart/Chart.d.ts +12 -1
  14. package/dist/Chart/Chart.d.ts.map +1 -1
  15. package/dist/Chart/Chart.js +51 -3
  16. package/dist/CodeEditor/CodeEditor.d.ts +14 -2
  17. package/dist/CodeEditor/CodeEditor.d.ts.map +1 -1
  18. package/dist/CodeEditor/CodeEditor.js +110 -9
  19. package/dist/Radial/RadialChart.d.ts +12 -1
  20. package/dist/Radial/RadialChart.d.ts.map +1 -1
  21. package/dist/Radial/RadialChart.js +89 -4
  22. package/dist/RichTextEditor/RichTextEditor.d.ts +51 -5
  23. package/dist/RichTextEditor/RichTextEditor.d.ts.map +1 -1
  24. package/dist/RichTextEditor/RichTextEditor.js +163 -17
  25. package/dist/RichTextEditor/editorNodes.d.ts +8 -0
  26. package/dist/RichTextEditor/editorNodes.d.ts.map +1 -0
  27. package/dist/RichTextEditor/editorNodes.js +23 -0
  28. package/dist/RichTextEditor/index.d.ts +3 -1
  29. package/dist/RichTextEditor/index.d.ts.map +1 -1
  30. package/dist/RichTextEditor/index.js +2 -1
  31. package/dist/RichTextEditor/markdownSerializers.d.ts +40 -0
  32. package/dist/RichTextEditor/markdownSerializers.d.ts.map +1 -0
  33. package/dist/RichTextEditor/markdownSerializers.js +85 -0
  34. package/dist/Sankey/SankeyChart.d.ts +13 -1
  35. package/dist/Sankey/SankeyChart.d.ts.map +1 -1
  36. package/dist/Sankey/SankeyChart.js +53 -5
  37. package/dist/ThreeD/ThreeDChart.d.ts +16 -3
  38. package/dist/ThreeD/ThreeDChart.d.ts.map +1 -1
  39. package/dist/ThreeD/ThreeDChart.js +63 -5
  40. package/dist/index.d.ts +2 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +4 -1
  43. package/dist/lab.css +20 -0
  44. package/package.json +13 -3
  45. package/src/BottomSheet/BottomSheet.doc.mjs +105 -0
  46. package/src/BottomSheet/BottomSheet.test.tsx +297 -0
  47. package/src/BottomSheet/BottomSheet.tsx +460 -0
  48. package/src/BottomSheet/index.ts +11 -0
  49. package/src/BottomSheet/snapOffsets.test.ts +146 -0
  50. package/src/BottomSheet/snapOffsets.ts +126 -0
  51. package/src/BottomSheet/useSheetGestures.doc.mjs +32 -0
  52. package/src/BottomSheet/useSheetGestures.test.ts +410 -0
  53. package/src/BottomSheet/useSheetGestures.ts +686 -0
  54. package/src/Chart/Chart.test.tsx +126 -0
  55. package/src/Chart/Chart.tsx +59 -0
  56. package/src/ChatReasoning/ChatReasoning.doc.mjs +49 -2
  57. package/src/CodeEditor/CodeEditor.doc.mjs +67 -2
  58. package/src/CodeEditor/CodeEditor.test.tsx +172 -2
  59. package/src/CodeEditor/CodeEditor.tsx +142 -9
  60. package/src/LogStream/LogStream.doc.mjs +53 -2
  61. package/src/Radial/RadialChart.test.tsx +157 -0
  62. package/src/Radial/RadialChart.tsx +102 -1
  63. package/src/RichTextEditor/RichTextEditor.doc.mjs +17 -1
  64. package/src/RichTextEditor/RichTextEditor.test.tsx +269 -3
  65. package/src/RichTextEditor/RichTextEditor.tsx +273 -45
  66. package/src/RichTextEditor/editorNodes.ts +34 -0
  67. package/src/RichTextEditor/index.ts +7 -0
  68. package/src/RichTextEditor/markdownSerializers.ts +105 -0
  69. package/src/Sankey/SankeyChart.test.tsx +95 -0
  70. package/src/Sankey/SankeyChart.tsx +55 -1
  71. package/src/Schedule/Schedule.doc.mjs +143 -3
  72. package/src/Stepper/Stepper.doc.mjs +6 -0
  73. package/src/ThreeD/ThreeDChart.test.tsx +251 -0
  74. package/src/ThreeD/ThreeDChart.tsx +84 -6
  75. package/src/__tests__/labApiContractDrift.test.tsx +57 -0
  76. package/src/index.ts +7 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @file BottomSheet.tsx
3
+ * @input Uses React, StyleX, theme tokens, core hooks (useScrollLock), utils, useSheetGestures
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: grab handle, snap
8
+ * points, and swipe-to-dismiss. It owns a native modal `<dialog>` directly and
9
+ * composes core primitives (`useScrollLock`, `<dialog>.showModal()` for the
10
+ * top layer + focus trap, a `::backdrop` scrim) rather than delegating to a
11
+ * heavier overlay component — so it controls its own sizing and can render a
12
+ * full-height transparent shell with the visible sheet bottom-anchored inside.
13
+ * That shell is what lets the sheet translate freely (including a little past
14
+ * fully-open) without clipping against a fixed dialog edge.
15
+ *
16
+ * The drag/snap/dismiss machinery lives in `useSheetGestures`; the offset
17
+ * geometry lives in the pure, tested `snapOffsets` module. Both are internal
18
+ * to BottomSheet and not exported from the lab entry point.
19
+ *
20
+ * SYNC: When modified, update these files to stay in sync:
21
+ * - /packages/lab/src/BottomSheet/BottomSheet.doc.mjs (props table, features, usage)
22
+ * - /packages/lab/src/BottomSheet/BottomSheet.test.tsx (tests for new/changed behavior)
23
+ * - /packages/lab/src/BottomSheet/index.ts (exports if types change)
24
+ * - /apps/storybook/stories/BottomSheet.stories.tsx (examples and visual coverage)
25
+ */
26
+ import { type ReactNode } from 'react';
27
+ import type { BaseProps } from '@astryxdesign/core';
28
+ /**
29
+ * Height budget for each named size, as a fraction of the viewport:
30
+ * - `hug` — fits its content, never taller than 92%.
31
+ * - `capped` — a scrolling mid-height panel (62%).
32
+ * - `tall` — a pinned near-full panel (92%); use when content streams in so
33
+ * the sheet doesn't resize under the user.
34
+ */
35
+ declare const HEIGHT_BUDGETS: {
36
+ readonly hug: "92dvh";
37
+ readonly capped: "62dvh";
38
+ readonly tall: "92dvh";
39
+ };
40
+ export type BottomSheetHeight = keyof typeof HEIGHT_BUDGETS;
41
+ export interface BottomSheetProps extends BaseProps<HTMLDialogElement> {
42
+ /** Ref forwarded to the underlying <dialog> element. */
43
+ ref?: React.Ref<HTMLDialogElement>;
44
+ /** Whether the sheet is open. Fully controlled — pair with `onOpenChange`. */
45
+ isOpen: boolean;
46
+ /**
47
+ * Called when the sheet opens or closes. The boolean is the requested next
48
+ * state (`false` on Escape, scrim click, or a swipe past the dismiss
49
+ * threshold). The caller owns the open state.
50
+ */
51
+ onOpenChange: (isOpen: boolean) => void;
52
+ /**
53
+ * Accessible label for the sheet (required — the sheet has no built-in
54
+ * heading to derive a name from).
55
+ */
56
+ label: string;
57
+ /** Sheet content, rendered below the grab handle in a scrollable area. */
58
+ children: ReactNode;
59
+ /**
60
+ * How tall the sheet is. A named budget, or any explicit height:
61
+ * - `'hug'` — fits its content, never taller than 92% of the viewport.
62
+ * - `'capped'` — a scrolling mid-height panel (~62%).
63
+ * - `'tall'` — a pinned near-full panel (~92%); use when content streams in
64
+ * so the sheet doesn't resize under the user.
65
+ * - a `number` (px) or CSS length string (e.g. `'70dvh'`, `480`) for a
66
+ * custom budget.
67
+ *
68
+ * The user can still drag between snap points regardless of the starting
69
+ * height. On viewports shorter than the budget the sheet fills the
70
+ * available height.
71
+ * @default 'capped'
72
+ */
73
+ height?: BottomSheetHeight | number | string;
74
+ /** Test ID for the root element. */
75
+ 'data-testid'?: string;
76
+ }
77
+ /**
78
+ * A mobile touch sheet that rises from the bottom edge, with a grab handle,
79
+ * drag-to-resize snap points, and swipe-to-dismiss. It owns a native modal
80
+ * `<dialog>` (top layer, focus trap, `::backdrop` scrim) and locks body scroll
81
+ * while open; a slow drag settles to the nearest snap point, a flick down
82
+ * dismisses, and Escape closes — so the swipe always has a keyboard
83
+ * equivalent.
84
+ *
85
+ * @example
86
+ * ```
87
+ * const [isOpen, setIsOpen] = useState(false);
88
+ * <BottomSheet
89
+ * isOpen={isOpen}
90
+ * onOpenChange={setIsOpen}
91
+ * label="Filters">
92
+ * <FilterControls />
93
+ * </BottomSheet>
94
+ * ```
95
+ */
96
+ export declare function BottomSheet({ ref, isOpen, onOpenChange, label, children, height, xstyle, ...props }: BottomSheetProps): import("react").JSX.Element;
97
+ export declare namespace BottomSheet {
98
+ var displayName: string;
99
+ }
100
+ export {};
101
+ //# sourceMappingURL=BottomSheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BottomSheet.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/BottomSheet.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAiC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAErE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAqBlD;;;;;;GAMG;AACH,QAAA,MAAM,cAAc;;;;CAIV,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,cAAc,CAAC;AAuJ5D,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;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,MAAM,CAAC;IAE7C,oCAAoC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,GAAG,EACH,MAAM,EACN,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,MAAiB,EACjB,MAAM,EACN,GAAG,KAAK,EACT,EAAE,gBAAgB,+BAyKlB;yBAlLe,WAAW"}
@@ -0,0 +1,297 @@
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, core hooks (useScrollLock), utils, useSheetGestures
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: grab handle, snap
12
+ * points, and swipe-to-dismiss. It owns a native modal `<dialog>` directly and
13
+ * composes core primitives (`useScrollLock`, `<dialog>.showModal()` for the
14
+ * top layer + focus trap, a `::backdrop` scrim) rather than delegating to a
15
+ * heavier overlay component — so it controls its own sizing and can render a
16
+ * full-height transparent shell with the visible sheet bottom-anchored inside.
17
+ * That shell is what lets the sheet translate freely (including a little past
18
+ * fully-open) without clipping against a fixed dialog edge.
19
+ *
20
+ * The drag/snap/dismiss machinery lives in `useSheetGestures`; the offset
21
+ * geometry lives in the pure, tested `snapOffsets` module. Both are internal
22
+ * to BottomSheet and not exported from the lab entry point.
23
+ *
24
+ * SYNC: When modified, update these files to stay in sync:
25
+ * - /packages/lab/src/BottomSheet/BottomSheet.doc.mjs (props table, features, usage)
26
+ * - /packages/lab/src/BottomSheet/BottomSheet.test.tsx (tests for new/changed behavior)
27
+ * - /packages/lab/src/BottomSheet/index.ts (exports if types change)
28
+ * - /apps/storybook/stories/BottomSheet.stories.tsx (examples and visual coverage)
29
+ */
30
+ import { useCallback, useEffect, useRef } from 'react';
31
+ import * as stylex from '@stylexjs/stylex';
32
+ import '@astryxdesign/core/theme/tokens.stylex';
33
+ import { colorVars, durationVars, easeVars, radiusVars, shadowVars, sizeVars, spacingVars } from '@astryxdesign/core/theme/tokens.stylex';
34
+ import { useDevWarning, useScrollLock } from '@astryxdesign/core/hooks';
35
+ import { mergeProps, mergeRefs, themeProps } from '@astryxdesign/core/utils';
36
+ import { useSheetGestures } from "./useSheetGestures.js";
37
+
38
+ // Detent fractions of the viewport, ascending (peek / mid / full). Detents
39
+ // that land within DETENT_DEDUP_PX of each other (e.g. a hug height next to a
40
+ // snap) are de-duped in the gesture hook, and any taller than the sheet are
41
+ // dropped, so a full set is safe to offer. The shortest is a glance "peek";
42
+ // the scrim hides as the sheet collapses onto it (see onScrimOpacity).
43
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
44
+ const SNAP_FRACTIONS = [0.14, 0.5, 0.92];
45
+
46
+ /**
47
+ * Height budget for each named size, as a fraction of the viewport:
48
+ * - `hug` — fits its content, never taller than 92%.
49
+ * - `capped` — a scrolling mid-height panel (62%).
50
+ * - `tall` — a pinned near-full panel (92%); use when content streams in so
51
+ * the sheet doesn't resize under the user.
52
+ */
53
+ const HEIGHT_BUDGETS = {
54
+ hug: '92dvh',
55
+ // upper bound only; the sheet hugs its content beneath it
56
+ capped: '62dvh',
57
+ tall: '92dvh'
58
+ };
59
+ // px the sheet extends below its budget as reserved padding, revealed by an
60
+ // upward overdrag rather than showing a gap.
61
+ // SYNC: must match OVERSCROLL_MAX in useSheetGestures.ts (the drag cap).
62
+ const OVERSCROLL_PADDING = 48;
63
+
64
+ /**
65
+ * Default snap detents in px, resolved against the *visual* viewport (like iOS
66
+ * detents), so a mid rest point is ~half the screen regardless of the sheet's
67
+ * own height budget. Read lazily at drag start (no persistent listener) so it
68
+ * reflects the live viewport after a rotation or the virtual keyboard opening.
69
+ * SSR-safe: returns `[]` off the client.
70
+ */
71
+ function defaultSnapHeights() {
72
+ if (typeof window === 'undefined') {
73
+ return [];
74
+ }
75
+ const vh = window.visualViewport?.height ?? window.innerHeight;
76
+ return SNAP_FRACTIONS.map(f => f * vh);
77
+ }
78
+ const styles = {
79
+ sheet: {
80
+ kfzvcC: "x67bb7w",
81
+ kB7OPa: "x9f619",
82
+ k1xSpc: "x78zum5",
83
+ kXwgrk: "xdt5ytf",
84
+ kAzted: "x2lwn1j",
85
+ kzqmXN: "xh8yej3",
86
+ ks0D6T: "x11gisft",
87
+ kWkggS: "x10xzikg",
88
+ krdFHd: "x81l70g",
89
+ kfmiAY: "x7hs6f1",
90
+ kGVxlE: "x1kcpxr7",
91
+ kI3sdo: "x1a2a7pz",
92
+ kVQacm: "xb3r6kr",
93
+ kGO01o: "x1wkw6tp",
94
+ k1K539: "x1if0o47",
95
+ k3aq6I: "xnn1q72 xhbqy3z",
96
+ k1ekBW: "x11xpdln",
97
+ kIyJzY: "x80gvsz",
98
+ kAMwcw: "xlr8y92",
99
+ k6sLGO: "x1so62im",
100
+ k6CgDc: "xzg1mie",
101
+ $$css: true
102
+ },
103
+ sheetClosing: {
104
+ k3aq6I: "x1weeur4",
105
+ $$css: true
106
+ },
107
+ budget: {
108
+ kZKoxP: "x1tpcejd",
109
+ $$css: true
110
+ },
111
+ hugHeight: {
112
+ kZKoxP: "xg7h5cd",
113
+ kskxy: "x14bu9tk",
114
+ $$css: true
115
+ }
116
+ };
117
+ /**
118
+ * A mobile touch sheet that rises from the bottom edge, with a grab handle,
119
+ * drag-to-resize snap points, and swipe-to-dismiss. It owns a native modal
120
+ * `<dialog>` (top layer, focus trap, `::backdrop` scrim) and locks body scroll
121
+ * while open; a slow drag settles to the nearest snap point, a flick down
122
+ * dismisses, and Escape closes — so the swipe always has a keyboard
123
+ * equivalent.
124
+ *
125
+ * @example
126
+ * ```
127
+ * const [isOpen, setIsOpen] = useState(false);
128
+ * <BottomSheet
129
+ * isOpen={isOpen}
130
+ * onOpenChange={setIsOpen}
131
+ * label="Filters">
132
+ * <FilterControls />
133
+ * </BottomSheet>
134
+ * ```
135
+ */
136
+ export function BottomSheet({
137
+ ref,
138
+ isOpen,
139
+ onOpenChange,
140
+ label,
141
+ children,
142
+ height = 'capped',
143
+ xstyle,
144
+ ...props
145
+ }) {
146
+ const dialogRef = useRef(null);
147
+ const triggerRef = useRef(null);
148
+ const sheetNodeRef = useRef(null);
149
+ const close = useCallback(() => onOpenChange(false), [onOpenChange]);
150
+
151
+ // Set imperatively (not via state) so a 60fps drag doesn't re-render. The
152
+ // hook reports the scrim opacity for the current detent (hidden at the peek).
153
+ const handleScrimOpacity = useCallback(opacity => {
154
+ dialogRef.current?.style.setProperty('--_sheet-scrim-opacity', String(opacity));
155
+ }, []);
156
+ const {
157
+ contentProps,
158
+ handleProps,
159
+ bodyProps,
160
+ sheetRef
161
+ } = useSheetGestures({
162
+ isOpen,
163
+ onDismiss: close,
164
+ snapHeights: defaultSnapHeights,
165
+ onScrimOpacity: handleScrimOpacity
166
+ });
167
+
168
+ // showModal() enters the top layer with a focus trap + ::backdrop; on close
169
+ // we animate out (below) then restore focus to the opener.
170
+ useEffect(() => {
171
+ const dialog = dialogRef.current;
172
+ if (!dialog) {
173
+ return;
174
+ }
175
+ if (isOpen) {
176
+ triggerRef.current = document.activeElement;
177
+ dialog.style.setProperty('--_sheet-scrim-opacity', '1');
178
+ if (!dialog.open) {
179
+ dialog.showModal();
180
+ // Override showModal()'s default (first tabbable) to land on the panel,
181
+ // so AT reads from the label — unless a descendant sets data-autofocus.
182
+ const autofocus = dialog.querySelector('[data-autofocus]');
183
+ if (autofocus) {
184
+ autofocus.focus();
185
+ } else {
186
+ sheetNodeRef.current?.focus();
187
+ }
188
+ }
189
+ } else if (dialog.open) {
190
+ // Wait for the slide-out (sheetClosing, applied this render) before
191
+ // close() releases the top layer. Timeout backstops a missing
192
+ // transitionend (reduced motion, hidden tab).
193
+ const sheet = sheetNodeRef.current;
194
+ let done = false;
195
+ const finish = () => {
196
+ if (done) {
197
+ return;
198
+ }
199
+ done = true;
200
+ clearTimeout(timer);
201
+ sheet?.removeEventListener('transitionend', onEnd);
202
+ if (dialog.open) {
203
+ dialog.close();
204
+ }
205
+ triggerRef.current?.focus();
206
+ triggerRef.current = null;
207
+ };
208
+ const onEnd = event => {
209
+ if (event.target === sheet && event.propertyName === 'transform') {
210
+ finish();
211
+ }
212
+ };
213
+ sheet?.addEventListener('transitionend', onEnd);
214
+ // Backstop above --duration-medium (410ms) so transitionend normally
215
+ // fires first; this only covers environments that don't emit it.
216
+ const timer = setTimeout(finish, 450);
217
+ return () => {
218
+ clearTimeout(timer);
219
+ sheet?.removeEventListener('transitionend', onEnd);
220
+ };
221
+ }
222
+ }, [isOpen]);
223
+ useScrollLock(isOpen);
224
+
225
+ // Enforce an accessible name: label is required by types, but a JS caller
226
+ // can still pass an empty string, leaving the sheet unnamed.
227
+ useDevWarning('BottomSheet', 'requires a non-empty `label` for an accessible name; the open sheet ' + 'has no built-in heading to derive one from.', isOpen && !label);
228
+ const handleCancel = useCallback(event => {
229
+ event.preventDefault();
230
+ close();
231
+ }, [close]);
232
+ // Explicit Escape handler in addition to the native `cancel` event, for
233
+ // environments that don't synthesize `cancel`.
234
+ const handleKeyDown = useCallback(event => {
235
+ if (event.key === 'Escape') {
236
+ event.preventDefault();
237
+ close();
238
+ }
239
+ }, [close]);
240
+ const handleClick = useCallback(event => {
241
+ if (event.target === event.currentTarget) {
242
+ close();
243
+ }
244
+ }, [close]);
245
+ const isNamed = typeof height === 'string' && height in HEIGHT_BUDGETS;
246
+ const budget = isNamed ? HEIGHT_BUDGETS[height] : typeof height === 'number' ? `${height}px` : height;
247
+ const isHug = height === 'hug';
248
+ return /*#__PURE__*/_jsx("dialog", {
249
+ ...{
250
+ 0: {
251
+ className: "xixxii4 x10a8y8t x1o6l61p xtdtrs8 x1x1rfll x7ab17h x1ghz6dp x1717udv xjbqb8w x1rea2x4 x47corl x1s85apg x1a2a7pz xnixb3f x1abwkk1 xni466t xxiuuzi xc0dz0a xft5bk6 x15h3t91 x142juwg x1viac0w"
252
+ },
253
+ 1: {
254
+ className: "xixxii4 x10a8y8t x1o6l61p xtdtrs8 x1x1rfll x7ab17h x1ghz6dp x1717udv xjbqb8w x1rea2x4 x47corl x1a2a7pz x1lliihq xnixb3f x1abwkk1 xni466t xxiuuzi xc0dz0a xft5bk6 x15h3t91 x142juwg x1viac0w"
255
+ }
256
+ }[!!isOpen << 0],
257
+ ref: mergeRefs(ref, dialogRef),
258
+ "aria-label": label,
259
+ "aria-modal": "true",
260
+ onCancel: handleCancel,
261
+ onClick: handleClick,
262
+ onKeyDown: handleKeyDown,
263
+ ...props,
264
+ children: /*#__PURE__*/_jsx("div", {
265
+ ...{
266
+ className: "x10l6tqk x17y0mx6 x1ey2m1c x78zum5 xl56j7k x47corl"
267
+ },
268
+ children: /*#__PURE__*/_jsxs("div", {
269
+ ref: mergeRefs(sheetRef, sheetNodeRef),
270
+ tabIndex: -1,
271
+ ...mergeProps(themeProps('bottom-sheet'), stylex.props(styles.sheet, isHug ? styles.hugHeight : styles.budget, !isOpen && styles.sheetClosing, xstyle), undefined, {
272
+ ['--_sheet-budget']: budget,
273
+ ...contentProps.style
274
+ }),
275
+ children: [/*#__PURE__*/_jsx("div", {
276
+ ...{
277
+ className: "x2lah0s x78zum5 x6s0dn4 xl56j7k x1k15mir x5ve5x3 x1jm3nie"
278
+ },
279
+ ...handleProps,
280
+ "aria-hidden": "true",
281
+ children: /*#__PURE__*/_jsx("div", {
282
+ ...{
283
+ className: "x1m747yf x11c6zpc xjspbzw x1m4xfpy"
284
+ }
285
+ })
286
+ }), /*#__PURE__*/_jsx("div", {
287
+ ...{
288
+ className: "x1iyjqo2 x2lwn1j x1odjw0f x8du1vd xx69xxh"
289
+ },
290
+ ...bodyProps,
291
+ children: children
292
+ })]
293
+ })
294
+ })
295
+ });
296
+ }
297
+ 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,54 @@
1
+ /**
2
+ * @file snapOffsets.ts
3
+ * @input Pure geometry helpers — no React, no DOM.
4
+ * @output Converts snap points to translateY offsets for the sheet surface.
5
+ * @position Internal to BottomSheet; consumed by useSheetGestures, tested by
6
+ * snapOffsets.test.ts.
7
+ *
8
+ * The sheet is rendered at its full (fully-open) height and *translated down*
9
+ * to express shorter detents — translate is GPU-composited, so this is cheaper
10
+ * than animating layout height. A detent is therefore a translateY offset in
11
+ * px from the fully-open position (0 = fully open, larger = more collapsed).
12
+ * These helpers turn snap points into that offset list; they are pure so the
13
+ * geometry can be unit-tested without a DOM.
14
+ */
15
+ /**
16
+ * Detents whose resting offsets land within this many px of each other are
17
+ * treated as the same stop (the taller one wins). Stops the sheet from having
18
+ * two near-identical rest positions — e.g. when a content-hugging height sits
19
+ * a hair away from a fractional snap point.
20
+ */
21
+ export declare const DETENT_DEDUP_PX = 48;
22
+ /** Resolve snap points given as viewport fractions (0, 1] to px heights. */
23
+ export declare function snapFractionsToHeights(fractions: readonly number[], viewportPx: number): number[];
24
+ /**
25
+ * Given the sheet's full height (px, as rendered fully open) and a set of
26
+ * candidate detent *visible heights* (px), return the resting translateY
27
+ * offsets from fully-open, ascending and de-duplicated.
28
+ *
29
+ * - `0` (fully open) is always the first detent.
30
+ * - Only heights strictly shorter than the sheet become collapsed detents; a
31
+ * height >= the sheet can't be shown by translating down, so it's dropped.
32
+ * - Offsets closer than `dedupPx` collapse to the smaller (taller) offset, so
33
+ * near-identical detents — e.g. a hug height ≈ a snap point — become one stop.
34
+ */
35
+ export declare function computeDetentOffsets(sheetHeight: number, detentHeights: readonly number[], dedupPx?: number): number[];
36
+ /** Nearest value in `offsets` to `value` (offsets must be non-empty). */
37
+ export declare function nearestOffset(value: number, offsets: readonly number[]): number;
38
+ /**
39
+ * Scrim opacity (1 = fully visible, 0 = hidden) for a drag/settle `offset`.
40
+ * The scrim stays full while the sheet is at or above its second-shortest
41
+ * detent, then fades to 0 as it collapses onto the shortest ("peek") detent —
42
+ * a glance state that reveals the content behind — and stays hidden below it.
43
+ * A single-detent sheet has no peek, so it instead fades across the dismiss
44
+ * overshoot toward `dismissOffset`.
45
+ */
46
+ export declare function scrimOpacityForOffset(offset: number, offsets: readonly number[], dismissOffset: number): number;
47
+ /**
48
+ * Settle target for a released drag. Restricts candidates to the drag
49
+ * direction so a committed drag never snaps *back past* where it started
50
+ * (a down-drag settles at/below the start, an up-drag at/above), then picks
51
+ * the nearest remaining detent. `dir`: 1 = dragged down, -1 = up, 0 = neither.
52
+ */
53
+ export declare function resolveSettleOffset(value: number, offsets: readonly number[], dir: number, baseOffset: number): number;
54
+ //# sourceMappingURL=snapOffsets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapOffsets.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/snapOffsets.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,4EAA4E;AAC5E,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,UAAU,EAAE,MAAM,GACjB,MAAM,EAAE,CAEV;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,OAAO,GAAE,MAAwB,GAChC,MAAM,EAAE,CAcV;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,MAAM,CAKR;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,aAAa,EAAE,MAAM,GACpB,MAAM,CAWR;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,GACjB,MAAM,CAcR"}
@@ -0,0 +1,101 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file snapOffsets.ts
5
+ * @input Pure geometry helpers — no React, no DOM.
6
+ * @output Converts snap points to translateY offsets for the sheet surface.
7
+ * @position Internal to BottomSheet; consumed by useSheetGestures, tested by
8
+ * snapOffsets.test.ts.
9
+ *
10
+ * The sheet is rendered at its full (fully-open) height and *translated down*
11
+ * to express shorter detents — translate is GPU-composited, so this is cheaper
12
+ * than animating layout height. A detent is therefore a translateY offset in
13
+ * px from the fully-open position (0 = fully open, larger = more collapsed).
14
+ * These helpers turn snap points into that offset list; they are pure so the
15
+ * geometry can be unit-tested without a DOM.
16
+ */
17
+
18
+ /**
19
+ * Detents whose resting offsets land within this many px of each other are
20
+ * treated as the same stop (the taller one wins). Stops the sheet from having
21
+ * two near-identical rest positions — e.g. when a content-hugging height sits
22
+ * a hair away from a fractional snap point.
23
+ */
24
+ export const DETENT_DEDUP_PX = 48;
25
+
26
+ /** Resolve snap points given as viewport fractions (0, 1] to px heights. */
27
+ export function snapFractionsToHeights(fractions, viewportPx) {
28
+ return fractions.filter(f => f > 0 && f <= 1).map(f => f * viewportPx);
29
+ }
30
+
31
+ /**
32
+ * Given the sheet's full height (px, as rendered fully open) and a set of
33
+ * candidate detent *visible heights* (px), return the resting translateY
34
+ * offsets from fully-open, ascending and de-duplicated.
35
+ *
36
+ * - `0` (fully open) is always the first detent.
37
+ * - Only heights strictly shorter than the sheet become collapsed detents; a
38
+ * height >= the sheet can't be shown by translating down, so it's dropped.
39
+ * - Offsets closer than `dedupPx` collapse to the smaller (taller) offset, so
40
+ * near-identical detents — e.g. a hug height ≈ a snap point — become one stop.
41
+ */
42
+ export function computeDetentOffsets(sheetHeight, detentHeights, dedupPx = DETENT_DEDUP_PX) {
43
+ const collapsed = detentHeights.filter(h => h > 0 && h < sheetHeight).map(h => sheetHeight - h);
44
+ const ascending = [0, ...collapsed].sort((a, b) => a - b);
45
+ const deduped = [];
46
+ for (const offset of ascending) {
47
+ const last = deduped[deduped.length - 1];
48
+ if (last === undefined || offset - last >= dedupPx) {
49
+ deduped.push(offset);
50
+ }
51
+ }
52
+ return deduped;
53
+ }
54
+
55
+ /** Nearest value in `offsets` to `value` (offsets must be non-empty). */
56
+ export function nearestOffset(value, offsets) {
57
+ return offsets.reduce((best, o) => Math.abs(o - value) < Math.abs(best - value) ? o : best, offsets[0]);
58
+ }
59
+
60
+ /**
61
+ * Scrim opacity (1 = fully visible, 0 = hidden) for a drag/settle `offset`.
62
+ * The scrim stays full while the sheet is at or above its second-shortest
63
+ * detent, then fades to 0 as it collapses onto the shortest ("peek") detent —
64
+ * a glance state that reveals the content behind — and stays hidden below it.
65
+ * A single-detent sheet has no peek, so it instead fades across the dismiss
66
+ * overshoot toward `dismissOffset`.
67
+ */
68
+ export function scrimOpacityForOffset(offset, offsets, dismissOffset) {
69
+ const shortest = offsets[offsets.length - 1];
70
+ const fadeStart = offsets.length >= 2 ? offsets[offsets.length - 2] : 0;
71
+ const fadeEnd = offsets.length >= 2 ? shortest : dismissOffset;
72
+ if (offset <= fadeStart) {
73
+ return 1;
74
+ }
75
+ if (offset >= fadeEnd) {
76
+ return 0;
77
+ }
78
+ return 1 - (offset - fadeStart) / (fadeEnd - fadeStart);
79
+ }
80
+
81
+ /**
82
+ * Settle target for a released drag. Restricts candidates to the drag
83
+ * direction so a committed drag never snaps *back past* where it started
84
+ * (a down-drag settles at/below the start, an up-drag at/above), then picks
85
+ * the nearest remaining detent. `dir`: 1 = dragged down, -1 = up, 0 = neither.
86
+ */
87
+ export function resolveSettleOffset(value, offsets, dir, baseOffset) {
88
+ let candidates = offsets;
89
+ if (dir > 0) {
90
+ const downward = offsets.filter(o => o >= baseOffset);
91
+ if (downward.length > 0) {
92
+ candidates = downward;
93
+ }
94
+ } else if (dir < 0) {
95
+ const upward = offsets.filter(o => o <= baseOffset);
96
+ if (upward.length > 0) {
97
+ candidates = upward;
98
+ }
99
+ }
100
+ return nearestOffset(value, candidates);
101
+ }