@ahnseungchan/webviewkit 2.0.0 → 2.2.0

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/dist/index.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  // src/constants.ts
2
- var STACK_TRANSITION_MS = 300;
2
+ var STACK_TRANSITION_MS = 500;
3
3
  var TRANSITION_END_GRACE_MS = 50;
4
4
  var BACK_TIMEOUT_MS = 1e3;
5
5
  var BRIDGE_TIMEOUT_MS = 5e3;
6
6
  var BRIDGE_HOST_WAIT_MS = 3e3;
7
7
  var BRIDGE_HOST_POLL_MS = 100;
8
+ var KEEP_ALIVE_LIMIT = 5;
9
+ var DEV_MOUNTED_PAGES_WARN = 15;
8
10
 
9
11
  // src/bridge/detect-host.ts
10
12
  var FLUTTER_READY_EVENT = "flutterInAppWebViewPlatformReady";
@@ -779,19 +781,61 @@ import styled4 from "@emotion/styled";
779
781
 
780
782
  // src/stack-router/stack-router-provider.tsx
781
783
  import styled3 from "@emotion/styled";
782
- import { createContext as createContext2, useContext as useContext2, useMemo } from "react";
784
+ import {
785
+ createContext as createContext2,
786
+ useContext as useContext3,
787
+ useEffect as useEffect6,
788
+ useMemo,
789
+ useRef as useRef6
790
+ } from "react";
783
791
 
784
792
  // src/stack-router/_components/header.tsx
785
793
  import styled from "@emotion/styled";
794
+ import { useContext as useContext2 } from "react";
795
+
796
+ // src/stack-router/activity-context.tsx
797
+ import { createContext, useContext, useEffect as useEffect2, useRef as useRef2 } from "react";
798
+ var ActivityContext = createContext(null);
799
+ function useActivity() {
800
+ const context = useContext(ActivityContext);
801
+ if (!context) {
802
+ throw new Error(
803
+ "useActivity must be used inside a StackRouterProvider page"
804
+ );
805
+ }
806
+ return context;
807
+ }
808
+ function useParams() {
809
+ return useActivity().params;
810
+ }
811
+ function useOnRevive(callback) {
812
+ const { revision, data } = useActivity();
813
+ const callbackRef = useRef2(callback);
814
+ callbackRef.current = callback;
815
+ const lastRevisionRef = useRef2(revision);
816
+ useEffect2(() => {
817
+ if (revision > lastRevisionRef.current) callbackRef.current(data);
818
+ lastRevisionRef.current = revision;
819
+ }, [revision, data]);
820
+ }
821
+
822
+ // src/stack-router/_components/header.tsx
786
823
  import { jsx as jsx3, jsxs as jsxs2 } from "@emotion/react/jsx-runtime";
787
824
  var Header = ({ title }) => {
825
+ var _a;
826
+ const activity = useContext2(ActivityContext);
788
827
  const { currentPath: currentPath2, canGoBack } = useStackRouter();
828
+ const path = (_a = activity == null ? void 0 : activity.path) != null ? _a : currentPath2;
829
+ const showBack = activity ? activity.index > 0 : canGoBack;
789
830
  return /* @__PURE__ */ jsxs2(StyledHeader, { children: [
790
- canGoBack && /* @__PURE__ */ jsx3(back_router_default, {}),
791
- /* @__PURE__ */ jsx3(StyledTitle, { children: title != null ? title : currentPath2 })
831
+ showBack && /* @__PURE__ */ jsx3(back_router_default, {}),
832
+ /* @__PURE__ */ jsx3(StyledTitle, { children: title != null ? title : path })
792
833
  ] });
793
834
  };
794
835
  var StyledHeader = styled.header`
836
+ position: sticky;
837
+ top: 0;
838
+ z-index: 1;
795
839
  width: 100%;
796
840
  height: 64px;
797
841
  flex-shrink: 0;
@@ -804,32 +848,48 @@ var StyledHeader = styled.header`
804
848
  `;
805
849
  var StyledTitle = styled.h1`
806
850
  margin: 0;
851
+ min-width: 0;
807
852
  font-size: 1.25rem;
808
853
  font-weight: 600;
809
854
  color: var(--wvk-header-color, #111827);
855
+ white-space: nowrap;
856
+ overflow: hidden;
857
+ text-overflow: ellipsis;
810
858
  `;
811
859
  var header_default = Header;
812
860
 
813
861
  // src/stack-router/_components/stack-page.tsx
814
862
  import { css, keyframes } from "@emotion/react";
815
863
  import styled2 from "@emotion/styled";
816
- import { useLayoutEffect, useRef as useRef2 } from "react";
864
+ import { useLayoutEffect, useRef as useRef3 } from "react";
817
865
  import { jsx as jsx4 } from "@emotion/react/jsx-runtime";
818
- var slideInRight = keyframes`
819
- from { transform: translate3d(100%, 0, 0); }
820
- to { transform: translate3d(0, 0, 0); }
821
- `;
822
- var slideOutLeft = keyframes`
823
- from { transform: translate3d(0, 0, 0); opacity: 1; }
824
- to { transform: translate3d(-30%, 0, 0); opacity: 0.6; }
866
+ var IOS_EASING = "cubic-bezier(0.32, 0.72, 0, 1)";
867
+ var FADE_EASING = "ease";
868
+ var PARALLAX_OFFSET = "-30%";
869
+ var EDGE_SHADOW = "var(--wvk-edge-shadow, -6px 0 24px rgba(0, 0, 0, 0.12))";
870
+ var coverIn = keyframes`
871
+ from { transform: translate3d(100%, 0, 0); box-shadow: none; }
872
+ to { transform: translate3d(0, 0, 0); box-shadow: ${EDGE_SHADOW}; }
825
873
  `;
826
- var slideInLeft = keyframes`
827
- from { transform: translate3d(-30%, 0, 0); opacity: 0.6; }
828
- to { transform: translate3d(0, 0, 0); opacity: 1; }
874
+ var coverOut = keyframes`
875
+ from { transform: translate3d(0, 0, 0); box-shadow: ${EDGE_SHADOW}; }
876
+ to { transform: translate3d(100%, 0, 0); box-shadow: none; }
829
877
  `;
830
- var slideOutRight = keyframes`
878
+ var parallaxOut = keyframes`
831
879
  from { transform: translate3d(0, 0, 0); }
832
- to { transform: translate3d(100%, 0, 0); }
880
+ to { transform: translate3d(${PARALLAX_OFFSET}, 0, 0); }
881
+ `;
882
+ var parallaxIn = keyframes`
883
+ from { transform: translate3d(${PARALLAX_OFFSET}, 0, 0); }
884
+ to { transform: translate3d(0, 0, 0); }
885
+ `;
886
+ var dimIn = keyframes`
887
+ from { opacity: 0; }
888
+ to { opacity: 1; }
889
+ `;
890
+ var dimOut = keyframes`
891
+ from { opacity: 1; }
892
+ to { opacity: 0; }
833
893
  `;
834
894
  var fadeIn = keyframes`
835
895
  from { opacity: 0; }
@@ -839,14 +899,26 @@ var fadeOut = keyframes`
839
899
  from { opacity: 1; }
840
900
  to { opacity: 0; }
841
901
  `;
842
- var SLIDE_KEYFRAMES = {
843
- push: { enter: slideInRight, exit: slideOutLeft },
844
- pop: { enter: slideInLeft, exit: slideOutRight }
845
- };
846
- var FADE_KEYFRAMES = { enter: fadeIn, exit: fadeOut };
847
- function pickKeyframes(transition, direction, pageState) {
848
- if (transition === "fade") return FADE_KEYFRAMES[pageState];
849
- return SLIDE_KEYFRAMES[direction][pageState];
902
+ var layerOf = (direction, pageState) => direction === "push" === (pageState === "enter") ? "upper" : "lower";
903
+ function slideStyles(direction, pageState, duration) {
904
+ const timing = `${duration}ms ${IOS_EASING} forwards`;
905
+ if (layerOf(direction, pageState) === "upper") {
906
+ return css`
907
+ animation: ${direction === "push" ? coverIn : coverOut} ${timing};
908
+ `;
909
+ }
910
+ return css`
911
+ animation: ${direction === "push" ? parallaxOut : parallaxIn} ${timing};
912
+ &::after {
913
+ content: "";
914
+ /* transform 중인 페이지 기준 fixed = 스크롤과 무관하게 페이지 전체를 덮음 */
915
+ position: fixed;
916
+ inset: 0;
917
+ pointer-events: none;
918
+ background: var(--wvk-dim-color, rgba(0, 0, 0, 0.12));
919
+ animation: ${direction === "push" ? dimIn : dimOut} ${timing};
920
+ }
921
+ `;
850
922
  }
851
923
  var PageWrapper = styled2.div`
852
924
  position: absolute;
@@ -855,14 +927,20 @@ var PageWrapper = styled2.div`
855
927
  -webkit-overflow-scrolling: touch;
856
928
  background: var(--wvk-page-bg, #ffffff);
857
929
  z-index: ${({ zIndex }) => zIndex};
858
- visibility: ${({ pageState }) => pageState === "covered" ? "hidden" : "visible"};
930
+ visibility: ${({ pageState }) => pageState === "covered" || pageState === "cached" ? "hidden" : "visible"};
859
931
  ${({ pageState, direction, transitionType, duration }) => {
860
932
  if (pageState !== "enter" && pageState !== "exit") return "";
861
933
  if (transitionType === "none" || duration === 0) return "";
934
+ if (transitionType === "fade") {
935
+ return css`
936
+ will-change: opacity;
937
+ animation: ${pageState === "enter" ? fadeIn : fadeOut} ${duration}ms
938
+ ${FADE_EASING} forwards;
939
+ `;
940
+ }
862
941
  return css`
863
- will-change: transform, opacity;
864
- animation: ${pickKeyframes(transitionType, direction, pageState)}
865
- ${duration}ms ease-in-out forwards;
942
+ will-change: transform;
943
+ ${slideStyles(direction, pageState, duration)}
866
944
  `;
867
945
  }}
868
946
  `;
@@ -872,7 +950,7 @@ function StackPage({
872
950
  children,
873
951
  ...wrapperProps
874
952
  }) {
875
- const ref = useRef2(null);
953
+ const ref = useRef3(null);
876
954
  useLayoutEffect(() => {
877
955
  var _a;
878
956
  (_a = ref.current) == null ? void 0 : _a.toggleAttribute("inert", !isTop);
@@ -885,39 +963,126 @@ function StackPage({
885
963
  "data-state": wrapperProps.pageState,
886
964
  "aria-hidden": isTop ? void 0 : true,
887
965
  onAnimationEnd: (event) => {
888
- if (event.target === event.currentTarget) onAnimationEnd == null ? void 0 : onAnimationEnd();
966
+ if (event.target === event.currentTarget && !event.pseudoElement) {
967
+ onAnimationEnd == null ? void 0 : onAnimationEnd();
968
+ }
889
969
  },
890
970
  children
891
971
  }
892
972
  );
893
973
  }
894
974
 
895
- // src/stack-router/activity-context.tsx
896
- import { createContext, useContext } from "react";
897
- var ActivityContext = createContext(null);
898
- function useActivity() {
899
- const context = useContext(ActivityContext);
900
- if (!context) {
901
- throw new Error(
902
- "useActivity must be used inside a StackRouterProvider page"
903
- );
904
- }
905
- return context;
906
- }
907
- function useParams() {
908
- return useActivity().params;
909
- }
910
-
911
975
  // src/stack-router/stack-reducer.ts
912
976
  var topOf = (entries) => entries[entries.length - 1];
913
- function createStackState(entry) {
914
- return { entries: [entry], leaving: [], transition: null };
977
+ var bumpRevision = (entry) => {
978
+ var _a;
979
+ return {
980
+ ...entry,
981
+ revision: ((_a = entry.revision) != null ? _a : 0) + 1
982
+ };
983
+ };
984
+ function createStackState(entry, cacheLimit = 0) {
985
+ return {
986
+ entries: [entry],
987
+ leaving: [],
988
+ transition: null,
989
+ cached: [],
990
+ cacheLimit
991
+ };
992
+ }
993
+ function cacheEntries(state, removed) {
994
+ const { entries, cached, cacheLimit } = state;
995
+ const incoming = removed.filter((entry) => entry.keepAlive);
996
+ if (cacheLimit <= 0 || incoming.length === 0) return cached;
997
+ const incomingPaths = new Set(incoming.map((entry) => entry.path));
998
+ const merged = [
999
+ ...cached.filter((entry) => !incomingPaths.has(entry.path)),
1000
+ ...incoming
1001
+ ];
1002
+ const capacity = cacheLimit - entries.filter((e) => e.keepAlive).length;
1003
+ return capacity > 0 ? merged.slice(-capacity) : [];
1004
+ }
1005
+ function withoutPath(list, path) {
1006
+ return list.filter((entry) => entry.path !== path);
915
1007
  }
1008
+ function flushLeaving(state, nextEntries) {
1009
+ const alive = new Set(nextEntries.map((entry) => entry.key));
1010
+ const removed = state.leaving.filter((entry) => !alive.has(entry.key));
1011
+ return cacheEntries({ ...state, entries: nextEntries }, removed);
1012
+ }
1013
+ function revive(state, action) {
1014
+ const { direction, index, animate } = action;
1015
+ const entry = bumpRevision(action.entry);
1016
+ const previousTop = topOf(state.entries);
1017
+ const entries = [...state.entries.slice(0, index), entry];
1018
+ const removed = state.entries.slice(index);
1019
+ const cached = state.cached.filter((item) => item.key !== entry.key);
1020
+ const base = { ...state, entries, cached };
1021
+ if (!animate || direction === "pop" && removed.length === 0) {
1022
+ return {
1023
+ ...base,
1024
+ cached: cacheEntries(base, removed),
1025
+ leaving: [],
1026
+ transition: null
1027
+ };
1028
+ }
1029
+ if (direction === "push") {
1030
+ const stillInStack = !removed.includes(previousTop);
1031
+ return {
1032
+ ...base,
1033
+ cached: cacheEntries(
1034
+ base,
1035
+ removed.filter((item) => item !== previousTop)
1036
+ ),
1037
+ leaving: stillInStack ? [] : [previousTop],
1038
+ transition: {
1039
+ type: "push",
1040
+ enterKey: entry.key,
1041
+ exitKey: previousTop.key
1042
+ }
1043
+ };
1044
+ }
1045
+ return {
1046
+ ...base,
1047
+ leaving: removed,
1048
+ transition: {
1049
+ type: "pop",
1050
+ enterKey: entry.key,
1051
+ exitKeys: removed.map((item) => item.key)
1052
+ }
1053
+ };
1054
+ }
1055
+ function findRevivable(state, path, opts = {}) {
1056
+ const matches = (entry) => entry.path === path;
1057
+ const fromStack = opts.includeRemovable ? [...state.entries.slice(1)].reverse().find((entry) => entry.keepAlive && matches(entry)) : void 0;
1058
+ return fromStack != null ? fromStack : [...state.cached].reverse().find((entry) => matches(entry));
1059
+ }
1060
+ var OVERWRITES_LEAVING = /* @__PURE__ */ new Set([
1061
+ "PUSH",
1062
+ "POP_TO",
1063
+ "RESET",
1064
+ "RESET_PUSH",
1065
+ "RESTORE",
1066
+ "REVIVE"
1067
+ ]);
916
1068
  function stackReducer(state, action) {
1069
+ if (!OVERWRITES_LEAVING.has(action.type) || state.leaving.length === 0) {
1070
+ return reduce(state, action);
1071
+ }
1072
+ const settled = { ...state, leaving: [] };
1073
+ const next = reduce(settled, action);
1074
+ if (next === settled) return state;
1075
+ return reduce(
1076
+ { ...settled, cached: flushLeaving(state, next.entries) },
1077
+ action
1078
+ );
1079
+ }
1080
+ function reduce(state, action) {
917
1081
  switch (action.type) {
918
1082
  case "PUSH": {
919
1083
  const previousTop = topOf(state.entries);
920
1084
  return {
1085
+ ...state,
921
1086
  entries: [...state.entries, action.entry],
922
1087
  leaving: [],
923
1088
  transition: action.animate ? {
@@ -927,20 +1092,34 @@ function stackReducer(state, action) {
927
1092
  } : null
928
1093
  };
929
1094
  }
930
- case "REPLACE":
1095
+ case "REPLACE": {
1096
+ const entries = [...state.entries.slice(0, -1), action.entry];
1097
+ const { path } = action.entry;
931
1098
  return {
932
1099
  ...state,
933
- entries: [...state.entries.slice(0, -1), action.entry]
1100
+ entries,
1101
+ cached: cacheEntries(
1102
+ { ...state, entries, cached: withoutPath(state.cached, path) },
1103
+ withoutPath([topOf(state.entries)], path)
1104
+ )
934
1105
  };
1106
+ }
935
1107
  case "POP_TO": {
936
1108
  const index = Math.max(0, action.index);
937
1109
  if (index >= state.entries.length - 1) return state;
938
1110
  const entries = state.entries.slice(0, index + 1);
939
1111
  const leaving = state.entries.slice(index + 1);
940
1112
  if (!action.animate) {
941
- return { entries, leaving: [], transition: null };
1113
+ return {
1114
+ ...state,
1115
+ entries,
1116
+ leaving: [],
1117
+ transition: null,
1118
+ cached: cacheEntries({ ...state, entries }, leaving)
1119
+ };
942
1120
  }
943
1121
  return {
1122
+ ...state,
944
1123
  entries,
945
1124
  leaving,
946
1125
  transition: {
@@ -950,21 +1129,80 @@ function stackReducer(state, action) {
950
1129
  }
951
1130
  };
952
1131
  }
953
- case "RESET":
954
- return createStackState(action.entry);
1132
+ case "RESET": {
1133
+ const next = createStackState(action.entry, state.cacheLimit);
1134
+ const { path } = action.entry;
1135
+ return {
1136
+ ...next,
1137
+ cached: cacheEntries(
1138
+ {
1139
+ ...state,
1140
+ entries: next.entries,
1141
+ cached: withoutPath(state.cached, path)
1142
+ },
1143
+ withoutPath(state.entries, path)
1144
+ )
1145
+ };
1146
+ }
1147
+ case "REVIVE":
1148
+ return revive(state, action);
1149
+ case "RESET_PUSH": {
1150
+ const [root] = state.entries;
1151
+ const previousTop = topOf(state.entries);
1152
+ const reusedKey = action.entry.key;
1153
+ const isReused = [...state.entries, ...state.cached].some(
1154
+ (entry) => entry.key === reusedKey
1155
+ );
1156
+ const entries = [
1157
+ root,
1158
+ isReused ? bumpRevision(action.entry) : action.entry
1159
+ ];
1160
+ const sameTop = previousTop.key === reusedKey;
1161
+ const dropped = (action.animate ? state.entries.slice(1, -1) : state.entries.slice(1)).filter((entry) => entry.key !== reusedKey);
1162
+ const cached = state.cached.filter((entry) => entry.key !== reusedKey);
1163
+ const exiting = action.animate && !sameTop;
1164
+ return {
1165
+ ...state,
1166
+ entries,
1167
+ cached: cacheEntries({ ...state, entries, cached }, dropped),
1168
+ leaving: exiting ? [previousTop] : [],
1169
+ transition: exiting ? {
1170
+ type: "push",
1171
+ enterKey: action.entry.key,
1172
+ exitKey: previousTop.key
1173
+ } : null
1174
+ };
1175
+ }
955
1176
  case "RESTORE":
956
- return { entries: action.entries, leaving: [], transition: null };
957
- case "END_TRANSITION":
1177
+ return {
1178
+ ...state,
1179
+ entries: action.entries,
1180
+ leaving: [],
1181
+ transition: null
1182
+ };
1183
+ case "END_TRANSITION": {
958
1184
  if (!state.transition && state.leaving.length === 0) return state;
959
- return { ...state, leaving: [], transition: null };
1185
+ return {
1186
+ ...state,
1187
+ leaving: [],
1188
+ transition: null,
1189
+ cached: flushLeaving(state, state.entries)
1190
+ };
1191
+ }
960
1192
  default:
961
1193
  return state;
962
1194
  }
963
1195
  }
964
1196
  function getRenderedPages(state) {
965
- const { entries, leaving, transition } = state;
1197
+ const { entries, leaving, transition, cached } = state;
966
1198
  const top = topOf(entries);
967
- return [...entries, ...leaving].map((entry) => {
1199
+ const ordered = (transition == null ? void 0 : transition.type) === "push" ? [...entries.slice(0, -1), ...leaving, top] : [...entries, ...leaving];
1200
+ const cachedPages = cached.map((entry) => ({
1201
+ entry,
1202
+ pageState: "cached",
1203
+ isTop: false
1204
+ }));
1205
+ const stackPages = ordered.map((entry) => {
968
1206
  const isTop = entry.key === top.key;
969
1207
  let pageState = isTop ? "idle" : "covered";
970
1208
  if ((transition == null ? void 0 : transition.type) === "push") {
@@ -977,10 +1215,11 @@ function getRenderedPages(state) {
977
1215
  }
978
1216
  return { entry, pageState, isTop };
979
1217
  });
1218
+ return [...cachedPages, ...stackPages];
980
1219
  }
981
1220
 
982
1221
  // src/stack-router/use-prefers-reduced-motion.ts
983
- import { useEffect as useEffect2, useState } from "react";
1222
+ import { useEffect as useEffect3, useState } from "react";
984
1223
  var QUERY = "(prefers-reduced-motion: reduce)";
985
1224
  var getMediaQuery = () => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia(QUERY) : null;
986
1225
  function usePrefersReducedMotion() {
@@ -990,7 +1229,7 @@ function usePrefersReducedMotion() {
990
1229
  return (_b = (_a = getMediaQuery()) == null ? void 0 : _a.matches) != null ? _b : false;
991
1230
  }
992
1231
  );
993
- useEffect2(() => {
1232
+ useEffect3(() => {
994
1233
  var _a;
995
1234
  const mediaQuery = getMediaQuery();
996
1235
  if (!mediaQuery) return;
@@ -1005,13 +1244,13 @@ function usePrefersReducedMotion() {
1005
1244
  }
1006
1245
 
1007
1246
  // src/stack-router/use-stack-bridge-sync.ts
1008
- import { useEffect as useEffect3, useRef as useRef3 } from "react";
1247
+ import { useEffect as useEffect4, useRef as useRef4 } from "react";
1009
1248
  function useStackBridgeSync(bridge, router, onBackAtRoot) {
1010
1249
  const enabled = Boolean(bridge);
1011
1250
  const options = typeof bridge === "object" ? bridge : {};
1012
- const latest = useRef3({ router, options, onBackAtRoot });
1251
+ const latest = useRef4({ router, options, onBackAtRoot });
1013
1252
  latest.current = { router, options, onBackAtRoot };
1014
- useEffect3(() => {
1253
+ useEffect4(() => {
1015
1254
  if (!enabled) return;
1016
1255
  return installBridgeHost({
1017
1256
  allowUrl: latest.current.options.allowUrl,
@@ -1036,7 +1275,7 @@ function useStackBridgeSync(bridge, router, onBackAtRoot) {
1036
1275
  }, [enabled]);
1037
1276
  const { depth, currentPath: currentPath2 } = router;
1038
1277
  const emit = enabled && options.emitStackChanged !== false;
1039
- useEffect3(() => {
1278
+ useEffect4(() => {
1040
1279
  if (!emit || !isNativeBridgeHost(detectBridgeHost())) return;
1041
1280
  createWebViewBridgeClient().notifyStackChanged({ depth, path: currentPath2 }).catch((error) => {
1042
1281
  var _a, _b;
@@ -1046,7 +1285,7 @@ function useStackBridgeSync(bridge, router, onBackAtRoot) {
1046
1285
  }
1047
1286
 
1048
1287
  // src/stack-router/use-stack-navigation.ts
1049
- import { useCallback, useEffect as useEffect4, useReducer, useRef as useRef4 } from "react";
1288
+ import { useCallback, useEffect as useEffect5, useReducer, useRef as useRef5 } from "react";
1050
1289
 
1051
1290
  // src/stack-router/match-path.ts
1052
1291
  var splitSegments = (pathname) => pathname.split("/").filter((segment) => segment.length > 0);
@@ -1097,32 +1336,52 @@ var isDev2 = () => {
1097
1336
  var _a;
1098
1337
  return typeof process !== "undefined" && ((_a = process.env) == null ? void 0 : _a.NODE_ENV) !== "production";
1099
1338
  };
1100
- function createEntry(hit, path, data, key = createBridgeId()) {
1339
+ function createEntry(hit, path, cacheLimit, data, key = createBridgeId()) {
1101
1340
  return {
1102
1341
  key,
1103
1342
  path,
1104
1343
  pattern: hit.activity.path,
1105
1344
  params: hit.match.params,
1106
1345
  query: hit.match.query,
1107
- data
1346
+ data,
1347
+ keepAlive: hit.activity.keepAlive === true && cacheLimit > 0
1108
1348
  };
1109
1349
  }
1110
1350
  var currentUrl = () => window.location.pathname + window.location.search;
1111
1351
  function useStackNavigation(options) {
1112
- const optionsRef = useRef4(options);
1352
+ const optionsRef = useRef5(options);
1113
1353
  optionsRef.current = options;
1114
1354
  const [state, dispatch] = useReducer(stackReducer, void 0, () => {
1355
+ var _a;
1115
1356
  const { initialActivity } = options;
1357
+ const cacheLimit = (_a = options.keepAliveLimit) != null ? _a : KEEP_ALIVE_LIMIT;
1116
1358
  return createStackState(
1117
1359
  createEntry(
1118
1360
  { activity: initialActivity, match: { params: {}, query: {} } },
1119
- initialActivity.path
1120
- )
1361
+ initialActivity.path,
1362
+ cacheLimit
1363
+ ),
1364
+ cacheLimit
1121
1365
  );
1122
1366
  });
1123
- const stateRef = useRef4(state);
1367
+ const stateRef = useRef5(state);
1124
1368
  stateRef.current = state;
1125
- const busyRef = useRef4(false);
1369
+ const newEntry = (hit, path, data, key) => createEntry(hit, path, stateRef.current.cacheLimit, data, key);
1370
+ const busyRef = useRef5(false);
1371
+ const ignorePopRef = useRef5(0);
1372
+ const pendingTravelRef = useRef5(0);
1373
+ const isLocked = () => busyRef.current || pendingTravelRef.current > 0;
1374
+ const travel = (delta, ignorePop) => {
1375
+ pendingTravelRef.current += 1;
1376
+ if (ignorePop) ignorePopRef.current += 1;
1377
+ return history.travel(delta).then((popped) => {
1378
+ pendingTravelRef.current -= 1;
1379
+ if (!popped && ignorePop) {
1380
+ ignorePopRef.current = Math.max(0, ignorePopRef.current - 1);
1381
+ }
1382
+ return popped;
1383
+ });
1384
+ };
1126
1385
  const resolve = useCallback((path) => {
1127
1386
  const { activities, initialActivity } = optionsRef.current;
1128
1387
  return findActivity([...activities, initialActivity], path);
@@ -1144,27 +1403,33 @@ function useStackNavigation(options) {
1144
1403
  const animate = () => optionsRef.current.duration > 0;
1145
1404
  const push = useCallback(
1146
1405
  (path, data) => {
1147
- if (busyRef.current) return false;
1406
+ if (isLocked()) return false;
1148
1407
  const hit = resolveOrNotify(path);
1149
1408
  if (!hit) return false;
1150
- const entry = createEntry(hit, path, data);
1409
+ const { entries } = stateRef.current;
1410
+ const revived = findRevivable(stateRef.current, path);
1411
+ const entry = revived ? { ...revived, data } : newEntry(hit, path, data);
1151
1412
  busyRef.current = true;
1152
- history.push(path, {
1153
- key: entry.key,
1154
- index: stateRef.current.entries.length,
1155
- data
1156
- });
1157
- dispatch({ type: "PUSH", entry, animate: animate() });
1413
+ history.push(path, { key: entry.key, index: entries.length, data });
1414
+ dispatch(
1415
+ revived ? {
1416
+ type: "REVIVE",
1417
+ entry,
1418
+ direction: "push",
1419
+ index: entries.length,
1420
+ animate: animate()
1421
+ } : { type: "PUSH", entry, animate: animate() }
1422
+ );
1158
1423
  return true;
1159
1424
  },
1160
1425
  [resolveOrNotify]
1161
1426
  );
1162
1427
  const replace = useCallback(
1163
1428
  (path, data) => {
1164
- if (busyRef.current) return false;
1429
+ if (isLocked()) return false;
1165
1430
  const hit = resolveOrNotify(path);
1166
1431
  if (!hit) return false;
1167
- const entry = createEntry(hit, path, data);
1432
+ const entry = newEntry(hit, path, data);
1168
1433
  history.replace(path, {
1169
1434
  key: entry.key,
1170
1435
  index: stateRef.current.entries.length - 1,
@@ -1177,19 +1442,44 @@ function useStackNavigation(options) {
1177
1442
  );
1178
1443
  const reset = useCallback(
1179
1444
  (path, data) => {
1180
- if (busyRef.current) return false;
1445
+ if (isLocked()) return false;
1181
1446
  const hit = resolveOrNotify(path);
1182
1447
  if (!hit) return false;
1183
- const entry = createEntry(hit, path, data);
1184
- history.replace(path, { key: entry.key, index: 0, data });
1448
+ const entry = newEntry(hit, path, data);
1449
+ const depth = stateRef.current.entries.length;
1185
1450
  dispatch({ type: "RESET", entry });
1451
+ const commit = () => {
1452
+ history.replace(path, { key: entry.key, index: 0, data });
1453
+ };
1454
+ if (depth === 1) commit();
1455
+ else void travel(-(depth - 1), true).then(commit);
1186
1456
  return true;
1187
1457
  },
1188
1458
  [resolveOrNotify]
1189
1459
  );
1460
+ const resetPush = useCallback(
1461
+ (path, data) => {
1462
+ const { entries } = stateRef.current;
1463
+ if (entries.length === 1) return push(path, data);
1464
+ if (isLocked()) return false;
1465
+ const hit = resolveOrNotify(path);
1466
+ if (!hit) return false;
1467
+ const revivable = findRevivable(stateRef.current, path, {
1468
+ includeRemovable: true
1469
+ });
1470
+ const entry = revivable ? { ...revivable, data } : newEntry(hit, path, data);
1471
+ busyRef.current = true;
1472
+ dispatch({ type: "RESET_PUSH", entry, animate: animate() });
1473
+ void travel(-(entries.length - 1), true).then(() => {
1474
+ history.push(path, { key: entry.key, index: 1, data });
1475
+ });
1476
+ return true;
1477
+ },
1478
+ [push, resolveOrNotify]
1479
+ );
1190
1480
  const pop = useCallback((count = 1) => {
1191
1481
  var _a, _b;
1192
- if (busyRef.current) return false;
1482
+ if (isLocked()) return false;
1193
1483
  const { entries } = stateRef.current;
1194
1484
  if (entries.length <= 1) {
1195
1485
  (_b = (_a = optionsRef.current).onBackAtRoot) == null ? void 0 : _b.call(_a);
@@ -1198,12 +1488,8 @@ function useStackNavigation(options) {
1198
1488
  const delta = Math.min(Math.max(1, count), entries.length - 1);
1199
1489
  const targetIndex = entries.length - 1 - delta;
1200
1490
  busyRef.current = true;
1201
- void history.travel(-delta).then((popped) => {
1202
- if (!popped) {
1203
- busyRef.current = false;
1204
- dispatch({ type: "POP_TO", index: targetIndex, animate: false });
1205
- }
1206
- });
1491
+ dispatch({ type: "POP_TO", index: targetIndex, animate: animate() });
1492
+ void travel(-delta, false);
1207
1493
  return true;
1208
1494
  }, []);
1209
1495
  const back = useCallback(() => pop(1), [pop]);
@@ -1222,8 +1508,12 @@ function useStackNavigation(options) {
1222
1508
  const endTransition = useCallback(() => {
1223
1509
  dispatch({ type: "END_TRANSITION" });
1224
1510
  }, []);
1225
- useEffect4(() => {
1511
+ useEffect5(() => {
1226
1512
  const onPop = (historyState) => {
1513
+ if (ignorePopRef.current > 0) {
1514
+ ignorePopRef.current -= 1;
1515
+ return;
1516
+ }
1227
1517
  const { entries } = stateRef.current;
1228
1518
  const index = historyState.key ? entries.findIndex((entry2) => entry2.key === historyState.key) : -1;
1229
1519
  if (index !== -1) {
@@ -1233,12 +1523,26 @@ function useStackNavigation(options) {
1233
1523
  }
1234
1524
  return;
1235
1525
  }
1526
+ const cachedEntry = historyState.key ? stateRef.current.cached.find((item) => item.key === historyState.key) : void 0;
1527
+ const targetIndex = historyState.index;
1528
+ if (cachedEntry && typeof targetIndex === "number" && // 루트 자리(0)는 되살리지 않는다. 아래 일반 처리(reset)로 넘긴다
1529
+ targetIndex >= 1 && targetIndex <= entries.length) {
1530
+ busyRef.current = true;
1531
+ dispatch({
1532
+ type: "REVIVE",
1533
+ entry: { ...cachedEntry, data: historyState.data },
1534
+ direction: targetIndex === entries.length ? "push" : "pop",
1535
+ index: targetIndex,
1536
+ animate: animate()
1537
+ });
1538
+ return;
1539
+ }
1236
1540
  const hit = resolveOrNotify(historyState.path);
1237
1541
  if (!hit) {
1238
1542
  busyRef.current = false;
1239
1543
  return;
1240
1544
  }
1241
- const entry = createEntry(
1545
+ const entry = newEntry(
1242
1546
  hit,
1243
1547
  historyState.path,
1244
1548
  historyState.data,
@@ -1256,8 +1560,8 @@ function useStackNavigation(options) {
1256
1560
  removeListener();
1257
1561
  };
1258
1562
  }, [resolveOrNotify]);
1259
- const restoredRef = useRef4(false);
1260
- useEffect4(() => {
1563
+ const restoredRef = useRef5(false);
1564
+ useEffect5(() => {
1261
1565
  if (restoredRef.current) return;
1262
1566
  restoredRef.current = true;
1263
1567
  const root = stateRef.current.entries[0];
@@ -1268,7 +1572,7 @@ function useStackNavigation(options) {
1268
1572
  return;
1269
1573
  }
1270
1574
  const savedData = history.getCurrentState().data;
1271
- const top = createEntry(hit, url, savedData);
1575
+ const top = newEntry(hit, url, savedData);
1272
1576
  if (optionsRef.current.restoreStack === "top-only") {
1273
1577
  history.replace(url, { key: top.key, index: 0, data: savedData });
1274
1578
  dispatch({ type: "RESTORE", entries: [top] });
@@ -1278,7 +1582,7 @@ function useStackNavigation(options) {
1278
1582
  history.push(url, { key: top.key, index: 1, data: savedData });
1279
1583
  dispatch({ type: "RESTORE", entries: [root, top] });
1280
1584
  }, [resolve]);
1281
- useEffect4(() => {
1585
+ useEffect5(() => {
1282
1586
  if (!state.transition) {
1283
1587
  busyRef.current = false;
1284
1588
  return;
@@ -1289,14 +1593,24 @@ function useStackNavigation(options) {
1289
1593
  );
1290
1594
  return () => clearTimeout(timer);
1291
1595
  }, [state, endTransition]);
1292
- return { state, push, replace, reset, back, pop, popTo, endTransition };
1596
+ return {
1597
+ state,
1598
+ push,
1599
+ replace,
1600
+ reset,
1601
+ resetPush,
1602
+ back,
1603
+ pop,
1604
+ popTo,
1605
+ endTransition
1606
+ };
1293
1607
  }
1294
1608
 
1295
1609
  // src/stack-router/stack-router-provider.tsx
1296
1610
  import { jsx as jsx5, jsxs as jsxs3 } from "@emotion/react/jsx-runtime";
1297
1611
  var StackRouterContext = createContext2(null);
1298
1612
  var useStackRouter = () => {
1299
- const context = useContext2(StackRouterContext);
1613
+ const context = useContext3(StackRouterContext);
1300
1614
  if (!context) {
1301
1615
  throw new Error("useStackRouter must be used within a StackRouterProvider");
1302
1616
  }
@@ -1306,6 +1620,8 @@ var isDevEnv = () => {
1306
1620
  var _a;
1307
1621
  return typeof process !== "undefined" && ((_a = process.env) == null ? void 0 : _a.NODE_ENV) === "development";
1308
1622
  };
1623
+ var MIN_MOUNTED_PAGES = 1;
1624
+ var CACHED_ACTIVITY_INDEX = -1;
1309
1625
  var StackRouterProvider = ({
1310
1626
  initialActivity,
1311
1627
  Activities = [],
@@ -1315,6 +1631,8 @@ var StackRouterProvider = ({
1315
1631
  transition = "slide",
1316
1632
  duration = STACK_TRANSITION_MS,
1317
1633
  restoreStack = "with-root",
1634
+ keepAliveLimit = KEEP_ALIVE_LIMIT,
1635
+ maxMountedPages,
1318
1636
  safeArea = false,
1319
1637
  onBackAtRoot,
1320
1638
  onNotFound,
@@ -1328,6 +1646,7 @@ var StackRouterProvider = ({
1328
1646
  activities: Activities,
1329
1647
  duration: effectiveDuration,
1330
1648
  restoreStack,
1649
+ keepAliveLimit,
1331
1650
  onBackAtRoot,
1332
1651
  onNotFound
1333
1652
  });
@@ -1359,6 +1678,7 @@ var StackRouterProvider = ({
1359
1678
  push: navigation.push,
1360
1679
  replace: navigation.replace,
1361
1680
  reset: navigation.reset,
1681
+ resetPush: navigation.resetPush,
1362
1682
  back: navigation.back,
1363
1683
  pop: navigation.pop,
1364
1684
  popTo: navigation.popTo,
@@ -1369,11 +1689,60 @@ var StackRouterProvider = ({
1369
1689
  useStackBridgeSync(bridge, value, onBackAtRoot);
1370
1690
  const direction = (_b = (_a = state.transition) == null ? void 0 : _a.type) != null ? _b : "push";
1371
1691
  const pages = getRenderedPages(state);
1692
+ const stackIndexByKey = new Map(
1693
+ state.entries.map((entry, index) => [entry.key, index])
1694
+ );
1695
+ const activityIndexOf = (entry) => {
1696
+ const stackIndex = stackIndexByKey.get(entry.key);
1697
+ if (stackIndex !== void 0) return stackIndex;
1698
+ const leavingIndex = state.leaving.findIndex(
1699
+ (item) => item.key === entry.key
1700
+ );
1701
+ return leavingIndex === -1 ? CACHED_ACTIVITY_INDEX : state.entries.length + leavingIndex;
1702
+ };
1703
+ const mountLimit = maxMountedPages === void 0 || !Number.isFinite(maxMountedPages) ? Infinity : Math.max(MIN_MOUNTED_PAGES, maxMountedPages);
1704
+ const mountedKeysRef = useRef6(/* @__PURE__ */ new Set());
1705
+ const wasMounted = mountedKeysRef.current;
1706
+ const shouldMount = (page) => {
1707
+ const { entry, pageState } = page;
1708
+ if (pageState === "cached" || entry.keepAlive) return true;
1709
+ const stackIndex = stackIndexByKey.get(entry.key);
1710
+ if (stackIndex === void 0) return wasMounted.has(entry.key);
1711
+ if (pageState === "enter" || pageState === "exit") return true;
1712
+ return state.entries.length - stackIndex <= mountLimit;
1713
+ };
1714
+ const mountedKeys = new Set(
1715
+ pages.filter(shouldMount).map((page) => page.entry.key)
1716
+ );
1717
+ mountedKeysRef.current = mountedKeys;
1718
+ const mountedCount = mountedKeys.size;
1719
+ const domOrderRef = useRef6([]);
1720
+ const currentKeys = new Set(pages.map((page) => page.entry.key));
1721
+ const knownKeys = new Set(domOrderRef.current);
1722
+ domOrderRef.current = [
1723
+ ...domOrderRef.current.filter((key) => currentKeys.has(key)),
1724
+ ...pages.map((page) => page.entry.key).filter((key) => !knownKeys.has(key))
1725
+ ];
1726
+ const domOrder = domOrderRef.current;
1727
+ const domPages = pages.map((page, index) => ({ page, index })).sort(
1728
+ (a, b) => domOrder.indexOf(a.page.entry.key) - domOrder.indexOf(b.page.entry.key)
1729
+ );
1730
+ const hasWarnedRef = useRef6(false);
1731
+ useEffect6(() => {
1732
+ if (hasWarnedRef.current || !isDevEnv() || mountedCount <= DEV_MOUNTED_PAGES_WARN) {
1733
+ return;
1734
+ }
1735
+ hasWarnedRef.current = true;
1736
+ console.warn(
1737
+ `[webviewkit] \uB9C8\uC6B4\uD2B8\uB41C \uD398\uC774\uC9C0\uAC00 ${mountedCount}\uAC1C\uC785\uB2C8\uB2E4. \uBA54\uBAA8\uB9AC \uC0AC\uC6A9\uC774 \uCEE4\uC9C8 \uC218 \uC788\uC73C\uB2C8 maxMountedPages \uB85C \uC0C1\uD55C\uC744 \uB450\uC138\uC694.`
1738
+ );
1739
+ }, [mountedCount]);
1372
1740
  const showDevTools = enableDevTools != null ? enableDevTools : isDevEnv();
1373
1741
  return /* @__PURE__ */ jsxs3(StackRouterContext.Provider, { value, children: [
1374
- /* @__PURE__ */ jsx5(LayoutContainer, { safeArea, children: /* @__PURE__ */ jsxs3(Layout, { maxWidth, "data-testid": "layout", children: [
1375
- isShowHeader && /* @__PURE__ */ jsx5(header_default, {}),
1376
- /* @__PURE__ */ jsx5(PageContainer, { children: pages.map(({ entry, pageState, isTop }, index) => /* @__PURE__ */ jsx5(
1742
+ /* @__PURE__ */ jsx5(LayoutContainer, { safeArea, children: /* @__PURE__ */ jsx5(Layout, { maxWidth, "data-testid": "layout", children: /* @__PURE__ */ jsx5(PageContainer, { children: domPages.map(({ page, index }) => {
1743
+ var _a2;
1744
+ const { entry, pageState, isTop } = page;
1745
+ return /* @__PURE__ */ jsx5(
1377
1746
  ActivityContext.Provider,
1378
1747
  {
1379
1748
  value: {
@@ -1383,10 +1752,12 @@ var StackRouterProvider = ({
1383
1752
  params: entry.params,
1384
1753
  query: entry.query,
1385
1754
  data: entry.data,
1386
- index,
1387
- isTop
1755
+ index: activityIndexOf(entry),
1756
+ isTop,
1757
+ isCached: pageState === "cached",
1758
+ revision: (_a2 = entry.revision) != null ? _a2 : 0
1388
1759
  },
1389
- children: /* @__PURE__ */ jsx5(
1760
+ children: /* @__PURE__ */ jsxs3(
1390
1761
  StackPage,
1391
1762
  {
1392
1763
  pageState,
@@ -1396,13 +1767,16 @@ var StackRouterProvider = ({
1396
1767
  zIndex: index,
1397
1768
  isTop,
1398
1769
  onAnimationEnd: pageState === "enter" ? endTransition : void 0,
1399
- children: elementFor(entry.pattern)
1770
+ children: [
1771
+ mountedKeys.has(entry.key) && isShowHeader && /* @__PURE__ */ jsx5(header_default, {}),
1772
+ mountedKeys.has(entry.key) ? elementFor(entry.pattern) : null
1773
+ ]
1400
1774
  }
1401
1775
  )
1402
1776
  },
1403
1777
  entry.key
1404
- )) })
1405
- ] }) }),
1778
+ );
1779
+ }) }) }) }),
1406
1780
  /* @__PURE__ */ jsx5(
1407
1781
  StackDevRoadmap,
1408
1782
  {
@@ -1506,6 +1880,7 @@ export {
1506
1880
  FLUTTER_READY_EVENT,
1507
1881
  FLUTTER_UNIFIED_HANDLER,
1508
1882
  HistoryAction,
1883
+ KEEP_ALIVE_LIMIT,
1509
1884
  RN_BRIDGE_HANDLER,
1510
1885
  STACK_BRIDGE_METHODS,
1511
1886
  STACK_TRANSITION_MS,
@@ -1539,6 +1914,7 @@ export {
1539
1914
  serializeBridgeMessage,
1540
1915
  toStackPath,
1541
1916
  useActivity,
1917
+ useOnRevive,
1542
1918
  useParams,
1543
1919
  useStackRouter,
1544
1920
  waitForBridgeHost