@vkma.dev/router 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/dist/core/bridge-like.d.ts +25 -0
- package/dist/core/bridge-like.d.ts.map +1 -0
- package/dist/core/bridge-like.js +13 -0
- package/dist/core/bridge-like.js.map +1 -0
- package/dist/core/bridge-sync.d.ts +37 -0
- package/dist/core/bridge-sync.d.ts.map +1 -0
- package/dist/core/bridge-sync.js +58 -0
- package/dist/core/bridge-sync.js.map +1 -0
- package/dist/core/hash-params.d.ts +40 -0
- package/dist/core/hash-params.d.ts.map +1 -0
- package/dist/core/hash-params.js +60 -0
- package/dist/core/hash-params.js.map +1 -0
- package/dist/core/loose-route.js +9 -0
- package/dist/core/loose-route.js.map +1 -0
- package/dist/core/panels-stack.d.ts +28 -0
- package/dist/core/panels-stack.d.ts.map +1 -0
- package/dist/core/panels-stack.js +60 -0
- package/dist/core/panels-stack.js.map +1 -0
- package/dist/core/state-keys.d.ts +10 -0
- package/dist/core/state-keys.d.ts.map +1 -0
- package/dist/core/state-keys.js +21 -0
- package/dist/core/state-keys.js.map +1 -0
- package/dist/core/swipe-back.d.ts +25 -0
- package/dist/core/swipe-back.d.ts.map +1 -0
- package/dist/core/swipe-back.js +53 -0
- package/dist/core/swipe-back.js.map +1 -0
- package/dist/core/vk-history.d.ts +41 -0
- package/dist/core/vk-history.d.ts.map +1 -0
- package/dist/core/vk-history.js +115 -0
- package/dist/core/vk-history.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/preset/app.d.ts +35 -0
- package/dist/preset/app.d.ts.map +1 -0
- package/dist/preset/app.js +134 -0
- package/dist/preset/app.js.map +1 -0
- package/dist/preset/builders.d.ts +209 -0
- package/dist/preset/builders.d.ts.map +1 -0
- package/dist/preset/builders.js +209 -0
- package/dist/preset/builders.js.map +1 -0
- package/dist/preset/define-panel.d.ts +77 -0
- package/dist/preset/define-panel.d.ts.map +1 -0
- package/dist/preset/define-panel.js +42 -0
- package/dist/preset/define-panel.js.map +1 -0
- package/dist/preset/define-view.d.ts +42 -0
- package/dist/preset/define-view.d.ts.map +1 -0
- package/dist/preset/define-view.js +43 -0
- package/dist/preset/define-view.js.map +1 -0
- package/dist/preset/inflate.d.ts +17 -0
- package/dist/preset/inflate.d.ts.map +1 -0
- package/dist/preset/inflate.js +40 -0
- package/dist/preset/inflate.js.map +1 -0
- package/dist/preset/navigator.d.ts +75 -0
- package/dist/preset/navigator.d.ts.map +1 -0
- package/dist/preset/navigator.js +116 -0
- package/dist/preset/navigator.js.map +1 -0
- package/dist/preset.d.ts +17 -0
- package/dist/preset.js +16 -0
- package/dist/react/hooks.d.ts +69 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +101 -0
- package/dist/react/hooks.js.map +1 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +1 -0
- package/dist/vkui/choreography.js +59 -0
- package/dist/vkui/choreography.js.map +1 -0
- package/dist/vkui/modals.d.ts +49 -0
- package/dist/vkui/modals.d.ts.map +1 -0
- package/dist/vkui/modals.js +116 -0
- package/dist/vkui/modals.js.map +1 -0
- package/dist/vkui/not-found.d.ts +9 -0
- package/dist/vkui/not-found.d.ts.map +1 -0
- package/dist/vkui/not-found.js +23 -0
- package/dist/vkui/not-found.js.map +1 -0
- package/dist/vkui/popouts.d.ts +34 -0
- package/dist/vkui/popouts.d.ts.map +1 -0
- package/dist/vkui/popouts.js +69 -0
- package/dist/vkui/popouts.js.map +1 -0
- package/dist/vkui/vk-view.d.ts +26 -0
- package/dist/vkui/vk-view.d.ts.map +1 -0
- package/dist/vkui/vk-view.js +81 -0
- package/dist/vkui/vk-view.js.map +1 -0
- package/dist/vkui.d.ts +5 -0
- package/dist/vkui.js +5 -0
- package/package.json +70 -3
- package/src/core/bridge-like.ts +25 -0
- package/src/core/bridge-sync.ts +95 -0
- package/src/core/hash-params.ts +73 -0
- package/src/core/loose-route.ts +17 -0
- package/src/core/panels-stack.ts +85 -0
- package/src/core/state-keys.ts +39 -0
- package/src/core/swipe-back.ts +80 -0
- package/src/core/vk-history.ts +146 -0
- package/src/index.ts +30 -0
- package/src/preset/app.tsx +192 -0
- package/src/preset/builders.tsx +566 -0
- package/src/preset/define-panel.ts +109 -0
- package/src/preset/define-view.ts +73 -0
- package/src/preset/inflate.ts +57 -0
- package/src/preset/navigator.ts +250 -0
- package/src/preset.ts +51 -0
- package/src/react/hooks.ts +168 -0
- package/src/react.ts +11 -0
- package/src/vkui/choreography.ts +105 -0
- package/src/vkui/modals.tsx +216 -0
- package/src/vkui/not-found.tsx +21 -0
- package/src/vkui/popouts.tsx +106 -0
- package/src/vkui/vk-view.tsx +147 -0
- package/src/vkui.ts +17 -0
- package/README.md +0 -3
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { parseHashLocation, parseHashPayload, readHrefFromHash, serializeHashPayload, writeHashParams } from "./hash-params.js";
|
|
2
|
+
import { createBrowserHistory } from "@tanstack/history";
|
|
3
|
+
//#region src/core/vk-history.ts
|
|
4
|
+
/**
|
|
5
|
+
* VK history: the same trick TanStack's own hash history uses —
|
|
6
|
+
* `createBrowserHistory` with swapped `parseLocation`/`createHref` — so we
|
|
7
|
+
* inherit blockers with rollback, popstate classification, throttling and
|
|
8
|
+
* TSR index/key stamping.
|
|
9
|
+
*
|
|
10
|
+
* Modes (parity with the old router's three factories):
|
|
11
|
+
* - `"hash"` (default) → old `createHashRouter`: the hash IS the location
|
|
12
|
+
* (`#/item/7?tab=x`).
|
|
13
|
+
* - `"hash-param"` → old `createHashParamRouter`: the whole router
|
|
14
|
+
* location rides in one hash query param (`#?path=%2Fitem%2F7`), foreign
|
|
15
|
+
* hash params (launch params, UTM) are preserved next to it, and the
|
|
16
|
+
* real `location.search` is never touched.
|
|
17
|
+
*
|
|
18
|
+
* Deep links written by the OTHER mode resolve too (legacy tolerance).
|
|
19
|
+
* `"browser"` → pass TanStack's default history instead (omit `history`
|
|
20
|
+
* in createRouter) — old `createBrowserRouter`.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_PATH_PARAM = "path";
|
|
23
|
+
/** The history plus the payload codecs the bridge sync layer needs. */
|
|
24
|
+
function createVkHistory(options = {}) {
|
|
25
|
+
const win = options.window ?? window;
|
|
26
|
+
const pathParam = options.pathParam ?? "path";
|
|
27
|
+
const mode = options.mode ?? "hash";
|
|
28
|
+
let lastIndex = -1;
|
|
29
|
+
/**
|
|
30
|
+
* A history entry without TanStack state (manual hash edit / foreign
|
|
31
|
+
* pushState) gets a FRESH index = lastIndex + 1, so popstate classifies
|
|
32
|
+
* it as FORWARD — the entry behaves like a push (an entry parsed as
|
|
33
|
+
* index 0 would desync any parallel stack mirror).
|
|
34
|
+
*/
|
|
35
|
+
const stampMissingState = () => {
|
|
36
|
+
lastIndex += 1;
|
|
37
|
+
const key = Math.random().toString(36).slice(2, 8);
|
|
38
|
+
const state = {
|
|
39
|
+
__TSR_index: lastIndex,
|
|
40
|
+
key,
|
|
41
|
+
__TSR_key: key
|
|
42
|
+
};
|
|
43
|
+
win.history.replaceState(state, "", win.location.href);
|
|
44
|
+
return state;
|
|
45
|
+
};
|
|
46
|
+
const initStamp = () => {
|
|
47
|
+
const rawState = win.history.state;
|
|
48
|
+
if (rawState?.__TSR_key) {
|
|
49
|
+
if (rawState.__TSR_index > lastIndex) lastIndex = rawState.__TSR_index;
|
|
50
|
+
return rawState;
|
|
51
|
+
}
|
|
52
|
+
return stampMissingState();
|
|
53
|
+
};
|
|
54
|
+
if (mode === "hash-param") {
|
|
55
|
+
const parseLocation = () => {
|
|
56
|
+
const state = initStamp();
|
|
57
|
+
const href = readHrefFromHash(win, pathParam);
|
|
58
|
+
const url = new URL(href, win.location.origin);
|
|
59
|
+
return {
|
|
60
|
+
href,
|
|
61
|
+
pathname: url.pathname,
|
|
62
|
+
search: url.search,
|
|
63
|
+
hash: url.hash,
|
|
64
|
+
state
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
history: createBrowserHistory({
|
|
69
|
+
window: win,
|
|
70
|
+
parseLocation,
|
|
71
|
+
createHref: (href) => writeHashParams(win, pathParam, href)
|
|
72
|
+
}),
|
|
73
|
+
/** Hash content for a router href — the SetLocation payload. */
|
|
74
|
+
serializePayload: (href) => serializeHashPayload(win, pathParam, href),
|
|
75
|
+
/** Router href from a SetLocation/ChangeFragment payload. */
|
|
76
|
+
parsePayload: (payload) => parseHashPayload(payload, pathParam),
|
|
77
|
+
/** Router href from the current document hash. */
|
|
78
|
+
parseCurrentHash: () => readHrefFromHash(win, pathParam)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const hrefFromHash = () => {
|
|
82
|
+
const hash = win.location.hash;
|
|
83
|
+
if (hash.startsWith("#?")) return new URLSearchParams(hash.slice(2)).get(pathParam) ?? "/";
|
|
84
|
+
return hash.length > 1 ? hash.slice(1) : "/";
|
|
85
|
+
};
|
|
86
|
+
const parseLocation = () => {
|
|
87
|
+
const state = initStamp();
|
|
88
|
+
const href = hrefFromHash();
|
|
89
|
+
const url = new URL(href, win.location.origin);
|
|
90
|
+
return {
|
|
91
|
+
href,
|
|
92
|
+
pathname: url.pathname,
|
|
93
|
+
search: url.search,
|
|
94
|
+
hash: url.hash,
|
|
95
|
+
state
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
history: createBrowserHistory({
|
|
100
|
+
window: win,
|
|
101
|
+
parseLocation,
|
|
102
|
+
createHref: (href) => `${win.location.pathname}${win.location.search}#${href}`
|
|
103
|
+
}),
|
|
104
|
+
/** Hash content for a router href — the SetLocation payload. */
|
|
105
|
+
serializePayload: (href) => href,
|
|
106
|
+
/** Router href from a SetLocation/ChangeFragment payload. */
|
|
107
|
+
parsePayload: (payload) => parseHashLocation(payload),
|
|
108
|
+
/** Router href from the current document hash. */
|
|
109
|
+
parseCurrentHash: hrefFromHash
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
export { DEFAULT_PATH_PARAM, createVkHistory };
|
|
114
|
+
|
|
115
|
+
//# sourceMappingURL=vk-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vk-history.js","names":[],"sources":["../../src/core/vk-history.ts"],"sourcesContent":["import { createBrowserHistory } from \"@tanstack/history\";\nimport {\n\tparseHashLocation,\n\tparseHashPayload,\n\treadHrefFromHash,\n\tserializeHashPayload,\n\twriteHashParams,\n} from \"./hash-params.ts\";\n\n/**\n * VK history: the same trick TanStack's own hash history uses —\n * `createBrowserHistory` with swapped `parseLocation`/`createHref` — so we\n * inherit blockers with rollback, popstate classification, throttling and\n * TSR index/key stamping.\n *\n * Modes (parity with the old router's three factories):\n * - `\"hash\"` (default) → old `createHashRouter`: the hash IS the location\n * (`#/item/7?tab=x`).\n * - `\"hash-param\"` → old `createHashParamRouter`: the whole router\n * location rides in one hash query param (`#?path=%2Fitem%2F7`), foreign\n * hash params (launch params, UTM) are preserved next to it, and the\n * real `location.search` is never touched.\n *\n * Deep links written by the OTHER mode resolve too (legacy tolerance).\n * `\"browser\"` → pass TanStack's default history instead (omit `history`\n * in createRouter) — old `createBrowserRouter`.\n */\n\nexport const DEFAULT_PATH_PARAM = \"path\";\n\nexport type VkHistoryMode = \"hash\" | \"hash-param\";\n\ninterface HistoryState {\n\tkey: string;\n\t__TSR_key: string;\n\t__TSR_index: number;\n}\n\nexport interface VkHistoryOptions {\n\twindow?: Window;\n\t/** Hash query param carrying the router location (hash-param mode). */\n\tpathParam?: string;\n\t/** URL mode. Default `\"hash\"`. */\n\tmode?: VkHistoryMode;\n}\n\n/** The history plus the payload codecs the bridge sync layer needs. */\nexport function createVkHistory(options: VkHistoryOptions = {}) {\n\tconst win = options.window ?? window;\n\tconst pathParam = options.pathParam ?? DEFAULT_PATH_PARAM;\n\tconst mode = options.mode ?? \"hash\";\n\n\tlet lastIndex = -1;\n\n\t/**\n\t * A history entry without TanStack state (manual hash edit / foreign\n\t * pushState) gets a FRESH index = lastIndex + 1, so popstate classifies\n\t * it as FORWARD — the entry behaves like a push (an entry parsed as\n\t * index 0 would desync any parallel stack mirror).\n\t */\n\tconst stampMissingState = (): HistoryState => {\n\t\tlastIndex += 1;\n\t\tconst key = Math.random().toString(36).slice(2, 8);\n\t\tconst state: HistoryState = { __TSR_index: lastIndex, key, __TSR_key: key };\n\t\twin.history.replaceState(state, \"\", win.location.href);\n\t\treturn state;\n\t};\n\n\tconst initStamp = (): HistoryState => {\n\t\tconst rawState = win.history.state as HistoryState | null;\n\t\tif (rawState?.__TSR_key) {\n\t\t\tif (rawState.__TSR_index > lastIndex) {\n\t\t\t\tlastIndex = rawState.__TSR_index;\n\t\t\t}\n\t\t\treturn rawState;\n\t\t}\n\t\treturn stampMissingState();\n\t};\n\n\tif (mode === \"hash-param\") {\n\t\tconst parseLocation = () => {\n\t\t\tconst state = initStamp();\n\t\t\tconst href = readHrefFromHash(win, pathParam);\n\t\t\tconst url = new URL(href, win.location.origin);\n\t\t\treturn {\n\t\t\t\thref,\n\t\t\t\tpathname: url.pathname,\n\t\t\t\tsearch: url.search,\n\t\t\t\thash: url.hash,\n\t\t\t\tstate,\n\t\t\t};\n\t\t};\n\n\t\treturn {\n\t\t\thistory: createBrowserHistory({\n\t\t\t\twindow: win,\n\t\t\t\tparseLocation,\n\t\t\t\tcreateHref: href => writeHashParams(win, pathParam, href),\n\t\t\t}),\n\t\t\t/** Hash content for a router href — the SetLocation payload. */\n\t\t\tserializePayload: (href: string) => serializeHashPayload(win, pathParam, href),\n\t\t\t/** Router href from a SetLocation/ChangeFragment payload. */\n\t\t\tparsePayload: (payload: string) => parseHashPayload(payload, pathParam),\n\t\t\t/** Router href from the current document hash. */\n\t\t\tparseCurrentHash: () => readHrefFromHash(win, pathParam),\n\t\t};\n\t}\n\n\t// hash mode: the hash IS the location; legacy `#?path=` links resolve too\n\tconst hrefFromHash = (): string => {\n\t\tconst hash = win.location.hash;\n\t\tif (hash.startsWith(\"#?\")) {\n\t\t\t// legacy hash-param deep link — resolve the path param out of it\n\t\t\treturn new URLSearchParams(hash.slice(2)).get(pathParam) ?? \"/\";\n\t\t}\n\t\treturn hash.length > 1 ? hash.slice(1) : \"/\";\n\t};\n\tconst parseLocation = () => {\n\t\tconst state = initStamp();\n\t\tconst href = hrefFromHash();\n\t\tconst url = new URL(href, win.location.origin);\n\t\treturn {\n\t\t\thref,\n\t\t\tpathname: url.pathname,\n\t\t\tsearch: url.search,\n\t\t\thash: url.hash,\n\t\t\tstate,\n\t\t};\n\t};\n\n\treturn {\n\t\thistory: createBrowserHistory({\n\t\t\twindow: win,\n\t\t\tparseLocation,\n\t\t\tcreateHref: href => `${win.location.pathname}${win.location.search}#${href}`,\n\t\t}),\n\t\t/** Hash content for a router href — the SetLocation payload. */\n\t\tserializePayload: (href: string) => href,\n\t\t/** Router href from a SetLocation/ChangeFragment payload. */\n\t\tparsePayload: (payload: string) => parseHashLocation(payload),\n\t\t/** Router href from the current document hash. */\n\t\tparseCurrentHash: hrefFromHash,\n\t};\n}\n\nexport type VkHistory = ReturnType<typeof createVkHistory>;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,qBAAqB;;AAmBlC,SAAgB,gBAAgB,UAA4B,CAAC,GAAG;CAC/D,MAAM,MAAM,QAAQ,UAAU;CAC9B,MAAM,YAAY,QAAQ,aAAA;CAC1B,MAAM,OAAO,QAAQ,QAAQ;CAE7B,IAAI,YAAY;;;;;;;CAQhB,MAAM,0BAAwC;EAC7C,aAAa;EACb,MAAM,MAAM,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;EACjD,MAAM,QAAsB;GAAE,aAAa;GAAW;GAAK,WAAW;EAAI;EAC1E,IAAI,QAAQ,aAAa,OAAO,IAAI,IAAI,SAAS,IAAI;EACrD,OAAO;CACR;CAEA,MAAM,kBAAgC;EACrC,MAAM,WAAW,IAAI,QAAQ;EAC7B,IAAI,UAAU,WAAW;GACxB,IAAI,SAAS,cAAc,WAC1B,YAAY,SAAS;GAEtB,OAAO;EACR;EACA,OAAO,kBAAkB;CAC1B;CAEA,IAAI,SAAS,cAAc;EAC1B,MAAM,sBAAsB;GAC3B,MAAM,QAAQ,UAAU;GACxB,MAAM,OAAO,iBAAiB,KAAK,SAAS;GAC5C,MAAM,MAAM,IAAI,IAAI,MAAM,IAAI,SAAS,MAAM;GAC7C,OAAO;IACN;IACA,UAAU,IAAI;IACd,QAAQ,IAAI;IACZ,MAAM,IAAI;IACV;GACD;EACD;EAEA,OAAO;GACN,SAAS,qBAAqB;IAC7B,QAAQ;IACR;IACA,aAAY,SAAQ,gBAAgB,KAAK,WAAW,IAAI;GACzD,CAAC;;GAED,mBAAmB,SAAiB,qBAAqB,KAAK,WAAW,IAAI;;GAE7E,eAAe,YAAoB,iBAAiB,SAAS,SAAS;;GAEtE,wBAAwB,iBAAiB,KAAK,SAAS;EACxD;CACD;CAGA,MAAM,qBAA6B;EAClC,MAAM,OAAO,IAAI,SAAS;EAC1B,IAAI,KAAK,WAAW,IAAI,GAEvB,OAAO,IAAI,gBAAgB,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,SAAS,KAAK;EAE7D,OAAO,KAAK,SAAS,IAAI,KAAK,MAAM,CAAC,IAAI;CAC1C;CACA,MAAM,sBAAsB;EAC3B,MAAM,QAAQ,UAAU;EACxB,MAAM,OAAO,aAAa;EAC1B,MAAM,MAAM,IAAI,IAAI,MAAM,IAAI,SAAS,MAAM;EAC7C,OAAO;GACN;GACA,UAAU,IAAI;GACd,QAAQ,IAAI;GACZ,MAAM,IAAI;GACV;EACD;CACD;CAEA,OAAO;EACN,SAAS,qBAAqB;GAC7B,QAAQ;GACR;GACA,aAAY,SAAQ,GAAG,IAAI,SAAS,WAAW,IAAI,SAAS,OAAO,GAAG;EACvE,CAAC;;EAED,mBAAmB,SAAiB;;EAEpC,eAAe,YAAoB,kBAAkB,OAAO;;EAE5D,kBAAkB;CACnB;AACD"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DEFAULT_PATH_PARAM, VkHistory, VkHistoryMode, VkHistoryOptions, createVkHistory } from "./core/vk-history.js";
|
|
2
|
+
import { PanelsStackState, applyLocation, initialPanelsStack, panelsHistoryOf } from "./core/panels-stack.js";
|
|
3
|
+
import { POPOUT_STATE_KEY, PopoutState, readPopoutState } from "./core/state-keys.js";
|
|
4
|
+
import { BridgeEventEnvelope, VkBridgeLike, normalizeBridgeEvent } from "./core/bridge-like.js";
|
|
5
|
+
import { BridgeSyncOptions, createBridgeSync } from "./core/bridge-sync.js";
|
|
6
|
+
import { createSwipeBackManager } from "./core/swipe-back.js";
|
|
7
|
+
import { parseHashLocation, parseHashPayload, readHashParams, readHrefFromHash, serializeHashLocation, serializeHashPayload, writeHashParams } from "./core/hash-params.js";
|
|
8
|
+
export { type BridgeEventEnvelope, type BridgeSyncOptions, DEFAULT_PATH_PARAM, POPOUT_STATE_KEY, type PanelsStackState, type PopoutState, type VkBridgeLike, type VkHistory, type VkHistoryMode, type VkHistoryOptions, applyLocation, createBridgeSync, createSwipeBackManager, createVkHistory, initialPanelsStack, normalizeBridgeEvent, panelsHistoryOf, parseHashLocation, parseHashPayload, readHashParams, readHrefFromHash, readPopoutState, serializeHashLocation, serializeHashPayload, writeHashParams };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { parseHashLocation, parseHashPayload, readHashParams, readHrefFromHash, serializeHashLocation, serializeHashPayload, writeHashParams } from "./core/hash-params.js";
|
|
2
|
+
import { DEFAULT_PATH_PARAM, createVkHistory } from "./core/vk-history.js";
|
|
3
|
+
import { applyLocation, initialPanelsStack, panelsHistoryOf } from "./core/panels-stack.js";
|
|
4
|
+
import { POPOUT_STATE_KEY, readPopoutState } from "./core/state-keys.js";
|
|
5
|
+
import { normalizeBridgeEvent } from "./core/bridge-like.js";
|
|
6
|
+
import { createBridgeSync } from "./core/bridge-sync.js";
|
|
7
|
+
import { createSwipeBackManager } from "./core/swipe-back.js";
|
|
8
|
+
export { DEFAULT_PATH_PARAM, POPOUT_STATE_KEY, applyLocation, createBridgeSync, createSwipeBackManager, createVkHistory, initialPanelsStack, normalizeBridgeEvent, panelsHistoryOf, parseHashLocation, parseHashPayload, readHashParams, readHrefFromHash, readPopoutState, serializeHashLocation, serializeHashPayload, writeHashParams };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AppHandle } from "./builders.js";
|
|
2
|
+
//#region src/preset/app.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Default root shell: the active view + the modal/popout hosts + inflate.
|
|
5
|
+
* The active view is the one containing the deepest matched route
|
|
6
|
+
* (single-view apps always get their only view). Multi-story apps with
|
|
7
|
+
* Epic/Root are built with a custom `root` shell out of the same pieces.
|
|
8
|
+
*
|
|
9
|
+
* An unmatched URL (global 404 or a `notFound()` thrown by `beforeLoad`)
|
|
10
|
+
* renders the VKUI `NotFound` instead of a view — falling back to the
|
|
11
|
+
* first view here would show its lazy panel as a spinner that never
|
|
12
|
+
* resolves, because an unmatched panel's chunk is never loaded.
|
|
13
|
+
*/
|
|
14
|
+
export declare function AppRootShell({ app, inflate }: {
|
|
15
|
+
app: AppHandle;
|
|
16
|
+
inflate?: boolean;
|
|
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;
|
|
26
|
+
/**
|
|
27
|
+
* Inflate (spike parity): a cold start (first history entry) on a panel
|
|
28
|
+
* that is NOT the first of its view replays the panel flow before it, so
|
|
29
|
+
* back walks the natural chain instead of closing the app. One-shot, waits
|
|
30
|
+
* for the router to go idle, skips warm navigations. `AppRootShell` runs
|
|
31
|
+
* it automatically; custom shells (Epic/Tabbar) call the hook themselves.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useInflate(app: AppHandle, enabled?: boolean): void;
|
|
34
|
+
//#endregion
|
|
35
|
+
//# sourceMappingURL=app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","names":[],"sources":["../../src/preset/app.tsx"],"mappings":";;;;;;;;;;;;;wBA2DgB,eAAe,KAAK;EAAoB,KAAK;EAAW;oBAAmB,IAAA;;;;;;;;wBA4B3E,eAAe,KAAK;;;;;;;;wBAiBpB,WAAW,KAAK,WAAW"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { looseRouteOf } from "../core/loose-route.js";
|
|
2
|
+
import { VkView, storyKeyOf } from "../vkui/vk-view.js";
|
|
3
|
+
import { Modals } from "../vkui/modals.js";
|
|
4
|
+
import { Popouts } from "../vkui/popouts.js";
|
|
5
|
+
import { NotFound } from "../vkui/not-found.js";
|
|
6
|
+
import { deriveInflateChain, interpolatePath } from "./inflate.js";
|
|
7
|
+
import { useRouter, useRouterState } from "@tanstack/react-router";
|
|
8
|
+
import { useEffect, useRef } from "react";
|
|
9
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
//#region src/preset/app.tsx
|
|
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
|
+
/**
|
|
30
|
+
* Default root shell: the active view + the modal/popout hosts + inflate.
|
|
31
|
+
* The active view is the one containing the deepest matched route
|
|
32
|
+
* (single-view apps always get their only view). Multi-story apps with
|
|
33
|
+
* Epic/Root are built with a custom `root` shell out of the same pieces.
|
|
34
|
+
*
|
|
35
|
+
* An unmatched URL (global 404 or a `notFound()` thrown by `beforeLoad`)
|
|
36
|
+
* renders the VKUI `NotFound` instead of a view — falling back to the
|
|
37
|
+
* first view here would show its lazy panel as a spinner that never
|
|
38
|
+
* resolves, because an unmatched panel's chunk is never loaded.
|
|
39
|
+
*/
|
|
40
|
+
function AppRootShell({ app, inflate = true }) {
|
|
41
|
+
useInflate(app, inflate);
|
|
42
|
+
const views = Object.values(app.views);
|
|
43
|
+
const activeViewId = useRouterState({ select: (state) => owningViewIdOf(state.matches, views) });
|
|
44
|
+
const activeView = activeViewId === null ? void 0 : Object.values(app.views).find((view) => view.id === activeViewId);
|
|
45
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
46
|
+
activeView !== void 0 ? /* @__PURE__ */ jsx(VkView, { view: activeView }) : /* @__PURE__ */ jsx(NotFound, {}),
|
|
47
|
+
/* @__PURE__ */ jsx(Modals, { entries: app.modals }),
|
|
48
|
+
/* @__PURE__ */ jsx(Popouts, { entries: app.popouts })
|
|
49
|
+
] });
|
|
50
|
+
}
|
|
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
|
+
/**
|
|
66
|
+
* Inflate (spike parity): a cold start (first history entry) on a panel
|
|
67
|
+
* that is NOT the first of its view replays the panel flow before it, so
|
|
68
|
+
* back walks the natural chain instead of closing the app. One-shot, waits
|
|
69
|
+
* for the router to go idle, skips warm navigations. `AppRootShell` runs
|
|
70
|
+
* it automatically; custom shells (Epic/Tabbar) call the hook themselves.
|
|
71
|
+
*/
|
|
72
|
+
function useInflate(app, enabled = true) {
|
|
73
|
+
const router = useRouter();
|
|
74
|
+
const snapshot = useRouterState({ select: (state) => {
|
|
75
|
+
let panelKey = null;
|
|
76
|
+
let panelPath = null;
|
|
77
|
+
const modalPaths = [];
|
|
78
|
+
for (let i = state.matches.length - 1; i >= 0; i--) {
|
|
79
|
+
const routeId = state.matches[i]?.routeId;
|
|
80
|
+
if (routeId === void 0) continue;
|
|
81
|
+
const route = looseRouteOf(router, routeId);
|
|
82
|
+
const staticData = route?.options.staticData;
|
|
83
|
+
if (staticData?.modal !== void 0 && route !== void 0) {
|
|
84
|
+
if (route.fullPath !== void 0) modalPaths.push(route.fullPath);
|
|
85
|
+
}
|
|
86
|
+
if (staticData?.panel !== void 0) {
|
|
87
|
+
panelKey = staticData.panel;
|
|
88
|
+
panelPath = route?.fullPath ?? null;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
status: state.status,
|
|
94
|
+
index: state.location.state?.__TSR_index ?? 0,
|
|
95
|
+
href: state.location.href,
|
|
96
|
+
panelKey,
|
|
97
|
+
panelPath,
|
|
98
|
+
modalPaths: modalPaths.toReversed(),
|
|
99
|
+
deepestParams: state.matches[state.matches.length - 1]?.params ?? {}
|
|
100
|
+
};
|
|
101
|
+
} });
|
|
102
|
+
const done = useRef(false);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (done.current || !enabled || snapshot.status !== "idle") return;
|
|
105
|
+
done.current = true;
|
|
106
|
+
if (snapshot.index > 0 || snapshot.panelKey === null) return;
|
|
107
|
+
const target = app.panels[snapshot.panelKey];
|
|
108
|
+
if (target === void 0) return;
|
|
109
|
+
const ordered = Object.values(app.panels).filter((panel) => panel.viewId === target.viewId);
|
|
110
|
+
let chain = deriveInflateChain(ordered, target.routeId, snapshot.deepestParams);
|
|
111
|
+
if (snapshot.modalPaths.length > 0 && snapshot.panelPath !== null) {
|
|
112
|
+
const parentHref = interpolatePath(snapshot.panelPath, snapshot.deepestParams);
|
|
113
|
+
if (parentHref !== null && chain[chain.length - 1] !== parentHref) chain = [...chain, parentHref];
|
|
114
|
+
}
|
|
115
|
+
for (const modalPath of snapshot.modalPaths) {
|
|
116
|
+
const modalHref = interpolatePath(modalPath, snapshot.deepestParams);
|
|
117
|
+
if (modalHref !== null && chain[chain.length - 1] !== modalHref) chain = [...chain, modalHref];
|
|
118
|
+
}
|
|
119
|
+
if (chain.length === 0) return;
|
|
120
|
+
const { href: targetHref } = snapshot;
|
|
121
|
+
(async () => {
|
|
122
|
+
await router.navigate({
|
|
123
|
+
href: chain[0],
|
|
124
|
+
replace: true
|
|
125
|
+
});
|
|
126
|
+
for (let i = 1; i < chain.length; i++) await router.navigate({ href: chain[i] });
|
|
127
|
+
await router.navigate({ href: targetHref });
|
|
128
|
+
})();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
export { AppRootShell, owningViewIdOf, useActiveStory, useInflate };
|
|
133
|
+
|
|
134
|
+
//# sourceMappingURL=app.js.map
|
|
@@ -0,0 +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 { 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(\n\t\t\tview => routeId === view.id || routeId.startsWith(`${view.id}/`),\n\t\t);\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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"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { ModalEntry } from "../vkui/modals.js";
|
|
2
|
+
import { PopoutEntry } from "../vkui/popouts.js";
|
|
3
|
+
import { PanelDefinition } from "./define-panel.js";
|
|
4
|
+
import { ViewDefinition } from "./define-view.js";
|
|
5
|
+
import { AnyRoute, RouteMask } from "@tanstack/react-router";
|
|
6
|
+
import { ComponentType, LazyExoticComponent, ReactNode } from "react";
|
|
7
|
+
//#region src/preset/builders.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Typed builders that generate the TanStack route tree in the /vkui
|
|
10
|
+
* convention: view = pathless layout route, panels = path children, modals
|
|
11
|
+
* = sibling child routes of their panel (chained modals are siblings — the
|
|
12
|
+
* host closes one before opening the next). Local route modals are
|
|
13
|
+
* URL-masked back to their panel (`app.routeMasks` → `createRouter`'s
|
|
14
|
+
* `routeMasks`): the address stays at the panel while any modal level is
|
|
15
|
+
* open, the raw modal path still deep-links (parsing is never masked), and
|
|
16
|
+
* the mask survives reload (history.state.__tempLocation). Global modals
|
|
17
|
+
* are rendered by the host from `?modal=<id>` on any panel (deep-linkable;
|
|
18
|
+
* the imperative open carries its own mask). Every node accepts raw
|
|
19
|
+
* TanStack options (`loader`, `beforeLoad`, `validateSearch`, `lazy`, …)
|
|
20
|
+
* passed through untouched, and the underlying routes stay reachable for
|
|
21
|
+
* escape hatches.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Route options with the builder-managed fields removed. `AnyRoute`'s
|
|
25
|
+
* options carry TanStack's generics pre-instanced, so this stays a plain
|
|
26
|
+
* structural type; the registered route tree provides the real typing for
|
|
27
|
+
* navigation, loaders and search.
|
|
28
|
+
*/
|
|
29
|
+
export type LooseRouteOptions = Partial<Omit<NonNullable<AnyRoute>["options"], "getParentRoute" | "path" | "id">>;
|
|
30
|
+
/** A renderable modal entry component — plain or React.lazy. */
|
|
31
|
+
export type ModalEntryComponent = ComponentType | LazyExoticComponent<ComponentType>;
|
|
32
|
+
/**
|
|
33
|
+
* A declared modal: a renderable component, or a component with ITS OWN
|
|
34
|
+
* nested modals. Nested modals are child routes of their parent modal and
|
|
35
|
+
* stack ON TOP of it (back unwinds the stack instead of closing everything).
|
|
36
|
+
*/
|
|
37
|
+
export interface ModalDeclaration {
|
|
38
|
+
component: ModalEntryComponent;
|
|
39
|
+
modals?: Record<string, ModalComponent>;
|
|
40
|
+
}
|
|
41
|
+
/** What a modals record accepts: either shape. */
|
|
42
|
+
export type ModalComponent = ModalEntryComponent | ModalDeclaration;
|
|
43
|
+
export interface PanelOptions extends LooseRouteOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Chunk splitting without file-based routing: resolves to the panel's
|
|
46
|
+
* component. The preset bridges it into TanStack's `route.lazy()` (which
|
|
47
|
+
* expects a `{ options }`-shaped lazy route) and hands the caller the
|
|
48
|
+
* panel's route id. The payload is either `{ component }` or a dynamic
|
|
49
|
+
* import namespace with a `default` export (`lazy: () => import("./feed")`).
|
|
50
|
+
*/
|
|
51
|
+
lazy?: PanelLazy;
|
|
52
|
+
/** Route-level modals: sibling child routes of this panel (recursible). */
|
|
53
|
+
modals?: Record<string, ModalComponent>;
|
|
54
|
+
/**
|
|
55
|
+
* Panel-local popouts: resolved BEFORE the global registry while this
|
|
56
|
+
* panel is the deepest matched one. The same id declared globally is a
|
|
57
|
+
* dev-time error.
|
|
58
|
+
*/
|
|
59
|
+
popouts?: Record<string, PopoutEntry["Component"]>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The lazy loader shape: an explicit `{ component }` payload or a dynamic
|
|
63
|
+
* import namespace whose `default` export is the component. The arrow keeps
|
|
64
|
+
* the chunk out of the main graph; the union keeps call sites terse
|
|
65
|
+
* (`lazy: () => import("./feed")`).
|
|
66
|
+
*/
|
|
67
|
+
export type PanelLazy = (routeId: string) => Promise<{
|
|
68
|
+
component: ComponentType;
|
|
69
|
+
} | {
|
|
70
|
+
default: ComponentType;
|
|
71
|
+
}>;
|
|
72
|
+
export interface ViewOptions extends LooseRouteOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Panels of the view: raw inline options (small apps, wrap `lazy` in
|
|
75
|
+
* `panel()`) or a {@link PanelDefinition} from `definePanel` — the
|
|
76
|
+
* recommended shape, whose handle lives in a leaf module so panel chunks
|
|
77
|
+
* never import the app back.
|
|
78
|
+
*/
|
|
79
|
+
panels: Record<string, PanelOptions | PanelDefinition>;
|
|
80
|
+
}
|
|
81
|
+
export interface PanelHandle<TParams = Record<string, string>, TSearch = Record<string, unknown>, TLoaderData = unknown> {
|
|
82
|
+
/** Full TanStack route id — the `from` for the /react data hooks. */
|
|
83
|
+
readonly routeId: string;
|
|
84
|
+
/** URL path of the panel. */
|
|
85
|
+
path: string;
|
|
86
|
+
/** The view route id the panel belongs to. */
|
|
87
|
+
viewId: string;
|
|
88
|
+
/** Phantom carrier — the panel's data types for the /react hooks. */
|
|
89
|
+
readonly types?: {
|
|
90
|
+
params: TParams;
|
|
91
|
+
search: TSearch;
|
|
92
|
+
loaderData: TLoaderData;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** `view:feed` + panel `post/$id` → the runtime id `/view:feed/post/$id`. */
|
|
96
|
+
export type PanelRouteId<V extends string, P extends string> = P extends "index" ? `/view:${V}/` : `/view:${V}/${P}`;
|
|
97
|
+
/** `$id`-segments of a panel path → the params object type. */
|
|
98
|
+
export type ParamsOfKey<P extends string> = P extends `${infer Head}/${infer Rest}` ? ParamsOfSegment<Head> & ParamsOfKey<Rest> : ParamsOfSegment<P>;
|
|
99
|
+
type ParamsOfSegment<S extends string> = S extends `$${infer Name}` ? { [K in Name]: string; } : {};
|
|
100
|
+
export type SearchOf<O extends PanelOptions> = O extends {
|
|
101
|
+
validateSearch: (search: never) => infer S;
|
|
102
|
+
} ? S : Record<string, never>;
|
|
103
|
+
export type LoaderDataOf<O extends PanelOptions> = O extends {
|
|
104
|
+
loader: (...args: never[]) => infer L;
|
|
105
|
+
} ? Awaited<L> : unknown;
|
|
106
|
+
/**
|
|
107
|
+
* The options a panels-map member declares — a {@link PanelDefinition}
|
|
108
|
+
* carries them in its third type parameter, raw inline options ARE the
|
|
109
|
+
* options.
|
|
110
|
+
*/
|
|
111
|
+
export type OptionsOf<M extends PanelOptions | PanelDefinition> = M extends PanelDefinition<any, any, infer O> ? O : M;
|
|
112
|
+
type UnionToIntersection<U> = (U extends unknown ? (arg: U) => void : never) extends ((arg: infer I) => void) ? I : never;
|
|
113
|
+
/**
|
|
114
|
+
* The declared panels map with LITERAL route ids and option-derived data
|
|
115
|
+
* types: `app.panels["feed/post/$id"]` autocompletes, is never undefined,
|
|
116
|
+
* its `routeId` is the literal runtime id, and `usePanelParams(handle)` &
|
|
117
|
+
* co. resolve params / search / loaderData from the declared options.
|
|
118
|
+
*/
|
|
119
|
+
export type PanelHandlesOf<TViews extends Record<string, ViewOptions>> = UnionToIntersection<{ [V in keyof TViews & string]: { [P in keyof TViews[V]["panels"] & string as `${V}/${P}`]: PanelHandle<ParamsOfKey<P>, SearchOf<OptionsOf<TViews[V]["panels"][P]>>, LoaderDataOf<OptionsOf<TViews[V]["panels"][P]>>> & {
|
|
120
|
+
readonly routeId: PanelRouteId<V, P>;
|
|
121
|
+
}; }; }[keyof TViews & string]>;
|
|
122
|
+
export interface AppHandle {
|
|
123
|
+
routeTree: AnyRoute;
|
|
124
|
+
/** Modal entries for the `<Modals>` host. */
|
|
125
|
+
modals: ModalEntry[];
|
|
126
|
+
/**
|
|
127
|
+
* Declarative URL masks for the local route modals — pass into
|
|
128
|
+
* `createRouter({ routeMasks })`, or a modal open puts its own path in
|
|
129
|
+
* the address bar.
|
|
130
|
+
*/
|
|
131
|
+
routeMasks: RouteMask<any>[];
|
|
132
|
+
/** Popout registry for the `<Popouts>` host. */
|
|
133
|
+
popouts: PopoutEntry[];
|
|
134
|
+
/** Panel handles keyed by `${viewKey}/${panelKey}`. */
|
|
135
|
+
panels: Record<string, PanelHandle>;
|
|
136
|
+
views: Record<string, AnyRoute>;
|
|
137
|
+
/** Inflate flag (see {@link AppOptions.inflate}). */
|
|
138
|
+
inflate: boolean;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The app handle with the DECLARED panels shape: literal keys
|
|
142
|
+
* (`app.panels["feed/post/$id"]` — autocomplete, never undefined) and
|
|
143
|
+
* literal route ids that drive the /react hooks' typing. Structurally an
|
|
144
|
+
* AppHandle, so it flows into AppRootShell / useInflate / deriveInflateChain.
|
|
145
|
+
*/
|
|
146
|
+
export interface TypedAppHandle<TViews extends Record<string, ViewOptions>> {
|
|
147
|
+
routeTree: AnyRoute;
|
|
148
|
+
modals: ModalEntry[];
|
|
149
|
+
routeMasks: RouteMask<any>[];
|
|
150
|
+
popouts: PopoutEntry[];
|
|
151
|
+
panels: PanelHandlesOf<TViews>;
|
|
152
|
+
views: Record<string, AnyRoute>;
|
|
153
|
+
inflate: boolean;
|
|
154
|
+
}
|
|
155
|
+
export interface AppOptionsBase {
|
|
156
|
+
/**
|
|
157
|
+
* Custom root shell (renders inside the router context). Defaults to
|
|
158
|
+
* AppRootShell: the active view + modal/popout hosts. Deliberately
|
|
159
|
+
* LOOSE (`AppHandle`, not `TypedAppHandle<TViews>`): a contextual type
|
|
160
|
+
* mentioning TViews makes the app const's own initializer circular
|
|
161
|
+
* (TS7022) — typed panel handles are read from the RETURN value.
|
|
162
|
+
*/
|
|
163
|
+
root?: (props: {
|
|
164
|
+
app: AppHandle;
|
|
165
|
+
}) => ReactNode;
|
|
166
|
+
/** Global modals: `?modal=<id>` opens them from any panel. */
|
|
167
|
+
modals?: Record<string, ModalEntryComponent>;
|
|
168
|
+
/** Global popout registry. */
|
|
169
|
+
popouts?: Record<string, PopoutEntry["Component"]>;
|
|
170
|
+
/**
|
|
171
|
+
* Inflate (default `true`): a cold start on a deep-linked panel replays
|
|
172
|
+
* the panel flow before it, so back walks the natural chain.
|
|
173
|
+
*/
|
|
174
|
+
inflate?: boolean;
|
|
175
|
+
}
|
|
176
|
+
export interface AppOptions<TViews extends Record<string, ViewOptions> = Record<string, ViewOptions>> extends AppOptionsBase {
|
|
177
|
+
views: TViews;
|
|
178
|
+
}
|
|
179
|
+
/** `createApp` with `defineView` hubs — handles live on the panel definitions. */
|
|
180
|
+
export interface AppDefinitionOptions extends AppOptionsBase {
|
|
181
|
+
views: readonly ViewDefinition[] | Record<string, ViewDefinition>;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Type-erasing wrapper for `lazy` panel chunks. The chunk module imports
|
|
185
|
+
* the app back for its typed panel handles — passing its component types
|
|
186
|
+
* straight into `views` makes the app's own inference circular (the
|
|
187
|
+
* component's inferred type depends on `app.panels`). `panel()` erases the
|
|
188
|
+
* chunk to `{ component: ComponentType }` at the boundary.
|
|
189
|
+
*
|
|
190
|
+
* ```tsx
|
|
191
|
+
* lazy: panel(() => import("./panels/feed").then(m => ({ component: m.FeedPanel })))
|
|
192
|
+
* lazy: panel(routeId => import("./panels/post.lazy").then(m => m.postLazy(routeId)))
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
/**
|
|
196
|
+
* Identity eraser for RAW inline `lazy` loaders (small apps declaring panels
|
|
197
|
+
* directly in `createApp`): the chunk modules import the app back for their
|
|
198
|
+
* handles, so the loader's return type must not carry the chunk's component
|
|
199
|
+
* type into the app's own inference. Apps using `definePanel` don't need it
|
|
200
|
+
* — the definition boundary already erases.
|
|
201
|
+
*/
|
|
202
|
+
export declare function panel(load: PanelLazy): PanelLazy;
|
|
203
|
+
/** Definition-form views: the typed handles live on the panel definitions. */
|
|
204
|
+
export declare function createApp(options: AppDefinitionOptions): AppHandle;
|
|
205
|
+
/** Raw inline options: `app.panels` is fully typed from the declared tree. */
|
|
206
|
+
export declare function createApp<const TViews extends Record<string, ViewOptions>>(options: AppOptions<TViews>): TypedAppHandle<TViews>;
|
|
207
|
+
//#endregion
|
|
208
|
+
export {};
|
|
209
|
+
//# sourceMappingURL=builders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builders.d.ts","names":[],"sources":["../../src/preset/builders.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;YAoCY,oBAAoB,QAC/B,KAAK,YAAY;;YAIN,sBAAsB,gBAAgB,oBAAoB;;;;;;iBAOrD;EAChB,WAAW;EACX,SAAS,eAAe;;;YAIb,iBAAiB,sBAAsB;iBAElC,qBAAqB;;;;;;;;EAQrC,OAAO;;EAEP,SAAS,eAAe;;;;;;EAMxB,UAAU,eAAe;;;;;;;;YASd,aACX,oBACI;EAAU,WAAW;;EAAoB,SAAS;;iBAEtC,oBAAoB;;;;;;;EAOpC,QAAQ,eAAe,eAAe;;iBAGtB,YAChB,UAAU,wBACV,UAAU,yBACV;;WAGS;;EAET;;EAEA;;WAES;IAAU,QAAQ;IAAS,QAAQ;IAAS,YAAY;;;;YAItD,aAAa,kBAAkB,oBAAoB,6BACnD,gBACA,KAAK;;YAGL,YAAY,oBAAoB,mBAAmB,cAAc,SAC1E,gBAAgB,QAAQ,YAAY,QACpC,gBAAgB;KACd,gBAAgB,oBAAoB,oBAAoB,YAAY,KAAK;YAClE,SAAS,UAAU,gBAAgB;EAC9C,iBAAiB,wBAAwB;IAEvC,IACA;YACS,aAAa,UAAU,gBAAgB;EAClD,YAAY,wBAAwB;IAElC,QAAQ;;;;;;YAQC,UAAU,UAAU,eAAe,mBAC9C,UAAU,gCAAgC,KAAK,IAAI;KAE/C,oBAAoB,MAAM,qBAAqB,KAAK,8BACxD,WAAW,cAET;;;;;;;YASS,eAAe,eAAe,eAAe,gBAAgB,uBAEtE,WAAW,qBACV,WAAW,OAAO,4BAA4B,KAAK,MAAM,YACzD,YAAY,IACZ,SAAS,UAAU,OAAO,aAAa,MACvC,aAAa,UAAU,OAAO,aAAa;WAElC,SAAS,aAAa,GAAG;cAG7B;iBAGQ;EAChB,WAAW;;EAEX,QAAQ;;;;;;EAMR,YAAY;;EAEZ,SAAS;;EAET,QAAQ,eAAe;EACvB,OAAO,eAAe;;EAEtB;;;;;;;;iBASgB,eAAe,eAAe,eAAe;EAC7D,WAAW;EACX,QAAQ;EACR,YAAY;EACZ,SAAS;EACT,QAAQ,eAAe;EACvB,OAAO,eAAe;EACtB;;iBAGgB;;;;;;;;EAQhB,QAAQ;IAAS,KAAK;QAAgB;;EAEtC,SAAS,eAAe;;EAExB,UAAU,eAAe;;;;;EAKzB;;iBAGgB,WAChB,eAAe,eAAe,eAAe,eAAe,sBACnD;EACT,OAAO;;;iBAIS,6BAA6B;EAC7C,gBAAgB,mBAAmB,eAAe;;;;;;;;;;;;;;;;;;;;;wBAsBnC,MAAM,MAAM,YAAY;;wBA6GxB,UAAU,SAAS,uBAAuB;;wBAE1C,gBAAgB,eAAe,eAAe,cAC7D,SAAS,WAAW,UAClB,eAAe"}
|