mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
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:
@@ -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"
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+10
-7
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -24,5 +24,4 @@ export interface StackedBarChartProps {
|
||||
|
||||
export interface TooltipData {
|
||||
fractions: FractionData[];
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user