akanjs 2.3.11-rc.8 → 2.3.11
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/CHANGELOG.md +21 -0
- package/client/csrTypes.ts +2 -0
- package/common/routeConvention.ts +8 -5
- package/package.json +1 -1
- package/server/routeTreeBuilder.ts +37 -4
- package/types/client/csrTypes.d.ts +2 -0
- package/types/common/routeConvention.d.ts +1 -1
- package/types/ui/Button.d.ts +6 -1
- package/types/ui/DatePicker.d.ts +12 -9
- package/types/ui/Dropdown.d.ts +6 -1
- package/types/ui/Empty.d.ts +7 -1
- package/types/ui/Input.d.ts +12 -8
- package/types/ui/Loading/index.d.ts +11 -6
- package/types/ui/Menu.d.ts +9 -4
- package/types/ui/Modal.d.ts +11 -1
- package/types/ui/Pagination.d.ts +6 -1
- package/types/ui/Popconfirm.d.ts +7 -1
- package/types/ui/Radio.d.ts +7 -4
- package/types/ui/Select.d.ts +6 -1
- package/types/ui/Table.d.ts +6 -1
- package/types/ui/ToggleSelect.d.ts +11 -7
- package/types/ui/UiOverride/Provider.d.ts +14 -0
- package/types/ui/UiOverride/context.d.ts +72 -0
- package/types/ui/UiOverride/createOverridable.d.ts +9 -0
- package/types/ui/UiOverride/index.d.ts +5 -0
- package/types/ui/UiOverride/override.d.ts +18 -0
- package/types/ui/UiOverride/useUiOverride.d.ts +7 -0
- package/types/ui/UiOverride.d.ts +1 -0
- package/types/ui/Unauthorized.d.ts +8 -3
- package/types/ui/index.d.ts +1 -0
- package/ui/Button.tsx +15 -2
- package/ui/DatePicker.tsx +19 -9
- package/ui/Dropdown.tsx +9 -1
- package/ui/Empty.tsx +11 -1
- package/ui/Input.tsx +22 -14
- package/ui/Loading/index.tsx +15 -1
- package/ui/Menu.tsx +12 -3
- package/ui/Modal.tsx +13 -1
- package/ui/Pagination.tsx +9 -1
- package/ui/Popconfirm.tsx +9 -1
- package/ui/Radio.tsx +14 -3
- package/ui/Select.tsx +22 -2
- package/ui/Table.tsx +8 -1
- package/ui/ToggleSelect.tsx +31 -5
- package/ui/UiOverride/Provider.tsx +22 -0
- package/ui/UiOverride/context.ts +85 -0
- package/ui/UiOverride/createOverridable.tsx +25 -0
- package/ui/UiOverride/index.ts +5 -0
- package/ui/UiOverride/override.ts +19 -0
- package/ui/UiOverride/useUiOverride.ts +15 -0
- package/ui/Unauthorized.tsx +12 -3
- package/ui/index.ts +10 -0
- package/webkit/bootCsr.tsx +28 -5
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { type ComponentType, createContext } from "react";
|
|
3
|
+
|
|
4
|
+
import type { ButtonProps } from "../Button";
|
|
5
|
+
import type { DatePickerProps, RangePickerProps, TimePickerProps } from "../DatePicker";
|
|
6
|
+
import type { DropdownProps } from "../Dropdown";
|
|
7
|
+
import type { EmptyProps } from "../Empty";
|
|
8
|
+
import type { CheckboxProps, EmailProps, InputProps, NumberProps, PasswordProps, TextAreaProps } from "../Input";
|
|
9
|
+
import type { LoadingProps as LoadingButtonProps } from "../Loading/Button";
|
|
10
|
+
import type { LoadingProps as LoadingInputProps } from "../Loading/Input";
|
|
11
|
+
import type { ProgressBarProps } from "../Loading/ProgressBar";
|
|
12
|
+
import type { SkeletonProps } from "../Loading/Skeleton";
|
|
13
|
+
import type { SpinProps } from "../Loading/Spin";
|
|
14
|
+
import type { MenuProps } from "../Menu";
|
|
15
|
+
import type { ModalProps } from "../Modal";
|
|
16
|
+
import type { PaginationProps } from "../Pagination";
|
|
17
|
+
import type { PopconfirmProps } from "../Popconfirm";
|
|
18
|
+
import type { ItemProps as RadioItemProps, RadioProps } from "../Radio";
|
|
19
|
+
import type { SelectProps } from "../Select";
|
|
20
|
+
import type { TableProps } from "../Table";
|
|
21
|
+
import type { MultiProps as ToggleSelectMultiProps, ToggleSelectProps } from "../ToggleSelect";
|
|
22
|
+
import type { UnauthorizedProps } from "../Unauthorized";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Registry of framework UI components an app may replace per-route through a
|
|
26
|
+
* `page/**\/_overrides.tsx` manifest. Each entry is keyed by the framework
|
|
27
|
+
* component name and typed to that component's public prop contract, so an
|
|
28
|
+
* override is checked as a drop-in replacement.
|
|
29
|
+
*
|
|
30
|
+
* Extend this interface (and add a matching `createOverridable(...)` call in the
|
|
31
|
+
* component file) to make another component overridable. Generic components
|
|
32
|
+
* (e.g. `Button`, `Select`) and compound components that carry static
|
|
33
|
+
* sub-components (e.g. `Tab`, `Field`, `Input`) need dedicated slot support
|
|
34
|
+
* before they can be listed here without losing call-site typing.
|
|
35
|
+
*/
|
|
36
|
+
export interface AkanUiOverrides {
|
|
37
|
+
|
|
38
|
+
Modal: ComponentType<ModalProps>;
|
|
39
|
+
Empty: ComponentType<EmptyProps>;
|
|
40
|
+
Pagination: ComponentType<PaginationProps>;
|
|
41
|
+
Popconfirm: ComponentType<PopconfirmProps>;
|
|
42
|
+
Dropdown: ComponentType<DropdownProps>;
|
|
43
|
+
Table: ComponentType<TableProps>;
|
|
44
|
+
Menu: ComponentType<MenuProps>;
|
|
45
|
+
Unauthorized: ComponentType<UnauthorizedProps>;
|
|
46
|
+
|
|
47
|
+
Button: ComponentType<ButtonProps<unknown>>;
|
|
48
|
+
Select: ComponentType<SelectProps<string | number | boolean | null | undefined>>;
|
|
49
|
+
|
|
50
|
+
Input: ComponentType<InputProps>;
|
|
51
|
+
InputTextArea: ComponentType<TextAreaProps>;
|
|
52
|
+
InputPassword: ComponentType<PasswordProps>;
|
|
53
|
+
InputEmail: ComponentType<EmailProps>;
|
|
54
|
+
InputNumber: ComponentType<NumberProps>;
|
|
55
|
+
InputCheckbox: ComponentType<CheckboxProps>;
|
|
56
|
+
|
|
57
|
+
Radio: ComponentType<RadioProps>;
|
|
58
|
+
RadioItem: ComponentType<RadioItemProps>;
|
|
59
|
+
|
|
60
|
+
DatePicker: ComponentType<DatePickerProps>;
|
|
61
|
+
DatePickerRangePicker: ComponentType<RangePickerProps>;
|
|
62
|
+
DatePickerTimePicker: ComponentType<TimePickerProps>;
|
|
63
|
+
|
|
64
|
+
ToggleSelect: ComponentType<ToggleSelectProps<string | number | boolean | null>>;
|
|
65
|
+
ToggleSelectMulti: ComponentType<ToggleSelectMultiProps>;
|
|
66
|
+
|
|
67
|
+
LoadingSpin: ComponentType<SpinProps>;
|
|
68
|
+
LoadingSkeleton: ComponentType<SkeletonProps>;
|
|
69
|
+
LoadingProgressBar: ComponentType<ProgressBarProps>;
|
|
70
|
+
LoadingButton: ComponentType<LoadingButtonProps>;
|
|
71
|
+
LoadingInput: ComponentType<LoadingInputProps>;
|
|
72
|
+
LoadingArea: ComponentType<Record<string, never>>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type AkanUiOverrideName = keyof AkanUiOverrides;
|
|
76
|
+
|
|
77
|
+
/** Prop contract an app-authored Modal override must satisfy. */
|
|
78
|
+
export type AkanModalComponent = AkanUiOverrides["Modal"];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Holds the override map for the current route subtree. Empty at the root, then
|
|
82
|
+
* merged (child wins) by each nested `UiOverrideProvider`, mirroring how nested
|
|
83
|
+
* `_layout.tsx` / `_overrides.tsx` stack down the route tree.
|
|
84
|
+
*/
|
|
85
|
+
export const UiOverrideContext = createContext<Partial<AkanUiOverrides>>({});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { type ComponentType, createElement, type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import type { AkanUiOverrides } from "./context";
|
|
5
|
+
import { useUiOverride } from "./useUiOverride";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Wraps a framework component so a matching `_overrides.tsx` entry replaces it,
|
|
9
|
+
* scoped to that route subtree. Falls back to `Default` when no override is
|
|
10
|
+
* active, so every existing call site keeps working untouched. The proxy erases
|
|
11
|
+
* its prop type internally and re-asserts the slot's public component type on
|
|
12
|
+
* the way out, so call sites stay fully typed against the slot contract.
|
|
13
|
+
*/
|
|
14
|
+
export const createOverridable = <K extends keyof AkanUiOverrides>(
|
|
15
|
+
name: K,
|
|
16
|
+
Default: AkanUiOverrides[K],
|
|
17
|
+
): AkanUiOverrides[K] => {
|
|
18
|
+
|
|
19
|
+
const Fallback = Default as unknown as ComponentType<Record<string, unknown>>;
|
|
20
|
+
const Overridable = (props: Record<string, unknown>): ReactNode => {
|
|
21
|
+
const Override = useUiOverride(name) as unknown as ComponentType<Record<string, unknown>> | undefined;
|
|
22
|
+
return createElement(Override ?? Fallback, props);
|
|
23
|
+
};
|
|
24
|
+
return Overridable as unknown as AkanUiOverrides[K];
|
|
25
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { AkanModalComponent, AkanUiOverrideName, AkanUiOverrides } from "./context";
|
|
2
|
+
export { createOverridable } from "./createOverridable";
|
|
3
|
+
export { override } from "./override";
|
|
4
|
+
export { UiOverrideProvider, type UiOverrideProviderProps } from "./Provider";
|
|
5
|
+
export { useUiOverride } from "./useUiOverride";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AkanUiOverrides } from "./context";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Typed manifest builder for a `page/_overrides.tsx` file:
|
|
5
|
+
*
|
|
6
|
+
* ```tsx
|
|
7
|
+
* export default override({ Modal: BrandModal });
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* It type-checks each binding against that slot's public component contract and rejects unknown slot names as
|
|
11
|
+
* excess properties — so the manifest is validated at compile time and app components need no annotation of
|
|
12
|
+
* their own. Keys are the framework component names (`Modal`), matching `useUiOverride` and the `<Modal>` call
|
|
13
|
+
* sites.
|
|
14
|
+
*
|
|
15
|
+
* It is a pure, server-safe identity function (no React, no `createContext`), so the manifest needs no
|
|
16
|
+
* `"use client"` directive. The framework generates a `"use client"` wrapper layout that reads this map and
|
|
17
|
+
* mounts the override provider around the route subtree (see `writeGeneratedOverridesLayoutFile`).
|
|
18
|
+
*/
|
|
19
|
+
export const override = (overrides: Partial<AkanUiOverrides>): Partial<AkanUiOverrides> => overrides;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
|
|
4
|
+
import { type AkanUiOverrides, UiOverrideContext } from "./context";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the active override for a framework component in the current route
|
|
8
|
+
* subtree, or `undefined` when no `_overrides.tsx` in the route's ancestry
|
|
9
|
+
* declares one.
|
|
10
|
+
*/
|
|
11
|
+
export const useUiOverride = <K extends keyof AkanUiOverrides>(name: K): AkanUiOverrides[K] | undefined => {
|
|
12
|
+
|
|
13
|
+
const overrides = useContext(UiOverrideContext) as Record<string, unknown>;
|
|
14
|
+
return overrides[name] as AkanUiOverrides[K] | undefined;
|
|
15
|
+
};
|
package/ui/Unauthorized.tsx
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
import { clsx, usePage } from "akanjs/client";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
import { AiOutlineBlock } from "react-icons/ai";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import { createOverridable } from "./UiOverride";
|
|
7
|
+
|
|
8
|
+
export interface UnauthorizedProps {
|
|
7
9
|
className?: string;
|
|
8
10
|
description?: ReactNode;
|
|
9
11
|
children?: ReactNode;
|
|
10
12
|
minHeight?: number;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export const
|
|
15
|
+
export const DefaultUnauthorized = ({ className = "", description, children, minHeight = 300 }: UnauthorizedProps) => {
|
|
14
16
|
const { l } = usePage();
|
|
15
17
|
return (
|
|
16
18
|
<div>
|
|
@@ -27,3 +29,10 @@ export const Unauthorized = ({ className = "", description, children, minHeight
|
|
|
27
29
|
</div>
|
|
28
30
|
);
|
|
29
31
|
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Unauthorized-state placeholder. Resolves to a route-scoped override when a
|
|
35
|
+
* `page/**\/_overrides.tsx` in the route's ancestry declares one, otherwise
|
|
36
|
+
* renders {@link DefaultUnauthorized}.
|
|
37
|
+
*/
|
|
38
|
+
export const Unauthorized = createOverridable("Unauthorized", DefaultUnauthorized);
|
package/ui/index.ts
CHANGED
|
@@ -41,4 +41,14 @@ export { System, type WebAppManifest } from "./System";
|
|
|
41
41
|
export { Tab } from "./Tab";
|
|
42
42
|
export { Table } from "./Table";
|
|
43
43
|
export { ToggleSelect } from "./ToggleSelect";
|
|
44
|
+
export {
|
|
45
|
+
type AkanModalComponent,
|
|
46
|
+
type AkanUiOverrideName,
|
|
47
|
+
type AkanUiOverrides,
|
|
48
|
+
createOverridable,
|
|
49
|
+
override,
|
|
50
|
+
UiOverrideProvider,
|
|
51
|
+
type UiOverrideProviderProps,
|
|
52
|
+
useUiOverride,
|
|
53
|
+
} from "./UiOverride";
|
|
44
54
|
export { Unauthorized } from "./Unauthorized";
|
package/webkit/bootCsr.tsx
CHANGED
|
@@ -7,12 +7,12 @@ import {
|
|
|
7
7
|
type LayoutModule,
|
|
8
8
|
type PageConfig,
|
|
9
9
|
type PathRoute,
|
|
10
|
-
readCssSafeAreaInsets,
|
|
11
|
-
resolvePageState,
|
|
12
10
|
type Route,
|
|
13
11
|
type RouteGuide,
|
|
14
12
|
type RouteModule,
|
|
15
13
|
type RouteRender,
|
|
14
|
+
readCssSafeAreaInsets,
|
|
15
|
+
resolvePageState,
|
|
16
16
|
storage,
|
|
17
17
|
validatePageConfig,
|
|
18
18
|
} from "akanjs/client";
|
|
@@ -113,7 +113,8 @@ export const bootCsr = async (context: Record<string, CsrRouteModuleEntry>) => {
|
|
|
113
113
|
validateRouteModuleExports(key, pageContent);
|
|
114
114
|
validatePageConfig(key, (pageContent as RouteModuleWithConfig).pageConfig);
|
|
115
115
|
asyncDefaultMap[key] = entry.isAsyncDefault;
|
|
116
|
-
|
|
116
|
+
|
|
117
|
+
if (pageContent.default || parsed.kind === "overrides") pages[key] = pageContent;
|
|
117
118
|
}),
|
|
118
119
|
);
|
|
119
120
|
const cssSafeArea = readCssSafeAreaInsets();
|
|
@@ -138,6 +139,18 @@ export const bootCsr = async (context: Record<string, CsrRouteModuleEntry>) => {
|
|
|
138
139
|
if (!targetPath) continue;
|
|
139
140
|
const page = pages[filePath];
|
|
140
141
|
if (!page) continue;
|
|
142
|
+
if (parsed.kind === "overrides") {
|
|
143
|
+
|
|
144
|
+
const overridesRender: RouteRender = {
|
|
145
|
+
render: page.default as never,
|
|
146
|
+
isAsync: asyncDefaultMap[filePath] || page.default?.constructor.name === "AsyncFunction",
|
|
147
|
+
};
|
|
148
|
+
targetRouteMap.set(targetPath, {
|
|
149
|
+
...(targetRouteMap.get(targetPath) ?? { path: targetPath, children: new Map<string, Route>() }),
|
|
150
|
+
renderOverrides: overridesRender,
|
|
151
|
+
} as Route);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
141
154
|
const layoutPage = parsed.kind === "layout" ? (page as LayoutModule) : null;
|
|
142
155
|
const routeRender: RouteRender = {
|
|
143
156
|
render: page.default as never,
|
|
@@ -179,15 +192,20 @@ export const bootCsr = async (context: Record<string, CsrRouteModuleEntry>) => {
|
|
|
179
192
|
const currentRootLayout = isRoot && route.renderLayout ? route.renderLayout : null;
|
|
180
193
|
const currentLayout = !isRoot && route.renderLayout ? route.renderLayout : null;
|
|
181
194
|
const currentLayoutConfig = route.renderLayout && route.layoutPageConfig ? route.layoutPageConfig : null;
|
|
195
|
+
|
|
196
|
+
const currentOverrideRenders = route.renderOverrides ? [route.renderOverrides] : [];
|
|
182
197
|
const renderRootLayouts = [...parentRootLayouts, ...(currentRootLayout ? [currentRootLayout] : [])];
|
|
183
|
-
const renderLayouts = [...parentLayouts, ...(currentLayout ? [currentLayout] : [])];
|
|
198
|
+
const renderLayouts = [...parentLayouts, ...currentOverrideRenders, ...(currentLayout ? [currentLayout] : [])];
|
|
184
199
|
const pageConfigChain = [
|
|
185
200
|
...parentPageConfigChain,
|
|
186
201
|
...(currentRootLayout || currentLayout ? (currentLayoutConfig ? [currentLayoutConfig] : []) : []),
|
|
187
202
|
];
|
|
188
203
|
const pageRenderRootLayouts =
|
|
189
204
|
route.pageIncludesOwnLayout === false && currentRootLayout ? parentRootLayouts : renderRootLayouts;
|
|
190
|
-
const pageRenderLayouts =
|
|
205
|
+
const pageRenderLayouts =
|
|
206
|
+
route.pageIncludesOwnLayout === false && currentLayout
|
|
207
|
+
? [...parentLayouts, ...currentOverrideRenders]
|
|
208
|
+
: renderLayouts;
|
|
191
209
|
const pageRenderConfigChain =
|
|
192
210
|
route.pageIncludesOwnLayout === false && (currentRootLayout || currentLayout)
|
|
193
211
|
? parentPageConfigChain
|
|
@@ -280,6 +298,11 @@ function initializeMobileTargetFromSearch() {
|
|
|
280
298
|
|
|
281
299
|
function validateRouteModuleExports(key: string, mod: RouteModule) {
|
|
282
300
|
const parsed = parseRouteModuleKey(key);
|
|
301
|
+
if (parsed.kind === "overrides") {
|
|
302
|
+
|
|
303
|
+
if (!mod.default) throw new Error(`[route-convention] ${key} generated override wrapper has no default export`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
283
306
|
const allowed =
|
|
284
307
|
parsed.kind === "page"
|
|
285
308
|
? new Set(["default", "pageConfig", "head", "metadata", "generateHead", "generateMetadata", "Loading"])
|