feat: add settings menu
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m40s

This commit is contained in:
Hewston Fox
2026-03-22 04:08:56 +02:00
parent 2a1115b66f
commit 5e9acffa09
89 changed files with 3412 additions and 216 deletions

View File

@@ -24,6 +24,8 @@
&:disabled {
pointer-events: none;
opacity: 0.9;
filter: grayscale(0.6);
}
}

View File

@@ -1,6 +1,7 @@
import { motion, type HTMLMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
import { usePlaySound } from "@/audio";
import tg from "@/tg";
import classes from "./Button.module.css";
@@ -18,10 +19,13 @@ const VARIANTS_MAP = {
} satisfies Record<Exclude<Props["variant"], undefined>, string>;
export default function Button({ className, variant = "blue", onClick, ...props }: Props) {
const play = usePlaySound();
return (
<motion.button
{...props}
onClick={(e) => {
play("click");
tg.hapticFeedback.click();
onClick?.(e);
}}

View File

@@ -0,0 +1,74 @@
@layer base {
.wrapper {
display: flex;
flex-direction: row;
width: 100%;
height: 40px;
flex-shrink: 0;
gap: 1px;
border-radius: 9999px;
overflow: hidden;
}
.item {
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
}
.startItem {
border-radius: 9999px 5px 5px 9999px;
}
.endItem {
border-radius: 5px 9999px 9999px 5px;
}
.button {
cursor: pointer;
flex: 1;
flex-shrink: 0;
user-select: none;
background: none;
border: none;
padding: 8px;
display: flex;
align-items: center;
justify-content: center;
&:disabled {
opacity: 0.5;
pointer-events: none;
}
}
.state {
flex: 1;
padding: 8px;
font-weight: 700;
font-size: 14px;
line-height: 1;
}
.mirrored {
transform: scaleX(-1);
}
.blueWrapper {
padding: 3px;
border-radius: 9999px;
}
.blueItem {
background: #bef9fb;
box-shadow:
0px 1px 0px 0px #ffffffbf inset,
-1px 0px 0px 0px #00000059 inset,
1px 0px 0px 0px #00000059 inset,
0px -1px 0px 0px #00000059 inset;
color: #0c2836;
--icon-fill: #227873;
--icon-stroke: #0c2836;
}
}

View File

@@ -0,0 +1,107 @@
import { motion } from "motion/react";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import tg from "@/tg";
import ContentSurface from "@components/surface/ContentSurface";
import { BlueSurface } from "@components/surface/BlueSectionSurface";
import LightSurface from "@components/surface/LightSurface";
import BackIcon from "./icons/BackIcon";
import StartIcon from "./icons/StartIcon";
import classes from "./Pagination.module.css";
import { usePlaySound } from "@/audio";
type Props = {
value: number;
total: number;
onChange?: (page: number) => unknown;
variant?: "default" | "blue";
};
export default function Pagination({ value, total, onChange, variant = "default" }: Props) {
const { t } = useTranslation();
const play = usePlaySound();
const isAtStart = value <= 1 || total <= 1;
const isAtEnd = value >= total || total <= 1;
const isBlue = variant === "blue";
const Wrapper = isBlue ? BlueSurface : ContentSurface;
const ItemSurface = isBlue ? motion.div : LightSurface;
const itemClass = (...extra: Parameters<typeof clsx>) =>
clsx(classes.item, isBlue ? classes.blueItem : undefined, ...extra);
return (
<Wrapper className={clsx(classes.wrapper, isBlue ? classes.blueWrapper : "rounded-full")}>
<ItemSurface className={itemClass(classes.startItem)}>
<motion.button
type="button"
whileTap={{ scale: 0.95 }}
className={classes.button}
onClick={() => {
play("click");
tg.hapticFeedback.click();
onChange?.(1);
}}
disabled={isAtStart}
>
<StartIcon className="w-5 h-4.5" />
</motion.button>
</ItemSurface>
<ItemSurface className={itemClass()}>
<motion.button
type="button"
whileTap={{ scale: 0.95 }}
className={classes.button}
onClick={() => {
play("click");
tg.hapticFeedback.click();
onChange?.(value - 1);
}}
disabled={isAtStart}
>
<BackIcon className="w-4 h-5" />
</motion.button>
</ItemSurface>
<ItemSurface className={itemClass(classes.state)}>
{value} {t("pagination.of")} {total}
</ItemSurface>
<ItemSurface className={itemClass()}>
<motion.button
type="button"
whileTap={{ scale: 0.95 }}
className={classes.button}
onClick={() => {
play("click");
tg.hapticFeedback.click();
onChange?.(value + 1);
}}
disabled={isAtEnd}
>
<BackIcon className={clsx("w-4 h-5", classes.mirrored)} />
</motion.button>
</ItemSurface>
<ItemSurface className={itemClass(classes.endItem)}>
<motion.button
type="button"
whileTap={{ scale: 0.95 }}
className={classes.button}
onClick={() => {
play("click");
tg.hapticFeedback.click();
onChange?.(total);
}}
disabled={isAtEnd}
>
<StartIcon className={clsx("w-5 h-4.5", classes.mirrored)} />
</motion.button>
</ItemSurface>
</Wrapper>
);
}

View File

@@ -0,0 +1,3 @@
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.4341 0.835938C13.8355 0.426426 13.086 0.362755 12.4019 0.804688V0.805664L1.61182 7.77344C0.889589 8.23986 0.50054 9.09777 0.500488 10C0.500488 10.9023 0.889512 11.7611 1.61182 12.2275L12.4019 19.1953C13.0867 19.6375 13.8357 19.5743 14.4341 19.165C15.0406 18.7501 15.5005 17.9663 15.5005 16.9668V3.0332C15.5005 2.03451 15.0407 1.2511 14.4341 0.835938Z" fill="#774923" stroke="#3F2814"/>
</svg>

After

Width:  |  Height:  |  Size: 502 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.36816 0.5H1.89453C1.71624 0.500029 1.53882 0.537515 1.37207 0.611328C1.20516 0.685217 1.05109 0.794386 0.919922 0.93457C0.788697 1.07482 0.683015 1.2432 0.610352 1.43066C0.537677 1.61818 0.5 1.82068 0.5 2.02539V15.9746C0.5 16.1793 0.537677 16.3818 0.610352 16.5693C0.683014 16.7568 0.788695 16.9252 0.919922 17.0654C1.05109 17.2056 1.20517 17.3148 1.37207 17.3887C1.53882 17.4625 1.71624 17.5 1.89453 17.5H3.36816C3.72683 17.5 4.07828 17.348 4.34277 17.0654C4.60835 16.7816 4.7627 16.3894 4.7627 15.9746V2.02539C4.7627 1.6106 4.60835 1.2184 4.34277 0.93457C4.07828 0.65202 3.72683 0.5 3.36816 0.5ZM18.9141 0.500977C18.8201 0.507575 18.7262 0.541698 18.6436 0.603516L8.11719 8.47852C8.04438 8.53306 7.98186 8.6082 7.9375 8.69922C7.89307 8.79047 7.86914 8.89398 7.86914 9C7.86914 9.10602 7.89307 9.20953 7.9375 9.30078C7.98186 9.3918 8.04438 9.46694 8.11719 9.52148L18.6436 17.3955C18.7262 17.4573 18.8201 17.4915 18.9141 17.498C19.0078 17.5045 19.1032 17.4841 19.1904 17.4355C19.2782 17.3867 19.3558 17.31 19.4121 17.2109C19.4684 17.1118 19.4999 16.9956 19.5 16.875V1.125L19.4941 1.03516C19.4827 0.946822 19.4544 0.862538 19.4121 0.788086C19.3559 0.689138 19.278 0.613272 19.1904 0.564453C19.1031 0.515936 19.0079 0.494413 18.9141 0.500977Z" fill="#774923" stroke="#3F2814"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,23 @@
import { motion, type SVGMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
type Props = Omit<SVGMotionProps<SVGElement>, "className"> & {
className?: ClassValue;
};
export default function BackIcon(props: Props) {
return (
<motion.svg
viewBox="0 0 16 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
className={clsx(props.className)}
>
<path
d="M14.4341 0.835938C13.8355 0.426426 13.086 0.362755 12.4019 0.804688V0.805664L1.61182 7.77344C0.889589 8.23986 0.50054 9.09777 0.500488 10C0.500488 10.9023 0.889512 11.7611 1.61182 12.2275L12.4019 19.1953C13.0867 19.6375 13.8357 19.5743 14.4341 19.165C15.0406 18.7501 15.5005 17.9663 15.5005 16.9668V3.0332C15.5005 2.03451 15.0407 1.2511 14.4341 0.835938Z"
style={{ fill: "var(--icon-fill, #774923)", stroke: "var(--icon-stroke, #3F2814)" }}
/>
</motion.svg>
);
}

View File

@@ -0,0 +1,23 @@
import { motion, type SVGMotionProps } from "motion/react";
import clsx, { type ClassValue } from "clsx";
type Props = Omit<SVGMotionProps<SVGElement>, "className"> & {
className?: ClassValue;
};
export default function StartIcon(props: Props) {
return (
<motion.svg
viewBox="0 0 20 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
className={clsx(props.className)}
>
<path
d="M3.36816 0.5H1.89453C1.71624 0.500029 1.53882 0.537515 1.37207 0.611328C1.20516 0.685217 1.05109 0.794386 0.919922 0.93457C0.788697 1.07482 0.683015 1.2432 0.610352 1.43066C0.537677 1.61818 0.5 1.82068 0.5 2.02539V15.9746C0.5 16.1793 0.537677 16.3818 0.610352 16.5693C0.683014 16.7568 0.788695 16.9252 0.919922 17.0654C1.05109 17.2056 1.20517 17.3148 1.37207 17.3887C1.53882 17.4625 1.71624 17.5 1.89453 17.5H3.36816C3.72683 17.5 4.07828 17.348 4.34277 17.0654C4.60835 16.7816 4.7627 16.3894 4.7627 15.9746V2.02539C4.7627 1.6106 4.60835 1.2184 4.34277 0.93457C4.07828 0.65202 3.72683 0.5 3.36816 0.5ZM18.9141 0.500977C18.8201 0.507575 18.7262 0.541698 18.6436 0.603516L8.11719 8.47852C8.04438 8.53306 7.98186 8.6082 7.9375 8.69922C7.89307 8.79047 7.86914 8.89398 7.86914 9C7.86914 9.10602 7.89307 9.20953 7.9375 9.30078C7.98186 9.3918 8.04438 9.46694 8.11719 9.52148L18.6436 17.3955C18.7262 17.4573 18.8201 17.4915 18.9141 17.498C19.0078 17.5045 19.1032 17.4841 19.1904 17.4355C19.2782 17.3867 19.3558 17.31 19.4121 17.2109C19.4684 17.1118 19.4999 16.9956 19.5 16.875V1.125L19.4941 1.03516C19.4827 0.946822 19.4544 0.862538 19.4121 0.788086C19.3559 0.689138 19.278 0.613272 19.1904 0.564453C19.1031 0.515936 19.0079 0.494413 18.9141 0.500977Z"
style={{ fill: "var(--icon-fill, #774923)", stroke: "var(--icon-stroke, #3F2814)" }}
/>
</motion.svg>
);
}

View File

@@ -0,0 +1 @@
export { default } from "./Pagination";

View File

@@ -4,6 +4,8 @@ import DarkSurface from "@components/surface/DarkSurface";
import { motion, type HTMLMotionProps } from "motion/react";
import classes from "./TabSelector.module.css";
import { usePlaySound } from "@/audio";
import tg from "@/tg";
type Tab = {
key: string;
@@ -18,6 +20,8 @@ type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
};
export default function TabSelector({ tabs, value, onChange, className, ...props }: Props) {
const play = usePlaySound();
const selectedIndex = value != null ? tabs.findIndex((tab) => tab.key === value) : -1;
return (
@@ -29,7 +33,11 @@ export default function TabSelector({ tabs, value, onChange, className, ...props
type="button"
key={tab.key}
whileTap={{ scale: 0.95 }}
onClick={() => onChange?.(tab.key)}
onClick={() => {
play("click");
tg.hapticFeedback.click();
onChange?.(tab.key);
}}
className={classes.tab}
>
{tab.title}

View File

@@ -4,6 +4,8 @@ import { type ReactNode, useRef, useState, type ChangeEvent, useId } from "react
import KeyboardIcon from "@components/icons/KeyboardIcon";
import classes from "./NumberInput.module.css";
import { usePlaySound } from "@/audio";
import tg from "@/tg";
type Props = Omit<HTMLMotionProps<"input">, "className" | "type" | "onChange"> & {
className?: ClassValue;
@@ -29,6 +31,8 @@ export default function NumberInput({
onChange,
...props
}: Props) {
const play = usePlaySound();
const stableId = useId();
const inputRef = useRef<HTMLInputElement>(null);
const id = props.id ?? stableId;
@@ -66,7 +70,11 @@ export default function NumberInput({
<motion.button
whileTap={{ scale: 0.95 }}
className={classes.iconButton}
onClick={() => inputRef.current?.focus()}
onClick={() => {
play("click");
tg.hapticFeedback.click();
inputRef.current?.focus();
}}
>
<KeyboardIcon className={classes.icon} />
</motion.button>

View File

@@ -1,9 +1,11 @@
import clsx, { type ClassValue } from "clsx";
import { useTranslation } from "react-i18next";
import ContentSurface from "@components/surface/ContentSurface";
import LightSurface from "@components/surface/LightSurface";
import { motion, type HTMLMotionProps } from "motion/react";
import classes from "./SwitchInput.module.css";
import tg from "@/tg";
type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
value?: boolean | null;
@@ -12,6 +14,7 @@ type Props = Omit<HTMLMotionProps<"div">, "className" | "onChange"> & {
};
export default function SwitchInput({ value, onChange, className, ...props }: Props) {
const { t } = useTranslation();
const selectedIndex = value != null ? (value ? 0 : 1) : -1;
return (
@@ -19,24 +22,24 @@ export default function SwitchInput({ value, onChange, className, ...props }: Pr
{...props}
className={clsx(classes.container, className)}
whileTap={{ scale: 1.1 }}
onClick={() => {
tg.hapticFeedback.click();
onChange?.(!value);
}}
>
<div className={classes.optionsContainer}>
<div className={classes.options}>
<motion.button
type="button"
whileTap={{ scale: 0.95 }}
onClick={() => onChange?.(true)}
className={clsx(classes.option, value === true && classes.selected)}
>
on
{t("common.on")}
</motion.button>
<motion.button
type="button"
whileTap={{ scale: 0.95 }}
onClick={() => onChange?.(false)}
className={clsx(classes.option, value === false && classes.selected)}
>
off
{t("common.off")}
</motion.button>
</div>
{selectedIndex >= 0 && (

View File

@@ -0,0 +1,57 @@
import { createContext, useCallback, useContext, useRef, useState } from "react";
import type { ReactNode } from "react";
type LiftContextValue = {
liftedIds: Set<string>;
alwaysLiftedIds: Set<string>;
setLiftedIds: (ids: string[]) => void;
registerAlways: (id: string) => void;
unregisterAlways: (id: string) => void;
portalContainer: HTMLElement | null;
setPortalContainer: (el: HTMLElement | null) => void;
};
const LiftContext = createContext<LiftContextValue | null>(null);
export function useLift(): LiftContextValue {
const ctx = useContext(LiftContext);
if (!ctx) throw new Error("useLift must be used within LiftProvider");
return ctx;
}
export function LiftProvider({ children }: { children: ReactNode }) {
const [liftedIds, setLiftedIdsRaw] = useState<Set<string>>(new Set());
const [alwaysLiftedIds, setAlwaysLiftedIds] = useState<Set<string>>(new Set());
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
const alwaysRef = useRef<Set<string>>(new Set());
const setLiftedIds = useCallback((ids: string[]) => {
setLiftedIdsRaw(new Set(ids));
}, []);
const registerAlways = useCallback((id: string) => {
alwaysRef.current.add(id);
setAlwaysLiftedIds(new Set(alwaysRef.current));
}, []);
const unregisterAlways = useCallback((id: string) => {
alwaysRef.current.delete(id);
setAlwaysLiftedIds(new Set(alwaysRef.current));
}, []);
return (
<LiftContext
value={{
liftedIds,
alwaysLiftedIds,
setLiftedIds,
registerAlways,
unregisterAlways,
portalContainer,
setPortalContainer,
}}
>
{children}
</LiftContext>
);
}

View File

@@ -0,0 +1,8 @@
@layer base {
.liftLayer {
position: fixed;
inset: 0;
z-index: 103;
pointer-events: none;
}
}

View File

@@ -0,0 +1,16 @@
import { useCallback } from "react";
import { useLift } from "./LiftContext";
import classes from "./LiftLayer.module.css";
export function LiftLayer() {
const { setPortalContainer } = useLift();
const refCallback = useCallback(
(node: HTMLDivElement | null) => {
setPortalContainer(node);
},
[setPortalContainer],
);
return <div ref={refCallback} className={classes.liftLayer} />;
}

View File

@@ -0,0 +1,121 @@
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
import type { ReactNode } from "react";
import { createPortal } from "react-dom";
import { useLift } from "./LiftContext";
type LiftableProps = {
id?: string;
always?: boolean;
};
type Props = LiftableProps & {
children: ReactNode;
};
export function useLiftable<T extends Record<string, unknown> = Record<string, never>>(
render: (props: { isLifted: boolean } & T) => ReactNode,
options?: LiftableProps & T,
): { id: string; element: ReactNode } {
const autoId = useId();
const {
id: idProp,
always,
...extraProps
} = (options ?? {}) as LiftableProps & Record<string, unknown>;
const id = idProp ?? autoId;
const { liftedIds, alwaysLiftedIds, registerAlways, unregisterAlways } = useLift();
useEffect(() => {
if (always) {
registerAlways(id);
return () => unregisterAlways(id);
}
}, [always, id, registerAlways, unregisterAlways]);
const isLifted = liftedIds.has(id) || alwaysLiftedIds.has(id);
const element = (
<Liftable id={id}>{render({ isLifted, ...extraProps } as { isLifted: boolean } & T)}</Liftable>
);
return { id, element };
}
export function Liftable({ id: idProp, always, children }: Props) {
const autoId = useId();
const id = idProp ?? autoId;
const { liftedIds, alwaysLiftedIds, registerAlways, unregisterAlways, portalContainer } =
useLift();
const wrapperRef = useRef<HTMLDivElement>(null);
const [rect, setRect] = useState<DOMRect | null>(null);
useEffect(() => {
if (always) {
registerAlways(id);
return () => unregisterAlways(id);
}
}, [always, id, registerAlways, unregisterAlways]);
const isLifted = liftedIds.has(id) || alwaysLiftedIds.has(id);
useLayoutEffect(() => {
if (isLifted && wrapperRef.current) {
setRect(wrapperRef.current.getBoundingClientRect());
}
if (!isLifted) {
setRect(null);
}
}, [isLifted]);
// Re-measure on resize while lifted
useEffect(() => {
if (!isLifted || !wrapperRef.current) return;
const measure = () => {
if (wrapperRef.current) {
setRect(wrapperRef.current.getBoundingClientRect());
}
};
const observer = new ResizeObserver(measure);
observer.observe(wrapperRef.current);
window.addEventListener("resize", measure);
return () => {
observer.disconnect();
window.removeEventListener("resize", measure);
};
}, [isLifted]);
// When lifted and we have measurements + portal target, render in portal
if (isLifted && rect && portalContainer) {
return (
<>
{/* Placeholder preserves layout space */}
<div
ref={wrapperRef}
style={{ width: rect.width, height: rect.height, visibility: "hidden" }}
/>
{/* Portal children above blur */}
{createPortal(
<div
style={{
position: "fixed",
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
pointerEvents: "auto",
}}
>
{children}
</div>,
portalContainer,
)}
</>
);
}
// Normal inline rendering
return <div ref={wrapperRef}>{children}</div>;
}

View File

@@ -0,0 +1,3 @@
export { LiftProvider, useLift } from "./LiftContext";
export { LiftLayer } from "./LiftLayer";
export { Liftable, useLiftable } from "./Liftable";

View File

@@ -1,11 +1,27 @@
@layer base {
.overlay {
position: fixed;
inset: 0;
z-index: 100;
}
.modalContainer {
width: 100%;
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100;
z-index: 102;
}
.modalInner {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 320px;
}
.modal {
@@ -13,7 +29,19 @@
flex-direction: column;
gap: 6px;
width: 100%;
max-width: 320px;
max-height: calc(
var(--safe-area-height) - var(--header-total) - var(--navigation-total) - 70px
);
overflow: auto;
padding: 13px;
}
.title {
color: #ffffff;
-webkit-text-stroke: 0.7px #331b01;
font-weight: 700;
font-size: 24px;
margin-bottom: 12px;
text-align: center;
}
}

View File

@@ -1,36 +1,85 @@
import { useEffect, useRef } from "react";
import type { ReactNode } from "react";
import { AnimatePresence, motion } from "motion/react";
import SectionSurface from "../../surface/SectionSurface/SectionSurface";
import SectionSurface from "@components/surface/SectionSurface";
import { useLift } from "@components/lift";
import classes from "./Modal.module.css";
import { clsx, type ClassValue } from "clsx";
type Props = {
open: boolean;
children: ReactNode;
onClose: () => void;
liftIds?: string[];
title?: ReactNode;
className?: ClassValue;
};
export default function Modal({ open, children, onClose }: Props) {
export default function Modal({ open, children, onClose, liftIds, title, className }: Props) {
const { setLiftedIds } = useLift();
const prevLiftIdsRef = useRef<string>("");
useEffect(() => {
const key = liftIds?.join(",") ?? "";
if (open && key) {
if (key !== prevLiftIdsRef.current) {
prevLiftIdsRef.current = key;
setLiftedIds(liftIds!);
}
}
if (!open && prevLiftIdsRef.current) {
prevLiftIdsRef.current = "";
setLiftedIds([]);
}
}, [open, liftIds, setLiftedIds]);
useEffect(() => {
return () => {
if (prevLiftIdsRef.current) {
setLiftedIds([]);
}
};
}, [setLiftedIds]);
return (
<AnimatePresence>
{open && (
<motion.div
className={classes.overlay}
initial={{ backdropFilter: "blur(0px)" }}
animate={{ backdropFilter: "blur(8px)" }}
exit={{ backdropFilter: "blur(0px)" }}
transition={{ duration: 0.2 }}
onClick={onClose}
>
<SectionSurface
className={classes.modal}
exit={{ scale: 0 }}
transition={{ duration: 0.2, type: "spring" }}
onClick={(e) => e.stopPropagation()}
<>
<motion.div
className={classes.overlay}
initial={{ backdropFilter: "blur(0px)" }}
animate={{ backdropFilter: "blur(8px)" }}
exit={{ backdropFilter: "blur(0px)" }}
transition={{ duration: 0.2 }}
onClick={onClose}
/>
<motion.div
className={classes.modalContainer}
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={onClose}
>
{children}
</SectionSurface>
</motion.div>
{/* oxlint-disable-next-line jsx_a11y/no-static-element-interactions*/}
<div
className={classes.modalInner}
// oxlint-disable-next-line jsx_a11y/click-events-have-key-events
onClick={(e) => e.stopPropagation()}
>
{title && <div className={classes.title}>{title}</div>}
<SectionSurface
className={clsx(classes.modal, className)}
exit={{ scale: 0 }}
transition={{ duration: 0.2, type: "spring" }}
>
{children}
</SectionSurface>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);

View File

@@ -1,5 +1,3 @@
@reference "@/index.css";
@layer base {
.blueSectionSurface {
background: linear-gradient(180deg, #278789 0%, #206f66 100%);