✨(react) add Toast component
This component allows to create dynamic Toast appearing at the bottom of the screen for few seconds.
This commit is contained in:
145
packages/react/src/components/Toast/ToastProvider.tsx
Normal file
145
packages/react/src/components/Toast/ToastProvider.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import React, { PropsWithChildren, useContext, useMemo, useRef } from "react";
|
||||
import { Toast, ToastProps } from ":/components/Toast/index";
|
||||
import { tokens } from ":/cunningham-tokens";
|
||||
import { Queue } from ":/utils/Queue";
|
||||
|
||||
export enum ToastType {
|
||||
INFO = "info",
|
||||
SUCCESS = "success",
|
||||
WARNING = "warning",
|
||||
ERROR = "error",
|
||||
NEUTRAL = "neutral",
|
||||
}
|
||||
|
||||
export interface ToastProviderContext {
|
||||
toast: (
|
||||
message: string,
|
||||
type?: ToastType,
|
||||
options?: Partial<Omit<ToastInterface, "message" | "type">>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
const ToastContext = React.createContext<ToastProviderContext>(
|
||||
undefined as any,
|
||||
);
|
||||
|
||||
export const useToastProvider = () => {
|
||||
const context = useContext(ToastContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useToastProvider must be used within a ToastProvider.");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
type ToastInterface = ToastProps & {
|
||||
i: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
let toastsCounter = 1;
|
||||
const DEFAULT_TOAST_DURATION = 6000;
|
||||
|
||||
const getSlideInDuration = (): number => {
|
||||
return parseInt(
|
||||
tokens.themes.default.components.toast["slide-in-duration"].replace(
|
||||
"ms",
|
||||
"",
|
||||
),
|
||||
10,
|
||||
);
|
||||
};
|
||||
|
||||
export const ToastProvider = ({ children }: PropsWithChildren) => {
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const previousContainerHeight = useRef(0);
|
||||
const [list, setList] = React.useState<ToastInterface[]>([]);
|
||||
const queue = useRef(new Queue());
|
||||
|
||||
const context: ToastProviderContext = useMemo(
|
||||
() => ({
|
||||
toast: (message, type = ToastType.NEUTRAL, options = {}) => {
|
||||
// We want to wait for the previous toast to be added ( taking into account the animation )
|
||||
// before adding a new one, that's why we use a queue.
|
||||
queue.current?.push(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
// We need to snapshot the value as the setList execution is async.
|
||||
const currentIndex = toastsCounter;
|
||||
toastsCounter += 1;
|
||||
previousContainerHeight.current = container.current!.offsetHeight;
|
||||
setList((currentList) => {
|
||||
return [
|
||||
...currentList,
|
||||
{
|
||||
...options,
|
||||
message,
|
||||
i: currentIndex,
|
||||
type,
|
||||
duration: options?.duration ?? DEFAULT_TOAST_DURATION,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// We consider that the toast is added when its animation is done.
|
||||
resolve();
|
||||
}, getSlideInDuration());
|
||||
}),
|
||||
);
|
||||
},
|
||||
}),
|
||||
[container, previousContainerHeight, list],
|
||||
);
|
||||
|
||||
const animateContainer = async () => {
|
||||
// FLIP pattern: https://aerotwist.com/blog/flip-your-animations/
|
||||
// FIRST
|
||||
const first = previousContainerHeight.current;
|
||||
|
||||
// LAST
|
||||
const last = container.current!.offsetHeight;
|
||||
|
||||
// INVERT
|
||||
const invert = last - first;
|
||||
|
||||
// PLAY
|
||||
container.current!.animate(
|
||||
[
|
||||
{ transform: `translateY(${invert}px)` },
|
||||
{ transform: "translateY(0)" },
|
||||
],
|
||||
{
|
||||
duration: getSlideInDuration(),
|
||||
easing: "ease",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// We only want this to be triggered when an item gets ADDED to the list, not when
|
||||
// it gets removed, that's why we use toastsCounter as a dependency and not only
|
||||
// list.
|
||||
React.useEffect(() => {
|
||||
animateContainer();
|
||||
}, [toastsCounter]);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={context}>
|
||||
{children}
|
||||
<div className="c__toast__container" ref={container}>
|
||||
{list.map((toast) => (
|
||||
<Toast
|
||||
key={toast.i}
|
||||
onDelete={() => {
|
||||
setList((value) => {
|
||||
return value.filter((t) => t.i !== toast.i);
|
||||
});
|
||||
}}
|
||||
{...toast}
|
||||
>
|
||||
{toast.message}
|
||||
</Toast>
|
||||
))}
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
};
|
||||
153
packages/react/src/components/Toast/index.scss
Normal file
153
packages/react/src/components/Toast/index.scss
Normal file
@@ -0,0 +1,153 @@
|
||||
@use "../../utils/responsive" as *;
|
||||
|
||||
.c__toast__container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding-left: 3rem;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
@include media(sm) {
|
||||
.c__toast__container {
|
||||
padding: 0.5rem;
|
||||
right: 0;
|
||||
|
||||
.c__toast {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c__toast {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 6px 2px #0C1A2B26;
|
||||
background-color: var(--c--components--toast--background-color);
|
||||
min-width: 18rem;
|
||||
max-width: 27rem;
|
||||
overflow: hidden;
|
||||
will-change: transform;
|
||||
color: var(--c--components--toast--color);
|
||||
font-weight: var(--c--components--toast--font-weight);
|
||||
|
||||
&:not(.c__toast--no-animate) {
|
||||
animation: slide-in var(--c--components--toast--slide-in-duration) ease;
|
||||
}
|
||||
|
||||
&--disappear:not(.c__toast--no-animate) {
|
||||
animation: slide-out var(--c--components--toast--slide-out-duration) ease forwards;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
span {
|
||||
font-size: var(--c--components--toast--icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&__buttons {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__children {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.c__progress-bar {
|
||||
--c--progress--color: var(--c--theme--colors--greyscale-600);
|
||||
}
|
||||
|
||||
&--info {
|
||||
.c__progress-bar {
|
||||
--c--progress--color: var(--c--theme--colors--primary-500);
|
||||
}
|
||||
|
||||
.c__toast__icon {
|
||||
color: var(--c--theme--colors--primary-600);
|
||||
}
|
||||
}
|
||||
|
||||
&--success {
|
||||
.c__progress-bar {
|
||||
--c--progress--color: var(--c--theme--colors--success-500);
|
||||
}
|
||||
|
||||
.c__toast__icon {
|
||||
color: var(--c--theme--colors--success-600);
|
||||
}
|
||||
}
|
||||
|
||||
&--warning {
|
||||
.c__progress-bar {
|
||||
--c--progress--color: var(--c--theme--colors--warning-500);
|
||||
}
|
||||
|
||||
.c__toast__icon {
|
||||
color: var(--c--theme--colors--warning-600);
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
.c__progress-bar {
|
||||
--c--progress--color: var(--c--theme--colors--danger-500);
|
||||
}
|
||||
|
||||
.c__toast__icon {
|
||||
color: var(--c--theme--colors--danger-600);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@keyframes slide-out {
|
||||
from {
|
||||
transform: translateX(0)
|
||||
}
|
||||
to {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
transform: translateY(200px)
|
||||
}
|
||||
}
|
||||
|
||||
.c__progress-bar {
|
||||
height: var(--c--components--toast--progress-bar-height);
|
||||
--c--progress--color: var(--c--theme--colors--primary-500);
|
||||
|
||||
&__content {
|
||||
height: 100%;
|
||||
background-color: var(--c--progress--color);
|
||||
}
|
||||
}
|
||||
|
||||
162
packages/react/src/components/Toast/index.spec.tsx
Normal file
162
packages/react/src/components/Toast/index.spec.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
} from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { within } from "@testing-library/dom";
|
||||
import { CunninghamProvider } from ":/components/Provider";
|
||||
import { ToastType, useToastProvider } from ":/components/Toast/ToastProvider";
|
||||
import { Button } from ":/components/Button";
|
||||
|
||||
describe("<Toast />", () => {
|
||||
const Wrapper = ({ children }: PropsWithChildren) => {
|
||||
return <CunninghamProvider>{children}</CunninghamProvider>;
|
||||
};
|
||||
|
||||
it("shows a toast when clicking on the button and disappears", async () => {
|
||||
const Inner = () => {
|
||||
const { toast } = useToastProvider();
|
||||
return (
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast("Toast content", ToastType.NEUTRAL, { duration: 10 })
|
||||
}
|
||||
>
|
||||
Create toast
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
render(<Inner />, { wrapper: Wrapper });
|
||||
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByText("Create toast");
|
||||
|
||||
// No toast displayed.
|
||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(button);
|
||||
|
||||
// Toast displayed.
|
||||
const toast = await screen.findByRole("alert");
|
||||
expect(toast).toHaveTextContent("Toast content");
|
||||
|
||||
await waitForElementToBeRemoved(toast);
|
||||
});
|
||||
|
||||
it("shows a toast with a primary button", async () => {
|
||||
let flag = false;
|
||||
const Inner = () => {
|
||||
const { toast } = useToastProvider();
|
||||
return (
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast("Toast content", ToastType.NEUTRAL, {
|
||||
primaryLabel: "Action",
|
||||
primaryOnClick: () => {
|
||||
flag = true;
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Create toast
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
render(<Inner />, { wrapper: Wrapper });
|
||||
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByText("Create toast");
|
||||
|
||||
// No toast displayed.
|
||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(button);
|
||||
|
||||
// Toast displayed.
|
||||
const toast = await screen.findByRole("alert");
|
||||
expect(toast).toHaveTextContent("Toast content");
|
||||
// Toast has a button.
|
||||
const $button = within(toast).getByRole("button", { name: "Action" });
|
||||
|
||||
// Button is not clicked yet.
|
||||
expect(flag).toBe(false);
|
||||
await user.click($button);
|
||||
// Button is clicked.
|
||||
await waitFor(() => expect(flag).toBe(true));
|
||||
});
|
||||
it("shows a toast with custom buttons", async () => {
|
||||
const Inner = () => {
|
||||
const { toast } = useToastProvider();
|
||||
return (
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast("Toast content", ToastType.NEUTRAL, {
|
||||
actions: <Button color="tertiary">Tertiary</Button>,
|
||||
})
|
||||
}
|
||||
>
|
||||
Create toast
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
render(<Inner />, { wrapper: Wrapper });
|
||||
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByText("Create toast");
|
||||
|
||||
// No toast displayed.
|
||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(button);
|
||||
|
||||
// Toast displayed.
|
||||
const toast = await screen.findByRole("alert");
|
||||
expect(toast).toHaveTextContent("Toast content");
|
||||
// Toast has custom button.
|
||||
within(toast).getByRole("button", { name: "Tertiary" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
[ToastType.INFO, "info"],
|
||||
[ToastType.SUCCESS, "task_alt"],
|
||||
[ToastType.WARNING, "warning"],
|
||||
[ToastType.ERROR, "cancel"],
|
||||
[ToastType.NEUTRAL, undefined],
|
||||
])("shows a %s toast", async (type, iconName) => {
|
||||
const Inner = () => {
|
||||
const { toast } = useToastProvider();
|
||||
return (
|
||||
<Button onClick={() => toast("Toast content", type)}>
|
||||
Create toast
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
render(<Inner />, { wrapper: Wrapper });
|
||||
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByText("Create toast");
|
||||
|
||||
// No toast displayed.
|
||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(button);
|
||||
|
||||
// Toast displayed.
|
||||
const toast = await screen.findByRole("alert");
|
||||
expect(toast).toHaveTextContent("Toast content");
|
||||
if (iconName === undefined) {
|
||||
const icon = document.querySelector(".c__toast__icon");
|
||||
expect(icon).not.toBeInTheDocument();
|
||||
} else {
|
||||
const icon = document.querySelector(".c__toast__icon");
|
||||
expect(icon).toHaveTextContent(iconName);
|
||||
}
|
||||
});
|
||||
});
|
||||
120
packages/react/src/components/Toast/index.stories.tsx
Normal file
120
packages/react/src/components/Toast/index.stories.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import React, { useEffect } from "react";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { ProgressBar, Toast } from ":/components/Toast/index";
|
||||
import { Button } from ":/components/Button";
|
||||
import { ToastType, useToastProvider } from ":/components/Toast/ToastProvider";
|
||||
|
||||
const meta: Meta<typeof Toast> = {
|
||||
title: "Components/Toast",
|
||||
component: Toast,
|
||||
args: {
|
||||
children: "Corrupti vestigium aiunt aeneus demulceo consequatur.",
|
||||
duration: 30000,
|
||||
disableAnimate: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Toast>;
|
||||
|
||||
export const Demo: Story = {
|
||||
render: () => {
|
||||
const { toast } = useToastProvider();
|
||||
const TYPES = [
|
||||
ToastType.INFO,
|
||||
ToastType.SUCCESS,
|
||||
ToastType.WARNING,
|
||||
ToastType.ERROR,
|
||||
ToastType.NEUTRAL,
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
toast(faker.lorem.sentence({ min: 5, max: 10 }), ToastType.SUCCESS, {
|
||||
primaryLabel: "Read more",
|
||||
primaryOnClick: () => {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert("Clicked here !");
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ height: "300px" }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const type = TYPES[Math.floor(Math.random() * TYPES.length)];
|
||||
toast(faker.lorem.sentence({ min: 5, max: 10 }), type, {
|
||||
primaryLabel: "Primary",
|
||||
primaryOnClick: () => {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert("Clicked here !");
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Create toast!
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Info: Story = {
|
||||
args: {
|
||||
type: ToastType.INFO,
|
||||
},
|
||||
};
|
||||
|
||||
export const InfoWithButton: Story = {
|
||||
args: {
|
||||
type: ToastType.INFO,
|
||||
primaryLabel: "Primary",
|
||||
},
|
||||
};
|
||||
|
||||
export const InfoCustom: Story = {
|
||||
args: {
|
||||
type: ToastType.INFO,
|
||||
actions: (
|
||||
<>
|
||||
<Button color="primary">Primary</Button>
|
||||
<Button color="secondary">Secondary</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const Success: Story = {
|
||||
args: {
|
||||
type: ToastType.SUCCESS,
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
type: ToastType.WARNING,
|
||||
},
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
type: ToastType.ERROR,
|
||||
},
|
||||
};
|
||||
|
||||
export const Neutral: Story = {
|
||||
args: {
|
||||
type: ToastType.NEUTRAL,
|
||||
},
|
||||
};
|
||||
|
||||
export const ProgressBarExample: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<div>
|
||||
<ProgressBar duration={6000} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
120
packages/react/src/components/Toast/index.tsx
Normal file
120
packages/react/src/components/Toast/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, {
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react";
|
||||
import classNames from "classnames";
|
||||
import { ToastType } from ":/components/Toast/ToastProvider";
|
||||
import { iconFromType } from ":/components/Alert/Utils";
|
||||
import { Button, ButtonProps } from ":/components/Button";
|
||||
|
||||
export interface ToastProps extends PropsWithChildren {
|
||||
duration: number;
|
||||
type: ToastType;
|
||||
onDelete?: () => void;
|
||||
icon?: ReactNode;
|
||||
primaryLabel?: string;
|
||||
primaryOnClick?: ButtonProps["onClick"];
|
||||
primaryProps?: ButtonProps;
|
||||
disableAnimate?: boolean;
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
export const Toast = (props: ToastProps) => {
|
||||
const [animateDisappear, setAnimateDisappear] = React.useState(false);
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const disappearTimeout = useRef<NodeJS.Timeout>();
|
||||
|
||||
// Register a timeout to remove the toast after the duration.
|
||||
useEffect(() => {
|
||||
if (props.disableAnimate) {
|
||||
return;
|
||||
}
|
||||
disappearTimeout.current = setTimeout(async () => {
|
||||
setAnimateDisappear(true);
|
||||
disappearTimeout.current = undefined;
|
||||
}, props.duration);
|
||||
return () => {
|
||||
if (disappearTimeout.current) {
|
||||
clearTimeout(disappearTimeout.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const removeAfterAnimation = async () => {
|
||||
await Promise.allSettled(
|
||||
container.current!.getAnimations().map((animation) => animation.finished),
|
||||
);
|
||||
props.onDelete?.();
|
||||
};
|
||||
|
||||
// Remove the toast after the animation finishes.
|
||||
useEffect(() => {
|
||||
if (animateDisappear) {
|
||||
removeAfterAnimation();
|
||||
}
|
||||
}, [animateDisappear]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={container}
|
||||
className={classNames("c__toast", "c__toast--" + props.type, {
|
||||
"c__toast--disappear": animateDisappear,
|
||||
"c__toast--no-animate": props.disableAnimate,
|
||||
})}
|
||||
role="alert"
|
||||
>
|
||||
<ProgressBar duration={props.duration} />
|
||||
<div className="c__toast__content">
|
||||
{props.primaryLabel && (
|
||||
<div className="c__toast__content__buttons">
|
||||
<Button
|
||||
color="primary-text"
|
||||
onClick={props.primaryOnClick}
|
||||
{...props.primaryProps}
|
||||
>
|
||||
{props.primaryLabel}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{props.actions}
|
||||
<div className="c__toast__content__children">{props.children}</div>
|
||||
<ToastIcon {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToastIcon = ({ type, ...props }: ToastProps) => {
|
||||
const icon = useMemo(() => iconFromType(type), [type]);
|
||||
if (props.icon) {
|
||||
return props.icon;
|
||||
}
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="c__toast__icon">
|
||||
<span className="material-icons">{icon}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProgressBar = ({ duration }: { duration: number }) => {
|
||||
const content = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
content.current!.animate([{ width: "0%" }, { width: "100%" }], {
|
||||
duration,
|
||||
easing: "linear",
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="c__progress-bar">
|
||||
<div className="c__progress-bar__content" ref={content} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
13
packages/react/src/components/Toast/tokens.ts
Normal file
13
packages/react/src/components/Toast/tokens.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { DefaultTokens } from "@openfun/cunningham-tokens";
|
||||
|
||||
export const tokens = (defaults: DefaultTokens) => {
|
||||
return {
|
||||
"slide-in-duration": "1000ms",
|
||||
"slide-out-duration": "300ms",
|
||||
"background-color": defaults.theme.colors["greyscale-100"],
|
||||
color: defaults.theme.colors["greyscale-900"],
|
||||
"font-weight": defaults.theme.font.weights.regular,
|
||||
"icon-size": defaults.theme.font.sizes.l,
|
||||
"progress-bar-height": "3px",
|
||||
};
|
||||
};
|
||||
@@ -112,6 +112,13 @@
|
||||
--c--theme--breakpoints--lg: 992px;
|
||||
--c--theme--breakpoints--xl: 1200px;
|
||||
--c--theme--breakpoints--xxl: 1400px;
|
||||
--c--components--toast--slide-in-duration: 1000ms;
|
||||
--c--components--toast--slide-out-duration: 300ms;
|
||||
--c--components--toast--background-color: var(--c--theme--colors--greyscale-100);
|
||||
--c--components--toast--color: var(--c--theme--colors--greyscale-900);
|
||||
--c--components--toast--font-weight: var(--c--theme--font--weights--regular);
|
||||
--c--components--toast--icon-size: var(--c--theme--font--sizes--l);
|
||||
--c--components--toast--progress-bar-height: 3px;
|
||||
--c--components--forms-textarea--font-weight: var(--c--theme--font--weights--regular);
|
||||
--c--components--forms-textarea--font-size: var(--c--theme--font--sizes--l);
|
||||
--c--components--forms-textarea--border-radius: 8px;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -134,6 +134,15 @@ $themes: (
|
||||
)
|
||||
),
|
||||
'components': (
|
||||
'toast': (
|
||||
'slide-in-duration': 1000ms,
|
||||
'slide-out-duration': 300ms,
|
||||
'background-color': #FAFAFB,
|
||||
'color': #0C1A2B,
|
||||
'font-weight': 400,
|
||||
'icon-size': 1rem,
|
||||
'progress-bar-height': 3px
|
||||
),
|
||||
'forms-textarea': (
|
||||
'font-weight': 400,
|
||||
'font-size': 1rem,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -21,6 +21,7 @@
|
||||
@use "./components/Loader";
|
||||
@use "./components/Pagination";
|
||||
@use "./components/Popover";
|
||||
@use "./components/Toast";
|
||||
|
||||
body {
|
||||
font-family: var(--c--theme--font--families--base);
|
||||
|
||||
@@ -20,6 +20,8 @@ export * from "./components/Loader";
|
||||
export * from "./components/Pagination";
|
||||
export * from "./components/Popover";
|
||||
export * from "./components/Provider";
|
||||
export * from "./components/Toast";
|
||||
export * from "./components/Toast/ToastProvider";
|
||||
|
||||
export type DefaultTokens = PartialNested<typeof tokens.themes.default>;
|
||||
export const defaultTokens = tokens.themes.default;
|
||||
|
||||
Reference in New Issue
Block a user