feat(webapp): add light mode support to dashboard
Add theme switching capability with light, dark, and system modes: - Enable Tailwind CSS dark mode with class strategy - Add CSS variables for theme-aware semantic colors - Create ThemeProvider context for managing theme state - Add ThemeToggle component in side menu and account settings - Persist theme preference to user dashboard preferences - Include ThemeScript to prevent flash of wrong theme on load Slack thread: https://triggerdotdev.slack.com/archives/C04CR1HUWBV/p1769765128711039 https://claude.ai/code/session_01VDVgh75Xa4LA9YH9aFLnjB
This commit is contained in:
@@ -98,6 +98,7 @@ import { TextLink } from "../primitives/TextLink";
|
||||
import { SimpleTooltip, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
|
||||
import { ShortcutsAutoOpen } from "../Shortcuts";
|
||||
import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
import { ThemeToggle } from "../primitives/ThemeToggle";
|
||||
import { EnvironmentSelector } from "./EnvironmentSelector";
|
||||
import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
|
||||
import { SideMenuHeader } from "./SideMenuHeader";
|
||||
@@ -892,7 +893,10 @@ function HelpAndAI({ isCollapsed }: { isCollapsed: boolean }) {
|
||||
<LayoutGroup>
|
||||
<div className={cn("flex w-full", isCollapsed ? "flex-col-reverse gap-1" : "items-center justify-between")}>
|
||||
<ShortcutsAutoOpen />
|
||||
<HelpAndFeedback isCollapsed={isCollapsed} />
|
||||
<div className={cn("flex", isCollapsed ? "flex-col gap-1" : "items-center gap-0.5")}>
|
||||
<ThemeToggle isCollapsed={isCollapsed} />
|
||||
<HelpAndFeedback isCollapsed={isCollapsed} />
|
||||
</div>
|
||||
<AskAI isCollapsed={isCollapsed} />
|
||||
</div>
|
||||
</LayoutGroup>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import type { ThemePreference } from "~/services/dashboardPreferences.server";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
|
||||
interface ThemeContextValue {
|
||||
theme: Theme;
|
||||
themePreference: ThemePreference;
|
||||
setThemePreference: (preference: ThemePreference) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
||||
|
||||
function getSystemTheme(): Theme {
|
||||
if (typeof window === "undefined") {
|
||||
return "dark";
|
||||
}
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
function resolveTheme(preference: ThemePreference): Theme {
|
||||
if (preference === "system") {
|
||||
return getSystemTheme();
|
||||
}
|
||||
return preference;
|
||||
}
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: ReactNode;
|
||||
initialPreference?: ThemePreference;
|
||||
isLoggedIn?: boolean;
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
initialPreference = "dark",
|
||||
isLoggedIn = false,
|
||||
}: ThemeProviderProps) {
|
||||
const [themePreference, setThemePreferenceState] = useState<ThemePreference>(initialPreference);
|
||||
const [theme, setTheme] = useState<Theme>(() => resolveTheme(initialPreference));
|
||||
const fetcher = useFetcher();
|
||||
|
||||
// Update the HTML class when theme changes
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.classList.remove("light", "dark");
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
// Listen for system theme changes when preference is "system"
|
||||
useEffect(() => {
|
||||
if (themePreference !== "system") {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
setTheme(e.matches ? "dark" : "light");
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||
}, [themePreference]);
|
||||
|
||||
const setThemePreference = useCallback(
|
||||
(preference: ThemePreference) => {
|
||||
setThemePreferenceState(preference);
|
||||
setTheme(resolveTheme(preference));
|
||||
|
||||
// Persist to server if logged in
|
||||
if (isLoggedIn) {
|
||||
fetcher.submit(
|
||||
{ theme: preference },
|
||||
{ method: "POST", action: "/resources/preferences/theme" }
|
||||
);
|
||||
}
|
||||
|
||||
// Also store in localStorage for non-logged-in users and faster hydration
|
||||
localStorage.setItem("theme-preference", preference);
|
||||
},
|
||||
[isLoggedIn, fetcher]
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, themePreference, setThemePreference }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// Script to prevent flash of wrong theme on initial load
|
||||
// This should be injected into the <head> before any content renders
|
||||
export function ThemeScript({ initialPreference }: { initialPreference?: ThemePreference }) {
|
||||
const script = `
|
||||
(function() {
|
||||
var preference = ${JSON.stringify(initialPreference ?? null)} || localStorage.getItem('theme-preference') || 'dark';
|
||||
var theme = preference;
|
||||
if (preference === 'system') {
|
||||
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
document.documentElement.classList.add(theme);
|
||||
})();
|
||||
`;
|
||||
|
||||
return <script dangerouslySetInnerHTML={{ __html: script }} />;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { ComputerDesktopIcon, MoonIcon, SunIcon } from "@heroicons/react/20/solid";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { Popover, PopoverContent, PopoverMenuItem, PopoverTrigger } from "./Popover";
|
||||
import { Button } from "./Buttons";
|
||||
import { SimpleTooltip } from "./Tooltip";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { useState } from "react";
|
||||
import type { ThemePreference } from "~/services/dashboardPreferences.server";
|
||||
|
||||
const themeOptions: { value: ThemePreference; label: string; icon: typeof SunIcon }[] = [
|
||||
{ value: "light", label: "Light", icon: SunIcon },
|
||||
{ value: "dark", label: "Dark", icon: MoonIcon },
|
||||
{ value: "system", label: "System", icon: ComputerDesktopIcon },
|
||||
];
|
||||
|
||||
interface ThemeToggleProps {
|
||||
className?: string;
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
|
||||
export function ThemeToggle({ className, isCollapsed = false }: ThemeToggleProps) {
|
||||
const { themePreference, setThemePreference } = useTheme();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const currentOption = themeOptions.find((opt) => opt.value === themePreference) ?? themeOptions[1];
|
||||
const CurrentIcon = currentOption.icon;
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="minimal/small"
|
||||
className={cn("aspect-square h-7 p-1", className)}
|
||||
LeadingIcon={CurrentIcon}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
}
|
||||
content={`Theme: ${currentOption.label}`}
|
||||
side={isCollapsed ? "right" : "top"}
|
||||
hidden={isOpen}
|
||||
disableHoverableContent
|
||||
/>
|
||||
<PopoverContent
|
||||
className="min-w-[10rem] p-1"
|
||||
side={isCollapsed ? "right" : "top"}
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
>
|
||||
{themeOptions.map((option) => (
|
||||
<PopoverMenuItem
|
||||
key={option.value}
|
||||
icon={option.icon}
|
||||
title={option.label}
|
||||
isSelected={themePreference === option.value}
|
||||
onClick={() => {
|
||||
setThemePreference(option.value);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
interface ThemeToggleButtonsProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ThemeToggleButtons({ className }: ThemeToggleButtonsProps) {
|
||||
const { themePreference, setThemePreference } = useTheme();
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 rounded-md bg-tertiary p-0.5", className)}>
|
||||
{themeOptions.map((option) => {
|
||||
const Icon = option.icon;
|
||||
const isSelected = themePreference === option.value;
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => setThemePreference(option.value)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded px-2 py-1 text-xs transition-colors",
|
||||
isSelected
|
||||
? "bg-background-bright text-text-bright shadow-sm"
|
||||
: "text-text-dimmed hover:text-text-bright"
|
||||
)}
|
||||
title={option.label}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
<span className="hidden sm:inline">{option.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+27
-16
@@ -9,12 +9,14 @@ import tailwindStylesheetUrl from "~/tailwind.css";
|
||||
import { RouteErrorDisplay } from "./components/ErrorDisplay";
|
||||
import { AppContainer, MainCenteredContainer } from "./components/layout/AppLayout";
|
||||
import { ShortcutsProvider } from "./components/primitives/ShortcutsProvider";
|
||||
import { ThemeProvider, ThemeScript } from "./components/primitives/ThemeProvider";
|
||||
import { Toast } from "./components/primitives/Toast";
|
||||
import { env } from "./env.server";
|
||||
import { featuresForRequest } from "./features.server";
|
||||
import { usePostHog } from "./hooks/usePostHog";
|
||||
import { getUser } from "./services/session.server";
|
||||
import { appEnvTitleTag } from "./utils";
|
||||
import type { ThemePreference } from "./services/dashboardPreferences.server";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
|
||||
@@ -55,9 +57,12 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
websiteId: env.KAPA_AI_WEBSITE_ID,
|
||||
};
|
||||
|
||||
const user = await getUser(request);
|
||||
const themePreference: ThemePreference = user?.dashboardPreferences?.theme ?? "dark";
|
||||
|
||||
return typedjson(
|
||||
{
|
||||
user: await getUser(request),
|
||||
user,
|
||||
toastMessage,
|
||||
posthogProjectKey,
|
||||
features,
|
||||
@@ -65,6 +70,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
appOrigin: env.APP_ORIGIN,
|
||||
triggerCliTag: env.TRIGGER_CLI_TAG,
|
||||
kapa,
|
||||
themePreference,
|
||||
},
|
||||
{ headers: { "Set-Cookie": await commitSession(session) } }
|
||||
);
|
||||
@@ -83,21 +89,23 @@ export const shouldRevalidate: ShouldRevalidateFunction = (options) => {
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<>
|
||||
<html lang="en" className="h-full">
|
||||
<html lang="en" className="h-full dark">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
|
||||
<ThemeScript />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="h-full overflow-hidden bg-background-dimmed">
|
||||
<ShortcutsProvider>
|
||||
<AppContainer>
|
||||
<MainCenteredContainer>
|
||||
<RouteErrorDisplay />
|
||||
</MainCenteredContainer>
|
||||
</AppContainer>
|
||||
</ShortcutsProvider>
|
||||
<ThemeProvider initialPreference="dark" isLoggedIn={false}>
|
||||
<ShortcutsProvider>
|
||||
<AppContainer>
|
||||
<MainCenteredContainer>
|
||||
<RouteErrorDisplay />
|
||||
</MainCenteredContainer>
|
||||
</AppContainer>
|
||||
</ShortcutsProvider>
|
||||
</ThemeProvider>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
@@ -106,21 +114,24 @@ export function ErrorBoundary() {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { posthogProjectKey, kapa } = useTypedLoaderData<typeof loader>();
|
||||
const { posthogProjectKey, kapa, themePreference, user } = useTypedLoaderData<typeof loader>();
|
||||
usePostHog(posthogProjectKey);
|
||||
|
||||
return (
|
||||
<>
|
||||
<html lang="en" className="h-full">
|
||||
<html lang="en" className="h-full dark">
|
||||
<head>
|
||||
<ThemeScript initialPreference={themePreference} />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="h-full overflow-hidden bg-background-dimmed">
|
||||
<ShortcutsProvider>
|
||||
<Outlet />
|
||||
<Toast />
|
||||
</ShortcutsProvider>
|
||||
<ThemeProvider initialPreference={themePreference} isLoggedIn={!!user}>
|
||||
<ShortcutsProvider>
|
||||
<Outlet />
|
||||
<Toast />
|
||||
</ShortcutsProvider>
|
||||
</ThemeProvider>
|
||||
<ScrollRestoration />
|
||||
<ExternalScripts />
|
||||
<Scripts />
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { ThemeToggleButtons } from "~/components/primitives/ThemeToggle";
|
||||
import { prisma } from "~/db.server";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
@@ -196,6 +197,14 @@ export default function Page() {
|
||||
/>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
<div className="mt-6 w-full border-t border-grid-dimmed pt-6">
|
||||
<Header2 className="mb-3">Appearance</Header2>
|
||||
<InputGroup>
|
||||
<Label>Theme</Label>
|
||||
<ThemeToggleButtons />
|
||||
<Hint>Choose your preferred color scheme</Hint>
|
||||
</InputGroup>
|
||||
</div>
|
||||
</MainHorizontallyCenteredContainer>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { json, type ActionFunctionArgs } from "@remix-run/node";
|
||||
import { z } from "zod";
|
||||
import { ThemePreference, updateThemePreference } from "~/services/dashboardPreferences.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
|
||||
const RequestSchema = z.object({
|
||||
theme: ThemePreference,
|
||||
});
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const user = await requireUser(request);
|
||||
|
||||
const formData = await request.formData();
|
||||
const rawData = Object.fromEntries(formData);
|
||||
|
||||
const result = RequestSchema.safeParse(rawData);
|
||||
if (!result.success) {
|
||||
return json({ success: false, error: "Invalid request data" }, { status: 400 });
|
||||
}
|
||||
|
||||
await updateThemePreference({
|
||||
user,
|
||||
theme: result.data.theme,
|
||||
});
|
||||
|
||||
return json({ success: true });
|
||||
}
|
||||
@@ -10,6 +10,9 @@ const SideMenuPreferences = z.object({
|
||||
|
||||
export type SideMenuPreferences = z.infer<typeof SideMenuPreferences>;
|
||||
|
||||
export const ThemePreference = z.enum(["light", "dark", "system"]).default("dark");
|
||||
export type ThemePreference = z.infer<typeof ThemePreference>;
|
||||
|
||||
const DashboardPreferences = z.object({
|
||||
version: z.literal("1"),
|
||||
currentProjectId: z.string().optional(),
|
||||
@@ -20,6 +23,7 @@ const DashboardPreferences = z.object({
|
||||
})
|
||||
),
|
||||
sideMenu: SideMenuPreferences.optional(),
|
||||
theme: ThemePreference.optional(),
|
||||
});
|
||||
|
||||
export type DashboardPreferences = z.infer<typeof DashboardPreferences>;
|
||||
@@ -151,3 +155,34 @@ export async function updateSideMenuPreferences({
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateThemePreference({
|
||||
user,
|
||||
theme,
|
||||
}: {
|
||||
user: UserFromSession;
|
||||
theme: ThemePreference;
|
||||
}) {
|
||||
if (user.isImpersonating) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only update if something changed
|
||||
if (user.dashboardPreferences.theme === theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedPreferences: DashboardPreferences = {
|
||||
...user.dashboardPreferences,
|
||||
theme,
|
||||
};
|
||||
|
||||
return prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
dashboardPreferences: updatedPreferences,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,17 +33,41 @@
|
||||
::-moz-selection {
|
||||
@apply bg-text-bright/30 text-text-bright;
|
||||
}
|
||||
|
||||
/* shadcn charts: https://ui.shadcn.com/docs/components/chart#add-a-grid */
|
||||
|
||||
/* Theme color variables */
|
||||
/* Light mode (default) */
|
||||
:root {
|
||||
/* Theme-aware semantic colors */
|
||||
--color-secondary: #D7D9DD; /* charcoal[200] */
|
||||
--color-tertiary: #E8E9EC; /* charcoal[100] */
|
||||
--color-text-dimmed: #5F6570; /* charcoal[500] */
|
||||
--color-text-bright: #272A2E; /* charcoal[700] */
|
||||
--color-background-bright: #E8E9EC; /* charcoal[100] */
|
||||
--color-background-dimmed: #F5F5F7; /* lighter than charcoal[100] */
|
||||
--color-grid-bright: #B5B8C0; /* charcoal[300] */
|
||||
--color-grid-dimmed: #D7D9DD; /* charcoal[200] */
|
||||
|
||||
/* shadcn charts */
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
|
||||
/* Dark mode */
|
||||
.dark {
|
||||
/* Theme-aware semantic colors */
|
||||
--color-secondary: #2C3034; /* charcoal[650] */
|
||||
--color-tertiary: #272A2E; /* charcoal[700] */
|
||||
--color-text-dimmed: #878C99; /* charcoal[400] */
|
||||
--color-text-bright: #D7D9DD; /* charcoal[200] */
|
||||
--color-background-bright: #1A1B1F; /* charcoal[800] */
|
||||
--color-background-dimmed: #15171A; /* charcoal[850] */
|
||||
--color-grid-bright: #272A2E; /* charcoal[700] */
|
||||
--color-grid-dimmed: #212327; /* charcoal[750] */
|
||||
|
||||
/* shadcn charts */
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
@@ -116,10 +140,10 @@
|
||||
/* Streamdown markdown styling */
|
||||
.streamdown-container {
|
||||
/* Streamdown uses shadcn/ui CSS variables - define them for our theme */
|
||||
--muted: 220 13% 20%;
|
||||
--muted-foreground: 215 14% 60%;
|
||||
--foreground: 210 20% 90%;
|
||||
--border: 217 19% 27%;
|
||||
--muted: 220 13% 91%;
|
||||
--muted-foreground: 215 14% 40%;
|
||||
--foreground: 210 20% 15%;
|
||||
--border: 217 19% 80%;
|
||||
|
||||
& p {
|
||||
@apply my-1;
|
||||
@@ -150,13 +174,13 @@
|
||||
}
|
||||
/* Inline code (not in pre blocks) */
|
||||
& code:not(pre code) {
|
||||
@apply bg-charcoal-700 px-1 py-0.5 rounded text-text-bright font-mono;
|
||||
@apply bg-secondary px-1 py-0.5 rounded text-text-bright font-mono;
|
||||
}
|
||||
& blockquote {
|
||||
@apply border-l-2 border-charcoal-600 pl-3 my-2 italic;
|
||||
@apply border-l-2 border-grid-bright pl-3 my-2 italic;
|
||||
}
|
||||
& a {
|
||||
@apply text-blue-400 hover:underline;
|
||||
@apply text-blue-500 dark:text-blue-400 hover:underline;
|
||||
}
|
||||
& strong {
|
||||
@apply font-semibold text-text-bright;
|
||||
@@ -165,24 +189,24 @@
|
||||
@apply italic;
|
||||
}
|
||||
& hr {
|
||||
@apply my-2 border-charcoal-600;
|
||||
@apply my-2 border-grid-bright;
|
||||
}
|
||||
& table {
|
||||
@apply w-full my-2 border-collapse;
|
||||
}
|
||||
& th, & td {
|
||||
@apply border border-charcoal-600 px-2 py-1 text-left;
|
||||
@apply border border-grid-bright px-2 py-1 text-left;
|
||||
}
|
||||
& th {
|
||||
@apply bg-charcoal-700 font-semibold;
|
||||
@apply bg-secondary font-semibold;
|
||||
}
|
||||
|
||||
/* Streamdown code block container */
|
||||
& [data-code-block-container] {
|
||||
@apply my-2 border-charcoal-700;
|
||||
@apply my-2 border-grid-bright;
|
||||
}
|
||||
& [data-code-block-header] {
|
||||
@apply bg-charcoal-800 text-text-dimmed border-b border-charcoal-700;
|
||||
@apply bg-background-bright text-text-dimmed border-b border-grid-bright;
|
||||
}
|
||||
/* Hide light mode code block, show dark mode */
|
||||
& [data-code-block].dark\:hidden {
|
||||
@@ -194,7 +218,7 @@
|
||||
/* Override the bg-muted/40 class to let inline styles work */
|
||||
& [data-code-block] pre {
|
||||
background-color: inherit !important;
|
||||
@apply scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600;
|
||||
@apply scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-400 dark:scrollbar-thumb-charcoal-600;
|
||||
}
|
||||
& [data-code-block] pre code {
|
||||
@apply bg-transparent;
|
||||
@@ -203,3 +227,11 @@
|
||||
@apply leading-relaxed;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode streamdown overrides */
|
||||
.dark .streamdown-container {
|
||||
--muted: 220 13% 20%;
|
||||
--muted-foreground: 215 14% 60%;
|
||||
--foreground: 210 20% 90%;
|
||||
--border: 217 19% 27%;
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ const lavender = {
|
||||
|
||||
/** Trigger.dev custom palette */
|
||||
|
||||
/** Text colors */
|
||||
/** Text colors - Dark mode values (used as CSS variable fallbacks) */
|
||||
const primary = apple[500];
|
||||
const secondary = charcoal[650];
|
||||
const tertiary = charcoal[700];
|
||||
@@ -153,6 +153,16 @@ const stagingEnv = colors.orange[400];
|
||||
const previewEnv = colors.yellow[400];
|
||||
const prodEnv = mint[500];
|
||||
|
||||
/** Light mode color values */
|
||||
const lightTextDimmed = charcoal[500];
|
||||
const lightTextBright = charcoal[800];
|
||||
const lightBackgroundBright = charcoal[100];
|
||||
const lightBackgroundDimmed = charcoal[200];
|
||||
const lightGridBright = charcoal[300];
|
||||
const lightGridDimmed = charcoal[200];
|
||||
const lightSecondary = charcoal[200];
|
||||
const lightTertiary = charcoal[100];
|
||||
|
||||
/** Icon colors */
|
||||
const tasks = colors.blue[500];
|
||||
const runs = colors.indigo[500];
|
||||
@@ -178,6 +188,7 @@ const radius = "0.5rem";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: ["./app/**/*.{ts,jsx,tsx}"],
|
||||
theme: {
|
||||
container: {
|
||||
@@ -217,15 +228,16 @@ module.exports = {
|
||||
mint,
|
||||
sun,
|
||||
primary,
|
||||
secondary,
|
||||
tertiary,
|
||||
// Theme-aware colors using CSS variables with fallbacks
|
||||
secondary: "var(--color-secondary)",
|
||||
tertiary: "var(--color-tertiary)",
|
||||
"text-link": textLink,
|
||||
"text-dimmed": textDimmed,
|
||||
"text-bright": textBright,
|
||||
"background-bright": backgroundBright,
|
||||
"background-dimmed": backgroundDimmed,
|
||||
"grid-bright": gridBright,
|
||||
"grid-dimmed": gridDimmed,
|
||||
"text-dimmed": "var(--color-text-dimmed)",
|
||||
"text-bright": "var(--color-text-bright)",
|
||||
"background-bright": "var(--color-background-bright)",
|
||||
"background-dimmed": "var(--color-background-dimmed)",
|
||||
"grid-bright": "var(--color-grid-bright)",
|
||||
"grid-dimmed": "var(--color-grid-dimmed)",
|
||||
success,
|
||||
pending,
|
||||
warning,
|
||||
|
||||
Reference in New Issue
Block a user