diff --git a/src/App.test.tsx b/src/App.test.tsx
index 2a68616..c05057a 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -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();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
+test('renders welcome message', () => {
+ render(
+
+
+
+
+
+ );
+ const heading = screen.getByText(/Welcome to your Games Database/i);
+ expect(heading).toBeInTheDocument();
});
diff --git a/src/App.tsx b/src/App.tsx
index 45e72cb..d93755b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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)
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]);
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx
new file mode 100644
index 0000000..0afc8ef
--- /dev/null
+++ b/src/components/ui/alert.tsx
@@ -0,0 +1,29 @@
+import { Alert as ChakraAlert } from "@chakra-ui/react"
+import * as React from "react"
+
+export interface AlertProps extends Omit {
+ startElement?: React.ReactNode
+ endElement?: React.ReactNode
+ title?: React.ReactNode
+ icon?: React.ReactElement
+}
+
+export const Alert = React.forwardRef(
+ function Alert(props, ref) {
+ const { title, children, icon, startElement, endElement, ...rest } = props
+ return (
+
+ {startElement || {icon}}
+ {children ? (
+
+ {title}
+ {children}
+
+ ) : (
+ {title}
+ )}
+ {endElement}
+
+ )
+ },
+)
diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..0f1d3a3
--- /dev/null
+++ b/src/components/ui/avatar.tsx
@@ -0,0 +1,34 @@
+import {
+ Avatar as ChakraAvatar,
+ AvatarGroup as ChakraAvatarGroup,
+} from "@chakra-ui/react"
+import * as React from "react"
+
+type ImageProps = React.ImgHTMLAttributes
+
+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(
+ function Avatar(props, ref) {
+ const { name, src, srcSet, loading, icon, fallback, children, ...rest } =
+ props
+ return (
+
+
+ {icon || fallback}
+
+
+ {children}
+
+ )
+ },
+)
+
+export const AvatarGroup = ChakraAvatarGroup
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..246e2f0
--- /dev/null
+++ b/src/components/ui/checkbox.tsx
@@ -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
+ rootRef?: React.RefObject
+}
+
+export const Checkbox = React.forwardRef(
+ function Checkbox(props, ref) {
+ const { icon, children, inputProps, rootRef, ...rest } = props
+ return (
+
+
+
+ {icon || }
+
+ {children != null && (
+ {children}
+ )}
+
+ )
+ },
+)
diff --git a/src/components/ui/close-button.tsx b/src/components/ui/close-button.tsx
new file mode 100644
index 0000000..94af488
--- /dev/null
+++ b/src/components/ui/close-button.tsx
@@ -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 (
+
+ {props.children ?? }
+
+ )
+})
diff --git a/src/components/ui/color-mode.tsx b/src/components/ui/color-mode.tsx
new file mode 100644
index 0000000..3ad064a
--- /dev/null
+++ b/src/components/ui/color-mode.tsx
@@ -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 (
+
+ )
+}
+
+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(light: T, dark: T) {
+ const { colorMode } = useColorMode()
+ return colorMode === "dark" ? dark : light
+}
+
+export function ColorModeIcon() {
+ const { colorMode } = useColorMode()
+ return colorMode === "dark" ? :
+}
+
+interface ColorModeButtonProps extends Omit {}
+
+export const ColorModeButton = React.forwardRef<
+ HTMLButtonElement,
+ ColorModeButtonProps
+>(function ColorModeButton(props, ref) {
+ const { toggleColorMode } = useColorMode()
+ return (
+ }>
+
+
+
+
+ )
+})
+
+export const LightMode = React.forwardRef(
+ function LightMode(props, ref) {
+ return (
+
+ )
+ },
+)
+
+export const DarkMode = React.forwardRef(
+ function DarkMode(props, ref) {
+ return (
+
+ )
+ },
+)
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..44ff61a
--- /dev/null
+++ b/src/components/ui/dialog.tsx
@@ -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
+ backdrop?: boolean
+}
+
+export const DialogContent = React.forwardRef<
+ HTMLDivElement,
+ DialogContentProps
+>(function DialogContent(props, ref) {
+ const {
+ children,
+ portalled = true,
+ portalRef,
+ backdrop = true,
+ ...rest
+ } = props
+
+ return (
+
+ {backdrop && }
+
+
+ {children}
+
+
+
+ )
+})
+
+export const DialogCloseTrigger = React.forwardRef<
+ HTMLButtonElement,
+ ChakraDialog.CloseTriggerProps
+>(function DialogCloseTrigger(props, ref) {
+ return (
+
+
+ {props.children}
+
+
+ )
+})
+
+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
diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx
new file mode 100644
index 0000000..4b189b5
--- /dev/null
+++ b/src/components/ui/drawer.tsx
@@ -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
+ 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 (
+
+ {backdrop && }
+
+
+ {children}
+
+
+
+ )
+})
+
+export const DrawerCloseTrigger = React.forwardRef<
+ HTMLButtonElement,
+ ChakraDrawer.CloseTriggerProps
+>(function DrawerCloseTrigger(props, ref) {
+ return (
+
+
+
+ )
+})
+
+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
diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx
new file mode 100644
index 0000000..4866269
--- /dev/null
+++ b/src/components/ui/field.tsx
@@ -0,0 +1,33 @@
+import { Field as ChakraField } from "@chakra-ui/react"
+import * as React from "react"
+
+export interface FieldProps extends Omit {
+ label?: React.ReactNode
+ helperText?: React.ReactNode
+ errorText?: React.ReactNode
+ optionalText?: React.ReactNode
+}
+
+export const Field = React.forwardRef(
+ function Field(props, ref) {
+ const { label, children, helperText, errorText, optionalText, ...rest } =
+ props
+ return (
+
+ {label && (
+
+ {label}
+
+
+ )}
+ {children}
+ {helperText && (
+ {helperText}
+ )}
+ {errorText && (
+ {errorText}
+ )}
+
+ )
+ },
+)
diff --git a/src/components/ui/menu.tsx b/src/components/ui/menu.tsx
new file mode 100644
index 0000000..79ceb07
--- /dev/null
+++ b/src/components/ui/menu.tsx
@@ -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
+}
+
+export const MenuContent = React.forwardRef(
+ function MenuContent(props, ref) {
+ const { portalled = true, portalRef, ...rest } = props
+ return (
+
+
+
+
+
+ )
+ },
+)
+
+export const MenuArrow = React.forwardRef<
+ HTMLDivElement,
+ ChakraMenu.ArrowProps
+>(function MenuArrow(props, ref) {
+ return (
+
+
+
+ )
+})
+
+export const MenuCheckboxItem = React.forwardRef<
+ HTMLDivElement,
+ ChakraMenu.CheckboxItemProps
+>(function MenuCheckboxItem(props, ref) {
+ return (
+
+
+
+
+
+
+ {props.children}
+
+ )
+})
+
+export const MenuRadioItem = React.forwardRef<
+ HTMLDivElement,
+ ChakraMenu.RadioItemProps
+>(function MenuRadioItem(props, ref) {
+ const { children, ...rest } = props
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+})
+
+export const MenuItemGroup = React.forwardRef<
+ HTMLDivElement,
+ ChakraMenu.ItemGroupProps
+>(function MenuItemGroup(props, ref) {
+ const { title, children, ...rest } = props
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+ {children}
+
+ )
+})
+
+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 (
+
+ {startIcon}
+ {children}
+
+
+ )
+})
+
+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
diff --git a/src/components/ui/number-input.tsx b/src/components/ui/number-input.tsx
new file mode 100644
index 0000000..0948535
--- /dev/null
+++ b/src/components/ui/number-input.tsx
@@ -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 (
+
+ {children}
+
+
+
+
+
+ )
+})
+
+export const NumberInputField = ChakraNumberInput.Input
+export const NumberInputScrubber = ChakraNumberInput.Scrubber
+export const NumberInputLabel = ChakraNumberInput.Label
diff --git a/src/components/ui/provider.tsx b/src/components/ui/provider.tsx
new file mode 100644
index 0000000..ec44b3f
--- /dev/null
+++ b/src/components/ui/provider.tsx
@@ -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 (
+
+
+
+ )
+}
diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx
new file mode 100644
index 0000000..e3690c0
--- /dev/null
+++ b/src/components/ui/switch.tsx
@@ -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
+ rootRef?: React.RefObject
+ trackLabel?: { on: React.ReactNode; off: React.ReactNode }
+ thumbLabel?: { on: React.ReactNode; off: React.ReactNode }
+}
+
+export const Switch = React.forwardRef(
+ function Switch(props, ref) {
+ const { inputProps, children, rootRef, trackLabel, thumbLabel, ...rest } =
+ props
+
+ return (
+
+
+
+
+ {thumbLabel && (
+
+ {thumbLabel?.on}
+
+ )}
+
+ {trackLabel && (
+
+ {trackLabel.on}
+
+ )}
+
+ {children != null && (
+ {children}
+ )}
+
+ )
+ },
+)
diff --git a/src/components/ui/tag.tsx b/src/components/ui/tag.tsx
new file mode 100644
index 0000000..df02291
--- /dev/null
+++ b/src/components/ui/tag.tsx
@@ -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(
+ 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 (
+
+ {startElement && (
+ {startElement}
+ )}
+ {children}
+ {endElement && (
+ {endElement}
+ )}
+ {closable && (
+
+
+
+ )}
+
+ )
+ },
+)
diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx
new file mode 100644
index 0000000..5d70a35
--- /dev/null
+++ b/src/components/ui/toaster.tsx
@@ -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 (
+
+
+ {(toast) => (
+
+ {toast.type === "loading" ? (
+
+ ) : (
+
+ )}
+
+ {toast.title && {toast.title}}
+ {toast.description && (
+ {toast.description}
+ )}
+
+ {toast.action && (
+ {toast.action.label}
+ )}
+ {toast.closable && }
+
+ )}
+
+
+ )
+}
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..0129778
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -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
+ content: React.ReactNode
+ contentProps?: ChakraTooltip.ContentProps
+ disabled?: boolean
+}
+
+export const Tooltip = React.forwardRef(
+ function Tooltip(props, ref) {
+ const {
+ showArrow,
+ children,
+ disabled,
+ portalled = true,
+ content,
+ contentProps,
+ portalRef,
+ ...rest
+ } = props
+
+ if (disabled) return children
+
+ return (
+
+ {children}
+
+
+
+ {showArrow && (
+
+
+
+ )}
+ {content}
+
+
+
+
+ )
+ },
+)
diff --git a/src/index.tsx b/src/index.tsx
index 7062cd0..2dae8ab 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -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(
-
-
+
-
+
+
);
diff --git a/src/stateManagement/userState.ts b/src/stateManagement/userState.ts
index fadc9ba..df6a0be 100644
--- a/src/stateManagement/userState.ts
+++ b/src/stateManagement/userState.ts
@@ -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,
}])
}
diff --git a/src/styles/theme.tsx b/src/styles/theme.tsx
index 0ea0604..784f77f 100644
--- a/src/styles/theme.tsx
+++ b/src/styles/theme.tsx
@@ -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) =>
+ 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',
+ },
+ },
+ },
},
})
-
\ No newline at end of file
+
+// 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)