From f32192221699216aaccca2a32b68106f543fd3af Mon Sep 17 00:00:00 2001 From: Paolo Scattolin Date: Tue, 10 Feb 2026 15:54:36 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=8E=A8=20theming:=20Add=20dark=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Badge.tsx | 14 +-- src/components/Button.tsx | 4 +- src/components/Form.tsx | 4 + src/components/Icon.tsx | 6 +- src/components/NotificationCard.tsx | 14 +-- src/components/Spinner.tsx | 16 ++-- src/components/utils.test.ts | 108 +++++++++++++++++------ src/components/utils.ts | 51 +++++++---- src/main.tsx | 20 +++-- src/pages/PlaceholderPage.tsx | 26 +++--- src/pages/main-page/SettingsPage.tsx | 26 ++++++ src/pages/main-page/StatusIndicator.tsx | 9 +- src/theme/GlobalStyles.ts | 6 ++ src/theme/ThemeModeContext.ts | 17 ++++ src/theme/ThemeModeProvider.tsx | 71 +++++++++++++++ src/theme/ThemedApp.tsx | 16 ++++ src/theme/index.ts | 5 +- src/theme/theme.ts | 109 ++++++++++++++++++++++++ 18 files changed, 428 insertions(+), 94 deletions(-) create mode 100644 src/theme/ThemeModeContext.ts create mode 100644 src/theme/ThemeModeProvider.tsx create mode 100644 src/theme/ThemedApp.tsx diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index cafb911..7e694ec 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { BadgeShape, getThemeColorsByVariant, ThemeVariant } from "./utils"; +import { BadgeShape, ThemeVariant, getVariantColors } from "./utils"; interface BadgeProps extends React.HTMLAttributes { $variant?: ThemeVariant; @@ -31,16 +31,16 @@ export const Badge = styled.span` border-radius: ${({ $shape = BadgeShape.Pill, theme }) => $shape === BadgeShape.Rounded ? theme.radius.md : theme.radius.full}; border: 1px solid - ${({ $variant = ThemeVariant.Info }) => - getThemeColorsByVariant($variant).border}; + ${({ $variant = ThemeVariant.Info, theme }) => + getVariantColors(theme, $variant).border}; font: ${({ theme }) => theme.typography.small.font}; line-height: ${({ theme }) => theme.fonts.lineHeight.mini}; - background: ${({ $variant = ThemeVariant.Info }) => - getThemeColorsByVariant($variant).bg}; - color: ${({ $variant = ThemeVariant.Info }) => - getThemeColorsByVariant($variant).text}; + background: ${({ $variant = ThemeVariant.Info, theme }) => + getVariantColors(theme, $variant).bg}; + color: ${({ $variant = ThemeVariant.Info, theme }) => + getVariantColors(theme, $variant).text}; ${({ $isInteractive, theme }) => $isInteractive diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 3c241c9..5bec95d 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,8 +1,8 @@ import styled from "styled-components"; import { ButtonWidth, - getThemeColorsByVariant, getThemeSizeValuesByThemeSize, + getVariantColors, ThemeSize, ThemeVariant, } from "./utils"; @@ -67,7 +67,7 @@ export const Button = styled.button` transition: all ${({ theme }) => theme.transitions.base}; ${({ $variant = ThemeVariant.Info, $ghost, theme }) => { - const colors = getThemeColorsByVariant($variant); + const colors = getVariantColors(theme, $variant); const baseBg = colors.solidBg; const hoverBg = colors.text; const textColor = colors.solidText; diff --git a/src/components/Form.tsx b/src/components/Form.tsx index d8bbac2..ef908da 100644 --- a/src/components/Form.tsx +++ b/src/components/Form.tsx @@ -19,6 +19,8 @@ export const Input = styled.input` padding: ${({ theme }) => theme.spacing.md} ${({ theme }) => theme.spacing.md}; font: ${({ theme }) => theme.typography.caption.font}; + color: ${({ theme }) => theme.colors.text.primary}; + background: ${({ theme }) => theme.colors.background.primary}; transition: border-color ${({ theme }) => theme.transitions.base}; @@ -58,6 +60,8 @@ const StyledSelect = styled.select` padding: ${({ theme }) => `${theme.spacing.sm} ${theme.spacing.md}`}; font: ${({ theme }) => theme.typography.body.font}; + color: ${({ theme }) => theme.colors.text.primary}; + background: ${({ theme }) => theme.colors.background.primary}; line-height: 1.5; transition: border-color ${({ theme }) => theme.transitions.base}; diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 5af12a2..56d2061 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -1,8 +1,8 @@ import { LucideIcon } from "lucide-react"; import styled from "styled-components"; import { - getThemeColorsByVariant, getThemePixelSizeValuesByThemeSize, + getVariantColors, ThemeSize, ThemeVariant, } from "./utils"; @@ -36,10 +36,10 @@ const IconWrapper = styled.span<{ transition: all ${({ theme }) => theme.transitions.base}; - ${({ $variant, $ghost, $round }) => { + ${({ $variant, $ghost, $round, theme }) => { if (!$round || !$variant) return ""; - const colors = getThemeColorsByVariant($variant); + const colors = getVariantColors(theme, $variant); const baseBg = colors.solidBg; const textColor = colors.solidText; const borderColor = colors.border; diff --git a/src/components/NotificationCard.tsx b/src/components/NotificationCard.tsx index f2f24eb..b702e12 100644 --- a/src/components/NotificationCard.tsx +++ b/src/components/NotificationCard.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { ThemeVariant, getThemeColorsByVariant } from "./utils"; +import { ThemeVariant, getVariantColors } from "./utils"; export interface NotificationCardProps extends React.HTMLAttributes { $variant?: ThemeVariant; @@ -38,13 +38,13 @@ export const NotificationCard = styled.div` border-radius: ${({ theme }) => theme.radius.md}; border: 1px solid - ${({ $variant = ThemeVariant.Info }) => - getThemeColorsByVariant($variant).border}; + ${({ $variant = ThemeVariant.Info, theme }) => + getVariantColors(theme, $variant).border}; - background: ${({ $variant = ThemeVariant.Info }) => - getThemeColorsByVariant($variant).bg}; - color: ${({ $variant = ThemeVariant.Info }) => - getThemeColorsByVariant($variant).text}; + background: ${({ $variant = ThemeVariant.Info, theme }) => + getVariantColors(theme, $variant).bg}; + color: ${({ $variant = ThemeVariant.Info, theme }) => + getVariantColors(theme, $variant).text}; font: ${({ theme }) => theme.typography.body.font}; `; diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index db375e9..2969481 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -1,10 +1,11 @@ import { spin } from "@/theme/animations"; +import { theme } from "@/theme/theme"; import React from "react"; import styled from "styled-components"; import { - getThemeColorsByVariant, getThemePixelSizeValuesByThemeSize, getThemeSizeValuesByThemeSize, + getVariantColors, ThemeSize, ThemeVariant, } from "./utils"; @@ -35,10 +36,10 @@ const SpinnerContainer = styled.div<{ border-radius: 50%; - background-color: ${({ $variant }) => - getThemeColorsByVariant($variant).solidBg}; + background-color: ${({ $variant, theme }) => + getVariantColors(theme, $variant).solidBg}; border: 1px solid - ${({ $variant }) => getThemeColorsByVariant($variant).border}; + ${({ $variant, theme }) => getVariantColors(theme, $variant).border}; `; export interface SpinnerProps extends React.HTMLAttributes { @@ -77,9 +78,12 @@ export const Spinner: React.FC = ({ className, ...rest }) => { - const resolvedColor = $color || getThemeColorsByVariant($variant).solidBg; + // Note: We can't access theme context here in the component function, + // so we continue using the static theme for optional color overrides. + // This is acceptable since $color and $trackColor are explicit overrides. + const resolvedColor = $color || getVariantColors(theme, $variant).solidBg; const resolvedTrackColor = - $trackColor || getThemeColorsByVariant($variant).solidText; + $trackColor || getVariantColors(theme, $variant).solidText; const spinner = ( { - test("getThemeColorsByVariant returns correct colors for all variants", () => { - expect(getThemeColorsByVariant(ThemeVariant.Success)).toBe( - theme.colors.success, - ); - expect(getThemeColorsByVariant(ThemeVariant.Warning)).toBe( - theme.colors.warning, - ); - expect(getThemeColorsByVariant(ThemeVariant.Error)).toBe( - theme.colors.error, - ); - expect(getThemeColorsByVariant(ThemeVariant.Info)).toBe(theme.colors.info); - expect(getThemeColorsByVariant(ThemeVariant.Primary)).toBe( - theme.colors.info, - ); - expect(getThemeColorsByVariant(ThemeVariant.Accent)).toBe( - theme.colors.accent, - ); - expect(getThemeColorsByVariant(ThemeVariant.Secondary)).toBe( - theme.colors.accent, - ); - }); + describe("getVariantColors", () => { + test("returns correct colors for all variants with light theme", () => { + expect(getVariantColors(theme, ThemeVariant.Success)).toEqual( + theme.colors.success, + ); + expect(getVariantColors(theme, ThemeVariant.Warning)).toEqual( + theme.colors.warning, + ); + expect(getVariantColors(theme, ThemeVariant.Error)).toEqual( + theme.colors.error, + ); + expect(getVariantColors(theme, ThemeVariant.Info)).toEqual( + theme.colors.info, + ); + expect(getVariantColors(theme, ThemeVariant.Primary)).toEqual( + theme.colors.info, + ); + expect(getVariantColors(theme, ThemeVariant.Accent)).toEqual( + theme.colors.accent, + ); + expect(getVariantColors(theme, ThemeVariant.Secondary)).toEqual( + theme.colors.accent, + ); + }); + + test("returns correct colors for all variants with dark theme", () => { + expect(getVariantColors(darkTheme, ThemeVariant.Success)).toEqual( + darkTheme.colors.success, + ); + expect(getVariantColors(darkTheme, ThemeVariant.Warning)).toEqual( + darkTheme.colors.warning, + ); + expect(getVariantColors(darkTheme, ThemeVariant.Error)).toEqual( + darkTheme.colors.error, + ); + expect(getVariantColors(darkTheme, ThemeVariant.Info)).toEqual( + darkTheme.colors.info, + ); + expect(getVariantColors(darkTheme, ThemeVariant.Primary)).toEqual( + darkTheme.colors.info, + ); + expect(getVariantColors(darkTheme, ThemeVariant.Accent)).toEqual( + darkTheme.colors.accent, + ); + expect(getVariantColors(darkTheme, ThemeVariant.Secondary)).toEqual( + darkTheme.colors.accent, + ); + }); + + test("returns different colors for light and dark themes", () => { + const lightSuccess = getVariantColors(theme, ThemeVariant.Success); + const darkSuccess = getVariantColors(darkTheme, ThemeVariant.Success); + + expect(lightSuccess.bg).not.toBe(darkSuccess.bg); + expect(lightSuccess.text).not.toBe(darkSuccess.text); + expect(lightSuccess.border).not.toBe(darkSuccess.border); + }); + + test("has consistent structure across all variants", () => { + const variants = [ + ThemeVariant.Success, + ThemeVariant.Warning, + ThemeVariant.Error, + ThemeVariant.Info, + ThemeVariant.Accent, + ]; - test("getThemeColorsByVariant returns info color as fallback for invalid variant", () => { - const invalidVariant = "invalid" as ThemeVariant; - expect(getThemeColorsByVariant(invalidVariant)).toBe(theme.colors.info); + variants.forEach((variant) => { + const colors = getVariantColors(theme, variant); + expect(colors).toHaveProperty("bg"); + expect(colors).toHaveProperty("border"); + expect(colors).toHaveProperty("text"); + expect(colors).toHaveProperty("solidBg"); + expect(colors).toHaveProperty("solidText"); + expect(typeof colors.bg).toBe("string"); + expect(typeof colors.border).toBe("string"); + expect(typeof colors.text).toBe("string"); + expect(typeof colors.solidBg).toBe("string"); + expect(typeof colors.solidText).toBe("string"); + }); + }); }); test("getThemeSizeValuesByThemeSize returns correct size values for all sizes", () => { diff --git a/src/components/utils.ts b/src/components/utils.ts index 835fc86..8f3fe7c 100644 --- a/src/components/utils.ts +++ b/src/components/utils.ts @@ -1,7 +1,7 @@ // Whenever you export both components and other things TypeScript complaints that fast-reload // cannot handle mixed exports. For this reason, we export them here. -import { theme } from "@/theme/theme"; +import { theme, Theme } from "@/theme/theme"; import { createContext, useContext } from "react"; // ENUMS & CONSTANTS @@ -35,17 +35,6 @@ export const enum ThemeSize { FourXL = "4xl", } -const THEME_COLORS_BY_VARIANT: Record = - { - [ThemeVariant.Success]: theme.colors.success, - [ThemeVariant.Warning]: theme.colors.warning, - [ThemeVariant.Error]: theme.colors.error, - [ThemeVariant.Info]: theme.colors.info, - [ThemeVariant.Primary]: theme.colors.info, - [ThemeVariant.Accent]: theme.colors.accent, - [ThemeVariant.Secondary]: theme.colors.accent, - }; - const THEME_SIZE_VALUES_BY_THEME_SIZE: Record = { [ThemeSize.ExtraSmall]: theme.size.xs, [ThemeSize.Small]: theme.size.sm, @@ -108,17 +97,43 @@ export const useTabs = () => { return context; }; +// TYPES + +export type VariantColors = { + bg: string; + border: string; + text: string; + solidBg: string; + solidText: string; +}; + // FUNCTIONS /** - * Maps the theme colors based on the provided theme variant. - * @param variant - The ThemeVariant to map. - * @returns The corresponding theme colors for the given variant. + * Maps a theme variant to the corresponding color set from a theme object. + * This function reads colors from the provided theme parameter, allowing it to work + * with both light and dark themes dynamically. + * @param themeObj - The theme object containing color definitions (light or dark theme) + * @param variant - The ThemeVariant to map + * @returns The corresponding theme colors for the given variant * @example - * const colors = getThemeColorsByVariant(ThemeVariant.Success); + * // In a styled-component (theme comes from context) + * const colors = getVariantColors(theme, ThemeVariant.Success); */ -export const getThemeColorsByVariant = (variant: ThemeVariant) => { - return THEME_COLORS_BY_VARIANT[variant] ?? theme.colors.info; +export const getVariantColors = ( + themeObj: Theme, + variant: ThemeVariant, +): VariantColors => { + const variantMap: Record = { + [ThemeVariant.Success]: "success", + [ThemeVariant.Warning]: "warning", + [ThemeVariant.Error]: "error", + [ThemeVariant.Info]: "info", + [ThemeVariant.Primary]: "info", + [ThemeVariant.Accent]: "accent", + [ThemeVariant.Secondary]: "accent", + }; + return themeObj.colors[variantMap[variant]] as VariantColors; }; /** diff --git a/src/main.tsx b/src/main.tsx index ff4c113..d61ad16 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,11 +2,11 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router"; -import { ThemeProvider } from "styled-components"; import App from "./App"; -import { theme } from "./theme"; +import { ThemeModeProvider } from "./theme"; import { GlobalStyles } from "./theme/GlobalStyles"; +import { ThemedApp } from "./theme/ThemedApp"; // Create a client for React Query const queryClient = new QueryClient(); @@ -14,12 +14,14 @@ const queryClient = new QueryClient(); createRoot(document.getElementById("root")!).render( - - - - - - + + + + + + + + - + , ); diff --git a/src/pages/PlaceholderPage.tsx b/src/pages/PlaceholderPage.tsx index 34d1715..99a591e 100644 --- a/src/pages/PlaceholderPage.tsx +++ b/src/pages/PlaceholderPage.tsx @@ -1,23 +1,27 @@ import { Text } from "@/components"; -import { theme } from "@/theme/theme"; +import styled from "styled-components"; interface PlaceholderPageProps { content: string; } +const PlaceholderContainer = styled.div` + padding: ${({ theme }) => theme.spacing.xl}; + background: ${({ theme }) => theme.colors.background.primary}; + border-radius: ${({ theme }) => theme.radius.md}; + border: 1px solid ${({ theme }) => theme.colors.border.medium}; +`; + +const PlaceholderText = styled(Text)` + color: ${({ theme }) => theme.colors.text.tertiary}; +`; + export const PlaceholderPage: React.FC = ({ content, }) => { return ( -
- {content} -
+ + {content} + ); }; diff --git a/src/pages/main-page/SettingsPage.tsx b/src/pages/main-page/SettingsPage.tsx index 6e499f4..9a9dd50 100644 --- a/src/pages/main-page/SettingsPage.tsx +++ b/src/pages/main-page/SettingsPage.tsx @@ -10,6 +10,7 @@ import { CardHeader, CardTitle, FormLabel, + RadioGroup, Spacer, SuspenseCard, ThemeVariant, @@ -19,10 +20,13 @@ import { DetailsItem, DetailsValue, } from "@/pages/main-page/monitoring-page/ExecutionDetailPage.styles"; +import { ThemeMode, useThemeMode } from "@/theme"; import { formatDateTime, formatDuration } from "@/utils"; import { CreditsEasterEgg } from "./Credits"; export const SettingsPage: React.FC = () => { + const { mode, setMode } = useThemeMode(); + const { data, isLoading, isError, error } = useQuery({ queryKey: ["system-status"], queryFn: getSystemStatus, @@ -39,6 +43,28 @@ export const SettingsPage: React.FC = () => { $errorMessage={parsedError?.message} > + + + Appearance + + + + + Theme + setMode(v as ThemeMode)} + /> + + + + SOARCA information diff --git a/src/pages/main-page/StatusIndicator.tsx b/src/pages/main-page/StatusIndicator.tsx index 5a358e7..9757493 100644 --- a/src/pages/main-page/StatusIndicator.tsx +++ b/src/pages/main-page/StatusIndicator.tsx @@ -2,7 +2,8 @@ import { useQuery } from "@tanstack/react-query"; import { css, styled } from "styled-components"; import { getPingStatus } from "@/api/status"; -import { getThemeColorsByVariant, ThemeVariant } from "@/components/utils"; +import { getVariantColors, ThemeVariant } from "@/components/utils"; +import { theme } from "@/theme"; import { pulse } from "@/theme/animations"; export const StatusIndicator: React.FC = () => { @@ -57,10 +58,10 @@ const StatusDot = styled.div<{ background-color: ${({ $isOnline, $isReconnecting }) => $isOnline - ? getThemeColorsByVariant(ThemeVariant.Success).solidBg + ? getVariantColors(theme, ThemeVariant.Success).solidBg : $isReconnecting - ? getThemeColorsByVariant(ThemeVariant.Warning).solidBg - : getThemeColorsByVariant(ThemeVariant.Error).solidBg}; + ? getVariantColors(theme, ThemeVariant.Warning).solidBg + : getVariantColors(theme, ThemeVariant.Error).solidBg}; animation: ${({ $isOnline, $isReconnecting }) => $isOnline || $isReconnecting diff --git a/src/theme/GlobalStyles.ts b/src/theme/GlobalStyles.ts index 8a7c23b..f9e1954 100644 --- a/src/theme/GlobalStyles.ts +++ b/src/theme/GlobalStyles.ts @@ -5,4 +5,10 @@ export const GlobalStyles = createGlobalStyle` margin: 0; padding: 0; } + + body { + background-color: ${({ theme }) => theme.colors.background.primary}; + color: ${({ theme }) => theme.colors.text.primary}; + transition: background-color 0.2s ease, color 0.2s ease; + } `; diff --git a/src/theme/ThemeModeContext.ts b/src/theme/ThemeModeContext.ts new file mode 100644 index 0000000..a844659 --- /dev/null +++ b/src/theme/ThemeModeContext.ts @@ -0,0 +1,17 @@ +import { createContext, useContext } from "react"; +import type { ThemeModeContextValue } from "./ThemeModeProvider"; + +/** Context and hook for managing theme mode (auto/light/dark) and system preference. */ +export const ThemeModeCtx = createContext( + undefined, +); + +/** + * Hook to access the current theme mode, resolved theme, and setter. + */ +export const useThemeMode = (): ThemeModeContextValue => { + const ctx = useContext(ThemeModeCtx); + if (!ctx) + throw new Error("useThemeMode must be used within a ThemeModeProvider"); + return ctx; +}; diff --git a/src/theme/ThemeModeProvider.tsx b/src/theme/ThemeModeProvider.tsx new file mode 100644 index 0000000..63d45ab --- /dev/null +++ b/src/theme/ThemeModeProvider.tsx @@ -0,0 +1,71 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { ThemeModeCtx } from "./ThemeModeContext"; + +/** The three user-selectable modes */ +export type ThemeMode = "auto" | "light" | "dark"; +/** The resolved (effective) theme applied to the UI */ +export type ResolvedTheme = "light" | "dark"; + +export interface ThemeModeContextValue { + /** Current user-selected mode */ + mode: ThemeMode; + /** Resolved theme after applying system preference (when mode === "auto") */ + resolved: ResolvedTheme; + /** Update the user-selected mode */ + setMode: (mode: ThemeMode) => void; +} + +const STORAGE_KEY = "soarca-theme-mode"; + +/** + * Reads the system color-scheme preference via matchMedia. + */ +const getSystemPreference = (): ResolvedTheme => + window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + +/** + * Reads the persisted theme mode from localStorage, defaulting to "auto". + */ +const getPersistedMode = (): ThemeMode => { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored === "light" || stored === "dark" || stored === "auto") + return stored; + return "auto"; +}; + +/** + * Provider that manages theme mode (auto/light/dark), detects the system + * preference when set to "auto", and persists the choice to localStorage. + */ +export const ThemeModeProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [mode, setModeState] = useState(getPersistedMode); + const [systemPref, setSystemPref] = + useState(getSystemPreference); + + // Listen for OS-level theme changes + useEffect(() => { + const mql = window.matchMedia("(prefers-color-scheme: dark)"); + const handler = (e: MediaQueryListEvent) => + setSystemPref(e.matches ? "dark" : "light"); + mql.addEventListener("change", handler); + return () => mql.removeEventListener("change", handler); + }, []); + + const setMode = useCallback((next: ThemeMode) => { + setModeState(next); + localStorage.setItem(STORAGE_KEY, next); + }, []); + + const resolved: ResolvedTheme = mode === "auto" ? systemPref : mode; + + const value = useMemo( + () => ({ mode, resolved, setMode }), + [mode, resolved, setMode], + ); + + return ( + {children} + ); +}; diff --git a/src/theme/ThemedApp.tsx b/src/theme/ThemedApp.tsx new file mode 100644 index 0000000..51bd728 --- /dev/null +++ b/src/theme/ThemedApp.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { ThemeProvider } from "styled-components"; + +import { darkTheme, theme, useThemeMode } from "."; + +/** + * Hooks into the theme context to determine the active theme and provides it to the app via ThemeProvider. + */ +export const ThemedApp: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const { resolved } = useThemeMode(); + const activeTheme = resolved === "dark" ? darkTheme : theme; + + return {children}; +}; diff --git a/src/theme/index.ts b/src/theme/index.ts index d22b13c..ec25b4b 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -1,2 +1,5 @@ -export { theme } from "./theme"; +export { darkTheme, theme } from "./theme"; export type { Theme } from "./theme"; +export { useThemeMode } from "./ThemeModeContext"; +export { ThemeModeProvider } from "./ThemeModeProvider"; +export type { ResolvedTheme, ThemeMode } from "./ThemeModeProvider"; diff --git a/src/theme/theme.ts b/src/theme/theme.ts index b2357f9..96f297f 100644 --- a/src/theme/theme.ts +++ b/src/theme/theme.ts @@ -230,4 +230,113 @@ export const theme = { }, }; +export const darkTheme: Theme = { + ...theme, + colors: { + primary: { + main: "#60a5fa", + hover: "#93bbfd", + disabled: "#6b7280", + bg: "#1e2a3a", + border: "#60a5fa", + text: "#ffffff", + }, + secondary: { + main: "#374151", + hover: "#4b5563", + text: "#e5e7eb", + bg: "#1f2937", + }, + error: { + bg: "#3b1c1c", + border: "#ef4444", + text: "#fca5a5", + solidBg: "#dc2626", + solidText: "#ffffff", + }, + info: { + bg: "#1e2a3a", + border: "#60a5fa", + text: "#93c5fd", + solidBg: "#3b82f6", + solidText: "#ffffff", + }, + success: { + bg: "#1a2e1a", + border: "#22c55e", + text: "#86efac", + solidBg: "#16a34a", + solidText: "#ffffff", + }, + warning: { + bg: "#2e2a1a", + border: "#facc15", + text: "#fde68a", + solidBg: "#eab308", + solidText: "#1f2937", + }, + accent: { + bg: "#2a1f3d", + border: "#8b7cf7", + text: "#c4b5fd", + solidBg: "#6a5ac7", + solidText: "#ffffff", + }, + gray: { + 50: "#111827", + 100: "#1f2937", + 200: "#374151", + 300: "#4b5563", + 400: "#6b7280", + 500: "#9ca3af", + 600: "#d1d5db", + 700: "#e5e7eb", + 800: "#f3f4f6", + 900: "#f9fafb", + }, + text: { + primary: "#f3f4f6", + secondary: "#d1d5db", + tertiary: "#9ca3af", + placeholder: "#6b7280", + }, + background: { + primary: "#111827", + secondary: "#1f2937", + tertiary: "#171f2e", + overlay: "rgba(0, 0, 0, 0.7)", + }, + border: { + light: "#374151", + medium: "#4b5563", + dark: "#6b7280", + }, + table: { + headerBg: "#1e3a5f", + headerText: "#e5e7eb", + rowEvenBg: "#111827", + rowOddBg: "#1a2332", + rowText: "#e5e7eb", + border: "#374151", + }, + palette: { + primary: "#60a5fa", + primaryDark: "#3b82f6", + secondary: "#34d399", + tertiary: "#67e8f9", + background: "#0f172a", + surface: "#1e293b", + textPrimary: "#f1f5f9", + textSecondary: "#cbd5e1", + accent: "#8b7cf7", + }, + }, + shadows: { + sm: "0 1px 2px rgba(0, 0, 0, 0.3)", + base: "0 1px 3px rgba(0, 0, 0, 0.4)", + md: "0 4px 6px rgba(0, 0, 0, 0.4)", + lg: "0 10px 15px rgba(0, 0, 0, 0.4)", + }, +}; + export type Theme = typeof theme; From 6dadecf57a72204b556c65233cc54be0449bbdb5 Mon Sep 17 00:00:00 2001 From: Paolo Scattolin Date: Wed, 18 Feb 2026 10:53:24 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=8E=A8=20theming:=20Extend=20theming?= =?UTF-8?q?=20to=20scrollbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/theme/GlobalStyles.ts | 52 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/theme/GlobalStyles.ts b/src/theme/GlobalStyles.ts index f9e1954..9710227 100644 --- a/src/theme/GlobalStyles.ts +++ b/src/theme/GlobalStyles.ts @@ -1,14 +1,64 @@ import { createGlobalStyle } from "styled-components"; export const GlobalStyles = createGlobalStyle` - * { + *, *::before, *::after { margin: 0; padding: 0; } + html, body, #root { + height: 100%; + width: 100%; + } + body { background-color: ${({ theme }) => theme.colors.background.primary}; color: ${({ theme }) => theme.colors.text.primary}; + font-family: ${({ theme }) => theme.fonts.family.primary}; transition: background-color 0.2s ease, color 0.2s ease; } + + :root { + --scrollbar-track: ${({ theme }) => theme.colors.background.tertiary}; + --scrollbar-thumb: ${({ theme }) => theme.colors.border.light}; + --scrollbar-thumb-hover: ${({ theme }) => theme.colors.primary.hover}; + --scrollbar-size: 10px; + --scrollbar-radius: 9999px; + } + + /* Firefox */ + * { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); + } + + /* WebKit (Chrome, Edge, Safari) */ + *::-webkit-scrollbar { + width: var(--scrollbar-size); + height: var(--scrollbar-size); + } + + *::-webkit-scrollbar-track { + background: var(--scrollbar-track); + border-radius: var(--scrollbar-radius); + } + + *::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb); + border-radius: var(--scrollbar-radius); + border: 3px solid var(--scrollbar-track); + min-height: 28px; + } + + *::-webkit-scrollbar-thumb:hover { + background-color: var(--scrollbar-thumb-hover); + } + + *::-webkit-scrollbar-corner { + background: var(--scrollbar-track); + } + + pre, code, textarea { + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); + } `;