akanjs 3.0.0-alpha.5 → 3.0.0-alpha.6
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/cn.ts +7 -0
- package/client/csrTypes.ts +2 -0
- package/client/frameDebug.ts +3 -2
- package/package.json +1 -1
- package/server/webRouter.ts +96 -0
- package/types/client/cn.d.ts +5 -0
- package/types/client/csrTypes.d.ts +2 -0
- package/types/ui/Dropdown.d.ts +2 -0
- package/types/ui/Layout/BottomInset.d.ts +2 -1
- package/types/ui/Layout/index.d.ts +1 -1
- package/types/ui/Signal/style.d.ts +1 -1
- package/types/ui/UiOverride/context.d.ts +12 -2
- package/types/ui/index.d.ts +2 -1
- package/types/ui/overlayLayer.d.ts +24 -0
- package/types/ui/recipe/badgeRecipe.d.ts +7 -3
- package/types/ui/recipe/buttonRecipe.d.ts +7 -3
- package/types/ui/recipe/inputRecipe.d.ts +5 -1
- package/types/webkit/index.d.ts +1 -0
- package/types/webkit/lazy.d.ts +12 -0
- package/types/webkit/useCsrValues.d.ts +3 -3
- package/types/webkit/useEscapeKey.d.ts +5 -0
- package/types/webkit/useFrameRuntime.d.ts +6 -1
- package/ui/Badge.tsx +2 -2
- package/ui/BottomSheet.tsx +5 -0
- package/ui/Button.tsx +3 -1
- package/ui/Constant/Doc.tsx +1 -1
- package/ui/Data/ListContainer.tsx +1 -1
- package/ui/DatePicker.tsx +4 -3
- package/ui/Dialog/Modal.tsx +12 -15
- package/ui/DraggableList.tsx +3 -1
- package/ui/Dropdown.tsx +33 -11
- package/ui/Field.tsx +14 -8
- package/ui/Input.tsx +7 -5
- package/ui/Layout/BottomInset.tsx +6 -1
- package/ui/Loading/ProgressBar.tsx +8 -1
- package/ui/Menu.tsx +7 -8
- package/ui/Model/EditModal.tsx +37 -2
- package/ui/Model/SureToRemove.tsx +2 -1
- package/ui/Model/index_.tsx +42 -15
- package/ui/ObjectId.tsx +3 -4
- package/ui/Pagination.tsx +3 -4
- package/ui/Popconfirm.tsx +8 -7
- package/ui/Select.tsx +4 -3
- package/ui/System/CSR.tsx +6 -2
- package/ui/ToggleSelect.tsx +4 -3
- package/ui/Tooltip.tsx +2 -1
- package/ui/UiOverride/context.ts +12 -2
- package/ui/index.ts +8 -1
- package/ui/overlayLayer.ts +39 -0
- package/ui/recipe/badgeRecipe.ts +22 -3
- package/ui/recipe/buttonRecipe.ts +51 -2
- package/ui/recipe/factory.ts +2 -2
- package/ui/recipe/inputRecipe.ts +18 -4
- package/webkit/index.ts +1 -0
- package/webkit/lazy.tsx +22 -2
- package/webkit/useCsrValues.ts +121 -4
- package/webkit/useEscapeKey.tsx +42 -0
- package/webkit/useFrameRuntime.ts +65 -32
package/client/cn.ts
CHANGED
|
@@ -38,10 +38,17 @@ export const colorTokens = [
|
|
|
38
38
|
"ring",
|
|
39
39
|
];
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Akan's semantic radius tokens (`--radius-box` / `--radius-field` in ui/styles.css). Without them
|
|
43
|
+
* `cn("rounded-field", "rounded-full")` keeps both classes and stylesheet order decides the winner.
|
|
44
|
+
*/
|
|
45
|
+
export const radiusTokens = ["box", "field"];
|
|
46
|
+
|
|
41
47
|
const twMerge = extendTailwindMerge({
|
|
42
48
|
extend: {
|
|
43
49
|
theme: {
|
|
44
50
|
color: colorTokens,
|
|
51
|
+
radius: radiusTokens,
|
|
45
52
|
},
|
|
46
53
|
},
|
|
47
54
|
});
|
package/client/csrTypes.ts
CHANGED
|
@@ -455,6 +455,7 @@ export interface FrameLayoutState {
|
|
|
455
455
|
keyboard: KeyboardFrameState;
|
|
456
456
|
contentViewport: FrameContentViewportState;
|
|
457
457
|
keyboardAccessory: KeyboardAccessoryFrameState;
|
|
458
|
+
contentAnchor?: "bottom";
|
|
458
459
|
platformProfile: FramePlatformProfile;
|
|
459
460
|
zIndex: FrameLayerZIndex;
|
|
460
461
|
pageStateByPath: Map<string, PageState>;
|
|
@@ -463,6 +464,7 @@ export interface FrameSlotRegistration {
|
|
|
463
464
|
scope?: FrameSlotScope;
|
|
464
465
|
type: FrameSlotType;
|
|
465
466
|
role?: FrameSlotRole;
|
|
467
|
+
contentAnchor?: "bottom";
|
|
466
468
|
height?: number;
|
|
467
469
|
estimatedHeight?: number;
|
|
468
470
|
source?: "navbar" | "topInset" | "bottomInset" | "bottomTab" | (string & {});
|
package/client/frameDebug.ts
CHANGED
|
@@ -20,9 +20,10 @@ const isFrameDebugEnabled = () => {
|
|
|
20
20
|
export function debugFrame(event: string, payload: DebugPayload = {}) {
|
|
21
21
|
if (!isFrameDebugEnabled()) return;
|
|
22
22
|
debugSeq += 1;
|
|
23
|
-
|
|
23
|
+
const details = {
|
|
24
24
|
href: window.location.href,
|
|
25
25
|
now: Math.round(performance.now()),
|
|
26
26
|
...payload,
|
|
27
|
-
}
|
|
27
|
+
};
|
|
28
|
+
console.info(`[akan:frame:${debugSessionId}:${debugSeq}] ${event}`, details, JSON.stringify(details));
|
|
28
29
|
}
|
package/package.json
CHANGED
package/server/webRouter.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
3
4
|
import { getEnv } from "akanjs/base";
|
|
4
5
|
import {
|
|
5
6
|
type AkanI18nConfig,
|
|
@@ -61,6 +62,18 @@ export const DEFAULT_HTML_RESULT_CACHE_MAX_BODY_BYTES = 2 * 1024 * 1024;
|
|
|
61
62
|
const ROUTE_CACHE_SWEEP_INTERVAL_MS = 60_000;
|
|
62
63
|
const APPLE_APP_SITE_ASSOCIATION_PATH = "/.well-known/apple-app-site-association";
|
|
63
64
|
const ANDROID_ASSET_LINKS_PATH = "/.well-known/assetlinks.json";
|
|
65
|
+
const FIREBASE_MESSAGING_SW_PATH = "/firebase-messaging-sw.js";
|
|
66
|
+
const FIREBASE_WEB_SDK_VERSION = "12.13.0";
|
|
67
|
+
|
|
68
|
+
interface FirebaseClientEnvConfig {
|
|
69
|
+
apiKey: string;
|
|
70
|
+
authDomain?: string;
|
|
71
|
+
projectId: string;
|
|
72
|
+
storageBucket?: string;
|
|
73
|
+
messagingSenderId: string;
|
|
74
|
+
appId: string;
|
|
75
|
+
vapidKey?: string;
|
|
76
|
+
}
|
|
64
77
|
|
|
65
78
|
export function createRscRedirectResponse(
|
|
66
79
|
location: string,
|
|
@@ -488,6 +501,16 @@ export class WebRouter {
|
|
|
488
501
|
WebRouter.#deepLinkAssociationResponse(ANDROID_ASSET_LINKS_PATH, this.#artifact, {
|
|
489
502
|
cacheControl: this.#prodMode ? "public, max-age=3600" : "no-store",
|
|
490
503
|
}) ?? new Response("Not Found", { status: 404 }),
|
|
504
|
+
[FIREBASE_MESSAGING_SW_PATH]: async () => {
|
|
505
|
+
this.#requestStats.staticAsset += 1;
|
|
506
|
+
const firebaseConfig = await WebRouter.#resolveFirebaseClientConfig();
|
|
507
|
+
return new Response(WebRouter.#createFirebaseMessagingServiceWorker(firebaseConfig), {
|
|
508
|
+
headers: {
|
|
509
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
510
|
+
"Cache-Control": "no-store",
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
},
|
|
491
514
|
"/*": async (req) => {
|
|
492
515
|
const url = new URL(req.url);
|
|
493
516
|
if (WebRouter.#isImageOptimizerPath(url.pathname)) {
|
|
@@ -1008,6 +1031,79 @@ export class WebRouter {
|
|
|
1008
1031
|
return process.env.AKAN_APP_DIR ?? path.dirname(Bun.main);
|
|
1009
1032
|
}
|
|
1010
1033
|
|
|
1034
|
+
static async #resolveFirebaseClientConfig(): Promise<FirebaseClientEnvConfig | null> {
|
|
1035
|
+
const envPath = path.join(WebRouter.#resolveAppDir(), "env", "env.client.ts");
|
|
1036
|
+
if (!fs.existsSync(envPath)) return null;
|
|
1037
|
+
try {
|
|
1038
|
+
const envUrl = pathToFileURL(envPath);
|
|
1039
|
+
envUrl.searchParams.set("t", String(Date.now()));
|
|
1040
|
+
const envModule = (await import(envUrl.href)) as { env?: { firebase?: unknown } };
|
|
1041
|
+
return WebRouter.#normalizeFirebaseClientConfig(envModule.env?.firebase);
|
|
1042
|
+
} catch {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
static #normalizeFirebaseClientConfig(config: unknown): FirebaseClientEnvConfig | null {
|
|
1048
|
+
if (!config || typeof config !== "object") return null;
|
|
1049
|
+
const value = config as Partial<Record<keyof FirebaseClientEnvConfig, unknown>>;
|
|
1050
|
+
if (
|
|
1051
|
+
typeof value.apiKey !== "string" ||
|
|
1052
|
+
typeof value.projectId !== "string" ||
|
|
1053
|
+
typeof value.messagingSenderId !== "string" ||
|
|
1054
|
+
typeof value.appId !== "string"
|
|
1055
|
+
) {
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
return {
|
|
1059
|
+
apiKey: value.apiKey,
|
|
1060
|
+
...(typeof value.authDomain === "string" ? { authDomain: value.authDomain } : {}),
|
|
1061
|
+
projectId: value.projectId,
|
|
1062
|
+
...(typeof value.storageBucket === "string" ? { storageBucket: value.storageBucket } : {}),
|
|
1063
|
+
messagingSenderId: value.messagingSenderId,
|
|
1064
|
+
appId: value.appId,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
static #createFirebaseMessagingServiceWorker(config: FirebaseClientEnvConfig | null): string {
|
|
1069
|
+
const configJson = JSON.stringify(config);
|
|
1070
|
+
return `/* Generated by Akan.js. Do not edit. */
|
|
1071
|
+
const firebaseConfig = ${configJson};
|
|
1072
|
+
|
|
1073
|
+
if (firebaseConfig) {
|
|
1074
|
+
importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-app-compat.js");
|
|
1075
|
+
importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-messaging-compat.js");
|
|
1076
|
+
|
|
1077
|
+
firebase.initializeApp(firebaseConfig);
|
|
1078
|
+
const messaging = firebase.messaging();
|
|
1079
|
+
|
|
1080
|
+
const notificationUrl = (payload) =>
|
|
1081
|
+
payload?.data?.url || payload?.fcmOptions?.link || payload?.notification?.click_action;
|
|
1082
|
+
|
|
1083
|
+
messaging.onBackgroundMessage((payload) => {
|
|
1084
|
+
const title = payload?.notification?.title || "";
|
|
1085
|
+
const options = {
|
|
1086
|
+
body: payload?.notification?.body,
|
|
1087
|
+
icon: payload?.notification?.icon,
|
|
1088
|
+
image: payload?.notification?.image,
|
|
1089
|
+
data: {
|
|
1090
|
+
url: notificationUrl(payload),
|
|
1091
|
+
FCM_MSG: payload,
|
|
1092
|
+
},
|
|
1093
|
+
};
|
|
1094
|
+
self.registration.showNotification(title, options);
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
self.addEventListener("notificationclick", (event) => {
|
|
1099
|
+
const url = event.notification?.data?.url || event.notification?.data?.FCM_MSG?.data?.url;
|
|
1100
|
+
event.notification?.close();
|
|
1101
|
+
if (!url) return;
|
|
1102
|
+
event.waitUntil(clients.openWindow(url));
|
|
1103
|
+
});
|
|
1104
|
+
`;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1011
1107
|
static #normalizeArtifact(artifact: BaseBuildArtifact, artifactDir: string): BaseBuildArtifact {
|
|
1012
1108
|
const normalizedArtifactDir = path.resolve(artifactDir);
|
|
1013
1109
|
const pagesBundlePath = WebRouter.#resolveArtifactPath(artifact.pagesBundlePath, normalizedArtifactDir);
|
package/types/client/cn.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ import { type ClassNameValue } from "tailwind-merge";
|
|
|
6
6
|
* `cn("bg-primary", "bg-open")` correctly resolves to `"bg-open"` instead of keeping both.
|
|
7
7
|
*/
|
|
8
8
|
export declare const colorTokens: string[];
|
|
9
|
+
/**
|
|
10
|
+
* Akan's semantic radius tokens (`--radius-box` / `--radius-field` in ui/styles.css). Without them
|
|
11
|
+
* `cn("rounded-field", "rounded-full")` keeps both classes and stylesheet order decides the winner.
|
|
12
|
+
*/
|
|
13
|
+
export declare const radiusTokens: string[];
|
|
9
14
|
/** The one class-combining function: joins strings/arrays/conditional parts (`cond && "x"`) and
|
|
10
15
|
* resolves Tailwind conflicts with a shared tailwind-merge instance that knows Akan's semantic
|
|
11
16
|
* color tokens. clsx-style object syntax (`{ x: cond }`) is not supported — write `cond && "x"`. */
|
|
@@ -428,6 +428,7 @@ export interface FrameLayoutState {
|
|
|
428
428
|
keyboard: KeyboardFrameState;
|
|
429
429
|
contentViewport: FrameContentViewportState;
|
|
430
430
|
keyboardAccessory: KeyboardAccessoryFrameState;
|
|
431
|
+
contentAnchor?: "bottom";
|
|
431
432
|
platformProfile: FramePlatformProfile;
|
|
432
433
|
zIndex: FrameLayerZIndex;
|
|
433
434
|
pageStateByPath: Map<string, PageState>;
|
|
@@ -436,6 +437,7 @@ export interface FrameSlotRegistration {
|
|
|
436
437
|
scope?: FrameSlotScope;
|
|
437
438
|
type: FrameSlotType;
|
|
438
439
|
role?: FrameSlotRole;
|
|
440
|
+
contentAnchor?: "bottom";
|
|
439
441
|
height?: number;
|
|
440
442
|
estimatedHeight?: number;
|
|
441
443
|
source?: "navbar" | "topInset" | "bottomInset" | "bottomTab" | (string & {});
|
package/types/ui/Dropdown.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
+
/** Put this on a menu item that runs its own interaction (a switch, a copy button) to keep the menu open. */
|
|
3
|
+
export declare const DROPDOWN_KEEP_OPEN_ATTR = "data-dropdown-keep-open";
|
|
2
4
|
export interface DropdownProps {
|
|
3
5
|
/** Button/trigger content. */
|
|
4
6
|
value: ReactNode;
|
|
@@ -4,10 +4,11 @@ export interface BottomInsetProps {
|
|
|
4
4
|
className?: string;
|
|
5
5
|
children: ReactNode;
|
|
6
6
|
keyboardSticky?: boolean;
|
|
7
|
+
contentAnchor?: "bottom";
|
|
7
8
|
role?: "bottomChrome" | "keyboardAccessory";
|
|
8
9
|
estimatedHeight?: number;
|
|
9
10
|
frameScope?: FrameSlotRegistration["scope"];
|
|
10
11
|
frameSource?: FrameSlotRegistration["source"];
|
|
11
12
|
frameCache?: boolean;
|
|
12
13
|
}
|
|
13
|
-
export declare const BottomInset: ({ className, children, keyboardSticky, role, estimatedHeight, frameScope, frameSource, frameCache, }: BottomInsetProps) => import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export declare const BottomInset: ({ className, children, keyboardSticky, contentAnchor, role, estimatedHeight, frameScope, frameSource, frameCache, }: BottomInsetProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -4,7 +4,7 @@ export declare const Layout: {
|
|
|
4
4
|
Navbar: ({ back, className, height, children, title, left, right }: import("./Navbar.d.ts").NavbarProps) => import("react/jsx-runtime").JSX.Element;
|
|
5
5
|
TopInset: ({ className, children, estimatedHeight }: import("./TopInset.d.ts").TopInsetProps) => import("react/jsx-runtime").JSX.Element;
|
|
6
6
|
BottomTab: ({ className, tabs, height }: import("./BottomTab.d.ts").BottomTabProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
-
BottomInset: ({ className, children, keyboardSticky, role, estimatedHeight, frameScope, frameSource, frameCache, }: import("./BottomInset.d.ts").BottomInsetProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
BottomInset: ({ className, children, keyboardSticky, contentAnchor, role, estimatedHeight, frameScope, frameSource, frameCache, }: import("./BottomInset.d.ts").BottomInsetProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
8
|
Template: ({ className, children }: import("./Template.d.ts").TemplateProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
9
|
Unit: ({ className, children, href }: import("./Unit.d.ts").UnitProps) => import("react/jsx-runtime").JSX.Element;
|
|
10
10
|
View: ({ className, children }: import("./View.d.ts").ViewProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -11,4 +11,4 @@ export declare const signalUi: {
|
|
|
11
11
|
export declare const getEndpointBadgeClassName: (type: string) => string;
|
|
12
12
|
export declare const getGuardBadgeClassName: (guard: string) => string;
|
|
13
13
|
export declare const getStatusBadgeClassName: (status: string) => string;
|
|
14
|
-
export declare const getStatusTextareaClassName: (status: string) => "" | "pointer-events-none opacity-50" | "border-destructive text-destructive"
|
|
14
|
+
export declare const getStatusTextareaClassName: (status: string) => "" | "border-primary" | "pointer-events-none opacity-50" | "border-destructive text-destructive";
|
|
@@ -16,7 +16,7 @@ import type { ModalProps } from "../Modal.d.ts";
|
|
|
16
16
|
import type { PaginationProps } from "../Pagination.d.ts";
|
|
17
17
|
import type { PopconfirmProps } from "../Popconfirm.d.ts";
|
|
18
18
|
import type { ItemProps as RadioItemProps, RadioProps } from "../Radio.d.ts";
|
|
19
|
-
import type { BadgeVariants, ButtonVariants } from "../recipe.d.ts";
|
|
19
|
+
import type { BadgeVariants, ButtonVariants, InputSurfaceVariants } from "../recipe.d.ts";
|
|
20
20
|
import type { SelectProps } from "../Select.d.ts";
|
|
21
21
|
import type { TableProps } from "../Table.d.ts";
|
|
22
22
|
import type { MultiProps as ToggleSelectMultiProps, ToggleSelectProps } from "../ToggleSelect.d.ts";
|
|
@@ -75,11 +75,21 @@ export type AkanModalComponent = AkanUiOverrides["Modal"];
|
|
|
75
75
|
* manifest: `override({ recipes: { button: neonButtonRecipe } })`. A recipe swap changes
|
|
76
76
|
* only the className factory — the component's structure/behavior (async states, focus,
|
|
77
77
|
* a11y) is untouched. Each replacement must accept the framework recipe's full variant
|
|
78
|
-
* contract, so every call site keeps working
|
|
78
|
+
* contract, so every call site keeps working (a replacement with *extra* optional axes is
|
|
79
|
+
* assignable — contravariance — but those axes are only reachable from code that knows the
|
|
80
|
+
* replacement's own type).
|
|
81
|
+
*
|
|
82
|
+
* Scope: a recipe slot is a **client-side, route-scoped restyle**. It reaches framework
|
|
83
|
+
* client components, which resolve through `useUiRecipe(...)`. It never reaches a raw
|
|
84
|
+
* `xRecipe(...)` call in app JSX (statically imported — no context), nor server components
|
|
85
|
+
* (`Unit`/`View`), which intentionally render the canonical framework recipe. Extending the
|
|
86
|
+
* *vocabulary* is not this slot's job: add the axis to the framework recipe, or author an
|
|
87
|
+
* app recipe in `apps/<app>/ui/Recipe/`.
|
|
79
88
|
*/
|
|
80
89
|
export interface AkanUiRecipes {
|
|
81
90
|
button: (variants?: ButtonVariants, className?: ClassValue) => string;
|
|
82
91
|
badge: (variants?: BadgeVariants, className?: ClassValue) => string;
|
|
92
|
+
input: (variants?: InputSurfaceVariants, className?: ClassValue) => string;
|
|
83
93
|
}
|
|
84
94
|
/** Shape of an `_overrides.tsx` manifest: component slots plus an optional recipe-slot map. */
|
|
85
95
|
export type AkanUiOverrideManifest = Partial<AkanUiOverrides> & {
|
package/types/ui/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { DatePicker } from "./DatePicker.d.ts";
|
|
|
12
12
|
export { Dialog } from "./Dialog.d.ts";
|
|
13
13
|
export { DragAction } from "./DragAction.d.ts";
|
|
14
14
|
export { DraggableList } from "./DraggableList.d.ts";
|
|
15
|
-
export { Dropdown } from "./Dropdown.d.ts";
|
|
15
|
+
export { DROPDOWN_KEEP_OPEN_ATTR, Dropdown } from "./Dropdown.d.ts";
|
|
16
16
|
export { Empty } from "./Empty.d.ts";
|
|
17
17
|
export { Field } from "./Field.d.ts";
|
|
18
18
|
export { FontFace } from "./FontFace.d.ts";
|
|
@@ -29,6 +29,7 @@ export { Modal } from "./Modal.d.ts";
|
|
|
29
29
|
export { Model } from "./Model.d.ts";
|
|
30
30
|
export { More } from "./More.d.ts";
|
|
31
31
|
export { ObjectId } from "./ObjectId.d.ts";
|
|
32
|
+
export { isOwnOverlayClick, OVERLAY_LAYER_ATTR, OverlayOwnerProvider, useOverlayLayerProps, useOverlayScope, } from "./overlayLayer.d.ts";
|
|
32
33
|
export { Pagination } from "./Pagination.d.ts";
|
|
33
34
|
export { Popconfirm } from "./Popconfirm.d.ts";
|
|
34
35
|
export { Portal } from "./Portal.d.ts";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlay surfaces render through `createPortal(document.body)`, so they leave the DOM subtree of the
|
|
3
|
+
* component that opened them, and a dismiss check asking `ref.current.contains(target)` reads every
|
|
4
|
+
* click inside a dialog it opened *itself* as an outside click.
|
|
5
|
+
*
|
|
6
|
+
* React context reaches through a portal where the DOM does not, so an overlay can read which
|
|
7
|
+
* dismissable scope rendered it and stamp that owner on the roots it portals out. A scope then
|
|
8
|
+
* recognises its own overlays exactly, instead of guessing from how close a dialog happens to be.
|
|
9
|
+
*/
|
|
10
|
+
export declare const OVERLAY_LAYER_ATTR = "data-akan-overlay";
|
|
11
|
+
/** Wrap the content a dismissable container owns, with the scope from {@link useOverlayScope}. */
|
|
12
|
+
export declare const OverlayOwnerProvider: import("react").Provider<string>;
|
|
13
|
+
/** Scope a dismissable container hands to its content. Nests as a `parent/child` path. */
|
|
14
|
+
export declare const useOverlayScope: (id: string) => string;
|
|
15
|
+
/** Props an overlay spreads onto every root it portals out of its own tree. */
|
|
16
|
+
export declare const useOverlayLayerProps: () => {
|
|
17
|
+
"data-akan-overlay": string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* True when the click landed in an overlay that `scope` — or a scope nested inside it — rendered.
|
|
21
|
+
* An overlay nobody owns, and one owned by an unrelated scope, both read as an ordinary outside
|
|
22
|
+
* click, so a menu does not linger behind a dialog it has nothing to do with.
|
|
23
|
+
*/
|
|
24
|
+
export declare const isOwnOverlayClick: (target: EventTarget | null, scope: string) => boolean;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
/** 뱃지 look — 시맨틱 variant
|
|
1
|
+
/** 뱃지 look — 시맨틱 variant × size, outline 플래그는 색을 유지한 외곽선 스타일. `<Badge>` 가 소비하며, recipes.badge 슬롯으로 교체 가능. */
|
|
2
2
|
export declare const badgeRecipe: (variants?: Omit<({
|
|
3
|
-
|
|
3
|
+
size?: "lg" | "md" | "sm" | "xs" | undefined;
|
|
4
|
+
outline?: boolean | undefined;
|
|
5
|
+
variant?: "default" | "error" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "neutral" | "outline" | undefined;
|
|
4
6
|
} & {
|
|
5
7
|
class?: import("tailwind-variants").ClassValue;
|
|
6
8
|
className?: never;
|
|
7
9
|
}) | ({
|
|
8
|
-
|
|
10
|
+
size?: "lg" | "md" | "sm" | "xs" | undefined;
|
|
11
|
+
outline?: boolean | undefined;
|
|
12
|
+
variant?: "default" | "error" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "neutral" | "outline" | undefined;
|
|
9
13
|
} & {
|
|
10
14
|
class?: never;
|
|
11
15
|
className?: import("tailwind-variants").ClassValue;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
/** 버튼 look — 시맨틱 variant × size
|
|
1
|
+
/** 버튼 look — 시맨틱 variant × size × shape, outline 플래그는 색을 유지한 외곽선 스타일. `<Button>` 이 소비하며, `_overrides.tsx` 의 recipes.button 슬롯으로 교체 가능. */
|
|
2
2
|
export declare const buttonRecipe: (variants?: Omit<({
|
|
3
3
|
size?: "lg" | "md" | "sm" | "xs" | "icon" | undefined;
|
|
4
|
-
|
|
4
|
+
outline?: boolean | undefined;
|
|
5
|
+
variant?: "link" | "default" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "destructive" | "neutral" | "outline" | "ghost" | undefined;
|
|
6
|
+
shape?: "default" | "circle" | "square" | undefined;
|
|
5
7
|
} & {
|
|
6
8
|
class?: import("tailwind-variants").ClassValue;
|
|
7
9
|
className?: never;
|
|
8
10
|
}) | ({
|
|
9
11
|
size?: "lg" | "md" | "sm" | "xs" | "icon" | undefined;
|
|
10
|
-
|
|
12
|
+
outline?: boolean | undefined;
|
|
13
|
+
variant?: "link" | "default" | "info" | "warning" | "success" | "primary" | "secondary" | "accent" | "destructive" | "neutral" | "outline" | "ghost" | undefined;
|
|
14
|
+
shape?: "default" | "circle" | "square" | undefined;
|
|
11
15
|
} & {
|
|
12
16
|
class?: never;
|
|
13
17
|
className?: import("tailwind-variants").ClassValue;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
/** 입력 표면 look — Input/TextArea 가 공유하는 필드 셸. kind 로 한 줄 필드(field)/멀티라인(area)
|
|
1
|
+
/** 입력 표면 look — Input/TextArea/Select 가 공유하는 필드 셸. kind 로 한 줄 필드(field)/멀티라인(area), tone 으로 강조/오류 상태를 고른다. */
|
|
2
2
|
export declare const inputRecipe: (variants?: Omit<({
|
|
3
|
+
size?: "xl" | "lg" | "md" | "sm" | "xs" | undefined;
|
|
3
4
|
kind?: "field" | "area" | undefined;
|
|
5
|
+
tone?: "default" | "error" | "primary" | undefined;
|
|
4
6
|
} & {
|
|
5
7
|
class?: import("tailwind-variants").ClassValue;
|
|
6
8
|
className?: never;
|
|
7
9
|
}) | ({
|
|
10
|
+
size?: "xl" | "lg" | "md" | "sm" | "xs" | undefined;
|
|
8
11
|
kind?: "field" | "area" | undefined;
|
|
12
|
+
tone?: "default" | "error" | "primary" | undefined;
|
|
9
13
|
} & {
|
|
10
14
|
class?: never;
|
|
11
15
|
className?: import("tailwind-variants").ClassValue;
|
package/types/webkit/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { useCodepush } from "./useCodepush.d.ts";
|
|
|
8
8
|
export { useContact } from "./useContact.d.ts";
|
|
9
9
|
export { useCsrValues } from "./useCsrValues.d.ts";
|
|
10
10
|
export { useDebounce } from "./useDebounce.d.ts";
|
|
11
|
+
export { useEscapeKey } from "./useEscapeKey.d.ts";
|
|
11
12
|
export { useFetch, useFetchFn } from "./useFetch.d.ts";
|
|
12
13
|
export { useGeoLocation } from "./useGeoLocation.d.ts";
|
|
13
14
|
export { useHistory } from "./useHistory.d.ts";
|
package/types/webkit/lazy.d.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* `suspense` puts a Suspense boundary around the lazy component, so a chunk that resolves *after* the
|
|
4
|
+
* page is painted — a modal body, a dropdown menu — suspends only itself. Without one the suspension
|
|
5
|
+
* travels up to the nearest boundary, which is the route (`server/routeElementComposer.tsx`), and the
|
|
6
|
+
* whole page repaints as its loading fallback on the first open.
|
|
7
|
+
*
|
|
8
|
+
* It is opt-in rather than the default because a boundary also changes server rendering: under the
|
|
9
|
+
* default `renderMode: "stream"` the boundary's subtree leaves the shell as `loading` and arrives later
|
|
10
|
+
* in the stream. That is fine for an interaction shell and wrong for a page body, which SEO snapshots,
|
|
11
|
+
* prerendering and pre-hydration E2E read out of the shell.
|
|
12
|
+
*/
|
|
2
13
|
type LazyOption = {
|
|
3
14
|
ssr?: boolean;
|
|
15
|
+
suspense?: boolean;
|
|
4
16
|
loading?: () => ReactNode;
|
|
5
17
|
};
|
|
6
18
|
type LoadedOf<Loaded> = Loaded extends {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type NavigationIntent, type PathRoute, type RouteGuide, type RouterInstance, type TransitionType } from "akanjs/client";
|
|
1
|
+
import { type NavigationIntent, type PageTransition, type PathRoute, type RouteGuide, type RouterInstance, type TransitionType } from "akanjs/client";
|
|
2
2
|
export declare const useCsrValues: (rootRouteGuide: RouteGuide, pathRoutes: PathRoute[]) => {
|
|
3
|
+
page: PageTransition | null;
|
|
3
4
|
topSafeArea: import("akanjs/client").SafeAreaTransition | null;
|
|
4
|
-
|
|
5
|
-
prevPage: import("akanjs/client").PageTransition | null;
|
|
5
|
+
prevPage: PageTransition | null;
|
|
6
6
|
topInset: import("akanjs/client").ContainerTransition | null;
|
|
7
7
|
bottomInset: import("akanjs/client").ContainerTransition | null;
|
|
8
8
|
topLeftAction: import("akanjs/client").ContainerTransition | null;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Close `onEscape` on Escape while `active`, but only while this surface is the topmost active one.
|
|
3
|
+
* The callback is read through a ref, so a fresh closure per render does not reshuffle the stack.
|
|
4
|
+
*/
|
|
5
|
+
export declare const useEscapeKey: (active: boolean, onEscape: () => void) => void;
|
|
@@ -4,7 +4,10 @@ export type FrameSlotBucket = "active" | "pending";
|
|
|
4
4
|
export type FrameSlotMapByBucket = Record<FrameSlotBucket, FrameSlotMap>;
|
|
5
5
|
export declare const PENDING_FRAME_READY_TIMEOUT_MS = 80;
|
|
6
6
|
export declare const PENDING_FRAME_READY_MAX_TIMEOUT_MS = 120;
|
|
7
|
-
export declare const
|
|
7
|
+
export declare const KEYBOARD_SHOW_ANIMATION_DURATION_MS = 420;
|
|
8
|
+
export declare const KEYBOARD_HIDE_ANIMATION_DURATION_MS = 90;
|
|
9
|
+
export declare const KEYBOARD_SHOW_ANIMATION_EASING = "cubic-bezier(0.16, 1, 0.3, 1)";
|
|
10
|
+
export declare const KEYBOARD_HIDE_ANIMATION_EASING = "cubic-bezier(0.7, 0, 0.84, 0)";
|
|
8
11
|
export declare const FRAME_Z_INDEX: {
|
|
9
12
|
readonly page: 10;
|
|
10
13
|
readonly previousPage: 0;
|
|
@@ -39,6 +42,7 @@ export declare function useFrameViewport(): {
|
|
|
39
42
|
export declare function getKeyboardAccessorySlots(path: string, frameSlots: FrameSlotMap): FrameSlotRegistration[];
|
|
40
43
|
export declare function resolveKeyboardAccessoryHeight(path: string, frameSlots: FrameSlotMap): number;
|
|
41
44
|
export declare function hasKeyboardStickySlot(path: string, frameSlots: FrameSlotMap): boolean;
|
|
45
|
+
export declare function hasBottomAnchoredKeyboardSlot(path: string, frameSlots: FrameSlotMap): boolean;
|
|
42
46
|
export declare function resolveKeyboardFrame({ keyboardHeight, bottomSafeArea, visualViewportKeyboardHeight, platformProfile, sticky, freeze, }: {
|
|
43
47
|
keyboardHeight: number;
|
|
44
48
|
bottomSafeArea: number;
|
|
@@ -57,6 +61,7 @@ export declare function useKeyboardFrame({ bottomSafeArea, sticky, viewport, pla
|
|
|
57
61
|
bottomSafeArea: number;
|
|
58
62
|
sticky: boolean;
|
|
59
63
|
viewport: {
|
|
64
|
+
height: number;
|
|
60
65
|
visualHeight: number;
|
|
61
66
|
visualOffsetTop: number;
|
|
62
67
|
};
|
package/ui/Badge.tsx
CHANGED
|
@@ -7,10 +7,10 @@ export { badgeRecipe, type BadgeVariants };
|
|
|
7
7
|
|
|
8
8
|
export type BadgeProps = HTMLAttributes<HTMLSpanElement> & BadgeVariants;
|
|
9
9
|
|
|
10
|
-
const DefaultBadge = ({ className, variant, ...rest }: BadgeProps) => {
|
|
10
|
+
const DefaultBadge = ({ className, variant, size, outline, ...rest }: BadgeProps) => {
|
|
11
11
|
|
|
12
12
|
const recipe = useUiRecipe("badge") ?? badgeRecipe;
|
|
13
|
-
return <span className={recipe({ variant }, className)} {...rest} />;
|
|
13
|
+
return <span className={recipe({ variant, size, outline }, className)} {...rest} />;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
/** Status/label pill. Route-overridable via `page/**\/_overrides.tsx` (slot `Badge`). */
|
package/ui/BottomSheet.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { useDrag } from "@use-gesture/react";
|
|
|
3
3
|
import { cn } from "akanjs/client";
|
|
4
4
|
import { st } from "akanjs/store";
|
|
5
5
|
import { animated } from "akanjs/ui";
|
|
6
|
+
import { useEscapeKey } from "akanjs/webkit";
|
|
6
7
|
import { forwardRef, type ReactNode, useEffect, useImperativeHandle, useRef } from "react";
|
|
7
8
|
import { BiX } from "react-icons/bi";
|
|
8
9
|
import { config, useSpring } from "react-spring";
|
|
@@ -54,6 +55,10 @@ export const BottomSheet = forwardRef<BottomSheetRef, BottomSheetProps>(
|
|
|
54
55
|
close: closeModal,
|
|
55
56
|
}));
|
|
56
57
|
|
|
58
|
+
useEscapeKey(open, () => {
|
|
59
|
+
void closeModal();
|
|
60
|
+
});
|
|
61
|
+
|
|
57
62
|
useEffect(() => {
|
|
58
63
|
if (open) void openModal();
|
|
59
64
|
else void closeModal();
|
package/ui/Button.tsx
CHANGED
|
@@ -61,6 +61,8 @@ const DefaultButton = <Result = unknown>({
|
|
|
61
61
|
className,
|
|
62
62
|
variant,
|
|
63
63
|
size,
|
|
64
|
+
shape,
|
|
65
|
+
outline,
|
|
64
66
|
type = "button",
|
|
65
67
|
loadingMode = "hold",
|
|
66
68
|
showError = true,
|
|
@@ -84,7 +86,7 @@ const DefaultButton = <Result = unknown>({
|
|
|
84
86
|
<button
|
|
85
87
|
type={type}
|
|
86
88
|
className={recipe(
|
|
87
|
-
{ variant, size },
|
|
89
|
+
{ variant, size, shape, outline },
|
|
88
90
|
|
|
89
91
|
cn(loadingMode === "hold" && "relative", busy && !rest.disabled && "disabled:opacity-100", className),
|
|
90
92
|
)}
|
package/ui/Constant/Doc.tsx
CHANGED
|
@@ -238,7 +238,7 @@ const EnumList = ({ enums = getConstantSchemaDoc().enums }: EnumProps) => {
|
|
|
238
238
|
const { l } = usePage();
|
|
239
239
|
return (
|
|
240
240
|
<div className="overflow-x-auto rounded-xl bg-muted p-3">
|
|
241
|
-
<table className=
|
|
241
|
+
<table className={tableClass}>
|
|
242
242
|
<thead>
|
|
243
243
|
<tr>
|
|
244
244
|
<th>Key</th>
|
|
@@ -268,7 +268,7 @@ export default function ListContainer<
|
|
|
268
268
|
<div className={cn("m-4", className)}>
|
|
269
269
|
<div className="mb-3 flex flex-wrap justify-between">
|
|
270
270
|
<div className="flex pb-1">
|
|
271
|
-
<p className="
|
|
271
|
+
<p className="text-lg">
|
|
272
272
|
{title ?? l._(`${sliceName}.modelName`)}({modelInsight.count})
|
|
273
273
|
</p>
|
|
274
274
|
<div className="ml-3 flex items-center">
|
package/ui/DatePicker.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { lazy } from "akanjs/webkit";
|
|
|
5
5
|
import { type FocusEvent, useEffect, useRef } from "react";
|
|
6
6
|
import { AiOutlineSwapRight } from "react-icons/ai";
|
|
7
7
|
|
|
8
|
+
import { inputRecipe } from "./recipe";
|
|
8
9
|
import { createOverridable } from "./UiOverride";
|
|
9
10
|
|
|
10
11
|
const reactDatePickerPackage = "react-datepicker";
|
|
@@ -58,7 +59,7 @@ const DefaultDatePicker = ({
|
|
|
58
59
|
|
|
59
60
|
return (
|
|
60
61
|
<ReactDatePicker
|
|
61
|
-
className={
|
|
62
|
+
className={inputRecipe({}, ["text-center", className])}
|
|
62
63
|
selected={value ? value.toDate() : new Date()}
|
|
63
64
|
disabledKeyboardNavigation
|
|
64
65
|
onFocus={(e: FocusEvent<HTMLInputElement>) => {
|
|
@@ -110,9 +111,9 @@ const DefaultRangePicker = ({
|
|
|
110
111
|
onChange([value[0] ?? dayjs(), dayjs(date)]);
|
|
111
112
|
};
|
|
112
113
|
|
|
113
|
-
const pickerClassName = "m-0
|
|
114
|
+
const pickerClassName = "m-0 h-full w-full border-none bg-transparent p-3 text-center focus:outline-hidden z-50";
|
|
114
115
|
return (
|
|
115
|
-
<div className={
|
|
116
|
+
<div className={inputRecipe({}, ["flex h-full w-fit items-center gap-2 p-0", className])}>
|
|
116
117
|
<ReactDatePicker
|
|
117
118
|
className={pickerClassName}
|
|
118
119
|
selected={value[0] ? value[0].toDate() : undefined}
|
package/ui/Dialog/Modal.tsx
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import { useDrag } from "@use-gesture/react";
|
|
3
3
|
import { cn, usePage } from "akanjs/client";
|
|
4
4
|
import { animated } from "akanjs/ui";
|
|
5
|
+
import { useEscapeKey } from "akanjs/webkit";
|
|
5
6
|
import { type ReactNode, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
|
|
6
7
|
import { createPortal } from "react-dom";
|
|
7
8
|
import { BiX } from "react-icons/bi";
|
|
8
9
|
import { config, useSpring } from "react-spring";
|
|
9
10
|
import { buttonRecipe } from "../Button";
|
|
11
|
+
import { useOverlayLayerProps } from "../overlayLayer";
|
|
10
12
|
|
|
11
13
|
import { DialogContext } from "./context";
|
|
12
14
|
|
|
@@ -35,6 +37,8 @@ export const Modal = ({ className, bodyClassName, confirmClose, children, onCanc
|
|
|
35
37
|
const focusedElementRef = useRef<HTMLElement | null>(null);
|
|
36
38
|
const titleId = useId();
|
|
37
39
|
const contentId = useId();
|
|
40
|
+
|
|
41
|
+
const overlayLayerProps = useOverlayLayerProps();
|
|
38
42
|
const [{ translate }, api] = useSpring(() => ({ translate: 1 }));
|
|
39
43
|
const [portalElement, setPortalElement] = useState<HTMLElement | null>(null);
|
|
40
44
|
const [isMounted, setIsMounted] = useState(open);
|
|
@@ -160,20 +164,7 @@ export const Modal = ({ className, bodyClassName, confirmClose, children, onCanc
|
|
|
160
164
|
};
|
|
161
165
|
}, [isMounted, portalElement]);
|
|
162
166
|
|
|
163
|
-
|
|
164
|
-
if (!isMounted) return;
|
|
165
|
-
|
|
166
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
167
|
-
if (event.key !== "Escape") return;
|
|
168
|
-
event.preventDefault();
|
|
169
|
-
requestClose();
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
window.addEventListener("keydown", onKeyDown);
|
|
173
|
-
return () => {
|
|
174
|
-
window.removeEventListener("keydown", onKeyDown);
|
|
175
|
-
};
|
|
176
|
-
}, [isMounted, requestClose]);
|
|
167
|
+
useEscapeKey(isMounted, requestClose);
|
|
177
168
|
|
|
178
169
|
useEffect(() => {
|
|
179
170
|
return () => {
|
|
@@ -186,17 +177,23 @@ export const Modal = ({ className, bodyClassName, confirmClose, children, onCanc
|
|
|
186
177
|
return createPortal(
|
|
187
178
|
<>
|
|
188
179
|
<div
|
|
180
|
+
{...overlayLayerProps}
|
|
189
181
|
className={cn("fixed inset-0 z-10", showBackground && "animate-fadeIn bg-black/50 backdrop-blur-md")}
|
|
190
182
|
onClick={(event) => {
|
|
191
183
|
if (event.target !== event.currentTarget) return;
|
|
192
184
|
requestClose();
|
|
193
185
|
}}
|
|
194
186
|
/>
|
|
195
|
-
<div
|
|
187
|
+
<div
|
|
188
|
+
{...overlayLayerProps}
|
|
189
|
+
className="fixed top-1/2 left-1/2 z-10 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center"
|
|
190
|
+
>
|
|
196
191
|
<div className="z-10">
|
|
197
192
|
<animated.div
|
|
198
193
|
ref={ref}
|
|
199
194
|
style={{ translateY, opacity }}
|
|
195
|
+
|
|
196
|
+
className="outline-none"
|
|
200
197
|
role="dialog"
|
|
201
198
|
aria-modal="true"
|
|
202
199
|
aria-labelledby={title ? titleId : undefined}
|