@seed-design/stackflow 0.0.1-alpha-20250210081704
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/components/AppBar/AppBar.d.ts +29 -0
- package/lib/components/AppBar/AppBar.namespace.d.ts +2 -0
- package/lib/components/AppBar/index.d.ts +3 -0
- package/lib/components/AppScreen/AppScreen.d.ts +18 -0
- package/lib/components/AppScreen/AppScreen.namespace.d.ts +2 -0
- package/lib/components/AppScreen/index.d.ts +3 -0
- package/lib/components/index.d.ts +2 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +786 -0
- package/lib/index.js.map +7 -0
- package/lib/index.mjs +759 -0
- package/lib/index.mjs.map +7 -0
- package/lib/plugin.d.ts +5 -0
- package/lib/primitive/AppBar/AppBar.d.ts +14 -0
- package/lib/primitive/AppBar/AppBar.namespace.d.ts +1 -0
- package/lib/primitive/AppBar/index.d.ts +2 -0
- package/lib/primitive/AppBar/useAppBar.d.ts +279 -0
- package/lib/primitive/AppBar/useAppBarContext.d.ts +7 -0
- package/lib/primitive/AppScreen/AppScreen.d.ts +14 -0
- package/lib/primitive/AppScreen/AppScreen.namespace.d.ts +1 -0
- package/lib/primitive/AppScreen/index.d.ts +3 -0
- package/lib/primitive/AppScreen/useAppScreen.d.ts +1080 -0
- package/lib/primitive/AppScreen/useAppScreenContext.d.ts +7 -0
- package/lib/primitive/GlobalInteraction/GlobalInteraction.d.ts +4 -0
- package/lib/primitive/GlobalInteraction/index.d.ts +2 -0
- package/lib/primitive/GlobalInteraction/useGlobalInteraction.d.ts +53 -0
- package/lib/primitive/GlobalInteraction/useGlobalInteractionContext.d.ts +7 -0
- package/lib/primitive/GlobalInteraction/useSwipeBack.d.ts +8 -0
- package/lib/primitive/index.d.ts +3 -0
- package/lib/primitive/private/useElementOffset.d.ts +4 -0
- package/lib/primitive/private/useTopActivity.d.ts +4 -0
- package/lib/utils/createStyleContext.d.ts +23 -0
- package/lib/utils/createWithStateProps.d.ts +9 -0
- package/package.json +56 -0
- package/src/components/AppBar/AppBar.namespace.ts +19 -0
- package/src/components/AppBar/AppBar.tsx +109 -0
- package/src/components/AppBar/index.tsx +21 -0
- package/src/components/AppScreen/AppScreen.namespace.ts +13 -0
- package/src/components/AppScreen/AppScreen.tsx +65 -0
- package/src/components/AppScreen/index.tsx +15 -0
- package/src/components/index.tsx +2 -0
- package/src/index.ts +4 -0
- package/src/plugin.tsx +20 -0
- package/src/primitive/AppBar/AppBar.namespace.ts +10 -0
- package/src/primitive/AppBar/AppBar.tsx +52 -0
- package/src/primitive/AppBar/index.ts +12 -0
- package/src/primitive/AppBar/useAppBar.tsx +42 -0
- package/src/primitive/AppBar/useAppBarContext.ts +19 -0
- package/src/primitive/AppScreen/AppScreen.namespace.ts +10 -0
- package/src/primitive/AppScreen/AppScreen.tsx +66 -0
- package/src/primitive/AppScreen/index.ts +13 -0
- package/src/primitive/AppScreen/useAppScreen.tsx +68 -0
- package/src/primitive/AppScreen/useAppScreenContext.ts +19 -0
- package/src/primitive/GlobalInteraction/GlobalInteraction.tsx +25 -0
- package/src/primitive/GlobalInteraction/index.ts +6 -0
- package/src/primitive/GlobalInteraction/useGlobalInteraction.ts +178 -0
- package/src/primitive/GlobalInteraction/useGlobalInteractionContext.tsx +21 -0
- package/src/primitive/GlobalInteraction/useSwipeBack.ts +56 -0
- package/src/primitive/index.tsx +3 -0
- package/src/primitive/private/useElementOffset.ts +37 -0
- package/src/primitive/private/useTopActivity.ts +31 -0
- package/src/utils/createStyleContext.tsx +125 -0
- package/src/utils/createWithStateProps.tsx +28 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { mergeProps } from "@seed-design/dom-utils";
|
|
2
|
+
import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import { useAppScreen, type UseAppScreenProps } from "./useAppScreen";
|
|
5
|
+
import { AppScreenProvider, useAppScreenContext } from "./useAppScreenContext";
|
|
6
|
+
|
|
7
|
+
export interface AppScreenRootProps
|
|
8
|
+
extends PrimitiveProps,
|
|
9
|
+
UseAppScreenProps,
|
|
10
|
+
React.HTMLAttributes<HTMLDivElement> {}
|
|
11
|
+
|
|
12
|
+
export const AppScreenRoot = forwardRef<HTMLDivElement, AppScreenRootProps>((props, ref) => {
|
|
13
|
+
const {
|
|
14
|
+
swipeBackDisplacementRatioThreshold,
|
|
15
|
+
swipeBackVelocityThreshold,
|
|
16
|
+
onSwipeBackEnd,
|
|
17
|
+
onSwipeBackMove,
|
|
18
|
+
onSwipeBackStart,
|
|
19
|
+
...otherProps
|
|
20
|
+
} = props;
|
|
21
|
+
const api = useAppScreen({
|
|
22
|
+
swipeBackDisplacementRatioThreshold,
|
|
23
|
+
swipeBackVelocityThreshold,
|
|
24
|
+
onSwipeBackEnd,
|
|
25
|
+
onSwipeBackMove,
|
|
26
|
+
onSwipeBackStart,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<AppScreenProvider value={api}>
|
|
31
|
+
<Primitive.div
|
|
32
|
+
ref={ref}
|
|
33
|
+
data-stackflow-component-name="AppScreen"
|
|
34
|
+
{...mergeProps(api.activityProps, otherProps)}
|
|
35
|
+
/>
|
|
36
|
+
</AppScreenProvider>
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
AppScreenRoot.displayName = "AppScreenRoot";
|
|
40
|
+
|
|
41
|
+
export interface AppScreenDimProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
|
|
42
|
+
|
|
43
|
+
export const AppScreenDim = forwardRef<HTMLDivElement, AppScreenDimProps>((props, ref) => {
|
|
44
|
+
const { dimProps } = useAppScreenContext();
|
|
45
|
+
|
|
46
|
+
return <Primitive.div ref={ref} {...mergeProps(dimProps, props)} />;
|
|
47
|
+
});
|
|
48
|
+
AppScreenDim.displayName = "AppScreenDim";
|
|
49
|
+
|
|
50
|
+
export interface AppScreenEdgeProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
|
|
51
|
+
|
|
52
|
+
export const AppScreenEdge = forwardRef<HTMLDivElement, AppScreenEdgeProps>((props, ref) => {
|
|
53
|
+
const { edgeProps } = useAppScreenContext();
|
|
54
|
+
|
|
55
|
+
return <Primitive.div ref={ref} {...mergeProps(edgeProps, props)} />;
|
|
56
|
+
});
|
|
57
|
+
AppScreenEdge.displayName = "AppScreenEdge";
|
|
58
|
+
|
|
59
|
+
export interface AppScreenLayerProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
|
|
60
|
+
|
|
61
|
+
export const AppScreenLayer = forwardRef<HTMLDivElement, AppScreenLayerProps>((props, ref) => {
|
|
62
|
+
const { layerProps } = useAppScreenContext();
|
|
63
|
+
|
|
64
|
+
return <Primitive.div ref={ref} {...mergeProps(layerProps, props)} />;
|
|
65
|
+
});
|
|
66
|
+
AppScreenLayer.displayName = "AppScreenLayer";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
AppScreenDim,
|
|
3
|
+
AppScreenEdge,
|
|
4
|
+
AppScreenLayer,
|
|
5
|
+
AppScreenRoot,
|
|
6
|
+
type AppScreenDimProps,
|
|
7
|
+
type AppScreenEdgeProps,
|
|
8
|
+
type AppScreenLayerProps,
|
|
9
|
+
type AppScreenRootProps,
|
|
10
|
+
} from "./AppScreen";
|
|
11
|
+
|
|
12
|
+
export * as AppScreen from "./AppScreen.namespace";
|
|
13
|
+
export { useAppScreenContext, type UseAppScreenContext } from "./useAppScreenContext";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { dataAttr, elementProps } from "@seed-design/dom-utils";
|
|
2
|
+
import { useNullableActivity, useZIndexBase } from "@stackflow/react-ui-core";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { type UseSwipeBackProps, useSwipeBack } from "../GlobalInteraction/useSwipeBack";
|
|
5
|
+
|
|
6
|
+
export interface UseAppScreenProps extends UseSwipeBackProps {}
|
|
7
|
+
|
|
8
|
+
export type UseAppScreenReturn = ReturnType<typeof useAppScreen>;
|
|
9
|
+
|
|
10
|
+
export function useAppScreen(props: UseAppScreenProps) {
|
|
11
|
+
const activity = useNullableActivity();
|
|
12
|
+
|
|
13
|
+
const transitionState = activity?.transitionState ?? "enter-done";
|
|
14
|
+
|
|
15
|
+
const { activityProps, layerProps, edgeProps } = useSwipeBack(props);
|
|
16
|
+
|
|
17
|
+
const zIndexBase = useZIndexBase();
|
|
18
|
+
const zIndexStyle = useMemo(
|
|
19
|
+
() =>
|
|
20
|
+
({
|
|
21
|
+
"--z-index-base": zIndexBase.toString(),
|
|
22
|
+
}) as React.CSSProperties,
|
|
23
|
+
[zIndexBase],
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const stateProps = useMemo(
|
|
27
|
+
() => ({
|
|
28
|
+
"data-activity-is-top": dataAttr(activity?.isTop),
|
|
29
|
+
"data-activity-is-active": dataAttr(activity?.isActive),
|
|
30
|
+
"data-transition-state": transitionState,
|
|
31
|
+
}),
|
|
32
|
+
[transitionState, activity?.isActive, activity?.isTop],
|
|
33
|
+
) as React.HTMLAttributes<HTMLElement>;
|
|
34
|
+
|
|
35
|
+
return useMemo(
|
|
36
|
+
() => ({
|
|
37
|
+
activity,
|
|
38
|
+
stateProps,
|
|
39
|
+
activityProps: elementProps({
|
|
40
|
+
"data-part": "activity",
|
|
41
|
+
"data-activity-type": "full-screen",
|
|
42
|
+
...activityProps,
|
|
43
|
+
...stateProps,
|
|
44
|
+
"data-activity-id": activity?.id,
|
|
45
|
+
style: zIndexStyle,
|
|
46
|
+
// FIXME: @stackflow/react should prevent activity.id hydration mismatch; this is temporal fix.
|
|
47
|
+
suppressHydrationWarning: true,
|
|
48
|
+
}),
|
|
49
|
+
dimProps: elementProps({
|
|
50
|
+
"data-part": "dim",
|
|
51
|
+
...stateProps,
|
|
52
|
+
}),
|
|
53
|
+
layerProps: elementProps({
|
|
54
|
+
"data-part": "layer",
|
|
55
|
+
...stateProps,
|
|
56
|
+
...layerProps,
|
|
57
|
+
}),
|
|
58
|
+
edgeProps: elementProps({
|
|
59
|
+
"data-part": "edge",
|
|
60
|
+
"aria-hidden": true,
|
|
61
|
+
tabIndex: -1,
|
|
62
|
+
...edgeProps,
|
|
63
|
+
...stateProps,
|
|
64
|
+
}),
|
|
65
|
+
}),
|
|
66
|
+
[activity, zIndexStyle, stateProps, activityProps, edgeProps, layerProps],
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { UseAppScreenReturn } from "./useAppScreen";
|
|
3
|
+
|
|
4
|
+
export interface UseAppScreenContext extends UseAppScreenReturn {}
|
|
5
|
+
|
|
6
|
+
const AppScreenContext = createContext<UseAppScreenContext | null>(null);
|
|
7
|
+
|
|
8
|
+
export const AppScreenProvider = AppScreenContext.Provider;
|
|
9
|
+
|
|
10
|
+
export function useAppScreenContext<T extends boolean | undefined = true>({
|
|
11
|
+
strict = true,
|
|
12
|
+
}: { strict?: T } = {}): T extends false ? UseAppScreenContext | null : UseAppScreenContext {
|
|
13
|
+
const context = useContext(AppScreenContext);
|
|
14
|
+
if (!context && strict) {
|
|
15
|
+
throw new Error("useAppScreenContext must be used within a AppScreen");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return context as UseAppScreenContext;
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { composeRefs } from "@radix-ui/react-compose-refs";
|
|
2
|
+
import { mergeProps } from "@seed-design/dom-utils";
|
|
3
|
+
import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import { useGlobalInteraction } from "./useGlobalInteraction";
|
|
6
|
+
import { GlobalInteractionProvider } from "./useGlobalInteractionContext";
|
|
7
|
+
|
|
8
|
+
export interface GlobalInteractionProps
|
|
9
|
+
extends PrimitiveProps,
|
|
10
|
+
React.HTMLAttributes<HTMLDivElement> {}
|
|
11
|
+
|
|
12
|
+
export const GlobalInteraction = forwardRef<HTMLDivElement, GlobalInteractionProps>(
|
|
13
|
+
(props, ref) => {
|
|
14
|
+
const api = useGlobalInteraction();
|
|
15
|
+
return (
|
|
16
|
+
<GlobalInteractionProvider value={api}>
|
|
17
|
+
<Primitive.div
|
|
18
|
+
ref={composeRefs(api.stackRef, ref)}
|
|
19
|
+
{...mergeProps(api.stackProps, props)}
|
|
20
|
+
/>
|
|
21
|
+
</GlobalInteractionProvider>
|
|
22
|
+
);
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
GlobalInteraction.displayName = "GlobalInteraction";
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { useCallbackRef } from "@radix-ui/react-use-callback-ref";
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useTopActivity } from "../private/useTopActivity";
|
|
4
|
+
|
|
5
|
+
export type SwipeBackState = "idle" | "swiping" | "transitioning";
|
|
6
|
+
|
|
7
|
+
export type UseGlobalInteractionReturn = ReturnType<typeof useGlobalInteraction>;
|
|
8
|
+
|
|
9
|
+
export interface SwipeBackContext {
|
|
10
|
+
x0: number;
|
|
11
|
+
t0: number;
|
|
12
|
+
displacement: number;
|
|
13
|
+
displacementRatio: number;
|
|
14
|
+
velocity: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SwipeBackProps {
|
|
18
|
+
/**
|
|
19
|
+
* The threshold to determine whether the swipe back is intentional.
|
|
20
|
+
* @default 0.4
|
|
21
|
+
*/
|
|
22
|
+
swipeBackDisplacementRatioThreshold?: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The threshold to determine whether the swipe back is intentional.
|
|
26
|
+
* @default 1
|
|
27
|
+
*/
|
|
28
|
+
swipeBackVelocityThreshold?: number;
|
|
29
|
+
|
|
30
|
+
onSwipeBackStart?: () => void;
|
|
31
|
+
onSwipeBackMove?: (props: { displacement: number; displacementRatio: number }) => void;
|
|
32
|
+
onSwipeBackEnd?: (props: { swiped: boolean }) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StartSwipeBackProps {
|
|
36
|
+
x0: number;
|
|
37
|
+
t0: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MoveSwipeBackProps {
|
|
41
|
+
x: number;
|
|
42
|
+
t: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// biome-ignore lint/suspicious/noEmptyInterface: intentionally empty for future extension
|
|
46
|
+
export interface EndSwipeBackProps {}
|
|
47
|
+
|
|
48
|
+
export function useGlobalInteraction() {
|
|
49
|
+
const [swipeBackState, setSwipeBackState] = useState<SwipeBackState>("idle");
|
|
50
|
+
|
|
51
|
+
const swipeBackContextRef = useRef<SwipeBackContext>({
|
|
52
|
+
x0: 0,
|
|
53
|
+
t0: 0,
|
|
54
|
+
displacement: 0,
|
|
55
|
+
displacementRatio: 0,
|
|
56
|
+
velocity: 0,
|
|
57
|
+
});
|
|
58
|
+
const stackRef = useRef<HTMLDivElement>(null);
|
|
59
|
+
|
|
60
|
+
const setSwipeBackContext = useCallback((ctx: SwipeBackContext) => {
|
|
61
|
+
swipeBackContextRef.current = ctx;
|
|
62
|
+
stackRef.current?.style.setProperty(
|
|
63
|
+
"--swipe-back-displacement",
|
|
64
|
+
`${ctx.displacement.toString()}px`,
|
|
65
|
+
);
|
|
66
|
+
stackRef.current?.style.setProperty(
|
|
67
|
+
"--swipe-back-displacement-ratio",
|
|
68
|
+
ctx.displacementRatio.toString(),
|
|
69
|
+
);
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const getSwipeBackEvents = useCallback(
|
|
73
|
+
(props: SwipeBackProps) => {
|
|
74
|
+
const {
|
|
75
|
+
swipeBackDisplacementRatioThreshold: displacementRatioThreshold = 0.4,
|
|
76
|
+
swipeBackVelocityThreshold: velocityThreshold = 1,
|
|
77
|
+
} = props;
|
|
78
|
+
const onSwipeStart = useCallbackRef(props.onSwipeBackStart);
|
|
79
|
+
const onSwipeMove = useCallbackRef(props.onSwipeBackMove);
|
|
80
|
+
const onSwipeEnd = useCallbackRef(props.onSwipeBackEnd);
|
|
81
|
+
|
|
82
|
+
const startSwipeBack = useCallback(
|
|
83
|
+
({ x0, t0 }: StartSwipeBackProps) => {
|
|
84
|
+
setSwipeBackContext({
|
|
85
|
+
x0,
|
|
86
|
+
t0,
|
|
87
|
+
displacement: 0,
|
|
88
|
+
displacementRatio: 0,
|
|
89
|
+
velocity: 0,
|
|
90
|
+
});
|
|
91
|
+
setSwipeBackState("swiping");
|
|
92
|
+
|
|
93
|
+
onSwipeStart?.();
|
|
94
|
+
},
|
|
95
|
+
[onSwipeStart],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const moveSwipeBack = useCallback(
|
|
99
|
+
({ x, t }: MoveSwipeBackProps) => {
|
|
100
|
+
const displacement = x - swipeBackContextRef.current.x0;
|
|
101
|
+
const displacementRatio = displacement / window.innerWidth;
|
|
102
|
+
const velocity = displacement / (t - swipeBackContextRef.current.t0);
|
|
103
|
+
setSwipeBackContext({
|
|
104
|
+
...swipeBackContextRef.current,
|
|
105
|
+
displacement,
|
|
106
|
+
displacementRatio,
|
|
107
|
+
velocity,
|
|
108
|
+
});
|
|
109
|
+
setSwipeBackState("swiping");
|
|
110
|
+
|
|
111
|
+
onSwipeMove?.({ displacement, displacementRatio });
|
|
112
|
+
},
|
|
113
|
+
[onSwipeMove],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const endSwipeBack = useCallback(
|
|
117
|
+
(_: EndSwipeBackProps) => {
|
|
118
|
+
const swiped =
|
|
119
|
+
swipeBackContextRef.current.displacementRatio > displacementRatioThreshold ||
|
|
120
|
+
swipeBackContextRef.current.velocity > velocityThreshold;
|
|
121
|
+
|
|
122
|
+
// we intentionally do not reset context here, so that the displacement can be used for exit animation
|
|
123
|
+
setSwipeBackState("transitioning");
|
|
124
|
+
|
|
125
|
+
onSwipeEnd?.({ swiped });
|
|
126
|
+
},
|
|
127
|
+
[onSwipeEnd, displacementRatioThreshold, velocityThreshold],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const reset = useCallback(() => {
|
|
131
|
+
setSwipeBackContext({
|
|
132
|
+
x0: 0,
|
|
133
|
+
t0: 0,
|
|
134
|
+
displacement: 0,
|
|
135
|
+
displacementRatio: 0,
|
|
136
|
+
velocity: 0,
|
|
137
|
+
});
|
|
138
|
+
setSwipeBackState("idle");
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
return useMemo(
|
|
142
|
+
() => ({
|
|
143
|
+
startSwipeBack,
|
|
144
|
+
moveSwipeBack,
|
|
145
|
+
endSwipeBack,
|
|
146
|
+
reset,
|
|
147
|
+
}),
|
|
148
|
+
[startSwipeBack, moveSwipeBack, endSwipeBack, reset],
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
[setSwipeBackContext],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const topActivity = useTopActivity();
|
|
155
|
+
|
|
156
|
+
const stackProps = useMemo(
|
|
157
|
+
() => ({
|
|
158
|
+
"data-swipe-back-state": swipeBackState,
|
|
159
|
+
"data-global-transition-state": topActivity.transitionState,
|
|
160
|
+
"data-top-activity-type": topActivity.activityType,
|
|
161
|
+
}),
|
|
162
|
+
[swipeBackState, topActivity.transitionState, topActivity.activityType],
|
|
163
|
+
) as React.HTMLAttributes<HTMLElement>;
|
|
164
|
+
|
|
165
|
+
return useMemo(
|
|
166
|
+
() => ({
|
|
167
|
+
stackRef,
|
|
168
|
+
swipeBackContextRef,
|
|
169
|
+
swipeBackState,
|
|
170
|
+
setSwipeBackState,
|
|
171
|
+
setSwipeBackContext,
|
|
172
|
+
getSwipeBackEvents,
|
|
173
|
+
|
|
174
|
+
stackProps,
|
|
175
|
+
}),
|
|
176
|
+
[swipeBackState, setSwipeBackContext, getSwipeBackEvents, stackProps],
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { UseGlobalInteractionReturn } from "./useGlobalInteraction";
|
|
3
|
+
|
|
4
|
+
export interface UseGlobalInteractionContext extends UseGlobalInteractionReturn {}
|
|
5
|
+
|
|
6
|
+
const GlobalInteractionContext = createContext<UseGlobalInteractionContext | null>(null);
|
|
7
|
+
|
|
8
|
+
export const GlobalInteractionProvider = GlobalInteractionContext.Provider;
|
|
9
|
+
|
|
10
|
+
export function useGlobalInteractionContext<T extends boolean | undefined = true>({
|
|
11
|
+
strict = true,
|
|
12
|
+
}: { strict?: T } = {}): T extends false
|
|
13
|
+
? UseGlobalInteractionContext | null
|
|
14
|
+
: UseGlobalInteractionContext {
|
|
15
|
+
const context = useContext(GlobalInteractionContext);
|
|
16
|
+
if (!context && strict) {
|
|
17
|
+
throw new Error("useGlobalInteractionContext must be used within a GlobalInteractionProvider");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return context as UseGlobalInteractionContext;
|
|
21
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useEffect, useMemo } from "react";
|
|
2
|
+
import type { SwipeBackProps } from "./useGlobalInteraction";
|
|
3
|
+
import { useGlobalInteractionContext } from "./useGlobalInteractionContext";
|
|
4
|
+
|
|
5
|
+
export interface UseSwipeBackProps extends SwipeBackProps {}
|
|
6
|
+
|
|
7
|
+
export function useSwipeBack(props: UseSwipeBackProps) {
|
|
8
|
+
const globalInteraction = useGlobalInteractionContext();
|
|
9
|
+
const events = globalInteraction.getSwipeBackEvents(props);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
return () => {
|
|
13
|
+
events.reset();
|
|
14
|
+
};
|
|
15
|
+
}, [events]);
|
|
16
|
+
|
|
17
|
+
return useMemo(
|
|
18
|
+
() => ({
|
|
19
|
+
activityProps: {
|
|
20
|
+
"data-swipe-back": "",
|
|
21
|
+
} as React.HTMLAttributes<HTMLDivElement>,
|
|
22
|
+
layerProps: {
|
|
23
|
+
onTransitionEnd: (e) => {
|
|
24
|
+
if (e.target === e.currentTarget) {
|
|
25
|
+
events.reset();
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
onAnimationEnd: (e) => {
|
|
29
|
+
if (e.target === e.currentTarget) {
|
|
30
|
+
events.reset();
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
} as React.HTMLAttributes<HTMLDivElement>,
|
|
34
|
+
edgeProps: {
|
|
35
|
+
tabIndex: -1,
|
|
36
|
+
onTouchStart: (e) => {
|
|
37
|
+
const x0 = e.touches[0].clientX;
|
|
38
|
+
const t0 = Date.now();
|
|
39
|
+
events.startSwipeBack({ x0, t0 });
|
|
40
|
+
},
|
|
41
|
+
onTouchMove: (e) => {
|
|
42
|
+
const x = e.touches[0].clientX;
|
|
43
|
+
const t = Date.now();
|
|
44
|
+
events.moveSwipeBack({ x, t });
|
|
45
|
+
},
|
|
46
|
+
onTouchEnd: () => {
|
|
47
|
+
events.endSwipeBack({});
|
|
48
|
+
},
|
|
49
|
+
onTouchCancel: () => {
|
|
50
|
+
events.endSwipeBack({});
|
|
51
|
+
},
|
|
52
|
+
} as React.HTMLAttributes<HTMLElement>,
|
|
53
|
+
}),
|
|
54
|
+
[events],
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
const useLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
|
|
4
|
+
|
|
5
|
+
export function useElementOffset(element: HTMLElement | null) {
|
|
6
|
+
const [offset, setOffset] = React.useState<{ fromLeft: number; fromRight: number } | undefined>(
|
|
7
|
+
undefined,
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
if (!element) {
|
|
12
|
+
setOffset(undefined);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (element) {
|
|
17
|
+
// provide as early as possible
|
|
18
|
+
setOffset({
|
|
19
|
+
fromLeft: element.offsetLeft + element.offsetWidth,
|
|
20
|
+
fromRight: (element.offsetParent ?? document.body).clientWidth - element.offsetLeft,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
24
|
+
const fromLeft = element.offsetLeft + element.offsetWidth;
|
|
25
|
+
const fromRight = (element.offsetParent ?? document.body).clientWidth - element.offsetLeft;
|
|
26
|
+
|
|
27
|
+
setOffset({ fromLeft, fromRight });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
resizeObserver.observe(element);
|
|
31
|
+
|
|
32
|
+
return () => resizeObserver.unobserve(element);
|
|
33
|
+
}
|
|
34
|
+
}, [element]);
|
|
35
|
+
|
|
36
|
+
return offset;
|
|
37
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useStack } from "@stackflow/react";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export function useTopActivity() {
|
|
5
|
+
const stack = useStack();
|
|
6
|
+
if (!stack) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
"useStack is not available in the context. Make sure you are using @stackflow/react >= 1.4.1. (https://github.com/daangn/stackflow/blob/main/integrations/react/CHANGELOG.md#141)",
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const topId = stack.activities.find((activity) => activity.isTop)?.id;
|
|
13
|
+
const topActivity = stack.activities.find((activity) => activity.isTop);
|
|
14
|
+
const transitionState = topActivity?.transitionState ?? "enter-done";
|
|
15
|
+
|
|
16
|
+
// TODO: There should be a better way to handle this. We should not rely on the DOM element to get the activity type.
|
|
17
|
+
const [topEl, setTopEl] = useState<HTMLElement | null>(null);
|
|
18
|
+
const activityType = topEl?.dataset["activityType"];
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!topId) return;
|
|
22
|
+
|
|
23
|
+
const el = document.querySelector<HTMLElement>(`[data-activity-id="${topId}"]`);
|
|
24
|
+
setTopEl(el);
|
|
25
|
+
}, [topId]);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
transitionState,
|
|
29
|
+
activityType,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { createContext, forwardRef, useContext } from "react";
|
|
3
|
+
|
|
4
|
+
type Recipe<
|
|
5
|
+
Props extends Record<string, string | boolean | undefined>,
|
|
6
|
+
Classnames extends Record<string, string>,
|
|
7
|
+
> = ((props?: Props) => Classnames) & {
|
|
8
|
+
splitVariantProps: <T extends Props>(props: T) => [Props, Omit<T, keyof Props>];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function createStyleContext<
|
|
12
|
+
Props extends Record<string, string | boolean | undefined>,
|
|
13
|
+
Classnames extends Record<string, string>,
|
|
14
|
+
>(recipe: Recipe<Props, Classnames>) {
|
|
15
|
+
const ClassNamesContext = createContext<Classnames | null>(null);
|
|
16
|
+
const PropsContext = createContext<Props | null>(null);
|
|
17
|
+
|
|
18
|
+
const ClassNamesProvider = ({
|
|
19
|
+
children,
|
|
20
|
+
value,
|
|
21
|
+
}: { children: React.ReactNode; value: Classnames }) => {
|
|
22
|
+
return <ClassNamesContext.Provider value={value}>{children}</ClassNamesContext.Provider>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const PropsProvider = ({ children, value }: { children: React.ReactNode; value: Props }) => {
|
|
26
|
+
return <PropsContext.Provider value={value}>{children}</PropsContext.Provider>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function useClassNames() {
|
|
30
|
+
const context = useContext(ClassNamesContext);
|
|
31
|
+
if (context === null) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
"useClassNames must be used within a ClassNamesProvider. Did you forget to wrap your component in a ClassNamesProvider?",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return context;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function useProps() {
|
|
41
|
+
return useContext(PropsContext);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const withRootProvider = <P,>(
|
|
45
|
+
Component: React.ElementType<any>,
|
|
46
|
+
options?: {
|
|
47
|
+
defaultProps?: Partial<P>;
|
|
48
|
+
},
|
|
49
|
+
): React.ForwardRefExoticComponent<React.PropsWithoutRef<P>> => {
|
|
50
|
+
const { defaultProps } = options ?? {};
|
|
51
|
+
|
|
52
|
+
const StyledComponent = (innerProps: any) => {
|
|
53
|
+
const props = { ...(defaultProps ?? {}), ...useProps(), ...innerProps } as Props &
|
|
54
|
+
React.HTMLAttributes<HTMLElement>;
|
|
55
|
+
const [variantProps, otherProps] = recipe.splitVariantProps(props);
|
|
56
|
+
const classNames = recipe(variantProps); // TODO: should we memoize this?
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<ClassNamesProvider value={classNames}>
|
|
60
|
+
<Component {...otherProps} />
|
|
61
|
+
</ClassNamesProvider>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// @ts-ignore
|
|
66
|
+
StyledComponent.displayName = Component.displayName || Component.name;
|
|
67
|
+
|
|
68
|
+
return StyledComponent as any;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const withProvider = <T, P>(
|
|
72
|
+
Component: React.ElementType<any>,
|
|
73
|
+
slot: keyof Classnames,
|
|
74
|
+
options?: {
|
|
75
|
+
defaultProps?: Partial<P>;
|
|
76
|
+
},
|
|
77
|
+
): React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<T>> => {
|
|
78
|
+
const { defaultProps } = options ?? {};
|
|
79
|
+
|
|
80
|
+
const StyledComponent = forwardRef<any, any>((innerProps, ref) => {
|
|
81
|
+
const props = { ...(defaultProps ?? {}), ...useProps(), ...innerProps } as Props &
|
|
82
|
+
React.HTMLAttributes<HTMLElement>;
|
|
83
|
+
const [variantProps, otherProps] = recipe.splitVariantProps(props);
|
|
84
|
+
const classNames = recipe(variantProps); // TODO: should we memoize this?
|
|
85
|
+
const className = classNames[slot as keyof typeof classNames];
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<ClassNamesProvider value={classNames}>
|
|
89
|
+
<Component ref={ref} {...otherProps} className={clsx(className, props.className)} />
|
|
90
|
+
</ClassNamesProvider>
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// @ts-ignore
|
|
95
|
+
StyledComponent.displayName = Component.displayName || Component.name;
|
|
96
|
+
|
|
97
|
+
return StyledComponent as any;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const withContext = <T, P>(
|
|
101
|
+
Component: React.ElementType<any>,
|
|
102
|
+
slot?: keyof Classnames,
|
|
103
|
+
): React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<T>> => {
|
|
104
|
+
const StyledComponent = forwardRef<any, React.HTMLAttributes<HTMLElement>>((props, ref) => {
|
|
105
|
+
const classNames = useClassNames();
|
|
106
|
+
const className = classNames?.[slot as keyof typeof classNames];
|
|
107
|
+
|
|
108
|
+
return <Component ref={ref} {...props} className={clsx(className, props.className)} />;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// @ts-ignore
|
|
112
|
+
StyledComponent.displayName = Component.displayName || Component.name;
|
|
113
|
+
return StyledComponent as any;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
ClassNamesProvider,
|
|
118
|
+
PropsProvider,
|
|
119
|
+
useClassNames,
|
|
120
|
+
useProps,
|
|
121
|
+
withRootProvider,
|
|
122
|
+
withProvider,
|
|
123
|
+
withContext,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
|
|
3
|
+
type AtLeastOne<T> = [T, ...T[]];
|
|
4
|
+
|
|
5
|
+
export function createWithStateProps(
|
|
6
|
+
useContexts: AtLeastOne<
|
|
7
|
+
(prop?: { strict?: boolean }) => { stateProps: React.HTMLAttributes<HTMLElement> } | null
|
|
8
|
+
>,
|
|
9
|
+
options?: { strict?: boolean },
|
|
10
|
+
) {
|
|
11
|
+
const strict = options?.strict ?? true;
|
|
12
|
+
|
|
13
|
+
return function withStateProps<P, R>(Component: React.ComponentType<P & React.RefAttributes<R>>) {
|
|
14
|
+
const Node = forwardRef<R, P>((props, ref) => {
|
|
15
|
+
const stateProps = {};
|
|
16
|
+
for (const useContext of useContexts) {
|
|
17
|
+
Object.assign(stateProps, useContext({ strict })?.stateProps);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
return <Component ref={ref} {...stateProps} {...props} />;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
Node.displayName = Component.displayName || Component.name;
|
|
25
|
+
|
|
26
|
+
return Node;
|
|
27
|
+
};
|
|
28
|
+
}
|