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
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
interface UnauthorizedProps {
|
|
2
|
+
export interface UnauthorizedProps {
|
|
3
3
|
className?: string;
|
|
4
4
|
description?: ReactNode;
|
|
5
5
|
children?: ReactNode;
|
|
6
6
|
minHeight?: number;
|
|
7
7
|
}
|
|
8
|
-
export declare const
|
|
9
|
-
|
|
8
|
+
export declare const DefaultUnauthorized: ({ className, description, children, minHeight }: UnauthorizedProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
/**
|
|
10
|
+
* Unauthorized-state placeholder. Resolves to a route-scoped override when a
|
|
11
|
+
* `page/**\/_overrides.tsx` in the route's ancestry declares one, otherwise
|
|
12
|
+
* renders {@link DefaultUnauthorized}.
|
|
13
|
+
*/
|
|
14
|
+
export declare const Unauthorized: import("react").ComponentType<UnauthorizedProps>;
|
package/types/ui/index.d.ts
CHANGED
|
@@ -41,4 +41,5 @@ export { System, type WebAppManifest } from "./System.d.ts";
|
|
|
41
41
|
export { Tab } from "./Tab.d.ts";
|
|
42
42
|
export { Table } from "./Table.d.ts";
|
|
43
43
|
export { ToggleSelect } from "./ToggleSelect.d.ts";
|
|
44
|
+
export { type AkanModalComponent, type AkanUiOverrideName, type AkanUiOverrides, createOverridable, override, UiOverrideProvider, type UiOverrideProviderProps, useUiOverride, } from "./UiOverride.d.ts";
|
|
44
45
|
export { Unauthorized } from "./Unauthorized.d.ts";
|
package/ui/Button.tsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { clsx, usePage } from "akanjs/client";
|
|
3
3
|
import type React from "react";
|
|
4
|
-
import { type ButtonHTMLAttributes, useState } from "react";
|
|
4
|
+
import { type ButtonHTMLAttributes, type ComponentType, createElement, useState } from "react";
|
|
5
5
|
import { AiOutlineCheckCircle } from "react-icons/ai";
|
|
6
6
|
|
|
7
|
+
import { useUiOverride } from "./UiOverride";
|
|
8
|
+
|
|
7
9
|
export type ButtonProps<Result> = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> & {
|
|
8
10
|
/** Async-aware click handler. Call onError to show the localized error state without throwing. */
|
|
9
11
|
onClick: (
|
|
@@ -14,7 +16,7 @@ export type ButtonProps<Result> = Omit<ButtonHTMLAttributes<HTMLButtonElement>,
|
|
|
14
16
|
onSuccess?: (result: Result) => void;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
const DefaultButton = <Result = unknown>({ className, children, onClick, onSuccess, ...rest }: ButtonProps<Result>) => {
|
|
18
20
|
const { l } = usePage();
|
|
19
21
|
const [state, setState] = useState<{
|
|
20
22
|
mode: "idle" | "loading" | "success" | "error";
|
|
@@ -68,3 +70,14 @@ export const Button = <Result = unknown>({ className, children, onClick, onSucce
|
|
|
68
70
|
</>
|
|
69
71
|
);
|
|
70
72
|
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Async-aware button. Resolves to a route-scoped override when a `page/**\/_overrides.tsx`
|
|
76
|
+
* in the route's ancestry declares one, otherwise renders {@link DefaultButton}. The public
|
|
77
|
+
* generic signature is preserved, so `<Button<Todo> onSuccess={(r: Todo) => …} />` still infers.
|
|
78
|
+
*/
|
|
79
|
+
export const Button = <Result = unknown>(props: ButtonProps<Result>) => {
|
|
80
|
+
const Override = useUiOverride("Button");
|
|
81
|
+
const Impl = (Override ?? DefaultButton) as unknown as ComponentType<ButtonProps<Result>>;
|
|
82
|
+
return createElement(Impl, props);
|
|
83
|
+
};
|
package/ui/DatePicker.tsx
CHANGED
|
@@ -5,6 +5,8 @@ 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 { createOverridable } from "./UiOverride";
|
|
9
|
+
|
|
8
10
|
const reactDatePickerPackage = "react-datepicker";
|
|
9
11
|
const reactDatePickerStylePath = "react-datepicker/dist/react-datepicker.css";
|
|
10
12
|
|
|
@@ -15,7 +17,7 @@ const ReactDatePicker = lazy(
|
|
|
15
17
|
},
|
|
16
18
|
{ ssr: false },
|
|
17
19
|
);
|
|
18
|
-
interface DatePickerProps {
|
|
20
|
+
export interface DatePickerProps {
|
|
19
21
|
value?: Dayjs | null;
|
|
20
22
|
onChange: (value: Dayjs | null) => void;
|
|
21
23
|
showTime?: boolean;
|
|
@@ -27,7 +29,7 @@ interface DatePickerProps {
|
|
|
27
29
|
defaultValue?: Dayjs;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
const DefaultDatePicker = ({
|
|
31
33
|
value,
|
|
32
34
|
onChange,
|
|
33
35
|
showTime,
|
|
@@ -74,7 +76,7 @@ export const DatePicker = ({
|
|
|
74
76
|
);
|
|
75
77
|
};
|
|
76
78
|
|
|
77
|
-
interface RangePickerProps {
|
|
79
|
+
export interface RangePickerProps {
|
|
78
80
|
value: [Dayjs | null, Dayjs | null];
|
|
79
81
|
onChange: (value: [Dayjs | null, Dayjs | null]) => void;
|
|
80
82
|
format?: string;
|
|
@@ -84,7 +86,7 @@ interface RangePickerProps {
|
|
|
84
86
|
className?: string;
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
const
|
|
89
|
+
const DefaultRangePicker = ({
|
|
88
90
|
value,
|
|
89
91
|
onChange,
|
|
90
92
|
format = "yyyy-MM-dd",
|
|
@@ -143,9 +145,7 @@ const RangePicker = ({
|
|
|
143
145
|
);
|
|
144
146
|
};
|
|
145
147
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
interface TimePickerProps {
|
|
148
|
+
export interface TimePickerProps {
|
|
149
149
|
value: Dayjs | null;
|
|
150
150
|
onChange: (value: Dayjs) => void;
|
|
151
151
|
format?: string;
|
|
@@ -155,7 +155,7 @@ interface TimePickerProps {
|
|
|
155
155
|
disabled?: boolean;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
const
|
|
158
|
+
const DefaultTimePicker = ({
|
|
159
159
|
disabled,
|
|
160
160
|
className,
|
|
161
161
|
value,
|
|
@@ -187,5 +187,15 @@ const TimePicker = ({
|
|
|
187
187
|
);
|
|
188
188
|
};
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
const DatePickerBase = createOverridable("DatePicker", DefaultDatePicker);
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Date picker. `DatePicker`, `DatePicker.RangePicker`, and `DatePicker.TimePicker` each resolve to a
|
|
194
|
+
* route-scoped override when a `page/**\/_overrides.tsx` in the route's ancestry declares one (slots
|
|
195
|
+
* `DatePicker`, `DatePickerRangePicker`, `DatePickerTimePicker`).
|
|
196
|
+
*/
|
|
197
|
+
export const DatePicker = Object.assign(DatePickerBase, {
|
|
198
|
+
RangePicker: createOverridable("DatePickerRangePicker", DefaultRangePicker),
|
|
199
|
+
TimePicker: createOverridable("DatePickerTimePicker", DefaultTimePicker),
|
|
200
|
+
});
|
|
191
201
|
|
package/ui/Dropdown.tsx
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { clsx } from "akanjs/client";
|
|
3
3
|
import { type ReactNode, useState } from "react";
|
|
4
4
|
|
|
5
|
+
import { createOverridable } from "./UiOverride";
|
|
6
|
+
|
|
5
7
|
export interface DropdownProps {
|
|
6
8
|
/** Button/trigger content. */
|
|
7
9
|
value: ReactNode;
|
|
@@ -15,7 +17,7 @@ export interface DropdownProps {
|
|
|
15
17
|
dropdownClassName?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
export const
|
|
20
|
+
export const DefaultDropdown = ({ value, content, className, buttonClassName, dropdownClassName }: DropdownProps) => {
|
|
19
21
|
const [opened, setOpened] = useState(false);
|
|
20
22
|
return (
|
|
21
23
|
<div
|
|
@@ -49,3 +51,9 @@ export const Dropdown = ({ value, content, className, buttonClassName, dropdownC
|
|
|
49
51
|
</div>
|
|
50
52
|
);
|
|
51
53
|
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Dropdown. Resolves to a route-scoped override when a `page/**\/_overrides.tsx`
|
|
57
|
+
* in the route's ancestry declares one, otherwise renders {@link DefaultDropdown}.
|
|
58
|
+
*/
|
|
59
|
+
export const Dropdown = createOverridable("Dropdown", DefaultDropdown);
|
package/ui/Empty.tsx
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { clsx, usePage } from "akanjs/client";
|
|
2
3
|
import type { ReactNode } from "react";
|
|
3
4
|
import { AiOutlineMeh } from "react-icons/ai";
|
|
4
5
|
|
|
6
|
+
import { createOverridable } from "./UiOverride";
|
|
7
|
+
|
|
5
8
|
export interface EmptyProps {
|
|
6
9
|
/** Additional classes for the empty-state body. */
|
|
7
10
|
className?: string;
|
|
@@ -13,7 +16,7 @@ export interface EmptyProps {
|
|
|
13
16
|
minHeight?: number;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
export const
|
|
19
|
+
export const DefaultEmpty = ({ className = "", description, children, minHeight = 300 }: EmptyProps) => {
|
|
17
20
|
const { l } = usePage();
|
|
18
21
|
return (
|
|
19
22
|
<div>
|
|
@@ -30,3 +33,10 @@ export const Empty = ({ className = "", description, children, minHeight = 300 }
|
|
|
30
33
|
</div>
|
|
31
34
|
);
|
|
32
35
|
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Empty-state placeholder. Resolves to a route-scoped override when a
|
|
39
|
+
* `page/**\/_overrides.tsx` in the route's ancestry declares one, otherwise
|
|
40
|
+
* renders {@link DefaultEmpty}.
|
|
41
|
+
*/
|
|
42
|
+
export const Empty = createOverridable("Empty", DefaultEmpty);
|
package/ui/Input.tsx
CHANGED
|
@@ -13,6 +13,8 @@ import React, {
|
|
|
13
13
|
} from "react";
|
|
14
14
|
import { AiOutlineEye, AiOutlineEyeInvisible } from "react-icons/ai";
|
|
15
15
|
|
|
16
|
+
import { createOverridable } from "./UiOverride";
|
|
17
|
+
|
|
16
18
|
export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "onChange"> & {
|
|
17
19
|
/** Visual input style. */
|
|
18
20
|
inputStyleType?: "bordered" | "borderless" | "underline";
|
|
@@ -38,7 +40,7 @@ export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "
|
|
|
38
40
|
/** Called when Escape is pressed after blurring the input. */
|
|
39
41
|
onPressEscape?: (e: KeyboardEvent<HTMLInputElement>) => void;
|
|
40
42
|
};
|
|
41
|
-
|
|
43
|
+
const DefaultInput = ({
|
|
42
44
|
className,
|
|
43
45
|
nullable,
|
|
44
46
|
inputRef,
|
|
@@ -145,7 +147,7 @@ export type TextAreaProps = Omit<
|
|
|
145
147
|
validate: (value: string) => boolean | string;
|
|
146
148
|
onPressEscape?: (e: KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
147
149
|
};
|
|
148
|
-
const
|
|
150
|
+
const DefaultTextArea = ({
|
|
149
151
|
className,
|
|
150
152
|
nullable,
|
|
151
153
|
value,
|
|
@@ -218,7 +220,6 @@ const TextArea = ({
|
|
|
218
220
|
</div>
|
|
219
221
|
);
|
|
220
222
|
};
|
|
221
|
-
Input.TextArea = TextArea;
|
|
222
223
|
|
|
223
224
|
export type PasswordProps = Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "type" | "onChange"> & {
|
|
224
225
|
value: string;
|
|
@@ -233,7 +234,7 @@ export type PasswordProps = Omit<InputHTMLAttributes<HTMLInputElement>, "value"
|
|
|
233
234
|
onChange?: (value: string, e?: ChangeEvent<HTMLInputElement>) => void;
|
|
234
235
|
validate: (value: string) => boolean | string;
|
|
235
236
|
};
|
|
236
|
-
const
|
|
237
|
+
const DefaultPassword = ({
|
|
237
238
|
className,
|
|
238
239
|
nullable,
|
|
239
240
|
value,
|
|
@@ -330,8 +331,6 @@ const Password = ({
|
|
|
330
331
|
);
|
|
331
332
|
};
|
|
332
333
|
|
|
333
|
-
Input.Password = Password;
|
|
334
|
-
|
|
335
334
|
export type EmailProps = Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "type" | "onChange"> & {
|
|
336
335
|
inputStyleType?: "bordered" | "borderless" | "underline";
|
|
337
336
|
value: string;
|
|
@@ -346,7 +345,7 @@ export type EmailProps = Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "
|
|
|
346
345
|
onChange?: (value: string, e?: ChangeEvent<HTMLInputElement>) => void;
|
|
347
346
|
validate: (value: string) => boolean | string;
|
|
348
347
|
};
|
|
349
|
-
const
|
|
348
|
+
const DefaultEmail = ({
|
|
350
349
|
inputStyleType = "bordered",
|
|
351
350
|
className,
|
|
352
351
|
nullable,
|
|
@@ -445,8 +444,6 @@ const Email = ({
|
|
|
445
444
|
);
|
|
446
445
|
};
|
|
447
446
|
|
|
448
|
-
Input.Email = Email;
|
|
449
|
-
|
|
450
447
|
export type NumberProps = Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "type" | "onChange"> & {
|
|
451
448
|
value: number | null;
|
|
452
449
|
nullable?: boolean;
|
|
@@ -464,7 +461,7 @@ export type NumberProps = Omit<InputHTMLAttributes<HTMLInputElement>, "value" |
|
|
|
464
461
|
formatter?: (value: string) => string;
|
|
465
462
|
parser?: (value: string) => string;
|
|
466
463
|
};
|
|
467
|
-
const
|
|
464
|
+
const DefaultNumber = ({
|
|
468
465
|
className,
|
|
469
466
|
nullable,
|
|
470
467
|
value,
|
|
@@ -612,15 +609,13 @@ const Number = ({
|
|
|
612
609
|
);
|
|
613
610
|
};
|
|
614
611
|
|
|
615
|
-
Input.Number = Number;
|
|
616
|
-
|
|
617
612
|
export type CheckboxProps = Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> & {
|
|
618
613
|
className?: string;
|
|
619
614
|
checked: boolean;
|
|
620
615
|
onChange: (checked: boolean, e: ChangeEvent<HTMLInputElement>) => void;
|
|
621
616
|
};
|
|
622
617
|
|
|
623
|
-
const
|
|
618
|
+
const DefaultCheckbox = ({ checked, onChange, className, ...rest }: CheckboxProps) => {
|
|
624
619
|
return (
|
|
625
620
|
<input
|
|
626
621
|
{...rest}
|
|
@@ -633,4 +628,17 @@ const Checkbox = ({ checked, onChange, className, ...rest }: CheckboxProps) => {
|
|
|
633
628
|
/>
|
|
634
629
|
);
|
|
635
630
|
};
|
|
636
|
-
|
|
631
|
+
const InputBase = createOverridable("Input", DefaultInput);
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Text input plus its field variants. Each leaf resolves to a route-scoped override when a
|
|
635
|
+
* `page/**\/_overrides.tsx` in the route's ancestry declares one (slots `Input`, `InputTextArea`,
|
|
636
|
+
* `InputPassword`, `InputEmail`, `InputNumber`, `InputCheckbox`), otherwise renders the default.
|
|
637
|
+
*/
|
|
638
|
+
export const Input = Object.assign(InputBase, {
|
|
639
|
+
TextArea: createOverridable("InputTextArea", DefaultTextArea),
|
|
640
|
+
Password: createOverridable("InputPassword", DefaultPassword),
|
|
641
|
+
Email: createOverridable("InputEmail", DefaultEmail),
|
|
642
|
+
Number: createOverridable("InputNumber", DefaultNumber),
|
|
643
|
+
Checkbox: createOverridable("InputCheckbox", DefaultCheckbox),
|
|
644
|
+
});
|
package/ui/Loading/index.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createOverridable } from "../UiOverride";
|
|
1
3
|
import { Area } from "./Area";
|
|
2
4
|
import { Button } from "./Button";
|
|
3
5
|
import { Input } from "./Input";
|
|
@@ -5,4 +7,16 @@ import { ProgressBar } from "./ProgressBar";
|
|
|
5
7
|
import { Skeleton } from "./Skeleton";
|
|
6
8
|
import { Spin } from "./Spin";
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Loading indicators. Each member is an independent override slot (`LoadingSpin`, `LoadingSkeleton`,
|
|
12
|
+
* `LoadingProgressBar`, `LoadingButton`, `LoadingInput`, `LoadingArea`), resolved from the closest
|
|
13
|
+
* `page/**\/_overrides.tsx` in the route's ancestry, otherwise the shipped default.
|
|
14
|
+
*/
|
|
15
|
+
export const Loading = {
|
|
16
|
+
Area: createOverridable("LoadingArea", Area),
|
|
17
|
+
Button: createOverridable("LoadingButton", Button),
|
|
18
|
+
Input: createOverridable("LoadingInput", Input),
|
|
19
|
+
ProgressBar: createOverridable("LoadingProgressBar", ProgressBar),
|
|
20
|
+
Skeleton: createOverridable("LoadingSkeleton", Skeleton),
|
|
21
|
+
Spin: createOverridable("LoadingSpin", Spin),
|
|
22
|
+
};
|
package/ui/Menu.tsx
CHANGED
|
@@ -4,7 +4,9 @@ import { st } from "akanjs/store";
|
|
|
4
4
|
import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
5
5
|
import { AiFillCaretDown, AiOutlineEllipsis } from "react-icons/ai";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import { createOverridable } from "./UiOverride";
|
|
8
|
+
|
|
9
|
+
export interface MenuItem {
|
|
8
10
|
label: ReactNode;
|
|
9
11
|
key: string;
|
|
10
12
|
children?: MenuItem[];
|
|
@@ -12,7 +14,7 @@ interface MenuItem {
|
|
|
12
14
|
type?: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
interface MenuProps {
|
|
17
|
+
export interface MenuProps {
|
|
16
18
|
className?: string;
|
|
17
19
|
ulClassName?: string;
|
|
18
20
|
liClassName?: string;
|
|
@@ -29,7 +31,7 @@ interface MenuProps {
|
|
|
29
31
|
onMouseLeave?: () => void;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
export const
|
|
34
|
+
export const DefaultMenu = ({
|
|
33
35
|
items,
|
|
34
36
|
onClick,
|
|
35
37
|
selectedKeys,
|
|
@@ -271,3 +273,10 @@ const OverflowMenu = ({ overflowItems, onClick }: OverflowMenuProps) => {
|
|
|
271
273
|
</li>
|
|
272
274
|
);
|
|
273
275
|
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Navigation menu. Resolves to a route-scoped override when a
|
|
279
|
+
* `page/**\/_overrides.tsx` in the route's ancestry declares one, otherwise
|
|
280
|
+
* renders {@link DefaultMenu}.
|
|
281
|
+
*/
|
|
282
|
+
export const Menu = createOverridable("Menu", DefaultMenu);
|
package/ui/Modal.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
import { Dialog } from "./Dialog";
|
|
5
|
+
import { createOverridable } from "./UiOverride";
|
|
5
6
|
|
|
6
7
|
export interface ModalProps {
|
|
7
8
|
/** Additional classes for the modal surface. */
|
|
@@ -21,7 +22,11 @@ export interface ModalProps {
|
|
|
21
22
|
confirmClose?: boolean;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Default akanjs modal skin. Kept as the fallback implementation; apps replace
|
|
27
|
+
* it per-route through a `page/**\/_overrides.tsx` manifest.
|
|
28
|
+
*/
|
|
29
|
+
export const DefaultModal = ({
|
|
25
30
|
className,
|
|
26
31
|
title,
|
|
27
32
|
action,
|
|
@@ -41,3 +46,10 @@ export const Modal = ({
|
|
|
41
46
|
</Dialog>
|
|
42
47
|
);
|
|
43
48
|
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Public Modal. Resolves to a route-scoped override when a `_overrides.tsx` in
|
|
52
|
+
* the route's ancestry declares one, otherwise renders {@link DefaultModal}.
|
|
53
|
+
* The proxy is transparent to every existing `<Modal … />` call site.
|
|
54
|
+
*/
|
|
55
|
+
export const Modal = createOverridable("Modal", DefaultModal);
|
package/ui/Pagination.tsx
CHANGED
|
@@ -3,6 +3,8 @@ import { clsx } from "akanjs/client";
|
|
|
3
3
|
import type { FC, ReactNode } from "react";
|
|
4
4
|
import { BiChevronLeft, BiChevronRight, BiDotsHorizontalRounded } from "react-icons/bi";
|
|
5
5
|
|
|
6
|
+
import { createOverridable } from "./UiOverride";
|
|
7
|
+
|
|
6
8
|
export interface PaginationProps {
|
|
7
9
|
/** Current 1-based page number. */
|
|
8
10
|
currentPage: number;
|
|
@@ -22,7 +24,7 @@ export interface PaginationProps {
|
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
export const
|
|
27
|
+
export const DefaultPagination: FC<PaginationProps> = ({
|
|
26
28
|
currentPage,
|
|
27
29
|
total,
|
|
28
30
|
onPageSelect,
|
|
@@ -118,3 +120,9 @@ export const Pagination: FC<PaginationProps> = ({
|
|
|
118
120
|
</div>
|
|
119
121
|
);
|
|
120
122
|
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Pager. Resolves to a route-scoped override when a `page/**\/_overrides.tsx` in
|
|
126
|
+
* the route's ancestry declares one, otherwise renders {@link DefaultPagination}.
|
|
127
|
+
*/
|
|
128
|
+
export const Pagination = createOverridable("Pagination", DefaultPagination);
|
package/ui/Popconfirm.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { type ButtonHTMLAttributes, type ReactNode, useEffect, useState } from "
|
|
|
6
6
|
import { BiMessageRoundedError } from "react-icons/bi";
|
|
7
7
|
|
|
8
8
|
import { animated } from "./animated";
|
|
9
|
+
import { createOverridable } from "./UiOverride";
|
|
9
10
|
|
|
10
11
|
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
11
12
|
loading?: boolean;
|
|
@@ -34,7 +35,7 @@ export interface PopconfirmProps {
|
|
|
34
35
|
decoClassName?: string;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export const
|
|
38
|
+
export const DefaultPopconfirm = ({
|
|
38
39
|
title,
|
|
39
40
|
description,
|
|
40
41
|
onConfirm,
|
|
@@ -133,3 +134,10 @@ export const Popconfirm = ({
|
|
|
133
134
|
</>
|
|
134
135
|
);
|
|
135
136
|
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Confirmation popover. Resolves to a route-scoped override when a
|
|
140
|
+
* `page/**\/_overrides.tsx` in the route's ancestry declares one, otherwise
|
|
141
|
+
* renders {@link DefaultPopconfirm}.
|
|
142
|
+
*/
|
|
143
|
+
export const Popconfirm = createOverridable("Popconfirm", DefaultPopconfirm);
|
package/ui/Radio.tsx
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { clsx } from "akanjs/client";
|
|
3
3
|
import type { ReactElement, ReactNode } from "react";
|
|
4
4
|
|
|
5
|
+
import { createOverridable } from "./UiOverride";
|
|
6
|
+
|
|
5
7
|
export interface RadioProps {
|
|
6
8
|
value: string | number | null;
|
|
7
9
|
className?: string;
|
|
@@ -9,7 +11,7 @@ export interface RadioProps {
|
|
|
9
11
|
children: ReactNode | ReactElement | ReactElement[];
|
|
10
12
|
onChange: (value: string | number | null, idx: number) => void;
|
|
11
13
|
}
|
|
12
|
-
|
|
14
|
+
const DefaultRadio = ({ value, children, disabled, className, onChange }: RadioProps) => {
|
|
13
15
|
return (
|
|
14
16
|
<div className={clsx(`flex gap-2`, className)}>
|
|
15
17
|
{(children as ReactElement<{ value: string | number | null }>[]).map((child, idx) => {
|
|
@@ -47,7 +49,16 @@ export interface ItemProps {
|
|
|
47
49
|
onChange?: (value: string) => void;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
const
|
|
52
|
+
const DefaultItem = ({ value, className, children }: ItemProps) => {
|
|
51
53
|
return <div className={clsx("", className)}>{children}</div>;
|
|
52
54
|
};
|
|
53
|
-
|
|
55
|
+
|
|
56
|
+
const RadioBase = createOverridable("Radio", DefaultRadio);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Radio group. `Radio` and `Radio.Item` each resolve to a route-scoped override when a
|
|
60
|
+
* `page/**\/_overrides.tsx` in the route's ancestry declares one (slots `Radio`, `RadioItem`).
|
|
61
|
+
*/
|
|
62
|
+
export const Radio = Object.assign(RadioBase, {
|
|
63
|
+
Item: createOverridable("RadioItem", DefaultItem),
|
|
64
|
+
});
|
package/ui/Select.tsx
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import { type Cls, type EnumInstance, isEnum } from "akanjs/base";
|
|
3
3
|
import { clsx, usePage } from "akanjs/client";
|
|
4
4
|
import { useDebounce } from "akanjs/webkit";
|
|
5
|
-
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { type ComponentType, createElement, type ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
|
6
6
|
import { BiCheck, BiChevronDown, BiX } from "react-icons/bi";
|
|
7
7
|
import { BsQuestionCircleFill } from "react-icons/bs";
|
|
8
8
|
import { TiDelete } from "react-icons/ti";
|
|
9
9
|
|
|
10
|
+
import { useUiOverride } from "./UiOverride";
|
|
11
|
+
|
|
10
12
|
interface LabelOption<T> {
|
|
11
13
|
label: string | boolean | number;
|
|
12
14
|
value: T;
|
|
@@ -53,7 +55,7 @@ export interface SelectProps<
|
|
|
53
55
|
renderSelected?: (value: T) => ReactNode;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
const DefaultSelect = <
|
|
57
59
|
T extends string | number | boolean | null | undefined,
|
|
58
60
|
Multiple extends boolean = false,
|
|
59
61
|
Searchable extends boolean = false,
|
|
@@ -320,3 +322,21 @@ export const Select = <
|
|
|
320
322
|
</div>
|
|
321
323
|
);
|
|
322
324
|
};
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Select. Resolves to a route-scoped override when a `page/**\/_overrides.tsx` in the route's
|
|
328
|
+
* ancestry declares one, otherwise renders {@link DefaultSelect}. The public generic signature is
|
|
329
|
+
* preserved, so `<Select<MyEnum, true> …/>` still infers the value/onChange shape.
|
|
330
|
+
*/
|
|
331
|
+
export const Select = <
|
|
332
|
+
T extends string | number | boolean | null | undefined,
|
|
333
|
+
Multiple extends boolean = false,
|
|
334
|
+
Searchable extends boolean = false,
|
|
335
|
+
Option extends Options<T> = Options<T>,
|
|
336
|
+
>(
|
|
337
|
+
props: SelectProps<T, Multiple, Searchable, Option>,
|
|
338
|
+
) => {
|
|
339
|
+
const Override = useUiOverride("Select");
|
|
340
|
+
const Impl = (Override ?? DefaultSelect) as unknown as ComponentType<SelectProps<T, Multiple, Searchable, Option>>;
|
|
341
|
+
return createElement(Impl, props);
|
|
342
|
+
};
|
package/ui/Table.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { AiOutlineLoading3Quarters } from "react-icons/ai";
|
|
|
8
8
|
|
|
9
9
|
import { Empty } from "./Empty";
|
|
10
10
|
import { Pagination, type PaginationProps } from "./Pagination";
|
|
11
|
+
import { createOverridable } from "./UiOverride";
|
|
11
12
|
|
|
12
13
|
export interface Column {
|
|
13
14
|
/** Stable column key. Defaults to index when omitted. */
|
|
@@ -45,7 +46,7 @@ export interface TableProps {
|
|
|
45
46
|
rowKey?: (model: any) => string;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
export const
|
|
49
|
+
export const DefaultTable = ({
|
|
49
50
|
columns,
|
|
50
51
|
dataSource,
|
|
51
52
|
loading,
|
|
@@ -129,3 +130,9 @@ export const Table = ({
|
|
|
129
130
|
</div>
|
|
130
131
|
);
|
|
131
132
|
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Data table. Resolves to a route-scoped override when a `page/**\/_overrides.tsx`
|
|
136
|
+
* in the route's ancestry declares one, otherwise renders {@link DefaultTable}.
|
|
137
|
+
*/
|
|
138
|
+
export const Table = createOverridable("Table", DefaultTable);
|
package/ui/ToggleSelect.tsx
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { clsx, usePage } from "akanjs/client";
|
|
3
|
+
import { type ComponentType, createElement } from "react";
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
import { createOverridable, useUiOverride } from "./UiOverride";
|
|
6
|
+
|
|
7
|
+
export interface ToggleSelectProps<I extends string | number | boolean | null> {
|
|
4
8
|
className?: string;
|
|
5
9
|
btnClassName?: string;
|
|
6
10
|
items: string[] | number[] | { label: string; value: I; disabled?: boolean }[];
|
|
@@ -10,7 +14,7 @@ interface ToggleSelectProps<I extends string | number | boolean | null> {
|
|
|
10
14
|
onChange: (value: I, idx: number) => void;
|
|
11
15
|
disabled?: boolean;
|
|
12
16
|
}
|
|
13
|
-
|
|
17
|
+
const DefaultToggleSelect = <I extends string | number | boolean | null>({
|
|
14
18
|
className,
|
|
15
19
|
btnClassName,
|
|
16
20
|
items,
|
|
@@ -72,7 +76,7 @@ export const ToggleSelect = <I extends string | number | boolean | null>({
|
|
|
72
76
|
);
|
|
73
77
|
};
|
|
74
78
|
|
|
75
|
-
interface MultiProps {
|
|
79
|
+
export interface MultiProps {
|
|
76
80
|
className?: string;
|
|
77
81
|
btnClassName?: string;
|
|
78
82
|
items: string[] | number[] | { label: string; value: string | number; disabled?: boolean }[];
|
|
@@ -82,7 +86,16 @@ interface MultiProps {
|
|
|
82
86
|
onChange: (value: string[] | number[]) => void;
|
|
83
87
|
disabled?: boolean;
|
|
84
88
|
}
|
|
85
|
-
const
|
|
89
|
+
const DefaultMulti = ({
|
|
90
|
+
className,
|
|
91
|
+
btnClassName,
|
|
92
|
+
items,
|
|
93
|
+
nullable,
|
|
94
|
+
validate,
|
|
95
|
+
value,
|
|
96
|
+
onChange,
|
|
97
|
+
disabled,
|
|
98
|
+
}: MultiProps) => {
|
|
86
99
|
const { l } = usePage();
|
|
87
100
|
const validateResult = validate(value);
|
|
88
101
|
|
|
@@ -141,4 +154,17 @@ const Multi = ({ className, btnClassName, items, nullable, validate, value, onCh
|
|
|
141
154
|
</div>
|
|
142
155
|
);
|
|
143
156
|
};
|
|
144
|
-
|
|
157
|
+
const ToggleSelectBase = <I extends string | number | boolean | null>(props: ToggleSelectProps<I>) => {
|
|
158
|
+
const Override = useUiOverride("ToggleSelect");
|
|
159
|
+
const Impl = (Override ?? DefaultToggleSelect) as unknown as ComponentType<ToggleSelectProps<I>>;
|
|
160
|
+
return createElement(Impl, props);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Toggle-select. `ToggleSelect` keeps its generic signature (so `<ToggleSelect<Status> …/>` still
|
|
165
|
+
* infers), and both it and `ToggleSelect.Multi` resolve to a route-scoped override when a
|
|
166
|
+
* `page/**\/_overrides.tsx` in the route's ancestry declares one (slots `ToggleSelect`, `ToggleSelectMulti`).
|
|
167
|
+
*/
|
|
168
|
+
export const ToggleSelect = Object.assign(ToggleSelectBase, {
|
|
169
|
+
Multi: createOverridable("ToggleSelectMulti", DefaultMulti),
|
|
170
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { type ReactNode, useContext, useMemo } from "react";
|
|
3
|
+
|
|
4
|
+
import { type AkanUiOverrides, UiOverrideContext } from "./context";
|
|
5
|
+
|
|
6
|
+
export interface UiOverrideProviderProps {
|
|
7
|
+
/** Override map for this subtree; merged over any ancestor overrides. */
|
|
8
|
+
value?: Partial<AkanUiOverrides>;
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Supplies route-scoped UI overrides. Merges its own `value` over the overrides
|
|
14
|
+
* inherited from ancestors so the closest declaration wins, matching nested
|
|
15
|
+
* `_overrides.tsx` resolution. `routeTreeBuilder` mounts one of these per route
|
|
16
|
+
* node once the `_overrides.tsx` convention is wired.
|
|
17
|
+
*/
|
|
18
|
+
export const UiOverrideProvider = ({ value, children }: UiOverrideProviderProps) => {
|
|
19
|
+
const parent = useContext(UiOverrideContext);
|
|
20
|
+
const merged = useMemo(() => ({ ...parent, ...value }), [parent, value]);
|
|
21
|
+
return <UiOverrideContext.Provider value={merged}>{children}</UiOverrideContext.Provider>;
|
|
22
|
+
};
|