feat: add header
This commit is contained in:
@@ -7,5 +7,15 @@
|
||||
background-image: url("./assets/main-bg.svg");
|
||||
background-size: auto 101%;
|
||||
background-position: center;
|
||||
|
||||
.main {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
--padding-x: 16px;
|
||||
padding: calc(var(--header-total) + var(--padding-x)) var(--safe-right)
|
||||
calc(var(--navigation-total) + var(--padding-x)) var(--safe-left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ export default function RootLayout({ children, hideControls }: Props) {
|
||||
<div className={classes.rootLayout}>
|
||||
{!hideControls && <Header />}
|
||||
|
||||
<main className="h-full overflow-y-auto overflow-x-hidden pt-[calc(var(--header-total)+16px)] pb-[calc(var(--navigation-total)+16px)]">
|
||||
{children}
|
||||
</main>
|
||||
<main className={classes.main}>{children}</main>
|
||||
|
||||
{!hideControls && <Navigation />}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
:root {
|
||||
--header-height: 90px;
|
||||
--header-padding: calc(
|
||||
var(--tg-viewport-safe-area-inset-top, 0px) +
|
||||
var(--tg-viewport-content-safe-area-inset-top, 0px)
|
||||
);
|
||||
--header-padding: var(--safe-top);
|
||||
--header-total: calc(var(--header-height) + var(--header-padding));
|
||||
}
|
||||
|
||||
@@ -14,8 +11,29 @@
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
height: var(--header-total);
|
||||
|
||||
padding-top: var(--header-padding);
|
||||
background: rgb(59 130 246 / 0.3);
|
||||
padding-left: var(--safe-left);
|
||||
padding-right: var(--safe-right);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
bottom: -20px;
|
||||
backdrop-filter: blur(10px);
|
||||
mask-image: linear-gradient(to bottom, black 0%, black calc(100% - 50px), transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
height: var(--header-height);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import classes from "./Header.module.css";
|
||||
import Profile from "./components/Profile";
|
||||
import Settings from "./components/Settings";
|
||||
|
||||
export default function Header() {
|
||||
return <header className={classes.header} />;
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<div className={classes.content}>
|
||||
<Profile />
|
||||
<Settings />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
@layer base {
|
||||
.profile {
|
||||
pointer-events: auto;
|
||||
width: 200px;
|
||||
aspect-ratio: 2.18;
|
||||
background-image: url("./assets/profile-bg.svg");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.left {
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rightTop {
|
||||
}
|
||||
|
||||
.rightBottom {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import classes from "./Profile.module.css";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
export default function Profile() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ scale: 0, x: "-100vw" }}
|
||||
animate={{ scale: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, type: "spring", bounce: 0.2 }}
|
||||
className={classes.profile}
|
||||
>
|
||||
<div className={classes.left} />
|
||||
<div className={classes.right}>
|
||||
<div className={classes.rightTop} />
|
||||
<div className={classes.rightBottom} />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
@@ -0,0 +1 @@
|
||||
export { default } from "./Profile";
|
||||
@@ -0,0 +1,35 @@
|
||||
@layer base {
|
||||
.settings {
|
||||
margin-left: auto;
|
||||
width: 80px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: url("./assets/settings-bg.svg") center / contain no-repeat;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 7px;
|
||||
border-radius: 5px;
|
||||
background-color: #563417;
|
||||
background-image: linear-gradient(180deg, #ebcb71 0%, #aa6831 100%);
|
||||
box-shadow:
|
||||
0px 1px 1px 0px #00000073,
|
||||
0px 1px 0px 0px #ffefd1 inset,
|
||||
0px -1px 0px 0px #0000008c inset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useState } from "react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import tg from "@/tg";
|
||||
|
||||
import classes from "./Settings.module.css";
|
||||
|
||||
export default function Settings() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const toggle = () => {
|
||||
tg.hapticFeedback.click();
|
||||
setIsOpen((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
initial={{ scale: 0, x: "50%", y: "-50%" }}
|
||||
animate={{ scale: 1, x: 0, y: 0 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ duration: 0.1, type: "spring" }}
|
||||
className={classes.settings}
|
||||
onClick={toggle}
|
||||
>
|
||||
<div className={classes.bars}>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.span
|
||||
key={i}
|
||||
className={classes.bar}
|
||||
initial={{ scale: 0 }}
|
||||
animate={
|
||||
isOpen
|
||||
? i === 0
|
||||
? { scale: 1, rotate: 45, y: 12 }
|
||||
: i === 1
|
||||
? { scale: 1, opacity: 0, scaleX: 0 }
|
||||
: { scale: 1, rotate: -45, y: -12 }
|
||||
: { scale: 1, rotate: 0, y: 0, opacity: 1, scaleX: 1 }
|
||||
}
|
||||
transition={{
|
||||
scale: { delay: 0.05 * i + 0.1, duration: 0.15, type: "spring" },
|
||||
default: { duration: 0.1 },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1 @@
|
||||
export { default } from "./Settings";
|
||||
@@ -1,6 +1,6 @@
|
||||
:root {
|
||||
--navigation-height: 74px;
|
||||
--navigation-padding: var(--tg-viewport-safe-area-inset-bottom, 0px);
|
||||
--navigation-padding: var(--safe-bottom);
|
||||
--navigation-total: calc(var(--navigation-height) + var(--navigation-padding));
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
max-width: 500px;
|
||||
padding-left: var(--safe-left);
|
||||
padding-right: var(--safe-right);
|
||||
margin: auto;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
@@ -84,9 +86,10 @@
|
||||
border-radius: 27px 0 0 27px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import menuIcon from "./assets/menu.svg";
|
||||
import rouletteIcon from "./assets/roulette.svg";
|
||||
import tasksIcon from "./assets/tasks.svg";
|
||||
import earningsIcon from "./assets/earnings.svg";
|
||||
import tg from "@/tg";
|
||||
import tg, { useTelegramViewportValue } from "@/tg";
|
||||
|
||||
const ANIMATION_DURATION = 0.2;
|
||||
const SPRING_ANIMATION = {
|
||||
@@ -62,33 +62,34 @@ type BarProps = {
|
||||
function NavBar({ labelKey, icon, active, entranceDelay, onClick }: BarProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isInitial = useRef(true);
|
||||
useEffect(() => {
|
||||
isInitial.current = false;
|
||||
}, []);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
const height = active ? ACTIVE_BAR_HEIGHT : BAR_HEIGHT;
|
||||
const safeAreaBottom = useTelegramViewportValue("safeAreaInsetBottom");
|
||||
const safeContentBottom = useTelegramViewportValue("contentSafeAreaInsetBottom");
|
||||
const height = (active ? ACTIVE_BAR_HEIGHT : BAR_HEIGHT) + safeAreaBottom + safeContentBottom;
|
||||
const offscreenOffset = height * 0.3;
|
||||
return (
|
||||
<GlassSurface
|
||||
className={[classes.bar, active && classes.active]}
|
||||
variants={{
|
||||
hidden: {
|
||||
translateY: ACTIVE_BAR_HEIGHT + OFFSCREEN_BAR_OFFSET,
|
||||
translateY: height + offscreenOffset,
|
||||
height,
|
||||
},
|
||||
visible: {
|
||||
translateY: OFFSCREEN_BAR_OFFSET,
|
||||
height: height + OFFSCREEN_BAR_OFFSET,
|
||||
translateY: offscreenOffset,
|
||||
height: height + offscreenOffset,
|
||||
transition: { ...SPRING_ANIMATION, delay: entranceDelay },
|
||||
},
|
||||
ready: {
|
||||
translateY: OFFSCREEN_BAR_OFFSET,
|
||||
height: height + OFFSCREEN_BAR_OFFSET,
|
||||
translateY: offscreenOffset,
|
||||
height: height + offscreenOffset,
|
||||
transition: SPRING_ANIMATION,
|
||||
},
|
||||
}}
|
||||
onAnimationComplete={(variant) => variant === "visible" && setIsReady(true)}
|
||||
initial="hidden"
|
||||
animate={isInitial.current ? "visible" : "ready"}
|
||||
animate={isReady ? "ready" : "visible"}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={onClick}
|
||||
>
|
||||
@@ -110,34 +111,37 @@ type MenuBarProps = {
|
||||
|
||||
const MENU_BAR_WIDTH = 94;
|
||||
const ACTIVE_MENU_BAR_WIDTH = 104;
|
||||
const OFFSCREEN_MENU_BAR_OFFSET = 20;
|
||||
|
||||
function MenuBar({ labelKey, icon, delay, active, onClick }: MenuBarProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
const width = active ? ACTIVE_MENU_BAR_WIDTH : MENU_BAR_WIDTH;
|
||||
const safeAreaRight = useTelegramViewportValue("safeAreaInsetRight");
|
||||
const safeContentRight = useTelegramViewportValue("contentSafeAreaInsetRight");
|
||||
const width =
|
||||
(active ? ACTIVE_MENU_BAR_WIDTH : MENU_BAR_WIDTH) + safeAreaRight + safeContentRight;
|
||||
const offscreenOffset = width * 0.3;
|
||||
return (
|
||||
<GlassSurface
|
||||
className={[classes.menuBar, active && classes.active]}
|
||||
variants={{
|
||||
hidden: {
|
||||
translateX: width + OFFSCREEN_MENU_BAR_OFFSET,
|
||||
translateX: width + offscreenOffset,
|
||||
width,
|
||||
},
|
||||
visible: {
|
||||
translateX: OFFSCREEN_MENU_BAR_OFFSET,
|
||||
translateX: offscreenOffset,
|
||||
transition: { ...SPRING_ANIMATION, delay: delay },
|
||||
width,
|
||||
},
|
||||
ready: {
|
||||
translateX: OFFSCREEN_MENU_BAR_OFFSET,
|
||||
translateX: offscreenOffset,
|
||||
transition: SPRING_ANIMATION,
|
||||
width,
|
||||
},
|
||||
exit: {
|
||||
translateX: width + OFFSCREEN_MENU_BAR_OFFSET + 10,
|
||||
translateX: width + offscreenOffset,
|
||||
transition: { ...SPRING_ANIMATION, delay: delay },
|
||||
width,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user