@seed-design/stackflow 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.
Files changed (127) hide show
  1. package/lib/components/AppBar/AppBar.cjs +115 -0
  2. package/lib/components/AppBar/AppBar.d.ts +34 -0
  3. package/lib/components/AppBar/AppBar.js +103 -0
  4. package/lib/components/AppBar/AppBar.namespace.cjs +16 -0
  5. package/lib/components/AppBar/AppBar.namespace.d.ts +2 -0
  6. package/lib/components/AppBar/AppBar.namespace.js +1 -0
  7. package/lib/components/AppBar/index.cjs +18 -0
  8. package/lib/components/AppBar/index.d.ts +3 -0
  9. package/lib/components/AppBar/index.js +3 -0
  10. package/lib/components/AppScreen/AppScreen.cjs +68 -0
  11. package/lib/components/AppScreen/AppScreen.d.ts +23 -0
  12. package/lib/components/AppScreen/AppScreen.js +60 -0
  13. package/lib/components/AppScreen/AppScreen.namespace.cjs +13 -0
  14. package/lib/components/AppScreen/AppScreen.namespace.d.ts +2 -0
  15. package/lib/components/AppScreen/AppScreen.namespace.js +1 -0
  16. package/lib/components/AppScreen/index.cjs +15 -0
  17. package/lib/components/AppScreen/index.d.ts +3 -0
  18. package/lib/components/AppScreen/index.js +3 -0
  19. package/lib/components/index.cjs +26 -0
  20. package/lib/components/index.d.ts +2 -0
  21. package/lib/components/index.js +6 -0
  22. package/lib/hooks/index.cjs +9 -0
  23. package/lib/hooks/index.d.ts +1 -0
  24. package/lib/hooks/index.js +1 -0
  25. package/lib/hooks/useActivityZIndexBase.cjs +14 -0
  26. package/lib/hooks/useActivityZIndexBase.d.ts +18 -0
  27. package/lib/hooks/useActivityZIndexBase.js +10 -0
  28. package/lib/index.cjs +32 -0
  29. package/lib/index.d.ts +4 -0
  30. package/lib/index.js +10 -0
  31. package/lib/plugin.cjs +18 -0
  32. package/lib/plugin.d.ts +7 -0
  33. package/lib/plugin.js +14 -0
  34. package/lib/primitive/AppBar/AppBar.cjs +44 -0
  35. package/lib/primitive/AppBar/AppBar.d.ts +15 -0
  36. package/lib/primitive/AppBar/AppBar.js +37 -0
  37. package/lib/primitive/AppBar/AppBar.namespace.cjs +12 -0
  38. package/lib/primitive/AppBar/AppBar.namespace.d.ts +1 -0
  39. package/lib/primitive/AppBar/AppBar.namespace.js +1 -0
  40. package/lib/primitive/AppBar/index.cjs +14 -0
  41. package/lib/primitive/AppBar/index.d.ts +2 -0
  42. package/lib/primitive/AppBar/index.js +3 -0
  43. package/lib/primitive/AppBar/useAppBar.cjs +39 -0
  44. package/lib/primitive/AppBar/useAppBar.d.ts +294 -0
  45. package/lib/primitive/AppBar/useAppBar.js +35 -0
  46. package/lib/primitive/AppBar/useAppBarContext.cjs +21 -0
  47. package/lib/primitive/AppBar/useAppBarContext.d.ts +8 -0
  48. package/lib/primitive/AppBar/useAppBarContext.js +16 -0
  49. package/lib/primitive/AppScreen/AppScreen.cjs +64 -0
  50. package/lib/primitive/AppScreen/AppScreen.d.ts +15 -0
  51. package/lib/primitive/AppScreen/AppScreen.js +57 -0
  52. package/lib/primitive/AppScreen/AppScreen.namespace.cjs +12 -0
  53. package/lib/primitive/AppScreen/AppScreen.namespace.d.ts +1 -0
  54. package/lib/primitive/AppScreen/AppScreen.namespace.js +1 -0
  55. package/lib/primitive/AppScreen/index.cjs +16 -0
  56. package/lib/primitive/AppScreen/index.d.ts +3 -0
  57. package/lib/primitive/AppScreen/index.js +4 -0
  58. package/lib/primitive/AppScreen/useAppScreen.cjs +66 -0
  59. package/lib/primitive/AppScreen/useAppScreen.d.ts +1138 -0
  60. package/lib/primitive/AppScreen/useAppScreen.js +62 -0
  61. package/lib/primitive/AppScreen/useAppScreenContext.cjs +21 -0
  62. package/lib/primitive/AppScreen/useAppScreenContext.d.ts +8 -0
  63. package/lib/primitive/AppScreen/useAppScreenContext.js +16 -0
  64. package/lib/primitive/GlobalInteraction/GlobalInteraction.cjs +28 -0
  65. package/lib/primitive/GlobalInteraction/GlobalInteraction.d.ts +5 -0
  66. package/lib/primitive/GlobalInteraction/GlobalInteraction.js +24 -0
  67. package/lib/primitive/GlobalInteraction/index.cjs +11 -0
  68. package/lib/primitive/GlobalInteraction/index.d.ts +2 -0
  69. package/lib/primitive/GlobalInteraction/index.js +2 -0
  70. package/lib/primitive/GlobalInteraction/useGlobalInteraction.cjs +136 -0
  71. package/lib/primitive/GlobalInteraction/useGlobalInteraction.d.ts +54 -0
  72. package/lib/primitive/GlobalInteraction/useGlobalInteraction.js +132 -0
  73. package/lib/primitive/GlobalInteraction/useGlobalInteractionContext.cjs +21 -0
  74. package/lib/primitive/GlobalInteraction/useGlobalInteractionContext.d.ts +8 -0
  75. package/lib/primitive/GlobalInteraction/useGlobalInteractionContext.js +16 -0
  76. package/lib/primitive/GlobalInteraction/useSwipeBack.cjs +58 -0
  77. package/lib/primitive/GlobalInteraction/useSwipeBack.d.ts +8 -0
  78. package/lib/primitive/GlobalInteraction/useSwipeBack.js +54 -0
  79. package/lib/primitive/index.cjs +27 -0
  80. package/lib/primitive/index.d.ts +3 -0
  81. package/lib/primitive/index.js +9 -0
  82. package/lib/primitive/private/useElementOffset.cjs +54 -0
  83. package/lib/primitive/private/useElementOffset.d.ts +4 -0
  84. package/lib/primitive/private/useElementOffset.js +31 -0
  85. package/lib/primitive/private/useTopActivity.cjs +34 -0
  86. package/lib/primitive/private/useTopActivity.d.ts +5 -0
  87. package/lib/primitive/private/useTopActivity.js +30 -0
  88. package/lib/utils/createStyleContext.cjs +77 -0
  89. package/lib/utils/createStyleContext.d.ts +23 -0
  90. package/lib/utils/createStyleContext.js +73 -0
  91. package/lib/utils/createWithStateProps.d.ts +10 -0
  92. package/lib/utils/styled.cjs +25 -0
  93. package/lib/utils/styled.d.ts +16 -0
  94. package/lib/utils/styled.js +21 -0
  95. package/package.json +61 -0
  96. package/src/components/AppBar/AppBar.namespace.ts +21 -0
  97. package/src/components/AppBar/AppBar.tsx +146 -0
  98. package/src/components/AppBar/index.ts +23 -0
  99. package/src/components/AppScreen/AppScreen.namespace.ts +14 -0
  100. package/src/components/AppScreen/AppScreen.tsx +83 -0
  101. package/src/components/AppScreen/index.ts +16 -0
  102. package/src/components/index.ts +2 -0
  103. package/src/hooks/index.ts +1 -0
  104. package/src/hooks/useActivityZIndexBase.ts +26 -0
  105. package/src/index.ts +6 -0
  106. package/src/plugin.tsx +27 -0
  107. package/src/primitive/AppBar/AppBar.namespace.ts +10 -0
  108. package/src/primitive/AppBar/AppBar.tsx +52 -0
  109. package/src/primitive/AppBar/index.ts +12 -0
  110. package/src/primitive/AppBar/useAppBar.tsx +42 -0
  111. package/src/primitive/AppBar/useAppBarContext.ts +21 -0
  112. package/src/primitive/AppScreen/AppScreen.namespace.ts +10 -0
  113. package/src/primitive/AppScreen/AppScreen.tsx +72 -0
  114. package/src/primitive/AppScreen/index.ts +13 -0
  115. package/src/primitive/AppScreen/useAppScreen.tsx +69 -0
  116. package/src/primitive/AppScreen/useAppScreenContext.ts +21 -0
  117. package/src/primitive/GlobalInteraction/GlobalInteraction.tsx +25 -0
  118. package/src/primitive/GlobalInteraction/index.ts +6 -0
  119. package/src/primitive/GlobalInteraction/useGlobalInteraction.ts +188 -0
  120. package/src/primitive/GlobalInteraction/useGlobalInteractionContext.tsx +21 -0
  121. package/src/primitive/GlobalInteraction/useSwipeBack.ts +56 -0
  122. package/src/primitive/index.ts +3 -0
  123. package/src/primitive/private/useElementOffset.ts +37 -0
  124. package/src/primitive/private/useTopActivity.ts +33 -0
  125. package/src/utils/createStyleContext.tsx +128 -0
  126. package/src/utils/createWithStateProps.tsx +28 -0
  127. package/src/utils/styled.ts +41 -0
@@ -0,0 +1,21 @@
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
+ }: {
13
+ strict?: T;
14
+ } = {}): T extends false ? UseAppScreenContext | null : UseAppScreenContext {
15
+ const context = useContext(AppScreenContext);
16
+ if (!context && strict) {
17
+ throw new Error("useAppScreenContext must be used within a AppScreen");
18
+ }
19
+
20
+ return context as UseAppScreenContext;
21
+ }
@@ -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,6 @@
1
+ export { GlobalInteraction, type GlobalInteractionProps } from "./GlobalInteraction";
2
+
3
+ export {
4
+ useGlobalInteractionContext,
5
+ type UseGlobalInteractionContext,
6
+ } from "./useGlobalInteractionContext";
@@ -0,0 +1,188 @@
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" | "canceling" | "completing";
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((prev) => (prev === "swiping" ? prev : "swiping"));
92
+ onSwipeStart?.();
93
+ },
94
+ [onSwipeStart],
95
+ );
96
+
97
+ const moveSwipeBack = useCallback(
98
+ ({ x, t }: MoveSwipeBackProps) => {
99
+ const displacement = Math.max(0, x - swipeBackContextRef.current.x0);
100
+ const displacementRatio = displacement / window.innerWidth;
101
+ const velocity = displacement / (t - swipeBackContextRef.current.t0);
102
+ setSwipeBackContext({
103
+ ...swipeBackContextRef.current,
104
+ displacement,
105
+ displacementRatio,
106
+ velocity,
107
+ });
108
+ setSwipeBackState((prev) => (prev === "swiping" ? prev : "swiping"));
109
+ onSwipeMove?.({ displacement, displacementRatio });
110
+ },
111
+ [onSwipeMove],
112
+ );
113
+
114
+ const endSwipeBack = useCallback(
115
+ (_: EndSwipeBackProps) => {
116
+ const swiped =
117
+ swipeBackContextRef.current.displacementRatio > displacementRatioThreshold ||
118
+ swipeBackContextRef.current.velocity > velocityThreshold;
119
+
120
+ if (swiped) {
121
+ stackRef.current?.style.setProperty("--swipe-back-target", "100%");
122
+ setSwipeBackState("completing");
123
+ } else {
124
+ stackRef.current?.style.setProperty("--swipe-back-target", "0");
125
+ setSwipeBackState("canceling");
126
+ }
127
+
128
+ onSwipeEnd?.({ swiped });
129
+ },
130
+ [onSwipeEnd, displacementRatioThreshold, velocityThreshold],
131
+ );
132
+
133
+ const reset = useCallback(() => {
134
+ setSwipeBackContext({
135
+ x0: 0,
136
+ t0: 0,
137
+ displacement: 0,
138
+ displacementRatio: 0,
139
+ velocity: 0,
140
+ });
141
+ stackRef.current?.style.setProperty("--swipe-back-target", "0");
142
+ setSwipeBackState("idle");
143
+ }, []);
144
+
145
+ return useMemo(
146
+ () => ({
147
+ startSwipeBack,
148
+ moveSwipeBack,
149
+ endSwipeBack,
150
+ reset,
151
+ }),
152
+ [startSwipeBack, moveSwipeBack, endSwipeBack, reset],
153
+ );
154
+ },
155
+ [setSwipeBackContext],
156
+ );
157
+
158
+ const topActivity = useTopActivity();
159
+
160
+ const stackProps = useMemo(
161
+ () => ({
162
+ "data-swipe-back-state": swipeBackState,
163
+ "data-global-transition-state": topActivity.transitionState,
164
+ "data-top-activity-type": topActivity.activityType,
165
+ "data-top-transition-style": topActivity.transitionStyle,
166
+ }),
167
+ [
168
+ swipeBackState,
169
+ topActivity.transitionState,
170
+ topActivity.activityType,
171
+ topActivity.transitionStyle,
172
+ ],
173
+ ) as React.HTMLAttributes<HTMLElement>;
174
+
175
+ return useMemo(
176
+ () => ({
177
+ stackRef,
178
+ swipeBackContextRef,
179
+ swipeBackState,
180
+ setSwipeBackState,
181
+ setSwipeBackContext,
182
+ getSwipeBackEvents,
183
+
184
+ stackProps,
185
+ }),
186
+ [swipeBackState, setSwipeBackContext, getSwipeBackEvents, stackProps],
187
+ );
188
+ }
@@ -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
+ }: {
13
+ strict?: T;
14
+ } = {}): T extends false ? UseGlobalInteractionContext | null : 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
+ onAnimationEnd: (e) => {
24
+ if (e.target === e.currentTarget) {
25
+ events.reset();
26
+ }
27
+ },
28
+ onTransitionEnd: (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,3 @@
1
+ export * from "./AppScreen";
2
+ export * from "./AppBar";
3
+ export * from "./GlobalInteraction";
@@ -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,33 @@
1
+ import { useStack } from "@stackflow/react";
2
+ import { useLayoutEffect, 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
+ const transitionStyle = topEl?.dataset["transitionStyle"];
20
+
21
+ useLayoutEffect(() => {
22
+ if (!topId) return;
23
+
24
+ const el = document.querySelector<HTMLElement>(`[data-activity-id="${topId}"]`);
25
+ setTopEl(el);
26
+ }, [topId]);
27
+
28
+ return {
29
+ transitionState,
30
+ activityType,
31
+ transitionStyle,
32
+ };
33
+ }
@@ -0,0 +1,128 @@
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
+ }: {
22
+ children: React.ReactNode;
23
+ value: Classnames;
24
+ }) => {
25
+ return <ClassNamesContext.Provider value={value}>{children}</ClassNamesContext.Provider>;
26
+ };
27
+
28
+ const PropsProvider = ({ children, value }: { children: React.ReactNode; value: Props }) => {
29
+ return <PropsContext.Provider value={value}>{children}</PropsContext.Provider>;
30
+ };
31
+
32
+ function useClassNames() {
33
+ const context = useContext(ClassNamesContext);
34
+ if (context === null) {
35
+ throw new Error(
36
+ "useClassNames must be used within a ClassNamesProvider. Did you forget to wrap your component in a ClassNamesProvider?",
37
+ );
38
+ }
39
+
40
+ return context;
41
+ }
42
+
43
+ function useProps() {
44
+ return useContext(PropsContext);
45
+ }
46
+
47
+ const withRootProvider = <P,>(
48
+ Component: React.ElementType<any>,
49
+ options?: {
50
+ defaultProps?: Partial<P>;
51
+ },
52
+ ): React.ForwardRefExoticComponent<React.PropsWithoutRef<P>> => {
53
+ const { defaultProps } = options ?? {};
54
+
55
+ const StyledComponent = (innerProps: any) => {
56
+ const props = { ...(defaultProps ?? {}), ...useProps(), ...innerProps } as Props &
57
+ React.HTMLAttributes<HTMLElement>;
58
+ const [variantProps, otherProps] = recipe.splitVariantProps(props);
59
+ const classNames = recipe(variantProps); // TODO: should we memoize this?
60
+
61
+ return (
62
+ <ClassNamesProvider value={classNames}>
63
+ <Component {...otherProps} />
64
+ </ClassNamesProvider>
65
+ );
66
+ };
67
+
68
+ // @ts-ignore
69
+ StyledComponent.displayName = Component.displayName || Component.name;
70
+
71
+ return StyledComponent as any;
72
+ };
73
+
74
+ const withProvider = <T, P>(
75
+ Component: React.ElementType<any>,
76
+ slot: keyof Classnames,
77
+ options?: {
78
+ defaultProps?: Partial<P>;
79
+ },
80
+ ): React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<T>> => {
81
+ const { defaultProps } = options ?? {};
82
+
83
+ const StyledComponent = forwardRef<any, any>((innerProps, ref) => {
84
+ const props = { ...(defaultProps ?? {}), ...useProps(), ...innerProps } as Props &
85
+ React.HTMLAttributes<HTMLElement>;
86
+ const [variantProps, otherProps] = recipe.splitVariantProps(props);
87
+ const classNames = recipe(variantProps); // TODO: should we memoize this?
88
+ const className = classNames[slot as keyof typeof classNames];
89
+
90
+ return (
91
+ <ClassNamesProvider value={classNames}>
92
+ <Component ref={ref} {...otherProps} className={clsx(className, props.className)} />
93
+ </ClassNamesProvider>
94
+ );
95
+ });
96
+
97
+ // @ts-ignore
98
+ StyledComponent.displayName = Component.displayName || Component.name;
99
+
100
+ return StyledComponent as any;
101
+ };
102
+
103
+ const withContext = <T, P>(
104
+ Component: React.ElementType<any>,
105
+ slot?: keyof Classnames,
106
+ ): React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<T>> => {
107
+ const StyledComponent = forwardRef<any, React.HTMLAttributes<HTMLElement>>((props, ref) => {
108
+ const classNames = useClassNames();
109
+ const className = classNames?.[slot as keyof typeof classNames];
110
+
111
+ return <Component ref={ref} {...props} className={clsx(className, props.className)} />;
112
+ });
113
+
114
+ // @ts-ignore
115
+ StyledComponent.displayName = Component.displayName || Component.name;
116
+ return StyledComponent as any;
117
+ };
118
+
119
+ return {
120
+ ClassNamesProvider,
121
+ PropsProvider,
122
+ useClassNames,
123
+ useProps,
124
+ withRootProvider,
125
+ withProvider,
126
+ withContext,
127
+ };
128
+ }
@@ -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
+ }
@@ -0,0 +1,41 @@
1
+ import type { ScopedColorBg, ScopedColorPalette, ScopedColorBanner } from "@seed-design/css/vars";
2
+ import { vars } from "@seed-design/css/vars";
3
+
4
+ /**
5
+ * A minimal subset of `StyleProps` from `@seed-design/react`.
6
+ * Copied here to avoid package dependency. May be extracted to a shared package later.
7
+ */
8
+ export interface BoxBackgroundProps {
9
+ /**
10
+ * Shorthand for `background`.
11
+ */
12
+ bg?: ScopedColorBg | ScopedColorPalette | ScopedColorBanner | (string & {});
13
+
14
+ background?: ScopedColorBg | ScopedColorPalette | ScopedColorBanner | (string & {});
15
+ }
16
+
17
+ function handleColor(color: string | undefined) {
18
+ if (!color) {
19
+ return undefined;
20
+ }
21
+ const [type, value] = color.split(".");
22
+
23
+ // @ts-expect-error - dynamic access
24
+ return vars.$color[type]?.[value] ?? color;
25
+ }
26
+
27
+ export function useBoxBackgroundProps<T extends BoxBackgroundProps>(
28
+ props: T,
29
+ ): {
30
+ style: React.CSSProperties;
31
+ restProps: Omit<T, keyof BoxBackgroundProps>;
32
+ } {
33
+ const { bg, background, ...restProps } = props;
34
+
35
+ return {
36
+ style: {
37
+ "--seed-box-background": handleColor(background ?? bg),
38
+ } as React.CSSProperties,
39
+ restProps: restProps as Omit<T, keyof BoxBackgroundProps>,
40
+ };
41
+ }