base
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./toast";
|
||||
@@ -0,0 +1,709 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Toast, ToastStatic, setToast, updateToast, setPromiseToast, TOAST_TYPE } from "./toast";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Toast",
|
||||
component: Toast,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
theme: "light",
|
||||
},
|
||||
} satisfies Meta<typeof Toast>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Provider: Story = {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Toast theme="light" />
|
||||
<div className="space-y-2">
|
||||
<p className="text-gray-600 text-13">
|
||||
Toast provider is required to display toasts. It should be added to your app root.
|
||||
</p>
|
||||
<code className="bg-gray-100 block rounded-sm p-2 text-11">{`<Toast theme="light" />`}</code>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Success: Story = {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Toast theme="light" />
|
||||
<button
|
||||
onClick={() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Your changes have been saved successfully.",
|
||||
})
|
||||
}
|
||||
className="rounded-sm bg-success-primary px-4 py-2 text-13 text-on-color hover:bg-success-primary/90"
|
||||
>
|
||||
Show Success Toast
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Toast theme="light" />
|
||||
<button
|
||||
onClick={() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error",
|
||||
message: "Something went wrong. Please try again.",
|
||||
})
|
||||
}
|
||||
className="rounded-sm bg-danger-primary px-4 py-2 text-13 text-on-color hover:bg-danger-primary/90"
|
||||
>
|
||||
Show Error Toast
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Toast theme="light" />
|
||||
<button
|
||||
onClick={() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.WARNING,
|
||||
title: "Warning",
|
||||
message: "This action cannot be undone.",
|
||||
})
|
||||
}
|
||||
className="rounded-sm bg-warning-primary px-4 py-2 text-13 text-on-color hover:bg-warning-primary/90"
|
||||
>
|
||||
Show Warning Toast
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Info: Story = {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Toast theme="light" />
|
||||
<button
|
||||
onClick={() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.INFO,
|
||||
title: "Information",
|
||||
message: "Here's some helpful information for you.",
|
||||
})
|
||||
}
|
||||
className="rounded-sm bg-accent-primary px-4 py-2 text-13 text-on-color hover:bg-accent-primary/90"
|
||||
>
|
||||
Show Info Toast
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Toast theme="light" />
|
||||
<button
|
||||
onClick={() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.LOADING,
|
||||
title: "Loading...",
|
||||
})
|
||||
}
|
||||
className="rounded-sm border border-subtle bg-layer-2 px-4 py-2 text-13 text-primary hover:bg-layer-1"
|
||||
>
|
||||
Show Loading Toast
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithActionItems: Story = {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Toast theme="light" />
|
||||
<button
|
||||
onClick={() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "File uploaded",
|
||||
message: "Your file has been uploaded successfully.",
|
||||
actionItems: (
|
||||
<>
|
||||
<button className="text-13 font-medium text-primary hover:text-secondary">Button</button>
|
||||
<button className="text-13 font-medium text-primary hover:text-secondary">Button</button>
|
||||
</>
|
||||
),
|
||||
})
|
||||
}
|
||||
className="rounded-sm bg-success-primary px-4 py-2 text-13 text-on-color hover:bg-success-primary/90"
|
||||
>
|
||||
Show Toast with Action
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const UpdateToast: Story = {
|
||||
render() {
|
||||
const handleUpdate = () => {
|
||||
const id = setToast({
|
||||
type: TOAST_TYPE.LOADING,
|
||||
title: "Processing...",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
updateToast(id, {
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Complete!",
|
||||
message: "The operation has finished successfully.",
|
||||
});
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toast theme="light" />
|
||||
<button
|
||||
onClick={handleUpdate}
|
||||
className="rounded-sm bg-accent-primary px-4 py-2 text-13 text-on-color hover:bg-accent-primary/90"
|
||||
>
|
||||
Update Toast After 2s
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const PromiseToast: Story = {
|
||||
render() {
|
||||
const handlePromise = () => {
|
||||
const promise = new Promise<{ name?: string; error?: string }>((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
Math.random() > 0.5 ? resolve({ name: "Success data" }) : reject({ error: "Failed" });
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
setPromiseToast(promise, {
|
||||
loading: "Processing request...",
|
||||
success: {
|
||||
title: "Request completed!",
|
||||
message: (data) => `Successfully processed: ${data.name}`,
|
||||
},
|
||||
error: {
|
||||
title: "Request failed",
|
||||
message: (error) => `Error: ${error.error}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toast theme="light" />
|
||||
<button
|
||||
onClick={handlePromise}
|
||||
className="rounded-sm bg-accent-primary px-4 py-2 text-13 text-on-color hover:bg-accent-primary/90"
|
||||
>
|
||||
Show Promise Toast
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const AllTypes: Story = {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Toast theme="light" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success",
|
||||
message: "Operation successful",
|
||||
})
|
||||
}
|
||||
className="rounded-sm bg-success-primary px-3 py-2 text-13 text-on-color hover:bg-success-primary/90"
|
||||
>
|
||||
Success
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error",
|
||||
message: "Operation failed",
|
||||
})
|
||||
}
|
||||
className="rounded-sm bg-danger-primary px-3 py-2 text-13 text-on-color hover:bg-danger-primary/90"
|
||||
>
|
||||
Error
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.WARNING,
|
||||
title: "Warning",
|
||||
message: "Please be careful",
|
||||
})
|
||||
}
|
||||
className="rounded-sm bg-warning-primary px-3 py-2 text-13 text-on-color hover:bg-warning-primary/90"
|
||||
>
|
||||
Warning
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.INFO,
|
||||
title: "Info",
|
||||
message: "Here's some info",
|
||||
})
|
||||
}
|
||||
className="rounded-sm bg-accent-primary px-3 py-2 text-13 text-on-color hover:bg-accent-primary/90"
|
||||
>
|
||||
Info
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.LOADING,
|
||||
title: "Loading",
|
||||
})
|
||||
}
|
||||
className="rounded-sm border border-subtle bg-layer-2 px-3 py-2 text-13 text-primary hover:bg-layer-1"
|
||||
>
|
||||
Loading
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleToasts: Story = {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Toast theme="light" />
|
||||
<button
|
||||
onClick={() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "First toast",
|
||||
message: "This is the first toast",
|
||||
});
|
||||
setTimeout(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.INFO,
|
||||
title: "Second toast",
|
||||
message: "This is the second toast",
|
||||
});
|
||||
}, 500);
|
||||
setTimeout(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.WARNING,
|
||||
title: "Third toast",
|
||||
message: "This is the third toast",
|
||||
});
|
||||
}, 1000);
|
||||
}}
|
||||
className="rounded-sm bg-accent-primary px-4 py-2 text-13 text-on-color hover:bg-accent-primary/90"
|
||||
>
|
||||
Show Multiple Toasts
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const TitleOnly: Story = {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Toast theme="light" />
|
||||
<button
|
||||
onClick={() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Saved!",
|
||||
})
|
||||
}
|
||||
className="rounded-sm bg-success-primary px-4 py-2 text-13 text-on-color hover:bg-success-primary/90"
|
||||
>
|
||||
Show Title Only
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const LongMessage: Story = {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Toast theme="light" />
|
||||
<button
|
||||
onClick={() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.INFO,
|
||||
title: "Important Information",
|
||||
message:
|
||||
"This is a longer message that provides more detailed information about what happened and what the user should do next.",
|
||||
})
|
||||
}
|
||||
className="rounded-sm bg-accent-primary px-4 py-2 text-13 text-on-color hover:bg-accent-primary/90"
|
||||
>
|
||||
Show Long Message
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ========== Static Variants ==========
|
||||
|
||||
export const StaticVariants: Story = {
|
||||
render() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<p className="mb-2 text-13 font-medium text-secondary">All toast variants (static):</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<ToastStatic type={TOAST_TYPE.SUCCESS} title="Success" message="Your changes have been saved successfully." />
|
||||
<ToastStatic type={TOAST_TYPE.ERROR} title="Error" message="Something went wrong. Please try again." />
|
||||
<ToastStatic type={TOAST_TYPE.WARNING} title="Warning" message="This action cannot be undone." />
|
||||
<ToastStatic type={TOAST_TYPE.INFO} title="Information" message="Here's some helpful information for you." />
|
||||
<ToastStatic type={TOAST_TYPE.LOADING} title="Loading..." />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const StaticSuccess: Story = {
|
||||
render() {
|
||||
return (
|
||||
<ToastStatic
|
||||
type={TOAST_TYPE.SUCCESS}
|
||||
title="Changes saved"
|
||||
message="Your changes have been saved successfully."
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const StaticError: Story = {
|
||||
render() {
|
||||
return <ToastStatic type={TOAST_TYPE.ERROR} title="Upload failed" message="Failed to upload file. Try again." />;
|
||||
},
|
||||
};
|
||||
|
||||
export const StaticWarning: Story = {
|
||||
render() {
|
||||
return (
|
||||
<ToastStatic
|
||||
type={TOAST_TYPE.WARNING}
|
||||
title="Unsaved changes"
|
||||
message="You have unsaved changes. Save before leaving."
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const StaticInfo: Story = {
|
||||
render() {
|
||||
return (
|
||||
<ToastStatic
|
||||
type={TOAST_TYPE.INFO}
|
||||
title="New feature available"
|
||||
message="Check out our latest feature in settings."
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const StaticLoading: Story = {
|
||||
render() {
|
||||
return <ToastStatic type={TOAST_TYPE.LOADING} title="Processing your request..." />;
|
||||
},
|
||||
};
|
||||
|
||||
export const StaticWithActions: Story = {
|
||||
render() {
|
||||
return (
|
||||
<ToastStatic
|
||||
type={TOAST_TYPE.SUCCESS}
|
||||
title="File uploaded"
|
||||
message="Your file has been uploaded successfully."
|
||||
actionItems={
|
||||
<>
|
||||
<button className="text-13 font-medium text-primary hover:text-secondary">Button</button>
|
||||
<button className="text-13 font-medium text-primary hover:text-secondary">Button</button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const StaticDarkMode: Story = {
|
||||
render() {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-lg bg-canvas p-6" data-theme="dark">
|
||||
<p className="mb-2 text-13 font-medium text-secondary">Toast variants in dark mode:</p>
|
||||
<ToastStatic
|
||||
type={TOAST_TYPE.SUCCESS}
|
||||
title="Success"
|
||||
message="Operation completed successfully."
|
||||
theme="dark"
|
||||
/>
|
||||
<ToastStatic type={TOAST_TYPE.ERROR} title="Error" message="An error occurred." theme="dark" />
|
||||
<ToastStatic type={TOAST_TYPE.WARNING} title="Warning" message="Please proceed with caution." theme="dark" />
|
||||
<ToastStatic type={TOAST_TYPE.INFO} title="Information" message="Here's some useful info." theme="dark" />
|
||||
<ToastStatic type={TOAST_TYPE.LOADING} title="Loading..." theme="dark" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ========== Design Tokens Documentation ==========
|
||||
|
||||
export const DesignTokens: Story = {
|
||||
render() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="mb-2 text-16 font-semibold text-primary">Toast Design Tokens</h2>
|
||||
<p className="text-13 text-secondary">
|
||||
The toast component uses semantic design tokens from the Plane design system.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-14 font-semibold text-primary">Token Mapping by Variant</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full rounded-lg border border-subtle text-11">
|
||||
<thead>
|
||||
<tr className="bg-layer-1">
|
||||
<th className="border-b border-subtle px-3 py-2 text-left font-semibold text-primary">Variant</th>
|
||||
<th className="border-b border-subtle px-3 py-2 text-left font-semibold text-primary">Title Text</th>
|
||||
<th className="border-b border-subtle px-3 py-2 text-left font-semibold text-primary">Icon BG</th>
|
||||
<th className="border-b border-subtle px-3 py-2 text-left font-semibold text-primary">Toast BG</th>
|
||||
<th className="border-b border-subtle px-3 py-2 text-left font-semibold text-primary">Border</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b border-subtle">
|
||||
<td className="px-3 py-2 font-medium text-primary">Success</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-primary</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-success-primary</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-surface-1</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">border-subtle</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-b border-subtle">
|
||||
<td className="px-3 py-2 font-medium text-primary">Error</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-primary</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-danger-primary</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-surface-1</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">border-subtle</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-b border-subtle">
|
||||
<td className="px-3 py-2 font-medium text-primary">Warning</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-primary</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-warning-primary</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-surface-1</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">border-subtle</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-b border-subtle">
|
||||
<td className="px-3 py-2 font-medium text-primary">Info</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-primary</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-accent-primary</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-surface-1</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">border-subtle</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-medium text-primary">Loading</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-primary</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-layer-2</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">bg-surface-1</code>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">border-subtle</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h3 className="mb-3 text-14 font-semibold text-primary">Typography</h3>
|
||||
<ul className="space-y-2 text-12 text-secondary">
|
||||
<li>
|
||||
<span className="font-medium text-primary">Title:</span>{" "}
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-14 font-semibold</code>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-primary">Message:</span>{" "}
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-13</code>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-primary">Message Color:</span>{" "}
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-secondary</code>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-primary">Action Button:</span>{" "}
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-13 font-medium</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-14 font-semibold text-primary">Dimensions & Styling</h3>
|
||||
<ul className="space-y-2 text-12 text-secondary">
|
||||
<li>
|
||||
<span className="font-medium text-primary">Width:</span> 350px
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-primary">Padding:</span> 16px (p-4)
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-primary">Border Radius:</span> 8px (rounded-lg)
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-primary">Shadow:</span>{" "}
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">shadow-raised-200</code>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-primary">Border Width:</span> 1px
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-14 font-semibold text-primary">Icon Specifications</h3>
|
||||
<ul className="space-y-2 text-12 text-secondary">
|
||||
<li>
|
||||
<span className="font-medium text-primary">Icon Size:</span> 20x20px
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-primary">Icon Stroke Width:</span> 2px
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-primary">Icon Container:</span> 40x40px circular (w-10 h-10
|
||||
rounded-full)
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-primary">Icon Color:</span>{" "}
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">text-on-color</code>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-primary">Icon Background:</span>{" "}
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">
|
||||
bg-{"{"}variant{"}"}-primary
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-primary">Close Icon Size:</span> 16x16px
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-primary">Close Icon Color:</span>{" "}
|
||||
<code className="rounded bg-layer-1 px-1.5 py-0.5 text-10">
|
||||
text-icon-secondary hover:text-icon-tertiary
|
||||
</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-layer-1 p-4">
|
||||
<h3 className="mb-2 text-14 font-semibold text-primary">Visual Examples</h3>
|
||||
<p className="mb-4 text-12 text-secondary">See how the tokens are applied across all toast variants:</p>
|
||||
<div className="flex flex-col gap-3">
|
||||
<ToastStatic
|
||||
type={TOAST_TYPE.SUCCESS}
|
||||
title="Success"
|
||||
message="Your changes have been saved successfully."
|
||||
/>
|
||||
<ToastStatic type={TOAST_TYPE.ERROR} title="Error" message="Something went wrong. Please try again." />
|
||||
<ToastStatic type={TOAST_TYPE.WARNING} title="Warning" message="This action cannot be undone." />
|
||||
<ToastStatic type={TOAST_TYPE.INFO} title="Information" message="Here's some helpful information." />
|
||||
<ToastStatic type={TOAST_TYPE.LOADING} title="Loading..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { Toast as BaseToast } from "@base-ui-components/react/toast";
|
||||
import { AlertTriangle, CheckIcon, InfoIcon, XIcon } from "lucide-react";
|
||||
import { CloseIcon } from "../icons/actions/close-icon";
|
||||
// spinner
|
||||
import { CircularBarSpinner } from "../spinners/circular-bar-spinner";
|
||||
import { cn } from "../utils/classname";
|
||||
|
||||
export enum TOAST_TYPE {
|
||||
SUCCESS = "success",
|
||||
ERROR = "error",
|
||||
INFO = "info",
|
||||
WARNING = "warning",
|
||||
LOADING = "loading",
|
||||
LOADING_TOAST = "loading-toast",
|
||||
}
|
||||
|
||||
type SetToastProps =
|
||||
| {
|
||||
type: TOAST_TYPE.LOADING;
|
||||
title?: string;
|
||||
}
|
||||
| {
|
||||
id?: string | number;
|
||||
type: Exclude<TOAST_TYPE, TOAST_TYPE.LOADING>;
|
||||
title: string;
|
||||
message?: string;
|
||||
actionItems?: React.ReactNode;
|
||||
};
|
||||
|
||||
type PromiseToastCallback<ToastData> = (data: ToastData) => string;
|
||||
type ActionItemsPromiseToastCallback<ToastData> = (data: ToastData) => React.ReactNode;
|
||||
|
||||
type PromiseToastData<ToastData> = {
|
||||
title: string;
|
||||
message?: PromiseToastCallback<ToastData>;
|
||||
actionItems?: ActionItemsPromiseToastCallback<ToastData>;
|
||||
};
|
||||
|
||||
type PromiseToastOptions<ToastData> = {
|
||||
loading?: string;
|
||||
success: PromiseToastData<ToastData>;
|
||||
error: PromiseToastData<ToastData>;
|
||||
};
|
||||
|
||||
export type ToastProps = {
|
||||
theme: "light" | "dark" | "system";
|
||||
};
|
||||
|
||||
const toastManager = BaseToast.createToastManager();
|
||||
|
||||
export function Toast(props: ToastProps) {
|
||||
return (
|
||||
<BaseToast.Provider toastManager={toastManager}>
|
||||
<BaseToast.Portal>
|
||||
<BaseToast.Viewport data-theme={props.theme}>
|
||||
<ToastList />
|
||||
</BaseToast.Viewport>
|
||||
</BaseToast.Portal>
|
||||
</BaseToast.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const TOAST_DATA = {
|
||||
[TOAST_TYPE.SUCCESS]: {
|
||||
icon: <CheckIcon width={12} height={12} className="text-on-color" />,
|
||||
iconBgClassName: "bg-success-primary",
|
||||
backgroundColorClassName: "!bg-surface-1",
|
||||
borderColorClassName: "border-subtle",
|
||||
},
|
||||
[TOAST_TYPE.ERROR]: {
|
||||
icon: <XIcon width={12} height={12} className="text-on-color" />,
|
||||
iconBgClassName: "bg-danger-primary",
|
||||
backgroundColorClassName: "bg-surface-1",
|
||||
borderColorClassName: "border-subtle",
|
||||
},
|
||||
[TOAST_TYPE.WARNING]: {
|
||||
icon: <AlertTriangle width={12} height={12} className="text-on-color" />,
|
||||
iconBgClassName: "bg-warning-primary",
|
||||
backgroundColorClassName: "bg-surface-1",
|
||||
borderColorClassName: "border-subtle",
|
||||
},
|
||||
[TOAST_TYPE.INFO]: {
|
||||
icon: <InfoIcon width={12} height={12} className="text-on-color" />,
|
||||
iconBgClassName: "bg-accent-primary",
|
||||
backgroundColorClassName: "bg-surface-1",
|
||||
borderColorClassName: "border-subtle",
|
||||
},
|
||||
[TOAST_TYPE.LOADING]: {
|
||||
icon: <CircularBarSpinner className="text-on-color" />,
|
||||
iconBgClassName: "bg-layer-2",
|
||||
backgroundColorClassName: "bg-surface-1",
|
||||
borderColorClassName: "border-subtle",
|
||||
},
|
||||
[TOAST_TYPE.LOADING_TOAST]: {
|
||||
icon: <CircularBarSpinner className="text-on-color" />,
|
||||
iconBgClassName: "bg-layer-2",
|
||||
backgroundColorClassName: "bg-surface-1",
|
||||
borderColorClassName: "border-subtle",
|
||||
},
|
||||
};
|
||||
|
||||
function ToastList() {
|
||||
const { toasts } = BaseToast.useToastManager();
|
||||
return toasts.map((toast) => <ToastRender key={toast.id} id={toast.id} toast={toast} />);
|
||||
}
|
||||
|
||||
function ToastRender({ id, toast }: { id: React.Key; toast: BaseToast.Root.ToastObject }) {
|
||||
const toastData = toast.data as SetToastProps;
|
||||
const type = toastData.type as TOAST_TYPE;
|
||||
const data = TOAST_DATA[type];
|
||||
|
||||
return (
|
||||
<BaseToast.Root
|
||||
toast={toast}
|
||||
key={id}
|
||||
className={cn(
|
||||
// Base layout and positioning
|
||||
"group flex w-[350px] items-center rounded-lg border shadow-raised-200",
|
||||
"absolute right-3 bottom-3 z-[calc(1000-var(--toast-index))]",
|
||||
"ease-[cubic-bezier(0.22,1,0.36,1)] transition-[opacity,transform] duration-500 select-none",
|
||||
|
||||
// Default transform with stacking and scaling
|
||||
"[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+calc(min(var(--toast-index),10)*-10px)))_scale(calc(max(0,1-(var(--toast-index)*0.1))))]",
|
||||
|
||||
// Pseudo-element for gap spacing
|
||||
"after:absolute after:bottom-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
|
||||
|
||||
// State-based opacity
|
||||
"data-[ending-style]:opacity-0 data-[limited]:opacity-0",
|
||||
|
||||
// Starting animation
|
||||
"data-[starting-style]:[transform:translateY(150%)]",
|
||||
|
||||
// Expanded state transform
|
||||
"data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y)))]",
|
||||
|
||||
// Swipe direction endings - consolidated
|
||||
"data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]",
|
||||
"data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]",
|
||||
"data-[ending-style]:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]",
|
||||
"data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]",
|
||||
|
||||
// Default ending transform for non-limited toasts
|
||||
"data-[ending-style]:[&:not([data-limited])]:[transform:translateY(150%)]",
|
||||
|
||||
data.backgroundColorClassName,
|
||||
data.borderColorClassName
|
||||
)}
|
||||
style={{
|
||||
["--gap" as string]: "1rem",
|
||||
["--offset-y" as string]:
|
||||
"calc(var(--toast-offset-y) * -1 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y))",
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<BaseToast.Close className="absolute top-3 right-3 cursor-pointer text-icon-secondary hover:text-icon-tertiary">
|
||||
<CloseIcon strokeWidth={1.5} width={16} height={16} />
|
||||
</BaseToast.Close>
|
||||
<div className="flex w-full items-start gap-2 p-4">
|
||||
<div className="py-1">
|
||||
{data.icon && (
|
||||
<div
|
||||
className={cn("flex size-4 flex-shrink-0 items-center justify-center rounded-full", data.iconBgClassName)}
|
||||
>
|
||||
{data.icon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<BaseToast.Title className="text-h6-medium text-primary">
|
||||
{toastData.type === TOAST_TYPE.LOADING ? (toastData.title ?? "Loading...") : toastData.title}
|
||||
</BaseToast.Title>
|
||||
{toastData.type !== TOAST_TYPE.LOADING && toastData.message && (
|
||||
<BaseToast.Description className="text-body-xs-regular text-tertiary">
|
||||
{toastData.message}
|
||||
</BaseToast.Description>
|
||||
)}
|
||||
{toastData.type !== TOAST_TYPE.LOADING && toastData.actionItems && (
|
||||
<div className="flex items-center gap-2">{toastData.actionItems}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BaseToast.Root>
|
||||
);
|
||||
}
|
||||
|
||||
// Static toast component for Storybook and documentation
|
||||
export type ToastStaticProps = {
|
||||
type: TOAST_TYPE;
|
||||
title: string;
|
||||
message?: string;
|
||||
actionItems?: React.ReactNode;
|
||||
theme?: "light" | "dark";
|
||||
};
|
||||
|
||||
export function ToastStatic({ type, title, message, actionItems, theme = "light" }: ToastStaticProps) {
|
||||
const data = TOAST_DATA[type];
|
||||
|
||||
return (
|
||||
<div data-theme={theme} className="inline-block">
|
||||
<div
|
||||
className={cn(
|
||||
// Base layout and positioning
|
||||
"group flex w-[350px] items-start rounded-lg border border-subtle-1 shadow-overlay-100",
|
||||
"relative",
|
||||
data.backgroundColorClassName,
|
||||
data.borderColorClassName
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-1 right-1 cursor-default text-icon-tertiary">
|
||||
<CloseIcon strokeWidth={1.5} width={14} height={14} />
|
||||
</div>
|
||||
<div className="flex w-full items-start gap-3 p-4">
|
||||
<div className="py-1">
|
||||
{data.icon && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-4 flex-shrink-0 items-center justify-center rounded-full",
|
||||
data.iconBgClassName
|
||||
)}
|
||||
>
|
||||
{data.icon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<div className="text-h6-medium text-primary">
|
||||
{type === TOAST_TYPE.LOADING ? (title ?? "Loading...") : title}
|
||||
</div>
|
||||
{type !== TOAST_TYPE.LOADING && message && (
|
||||
<div className="text-body-xs-regular text-tertiary">{message}</div>
|
||||
)}
|
||||
{type !== TOAST_TYPE.LOADING && actionItems && <div className="flex items-center gap-2">{actionItems}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const setToast = (props: SetToastProps) => {
|
||||
let toastId: string | undefined;
|
||||
if (props.type !== TOAST_TYPE.LOADING) {
|
||||
toastId = toastManager.add({
|
||||
data: {
|
||||
type: props.type,
|
||||
title: props.title,
|
||||
message: props.message,
|
||||
actionItems: props.actionItems,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
toastId = toastManager.add({
|
||||
data: {
|
||||
type: props.type,
|
||||
title: props.title,
|
||||
},
|
||||
});
|
||||
}
|
||||
return toastId;
|
||||
};
|
||||
|
||||
export const updateToast = (id: string, props: SetToastProps) => {
|
||||
toastManager.update(id, {
|
||||
data:
|
||||
props.type === TOAST_TYPE.LOADING
|
||||
? {
|
||||
type: TOAST_TYPE.LOADING,
|
||||
title: props.title,
|
||||
}
|
||||
: {
|
||||
type: props.type,
|
||||
title: props.title,
|
||||
message: props.message,
|
||||
actionItems: props.actionItems,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const setPromiseToast = <ToastData,>(
|
||||
promise: Promise<ToastData>,
|
||||
options: PromiseToastOptions<ToastData>
|
||||
): void => {
|
||||
toastManager.promise(promise, {
|
||||
loading: {
|
||||
data: {
|
||||
title: options.loading ?? "Loading...",
|
||||
type: TOAST_TYPE.LOADING,
|
||||
message: undefined,
|
||||
actionItems: undefined,
|
||||
},
|
||||
},
|
||||
success: (data) => ({
|
||||
data: {
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: options.success.title,
|
||||
message: options.success.message?.(data),
|
||||
actionItems: options.success.actionItems?.(data),
|
||||
},
|
||||
}),
|
||||
error: (data) => ({
|
||||
data: {
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: options.error.title,
|
||||
message: options.error.message?.(data),
|
||||
actionItems: options.error.actionItems?.(data),
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const dismissToast = (tId: string) => {
|
||||
toastManager.close(tId);
|
||||
};
|
||||
Reference in New Issue
Block a user