diff --git a/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/budget_panel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/budget_panel.tsx index ad0f04f77aa..18b8e774aae 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/budget_panel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/budget_panel.tsx @@ -6,6 +6,9 @@ import { Plus, Wallet } from "lucide-react"; import React, { useCallback, useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { prism } from "react-syntax-highlighter/dist/esm/styles/prism"; + +import { useSyntaxTheme } from "@/hooks/useSyntaxTheme"; import { LegacyPageHeader } from "@/components/shared/LegacyPageHeader"; import { ToolbarSeparator } from "@/components/shared/ToolbarSeparator"; import { Button } from "@/components/ui/button"; @@ -25,6 +28,7 @@ interface BudgetSettingsPageProps { } const BudgetPanel: React.FC = ({ accessToken }) => { + const syntaxTheme = useSyntaxTheme(prism); const [isCreateModelVisible, setIsCreateModelVisible] = useState(false); const [isEditModalVisible, setIsEditModalVisible] = useState(false); const [selectedBudget, setSelectedBudget] = useState(null); @@ -150,13 +154,19 @@ const BudgetPanel: React.FC = ({ accessToken }) => { - {CREATE_END_USER_CURL_COMMAND} + + {CREATE_END_USER_CURL_COMMAND} + - {CHAT_COMPLETIONS_CURL_COMMAND} + + {CHAT_COMPLETIONS_CURL_COMMAND} + - {OPENAI_SDK_PYTHON_CODE} + + {OPENAI_SDK_PYTHON_CODE} + diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.test.tsx index 647258b3d48..c218db5b914 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.test.tsx @@ -15,6 +15,9 @@ vi.mock("react-syntax-highlighter", () => ({ vi.mock("react-syntax-highlighter/dist/esm/styles/prism", () => ({ coy: {}, + oneDark: {}, + oneLight: {}, + prism: {}, })); vi.mock("@/components/chat_ui/ReasoningContent", () => ({ diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.tsx index 2540c8f58b5..eb24463cfd3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.tsx @@ -3,6 +3,8 @@ import React from "react"; import ReactMarkdown from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; + +import { useSyntaxTheme } from "@/hooks/useSyntaxTheme"; import { CodeInterpreterResult } from "@/components/llm_calls/code_interpreter_handler"; import A2AMetrics from "./A2AMetrics"; import AudioRenderer from "./AudioRenderer"; @@ -38,6 +40,7 @@ function ChatMessageBubble({ codeInterpreterResult, accessToken, }: ChatMessageBubbleProps) { + const syntaxTheme = useSyntaxTheme(coy); const isUser = message.role === "user"; return ( @@ -143,13 +146,13 @@ function ChatMessageBubble({ const match = /language-(\w+)/.exec(className || ""); return !inline && match ? ( {String(children).replace(/\n$/, "")} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx index 2dd3972a714..cc964b2833b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx @@ -21,6 +21,8 @@ import { import React, { useEffect, useMemo, useRef, useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; + +import { useSyntaxTheme } from "@/hooks/useSyntaxTheme"; import { v4 as uuidv4 } from "uuid"; import useCan from "@/app/(dashboard)/hooks/useCan"; import GuardrailSelector from "@/components/guardrails/GuardrailSelector"; @@ -123,6 +125,7 @@ const ChatUI: React.FC = ({ simplified = false, fixedModel, }) => { + const syntaxTheme = useSyntaxTheme(coy); const canViewPolicies = useCan("viewPolicies"); const [mcpServers, setMCPServers] = useState([]); const [mcpToolsets, setMCPToolsets] = useState([]); @@ -2126,7 +2129,7 @@ const ChatUI: React.FC = ({ } + style={syntaxTheme} wrapLines={true} wrapLongLines={true} className="rounded-md" diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterOutput.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterOutput.tsx index 91972b5d770..1ddcec75173 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterOutput.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterOutput.tsx @@ -2,6 +2,8 @@ import React, { useEffect, useState } from "react"; import { Code, Download, FileImage, FileText, Loader2 } from "lucide-react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; + +import { useSyntaxTheme } from "@/hooks/useSyntaxTheme"; import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; @@ -33,6 +35,7 @@ function isImageFilename(filename: string | undefined): boolean { } const CodeInterpreterOutput: React.FC = ({ code, annotations = [], accessToken }) => { + const syntaxTheme = useSyntaxTheme(coy); const [imageUrls, setImageUrls] = useState>({}); const [loadingImages, setLoadingImages] = useState>({}); const [codeOpen, setCodeOpen] = useState(false); @@ -147,7 +150,7 @@ const CodeInterpreterOutput: React.FC = ({ code, ann
; } @@ -73,13 +76,13 @@ export function MessageDisplay({ messages, isLoading }: MessageDisplayProps) { const match = /language-(\w+)/.exec(className || ""); return !inline && match ? ( {String(children).replace(/\n$/, "")} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptCodeSnippets.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptCodeSnippets.tsx index 75a74fff6a1..af7d6421265 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptCodeSnippets.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptCodeSnippets.tsx @@ -2,6 +2,8 @@ import React, { useState } from "react"; import { CodeIcon, CopyIcon } from "lucide-react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; + +import { useSyntaxTheme } from "@/hooks/useSyntaxTheme"; import { toast } from "@/lib/toast"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; @@ -34,6 +36,7 @@ const PromptCodeSnippets: React.FC = ({ version = "1", proxySettings, }) => { + const syntaxTheme = useSyntaxTheme(coy); const [isModalVisible, setIsModalVisible] = useState(false); const [selectedLanguage, setSelectedLanguage] = useState<"curl" | "python" | "javascript">("curl"); const [selectedTab, setSelectedTab] = useState("basic"); @@ -296,7 +299,7 @@ main();`; = ({ message }) => { + const syntaxTheme = useSyntaxTheme(coy); return (
= ({ message }) => { const match = /language-(\w+)/.exec(className || ""); return !inline && match ? ( {String(children).replace(/\n$/, "")} diff --git a/ui/litellm-dashboard/src/app/chat/page.integration.test.tsx b/ui/litellm-dashboard/src/app/chat/page.integration.test.tsx index 29267e522d9..ec884a90d7b 100644 --- a/ui/litellm-dashboard/src/app/chat/page.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/chat/page.integration.test.tsx @@ -35,7 +35,7 @@ vi.mock("react-syntax-highlighter", () => ({ Prism: ({ children }: { children: string }) =>
{children}
, })); -vi.mock("react-syntax-highlighter/dist/esm/styles/prism", () => ({ coy: {} })); +vi.mock("react-syntax-highlighter/dist/esm/styles/prism", () => ({ coy: {}, oneDark: {}, oneLight: {}, prism: {} })); vi.mock("@/contexts/ChatShellContext", () => ({ useChatShell: () => { diff --git a/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx b/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx index acfafb62f84..299104b271b 100644 --- a/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx +++ b/ui/litellm-dashboard/src/components/AIHub/ModelHubTable.tsx @@ -33,6 +33,9 @@ import { Copy, Inbox } from "lucide-react"; import { useRouter } from "next/navigation"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { prism } from "react-syntax-highlighter/dist/esm/styles/prism"; + +import { useSyntaxTheme } from "@/hooks/useSyntaxTheme"; import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"; import { checkTokenValidity } from "@/utils/jwtUtils"; import { getCookie } from "@/utils/cookieUtils"; @@ -58,6 +61,7 @@ function HubEmptyState({ title, body }: { title: string; body: string }) { } const ModelHubTable: React.FC = ({ accessToken, publicPage, premiumUser, userRole }) => { + const syntaxTheme = useSyntaxTheme(prism); // Admin Viewer follows the read-parity rule: see the AI Hub catalog, but // cannot toggle public visibility (write). const canModify = isProxyAdminRole(userRole || ""); @@ -736,7 +740,7 @@ const ModelHubTable: React.FC = ({ accessToken, publicPage, {/* Usage Example */}

Usage Example

- + {`import openai client = openai.OpenAI( @@ -1057,7 +1061,7 @@ print(response.choices[0].message.content)`} {/* Usage Example */}

Usage Example

- + {`from fastmcp import Client import asyncio diff --git a/ui/litellm-dashboard/src/components/CodeBlock.tsx b/ui/litellm-dashboard/src/components/CodeBlock.tsx index cc944f95901..2e5dd96eef7 100644 --- a/ui/litellm-dashboard/src/components/CodeBlock.tsx +++ b/ui/litellm-dashboard/src/components/CodeBlock.tsx @@ -3,12 +3,15 @@ import { CheckIcon, ClipboardIcon } from "lucide-react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism"; +import { useSyntaxTheme } from "@/hooks/useSyntaxTheme"; + interface CodeBlockProps { code: string; language: string; } const CodeBlock = ({ code, language }: CodeBlockProps) => { + const syntaxTheme = useSyntaxTheme(oneLight); const [copied, setCopied] = useState(false); const copyToClipboard = () => { navigator.clipboard.writeText(code); @@ -27,7 +30,7 @@ const CodeBlock = ({ code, language }: CodeBlockProps) => { & { node?: unknown }) { + const syntaxTheme = useSyntaxTheme(coy); const match = /language-(\w+)/.exec(className || ""); return match ? ( - } - language={match[1]} - PreTag="div" - className="rounded-md my-2" - {...(props as Record)} - > + {String(children).replace(/\n$/, "")} ) : ( diff --git a/ui/litellm-dashboard/src/components/chat_ui/ReasoningContent.tsx b/ui/litellm-dashboard/src/components/chat_ui/ReasoningContent.tsx index c54b3180248..73c952eab3a 100644 --- a/ui/litellm-dashboard/src/components/chat_ui/ReasoningContent.tsx +++ b/ui/litellm-dashboard/src/components/chat_ui/ReasoningContent.tsx @@ -2,6 +2,8 @@ import React, { useState } from "react"; import ReactMarkdown from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; + +import { useSyntaxTheme } from "@/hooks/useSyntaxTheme"; import { ChevronDown, ChevronRight, Lightbulb } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; @@ -11,6 +13,7 @@ interface ReasoningContentProps { } const ReasoningContent: React.FC = ({ reasoningContent }) => { + const syntaxTheme = useSyntaxTheme(coy); const [isExpanded, setIsExpanded] = useState(true); if (!reasoningContent) return null; @@ -59,7 +62,7 @@ const ReasoningContent: React.FC = ({ reasoningContent }) wrapLines={true} wrapLongLines={true} {...props} - style={coy as { [key: string]: React.CSSProperties }} + style={syntaxTheme} > {String(children).replace(/\n$/, "")} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx index e80b447f402..798e786d69e 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx @@ -189,13 +189,13 @@ function NavigationSection({ onClose: () => void; }) { const keyboardShortcutStyle = { - border: "1px solid #d9d9d9", + border: "1px solid var(--color-border)", borderRadius: 4, padding: "0 4px", fontSize: 12, fontFamily: "monospace", marginLeft: 4, - background: "#fafafa", + background: "var(--color-muted)", }; const splitStyle = { width: 1, height: 20, background: COLOR_BORDER }; diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/InputCard.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/InputCard.tsx index 5f4c623fae1..d367c69f263 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/InputCard.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/InputCard.tsx @@ -39,7 +39,7 @@ export function InputCard({ messages, promptTokens, inputCost }: InputCardProps) return (
-
Loading request & response data...
+
+ Loading request & response data... +
) : ( ) : ( -
+
Response data not available
)} @@ -631,6 +640,11 @@ export function GuardrailJumpLink({ guardrailEntries }: { guardrailEntries: any[
{allPassed ? "\u2713" : "\u2717"} {guardrailEntries.length} guardrail{guardrailEntries.length !== 1 ? "s" : ""}{" "} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/RealtimePrettyView.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/RealtimePrettyView.tsx index 5441bcbc4cf..82f0d3e7fb2 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/RealtimePrettyView.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/RealtimePrettyView.tsx @@ -106,10 +106,10 @@ export function RealtimePrettyView({ response, metrics }: RealtimePrettyViewProp {!sessionEvent && responseEvents.length === 0 && (
{ - e.currentTarget.style.background = "#f5f5f5"; + e.currentTarget.style.background = "var(--color-accent)"; }} onMouseLeave={(e) => { - e.currentTarget.style.background = "#fafafa"; + e.currentTarget.style.background = "var(--color-muted)"; }} >
@@ -236,11 +236,11 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou style={{ fontSize: 12, lineHeight: 1.6, - color: "#595959", - background: "#fafafa", + color: "var(--color-muted-foreground)", + background: "var(--color-muted)", padding: "8px 12px", borderRadius: 4, - border: "1px solid #f0f0f0", + border: "1px solid var(--color-border)", whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: 120, @@ -282,7 +282,7 @@ function ConversationCard({ return (
{/* Turn header */} @@ -425,7 +425,7 @@ function OutputMessage({ output }: { output: RealtimeOutputItem }) { style={{ fontSize: 13, lineHeight: 1.7, - color: "#262626", + color: "var(--color-foreground)", whiteSpace: "pre-wrap", wordBreak: "break-word", }} @@ -484,7 +484,7 @@ function ConfigRow({ label, value }: { label: string; value: any }) { {label} -
{String(value)}
+
{String(value)}
); } diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts index 67c274a6d29..2771b9b15fe 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts @@ -27,9 +27,9 @@ export const FONT_SIZE_MEDIUM = 13; export const FONT_SIZE_HEADER = 16; // Colors -export const COLOR_BORDER = "#f0f0f0"; -export const COLOR_BACKGROUND = "#fff"; -export const COLOR_BG_LIGHT = "#fafafa"; +export const COLOR_BORDER = "var(--color-border)"; +export const COLOR_BACKGROUND = "var(--color-background)"; +export const COLOR_BG_LIGHT = "var(--color-muted)"; // Spacing export const SPACING_SMALL = 4; diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts index 1f73da1d30e..82ee081a7f8 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts @@ -19,27 +19,27 @@ import { export const ROLE_STYLES: Record = { system: { background: "transparent", - borderColor: "#8c8c8c", + borderColor: "var(--color-muted-foreground)", label: "SYSTEM", - labelColor: "#8c8c8c", + labelColor: "var(--color-muted-foreground)", }, user: { background: "transparent", - borderColor: "#1677ff", + borderColor: "var(--color-info)", label: "USER", - labelColor: "#1677ff", + labelColor: "var(--color-info)", }, assistant: { background: "transparent", - borderColor: "#52c41a", + borderColor: "var(--color-success)", label: "ASSISTANT", - labelColor: "#52c41a", + labelColor: "var(--color-success)", }, tool: { background: "transparent", - borderColor: "#fa8c16", + borderColor: "var(--color-warning)", label: "TOOL RESULT", - labelColor: "#fa8c16", + labelColor: "var(--color-warning)", }, }; diff --git a/ui/litellm-dashboard/src/hooks/useIsDarkMode.test.tsx b/ui/litellm-dashboard/src/hooks/useIsDarkMode.test.tsx new file mode 100644 index 00000000000..c12beda4db4 --- /dev/null +++ b/ui/litellm-dashboard/src/hooks/useIsDarkMode.test.tsx @@ -0,0 +1,42 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { useIsDarkMode } from "./useIsDarkMode"; + +beforeEach(() => { + document.documentElement.classList.remove("dark"); +}); + +afterAll(() => { + document.documentElement.classList.remove("dark"); +}); + +describe("useIsDarkMode", () => { + it("reports the dark class already on the root element at mount", () => { + document.documentElement.classList.add("dark"); + + const { result } = renderHook(() => useIsDarkMode()); + + expect(result.current).toBe(true); + }); + + it("follows the root element's dark class as it is toggled", async () => { + const { result } = renderHook(() => useIsDarkMode()); + expect(result.current).toBe(false); + + document.documentElement.classList.add("dark"); + await waitFor(() => expect(result.current).toBe(true)); + + document.documentElement.classList.remove("dark"); + await waitFor(() => expect(result.current).toBe(false)); + }); + + it("stops observing the root element once unmounted", () => { + const disconnect = vi.spyOn(MutationObserver.prototype, "disconnect"); + + const { unmount } = renderHook(() => useIsDarkMode()); + unmount(); + + expect(disconnect).toHaveBeenCalled(); + disconnect.mockRestore(); + }); +}); diff --git a/ui/litellm-dashboard/src/hooks/useIsDarkMode.ts b/ui/litellm-dashboard/src/hooks/useIsDarkMode.ts new file mode 100644 index 00000000000..bccaa31cb3a --- /dev/null +++ b/ui/litellm-dashboard/src/hooks/useIsDarkMode.ts @@ -0,0 +1,13 @@ +import { useSyncExternalStore } from "react"; + +const subscribe = (onStoreChange: () => void): (() => void) => { + const observer = new MutationObserver(onStoreChange); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] }); + return () => observer.disconnect(); +}; + +const getSnapshot = (): boolean => document.documentElement.classList.contains("dark"); + +const getServerSnapshot = (): boolean => false; + +export const useIsDarkMode = (): boolean => useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); diff --git a/ui/litellm-dashboard/src/hooks/useSyntaxTheme.test.tsx b/ui/litellm-dashboard/src/hooks/useSyntaxTheme.test.tsx new file mode 100644 index 00000000000..13b7d8136ef --- /dev/null +++ b/ui/litellm-dashboard/src/hooks/useSyntaxTheme.test.tsx @@ -0,0 +1,47 @@ +import { act, renderHook } from "@testing-library/react"; +import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"; +import { afterAll, beforeEach, describe, expect, it } from "vitest"; +import { useSyntaxTheme, type SyntaxTheme } from "./useSyntaxTheme"; + +const callerLightTheme: SyntaxTheme = { 'code[class*="language-"]': { color: "rebeccapurple" } }; + +const setRootDark = async (enabled: boolean) => { + await act(async () => { + document.documentElement.classList.toggle("dark", enabled); + await Promise.resolve(); + }); +}; + +beforeEach(() => { + document.documentElement.classList.remove("dark"); +}); + +afterAll(() => { + document.documentElement.classList.remove("dark"); +}); + +describe("useSyntaxTheme", () => { + it("keeps the caller's own stylesheet in light mode", () => { + const { result } = renderHook(() => useSyntaxTheme(callerLightTheme)); + + expect(result.current).toBe(callerLightTheme); + }); + + it("swaps to oneDark when the root element turns dark", async () => { + const { result } = renderHook(() => useSyntaxTheme(callerLightTheme)); + + await setRootDark(true); + + expect(result.current).toBe(oneDark); + }); + + it("restores the caller's stylesheet when dark mode is turned back off", async () => { + document.documentElement.classList.add("dark"); + const { result } = renderHook(() => useSyntaxTheme(callerLightTheme)); + expect(result.current).toBe(oneDark); + + await setRootDark(false); + + expect(result.current).toBe(callerLightTheme); + }); +}); diff --git a/ui/litellm-dashboard/src/hooks/useSyntaxTheme.ts b/ui/litellm-dashboard/src/hooks/useSyntaxTheme.ts new file mode 100644 index 00000000000..80f5b514751 --- /dev/null +++ b/ui/litellm-dashboard/src/hooks/useSyntaxTheme.ts @@ -0,0 +1,8 @@ +import type { CSSProperties } from "react"; +import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"; + +import { useIsDarkMode } from "./useIsDarkMode"; + +export type SyntaxTheme = Record; + +export const useSyntaxTheme = (light: SyntaxTheme): SyntaxTheme => (useIsDarkMode() ? oneDark : light);