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 React from 'react';
|
||||||
import { render, screen } from '@testing-library/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', () => {
|
test('renders welcome message', () => {
|
||||||
render(<App />);
|
render(
|
||||||
const linkElement = screen.getByText(/learn react/i);
|
<Provider>
|
||||||
expect(linkElement).toBeInTheDocument();
|
<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 ResetPassword from './components/authComponents/ResetPassword'
|
||||||
import UserProfile from './components/userComponents/UserProfile'
|
import UserProfile from './components/userComponents/UserProfile'
|
||||||
import EditUser from './components/userComponents/EditUser'
|
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";
|
import globalRouter from "./api/globalRouter";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -44,8 +45,10 @@ function App() {
|
|||||||
if (!appLoad) <LoadingModal />
|
if (!appLoad) <LoadingModal />
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(loadedPreferences.get().theme !== colorMode) {
|
const preferredTheme = loadedPreferences.get().theme
|
||||||
|
if(preferredTheme && preferredTheme !== colorMode) {
|
||||||
localStorage.removeItem('chakra-ui-color-mode')
|
localStorage.removeItem('chakra-ui-color-mode')
|
||||||
|
localStorage.removeItem('theme')
|
||||||
toggleColorMode()
|
toggleColorMode()
|
||||||
}
|
}
|
||||||
}, [loadedPreferences.get().theme]);
|
}, [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 {createRoot} from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import reportWebVitals from "./reportWebVitals";
|
import reportWebVitals from "./reportWebVitals";
|
||||||
import "react-toastify/dist/ReactToastify.min.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import "./styles/typography.css";
|
import "./styles/typography.css";
|
||||||
import "./styles/videoBackground.css";
|
import "./styles/videoBackground.css";
|
||||||
import "./styles/overrides.css";
|
import "./styles/overrides.css";
|
||||||
import "react-responsive-carousel/lib/styles/carousel.min.css";
|
import "react-responsive-carousel/lib/styles/carousel.min.css";
|
||||||
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
||||||
import {ChakraProvider, ColorModeScript} from "@chakra-ui/react";
|
import {Provider} from "./components/ui/provider";
|
||||||
import theme from "./styles/theme";
|
import {Toaster} from "./components/ui/toaster";
|
||||||
import NotFound from "./errors/NotFound";
|
import NotFound from "./errors/NotFound";
|
||||||
|
|
||||||
const container = document.getElementById("root");
|
const container = document.getElementById("root");
|
||||||
@@ -26,10 +26,10 @@ const router = createBrowserRouter([
|
|||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ChakraProvider theme={theme}>
|
<Provider>
|
||||||
<ColorModeScript initialColorMode={theme.config.initialColorMode}/>
|
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</ChakraProvider>
|
<Toaster />
|
||||||
|
</Provider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const getUser = async (token: string) => {
|
|||||||
if(userPreferencesExist === -1) {
|
if(userPreferencesExist === -1) {
|
||||||
userPreferences.merge([{
|
userPreferences.merge([{
|
||||||
id: user.data._id,
|
id: user.data._id,
|
||||||
theme: 'light',
|
theme: 'dark',
|
||||||
stickyNav: true,
|
stickyNav: true,
|
||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
|
|||||||
+327
-12
@@ -1,18 +1,333 @@
|
|||||||
import { extendTheme, Theme } from '@chakra-ui/react'
|
import {
|
||||||
import { mode } from '@chakra-ui/theme-tools'
|
createSystem,
|
||||||
import {loadedPreferences} from "../stateManagement/userState";
|
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({
|
// Chakra v2 solid fills were bright in dark mode (200-shade bg + dark text),
|
||||||
initialColorMode: loadedPreferences.theme,
|
// while v3 solid fills stay dark with white text. Restore the v2 behavior so
|
||||||
useSystemColorMode: false,
|
// buttons/tags read the same as before. This also fixes Switch/Checkbox
|
||||||
styles: {
|
// checked fills, which consume the same tokens.
|
||||||
global: (props: Theme) => ({
|
const v2Solid = (palette: 'gray' | 'blue' | 'green' | 'red') => {
|
||||||
body: {
|
if (palette === 'gray') {
|
||||||
bg: mode('gray.200', 'gray.800')(props),
|
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