diff --git a/src/components/modals/ActionModal/ActionModal.module.css b/src/components/modals/ActionModal/ActionModal.module.css
index 3326de5..cba9f97 100644
--- a/src/components/modals/ActionModal/ActionModal.module.css
+++ b/src/components/modals/ActionModal/ActionModal.module.css
@@ -1,29 +1,11 @@
@layer base {
- .overlay {
- position: fixed;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 100;
- }
+ .content {
+ border-radius: 22px;
- .modal {
- display: flex;
- flex-direction: column;
- gap: 6px;
- width: 100%;
- max-width: 320px;
- padding: 13px;
-
- .content {
- border-radius: 22px;
-
- .description {
- padding: 12px;
- border-radius: 18px;
- text-align: center;
- }
+ .description {
+ padding: 12px;
+ border-radius: 18px;
+ text-align: center;
}
}
diff --git a/src/components/modals/ActionModal/ActionModal.tsx b/src/components/modals/ActionModal/ActionModal.tsx
index 0db9ae1..25fc55e 100644
--- a/src/components/modals/ActionModal/ActionModal.tsx
+++ b/src/components/modals/ActionModal/ActionModal.tsx
@@ -1,7 +1,6 @@
-import { AnimatePresence, motion } from "motion/react";
import { useTranslation } from "react-i18next";
-import SectionSurface from "../../surface/SectionSurface/SectionSurface";
+import Modal from "../Modal/Modal";
import ContentSurface from "../../surface/ContentSurface/ContentSurface";
import LightSurface from "../../surface/LightSurface/LightSurface";
import Button from "../../atoms/Button/Button";
@@ -27,36 +26,20 @@ export default function ActionModal({ open, description, onClose, onConfirm, con
const { t } = useTranslation();
return (
-
- {open && (
-
-
-
- {description}
-
-
-
- {onConfirm != null && (
-
- )}
-
-
-
- )}
-
+
+
+ {description}
+
+
+
+ {onConfirm != null && (
+
+ )}
+
+
);
}
diff --git a/src/components/modals/Modal/Modal.module.css b/src/components/modals/Modal/Modal.module.css
new file mode 100644
index 0000000..b79986c
--- /dev/null
+++ b/src/components/modals/Modal/Modal.module.css
@@ -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;
+ }
+}
diff --git a/src/components/modals/Modal/Modal.tsx b/src/components/modals/Modal/Modal.tsx
new file mode 100644
index 0000000..b93c62a
--- /dev/null
+++ b/src/components/modals/Modal/Modal.tsx
@@ -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 (
+
+ {open && (
+
+ e.stopPropagation()}
+ >
+ {children}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/modals/Modal/index.ts b/src/components/modals/Modal/index.ts
new file mode 100644
index 0000000..09b91f7
--- /dev/null
+++ b/src/components/modals/Modal/index.ts
@@ -0,0 +1 @@
+export { default } from "./Modal";
diff --git a/src/routes/-/RootLayout/RootLayout.module.css b/src/routes/-/RootLayout/RootLayout.module.css
index 10f5d94..1ca0173 100644
--- a/src/routes/-/RootLayout/RootLayout.module.css
+++ b/src/routes/-/RootLayout/RootLayout.module.css
@@ -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);
+ }
}
}
diff --git a/src/routes/-/RootLayout/RootLayout.tsx b/src/routes/-/RootLayout/RootLayout.tsx
index 0a4a303..32c6c7e 100644
--- a/src/routes/-/RootLayout/RootLayout.tsx
+++ b/src/routes/-/RootLayout/RootLayout.tsx
@@ -13,9 +13,7 @@ export default function RootLayout({ children, hideControls }: Props) {
{!hideControls && }
-
- {children}
-
+ {children}
{!hideControls && }
diff --git a/src/routes/-/RootLayout/components/Header/Header.module.css b/src/routes/-/RootLayout/components/Header/Header.module.css
index ee2a687..d661c2e 100644
--- a/src/routes/-/RootLayout/components/Header/Header.module.css
+++ b/src/routes/-/RootLayout/components/Header/Header.module.css
@@ -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;
+ }
}
}
diff --git a/src/routes/-/RootLayout/components/Header/Header.tsx b/src/routes/-/RootLayout/components/Header/Header.tsx
index 36e8a19..32b6e24 100644
--- a/src/routes/-/RootLayout/components/Header/Header.tsx
+++ b/src/routes/-/RootLayout/components/Header/Header.tsx
@@ -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 ;
+ return (
+
+ );
}
diff --git a/src/routes/-/RootLayout/components/Header/components/Profile/Profile.module.css b/src/routes/-/RootLayout/components/Header/components/Profile/Profile.module.css
new file mode 100644
index 0000000..43664d5
--- /dev/null
+++ b/src/routes/-/RootLayout/components/Header/components/Profile/Profile.module.css
@@ -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 {
+ }
+}
diff --git a/src/routes/-/RootLayout/components/Header/components/Profile/Profile.tsx b/src/routes/-/RootLayout/components/Header/components/Profile/Profile.tsx
new file mode 100644
index 0000000..1d27c91
--- /dev/null
+++ b/src/routes/-/RootLayout/components/Header/components/Profile/Profile.tsx
@@ -0,0 +1,19 @@
+import classes from "./Profile.module.css";
+import { motion } from "motion/react";
+
+export default function Profile() {
+ return (
+
+
+
+
+ );
+}
diff --git a/src/routes/-/RootLayout/components/Header/assets/user-bar.svg b/src/routes/-/RootLayout/components/Header/components/Profile/assets/profile-bg.svg
similarity index 100%
rename from src/routes/-/RootLayout/components/Header/assets/user-bar.svg
rename to src/routes/-/RootLayout/components/Header/components/Profile/assets/profile-bg.svg
diff --git a/src/routes/-/RootLayout/components/Header/components/Profile/index.ts b/src/routes/-/RootLayout/components/Header/components/Profile/index.ts
new file mode 100644
index 0000000..2623c86
--- /dev/null
+++ b/src/routes/-/RootLayout/components/Header/components/Profile/index.ts
@@ -0,0 +1 @@
+export { default } from "./Profile";
diff --git a/src/routes/-/RootLayout/components/Header/components/Settings/Settings.module.css b/src/routes/-/RootLayout/components/Header/components/Settings/Settings.module.css
new file mode 100644
index 0000000..805ba39
--- /dev/null
+++ b/src/routes/-/RootLayout/components/Header/components/Settings/Settings.module.css
@@ -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;
+ }
+}
diff --git a/src/routes/-/RootLayout/components/Header/components/Settings/Settings.tsx b/src/routes/-/RootLayout/components/Header/components/Settings/Settings.tsx
new file mode 100644
index 0000000..56c8c64
--- /dev/null
+++ b/src/routes/-/RootLayout/components/Header/components/Settings/Settings.tsx
@@ -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 (
+
+
+ {[0, 1, 2].map((i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/routes/-/RootLayout/components/Header/assets/menu.svg b/src/routes/-/RootLayout/components/Header/components/Settings/assets/settings-bg.svg
similarity index 100%
rename from src/routes/-/RootLayout/components/Header/assets/menu.svg
rename to src/routes/-/RootLayout/components/Header/components/Settings/assets/settings-bg.svg
diff --git a/src/routes/-/RootLayout/components/Header/components/Settings/index.ts b/src/routes/-/RootLayout/components/Header/components/Settings/index.ts
new file mode 100644
index 0000000..41d6622
--- /dev/null
+++ b/src/routes/-/RootLayout/components/Header/components/Settings/index.ts
@@ -0,0 +1 @@
+export { default } from "./Settings";
diff --git a/src/routes/-/RootLayout/components/Navigation/Navigation.module.css b/src/routes/-/RootLayout/components/Navigation/Navigation.module.css
index a2ccb18..f47a38c 100644
--- a/src/routes/-/RootLayout/components/Navigation/Navigation.module.css
+++ b/src/routes/-/RootLayout/components/Navigation/Navigation.module.css
@@ -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;
}
diff --git a/src/routes/-/RootLayout/components/Navigation/Navigation.tsx b/src/routes/-/RootLayout/components/Navigation/Navigation.tsx
index f9fb12e..2aac27d 100644
--- a/src/routes/-/RootLayout/components/Navigation/Navigation.tsx
+++ b/src/routes/-/RootLayout/components/Navigation/Navigation.tsx
@@ -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 (
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 (
;
};
+const mockCssVar = (key: string, px: number) =>
+ document.documentElement.style.setProperty(key, `${px}px`);
+
const externalTgApi = {
init: () => {
tg.setDebug(import.meta.env.DEV);
@@ -124,6 +128,20 @@ const externalTgApi = {
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(
import.meta.env.DEV ? "TMA Debug mode in Web initialized" : "Telegram Mini App initialized",
);
@@ -140,6 +158,59 @@ const externalTgApi = {
},
},
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: {
async clear() {
return fallbackImplementation(
@@ -149,27 +220,27 @@ const externalTgApi = {
() => Object.values(STORAGE_KEYS).forEach((key) => localStorage.removeItem(key)),
);
},
- async getItem(key: StorageKey, options?: tg.InvokeCustomMethodFpOptions) {
+ async getItem(key: StorageKey) {
return fallbackImplementation(
true,
- [key, options],
- (key, options) => tg.cloudStorage.getItem.ifAvailable(key, options),
+ [key],
+ (key) => tg.cloudStorage.getItem.ifAvailable(key),
(key) => localStorage.getItem(key) ?? "",
);
},
- async setItem(key: StorageKey, value: string, options?: tg.InvokeCustomMethodFpOptions) {
+ async setItem(key: StorageKey, value: string) {
return fallbackImplementation(
true,
- [key, value, options],
- (key, value, options) => tg.cloudStorage.setItem.ifAvailable(key, value, options),
+ [key, value],
+ (key, value) => tg.cloudStorage.setItem.ifAvailable(key, value),
(key, value) => localStorage.setItem(key, value),
);
},
- async deleteItem(key: StorageKey, options?: tg.InvokeCustomMethodFpOptions) {
+ async deleteItem(key: StorageKey) {
return fallbackImplementation(
true,
- [key, options],
- (key, options) => tg.cloudStorage.deleteItem.ifAvailable(key, options),
+ [key],
+ (key) => tg.cloudStorage.deleteItem.ifAvailable(key),
(key) => localStorage.removeItem(key as string),
);
},
@@ -178,5 +249,12 @@ const externalTgApi = {
export default externalTgApi;
-// @ts-expect-error
-window.TG = externalTgApi;
+if (import.meta.env.DEV) {
+ // @ts-expect-error
+ window.TG = externalTgApi;
+}
+
+export const useTelegramViewportValue = (key: keyof typeof externalTgApi.viewportCss) => {
+ useSignal(externalTgApi.viewport[key]);
+ return externalTgApi.viewportCss[key]();
+};