fix theme issues, remove dead rainbow mode code, standardize theme us… (#6668)

Fix issues with the theme of the app that caused some things to persist
in light mode/dark mode whilst the rest of the app was the opposite
theme.

Removed dead rainbow mode code.

Added system theme option to settings.
This commit is contained in:
EthanHealy01
2026-06-18 12:42:19 +00:00
committed by GitHub
parent 8f46ca0d92
commit 18da914bf9
38 changed files with 319 additions and 982 deletions
@@ -6735,8 +6735,9 @@ logout = "Log out"
setAsDefault = "Set as Default"
theme = "Theme"
themeDark = "Dark"
themeDescription = "Switch between light and dark mode"
themeDescription = "Choose light, dark, or follow your system"
themeLight = "Light"
themeSystem = "System"
title = "General"
user = "User"
+2 -2
View File
@@ -3,7 +3,7 @@ import { Routes, Route } from "react-router-dom";
import { AppProviders } from "@app/components/AppProviders";
import { AppLayout } from "@app/components/AppLayout";
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
import { RainbowThemeProvider } from "@app/components/shared/RainbowThemeProvider";
import { ThemeProvider } from "@app/components/shared/ThemeProvider";
import { PreferencesProvider } from "@app/contexts/PreferencesContext";
import HomePage from "@app/pages/HomePage";
import MobileScannerPage from "@app/pages/MobileScannerPage";
@@ -22,7 +22,7 @@ import "@app/utils/fileIdSafety";
function PublicRouteProviders({ children }: { children: React.ReactNode }) {
return (
<PreferencesProvider>
<RainbowThemeProvider>{children}</RainbowThemeProvider>
<ThemeProvider>{children}</ThemeProvider>
</PreferencesProvider>
);
}
@@ -1,5 +1,5 @@
import { ReactNode, useEffect } from "react";
import { RainbowThemeProvider } from "@app/components/shared/RainbowThemeProvider";
import { ThemeProvider } from "@app/components/shared/ThemeProvider";
import { FileContextProvider } from "@app/contexts/FileContext";
import { NavigationProvider } from "@app/contexts/NavigationContext";
import { ToolRegistryProvider } from "@app/contexts/ToolRegistryProvider";
@@ -114,7 +114,7 @@ export function AppProviders({
}: AppProvidersProps) {
return (
<PreferencesProvider>
<RainbowThemeProvider>
<ThemeProvider>
<ErrorBoundary>
<BannerProvider>
<AppConfigProvider
@@ -172,7 +172,7 @@ export function AppProviders({
</AppConfigProvider>
</BannerProvider>
</ErrorBoundary>
</RainbowThemeProvider>
</ThemeProvider>
</PreferencesProvider>
);
}
@@ -1,6 +1,5 @@
import { useEffect, useState, Suspense, lazy } from "react";
import { Box, Loader, Center } from "@mantine/core";
import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import { useFileHandler } from "@app/hooks/useFileHandler";
import { useFileState } from "@app/contexts/FileContext";
@@ -34,7 +33,6 @@ const FileManagerView = lazy(
// No props needed - component uses contexts directly
export default function Workbench() {
const { isRainbowMode } = useRainbowThemeContext();
const { config } = useAppConfig();
// The consent banner used to be initialised by the footer; the legal links
@@ -199,14 +197,7 @@ export default function Workbench() {
<Box
className="flex-1 h-full min-w-0 relative flex flex-col"
data-tour="workbench"
style={
isRainbowMode
? // No background color in rainbow mode, but still pin min-width:0
// so inner flex children (files-page toolbar, etc.) actually
// shrink on narrow viewports.
{ minWidth: 0 }
: { backgroundColor: "var(--bg-background)", minWidth: 0 }
}
style={{ backgroundColor: "var(--bg-background)", minWidth: 0 }}
>
{/* Workbench Bar - animates in/out based on file presence */}
{currentView !== "myFiles" &&
@@ -153,17 +153,18 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({
const canProceed = await confirmIfDirty();
if (!canProceed) return;
// Pop back to whatever the user came from (files / viewer / tools).
// location.key === "default" means /settings was the first entry in
// this tab (deep link / refresh), so there's nothing to pop to;
// fall back to home in that case.
if (location.key === "default") {
navigate("/", { replace: true });
} else {
navigate(-1);
// Only unwind history if settings was opened via the URL; opened via state
// there's no /settings entry to pop and navigate(-1) would jump to /files.
if (location.pathname.startsWith("/settings")) {
// "default" key = first entry (deep link/refresh); nothing to pop to.
if (location.key === "default") {
navigate("/", { replace: true });
} else {
navigate(-1);
}
}
onClose();
}, [confirmIfDirty, location.key, navigate, onClose]);
}, [confirmIfDirty, location.key, location.pathname, navigate, onClose]);
// Synchronous wrapper for contexts (e.g. tour buttons) that need () => void
const handleCloseSync = useCallback(() => {
@@ -10,7 +10,6 @@ interface FooterProps {
cookiePolicy?: string;
impressum?: string;
analyticsEnabled?: boolean;
forceLightMode?: boolean;
}
export default function Footer({
@@ -20,7 +19,6 @@ export default function Footer({
cookiePolicy,
impressum,
analyticsEnabled,
forceLightMode = false,
}: FooterProps) {
const { t } = useTranslation();
const { footerInfo } = useFooterInfo();
@@ -38,7 +36,6 @@ export default function Footer({
const { showCookiePreferences } = useCookieConsent({
analyticsEnabled: finalAnalyticsEnabled,
forceLightMode,
});
// Default URLs
@@ -56,12 +53,8 @@ export default function Footer({
<div
style={{
height: "var(--footer-height)",
backgroundColor: forceLightMode
? "#f1f3f5"
: "var(--mantine-color-gray-1)",
borderTop: forceLightMode
? "1px solid #e9ecef"
: "1px solid var(--mantine-color-gray-2)",
backgroundColor: "var(--mantine-color-gray-1)",
borderTop: "1px solid var(--mantine-color-gray-2)",
display: "flex",
flexDirection: "column",
justifyContent: "center",
@@ -74,7 +67,6 @@ export default function Footer({
direction="row"
style={{
fontSize: "0.75rem",
color: forceLightMode ? "#495057" : undefined,
}}
>
<a
@@ -15,7 +15,6 @@ import SignPopout, {
SIGN_REQUEST_WORKBENCH_TYPE,
SESSION_DETAIL_WORKBENCH_TYPE,
} from "@app/components/shared/signing/SignPopout";
import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
import { useFilesModalContext } from "@app/contexts/FilesModalContext";
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import { useFileSelection, useFileState } from "@app/contexts/file/fileHooks";
@@ -62,7 +61,6 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const { isRainbowMode } = useRainbowThemeContext();
const { isFilesModalOpen } = useFilesModalContext();
const {
handleReaderToggle,
@@ -809,7 +807,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
ref={ref}
data-sidebar="quick-access"
data-tour="quick-access-bar"
className={`h-screen flex flex-col w-16 quick-access-bar-main ${isRainbowMode ? "rainbow-mode" : ""}`}
className="h-screen flex flex-col w-16 quick-access-bar-main"
>
{/* Fixed header outside scrollable area */}
<div className="quick-access-header">
@@ -1,79 +0,0 @@
import { createContext, useContext, useEffect, ReactNode } from "react";
import { MantineProvider } from "@mantine/core";
import { useRainbowTheme } from "@app/hooks/useRainbowTheme";
import { mantineTheme } from "@app/theme/mantineTheme";
import rainbowStyles from "@app/styles/rainbow.module.css";
import { ToastProvider } from "@app/components/toast";
import ToastRenderer from "@app/components/toast/ToastRenderer";
import { ToastPortalBinder } from "@app/components/toast";
import type { ThemeMode } from "@app/constants/theme";
// SUI shared design-system tokens (used by @shared/components). Additive — its
// var names don't clash with the editor's own theme.css. The effect below
// bridges Mantine's color scheme to the `data-theme` attribute SUI keys on.
import "@shared/tokens/tokens.css";
interface RainbowThemeContextType {
themeMode: ThemeMode;
isRainbowMode: boolean;
isToggleDisabled: boolean;
toggleTheme: () => void;
activateRainbow: () => void;
deactivateRainbow: () => void;
}
const RainbowThemeContext = createContext<RainbowThemeContextType | null>(null);
export function useRainbowThemeContext() {
const context = useContext(RainbowThemeContext);
if (!context) {
throw new Error(
"useRainbowThemeContext must be used within RainbowThemeProvider",
);
}
return context;
}
interface RainbowThemeProviderProps {
children: ReactNode;
}
export function RainbowThemeProvider({ children }: RainbowThemeProviderProps) {
const rainbowTheme = useRainbowTheme();
// Determine the Mantine color scheme
const mantineColorScheme =
rainbowTheme.themeMode === "rainbow" ? "dark" : rainbowTheme.themeMode;
// Bridge the resolved scheme to the `data-theme` attribute SUI's tokens.css
// keys its dark palette on (the editor otherwise only sets Mantine's
// data-mantine-color-scheme), so @shared/components theme correctly.
useEffect(() => {
document.documentElement.setAttribute(
"data-theme",
mantineColorScheme === "dark" ? "dark" : "light",
);
}, [mantineColorScheme]);
return (
<RainbowThemeContext.Provider value={rainbowTheme}>
<MantineProvider
theme={mantineTheme}
defaultColorScheme={mantineColorScheme}
forceColorScheme={mantineColorScheme}
>
<div
className={
rainbowTheme.isRainbowMode ? rainbowStyles.rainbowMode : ""
}
style={{ minHeight: "100vh" }}
>
<ToastProvider>
<ToastPortalBinder />
{children}
<ToastRenderer />
</ToastProvider>
</div>
</MantineProvider>
</RainbowThemeContext.Provider>
);
}
@@ -0,0 +1,101 @@
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
ReactNode,
} from "react";
import { MantineProvider } from "@mantine/core";
import { useIsomorphicEffect } from "@mantine/hooks";
import { usePreferences } from "@app/contexts/PreferencesContext";
import { mantineTheme } from "@app/theme/mantineTheme";
import { ToastProvider } from "@app/components/toast";
import ToastRenderer from "@app/components/toast/ToastRenderer";
import { ToastPortalBinder } from "@app/components/toast";
import {
type ThemeMode,
getSystemTheme,
resolveColorScheme,
} from "@app/constants/theme";
// SUI shared design-system tokens (used by @shared/components); key on `data-theme`.
import "@shared/tokens/tokens.css";
interface ThemeContextType {
themeMode: ThemeMode;
setTheme: (mode: ThemeMode) => void;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}
interface ThemeProviderProps {
children: ReactNode;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
// preferences.theme is the single source of truth.
const { preferences, updatePreference } = usePreferences();
const themeMode = preferences.theme;
const setTheme = useCallback(
(mode: ThemeMode) => updatePreference("theme", mode),
[updatePreference],
);
// Track the OS scheme so "system" updates live; only subscribe while on system.
// If matchMedia is unavailable we bail out and keep the seeded value, which
// getSystemTheme already defaults to light — so it never crashes.
const [systemScheme, setSystemScheme] = useState(getSystemTheme);
useIsomorphicEffect(() => {
if (themeMode !== "system") return;
if (
typeof window === "undefined" ||
typeof window.matchMedia !== "function"
) {
return;
}
const media = window.matchMedia("(prefers-color-scheme: dark)");
const update = () => setSystemScheme(media.matches ? "dark" : "light");
update();
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, [themeMode]);
// The theme preference resolved to a concrete light/dark scheme.
const colorScheme = resolveColorScheme(themeMode, systemScheme);
// Mantine drives `data-mantine-color-scheme`; mirror it to `data-theme` for SUI tokens.
useIsomorphicEffect(() => {
document.documentElement.setAttribute("data-theme", colorScheme);
}, [colorScheme]);
const value = useMemo<ThemeContextType>(
() => ({ themeMode, setTheme }),
[themeMode, setTheme],
);
return (
<ThemeContext.Provider value={value}>
<MantineProvider
theme={mantineTheme}
defaultColorScheme={colorScheme}
forceColorScheme={colorScheme}
>
<div style={{ minHeight: "100vh" }}>
<ToastProvider>
<ToastPortalBinder />
{children}
<ToastRenderer />
</ToastProvider>
</div>
</MantineProvider>
</ThemeContext.Provider>
);
}
@@ -1,7 +1,5 @@
import React, { useState, useCallback, useMemo } from "react";
import { SegmentedControl, Loader } from "@mantine/core";
import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
import rainbowStyles from "@app/styles/rainbow.module.css";
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
import GridViewIcon from "@mui/icons-material/GridView";
import FolderIcon from "@mui/icons-material/Folder";
@@ -148,7 +146,6 @@ const TopControls = ({
onFileSelect,
onFileRemove,
}: TopControlsProps) => {
const { isRainbowMode } = useRainbowThemeContext();
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
const pageEditorState = usePageEditorDropdownState();
@@ -213,7 +210,6 @@ const TopControls = ({
onChange={handleViewChange}
color="blue"
fullWidth
className={isRainbowMode ? rainbowStyles.rainbowSegmentedControl : ""}
style={{
transition: "all 0.2s ease",
opacity: switchingTo ? 0.8 : 1,
@@ -19,8 +19,9 @@ import {
import { useTranslation } from "react-i18next";
import { usePreferences } from "@app/contexts/PreferencesContext";
import { useAppConfig } from "@app/contexts/AppConfigContext";
import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
import { useTheme } from "@app/components/shared/ThemeProvider";
import LanguageSelector from "@app/components/shared/LanguageSelector";
import type { ThemeMode } from "@app/constants/theme";
import type { ToolPanelMode } from "@app/constants/toolPanel";
import type {
StartupView,
@@ -85,7 +86,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
const { t } = useTranslation();
const { preferences, updatePreference } = usePreferences();
const { config } = useAppConfig();
const { toggleTheme, themeMode } = useRainbowThemeContext();
const { setTheme, themeMode } = useTheme();
const [fileLimitInput, setFileLimitInput] = useState<number | string>(
preferences.autoUnzipFileLimit,
);
@@ -499,15 +500,13 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
<Text size="xs" c="dimmed" mt={4}>
{t(
"settings.general.themeDescription",
"Switch between light and dark mode",
"Choose light, dark, or follow your system",
)}
</Text>
</div>
<SegmentedControl
value={themeMode === "rainbow" ? "dark" : themeMode}
onChange={(val) => {
if ((themeMode === "dark") !== (val === "dark")) toggleTheme();
}}
value={themeMode}
onChange={(val) => setTheme(val as ThemeMode)}
data={[
{
label: t("settings.general.themeLight", "Light"),
@@ -517,6 +516,10 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
label: t("settings.general.themeDark", "Dark"),
value: "dark",
},
{
label: t("settings.general.themeSystem", "System"),
value: "system",
},
]}
/>
</div>
@@ -51,23 +51,8 @@
direction: ltr; /* keep layout stable when document is rtl */
}
/* Rainbow mode container */
.quick-access-bar-main.rainbow-mode {
background-color: var(--bg-muted);
width: 4.5rem;
min-width: 4.5rem;
max-width: 4.5rem;
position: relative;
z-index: 10;
border-right: 1px solid var(--border-default);
flex-shrink: 0;
box-sizing: border-box;
direction: ltr;
}
/* RTL adjustments keep the bar on-screen and separated from content */
:root[dir="rtl"] .quick-access-bar-main,
:root[dir="rtl"] .quick-access-bar-main.rainbow-mode {
:root[dir="rtl"] .quick-access-bar-main {
border-right: none;
border-left: 1px solid var(--border-default);
}
@@ -1,10 +1,8 @@
import { useMemo, useState } from "react";
import { ActionIcon } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import { useSidebarContext } from "@app/contexts/SidebarContext";
import rainbowStyles from "@app/styles/rainbow.module.css";
import { useIsMobile } from "@app/hooks/useIsMobile";
import ToolPanel from "@app/components/tools/ToolPanel";
import ToolSearch from "@app/components/tools/toolPicker/ToolSearch";
@@ -43,7 +41,6 @@ import "@app/components/tools/ToolPanel.css";
*/
export default function RightSidebar() {
const { t } = useTranslation();
const { isRainbowMode } = useRainbowThemeContext();
const { sidebarRefs } = useSidebarContext();
const { toolPanelRef, quickAccessRef } = sidebarRefs;
const isMobile = useIsMobile();
@@ -207,9 +204,7 @@ export default function RightSidebar() {
ref={toolPanelRef}
data-sidebar="tool-panel"
data-tour={fullscreenExpanded ? undefined : "tool-panel"}
className={`tool-panel flex flex-col ${fullscreenExpanded ? "tool-panel--fullscreen-active" : "overflow-hidden"} bg-[var(--bg-toolbar)] border-l border-[var(--border-subtle)] transition-all duration-300 ease-out ${
isRainbowMode ? rainbowStyles.rainbowPaper : ""
} ${isMobile ? "h-full border-r-0" : "h-screen"} ${fullscreenExpanded ? "tool-panel--fullscreen" : ""}`}
className={`tool-panel flex flex-col ${fullscreenExpanded ? "tool-panel--fullscreen-active" : "overflow-hidden"} bg-[var(--bg-toolbar)] border-l border-[var(--border-subtle)] transition-all duration-300 ease-out ${isMobile ? "h-full border-r-0" : "h-screen"} ${fullscreenExpanded ? "tool-panel--fullscreen" : ""}`}
style={{
width: computedWidth(),
padding: "0",
+31 -6
View File
@@ -1,10 +1,35 @@
// Theme constants and utilities
export type ThemeMode = "light" | "dark" | "rainbow";
// Stored theme preference. "system" follows the OS.
export type ThemeMode = "light" | "dark" | "system";
// Detect OS theme preference
export function getSystemTheme(): "light" | "dark" {
return window?.matchMedia?.("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
// The concrete scheme applied to the UI.
export type ColorScheme = "light" | "dark";
// Detect the OS theme preference. Never throws: if the environment can't be
// read (no window, no matchMedia, or it errors), defaults to "light".
export function getSystemTheme(): ColorScheme {
try {
if (
typeof window === "undefined" ||
typeof window.matchMedia !== "function"
) {
return "light";
}
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
} catch {
return "light";
}
}
// Resolve a theme preference to a concrete light/dark scheme.
// Falls back to systemScheme for unrecognised values (e.g. stale "rainbow").
export function resolveColorScheme(
mode: ThemeMode,
systemScheme: ColorScheme,
): ColorScheme {
if (mode === "light" || mode === "dark") return mode;
return systemScheme;
}
@@ -1,6 +1,7 @@
import { useEffect, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { BASE_PATH } from "@app/constants/app";
import { getSystemTheme } from "@app/constants/theme";
import { useAppConfig } from "@app/contexts/AppConfigContext";
import {
Z_INDEX_COOKIE_CONSENT_BANNER,
@@ -26,7 +27,6 @@ declare global {
interface CookieConsentConfig {
analyticsEnabled?: boolean;
forceLightMode?: boolean;
}
// Shard so Mantine's scroll-lock doesn't swallow events on the consent dialog;
@@ -41,7 +41,6 @@ export const COOKIE_CONSENT_SCROLL_SHARD = {
export const useCookieConsent = ({
analyticsEnabled = false,
forceLightMode = false,
}: CookieConsentConfig = {}) => {
const { t } = useTranslation();
const { config } = useAppConfig();
@@ -75,9 +74,6 @@ export const useCookieConsent = ({
}
if (window.CookieConsent) {
if (forceLightMode) {
document.documentElement.classList.remove("cc--darkmode");
}
setIsInitialized(true);
return;
}
@@ -87,11 +83,6 @@ export const useCookieConsent = ({
script.onload = () => {
setTimeout(() => {
const detectTheme = () => {
if (forceLightMode) {
document.documentElement.classList.remove("cc--darkmode");
return false;
}
const mantineScheme = document.documentElement.getAttribute(
"data-mantine-color-scheme",
);
@@ -99,9 +90,7 @@ export const useCookieConsent = ({
document.documentElement.classList.contains("light");
const hasDarkClass =
document.documentElement.classList.contains("dark");
const systemPrefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
const systemPrefersDark = getSystemTheme() === "dark";
let isDarkMode: boolean;
if (mantineScheme) {
@@ -125,24 +114,22 @@ export const useCookieConsent = ({
return;
}
if (!forceLightMode) {
const themeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
(mutation.attributeName === "data-mantine-color-scheme" ||
mutation.attributeName === "class")
) {
detectTheme();
}
});
const themeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
(mutation.attributeName === "data-mantine-color-scheme" ||
mutation.attributeName === "class")
) {
detectTheme();
}
});
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-mantine-color-scheme", "class"],
});
}
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-mantine-color-scheme", "class"],
});
try {
const overrides = getCookieConsentOverrides();
@@ -319,32 +306,19 @@ export const useCookieConsent = ({
document.head.removeChild(customCSS);
}
};
}, [
analyticsEnabled,
config?.enablePosthog,
config?.enableScarf,
t,
forceLightMode,
]);
}, [analyticsEnabled, config?.enablePosthog, config?.enableScarf, t]);
useEffect(() => {
if (!isInitialized) return;
const detectTheme = () => {
if (forceLightMode) {
document.documentElement.classList.remove("cc--darkmode");
return false;
}
const mantineScheme = document.documentElement.getAttribute(
"data-mantine-color-scheme",
);
const hasLightClass =
document.documentElement.classList.contains("light");
const hasDarkClass = document.documentElement.classList.contains("dark");
const systemPrefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
const systemPrefersDark = getSystemTheme() === "dark";
let isDarkMode: boolean;
if (mantineScheme) {
@@ -363,32 +337,25 @@ export const useCookieConsent = ({
detectTheme();
let themeObserver: MutationObserver | null = null;
if (!forceLightMode) {
themeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
(mutation.attributeName === "data-mantine-color-scheme" ||
mutation.attributeName === "class")
) {
detectTheme();
}
});
const themeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
(mutation.attributeName === "data-mantine-color-scheme" ||
mutation.attributeName === "class")
) {
detectTheme();
}
});
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-mantine-color-scheme", "class"],
});
}
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-mantine-color-scheme", "class"],
});
return () => {
if (themeObserver) {
themeObserver.disconnect();
}
};
}, [forceLightMode, isInitialized]);
return () => themeObserver.disconnect();
}, [isInitialized]);
useEffect(() => {
if (!isInitialized || !window.CookieConsent) return;
@@ -1,203 +0,0 @@
import { useCallback, useRef, useEffect } from "react";
import { usePreferences } from "@app/contexts/PreferencesContext";
import type { ThemeMode } from "@app/constants/theme";
interface RainbowThemeHook {
themeMode: ThemeMode;
isRainbowMode: boolean;
isToggleDisabled: boolean;
toggleTheme: () => void;
activateRainbow: () => void;
deactivateRainbow: () => void;
}
const allowRainbowMode = false; // Override to allow/disallow fun
export function useRainbowTheme(): RainbowThemeHook {
const { preferences, updatePreference } = usePreferences();
const themeMode = preferences.theme;
// Track rapid toggles for easter egg
const toggleCount = useRef(0);
const lastToggleTime = useRef(Date.now());
const isToggleDisabled = useRef(false);
// Apply rainbow class to body whenever theme changes
useEffect(() => {
if (themeMode === "rainbow") {
document.body.classList.add("rainbow-mode-active");
showRainbowNotification();
} else {
document.body.classList.remove("rainbow-mode-active");
}
}, [themeMode]);
const showRainbowNotification = () => {
// Remove any existing notification
const existingNotification = document.getElementById(
"rainbow-notification",
);
if (existingNotification) {
existingNotification.remove();
}
// Create and show rainbow notification
const notification = document.createElement("div");
notification.id = "rainbow-notification";
notification.innerHTML =
"🌈 RAINBOW MODE ACTIVATED! 🌈<br><small>Button disabled for 3 seconds, then click to exit</small>";
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(45deg, #ff0000, #ff8800, #ffff00, #88ff00, #00ff88, #00ffff, #0088ff, #8800ff);
background-size: 300% 300%;
animation: rainbowBackground 1s ease infinite;
color: white;
padding: 15px 20px;
border-radius: 25px;
font-weight: bold;
font-size: 16px;
z-index: 1000;
border: 2px solid white;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.8);
user-select: none;
pointer-events: none;
transition: opacity 0.3s ease;
`;
document.body.appendChild(notification);
// Auto-remove notification after 3 seconds
setTimeout(() => {
if (notification) {
notification.style.opacity = "0";
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}
}, 3000);
};
const showExitNotification = () => {
// Remove any existing notification
const existingNotification = document.getElementById(
"rainbow-exit-notification",
);
if (existingNotification) {
existingNotification.remove();
}
// Create and show exit notification
const notification = document.createElement("div");
notification.id = "rainbow-exit-notification";
notification.innerHTML = "🌙 Rainbow mode deactivated";
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(45deg, #333, #666);
color: white;
padding: 15px 20px;
border-radius: 25px;
font-weight: bold;
font-size: 16px;
z-index: 1000;
border: 2px solid #999;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
user-select: none;
pointer-events: none;
transition: opacity 0.3s ease;
`;
document.body.appendChild(notification);
// Auto-remove notification after 2 seconds
setTimeout(() => {
if (notification) {
notification.style.opacity = "0";
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}
}, 2000);
};
const toggleTheme = useCallback(() => {
// Don't allow toggle if disabled
if (isToggleDisabled.current) {
return;
}
const currentTime = Date.now();
// Simple exit from rainbow mode with single click (after cooldown period)
if (themeMode === "rainbow") {
updatePreference("theme", "light");
console.log("🌈 Rainbow mode deactivated. Thanks for trying it!");
showExitNotification();
return;
}
// Reset counter if too much time has passed (2.5 seconds)
if (currentTime - lastToggleTime.current > 2500) {
toggleCount.current = 1;
} else {
toggleCount.current++;
}
lastToggleTime.current = currentTime;
// Easter egg: Activate rainbow mode after 10 rapid toggles
if (allowRainbowMode && toggleCount.current >= 10) {
updatePreference("theme", "rainbow");
console.log(
"🌈 RAINBOW MODE ACTIVATED! 🌈 You found the secret easter egg!",
);
console.log(
"🌈 Button will be disabled for 3 seconds, then click once to exit!",
);
// Disable toggle for 3 seconds
isToggleDisabled.current = true;
setTimeout(() => {
isToggleDisabled.current = false;
console.log(
"🌈 Theme toggle re-enabled! Click once to exit rainbow mode.",
);
}, 3000);
// Reset counter
toggleCount.current = 0;
return;
}
// Normal theme switching
const nextTheme = themeMode === "light" ? "dark" : "light";
updatePreference("theme", nextTheme);
}, [themeMode, updatePreference]);
const activateRainbow = useCallback(() => {
updatePreference("theme", "rainbow");
console.log("🌈 Rainbow mode manually activated!");
}, [updatePreference]);
const deactivateRainbow = useCallback(() => {
if (themeMode === "rainbow") {
updatePreference("theme", "light");
console.log("🌈 Rainbow mode manually deactivated.");
}
}, [themeMode, updatePreference]);
return {
themeMode,
isRainbowMode: themeMode === "rainbow",
isToggleDisabled: isToggleDisabled.current,
toggleTheme,
activateRainbow,
deactivateRainbow,
};
}
@@ -2,7 +2,7 @@ import {
type ToolPanelMode,
DEFAULT_TOOL_PANEL_MODE,
} from "@app/constants/toolPanel";
import { type ThemeMode, getSystemTheme } from "@app/constants/theme";
import { type ThemeMode } from "@app/constants/theme";
export type LogoVariant = "modern" | "classic";
@@ -46,7 +46,7 @@ export const DEFAULT_PREFERENCES: UserPreferences = {
defaultToolPanelMode: DEFAULT_TOOL_PANEL_MODE,
defaultStartupView: "tools",
defaultViewerZoom: "auto",
theme: getSystemTheme(),
theme: "system",
toolPanelModePromptSeen: false,
hasSelectedToolPanelMode: false,
showLegacyToolDescriptions: false,
@@ -1,345 +0,0 @@
/* Rainbow Mode Styles - Easter Egg! */
@keyframes rainbowBackground {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@keyframes rainbowBorder {
0% {
border-color: #ff0000;
box-shadow: 0 0 15px #ff0000;
}
14% {
border-color: #ff8800;
box-shadow: 0 0 15px #ff8800;
}
28% {
border-color: #ffff00;
box-shadow: 0 0 15px #ffff00;
}
42% {
border-color: #88ff00;
box-shadow: 0 0 15px #88ff00;
}
57% {
border-color: #00ff88;
box-shadow: 0 0 15px #00ff88;
}
71% {
border-color: #0088ff;
box-shadow: 0 0 15px #0088ff;
}
85% {
border-color: #8800ff;
box-shadow: 0 0 15px #8800ff;
}
100% {
border-color: #ff0000;
box-shadow: 0 0 15px #ff0000;
}
}
@keyframes rainbowText {
0% {
color: #ff0000;
text-shadow: 0 0 10px #ff0000;
}
14% {
color: #ff8800;
text-shadow: 0 0 10px #ff8800;
}
28% {
color: #ffff00;
text-shadow: 0 0 10px #ffff00;
}
42% {
color: #88ff00;
text-shadow: 0 0 10px #88ff00;
}
57% {
color: #00ff88;
text-shadow: 0 0 10px #00ff88;
}
71% {
color: #0088ff;
text-shadow: 0 0 10px #0088ff;
}
85% {
color: #8800ff;
text-shadow: 0 0 10px #8800ff;
}
100% {
color: #ff0000;
text-shadow: 0 0 10px #ff0000;
}
}
@keyframes rainbowPulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
/* Main rainbow theme class */
.rainbowMode {
background: linear-gradient(
-45deg,
#ff0000,
#ff8800,
#ffff00,
#88ff00,
#00ff88,
#00ffff,
#0088ff,
#8800ff,
#ff0088,
#ff0000
) !important;
background-size: 400% 400% !important;
animation: rainbowBackground 3s ease infinite !important;
color: white !important;
overflow-x: hidden;
}
/* Rainbow components */
.rainbowCard {
background: linear-gradient(
45deg,
#ff0000,
#ff8800,
#ffff00,
#88ff00,
#00ff88,
#00ffff,
#0088ff,
#8800ff,
#ff0088
) !important;
background-size: 400% 400% !important;
animation:
rainbowBackground 4s ease infinite,
rainbowBorder 2s linear infinite !important;
color: white !important;
border: 2px solid !important;
border-radius: 15px !important;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3) !important;
}
.rainbowButton {
background: linear-gradient(
45deg,
#ff0000,
#ff8800,
#ffff00,
#88ff00,
#00ff88,
#00ffff,
#0088ff,
#8800ff,
#ff0088
) !important;
background-size: 300% 300% !important;
animation:
rainbowBackground 2s ease infinite,
rainbowBorder 1s linear infinite !important;
border: 2px solid !important;
color: white !important;
border-radius: 8px !important;
transition: all 0.3s ease !important;
font-weight: bold !important;
}
.rainbowButton:hover {
transform: scale(1.05) !important;
animation:
rainbowBackground 1s ease infinite,
rainbowBorder 0.5s linear infinite,
rainbowPulse 0.5s ease infinite !important;
box-shadow: 0 0 25px rgba(255, 255, 255, 0.6) !important;
}
.rainbowInput {
background: linear-gradient(
90deg,
#ff0000,
#ff8800,
#ffff00,
#88ff00,
#00ff88,
#00ffff,
#0088ff,
#8800ff,
#ff0088
) !important;
background-size: 300% 300% !important;
animation:
rainbowBackground 2.5s ease infinite,
rainbowBorder 1.5s linear infinite !important;
border: 2px solid !important;
color: white !important;
border-radius: 8px !important;
}
.rainbowInput::placeholder {
color: rgba(255, 255, 255, 0.8) !important;
}
.rainbowText {
animation: rainbowText 3s linear infinite !important;
font-weight: bold !important;
text-shadow: 0 0 10px currentColor !important;
}
.rainbowSegmentedControl {
background: linear-gradient(
45deg,
#ff0000,
#ff8800,
#ffff00,
#88ff00,
#00ff88,
#00ffff,
#0088ff,
#8800ff,
#ff0088
) !important;
background-size: 400% 400% !important;
animation:
rainbowBackground 3s ease infinite,
rainbowBorder 2s linear infinite !important;
border: 2px solid !important;
border-radius: 12px !important;
padding: 4px !important;
}
.rainbowPaper {
background: linear-gradient(
90deg,
#00ffff,
#0088ff,
#8800ff,
#ff0088
) !important;
background-size: 100% 100% !important;
animation:
rainbowBackground 3.5s ease infinite,
rainbowBorder 2s linear infinite !important;
border: 2px solid !important;
color: white !important;
border-radius: 12px !important;
}
/* Easter egg notification */
.rainbowNotification {
position: fixed !important;
top: 20px !important;
right: 20px !important;
background: linear-gradient(
45deg,
#ffff00,
#88ff00,
#00ff88,
#00ffff
) !important;
background-size: 300% 300% !important;
animation:
rainbowBackground 1s ease infinite,
rainbowBorder 0.5s linear infinite !important;
color: white !important;
padding: 15px 20px !important;
border-radius: 25px !important;
font-weight: bold !important;
font-size: 16px !important;
z-index: 10000 !important;
border: 2px solid white !important;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.8) !important;
user-select: none !important;
pointer-events: none !important;
}
/* Specific component overrides */
.rainbowMode [data-mantine-color-scheme] {
background: linear-gradient(
-45deg,
#ff0000,
#ff8800,
#ffff00,
#88ff00,
#00ff88,
#00ffff,
#0088ff,
#8800ff,
#ff0088,
#ff0000
) !important;
background-size: 400% 400% !important;
animation: rainbowBackground 3s ease infinite !important;
}
/* Make all buttons rainbow in rainbow mode */
.rainbowMode button {
background: linear-gradient(
45deg,
#ffff00,
#88ff00,
#00ff88,
#00ffff
) !important;
background-size: 100% 100% !important;
animation: rainbowBackground 2s ease infinite !important;
border: 2px solid !important;
animation:
rainbowBackground 2s ease infinite,
rainbowBorder 1.5s linear infinite !important;
color: white !important;
font-weight: bold !important;
}
/* Make all inputs rainbow in rainbow mode */
.rainbowMode input,
.rainbowMode select,
.rainbowMode textarea {
background: linear-gradient(
90deg,
#ffff00,
#88ff00,
#00ff88,
#00ffff
) !important;
background-size: 100% 100% !important;
animation: rainbowBackground 2.5s ease infinite !important;
border: 2px solid !important;
animation:
rainbowBackground 2.5s ease infinite,
rainbowBorder 1.5s linear infinite !important;
color: white !important;
}
/* Rainbow text class */
.rainbowText {
animation: rainbowText 3s linear infinite !important;
font-weight: bold !important;
text-shadow: 0 0 10px currentColor !important;
}
/* Make all text rainbow */
.rainbowMode h1,
.rainbowMode h2,
.rainbowMode h3,
.rainbowMode h4,
.rainbowMode h5,
.rainbowMode h6 {
animation: rainbowText 3s linear infinite !important;
font-weight: bold !important;
}
@@ -43,7 +43,6 @@ export const Z_INDEX_SIGN_IN_MODAL = 9000;
// Floating viewer menus rendered through document.body portals.
export const Z_INDEX_VIEWER_FLOATING_MENU = 10000;
// Toast notifications and error displays - Always on top (higher than rainbow theme at 10000)
export const Z_INDEX_TOAST = 10001;
// Signature preview overlays inside the PDF viewer
@@ -21,27 +21,6 @@ export const DesktopAuthLayout: React.FC<DesktopAuthLayoutProps> = ({
[logoVariant, t],
);
// Force light mode on auth pages
useEffect(() => {
const htmlElement = document.documentElement;
const previousColorScheme = htmlElement.getAttribute(
"data-mantine-color-scheme",
);
// Set light mode
htmlElement.setAttribute("data-mantine-color-scheme", "light");
// Cleanup: restore previous theme when leaving auth pages
return () => {
if (previousColorScheme) {
htmlElement.setAttribute(
"data-mantine-color-scheme",
previousColorScheme,
);
}
};
}, []);
useEffect(() => {
const update = () => {
// Use viewport to avoid hysteresis when the card is already in single-column mode
@@ -14,7 +14,7 @@
align-items: center;
justify-content: flex-start;
padding: 0.75rem 1rem; /* 12px 16px */
border: 1px solid #d1d5db;
border: 1px solid var(--auth-input-border-light-only);
border-radius: 0.75rem; /* 12px */
background-color: var(--auth-card-bg-light-only);
font-size: 1rem; /* 16px */
@@ -27,7 +27,7 @@
}
.oauth-button-vertical-desktop:hover:not(:disabled) {
background-color: #f3f4f6;
background-color: var(--bg-raised);
}
.oauth-button-vertical-desktop:disabled {
+2 -2
View File
@@ -4,7 +4,7 @@ import { AppProviders } from "@app/components/AppProviders";
import { AppLayout } from "@app/components/AppLayout";
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
import { PreferencesProvider } from "@app/contexts/PreferencesContext";
import { RainbowThemeProvider } from "@app/components/shared/RainbowThemeProvider";
import { ThemeProvider } from "@app/components/shared/ThemeProvider";
import Landing from "@app/routes/Landing";
import Login from "@app/routes/Login";
import Signup from "@app/routes/Signup";
@@ -31,7 +31,7 @@ import "@app/utils/fileIdSafety";
function PublicRouteProviders({ children }: { children: React.ReactNode }) {
return (
<PreferencesProvider>
<RainbowThemeProvider>{children}</RainbowThemeProvider>
<ThemeProvider>{children}</ThemeProvider>
</PreferencesProvider>
);
}
@@ -1,5 +1,10 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { createTheme, MantineProvider, Popover } from "@mantine/core";
import {
createTheme,
MantineProvider,
Popover,
useMantineColorScheme,
} from "@mantine/core";
import { Rnd } from "react-rnd";
import { useTranslation } from "react-i18next";
import { ChatFABButton } from "@shared/components/ChatFABButton";
@@ -41,6 +46,12 @@ export function ChatFAB() {
// from the local app-config. Either way the AI engine drives FAB visibility.
const enabled = useAiEngineEnabled();
// Scope the panel's nested MantineProvider to this ref; unscoped it writes
// its color scheme onto <html> and overrides the whole app's theme.
const panelThemeRootRef = useRef<HTMLDivElement>(null);
const { colorScheme } = useMantineColorScheme();
const panelColorScheme = colorScheme === "dark" ? "dark" : "light";
// Detect loading → done transition. If the FAB is closed when the agent
// finishes, show the tick badge until the user opens the panel.
const isOpenRef = useRef(isOpen);
@@ -221,11 +232,17 @@ export function ChatFAB() {
}}
>
<ChatFABWindow open={isOpen} onDoubleClick={handleHeaderDoubleClick}>
<MantineProvider theme={FAB_PANEL_THEME}>
<ChatPanel
onBack={() => setIsOpen(false)}
backLabel={t("chat.fab.close", "Close chat")}
/>
<MantineProvider
theme={FAB_PANEL_THEME}
getRootElement={() => panelThemeRootRef.current ?? undefined}
forceColorScheme={panelColorScheme}
>
<div ref={panelThemeRootRef} style={{ display: "contents" }}>
<ChatPanel
onBack={() => setIsOpen(false)}
backLabel={t("chat.fab.close", "Close chat")}
/>
</div>
</MantineProvider>
</ChatFABWindow>
</Rnd>
@@ -44,7 +44,7 @@ accent / `PanelHeader` icon slot / `Checkbox` leadingIcon / `MetricCard size`)
were **built up in SUI** as part of this work — each has a Storybook story.
Bootstrapping: the editor loads `@shared/tokens/tokens.css` globally via
`RainbowThemeProvider`, which also mirrors the Mantine colour scheme onto
`ThemeProvider`, which also mirrors the Mantine colour scheme onto
`<html data-theme>` (SUI's dark palette keys on `data-theme`). A global
`@shared` alias in `editor/vite.config.ts` + `vitest.config.ts` resolves the
shared components' own `@shared/*.css` self-imports.
@@ -143,10 +143,11 @@ export default function Landing() {
border: "1px solid rgba(37, 99, 235, 0.2)",
}}
>
<p
style={{ margin: "0 0 0.75rem 0", color: "rgba(15, 23, 42, 0.8)" }}
>
{t("backendStartup.unreachable")}
<p style={{ margin: "0 0 0.75rem 0", color: "var(--text-primary)" }}>
{t(
"backendStartup.unreachable",
"The application cannot currently connect to the backend. Verify the backend status and network connectivity, then try again.",
)}
</p>
<button
type="button"
@@ -490,10 +490,11 @@ export default function Login() {
border: "1px solid rgba(37, 99, 235, 0.2)",
}}
>
<p
style={{ margin: "0 0 0.75rem 0", color: "rgba(15, 23, 42, 0.8)" }}
>
{t("backendStartup.unreachable")}
<p style={{ margin: "0 0 0.75rem 0", color: "var(--text-primary)" }}>
{t(
"backendStartup.unreachable",
"The application cannot currently connect to the backend. Verify the backend status and network connectivity, then try again.",
)}
</p>
<button
type="button"
@@ -20,27 +20,6 @@ export default function AuthLayout({ children }: AuthLayoutProps) {
[logoVariant, t],
);
// Force light mode on auth pages
useEffect(() => {
const htmlElement = document.documentElement;
const previousColorScheme = htmlElement.getAttribute(
"data-mantine-color-scheme",
);
// Set light mode
htmlElement.setAttribute("data-mantine-color-scheme", "light");
// Cleanup: restore previous theme when leaving auth pages
return () => {
if (previousColorScheme) {
htmlElement.setAttribute(
"data-mantine-color-scheme",
previousColorScheme,
);
}
};
}, []);
useEffect(() => {
const update = () => {
// Use viewport to avoid hysteresis when the card is already in single-column mode
@@ -88,7 +67,7 @@ export default function AuthLayout({ children }: AuthLayoutProps) {
zIndex: 10,
}}
>
<Footer forceLightMode={true} />
<Footer />
</div>
</div>
);
@@ -631,9 +631,16 @@
width: auto;
}
/* Two logo variants are rendered; show the one matching the active theme. */
.auth-logo-header--dark {
display: none;
}
[data-mantine-color-scheme="dark"] .auth-logo-header--light {
display: none;
}
[data-mantine-color-scheme="dark"] .auth-logo-header--dark {
display: block;
}
/* ── Page entrance animation ────────────────────────────────────────── */
@keyframes authFadeUp {
@@ -1,7 +1,7 @@
/* Auth-specific CSS variables for proprietary version */
:root {
/* Auth page colors (light mode only - auth pages force light mode) */
/* Auth page colors (light mode) */
--auth-bg-color-light-only: #f3f4f6;
--auth-card-bg: #ffffff;
--auth-card-bg-light-only: #ffffff;
+2 -2
View File
@@ -4,7 +4,7 @@ import { AppProviders } from "@app/components/AppProviders";
import { AppLayout } from "@app/components/AppLayout";
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
import { PreferencesProvider } from "@app/contexts/PreferencesContext";
import { RainbowThemeProvider } from "@app/components/shared/RainbowThemeProvider";
import { ThemeProvider } from "@app/components/shared/ThemeProvider";
import Landing from "@app/routes/Landing";
import Login from "@app/routes/Login";
import Signup from "@app/routes/Signup";
@@ -27,7 +27,7 @@ import "@app/utils/fileIdSafety";
function MinimalProviders({ children }: { children: React.ReactNode }) {
return (
<PreferencesProvider>
<RainbowThemeProvider>{children}</RainbowThemeProvider>
<ThemeProvider>{children}</ThemeProvider>
</PreferencesProvider>
);
}
+2 -2
View File
@@ -3,7 +3,7 @@ import { Routes, Route, useLocation } from "react-router-dom";
import { isAuthRoute } from "@app/utils/pathUtils";
import { AppProviders } from "@app/components/AppProviders";
import { PreferencesProvider } from "@app/contexts/PreferencesContext";
import { RainbowThemeProvider } from "@app/components/shared/RainbowThemeProvider";
import { ThemeProvider } from "@app/components/shared/ThemeProvider";
import { setBaseUrl } from "@app/constants/app";
import type { AppConfig } from "@app/contexts/AppConfigContext";
import { AppLayout } from "@app/components/AppLayout";
@@ -40,7 +40,7 @@ function handleConfigLoaded(config: AppConfig) {
function PublicRouteProviders({ children }: { children: ReactNode }) {
return (
<PreferencesProvider>
<RainbowThemeProvider>{children}</RainbowThemeProvider>
<ThemeProvider>{children}</ThemeProvider>
</PreferencesProvider>
);
}
@@ -0,0 +1,14 @@
.chart-tooltip {
background: var(--bg-surface);
color: var(--text-primary);
border: 1px solid transparent;
box-shadow: var(--shadow-md);
padding: 8px 10px;
font-size: 12px;
line-height: 1.25;
border-radius: 8px;
}
[data-mantine-color-scheme="dark"] .chart-tooltip {
border-color: var(--border-subtle);
box-shadow: none;
}
@@ -7,15 +7,12 @@ import {
FractionData,
} from "@app/types/charts";
import { generateTooltipHTML } from "@app/components/shared/charts/stackedBarChart/StackedBarTooltip";
import {
detectTheme,
getChartThemeVars,
} from "@app/components/shared/charts/utils/themeUtils";
import { createTooltipPositioner } from "@app/components/shared/charts/utils/tooltipUtils";
import {
createRoundedRectPath,
createScale,
} from "@app/components/shared/charts/utils/d3Utils";
import "@app/components/shared/charts/StackedBarChart.css";
export default function StackedBarChart({
fractions,
@@ -35,13 +32,6 @@ export default function StackedBarChart({
const reactId = useId();
const clipId = useMemo(() => `clip-${reactId}`, [reactId]);
// Memoize theme detection to avoid recalculation
const theme = useMemo(() => detectTheme(), []);
const themeVars = useMemo(
() => getChartThemeVars(theme.isDark),
[theme.isDark],
);
// Memoize tooltip positioner
const tooltipPositioner = useMemo(
() => createTooltipPositioner(tooltipPosition),
@@ -57,22 +47,11 @@ export default function StackedBarChart({
[tooltipPositioner],
);
const setTooltipContent = useCallback(
(labelHtml: string) => {
const tooltip = tooltipRef.current;
if (!tooltip) return;
tooltip.innerHTML = labelHtml;
tooltip.style.background = themeVars.background;
tooltip.style.color = themeVars.textPrimary;
tooltip.style.border = themeVars.border;
tooltip.style.boxShadow = themeVars.boxShadow;
tooltip.style.padding = "8px 10px";
tooltip.style.fontSize = "12px";
tooltip.style.lineHeight = "1.25";
tooltip.style.borderRadius = "8px";
},
[themeVars],
);
const setTooltipContent = useCallback((labelHtml: string) => {
const tooltip = tooltipRef.current;
if (!tooltip) return;
tooltip.innerHTML = labelHtml;
}, []);
const hideTooltip = useCallback(() => {
const tooltip = tooltipRef.current;
@@ -126,8 +105,8 @@ export default function StackedBarChart({
.attr("height", height)
.attr("rx", radius)
.attr("ry", radius)
.attr("fill", themeVars.inactive)
.attr("stroke", themeVars.cardBorder);
.attr("fill", "var(--usage-inactive)")
.attr("stroke", "var(--api-keys-card-border)");
// Define a clipPath that will reveal the used portion from left to right
const defs = svg.append("defs");
@@ -225,7 +204,6 @@ export default function StackedBarChart({
.on("mouseenter", (event: MouseEvent) => {
const tooltipData: TooltipData = {
fractions: data,
isDark: theme.isDark,
};
const html = generateTooltipHTML(tooltipData);
setTooltipContent(html);
@@ -267,7 +245,6 @@ export default function StackedBarChart({
animate,
animationDurationMs,
clipId,
themeVars,
setTooltipContent,
hideTooltip,
positionTooltip,
@@ -279,6 +256,7 @@ export default function StackedBarChart({
<div ref={containerRef} />
<div
ref={tooltipRef}
className="chart-tooltip"
style={{
position: "absolute",
left: 0,
@@ -325,10 +303,10 @@ export default function StackedBarChart({
style={{
width: 10,
height: 10,
background: themeVars.inactive,
background: "var(--usage-inactive)",
display: "inline-block",
borderRadius: 2,
outline: `1px solid ${themeVars.cardBorder}`,
outline: "1px solid var(--api-keys-card-border)",
}}
/>
<Text size="sm">Remaining</Text>
@@ -1,68 +0,0 @@
/**
* Utility functions for theme detection and styling in D3 charts
*/
export interface ThemeInfo {
isDark: boolean;
schemeAttr: string | null | undefined;
prefersDark: boolean;
}
/**
* Detects the current theme from various sources
* @returns ThemeInfo object with theme detection results
*/
export function detectTheme(): ThemeInfo {
const rootEl =
typeof document !== "undefined" ? document.documentElement : null;
const schemeAttr = rootEl?.getAttribute("data-mantine-color-scheme");
const prefersDark =
typeof window !== "undefined" &&
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches;
const isDark = schemeAttr ? schemeAttr === "dark" : prefersDark;
return {
isDark,
schemeAttr,
prefersDark,
};
}
/**
* Gets CSS custom properties for chart styling
* @param isDark Whether the theme is dark
* @returns Object with CSS custom property values
*/
export function getChartThemeVars(isDark: boolean) {
return {
background: "var(--bg-surface)",
textPrimary: "var(--text-primary)",
border: isDark ? "1px solid var(--border-subtle)" : "1px solid transparent",
boxShadow: isDark ? "none" : "var(--shadow-md)",
inactive: "var(--usage-inactive)",
cardBorder: "var(--api-keys-card-border)",
};
}
/**
* Applies consistent tooltip styling
* @param tooltipElement The tooltip DOM element
* @param isDark Whether the theme is dark
*/
export function applyTooltipStyles(
tooltipElement: HTMLElement,
isDark: boolean,
) {
const themeVars = getChartThemeVars(isDark);
tooltipElement.style.background = themeVars.background;
tooltipElement.style.color = themeVars.textPrimary;
tooltipElement.style.border = themeVars.border;
tooltipElement.style.boxShadow = themeVars.boxShadow;
tooltipElement.style.padding = "8px 10px";
tooltipElement.style.fontSize = "12px";
tooltipElement.style.lineHeight = "1.25";
tooltipElement.style.borderRadius = "8px";
}
@@ -30,27 +30,6 @@ export default function AuthLayout({
// Use either overflow detection or email form expansion to determine scrollable state
const shouldBeScrollable = isOverflowing || isEmailFormExpanded;
// Force light mode on auth pages
useEffect(() => {
const htmlElement = document.documentElement;
const previousColorScheme = htmlElement.getAttribute(
"data-mantine-color-scheme",
);
// Set light mode
htmlElement.setAttribute("data-mantine-color-scheme", "light");
// Cleanup: restore previous theme when leaving auth pages
return () => {
if (previousColorScheme) {
htmlElement.setAttribute(
"data-mantine-color-scheme",
previousColorScheme,
);
}
};
}, []);
useEffect(() => {
const update = () => {
// Use viewport to avoid hysteresis when the card is already in single-column mode
@@ -101,7 +80,7 @@ export default function AuthLayout({
marginRight: "-1.5rem",
}}
>
<Footer forceLightMode={true} analyticsEnabled />
<Footer analyticsEnabled />
</div>
</div>
);
@@ -37,6 +37,19 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
[data-mantine-color-scheme="dark"] .oauth-button-fullwidth {
background-color: var(--bg-surface);
color: var(--text-primary);
border-color: var(--border-default);
box-shadow: none;
}
[data-mantine-color-scheme="dark"]
.oauth-button-fullwidth:hover:not(:disabled) {
background-color: var(--bg-raised);
box-shadow: none;
}
.auth-dropdown-wrapper {
position: relative;
}
@@ -63,6 +76,13 @@
padding: 0.25rem;
}
[data-mantine-color-scheme="dark"] .auth-dropdown {
background-color: var(--bg-surface);
color: var(--text-primary);
border-color: var(--border-default);
box-shadow: var(--shadow-md);
}
.auth-dropdown-item {
display: block;
width: 100%;
@@ -75,12 +95,20 @@
cursor: pointer;
}
[data-mantine-color-scheme="dark"] .auth-dropdown-item {
color: var(--text-primary);
}
.auth-guest-button {
background-color: #ffffff;
color: #9c2f30;
border: 2px solid currentColor;
}
[data-mantine-color-scheme="dark"] .auth-guest-button {
background-color: transparent;
}
/* ── Logo block ─────────────────────────────────────────────────────── */
.auth-logo-block {
display: flex;
@@ -96,10 +124,6 @@
width: auto;
}
.auth-logo-header--dark {
display: none;
}
/* ── Page entrance animation ────────────────────────────────────────── */
@keyframes authFadeUp {
from {
+13 -13
View File
@@ -100,22 +100,22 @@
--color-orange-300: #ff7a45;
--color-orange-400: #d84a1b;
/* Auth page colors (light mode only - auth pages force light mode) */
--auth-bg-color-light-only: #f3f4f6;
--auth-card-bg: #ffffff;
--auth-card-bg-light-only: #ffffff;
--auth-label-text-light-only: #374151;
--auth-input-border-light-only: #d1d5db;
--auth-input-bg-light-only: #ffffff;
--auth-input-text-light-only: #111827;
/* Auth page colors (dark mode) — mirror proprietary so the auth card themes dark */
--auth-bg-color-light-only: var(--bg-muted);
--auth-card-bg: var(--bg-surface);
--auth-card-bg-light-only: var(--bg-surface);
--auth-label-text-light-only: var(--text-secondary);
--auth-input-border-light-only: var(--border-default);
--auth-input-bg-light-only: var(--bg-raised);
--auth-input-text-light-only: var(--text-primary);
--auth-border-focus-light-only: #3b82f6;
--auth-focus-ring-light-only: rgba(59, 130, 246, 0.1);
--auth-focus-ring-light-only: rgba(59, 130, 246, 0.2);
--auth-button-bg-light-only: #af3434;
--auth-button-text-light-only: #ffffff;
--auth-magic-button-bg-light-only: #e5e7eb;
--auth-magic-button-text-light-only: #374151;
--auth-text-primary-light-only: #111827;
--auth-text-secondary-light-only: #6b7280;
--auth-magic-button-bg-light-only: var(--bg-raised);
--auth-magic-button-text-light-only: var(--text-primary);
--auth-text-primary-light-only: var(--text-primary);
--auth-text-secondary-light-only: var(--text-secondary);
--text-divider-rule-rgb-light: 229, 231, 235;
--text-divider-label-rgb-light: 156, 163, 175;
--tool-subcategory-rule-color-light: #e5e7eb;
-1
View File
@@ -24,5 +24,4 @@ export interface StackedBarChartProps {
export interface TooltipData {
fractions: FractionData[];
isDark: boolean;
}