@vkma.dev/router 0.1.1 → 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.
@@ -1 +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"}
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"}
@@ -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 { 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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"}
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":";;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,eACf,SACA,OACgB;CAKhB,IAJiB,QAAQ,MACxB,UACC,MAAM,WAAW,cAAe,MAAkC,cAAc,IAEvE,GACV,OAAO;CAER,KAAK,IAAI,IAAI,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK;EAC7C,MAAM,UAAU,QAAQ,EAAE,EAAE,WAAW;EACvC,MAAM,OAAO,MAAM,MAAK,SAAQ,YAAY,KAAK,MAAM,QAAQ,WAAW,GAAG,KAAK,GAAG,EAAE,CAAC;EACxF,IAAI,SAAS,KAAA,GACZ,OAAO,KAAK;CAEd;CACA,OAAO;AACR;;;;;;;;;;;;AAaA,SAAgB,aAAa,EAAE,KAAK,UAAU,QAA+C;CAC5F,WAAW,KAAK,OAAO;CACvB,MAAM,QAAQ,OAAO,OAAO,IAAI,KAAK;CAErC,MAAM,eAAe,eAAe,EACnC,SAAS,UAAyB,eAAe,MAAM,SAAS,KAAK,EACtE,CAAC;CACD,MAAM,aACL,iBAAiB,OACd,KAAA,IACA,OAAO,OAAO,IAAI,KAAK,CAAC,CAAC,MAAK,SAAQ,KAAK,OAAO,YAAY;CAElE,OACC,qBAAA,UAAA,EAAA,UAAA;EACE,eAAe,KAAA,IAAY,oBAAC,QAAD,EAAQ,MAAM,WAAa,CAAA,IAAI,oBAAC,UAAD,CAAW,CAAA;EACtE,oBAAC,QAAD,EAAQ,SAAS,IAAI,OAAS,CAAA;EAC9B,oBAAC,SAAD,EAAS,SAAS,IAAI,QAAU,CAAA;CAC/B,EAAA,CAAA;AAEJ;;;;;;;;AASA,SAAgB,eAAe,KAA+B;CAC7D,MAAM,QAAQ,OAAO,OAAO,IAAI,KAAK;CACrC,OAAO,eAAe,EACrB,SAAS,UAAyB;EACjC,MAAM,SAAS,eAAe,MAAM,SAAS,KAAK;EAClD,OAAO,WAAW,OAAO,OAAO,WAAW,MAAM;CAClD,EACD,CAAC;AACF;;;;;;;;AASA,SAAgB,WAAW,KAAgB,UAAU,MAAY;CAChE,MAAM,SAAS,UAAU;CACzB,MAAM,WAAW,eAAe,EAC/B,SAAQ,UAAS;EAChB,IAAI,WAA0B;EAC9B,IAAI,YAA2B;EAC/B,MAAM,aAAuB,CAAC;EAC9B,KAAK,IAAI,IAAI,MAAM,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK;GACnD,MAAM,UAAU,MAAM,QAAQ,EAAE,EAAE;GAClC,IAAI,YAAY,KAAA,GACf;GAED,MAAM,QAAQ,aAAa,QAAQ,OAAO;GAC1C,MAAM,aAAa,OAAO,QAAQ;GAClC,IAAI,YAAY,UAAU,KAAA,KAAa,UAAU,KAAA,GAE5C;QAAA,MAAM,aAAa,KAAA,GACtB,WAAW,KAAK,MAAM,QAAQ;GAAA;GAGhC,IAAI,YAAY,UAAU,KAAA,GAAW;IACpC,WAAW,WAAW;IACtB,YAAY,OAAO,YAAY;IAC/B;GACD;EACD;EACA,OAAO;GACN,QAAQ,MAAM;GACd,OACE,MAAM,SAAS,OAAgD,eAChE;GACD,MAAM,MAAM,SAAS;GACrB;GACA;GACA,YAAY,WAAW,WAAW;GAClC,eAEE,MAAM,QAAQ,MAAM,QAAQ,SAAS,EAAE,EAGrC,UAAU,CAAC;EAChB;CACD,EACD,CAAC;CACD,MAAM,OAAO,OAAO,KAAK;CAEzB,gBAAgB;EACf,IAAI,KAAK,WAAW,CAAC,WAAW,SAAS,WAAW,QACnD;EAED,KAAK,UAAU;EACf,IAAI,SAAS,QAAQ,KAAK,SAAS,aAAa,MAC/C;EAED,MAAM,SAAS,IAAI,OAAO,SAAS;EACnC,IAAI,WAAW,KAAA,GACd;EAED,MAAM,UAAU,OAAO,OAAO,IAAI,MAAM,CAAC,CAAC,QAAO,UAAS,MAAM,WAAW,OAAO,MAAM;EACxF,IAAI,QAAQ,mBAAmB,SAAS,OAAO,SAAS,SAAS,aAAa;EAM9E,IAAI,SAAS,WAAW,SAAS,KAAK,SAAS,cAAc,MAAM;GAClE,MAAM,aAAa,gBAAgB,SAAS,WAAW,SAAS,aAAa;GAC7E,IAAI,eAAe,QAAQ,MAAM,MAAM,SAAS,OAAO,YACtD,QAAQ,CAAC,GAAG,OAAO,UAAU;EAE/B;EACA,KAAK,MAAM,aAAa,SAAS,YAAY;GAC5C,MAAM,YAAY,gBAAgB,WAAW,SAAS,aAAa;GACnE,IAAI,cAAc,QAAQ,MAAM,MAAM,SAAS,OAAO,WACrD,QAAQ,CAAC,GAAG,OAAO,SAAS;EAE9B;EACA,IAAI,MAAM,WAAW,GACpB;EAED,MAAM,EAAE,MAAM,eAAe;EAC7B,CAAM,YAAY;GACjB,MAAM,OAAO,SAAS;IAAE,MAAM,MAAM;IAAI,SAAS;GAAK,CAAC;GACvD,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KACjC,MAAM,OAAO,SAAS,EAAE,MAAM,MAAM,GAAG,CAAC;GAEzC,MAAM,OAAO,SAAS,EAAE,MAAM,WAAW,CAAC;EAC3C,EAAA,CAAG;CACJ,CAAC;AACF"}
@@ -1,6 +1,13 @@
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;
@@ -1 +1 @@
1
- {"version":3,"file":"vk-view.d.ts","names":[],"sources":["../../src/vkui/vk-view.tsx"],"mappings":";;wBAUgB,UAAU,OAAO;iBAoChB;;EAEhB,MAAM;;EAEN;;EAEA;;;;;;;;;;;;;;wBAee,SAAS,MAAM,KAAK,eAAe,8BAAW,IAAA"}
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"}
@@ -1,7 +1,7 @@
1
1
  import { usePanelsHistory } from "../react/hooks.js";
2
2
  import { useRouterState } from "@tanstack/react-router";
3
- import { Panel, Spinner, View } from "@vkontakte/vkui";
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 ?? "";
@@ -67,13 +67,7 @@ function VkView({ view, nav, onSwipeBack }) {
67
67
  });
68
68
  }
69
69
  function PanelSpinnerFallback() {
70
- return /* @__PURE__ */ jsx("div", {
71
- style: {
72
- paddingTop: 64,
73
- textAlign: "center"
74
- },
75
- children: /* @__PURE__ */ jsx(Spinner, { size: "l" })
76
- });
70
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(PanelHeader, {}), /* @__PURE__ */ jsx(PanelSpinner, { size: "l" })] });
77
71
  }
78
72
  //#endregion
79
73
  export { VkView, panelIdOf, storyKeyOf };
@@ -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, Spinner, 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\treturn (\n\t\t<div style={{ paddingTop: 64, textAlign: \"center\" }}>\n\t\t\t<Spinner size=\"l\" />\n\t\t</div>\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;CAC/B,OACC,oBAAC,OAAD;EAAK,OAAO;GAAE,YAAY;GAAI,WAAW;EAAS;EACjD,UAAA,oBAAC,SAAD,EAAS,MAAK,IAAK,CAAA;CACf,CAAA;AAEP"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vkma.dev/router",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "VK Mini Apps router on top of TanStack Router — VKUI views/panels/modals/popouts, VK history and bridge sync",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  import type { AnyRoute } from "@tanstack/react-router";
2
2
  import { useRouterState } from "@tanstack/react-router";
3
- import { Panel, Spinner, View } from "@vkontakte/vkui";
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
 
@@ -139,9 +139,13 @@ export function VkView({ view, nav, onSwipeBack }: VkViewProps) {
139
139
  }
140
140
 
141
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).
142
145
  return (
143
- <div style={{ paddingTop: 64, textAlign: "center" }}>
144
- <Spinner size="l" />
145
- </div>
146
+ <>
147
+ <PanelHeader />
148
+ <PanelSpinner size="l" />
149
+ </>
146
150
  );
147
151
  }
package/src/vkui.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { VkView, panelIdOf, type VkViewProps } from "./vkui/vk-view.tsx";
1
+ export { VkView, panelIdOf, storyKeyOf, type VkViewProps } from "./vkui/vk-view.tsx";
2
2
  export {
3
3
  Modals,
4
4
  ModalPageSurface,