diff --git a/apps/dev-playground/client/index.html b/apps/dev-playground/client/index.html index a20a7a0..149833a 100644 --- a/apps/dev-playground/client/index.html +++ b/apps/dev-playground/client/index.html @@ -7,6 +7,31 @@
+ diff --git a/apps/dev-playground/client/src/assets/databricks-logo-white.svg b/apps/dev-playground/client/src/assets/databricks-logo-white.svg new file mode 100644 index 0000000..5b67a67 --- /dev/null +++ b/apps/dev-playground/client/src/assets/databricks-logo-white.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/dev-playground/client/src/assets/databricks-logo.svg b/apps/dev-playground/client/src/assets/databricks-logo.svg new file mode 100644 index 0000000..efea639 --- /dev/null +++ b/apps/dev-playground/client/src/assets/databricks-logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/dev-playground/client/src/components/analytics/databricks-logo.tsx b/apps/dev-playground/client/src/components/analytics/databricks-logo.tsx index 69bf838..64cce6f 100644 --- a/apps/dev-playground/client/src/components/analytics/databricks-logo.tsx +++ b/apps/dev-playground/client/src/components/analytics/databricks-logo.tsx @@ -1,74 +1,84 @@ -import { useId } from "react"; +import { useEffect, useState } from "react"; +import databricksLogo from "@/assets/databricks-logo.svg"; +import databricksLogoWhite from "@/assets/databricks-logo-white.svg"; + +function useDarkMode() { + const [isDark, setIsDark] = useState(() => { + if (typeof window === "undefined") return false; + const root = document.documentElement; + // Check if dark class is explicitly set + if (root.classList.contains("dark")) return true; + if (root.classList.contains("light")) return false; + // Fallback to system preference + return window.matchMedia("(prefers-color-scheme: dark)").matches; + }); + + useEffect(() => { + if (typeof window === "undefined") return; + + const root = document.documentElement; + + const checkTheme = () => { + if (root.classList.contains("dark")) { + setIsDark(true); + } else if (root.classList.contains("light")) { + setIsDark(false); + } else { + // No explicit class, use system preference + setIsDark(window.matchMedia("(prefers-color-scheme: dark)").matches); + } + }; + + // Check initial theme + checkTheme(); + + // Observe changes to the classList + const observer = new MutationObserver(checkTheme); + observer.observe(root, { + attributes: true, + attributeFilter: ["class"], + }); + + // Also listen to system theme changes + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handleMediaChange = () => { + // Only update if no explicit class is set + if ( + !root.classList.contains("dark") && + !root.classList.contains("light") + ) { + setIsDark(mediaQuery.matches); + } + }; + + if (mediaQuery.addEventListener) { + mediaQuery.addEventListener("change", handleMediaChange); + return () => { + observer.disconnect(); + mediaQuery.removeEventListener("change", handleMediaChange); + }; + } else { + mediaQuery.addListener(handleMediaChange); + return () => { + observer.disconnect(); + mediaQuery.removeListener(handleMediaChange); + }; + } + }, []); + + return isDark; +} export function DatabricksLogo() { - const titleId = useId(); - const clipPathId = useId(); + const isDark = useDarkMode(); + const logoSrc = isDark ? databricksLogoWhite : databricksLogo; + return ( - - Databricks - - - - - - - - - - - - - - - - - - - - + Databricks ); } diff --git a/apps/dev-playground/client/src/components/theme-selector.tsx b/apps/dev-playground/client/src/components/theme-selector.tsx new file mode 100644 index 0000000..9cc96e0 --- /dev/null +++ b/apps/dev-playground/client/src/components/theme-selector.tsx @@ -0,0 +1,140 @@ +import { useEffect, useState } from "react"; +import { MoonIcon, SunIcon, MonitorIcon } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@databricks/app-kit-ui/react"; +import { Button } from "@databricks/app-kit-ui/react"; + +type Theme = "light" | "dark" | "system"; + +const THEME_STORAGE_KEY = "app-kit-playground-theme"; + +function getSystemTheme(): "light" | "dark" { + if (typeof window === "undefined") return "light"; + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; +} + +function getStoredTheme(): Theme { + if (typeof window === "undefined") return "system"; + const stored = localStorage.getItem(THEME_STORAGE_KEY); + return (stored as Theme) || "system"; +} + +function applyTheme(theme: Theme) { + if (typeof window === "undefined") return; + + const root = document.documentElement; + root.classList.remove("light", "dark"); + + if (theme === "system") { + const systemTheme = getSystemTheme(); + root.classList.add(systemTheme); + } else { + root.classList.add(theme); + } +} + +export function ThemeSelector() { + const [theme, setTheme] = useState(() => getStoredTheme()); + const [mounted, setMounted] = useState(false); + const [systemTheme, setSystemTheme] = useState<"light" | "dark">(() => + getSystemTheme(), + ); + + useEffect(() => { + setMounted(true); + applyTheme(theme); + }, [theme]); + + useEffect(() => { + // Listen for system theme changes + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handleChange = (e: MediaQueryListEvent | MediaQueryList) => { + const isDark = e.matches; + setSystemTheme(isDark ? "dark" : "light"); + // Apply theme if current theme is "system" + if (theme === "system") { + applyTheme("system"); + } + }; + + // Set initial system theme + handleChange(mediaQuery); + + if (mediaQuery.addEventListener) { + mediaQuery.addEventListener("change", handleChange); + return () => mediaQuery.removeEventListener("change", handleChange); + } else { + mediaQuery.addListener(handleChange); + return () => mediaQuery.removeListener(handleChange); + } + }, [theme]); + + const handleThemeChange = (newTheme: Theme) => { + setTheme(newTheme); + localStorage.setItem(THEME_STORAGE_KEY, newTheme); + applyTheme(newTheme); + }; + + // Get current effective theme for icon display + const effectiveTheme = theme === "system" ? systemTheme : theme; + + if (!mounted) { + // Return a placeholder to avoid hydration mismatch + return ( + + ); + } + + return ( + + + + + + handleThemeChange("light")} + className="cursor-pointer" + > + + Light + {theme === "light" && } + + handleThemeChange("dark")} + className="cursor-pointer" + > + + Dark + {theme === "dark" && } + + handleThemeChange("system")} + className="cursor-pointer" + > + + System + {theme === "system" && } + + + + ); +} diff --git a/apps/dev-playground/client/src/routes/__root.tsx b/apps/dev-playground/client/src/routes/__root.tsx index c4baee2..14f417d 100644 --- a/apps/dev-playground/client/src/routes/__root.tsx +++ b/apps/dev-playground/client/src/routes/__root.tsx @@ -7,6 +7,7 @@ import { } from "@tanstack/react-router"; import { ErrorComponent } from "@/components/error-component"; import { Button, TooltipProvider } from "@databricks/app-kit-ui/react"; +import { ThemeSelector } from "@/components/theme-selector"; export const Route = createRootRoute({ component: RootComponent, @@ -30,7 +31,7 @@ function RootComponent() { App Kit Playground -
+
- +
diff --git a/apps/dev-playground/client/src/routes/index.tsx b/apps/dev-playground/client/src/routes/index.tsx index 43047cf..ce99bc3 100644 --- a/apps/dev-playground/client/src/routes/index.tsx +++ b/apps/dev-playground/client/src/routes/index.tsx @@ -4,6 +4,7 @@ import { useNavigate, } from "@tanstack/react-router"; import { Button, Card } from "@databricks/app-kit-ui/react"; +import { ThemeSelector } from "@/components/theme-selector"; export const Route = createFileRoute("/")({ component: IndexRoute, @@ -17,6 +18,9 @@ function IndexRoute() { return (
+
+ +

diff --git a/packages/app-kit-ui/src/react/styles/globals.css b/packages/app-kit-ui/src/react/styles/globals.css index 9650fcc..79148ae 100644 --- a/packages/app-kit-ui/src/react/styles/globals.css +++ b/packages/app-kit-ui/src/react/styles/globals.css @@ -42,8 +42,49 @@ --sidebar-ring: oklch(0.705 0.015 286.067); } +/* Dark theme via class (takes precedence over media query) */ +.dark { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.92 0.004 286.32); + --primary-foreground: oklch(0.21 0.006 285.885); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --destructive-foreground: oklch(0.985 0 0); + --success: oklch(0.67 0.12 167); + --success-foreground: oklch(1 0 0); + --warning: oklch(0.83 0.165 85); + --warning-foreground: oklch(0.199 0.027 238.732); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.552 0.016 285.938); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.552 0.016 285.938); +} + +/* Dark theme via media query (fallback when no class is set) */ @media (prefers-color-scheme: dark) { - :root { + :root:not(.light) { --background: oklch(0.141 0.005 285.823); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885); @@ -217,8 +258,11 @@ *, *::before, *::after { + /* biome-ignore lint/complexity/noImportantStyles: !important is intentional for accessibility - forces reduced motion regardless of specificity */ animation-duration: 0.01ms !important; + /* biome-ignore lint/complexity/noImportantStyles: !important is intentional for accessibility - forces reduced motion regardless of specificity */ animation-iteration-count: 1 !important; + /* biome-ignore lint/complexity/noImportantStyles: !important is intentional for accessibility - forces reduced motion regardless of specificity */ transition-duration: 0.01ms !important; } }