mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
chore(frontend): Restyle toasts and replace all current instances with new one (#6854)
This commit is contained in:
parent
a4908f9a75
commit
83851c398d
@ -2,11 +2,13 @@ import { createRoutesStub } from "react-router";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import toast from "react-hot-toast";
|
||||
import App from "#/routes/_oh.app/route";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import * as CustomToast from "#/utils/custom-toast-handlers";
|
||||
|
||||
describe("App", () => {
|
||||
const errorToastSpy = vi.spyOn(CustomToast, "displayErrorToast");
|
||||
|
||||
const RouteStub = createRoutesStub([
|
||||
{ Component: App, path: "/conversation/:conversationId" },
|
||||
]);
|
||||
@ -34,26 +36,19 @@ describe("App", () => {
|
||||
await screen.findByTestId("app-route");
|
||||
});
|
||||
|
||||
it(
|
||||
"should call endSession if the user does not have permission to view conversation",
|
||||
async () => {
|
||||
const errorToastSpy = vi.spyOn(toast, "error");
|
||||
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
|
||||
it("should call endSession if the user does not have permission to view conversation", async () => {
|
||||
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
|
||||
|
||||
getConversationSpy.mockResolvedValue(null);
|
||||
renderWithProviders(
|
||||
<RouteStub initialEntries={["/conversation/9999"]} />,
|
||||
);
|
||||
getConversationSpy.mockResolvedValue(null);
|
||||
renderWithProviders(<RouteStub initialEntries={["/conversation/9999"]} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(endSessionMock).toHaveBeenCalledOnce();
|
||||
expect(errorToastSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
},
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(endSessionMock).toHaveBeenCalledOnce();
|
||||
expect(errorToastSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call endSession if the user has permission", async () => {
|
||||
const errorToastSpy = vi.spyOn(toast, "error");
|
||||
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
|
||||
|
||||
getConversationSpy.mockResolvedValue({
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { trackError, showErrorToast, showChatError } from "#/utils/error-handler";
|
||||
import posthog from "posthog-js";
|
||||
import toast from "react-hot-toast";
|
||||
import {
|
||||
trackError,
|
||||
showErrorToast,
|
||||
showChatError,
|
||||
} from "#/utils/error-handler";
|
||||
import * as Actions from "#/services/actions";
|
||||
import * as CustomToast from "#/utils/custom-toast-handlers";
|
||||
|
||||
vi.mock("posthog-js", () => ({
|
||||
default: {
|
||||
@ -10,12 +14,6 @@ vi.mock("posthog-js", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("#/services/actions", () => ({
|
||||
handleStatusMessage: vi.fn(),
|
||||
}));
|
||||
@ -38,9 +36,12 @@ describe("Error Handler", () => {
|
||||
|
||||
trackError(error);
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Test error"), {
|
||||
error_source: "test",
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Test error"),
|
||||
{
|
||||
error_source: "test",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should include additional metadata in PostHog event", () => {
|
||||
@ -55,15 +56,19 @@ describe("Error Handler", () => {
|
||||
|
||||
trackError(error);
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Test error"), {
|
||||
error_source: "test",
|
||||
extra: "info",
|
||||
details: { foo: "bar" },
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Test error"),
|
||||
{
|
||||
error_source: "test",
|
||||
extra: "info",
|
||||
details: { foo: "bar" },
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("showErrorToast", () => {
|
||||
const errorToastSpy = vi.spyOn(CustomToast, "displayErrorToast");
|
||||
it("should log error and show toast", () => {
|
||||
const error = {
|
||||
message: "Toast error",
|
||||
@ -73,12 +78,15 @@ describe("Error Handler", () => {
|
||||
showErrorToast(error);
|
||||
|
||||
// Verify PostHog logging
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Toast error"), {
|
||||
error_source: "toast-test",
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Toast error"),
|
||||
{
|
||||
error_source: "toast-test",
|
||||
},
|
||||
);
|
||||
|
||||
// Verify toast was shown
|
||||
expect(toast.error).toHaveBeenCalled();
|
||||
expect(errorToastSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should include metadata in PostHog event when showing toast", () => {
|
||||
@ -90,10 +98,13 @@ describe("Error Handler", () => {
|
||||
|
||||
showErrorToast(error);
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Toast error"), {
|
||||
error_source: "toast-test",
|
||||
context: "testing",
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Toast error"),
|
||||
{
|
||||
error_source: "toast-test",
|
||||
context: "testing",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should log errors from different sources with appropriate metadata", () => {
|
||||
@ -104,10 +115,13 @@ describe("Error Handler", () => {
|
||||
metadata: { id: "error.agent" },
|
||||
});
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Agent error"), {
|
||||
error_source: "agent-status",
|
||||
id: "error.agent",
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Agent error"),
|
||||
{
|
||||
error_source: "agent-status",
|
||||
id: "error.agent",
|
||||
},
|
||||
);
|
||||
|
||||
showErrorToast({
|
||||
message: "Server error",
|
||||
@ -115,11 +129,14 @@ describe("Error Handler", () => {
|
||||
metadata: { error_code: 500, details: "Internal error" },
|
||||
});
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Server error"), {
|
||||
error_source: "server",
|
||||
error_code: 500,
|
||||
details: "Internal error",
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Server error"),
|
||||
{
|
||||
error_source: "server",
|
||||
error_code: 500,
|
||||
details: "Internal error",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should log feedback submission errors with conversation context", () => {
|
||||
@ -130,11 +147,14 @@ describe("Error Handler", () => {
|
||||
metadata: { conversationId: "123", error },
|
||||
});
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Feedback submission failed"), {
|
||||
error_source: "feedback",
|
||||
conversationId: "123",
|
||||
error,
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Feedback submission failed"),
|
||||
{
|
||||
error_source: "feedback",
|
||||
conversationId: "123",
|
||||
error,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -149,9 +169,12 @@ describe("Error Handler", () => {
|
||||
showChatError(error);
|
||||
|
||||
// Verify PostHog logging
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Chat error"), {
|
||||
error_source: "chat-test",
|
||||
});
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(
|
||||
new Error("Chat error"),
|
||||
{
|
||||
error_source: "chat-test",
|
||||
},
|
||||
);
|
||||
|
||||
// Verify error message was shown in chat
|
||||
expect(Actions.handleStatusMessage).toHaveBeenCalledWith({
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import toast from "react-hot-toast";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useParams } from "react-router";
|
||||
@ -23,6 +22,7 @@ import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bott
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
|
||||
import { downloadTrajectory } from "#/utils/download-files";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
function getEntryPoint(
|
||||
hasRepository: boolean | null,
|
||||
@ -98,7 +98,7 @@ export function ChatInterface() {
|
||||
|
||||
const onClickExportTrajectoryButton = () => {
|
||||
if (!params.conversationId) {
|
||||
toast.error("ConversationId unknown, cannot download trajectory");
|
||||
displayErrorToast("ConversationId unknown, cannot download trajectory");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -110,7 +110,7 @@ export function ChatInterface() {
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
displayErrorToast(error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -2,7 +2,6 @@ import React from "react";
|
||||
import { FaListUl } from "react-icons/fa";
|
||||
import { useDispatch } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import toast from "react-hot-toast";
|
||||
import { NavLink, useLocation } from "react-router";
|
||||
import { useGitHubUser } from "#/hooks/query/use-github-user";
|
||||
import { UserActions } from "./user-actions";
|
||||
@ -22,6 +21,7 @@ import { ConversationPanelWrapper } from "../conversation-panel/conversation-pan
|
||||
import { useLogout } from "#/hooks/mutation/use-logout";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { HIDE_LLM_SETTINGS } from "#/utils/feature-flags";
|
||||
|
||||
export function Sidebar() {
|
||||
@ -58,7 +58,7 @@ export function Sidebar() {
|
||||
) {
|
||||
// We don't show toast errors for settings in the global error handler
|
||||
// because we have a special case for 404 errors
|
||||
toast.error(
|
||||
displayErrorToast(
|
||||
"Something went wrong while fetching settings. Please reload the page.",
|
||||
);
|
||||
} else if (settingsError?.status === 404) {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { MutateOptions } from "@tanstack/react-query";
|
||||
import toast from "react-hot-toast";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { PostSettings, Settings } from "#/types/settings";
|
||||
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
type SaveUserSettingsConfig = {
|
||||
onSuccess: MutateOptions<void, Error, Partial<PostSettings>>["onSuccess"];
|
||||
@ -47,7 +47,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
|
||||
onSuccess: config?.onSuccess,
|
||||
onError: (error) => {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
toast.error(errorMessage);
|
||||
displayErrorToast(errorMessage);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import toast from "react-hot-toast";
|
||||
import { Feedback } from "#/api/open-hands.types";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
type SubmitFeedbackArgs = {
|
||||
feedback: Feedback;
|
||||
@ -14,7 +14,7 @@ export const useSubmitFeedback = () => {
|
||||
mutationFn: ({ feedback }: SubmitFeedbackArgs) =>
|
||||
OpenHands.submitFeedback(conversationId, feedback),
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
displayErrorToast(error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M5.69949 5.72974L7.91965 7.9505L8.35077 7.51999L6.13001 5.29922L8.35077 3.07907L7.92026 2.64795L5.69949 4.86871L3.47934 2.64795L3.04883 3.07907L5.26898 5.29922L3.04883 7.51938L3.47934 7.9505L5.69949 5.72974Z"
|
||||
fill="black" />
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 13.0602L16.2123 16.7725L17.273 15.7118L13.5607 11.9995L17.273 8.28723L16.2123 7.22657L12.5 10.9389L8.78771 7.22656L7.72705 8.28722L11.4394 11.9995L7.72706 15.7119L8.78772 16.7725L12.5 13.0602Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 387 B After Width: | Height: | Size: 327 B |
@ -3,8 +3,8 @@ import {
|
||||
QueryCache,
|
||||
MutationCache,
|
||||
} from "@tanstack/react-query";
|
||||
import toast from "react-hot-toast";
|
||||
import { retrieveAxiosErrorMessage } from "./utils/retrieve-axios-error-message";
|
||||
import { displayErrorToast } from "./utils/custom-toast-handlers";
|
||||
|
||||
const shownErrors = new Set<string>();
|
||||
export const queryClientConfig: QueryClientConfig = {
|
||||
@ -14,7 +14,7 @@ export const queryClientConfig: QueryClientConfig = {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
|
||||
if (!shownErrors.has(errorMessage)) {
|
||||
toast.error(errorMessage || "An error occurred");
|
||||
displayErrorToast(errorMessage || "An error occurred");
|
||||
shownErrors.add(errorMessage);
|
||||
|
||||
setTimeout(() => {
|
||||
@ -28,7 +28,7 @@ export const queryClientConfig: QueryClientConfig = {
|
||||
onError: (error, _, __, mutation) => {
|
||||
if (!mutation?.meta?.disableToast) {
|
||||
const message = retrieveAxiosErrorMessage(error);
|
||||
toast.error(message);
|
||||
displayErrorToast(message);
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setImportedProjectZip } from "#/state/initial-query-slice";
|
||||
import { RootState } from "#/store";
|
||||
@ -7,6 +6,7 @@ import { base64ToBlob } from "#/utils/base64-to-blob";
|
||||
import { useUploadFiles } from "../../../hooks/mutation/use-upload-files";
|
||||
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
export const useHandleRuntimeActive = () => {
|
||||
const dispatch = useDispatch();
|
||||
@ -29,7 +29,7 @@ export const useHandleRuntimeActive = () => {
|
||||
{ files: [file] },
|
||||
{
|
||||
onError: () => {
|
||||
toast.error("Failed to upload project files.");
|
||||
displayErrorToast("Failed to upload project files.");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
||||
@ -7,6 +6,7 @@ import { addErrorMessage } from "#/state/chat-slice";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { ErrorObservation } from "#/types/core/observations";
|
||||
import { useEndSession } from "../../../hooks/use-end-session";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
interface ServerError {
|
||||
error: boolean | string;
|
||||
@ -32,15 +32,15 @@ export const useHandleWSEvents = () => {
|
||||
|
||||
if (isServerError(event)) {
|
||||
if (event.error_code === 401) {
|
||||
toast.error("Session expired.");
|
||||
displayErrorToast("Session expired.");
|
||||
endSession();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof event.error === "string") {
|
||||
toast.error(event.error);
|
||||
displayErrorToast(event.error);
|
||||
} else {
|
||||
toast.error(event.message);
|
||||
displayErrorToast(event.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ import React from "react";
|
||||
import { Outlet } from "react-router";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { FaServer } from "react-icons/fa";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import {
|
||||
@ -36,6 +35,7 @@ import { TerminalStatusLabel } from "#/components/features/terminal/terminal-sta
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { clearFiles, clearInitialPrompt } from "#/state/initial-query-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
function AppContent() {
|
||||
useConversationConfig();
|
||||
@ -66,7 +66,7 @@ function AppContent() {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isFetched && !conversation) {
|
||||
toast.error(
|
||||
displayErrorToast(
|
||||
"This conversation does not exist, or you do not have permission to access it.",
|
||||
);
|
||||
endSession();
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export const displayErrorToast = (error: string) => {
|
||||
toast.error(error, {
|
||||
position: "top-right",
|
||||
style: {
|
||||
background: "#454545",
|
||||
border: "1px solid #717888",
|
||||
color: "#fff",
|
||||
borderRadius: "4px",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const displaySuccessToast = (message: string) => {
|
||||
toast.success(message, {
|
||||
position: "top-right",
|
||||
style: {
|
||||
background: "#454545",
|
||||
border: "1px solid #717888",
|
||||
color: "#fff",
|
||||
borderRadius: "4px",
|
||||
},
|
||||
});
|
||||
};
|
||||
22
frontend/src/utils/custom-toast-handlers.tsx
Normal file
22
frontend/src/utils/custom-toast-handlers.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { CSSProperties } from "react";
|
||||
import toast, { ToastOptions } from "react-hot-toast";
|
||||
|
||||
const TOAST_STYLE: CSSProperties = {
|
||||
background: "#454545",
|
||||
border: "1px solid #717888",
|
||||
color: "#fff",
|
||||
borderRadius: "4px",
|
||||
};
|
||||
|
||||
const TOAST_OPTIONS: ToastOptions = {
|
||||
position: "top-right",
|
||||
style: TOAST_STYLE,
|
||||
};
|
||||
|
||||
export const displayErrorToast = (error: string) => {
|
||||
toast.error(error, TOAST_OPTIONS);
|
||||
};
|
||||
|
||||
export const displaySuccessToast = (message: string) => {
|
||||
toast.success(message, TOAST_OPTIONS);
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import posthog from "posthog-js";
|
||||
import toast from "react-hot-toast";
|
||||
import { handleStatusMessage } from "#/services/actions";
|
||||
import { displayErrorToast } from "./custom-toast-handlers";
|
||||
|
||||
interface ErrorDetails {
|
||||
message: string;
|
||||
@ -23,7 +23,7 @@ export function showErrorToast({
|
||||
metadata = {},
|
||||
}: ErrorDetails) {
|
||||
trackError({ message, source, metadata });
|
||||
toast.error(message);
|
||||
displayErrorToast(message);
|
||||
}
|
||||
|
||||
export function showChatError({
|
||||
|
||||
@ -19,6 +19,45 @@ export default {
|
||||
content: "#ECEDEE", // light gray, used mostly for text
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
enter: "toastIn 400ms cubic-bezier(0.21, 1.02, 0.73, 1)",
|
||||
leave: "toastOut 100ms ease-in forwards",
|
||||
},
|
||||
keyframes: {
|
||||
toastIn: {
|
||||
"0%": {
|
||||
opacity: "0",
|
||||
transform: "translateY(-100%) scale(0.8)",
|
||||
},
|
||||
"80%": {
|
||||
opacity: "1",
|
||||
transform: "translateY(0) scale(1.02)",
|
||||
},
|
||||
"100%": {
|
||||
opacity: "1",
|
||||
transform: "translateY(0) scale(1)",
|
||||
},
|
||||
},
|
||||
toastOut: {
|
||||
"0%": {
|
||||
opacity: "1",
|
||||
transform: "translateY(0) scale(1)",
|
||||
},
|
||||
"100%": {
|
||||
opacity: "0",
|
||||
transform: "translateY(-100%) scale(0.9)",
|
||||
},
|
||||
},
|
||||
colors: {
|
||||
primary: "#C9B974", // nice yellow
|
||||
base: "#171717", // dark background (neutral-900)
|
||||
"base-secondary": "#262626", // lighter background (neutral-800); also used for tooltips
|
||||
danger: "#E76A5E",
|
||||
success: "#A5E75E",
|
||||
tertiary: "#454545", // gray, used for inputs
|
||||
"tertiary-light": "#B7BDC2", // lighter gray, used for borders and placeholder text
|
||||
},
|
||||
},
|
||||
},
|
||||
darkMode: "class",
|
||||
plugins: [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user