feat: add header
This commit is contained in:
@@ -1,29 +1,11 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
.overlay {
|
.content {
|
||||||
position: fixed;
|
border-radius: 22px;
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
.description {
|
||||||
display: flex;
|
padding: 12px;
|
||||||
flex-direction: column;
|
border-radius: 18px;
|
||||||
gap: 6px;
|
text-align: center;
|
||||||
width: 100%;
|
|
||||||
max-width: 320px;
|
|
||||||
padding: 13px;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
border-radius: 22px;
|
|
||||||
|
|
||||||
.description {
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 18px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { AnimatePresence, motion } from "motion/react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import SectionSurface from "../../surface/SectionSurface/SectionSurface";
|
import Modal from "../Modal/Modal";
|
||||||
import ContentSurface from "../../surface/ContentSurface/ContentSurface";
|
import ContentSurface from "../../surface/ContentSurface/ContentSurface";
|
||||||
import LightSurface from "../../surface/LightSurface/LightSurface";
|
import LightSurface from "../../surface/LightSurface/LightSurface";
|
||||||
import Button from "../../atoms/Button/Button";
|
import Button from "../../atoms/Button/Button";
|
||||||
@@ -27,36 +26,20 @@ export default function ActionModal({ open, description, onClose, onConfirm, con
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<Modal open={open} onClose={onClose}>
|
||||||
{open && (
|
<ContentSurface className={classes.content}>
|
||||||
<motion.div
|
<LightSurface className={classes.description}>{description}</LightSurface>
|
||||||
className={classes.overlay}
|
</ContentSurface>
|
||||||
initial={{ backdropFilter: "blur(0px)" }}
|
<div className={classes.buttons}>
|
||||||
animate={{ backdropFilter: "blur(8px)" }}
|
<Button variant="blue" onClick={onClose} className={classes.button}>
|
||||||
exit={{ backdropFilter: "blur(0px)" }}
|
{t("actionModal.close")}
|
||||||
transition={{ duration: 0.2 }}
|
</Button>
|
||||||
>
|
{onConfirm != null && (
|
||||||
<SectionSurface
|
<Button variant="green" onClick={onConfirm} className={classes.button}>
|
||||||
className={classes.modal}
|
{confirmText}
|
||||||
exit={{ scale: 0 }}
|
</Button>
|
||||||
transition={{ duration: 0.2, type: "spring" }}
|
)}
|
||||||
>
|
</div>
|
||||||
<ContentSurface className={classes.content}>
|
</Modal>
|
||||||
<LightSurface className={classes.description}>{description}</LightSurface>
|
|
||||||
</ContentSurface>
|
|
||||||
<div className={classes.buttons}>
|
|
||||||
<Button variant="blue" onClick={onClose} className={classes.button}>
|
|
||||||
{t("actionModal.close")}
|
|
||||||
</Button>
|
|
||||||
{onConfirm != null && (
|
|
||||||
<Button variant="green" onClick={onConfirm} className={classes.button}>
|
|
||||||
{confirmText}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SectionSurface>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/components/modals/Modal/Modal.module.css
Normal file
19
src/components/modals/Modal/Modal.module.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
@layer base {
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
padding: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/components/modals/Modal/Modal.tsx
Normal file
37
src/components/modals/Modal/Modal.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
|
||||||
|
import SectionSurface from "../../surface/SectionSurface/SectionSurface";
|
||||||
|
import classes from "./Modal.module.css";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Modal({ open, children, onClose }: Props) {
|
||||||
|
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()}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SectionSurface>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/modals/Modal/index.ts
Normal file
1
src/components/modals/Modal/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./Modal";
|
||||||
@@ -7,5 +7,15 @@
|
|||||||
background-image: url("./assets/main-bg.svg");
|
background-image: url("./assets/main-bg.svg");
|
||||||
background-size: auto 101%;
|
background-size: auto 101%;
|
||||||
background-position: center;
|
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}>
|
<div className={classes.rootLayout}>
|
||||||
{!hideControls && <Header />}
|
{!hideControls && <Header />}
|
||||||
|
|
||||||
<main className="h-full overflow-y-auto overflow-x-hidden pt-[calc(var(--header-total)+16px)] pb-[calc(var(--navigation-total)+16px)]">
|
<main className={classes.main}>{children}</main>
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{!hideControls && <Navigation />}
|
{!hideControls && <Navigation />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
:root {
|
:root {
|
||||||
--header-height: 90px;
|
--header-height: 90px;
|
||||||
--header-padding: calc(
|
--header-padding: var(--safe-top);
|
||||||
var(--tg-viewport-safe-area-inset-top, 0px) +
|
|
||||||
var(--tg-viewport-content-safe-area-inset-top, 0px)
|
|
||||||
);
|
|
||||||
--header-total: calc(var(--header-height) + var(--header-padding));
|
--header-total: calc(var(--header-height) + var(--header-padding));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,8 +11,29 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
height: var(--header-total);
|
|
||||||
padding-top: var(--header-padding);
|
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 classes from "./Header.module.css";
|
||||||
|
import Profile from "./components/Profile";
|
||||||
|
import Settings from "./components/Settings";
|
||||||
|
|
||||||
export default function Header() {
|
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 {
|
:root {
|
||||||
--navigation-height: 74px;
|
--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));
|
--navigation-total: calc(var(--navigation-height) + var(--navigation-padding));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
|
padding-left: var(--safe-left);
|
||||||
|
padding-right: var(--safe-right);
|
||||||
margin: auto;
|
margin: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
@@ -84,9 +86,10 @@
|
|||||||
border-radius: 27px 0 0 27px;
|
border-radius: 27px 0 0 27px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
|
padding-left: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import menuIcon from "./assets/menu.svg";
|
|||||||
import rouletteIcon from "./assets/roulette.svg";
|
import rouletteIcon from "./assets/roulette.svg";
|
||||||
import tasksIcon from "./assets/tasks.svg";
|
import tasksIcon from "./assets/tasks.svg";
|
||||||
import earningsIcon from "./assets/earnings.svg";
|
import earningsIcon from "./assets/earnings.svg";
|
||||||
import tg from "@/tg";
|
import tg, { useTelegramViewportValue } from "@/tg";
|
||||||
|
|
||||||
const ANIMATION_DURATION = 0.2;
|
const ANIMATION_DURATION = 0.2;
|
||||||
const SPRING_ANIMATION = {
|
const SPRING_ANIMATION = {
|
||||||
@@ -62,33 +62,34 @@ type BarProps = {
|
|||||||
function NavBar({ labelKey, icon, active, entranceDelay, onClick }: BarProps) {
|
function NavBar({ labelKey, icon, active, entranceDelay, onClick }: BarProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isInitial = useRef(true);
|
const [isReady, setIsReady] = useState(false);
|
||||||
useEffect(() => {
|
|
||||||
isInitial.current = 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 (
|
return (
|
||||||
<GlassSurface
|
<GlassSurface
|
||||||
className={[classes.bar, active && classes.active]}
|
className={[classes.bar, active && classes.active]}
|
||||||
variants={{
|
variants={{
|
||||||
hidden: {
|
hidden: {
|
||||||
translateY: ACTIVE_BAR_HEIGHT + OFFSCREEN_BAR_OFFSET,
|
translateY: height + offscreenOffset,
|
||||||
height,
|
height,
|
||||||
},
|
},
|
||||||
visible: {
|
visible: {
|
||||||
translateY: OFFSCREEN_BAR_OFFSET,
|
translateY: offscreenOffset,
|
||||||
height: height + OFFSCREEN_BAR_OFFSET,
|
height: height + offscreenOffset,
|
||||||
transition: { ...SPRING_ANIMATION, delay: entranceDelay },
|
transition: { ...SPRING_ANIMATION, delay: entranceDelay },
|
||||||
},
|
},
|
||||||
ready: {
|
ready: {
|
||||||
translateY: OFFSCREEN_BAR_OFFSET,
|
translateY: offscreenOffset,
|
||||||
height: height + OFFSCREEN_BAR_OFFSET,
|
height: height + offscreenOffset,
|
||||||
transition: SPRING_ANIMATION,
|
transition: SPRING_ANIMATION,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
onAnimationComplete={(variant) => variant === "visible" && setIsReady(true)}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate={isInitial.current ? "visible" : "ready"}
|
animate={isReady ? "ready" : "visible"}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
@@ -110,34 +111,37 @@ type MenuBarProps = {
|
|||||||
|
|
||||||
const MENU_BAR_WIDTH = 94;
|
const MENU_BAR_WIDTH = 94;
|
||||||
const ACTIVE_MENU_BAR_WIDTH = 104;
|
const ACTIVE_MENU_BAR_WIDTH = 104;
|
||||||
const OFFSCREEN_MENU_BAR_OFFSET = 20;
|
|
||||||
|
|
||||||
function MenuBar({ labelKey, icon, delay, active, onClick }: MenuBarProps) {
|
function MenuBar({ labelKey, icon, delay, active, onClick }: MenuBarProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [isReady, setIsReady] = useState(false);
|
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 (
|
return (
|
||||||
<GlassSurface
|
<GlassSurface
|
||||||
className={[classes.menuBar, active && classes.active]}
|
className={[classes.menuBar, active && classes.active]}
|
||||||
variants={{
|
variants={{
|
||||||
hidden: {
|
hidden: {
|
||||||
translateX: width + OFFSCREEN_MENU_BAR_OFFSET,
|
translateX: width + offscreenOffset,
|
||||||
width,
|
width,
|
||||||
},
|
},
|
||||||
visible: {
|
visible: {
|
||||||
translateX: OFFSCREEN_MENU_BAR_OFFSET,
|
translateX: offscreenOffset,
|
||||||
transition: { ...SPRING_ANIMATION, delay: delay },
|
transition: { ...SPRING_ANIMATION, delay: delay },
|
||||||
width,
|
width,
|
||||||
},
|
},
|
||||||
ready: {
|
ready: {
|
||||||
translateX: OFFSCREEN_MENU_BAR_OFFSET,
|
translateX: offscreenOffset,
|
||||||
transition: SPRING_ANIMATION,
|
transition: SPRING_ANIMATION,
|
||||||
width,
|
width,
|
||||||
},
|
},
|
||||||
exit: {
|
exit: {
|
||||||
translateX: width + OFFSCREEN_MENU_BAR_OFFSET + 10,
|
translateX: width + offscreenOffset,
|
||||||
transition: { ...SPRING_ANIMATION, delay: delay },
|
transition: { ...SPRING_ANIMATION, delay: delay },
|
||||||
width,
|
width,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,15 +5,42 @@
|
|||||||
@theme {
|
@theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--safe-area-height: var(--tg-viewport-stable-height, 100dvh);
|
||||||
|
--safe-area-width: var(--tg-viewport-width, 100dvw);
|
||||||
|
|
||||||
|
--safe-area-top: var(--tg-viewport-safe-area-inset-top, 0px);
|
||||||
|
--safe-content-top: var(--tg-viewport-content-safe-area-inset-top, 0px);
|
||||||
|
--safe-top: calc(var(--safe-area-top) + var(--safe-content-top));
|
||||||
|
|
||||||
|
--safe-area-right: var(--tg-viewport-safe-area-inset-right, 0px);
|
||||||
|
--safe-content-right: var(--tg-viewport-content-safe-area-inset-right, 0px);
|
||||||
|
--safe-right: calc(var(--safe-area-right) + var(--safe-content-right));
|
||||||
|
|
||||||
|
--safe-area-bottom: var(--tg-viewport-safe-area-inset-bottom, 0px);
|
||||||
|
--safe-content-bottom: var(--tg-viewport-content-safe-area-inset-bottom, 0px);
|
||||||
|
--safe-bottom: calc(var(--safe-area-bottom) + var(--safe-content-bottom));
|
||||||
|
|
||||||
|
--safe-area-left: var(--tg-viewport-safe-area-inset-left, 0px);
|
||||||
|
--safe-content-left: var(--tg-viewport-content-safe-area-inset-left, 0px);
|
||||||
|
--safe-left: calc(var(--safe-area-left) + var(--safe-content-left));
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
@apply w-dvw h-dvh bg-top-left bg-green-300;
|
background: #9ebf3e;
|
||||||
|
height: var(--safe-area-height);
|
||||||
|
width: var(--safe-area-width);
|
||||||
font-family: "BalsamiqSans", sans-serif;
|
font-family: "BalsamiqSans", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
@apply w-full h-full max-w-150 m-auto overflow-hidden relative;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|||||||
100
src/tg/index.ts
100
src/tg/index.ts
@@ -1,4 +1,5 @@
|
|||||||
import * as tg from "@tma.js/sdk-react";
|
import * as tg from "@tma.js/sdk-react";
|
||||||
|
import { useSignal } from "@tma.js/sdk-react";
|
||||||
|
|
||||||
export const STORAGE_KEYS = {
|
export const STORAGE_KEYS = {
|
||||||
authToken: "authToken",
|
authToken: "authToken",
|
||||||
@@ -37,6 +38,9 @@ const fallbackImplementation = <
|
|||||||
: Awaited<Result>;
|
: Awaited<Result>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockCssVar = (key: string, px: number) =>
|
||||||
|
document.documentElement.style.setProperty(key, `${px}px`);
|
||||||
|
|
||||||
const externalTgApi = {
|
const externalTgApi = {
|
||||||
init: () => {
|
init: () => {
|
||||||
tg.setDebug(import.meta.env.DEV);
|
tg.setDebug(import.meta.env.DEV);
|
||||||
@@ -124,6 +128,20 @@ const externalTgApi = {
|
|||||||
console.log("TMA closing behavior initialized");
|
console.log("TMA closing behavior initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
mockCssVar("--tg-viewport-content-safe-area-inset-top", 10);
|
||||||
|
mockCssVar("--tg-viewport-safe-area-inset-top", 10);
|
||||||
|
|
||||||
|
mockCssVar("--tg-viewport-content-safe-area-inset-bottom", 10);
|
||||||
|
mockCssVar("--tg-viewport-safe-area-inset-bottom", 20);
|
||||||
|
|
||||||
|
mockCssVar("--tg-viewport-content-safe-area-inset-left", 10);
|
||||||
|
mockCssVar("--tg-viewport-safe-area-inset-left", 10);
|
||||||
|
|
||||||
|
mockCssVar("--tg-viewport-content-safe-area-inset-right", 10);
|
||||||
|
mockCssVar("--tg-viewport-safe-area-inset-right", 10);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
import.meta.env.DEV ? "TMA Debug mode in Web initialized" : "Telegram Mini App initialized",
|
import.meta.env.DEV ? "TMA Debug mode in Web initialized" : "Telegram Mini App initialized",
|
||||||
);
|
);
|
||||||
@@ -140,6 +158,59 @@ const externalTgApi = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
initData: tg.initData,
|
initData: tg.initData,
|
||||||
|
viewport: tg.viewport,
|
||||||
|
viewportCss: {
|
||||||
|
stableHeight: () =>
|
||||||
|
parseFloat(document.documentElement.style.getPropertyValue("--tg-viewport-stable-height")),
|
||||||
|
|
||||||
|
height: () =>
|
||||||
|
parseFloat(document.documentElement.style.getPropertyValue("--tg-viewport-height")),
|
||||||
|
width: () => parseFloat(document.documentElement.style.getPropertyValue("--tg-viewport-width")),
|
||||||
|
|
||||||
|
safeAreaInsetTop: () =>
|
||||||
|
parseFloat(
|
||||||
|
document.documentElement.style.getPropertyValue("--tg-viewport-safe-area-inset-top"),
|
||||||
|
),
|
||||||
|
contentSafeAreaInsetTop: () =>
|
||||||
|
parseFloat(
|
||||||
|
document.documentElement.style.getPropertyValue(
|
||||||
|
"--tg-viewport-content-safe-area-inset-top",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
safeAreaInsetBottom: () =>
|
||||||
|
parseFloat(
|
||||||
|
document.documentElement.style.getPropertyValue("--tg-viewport-safe-area-inset-bottom"),
|
||||||
|
),
|
||||||
|
contentSafeAreaInsetBottom: () =>
|
||||||
|
parseFloat(
|
||||||
|
document.documentElement.style.getPropertyValue(
|
||||||
|
"--tg-viewport-content-safe-area-inset-bottom",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
safeAreaInsetLeft: () =>
|
||||||
|
parseFloat(
|
||||||
|
document.documentElement.style.getPropertyValue("--tg-viewport-safe-area-inset-left"),
|
||||||
|
),
|
||||||
|
contentSafeAreaInsetLeft: () =>
|
||||||
|
parseFloat(
|
||||||
|
document.documentElement.style.getPropertyValue(
|
||||||
|
"--tg-viewport-content-safe-area-inset-left",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
safeAreaInsetRight: () =>
|
||||||
|
parseFloat(
|
||||||
|
document.documentElement.style.getPropertyValue("--tg-viewport-safe-area-inset-right"),
|
||||||
|
),
|
||||||
|
contentSafeAreaInsetRight: () =>
|
||||||
|
parseFloat(
|
||||||
|
document.documentElement.style.getPropertyValue(
|
||||||
|
"--tg-viewport-content-safe-area-inset-right",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
storage: {
|
storage: {
|
||||||
async clear() {
|
async clear() {
|
||||||
return fallbackImplementation(
|
return fallbackImplementation(
|
||||||
@@ -149,27 +220,27 @@ const externalTgApi = {
|
|||||||
() => Object.values(STORAGE_KEYS).forEach((key) => localStorage.removeItem(key)),
|
() => Object.values(STORAGE_KEYS).forEach((key) => localStorage.removeItem(key)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
async getItem(key: StorageKey, options?: tg.InvokeCustomMethodFpOptions) {
|
async getItem(key: StorageKey) {
|
||||||
return fallbackImplementation(
|
return fallbackImplementation(
|
||||||
true,
|
true,
|
||||||
[key, options],
|
[key],
|
||||||
(key, options) => tg.cloudStorage.getItem.ifAvailable(key, options),
|
(key) => tg.cloudStorage.getItem.ifAvailable(key),
|
||||||
(key) => localStorage.getItem(key) ?? "",
|
(key) => localStorage.getItem(key) ?? "",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
async setItem(key: StorageKey, value: string, options?: tg.InvokeCustomMethodFpOptions) {
|
async setItem(key: StorageKey, value: string) {
|
||||||
return fallbackImplementation(
|
return fallbackImplementation(
|
||||||
true,
|
true,
|
||||||
[key, value, options],
|
[key, value],
|
||||||
(key, value, options) => tg.cloudStorage.setItem.ifAvailable(key, value, options),
|
(key, value) => tg.cloudStorage.setItem.ifAvailable(key, value),
|
||||||
(key, value) => localStorage.setItem(key, value),
|
(key, value) => localStorage.setItem(key, value),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
async deleteItem(key: StorageKey, options?: tg.InvokeCustomMethodFpOptions) {
|
async deleteItem(key: StorageKey) {
|
||||||
return fallbackImplementation(
|
return fallbackImplementation(
|
||||||
true,
|
true,
|
||||||
[key, options],
|
[key],
|
||||||
(key, options) => tg.cloudStorage.deleteItem.ifAvailable(key, options),
|
(key) => tg.cloudStorage.deleteItem.ifAvailable(key),
|
||||||
(key) => localStorage.removeItem(key as string),
|
(key) => localStorage.removeItem(key as string),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -178,5 +249,12 @@ const externalTgApi = {
|
|||||||
|
|
||||||
export default externalTgApi;
|
export default externalTgApi;
|
||||||
|
|
||||||
// @ts-expect-error
|
if (import.meta.env.DEV) {
|
||||||
window.TG = externalTgApi;
|
// @ts-expect-error
|
||||||
|
window.TG = externalTgApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTelegramViewportValue = (key: keyof typeof externalTgApi.viewportCss) => {
|
||||||
|
useSignal(externalTgApi.viewport[key]);
|
||||||
|
return externalTgApi.viewportCss[key]();
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user