akanjs 2.4.2-rc.2 → 3.0.0-alpha.0
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/base/baseEnv.ts +0 -6
- package/client/cn.ts +52 -0
- package/client/csrTypes.ts +2 -2
- package/client/frameConfig.ts +2 -2
- package/client/index.ts +1 -0
- package/client/types.ts +0 -4
- package/common/fileUpload.ts +1 -1
- package/common/index.ts +1 -0
- package/common/isThenable.ts +12 -0
- package/constant/purify.ts +9 -10
- package/fetch/client/fetchClient.ts +1 -1
- package/fetch/client/httpClient.ts +9 -5
- package/fetch/fetchType/sliceFetch.type.ts +1 -1
- package/package.json +4 -7
- package/server/akanApp.ts +2 -0
- package/server/systemPageDocument.tsx +12 -12
- package/signal/endpointInfo.ts +2 -1
- package/signal/slice.ts +1 -1
- package/signal/sliceInfo.ts +2 -1
- package/store/action.ts +18 -2
- package/store/baseSt.ts +2 -2
- package/store/state.ts +9 -1
- package/store/storeInstance.ts +2 -0
- package/test/sampleOf.ts +3 -3
- package/test/signalTestRuntime.ts +0 -2
- package/types/base/baseEnv.d.ts +0 -2
- package/types/client/cn.d.ts +12 -0
- package/types/client/index.d.ts +1 -0
- package/types/client/types.d.ts +0 -3
- package/types/common/fileUpload.d.ts +1 -1
- package/types/common/index.d.ts +1 -0
- package/types/common/isThenable.d.ts +9 -0
- package/types/constant/purify.d.ts +3 -2
- package/types/fetch/fetchType/sliceFetch.type.d.ts +1 -1
- package/types/signal/endpointInfo.d.ts +2 -2
- package/types/signal/slice.d.ts +1 -1
- package/types/signal/sliceInfo.d.ts +2 -2
- package/types/store/action.d.ts +1 -1
- package/types/store/state.d.ts +4 -2
- package/types/test/sampleOf.d.ts +2 -2
- package/types/ui/Badge.d.ts +6 -0
- package/types/ui/Button.d.ts +26 -6
- package/types/ui/Load/Units.d.ts +3 -1
- package/types/ui/RecentTime.d.ts +1 -1
- package/types/ui/Signal/Collapse.d.ts +12 -0
- package/types/ui/Signal/style.d.ts +5 -6
- package/types/ui/Switch.d.ts +14 -0
- package/types/ui/Tooltip.d.ts +11 -0
- package/types/ui/UiOverride/Provider.d.ts +6 -5
- package/types/ui/UiOverride/context.d.ts +22 -1
- package/types/ui/UiOverride/index.d.ts +2 -1
- package/types/ui/UiOverride/override.d.ts +7 -6
- package/types/ui/UiOverride/useUiRecipe.d.ts +8 -0
- package/types/ui/index.d.ts +5 -1
- package/types/ui/recipe/badgeRecipe.d.ts +13 -0
- package/types/ui/recipe/buttonRecipe.d.ts +15 -0
- package/types/ui/recipe/factory.d.ts +24 -0
- package/types/ui/recipe/index.d.ts +18 -0
- package/types/ui/recipe/inputRecipe.d.ts +13 -0
- package/types/ui/recipe.d.ts +1 -0
- package/ui/Badge.tsx +17 -0
- package/ui/BottomSheet.tsx +8 -10
- package/ui/Button.tsx +158 -48
- package/ui/Clipboard.tsx +3 -3
- package/ui/Constant/Doc.tsx +108 -65
- package/ui/Constant/Mermaid.tsx +9 -9
- package/ui/Data/CardList.tsx +3 -3
- package/ui/Data/Dashboard.tsx +22 -13
- package/ui/Data/Insight.tsx +6 -6
- package/ui/Data/Item.tsx +49 -34
- package/ui/Data/ListContainer.tsx +11 -7
- package/ui/Data/Pagination.tsx +2 -2
- package/ui/DatePicker.tsx +5 -5
- package/ui/Dialog/Content.tsx +2 -2
- package/ui/Dialog/Modal.tsx +12 -8
- package/ui/Dialog/Provider.tsx +2 -2
- package/ui/DragAction.tsx +2 -2
- package/ui/DraggableList.tsx +10 -5
- package/ui/Dropdown.tsx +33 -26
- package/ui/Empty.tsx +5 -5
- package/ui/Field.tsx +82 -70
- package/ui/Image.tsx +2 -2
- package/ui/Input.tsx +62 -60
- package/ui/Layout/BottomInset.tsx +2 -2
- package/ui/Layout/BottomTab.tsx +5 -5
- package/ui/Layout/Header.tsx +3 -3
- package/ui/Layout/LeftSider.tsx +6 -5
- package/ui/Layout/Navbar.tsx +2 -12
- package/ui/Layout/RightSider.tsx +10 -8
- package/ui/Layout/Sider.tsx +5 -4
- package/ui/Layout/Template.tsx +2 -2
- package/ui/Layout/TopInset.tsx +2 -2
- package/ui/Layout/Unit.tsx +2 -2
- package/ui/Layout/View.tsx +2 -2
- package/ui/Layout/Zone.tsx +2 -2
- package/ui/Link/Back.tsx +2 -2
- package/ui/Link/Close.tsx +2 -2
- package/ui/Link/CsrLink.tsx +6 -4
- package/ui/Link/Lang.tsx +2 -2
- package/ui/Link/SsrLink.tsx +6 -5
- package/ui/Load/Page.tsx +1 -1
- package/ui/Load/Pagination.tsx +2 -2
- package/ui/Load/Units.tsx +19 -4
- package/ui/Load/View.tsx +2 -2
- package/ui/Loading/Area.tsx +4 -2
- package/ui/Loading/Button.tsx +2 -2
- package/ui/Loading/Input.tsx +2 -2
- package/ui/Loading/ProgressBar.tsx +2 -2
- package/ui/Loading/Skeleton.tsx +6 -6
- package/ui/Loading/Spin.tsx +2 -2
- package/ui/Menu.tsx +29 -27
- package/ui/Model/Edit.tsx +3 -3
- package/ui/Model/EditModal.tsx +5 -5
- package/ui/Model/EditWrapper.tsx +2 -2
- package/ui/Model/New.tsx +3 -3
- package/ui/Model/NewWrapper_Client.tsx +2 -2
- package/ui/Model/Remove.tsx +5 -4
- package/ui/Model/RemoveWrapper.tsx +2 -2
- package/ui/Model/SureToRemove.tsx +7 -6
- package/ui/Model/View.tsx +2 -2
- package/ui/Model/ViewEditModal.tsx +20 -21
- package/ui/Model/ViewWrapper.tsx +2 -2
- package/ui/More.tsx +2 -2
- package/ui/ObjectId.tsx +9 -3
- package/ui/Pagination.tsx +14 -11
- package/ui/Popconfirm.tsx +20 -11
- package/ui/Radio.tsx +4 -4
- package/ui/RecentTime.tsx +30 -83
- package/ui/Refresh.tsx +1 -1
- package/ui/ScreenNavigator.tsx +2 -2
- package/ui/Select.tsx +21 -23
- package/ui/Signal/Arg.tsx +21 -18
- package/ui/Signal/Collapse.tsx +29 -0
- package/ui/Signal/Doc.tsx +27 -22
- package/ui/Signal/Listener.tsx +3 -3
- package/ui/Signal/Message.tsx +31 -17
- package/ui/Signal/Object.tsx +13 -8
- package/ui/Signal/PubSub.tsx +26 -16
- package/ui/Signal/Request.tsx +3 -2
- package/ui/Signal/Response.tsx +5 -4
- package/ui/Signal/RestApi.tsx +27 -17
- package/ui/Signal/style.ts +16 -16
- package/ui/Switch.tsx +62 -0
- package/ui/System/CSR.tsx +40 -35
- package/ui/System/Client.tsx +2 -2
- package/ui/System/DevModeToggle.tsx +10 -12
- package/ui/System/Messages.tsx +14 -16
- package/ui/System/Reconnect.tsx +7 -7
- package/ui/System/SSR.tsx +25 -21
- package/ui/System/SelectLanguage.tsx +24 -28
- package/ui/System/ThemeToggle.tsx +38 -45
- package/ui/Tab/Menu.tsx +20 -18
- package/ui/Tab/Panel.tsx +3 -3
- package/ui/Tab/Provider.tsx +2 -2
- package/ui/Table.tsx +11 -8
- package/ui/ToggleSelect.tsx +14 -18
- package/ui/Tooltip.tsx +55 -0
- package/ui/UiOverride/Provider.tsx +10 -6
- package/ui/UiOverride/context.ts +22 -2
- package/ui/UiOverride/index.ts +8 -1
- package/ui/UiOverride/override.ts +7 -6
- package/ui/UiOverride/useUiRecipe.ts +15 -0
- package/ui/Unauthorized.tsx +5 -5
- package/ui/index.ts +16 -0
- package/ui/recipe/badgeRecipe.ts +23 -0
- package/ui/recipe/buttonRecipe.ts +32 -0
- package/ui/recipe/factory.ts +32 -0
- package/ui/recipe/index.ts +18 -0
- package/ui/recipe/inputRecipe.ts +13 -0
- package/ui/styles.css +150 -21
package/ui/Button.tsx
CHANGED
|
@@ -1,80 +1,190 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { cn, usePage } from "akanjs/client";
|
|
3
|
+
import { isThenable } from "akanjs/common";
|
|
3
4
|
import type React from "react";
|
|
4
5
|
import { type ButtonHTMLAttributes, type ComponentType, createElement, useState } from "react";
|
|
5
|
-
import {
|
|
6
|
+
import { AiFillCheckCircle, AiOutlineLoading3Quarters } from "react-icons/ai";
|
|
7
|
+
import { type ButtonVariants, buttonRecipe } from "./recipe";
|
|
8
|
+
import { useUiOverride, useUiRecipe } from "./UiOverride";
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
export { buttonRecipe, type ButtonVariants };
|
|
8
11
|
|
|
9
|
-
export type ButtonProps<Result> = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> &
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
export type ButtonProps<Result> = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> &
|
|
13
|
+
ButtonVariants & {
|
|
14
|
+
/**
|
|
15
|
+
* Click handler. Returning a promise is what opts this button into the async states — a plain
|
|
16
|
+
* synchronous handler renders as an ordinary button with no spinner. Call onError to show the
|
|
17
|
+
* localized error state without throwing.
|
|
18
|
+
*/
|
|
19
|
+
onClick?: (
|
|
20
|
+
e: React.MouseEvent<HTMLButtonElement>,
|
|
21
|
+
{ onError }: { onError: (error: string) => void },
|
|
22
|
+
) => Promise<Result> | Result;
|
|
23
|
+
/** Called after the button briefly enters success state. */
|
|
24
|
+
onSuccess?: (result: Result) => void;
|
|
25
|
+
/**
|
|
26
|
+
* How the pending/success indicator is drawn. Both modes keep the button's box fixed — CSS cannot
|
|
27
|
+
* animate an auto width, so a resizing button can only ever snap.
|
|
28
|
+
*
|
|
29
|
+
* `hold` (default) fades a bare indicator over the children, so the box stays exactly the children's
|
|
30
|
+
* size. `replace` cross-fades to a labelled indicator ("Processing…"); both labels stay stacked in the
|
|
31
|
+
* DOM, so the box is the wider of the two from the start and still never moves — at the cost of an idle
|
|
32
|
+
* button that is as wide as its longest state.
|
|
33
|
+
*/
|
|
34
|
+
loadingMode?: "hold" | "replace";
|
|
35
|
+
/** Whether a failed action renders its message under the button. Off leaves it to the framework toast. */
|
|
36
|
+
showError?: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Success mark, entering with `checkIn` (akanjs/ui/styles.css — a 340ms fade plus a 6% overshoot; `pop`'s 1.5x
|
|
41
|
+
* overshoot from zero reads as cartoonish on a glyph this small).
|
|
42
|
+
*
|
|
43
|
+
* The colour is derived from the button, never named — any fixed token fails somewhere: `text-success` vanishes
|
|
44
|
+
* on a `success` button, and a `bg-success` disc lands an unrelated hue on every other fill.
|
|
45
|
+
*
|
|
46
|
+
* `AiFillCheckCircle` is a solid disc with the tick punched out, which is why it is used instead of an outline
|
|
47
|
+
* glyph plus a tinted halo. The disc is `currentColor` (the button's foreground) and the tick is the fill
|
|
48
|
+
* showing through, so the mark reuses the variant's own foreground/background pair — already contrast-checked
|
|
49
|
+
* by themeValidator — at full strength. An earlier attempt put an outline tick on a low-alpha current-colour
|
|
50
|
+
* halo, which did the opposite: tinting the local background toward the glyph's own colour *lowered* the
|
|
51
|
+
* contrast that made the tick readable. (Do not name the class here — Tailwind's source scanner reads comments
|
|
52
|
+
* too, so a class literal in prose compiles to a dead rule.)
|
|
53
|
+
*
|
|
54
|
+
* Sizing is in `em`, so one class covers `xs` through `lg`.
|
|
55
|
+
*/
|
|
56
|
+
const successIconClass = "animate-checkIn text-[1.15em]";
|
|
18
57
|
|
|
19
|
-
const
|
|
58
|
+
const successDwellMs = 700;
|
|
59
|
+
|
|
60
|
+
const DefaultButton = <Result = unknown>({
|
|
61
|
+
className,
|
|
62
|
+
variant,
|
|
63
|
+
size,
|
|
64
|
+
type = "button",
|
|
65
|
+
loadingMode = "hold",
|
|
66
|
+
showError = true,
|
|
67
|
+
children,
|
|
68
|
+
onClick,
|
|
69
|
+
onSuccess,
|
|
70
|
+
...rest
|
|
71
|
+
}: ButtonProps<Result>) => {
|
|
20
72
|
const { l } = usePage();
|
|
73
|
+
|
|
74
|
+
const recipe = useUiRecipe("button") ?? buttonRecipe;
|
|
75
|
+
|
|
21
76
|
const [state, setState] = useState<{
|
|
22
77
|
mode: "idle" | "loading" | "success" | "error";
|
|
23
78
|
error: string | null;
|
|
24
|
-
|
|
25
|
-
}>({
|
|
26
|
-
|
|
27
|
-
error: null,
|
|
28
|
-
times: 0,
|
|
29
|
-
});
|
|
79
|
+
shown: "loading" | "success";
|
|
80
|
+
}>({ mode: "idle", error: null, shown: "loading" });
|
|
81
|
+
const busy = state.mode === "loading" || state.mode === "success";
|
|
30
82
|
return (
|
|
31
83
|
<>
|
|
32
84
|
<button
|
|
33
|
-
|
|
85
|
+
type={type}
|
|
86
|
+
className={recipe(
|
|
87
|
+
{ variant, size },
|
|
88
|
+
|
|
89
|
+
cn(loadingMode === "hold" && "relative", busy && !rest.disabled && "disabled:opacity-100", className),
|
|
90
|
+
)}
|
|
34
91
|
{...rest}
|
|
35
|
-
disabled={!!rest.disabled ||
|
|
92
|
+
disabled={!!rest.disabled || busy}
|
|
36
93
|
onClick={(e) => {
|
|
37
|
-
|
|
94
|
+
if (!onClick) return;
|
|
95
|
+
let errored = false;
|
|
96
|
+
const result = onClick(e, {
|
|
97
|
+
onError: (error) => {
|
|
98
|
+
errored = true;
|
|
99
|
+
setState((s) => ({ ...s, mode: "error", error }));
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
if (!isThenable(result)) return;
|
|
103
|
+
if (!errored) setState({ mode: "loading", error: null, shown: "loading" });
|
|
38
104
|
void (async () => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
105
|
+
try {
|
|
106
|
+
const awaited = (await result) as Result;
|
|
107
|
+
|
|
108
|
+
if (errored) return;
|
|
109
|
+
setState({ mode: "success", error: null, shown: "success" });
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
|
|
112
|
+
setState((s) => ({ ...s, mode: "idle", error: null }));
|
|
113
|
+
onSuccess?.(awaited);
|
|
114
|
+
}, successDwellMs);
|
|
115
|
+
} catch {
|
|
116
|
+
|
|
117
|
+
setState((s) => ({ ...s, mode: "idle", error: null }));
|
|
118
|
+
}
|
|
49
119
|
})();
|
|
50
120
|
}}
|
|
51
121
|
>
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
122
|
+
{loadingMode === "replace" ? (
|
|
123
|
+
|
|
124
|
+
<span className="grid place-items-center">
|
|
125
|
+
<span
|
|
126
|
+
className={cn(
|
|
127
|
+
"col-start-1 row-start-1 flex items-center gap-2 transition-opacity duration-200",
|
|
128
|
+
busy && "opacity-0",
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
131
|
+
{children}
|
|
132
|
+
</span>
|
|
133
|
+
<span
|
|
134
|
+
aria-hidden={!busy}
|
|
135
|
+
className={cn(
|
|
136
|
+
"col-start-1 row-start-1 flex items-center gap-2 transition-opacity duration-200",
|
|
137
|
+
busy ? "opacity-100" : "opacity-0",
|
|
138
|
+
)}
|
|
139
|
+
>
|
|
140
|
+
{state.shown === "success" ? (
|
|
141
|
+
<>
|
|
142
|
+
<AiFillCheckCircle className={successIconClass} /> {l("base.processed")}
|
|
143
|
+
</>
|
|
144
|
+
) : (
|
|
145
|
+
<>
|
|
146
|
+
<AiOutlineLoading3Quarters className="animate-spin" /> {l("base.processing")}
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
149
|
+
</span>
|
|
150
|
+
</span>
|
|
151
|
+
) : (
|
|
57
152
|
<>
|
|
58
|
-
|
|
153
|
+
{/* Children keep their box while hidden, so the button's measured size never changes. */}
|
|
154
|
+
<span className={cn("inline-flex items-center gap-2 transition-opacity duration-200", busy && "opacity-0")}>
|
|
155
|
+
{children}
|
|
156
|
+
</span>
|
|
157
|
+
<span
|
|
158
|
+
aria-hidden={!busy}
|
|
159
|
+
className={cn(
|
|
160
|
+
"absolute inset-0 flex items-center justify-center transition-opacity duration-200",
|
|
161
|
+
busy ? "opacity-100" : "pointer-events-none opacity-0",
|
|
162
|
+
)}
|
|
163
|
+
>
|
|
164
|
+
{state.shown === "success" ? (
|
|
165
|
+
<AiFillCheckCircle className={successIconClass} />
|
|
166
|
+
) : (
|
|
167
|
+
<AiOutlineLoading3Quarters className="animate-spin" />
|
|
168
|
+
)}
|
|
169
|
+
</span>
|
|
59
170
|
</>
|
|
60
|
-
) : (
|
|
61
|
-
children
|
|
62
171
|
)}
|
|
63
172
|
</button>
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
</div>
|
|
173
|
+
{showError && state.error ? (
|
|
174
|
+
<span role="alert" className="mt-1 block text-center text-destructive text-xs">
|
|
175
|
+
{l(state.error as "base.error")}
|
|
176
|
+
</span>
|
|
69
177
|
) : null}
|
|
70
178
|
</>
|
|
71
179
|
);
|
|
72
180
|
};
|
|
73
181
|
|
|
74
182
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
183
|
+
* Button whose async states are driven by the handler's return value: return a promise and it shows a
|
|
184
|
+
* pending indicator, then a brief success state; return nothing and it behaves as a plain button. Resolves
|
|
185
|
+
* to a route-scoped override when a `page/**\/_overrides.tsx` in the route's ancestry declares one,
|
|
186
|
+
* otherwise renders {@link DefaultButton}. The public generic signature is preserved, so
|
|
187
|
+
* `<Button<Todo> onSuccess={(r: Todo) => …} />` still infers.
|
|
78
188
|
*/
|
|
79
189
|
export const Button = <Result = unknown>(props: ButtonProps<Result>) => {
|
|
80
190
|
const Override = useUiOverride("Button");
|
package/ui/Clipboard.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { cn } from "akanjs/client";
|
|
3
3
|
import { type ReactElement, useEffect, useState } from "react";
|
|
4
4
|
import { FaCheck } from "react-icons/fa";
|
|
5
5
|
import { MdContentCopy } from "react-icons/md";
|
|
@@ -30,8 +30,8 @@ export const Clipboard = ({ text, className }: ClipboardProps): ReactElement =>
|
|
|
30
30
|
onClick={async () => {
|
|
31
31
|
await handleCopy();
|
|
32
32
|
}}
|
|
33
|
-
className={
|
|
34
|
-
"flex h-6 w-6 items-center justify-center rounded-md bg-
|
|
33
|
+
className={cn(
|
|
34
|
+
"flex h-6 w-6 items-center justify-center rounded-md bg-foreground/10 text-foreground transition-all duration-300 hover:bg-foreground/20",
|
|
35
35
|
className,
|
|
36
36
|
)}
|
|
37
37
|
>
|