akanjs 3.0.0-alpha.104 → 3.0.0-alpha.106
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/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/server/rscPagePrompts.ts +2 -1
- package/types/ui/Toast.d.ts +43 -0
- package/types/ui/UiOverride/context.d.ts +3 -0
- package/types/ui/index.d.ts +1 -0
- package/ui/System/Messages.tsx +76 -122
- package/ui/Toast.tsx +132 -0
- package/ui/UiOverride/context.ts +4 -0
- package/ui/index.ts +9 -0
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/server/rscPagePrompts.ts
CHANGED
|
@@ -165,7 +165,8 @@ export class RscPagePrompts {
|
|
|
165
165
|
const status = (error as { statusCode?: unknown } | null)?.statusCode;
|
|
166
166
|
if (status === 401 || status === 403)
|
|
167
167
|
return { ok: false, reason: "forbidden", message: `A query the screen makes refused the caller (${status}).` };
|
|
168
|
-
if (status === 404)
|
|
168
|
+
if (status === 404)
|
|
169
|
+
return { ok: false, reason: "not-found", message: "A document the screen reads does not exist." };
|
|
169
170
|
RscPagePrompts.logger.warn(
|
|
170
171
|
`page prompt body threw: ${error instanceof Error ? (error.stack ?? error.message) : error}`,
|
|
171
172
|
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type ToastType = "info" | "success" | "error" | "warning" | "loading";
|
|
3
|
+
export interface ToastMessage {
|
|
4
|
+
key: string;
|
|
5
|
+
type: ToastType;
|
|
6
|
+
content: ReactNode;
|
|
7
|
+
/** Seconds the message is shown before the shell starts its exit. */
|
|
8
|
+
duration: number;
|
|
9
|
+
/** The shell started this message's exit: play the leave animation, then call `onClosed`. */
|
|
10
|
+
leaving: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ToastItemProps {
|
|
13
|
+
className?: string;
|
|
14
|
+
message: ToastMessage;
|
|
15
|
+
/** Dismisses early — starts the same exit the timer would. */
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
/** Takes the message off the screen. Call it when the exit animation has finished. */
|
|
18
|
+
onClosed: () => void;
|
|
19
|
+
}
|
|
20
|
+
export declare const DefaultToastItem: ({ className, message, onClose, onClosed }: ToastItemProps) => import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export interface ToastProps {
|
|
22
|
+
className?: string;
|
|
23
|
+
/** Oldest first, already capped by the store. A replacement showing fewer slices the list itself. */
|
|
24
|
+
messages: ToastMessage[];
|
|
25
|
+
/** The device's top safe area in pixels — the stack starts below it. */
|
|
26
|
+
topSafeArea: number;
|
|
27
|
+
/** Starts a message's exit, as the dismiss button and the elapsed timer both do. */
|
|
28
|
+
onClose: (key: string) => void;
|
|
29
|
+
/** Takes a message off the screen once its exit animation has finished. */
|
|
30
|
+
onClosed: (key: string) => void;
|
|
31
|
+
}
|
|
32
|
+
export declare const DefaultToast: ({ className, messages, topSafeArea, onClose, onClosed }: ToastProps) => import("react/jsx-runtime").JSX.Element;
|
|
33
|
+
/**
|
|
34
|
+
* The toast surface. `Toast` (the stack) and `Toast.Item` (one card) each resolve to a route-scoped override
|
|
35
|
+
* when a `page/**\/_overrides.tsx` in the route's ancestry declares one (slots `Toast`, `ToastItem`).
|
|
36
|
+
*
|
|
37
|
+
* `System`'s `Messages` renders this and is not a slot: it keeps the `msg.*` wiring, the store read, the
|
|
38
|
+
* body-level portal and the dismiss timers, so a replacement re-skins the surface without re-implementing
|
|
39
|
+
* when a toast appears and goes away.
|
|
40
|
+
*/
|
|
41
|
+
export declare const Toast: import("react").ComponentType<ToastProps> & {
|
|
42
|
+
Item: import("react").ComponentType<ToastItemProps>;
|
|
43
|
+
};
|
|
@@ -29,6 +29,7 @@ import type { ItemProps as RadioItemProps, RadioProps } from "../Radio.d.ts";
|
|
|
29
29
|
import type { BadgeVariants, ButtonVariants, InputSurfaceVariants } from "../recipe.d.ts";
|
|
30
30
|
import type { SelectProps } from "../Select.d.ts";
|
|
31
31
|
import type { TableProps } from "../Table.d.ts";
|
|
32
|
+
import type { ToastItemProps, ToastProps } from "../Toast.d.ts";
|
|
32
33
|
import type { MultiProps as ToggleSelectMultiProps, ToggleSelectProps } from "../ToggleSelect.d.ts";
|
|
33
34
|
import type { TooltipProps } from "../Tooltip.d.ts";
|
|
34
35
|
import type { UnauthorizedProps } from "../Unauthorized.d.ts";
|
|
@@ -79,6 +80,8 @@ export interface AkanUiOverrides {
|
|
|
79
80
|
DatePicker: ComponentType<DatePickerProps>;
|
|
80
81
|
DatePickerRangePicker: ComponentType<RangePickerProps>;
|
|
81
82
|
DatePickerTimePicker: ComponentType<TimePickerProps>;
|
|
83
|
+
Toast: ComponentType<ToastProps>;
|
|
84
|
+
ToastItem: ComponentType<ToastItemProps>;
|
|
82
85
|
ToggleSelect: ComponentType<ToggleSelectProps<string | number | boolean | null>>;
|
|
83
86
|
ToggleSelectMulti: ComponentType<ToggleSelectMultiProps>;
|
|
84
87
|
LoadingSpin: ComponentType<SpinProps>;
|
package/types/ui/index.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ export { Switch, type SwitchProps } from "./Switch.d.ts";
|
|
|
67
67
|
export { System, type WebAppManifest } from "./System.d.ts";
|
|
68
68
|
export { Tab } from "./Tab.d.ts";
|
|
69
69
|
export { Table } from "./Table.d.ts";
|
|
70
|
+
export { DefaultToast, DefaultToastItem, Toast, type ToastItemProps, type ToastMessage, type ToastProps, type ToastType, } from "./Toast.d.ts";
|
|
70
71
|
export { ToggleSelect } from "./ToggleSelect.d.ts";
|
|
71
72
|
export { Tooltip, type TooltipProps } from "./Tooltip.d.ts";
|
|
72
73
|
export { type AkanModalComponent, type AkanUiOverrideManifest, type AkanUiOverrideName, type AkanUiOverrides, type AkanUiRecipes, createOverridable, override, UiOverrideProvider, type UiOverrideProviderProps, useUiOverride, useUiRecipe, } from "./UiOverride.d.ts";
|
package/ui/System/Messages.tsx
CHANGED
|
@@ -1,132 +1,91 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { msg, usePage } from "akanjs/client";
|
|
3
3
|
import { st } from "akanjs/store";
|
|
4
|
-
import {
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
|
-
import {
|
|
7
|
-
AiOutlineCheckCircle,
|
|
8
|
-
AiOutlineClose,
|
|
9
|
-
AiOutlineExclamationCircle,
|
|
10
|
-
AiOutlineInfoCircle,
|
|
11
|
-
AiOutlineLoading3Quarters,
|
|
12
|
-
AiOutlineWarning,
|
|
13
|
-
} from "react-icons/ai";
|
|
14
6
|
|
|
15
|
-
|
|
7
|
+
import { Toast, type ToastMessage } from "../Toast";
|
|
16
8
|
|
|
17
|
-
interface MessageProps {
|
|
18
|
-
content: ReactNode;
|
|
19
|
-
type?: MessageType;
|
|
20
|
-
duration: number;
|
|
21
|
-
keyForMessage: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface TimeOutType {
|
|
25
|
-
key: string;
|
|
26
|
-
timeoutId: NodeJS.Timeout;
|
|
27
|
-
}
|
|
28
9
|
interface MsgOption {
|
|
29
10
|
key?: string;
|
|
30
11
|
duration?: number;
|
|
31
12
|
data?: Record<string, string | number>;
|
|
32
13
|
}
|
|
33
14
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
error: {
|
|
39
|
-
card: "border-destructive/35",
|
|
40
|
-
chip: "bg-destructive/15 text-destructive",
|
|
41
|
-
icon: <AiOutlineExclamationCircle />,
|
|
42
|
-
},
|
|
43
|
-
loading: {
|
|
44
|
-
card: "border-border",
|
|
45
|
-
chip: "bg-muted text-foreground/60",
|
|
46
|
-
icon: <AiOutlineLoading3Quarters className="animate-spin" />,
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
let timeOuts: TimeOutType[] = [];
|
|
51
|
-
|
|
52
|
-
const Message = ({ content, type = "info" as MessageType, duration, keyForMessage }: MessageProps) => {
|
|
53
|
-
const [preBlind, setPreBlind] = useState(false);
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
if (!content) return;
|
|
56
|
-
|
|
57
|
-
const existingTimeOut = timeOuts.find((item) => item.key === keyForMessage);
|
|
58
|
-
if (existingTimeOut) {
|
|
59
|
-
clearTimeout(existingTimeOut.timeoutId);
|
|
60
|
-
removeTimeOut(keyForMessage);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const timeoutId = setTimeout(() => {
|
|
64
|
-
setPreBlind(true);
|
|
65
|
-
}, duration * 1000);
|
|
66
|
-
addTimeOut(keyForMessage, timeoutId);
|
|
67
|
-
|
|
68
|
-
return () => {
|
|
69
|
-
clearTimeout(timeoutId);
|
|
70
|
-
};
|
|
71
|
-
}, [content, keyForMessage, type]);
|
|
72
|
-
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
if (!preBlind) return;
|
|
75
|
-
setTimeout(() => {
|
|
76
|
-
st.do.hideMessage(keyForMessage);
|
|
77
|
-
removeTimeOut(keyForMessage);
|
|
78
|
-
}, 100);
|
|
79
|
-
}, [preBlind]);
|
|
80
|
-
|
|
81
|
-
const addTimeOut = (key: string, timeoutId: NodeJS.Timeout) => {
|
|
82
|
-
const filteredTimeOuts = timeOuts.filter((item) => item.key !== key);
|
|
83
|
-
timeOuts = [...filteredTimeOuts, { key, timeoutId }];
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const removeTimeOut = (key: string) => {
|
|
87
|
-
timeOuts = timeOuts.filter((item) => item.key !== key);
|
|
88
|
-
};
|
|
15
|
+
interface ArmedMessage {
|
|
16
|
+
message: Omit<ToastMessage, "leaving">;
|
|
17
|
+
timeoutId: ReturnType<typeof setTimeout>;
|
|
18
|
+
}
|
|
89
19
|
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<div
|
|
93
|
-
data-state={preBlind}
|
|
94
|
-
className="pointer-events-auto w-full animate-fadeInDown15-150ms data-[state=true]:animate-smaller"
|
|
95
|
-
>
|
|
96
|
-
<div
|
|
97
|
-
className={cn(
|
|
98
|
-
"flex w-full items-start gap-3 rounded-box border bg-popover/95 p-3 text-popover-foreground shadow-lg backdrop-blur-sm",
|
|
99
|
-
tone.card,
|
|
100
|
-
)}
|
|
101
|
-
>
|
|
102
|
-
<div className={cn("flex size-7 shrink-0 items-center justify-center rounded-full text-base", tone.chip)}>
|
|
103
|
-
{tone.icon}
|
|
104
|
-
</div>
|
|
105
|
-
<span className="min-w-0 flex-1 self-center break-words text-sm leading-snug">{content}</span>
|
|
106
|
-
<button
|
|
107
|
-
aria-label="Dismiss"
|
|
108
|
-
className="-mr-1 shrink-0 self-center rounded-full p-1 text-foreground/30 transition-colors hover:text-foreground/70"
|
|
109
|
-
onClick={() => {
|
|
110
|
-
setPreBlind(true);
|
|
111
|
-
}}
|
|
112
|
-
type="button"
|
|
113
|
-
>
|
|
114
|
-
<AiOutlineClose className="text-sm" />
|
|
115
|
-
</button>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
);
|
|
119
|
-
};
|
|
20
|
+
const exitGraceMs = 2000;
|
|
120
21
|
|
|
121
22
|
export const Messages = () => {
|
|
122
23
|
const messages = st.use.messages({ agent: false });
|
|
123
24
|
const pageState = st.use.pageState({ agent: false });
|
|
124
25
|
const { l } = usePage();
|
|
125
26
|
const [portalElement, setPortalElement] = useState<HTMLElement | null>(null);
|
|
27
|
+
const [leavingKeys, setLeavingKeys] = useState<string[]>([]);
|
|
28
|
+
const timers = useRef(new Map<string, ArmedMessage>());
|
|
29
|
+
|
|
30
|
+
const dropMessage = (key: string) => {
|
|
31
|
+
const armed = timers.current.get(key);
|
|
32
|
+
if (armed) {
|
|
33
|
+
clearTimeout(armed.timeoutId);
|
|
34
|
+
timers.current.delete(key);
|
|
35
|
+
}
|
|
36
|
+
setLeavingKeys((keys) => keys.filter((leavingKey) => leavingKey !== key));
|
|
37
|
+
st.do.hideMessage(key);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const startLeaving = (key: string) => {
|
|
41
|
+
const armed = timers.current.get(key);
|
|
42
|
+
if (armed) {
|
|
43
|
+
clearTimeout(armed.timeoutId);
|
|
44
|
+
timers.current.set(key, { message: armed.message, timeoutId: setTimeout(() => dropMessage(key), exitGraceMs) });
|
|
45
|
+
}
|
|
46
|
+
setLeavingKeys((keys) => (keys.includes(key) ? keys : [...keys, key]));
|
|
47
|
+
};
|
|
48
|
+
|
|
126
49
|
useEffect(() => {
|
|
127
50
|
if (typeof document === "undefined") return;
|
|
128
51
|
setPortalElement(document.body);
|
|
129
52
|
}, []);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const running = timers.current;
|
|
56
|
+
const rearmed: string[] = [];
|
|
57
|
+
for (const message of messages) {
|
|
58
|
+
const armed = running.get(message.key);
|
|
59
|
+
|
|
60
|
+
if (armed?.message === message) continue;
|
|
61
|
+
if (armed) {
|
|
62
|
+
clearTimeout(armed.timeoutId);
|
|
63
|
+
rearmed.push(message.key);
|
|
64
|
+
}
|
|
65
|
+
running.set(message.key, {
|
|
66
|
+
message,
|
|
67
|
+
timeoutId: setTimeout(() => startLeaving(message.key), message.duration * 1000),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
for (const [key, armed] of [...running]) {
|
|
71
|
+
if (messages.some((message) => message.key === key)) continue;
|
|
72
|
+
clearTimeout(armed.timeoutId);
|
|
73
|
+
running.delete(key);
|
|
74
|
+
}
|
|
75
|
+
setLeavingKeys((keys) => {
|
|
76
|
+
const alive = keys.filter((key) => !rearmed.includes(key) && messages.some((message) => message.key === key));
|
|
77
|
+
return alive.length === keys.length ? keys : alive;
|
|
78
|
+
});
|
|
79
|
+
}, [messages]);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const running = timers.current;
|
|
83
|
+
return () => {
|
|
84
|
+
for (const armed of running.values()) clearTimeout(armed.timeoutId);
|
|
85
|
+
running.clear();
|
|
86
|
+
};
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
130
89
|
useEffect(() => {
|
|
131
90
|
Object.assign(msg, {
|
|
132
91
|
info: (msgKey: `${string}.${string}`, option = {} as MsgOption) => {
|
|
@@ -172,23 +131,18 @@ export const Messages = () => {
|
|
|
172
131
|
});
|
|
173
132
|
}, []);
|
|
174
133
|
if (!messages.length || !portalElement) return null;
|
|
134
|
+
const toastMessages: ToastMessage[] = messages.map((message) => ({
|
|
135
|
+
...message,
|
|
136
|
+
leaving: leavingKeys.includes(message.key),
|
|
137
|
+
}));
|
|
175
138
|
|
|
176
139
|
return createPortal(
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
<Message
|
|
184
|
-
content={message.content}
|
|
185
|
-
type={message.type}
|
|
186
|
-
duration={message.duration}
|
|
187
|
-
key={message.key}
|
|
188
|
-
keyForMessage={message.key}
|
|
189
|
-
/>
|
|
190
|
-
))}
|
|
191
|
-
</div>,
|
|
140
|
+
<Toast
|
|
141
|
+
messages={toastMessages}
|
|
142
|
+
topSafeArea={pageState.topSafeArea}
|
|
143
|
+
onClose={startLeaving}
|
|
144
|
+
onClosed={dropMessage}
|
|
145
|
+
/>,
|
|
192
146
|
portalElement,
|
|
193
147
|
);
|
|
194
148
|
};
|
package/ui/Toast.tsx
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "akanjs/client";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import {
|
|
5
|
+
AiOutlineCheckCircle,
|
|
6
|
+
AiOutlineClose,
|
|
7
|
+
AiOutlineExclamationCircle,
|
|
8
|
+
AiOutlineInfoCircle,
|
|
9
|
+
AiOutlineLoading3Quarters,
|
|
10
|
+
AiOutlineWarning,
|
|
11
|
+
} from "react-icons/ai";
|
|
12
|
+
|
|
13
|
+
import { createOverridable } from "./UiOverride";
|
|
14
|
+
|
|
15
|
+
export type ToastType = "info" | "success" | "error" | "warning" | "loading";
|
|
16
|
+
|
|
17
|
+
export interface ToastMessage {
|
|
18
|
+
key: string;
|
|
19
|
+
type: ToastType;
|
|
20
|
+
content: ReactNode;
|
|
21
|
+
/** Seconds the message is shown before the shell starts its exit. */
|
|
22
|
+
duration: number;
|
|
23
|
+
/** The shell started this message's exit: play the leave animation, then call `onClosed`. */
|
|
24
|
+
leaving: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const messageTone: { [key in ToastType]: { card: string; chip: string; icon: ReactNode } } = {
|
|
28
|
+
info: { card: "border-info/35", chip: "bg-info/15 text-info", icon: <AiOutlineInfoCircle /> },
|
|
29
|
+
success: { card: "border-success/35", chip: "bg-success/15 text-success", icon: <AiOutlineCheckCircle /> },
|
|
30
|
+
warning: { card: "border-warning/35", chip: "bg-warning/15 text-warning", icon: <AiOutlineWarning /> },
|
|
31
|
+
error: {
|
|
32
|
+
card: "border-destructive/35",
|
|
33
|
+
chip: "bg-destructive/15 text-destructive",
|
|
34
|
+
icon: <AiOutlineExclamationCircle />,
|
|
35
|
+
},
|
|
36
|
+
loading: {
|
|
37
|
+
card: "border-border",
|
|
38
|
+
chip: "bg-muted text-foreground/60",
|
|
39
|
+
icon: <AiOutlineLoading3Quarters className="animate-spin" />,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface ToastItemProps {
|
|
44
|
+
className?: string;
|
|
45
|
+
message: ToastMessage;
|
|
46
|
+
/** Dismisses early — starts the same exit the timer would. */
|
|
47
|
+
onClose: () => void;
|
|
48
|
+
/** Takes the message off the screen. Call it when the exit animation has finished. */
|
|
49
|
+
onClosed: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const DefaultToastItem = ({ className, message, onClose, onClosed }: ToastItemProps) => {
|
|
53
|
+
const tone = messageTone[message.type];
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
data-state={message.leaving ? "out" : "in"}
|
|
57
|
+
onAnimationEnd={() => {
|
|
58
|
+
if (message.leaving) onClosed();
|
|
59
|
+
}}
|
|
60
|
+
className={cn(
|
|
61
|
+
"pointer-events-auto w-full data-[state=in]:animate-fadeInDown15-150ms data-[state=out]:animate-smaller",
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
className={cn(
|
|
67
|
+
"flex w-full items-start gap-3 rounded-box border bg-popover/95 p-3 text-popover-foreground shadow-lg backdrop-blur-sm",
|
|
68
|
+
tone.card,
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<div className={cn("flex size-7 shrink-0 items-center justify-center rounded-full text-base", tone.chip)}>
|
|
72
|
+
{tone.icon}
|
|
73
|
+
</div>
|
|
74
|
+
<span className="min-w-0 flex-1 self-center break-words text-sm leading-snug">{message.content}</span>
|
|
75
|
+
<button
|
|
76
|
+
aria-label="Dismiss"
|
|
77
|
+
className="-mr-1 shrink-0 self-center rounded-full p-1 text-foreground/30 transition-colors hover:text-foreground/70"
|
|
78
|
+
onClick={onClose}
|
|
79
|
+
type="button"
|
|
80
|
+
>
|
|
81
|
+
<AiOutlineClose className="text-sm" />
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export interface ToastProps {
|
|
89
|
+
className?: string;
|
|
90
|
+
/** Oldest first, already capped by the store. A replacement showing fewer slices the list itself. */
|
|
91
|
+
messages: ToastMessage[];
|
|
92
|
+
/** The device's top safe area in pixels — the stack starts below it. */
|
|
93
|
+
topSafeArea: number;
|
|
94
|
+
/** Starts a message's exit, as the dismiss button and the elapsed timer both do. */
|
|
95
|
+
onClose: (key: string) => void;
|
|
96
|
+
/** Takes a message off the screen once its exit animation has finished. */
|
|
97
|
+
onClosed: (key: string) => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const DefaultToast = ({ className, messages, topSafeArea, onClose, onClosed }: ToastProps) => (
|
|
101
|
+
<div
|
|
102
|
+
id="toast"
|
|
103
|
+
className={cn(
|
|
104
|
+
"pointer-events-none fixed top-0 left-1/2 z-[100] flex h-fit w-full max-w-md -translate-x-1/2 flex-col items-center gap-2 px-4 pt-3",
|
|
105
|
+
className,
|
|
106
|
+
)}
|
|
107
|
+
style={{ marginTop: topSafeArea }}
|
|
108
|
+
>
|
|
109
|
+
{messages.map((message) => (
|
|
110
|
+
<Toast.Item
|
|
111
|
+
key={message.key}
|
|
112
|
+
message={message}
|
|
113
|
+
onClose={() => onClose(message.key)}
|
|
114
|
+
onClosed={() => onClosed(message.key)}
|
|
115
|
+
/>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const ToastBase = createOverridable("Toast", DefaultToast);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The toast surface. `Toast` (the stack) and `Toast.Item` (one card) each resolve to a route-scoped override
|
|
124
|
+
* when a `page/**\/_overrides.tsx` in the route's ancestry declares one (slots `Toast`, `ToastItem`).
|
|
125
|
+
*
|
|
126
|
+
* `System`'s `Messages` renders this and is not a slot: it keeps the `msg.*` wiring, the store read, the
|
|
127
|
+
* body-level portal and the dismiss timers, so a replacement re-skins the surface without re-implementing
|
|
128
|
+
* when a toast appears and goes away.
|
|
129
|
+
*/
|
|
130
|
+
export const Toast = Object.assign(ToastBase, {
|
|
131
|
+
Item: createOverridable("ToastItem", DefaultToastItem),
|
|
132
|
+
});
|
package/ui/UiOverride/context.ts
CHANGED
|
@@ -31,6 +31,7 @@ import type { ItemProps as RadioItemProps, RadioProps } from "../Radio";
|
|
|
31
31
|
import type { BadgeVariants, ButtonVariants, InputSurfaceVariants } from "../recipe";
|
|
32
32
|
import type { SelectProps } from "../Select";
|
|
33
33
|
import type { TableProps } from "../Table";
|
|
34
|
+
import type { ToastItemProps, ToastProps } from "../Toast";
|
|
34
35
|
import type { MultiProps as ToggleSelectMultiProps, ToggleSelectProps } from "../ToggleSelect";
|
|
35
36
|
import type { TooltipProps } from "../Tooltip";
|
|
36
37
|
import type { UnauthorizedProps } from "../Unauthorized";
|
|
@@ -90,6 +91,9 @@ export interface AkanUiOverrides {
|
|
|
90
91
|
DatePickerRangePicker: ComponentType<RangePickerProps>;
|
|
91
92
|
DatePickerTimePicker: ComponentType<TimePickerProps>;
|
|
92
93
|
|
|
94
|
+
Toast: ComponentType<ToastProps>;
|
|
95
|
+
ToastItem: ComponentType<ToastItemProps>;
|
|
96
|
+
|
|
93
97
|
ToggleSelect: ComponentType<ToggleSelectProps<string | number | boolean | null>>;
|
|
94
98
|
ToggleSelectMulti: ComponentType<ToggleSelectMultiProps>;
|
|
95
99
|
|
package/ui/index.ts
CHANGED
|
@@ -108,6 +108,15 @@ export { Switch, type SwitchProps } from "./Switch";
|
|
|
108
108
|
export { System, type WebAppManifest } from "./System";
|
|
109
109
|
export { Tab } from "./Tab";
|
|
110
110
|
export { Table } from "./Table";
|
|
111
|
+
export {
|
|
112
|
+
DefaultToast,
|
|
113
|
+
DefaultToastItem,
|
|
114
|
+
Toast,
|
|
115
|
+
type ToastItemProps,
|
|
116
|
+
type ToastMessage,
|
|
117
|
+
type ToastProps,
|
|
118
|
+
type ToastType,
|
|
119
|
+
} from "./Toast";
|
|
111
120
|
export { ToggleSelect } from "./ToggleSelect";
|
|
112
121
|
export { Tooltip, type TooltipProps } from "./Tooltip";
|
|
113
122
|
|