@vkma.dev/router 0.1.0 → 0.1.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.
- package/dist/preset/app.d.ts +8 -0
- package/dist/preset/app.d.ts.map +1 -1
- package/dist/preset/app.js +36 -12
- package/dist/preset/app.js.map +1 -1
- package/dist/preset/navigator.js.map +1 -1
- package/dist/preset.d.ts +2 -2
- package/dist/preset.js +2 -2
- package/dist/vkui/vk-view.d.ts +8 -1
- package/dist/vkui/vk-view.d.ts.map +1 -1
- package/dist/vkui/vk-view.js +15 -11
- package/dist/vkui/vk-view.js.map +1 -1
- package/dist/vkui.d.ts +2 -2
- package/dist/vkui.js +2 -2
- package/package.json +1 -1
- package/src/preset/app.tsx +56 -22
- package/src/preset/navigator.ts +3 -5
- package/src/preset.ts +1 -1
- package/src/vkui/vk-view.tsx +21 -6
- package/src/vkui.ts +1 -1
package/dist/preset/app.d.ts
CHANGED
|
@@ -15,6 +15,14 @@ export declare function AppRootShell({ app, inflate }: {
|
|
|
15
15
|
app: AppHandle;
|
|
16
16
|
inflate?: boolean;
|
|
17
17
|
}): import("react").JSX.Element;
|
|
18
|
+
/**
|
|
19
|
+
* The active story key for Epic/Root shells: the view key the app author
|
|
20
|
+
* wrote in `defineView("feed")`, or null on a notFound navigation — render
|
|
21
|
+
* the NotFound screen instead of a story. Pairs with `<VkView view={…}>`
|
|
22
|
+
* whose nav defaults to the same key, so `activeStory === tab.id` compares
|
|
23
|
+
* user-authored keys on both sides.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useActiveStory(app: AppHandle): string | null;
|
|
18
26
|
/**
|
|
19
27
|
* Inflate (spike parity): a cold start (first history entry) on a panel
|
|
20
28
|
* that is NOT the first of its view replays the panel flow before it, so
|
package/dist/preset/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","names":[],"sources":["../../src/preset/app.tsx"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"app.d.ts","names":[],"sources":["../../src/preset/app.tsx"],"mappings":";;;;;;;;;;;;;wBAyDgB,eAAe,KAAK;EAAoB,KAAK;EAAW;oBAAmB,IAAA;;;;;;;;wBA4B3E,eAAe,KAAK;;;;;;;;wBAiBpB,WAAW,KAAK,WAAW"}
|
package/dist/preset/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { looseRouteOf } from "../core/loose-route.js";
|
|
2
|
-
import { VkView } from "../vkui/vk-view.js";
|
|
2
|
+
import { VkView, storyKeyOf } from "../vkui/vk-view.js";
|
|
3
3
|
import { Modals } from "../vkui/modals.js";
|
|
4
4
|
import { Popouts } from "../vkui/popouts.js";
|
|
5
5
|
import { NotFound } from "../vkui/not-found.js";
|
|
@@ -9,6 +9,24 @@ import { useEffect, useRef } from "react";
|
|
|
9
9
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
//#region src/preset/app.tsx
|
|
11
11
|
/**
|
|
12
|
+
* The view route owning the deepest matched route, or null. The walk starts
|
|
13
|
+
* at the deepest match because the chain depth varies — a modal is a masked
|
|
14
|
+
* CHILD route of its panel, so `/view:feed/feed/comments` must still resolve
|
|
15
|
+
* to the feed view. A notFound match anywhere in the chain wins over
|
|
16
|
+
* everything: the shell must show the NotFound screen, never a story
|
|
17
|
+
* fallback (an unmatched lazy panel's chunk never loads — a fallback story
|
|
18
|
+
* would spin forever).
|
|
19
|
+
*/
|
|
20
|
+
function owningViewIdOf(matches, views) {
|
|
21
|
+
if (matches.some((match) => match.status === "notFound" || match._notFound === true)) return null;
|
|
22
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
23
|
+
const routeId = matches[i]?.routeId ?? "";
|
|
24
|
+
const view = views.find((view) => routeId === view.id || routeId.startsWith(`${view.id}/`));
|
|
25
|
+
if (view !== void 0) return view.id;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
12
30
|
* Default root shell: the active view + the modal/popout hosts + inflate.
|
|
13
31
|
* The active view is the one containing the deepest matched route
|
|
14
32
|
* (single-view apps always get their only view). Multi-story apps with
|
|
@@ -21,16 +39,8 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
21
39
|
*/
|
|
22
40
|
function AppRootShell({ app, inflate = true }) {
|
|
23
41
|
useInflate(app, inflate);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const views = Object.values(app.views);
|
|
27
|
-
for (let i = state.matches.length - 1; i >= 0; i--) {
|
|
28
|
-
const routeId = state.matches[i]?.routeId ?? "";
|
|
29
|
-
const view = views.find((view) => routeId === view.id || routeId.startsWith(`${view.id}/`));
|
|
30
|
-
if (view !== void 0) return view.id;
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
33
|
-
} });
|
|
42
|
+
const views = Object.values(app.views);
|
|
43
|
+
const activeViewId = useRouterState({ select: (state) => owningViewIdOf(state.matches, views) });
|
|
34
44
|
const activeView = activeViewId === null ? void 0 : Object.values(app.views).find((view) => view.id === activeViewId);
|
|
35
45
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
36
46
|
activeView !== void 0 ? /* @__PURE__ */ jsx(VkView, { view: activeView }) : /* @__PURE__ */ jsx(NotFound, {}),
|
|
@@ -39,6 +49,20 @@ function AppRootShell({ app, inflate = true }) {
|
|
|
39
49
|
] });
|
|
40
50
|
}
|
|
41
51
|
/**
|
|
52
|
+
* The active story key for Epic/Root shells: the view key the app author
|
|
53
|
+
* wrote in `defineView("feed")`, or null on a notFound navigation — render
|
|
54
|
+
* the NotFound screen instead of a story. Pairs with `<VkView view={…}>`
|
|
55
|
+
* whose nav defaults to the same key, so `activeStory === tab.id` compares
|
|
56
|
+
* user-authored keys on both sides.
|
|
57
|
+
*/
|
|
58
|
+
function useActiveStory(app) {
|
|
59
|
+
const views = Object.values(app.views);
|
|
60
|
+
return useRouterState({ select: (state) => {
|
|
61
|
+
const viewId = owningViewIdOf(state.matches, views);
|
|
62
|
+
return viewId === null ? null : storyKeyOf(viewId);
|
|
63
|
+
} });
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
42
66
|
* Inflate (spike parity): a cold start (first history entry) on a panel
|
|
43
67
|
* that is NOT the first of its view replays the panel flow before it, so
|
|
44
68
|
* back walks the natural chain instead of closing the app. One-shot, waits
|
|
@@ -105,6 +129,6 @@ function useInflate(app, enabled = true) {
|
|
|
105
129
|
});
|
|
106
130
|
}
|
|
107
131
|
//#endregion
|
|
108
|
-
export { AppRootShell, useInflate };
|
|
132
|
+
export { AppRootShell, owningViewIdOf, useActiveStory, useInflate };
|
|
109
133
|
|
|
110
134
|
//# sourceMappingURL=app.js.map
|
package/dist/preset/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","names":[],"sources":["../../src/preset/app.tsx"],"sourcesContent":["import { useRouter, useRouterState } from \"@tanstack/react-router\";\nimport { useEffect, useRef } from \"react\";\nimport { looseRouteOf } from \"../core/loose-route.ts\";\nimport { Modals } from \"../vkui/modals.tsx\";\nimport { NotFound } from \"../vkui/not-found.tsx\";\nimport { Popouts } from \"../vkui/popouts.tsx\";\nimport { VkView } from \"../vkui/vk-view.tsx\";\nimport type { AppHandle } from \"./builders.tsx\";\nimport { deriveInflateChain, interpolatePath } from \"./inflate.ts\";\n\n/**\n * Default root shell: the active view + the modal/popout hosts + inflate.\n * The active view is the one containing the deepest matched route\n * (single-view apps always get their only view). Multi-story apps with\n * Epic/Root are built with a custom `root` shell out of the same pieces.\n *\n * An unmatched URL (global 404 or a `notFound()` thrown by `beforeLoad`)\n * renders the VKUI `NotFound` instead of a view — falling back to the\n * first view here would show its lazy panel as a spinner that never\n * resolves, because an unmatched panel's chunk is never loaded.\n */\nexport function AppRootShell({ app, inflate = true }: { app: AppHandle; inflate?: boolean }) {\n\tuseInflate(app, inflate);\n\n\tconst activeViewId = useRouterState({\n\t\tselect: state => {\n\t\t\tconst notFound = state.matches.some(\n\t\t\t\tmatch =>\n\t\t\t\t\t(match as { status?: string }).status === \"notFound\" ||\n\t\t\t\t\t(match as { _notFound?: boolean })._notFound === true,\n\t\t\t);\n\t\t\tif (notFound) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst views = Object.values(app.views);\n\t\t\tfor (let i = state.matches.length - 1; i >= 0; i--) {\n\t\t\t\tconst routeId = state.matches[i]?.routeId ?? \"\";\n\t\t\t\tconst view = views.find(\n\t\t\t\t\tview => routeId === view.id || routeId.startsWith(`${view.id}/`),\n\t\t\t\t);\n\t\t\t\tif (view !== undefined) {\n\t\t\t\t\treturn view.id;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t});\n\tconst activeView =\n\t\tactiveViewId === null\n\t\t\t? undefined\n\t\t\t: Object.values(app.views).find(view => view.id === activeViewId);\n\n\treturn (\n\t\t<>\n\t\t\t{activeView !== undefined ? <VkView view={activeView} /> : <NotFound />}\n\t\t\t<Modals entries={app.modals} />\n\t\t\t<Popouts entries={app.popouts} />\n\t\t</>\n\t);\n}\n\n/**\n * Inflate (spike parity): a cold start (first history entry) on a panel\n * that is NOT the first of its view replays the panel flow before it, so\n * back walks the natural chain instead of closing the app. One-shot, waits\n * for the router to go idle, skips warm navigations. `AppRootShell` runs\n * it automatically; custom shells (Epic/Tabbar) call the hook themselves.\n */\nexport function useInflate(app: AppHandle, enabled = true): void {\n\tconst router = useRouter();\n\tconst snapshot = useRouterState({\n\t\tselect: state => {\n\t\t\tlet panelKey: string | null = null;\n\t\t\tlet panelPath: string | null = null;\n\t\t\tconst modalPaths: string[] = [];\n\t\t\tfor (let i = state.matches.length - 1; i >= 0; i--) {\n\t\t\t\tconst routeId = state.matches[i]?.routeId;\n\t\t\t\tif (routeId === undefined) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst route = looseRouteOf(router, routeId);\n\t\t\t\tconst staticData = route?.options.staticData;\n\t\t\t\tif (staticData?.modal !== undefined && route !== undefined) {\n\t\t\t\t\t// modal levels of the deep link, deepest first (reversed below)\n\t\t\t\t\tif (route.fullPath !== undefined) {\n\t\t\t\t\t\tmodalPaths.push(route.fullPath);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (staticData?.panel !== undefined) {\n\t\t\t\t\tpanelKey = staticData.panel;\n\t\t\t\t\tpanelPath = route?.fullPath ?? null;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstatus: state.status,\n\t\t\t\tindex:\n\t\t\t\t\t(state.location.state as { __TSR_index?: number } | undefined)?.__TSR_index ??\n\t\t\t\t\t0,\n\t\t\t\thref: state.location.href,\n\t\t\t\tpanelKey,\n\t\t\t\tpanelPath,\n\t\t\t\tmodalPaths: modalPaths.toReversed(),\n\t\t\t\tdeepestParams:\n\t\t\t\t\t(\n\t\t\t\t\t\tstate.matches[state.matches.length - 1] as\n\t\t\t\t\t\t\t| { params?: Record<string, string> }\n\t\t\t\t\t\t\t| undefined\n\t\t\t\t\t)?.params ?? {},\n\t\t\t};\n\t\t},\n\t});\n\tconst done = useRef(false);\n\n\tuseEffect(() => {\n\t\tif (done.current || !enabled || snapshot.status !== \"idle\") {\n\t\t\treturn; // one-shot; retry until the router settles\n\t\t}\n\t\tdone.current = true;\n\t\tif (snapshot.index > 0 || snapshot.panelKey === null) {\n\t\t\treturn; // warm navigation or an unknown panel — nothing to inflate\n\t\t}\n\t\tconst target = app.panels[snapshot.panelKey];\n\t\tif (target === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tconst ordered = Object.values(app.panels).filter(panel => panel.viewId === target.viewId);\n\t\tlet chain = deriveInflateChain(ordered, target.routeId, snapshot.deepestParams);\n\t\t// a deep link INTO A ROUTE MODAL: the full ancestor chain must sit in\n\t\t// history below the target — the parent panel, then every modal level\n\t\t// (outer → inner) — or back skips the screens the user is logically\n\t\t// on. Only modal targets: for a panel target the parentHref IS the\n\t\t// current location and would replay a pointless no-op navigation\n\t\tif (snapshot.modalPaths.length > 0 && snapshot.panelPath !== null) {\n\t\t\tconst parentHref = interpolatePath(snapshot.panelPath, snapshot.deepestParams);\n\t\t\tif (parentHref !== null && chain[chain.length - 1] !== parentHref) {\n\t\t\t\tchain = [...chain, parentHref];\n\t\t\t}\n\t\t}\n\t\tfor (const modalPath of snapshot.modalPaths) {\n\t\t\tconst modalHref = interpolatePath(modalPath, snapshot.deepestParams);\n\t\t\tif (modalHref !== null && chain[chain.length - 1] !== modalHref) {\n\t\t\t\tchain = [...chain, modalHref];\n\t\t\t}\n\t\t}\n\t\tif (chain.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst { href: targetHref } = snapshot;\n\t\tvoid (async () => {\n\t\t\tawait router.navigate({ href: chain[0], replace: true });\n\t\t\tfor (let i = 1; i < chain.length; i++) {\n\t\t\t\tawait router.navigate({ href: chain[i] });\n\t\t\t}\n\t\t\tawait router.navigate({ href: targetHref });\n\t\t})();\n\t});\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app.js","names":[],"sources":["../../src/preset/app.tsx"],"sourcesContent":["import { useRouter, useRouterState } from \"@tanstack/react-router\";\nimport { useEffect, useRef } from \"react\";\nimport { looseRouteOf } from \"../core/loose-route.ts\";\nimport { Modals } from \"../vkui/modals.tsx\";\nimport { NotFound } from \"../vkui/not-found.tsx\";\nimport { Popouts } from \"../vkui/popouts.tsx\";\nimport { storyKeyOf, VkView } from \"../vkui/vk-view.tsx\";\nimport type { AppHandle } from \"./builders.tsx\";\nimport { deriveInflateChain, interpolatePath } from \"./inflate.ts\";\n\ninterface MatchLike {\n\trouteId?: string;\n\tstatus?: string;\n\t_notFound?: boolean;\n}\n\n/**\n * The view route owning the deepest matched route, or null. The walk starts\n * at the deepest match because the chain depth varies — a modal is a masked\n * CHILD route of its panel, so `/view:feed/feed/comments` must still resolve\n * to the feed view. A notFound match anywhere in the chain wins over\n * everything: the shell must show the NotFound screen, never a story\n * fallback (an unmatched lazy panel's chunk never loads — a fallback story\n * would spin forever).\n */\nexport function owningViewIdOf(\n\tmatches: readonly MatchLike[],\n\tviews: readonly { id: string }[],\n): string | null {\n\tconst notFound = matches.some(\n\t\tmatch =>\n\t\t\tmatch.status === \"notFound\" || (match as { _notFound?: boolean })._notFound === true,\n\t);\n\tif (notFound) {\n\t\treturn null;\n\t}\n\tfor (let i = matches.length - 1; i >= 0; i--) {\n\t\tconst routeId = matches[i]?.routeId ?? \"\";\n\t\tconst view = views.find(view => routeId === view.id || routeId.startsWith(`${view.id}/`));\n\t\tif (view !== undefined) {\n\t\t\treturn view.id;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Default root shell: the active view + the modal/popout hosts + inflate.\n * The active view is the one containing the deepest matched route\n * (single-view apps always get their only view). Multi-story apps with\n * Epic/Root are built with a custom `root` shell out of the same pieces.\n *\n * An unmatched URL (global 404 or a `notFound()` thrown by `beforeLoad`)\n * renders the VKUI `NotFound` instead of a view — falling back to the\n * first view here would show its lazy panel as a spinner that never\n * resolves, because an unmatched panel's chunk is never loaded.\n */\nexport function AppRootShell({ app, inflate = true }: { app: AppHandle; inflate?: boolean }) {\n\tuseInflate(app, inflate);\n\tconst views = Object.values(app.views);\n\n\tconst activeViewId = useRouterState({\n\t\tselect: (state): string | null => owningViewIdOf(state.matches, views),\n\t});\n\tconst activeView =\n\t\tactiveViewId === null\n\t\t\t? undefined\n\t\t\t: Object.values(app.views).find(view => view.id === activeViewId);\n\n\treturn (\n\t\t<>\n\t\t\t{activeView !== undefined ? <VkView view={activeView} /> : <NotFound />}\n\t\t\t<Modals entries={app.modals} />\n\t\t\t<Popouts entries={app.popouts} />\n\t\t</>\n\t);\n}\n\n/**\n * The active story key for Epic/Root shells: the view key the app author\n * wrote in `defineView(\"feed\")`, or null on a notFound navigation — render\n * the NotFound screen instead of a story. Pairs with `<VkView view={…}>`\n * whose nav defaults to the same key, so `activeStory === tab.id` compares\n * user-authored keys on both sides.\n */\nexport function useActiveStory(app: AppHandle): string | null {\n\tconst views = Object.values(app.views);\n\treturn useRouterState({\n\t\tselect: (state): string | null => {\n\t\t\tconst viewId = owningViewIdOf(state.matches, views);\n\t\t\treturn viewId === null ? null : storyKeyOf(viewId);\n\t\t},\n\t});\n}\n\n/**\n * Inflate (spike parity): a cold start (first history entry) on a panel\n * that is NOT the first of its view replays the panel flow before it, so\n * back walks the natural chain instead of closing the app. One-shot, waits\n * for the router to go idle, skips warm navigations. `AppRootShell` runs\n * it automatically; custom shells (Epic/Tabbar) call the hook themselves.\n */\nexport function useInflate(app: AppHandle, enabled = true): void {\n\tconst router = useRouter();\n\tconst snapshot = useRouterState({\n\t\tselect: state => {\n\t\t\tlet panelKey: string | null = null;\n\t\t\tlet panelPath: string | null = null;\n\t\t\tconst modalPaths: string[] = [];\n\t\t\tfor (let i = state.matches.length - 1; i >= 0; i--) {\n\t\t\t\tconst routeId = state.matches[i]?.routeId;\n\t\t\t\tif (routeId === undefined) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst route = looseRouteOf(router, routeId);\n\t\t\t\tconst staticData = route?.options.staticData;\n\t\t\t\tif (staticData?.modal !== undefined && route !== undefined) {\n\t\t\t\t\t// modal levels of the deep link, deepest first (reversed below)\n\t\t\t\t\tif (route.fullPath !== undefined) {\n\t\t\t\t\t\tmodalPaths.push(route.fullPath);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (staticData?.panel !== undefined) {\n\t\t\t\t\tpanelKey = staticData.panel;\n\t\t\t\t\tpanelPath = route?.fullPath ?? null;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstatus: state.status,\n\t\t\t\tindex:\n\t\t\t\t\t(state.location.state as { __TSR_index?: number } | undefined)?.__TSR_index ??\n\t\t\t\t\t0,\n\t\t\t\thref: state.location.href,\n\t\t\t\tpanelKey,\n\t\t\t\tpanelPath,\n\t\t\t\tmodalPaths: modalPaths.toReversed(),\n\t\t\t\tdeepestParams:\n\t\t\t\t\t(\n\t\t\t\t\t\tstate.matches[state.matches.length - 1] as\n\t\t\t\t\t\t\t| { params?: Record<string, string> }\n\t\t\t\t\t\t\t| undefined\n\t\t\t\t\t)?.params ?? {},\n\t\t\t};\n\t\t},\n\t});\n\tconst done = useRef(false);\n\n\tuseEffect(() => {\n\t\tif (done.current || !enabled || snapshot.status !== \"idle\") {\n\t\t\treturn; // one-shot; retry until the router settles\n\t\t}\n\t\tdone.current = true;\n\t\tif (snapshot.index > 0 || snapshot.panelKey === null) {\n\t\t\treturn; // warm navigation or an unknown panel — nothing to inflate\n\t\t}\n\t\tconst target = app.panels[snapshot.panelKey];\n\t\tif (target === undefined) {\n\t\t\treturn;\n\t\t}\n\t\tconst ordered = Object.values(app.panels).filter(panel => panel.viewId === target.viewId);\n\t\tlet chain = deriveInflateChain(ordered, target.routeId, snapshot.deepestParams);\n\t\t// a deep link INTO A ROUTE MODAL: the full ancestor chain must sit in\n\t\t// history below the target — the parent panel, then every modal level\n\t\t// (outer → inner) — or back skips the screens the user is logically\n\t\t// on. Only modal targets: for a panel target the parentHref IS the\n\t\t// current location and would replay a pointless no-op navigation\n\t\tif (snapshot.modalPaths.length > 0 && snapshot.panelPath !== null) {\n\t\t\tconst parentHref = interpolatePath(snapshot.panelPath, snapshot.deepestParams);\n\t\t\tif (parentHref !== null && chain[chain.length - 1] !== parentHref) {\n\t\t\t\tchain = [...chain, parentHref];\n\t\t\t}\n\t\t}\n\t\tfor (const modalPath of snapshot.modalPaths) {\n\t\t\tconst modalHref = interpolatePath(modalPath, snapshot.deepestParams);\n\t\t\tif (modalHref !== null && chain[chain.length - 1] !== modalHref) {\n\t\t\t\tchain = [...chain, modalHref];\n\t\t\t}\n\t\t}\n\t\tif (chain.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst { href: targetHref } = snapshot;\n\t\tvoid (async () => {\n\t\t\tawait router.navigate({ href: chain[0], replace: true });\n\t\t\tfor (let i = 1; i < chain.length; i++) {\n\t\t\t\tawait router.navigate({ href: chain[i] });\n\t\t\t}\n\t\t\tawait router.navigate({ href: targetHref });\n\t\t})();\n\t});\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigator.js","names":[],"sources":["../../src/preset/navigator.ts"],"sourcesContent":["import {\n\tuseNavigate,\n\tuseRouter,\n\tuseRouterState,\n\ttype NavigateOptions,\n} from \"@tanstack/react-router\";\nimport { looseRouteOf } from \"../core/loose-route.ts\";\nimport { MODAL_STATE_KEY } from \"../core/state-keys.ts\";\nimport { useHidePopout, useShowPopout } from \"../vkui/popouts.tsx\";\n\n/**\n * The parity navigator over TanStack primitives: push/replace/back/\n * backToFirst/showModal/hideModal/showPopout/hidePopout. Modal→modal\n * navigation REPLACES the entry (the host closes one modal before opening\n * the next — there is no modal stack); back from a modal returns to its\n * panel. `push`/`replace`/`showModal` accept full TanStack NavigateOptions\n * — typed by the registered route tree.\n */\n\nexport interface VkNavigator {\n\tpush<TParams extends Record<string, string>>(\n\t\tpage: PanelPage<TParams>,\n\t\tparams: TParams,\n\t\toptions?: Omit<NavigateOptions, \"to\" | \"params\">,\n\t): Promise<void>;\n\tpush(options: NavigateOptions): Promise<void>;\n\treplace<TParams extends Record<string, string>>(\n\t\tpage: PanelPage<TParams>,\n\t\tparams: TParams,\n\t\toptions?: Omit<NavigateOptions, \"to\" | \"params\">,\n\t): Promise<void>;\n\treplace(options: NavigateOptions): Promise<void>;\n\tback(): void;\n\t/** Pop all the way to the first history entry. */\n\tbackToFirst(): void;\n\t/** Open a modal route; replaces the entry if another modal is open.\n\t * `props` (serializable) land in history.state and are read by\n\t * `useModalProps()` inside the modal component. */\n\tshowModal(options: NavigateOptions & { props?: unknown }): Promise<void>;\n\t/** Close the current modal (back, or replace to the panel on first entry). */\n\thideModal(): void;\n\t/**\n\t * Open a global (`?modal=<id>`) modal from any panel, masking the URL so\n\t * the visible path stays at the panel. Back closes it.\n\t */\n\tshowGlobalModal(\n\t\tid: string,\n\t\toptions?: { state?: Record<string, unknown>; props?: unknown },\n\t): Promise<void>;\n\tshowPopout(id: string, props?: Record<string, unknown>): void;\n\thidePopout(): void;\n\t/**\n\t * Block navigation while `onLeave` returns true (old router's\n\t * `RouteNavigator.block`). The callback receives the current and the\n\t * next location plus the action — PUSH/REPLACE as-is, back/forward/go\n\t * mapped to POP (old-router `BlockerFunction` parity). Returns the\n\t * unblock function.\n\t */\n\tblock(onLeave: (args: BlockerArgs) => boolean): () => void;\n}\n\nfunction isModalRouteId(router: ReturnType<typeof useRouter>, routeId: string): boolean {\n\treturn looseRouteOf(router, routeId)?.options.staticData?.modal !== undefined;\n}\n\nexport interface BlockerArgs {\n\t/** The location the navigation starts from (TanStack history location). */\n\tcurrentLocation: { href: string; pathname: string };\n\t/** The location the navigation is heading to. */\n\tnextLocation: { href: string; pathname: string };\n\t/** PUSH/REPLACE as-is; back, forward and go are one POP (old-router parity). */\n\taction: \"PUSH\" | \"REPLACE\" | \"POP\";\n}\n\n/**\n * A typed page object — a panel definition (`defineView().panel(...)`) or\n * a panel handle (`app.panels[...]`). The params type flows from the\n * definition's phantom `types`, so `navigator.push(postPanel, { id })`\n * type-checks the params (old-router `Page`/`PageWithParams` parity,\n * without the `hasParams` discriminator quirk).\n */\nexport interface PanelPage<TParams = Record<string, string>> {\n\tpath: string;\n\ttypes?: { params: TParams };\n}\n\nexport function useNavigator(): VkNavigator {\n\tconst router = useRouter();\n\tconst navigate = useNavigate();\n\tconst location = useRouterState({ select: state => state.location });\n\tconst modalOpen = useRouterState({\n\t\tselect: state => state.matches.some(match => isModalRouteId(router, match.routeId)),\n\t});\n\tconst showPopout = useShowPopout();\n\tconst hidePopout = useHidePopout();\n\n\t/** `\"index\"` and an optional leading slash normalize to the full path. */\n\tconst pageTo = (page: PanelPage): string =>\n\t\tpage.path === \"index\" ? \"/\" : `/${page.path.replace(/^\\//, \"\")}`;\n\n\treturn {\n\t\tpush: ((\n\t\t\tpageOrOptions: NavigateOptions | PanelPage,\n\t\t\tparams?: Record<string, string>,\n\t\t\trest?: Omit<NavigateOptions, \"to\" | \"params\">,\n\t\t) =>\n\t\t\tparams === undefined\n\t\t\t\t? navigate({\n\t\t\t\t\t\t...(pageOrOptions as NavigateOptions),\n\t\t\t\t\t\treplace: false,\n\t\t\t\t\t})\n\t\t\t\t: navigate({\n\t\t\t\t\t\tto: pageTo(pageOrOptions as PanelPage),\n\t\t\t\t\t\tparams,\n\t\t\t\t\t\treplace: false,\n\t\t\t\t\t\t...rest,\n\t\t\t\t\t})) as VkNavigator[\"push\"],\n\t\treplace: ((\n\t\t\tpageOrOptions: NavigateOptions | PanelPage,\n\t\t\tparams?: Record<string, string>,\n\t\t\trest?: Omit<NavigateOptions, \"to\" | \"params\">,\n\t\t) =>\n\t\t\tparams === undefined\n\t\t\t\t? navigate({\n\t\t\t\t\t\t...(pageOrOptions as NavigateOptions),\n\t\t\t\t\t\treplace: true,\n\t\t\t\t\t})\n\t\t\t\t: navigate({\n\t\t\t\t\t\tto: pageTo(pageOrOptions as PanelPage),\n\t\t\t\t\t\tparams,\n\t\t\t\t\t\treplace: true,\n\t\t\t\t\t\t...rest,\n\t\t\t\t\t})) as VkNavigator[\"replace\"],\n\t\tback: () => {\n\t\t\trouter.history.back();\n\t\t},\n\t\tbackToFirst: () => {\n\t\t\tconst index =\n\t\t\t\t(location.state as { __TSR_index?: number } | undefined)?.__TSR_index ?? 0;\n\t\t\tif (index > 0) {\n\t\t\t\trouter.history.go(-index);\n\t\t\t}\n\t\t},\n\t\tshowModal: ({ props, ...options }) => {\n\t\t\t// nested modal: the target route is a CHILD of the currently open\n\t\t\t// modal — PUSH (back unwinds the stack); sibling modals keep the\n\t\t\t// old-router replace parity\n\t\t\tlet nested = false;\n\t\t\tif (modalOpen && typeof options.to === \"string\") {\n\t\t\t\tfor (let i = router.state.matches.length - 1; i >= 0; i--) {\n\t\t\t\t\tconst matchRouteId = router.state.matches[i]?.routeId;\n\t\t\t\t\tif (matchRouteId === undefined) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst route = looseRouteOf(router, matchRouteId);\n\t\t\t\t\tif (route?.options.staticData?.modal === undefined) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tnested =\n\t\t\t\t\t\troute.fullPath !== undefined && options.to.startsWith(`${route.fullPath}/`);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// TanStack types history state as its own updater shape — arbitrary\n\t\t\t// serializable keys go through a cast (same as the popout state)\n\t\t\tconst withProps =\n\t\t\t\tprops !== undefined\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tstate: {\n\t\t\t\t\t\t\t\t...(options.state as Record<string, unknown> | undefined),\n\t\t\t\t\t\t\t\t[MODAL_STATE_KEY]: { props },\n\t\t\t\t\t\t\t} as never,\n\t\t\t\t\t\t}\n\t\t\t\t\t: {};\n\t\t\treturn navigate({\n\t\t\t\t...options,\n\t\t\t\t// a modal open never changes the panel behind it — carry the\n\t\t\t\t// panel's search (?tab=…) onto the modal entry, or the covered\n\t\t\t\t// panel would snap to its default search for the whole time the\n\t\t\t\t// modal is open (and snap back on close). An explicit `search`\n\t\t\t\t// from the caller always wins.\n\t\t\t\tsearch: options.search ?? true,\n\t\t\t\treplace: nested ? false : modalOpen,\n\t\t\t\t...withProps,\n\t\t\t});\n\t\t},\n\t\thideModal: () => {\n\t\t\tif (router.history.canGoBack()) {\n\t\t\t\trouter.history.back();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// deep-linked modal on the first entry: replace down to the panel\n\t\t\tconst parentPath = location.pathname.replace(/\\/[^/]+$/, \"\") || \"/\";\n\t\t\tvoid router.navigate({ href: parentPath, replace: true });\n\t\t},\n\t\tshowPopout: (id, props) => {\n\t\t\tshowPopout(id, props);\n\t\t},\n\t\thidePopout: () => {\n\t\t\thidePopout();\n\t\t},\n\t\tshowGlobalModal: (id, { state, props, ...rest } = {}) => {\n\t\t\tconst { pathname, searchStr, hash } = router.state.location;\n\t\t\tconst params = new URLSearchParams(searchStr.replace(/^\\?/, \"\"));\n\t\t\tconst already = params.get(\"modal\") === id;\n\t\t\tparams.set(\"modal\", id);\n\t\t\tconst href = `${pathname}?${params.toString()}${hash}`;\n\t\t\t// the mask keeps the visible URL at the panel path while the modal\n\t\t\t// is open (the modal param rides in history.state)\n\t\t\tconst maskParams = new URLSearchParams(params);\n\t\t\tmaskParams.delete(\"modal\");\n\t\t\tconst maskQuery = maskParams.toString();\n\t\t\tconst maskHref = `${pathname}${maskQuery !== \"\" ? `?${maskQuery}` : \"\"}${hash}`;\n\t\t\tconst nextState =\n\t\t\t\tstate !== undefined || props !== undefined\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tstate: {\n\t\t\t\t\t\t\t\t...state,\n\t\t\t\t\t\t\t\t...(props !== undefined ? { [MODAL_STATE_KEY]: { props } } : {}),\n\t\t\t\t\t\t\t} as never,\n\t\t\t\t\t\t}\n\t\t\t\t\t: {};\n\t\t\treturn navigate({\n\t\t\t\thref,\n\t\t\t\tmask: { to: maskHref },\n\t\t\t\treplace: already || modalOpen,\n\t\t\t\t...rest,\n\t\t\t\t...nextState,\n\t\t\t});\n\t\t},\n\t\tblock: onLeave => {\n\t\t\tconst unblock = router.history.block({\n\t\t\t\tblockerFn: (args: {\n\t\t\t\t\tcurrentLocation: never;\n\t\t\t\t\tnextLocation: never;\n\t\t\t\t\taction: string;\n\t\t\t\t}) =>\n\t\t\t\t\tonLeave({\n\t\t\t\t\t\tcurrentLocation: args.currentLocation,\n\t\t\t\t\t\tnextLocation: args.nextLocation,\n\t\t\t\t\t\t// TanStack distinguishes BACK/FORWARD/GO; the old router's\n\t\t\t\t\t\t// BlockerFunction saw one POP — keep the parity\n\t\t\t\t\t\taction:\n\t\t\t\t\t\t\targs.action === \"PUSH\" || args.action === \"REPLACE\"\n\t\t\t\t\t\t\t\t? args.action\n\t\t\t\t\t\t\t\t: \"POP\",\n\t\t\t\t\t}),\n\t\t\t});\n\t\t\treturn unblock;\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"navigator.js","names":[],"sources":["../../src/preset/navigator.ts"],"sourcesContent":["import {\n\tuseNavigate,\n\tuseRouter,\n\tuseRouterState,\n\ttype NavigateOptions,\n} from \"@tanstack/react-router\";\nimport { looseRouteOf } from \"../core/loose-route.ts\";\nimport { MODAL_STATE_KEY } from \"../core/state-keys.ts\";\nimport { useHidePopout, useShowPopout } from \"../vkui/popouts.tsx\";\n\n/**\n * The parity navigator over TanStack primitives: push/replace/back/\n * backToFirst/showModal/hideModal/showPopout/hidePopout. Modal→modal\n * navigation REPLACES the entry (the host closes one modal before opening\n * the next — there is no modal stack); back from a modal returns to its\n * panel. `push`/`replace`/`showModal` accept full TanStack NavigateOptions\n * — typed by the registered route tree.\n */\n\nexport interface VkNavigator {\n\tpush<TParams extends Record<string, string>>(\n\t\tpage: PanelPage<TParams>,\n\t\tparams: TParams,\n\t\toptions?: Omit<NavigateOptions, \"to\" | \"params\">,\n\t): Promise<void>;\n\tpush(options: NavigateOptions): Promise<void>;\n\treplace<TParams extends Record<string, string>>(\n\t\tpage: PanelPage<TParams>,\n\t\tparams: TParams,\n\t\toptions?: Omit<NavigateOptions, \"to\" | \"params\">,\n\t): Promise<void>;\n\treplace(options: NavigateOptions): Promise<void>;\n\tback(): void;\n\t/** Pop all the way to the first history entry. */\n\tbackToFirst(): void;\n\t/** Open a modal route; replaces the entry if another modal is open.\n\t * `props` (serializable) land in history.state and are read by\n\t * `useModalProps()` inside the modal component. */\n\tshowModal(options: NavigateOptions & { props?: unknown }): Promise<void>;\n\t/** Close the current modal (back, or replace to the panel on first entry). */\n\thideModal(): void;\n\t/**\n\t * Open a global (`?modal=<id>`) modal from any panel, masking the URL so\n\t * the visible path stays at the panel. Back closes it.\n\t */\n\tshowGlobalModal(\n\t\tid: string,\n\t\toptions?: { state?: Record<string, unknown>; props?: unknown },\n\t): Promise<void>;\n\tshowPopout(id: string, props?: Record<string, unknown>): void;\n\thidePopout(): void;\n\t/**\n\t * Block navigation while `onLeave` returns true (old router's\n\t * `RouteNavigator.block`). The callback receives the current and the\n\t * next location plus the action — PUSH/REPLACE as-is, back/forward/go\n\t * mapped to POP (old-router `BlockerFunction` parity). Returns the\n\t * unblock function.\n\t */\n\tblock(onLeave: (args: BlockerArgs) => boolean): () => void;\n}\n\nfunction isModalRouteId(router: ReturnType<typeof useRouter>, routeId: string): boolean {\n\treturn looseRouteOf(router, routeId)?.options.staticData?.modal !== undefined;\n}\n\nexport interface BlockerArgs {\n\t/** The location the navigation starts from (TanStack history location). */\n\tcurrentLocation: { href: string; pathname: string };\n\t/** The location the navigation is heading to. */\n\tnextLocation: { href: string; pathname: string };\n\t/** PUSH/REPLACE as-is; back, forward and go are one POP (old-router parity). */\n\taction: \"PUSH\" | \"REPLACE\" | \"POP\";\n}\n\n/**\n * A typed page object — a panel definition (`defineView().panel(...)`) or\n * a panel handle (`app.panels[...]`). The params type flows from the\n * definition's phantom `types`, so `navigator.push(postPanel, { id })`\n * type-checks the params (old-router `Page`/`PageWithParams` parity,\n * without the `hasParams` discriminator quirk).\n */\nexport interface PanelPage<TParams = Record<string, string>> {\n\tpath: string;\n\ttypes?: { params: TParams };\n}\n\nexport function useNavigator(): VkNavigator {\n\tconst router = useRouter();\n\tconst navigate = useNavigate();\n\tconst location = useRouterState({ select: state => state.location });\n\tconst modalOpen = useRouterState({\n\t\tselect: state => state.matches.some(match => isModalRouteId(router, match.routeId)),\n\t});\n\tconst showPopout = useShowPopout();\n\tconst hidePopout = useHidePopout();\n\n\t/** `\"index\"` and an optional leading slash normalize to the full path. */\n\tconst pageTo = (page: PanelPage): string =>\n\t\tpage.path === \"index\" ? \"/\" : `/${page.path.replace(/^\\//, \"\")}`;\n\n\treturn {\n\t\tpush: ((\n\t\t\tpageOrOptions: NavigateOptions | PanelPage,\n\t\t\tparams?: Record<string, string>,\n\t\t\trest?: Omit<NavigateOptions, \"to\" | \"params\">,\n\t\t) =>\n\t\t\tparams === undefined\n\t\t\t\t? navigate({\n\t\t\t\t\t\t...(pageOrOptions as NavigateOptions),\n\t\t\t\t\t\treplace: false,\n\t\t\t\t\t})\n\t\t\t\t: navigate({\n\t\t\t\t\t\tto: pageTo(pageOrOptions as PanelPage),\n\t\t\t\t\t\tparams,\n\t\t\t\t\t\treplace: false,\n\t\t\t\t\t\t...rest,\n\t\t\t\t\t})) as VkNavigator[\"push\"],\n\t\treplace: ((\n\t\t\tpageOrOptions: NavigateOptions | PanelPage,\n\t\t\tparams?: Record<string, string>,\n\t\t\trest?: Omit<NavigateOptions, \"to\" | \"params\">,\n\t\t) =>\n\t\t\tparams === undefined\n\t\t\t\t? navigate({\n\t\t\t\t\t\t...(pageOrOptions as NavigateOptions),\n\t\t\t\t\t\treplace: true,\n\t\t\t\t\t})\n\t\t\t\t: navigate({\n\t\t\t\t\t\tto: pageTo(pageOrOptions as PanelPage),\n\t\t\t\t\t\tparams,\n\t\t\t\t\t\treplace: true,\n\t\t\t\t\t\t...rest,\n\t\t\t\t\t})) as VkNavigator[\"replace\"],\n\t\tback: () => {\n\t\t\trouter.history.back();\n\t\t},\n\t\tbackToFirst: () => {\n\t\t\tconst index =\n\t\t\t\t(location.state as { __TSR_index?: number } | undefined)?.__TSR_index ?? 0;\n\t\t\tif (index > 0) {\n\t\t\t\trouter.history.go(-index);\n\t\t\t}\n\t\t},\n\t\tshowModal: ({ props, ...options }) => {\n\t\t\t// nested modal: the target route is a CHILD of the currently open\n\t\t\t// modal — PUSH (back unwinds the stack); sibling modals keep the\n\t\t\t// old-router replace parity\n\t\t\tlet nested = false;\n\t\t\tif (modalOpen && typeof options.to === \"string\") {\n\t\t\t\tfor (let i = router.state.matches.length - 1; i >= 0; i--) {\n\t\t\t\t\tconst matchRouteId = router.state.matches[i]?.routeId;\n\t\t\t\t\tif (matchRouteId === undefined) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst route = looseRouteOf(router, matchRouteId);\n\t\t\t\t\tif (route?.options.staticData?.modal === undefined) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tnested =\n\t\t\t\t\t\troute.fullPath !== undefined && options.to.startsWith(`${route.fullPath}/`);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// TanStack types history state as its own updater shape — arbitrary\n\t\t\t// serializable keys go through a cast (same as the popout state)\n\t\t\tconst withProps =\n\t\t\t\tprops !== undefined\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tstate: {\n\t\t\t\t\t\t\t\t...(options.state as Record<string, unknown> | undefined),\n\t\t\t\t\t\t\t\t[MODAL_STATE_KEY]: { props },\n\t\t\t\t\t\t\t} as never,\n\t\t\t\t\t\t}\n\t\t\t\t\t: {};\n\t\t\treturn navigate({\n\t\t\t\t...options,\n\t\t\t\t// a modal open never changes the panel behind it — carry the\n\t\t\t\t// panel's search (?tab=…) onto the modal entry, or the covered\n\t\t\t\t// panel would snap to its default search for the whole time the\n\t\t\t\t// modal is open (and snap back on close). An explicit `search`\n\t\t\t\t// from the caller always wins.\n\t\t\t\tsearch: options.search ?? true,\n\t\t\t\treplace: nested ? false : modalOpen,\n\t\t\t\t...withProps,\n\t\t\t});\n\t\t},\n\t\thideModal: () => {\n\t\t\tif (router.history.canGoBack()) {\n\t\t\t\trouter.history.back();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// deep-linked modal on the first entry: replace down to the panel\n\t\t\tconst parentPath = location.pathname.replace(/\\/[^/]+$/, \"\") || \"/\";\n\t\t\tvoid router.navigate({ href: parentPath, replace: true });\n\t\t},\n\t\tshowPopout: (id, props) => {\n\t\t\tshowPopout(id, props);\n\t\t},\n\t\thidePopout: () => {\n\t\t\thidePopout();\n\t\t},\n\t\tshowGlobalModal: (id, { state, props, ...rest } = {}) => {\n\t\t\tconst { pathname, searchStr, hash } = router.state.location;\n\t\t\tconst params = new URLSearchParams(searchStr.replace(/^\\?/, \"\"));\n\t\t\tconst already = params.get(\"modal\") === id;\n\t\t\tparams.set(\"modal\", id);\n\t\t\tconst href = `${pathname}?${params.toString()}${hash}`;\n\t\t\t// the mask keeps the visible URL at the panel path while the modal\n\t\t\t// is open (the modal param rides in history.state)\n\t\t\tconst maskParams = new URLSearchParams(params);\n\t\t\tmaskParams.delete(\"modal\");\n\t\t\tconst maskQuery = maskParams.toString();\n\t\t\tconst maskHref = `${pathname}${maskQuery !== \"\" ? `?${maskQuery}` : \"\"}${hash}`;\n\t\t\tconst nextState =\n\t\t\t\tstate !== undefined || props !== undefined\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tstate: {\n\t\t\t\t\t\t\t\t...state,\n\t\t\t\t\t\t\t\t...(props !== undefined ? { [MODAL_STATE_KEY]: { props } } : {}),\n\t\t\t\t\t\t\t} as never,\n\t\t\t\t\t\t}\n\t\t\t\t\t: {};\n\t\t\treturn navigate({\n\t\t\t\thref,\n\t\t\t\tmask: { to: maskHref },\n\t\t\t\treplace: already || modalOpen,\n\t\t\t\t...rest,\n\t\t\t\t...nextState,\n\t\t\t});\n\t\t},\n\t\tblock: onLeave => {\n\t\t\tconst unblock = router.history.block({\n\t\t\t\t// args infer as TanStack's BlockerFnArgs (HistoryLocation × 2 +\n\t\t\t\t// action) — assignable to our structural BlockerArgs\n\t\t\t\tblockerFn: args =>\n\t\t\t\t\tonLeave({\n\t\t\t\t\t\tcurrentLocation: args.currentLocation,\n\t\t\t\t\t\tnextLocation: args.nextLocation,\n\t\t\t\t\t\t// TanStack distinguishes BACK/FORWARD/GO; the old router's\n\t\t\t\t\t\t// BlockerFunction saw one POP — keep the parity\n\t\t\t\t\t\taction:\n\t\t\t\t\t\t\targs.action === \"PUSH\" || args.action === \"REPLACE\"\n\t\t\t\t\t\t\t\t? args.action\n\t\t\t\t\t\t\t\t: \"POP\",\n\t\t\t\t\t}),\n\t\t\t});\n\t\t\treturn unblock;\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
package/dist/preset.d.ts
CHANGED
|
@@ -7,11 +7,11 @@ import { Popouts, useHidePopout, usePopoutState, useShowPopout } from "./vkui/po
|
|
|
7
7
|
import { PanelDefinition, definePanel, panelRouteId } from "./preset/define-panel.js";
|
|
8
8
|
import { ViewDefinition, defineView } from "./preset/define-view.js";
|
|
9
9
|
import { AppHandle, AppOptions, ModalComponent, ModalDeclaration, ModalEntryComponent, PanelHandle, PanelHandlesOf, PanelOptions, TypedAppHandle, ViewOptions, createApp, panel } from "./preset/builders.js";
|
|
10
|
-
import { AppRootShell, useInflate } from "./preset/app.js";
|
|
10
|
+
import { AppRootShell, useActiveStory, useInflate } from "./preset/app.js";
|
|
11
11
|
import { deriveInflateChain } from "./preset/inflate.js";
|
|
12
12
|
import { VkNavigator, useNavigator } from "./preset/navigator.js";
|
|
13
13
|
import { VkView } from "./vkui/vk-view.js";
|
|
14
14
|
import { NotFound } from "./vkui/not-found.js";
|
|
15
15
|
import "./vkui.js";
|
|
16
16
|
import { PanelRef, useEnableSwipeBack, useModalProps, usePanelLoaderData, usePanelParams, usePanelSearch, usePanelsHistory } from "./react/hooks.js";
|
|
17
|
-
export { type AppHandle, type AppOptions, AppRootShell, ModalCardSurface, type ModalComponent, type ModalDeclaration, type ModalEntry, type ModalEntryComponent, ModalPageSurface, Modals, NotFound, type PanelDefinition, type PanelHandle, type PanelHandlesOf, type PanelOptions, type PanelRef, Popouts, type TypedAppHandle, type ViewDefinition, type ViewOptions, type VkBridgeLike, type VkNavigator, VkView, createApp, createBridgeSync, createSwipeBackManager, createVkHistory, definePanel, defineView, deriveInflateChain, normalizeBridgeEvent, panel, panelRouteId, useEnableSwipeBack, useHidePopout, useInflate, useModalProps, useNavigator, usePanelLoaderData, usePanelParams, usePanelSearch, usePanelsHistory, usePopoutState, useShowPopout };
|
|
17
|
+
export { type AppHandle, type AppOptions, AppRootShell, ModalCardSurface, type ModalComponent, type ModalDeclaration, type ModalEntry, type ModalEntryComponent, ModalPageSurface, Modals, NotFound, type PanelDefinition, type PanelHandle, type PanelHandlesOf, type PanelOptions, type PanelRef, Popouts, type TypedAppHandle, type ViewDefinition, type ViewOptions, type VkBridgeLike, type VkNavigator, VkView, createApp, createBridgeSync, createSwipeBackManager, createVkHistory, definePanel, defineView, deriveInflateChain, normalizeBridgeEvent, panel, panelRouteId, useActiveStory, useEnableSwipeBack, useHidePopout, useInflate, useModalProps, useNavigator, usePanelLoaderData, usePanelParams, usePanelSearch, usePanelsHistory, usePopoutState, useShowPopout };
|
package/dist/preset.js
CHANGED
|
@@ -8,9 +8,9 @@ import { ModalCardSurface, ModalPageSurface, Modals } from "./vkui/modals.js";
|
|
|
8
8
|
import { Popouts, useHidePopout, usePopoutState, useShowPopout } from "./vkui/popouts.js";
|
|
9
9
|
import { NotFound } from "./vkui/not-found.js";
|
|
10
10
|
import { deriveInflateChain } from "./preset/inflate.js";
|
|
11
|
-
import { AppRootShell, useInflate } from "./preset/app.js";
|
|
11
|
+
import { AppRootShell, useActiveStory, useInflate } from "./preset/app.js";
|
|
12
12
|
import { definePanel, panelRouteId } from "./preset/define-panel.js";
|
|
13
13
|
import { defineView } from "./preset/define-view.js";
|
|
14
14
|
import { createApp, panel } from "./preset/builders.js";
|
|
15
15
|
import { useNavigator } from "./preset/navigator.js";
|
|
16
|
-
export { AppRootShell, ModalCardSurface, ModalPageSurface, Modals, NotFound, Popouts, VkView, createApp, createBridgeSync, createSwipeBackManager, createVkHistory, definePanel, defineView, deriveInflateChain, normalizeBridgeEvent, panel, panelRouteId, useEnableSwipeBack, useHidePopout, useInflate, useModalProps, useNavigator, usePanelLoaderData, usePanelParams, usePanelSearch, usePanelsHistory, usePopoutState, useShowPopout };
|
|
16
|
+
export { AppRootShell, ModalCardSurface, ModalPageSurface, Modals, NotFound, Popouts, VkView, createApp, createBridgeSync, createSwipeBackManager, createVkHistory, definePanel, defineView, deriveInflateChain, normalizeBridgeEvent, panel, panelRouteId, useActiveStory, useEnableSwipeBack, useHidePopout, useInflate, useModalProps, useNavigator, usePanelLoaderData, usePanelParams, usePanelSearch, usePanelsHistory, usePopoutState, useShowPopout };
|
package/dist/vkui/vk-view.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { AnyRoute } from "@tanstack/react-router";
|
|
2
2
|
//#region src/vkui/vk-view.d.ts
|
|
3
3
|
export declare function panelIdOf(route: AnyRoute): string;
|
|
4
|
+
/**
|
|
5
|
+
* The VKUI nav key of a view route: "/view:feed" → "feed". Epic/Root match
|
|
6
|
+
* children by getNavId, so the raw route id would leak the route-tree
|
|
7
|
+
* convention into the UI layer. This is also the key the app author wrote
|
|
8
|
+
* in `defineView("feed")`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function storyKeyOf(routeId: unknown): string;
|
|
4
11
|
export interface VkViewProps {
|
|
5
12
|
/** The pathless view layout route created by the preset. */
|
|
6
13
|
view: AnyRoute;
|
|
7
|
-
/** VKUI nav id of the View. Defaults to the
|
|
14
|
+
/** VKUI nav id of the View. Defaults to the story key (the defineView key). */
|
|
8
15
|
nav?: string;
|
|
9
16
|
/** Native swipe-back pop; defaults to `window.history.back()`. */
|
|
10
17
|
onSwipeBack?: () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vk-view.d.ts","names":[],"sources":["../../src/vkui/vk-view.tsx"],"mappings":";;wBAUgB,UAAU,OAAO;
|
|
1
|
+
{"version":3,"file":"vk-view.d.ts","names":[],"sources":["../../src/vkui/vk-view.tsx"],"mappings":";;wBAUgB,UAAU,OAAO;;;;;;;wBA+BjB,WAAW;iBAKV;;EAEhB,MAAM;;EAEN;;EAEA;;;;;;;;;;;;;;wBAee,SAAS,MAAM,KAAK,eAAe,8BAAW,IAAA"}
|
package/dist/vkui/vk-view.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { usePanelsHistory } from "../react/hooks.js";
|
|
2
2
|
import { useRouterState } from "@tanstack/react-router";
|
|
3
|
-
import { Panel,
|
|
4
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Panel, PanelHeader, PanelSpinner, View } from "@vkontakte/vkui";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/vkui/vk-view.tsx
|
|
6
6
|
function panelIdOf(route) {
|
|
7
7
|
return route.options.staticData?.panel ?? "";
|
|
@@ -18,6 +18,16 @@ function useMatchSummaries() {
|
|
|
18
18
|
return useRouterState({ select: (state) => state.matches.map((match) => `${match.routeId}#${match.status}`).join("|") });
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
+
* The VKUI nav key of a view route: "/view:feed" → "feed". Epic/Root match
|
|
22
|
+
* children by getNavId, so the raw route id would leak the route-tree
|
|
23
|
+
* convention into the UI layer. This is also the key the app author wrote
|
|
24
|
+
* in `defineView("feed")`.
|
|
25
|
+
*/
|
|
26
|
+
function storyKeyOf(routeId) {
|
|
27
|
+
const id = String(routeId);
|
|
28
|
+
return id.split(":")[1] ?? id;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
21
31
|
* Static-children view: every panel route renders as an always-present JSX
|
|
22
32
|
* child of VKUI <View>; the router only feeds `activePanel` (and `history`
|
|
23
33
|
* for the iOS swipe-back). VKUI hides inactive panels itself and infers
|
|
@@ -39,7 +49,7 @@ function VkView({ view, nav, onSwipeBack }) {
|
|
|
39
49
|
const activePanel = activeChild ? panelIdOf(activeChild) : "";
|
|
40
50
|
const history = usePanelsHistory(activePanel);
|
|
41
51
|
return /* @__PURE__ */ jsx(View, {
|
|
42
|
-
nav: nav ?? view.id,
|
|
52
|
+
nav: nav ?? storyKeyOf(view.id),
|
|
43
53
|
activePanel,
|
|
44
54
|
history,
|
|
45
55
|
onSwipeBack: onSwipeBack ?? (() => window.history.back()),
|
|
@@ -57,15 +67,9 @@ function VkView({ view, nav, onSwipeBack }) {
|
|
|
57
67
|
});
|
|
58
68
|
}
|
|
59
69
|
function PanelSpinnerFallback() {
|
|
60
|
-
return /* @__PURE__ */ jsx("
|
|
61
|
-
style: {
|
|
62
|
-
paddingTop: 64,
|
|
63
|
-
textAlign: "center"
|
|
64
|
-
},
|
|
65
|
-
children: /* @__PURE__ */ jsx(Spinner, { size: "l" })
|
|
66
|
-
});
|
|
70
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(PanelHeader, {}), /* @__PURE__ */ jsx(PanelSpinner, { size: "l" })] });
|
|
67
71
|
}
|
|
68
72
|
//#endregion
|
|
69
|
-
export { VkView, panelIdOf };
|
|
73
|
+
export { VkView, panelIdOf, storyKeyOf };
|
|
70
74
|
|
|
71
75
|
//# sourceMappingURL=vk-view.js.map
|
package/dist/vkui/vk-view.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vk-view.js","names":[],"sources":["../../src/vkui/vk-view.tsx"],"sourcesContent":["import type { AnyRoute } from \"@tanstack/react-router\";\nimport { useRouterState } from \"@tanstack/react-router\";\nimport { Panel,
|
|
1
|
+
{"version":3,"file":"vk-view.js","names":[],"sources":["../../src/vkui/vk-view.tsx"],"sourcesContent":["import type { AnyRoute } from \"@tanstack/react-router\";\nimport { useRouterState } from \"@tanstack/react-router\";\nimport { Panel, PanelHeader, PanelSpinner, View } from \"@vkontakte/vkui\";\nimport { type ComponentType } from \"react\";\nimport { usePanelsHistory } from \"../react/hooks.ts\";\n\nexport interface PanelStaticData {\n\tpanel: string;\n}\n\nexport function panelIdOf(route: AnyRoute): string {\n\treturn (route.options.staticData as PanelStaticData | undefined)?.panel ?? \"\";\n}\n\nfunction deepestPanelMatchId(view: AnyRoute, matchedRouteIds: string[]): string {\n\tconst panelIds = new Set(view.children.map((child: AnyRoute) => child.id));\n\tfor (let i = matchedRouteIds.length - 1; i >= 0; i--) {\n\t\tconst id = matchedRouteIds[i];\n\t\tif (id !== undefined && panelIds.has(id)) {\n\t\t\treturn id;\n\t\t}\n\t}\n\treturn view.children[0]?.id ?? \"\";\n}\n\nfunction useMatchSummaries(): string {\n\treturn useRouterState({\n\t\tselect: state =>\n\t\t\t// routeId + status: a lazy panel's chunk resolution flips the match\n\t\t\t// status, which must re-render the static children (the resolved\n\t\t\t// component merges into route.options at that point)\n\t\t\tstate.matches.map(match => `${match.routeId}#${match.status}`).join(\"|\"),\n\t});\n}\n\n/**\n * The VKUI nav key of a view route: \"/view:feed\" → \"feed\". Epic/Root match\n * children by getNavId, so the raw route id would leak the route-tree\n * convention into the UI layer. This is also the key the app author wrote\n * in `defineView(\"feed\")`.\n */\nexport function storyKeyOf(routeId: unknown): string {\n\tconst id = String(routeId);\n\treturn id.split(\":\")[1] ?? id;\n}\n\nexport interface VkViewProps {\n\t/** The pathless view layout route created by the preset. */\n\tview: AnyRoute;\n\t/** VKUI nav id of the View. Defaults to the story key (the defineView key). */\n\tnav?: string;\n\t/** Native swipe-back pop; defaults to `window.history.back()`. */\n\tonSwipeBack?: () => void;\n}\n\n/**\n * Static-children view: every panel route renders as an always-present JSX\n * child of VKUI <View>; the router only feeds `activePanel` (and `history`\n * for the iOS swipe-back). VKUI hides inactive panels itself and infers\n * the animation direction from the children order — a plain <Outlet>\n * breaks both. A lazy panel's `options.component` is undefined until its\n * chunk resolves (TanStack merges loaded options in place), so the panel\n * renders a spinner fallback until then. For the same reason TanStack's\n * Outlet-driven `pendingComponent` swap never happens on its own — while\n * the panel's match is pending (loader/beforeLoad), the route's\n * `pendingComponent` renders instead of the component.\n */\nexport function VkView({ view, nav, onSwipeBack }: VkViewProps) {\n\tuseMatchSummaries();\n\tconst matchedRouteIds = useRouterState({\n\t\tselect: state => state.matches.map(match => match.routeId),\n\t});\n\t// structurally-compared array — TanStack keeps the reference stable when\n\t// the selection is deep-equal. TanStack's Outlet would swap in the\n\t// route's pendingComponent itself, but static children render the\n\t// component directly, so the swap is ours to make while the loader runs\n\tconst pendingRouteIds = useRouterState({\n\t\tselect: state =>\n\t\t\tstate.matches\n\t\t\t\t.filter(match => (match as { status?: string }).status === \"pending\")\n\t\t\t\t.map(match => String(match.routeId)),\n\t});\n\t// a notFound match (notFound() from beforeLoad/validateSearch) must NOT\n\t// render its panel component: the component's data guards assume a valid\n\t// match, and the shell renders the NotFound screen for this navigation —\n\t// the panel itself stays invisible. Without the gate the component would\n\t// throw in a transient render and dirty the console on every cold 404\n\tconst notFoundRouteIds = useRouterState({\n\t\tselect: state =>\n\t\t\tstate.matches\n\t\t\t\t.filter(match => (match as { status?: string }).status === \"notFound\")\n\t\t\t\t.map(match => String(match.routeId)),\n\t});\n\tconst activeRouteId = deepestPanelMatchId(view, matchedRouteIds);\n\tconst activeChild =\n\t\tview.children.find((child: AnyRoute) => child.id === activeRouteId) ?? view.children[0];\n\tconst activePanel = activeChild ? panelIdOf(activeChild) : \"\";\n\n\tconst history = usePanelsHistory(activePanel);\n\n\treturn (\n\t\t<View\n\t\t\tnav={nav ?? storyKeyOf(view.id)}\n\t\t\tactivePanel={activePanel}\n\t\t\thistory={history}\n\t\t\tonSwipeBack={onSwipeBack ?? (() => window.history.back())}\n\t\t>\n\t\t\t{view.children.map((child: AnyRoute) => {\n\t\t\t\tconst id = panelIdOf(child);\n\t\t\t\tconst Component = child.options.component as ComponentType | undefined;\n\t\t\t\tconst Pending = child.options.pendingComponent as ComponentType | undefined;\n\t\t\t\tconst isPending = pendingRouteIds.includes(String(child.id));\n\t\t\t\tconst isNotFound = notFoundRouteIds.includes(String(child.id));\n\t\t\t\t// pending without a pendingComponent renders the FALLBACK, never\n\t\t\t\t// the component: a lazy chunk may have already merged its\n\t\t\t\t// component while the match is still pending (or parked on a\n\t\t\t\t// notFound boundary up the tree) — the component's data guards\n\t\t\t\t// assume a resolvable match and would throw in a hidden panel\n\t\t\t\treturn (\n\t\t\t\t\t<Panel key={id} nav={id}>\n\t\t\t\t\t\t{isPending ? (\n\t\t\t\t\t\t\tPending !== undefined ? (\n\t\t\t\t\t\t\t\t<Pending />\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<PanelSpinnerFallback />\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t) : isNotFound ? (\n\t\t\t\t\t\t\t<PanelSpinnerFallback />\n\t\t\t\t\t\t) : Component ? (\n\t\t\t\t\t\t\t<Component />\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<PanelSpinnerFallback />\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Panel>\n\t\t\t\t);\n\t\t\t})}\n\t\t</View>\n\t);\n}\n\nfunction PanelSpinnerFallback() {\n\t// The canonical VKUI loading screen: a header stub keeps the layout\n\t// structure while the lazy chunk is in flight, and PanelSpinner centers\n\t// itself inside the panel (a bare Spinner floats at the top of a void).\n\treturn (\n\t\t<>\n\t\t\t<PanelHeader />\n\t\t\t<PanelSpinner size=\"l\" />\n\t\t</>\n\t);\n}\n"],"mappings":";;;;;AAUA,SAAgB,UAAU,OAAyB;CAClD,OAAQ,MAAM,QAAQ,YAA4C,SAAS;AAC5E;AAEA,SAAS,oBAAoB,MAAgB,iBAAmC;CAC/E,MAAM,WAAW,IAAI,IAAI,KAAK,SAAS,KAAK,UAAoB,MAAM,EAAE,CAAC;CACzE,KAAK,IAAI,IAAI,gBAAgB,SAAS,GAAG,KAAK,GAAG,KAAK;EACrD,MAAM,KAAK,gBAAgB;EAC3B,IAAI,OAAO,KAAA,KAAa,SAAS,IAAI,EAAE,GACtC,OAAO;CAET;CACA,OAAO,KAAK,SAAS,EAAE,EAAE,MAAM;AAChC;AAEA,SAAS,oBAA4B;CACpC,OAAO,eAAe,EACrB,SAAQ,UAIP,MAAM,QAAQ,KAAI,UAAS,GAAG,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,CAAC,KAAK,GAAG,EACzE,CAAC;AACF;;;;;;;AAQA,SAAgB,WAAW,SAA0B;CACpD,MAAM,KAAK,OAAO,OAAO;CACzB,OAAO,GAAG,MAAM,GAAG,CAAC,CAAC,MAAM;AAC5B;;;;;;;;;;;;;AAuBA,SAAgB,OAAO,EAAE,MAAM,KAAK,eAA4B;CAC/D,kBAAkB;CAClB,MAAM,kBAAkB,eAAe,EACtC,SAAQ,UAAS,MAAM,QAAQ,KAAI,UAAS,MAAM,OAAO,EAC1D,CAAC;CAKD,MAAM,kBAAkB,eAAe,EACtC,SAAQ,UACP,MAAM,QACJ,QAAO,UAAU,MAA8B,WAAW,SAAS,CAAC,CACpE,KAAI,UAAS,OAAO,MAAM,OAAO,CAAC,EACtC,CAAC;CAMD,MAAM,mBAAmB,eAAe,EACvC,SAAQ,UACP,MAAM,QACJ,QAAO,UAAU,MAA8B,WAAW,UAAU,CAAC,CACrE,KAAI,UAAS,OAAO,MAAM,OAAO,CAAC,EACtC,CAAC;CACD,MAAM,gBAAgB,oBAAoB,MAAM,eAAe;CAC/D,MAAM,cACL,KAAK,SAAS,MAAM,UAAoB,MAAM,OAAO,aAAa,KAAK,KAAK,SAAS;CACtF,MAAM,cAAc,cAAc,UAAU,WAAW,IAAI;CAE3D,MAAM,UAAU,iBAAiB,WAAW;CAE5C,OACC,oBAAC,MAAD;EACC,KAAK,OAAO,WAAW,KAAK,EAAE;EACjB;EACJ;EACT,aAAa,sBAAsB,OAAO,QAAQ,KAAK;EAEtD,UAAA,KAAK,SAAS,KAAK,UAAoB;GACvC,MAAM,KAAK,UAAU,KAAK;GAC1B,MAAM,YAAY,MAAM,QAAQ;GAChC,MAAM,UAAU,MAAM,QAAQ;GAC9B,MAAM,YAAY,gBAAgB,SAAS,OAAO,MAAM,EAAE,CAAC;GAC3D,MAAM,aAAa,iBAAiB,SAAS,OAAO,MAAM,EAAE,CAAC;GAM7D,OACC,oBAAC,OAAD;IAAgB,KAAK;IACnB,UAAA,YACA,YAAY,KAAA,IACX,oBAAC,SAAD,CAAU,CAAA,IAEV,oBAAC,sBAAD,CAAuB,CAAA,IAErB,aACH,oBAAC,sBAAD,CAAuB,CAAA,IACpB,YACH,oBAAC,WAAD,CAAY,CAAA,IAEZ,oBAAC,sBAAD,CAAuB,CAAA;GAElB,GAdK,EAcL;EAET,CAAC;CACI,CAAA;AAER;AAEA,SAAS,uBAAuB;CAI/B,OACC,qBAAA,UAAA,EAAA,UAAA,CACC,oBAAC,aAAD,CAAc,CAAA,GACd,oBAAC,cAAD,EAAc,MAAK,IAAK,CAAA,CACvB,EAAA,CAAA;AAEJ"}
|
package/dist/vkui.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ModalCardSurface, ModalEntry, ModalEntryComponent, ModalPageSurface, Modals, ModalsProps } from "./vkui/modals.js";
|
|
2
2
|
import { PopoutEntry, Popouts, useHidePopout, usePopoutState, useShowPopout } from "./vkui/popouts.js";
|
|
3
|
-
import { VkView, VkViewProps, panelIdOf } from "./vkui/vk-view.js";
|
|
3
|
+
import { VkView, VkViewProps, panelIdOf, storyKeyOf } from "./vkui/vk-view.js";
|
|
4
4
|
import { NotFound } from "./vkui/not-found.js";
|
|
5
|
-
export { ModalCardSurface, type ModalEntry, type ModalEntryComponent, ModalPageSurface, Modals, type ModalsProps, NotFound, type PopoutEntry, Popouts, VkView, type VkViewProps, panelIdOf, useHidePopout, usePopoutState, useShowPopout };
|
|
5
|
+
export { ModalCardSurface, type ModalEntry, type ModalEntryComponent, ModalPageSurface, Modals, type ModalsProps, NotFound, type PopoutEntry, Popouts, VkView, type VkViewProps, panelIdOf, storyKeyOf, useHidePopout, usePopoutState, useShowPopout };
|
package/dist/vkui.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { VkView, panelIdOf } from "./vkui/vk-view.js";
|
|
1
|
+
import { VkView, panelIdOf, storyKeyOf } from "./vkui/vk-view.js";
|
|
2
2
|
import { ModalCardSurface, ModalPageSurface, Modals } from "./vkui/modals.js";
|
|
3
3
|
import { Popouts, useHidePopout, usePopoutState, useShowPopout } from "./vkui/popouts.js";
|
|
4
4
|
import { NotFound } from "./vkui/not-found.js";
|
|
5
|
-
export { ModalCardSurface, ModalPageSurface, Modals, NotFound, Popouts, VkView, panelIdOf, useHidePopout, usePopoutState, useShowPopout };
|
|
5
|
+
export { ModalCardSurface, ModalPageSurface, Modals, NotFound, Popouts, VkView, panelIdOf, storyKeyOf, useHidePopout, usePopoutState, useShowPopout };
|
package/package.json
CHANGED
package/src/preset/app.tsx
CHANGED
|
@@ -4,10 +4,46 @@ import { looseRouteOf } from "../core/loose-route.ts";
|
|
|
4
4
|
import { Modals } from "../vkui/modals.tsx";
|
|
5
5
|
import { NotFound } from "../vkui/not-found.tsx";
|
|
6
6
|
import { Popouts } from "../vkui/popouts.tsx";
|
|
7
|
-
import { VkView } from "../vkui/vk-view.tsx";
|
|
7
|
+
import { storyKeyOf, VkView } from "../vkui/vk-view.tsx";
|
|
8
8
|
import type { AppHandle } from "./builders.tsx";
|
|
9
9
|
import { deriveInflateChain, interpolatePath } from "./inflate.ts";
|
|
10
10
|
|
|
11
|
+
interface MatchLike {
|
|
12
|
+
routeId?: string;
|
|
13
|
+
status?: string;
|
|
14
|
+
_notFound?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The view route owning the deepest matched route, or null. The walk starts
|
|
19
|
+
* at the deepest match because the chain depth varies — a modal is a masked
|
|
20
|
+
* CHILD route of its panel, so `/view:feed/feed/comments` must still resolve
|
|
21
|
+
* to the feed view. A notFound match anywhere in the chain wins over
|
|
22
|
+
* everything: the shell must show the NotFound screen, never a story
|
|
23
|
+
* fallback (an unmatched lazy panel's chunk never loads — a fallback story
|
|
24
|
+
* would spin forever).
|
|
25
|
+
*/
|
|
26
|
+
export function owningViewIdOf(
|
|
27
|
+
matches: readonly MatchLike[],
|
|
28
|
+
views: readonly { id: string }[],
|
|
29
|
+
): string | null {
|
|
30
|
+
const notFound = matches.some(
|
|
31
|
+
match =>
|
|
32
|
+
match.status === "notFound" || (match as { _notFound?: boolean })._notFound === true,
|
|
33
|
+
);
|
|
34
|
+
if (notFound) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
38
|
+
const routeId = matches[i]?.routeId ?? "";
|
|
39
|
+
const view = views.find(view => routeId === view.id || routeId.startsWith(`${view.id}/`));
|
|
40
|
+
if (view !== undefined) {
|
|
41
|
+
return view.id;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
11
47
|
/**
|
|
12
48
|
* Default root shell: the active view + the modal/popout hosts + inflate.
|
|
13
49
|
* The active view is the one containing the deepest matched route
|
|
@@ -21,29 +57,10 @@ import { deriveInflateChain, interpolatePath } from "./inflate.ts";
|
|
|
21
57
|
*/
|
|
22
58
|
export function AppRootShell({ app, inflate = true }: { app: AppHandle; inflate?: boolean }) {
|
|
23
59
|
useInflate(app, inflate);
|
|
60
|
+
const views = Object.values(app.views);
|
|
24
61
|
|
|
25
62
|
const activeViewId = useRouterState({
|
|
26
|
-
select: state =>
|
|
27
|
-
const notFound = state.matches.some(
|
|
28
|
-
match =>
|
|
29
|
-
(match as { status?: string }).status === "notFound" ||
|
|
30
|
-
(match as { _notFound?: boolean })._notFound === true,
|
|
31
|
-
);
|
|
32
|
-
if (notFound) {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
const views = Object.values(app.views);
|
|
36
|
-
for (let i = state.matches.length - 1; i >= 0; i--) {
|
|
37
|
-
const routeId = state.matches[i]?.routeId ?? "";
|
|
38
|
-
const view = views.find(
|
|
39
|
-
view => routeId === view.id || routeId.startsWith(`${view.id}/`),
|
|
40
|
-
);
|
|
41
|
-
if (view !== undefined) {
|
|
42
|
-
return view.id;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return null;
|
|
46
|
-
},
|
|
63
|
+
select: (state): string | null => owningViewIdOf(state.matches, views),
|
|
47
64
|
});
|
|
48
65
|
const activeView =
|
|
49
66
|
activeViewId === null
|
|
@@ -59,6 +76,23 @@ export function AppRootShell({ app, inflate = true }: { app: AppHandle; inflate?
|
|
|
59
76
|
);
|
|
60
77
|
}
|
|
61
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The active story key for Epic/Root shells: the view key the app author
|
|
81
|
+
* wrote in `defineView("feed")`, or null on a notFound navigation — render
|
|
82
|
+
* the NotFound screen instead of a story. Pairs with `<VkView view={…}>`
|
|
83
|
+
* whose nav defaults to the same key, so `activeStory === tab.id` compares
|
|
84
|
+
* user-authored keys on both sides.
|
|
85
|
+
*/
|
|
86
|
+
export function useActiveStory(app: AppHandle): string | null {
|
|
87
|
+
const views = Object.values(app.views);
|
|
88
|
+
return useRouterState({
|
|
89
|
+
select: (state): string | null => {
|
|
90
|
+
const viewId = owningViewIdOf(state.matches, views);
|
|
91
|
+
return viewId === null ? null : storyKeyOf(viewId);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
62
96
|
/**
|
|
63
97
|
* Inflate (spike parity): a cold start (first history entry) on a panel
|
|
64
98
|
* that is NOT the first of its view replays the panel flow before it, so
|
package/src/preset/navigator.ts
CHANGED
|
@@ -230,11 +230,9 @@ export function useNavigator(): VkNavigator {
|
|
|
230
230
|
},
|
|
231
231
|
block: onLeave => {
|
|
232
232
|
const unblock = router.history.block({
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
action: string;
|
|
237
|
-
}) =>
|
|
233
|
+
// args infer as TanStack's BlockerFnArgs (HistoryLocation × 2 +
|
|
234
|
+
// action) — assignable to our structural BlockerArgs
|
|
235
|
+
blockerFn: args =>
|
|
238
236
|
onLeave({
|
|
239
237
|
currentLocation: args.currentLocation,
|
|
240
238
|
nextLocation: args.nextLocation,
|
package/src/preset.ts
CHANGED
|
@@ -9,7 +9,7 @@ export {
|
|
|
9
9
|
type TypedAppHandle,
|
|
10
10
|
type ViewOptions,
|
|
11
11
|
} from "./preset/builders.tsx";
|
|
12
|
-
export { AppRootShell, useInflate } from "./preset/app.tsx";
|
|
12
|
+
export { AppRootShell, useActiveStory, useInflate } from "./preset/app.tsx";
|
|
13
13
|
export { deriveInflateChain } from "./preset/inflate.ts";
|
|
14
14
|
export { useNavigator, type VkNavigator } from "./preset/navigator.ts";
|
|
15
15
|
export { definePanel, panelRouteId, type PanelDefinition } from "./preset/define-panel.ts";
|
package/src/vkui/vk-view.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AnyRoute } from "@tanstack/react-router";
|
|
2
2
|
import { useRouterState } from "@tanstack/react-router";
|
|
3
|
-
import { Panel,
|
|
3
|
+
import { Panel, PanelHeader, PanelSpinner, View } from "@vkontakte/vkui";
|
|
4
4
|
import { type ComponentType } from "react";
|
|
5
5
|
import { usePanelsHistory } from "../react/hooks.ts";
|
|
6
6
|
|
|
@@ -33,10 +33,21 @@ function useMatchSummaries(): string {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The VKUI nav key of a view route: "/view:feed" → "feed". Epic/Root match
|
|
38
|
+
* children by getNavId, so the raw route id would leak the route-tree
|
|
39
|
+
* convention into the UI layer. This is also the key the app author wrote
|
|
40
|
+
* in `defineView("feed")`.
|
|
41
|
+
*/
|
|
42
|
+
export function storyKeyOf(routeId: unknown): string {
|
|
43
|
+
const id = String(routeId);
|
|
44
|
+
return id.split(":")[1] ?? id;
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
export interface VkViewProps {
|
|
37
48
|
/** The pathless view layout route created by the preset. */
|
|
38
49
|
view: AnyRoute;
|
|
39
|
-
/** VKUI nav id of the View. Defaults to the
|
|
50
|
+
/** VKUI nav id of the View. Defaults to the story key (the defineView key). */
|
|
40
51
|
nav?: string;
|
|
41
52
|
/** Native swipe-back pop; defaults to `window.history.back()`. */
|
|
42
53
|
onSwipeBack?: () => void;
|
|
@@ -89,7 +100,7 @@ export function VkView({ view, nav, onSwipeBack }: VkViewProps) {
|
|
|
89
100
|
|
|
90
101
|
return (
|
|
91
102
|
<View
|
|
92
|
-
nav={nav ?? view.id}
|
|
103
|
+
nav={nav ?? storyKeyOf(view.id)}
|
|
93
104
|
activePanel={activePanel}
|
|
94
105
|
history={history}
|
|
95
106
|
onSwipeBack={onSwipeBack ?? (() => window.history.back())}
|
|
@@ -128,9 +139,13 @@ export function VkView({ view, nav, onSwipeBack }: VkViewProps) {
|
|
|
128
139
|
}
|
|
129
140
|
|
|
130
141
|
function PanelSpinnerFallback() {
|
|
142
|
+
// The canonical VKUI loading screen: a header stub keeps the layout
|
|
143
|
+
// structure while the lazy chunk is in flight, and PanelSpinner centers
|
|
144
|
+
// itself inside the panel (a bare Spinner floats at the top of a void).
|
|
131
145
|
return (
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
|
|
146
|
+
<>
|
|
147
|
+
<PanelHeader />
|
|
148
|
+
<PanelSpinner size="l" />
|
|
149
|
+
</>
|
|
135
150
|
);
|
|
136
151
|
}
|
package/src/vkui.ts
CHANGED