@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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/dist/core/bridge-like.d.ts +25 -0
  3. package/dist/core/bridge-like.d.ts.map +1 -0
  4. package/dist/core/bridge-like.js +13 -0
  5. package/dist/core/bridge-like.js.map +1 -0
  6. package/dist/core/bridge-sync.d.ts +37 -0
  7. package/dist/core/bridge-sync.d.ts.map +1 -0
  8. package/dist/core/bridge-sync.js +58 -0
  9. package/dist/core/bridge-sync.js.map +1 -0
  10. package/dist/core/hash-params.d.ts +40 -0
  11. package/dist/core/hash-params.d.ts.map +1 -0
  12. package/dist/core/hash-params.js +60 -0
  13. package/dist/core/hash-params.js.map +1 -0
  14. package/dist/core/loose-route.js +9 -0
  15. package/dist/core/loose-route.js.map +1 -0
  16. package/dist/core/panels-stack.d.ts +28 -0
  17. package/dist/core/panels-stack.d.ts.map +1 -0
  18. package/dist/core/panels-stack.js +60 -0
  19. package/dist/core/panels-stack.js.map +1 -0
  20. package/dist/core/state-keys.d.ts +10 -0
  21. package/dist/core/state-keys.d.ts.map +1 -0
  22. package/dist/core/state-keys.js +21 -0
  23. package/dist/core/state-keys.js.map +1 -0
  24. package/dist/core/swipe-back.d.ts +25 -0
  25. package/dist/core/swipe-back.d.ts.map +1 -0
  26. package/dist/core/swipe-back.js +53 -0
  27. package/dist/core/swipe-back.js.map +1 -0
  28. package/dist/core/vk-history.d.ts +41 -0
  29. package/dist/core/vk-history.d.ts.map +1 -0
  30. package/dist/core/vk-history.js +115 -0
  31. package/dist/core/vk-history.js.map +1 -0
  32. package/dist/index.d.ts +8 -0
  33. package/dist/index.js +8 -0
  34. package/dist/preset/app.d.ts +35 -0
  35. package/dist/preset/app.d.ts.map +1 -0
  36. package/dist/preset/app.js +134 -0
  37. package/dist/preset/app.js.map +1 -0
  38. package/dist/preset/builders.d.ts +209 -0
  39. package/dist/preset/builders.d.ts.map +1 -0
  40. package/dist/preset/builders.js +209 -0
  41. package/dist/preset/builders.js.map +1 -0
  42. package/dist/preset/define-panel.d.ts +77 -0
  43. package/dist/preset/define-panel.d.ts.map +1 -0
  44. package/dist/preset/define-panel.js +42 -0
  45. package/dist/preset/define-panel.js.map +1 -0
  46. package/dist/preset/define-view.d.ts +42 -0
  47. package/dist/preset/define-view.d.ts.map +1 -0
  48. package/dist/preset/define-view.js +43 -0
  49. package/dist/preset/define-view.js.map +1 -0
  50. package/dist/preset/inflate.d.ts +17 -0
  51. package/dist/preset/inflate.d.ts.map +1 -0
  52. package/dist/preset/inflate.js +40 -0
  53. package/dist/preset/inflate.js.map +1 -0
  54. package/dist/preset/navigator.d.ts +75 -0
  55. package/dist/preset/navigator.d.ts.map +1 -0
  56. package/dist/preset/navigator.js +116 -0
  57. package/dist/preset/navigator.js.map +1 -0
  58. package/dist/preset.d.ts +17 -0
  59. package/dist/preset.js +16 -0
  60. package/dist/react/hooks.d.ts +69 -0
  61. package/dist/react/hooks.d.ts.map +1 -0
  62. package/dist/react/hooks.js +101 -0
  63. package/dist/react/hooks.js.map +1 -0
  64. package/dist/react.d.ts +2 -0
  65. package/dist/react.js +1 -0
  66. package/dist/vkui/choreography.js +59 -0
  67. package/dist/vkui/choreography.js.map +1 -0
  68. package/dist/vkui/modals.d.ts +49 -0
  69. package/dist/vkui/modals.d.ts.map +1 -0
  70. package/dist/vkui/modals.js +116 -0
  71. package/dist/vkui/modals.js.map +1 -0
  72. package/dist/vkui/not-found.d.ts +9 -0
  73. package/dist/vkui/not-found.d.ts.map +1 -0
  74. package/dist/vkui/not-found.js +23 -0
  75. package/dist/vkui/not-found.js.map +1 -0
  76. package/dist/vkui/popouts.d.ts +34 -0
  77. package/dist/vkui/popouts.d.ts.map +1 -0
  78. package/dist/vkui/popouts.js +69 -0
  79. package/dist/vkui/popouts.js.map +1 -0
  80. package/dist/vkui/vk-view.d.ts +26 -0
  81. package/dist/vkui/vk-view.d.ts.map +1 -0
  82. package/dist/vkui/vk-view.js +81 -0
  83. package/dist/vkui/vk-view.js.map +1 -0
  84. package/dist/vkui.d.ts +5 -0
  85. package/dist/vkui.js +5 -0
  86. package/package.json +70 -3
  87. package/src/core/bridge-like.ts +25 -0
  88. package/src/core/bridge-sync.ts +95 -0
  89. package/src/core/hash-params.ts +73 -0
  90. package/src/core/loose-route.ts +17 -0
  91. package/src/core/panels-stack.ts +85 -0
  92. package/src/core/state-keys.ts +39 -0
  93. package/src/core/swipe-back.ts +80 -0
  94. package/src/core/vk-history.ts +146 -0
  95. package/src/index.ts +30 -0
  96. package/src/preset/app.tsx +192 -0
  97. package/src/preset/builders.tsx +566 -0
  98. package/src/preset/define-panel.ts +109 -0
  99. package/src/preset/define-view.ts +73 -0
  100. package/src/preset/inflate.ts +57 -0
  101. package/src/preset/navigator.ts +250 -0
  102. package/src/preset.ts +51 -0
  103. package/src/react/hooks.ts +168 -0
  104. package/src/react.ts +11 -0
  105. package/src/vkui/choreography.ts +105 -0
  106. package/src/vkui/modals.tsx +216 -0
  107. package/src/vkui/not-found.tsx +21 -0
  108. package/src/vkui/popouts.tsx +106 -0
  109. package/src/vkui/vk-view.tsx +147 -0
  110. package/src/vkui.ts +17 -0
  111. package/README.md +0 -3
@@ -0,0 +1,209 @@
1
+ import { AppRootShell } from "./app.js";
2
+ import "./define-panel.js";
3
+ import "./define-view.js";
4
+ import { createRootRoute, createRoute, createRouteMask } from "@tanstack/react-router";
5
+ import { jsx } from "react/jsx-runtime";
6
+ //#region src/preset/builders.tsx
7
+ /**
8
+ * Type-erasing wrapper for `lazy` panel chunks. The chunk module imports
9
+ * the app back for its typed panel handles — passing its component types
10
+ * straight into `views` makes the app's own inference circular (the
11
+ * component's inferred type depends on `app.panels`). `panel()` erases the
12
+ * chunk to `{ component: ComponentType }` at the boundary.
13
+ *
14
+ * ```tsx
15
+ * lazy: panel(() => import("./panels/feed").then(m => ({ component: m.FeedPanel })))
16
+ * lazy: panel(routeId => import("./panels/post.lazy").then(m => m.postLazy(routeId)))
17
+ * ```
18
+ */
19
+ /**
20
+ * Identity eraser for RAW inline `lazy` loaders (small apps declaring panels
21
+ * directly in `createApp`): the chunk modules import the app back for their
22
+ * handles, so the loader's return type must not carry the chunk's component
23
+ * type into the app's own inference. Apps using `definePanel` don't need it
24
+ * — the definition boundary already erases.
25
+ */
26
+ function panel(load) {
27
+ return load;
28
+ }
29
+ function normalizeViews(views) {
30
+ if (Array.isArray(views)) return views.map((definition) => ({
31
+ viewKey: definition.view,
32
+ viewRest: definition.options ?? {},
33
+ viewPanels: { ...definition.panels }
34
+ }));
35
+ return Object.entries(views).map(([viewKey, declared]) => {
36
+ if ("__vkmaView" in declared) {
37
+ const definition = declared;
38
+ if (definition.view !== viewKey) console.error(`[vkma-router] view "${definition.view}" is nested under key "${viewKey}"; the hub's view string and the nesting key must match`);
39
+ return {
40
+ viewKey: definition.view,
41
+ viewRest: definition.options ?? {},
42
+ viewPanels: { ...definition.panels }
43
+ };
44
+ }
45
+ const { panels, ...viewRest } = declared;
46
+ return {
47
+ viewKey,
48
+ viewRest,
49
+ viewPanels: panels ?? {}
50
+ };
51
+ });
52
+ }
53
+ /** Normalize a declared modal into its declaration form. */
54
+ function asDeclaration(value) {
55
+ return typeof value === "object" && value !== null && "component" in value ? value : { component: value };
56
+ }
57
+ /**
58
+ * Build a modal subtree under `parentRoute` (a panel or a parent modal) and
59
+ * register every entry (flat) into the host registry. Nested modals are
60
+ * child routes of their parent modal — the stacking is expressed by the
61
+ * route tree itself.
62
+ */
63
+ function createModalTree(parentRoute, modals, registry, baseKey, panelFullPath, parentFullPath, masks) {
64
+ const routes = [];
65
+ for (const [modalKey, declared] of Object.entries(modals)) {
66
+ const { component, modals: nested } = asDeclaration(declared);
67
+ const modalRoute = createRoute({
68
+ getParentRoute: () => parentRoute,
69
+ path: modalKey,
70
+ staticData: { modal: `${baseKey}/${modalKey}` }
71
+ });
72
+ const modalFullPath = parentFullPath === "/" ? `/${modalKey}` : `${parentFullPath}/${modalKey}`;
73
+ masks.push({
74
+ from: modalFullPath,
75
+ to: panelFullPath
76
+ });
77
+ registry.push({
78
+ get routeId() {
79
+ return modalRoute.id;
80
+ },
81
+ get id() {
82
+ return modalRoute.id;
83
+ },
84
+ Component: component
85
+ });
86
+ routes.push(modalRoute);
87
+ if (nested !== void 0) {
88
+ const children = createModalTree(modalRoute, nested, registry, `${baseKey}/${modalKey}`, panelFullPath, modalFullPath, masks);
89
+ if (children.length > 0) modalRoute.addChildren(children);
90
+ }
91
+ }
92
+ return routes;
93
+ }
94
+ function createApp(options) {
95
+ const modals = [];
96
+ const pendingMasks = [];
97
+ const popouts = Object.entries(options.popouts ?? {}).map(([id, Component]) => ({
98
+ id,
99
+ Component
100
+ }));
101
+ const globalPopoutIds = new Set(Object.keys(options.popouts ?? {}));
102
+ const views = {};
103
+ const panels = {};
104
+ /**
105
+ * TanStack finalizes route ids in `route.init()` during `createRouter` —
106
+ * they are not readable at build time. The handle therefore exposes
107
+ * GETTERS: consumers read `routeId` at render time, when the router
108
+ * (and the ids) already exist.
109
+ */
110
+ const deferredPanels = [];
111
+ /** full panel path → owning panel key, for the duplicate-path dev check */
112
+ const usedPaths = /* @__PURE__ */ new Map();
113
+ const usedViewKeys = /* @__PURE__ */ new Set();
114
+ const customRoot = options.root;
115
+ const rootRoute = createRootRoute({ component: customRoot ? (routeProps) => customRoot({
116
+ ...routeProps,
117
+ app: handle
118
+ }) : () => /* @__PURE__ */ jsx(AppRootShell, { app: handle }) });
119
+ for (const { viewKey, viewRest, viewPanels } of normalizeViews(options.views)) {
120
+ if (usedViewKeys.has(viewKey)) {
121
+ console.error(`[vkma-router] duplicate view "${viewKey}"; a view must be declared exactly once (one defineView hub)`);
122
+ continue;
123
+ }
124
+ usedViewKeys.add(viewKey);
125
+ const viewRoute = createRoute({
126
+ getParentRoute: () => rootRoute,
127
+ id: `view:${viewKey}`,
128
+ ...viewRest
129
+ });
130
+ const panelRoutes = [];
131
+ for (const [panelKey, declared] of Object.entries(viewPanels)) {
132
+ const panelOptions = "__vkmaPanel" in declared ? declared.options : declared;
133
+ if ("__vkmaPanel" in declared && declared.view !== viewKey) console.error(`[vkma-router] panel "${panelKey}" is defined for view "${declared.view}" but nested under "views.${viewKey}"; its handle route id and the tree would disagree`);
134
+ const { modals: panelModals, lazy, popouts: panelPopouts, ...panelRest } = panelOptions;
135
+ const panelRegistryKey = `${viewKey}/${panelKey}`;
136
+ const localPopoutIds = /* @__PURE__ */ new Set();
137
+ for (const [popoutId, Component] of Object.entries(panelPopouts ?? {})) {
138
+ if (globalPopoutIds.has(popoutId)) console.error(`[vkma-router] popout "${popoutId}" is declared both globally and on panel "${panelRegistryKey}"; the local one shadows the global — rename one of them`);
139
+ if (localPopoutIds.has(popoutId)) {
140
+ console.error(`[vkma-router] duplicate popout "${popoutId}" on panel "${panelRegistryKey}"`);
141
+ continue;
142
+ }
143
+ localPopoutIds.add(popoutId);
144
+ popouts.push({
145
+ id: popoutId,
146
+ Component,
147
+ panel: panelRegistryKey
148
+ });
149
+ }
150
+ const panelPath = panelKey === "index" ? "/" : panelKey;
151
+ const pathOwner = usedPaths.get(panelPath);
152
+ if (pathOwner !== void 0) console.error(`[vkma-router] duplicate panel path "${panelPath}" (${pathOwner} and ${viewKey}/${panelKey}); only one view may own "/" — give the other home panels distinct path segments (e.g. "feed")`);
153
+ else usedPaths.set(panelPath, `${viewKey}/${panelKey}`);
154
+ const panelRoute = createRoute({
155
+ getParentRoute: () => viewRoute,
156
+ path: panelPath,
157
+ staticData: { panel: `${viewKey}/${panelKey}` },
158
+ ...panelRest
159
+ });
160
+ if (lazy !== void 0) panelRoute.lazy(() => Promise.resolve(lazy(panelRoute.id)).then((payload) => ({ options: "component" in payload ? payload : { component: payload.default } })));
161
+ const panelFullPath = panelPath === "/" ? "/" : `/${panelPath.replace(/^\//, "")}`;
162
+ const modalRoutes = createModalTree(panelRoute, panelModals ?? {}, modals, `${viewKey}/${panelKey}`, panelFullPath, panelFullPath, pendingMasks);
163
+ panelRoute.addChildren(modalRoutes);
164
+ panelRoutes.push(panelRoute);
165
+ deferredPanels.push({
166
+ key: `${viewKey}/${panelKey}`,
167
+ path: panelPath,
168
+ route: panelRoute
169
+ });
170
+ }
171
+ viewRoute.addChildren(panelRoutes);
172
+ views[viewKey] = viewRoute;
173
+ }
174
+ rootRoute.addChildren(Object.values(views));
175
+ const routeMasks = pendingMasks.map((mask) => createRouteMask({
176
+ routeTree: rootRoute,
177
+ from: mask.from,
178
+ to: mask.to,
179
+ params: true
180
+ }));
181
+ for (const { key, path, route } of deferredPanels) panels[key] = {
182
+ get routeId() {
183
+ return route.id;
184
+ },
185
+ path,
186
+ get viewId() {
187
+ return String(route.id).match(/^\/?view:([^/]+)/)?.[1] ?? String(route.id);
188
+ }
189
+ };
190
+ for (const [modalKey, Modal] of Object.entries(options.modals ?? {})) modals.push({
191
+ routeId: `?modal=${modalKey}`,
192
+ id: modalKey,
193
+ Component: Modal
194
+ });
195
+ const handle = {
196
+ routeTree: rootRoute,
197
+ modals,
198
+ routeMasks,
199
+ popouts,
200
+ panels,
201
+ views,
202
+ inflate: options.inflate ?? true
203
+ };
204
+ return handle;
205
+ }
206
+ //#endregion
207
+ export { createApp, panel };
208
+
209
+ //# sourceMappingURL=builders.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builders.js","names":[],"sources":["../../src/preset/builders.tsx"],"sourcesContent":["import {\n\tcreateRootRoute,\n\tcreateRoute,\n\tcreateRouteMask,\n\ttype AnyRoute,\n\ttype RouteMask,\n} from \"@tanstack/react-router\";\nimport type { ComponentType, LazyExoticComponent, ReactNode } from \"react\";\nimport type { ModalEntry } from \"../vkui/modals.tsx\";\nimport type { PopoutEntry } from \"../vkui/popouts.tsx\";\nimport { AppRootShell } from \"./app.tsx\";\nimport { PANEL_DEFINITION, type PanelDefinition } from \"./define-panel.ts\";\nimport { VIEW_DEFINITION, type ViewDefinition } from \"./define-view.ts\";\n\n/**\n * Typed builders that generate the TanStack route tree in the /vkui\n * convention: view = pathless layout route, panels = path children, modals\n * = sibling child routes of their panel (chained modals are siblings — the\n * host closes one before opening the next). Local route modals are\n * URL-masked back to their panel (`app.routeMasks` → `createRouter`'s\n * `routeMasks`): the address stays at the panel while any modal level is\n * open, the raw modal path still deep-links (parsing is never masked), and\n * the mask survives reload (history.state.__tempLocation). Global modals\n * are rendered by the host from `?modal=<id>` on any panel (deep-linkable;\n * the imperative open carries its own mask). Every node accepts raw\n * TanStack options (`loader`, `beforeLoad`, `validateSearch`, `lazy`, …)\n * passed through untouched, and the underlying routes stay reachable for\n * escape hatches.\n */\n\n/**\n * Route options with the builder-managed fields removed. `AnyRoute`'s\n * options carry TanStack's generics pre-instanced, so this stays a plain\n * structural type; the registered route tree provides the real typing for\n * navigation, loaders and search.\n */\nexport type LooseRouteOptions = Partial<\n\tOmit<NonNullable<AnyRoute>[\"options\"], \"getParentRoute\" | \"path\" | \"id\">\n>;\n\n/** A renderable modal entry component — plain or React.lazy. */\nexport type ModalEntryComponent = ComponentType | LazyExoticComponent<ComponentType>;\n\n/**\n * A declared modal: a renderable component, or a component with ITS OWN\n * nested modals. Nested modals are child routes of their parent modal and\n * stack ON TOP of it (back unwinds the stack instead of closing everything).\n */\nexport interface ModalDeclaration {\n\tcomponent: ModalEntryComponent;\n\tmodals?: Record<string, ModalComponent>;\n}\n\n/** What a modals record accepts: either shape. */\nexport type ModalComponent = ModalEntryComponent | ModalDeclaration;\n\nexport interface PanelOptions extends LooseRouteOptions {\n\t/**\n\t * Chunk splitting without file-based routing: resolves to the panel's\n\t * component. The preset bridges it into TanStack's `route.lazy()` (which\n\t * expects a `{ options }`-shaped lazy route) and hands the caller the\n\t * panel's route id. The payload is either `{ component }` or a dynamic\n\t * import namespace with a `default` export (`lazy: () => import(\"./feed\")`).\n\t */\n\tlazy?: PanelLazy;\n\t/** Route-level modals: sibling child routes of this panel (recursible). */\n\tmodals?: Record<string, ModalComponent>;\n\t/**\n\t * Panel-local popouts: resolved BEFORE the global registry while this\n\t * panel is the deepest matched one. The same id declared globally is a\n\t * dev-time error.\n\t */\n\tpopouts?: Record<string, PopoutEntry[\"Component\"]>;\n}\n\n/**\n * The lazy loader shape: an explicit `{ component }` payload or a dynamic\n * import namespace whose `default` export is the component. The arrow keeps\n * the chunk out of the main graph; the union keeps call sites terse\n * (`lazy: () => import(\"./feed\")`).\n */\nexport type PanelLazy = (\n\trouteId: string,\n) => Promise<{ component: ComponentType } | { default: ComponentType }>;\n\nexport interface ViewOptions extends LooseRouteOptions {\n\t/**\n\t * Panels of the view: raw inline options (small apps, wrap `lazy` in\n\t * `panel()`) or a {@link PanelDefinition} from `definePanel` — the\n\t * recommended shape, whose handle lives in a leaf module so panel chunks\n\t * never import the app back.\n\t */\n\tpanels: Record<string, PanelOptions | PanelDefinition>;\n}\n\nexport interface PanelHandle<\n\tTParams = Record<string, string>,\n\tTSearch = Record<string, unknown>,\n\tTLoaderData = unknown,\n> {\n\t/** Full TanStack route id — the `from` for the /react data hooks. */\n\treadonly routeId: string;\n\t/** URL path of the panel. */\n\tpath: string;\n\t/** The view route id the panel belongs to. */\n\tviewId: string;\n\t/** Phantom carrier — the panel's data types for the /react hooks. */\n\treadonly types?: { params: TParams; search: TSearch; loaderData: TLoaderData };\n}\n\n/** `view:feed` + panel `post/$id` → the runtime id `/view:feed/post/$id`. */\nexport type PanelRouteId<V extends string, P extends string> = P extends \"index\"\n\t? `/view:${V}/`\n\t: `/view:${V}/${P}`;\n\n/** `$id`-segments of a panel path → the params object type. */\nexport type ParamsOfKey<P extends string> = P extends `${infer Head}/${infer Rest}`\n\t? ParamsOfSegment<Head> & ParamsOfKey<Rest>\n\t: ParamsOfSegment<P>;\ntype ParamsOfSegment<S extends string> = S extends `$${infer Name}` ? { [K in Name]: string } : {};\nexport type SearchOf<O extends PanelOptions> = O extends {\n\tvalidateSearch: (search: never) => infer S;\n}\n\t? S\n\t: Record<string, never>;\nexport type LoaderDataOf<O extends PanelOptions> = O extends {\n\tloader: (...args: never[]) => infer L;\n}\n\t? Awaited<L>\n\t: unknown;\n\n/**\n * The options a panels-map member declares — a {@link PanelDefinition}\n * carries them in its third type parameter, raw inline options ARE the\n * options.\n */\nexport type OptionsOf<M extends PanelOptions | PanelDefinition> =\n\tM extends PanelDefinition<any, any, infer O> ? O : M;\n\ntype UnionToIntersection<U> = (U extends unknown ? (arg: U) => void : never) extends (\n\targ: infer I,\n) => void\n\t? I\n\t: never;\n\n/**\n * The declared panels map with LITERAL route ids and option-derived data\n * types: `app.panels[\"feed/post/$id\"]` autocompletes, is never undefined,\n * its `routeId` is the literal runtime id, and `usePanelParams(handle)` &\n * co. resolve params / search / loaderData from the declared options.\n */\nexport type PanelHandlesOf<TViews extends Record<string, ViewOptions>> = UnionToIntersection<\n\t{\n\t\t[V in keyof TViews & string]: {\n\t\t\t[P in keyof TViews[V][\"panels\"] & string as `${V}/${P}`]: PanelHandle<\n\t\t\t\tParamsOfKey<P>,\n\t\t\t\tSearchOf<OptionsOf<TViews[V][\"panels\"][P]>>,\n\t\t\t\tLoaderDataOf<OptionsOf<TViews[V][\"panels\"][P]>>\n\t\t\t> & {\n\t\t\t\treadonly routeId: PanelRouteId<V, P>;\n\t\t\t};\n\t\t};\n\t}[keyof TViews & string]\n>;\n\nexport interface AppHandle {\n\trouteTree: AnyRoute;\n\t/** Modal entries for the `<Modals>` host. */\n\tmodals: ModalEntry[];\n\t/**\n\t * Declarative URL masks for the local route modals — pass into\n\t * `createRouter({ routeMasks })`, or a modal open puts its own path in\n\t * the address bar.\n\t */\n\trouteMasks: RouteMask<any>[];\n\t/** Popout registry for the `<Popouts>` host. */\n\tpopouts: PopoutEntry[];\n\t/** Panel handles keyed by `${viewKey}/${panelKey}`. */\n\tpanels: Record<string, PanelHandle>;\n\tviews: Record<string, AnyRoute>;\n\t/** Inflate flag (see {@link AppOptions.inflate}). */\n\tinflate: boolean;\n}\n\n/**\n * The app handle with the DECLARED panels shape: literal keys\n * (`app.panels[\"feed/post/$id\"]` — autocomplete, never undefined) and\n * literal route ids that drive the /react hooks' typing. Structurally an\n * AppHandle, so it flows into AppRootShell / useInflate / deriveInflateChain.\n */\nexport interface TypedAppHandle<TViews extends Record<string, ViewOptions>> {\n\trouteTree: AnyRoute;\n\tmodals: ModalEntry[];\n\trouteMasks: RouteMask<any>[];\n\tpopouts: PopoutEntry[];\n\tpanels: PanelHandlesOf<TViews>;\n\tviews: Record<string, AnyRoute>;\n\tinflate: boolean;\n}\n\nexport interface AppOptionsBase {\n\t/**\n\t * Custom root shell (renders inside the router context). Defaults to\n\t * AppRootShell: the active view + modal/popout hosts. Deliberately\n\t * LOOSE (`AppHandle`, not `TypedAppHandle<TViews>`): a contextual type\n\t * mentioning TViews makes the app const's own initializer circular\n\t * (TS7022) — typed panel handles are read from the RETURN value.\n\t */\n\troot?: (props: { app: AppHandle }) => ReactNode;\n\t/** Global modals: `?modal=<id>` opens them from any panel. */\n\tmodals?: Record<string, ModalEntryComponent>;\n\t/** Global popout registry. */\n\tpopouts?: Record<string, PopoutEntry[\"Component\"]>;\n\t/**\n\t * Inflate (default `true`): a cold start on a deep-linked panel replays\n\t * the panel flow before it, so back walks the natural chain.\n\t */\n\tinflate?: boolean;\n}\n\nexport interface AppOptions<\n\tTViews extends Record<string, ViewOptions> = Record<string, ViewOptions>,\n> extends AppOptionsBase {\n\tviews: TViews;\n}\n\n/** `createApp` with `defineView` hubs — handles live on the panel definitions. */\nexport interface AppDefinitionOptions extends AppOptionsBase {\n\tviews: readonly ViewDefinition[] | Record<string, ViewDefinition>;\n}\n\n/**\n * Type-erasing wrapper for `lazy` panel chunks. The chunk module imports\n * the app back for its typed panel handles — passing its component types\n * straight into `views` makes the app's own inference circular (the\n * component's inferred type depends on `app.panels`). `panel()` erases the\n * chunk to `{ component: ComponentType }` at the boundary.\n *\n * ```tsx\n * lazy: panel(() => import(\"./panels/feed\").then(m => ({ component: m.FeedPanel })))\n * lazy: panel(routeId => import(\"./panels/post.lazy\").then(m => m.postLazy(routeId)))\n * ```\n */\n/**\n * Identity eraser for RAW inline `lazy` loaders (small apps declaring panels\n * directly in `createApp`): the chunk modules import the app back for their\n * handles, so the loader's return type must not carry the chunk's component\n * type into the app's own inference. Apps using `definePanel` don't need it\n * — the definition boundary already erases.\n */\nexport function panel(load: PanelLazy): PanelLazy {\n\treturn load;\n}\n\n/** Normalized view entries the builder loop consumes. */\ninterface DeclaredView {\n\tviewKey: string;\n\tviewRest: LooseRouteOptions;\n\tviewPanels: Record<string, PanelOptions | PanelDefinition>;\n}\n\nfunction normalizeViews(views: unknown): DeclaredView[] {\n\tif (Array.isArray(views)) {\n\t\treturn (views as readonly ViewDefinition[]).map(definition => ({\n\t\t\tviewKey: definition.view,\n\t\t\tviewRest: definition.options ?? {},\n\t\t\tviewPanels: { ...definition.panels },\n\t\t}));\n\t}\n\treturn Object.entries(views as Record<string, unknown>).map(([viewKey, declared]) => {\n\t\tif (VIEW_DEFINITION in (declared as object)) {\n\t\t\tconst definition = declared as ViewDefinition;\n\t\t\tif (definition.view !== viewKey) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[vkma-router] view \"${definition.view}\" is nested under key \"${viewKey}\"; the hub's view string and the nesting key must match`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tviewKey: definition.view,\n\t\t\t\tviewRest: definition.options ?? {},\n\t\t\t\tviewPanels: { ...definition.panels },\n\t\t\t};\n\t\t}\n\t\tconst { panels, ...viewRest } = declared as ViewOptions;\n\t\treturn { viewKey, viewRest: viewRest as LooseRouteOptions, viewPanels: panels ?? {} };\n\t});\n}\n\n/** Normalize a declared modal into its declaration form. */\nfunction asDeclaration(value: ModalComponent): ModalDeclaration {\n\t// shape check, NOT typeof: lazy/memo/forwardRef components are objects\n\treturn typeof value === \"object\" && value !== null && \"component\" in value\n\t\t? value\n\t\t: { component: value };\n}\n\n/**\n * Build a modal subtree under `parentRoute` (a panel or a parent modal) and\n * register every entry (flat) into the host registry. Nested modals are\n * child routes of their parent modal — the stacking is expressed by the\n * route tree itself.\n */\nfunction createModalTree(\n\tparentRoute: AnyRoute,\n\tmodals: Record<string, ModalComponent>,\n\tregistry: ModalEntry[],\n\tbaseKey: string,\n\t/** Full path of the OWNING panel — every level's mask displays it. */\n\tpanelFullPath: string,\n\t/** Full path of the parent in the modal chain (panel or parent modal). */\n\tparentFullPath: string,\n\t/** Mask pairs collected for the whole app, mapped to RouteMask later. */\n\tmasks: Array<{ from: string; to: string }>,\n): AnyRoute[] {\n\tconst routes: AnyRoute[] = [];\n\tfor (const [modalKey, declared] of Object.entries(modals)) {\n\t\tconst { component, modals: nested } = asDeclaration(declared);\n\t\tconst modalRoute = createRoute({\n\t\t\tgetParentRoute: () => parentRoute,\n\t\t\tpath: modalKey,\n\t\t\tstaticData: { modal: `${baseKey}/${modalKey}` },\n\t\t});\n\t\tconst modalFullPath =\n\t\t\tparentFullPath === \"/\" ? `/${modalKey}` : `${parentFullPath}/${modalKey}`;\n\t\t// while ANY level of the chain is open the URL keeps showing the\n\t\t// OWNING panel's path — nested levels mask to the panel too, so the\n\t\t// display never leaks a modal segment (the raw modal path still\n\t\t// deep-links: parsing is never masked)\n\t\tmasks.push({ from: modalFullPath, to: panelFullPath });\n\t\tregistry.push({\n\t\t\tget routeId() {\n\t\t\t\treturn modalRoute.id;\n\t\t\t},\n\t\t\tget id() {\n\t\t\t\treturn modalRoute.id;\n\t\t\t},\n\t\t\tComponent: component,\n\t\t});\n\t\troutes.push(modalRoute);\n\t\tif (nested !== undefined) {\n\t\t\tconst children = createModalTree(\n\t\t\t\tmodalRoute,\n\t\t\t\tnested,\n\t\t\t\tregistry,\n\t\t\t\t`${baseKey}/${modalKey}`,\n\t\t\t\tpanelFullPath,\n\t\t\t\tmodalFullPath,\n\t\t\t\tmasks,\n\t\t\t);\n\t\t\tif (children.length > 0) {\n\t\t\t\t// addChildren REPLACES the children array — one call per route\n\t\t\t\tmodalRoute.addChildren(children);\n\t\t\t}\n\t\t}\n\t}\n\treturn routes;\n}\n\n/** Definition-form views: the typed handles live on the panel definitions. */\nexport function createApp(options: AppDefinitionOptions): AppHandle;\n/** Raw inline options: `app.panels` is fully typed from the declared tree. */\nexport function createApp<const TViews extends Record<string, ViewOptions>>(\n\toptions: AppOptions<TViews>,\n): TypedAppHandle<TViews>;\nexport function createApp(options: AppOptions | AppDefinitionOptions): any {\n\tconst modals: ModalEntry[] = [];\n\tconst pendingMasks: Array<{ from: string; to: string }> = [];\n\tconst popouts: PopoutEntry[] = Object.entries(options.popouts ?? {}).map(([id, Component]) => ({\n\t\tid,\n\t\tComponent,\n\t}));\n\tconst globalPopoutIds = new Set(Object.keys(options.popouts ?? {}));\n\n\tconst views: Record<string, AnyRoute> = {};\n\tconst panels: Record<string, PanelHandle> = {};\n\t/**\n\t * TanStack finalizes route ids in `route.init()` during `createRouter` —\n\t * they are not readable at build time. The handle therefore exposes\n\t * GETTERS: consumers read `routeId` at render time, when the router\n\t * (and the ids) already exist.\n\t */\n\tconst deferredPanels: Array<{\n\t\tkey: string;\n\t\tpath: string;\n\t\troute: AnyRoute;\n\t}> = [];\n\t/** full panel path → owning panel key, for the duplicate-path dev check */\n\tconst usedPaths = new Map<string, string>();\n\tconst usedViewKeys = new Set<string>();\n\n\tconst customRoot = options.root;\n\tconst rootRoute = createRootRoute({\n\t\t// a custom root receives the app handle (plus the raw match props);\n\t\t// the default is the auto-shell. Both close over the handle — the\n\t\t// component only runs at render time, after the binding exists.\n\t\tcomponent: (customRoot\n\t\t\t? (routeProps: Record<string, unknown>) =>\n\t\t\t\t\tcustomRoot({ ...routeProps, app: handle }) as never\n\t\t\t: () => <AppRootShell app={handle} />) as never,\n\t});\n\n\tfor (const { viewKey, viewRest, viewPanels } of normalizeViews(options.views)) {\n\t\tif (usedViewKeys.has(viewKey)) {\n\t\t\tconsole.error(\n\t\t\t\t`[vkma-router] duplicate view \"${viewKey}\"; a view must be declared exactly once (one defineView hub)`,\n\t\t\t);\n\t\t\tcontinue;\n\t\t}\n\t\tusedViewKeys.add(viewKey);\n\t\tconst viewRoute = createRoute({\n\t\t\tgetParentRoute: () => rootRoute,\n\t\t\tid: `view:${viewKey}`,\n\t\t\t...(viewRest as Partial<Record<string, unknown>>),\n\t\t});\n\n\t\tconst panelRoutes: AnyRoute[] = [];\n\t\tfor (const [panelKey, declared] of Object.entries(viewPanels)) {\n\t\t\t// a definition carries its options inside; raw options ARE the options\n\t\t\tconst panelOptions: PanelOptions =\n\t\t\t\tPANEL_DEFINITION in declared ? declared.options : declared;\n\t\t\tif (PANEL_DEFINITION in declared && declared.view !== viewKey) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[vkma-router] panel \"${panelKey}\" is defined for view \"${declared.view}\" but nested under \"views.${viewKey}\"; its handle route id and the tree would disagree`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst { modals: panelModals, lazy, popouts: panelPopouts, ...panelRest } = panelOptions;\n\t\t\t// panel-local popouts: resolved before the global registry while\n\t\t\t// this panel is the deepest matched one; a global id collision is\n\t\t\t// a dev-time error (the local declaration would shadow it)\n\t\t\tconst panelRegistryKey = `${viewKey}/${panelKey}`;\n\t\t\tconst localPopoutIds = new Set<string>();\n\t\t\tfor (const [popoutId, Component] of Object.entries(panelPopouts ?? {})) {\n\t\t\t\tif (globalPopoutIds.has(popoutId)) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[vkma-router] popout \"${popoutId}\" is declared both globally and on panel \"${panelRegistryKey}\"; the local one shadows the global — rename one of them`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (localPopoutIds.has(popoutId)) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[vkma-router] duplicate popout \"${popoutId}\" on panel \"${panelRegistryKey}\"`,\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tlocalPopoutIds.add(popoutId);\n\t\t\t\tpopouts.push({ id: popoutId, Component, panel: panelRegistryKey });\n\t\t\t}\n\t\t\tconst panelPath = panelKey === \"index\" ? \"/\" : panelKey;\n\t\t\t// dev check: two panels with the same full path silently break\n\t\t\t// matching (the router resolves the conflict arbitrarily) — a\n\t\t\t// multi-view app must give every home panel its own segment\n\t\t\tconst pathOwner = usedPaths.get(panelPath);\n\t\t\tif (pathOwner !== undefined) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[vkma-router] duplicate panel path \"${panelPath}\" (${pathOwner} and ${viewKey}/${panelKey}); only one view may own \"/\" — give the other home panels distinct path segments (e.g. \"feed\")`,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tusedPaths.set(panelPath, `${viewKey}/${panelKey}`);\n\t\t\t}\n\t\t\tconst panelRoute = createRoute({\n\t\t\t\tgetParentRoute: () => viewRoute,\n\t\t\t\tpath: panelPath,\n\t\t\t\tstaticData: { panel: `${viewKey}/${panelKey}` },\n\t\t\t\t...(panelRest as Partial<Record<string, unknown>>),\n\t\t\t});\n\t\t\tif (lazy !== undefined) {\n\t\t\t\t// runs on match load — the router (and ids) exist by then;\n\t\t\t\t// TanStack's route.lazy() merges `lazyRoute.options`, the\n\t\t\t\t// builder bridges the plain-options contract (both payload\n\t\t\t\t// shapes) into that shape\n\t\t\t\tpanelRoute.lazy(\n\t\t\t\t\t() =>\n\t\t\t\t\t\tPromise.resolve(lazy(panelRoute.id)).then(payload => ({\n\t\t\t\t\t\t\toptions:\n\t\t\t\t\t\t\t\t\"component\" in payload ? payload : { component: payload.default },\n\t\t\t\t\t\t})) as never,\n\t\t\t\t);\n\t\t\t}\n\t\t\t// the modal subtree (nested modals are child routes of their\n\t\t\t// parent modal); entries register flat into the host registry.\n\t\t\t// The view is a pathless layout route — the panel's full path is\n\t\t\t// just its declared path rooted at \"/\"\n\t\t\tconst panelFullPath = panelPath === \"/\" ? \"/\" : `/${panelPath.replace(/^\\//, \"\")}`;\n\t\t\tconst modalRoutes = createModalTree(\n\t\t\t\tpanelRoute,\n\t\t\t\tpanelModals ?? {},\n\t\t\t\tmodals,\n\t\t\t\t`${viewKey}/${panelKey}`,\n\t\t\t\tpanelFullPath,\n\t\t\t\tpanelFullPath,\n\t\t\t\tpendingMasks,\n\t\t\t);\n\t\t\t// addChildren REPLACES the children array — one call after the loop\n\t\t\t// (per-modal calls would orphan every modal but the last)\n\t\t\tpanelRoute.addChildren(modalRoutes);\n\t\t\tpanelRoutes.push(panelRoute);\n\t\t\tdeferredPanels.push({\n\t\t\t\tkey: `${viewKey}/${panelKey}`,\n\t\t\t\tpath: panelPath,\n\t\t\t\troute: panelRoute,\n\t\t\t});\n\t\t}\n\n\t\tviewRoute.addChildren(panelRoutes);\n\t\tviews[viewKey] = viewRoute;\n\t}\n\n\trootRoute.addChildren(Object.values(views));\n\n\t// Declarative masks for the local route modals: while a modal is open\n\t// the router displays the owning panel's path (createRouteMask is\n\t// identity — the router matches `from` at navigation time and stores\n\t// the real location in history.state.__tempLocation, so the raw modal\n\t// path still deep-links and the mask survives reload)\n\tconst routeMasks = pendingMasks.map(\n\t\tmask =>\n\t\t\tcreateRouteMask({\n\t\t\t\trouteTree: rootRoute as never,\n\t\t\t\tfrom: mask.from,\n\t\t\t\tto: mask.to,\n\t\t\t\t// modal routes are children of their panel — the params ($id, …)\n\t\t\t\t// are shared, forward them to the displayed location wholesale\n\t\t\t\tparams: true,\n\t\t\t} as never) as RouteMask<any>,\n\t);\n\n\tfor (const { key, path, route } of deferredPanels) {\n\t\tpanels[key] = {\n\t\t\tget routeId() {\n\t\t\t\treturn route.id;\n\t\t\t},\n\t\t\tpath,\n\t\t\tget viewId() {\n\t\t\t\t// runtime ids carry a leading slash: \"/view:feed\" — split(\"/\")[0]\n\t\t\t\t// is ALWAYS \"\" (that silently broke the inflate view filter: every\n\t\t\t\t// panel matched every view, so a cold home deep link replayed the\n\t\t\t\t// first other view's panels before the target)\n\t\t\t\tconst match = String(route.id).match(/^\\/?view:([^/]+)/);\n\t\t\t\treturn match?.[1] ?? String(route.id);\n\t\t\t},\n\t\t};\n\t}\n\n\t// global modals: no route — the Modals host opens them from the\n\t// `?modal=<id>` search param on any panel (deep-linkable; the\n\t// imperative open in useNavigator masks the URL back to the panel)\n\tfor (const [modalKey, Modal] of Object.entries(options.modals ?? {})) {\n\t\tmodals.push({\n\t\t\trouteId: `?modal=${modalKey}`,\n\t\t\tid: modalKey,\n\t\t\tComponent: Modal,\n\t\t});\n\t}\n\n\tconst handle: AppHandle = {\n\t\trouteTree: rootRoute,\n\t\tmodals,\n\t\trouteMasks,\n\t\tpopouts,\n\t\tpanels,\n\t\tviews,\n\t\tinflate: options.inflate ?? true,\n\t};\n\t// the TypedAppHandle overload's literal route-id types are only\n\t// computable through the caller's generic\n\treturn handle;\n}\n"],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ import { LoaderDataOf, PanelLazy, PanelOptions, PanelRouteId, ParamsOfKey, SearchOf } from "./builders.js";
2
+ //#region src/preset/define-panel.d.ts
3
+ /** Brand distinguishing a {@link PanelDefinition} from raw inline options. */
4
+ export declare const PANEL_DEFINITION: "__vkmaPanel";
5
+ /**
6
+ * A self-contained panel declaration: the definition IS the typed handle.
7
+ *
8
+ * Panel chunk modules import the DEFINITION (never the app) for their data
9
+ * hooks — the definition's stored type is CLOSED (its `options` are widened
10
+ * and the concrete option-derived types travel in the `types` phantom), so
11
+ * the type graph stays acyclic and the app import disappears from chunks
12
+ * entirely. `createApp` nests definitions under their view key and builds
13
+ * the routes from `options`.
14
+ */
15
+ export interface PanelDefinition<V extends string = string, P extends string = string, O extends PanelOptions = PanelOptions> {
16
+ readonly [PANEL_DEFINITION]: true;
17
+ /** The view (story) namespace — must match the key under `views`. */
18
+ readonly view: V;
19
+ /** The panel path segment (`"index"` = the view's home panel). */
20
+ readonly path: P;
21
+ /**
22
+ * The declared options, WIDENED on purpose: a widened stored type cannot
23
+ * reference chunk modules, so importing the definition from a chunk can
24
+ * never close a type cycle. The concrete option-derived data types
25
+ * travel in the phantom below.
26
+ */
27
+ readonly options: PanelOptions;
28
+ /** Runtime route id by convention (`/view:${view}/${path}`). */
29
+ readonly routeId: PanelRouteId<V, P>;
30
+ /**
31
+ * Phantom carrying the option-derived data types for the /react hooks.
32
+ * `O` is the options WITHOUT `lazy` (the loader is a separate factory
33
+ * parameter) — its arrow would otherwise embed the chunk module's
34
+ * (deferred) member types into the definition, the exact cycle this
35
+ * module exists to break.
36
+ */
37
+ readonly types?: {
38
+ params: ParamsOfKey<P>;
39
+ search: SearchOf<O>;
40
+ loaderData: LoaderDataOf<O>;
41
+ };
42
+ }
43
+ /**
44
+ * Declare a panel:
45
+ *
46
+ * ```ts
47
+ * export const feedPanel = definePanel("feed", "feed", {
48
+ * loader: async () => ({ posts }),
49
+ * }, () => import("./feed"));
50
+ * ```
51
+ *
52
+ * The returned definition nests directly into `createApp`'s
53
+ * `views[view].panels[path]` and doubles as the typed handle for the
54
+ * `/react` hooks — chunk modules import it instead of the app handle, which
55
+ * is what keeps the app's inference and the chunk graph acyclic. The view
56
+ * key MUST match the nesting key in `createApp` (a mismatch is a dev-time
57
+ * console error, not a silent hook break).
58
+ */
59
+ export declare function definePanel<const V extends string, const P extends string, O extends PanelOptions = PanelOptions>(view: V, path: P, options: O,
60
+ /**
61
+ * The lazy loader as a SEPARATE parameter — not a member of `options`.
62
+ * Its arrow references the UI chunk's module, whose member types include
63
+ * the (deferred) component signatures; if that arrow were part of the
64
+ * inferred `O`, a chunk component resolving hooks against this very
65
+ * definition would close the inference cycle (TS7022). A separate
66
+ * parameter is contextually typed by `PanelLazy` directly — the module
67
+ * type never enters `O`. Mirrors TanStack's own options/`route.lazy()`
68
+ * split.
69
+ */
70
+ lazy?: PanelLazy): PanelDefinition<V, P, O>;
71
+ /**
72
+ * The deterministic route-id convention shared with `createApp`'s tree
73
+ * assembly — one formula, two consumers, no drift.
74
+ */
75
+ export declare function panelRouteId(view: string, path: string): string;
76
+ //#endregion
77
+ //# sourceMappingURL=define-panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-panel.d.ts","names":[],"sources":["../../src/preset/define-panel.ts"],"mappings":";;;qBAUa;;;;;;;;;;;iBAYI,gBAChB,2BACA,2BACA,UAAU,eAAe;YAEf;;WAED,MAAM;;WAEN,MAAM;;;;;;;WAON,SAAS;;WAET,SAAS,aAAa,GAAG;;;;;;;;WAQzB;IACR,QAAQ,YAAY;IACpB,QAAQ,SAAS;IACjB,YAAY,aAAa;;;;;;;;;;;;;;;;;;;wBAoBX,kBACT,wBACA,kBACN,UAAU,eAAe,cAEzB,MAAM,GACN,MAAM,GACN,SAAS;;;;;;;;;;;AAWT,OAAO,YACL,gBAAgB,GAAG,GAAG;;;;;wBAgBT,aAAa,cAAc"}
@@ -0,0 +1,42 @@
1
+ //#region src/preset/define-panel.ts
2
+ /** Brand distinguishing a {@link PanelDefinition} from raw inline options. */
3
+ const PANEL_DEFINITION = "__vkmaPanel";
4
+ /**
5
+ * Declare a panel:
6
+ *
7
+ * ```ts
8
+ * export const feedPanel = definePanel("feed", "feed", {
9
+ * loader: async () => ({ posts }),
10
+ * }, () => import("./feed"));
11
+ * ```
12
+ *
13
+ * The returned definition nests directly into `createApp`'s
14
+ * `views[view].panels[path]` and doubles as the typed handle for the
15
+ * `/react` hooks — chunk modules import it instead of the app handle, which
16
+ * is what keeps the app's inference and the chunk graph acyclic. The view
17
+ * key MUST match the nesting key in `createApp` (a mismatch is a dev-time
18
+ * console error, not a silent hook break).
19
+ */
20
+ function definePanel(view, path, options, lazy) {
21
+ return {
22
+ [PANEL_DEFINITION]: true,
23
+ view,
24
+ path,
25
+ options: lazy === void 0 ? options : {
26
+ ...options,
27
+ lazy
28
+ },
29
+ routeId: panelRouteId(view, path)
30
+ };
31
+ }
32
+ /**
33
+ * The deterministic route-id convention shared with `createApp`'s tree
34
+ * assembly — one formula, two consumers, no drift.
35
+ */
36
+ function panelRouteId(view, path) {
37
+ return path === "index" ? `/view:${view}/` : `/view:${view}/${path}`;
38
+ }
39
+ //#endregion
40
+ export { PANEL_DEFINITION, definePanel, panelRouteId };
41
+
42
+ //# sourceMappingURL=define-panel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-panel.js","names":[],"sources":["../../src/preset/define-panel.ts"],"sourcesContent":["import type {\n\tLoaderDataOf,\n\tPanelOptions,\n\tPanelLazy,\n\tPanelRouteId,\n\tParamsOfKey,\n\tSearchOf,\n} from \"./builders.ts\";\n\n/** Brand distinguishing a {@link PanelDefinition} from raw inline options. */\nexport const PANEL_DEFINITION = \"__vkmaPanel\" as const;\n\n/**\n * A self-contained panel declaration: the definition IS the typed handle.\n *\n * Panel chunk modules import the DEFINITION (never the app) for their data\n * hooks — the definition's stored type is CLOSED (its `options` are widened\n * and the concrete option-derived types travel in the `types` phantom), so\n * the type graph stays acyclic and the app import disappears from chunks\n * entirely. `createApp` nests definitions under their view key and builds\n * the routes from `options`.\n */\nexport interface PanelDefinition<\n\tV extends string = string,\n\tP extends string = string,\n\tO extends PanelOptions = PanelOptions,\n> {\n\treadonly [PANEL_DEFINITION]: true;\n\t/** The view (story) namespace — must match the key under `views`. */\n\treadonly view: V;\n\t/** The panel path segment (`\"index\"` = the view's home panel). */\n\treadonly path: P;\n\t/**\n\t * The declared options, WIDENED on purpose: a widened stored type cannot\n\t * reference chunk modules, so importing the definition from a chunk can\n\t * never close a type cycle. The concrete option-derived data types\n\t * travel in the phantom below.\n\t */\n\treadonly options: PanelOptions;\n\t/** Runtime route id by convention (`/view:${view}/${path}`). */\n\treadonly routeId: PanelRouteId<V, P>;\n\t/**\n\t * Phantom carrying the option-derived data types for the /react hooks.\n\t * `O` is the options WITHOUT `lazy` (the loader is a separate factory\n\t * parameter) — its arrow would otherwise embed the chunk module's\n\t * (deferred) member types into the definition, the exact cycle this\n\t * module exists to break.\n\t */\n\treadonly types?: {\n\t\tparams: ParamsOfKey<P>;\n\t\tsearch: SearchOf<O>;\n\t\tloaderData: LoaderDataOf<O>;\n\t};\n}\n\n/**\n * Declare a panel:\n *\n * ```ts\n * export const feedPanel = definePanel(\"feed\", \"feed\", {\n * \tloader: async () => ({ posts }),\n * }, () => import(\"./feed\"));\n * ```\n *\n * The returned definition nests directly into `createApp`'s\n * `views[view].panels[path]` and doubles as the typed handle for the\n * `/react` hooks — chunk modules import it instead of the app handle, which\n * is what keeps the app's inference and the chunk graph acyclic. The view\n * key MUST match the nesting key in `createApp` (a mismatch is a dev-time\n * console error, not a silent hook break).\n */\nexport function definePanel<\n\tconst V extends string,\n\tconst P extends string,\n\tO extends PanelOptions = PanelOptions,\n>(\n\tview: V,\n\tpath: P,\n\toptions: O,\n\t/**\n\t * The lazy loader as a SEPARATE parameter — not a member of `options`.\n\t * Its arrow references the UI chunk's module, whose member types include\n\t * the (deferred) component signatures; if that arrow were part of the\n\t * inferred `O`, a chunk component resolving hooks against this very\n\t * definition would close the inference cycle (TS7022). A separate\n\t * parameter is contextually typed by `PanelLazy` directly — the module\n\t * type never enters `O`. Mirrors TanStack's own options/`route.lazy()`\n\t * split.\n\t */\n\tlazy?: PanelLazy,\n): PanelDefinition<V, P, O> {\n\treturn {\n\t\t[PANEL_DEFINITION]: true,\n\t\tview,\n\t\tpath,\n\t\toptions: lazy === undefined ? options : { ...options, lazy },\n\t\t// same formula as panelRouteId — TS cannot see the literal through the\n\t\t// template, the cast records the guarantee\n\t\trouteId: panelRouteId(view, path) as PanelRouteId<V, P>,\n\t};\n}\n\n/**\n * The deterministic route-id convention shared with `createApp`'s tree\n * assembly — one formula, two consumers, no drift.\n */\nexport function panelRouteId(view: string, path: string): string {\n\treturn path === \"index\" ? `/view:${view}/` : `/view:${view}/${path}`;\n}\n"],"mappings":";;AAUA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;AA6DhC,SAAgB,YAKf,MACA,MACA,SAWA,MAC2B;CAC3B,OAAO;GACL,mBAAmB;EACpB;EACA;EACA,SAAS,SAAS,KAAA,IAAY,UAAU;GAAE,GAAG;GAAS;EAAK;EAG3D,SAAS,aAAa,MAAM,IAAI;CACjC;AACD;;;;;AAMA,SAAgB,aAAa,MAAc,MAAsB;CAChE,OAAO,SAAS,UAAU,SAAS,KAAK,KAAK,SAAS,KAAK,GAAG;AAC/D"}
@@ -0,0 +1,42 @@
1
+ import { PanelDefinition } from "./define-panel.js";
2
+ import { LooseRouteOptions, PanelLazy, PanelOptions } from "./builders.js";
3
+ //#region src/preset/define-view.d.ts
4
+ /** Brand distinguishing a {@link ViewDefinition} from raw view options. */
5
+ export declare const VIEW_DEFINITION: "__vkmaView";
6
+ /**
7
+ * A view hub: the ONE place a view key is written. Created by
8
+ * {@link defineView}; its `panel()` method is the view-scoped
9
+ * `definePanel` — it creates the definition and registers it into the
10
+ * view's registry in call order (the order is what VKUI View children get,
11
+ * i.e. the push/pop animation direction).
12
+ */
13
+ export interface ViewDefinition<V extends string = string> {
14
+ readonly [VIEW_DEFINITION]: true;
15
+ /** The view (story) namespace and route id segment (`view:${view}`). */
16
+ readonly view: V;
17
+ /** Definitions registered by `panel()`, keyed by panel path. */
18
+ readonly panels: Record<string, PanelDefinition>;
19
+ /** Optional view-level route options passthrough (the view is a pathless layout). */
20
+ readonly options?: LooseRouteOptions;
21
+ }
22
+ export interface ViewScope<V extends string = string> {
23
+ /** View-scoped `definePanel` — the view argument is pre-bound. */
24
+ panel<const P extends string, O extends PanelOptions = PanelOptions>(path: P, options: O, lazy?: PanelLazy): PanelDefinition<V, P, O>;
25
+ }
26
+ /**
27
+ * Declare a view hub and its panel factory:
28
+ *
29
+ * ```ts
30
+ * export const feedView = defineView("feed");
31
+ *
32
+ * export const feedPanel = feedView.panel("feed", { loader: ... }, () => import("./feed.ui"));
33
+ * export const postPanel = feedView.panel("post/$id", { ... }, () => import("./post.ui"));
34
+ * ```
35
+ *
36
+ * The view key is written exactly once; `createApp` accepts the hub
37
+ * directly (`views: [feedView, ...]`). Duplicate view keys are a dev-time
38
+ * console error — a view must not sprawl across files.
39
+ */
40
+ export declare function defineView<const V extends string>(view: V, options?: LooseRouteOptions): ViewDefinition<V> & ViewScope<V>;
41
+ //#endregion
42
+ //# sourceMappingURL=define-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-view.d.ts","names":[],"sources":["../../src/preset/define-view.ts"],"mappings":";;;;qBAIa;;;;;;;;iBASI,eAAe;YACrB;;WAED,MAAM;;WAEN,QAAQ,eAAe;;WAEvB,UAAU;;iBAGH,UAAU;;EAE1B,YAAY,kBAAkB,UAAU,eAAe,cACtD,MAAM,GACN,SAAS,GACT,OAAO,YACL,gBAAgB,GAAG,GAAG;;;;;;;;;;;;;;;;wBAiBV,iBAAiB,kBAChC,MAAM,GACN,UAAU,oBACR,eAAe,KAAK,UAAU"}
@@ -0,0 +1,43 @@
1
+ import { definePanel } from "./define-panel.js";
2
+ //#region src/preset/define-view.ts
3
+ /** Brand distinguishing a {@link ViewDefinition} from raw view options. */
4
+ const VIEW_DEFINITION = "__vkmaView";
5
+ /**
6
+ * Declare a view hub and its panel factory:
7
+ *
8
+ * ```ts
9
+ * export const feedView = defineView("feed");
10
+ *
11
+ * export const feedPanel = feedView.panel("feed", { loader: ... }, () => import("./feed.ui"));
12
+ * export const postPanel = feedView.panel("post/$id", { ... }, () => import("./post.ui"));
13
+ * ```
14
+ *
15
+ * The view key is written exactly once; `createApp` accepts the hub
16
+ * directly (`views: [feedView, ...]`). Duplicate view keys are a dev-time
17
+ * console error — a view must not sprawl across files.
18
+ */
19
+ function defineView(view, options) {
20
+ const usedPanelPaths = /* @__PURE__ */ new Set();
21
+ const panels = {};
22
+ const definition = {
23
+ [VIEW_DEFINITION]: true,
24
+ view,
25
+ panels,
26
+ ...options !== void 0 ? { options } : {}
27
+ };
28
+ const scope = { panel(path, panelOptions, lazy) {
29
+ if (usedPanelPaths.has(path)) console.error(`[vkma-router] duplicate panel path "${path}" in view "${view}"; panels within a view must have distinct paths`);
30
+ usedPanelPaths.add(path);
31
+ const declared = definePanel(view, path, panelOptions, lazy);
32
+ panels[path] = declared;
33
+ return declared;
34
+ } };
35
+ return {
36
+ ...definition,
37
+ ...scope
38
+ };
39
+ }
40
+ //#endregion
41
+ export { VIEW_DEFINITION, defineView };
42
+
43
+ //# sourceMappingURL=define-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-view.js","names":[],"sources":["../../src/preset/define-view.ts"],"sourcesContent":["import type { LooseRouteOptions, PanelLazy, PanelOptions } from \"./builders.ts\";\nimport { definePanel, type PanelDefinition } from \"./define-panel.ts\";\n\n/** Brand distinguishing a {@link ViewDefinition} from raw view options. */\nexport const VIEW_DEFINITION = \"__vkmaView\" as const;\n\n/**\n * A view hub: the ONE place a view key is written. Created by\n * {@link defineView}; its `panel()` method is the view-scoped\n * `definePanel` — it creates the definition and registers it into the\n * view's registry in call order (the order is what VKUI View children get,\n * i.e. the push/pop animation direction).\n */\nexport interface ViewDefinition<V extends string = string> {\n\treadonly [VIEW_DEFINITION]: true;\n\t/** The view (story) namespace and route id segment (`view:${view}`). */\n\treadonly view: V;\n\t/** Definitions registered by `panel()`, keyed by panel path. */\n\treadonly panels: Record<string, PanelDefinition>;\n\t/** Optional view-level route options passthrough (the view is a pathless layout). */\n\treadonly options?: LooseRouteOptions;\n}\n\nexport interface ViewScope<V extends string = string> {\n\t/** View-scoped `definePanel` — the view argument is pre-bound. */\n\tpanel<const P extends string, O extends PanelOptions = PanelOptions>(\n\t\tpath: P,\n\t\toptions: O,\n\t\tlazy?: PanelLazy,\n\t): PanelDefinition<V, P, O>;\n}\n\n/**\n * Declare a view hub and its panel factory:\n *\n * ```ts\n * export const feedView = defineView(\"feed\");\n *\n * export const feedPanel = feedView.panel(\"feed\", { loader: ... }, () => import(\"./feed.ui\"));\n * export const postPanel = feedView.panel(\"post/$id\", { ... }, () => import(\"./post.ui\"));\n * ```\n *\n * The view key is written exactly once; `createApp` accepts the hub\n * directly (`views: [feedView, ...]`). Duplicate view keys are a dev-time\n * console error — a view must not sprawl across files.\n */\nexport function defineView<const V extends string>(\n\tview: V,\n\toptions?: LooseRouteOptions,\n): ViewDefinition<V> & ViewScope<V> {\n\tconst usedPanelPaths = new Set<string>();\n\tconst panels: Record<string, PanelDefinition> = {};\n\tconst definition: ViewDefinition<V> = {\n\t\t[VIEW_DEFINITION]: true,\n\t\tview,\n\t\tpanels,\n\t\t...(options !== undefined ? { options } : {}),\n\t};\n\tconst scope: ViewScope<V> = {\n\t\tpanel(path, panelOptions, lazy) {\n\t\t\tif (usedPanelPaths.has(path)) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[vkma-router] duplicate panel path \"${path}\" in view \"${view}\"; panels within a view must have distinct paths`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tusedPanelPaths.add(path);\n\t\t\tconst declared = definePanel(view, path, panelOptions, lazy);\n\t\t\tpanels[path] = declared;\n\t\t\treturn declared;\n\t\t},\n\t};\n\treturn { ...definition, ...scope };\n}\n"],"mappings":";;;AAIA,MAAa,kBAAkB;;;;;;;;;;;;;;;AA0C/B,SAAgB,WACf,MACA,SACmC;CACnC,MAAM,iCAAiB,IAAI,IAAY;CACvC,MAAM,SAA0C,CAAC;CACjD,MAAM,aAAgC;GACpC,kBAAkB;EACnB;EACA;EACA,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;CAC5C;CACA,MAAM,QAAsB,EAC3B,MAAM,MAAM,cAAc,MAAM;EAC/B,IAAI,eAAe,IAAI,IAAI,GAC1B,QAAQ,MACP,uCAAuC,KAAK,aAAa,KAAK,iDAC/D;EAED,eAAe,IAAI,IAAI;EACvB,MAAM,WAAW,YAAY,MAAM,MAAM,cAAc,IAAI;EAC3D,OAAO,QAAQ;EACf,OAAO;CACR,EACD;CACA,OAAO;EAAE,GAAG;EAAY,GAAG;CAAM;AAClC"}
@@ -0,0 +1,17 @@
1
+ import { PanelHandle } from "./builders.js";
2
+ //#region src/preset/inflate.d.ts
3
+ /**
4
+ * Build the chain of hrefs to replay BEFORE the target location.
5
+ *
6
+ * @param orderedPanels panels of the target's view, in declaration order
7
+ * @param targetRouteId route id of the deep-linked panel
8
+ * @param params params of the deep-linked location (fill intermediate
9
+ * panels' path params; a panel whose params cannot be filled
10
+ * is skipped from the chain)
11
+ * @returns hrefs excluding the target (the target is already the current
12
+ * history entry — replay replaces the first chain entry, pushes
13
+ * the rest, then re-pushes the target href)
14
+ */
15
+ export declare function deriveInflateChain(orderedPanels: PanelHandle[], targetRouteId: string, params: Record<string, string>): string[];
16
+ //#endregion
17
+ //# sourceMappingURL=inflate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inflate.d.ts","names":[],"sources":["../../src/preset/inflate.ts"],"mappings":";;;;;;;;;;;;;;wBAuBgB,mBACf,eAAe,eACf,uBACA,QAAQ"}
@@ -0,0 +1,40 @@
1
+ //#region src/preset/inflate.ts
2
+ /**
3
+ * Build the chain of hrefs to replay BEFORE the target location.
4
+ *
5
+ * @param orderedPanels panels of the target's view, in declaration order
6
+ * @param targetRouteId route id of the deep-linked panel
7
+ * @param params params of the deep-linked location (fill intermediate
8
+ * panels' path params; a panel whose params cannot be filled
9
+ * is skipped from the chain)
10
+ * @returns hrefs excluding the target (the target is already the current
11
+ * history entry — replay replaces the first chain entry, pushes
12
+ * the rest, then re-pushes the target href)
13
+ */
14
+ function deriveInflateChain(orderedPanels, targetRouteId, params) {
15
+ const targetIndex = orderedPanels.findIndex((panel) => panel.routeId === targetRouteId);
16
+ if (targetIndex <= 0) return [];
17
+ const chain = [];
18
+ for (const panel of orderedPanels.slice(0, targetIndex)) {
19
+ const interpolated = interpolatePath(panel.path, params);
20
+ if (interpolated !== null) chain.push(interpolated);
21
+ }
22
+ return chain;
23
+ }
24
+ /**
25
+ * Fill a route path's `$param` segments from the deep link's params.
26
+ * Returns null when a required param cannot be derived (the step is
27
+ * unrepresentable as a URL and must be skipped).
28
+ */
29
+ function interpolatePath(path, params) {
30
+ const interpolated = path.replace(/\$([A-Za-z0-9_]+)/g, (segment, key) => {
31
+ const value = params[key];
32
+ return value !== void 0 ? value : segment;
33
+ });
34
+ if (interpolated.includes("$")) return null;
35
+ return interpolated.startsWith("/") ? interpolated : `/${interpolated}`;
36
+ }
37
+ //#endregion
38
+ export { deriveInflateChain, interpolatePath };
39
+
40
+ //# sourceMappingURL=inflate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inflate.js","names":[],"sources":["../../src/preset/inflate.ts"],"sourcesContent":["/**\n * Inflate: a synthetic parent chain after a deep link. Deep-linked to the\n * 4th panel? Back should walk the natural flow (panel 1 → 2 → 3 → 4)\n * instead of closing the app. The chain is auto-derived from the view's\n * panel declaration order (declaration order IS the navigation flow — the\n * same convention VKUI animations rely on), replacing the old router's\n * manual `hierarchy` config.\n */\n\nimport type { PanelHandle } from \"../preset/builders.tsx\";\n\n/**\n * Build the chain of hrefs to replay BEFORE the target location.\n *\n * @param orderedPanels panels of the target's view, in declaration order\n * @param targetRouteId route id of the deep-linked panel\n * @param params params of the deep-linked location (fill intermediate\n * panels' path params; a panel whose params cannot be filled\n * is skipped from the chain)\n * @returns hrefs excluding the target (the target is already the current\n * history entry — replay replaces the first chain entry, pushes\n * the rest, then re-pushes the target href)\n */\nexport function deriveInflateChain(\n\torderedPanels: PanelHandle[],\n\ttargetRouteId: string,\n\tparams: Record<string, string>,\n): string[] {\n\tconst targetIndex = orderedPanels.findIndex(panel => panel.routeId === targetRouteId);\n\tif (targetIndex <= 0) {\n\t\treturn []; // first panel of the view (or unknown) — nothing to inflate\n\t}\n\tconst chain: string[] = [];\n\tfor (const panel of orderedPanels.slice(0, targetIndex)) {\n\t\tconst interpolated = interpolatePath(panel.path, params);\n\t\tif (interpolated !== null) {\n\t\t\tchain.push(interpolated); // a required param cannot be derived — skip the step\n\t\t}\n\t}\n\treturn chain;\n}\n\n/**\n * Fill a route path's `$param` segments from the deep link's params.\n * Returns null when a required param cannot be derived (the step is\n * unrepresentable as a URL and must be skipped).\n */\nexport function interpolatePath(path: string, params: Record<string, string>): string | null {\n\tconst interpolated = path.replace(/\\$([A-Za-z0-9_]+)/g, (segment, key: string) => {\n\t\tconst value = params[key];\n\t\treturn value !== undefined ? value : segment;\n\t});\n\tif (interpolated.includes(\"$\")) {\n\t\treturn null;\n\t}\n\treturn interpolated.startsWith(\"/\") ? interpolated : `/${interpolated}`;\n}\n"],"mappings":";;;;;;;;;;;;;AAuBA,SAAgB,mBACf,eACA,eACA,QACW;CACX,MAAM,cAAc,cAAc,WAAU,UAAS,MAAM,YAAY,aAAa;CACpF,IAAI,eAAe,GAClB,OAAO,CAAC;CAET,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,SAAS,cAAc,MAAM,GAAG,WAAW,GAAG;EACxD,MAAM,eAAe,gBAAgB,MAAM,MAAM,MAAM;EACvD,IAAI,iBAAiB,MACpB,MAAM,KAAK,YAAY;CAEzB;CACA,OAAO;AACR;;;;;;AAOA,SAAgB,gBAAgB,MAAc,QAA+C;CAC5F,MAAM,eAAe,KAAK,QAAQ,uBAAuB,SAAS,QAAgB;EACjF,MAAM,QAAQ,OAAO;EACrB,OAAO,UAAU,KAAA,IAAY,QAAQ;CACtC,CAAC;CACD,IAAI,aAAa,SAAS,GAAG,GAC5B,OAAO;CAER,OAAO,aAAa,WAAW,GAAG,IAAI,eAAe,IAAI;AAC1D"}