@ahnseungchan/webviewkit 1.0.0 → 1.0.2

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.
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ // import React from "react";
3
+ // import { render, screen, fireEvent } from "@testing-library/react";
4
+ // import BackRouter from "../stack-router/_components/back-router";
5
+ // import userEvent from "@testing-library/user-event";
6
+ // import { useStackRouter } from "../stack-router/stack-router-provider";
7
+ // jest.mock("../stack-router/stack-router-provider", () => ({
8
+ // useStackRouter: jest.fn(),
9
+ // }));
10
+ // describe("BackRouter", () => {
11
+ // const mockBack = jest.fn();
12
+ // const user = userEvent.setup();
13
+ // beforeEach(() => {
14
+ // // Reset all mocks before each test
15
+ // jest.clearAllMocks();
16
+ // // Setup default mock implementation
17
+ // (useStackRouter as jest.Mock).mockReturnValue({
18
+ // back: mockBack,
19
+ // });
20
+ // });
21
+ // it("renders back button with icon", () => {
22
+ // render(<BackRouter />);
23
+ // // Check if button exists
24
+ // const button = screen.getByRole("button");
25
+ // expect(button).toBeInTheDocument();
26
+ // // Check if SVG icon exists
27
+ // const svg = button.querySelector("svg");
28
+ // expect(svg).toBeInTheDocument();
29
+ // });
30
+ // it("calls back function when clicked", () => {
31
+ // render(<BackRouter />);
32
+ // const button = screen.getByRole("button");
33
+ // fireEvent.click(button);
34
+ // expect(mockBack).toHaveBeenCalledTimes(1);
35
+ // });
36
+ // it("applies correct styles", () => {
37
+ // render(<BackRouter />);
38
+ // const button = screen.getByRole("button");
39
+ // // Check initial styles
40
+ // expect(button).toHaveStyle({
41
+ // width: "40px",
42
+ // height: "40px",
43
+ // padding: "8px",
44
+ // borderRadius: "50%",
45
+ // });
46
+ // // Check if SVG has correct attributes
47
+ // const svg = button.querySelector("svg");
48
+ // expect(svg).toHaveAttribute("width", "32");
49
+ // expect(svg).toHaveAttribute("height", "32");
50
+ // expect(svg).toHaveAttribute("viewBox", "0 0 24 24");
51
+ // });
52
+ // it("handles hover and active states", async () => {
53
+ // render(<BackRouter />);
54
+ // const button = screen.getByRole("button");
55
+ // // expect hover
56
+ // await user.hover(button);
57
+ // expect(button).toHaveStyle({ color: "#666666" });
58
+ // // expect active
59
+ // await user.pointer({ keys: "[MouseLeft]", target: button });
60
+ // expect(button).toHaveStyle({ color: "#999999" });
61
+ // });
62
+ // });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,114 @@
1
+ import { history } from "../history";
2
+ // mocking window.history
3
+ const mockHistory = {
4
+ state: null,
5
+ pushState: jest.fn(),
6
+ replaceState: jest.fn(),
7
+ back: jest.fn(),
8
+ forward: jest.fn(),
9
+ go: jest.fn(),
10
+ };
11
+ // mocking window.location
12
+ const mockLocation = {
13
+ pathname: "/",
14
+ };
15
+ // mocking global window
16
+ Object.defineProperty(window, "history", {
17
+ value: mockHistory,
18
+ writable: true,
19
+ });
20
+ Object.defineProperty(window, "location", {
21
+ value: mockLocation,
22
+ writable: true,
23
+ });
24
+ describe("history", () => {
25
+ beforeEach(() => {
26
+ jest.clearAllMocks();
27
+ mockHistory.state = null;
28
+ mockLocation.pathname = "/";
29
+ });
30
+ describe("push", () => {
31
+ it("should push new history entry", () => {
32
+ const path = "/test";
33
+ const data = { title: "Test" };
34
+ history.push(path, data);
35
+ expect(mockHistory.pushState).toHaveBeenCalledWith({ path, data, action: "PUSH" }, "", path);
36
+ });
37
+ });
38
+ describe("replace", () => {
39
+ it("should replace current history entry", () => {
40
+ const path = "/test";
41
+ const data = { title: "Test" };
42
+ history.replace(path, data);
43
+ expect(mockHistory.replaceState).toHaveBeenCalledWith({ path, data, action: "REPLACE" }, "", path);
44
+ });
45
+ });
46
+ describe("navigation methods", () => {
47
+ it("should call history.back()", () => {
48
+ history.back();
49
+ expect(mockHistory.back).toHaveBeenCalled();
50
+ });
51
+ it("should call history.forward()", () => {
52
+ history.forward();
53
+ expect(mockHistory.forward).toHaveBeenCalled();
54
+ });
55
+ it("should call history.go()", () => {
56
+ const delta = 2;
57
+ history.go(delta);
58
+ expect(mockHistory.go).toHaveBeenCalledWith(delta);
59
+ });
60
+ });
61
+ describe("getCurrentState", () => {
62
+ it("should return current state", () => {
63
+ const mockState = {
64
+ path: "/test",
65
+ data: { title: "Test" },
66
+ };
67
+ mockHistory.state = mockState;
68
+ const state = history.getCurrentState();
69
+ expect(state).toEqual(mockState);
70
+ });
71
+ it("should return default state when history.state is null", () => {
72
+ mockHistory.state = null;
73
+ mockLocation.pathname = "/default";
74
+ const state = history.getCurrentState();
75
+ expect(state).toEqual({ path: "/default" });
76
+ });
77
+ });
78
+ describe("addListener", () => {
79
+ it("should add and remove listener", () => {
80
+ const listener = jest.fn();
81
+ const removeListener = history.addListener(listener);
82
+ const testState = {
83
+ path: "/test",
84
+ };
85
+ const popStateEvent = new PopStateEvent("popstate", {
86
+ state: testState,
87
+ });
88
+ window.dispatchEvent(popStateEvent);
89
+ expect(listener).toHaveBeenCalledWith(testState);
90
+ // Delete listener
91
+ removeListener();
92
+ // change state and dispatch event
93
+ const newState = { path: "/new" };
94
+ const newPopStateEvent = new PopStateEvent("popstate", {
95
+ state: newState,
96
+ });
97
+ window.dispatchEvent(newPopStateEvent);
98
+ // listener should not be called anymore
99
+ expect(listener).toHaveBeenCalledTimes(1);
100
+ });
101
+ it("should not add duplicate listener", () => {
102
+ const listener = jest.fn();
103
+ // add listener twice
104
+ history.addListener(listener);
105
+ history.addListener(listener);
106
+ // change state and dispatch event
107
+ const testState = { path: "/test" };
108
+ const popStateEvent = new PopStateEvent("popstate", { state: testState });
109
+ window.dispatchEvent(popStateEvent);
110
+ // listener should be called only once
111
+ expect(listener).toHaveBeenCalledTimes(1);
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ // import React from "react";
3
+ // import {
4
+ // render,
5
+ // screen,
6
+ // fireEvent,
7
+ // act,
8
+ // waitFor,
9
+ // } from "@testing-library/react";
10
+ // import StackRouter, {
11
+ // useStackRouter,
12
+ // } from "../stack-router/stack-router-provider";
13
+ // import { history } from "../history";
14
+ // // Test component that uses useStackRouter
15
+ // const MockPushComponent = ({ path }: { path: string }) => {
16
+ // const { push } = useStackRouter();
17
+ // return (
18
+ // <button
19
+ // onClick={() => push(path)}
20
+ // data-testid={`push-to-${path.replace("/", "")}`}
21
+ // >
22
+ // Navigate to {path}
23
+ // </button>
24
+ // );
25
+ // };
26
+ // const MockBackComponent = ({ path }: { path: string }) => {
27
+ // const { back } = useStackRouter();
28
+ // return (
29
+ // <button onClick={back} data-testid={`back-to-${path.replace("/", "")}`}>
30
+ // Back
31
+ // </button>
32
+ // );
33
+ // };
34
+ // // Mock components for testing
35
+ // const MockPage1 = () => (
36
+ // <div data-testid="page1">
37
+ // <MockPushComponent path="/page2" />
38
+ // Page1
39
+ // </div>
40
+ // );
41
+ // const MockPage2 = () => (
42
+ // <div data-testid="page2">
43
+ // <MockPushComponent path="/page3" />
44
+ // <MockBackComponent path="/page1" />
45
+ // Page2
46
+ // </div>
47
+ // );
48
+ // const MockPage3 = () => (
49
+ // <div data-testid="page3">
50
+ // <MockBackComponent path="/page2" />
51
+ // Page3
52
+ // </div>
53
+ // );
54
+ // const mockActivities = [
55
+ // { path: "/page1", element: <MockPage1 /> },
56
+ // { path: "/page2", element: <MockPage2 /> },
57
+ // { path: "/page3", element: <MockPage3 /> },
58
+ // ];
59
+ // // Helper function to wait for animations and state updates
60
+ // const waitForStateUpdate = async () => {
61
+ // await act(async () => {
62
+ // await new Promise((resolve) => setTimeout(resolve, 100));
63
+ // });
64
+ // };
65
+ // describe("StackRouter", () => {
66
+ // it("renders initial page correctly", () => {
67
+ // render(
68
+ // <StackRouter
69
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
70
+ // Activities={mockActivities}
71
+ // />
72
+ // );
73
+ // expect(screen.getByTestId("page1")).toBeInTheDocument();
74
+ // });
75
+ // it("navigates to new page when push is called", async () => {
76
+ // render(
77
+ // <StackRouter
78
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
79
+ // Activities={mockActivities}
80
+ // />
81
+ // );
82
+ // // Click the navigation button
83
+ // await act(async () => {
84
+ // fireEvent.click(screen.getByTestId("push-to-page2"));
85
+ // await waitForStateUpdate();
86
+ // });
87
+ // await waitFor(() => {
88
+ // expect(screen.getByText("Page2")).toBeInTheDocument();
89
+ // });
90
+ // });
91
+ // it("goes back when back is called", async () => {
92
+ // render(
93
+ // <StackRouter
94
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
95
+ // Activities={mockActivities}
96
+ // />
97
+ // );
98
+ // // First push to page2
99
+ // await act(async () => {
100
+ // fireEvent.click(screen.getByTestId("push-to-page2"));
101
+ // await waitForStateUpdate();
102
+ // });
103
+ // // Then go back
104
+ // await act(async () => {
105
+ // fireEvent.click(screen.getByTestId("back-to-page1"));
106
+ // await waitForStateUpdate();
107
+ // });
108
+ // await waitFor(() => {
109
+ // expect(screen.getByText("Page1")).toBeInTheDocument();
110
+ // });
111
+ // });
112
+ // it("handles multiple page transitions correctly", async () => {
113
+ // render(
114
+ // <StackRouter
115
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
116
+ // Activities={mockActivities}
117
+ // />
118
+ // );
119
+ // // Navigate through pages
120
+ // await act(async () => {
121
+ // fireEvent.click(screen.getByTestId("push-to-page2"));
122
+ // await waitForStateUpdate();
123
+ // });
124
+ // await waitFor(() => {
125
+ // expect(screen.getByText("Page2")).toBeInTheDocument();
126
+ // });
127
+ // await act(async () => {
128
+ // fireEvent.click(screen.getByTestId("push-to-page3"));
129
+ // await waitForStateUpdate();
130
+ // });
131
+ // await waitFor(() => {
132
+ // expect(screen.getByText("Page3")).toBeInTheDocument();
133
+ // });
134
+ // // Go back twice
135
+ // await act(async () => {
136
+ // fireEvent.click(screen.getByTestId("back-to-page2"));
137
+ // await waitForStateUpdate();
138
+ // });
139
+ // await waitFor(() => {
140
+ // expect(screen.getByText("Page2")).toBeInTheDocument();
141
+ // });
142
+ // await act(async () => {
143
+ // fireEvent.click(screen.getByTestId("back-to-page1"));
144
+ // await waitForStateUpdate();
145
+ // });
146
+ // await waitFor(() => {
147
+ // expect(screen.getByText("Page1")).toBeInTheDocument();
148
+ // });
149
+ // });
150
+ // it("maintains correct page order in stack", async () => {
151
+ // const { rerender } = render(
152
+ // <StackRouter
153
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
154
+ // Activities={mockActivities}
155
+ // />
156
+ // );
157
+ // // Navigate to page2
158
+ // await act(async () => {
159
+ // fireEvent.click(screen.getByTestId("push-to-page2"));
160
+ // await waitForStateUpdate();
161
+ // });
162
+ // await waitFor(() => {
163
+ // expect(screen.getByText("Page2")).toBeInTheDocument();
164
+ // });
165
+ // // Navigate to page3
166
+ // await act(async () => {
167
+ // fireEvent.click(screen.getByTestId("push-to-page3"));
168
+ // await waitForStateUpdate();
169
+ // });
170
+ // await waitFor(() => {
171
+ // expect(screen.getByText("Page3")).toBeInTheDocument();
172
+ // });
173
+ // });
174
+ // it("handles maxWidth prop correctly", () => {
175
+ // render(
176
+ // <StackRouter
177
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
178
+ // Activities={mockActivities}
179
+ // maxWidth="800px"
180
+ // />
181
+ // );
182
+ // const layout = screen.getByTestId("layout");
183
+ // expect(layout).toHaveStyle({ maxWidth: "800px" });
184
+ // });
185
+ // });
@@ -0,0 +1,10 @@
1
+ import { HistoryListener, HistoryState } from "./type";
2
+ export declare const history: {
3
+ push: (path: string, data?: any) => Promise<void>;
4
+ replace: (path: string, data: any) => Promise<void>;
5
+ back: () => Promise<void>;
6
+ forward: () => Promise<void>;
7
+ go: (delta: number) => Promise<void>;
8
+ getCurrentState: () => HistoryState;
9
+ addListener: (listener: HistoryListener) => () => boolean;
10
+ };
@@ -0,0 +1,82 @@
1
+ import { HistoryAction } from "./type";
2
+ const listeners = new Set();
3
+ const handlePopState = (event) => {
4
+ const state = event.state;
5
+ notifyListeners(state);
6
+ };
7
+ // notify to listeners
8
+ const notifyListeners = (state) => {
9
+ listeners.forEach((listener) => listener(state));
10
+ };
11
+ // initialize
12
+ window.addEventListener("popstate", handlePopState);
13
+ export const history = {
14
+ // add new history entry
15
+ push: async (path, data) => {
16
+ const state = {
17
+ path,
18
+ data,
19
+ action: HistoryAction.PUSH,
20
+ };
21
+ window.history.pushState(state, "", path);
22
+ // 애니메이션을 위한 지연
23
+ await new Promise((resolve) => setTimeout(resolve, 300));
24
+ notifyListeners(state);
25
+ },
26
+ // replace current history entry
27
+ replace: async (path, data) => {
28
+ const state = {
29
+ path,
30
+ data,
31
+ action: HistoryAction.REPLACE,
32
+ };
33
+ window.history.replaceState(state, "", path);
34
+ await new Promise((resolve) => setTimeout(resolve, 300));
35
+ notifyListeners(state);
36
+ },
37
+ // back to previous history entry
38
+ back: async () => {
39
+ window.history.back();
40
+ await new Promise((resolve) => {
41
+ const handlePopState = () => {
42
+ window.removeEventListener("popstate", handlePopState);
43
+ resolve();
44
+ };
45
+ window.addEventListener("popstate", handlePopState);
46
+ });
47
+ const state = {
48
+ path: window.location.pathname,
49
+ action: HistoryAction.POP,
50
+ };
51
+ notifyListeners(state);
52
+ },
53
+ // forward to next history entry
54
+ forward: async () => {
55
+ window.history.forward();
56
+ const state = {
57
+ path: window.location.pathname,
58
+ action: HistoryAction.PUSH,
59
+ };
60
+ await new Promise((resolve) => setTimeout(resolve, 300));
61
+ notifyListeners(state);
62
+ },
63
+ // go to specific history entry
64
+ go: async (delta) => {
65
+ window.history.go(delta);
66
+ const state = {
67
+ path: window.location.pathname,
68
+ action: HistoryAction.PUSH,
69
+ };
70
+ await new Promise((resolve) => setTimeout(resolve, 300));
71
+ notifyListeners(state);
72
+ },
73
+ // get current history entry
74
+ getCurrentState: () => window.history.state || {
75
+ path: window.location.pathname,
76
+ },
77
+ // change listener to history
78
+ addListener: (listener) => {
79
+ listeners.add(listener);
80
+ return () => listeners.delete(listener);
81
+ },
82
+ };
@@ -0,0 +1,5 @@
1
+ export type { StackRouterProviderProps, VisiblePage, DirectionType, HistoryState, HistoryListener, } from "./type";
2
+ export { Direction, HistoryAction } from "./type";
3
+ export { history } from "./history";
4
+ export { default as StackRouterProvider } from "./stack-router/stack-router-provider";
5
+ export { useStackRouter } from "./stack-router/stack-router-provider";
@@ -0,0 +1,6 @@
1
+ export { Direction, HistoryAction } from "./type";
2
+ // Export history API
3
+ export { history } from "./history";
4
+ // Export stack router components
5
+ export { default as StackRouterProvider } from "./stack-router/stack-router-provider";
6
+ export { useStackRouter } from "./stack-router/stack-router-provider";
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ // import React from "react";
3
+ // import { render, screen, fireEvent, act } from "@testing-library/react";
4
+ // import StackRouter from "../stack-router-provider";
5
+ // import { history } from "../../history";
6
+ // // Mock components for testing
7
+ // const MockPage1 = () => <div data-testid="page1">Page 1</div>;
8
+ // const MockPage2 = () => <div data-testid="page2">Page 2</div>;
9
+ // const MockPage3 = () => <div data-testid="page3">Page 3</div>;
10
+ // const mockActivities = [
11
+ // { path: "/page1", element: <MockPage1 /> },
12
+ // { path: "/page2", element: <MockPage2 /> },
13
+ // { path: "/page3", element: <MockPage3 /> },
14
+ // ];
15
+ // // Helper function to wait for animations
16
+ // const waitForAnimation = async () => {
17
+ // await act(async () => {
18
+ // await new Promise((resolve) => setTimeout(resolve, 700)); // 애니메이션 시간과 동일하게 설정
19
+ // });
20
+ // };
21
+ // describe("StackRouter", () => {
22
+ // beforeEach(() => {
23
+ // // Reset history before each test
24
+ // history.push("/page1");
25
+ // });
26
+ // it("renders initial page correctly", () => {
27
+ // render(
28
+ // <StackRouter
29
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
30
+ // Activities={mockActivities}
31
+ // />
32
+ // );
33
+ // expect(screen.getByTestId("page1")).toBeInTheDocument();
34
+ // });
35
+ // it("navigates to new page when push is called", async () => {
36
+ // const { rerender } = render(
37
+ // <StackRouter
38
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
39
+ // Activities={mockActivities}
40
+ // />
41
+ // );
42
+ // // Mock history.push
43
+ // history.push("/page2");
44
+ // // Wait for animation
45
+ // await waitForAnimation();
46
+ // rerender(
47
+ // <StackRouter
48
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
49
+ // Activities={mockActivities}
50
+ // />
51
+ // );
52
+ // // 페이지가 렌더링될 때까지 대기
53
+ // await screen.findByTestId("page2");
54
+ // expect(screen.getByTestId("page2")).toBeInTheDocument();
55
+ // });
56
+ // it("goes back when back is called", async () => {
57
+ // const { rerender } = render(
58
+ // <StackRouter
59
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
60
+ // Activities={mockActivities}
61
+ // />
62
+ // );
63
+ // // First push to page2
64
+ // history.push("/page2");
65
+ // await waitForAnimation();
66
+ // // Then go back
67
+ // history.back();
68
+ // await waitForAnimation();
69
+ // rerender(
70
+ // <StackRouter
71
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
72
+ // Activities={mockActivities}
73
+ // />
74
+ // );
75
+ // // 페이지가 렌더링될 때까지 대기
76
+ // await screen.findByTestId("page1");
77
+ // expect(screen.getByTestId("page1")).toBeInTheDocument();
78
+ // });
79
+ // it("handles multiple page transitions correctly", async () => {
80
+ // const { rerender } = render(
81
+ // <StackRouter
82
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
83
+ // Activities={mockActivities}
84
+ // />
85
+ // );
86
+ // // Navigate through pages
87
+ // history.push("/page2");
88
+ // await waitForAnimation();
89
+ // history.push("/page3");
90
+ // await waitForAnimation();
91
+ // rerender(
92
+ // <StackRouter
93
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
94
+ // Activities={mockActivities}
95
+ // />
96
+ // );
97
+ // // 페이지가 렌더링될 때까지 대기
98
+ // await screen.findByTestId("page3");
99
+ // expect(screen.getByTestId("page3")).toBeInTheDocument();
100
+ // // Go back twice
101
+ // history.back();
102
+ // await waitForAnimation();
103
+ // history.back();
104
+ // await waitForAnimation();
105
+ // rerender(
106
+ // <StackRouter
107
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
108
+ // Activities={mockActivities}
109
+ // />
110
+ // );
111
+ // // 페이지가 렌더링될 때까지 대기
112
+ // await screen.findByTestId("page1");
113
+ // expect(screen.getByTestId("page1")).toBeInTheDocument();
114
+ // });
115
+ // it("maintains correct page order in stack", async () => {
116
+ // const { rerender } = render(
117
+ // <StackRouter
118
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
119
+ // Activities={mockActivities}
120
+ // />
121
+ // );
122
+ // // Navigate to page2
123
+ // history.push("/page2");
124
+ // await waitForAnimation();
125
+ // // Navigate to page3
126
+ // history.push("/page3");
127
+ // await waitForAnimation();
128
+ // rerender(
129
+ // <StackRouter
130
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
131
+ // Activities={mockActivities}
132
+ // />
133
+ // );
134
+ // // 모든 페이지가 렌더링될 때까지 대기
135
+ // await screen.findByTestId("page3");
136
+ // // Check if all pages are in the correct order
137
+ // const pages = screen.getAllByTestId(/page[1-3]/);
138
+ // expect(pages[0]).toHaveTextContent("Page 1");
139
+ // expect(pages[1]).toHaveTextContent("Page 2");
140
+ // expect(pages[2]).toHaveTextContent("Page 3");
141
+ // });
142
+ // it("handles maxWidth prop correctly", () => {
143
+ // const { container } = render(
144
+ // <StackRouter
145
+ // initialActivity={{ path: "/page1", element: <MockPage1 /> }}
146
+ // Activities={mockActivities}
147
+ // maxWidth="800px"
148
+ // />
149
+ // );
150
+ // const layout = container.querySelector('div[style*="max-width: 800px"]');
151
+ // expect(layout).toBeInTheDocument();
152
+ // });
153
+ // });
@@ -0,0 +1,2 @@
1
+ declare const BackRouter: () => import("react/jsx-runtime").JSX.Element;
2
+ export default BackRouter;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import styled from "@emotion/styled";
3
+ import { useStackRouter } from "../stack-router-provider";
4
+ const BackRouter = () => {
5
+ const { back } = useStackRouter();
6
+ return (_jsx(BackButton, { onClick: back, children: _jsx(BackIcon, {}) }));
7
+ };
8
+ export default BackRouter;
9
+ /**
10
+ styles
11
+ */
12
+ const BackButton = styled.button `
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ width: 40px;
17
+ height: 40px;
18
+ border: none;
19
+ background: none;
20
+ cursor: pointer;
21
+ padding: 8px;
22
+ border-radius: 50%;
23
+ transition: all 0.2s;
24
+ color: #000000;
25
+
26
+ &:hover {
27
+ color: #666666;
28
+ }
29
+
30
+ &:active {
31
+ color: #999999;
32
+ }
33
+ `;
34
+ const BackIcon = () => (_jsx("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M12 20L4 12L12 4", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) }));
@@ -0,0 +1,2 @@
1
+ declare const Header: () => import("react/jsx-runtime").JSX.Element;
2
+ export default Header;
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import styled from "@emotion/styled";
4
+ import BackRouter from "./back-router";
5
+ import { useStackRouter } from "../stack-router-provider";
6
+ const Header = () => {
7
+ const { currentPath, visiblePages } = useStackRouter();
8
+ const [displayPath, setDisplayPath] = useState(currentPath);
9
+ const [isVisible, setIsVisible] = useState(visiblePages.length > 1);
10
+ useEffect(() => {
11
+ const timer = setTimeout(() => {
12
+ setDisplayPath(currentPath);
13
+ setIsVisible(visiblePages.length > 1);
14
+ }, 500);
15
+ return () => clearTimeout(timer);
16
+ }, [currentPath, visiblePages]);
17
+ return (_jsxs(StyledHeader, { children: [isVisible && _jsx(BackRouter, {}), _jsx(StyledTitle, { children: displayPath })] }));
18
+ };
19
+ /**
20
+ styles
21
+ */
22
+ const StyledHeader = styled.header `
23
+ width: 100%;
24
+ height: 64px;
25
+ background-color: white;
26
+ border-bottom: 1px solid #e5e7eb;
27
+ display: flex;
28
+ align-items: center;
29
+ padding: 0 1rem;
30
+ `;
31
+ const StyledTitle = styled.h1 `
32
+ font-size: 1.25rem;
33
+ font-weight: 600;
34
+ color: #111827;
35
+ `;
36
+ export default Header;
@@ -0,0 +1,25 @@
1
+ import React from "react";
2
+ import { HistoryState, StackRouterProviderProps } from "../type";
3
+ interface StackRouterContextProps {
4
+ currentPath: string;
5
+ currentState: HistoryState;
6
+ Activities: {
7
+ path: string;
8
+ element: React.ReactNode;
9
+ }[];
10
+ initialActivity: {
11
+ path: string;
12
+ element: React.ReactNode;
13
+ };
14
+ push: (path: string, data?: any) => void;
15
+ replace: (path: string, data?: any) => void;
16
+ back: () => void;
17
+ forward: () => void;
18
+ visiblePages: {
19
+ path: string;
20
+ element: React.ReactNode;
21
+ }[];
22
+ }
23
+ export declare const useStackRouter: () => StackRouterContextProps;
24
+ declare const StackRouter: ({ maxWidth, Activities, initialActivity, }: StackRouterProviderProps) => import("react/jsx-runtime").JSX.Element;
25
+ export default StackRouter;
@@ -0,0 +1,211 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useState } from "react";
3
+ import { history } from "../history";
4
+ import styled from "@emotion/styled";
5
+ import { keyframes, css } from "@emotion/react";
6
+ import { HistoryAction, Direction, } from "../type";
7
+ const StackRouterContext = createContext(null);
8
+ export const useStackRouter = () => {
9
+ const context = useContext(StackRouterContext);
10
+ if (!context) {
11
+ throw new Error("useStackRouter must be used within a StackRouterProvider");
12
+ }
13
+ return context;
14
+ };
15
+ const StackRouter = ({ maxWidth = "600px", Activities = [], initialActivity, }) => {
16
+ const [currentPath, setCurrentPath] = useState(history.getCurrentState().path);
17
+ const [currentState, setCurrentState] = useState(history.getCurrentState());
18
+ const [isTransitioning, setIsTransitioning] = useState(false);
19
+ const [direction, setDirection] = useState(Direction.FORWARD);
20
+ const [visiblePages, setVisiblePages] = useState([]);
21
+ // initialize visible pages
22
+ useEffect(() => {
23
+ setVisiblePages([
24
+ {
25
+ path: initialActivity.path,
26
+ element: initialActivity.element,
27
+ isEntering: true,
28
+ },
29
+ ]);
30
+ }, [initialActivity]);
31
+ // listen to history changes
32
+ useEffect(() => {
33
+ const removeListener = history.addListener((state) => {
34
+ if (!isTransitioning || direction !== Direction.BACKWARD) {
35
+ setCurrentPath(state?.path);
36
+ setCurrentState(state);
37
+ }
38
+ setIsTransitioning(true);
39
+ setDirection(state?.action === HistoryAction.POP
40
+ ? Direction.BACKWARD
41
+ : Direction.FORWARD);
42
+ });
43
+ return () => {
44
+ removeListener();
45
+ };
46
+ }, [isTransitioning, direction]);
47
+ // handle back
48
+ const handleBack = () => {
49
+ setIsTransitioning(true);
50
+ setDirection(Direction.BACKWARD);
51
+ setTimeout(() => {
52
+ setVisiblePages((prev) => prev.slice(0, -1));
53
+ }, 500);
54
+ history.back();
55
+ };
56
+ // update current path
57
+ useEffect(() => {
58
+ if (visiblePages.length > 0) {
59
+ setCurrentPath(visiblePages[visiblePages.length - 1].path);
60
+ }
61
+ }, [visiblePages]);
62
+ // handle push
63
+ const handlePush = (path, data) => {
64
+ setIsTransitioning(true);
65
+ setDirection(Direction.FORWARD);
66
+ const currentActivity = Activities.find((a) => a.path === path);
67
+ if (currentActivity) {
68
+ setVisiblePages((prev) => {
69
+ const existingIndex = prev.findIndex((a) => a.path === path);
70
+ if (existingIndex !== -1) {
71
+ const newActivities = [...prev];
72
+ const [removed] = newActivities.splice(existingIndex, 1);
73
+ return [...newActivities, { ...removed, isExiting: true }];
74
+ }
75
+ return [
76
+ ...prev,
77
+ { path, element: currentActivity.element, isExiting: true },
78
+ ];
79
+ });
80
+ setTimeout(() => {
81
+ setCurrentPath(path);
82
+ }, 500);
83
+ }
84
+ history.push(path, data);
85
+ };
86
+ const value = {
87
+ currentPath,
88
+ currentState,
89
+ initialActivity,
90
+ Activities,
91
+ visiblePages,
92
+ push: handlePush,
93
+ replace: history.replace,
94
+ back: handleBack,
95
+ forward: history.forward,
96
+ };
97
+ return (_jsx(StackRouterContext.Provider, { value: value, children: _jsx(LayoutContainer, { children: _jsx(Layout, { maxWidth: maxWidth, as: "div", "data-testid": "layout", children: _jsx(PageContainer, { children: visiblePages.map((page, index) => (_jsx(PageWrapper, { isTransitioning: isTransitioning, direction: direction, currentPage: index === visiblePages.length - 1, isEntering: page.isEntering, isExiting: page.isExiting, zIndex: index, children: page.element }, `${page.path}-${index}`))) }) }) }) }));
98
+ };
99
+ export default StackRouter;
100
+ /**styles */
101
+ const LayoutContainer = styled.div `
102
+ width: 100%;
103
+ height: 100vh;
104
+ display: flex;
105
+ justify-content: center;
106
+ background: white;
107
+ `;
108
+ const Layout = styled.div `
109
+ width: 100%;
110
+ max-width: ${({ maxWidth }) => maxWidth};
111
+ height: 100%;
112
+ position: relative;
113
+ overflow: hidden;
114
+ `;
115
+ const PageContainer = styled.div `
116
+ position: relative;
117
+ width: 100%;
118
+ height: 100%;
119
+ overflow: hidden;
120
+ background: white;
121
+ `;
122
+ const slideInRight = keyframes `
123
+ from {
124
+ transform: translateX(100%);
125
+ opacity: 0.2;
126
+ }
127
+ to {
128
+ transform: translateX(0);
129
+ opacity: 1;
130
+ }
131
+ `;
132
+ const slideOutLeft = keyframes `
133
+ from {
134
+ transform: translateX(0);
135
+ opacity: 1;
136
+ }
137
+ to {
138
+ transform: translateX(-30%);
139
+ opacity: 0.2;
140
+ }
141
+ `;
142
+ const slideInLeft = keyframes `
143
+ from {
144
+ transform: translateX(-30%);
145
+ opacity: 0.2;
146
+ }
147
+ to {
148
+ transform: translateX(0);
149
+ opacity: 1;
150
+ }
151
+ `;
152
+ const slideOutRight = keyframes `
153
+ from {
154
+ transform: translateX(0);
155
+ opacity: 1;
156
+ }
157
+ to {
158
+ transform: translateX(100%);
159
+ opacity: 0.2;
160
+ }
161
+ `;
162
+ const PageWrapper = styled.div `
163
+ position: absolute;
164
+ top: 0;
165
+ left: 0;
166
+ width: 100%;
167
+ height: 100%;
168
+ background: white;
169
+ cursor: grab;
170
+ user-select: none;
171
+ -webkit-user-select: none;
172
+ -webkit-touch-callout: none;
173
+ -webkit-tap-highlight-color: transparent;
174
+ z-index: ${({ zIndex }) => zIndex};
175
+ will-change: transform, opacity;
176
+ transform: translate3d(0, 0, 0);
177
+ animation: ${({ isTransitioning, direction, currentPage, isEntering, isExiting, }) => {
178
+ if (!isTransitioning)
179
+ return "none";
180
+ if (direction === "forward") {
181
+ if (isExiting) {
182
+ return css `
183
+ ${slideInRight} 0.7s ease-in-out forwards
184
+ `;
185
+ }
186
+ if (isEntering) {
187
+ return css `
188
+ ${slideOutLeft} 0.7s ease-in-out forwards
189
+ `;
190
+ }
191
+ }
192
+ else {
193
+ if (isExiting) {
194
+ return css `
195
+ ${slideOutRight} 0.7s ease-in-out forwards
196
+ `;
197
+ }
198
+ if (isEntering) {
199
+ return css `
200
+ ${slideInLeft} 0.7s ease-in-out forwards
201
+ `;
202
+ }
203
+ }
204
+ return "none";
205
+ }};
206
+ animation-fill-mode: forwards;
207
+
208
+ &:active {
209
+ cursor: grabbing;
210
+ }
211
+ `;
@@ -0,0 +1,34 @@
1
+ export interface StackRouterProviderProps {
2
+ maxWidth?: string;
3
+ isShowHeader?: boolean;
4
+ Activities?: {
5
+ path: string;
6
+ element: React.ReactNode;
7
+ }[];
8
+ initialActivity: {
9
+ path: string;
10
+ element: React.ReactNode;
11
+ };
12
+ }
13
+ export interface VisiblePage {
14
+ path: string;
15
+ element: React.ReactNode;
16
+ isEntering?: boolean;
17
+ isExiting?: boolean;
18
+ }
19
+ export declare enum Direction {
20
+ FORWARD = "forward",
21
+ BACKWARD = "backward"
22
+ }
23
+ export type DirectionType = Direction.FORWARD | Direction.BACKWARD;
24
+ export declare enum HistoryAction {
25
+ PUSH = "PUSH",
26
+ POP = "POP",
27
+ REPLACE = "REPLACE"
28
+ }
29
+ export interface HistoryState {
30
+ path: string;
31
+ data?: any;
32
+ action?: HistoryAction.PUSH | HistoryAction.POP | HistoryAction.REPLACE;
33
+ }
34
+ export type HistoryListener = (state: HistoryState) => void;
@@ -0,0 +1,11 @@
1
+ export var Direction;
2
+ (function (Direction) {
3
+ Direction["FORWARD"] = "forward";
4
+ Direction["BACKWARD"] = "backward";
5
+ })(Direction || (Direction = {}));
6
+ export var HistoryAction;
7
+ (function (HistoryAction) {
8
+ HistoryAction["PUSH"] = "PUSH";
9
+ HistoryAction["POP"] = "POP";
10
+ HistoryAction["REPLACE"] = "REPLACE";
11
+ })(HistoryAction || (HistoryAction = {}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahnseungchan/webviewkit",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Core WebView functionality for stack-based navigation",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -8,6 +8,9 @@
8
8
  "files": [
9
9
  "dist"
10
10
  ],
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
11
14
  "scripts": {
12
15
  "build": "tsc",
13
16
  "test": "jest src/__tests__",
@@ -57,8 +60,5 @@
57
60
  },
58
61
  "dependencies": {
59
62
  "gsap": "^3.13.0"
60
- },
61
- "publishConfig": {
62
- "access": "public"
63
63
  }
64
64
  }