akanjs 2.3.6-rc.2 → 2.3.6-rc.4

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 (60) hide show
  1. package/client/capacitor.ts +2 -0
  2. package/client/csrTypes.ts +132 -9
  3. package/client/device.ts +13 -4
  4. package/client/frameConfig.ts +248 -0
  5. package/client/frameDebug.ts +28 -0
  6. package/client/index.ts +2 -0
  7. package/client/router.ts +9 -0
  8. package/fetch/requestStorage.ts +11 -0
  9. package/package.json +1 -1
  10. package/server/hmr/clientScript.ts +28 -0
  11. package/server/hmr/wsHub.ts +29 -0
  12. package/server/routeElementComposer.tsx +56 -0
  13. package/server/routeTreeBuilder.ts +8 -0
  14. package/server/routing/apiRouter.ts +4 -1
  15. package/server/rscClient.tsx +8 -0
  16. package/server/rscWorker.tsx +30 -5
  17. package/store/baseSt.ts +2 -2
  18. package/types/client/capacitor.d.ts +4 -0
  19. package/types/client/csrTypes.d.ts +125 -7
  20. package/types/client/frameConfig.d.ts +19 -0
  21. package/types/client/frameDebug.d.ts +3 -0
  22. package/types/client/index.d.ts +2 -0
  23. package/types/client/router.d.ts +2 -0
  24. package/types/fetch/requestStorage.d.ts +3 -0
  25. package/types/server/hmr/clientScript.d.ts +1 -1
  26. package/types/server/hmr/wsHub.d.ts +6 -0
  27. package/types/server/routeElementComposer.d.ts +8 -0
  28. package/types/server/rscClient.d.ts +3 -0
  29. package/types/store/baseSt.d.ts +2 -2
  30. package/types/ui/Layout/BottomInset.d.ts +7 -1
  31. package/types/ui/Layout/Navbar.d.ts +1 -1
  32. package/types/ui/Layout/TopInset.d.ts +7 -0
  33. package/types/ui/Layout/index.d.ts +3 -2
  34. package/types/ui/Link/types.d.ts +9 -1
  35. package/types/ui/Portal.d.ts +2 -1
  36. package/types/ui/ServerPortal.d.ts +15 -0
  37. package/types/ui/System/Client.d.ts +7 -5
  38. package/types/ui/System/SSR.d.ts +3 -2
  39. package/types/ui/System/frameCssVars.d.ts +5 -0
  40. package/types/webkit/useCsrValues.d.ts +6 -1
  41. package/types/webkit/useFrameRuntime.d.ts +111 -0
  42. package/ui/Layout/BottomInset.tsx +89 -20
  43. package/ui/Layout/BottomTab.tsx +11 -6
  44. package/ui/Layout/Navbar.tsx +38 -12
  45. package/ui/Layout/TopInset.tsx +45 -0
  46. package/ui/Layout/TopLeftAction.tsx +2 -2
  47. package/ui/Layout/index.ts +2 -0
  48. package/ui/Link/types.ts +9 -1
  49. package/ui/Portal.tsx +16 -2
  50. package/ui/ServerPortal.tsx +49 -0
  51. package/ui/System/CSR.tsx +236 -90
  52. package/ui/System/Client.tsx +90 -10
  53. package/ui/System/SSR.tsx +119 -43
  54. package/ui/System/frameCssVars.ts +25 -0
  55. package/ui/styles.css +56 -0
  56. package/webkit/bootCsr.tsx +44 -35
  57. package/webkit/useCsrValues.ts +636 -105
  58. package/webkit/useFrameRuntime.ts +539 -0
  59. package/types/ui/Layout/BottomAction.d.ts +0 -6
  60. package/ui/Layout/BottomAction.tsx +0 -15
@@ -1,8 +1,16 @@
1
1
 
2
2
 
3
+ const SYNC_NAVIGATION_ENABLED =
4
+ process.env.AKAN_PUBLIC_SYNC_NAVIGATION === "true" ||
5
+ process.env.AKAN_PUBLIC_SYNC_NAVIGATION === "1" ||
6
+ process.env.SYNC_DOMAIN === "true" ||
7
+ process.env.SYNC_DOMAIN === "1";
8
+
3
9
  export const HMR_CLIENT_SCRIPT = `(function(){
4
10
  if (self.__AKAN_HMR_INSTALLED__) return;
5
11
  self.__AKAN_HMR_INSTALLED__ = true;
12
+ var syncNavigationEnabled = ${JSON.stringify(SYNC_NAVIGATION_ENABLED)};
13
+ var syncNavigationClientId = Math.random().toString(36).slice(2) + Date.now().toString(36);
6
14
  var proto = location.protocol === "https:" ? "wss:" : "ws:";
7
15
  var url = proto + "//" + location.host + "/_akan/hmr";
8
16
  var attempts = 0;
@@ -22,6 +30,19 @@ export const HMR_CLIENT_SCRIPT = `(function(){
22
30
  var overlayJobs = {};
23
31
  var buildErrorStates = {};
24
32
  self.__AKAN_HMR_PHASE__ = null;
33
+ self.__AKAN_DEV_SYNC_NAVIGATION__ = function(href, kind){
34
+ if (self.__AKAN_DEV_SYNC_NAVIGATION_APPLYING__ || !syncNavigationEnabled || !socket || socket.readyState !== WebSocket.OPEN) return;
35
+ try {
36
+ socket.send(JSON.stringify({
37
+ type: "sync-navigation",
38
+ clientId: syncNavigationClientId,
39
+ href: new URL(href, location.origin).pathname + new URL(href, location.origin).search + new URL(href, location.origin).hash,
40
+ kind: kind || "push"
41
+ }));
42
+ } catch(e) {
43
+ console.warn("[akan-hmr] sync navigation send failed", e);
44
+ }
45
+ };
25
46
 
26
47
  self.$RefreshReg$ = self.$RefreshReg$ || function(type, id){
27
48
  if (refreshRuntime) refreshRuntime.register(type, id);
@@ -71,6 +92,13 @@ export const HMR_CLIENT_SCRIPT = `(function(){
71
92
  }
72
93
  return;
73
94
  }
95
+ if (msg.type === "sync-navigation") {
96
+ if (!syncNavigationEnabled || msg.clientId === syncNavigationClientId || !msg.href) return;
97
+ window.dispatchEvent(new CustomEvent("akan:sync-navigation", {
98
+ detail: { href: msg.href, kind: msg.kind || "push" }
99
+ }));
100
+ return;
101
+ }
74
102
  if (msg.type === "build-status") { handleBuildStatus(msg); return; }
75
103
  if (msg.type === "ok") {
76
104
  clearBuildErrorOverlay({
@@ -22,6 +22,7 @@ export type HmrMessage =
22
22
  routeIds?: string[];
23
23
  }
24
24
  | { type: "css-update"; cssAssets?: Record<string, { cssUrl: string; cssRelPath: string }> }
25
+ | { type: "sync-navigation"; clientId: string; href: string; kind?: "push" | "replace" | "back" | "pop" }
25
26
  | { type: "ok"; generation?: number }
26
27
  | {
27
28
  type: "build-status";
@@ -63,4 +64,32 @@ export class HmrWsHub {
63
64
  const payload = JSON.stringify(msg);
64
65
  this.#publish?.(HMR_WS_TOPIC, payload);
65
66
  }
67
+
68
+ handleMessage(message: string): void {
69
+ if (!isSyncNavigationEnabled()) return;
70
+ let parsed: unknown;
71
+ try {
72
+ parsed = JSON.parse(message);
73
+ } catch {
74
+ return;
75
+ }
76
+ if (!isSyncNavigationMessage(parsed)) return;
77
+ this.broadcast(parsed);
78
+ }
66
79
  }
80
+
81
+ const isSyncNavigationEnabled = () =>
82
+ process.env.AKAN_PUBLIC_SYNC_NAVIGATION === "true" ||
83
+ process.env.AKAN_PUBLIC_SYNC_NAVIGATION === "1" ||
84
+ process.env.SYNC_DOMAIN === "true" ||
85
+ process.env.SYNC_DOMAIN === "1";
86
+
87
+ const isSyncNavigationMessage = (value: unknown): value is Extract<HmrMessage, { type: "sync-navigation" }> => {
88
+ if (!value || typeof value !== "object") return false;
89
+ const msg = value as Partial<Extract<HmrMessage, { type: "sync-navigation" }>>;
90
+ if (msg.type !== "sync-navigation") return false;
91
+ if (typeof msg.clientId !== "string" || msg.clientId.length === 0) return false;
92
+ if (typeof msg.href !== "string" || msg.href.length === 0) return false;
93
+ if (msg.kind === undefined) return true;
94
+ return msg.kind === "push" || msg.kind === "replace" || msg.kind === "back" || msg.kind === "pop";
95
+ };
@@ -2,11 +2,13 @@ import type {
2
2
  Head,
3
3
  LayoutErrorRender,
4
4
  LayoutFallbackRoute,
5
+ PageConfig,
5
6
  LayoutNotFoundRender,
6
7
  PathRoute,
7
8
  ResolvedHead,
8
9
  RouteRender,
9
10
  } from "akanjs/client";
11
+ import { getExplicitPageConfigKeys, resolvePageState } from "../client/frameConfig";
10
12
  import { Children, cloneElement, isValidElement, type ReactElement, type ReactNode, Suspense } from "react";
11
13
  import { resolveHeadResult } from "./metadata";
12
14
  import { type AkanRouteSegmentState, createAkanRouteSegments, createAkanSegmentOutletKey } from "./routeState";
@@ -14,6 +16,47 @@ import { isAkanRscPartialCommitEnabled } from "./rscPartialCommit";
14
16
  import { AkanSegmentOutletReference } from "./rscSegmentOutletReference";
15
17
 
16
18
  export class RouteElementComposer {
19
+ static async resolveSsrFramePathRoute({
20
+ pathRoute,
21
+ basePath,
22
+ }: {
23
+ pathRoute: PathRoute;
24
+ basePath?: string | null;
25
+ }): Promise<PathRoute> {
26
+ const pageConfigChain = await RouteElementComposer.#resolvePageConfigChain(pathRoute);
27
+ return {
28
+ ...pathRoute,
29
+ pageState: resolvePageState({
30
+ configChain: pageConfigChain,
31
+ path: pathRoute.path,
32
+ basePath: basePath ?? undefined,
33
+ platform: "web",
34
+ deviceSafeArea: { top: 0, bottom: 0 },
35
+ cssSafeArea: { top: 0, bottom: 0 },
36
+ }),
37
+ pageConfigChain,
38
+ explicitPageConfigKeys: getExplicitPageConfigKeys(pageConfigChain),
39
+ };
40
+ }
41
+
42
+ static async resolveSsrFallbackFrameState({
43
+ route,
44
+ basePath,
45
+ }: {
46
+ route: PathRoute | LayoutFallbackRoute;
47
+ basePath?: string | null;
48
+ }) {
49
+ const pageConfigChain = await RouteElementComposer.#resolveLayoutPageConfigChain(route);
50
+ return resolvePageState({
51
+ configChain: pageConfigChain,
52
+ path: route.path,
53
+ basePath: basePath ?? undefined,
54
+ platform: "web",
55
+ deviceSafeArea: { top: 0, bottom: 0 },
56
+ cssSafeArea: { top: 0, bottom: 0 },
57
+ });
58
+ }
59
+
17
60
  static compose({
18
61
  pathRoute,
19
62
  params,
@@ -231,6 +274,19 @@ export class RouteElementComposer {
231
274
  return [...pathRoute.renderRootLayouts, ...pathRoute.renderLayouts, pathRoute.renderPage];
232
275
  }
233
276
 
277
+ static async #resolveLayoutPageConfigChain(route: PathRoute | LayoutFallbackRoute): Promise<PageConfig[]> {
278
+ const configs = await Promise.all(
279
+ [...route.renderRootLayouts, ...route.renderLayouts].map((render) => render.getLayoutPageConfig?.()),
280
+ );
281
+ return configs.filter((config): config is PageConfig => Boolean(config));
282
+ }
283
+
284
+ static async #resolvePageConfigChain(pathRoute: PathRoute): Promise<PageConfig[]> {
285
+ const layoutConfigs = await RouteElementComposer.#resolveLayoutPageConfigChain(pathRoute);
286
+ const pageConfig = await pathRoute.renderPage.getPageConfig?.();
287
+ return [...layoutConfigs, ...(pageConfig ? [pageConfig] : [])];
288
+ }
289
+
234
290
  static #composeLoadingFallback(renders: RouteRender[], params: Record<string, string>): ReactNode {
235
291
  let element: ReactNode = null;
236
292
  for (let i = renders.length - 1; i >= 0; i--) {
@@ -18,6 +18,7 @@ import {
18
18
  parseRouteModuleKey,
19
19
  routeSegmentToTreePath,
20
20
  } from "akanjs/common";
21
+ import { validatePageConfig } from "../client/frameConfig";
21
22
  import { resolveHeadExport, resolveMetadataHead } from "./metadata";
22
23
 
23
24
  export type PagesContext = Record<string, () => Promise<RouteModule>>;
@@ -55,6 +56,7 @@ export class RouteTreeBuilder {
55
56
  ]);
56
57
  static readonly #rootLayoutExports = new Set([
57
58
  "default",
59
+ "pageConfig",
58
60
  "head",
59
61
  "metadata",
60
62
  "generateHead",
@@ -72,6 +74,7 @@ export class RouteTreeBuilder {
72
74
  ]);
73
75
  static readonly #layoutRouteExports = new Set([
74
76
  "default",
77
+ "pageConfig",
75
78
  "head",
76
79
  "metadata",
77
80
  "generateHead",
@@ -271,6 +274,7 @@ export class RouteTreeBuilder {
271
274
  RouteTreeBuilder.#moduleCacheStats.cacheMisses += 1;
272
275
  const mod = await loader();
273
276
  RouteTreeBuilder.#validateRouteModuleExports(key, kind, mod);
277
+ validatePageConfig(key, "pageConfig" in mod ? mod.pageConfig : undefined);
274
278
  if (!loaded) {
275
279
  RouteTreeBuilder.#moduleCacheStats.loadedModuleCount += 1;
276
280
  RouteTreeBuilder.#moduleCacheStats.loadedModuleKeys.push(key);
@@ -354,6 +358,10 @@ export class RouteTreeBuilder {
354
358
  return "pageConfig" in mod ? mod.pageConfig : undefined;
355
359
  };
356
360
  } else {
361
+ routeRender.getLayoutPageConfig = async () => {
362
+ const mod = await loadModule();
363
+ return "pageConfig" in mod ? mod.pageConfig : undefined;
364
+ };
357
365
  routeRender.resolveNotFound = async () => {
358
366
  const mod = (await loadModule()) as LayoutModule;
359
367
  routeRender.NotFound = mod.NotFound;
@@ -130,7 +130,10 @@ export class ApiRouter {
130
130
  },
131
131
  message: async (ws, message) => {
132
132
  const data = ws.data as WsTaggedData | undefined;
133
- if (data?.kind === "akan-hmr") return;
133
+ if (data?.kind === "akan-hmr") {
134
+ if (typeof message === "string" && typeof hmrHub?.handleMessage === "function") hmrHub.handleMessage(message);
135
+ return;
136
+ }
134
137
  try {
135
138
  if (typeof message === "string") {
136
139
  const msg = JSON.parse(message) as WebsocketReqData;
@@ -47,6 +47,9 @@ declare global {
47
47
  | undefined;
48
48
  var __AKAN_RSC_REFRESH__: ((options?: { buildId?: number }) => Promise<void>) | undefined;
49
49
  var __AKAN_RSC_CLEAR_CACHE__: (() => void) | undefined;
50
+ var __AKAN_DEV_SYNC_NAVIGATION__: ((href: string, kind: "push" | "replace" | "back" | "pop") => void) | undefined;
51
+ var __AKAN_DEV_SYNC_NAVIGATION_APPLYING__: boolean | undefined;
52
+ var __AKAN_GET_SYNC_ROUTE_HREF__: ((href: string) => string) | undefined;
50
53
  }
51
54
 
52
55
  function decodeBase64(b64: string): Uint8Array {
@@ -514,6 +517,11 @@ function Root(): ReactNode {
514
517
 
515
518
  window.addEventListener("popstate", () => {
516
519
  void globalThis.__AKAN_RSC_NAVIGATE__?.(window.location.href, { replace: true, scrollToTop: false });
520
+ window.setTimeout(() => {
521
+ if (globalThis.__AKAN_DEV_SYNC_NAVIGATION_APPLYING__) return;
522
+ const href = globalThis.__AKAN_GET_SYNC_ROUTE_HREF__?.(window.location.href) ?? window.location.href;
523
+ globalThis.__AKAN_DEV_SYNC_NAVIGATION__?.(href, "pop");
524
+ }, 0);
517
525
  });
518
526
 
519
527
  const hydrate = () => hydrateRoot(document, createElement(Root));
@@ -12,6 +12,7 @@ import {
12
12
  getRequestPolicy,
13
13
  getRequestTheme,
14
14
  requestStorage,
15
+ setRequestFrameState,
15
16
  untrackedCookies,
16
17
  untrackedRequest,
17
18
  updateRequestPolicy,
@@ -1164,6 +1165,12 @@ export class RscRenderer {
1164
1165
  error?: unknown;
1165
1166
  digest?: string;
1166
1167
  }): Promise<ReactNode | null> {
1168
+ setRequestFrameState(
1169
+ await RouteElementComposer.resolveSsrFallbackFrameState({
1170
+ route,
1171
+ basePath: this.#getBasePath(url),
1172
+ }),
1173
+ );
1167
1174
  const body = await RouteElementComposer.composeFallback({
1168
1175
  kind,
1169
1176
  route,
@@ -1218,17 +1225,22 @@ export class RscRenderer {
1218
1225
  this.#logger.verbose(
1219
1226
  `composing route element pathname=${url.pathname} search=${url.search || "(none)"} params=${JSON.stringify(match.params)}`,
1220
1227
  );
1221
- const routeHead = await RouteElementComposer.resolveHeadWithMetadata({
1228
+ const pathRoute = await RouteElementComposer.resolveSsrFramePathRoute({
1222
1229
  pathRoute: match.pathRoute,
1230
+ basePath: this.#getBasePath(url),
1231
+ });
1232
+ setRequestFrameState(pathRoute.pageState);
1233
+ const routeHead = await RouteElementComposer.resolveHeadWithMetadata({
1234
+ pathRoute,
1223
1235
  params: match.params,
1224
1236
  searchParams,
1225
1237
  });
1226
1238
  const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {
1227
- isSpecialRoute: match.pathRoute.isSpecialRoute,
1239
+ isSpecialRoute: pathRoute.isSpecialRoute,
1228
1240
  hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
1229
1241
  });
1230
1242
  const body = RouteElementComposer.compose({
1231
- pathRoute: match.pathRoute,
1243
+ pathRoute,
1232
1244
  params: match.params,
1233
1245
  searchParams,
1234
1246
  });
@@ -1245,7 +1257,7 @@ export class RscRenderer {
1245
1257
  : (routeHead.node ?? this.#renderDefaultHead())}
1246
1258
  {!routeHeadSnapshot &&
1247
1259
  shouldRenderLocaleAlternates({
1248
- isSpecialRoute: match.pathRoute.isSpecialRoute,
1260
+ isSpecialRoute: pathRoute.isSpecialRoute,
1249
1261
  hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
1250
1262
  })
1251
1263
  ? this.#renderLocaleAlternates(url)
@@ -1266,8 +1278,13 @@ export class RscRenderer {
1266
1278
  this.#logger.verbose(
1267
1279
  `composing route suffix pathname=${url.pathname} start=${patchStartIndex} params=${JSON.stringify(match.params)}`,
1268
1280
  );
1269
- return RouteElementComposer.composeSuffix({
1281
+ const pathRoute = await RouteElementComposer.resolveSsrFramePathRoute({
1270
1282
  pathRoute: match.pathRoute,
1283
+ basePath: this.#getBasePath(url),
1284
+ });
1285
+ setRequestFrameState(pathRoute.pageState);
1286
+ return RouteElementComposer.composeSuffix({
1287
+ pathRoute,
1271
1288
  params: match.params,
1272
1289
  searchParams,
1273
1290
  patchStartIndex,
@@ -1315,6 +1332,14 @@ export class RscRenderer {
1315
1332
  return <title key="title">{process.env.AKAN_PUBLIC_APP_NAME ?? "Akan App"}</title>;
1316
1333
  }
1317
1334
 
1335
+ #getBasePath(url: URL): string | null {
1336
+ return getBasePathFromPathname(url.pathname, {
1337
+ basePaths: this.#basePaths,
1338
+ i18n: this.#i18n,
1339
+ headerBasePath: untrackedRequest()?.headers.get("x-base-path"),
1340
+ });
1341
+ }
1342
+
1318
1343
  async #resolveRouteHeadSnapshot(
1319
1344
  url: URL,
1320
1345
  match: { pathRoute: PathRoute; params: Record<string, string> },
package/store/baseSt.ts CHANGED
@@ -44,8 +44,8 @@ export class BaseStore extends store("base" as const, () => ({
44
44
  bottomInset: 0,
45
45
  gesture: true,
46
46
  cache: false,
47
- topSafeAreaColor: "transparent",
48
- bottomSafeAreaColor: "transparent",
47
+ topSafeAreaColor: "var(--color-base-100, Canvas)",
48
+ bottomSafeAreaColor: "var(--color-base-100, Canvas)",
49
49
  } as PageState,
50
50
  devMode: false,
51
51
  deviceToken: "" as string,
@@ -12,6 +12,7 @@ export type CapacitorAppModule = {
12
12
  App: {
13
13
  addListener: (eventName: string, listenerFunc: (...args: unknown[]) => void) => Promise<unknown> | unknown;
14
14
  removeAllListeners: () => Promise<void> | void;
15
+ exitApp?: () => Promise<void> | void;
15
16
  getInfo: () => Promise<{
16
17
  id: string;
17
18
  version: string;
@@ -128,6 +129,9 @@ export type CapacitorKeyboardModule = {
128
129
  Keyboard: {
129
130
  show: () => Promise<void> | void;
130
131
  hide: () => Promise<void> | void;
132
+ setResizeMode?: (options: {
133
+ mode: "body" | "ionic" | "native" | "none";
134
+ }) => Promise<void> | void;
131
135
  addListener: (eventName: string, listenerFunc: (info: CapacitorKeyboardInfo) => void) => Promise<unknown> | unknown;
132
136
  removeAllListeners: () => Promise<void> | void;
133
137
  };
@@ -5,15 +5,19 @@ import type { AnimatedComponent, AnimatedProps, Interpolation, SpringValue } fro
5
5
  import type { RouterInstance } from "./router.d.ts";
6
6
  import type { ReactFont } from "./types.d.ts";
7
7
  export type TransitionType = "none" | "fade" | "bottomUp" | "stack" | "scaleOut";
8
+ export type PageSafeAreaConfig = boolean | "top" | "bottom" | {
9
+ top?: boolean;
10
+ bottom?: boolean;
11
+ android?: "auto" | "edge-to-edge" | "none";
12
+ };
8
13
  /** Per-page CSR configuration for transition, safe-area, and gesture behavior. */
9
14
  export interface PageConfig {
10
15
  transition?: TransitionType;
11
- safeArea?: boolean | "top" | "bottom";
12
- topInset?: boolean | number;
13
- /**
14
- * @default 48px
15
- */
16
- bottomInset?: boolean | number;
16
+ safeArea?: PageSafeAreaConfig;
17
+ /** Top chrome reservation in px. Use true for the default 48px reservation. */
18
+ topInset?: number | boolean;
19
+ /** Bottom chrome reservation in px. Use true for the default 48px reservation. */
20
+ bottomInset?: number | boolean;
17
21
  gesture?: boolean;
18
22
  cache?: boolean;
19
23
  /**
@@ -100,6 +104,7 @@ export interface RouteRender {
100
104
  resolveError?: () => PromiseOrObject<LayoutErrorRender | undefined>;
101
105
  resolveHead?: ResolveHead;
102
106
  getPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
107
+ getLayoutPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
103
108
  }
104
109
  export interface WebAppManifestIcon {
105
110
  src: string;
@@ -161,6 +166,7 @@ export interface PageModule {
161
166
  }
162
167
  export interface LayoutModule {
163
168
  default?: LayoutRender;
169
+ pageConfig?: PageConfig;
164
170
  head?: Head;
165
171
  metadata?: AkanMetadata;
166
172
  generateHead?: GenerateHead;
@@ -179,6 +185,8 @@ export interface LayoutModule {
179
185
  export type RouteModule = PageModule | LayoutModule;
180
186
  export interface Route {
181
187
  PageConfig?: PageConfig;
188
+ pageConfig?: PageConfig;
189
+ layoutPageConfig?: PageConfig;
182
190
  path: string;
183
191
  renderPage?: RouteRender;
184
192
  renderLayout?: RouteRender;
@@ -186,6 +194,8 @@ export interface Route {
186
194
  isSpecialRoute?: boolean;
187
195
  loader?: () => unknown;
188
196
  pageState?: PageState;
197
+ pageConfigChain?: PageConfig[];
198
+ explicitPageConfigKeys?: Partial<Record<keyof PageConfig, boolean>>;
189
199
  children: Map<string, Route>;
190
200
  }
191
201
  export type AnimatedDivProps = AnimatedComponent<"div"> extends ForwardRefExoticComponent<AnimatedProps<infer P>> ? P : never;
@@ -229,9 +239,23 @@ export interface Location {
229
239
  pathRoute: PathRoute;
230
240
  hash: string;
231
241
  }
242
+ export type CsrNavigationPhase = "idle" | "preparing" | "transitioning";
243
+ export type CsrNavigationKind = "push" | "replace" | "back" | "popForward" | "popBack";
244
+ export interface NavigationIntent {
245
+ id: number;
246
+ kind: CsrNavigationKind;
247
+ from: Location;
248
+ to: Location;
249
+ scrollTop: number;
250
+ scrollToTop?: boolean;
251
+ createdAt: number;
252
+ }
232
253
  export interface LocationState {
233
254
  location: Location;
234
255
  prevLocation: Location | null;
256
+ pendingLocation?: Location | null;
257
+ navigationIntent?: NavigationIntent | null;
258
+ phase: CsrNavigationPhase;
235
259
  }
236
260
  export interface History {
237
261
  type: "initial" | "forward" | "back";
@@ -252,6 +276,9 @@ export interface RouteState {
252
276
  clientHeight: number;
253
277
  location: Location;
254
278
  prevLocation: Location | null;
279
+ pendingLocation: Location | null;
280
+ navigationIntent: NavigationIntent | null;
281
+ phase: CsrNavigationPhase;
255
282
  history: RefObject<History>;
256
283
  topSafeAreaRef: RefObject<HTMLDivElement | null>;
257
284
  bottomSafeAreaRef: RefObject<HTMLDivElement | null>;
@@ -263,6 +290,8 @@ export interface RouteState {
263
290
  }>;
264
291
  router: RouterInstance;
265
292
  pathRoutes: PathRoute[];
293
+ registerFrameSlot: (path: string, slot: FrameSlotRegistration, bucket?: FrameSlotBucket) => () => void;
294
+ frameLayout: FrameLayoutState;
266
295
  }
267
296
  export type UseCsrTransition = CsrTransitionStyles & {
268
297
  pageBind: (...args: unknown[]) => ReactDOMAttributes;
@@ -277,11 +306,12 @@ export type CsrContextType = RouteState & UseCsrTransition;
277
306
  export declare const csrContext: import("react").Context<CsrContextType>;
278
307
  export declare const useCsr: () => CsrContextType;
279
308
  export interface PathContextType {
280
- pageType: "current" | "prev" | "cached";
309
+ pageType: "current" | "prev" | "cached" | "pending";
281
310
  location: Location;
282
311
  prefix?: string;
283
312
  gestureEnabled: boolean;
284
313
  setGestureEnabled: (enabled: boolean) => void;
314
+ registerFrameSlot: (slot: FrameSlotRegistration) => () => void;
285
315
  }
286
316
  export declare const pathContext: import("react").Context<PathContextType>;
287
317
  export declare const usePathCtx: () => PathContextType;
@@ -290,11 +320,99 @@ export interface PathRoute {
290
320
  pathSegments: string[];
291
321
  renderPage: RouteRender;
292
322
  pageState: PageState;
323
+ pageConfigChain?: PageConfig[];
324
+ explicitPageConfigKeys?: Partial<Record<keyof PageConfig, boolean>>;
293
325
  renderRootLayouts: RouteRender[];
294
326
  renderLayouts: RouteRender[];
295
327
  resolveHead?: ResolveHead;
296
328
  isSpecialRoute?: boolean;
297
329
  }
330
+ export type FrameSlotScope = "page" | "layout";
331
+ export type FrameSlotType = "topInset" | "bottomInset";
332
+ export type FrameSlotBucket = "active" | "pending";
333
+ export type FrameLayer = "page" | "topChrome" | "bottomChrome" | "keyboard" | "overlay";
334
+ export type FrameSlotRole = "topChrome" | "bottomChrome" | "keyboardAccessory";
335
+ export type FramePlatformProfile = "ios" | "android" | "web" | "mobileWeb";
336
+ export interface FrameViewportState {
337
+ width: number;
338
+ height: number;
339
+ visualWidth: number;
340
+ visualHeight: number;
341
+ visualOffsetTop: number;
342
+ }
343
+ export interface KeyboardFrameState {
344
+ height: number;
345
+ offset: number;
346
+ visible: boolean;
347
+ sticky: boolean;
348
+ frozen?: boolean;
349
+ source?: "native" | "visualViewport" | "fallback";
350
+ animationDuration?: number;
351
+ animationEasing?: string;
352
+ }
353
+ export interface FrameContentViewportState {
354
+ top: number;
355
+ bottom: number;
356
+ height: number;
357
+ }
358
+ export interface KeyboardAccessoryFrameState {
359
+ top: number;
360
+ bottom: number;
361
+ height: number;
362
+ visible: boolean;
363
+ slotHeight: number;
364
+ }
365
+ export interface FrameSnapshot {
366
+ location: Location;
367
+ pageState: PageState;
368
+ viewport: FrameViewportState;
369
+ frameSlots: FrameSlotRegistration[];
370
+ measuredAt: number;
371
+ }
372
+ export interface TransitionActionContext {
373
+ plan: TransitionPlan;
374
+ }
375
+ export interface TransitionAction {
376
+ type: "page" | "topChrome" | "bottomChrome" | "keyboard" | "safeArea" | (string & {});
377
+ run: (ctx: TransitionActionContext) => Promise<void> | void;
378
+ }
379
+ export interface TransitionPlan {
380
+ id: number;
381
+ intent: NavigationIntent;
382
+ type: TransitionType;
383
+ direction: "forward" | "back";
384
+ fromFrame: FrameSnapshot;
385
+ toFrame: FrameSnapshot;
386
+ actions: TransitionAction[];
387
+ duration: number;
388
+ }
389
+ export interface FrameLayerZIndex {
390
+ page: number;
391
+ previousPage: number;
392
+ cachedPage: number;
393
+ topChrome: number;
394
+ bottomChrome: number;
395
+ keyboard: number;
396
+ overlay: number;
397
+ }
398
+ export interface FrameLayoutState {
399
+ viewport: FrameViewportState;
400
+ keyboard: KeyboardFrameState;
401
+ contentViewport: FrameContentViewportState;
402
+ keyboardAccessory: KeyboardAccessoryFrameState;
403
+ platformProfile: FramePlatformProfile;
404
+ zIndex: FrameLayerZIndex;
405
+ pageStateByPath: Map<string, PageState>;
406
+ }
407
+ export interface FrameSlotRegistration {
408
+ scope?: FrameSlotScope;
409
+ type: FrameSlotType;
410
+ role?: FrameSlotRole;
411
+ height?: number;
412
+ estimatedHeight?: number;
413
+ source?: "navbar" | "topInset" | "bottomInset" | "bottomTab" | (string & {});
414
+ cache?: boolean;
415
+ }
298
416
  export interface LayoutFallbackRoute {
299
417
  path: string;
300
418
  pathSegments: string[];
@@ -0,0 +1,19 @@
1
+ import type { PageConfig, PageState } from "./csrTypes.d.ts";
2
+ export type DevicePlatform = "ios" | "android" | "web" | (string & {});
3
+ export type SafeAreaInsets = {
4
+ top: number;
5
+ bottom: number;
6
+ };
7
+ export interface ResolvePageStateOptions {
8
+ configChain?: PageConfig[];
9
+ path: string;
10
+ basePath?: string;
11
+ platform: DevicePlatform;
12
+ deviceSafeArea: SafeAreaInsets;
13
+ cssSafeArea?: SafeAreaInsets;
14
+ }
15
+ export declare function validatePageConfig(routeKey: string, config?: PageConfig): void;
16
+ export declare function mergePageConfigs(configChain?: PageConfig[]): PageConfig;
17
+ export declare function getExplicitPageConfigKeys(configChain?: PageConfig[]): Partial<Record<keyof PageConfig, boolean>>;
18
+ export declare function resolvePageState({ configChain, path, basePath, platform, deviceSafeArea, cssSafeArea, }: ResolvePageStateOptions): PageState;
19
+ export declare function readCssSafeAreaInsets(): SafeAreaInsets;
@@ -0,0 +1,3 @@
1
+ type DebugPayload = Record<string, unknown>;
2
+ export declare function debugFrame(event: string, payload?: DebugPayload): void;
3
+ export {};
@@ -4,6 +4,8 @@ export * from "./createFont.d.ts";
4
4
  export * from "./csrTypes.d.ts";
5
5
  export * from "./decorators.d.ts";
6
6
  export * from "./device.d.ts";
7
+ export * from "./frameConfig.d.ts";
8
+ export * from "./frameDebug.d.ts";
7
9
  export * from "./locale.d.ts";
8
10
  export * from "./makePageProto.d.ts";
9
11
  export * from "./router.d.ts";
@@ -43,6 +43,8 @@ export declare class AkanNotFoundError extends Error {
43
43
  }
44
44
  declare global {
45
45
  var __AKAN_ROUTER__: Router | undefined;
46
+ var __AKAN_DEV_SYNC_NAVIGATION__: ((href: string, kind: "push" | "replace" | "back" | "pop") => void) | undefined;
47
+ var __AKAN_DEV_SYNC_NAVIGATION_APPLYING__: boolean | undefined;
46
48
  }
47
49
  export declare const getPathInfo: (requestUrl: string, lang: string, prefix: string) => {
48
50
  path: string;
@@ -12,6 +12,7 @@ export interface AkanDynamicUsage {
12
12
  export interface AkanRequestStore {
13
13
  request: Request;
14
14
  theme?: AkanTheme;
15
+ frameState?: unknown;
15
16
  queryCache: Map<string, Promise<unknown>>;
16
17
  policy: AkanRequestPolicy;
17
18
  dynamicUsage: AkanDynamicUsage;
@@ -29,6 +30,8 @@ export declare function createRequestStore(request: Request, policy?: Partial<Om
29
30
  /** Stores theme preference on the active request when server rendering. */
30
31
  export declare function setRequestTheme(theme: AkanTheme | undefined): void;
31
32
  export declare function getRequestTheme(): AkanTheme | undefined;
33
+ export declare function setRequestFrameState(frameState: unknown): void;
34
+ export declare function getRequestFrameState<FrameState = unknown>(): FrameState | undefined;
32
35
  export declare function pushRequestFallback(storeOrRequest: Request | AkanRequestStore): () => void;
33
36
  /** Returns the active server request store from AsyncLocalStorage or the fallback stack. */
34
37
  export declare function getRequestStore(): AkanRequestStore | undefined;