akanjs 2.3.6-rc.1 → 2.3.6-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/capacitor.ts +2 -0
- package/client/csrTypes.ts +132 -9
- package/client/device.ts +13 -4
- package/client/frameConfig.ts +239 -0
- package/client/frameDebug.ts +28 -0
- package/client/index.ts +2 -0
- package/client/router.ts +9 -0
- package/fetch/requestStorage.ts +11 -0
- package/package.json +2 -2
- package/server/hmr/clientScript.ts +28 -0
- package/server/hmr/wsHub.ts +29 -0
- package/server/routeElementComposer.tsx +56 -0
- package/server/routeTreeBuilder.ts +8 -0
- package/server/routing/apiRouter.ts +4 -1
- package/server/rscClient.tsx +8 -0
- package/server/rscWorker.tsx +30 -5
- package/store/baseSt.ts +2 -2
- package/types/client/capacitor.d.ts +4 -0
- package/types/client/csrTypes.d.ts +125 -7
- package/types/client/frameConfig.d.ts +19 -0
- package/types/client/frameDebug.d.ts +3 -0
- package/types/client/index.d.ts +2 -0
- package/types/client/router.d.ts +2 -0
- package/types/fetch/requestStorage.d.ts +3 -0
- package/types/server/hmr/clientScript.d.ts +1 -1
- package/types/server/hmr/wsHub.d.ts +6 -0
- package/types/server/routeElementComposer.d.ts +8 -0
- package/types/server/rscClient.d.ts +3 -0
- package/types/store/baseSt.d.ts +2 -2
- package/types/ui/Layout/BottomInset.d.ts +7 -1
- package/types/ui/Layout/Navbar.d.ts +1 -1
- package/types/ui/Layout/TopInset.d.ts +7 -0
- package/types/ui/Layout/index.d.ts +3 -2
- package/types/ui/Link/types.d.ts +9 -1
- package/types/ui/Portal.d.ts +2 -1
- package/types/ui/ServerPortal.d.ts +15 -0
- package/types/ui/System/Client.d.ts +7 -5
- package/types/ui/System/SSR.d.ts +3 -2
- package/types/ui/System/frameCssVars.d.ts +5 -0
- package/types/webkit/useCsrValues.d.ts +6 -1
- package/types/webkit/useFrameRuntime.d.ts +111 -0
- package/ui/Layout/BottomInset.tsx +89 -20
- package/ui/Layout/BottomTab.tsx +11 -6
- package/ui/Layout/Navbar.tsx +38 -12
- package/ui/Layout/TopInset.tsx +45 -0
- package/ui/Layout/TopLeftAction.tsx +2 -2
- package/ui/Layout/index.ts +2 -0
- package/ui/Link/types.ts +9 -1
- package/ui/Portal.tsx +16 -2
- package/ui/ServerPortal.tsx +49 -0
- package/ui/System/CSR.tsx +236 -90
- package/ui/System/Client.tsx +90 -10
- package/ui/System/SSR.tsx +119 -43
- package/ui/System/frameCssVars.ts +25 -0
- package/ui/styles.css +56 -0
- package/webkit/bootCsr.tsx +44 -35
- package/webkit/useCsrValues.ts +636 -105
- package/webkit/useFrameRuntime.ts +539 -0
- package/types/ui/Layout/BottomAction.d.ts +0 -6
- package/ui/Layout/BottomAction.tsx +0 -15
package/client/capacitor.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type CapacitorAppModule = {
|
|
|
15
15
|
App: {
|
|
16
16
|
addListener: (eventName: string, listenerFunc: (...args: unknown[]) => void) => Promise<unknown> | unknown;
|
|
17
17
|
removeAllListeners: () => Promise<void> | void;
|
|
18
|
+
exitApp?: () => Promise<void> | void;
|
|
18
19
|
getInfo: () => Promise<{ id: string; version: string; build: string; [key: string]: unknown }>;
|
|
19
20
|
};
|
|
20
21
|
};
|
|
@@ -86,6 +87,7 @@ export type CapacitorKeyboardModule = {
|
|
|
86
87
|
Keyboard: {
|
|
87
88
|
show: () => Promise<void> | void;
|
|
88
89
|
hide: () => Promise<void> | void;
|
|
90
|
+
setResizeMode?: (options: { mode: "body" | "ionic" | "native" | "none" }) => Promise<void> | void;
|
|
89
91
|
addListener: (eventName: string, listenerFunc: (info: CapacitorKeyboardInfo) => void) => Promise<unknown> | unknown;
|
|
90
92
|
removeAllListeners: () => Promise<void> | void;
|
|
91
93
|
};
|
package/client/csrTypes.ts
CHANGED
|
@@ -7,15 +7,23 @@ import type { RouterInstance } from "./router";
|
|
|
7
7
|
import type { ReactFont } from "./types";
|
|
8
8
|
|
|
9
9
|
export type TransitionType = "none" | "fade" | "bottomUp" | "stack" | "scaleOut";
|
|
10
|
+
export type PageSafeAreaConfig =
|
|
11
|
+
| boolean
|
|
12
|
+
| "top"
|
|
13
|
+
| "bottom"
|
|
14
|
+
| {
|
|
15
|
+
top?: boolean;
|
|
16
|
+
bottom?: boolean;
|
|
17
|
+
android?: "auto" | "edge-to-edge" | "none";
|
|
18
|
+
};
|
|
10
19
|
/** Per-page CSR configuration for transition, safe-area, and gesture behavior. */
|
|
11
20
|
export interface PageConfig {
|
|
12
21
|
transition?: TransitionType;
|
|
13
|
-
safeArea?:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
bottomInset?: boolean | number;
|
|
22
|
+
safeArea?: PageSafeAreaConfig;
|
|
23
|
+
/** Top chrome reservation in px. Use 0 or omit for no reservation. */
|
|
24
|
+
topInset?: number;
|
|
25
|
+
/** Bottom chrome reservation in px. Use 0 or omit for no reservation. */
|
|
26
|
+
bottomInset?: number;
|
|
19
27
|
gesture?: boolean;
|
|
20
28
|
cache?: boolean;
|
|
21
29
|
/**
|
|
@@ -97,6 +105,7 @@ export interface RouteRender {
|
|
|
97
105
|
resolveError?: () => PromiseOrObject<LayoutErrorRender | undefined>;
|
|
98
106
|
resolveHead?: ResolveHead;
|
|
99
107
|
getPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
|
|
108
|
+
getLayoutPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
|
|
100
109
|
}
|
|
101
110
|
export interface WebAppManifestIcon {
|
|
102
111
|
src: string;
|
|
@@ -158,6 +167,7 @@ export interface PageModule {
|
|
|
158
167
|
}
|
|
159
168
|
export interface LayoutModule {
|
|
160
169
|
default?: LayoutRender;
|
|
170
|
+
pageConfig?: PageConfig;
|
|
161
171
|
head?: Head;
|
|
162
172
|
metadata?: AkanMetadata;
|
|
163
173
|
generateHead?: GenerateHead;
|
|
@@ -176,6 +186,8 @@ export interface LayoutModule {
|
|
|
176
186
|
export type RouteModule = PageModule | LayoutModule;
|
|
177
187
|
export interface Route {
|
|
178
188
|
PageConfig?: PageConfig;
|
|
189
|
+
pageConfig?: PageConfig;
|
|
190
|
+
layoutPageConfig?: PageConfig;
|
|
179
191
|
path: string;
|
|
180
192
|
renderPage?: RouteRender;
|
|
181
193
|
renderLayout?: RouteRender;
|
|
@@ -184,6 +196,8 @@ export interface Route {
|
|
|
184
196
|
|
|
185
197
|
loader?: () => unknown;
|
|
186
198
|
pageState?: PageState;
|
|
199
|
+
pageConfigChain?: PageConfig[];
|
|
200
|
+
explicitPageConfigKeys?: Partial<Record<keyof PageConfig, boolean>>;
|
|
187
201
|
|
|
188
202
|
children: Map<string, Route>;
|
|
189
203
|
}
|
|
@@ -226,8 +240,8 @@ export const defaultPageState: PageState = {
|
|
|
226
240
|
bottomInset: 0,
|
|
227
241
|
gesture: true,
|
|
228
242
|
cache: false,
|
|
229
|
-
topSafeAreaColor: "
|
|
230
|
-
bottomSafeAreaColor: "
|
|
243
|
+
topSafeAreaColor: "var(--color-base-100, Canvas)",
|
|
244
|
+
bottomSafeAreaColor: "var(--color-base-100, Canvas)",
|
|
231
245
|
};
|
|
232
246
|
|
|
233
247
|
export interface Location {
|
|
@@ -239,9 +253,23 @@ export interface Location {
|
|
|
239
253
|
pathRoute: PathRoute;
|
|
240
254
|
hash: string;
|
|
241
255
|
}
|
|
256
|
+
export type CsrNavigationPhase = "idle" | "preparing" | "transitioning";
|
|
257
|
+
export type CsrNavigationKind = "push" | "replace" | "back" | "popForward" | "popBack";
|
|
258
|
+
export interface NavigationIntent {
|
|
259
|
+
id: number;
|
|
260
|
+
kind: CsrNavigationKind;
|
|
261
|
+
from: Location;
|
|
262
|
+
to: Location;
|
|
263
|
+
scrollTop: number;
|
|
264
|
+
scrollToTop?: boolean;
|
|
265
|
+
createdAt: number;
|
|
266
|
+
}
|
|
242
267
|
export interface LocationState {
|
|
243
268
|
location: Location;
|
|
244
269
|
prevLocation: Location | null;
|
|
270
|
+
pendingLocation?: Location | null;
|
|
271
|
+
navigationIntent?: NavigationIntent | null;
|
|
272
|
+
phase: CsrNavigationPhase;
|
|
245
273
|
}
|
|
246
274
|
export interface History {
|
|
247
275
|
type: "initial" | "forward" | "back";
|
|
@@ -264,6 +292,9 @@ export interface RouteState {
|
|
|
264
292
|
clientHeight: number;
|
|
265
293
|
location: Location;
|
|
266
294
|
prevLocation: Location | null;
|
|
295
|
+
pendingLocation: Location | null;
|
|
296
|
+
navigationIntent: NavigationIntent | null;
|
|
297
|
+
phase: CsrNavigationPhase;
|
|
267
298
|
history: RefObject<History>;
|
|
268
299
|
topSafeAreaRef: RefObject<HTMLDivElement | null>;
|
|
269
300
|
bottomSafeAreaRef: RefObject<HTMLDivElement | null>;
|
|
@@ -273,6 +304,8 @@ export interface RouteState {
|
|
|
273
304
|
onBack: RefObject<{ [K in TransitionType]?: () => Promise<void> }>;
|
|
274
305
|
router: RouterInstance;
|
|
275
306
|
pathRoutes: PathRoute[];
|
|
307
|
+
registerFrameSlot: (path: string, slot: FrameSlotRegistration, bucket?: FrameSlotBucket) => () => void;
|
|
308
|
+
frameLayout: FrameLayoutState;
|
|
276
309
|
}
|
|
277
310
|
|
|
278
311
|
export type UseCsrTransition = CsrTransitionStyles & {
|
|
@@ -293,11 +326,12 @@ export const useCsr = () => {
|
|
|
293
326
|
};
|
|
294
327
|
|
|
295
328
|
export interface PathContextType {
|
|
296
|
-
pageType: "current" | "prev" | "cached";
|
|
329
|
+
pageType: "current" | "prev" | "cached" | "pending";
|
|
297
330
|
location: Location;
|
|
298
331
|
prefix?: string;
|
|
299
332
|
gestureEnabled: boolean;
|
|
300
333
|
setGestureEnabled: (enabled: boolean) => void;
|
|
334
|
+
registerFrameSlot: (slot: FrameSlotRegistration) => () => void;
|
|
301
335
|
}
|
|
302
336
|
export const pathContext = createContext<PathContextType>({} as unknown as PathContextType);
|
|
303
337
|
export const usePathCtx = () => {
|
|
@@ -310,12 +344,101 @@ export interface PathRoute {
|
|
|
310
344
|
pathSegments: string[];
|
|
311
345
|
renderPage: RouteRender;
|
|
312
346
|
pageState: PageState;
|
|
347
|
+
pageConfigChain?: PageConfig[];
|
|
348
|
+
explicitPageConfigKeys?: Partial<Record<keyof PageConfig, boolean>>;
|
|
313
349
|
renderRootLayouts: RouteRender[];
|
|
314
350
|
renderLayouts: RouteRender[];
|
|
315
351
|
resolveHead?: ResolveHead;
|
|
316
352
|
isSpecialRoute?: boolean;
|
|
317
353
|
}
|
|
318
354
|
|
|
355
|
+
export type FrameSlotScope = "page" | "layout";
|
|
356
|
+
export type FrameSlotType = "topInset" | "bottomInset";
|
|
357
|
+
export type FrameSlotBucket = "active" | "pending";
|
|
358
|
+
export type FrameLayer = "page" | "topChrome" | "bottomChrome" | "keyboard" | "overlay";
|
|
359
|
+
export type FrameSlotRole = "topChrome" | "bottomChrome" | "keyboardAccessory";
|
|
360
|
+
export type FramePlatformProfile = "ios" | "android" | "web" | "mobileWeb";
|
|
361
|
+
export interface FrameViewportState {
|
|
362
|
+
width: number;
|
|
363
|
+
height: number;
|
|
364
|
+
visualWidth: number;
|
|
365
|
+
visualHeight: number;
|
|
366
|
+
visualOffsetTop: number;
|
|
367
|
+
}
|
|
368
|
+
export interface KeyboardFrameState {
|
|
369
|
+
height: number;
|
|
370
|
+
offset: number;
|
|
371
|
+
visible: boolean;
|
|
372
|
+
sticky: boolean;
|
|
373
|
+
frozen?: boolean;
|
|
374
|
+
source?: "native" | "visualViewport" | "fallback";
|
|
375
|
+
animationDuration?: number;
|
|
376
|
+
animationEasing?: string;
|
|
377
|
+
}
|
|
378
|
+
export interface FrameContentViewportState {
|
|
379
|
+
top: number;
|
|
380
|
+
bottom: number;
|
|
381
|
+
height: number;
|
|
382
|
+
}
|
|
383
|
+
export interface KeyboardAccessoryFrameState {
|
|
384
|
+
top: number;
|
|
385
|
+
bottom: number;
|
|
386
|
+
height: number;
|
|
387
|
+
visible: boolean;
|
|
388
|
+
slotHeight: number;
|
|
389
|
+
}
|
|
390
|
+
export interface FrameSnapshot {
|
|
391
|
+
location: Location;
|
|
392
|
+
pageState: PageState;
|
|
393
|
+
viewport: FrameViewportState;
|
|
394
|
+
frameSlots: FrameSlotRegistration[];
|
|
395
|
+
measuredAt: number;
|
|
396
|
+
}
|
|
397
|
+
export interface TransitionActionContext {
|
|
398
|
+
plan: TransitionPlan;
|
|
399
|
+
}
|
|
400
|
+
export interface TransitionAction {
|
|
401
|
+
type: "page" | "topChrome" | "bottomChrome" | "keyboard" | "safeArea" | (string & {});
|
|
402
|
+
run: (ctx: TransitionActionContext) => Promise<void> | void;
|
|
403
|
+
}
|
|
404
|
+
export interface TransitionPlan {
|
|
405
|
+
id: number;
|
|
406
|
+
intent: NavigationIntent;
|
|
407
|
+
type: TransitionType;
|
|
408
|
+
direction: "forward" | "back";
|
|
409
|
+
fromFrame: FrameSnapshot;
|
|
410
|
+
toFrame: FrameSnapshot;
|
|
411
|
+
actions: TransitionAction[];
|
|
412
|
+
duration: number;
|
|
413
|
+
}
|
|
414
|
+
export interface FrameLayerZIndex {
|
|
415
|
+
page: number;
|
|
416
|
+
previousPage: number;
|
|
417
|
+
cachedPage: number;
|
|
418
|
+
topChrome: number;
|
|
419
|
+
bottomChrome: number;
|
|
420
|
+
keyboard: number;
|
|
421
|
+
overlay: number;
|
|
422
|
+
}
|
|
423
|
+
export interface FrameLayoutState {
|
|
424
|
+
viewport: FrameViewportState;
|
|
425
|
+
keyboard: KeyboardFrameState;
|
|
426
|
+
contentViewport: FrameContentViewportState;
|
|
427
|
+
keyboardAccessory: KeyboardAccessoryFrameState;
|
|
428
|
+
platformProfile: FramePlatformProfile;
|
|
429
|
+
zIndex: FrameLayerZIndex;
|
|
430
|
+
pageStateByPath: Map<string, PageState>;
|
|
431
|
+
}
|
|
432
|
+
export interface FrameSlotRegistration {
|
|
433
|
+
scope?: FrameSlotScope;
|
|
434
|
+
type: FrameSlotType;
|
|
435
|
+
role?: FrameSlotRole;
|
|
436
|
+
height?: number;
|
|
437
|
+
estimatedHeight?: number;
|
|
438
|
+
source?: "navbar" | "topInset" | "bottomInset" | "bottomTab" | (string & {});
|
|
439
|
+
cache?: boolean;
|
|
440
|
+
}
|
|
441
|
+
|
|
319
442
|
export interface LayoutFallbackRoute {
|
|
320
443
|
path: string;
|
|
321
444
|
pathSegments: string[];
|
package/client/device.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import type { RefObject } from "react";
|
|
3
|
+
import { debugFrame } from "./frameDebug";
|
|
3
4
|
import type {
|
|
4
5
|
CapacitorDeviceInfo,
|
|
5
6
|
CapacitorHapticsModule,
|
|
@@ -114,6 +115,7 @@ export class Device {
|
|
|
114
115
|
insets: { top: topSafeArea, bottom: bottomSafeArea },
|
|
115
116
|
},
|
|
116
117
|
] = await Promise.all([CapacitorDevice.getInfo(), CapacitorDevice.getLanguageCode(), SafeArea.getSafeAreaInsets()]);
|
|
118
|
+
if (info.platform === "ios") await Keyboard.setResizeMode?.({ mode: "none" });
|
|
117
119
|
const predefinedLangPath = window.location.pathname.split("/")[1]?.split("?")[0];
|
|
118
120
|
const predefinedLang = supportLanguages.find((language) => language === predefinedLangPath);
|
|
119
121
|
const device = new Device({
|
|
@@ -165,17 +167,24 @@ export class Device {
|
|
|
165
167
|
}
|
|
166
168
|
listenKeyboardChanged(onKeyboardChanged: (height: number) => void) {
|
|
167
169
|
if (this.info.platform === "web") return;
|
|
170
|
+
let currentHeight = 0;
|
|
171
|
+
const emitKeyboardHeight = (event: string, height: number) => {
|
|
172
|
+
debugFrame("keyboard.event", { event, height, previousHeight: currentHeight });
|
|
173
|
+
if (currentHeight === height) return;
|
|
174
|
+
currentHeight = height;
|
|
175
|
+
onKeyboardChanged(height);
|
|
176
|
+
};
|
|
168
177
|
void this.#keyboard.addListener("keyboardWillShow", (keyboard: CapacitorKeyboardInfo) => {
|
|
169
|
-
|
|
178
|
+
emitKeyboardHeight("keyboardWillShow", keyboard.keyboardHeight);
|
|
170
179
|
});
|
|
171
180
|
void this.#keyboard.addListener("keyboardDidShow", (keyboard: CapacitorKeyboardInfo) => {
|
|
172
|
-
|
|
181
|
+
emitKeyboardHeight("keyboardDidShow", keyboard.keyboardHeight);
|
|
173
182
|
});
|
|
174
183
|
void this.#keyboard.addListener("keyboardWillHide", () => {
|
|
175
|
-
|
|
184
|
+
emitKeyboardHeight("keyboardWillHide", 0);
|
|
176
185
|
});
|
|
177
186
|
void this.#keyboard.addListener("keyboardDidHide", () => {
|
|
178
|
-
|
|
187
|
+
emitKeyboardHeight("keyboardDidHide", 0);
|
|
179
188
|
});
|
|
180
189
|
}
|
|
181
190
|
unlistenKeyboardChanged() {
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { PageConfig, PageSafeAreaConfig, PageState, TransitionType } from "./csrTypes";
|
|
2
|
+
|
|
3
|
+
export type DevicePlatform = "ios" | "android" | "web" | (string & {});
|
|
4
|
+
export type SafeAreaInsets = { top: number; bottom: number };
|
|
5
|
+
|
|
6
|
+
export interface ResolvePageStateOptions {
|
|
7
|
+
configChain?: PageConfig[];
|
|
8
|
+
path: string;
|
|
9
|
+
basePath?: string;
|
|
10
|
+
platform: DevicePlatform;
|
|
11
|
+
deviceSafeArea: SafeAreaInsets;
|
|
12
|
+
cssSafeArea?: SafeAreaInsets;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const pageConfigKeys = new Set<keyof PageConfig>([
|
|
16
|
+
"transition",
|
|
17
|
+
"safeArea",
|
|
18
|
+
"topInset",
|
|
19
|
+
"bottomInset",
|
|
20
|
+
"gesture",
|
|
21
|
+
"cache",
|
|
22
|
+
"rscPatchHeadSafe",
|
|
23
|
+
"topSafeAreaColor",
|
|
24
|
+
"bottomSafeAreaColor",
|
|
25
|
+
]);
|
|
26
|
+
const transitionTypes = new Set<TransitionType>(["none", "fade", "bottomUp", "stack", "scaleOut"]);
|
|
27
|
+
|
|
28
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
29
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
|
+
|
|
31
|
+
const hasOwn = <Key extends PropertyKey>(value: object, key: Key) => Object.hasOwn(value, key);
|
|
32
|
+
|
|
33
|
+
export function validatePageConfig(routeKey: string, config?: PageConfig) {
|
|
34
|
+
if (config === undefined) return;
|
|
35
|
+
if (!isRecord(config)) throw new Error(`[route-convention] pageConfig in ${routeKey} must be an object.`);
|
|
36
|
+
const pageConfig = config as PageConfig;
|
|
37
|
+
for (const key of Object.keys(pageConfig)) {
|
|
38
|
+
if (!pageConfigKeys.has(key as keyof PageConfig)) {
|
|
39
|
+
throw new Error(`[route-convention] unsupported pageConfig option "${key}" in ${routeKey}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (pageConfig.transition !== undefined && !transitionTypes.has(pageConfig.transition)) {
|
|
43
|
+
throw new Error(`[route-convention] unsupported pageConfig.transition "${pageConfig.transition}" in ${routeKey}`);
|
|
44
|
+
}
|
|
45
|
+
if (pageConfig.topInset !== undefined && !isValidInsetValue(pageConfig.topInset)) {
|
|
46
|
+
throw new Error(`[route-convention] pageConfig.topInset in ${routeKey} must be a non-negative px number.`);
|
|
47
|
+
}
|
|
48
|
+
if (pageConfig.bottomInset !== undefined && !isValidInsetValue(pageConfig.bottomInset)) {
|
|
49
|
+
throw new Error(`[route-convention] pageConfig.bottomInset in ${routeKey} must be a non-negative px number.`);
|
|
50
|
+
}
|
|
51
|
+
validateSafeAreaConfig(routeKey, pageConfig.safeArea);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isValidInsetValue(value: unknown): value is number {
|
|
55
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validateSafeAreaConfig(routeKey: string, safeArea?: PageSafeAreaConfig) {
|
|
59
|
+
if (
|
|
60
|
+
safeArea === undefined ||
|
|
61
|
+
typeof safeArea === "boolean" ||
|
|
62
|
+
safeArea === "top" ||
|
|
63
|
+
safeArea === "bottom"
|
|
64
|
+
)
|
|
65
|
+
return;
|
|
66
|
+
if (!isRecord(safeArea)) throw new Error(`[route-convention] pageConfig.safeArea in ${routeKey} is invalid.`);
|
|
67
|
+
for (const key of Object.keys(safeArea)) {
|
|
68
|
+
if (key !== "top" && key !== "bottom" && key !== "android") {
|
|
69
|
+
throw new Error(`[route-convention] unsupported pageConfig.safeArea option "${key}" in ${routeKey}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (safeArea.top !== undefined && typeof safeArea.top !== "boolean") {
|
|
73
|
+
throw new Error(`[route-convention] pageConfig.safeArea.top in ${routeKey} must be a boolean.`);
|
|
74
|
+
}
|
|
75
|
+
if (safeArea.bottom !== undefined && typeof safeArea.bottom !== "boolean") {
|
|
76
|
+
throw new Error(`[route-convention] pageConfig.safeArea.bottom in ${routeKey} must be a boolean.`);
|
|
77
|
+
}
|
|
78
|
+
if (
|
|
79
|
+
safeArea.android !== undefined &&
|
|
80
|
+
safeArea.android !== "auto" &&
|
|
81
|
+
safeArea.android !== "edge-to-edge" &&
|
|
82
|
+
safeArea.android !== "none"
|
|
83
|
+
) {
|
|
84
|
+
throw new Error(`[route-convention] pageConfig.safeArea.android in ${routeKey} is invalid.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function mergePageConfigs(configChain: PageConfig[] = []): PageConfig {
|
|
89
|
+
const merged: PageConfig = {};
|
|
90
|
+
for (const config of configChain) {
|
|
91
|
+
for (const key of pageConfigKeys) {
|
|
92
|
+
const value = config[key];
|
|
93
|
+
if (value === undefined) continue;
|
|
94
|
+
if (key === "safeArea" && isRecord(merged.safeArea) && isRecord(value)) {
|
|
95
|
+
merged.safeArea = { ...merged.safeArea, ...value };
|
|
96
|
+
} else {
|
|
97
|
+
(merged as Record<keyof PageConfig, unknown>)[key] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return merged;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getExplicitPageConfigKeys(configChain: PageConfig[] = []): Partial<Record<keyof PageConfig, boolean>> {
|
|
105
|
+
const explicitKeys: Partial<Record<keyof PageConfig, boolean>> = {};
|
|
106
|
+
for (const config of configChain) {
|
|
107
|
+
for (const key of pageConfigKeys) {
|
|
108
|
+
if (hasOwn(config, key)) explicitKeys[key] = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return explicitKeys;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function resolvePageState({
|
|
115
|
+
configChain = [],
|
|
116
|
+
path,
|
|
117
|
+
basePath,
|
|
118
|
+
platform,
|
|
119
|
+
deviceSafeArea,
|
|
120
|
+
cssSafeArea,
|
|
121
|
+
}: ResolvePageStateOptions): PageState {
|
|
122
|
+
const profile = getPlatformFrameProfile(platform);
|
|
123
|
+
const routeRole = getRouteRoleFrameDefaults(path, platform, basePath);
|
|
124
|
+
const config = mergePageConfigs([profile, routeRole, ...configChain]);
|
|
125
|
+
const explicitKeys = getExplicitPageConfigKeys(configChain);
|
|
126
|
+
const safeArea = resolveSafeArea({
|
|
127
|
+
safeArea: config.safeArea,
|
|
128
|
+
platform,
|
|
129
|
+
deviceSafeArea,
|
|
130
|
+
cssSafeArea,
|
|
131
|
+
});
|
|
132
|
+
const transition = config.transition ?? "none";
|
|
133
|
+
return {
|
|
134
|
+
transition,
|
|
135
|
+
topSafeArea: safeArea.top,
|
|
136
|
+
bottomSafeArea: safeArea.bottom,
|
|
137
|
+
topInset: config.topInset ?? 0,
|
|
138
|
+
bottomInset: config.bottomInset ?? 0,
|
|
139
|
+
gesture: explicitKeys.gesture ? (config.gesture ?? false) : transition === "none" ? false : (config.gesture ?? false),
|
|
140
|
+
cache: config.cache ?? false,
|
|
141
|
+
topSafeAreaColor: config.topSafeAreaColor ?? "var(--color-base-100, Canvas)",
|
|
142
|
+
bottomSafeAreaColor: config.bottomSafeAreaColor ?? "var(--color-base-100, Canvas)",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getPlatformFrameProfile(platform: DevicePlatform): PageConfig {
|
|
147
|
+
if (platform === "ios") return { safeArea: true, transition: "stack" };
|
|
148
|
+
if (platform === "android") return { safeArea: { android: "auto" }, transition: "scaleOut", gesture: false };
|
|
149
|
+
return { safeArea: false, transition: "none", gesture: false };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getRouteRoleFrameDefaults(path: string, platform: DevicePlatform, basePath?: string): PageConfig {
|
|
153
|
+
const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "");
|
|
154
|
+
const depth = path
|
|
155
|
+
.split("/")
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.filter((segment) => segment !== "[lang]" && segment !== ":lang")
|
|
158
|
+
.filter((segment) => (normalizedBasePath ? segment !== normalizedBasePath : true)).length;
|
|
159
|
+
if (depth <= 1) return { transition: "none", gesture: false, cache: true };
|
|
160
|
+
return {
|
|
161
|
+
transition: platform === "ios" ? "stack" : platform === "android" ? "scaleOut" : "none",
|
|
162
|
+
gesture: platform === "ios",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resolveSafeArea({
|
|
167
|
+
safeArea,
|
|
168
|
+
platform,
|
|
169
|
+
deviceSafeArea,
|
|
170
|
+
cssSafeArea,
|
|
171
|
+
}: {
|
|
172
|
+
safeArea?: PageSafeAreaConfig;
|
|
173
|
+
platform: DevicePlatform;
|
|
174
|
+
deviceSafeArea: SafeAreaInsets;
|
|
175
|
+
cssSafeArea?: SafeAreaInsets;
|
|
176
|
+
}): SafeAreaInsets {
|
|
177
|
+
if (safeArea === false) return { top: 0, bottom: 0 };
|
|
178
|
+
const topEnabled =
|
|
179
|
+
safeArea === true ||
|
|
180
|
+
safeArea === "top" ||
|
|
181
|
+
(isRecord(safeArea) ? safeArea.top !== false : safeArea === undefined && platform !== "web");
|
|
182
|
+
const bottomEnabled =
|
|
183
|
+
safeArea === true ||
|
|
184
|
+
safeArea === "bottom" ||
|
|
185
|
+
(isRecord(safeArea) ? safeArea.bottom !== false : safeArea === undefined && platform !== "web");
|
|
186
|
+
if (platform === "android") {
|
|
187
|
+
const androidMode = isRecord(safeArea) ? (safeArea.android as "auto" | "edge-to-edge" | "none" | undefined) : "auto";
|
|
188
|
+
if (androidMode === "none") return { top: 0, bottom: 0 };
|
|
189
|
+
const source =
|
|
190
|
+
androidMode === "edge-to-edge"
|
|
191
|
+
? maxInsets(deviceSafeArea, cssSafeArea)
|
|
192
|
+
: cssSafeArea && hasInset(cssSafeArea)
|
|
193
|
+
? cssSafeArea
|
|
194
|
+
: null;
|
|
195
|
+
return {
|
|
196
|
+
top: topEnabled && source ? source.top : 0,
|
|
197
|
+
bottom: bottomEnabled && source ? source.bottom : 0,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
top: topEnabled ? deviceSafeArea.top : 0,
|
|
202
|
+
bottom: bottomEnabled ? deviceSafeArea.bottom : 0,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const hasInset = (insets: SafeAreaInsets) => insets.top > 0 || insets.bottom > 0;
|
|
207
|
+
|
|
208
|
+
const maxInsets = (a: SafeAreaInsets, b?: SafeAreaInsets): SafeAreaInsets => ({
|
|
209
|
+
top: Math.max(a.top, b?.top ?? 0),
|
|
210
|
+
bottom: Math.max(a.bottom, b?.bottom ?? 0),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
export function readCssSafeAreaInsets(): SafeAreaInsets {
|
|
214
|
+
if (typeof window === "undefined") return { top: 0, bottom: 0 };
|
|
215
|
+
const style = window.getComputedStyle?.(document.documentElement);
|
|
216
|
+
return {
|
|
217
|
+
top: readCssPixel(style?.getPropertyValue("--safe-area-inset-top")) || readCssEnvProbe("top"),
|
|
218
|
+
bottom: readCssPixel(style?.getPropertyValue("--safe-area-inset-bottom")) || readCssEnvProbe("bottom"),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function readCssEnvProbe(side: "top" | "bottom") {
|
|
223
|
+
if (typeof document === "undefined") return 0;
|
|
224
|
+
const probe = document.createElement("div");
|
|
225
|
+
probe.style.position = "fixed";
|
|
226
|
+
probe.style.visibility = "hidden";
|
|
227
|
+
probe.style.pointerEvents = "none";
|
|
228
|
+
probe.style[side] = `env(safe-area-inset-${side})`;
|
|
229
|
+
document.documentElement.appendChild(probe);
|
|
230
|
+
const value = readCssPixel(window.getComputedStyle(probe)[side]);
|
|
231
|
+
probe.remove();
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function readCssPixel(value?: string) {
|
|
236
|
+
if (!value) return 0;
|
|
237
|
+
const parsed = Number.parseFloat(value);
|
|
238
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
239
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
type DebugPayload = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
const debugSessionId = Math.random().toString(36).slice(2, 8);
|
|
6
|
+
let debugSeq = 0;
|
|
7
|
+
|
|
8
|
+
const isFrameDebugEnabled = () => {
|
|
9
|
+
if (typeof window === "undefined") return false;
|
|
10
|
+
const windowWithTarget = window as typeof window & { __AKAN_MOBILE_TARGET__?: unknown };
|
|
11
|
+
const search = new URLSearchParams(window.location.search);
|
|
12
|
+
return (
|
|
13
|
+
Boolean(windowWithTarget.__AKAN_MOBILE_TARGET__) ||
|
|
14
|
+
search.has("akanMobileTarget") ||
|
|
15
|
+
search.get("akanFrameDebug") === "1" ||
|
|
16
|
+
window.localStorage.getItem("akan:debug:frame") === "1"
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function debugFrame(event: string, payload: DebugPayload = {}) {
|
|
21
|
+
if (!isFrameDebugEnabled()) return;
|
|
22
|
+
debugSeq += 1;
|
|
23
|
+
console.info(`[akan:frame:${debugSessionId}:${debugSeq}] ${event}`, {
|
|
24
|
+
href: window.location.href,
|
|
25
|
+
now: Math.round(performance.now()),
|
|
26
|
+
...payload,
|
|
27
|
+
});
|
|
28
|
+
}
|
package/client/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ export * from "./createFont";
|
|
|
4
4
|
export * from "./csrTypes";
|
|
5
5
|
export * from "./decorators";
|
|
6
6
|
export * from "./device";
|
|
7
|
+
export * from "./frameConfig";
|
|
8
|
+
export * from "./frameDebug";
|
|
7
9
|
export * from "./locale";
|
|
8
10
|
export * from "./makePageProto";
|
|
9
11
|
export * from "./router";
|
package/client/router.ts
CHANGED
|
@@ -91,6 +91,8 @@ const getServerBasePath = (reqPathname: string, lang: string, headerBasePath: st
|
|
|
91
91
|
|
|
92
92
|
declare global {
|
|
93
93
|
var __AKAN_ROUTER__: Router | undefined;
|
|
94
|
+
var __AKAN_DEV_SYNC_NAVIGATION__: ((href: string, kind: "push" | "replace" | "back" | "pop") => void) | undefined;
|
|
95
|
+
var __AKAN_DEV_SYNC_NAVIGATION_APPLYING__: boolean | undefined;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
export const getPathInfo = (requestUrl: string, lang: string, prefix: string) => {
|
|
@@ -232,14 +234,21 @@ class Router {
|
|
|
232
234
|
Logger.log(`pathChange-start:${path}${hash ? `#${hash}` : ""}`);
|
|
233
235
|
window.parent.postMessage({ type: "pathChange", path, pathname, hash }, "*");
|
|
234
236
|
}
|
|
237
|
+
#postDevSyncNavigation(kind: "push" | "replace", href: string) {
|
|
238
|
+
if (globalThis.__AKAN_DEV_SYNC_NAVIGATION_APPLYING__) return;
|
|
239
|
+
const { path, search, hash } = this.#getPathInfo(href);
|
|
240
|
+
globalThis.__AKAN_DEV_SYNC_NAVIGATION__?.(`${path}${search ? `?${search}` : ""}${hash ? `#${hash}` : ""}`, kind);
|
|
241
|
+
}
|
|
235
242
|
push(href: string, routeOptions?: RouteOptions) {
|
|
236
243
|
this.#checkInitialized();
|
|
237
244
|
this.#instance.push(href, routeOptions);
|
|
245
|
+
this.#postDevSyncNavigation("push", href);
|
|
238
246
|
return undefined as never;
|
|
239
247
|
}
|
|
240
248
|
replace(href: string, routeOptions?: RouteOptions) {
|
|
241
249
|
this.#checkInitialized();
|
|
242
250
|
this.#instance.replace(href, routeOptions);
|
|
251
|
+
this.#postDevSyncNavigation("replace", href);
|
|
243
252
|
return undefined as never;
|
|
244
253
|
}
|
|
245
254
|
back(routeOptions?: RouteOptions) {
|
package/fetch/requestStorage.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface AkanDynamicUsage {
|
|
|
15
15
|
export interface AkanRequestStore {
|
|
16
16
|
request: Request;
|
|
17
17
|
theme?: AkanTheme;
|
|
18
|
+
frameState?: unknown;
|
|
18
19
|
queryCache: Map<string, Promise<unknown>>;
|
|
19
20
|
policy: AkanRequestPolicy;
|
|
20
21
|
dynamicUsage: AkanDynamicUsage;
|
|
@@ -91,6 +92,16 @@ export function getRequestTheme(): AkanTheme | undefined {
|
|
|
91
92
|
return getRequestStore()?.theme;
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
export function setRequestFrameState(frameState: unknown): void {
|
|
96
|
+
const store = getRequestStore();
|
|
97
|
+
if (!store) return;
|
|
98
|
+
store.frameState = frameState;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getRequestFrameState<FrameState = unknown>(): FrameState | undefined {
|
|
102
|
+
return getRequestStore()?.frameState as FrameState | undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
94
105
|
export function pushRequestFallback(storeOrRequest: Request | AkanRequestStore): () => void {
|
|
95
106
|
globalThis.__AKAN_REQUEST_FALLBACK_STACK__ ??= [];
|
|
96
107
|
const stack = globalThis.__AKAN_REQUEST_FALLBACK_STACK__;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akanjs",
|
|
3
|
-
"version": "2.3.6-rc.
|
|
3
|
+
"version": "2.3.6-rc.3",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
"chance": "^1.1.13",
|
|
198
198
|
"cordova-plugin-purchase": "^13.16.0",
|
|
199
199
|
"croner": "^10.0.1",
|
|
200
|
-
"daisyui": "
|
|
200
|
+
"daisyui": "5.5.23",
|
|
201
201
|
"ioredis": "^5.10.1",
|
|
202
202
|
"mermaid": "^11.15.0",
|
|
203
203
|
"postgres": "^3.4.9",
|