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)