feat(theme): migrate to Chakra v3 system with v2 look restoration

- Replace extendTheme/ColorModeScript with createSystem Provider +
  Toaster shims (components/ui, provider, color-mode) and next-themes;
  index.tsx renders Provider, App reads useColorMode from the shim and
  clears both chakra and legacy theme storage keys on preference change
- theme.tsx restores the v2 identity on v3 primitives: v2 gray/blue/
  green/red scales, v2 solid/contrast fills, v2 text-per-size for
  button/input/native-select/tag, v2 input heights, v2 heading scale
  (xl->4xl etc), v2 switch track/thumb, brighter link underlines
  (currentColor/60), +1px tag top padding, 17px body base font
- Default new-user theme light -> dark to match the established dark UI
- App.test rewritten around Provider + MemoryRouter + Home (old CRA
  'learn react' assertion no longer applies)
This commit is contained in:
2026-09-06 10:29:04 -04:00
parent 050e72bc8a
commit 4574dfb8f1
20 changed files with 1047 additions and 26 deletions
+13 -5
View File
@@ -1,9 +1,17 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
import { MemoryRouter } from 'react-router-dom';
import { Provider } from './components/ui/provider';
import Home from './components/Home';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
test('renders welcome message', () => {
render(
<Provider>
<MemoryRouter>
<Home />
</MemoryRouter>
</Provider>
);
const heading = screen.getByText(/Welcome to your Games Database/i);
expect(heading).toBeInTheDocument();
});
+5 -2
View File
@@ -23,7 +23,8 @@ import ForgotPassword from './components/authComponents/ForgotPassword'
import ResetPassword from './components/authComponents/ResetPassword'
import UserProfile from './components/userComponents/UserProfile'
import EditUser from './components/userComponents/EditUser'
import {Box, useColorMode} from "@chakra-ui/react";
import {Box} from "@chakra-ui/react";
import {useColorMode} from "./components/ui/color-mode";
import globalRouter from "./api/globalRouter";
function App() {
@@ -44,8 +45,10 @@ function App() {
if (!appLoad) <LoadingModal />
useEffect(() => {
if(loadedPreferences.get().theme !== colorMode) {
const preferredTheme = loadedPreferences.get().theme
if(preferredTheme && preferredTheme !== colorMode) {
localStorage.removeItem('chakra-ui-color-mode')
localStorage.removeItem('theme')
toggleColorMode()
}
}, [loadedPreferences.get().theme]);
+29
View File
@@ -0,0 +1,29 @@
import { Alert as ChakraAlert } from "@chakra-ui/react"
import * as React from "react"
export interface AlertProps extends Omit<ChakraAlert.RootProps, "title"> {
startElement?: React.ReactNode
endElement?: React.ReactNode
title?: React.ReactNode
icon?: React.ReactElement
}
export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
function Alert(props, ref) {
const { title, children, icon, startElement, endElement, ...rest } = props
return (
<ChakraAlert.Root ref={ref} {...rest}>
{startElement || <ChakraAlert.Indicator>{icon}</ChakraAlert.Indicator>}
{children ? (
<ChakraAlert.Content>
<ChakraAlert.Title>{title}</ChakraAlert.Title>
<ChakraAlert.Description>{children}</ChakraAlert.Description>
</ChakraAlert.Content>
) : (
<ChakraAlert.Title flex="1">{title}</ChakraAlert.Title>
)}
{endElement}
</ChakraAlert.Root>
)
},
)
+34
View File
@@ -0,0 +1,34 @@
import {
Avatar as ChakraAvatar,
AvatarGroup as ChakraAvatarGroup,
} from "@chakra-ui/react"
import * as React from "react"
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>
export interface AvatarProps extends ChakraAvatar.RootProps {
name?: string
src?: string
srcSet?: string
loading?: ImageProps["loading"]
icon?: React.ReactElement
fallback?: React.ReactNode
}
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
function Avatar(props, ref) {
const { name, src, srcSet, loading, icon, fallback, children, ...rest } =
props
return (
<ChakraAvatar.Root ref={ref} {...rest}>
<ChakraAvatar.Fallback name={name}>
{icon || fallback}
</ChakraAvatar.Fallback>
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
{children}
</ChakraAvatar.Root>
)
},
)
export const AvatarGroup = ChakraAvatarGroup
+25
View File
@@ -0,0 +1,25 @@
import { Checkbox as ChakraCheckbox } from "@chakra-ui/react"
import * as React from "react"
export interface CheckboxProps extends ChakraCheckbox.RootProps {
icon?: React.ReactNode
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
rootRef?: React.RefObject<HTMLLabelElement | null>
}
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
function Checkbox(props, ref) {
const { icon, children, inputProps, rootRef, ...rest } = props
return (
<ChakraCheckbox.Root ref={rootRef} {...rest}>
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
<ChakraCheckbox.Control>
{icon || <ChakraCheckbox.Indicator />}
</ChakraCheckbox.Control>
{children != null && (
<ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>
)}
</ChakraCheckbox.Root>
)
},
)
+17
View File
@@ -0,0 +1,17 @@
import type { ButtonProps } from "@chakra-ui/react"
import { IconButton as ChakraIconButton } from "@chakra-ui/react"
import * as React from "react"
import { LuX } from "react-icons/lu"
export type CloseButtonProps = ButtonProps
export const CloseButton = React.forwardRef<
HTMLButtonElement,
CloseButtonProps
>(function CloseButton(props, ref) {
return (
<ChakraIconButton variant="ghost" aria-label="Close" ref={ref} {...props}>
{props.children ?? <LuX />}
</ChakraIconButton>
)
})
+108
View File
@@ -0,0 +1,108 @@
"use client"
import type { IconButtonProps, SpanProps } from "@chakra-ui/react"
import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react"
import { ThemeProvider, useTheme } from "next-themes"
import type { ThemeProviderProps } from "next-themes"
import * as React from "react"
import { LuMoon, LuSun } from "react-icons/lu"
export interface ColorModeProviderProps extends ThemeProviderProps {}
export function ColorModeProvider(props: ColorModeProviderProps) {
return (
<ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange {...props} />
)
}
export type ColorMode = "light" | "dark"
export interface UseColorModeReturn {
colorMode: ColorMode
setColorMode: (colorMode: ColorMode) => void
toggleColorMode: () => void
}
export function useColorMode(): UseColorModeReturn {
const { resolvedTheme, setTheme, forcedTheme } = useTheme()
const colorMode = forcedTheme || resolvedTheme
const toggleColorMode = () => {
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}
return {
colorMode: colorMode as ColorMode,
setColorMode: setTheme,
toggleColorMode,
}
}
export function useColorModeValue<T>(light: T, dark: T) {
const { colorMode } = useColorMode()
return colorMode === "dark" ? dark : light
}
export function ColorModeIcon() {
const { colorMode } = useColorMode()
return colorMode === "dark" ? <LuMoon /> : <LuSun />
}
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
export const ColorModeButton = React.forwardRef<
HTMLButtonElement,
ColorModeButtonProps
>(function ColorModeButton(props, ref) {
const { toggleColorMode } = useColorMode()
return (
<ClientOnly fallback={<Skeleton boxSize="9" />}>
<IconButton
onClick={toggleColorMode}
variant="ghost"
aria-label="Toggle color mode"
size="sm"
ref={ref}
{...props}
css={{
_icon: {
width: "5",
height: "5",
},
}}
>
<ColorModeIcon />
</IconButton>
</ClientOnly>
)
})
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>(
function LightMode(props, ref) {
return (
<Span
color="fg"
display="contents"
className="chakra-theme light"
colorPalette="gray"
colorScheme="light"
ref={ref}
{...props}
/>
)
},
)
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>(
function DarkMode(props, ref) {
return (
<Span
color="fg"
display="contents"
className="chakra-theme dark"
colorPalette="gray"
colorScheme="dark"
ref={ref}
{...props}
/>
)
},
)
+62
View File
@@ -0,0 +1,62 @@
import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react"
import { CloseButton } from "./close-button"
import * as React from "react"
interface DialogContentProps extends ChakraDialog.ContentProps {
portalled?: boolean
portalRef?: React.RefObject<HTMLElement | null>
backdrop?: boolean
}
export const DialogContent = React.forwardRef<
HTMLDivElement,
DialogContentProps
>(function DialogContent(props, ref) {
const {
children,
portalled = true,
portalRef,
backdrop = true,
...rest
} = props
return (
<Portal disabled={!portalled} container={portalRef}>
{backdrop && <ChakraDialog.Backdrop />}
<ChakraDialog.Positioner>
<ChakraDialog.Content ref={ref} {...rest} asChild={false}>
{children}
</ChakraDialog.Content>
</ChakraDialog.Positioner>
</Portal>
)
})
export const DialogCloseTrigger = React.forwardRef<
HTMLButtonElement,
ChakraDialog.CloseTriggerProps
>(function DialogCloseTrigger(props, ref) {
return (
<ChakraDialog.CloseTrigger
position="absolute"
top="2"
insetEnd="2"
{...props}
asChild
>
<CloseButton size="sm" ref={ref}>
{props.children}
</CloseButton>
</ChakraDialog.CloseTrigger>
)
})
export const DialogRoot = ChakraDialog.Root
export const DialogFooter = ChakraDialog.Footer
export const DialogHeader = ChakraDialog.Header
export const DialogBody = ChakraDialog.Body
export const DialogBackdrop = ChakraDialog.Backdrop
export const DialogTitle = ChakraDialog.Title
export const DialogDescription = ChakraDialog.Description
export const DialogTrigger = ChakraDialog.Trigger
export const DialogActionTrigger = ChakraDialog.ActionTrigger
+61
View File
@@ -0,0 +1,61 @@
import { Drawer as ChakraDrawer, Portal } from "@chakra-ui/react"
import { CloseButton } from "./close-button"
import * as React from "react"
interface DrawerContentProps extends ChakraDrawer.ContentProps {
portalled?: boolean
portalRef?: React.RefObject<HTMLElement | null>
offset?: ChakraDrawer.ContentProps["padding"]
backdrop?: boolean
}
export const DrawerContent = React.forwardRef<
HTMLDivElement,
DrawerContentProps
>(function DrawerContent(props, ref) {
const {
children,
portalled = true,
portalRef,
offset,
backdrop = true,
...rest
} = props
return (
<Portal disabled={!portalled} container={portalRef}>
{backdrop && <ChakraDrawer.Backdrop />}
<ChakraDrawer.Positioner padding={offset}>
<ChakraDrawer.Content ref={ref} {...rest} asChild={false}>
{children}
</ChakraDrawer.Content>
</ChakraDrawer.Positioner>
</Portal>
)
})
export const DrawerCloseTrigger = React.forwardRef<
HTMLButtonElement,
ChakraDrawer.CloseTriggerProps
>(function DrawerCloseTrigger(props, ref) {
return (
<ChakraDrawer.CloseTrigger
position="absolute"
top="2"
insetEnd="2"
{...props}
asChild
>
<CloseButton size="sm" ref={ref} />
</ChakraDrawer.CloseTrigger>
)
})
export const DrawerTrigger = ChakraDrawer.Trigger
export const DrawerRoot = ChakraDrawer.Root
export const DrawerFooter = ChakraDrawer.Footer
export const DrawerHeader = ChakraDrawer.Header
export const DrawerBody = ChakraDrawer.Body
export const DrawerBackdrop = ChakraDrawer.Backdrop
export const DrawerDescription = ChakraDrawer.Description
export const DrawerTitle = ChakraDrawer.Title
export const DrawerActionTrigger = ChakraDrawer.ActionTrigger
+33
View File
@@ -0,0 +1,33 @@
import { Field as ChakraField } from "@chakra-ui/react"
import * as React from "react"
export interface FieldProps extends Omit<ChakraField.RootProps, "label"> {
label?: React.ReactNode
helperText?: React.ReactNode
errorText?: React.ReactNode
optionalText?: React.ReactNode
}
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
function Field(props, ref) {
const { label, children, helperText, errorText, optionalText, ...rest } =
props
return (
<ChakraField.Root ref={ref} {...rest}>
{label && (
<ChakraField.Label textStyle="md">
{label}
<ChakraField.RequiredIndicator fallback={optionalText} />
</ChakraField.Label>
)}
{children}
{helperText && (
<ChakraField.HelperText>{helperText}</ChakraField.HelperText>
)}
{errorText && (
<ChakraField.ErrorText>{errorText}</ChakraField.ErrorText>
)}
</ChakraField.Root>
)
},
)
+112
View File
@@ -0,0 +1,112 @@
"use client"
import { AbsoluteCenter, Menu as ChakraMenu, Portal } from "@chakra-ui/react"
import * as React from "react"
import { LuCheck, LuChevronRight } from "react-icons/lu"
interface MenuContentProps extends ChakraMenu.ContentProps {
portalled?: boolean
portalRef?: React.RefObject<HTMLElement | null>
}
export const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(
function MenuContent(props, ref) {
const { portalled = true, portalRef, ...rest } = props
return (
<Portal disabled={!portalled} container={portalRef}>
<ChakraMenu.Positioner>
<ChakraMenu.Content ref={ref} {...rest} />
</ChakraMenu.Positioner>
</Portal>
)
},
)
export const MenuArrow = React.forwardRef<
HTMLDivElement,
ChakraMenu.ArrowProps
>(function MenuArrow(props, ref) {
return (
<ChakraMenu.Arrow ref={ref} {...props}>
<ChakraMenu.ArrowTip />
</ChakraMenu.Arrow>
)
})
export const MenuCheckboxItem = React.forwardRef<
HTMLDivElement,
ChakraMenu.CheckboxItemProps
>(function MenuCheckboxItem(props, ref) {
return (
<ChakraMenu.CheckboxItem ps="8" ref={ref} {...props}>
<AbsoluteCenter axis="horizontal" insetStart="4" asChild>
<ChakraMenu.ItemIndicator>
<LuCheck />
</ChakraMenu.ItemIndicator>
</AbsoluteCenter>
{props.children}
</ChakraMenu.CheckboxItem>
)
})
export const MenuRadioItem = React.forwardRef<
HTMLDivElement,
ChakraMenu.RadioItemProps
>(function MenuRadioItem(props, ref) {
const { children, ...rest } = props
return (
<ChakraMenu.RadioItem ps="8" ref={ref} {...rest}>
<AbsoluteCenter axis="horizontal" insetStart="4" asChild>
<ChakraMenu.ItemIndicator>
<LuCheck />
</ChakraMenu.ItemIndicator>
</AbsoluteCenter>
<ChakraMenu.ItemText>{children}</ChakraMenu.ItemText>
</ChakraMenu.RadioItem>
)
})
export const MenuItemGroup = React.forwardRef<
HTMLDivElement,
ChakraMenu.ItemGroupProps
>(function MenuItemGroup(props, ref) {
const { title, children, ...rest } = props
return (
<ChakraMenu.ItemGroup ref={ref} {...rest}>
{title && (
<ChakraMenu.ItemGroupLabel userSelect="none">
{title}
</ChakraMenu.ItemGroupLabel>
)}
{children}
</ChakraMenu.ItemGroup>
)
})
export interface MenuTriggerItemProps extends ChakraMenu.ItemProps {
startIcon?: React.ReactNode
}
export const MenuTriggerItem = React.forwardRef<
HTMLDivElement,
MenuTriggerItemProps
>(function MenuTriggerItem(props, ref) {
const { startIcon, children, ...rest } = props
return (
<ChakraMenu.TriggerItem ref={ref} {...rest}>
{startIcon}
{children}
<LuChevronRight />
</ChakraMenu.TriggerItem>
)
})
export const MenuRadioItemGroup = ChakraMenu.RadioItemGroup
export const MenuContextTrigger = ChakraMenu.ContextTrigger
export const MenuRoot = ChakraMenu.Root
export const MenuSeparator = ChakraMenu.Separator
export const MenuItem = ChakraMenu.Item
export const MenuItemText = ChakraMenu.ItemText
export const MenuItemCommand = ChakraMenu.ItemCommand
export const MenuTrigger = ChakraMenu.Trigger
+28
View File
@@ -0,0 +1,28 @@
import { NumberInput as ChakraNumberInput } from "@chakra-ui/react"
import * as React from "react"
export interface NumberInputProps extends ChakraNumberInput.RootProps {}
export const NumberInputRoot = React.forwardRef<
HTMLDivElement,
NumberInputProps
>(function NumberInput(props, ref) {
const { children, ...rest } = props
return (
<ChakraNumberInput.Root
ref={ref}
variant="outline"
{...rest}
>
{children}
<ChakraNumberInput.Control>
<ChakraNumberInput.IncrementTrigger />
<ChakraNumberInput.DecrementTrigger />
</ChakraNumberInput.Control>
</ChakraNumberInput.Root>
)
})
export const NumberInputField = ChakraNumberInput.Input
export const NumberInputScrubber = ChakraNumberInput.Scrubber
export const NumberInputLabel = ChakraNumberInput.Label
+16
View File
@@ -0,0 +1,16 @@
"use client"
import { ChakraProvider } from "@chakra-ui/react"
import {
ColorModeProvider,
type ColorModeProviderProps,
} from "./color-mode"
import system from "../../styles/theme"
export function Provider(props: ColorModeProviderProps) {
return (
<ChakraProvider value={system}>
<ColorModeProvider {...props} />
</ChakraProvider>
)
}
+39
View File
@@ -0,0 +1,39 @@
import { Switch as ChakraSwitch } from "@chakra-ui/react"
import * as React from "react"
export interface SwitchProps extends ChakraSwitch.RootProps {
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
rootRef?: React.RefObject<HTMLLabelElement | null>
trackLabel?: { on: React.ReactNode; off: React.ReactNode }
thumbLabel?: { on: React.ReactNode; off: React.ReactNode }
}
export const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
function Switch(props, ref) {
const { inputProps, children, rootRef, trackLabel, thumbLabel, ...rest } =
props
return (
<ChakraSwitch.Root ref={rootRef} {...rest}>
<ChakraSwitch.HiddenInput ref={ref} {...inputProps} />
<ChakraSwitch.Control>
<ChakraSwitch.Thumb>
{thumbLabel && (
<ChakraSwitch.ThumbIndicator fallback={thumbLabel?.off}>
{thumbLabel?.on}
</ChakraSwitch.ThumbIndicator>
)}
</ChakraSwitch.Thumb>
{trackLabel && (
<ChakraSwitch.Indicator fallback={trackLabel.off}>
{trackLabel.on}
</ChakraSwitch.Indicator>
)}
</ChakraSwitch.Control>
{children != null && (
<ChakraSwitch.Label>{children}</ChakraSwitch.Label>
)}
</ChakraSwitch.Root>
)
},
)
+42
View File
@@ -0,0 +1,42 @@
import { Tag as ChakraTag } from "@chakra-ui/react"
import * as React from "react"
export interface TagProps extends ChakraTag.RootProps {
startElement?: React.ReactNode
endElement?: React.ReactNode
onClose?: VoidFunction
closable?: boolean
}
export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
function Tag(props, ref) {
const {
startElement,
endElement,
onClose,
closable = !!onClose,
children,
size,
...rest
} = props
// v2 tag text per size (v3 renders one step smaller)
const labelTextStyle = size === 'lg' ? 'md' : size === 'sm' ? 'xs' : 'sm'
return (
<ChakraTag.Root ref={ref} size={size} {...rest}>
{startElement && (
<ChakraTag.StartElement>{startElement}</ChakraTag.StartElement>
)}
<ChakraTag.Label textStyle={labelTextStyle}>{children}</ChakraTag.Label>
{endElement && (
<ChakraTag.EndElement>{endElement}</ChakraTag.EndElement>
)}
{closable && (
<ChakraTag.EndElement>
<ChakraTag.CloseTrigger onClick={onClose} />
</ChakraTag.EndElement>
)}
</ChakraTag.Root>
)
},
)
+43
View File
@@ -0,0 +1,43 @@
"use client"
import {
Toaster as ChakraToaster,
Portal,
Spinner,
Stack,
Toast,
createToaster,
} from "@chakra-ui/react"
export const toaster = createToaster({
placement: "bottom-end",
pauseOnPageIdle: true,
})
export const Toaster = () => {
return (
<Portal>
<ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
{(toast) => (
<Toast.Root width={{ md: "sm" }}>
{toast.type === "loading" ? (
<Spinner size="sm" color="blue.solid" />
) : (
<Toast.Indicator />
)}
<Stack gap="1" flex="1" maxWidth="100%">
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
{toast.description && (
<Toast.Description>{toast.description}</Toast.Description>
)}
</Stack>
{toast.action && (
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
)}
{toast.closable && <Toast.CloseTrigger />}
</Toast.Root>
)}
</ChakraToaster>
</Portal>
)
}
+46
View File
@@ -0,0 +1,46 @@
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
import * as React from "react"
export interface TooltipProps extends ChakraTooltip.RootProps {
showArrow?: boolean
portalled?: boolean
portalRef?: React.RefObject<HTMLElement | null>
content: React.ReactNode
contentProps?: ChakraTooltip.ContentProps
disabled?: boolean
}
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
function Tooltip(props, ref) {
const {
showArrow,
children,
disabled,
portalled = true,
content,
contentProps,
portalRef,
...rest
} = props
if (disabled) return children
return (
<ChakraTooltip.Root {...rest}>
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
<Portal disabled={!portalled} container={portalRef}>
<ChakraTooltip.Positioner>
<ChakraTooltip.Content ref={ref} {...contentProps}>
{showArrow && (
<ChakraTooltip.Arrow>
<ChakraTooltip.ArrowTip />
</ChakraTooltip.Arrow>
)}
{content}
</ChakraTooltip.Content>
</ChakraTooltip.Positioner>
</Portal>
</ChakraTooltip.Root>
)
},
)
+6 -6
View File
@@ -3,14 +3,14 @@ import React from "react";
import {createRoot} from "react-dom/client";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import "react-toastify/dist/ReactToastify.min.css";
import "react-toastify/dist/ReactToastify.css";
import "./styles/typography.css";
import "./styles/videoBackground.css";
import "./styles/overrides.css";
import "react-responsive-carousel/lib/styles/carousel.min.css";
import {createBrowserRouter, RouterProvider} from "react-router-dom";
import {ChakraProvider, ColorModeScript} from "@chakra-ui/react";
import theme from "./styles/theme";
import {Provider} from "./components/ui/provider";
import {Toaster} from "./components/ui/toaster";
import NotFound from "./errors/NotFound";
const container = document.getElementById("root");
@@ -26,10 +26,10 @@ const router = createBrowserRouter([
root.render(
<React.StrictMode>
<ChakraProvider theme={theme}>
<ColorModeScript initialColorMode={theme.config.initialColorMode}/>
<Provider>
<RouterProvider router={router} />
</ChakraProvider>
<Toaster />
</Provider>
</React.StrictMode>
);
+1 -1
View File
@@ -74,7 +74,7 @@ export const getUser = async (token: string) => {
if(userPreferencesExist === -1) {
userPreferences.merge([{
id: user.data._id,
theme: 'light',
theme: 'dark',
stickyNav: true,
}])
}
+327 -12
View File
@@ -1,18 +1,333 @@
import { extendTheme, Theme } from '@chakra-ui/react'
import { mode } from '@chakra-ui/theme-tools'
import {loadedPreferences} from "../stateManagement/userState";
import {
createSystem,
defaultConfig,
defineConfig,
defineRecipe,
defineSlotRecipe,
} from '@chakra-ui/react'
import {
buttonRecipe,
headingRecipe,
inputRecipe,
linkRecipe,
nativeSelectSlotRecipe,
numberInputSlotRecipe,
switchSlotRecipe,
tagSlotRecipe,
} from '@chakra-ui/react/theme'
// Chakra v2 color scales (blue-tinted). Chakra v3 replaced these with neutral
// scales and a near-black page background, which changed the app's whole look.
// Restoring the v2 scales keeps every component on the old colorscheme.
const v2Colors = {
gray: {
50: '#F7FAFC',
100: '#EDF2F7',
200: '#E2E8F0',
300: '#CBD5E0',
400: '#A0AEC0',
500: '#718096',
600: '#4A5568',
700: '#2D3748',
800: '#1A202C',
900: '#171923',
},
blue: {
50: '#EBF8FF',
100: '#BEE3F8',
200: '#90CDF4',
300: '#63B3ED',
400: '#4299E1',
500: '#3182CE',
600: '#2B6CB0',
700: '#2C5282',
800: '#2A4365',
900: '#1A365D',
},
green: {
50: '#F0FFF4',
100: '#C6F6D5',
200: '#9AE6B4',
300: '#68D391',
400: '#48BB78',
500: '#38A169',
600: '#2F855A',
700: '#276749',
800: '#22543D',
900: '#1C4532',
},
red: {
50: '#FFF5F5',
100: '#FED7D7',
200: '#FEB2B2',
300: '#FC8181',
400: '#F56565',
500: '#E53E3E',
600: '#C53030',
700: '#9B2C2C',
800: '#742A2A',
900: '#63171B',
},
} as const
const toTokens = (scale: Record<string, string>) =>
Object.fromEntries(Object.entries(scale).map(([shade, hex]) => [shade, { value: hex }]))
export default extendTheme({
initialColorMode: loadedPreferences.theme,
useSystemColorMode: false,
styles: {
global: (props: Theme) => ({
body: {
bg: mode('gray.200', 'gray.800')(props),
// Chakra v2 solid fills were bright in dark mode (200-shade bg + dark text),
// while v3 solid fills stay dark with white text. Restore the v2 behavior so
// buttons/tags read the same as before. This also fixes Switch/Checkbox
// checked fills, which consume the same tokens.
const v2Solid = (palette: 'gray' | 'blue' | 'green' | 'red') => {
if (palette === 'gray') {
return {
solid: { value: { _light: '{colors.gray.100}', _dark: '{colors.whiteAlpha.200}' } },
contrast: { value: { _light: '{colors.gray.800}', _dark: 'white' } },
}
}
return {
solid: { value: { _light: `{colors.${palette}.500}`, _dark: `{colors.${palette}.200}` } },
contrast: { value: { _light: 'white', _dark: '{colors.gray.800}' } },
}
}
// Chakra v3 shrunk component text a step (e.g. md buttons/inputs render sm
// text). v2 used same-name text per size — restore that.
// NOTE: recipe bases are spread as `any` — retyping Chakra's inferred
// variant unions through defineRecipe's generics is not worth it here;
// the objects below are complete recipes by construction (spread of the
// original + targeted size overrides).
const baseButton: any = buttonRecipe
const baseHeading: any = headingRecipe
const baseInput: any = inputRecipe
const baseNumberInput: any = numberInputSlotRecipe
const baseNativeSelect: any = nativeSelectSlotRecipe
const baseSwitch: any = switchSlotRecipe
const baseTag: any = tagSlotRecipe
const baseLink: any = linkRecipe
// v3 link underlines render at 20% opacity (currentColor/20) — too dim
// against dark surfaces. Brighten universally.
const v2LinkRecipe = defineRecipe({
...baseLink,
variants: {
...baseLink.variants,
variant: {
...baseLink.variants?.variant,
underline: {
...baseLink.variants?.variant?.underline,
textDecorationColor: 'currentColor/60',
},
})
plain: {
...baseLink.variants?.variant?.plain,
_hover: {
...baseLink.variants?.variant?.plain?._hover,
textDecorationColor: 'currentColor/60',
},
},
},
},
})
// v2 tag text per size (v3 renders one step smaller)
const v2TagRecipe = defineSlotRecipe({
...baseTag,
base: {
...baseTag.base,
root: { ...baseTag.base?.root, pt: '1px' },
},
variants: {
...baseTag.variants,
size: {
...baseTag.variants?.size,
md: {
...baseTag.variants?.size?.md,
label: { ...baseTag.variants?.size?.md?.label, textStyle: 'sm' },
},
lg: {
...baseTag.variants?.size?.lg,
label: { ...baseTag.variants?.size?.lg?.label, textStyle: 'md' },
},
},
},
})
const v2ButtonRecipe = defineRecipe({
...baseButton,
base: {
...baseButton.base,
fontWeight: 'semibold',
},
variants: {
...baseButton.variants,
size: {
...baseButton.variants?.size,
sm: { ...baseButton.variants?.size?.sm, textStyle: 'sm', h: '8', minW: '8' },
md: { ...baseButton.variants?.size?.md, textStyle: 'md' },
lg: { ...baseButton.variants?.size?.lg, textStyle: 'lg', h: '12', minW: '12' },
},
},
})
const v2InputRecipe = defineRecipe({
...baseInput,
variants: {
...baseInput.variants,
size: {
...baseInput.variants?.size,
sm: { ...baseInput.variants?.size?.sm, textStyle: 'sm', '--input-height': 'sizes.8' },
md: { ...baseInput.variants?.size?.md, textStyle: 'md' },
lg: { ...baseInput.variants?.size?.lg, textStyle: 'lg', '--input-height': 'sizes.12' },
},
},
})
// v2 headings were a full step larger than v3 same-name sizes
// (v2 xl = 3xl/4xl ≈ 30/36px, v3 xl = 20px). Restore v2 mapping universally
// so page titles match the old screenshots.
const v2HeadingRecipe = defineRecipe({
...baseHeading,
variants: {
...baseHeading.variants,
size: {
...baseHeading.variants?.size,
xs: { textStyle: 'sm' },
sm: { textStyle: 'md' },
md: { textStyle: 'xl' },
lg: { textStyle: '3xl' },
xl: { textStyle: '4xl' },
'2xl': { textStyle: '5xl' },
'3xl': { textStyle: '6xl' },
'4xl': { textStyle: '7xl' },
},
},
})
const v2NumberInputRecipe = defineSlotRecipe({
...baseNumberInput,
variants: {
...baseNumberInput.variants,
size: {
...baseNumberInput.variants?.size,
xs: {
...baseNumberInput.variants?.size?.xs,
input: { ...v2InputRecipe.variants?.size?.xs },
},
sm: {
...baseNumberInput.variants?.size?.sm,
input: { ...v2InputRecipe.variants?.size?.sm },
},
md: {
...baseNumberInput.variants?.size?.md,
input: { ...v2InputRecipe.variants?.size?.md },
},
lg: {
...baseNumberInput.variants?.size?.lg,
input: { ...v2InputRecipe.variants?.size?.lg },
},
},
},
})
const v2NativeSelectRecipe = defineSlotRecipe({
...baseNativeSelect,
variants: {
...baseNativeSelect.variants,
size: {
...baseNativeSelect.variants?.size,
md: {
...baseNativeSelect.variants?.size?.md,
field: { ...baseNativeSelect.variants?.size?.md?.field, textStyle: 'md' },
},
lg: {
...baseNativeSelect.variants?.size?.lg,
field: { ...baseNativeSelect.variants?.size?.lg?.field, textStyle: 'lg' },
},
},
},
})
// v2 Switch: unchecked track lighter grey (gray.300 / whiteAlpha.400),
// checked track blue (500 / 200), thumb white always. v3 uses a near-black
// unchecked track in dark mode and a dark thumb when checked.
const v2SwitchRecipe = defineSlotRecipe({
...baseSwitch,
variants: {
...baseSwitch.variants,
variant: {
...baseSwitch.variants?.variant,
solid: {
...baseSwitch.variants?.variant?.solid,
control: {
...baseSwitch.variants?.variant?.solid?.control,
bg: { base: 'gray.300', _dark: 'whiteAlpha.400' },
},
thumb: {
...baseSwitch.variants?.variant?.solid?.thumb,
bg: 'white',
_checked: {
...baseSwitch.variants?.variant?.solid?.thumb?._checked,
bg: 'white',
},
},
},
},
},
})
const config = defineConfig({
theme: {
recipes: {
button: v2ButtonRecipe,
heading: v2HeadingRecipe,
input: v2InputRecipe,
link: v2LinkRecipe,
},
slotRecipes: {
numberInput: v2NumberInputRecipe,
nativeSelect: v2NativeSelectRecipe,
switch: v2SwitchRecipe,
tag: v2TagRecipe,
},
tokens: {
colors: {
gray: {
...toTokens(v2Colors.gray),
// v3 surfaces that default to near-black (menu/dropdown/modal
// panels) used v2's gray.700 in dark mode.
950: { value: '#2D3748' },
},
blue: toTokens(v2Colors.blue),
green: toTokens(v2Colors.green),
red: toTokens(v2Colors.red),
},
},
semanticTokens: {
colors: {
gray: v2Solid('gray'),
blue: v2Solid('blue'),
green: v2Solid('green'),
red: v2Solid('red'),
},
},
},
globalCss: {
// NOTE: top margins of page content collapse past `body`, so the strip
// above the content is painted with `html`'s background. It must match
// the body or the page gets a mismatched band.
// Explicit `.dark` class selectors are used because Chakra v3's `_dark`
// condition only matches *descendants* of `.dark`, never `html.dark`
// itself.
'html:not(.dark)': {
bg: 'gray.200',
},
'html.dark': {
bg: 'gray.800',
},
body: {
bg: { base: 'gray.200', _dark: 'gray.800' },
fontSize: '1.0625rem',
},
},
})
export default createSystem(defaultConfig, config)