diff --git a/apps/dashboard/src/components/forms/status-page/form-custom-theme.tsx b/apps/dashboard/src/components/forms/status-page/form-custom-theme.tsx new file mode 100644 index 00000000..dab6f3ce --- /dev/null +++ b/apps/dashboard/src/components/forms/status-page/form-custom-theme.tsx @@ -0,0 +1,216 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { + type CustomTheme, + formatThemeVars, + generateThemeVarsTemplate, + parseThemeVarsText, + THEMES, + type ThemeMode, +} from "@openstatus/theme-store"; +import { Button } from "@openstatus/ui/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormMessage, +} from "@openstatus/ui/components/ui/form"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@openstatus/ui/components/ui/tabs"; +import { Textarea } from "@openstatus/ui/components/ui/textarea"; +import { cn } from "@openstatus/ui/lib/utils"; +import { isTRPCClientError } from "@trpc/client"; +import { ChevronsDownUp, ChevronsUpDown, Lock } from "lucide-react"; +import { useState, useTransition } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +import { Link } from "@/components/common/link"; +import { + FormCard, + FormCardContent, + FormCardDescription, + FormCardFooter, + FormCardFooterInfo, + FormCardHeader, + FormCardSeparator, + FormCardTitle, + FormCardUpgrade, +} from "@/components/forms/form-card"; + +const MODES = ["light", "dark"] as const; + +const modeText = z.string().superRefine((value, ctx) => { + for (const message of parseThemeVarsText(value).errors) { + ctx.addIssue({ code: "custom", message }); + } +}); + +const schema = z.object({ light: modeText, dark: modeText }); + +type FormValues = z.infer; + +export function FormCustomTheme({ + defaultValue, + onSubmit, + locked, + themeKey, +}: { + defaultValue?: CustomTheme | null; + onSubmit: (values: { customTheme: CustomTheme }) => Promise; + locked?: boolean; + themeKey?: string; +}) { + const theme = THEMES[themeKey ?? "default"] ?? THEMES.default; + const [mode, setMode] = useState("light"); + const [extended, setExtended] = useState(false); + const [isPending, startTransition] = useTransition(); + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + light: formatThemeVars(defaultValue?.light), + dark: formatThemeVars(defaultValue?.dark), + }, + }); + + function submitAction(values: FormValues) { + if (isPending) return; + + startTransition(async () => { + try { + const promise = onSubmit({ + customTheme: { + light: parseThemeVarsText(values.light).vars, + dark: parseThemeVarsText(values.dark).vars, + }, + }); + toast.promise(promise, { + loading: "Saving...", + success: "Saved", + error: (error) => { + if (isTRPCClientError(error)) { + return error.message; + } + return "Failed to save"; + }, + }); + await promise; + } catch (error) { + console.error(error); + } + }); + } + + return ( +
+ { + // jump to the first tab with a validation error + const errored = MODES.find((m) => errors[m]); + if (errored) setMode(errored); + })} + > + + {locked ? : null} + + Custom Theme + + Override the selected theme's CSS variables with your own + values for light and dark mode. + + + + + setMode(v as ThemeMode)}> +
+ + Light + Dark + + +
+ {MODES.map((m) => ( + + ( + + +