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:
+13
-5
@@ -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
@@ -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]);
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
})
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user