@seed-design/react-drawer 0.0.0-alpha-20260414104312

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/lib/index.js ADDED
@@ -0,0 +1,18 @@
1
+ import { D as DrawerBackdrop, a as DrawerCloseButton, b as DrawerContent, c as DrawerDescription, d as DrawerHandle, e as DrawerHeader, f as DrawerPositioner, g as DrawerRoot, h as DrawerTitle, i as DrawerTrigger } from './Drawer-12s-DPrL_6Um.js';
2
+ export { u as useDrawer, j as useDrawerContext } from './Drawer-12s-DPrL_6Um.js';
3
+
4
+ var Drawer_namespace = {
5
+ __proto__: null,
6
+ Backdrop: DrawerBackdrop,
7
+ CloseButton: DrawerCloseButton,
8
+ Content: DrawerContent,
9
+ Description: DrawerDescription,
10
+ Handle: DrawerHandle,
11
+ Header: DrawerHeader,
12
+ Positioner: DrawerPositioner,
13
+ Root: DrawerRoot,
14
+ Title: DrawerTitle,
15
+ Trigger: DrawerTrigger
16
+ };
17
+
18
+ export { Drawer_namespace as Drawer, DrawerBackdrop, DrawerCloseButton, DrawerContent, DrawerDescription, DrawerHandle, DrawerHeader, DrawerPositioner, DrawerRoot, DrawerTitle, DrawerTrigger };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@seed-design/react-drawer",
3
+ "version": "0.0.0-alpha-20260414104312",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/daangn/seed-design.git",
7
+ "directory": "packages/react-headless/drawer"
8
+ },
9
+ "sideEffects": false,
10
+ "type": "module",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./lib/index.d.ts",
14
+ "import": "./lib/index.js",
15
+ "require": "./lib/index.cjs"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "main": "./lib/index.cjs",
20
+ "files": [
21
+ "lib",
22
+ "src"
23
+ ],
24
+ "scripts": {
25
+ "clean": "rm -rf lib",
26
+ "build": "bunchee",
27
+ "lint:publish": "bun publint"
28
+ },
29
+ "dependencies": {
30
+ "@radix-ui/react-compose-refs": "^1.1.2",
31
+ "@radix-ui/react-focus-scope": "^1.1.4",
32
+ "@radix-ui/react-use-callback-ref": "^1.1.0",
33
+ "@seed-design/dom-utils": "1.0.0",
34
+ "@seed-design/react-dismissible-layer": "0.0.0-alpha-20260414104312",
35
+ "@seed-design/react-presence": "0.0.0-alpha-20260414104312",
36
+ "@seed-design/react-primitive": "0.0.0-alpha-20260414104312",
37
+ "@seed-design/react-use-controllable-state": "1.0.0",
38
+ "aria-hidden": "^1.2.6"
39
+ },
40
+ "devDependencies": {
41
+ "@types/react": "^19.1.6",
42
+ "react": "^19.1.0",
43
+ "react-dom": "^19.1.0"
44
+ },
45
+ "peerDependencies": {
46
+ "react": ">=18.0.0",
47
+ "react-dom": ">=18.0.0"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }
@@ -0,0 +1,22 @@
1
+ export {
2
+ DrawerBackdrop as Backdrop,
3
+ DrawerCloseButton as CloseButton,
4
+ DrawerContent as Content,
5
+ DrawerDescription as Description,
6
+ DrawerHandle as Handle,
7
+ DrawerHeader as Header,
8
+ DrawerPositioner as Positioner,
9
+ DrawerRoot as Root,
10
+ DrawerTitle as Title,
11
+ DrawerTrigger as Trigger,
12
+ type DrawerBackdropProps as BackdropProps,
13
+ type DrawerCloseButtonProps as CloseButtonProps,
14
+ type DrawerContentProps as ContentProps,
15
+ type DrawerDescriptionProps as DescriptionProps,
16
+ type DrawerHandleProps as HandleProps,
17
+ type DrawerHeaderProps as HeaderProps,
18
+ type DrawerPositionerProps as PositionerProps,
19
+ type DrawerRootProps as RootProps,
20
+ type DrawerTitleProps as TitleProps,
21
+ type DrawerTriggerProps as TriggerProps,
22
+ } from "./Drawer";
package/src/Drawer.tsx ADDED
@@ -0,0 +1,451 @@
1
+ "use client";
2
+
3
+ import { composeRefs } from "@radix-ui/react-compose-refs";
4
+ import { FocusScope } from "@radix-ui/react-focus-scope";
5
+ import { useCallbackRef } from "@radix-ui/react-use-callback-ref";
6
+ import { hideOthers } from "aria-hidden";
7
+ import { DismissibleLayer } from "@seed-design/react-dismissible-layer";
8
+ import { Presence } from "@seed-design/react-presence";
9
+ import { dataAttr, mergeProps } from "@seed-design/dom-utils";
10
+ import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
11
+ import type * as React from "react";
12
+ import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
13
+ import type { DrawerDirection } from "./types";
14
+ import { useDrawer, type UseDrawerProps } from "./useDrawer";
15
+ import { DrawerProvider, useDrawerContext } from "./useDrawerContext";
16
+
17
+ export interface DrawerRootProps extends UseDrawerProps {
18
+ children?: React.ReactNode;
19
+ }
20
+
21
+ export const DrawerRoot = (props: DrawerRootProps) => {
22
+ const api = useDrawer(props);
23
+ return <DrawerProvider value={api}>{props.children}</DrawerProvider>;
24
+ };
25
+
26
+ export interface DrawerTriggerProps
27
+ extends PrimitiveProps,
28
+ React.ButtonHTMLAttributes<HTMLButtonElement> {}
29
+
30
+ export const DrawerTrigger = forwardRef<HTMLButtonElement, DrawerTriggerProps>((props, ref) => {
31
+ const api = useDrawerContext();
32
+ return <Primitive.button ref={ref} {...mergeProps(api.triggerProps, props)} />;
33
+ });
34
+ DrawerTrigger.displayName = "DrawerTrigger";
35
+
36
+ export interface DrawerPositionerProps
37
+ extends PrimitiveProps,
38
+ React.HTMLAttributes<HTMLDivElement> {}
39
+
40
+ export const DrawerPositioner = forwardRef<HTMLDivElement, DrawerPositionerProps>((props, ref) => {
41
+ const api = useDrawerContext();
42
+ return <Primitive.div ref={ref} {...mergeProps(api.positionerProps, props)} />;
43
+ });
44
+ DrawerPositioner.displayName = "DrawerPositioner";
45
+
46
+ export interface DrawerBackdropProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
47
+
48
+ export const DrawerBackdrop = forwardRef<HTMLDivElement, DrawerBackdropProps>((props, ref) => {
49
+ const {
50
+ overlayRef,
51
+ onRelease,
52
+ modal,
53
+ snapPoints,
54
+ isOpen,
55
+ shouldFade,
56
+ shouldOverlayAnimate,
57
+ hasAnimationDone,
58
+ lazyMount,
59
+ unmountOnExit,
60
+ } = useDrawerContext();
61
+ const composedRef = composeRefs(ref, overlayRef);
62
+ const hasSnapPoints = snapPoints && snapPoints.length > 0;
63
+ const onMouseUp = useCallbackRef((event: React.PointerEvent<HTMLDivElement>) => onRelease(event));
64
+
65
+ if (!modal) {
66
+ return null;
67
+ }
68
+
69
+ return (
70
+ <Presence present={isOpen} unmountOnExit={unmountOnExit} lazyMount={lazyMount}>
71
+ <Primitive.div
72
+ ref={composedRef}
73
+ onMouseUp={onMouseUp}
74
+ data-snap-points={isOpen && hasSnapPoints ? "true" : "false"}
75
+ data-snap-points-overlay={isOpen && shouldFade ? "true" : "false"}
76
+ data-should-overlay-animate={shouldOverlayAnimate ? "true" : "false"}
77
+ data-open={dataAttr(isOpen)}
78
+ data-animation-done={hasAnimationDone ? "true" : "false"}
79
+ {...props}
80
+ />
81
+ </Presence>
82
+ );
83
+ });
84
+ DrawerBackdrop.displayName = "DrawerBackdrop";
85
+
86
+ export interface DrawerContentProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
87
+
88
+ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((props, ref) => {
89
+ const { style, ...restProps } = props;
90
+
91
+ const {
92
+ drawerRef,
93
+ onPress,
94
+ onRelease,
95
+ onDrag,
96
+ keyboardIsOpen,
97
+ snapPointsOffset,
98
+ activeSnapPointIndex,
99
+ modal,
100
+ isOpen,
101
+ direction,
102
+ snapPoints,
103
+ container,
104
+ handleOnly,
105
+ autoFocus,
106
+ closeDrawer,
107
+ closeOnInteractOutside,
108
+ closeOnEscape,
109
+ dismissible,
110
+ hasAnimationDone,
111
+ titleId,
112
+ descriptionId,
113
+ lazyMount,
114
+ unmountOnExit,
115
+ } = useDrawerContext();
116
+
117
+ const [contentNode, setContentNode] = useState<HTMLDivElement | null>(null);
118
+ const contentRef = useCallback((el: HTMLDivElement | null) => setContentNode(el), []);
119
+
120
+ // aria-hide everything except the content when modal
121
+ useEffect(() => {
122
+ if (!isOpen || !modal || !contentNode) return;
123
+ return hideOthers(contentNode);
124
+ }, [isOpen, modal, contentNode]);
125
+
126
+ // Needed to use transition instead of animations
127
+ const [delayedSnapPoints, setDelayedSnapPoints] = useState(false);
128
+ const pointerStartRef = useRef<{ x: number; y: number } | null>(null);
129
+ const lastKnownPointerEventRef = useRef<React.PointerEvent<HTMLDivElement> | null>(null);
130
+ const wasBeyondThePointRef = useRef(false);
131
+ const hasSnapPoints = snapPoints && snapPoints.length > 0;
132
+
133
+ const isDeltaInDirection = (
134
+ delta: { x: number; y: number },
135
+ dir: DrawerDirection,
136
+ threshold = 0,
137
+ ) => {
138
+ if (wasBeyondThePointRef.current) return true;
139
+
140
+ const deltaY = Math.abs(delta.y);
141
+ const deltaX = Math.abs(delta.x);
142
+ const isDeltaX = deltaX > deltaY;
143
+ const dFactor = ["bottom", "right"].includes(dir) ? 1 : -1;
144
+
145
+ if (dir === "left" || dir === "right") {
146
+ const isReverseDirection = delta.x * dFactor < 0;
147
+ if (!isReverseDirection && deltaX >= 0 && deltaX <= threshold) {
148
+ return isDeltaX;
149
+ }
150
+ } else {
151
+ const isReverseDirection = delta.y * dFactor < 0;
152
+ if (!isReverseDirection && deltaY >= 0 && deltaY <= threshold) {
153
+ return !isDeltaX;
154
+ }
155
+ }
156
+
157
+ wasBeyondThePointRef.current = true;
158
+ return true;
159
+ };
160
+
161
+ useEffect(() => {
162
+ if (hasSnapPoints) {
163
+ window.requestAnimationFrame(() => {
164
+ setDelayedSnapPoints(true);
165
+ });
166
+ }
167
+ }, []);
168
+
169
+ function handleOnPointerUp(event: React.PointerEvent<HTMLDivElement> | null) {
170
+ pointerStartRef.current = null;
171
+ wasBeyondThePointRef.current = false;
172
+ onRelease(event);
173
+ }
174
+
175
+ return (
176
+ <Presence present={isOpen} unmountOnExit={unmountOnExit} lazyMount={lazyMount}>
177
+ {/* DismissibleLayer must wrap FocusScope, not the other way around.
178
+ FocusScope asChild uses Slot to forward tabIndex/onKeyDown/ref to the
179
+ DOM element; if DismissibleLayer sits between them, those props are
180
+ swallowed by DismissibleLayer's own destructuring and never reach the DOM. */}
181
+ <DismissibleLayer
182
+ enabled={isOpen}
183
+ blockPointerEvents={modal}
184
+ onEscapeKeyDown={(e) => {
185
+ if (e.defaultPrevented) return;
186
+ if (!dismissible || !closeOnEscape) return;
187
+ closeDrawer(false, { reason: "escapeKeyDown", event: e });
188
+ }}
189
+ onPressOutside={(e) => {
190
+ if (e.defaultPrevented) return;
191
+ if (!modal) return;
192
+ if (keyboardIsOpen.current) keyboardIsOpen.current = false;
193
+
194
+ if (!dismissible || !closeOnInteractOutside) return;
195
+ closeDrawer(false, { reason: "interactOutside", event: e });
196
+ }}
197
+ onFocusOutside={() => {
198
+ // Focus trapping is handled by FocusScope — nothing to do here.
199
+ // The old Radix architecture needed e.preventDefault() here (PR #1187)
200
+ // to prevent cascade closes, but the new DismissibleLayer handles
201
+ // that via onCascadeDismiss instead.
202
+ }}
203
+ onCascadeDismiss={({ dismissedParent }) => {
204
+ closeDrawer(false, { reason: "cascadeDismiss", dismissedParent });
205
+ }}
206
+ >
207
+ <FocusScope
208
+ asChild
209
+ loop={modal}
210
+ trapped={modal && isOpen}
211
+ onMountAutoFocus={(e) => {
212
+ // prevent FocusScope's default autoFocus behavior
213
+ e.preventDefault();
214
+
215
+ // when autoFocus is true, FocusScope sets the focus to the first tabbable element; otherwise content;
216
+ // the desired behavior is to set the focus to the content regardless of whether there are tabbable elements or not when true]
217
+ if (autoFocus) {
218
+ drawerRef.current?.focus();
219
+ }
220
+
221
+ // later, we can do something like:
222
+ // when trigger has clicked using keyboard, focus the first tabbable element;
223
+ // when trigger has clicked using mouse, focus the content
224
+ // -> matches Menu behavior
225
+ }}
226
+ >
227
+ <Primitive.div
228
+ role="dialog"
229
+ aria-modal={modal}
230
+ aria-labelledby={titleId}
231
+ aria-describedby={descriptionId}
232
+ data-delayed-snap-points={delayedSnapPoints ? "true" : "false"}
233
+ data-drawer-direction={direction}
234
+ data-open={dataAttr(isOpen)}
235
+ data-animation-done={hasAnimationDone ? "true" : "false"}
236
+ data-drawer=""
237
+ data-snap-points={isOpen && hasSnapPoints ? "true" : "false"}
238
+ data-custom-container={container ? "true" : "false"}
239
+ {...restProps}
240
+ ref={composeRefs(ref, drawerRef, contentRef)}
241
+ style={{
242
+ ...(snapPointsOffset && snapPointsOffset.length > 0
243
+ ? ({
244
+ "--snap-point-height": `${snapPointsOffset[activeSnapPointIndex ?? 0]!}px`,
245
+ ...style,
246
+ } as React.CSSProperties)
247
+ : style),
248
+ ...(!modal && { pointerEvents: "auto" }),
249
+ }}
250
+ onPointerDown={(event) => {
251
+ if (handleOnly) return;
252
+ restProps.onPointerDown?.(event);
253
+ pointerStartRef.current = { x: event.pageX, y: event.pageY };
254
+ onPress(event);
255
+ }}
256
+ onPointerMove={(event) => {
257
+ lastKnownPointerEventRef.current = event;
258
+ if (handleOnly) return;
259
+ restProps.onPointerMove?.(event);
260
+ if (!pointerStartRef.current) return;
261
+ const yPosition = event.pageY - pointerStartRef.current.y;
262
+ const xPosition = event.pageX - pointerStartRef.current.x;
263
+
264
+ const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
265
+ const delta = { x: xPosition, y: yPosition };
266
+
267
+ const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
268
+ if (isAllowedToSwipe) onDrag(event);
269
+ else if (
270
+ Math.abs(xPosition) > swipeStartThreshold ||
271
+ Math.abs(yPosition) > swipeStartThreshold
272
+ ) {
273
+ pointerStartRef.current = null;
274
+ }
275
+ }}
276
+ onPointerUp={(event) => {
277
+ restProps.onPointerUp?.(event);
278
+ pointerStartRef.current = null;
279
+ wasBeyondThePointRef.current = false;
280
+ onRelease(event);
281
+ }}
282
+ onPointerOut={(event) => {
283
+ restProps.onPointerOut?.(event);
284
+ handleOnPointerUp(lastKnownPointerEventRef.current);
285
+ }}
286
+ onContextMenu={(event) => {
287
+ restProps.onContextMenu?.(event);
288
+ if (lastKnownPointerEventRef.current) {
289
+ handleOnPointerUp(lastKnownPointerEventRef.current);
290
+ }
291
+ }}
292
+ />
293
+ </FocusScope>
294
+ </DismissibleLayer>
295
+ </Presence>
296
+ );
297
+ });
298
+ DrawerContent.displayName = "DrawerContent";
299
+
300
+ export interface DrawerTitleProps
301
+ extends PrimitiveProps,
302
+ React.HTMLAttributes<HTMLHeadingElement> {}
303
+
304
+ export const DrawerTitle = forwardRef<HTMLHeadingElement, DrawerTitleProps>((props, ref) => {
305
+ const api = useDrawerContext();
306
+ return <Primitive.h2 ref={ref} {...mergeProps(api.titleProps, props)} />;
307
+ });
308
+ DrawerTitle.displayName = "DrawerTitle";
309
+
310
+ export interface DrawerDescriptionProps
311
+ extends PrimitiveProps,
312
+ React.HTMLAttributes<HTMLParagraphElement> {}
313
+
314
+ export const DrawerDescription = forwardRef<HTMLParagraphElement, DrawerDescriptionProps>(
315
+ (props, ref) => {
316
+ const api = useDrawerContext();
317
+ return <Primitive.p ref={ref} {...mergeProps(api.descriptionProps, props)} />;
318
+ },
319
+ );
320
+ DrawerDescription.displayName = "DrawerDescription";
321
+
322
+ export interface DrawerHeaderProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
323
+
324
+ export const DrawerHeader = forwardRef<HTMLDivElement, DrawerHeaderProps>((props, ref) => {
325
+ const api = useDrawerContext();
326
+ return <Primitive.div ref={ref} {...mergeProps(api.headerProps, props)} />;
327
+ });
328
+ DrawerHeader.displayName = "DrawerHeader";
329
+
330
+ export interface DrawerCloseButtonProps
331
+ extends PrimitiveProps,
332
+ React.ButtonHTMLAttributes<HTMLButtonElement> {}
333
+
334
+ export const DrawerCloseButton = forwardRef<HTMLButtonElement, DrawerCloseButtonProps>(
335
+ (props, ref) => {
336
+ const api = useDrawerContext();
337
+ return (
338
+ <Primitive.button
339
+ ref={composeRefs(ref, api.closeButtonRef)}
340
+ {...mergeProps(api.closeButtonProps, props)}
341
+ />
342
+ );
343
+ },
344
+ );
345
+
346
+ export interface DrawerHandleProps extends React.HTMLAttributes<HTMLDivElement> {
347
+ preventCycle?: boolean;
348
+ }
349
+
350
+ const LONG_HANDLE_PRESS_TIMEOUT = 250;
351
+ const DOUBLE_TAP_TIMEOUT = 120;
352
+
353
+ export const DrawerHandle = forwardRef<HTMLDivElement, DrawerHandleProps>((props, ref) => {
354
+ const { preventCycle = false, children, ...rest } = props;
355
+ const {
356
+ closeDrawer,
357
+ isDragging,
358
+ snapPoints,
359
+ activeSnapPoint,
360
+ setActiveSnapPoint,
361
+ dismissible,
362
+ handleOnly,
363
+ isOpen,
364
+ onPress,
365
+ onDrag,
366
+ onRelease,
367
+ } = useDrawerContext();
368
+
369
+ const closeTimeoutIdRef = useRef<number | null>(null);
370
+ const shouldCancelInteractionRef = useRef(false);
371
+
372
+ function handleStartCycle(event: React.MouseEvent<HTMLDivElement>) {
373
+ // Stop if this is the second click of a double click
374
+ if (shouldCancelInteractionRef.current) {
375
+ handleCancelInteraction();
376
+ return;
377
+ }
378
+ window.setTimeout(() => {
379
+ handleCycleSnapPoints(event);
380
+ }, DOUBLE_TAP_TIMEOUT);
381
+ }
382
+
383
+ function handleCycleSnapPoints(event: React.MouseEvent<HTMLDivElement>) {
384
+ // Prevent accidental taps while resizing drawer
385
+ if (isDragging || preventCycle || shouldCancelInteractionRef.current) {
386
+ handleCancelInteraction();
387
+ return;
388
+ }
389
+ // Make sure to clear the timeout id if the user releases the handle before the cancel timeout
390
+ handleCancelInteraction();
391
+
392
+ if (!snapPoints || snapPoints.length === 0) {
393
+ if (!dismissible) {
394
+ closeDrawer(false, { reason: "handleClickOnLastSnapPoint", event: event.nativeEvent });
395
+ }
396
+ return;
397
+ }
398
+
399
+ const isLastSnapPoint = activeSnapPoint === snapPoints[snapPoints.length - 1];
400
+
401
+ if (isLastSnapPoint && dismissible) {
402
+ closeDrawer(false, { reason: "handleClickOnLastSnapPoint", event: event.nativeEvent });
403
+ return;
404
+ }
405
+
406
+ const currentSnapIndex = snapPoints.findIndex((point) => point === activeSnapPoint);
407
+ if (currentSnapIndex === -1) return; // activeSnapPoint not found in snapPoints
408
+ const nextSnapPoint = snapPoints[currentSnapIndex + 1];
409
+ setActiveSnapPoint(nextSnapPoint);
410
+ }
411
+
412
+ function handleStartInteraction() {
413
+ closeTimeoutIdRef.current = window.setTimeout(() => {
414
+ // Cancel click interaction on a long press
415
+ shouldCancelInteractionRef.current = true;
416
+ }, LONG_HANDLE_PRESS_TIMEOUT);
417
+ }
418
+
419
+ function handleCancelInteraction() {
420
+ if (closeTimeoutIdRef.current) {
421
+ window.clearTimeout(closeTimeoutIdRef.current);
422
+ }
423
+ shouldCancelInteractionRef.current = false;
424
+ }
425
+
426
+ return (
427
+ <Primitive.div
428
+ ref={ref}
429
+ onClick={handleStartCycle}
430
+ onPointerCancel={handleCancelInteraction}
431
+ onPointerDown={(e) => {
432
+ if (handleOnly) onPress(e);
433
+ handleStartInteraction();
434
+ }}
435
+ onPointerMove={(e) => {
436
+ if (handleOnly) onDrag(e);
437
+ }}
438
+ onPointerUp={(e) => {
439
+ if (handleOnly) onRelease(e);
440
+ handleCancelInteraction();
441
+ }}
442
+ data-drawer-visible={isOpen ? "true" : "false"}
443
+ data-handle=""
444
+ aria-hidden="true"
445
+ {...rest}
446
+ >
447
+ {children}
448
+ </Primitive.div>
449
+ );
450
+ });
451
+ DrawerHandle.displayName = "DrawerHandle";
package/src/browser.ts ADDED
@@ -0,0 +1,44 @@
1
+ export function isMobileFirefox(): boolean | undefined {
2
+ if (typeof window === "undefined" || typeof navigator === "undefined") return false;
3
+ return (
4
+ (/Firefox/.test(navigator.userAgent) && /Mobile/.test(navigator.userAgent)) ||
5
+ /FxiOS/.test(navigator.userAgent)
6
+ );
7
+ }
8
+
9
+ export function isMac(): boolean | undefined {
10
+ return testPlatform(/^Mac/);
11
+ }
12
+
13
+ export function isIPhone(): boolean | undefined {
14
+ return testPlatform(/^iPhone/);
15
+ }
16
+
17
+ export function isSafari(): boolean | undefined {
18
+ return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
19
+ }
20
+
21
+ export function isIPad(): boolean | undefined {
22
+ return (
23
+ testPlatform(/^iPad/) ||
24
+ // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
25
+ (isMac() && navigator.maxTouchPoints > 1)
26
+ );
27
+ }
28
+
29
+ export function isIOS(): boolean | undefined {
30
+ return isIPhone() || isIPad();
31
+ }
32
+
33
+ export function isAndroid(): boolean | undefined {
34
+ if (typeof window === "undefined" || typeof navigator === "undefined") return false;
35
+
36
+ return /Android/.test(navigator.userAgent);
37
+ }
38
+
39
+ // TODO: use userAgent instead?
40
+ export function testPlatform(re: RegExp): boolean | undefined {
41
+ return typeof window !== "undefined" && window.navigator != null
42
+ ? re.test(window.navigator.platform)
43
+ : undefined;
44
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * TODO: move to recipe
3
+ */
4
+ export const TRANSITIONS = {
5
+ ENTER_DURATION: 0.3, // $duration.d6
6
+ EXIT_DURATION: 0.2, // $duration.d4
7
+ OVERLAY_ENTER_TIMING_FUNCTION: "cubic-bezier(0, 0, 0.15, 1)", // $timing-function.enter
8
+ OVERLAY_EXIT_TIMING_FUNCTION: "cubic-bezier(0.35, 0, 1, 1)", // $timing-function.exit
9
+ CONTENT_ENTER_TIMING_FUNCTION: "cubic-bezier(0.03, 0.4, 0.1, 1)", // $timing-function.enter-expressive
10
+ CONTENT_EXIT_TIMING_FUNCTION: "cubic-bezier(0.35, 0, 1, 1)", // $timing-function.exit
11
+ };
12
+
13
+ export const VELOCITY_THRESHOLD = 0.4;
14
+
15
+ export const CLOSE_THRESHOLD = 0.25;
16
+
17
+ export const SCROLL_LOCK_TIMEOUT = 100;
18
+
19
+ export const WINDOW_TOP_OFFSET = 26;
20
+
21
+ export const DRAG_CLASS = "seed-dragging";