akanjs 2.3.11-rc.9 → 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.
Files changed (53) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/client/csrTypes.ts +2 -0
  3. package/common/routeConvention.ts +8 -5
  4. package/package.json +1 -1
  5. package/server/routeTreeBuilder.ts +37 -4
  6. package/types/client/csrTypes.d.ts +2 -0
  7. package/types/common/routeConvention.d.ts +1 -1
  8. package/types/ui/Button.d.ts +6 -1
  9. package/types/ui/DatePicker.d.ts +12 -9
  10. package/types/ui/Dropdown.d.ts +6 -1
  11. package/types/ui/Empty.d.ts +7 -1
  12. package/types/ui/Input.d.ts +12 -8
  13. package/types/ui/Loading/index.d.ts +11 -6
  14. package/types/ui/Menu.d.ts +9 -4
  15. package/types/ui/Modal.d.ts +11 -1
  16. package/types/ui/Pagination.d.ts +6 -1
  17. package/types/ui/Popconfirm.d.ts +7 -1
  18. package/types/ui/Radio.d.ts +7 -4
  19. package/types/ui/Select.d.ts +6 -1
  20. package/types/ui/Table.d.ts +6 -1
  21. package/types/ui/ToggleSelect.d.ts +11 -7
  22. package/types/ui/UiOverride/Provider.d.ts +14 -0
  23. package/types/ui/UiOverride/context.d.ts +72 -0
  24. package/types/ui/UiOverride/createOverridable.d.ts +9 -0
  25. package/types/ui/UiOverride/index.d.ts +5 -0
  26. package/types/ui/UiOverride/override.d.ts +18 -0
  27. package/types/ui/UiOverride/useUiOverride.d.ts +7 -0
  28. package/types/ui/UiOverride.d.ts +1 -0
  29. package/types/ui/Unauthorized.d.ts +8 -3
  30. package/types/ui/index.d.ts +1 -0
  31. package/ui/Button.tsx +15 -2
  32. package/ui/DatePicker.tsx +19 -9
  33. package/ui/Dropdown.tsx +9 -1
  34. package/ui/Empty.tsx +11 -1
  35. package/ui/Input.tsx +22 -14
  36. package/ui/Loading/index.tsx +15 -1
  37. package/ui/Menu.tsx +12 -3
  38. package/ui/Modal.tsx +13 -1
  39. package/ui/Pagination.tsx +9 -1
  40. package/ui/Popconfirm.tsx +9 -1
  41. package/ui/Radio.tsx +14 -3
  42. package/ui/Select.tsx +22 -2
  43. package/ui/Table.tsx +8 -1
  44. package/ui/ToggleSelect.tsx +31 -5
  45. package/ui/UiOverride/Provider.tsx +22 -0
  46. package/ui/UiOverride/context.ts +85 -0
  47. package/ui/UiOverride/createOverridable.tsx +25 -0
  48. package/ui/UiOverride/index.ts +5 -0
  49. package/ui/UiOverride/override.ts +19 -0
  50. package/ui/UiOverride/useUiOverride.ts +15 -0
  51. package/ui/Unauthorized.tsx +12 -3
  52. package/ui/index.ts +10 -0
  53. 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
+ };
@@ -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
- interface UnauthorizedProps {
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 Unauthorized = ({ className = "", description, children, minHeight = 300 }: UnauthorizedProps) => {
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";
@@ -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
- if (pageContent.default) pages[key] = pageContent;
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 = route.pageIncludesOwnLayout === false && currentLayout ? parentLayouts : renderLayouts;
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"])