diff --git a/frontend/src/core/components/SplitButton.tsx b/frontend/src/core/components/SplitButton.tsx index ed4755c..e898609 100644 --- a/frontend/src/core/components/SplitButton.tsx +++ b/frontend/src/core/components/SplitButton.tsx @@ -40,7 +40,6 @@ export function SplitButton({ top?: number; bottom?: number; left: number; - transform: string; maxHeight: number; } | null>(null); const { width: windowWidth, height: windowHeight } = useWindowSize(); @@ -58,9 +57,8 @@ export function SplitButton({ const menuWidth = menuEl?.offsetWidth ?? 220; const menuHeight = menuEl?.offsetHeight ?? 320; - const anchorX = rect.left + rect.width / 2; + const anchorCenterX = rect.left + rect.width / 2; let left: number; - let transform: string; let top: number | undefined; let bottom: number | undefined; let maxHeight: number; @@ -82,18 +80,15 @@ export function SplitButton({ maxHeight = Math.max(100, availableBelow); } - if (anchorX - menuWidth / 2 < EDGE_PAD) { + if (anchorCenterX - menuWidth / 2 < EDGE_PAD) { left = EDGE_PAD; - transform = "none"; - } else if (anchorX + menuWidth / 2 > vw - EDGE_PAD) { + } else if (anchorCenterX + menuWidth / 2 > vw - EDGE_PAD) { left = vw - menuWidth - EDGE_PAD; - transform = "none"; } else { - left = anchorX; - transform = "translateX(-50%)"; + left = anchorCenterX - menuWidth / 2; } - setMenuPosition({ top, bottom, left, transform, maxHeight }); + setMenuPosition({ top, bottom, left, maxHeight }); }, []); const closeMenu = useCallback(() => { @@ -245,7 +240,6 @@ export function SplitButton({ ? { bottom: menuPosition.bottom } : { top: menuPosition.top }), left: menuPosition.left, - transform: menuPosition.transform, maxHeight: menuPosition.maxHeight, overflowY: "auto", }} diff --git a/frontend/src/core/components/css/split-button.module.scss b/frontend/src/core/components/css/split-button.module.scss index b8cce5f..41433b0 100644 --- a/frontend/src/core/components/css/split-button.module.scss +++ b/frontend/src/core/components/css/split-button.module.scss @@ -224,4 +224,8 @@ $menu-padding: 8px; scrollbar-width: thin; scrollbar-color: rgba($color-dark-on-surface, 0.25) transparent; + + mdui-list { + padding: 0; + } } \ No newline at end of file diff --git a/frontend/src/core/components/DownloadDialog.tsx b/frontend/src/pages/home/DownloadDialog.tsx similarity index 94% rename from frontend/src/core/components/DownloadDialog.tsx rename to frontend/src/pages/home/DownloadDialog.tsx index d6353b6..08c5923 100644 --- a/frontend/src/core/components/DownloadDialog.tsx +++ b/frontend/src/pages/home/DownloadDialog.tsx @@ -1,8 +1,8 @@ import type { ReactNode } from "react"; -import { StyledDialog } from "./StyledDialog"; +import { StyledDialog } from "@/core/components/StyledDialog"; import { MaterialButton, MaterialIcon } from "@/utils/material"; -import { OS_CONFIG, type DownloadOs } from "@/core/downloads/os"; -import styles from "./css/download-dialog.module.scss"; +import { OS_CONFIG, type DownloadOs } from "@/pages/home/os"; +import styles from "@/pages/home/download-dialog.module.scss"; interface DownloadDialogProps { open: boolean; @@ -98,7 +98,7 @@ export function DownloadDialog({ open, onOpenChange, os }: DownloadDialogProps)
-

Thanks for downloading

+

Спасибо за скачивание!

FromChat для  {osInfo.label} @@ -110,4 +110,3 @@ export function DownloadDialog({ open, onOpenChange, os }: DownloadDialogProps) ); } - diff --git a/frontend/src/pages/home/HomeFooter.tsx b/frontend/src/pages/home/HomeFooter.tsx index d4a4e7f..5e6de13 100644 --- a/frontend/src/pages/home/HomeFooter.tsx +++ b/frontend/src/pages/home/HomeFooter.tsx @@ -1,7 +1,7 @@ import { Link } from "react-router-dom"; import { MaterialIcon } from "@/utils/material"; -import { GitHubLink, GITHUB_WEB, GITHUB_APP, GITHUB_LICENSE } from "./homeLinks"; -import styles from "./home-footer.module.scss"; +import { GitHubLink, GITHUB_WEB, GITHUB_APP, GITHUB_LICENSE } from "@/pages/home/homeLinks"; +import styles from "@/pages/home/home-footer.module.scss"; export function HomeFooter() { return ( diff --git a/frontend/src/pages/home/HomeHeader.tsx b/frontend/src/pages/home/HomeHeader.tsx index 59b62f9..2dc1391 100644 --- a/frontend/src/pages/home/HomeHeader.tsx +++ b/frontend/src/pages/home/HomeHeader.tsx @@ -2,8 +2,8 @@ import { useNavigate } from "react-router-dom"; import { useUserStore } from "@/state/user"; import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen"; import { MaterialButton, MaterialIconButton } from "@/utils/material"; -import { GitHubLink, SupportLink } from "./homeLinks"; -import styles from "./home-header.module.scss"; +import { GitHubLink, SupportLink } from "@/pages/home/homeLinks"; +import styles from "@/pages/home/home-header.module.scss"; export function HomeHeader() { const navigate = useNavigate(); diff --git a/frontend/src/pages/home/HomePage.tsx b/frontend/src/pages/home/HomePage.tsx index c630a83..4d2d9b3 100644 --- a/frontend/src/pages/home/HomePage.tsx +++ b/frontend/src/pages/home/HomePage.tsx @@ -1,18 +1,18 @@ import { useNavigate } from "react-router-dom"; import { useState, type ReactNode } from "react"; -import styles from "./home.module.scss"; +import styles from "@/pages/home/home.module.scss"; import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen"; -import { MaterialButton, MaterialIcon, MaterialList, MaterialListItem } from "@/utils/material"; -import generalChatScreenshot from "../../images/screenshots/general-chat.png"; -import dmScreenshot from "../../images/screenshots/dm.png"; -import windowsIcon from "../../images/windows.svg"; -import linuxIcon from "../../images/linux.svg"; -import macIcon from "../../images/mac.svg"; -import { HomeHeader } from "./HomeHeader"; -import { HomeFooter } from "./HomeFooter"; +import { MaterialButton, MaterialIcon, MaterialIconButton, MaterialList, MaterialListItem } from "@/utils/material"; +import generalChatScreenshot from "@/images/screenshots/general-chat.png"; +import dmScreenshot from "@/images/screenshots/dm.png"; +import windowsIcon from "@/images/windows.svg"; +import linuxIcon from "@/images/linux.svg"; +import macIcon from "@/images/mac.svg"; +import { HomeHeader } from "@/pages/home/HomeHeader"; +import { HomeFooter } from "@/pages/home/HomeFooter"; import { SplitButton } from "@/core/components/SplitButton"; -import { DownloadDialog } from "@/core/components/DownloadDialog"; -import { OS_CONFIG, ALL_OS, detectOs, type DownloadOs } from "@/core/downloads/os"; +import { DownloadDialog } from "@/pages/home/DownloadDialog"; +import { OS_CONFIG, ALL_OS, detectOs, type DownloadOs } from "@/pages/home/os"; interface FeatureSectionProps { title: ReactNode; @@ -74,6 +74,14 @@ export default function HomePage() { return true; }; + const getButtonVariant = (os: DownloadOs): "filled" | "tonal" | "outlined" => { + const detectedOs = detectOs(); + if (os === detectedOs) return "filled"; + if (!isMobile && ["windows", "linux", "macos"].includes(os)) return "tonal"; + if (isMobile && (os === "android" || os === "ios") && os !== detectedOs) return "tonal"; + return "outlined"; + }; + return (

@@ -156,36 +164,63 @@ export default function HomePage() { Настольное приложение с уведомлениями и автономной работой или мобильное приложение для Android и iOS.

-
-
- Платформа - Описание - -
- {ALL_OS.map((os) => ( -
-
- - {OS_CONFIG[os].label} -
- - {OS_CONFIG[os].description} - -
- triggerDownload(os)} - > - Скачать - -
-
- ))} -
+ + + + + + + + + {ALL_OS.map((os) => ( + + + + + + ))} + +
ПлатформаОписание +
+ + {["windows", "linux", "macos"].includes(os) ? ( + + ) : ( + + )} + {OS_CONFIG[os].label} + + + + {OS_CONFIG[os].description} + + + + triggerDownload(os)} + > + Скачать + + triggerDownload(os)} + title={`Скачать ${OS_CONFIG[os].label}`} + /> + +
diff --git a/frontend/src/pages/home/_home-shared.scss b/frontend/src/pages/home/_home-shared.scss index 1174afa..75f8730 100644 --- a/frontend/src/pages/home/_home-shared.scss +++ b/frontend/src/pages/home/_home-shared.scss @@ -1,4 +1,4 @@ -@use "../../css/material" as *; +@use "@/css/material" as *; $gradient-rainbow: linear-gradient(45deg, #9333EA, #6366F1, #3B82F6, #A855F7, #D946EF, #EC4899, #7E22CE); $glow-purple: rgba(147, 51, 234, 0.5); diff --git a/frontend/src/core/components/css/download-dialog.module.scss b/frontend/src/pages/home/download-dialog.module.scss similarity index 96% rename from frontend/src/core/components/css/download-dialog.module.scss rename to frontend/src/pages/home/download-dialog.module.scss index f03da61..40f783b 100644 --- a/frontend/src/core/components/css/download-dialog.module.scss +++ b/frontend/src/pages/home/download-dialog.module.scss @@ -1,5 +1,5 @@ -@use "../../../css/material" as *; -@use "../../../css/colors" as *; +@use "@/css/material" as *; +@use "@/css/colors" as *; @use "sass:color"; .downloadDialog { @@ -111,4 +111,3 @@ padding-inline: 16px; } } - diff --git a/frontend/src/pages/home/home-footer.module.scss b/frontend/src/pages/home/home-footer.module.scss index 60eb8d9..59c49b3 100644 --- a/frontend/src/pages/home/home-footer.module.scss +++ b/frontend/src/pages/home/home-footer.module.scss @@ -1,4 +1,4 @@ -@use "../../css/material" as *; +@use "@/css/material" as *; @use "home-shared" as shared; .homepageFooter { @@ -37,7 +37,7 @@ .footerLogo { width: 40px; height: 40px; - background-image: url('../../images/logo_square.svg'); + background-image: url('@/images/logo_square.svg'); background-size: cover; background-position: center; background-repeat: no-repeat; @@ -111,11 +111,11 @@ } .footerLinkIconSvgTelegram { - mask-image: url('../../images/telegram.svg'); - -webkit-mask-image: url('../../images/telegram.svg'); + mask-image: url('@/images/telegram.svg'); + -webkit-mask-image: url('@/images/telegram.svg'); } .footerLinkIconSvgMax { - mask-image: url('../../images/max.svg'); - -webkit-mask-image: url('../../images/max.svg'); + mask-image: url('@/images/max.svg'); + -webkit-mask-image: url('@/images/max.svg'); } diff --git a/frontend/src/pages/home/home-header.module.scss b/frontend/src/pages/home/home-header.module.scss index 43d825c..619408f 100644 --- a/frontend/src/pages/home/home-header.module.scss +++ b/frontend/src/pages/home/home-header.module.scss @@ -1,4 +1,4 @@ -@use "../../css/material" as *; +@use "@/css/material" as *; @use "home-shared" as shared; .homepageHeader { @@ -35,7 +35,7 @@ .logoIcon { width: 40px; height: 40px; - background-image: url('../../images/logo_square.svg'); + background-image: url('@/images/logo_square.svg'); background-size: cover; background-position: center; background-repeat: no-repeat; diff --git a/frontend/src/pages/home/home.module.scss b/frontend/src/pages/home/home.module.scss index 92693a5..e6333be 100644 --- a/frontend/src/pages/home/home.module.scss +++ b/frontend/src/pages/home/home.module.scss @@ -1,4 +1,4 @@ -@use "../../css/material" as *; +@use "@/css/material" as *; @use "home-shared" as shared; // Shared variables (home-specific) @@ -15,7 +15,18 @@ $gradient-conic: conic-gradient( rgba(147, 51, 234, 0.5) 100% ); $radius-card: 20px; -$radius-pill: 9999px; +$radius-pill: 25px; + +// Download table variables +$download-cell-padding: 14px 20px; +$download-cell-gap: 12px; +$download-icon-size: 24px; +$download-col1-w: 35%; +$download-col2-w: 30%; +$download-col3-w: 35%; +$download-row-bg-hover: rgba($color-dark-surface-container-high, 0.6); +$download-header-bg: rgba($color-dark-surface-container-high, 0.4); +$download-text-color: $color-dark-on-surface-variant; // Mixins @mixin section-heading { @@ -44,6 +55,30 @@ $radius-pill: 9999px; padding: 0 32px; } +@mixin download-cell-inner($justify) { + display: flex; + justify-content: $justify; + align-items: center; + width: 100%; + min-width: 0; +} + +@mixin masked-icon($url-var) { + display: inline-block; + width: $download-icon-size; + height: $download-icon-size; + flex-shrink: 0; + background-color: $download-text-color; + mask-image: var(#{$url-var}); + -webkit-mask-image: var(#{$url-var}); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: contain; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; +} + .homepage { min-height: 100vh; display: flex; @@ -96,7 +131,7 @@ $radius-pill: 9999px; .titleLogo { width: 120px; height: 120px; - background-image: url('../../images/logo_square.svg'); + background-image: url('@/images/logo_square.svg'); background-size: cover; background-position: center; background-repeat: no-repeat; @@ -212,66 +247,169 @@ $radius-pill: 9999px; } .downloadTable { + display: table; user-select: none; width: 100%; max-width: 720px; margin: 0 auto; - border-radius: 20px; - background: rgba($color-dark-surface-container, 0.8); - border: 1px solid rgba($color-dark-outline, 0.3); - padding: 8px 16px; - display: flex; - flex-direction: column; - gap: 4px; - } + border-radius: 16px; + border-collapse: separate; + border-spacing: 0 6px; + background: transparent; + border: none; + table-layout: auto; - .downloadTableHeader { - display: grid; - grid-template-columns: 2fr 2fr auto; - padding: 8px 4px; - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.08em; - color: $color-dark-on-surface-variant; - } + th:first-child, + td:first-child { + width: 1%; + white-space: nowrap; + } - .downloadTableRow { - display: grid; - grid-template-columns: 2fr 2fr auto; - align-items: center; - padding: 8px 4px; - border-radius: 12px; - &:hover { - background-color: rgba($color-dark-surface-container-high, 0.8); + th:nth-child(2), + td:nth-child(2) { + width: 1%; + white-space: nowrap; + } + + th:last-child, + td:last-child { + width: 100%; + } + + thead { + display: table-header-group; + + tr th { + padding: $download-cell-padding; + display: table-cell; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: $download-text-color; + background: $download-header-bg; + vertical-align: middle; + transition: background-color 0.2s ease; + text-align: left; + + &:first-child { + border-radius: 12px 0 0 12px; + } + + &:last-child { + border-radius: 0 12px 12px 0; + } + } + } + + tbody { + display: table-row-group; + + tr { + display: table-row; + background: $color-dark-surface-container-low; + + td { + padding: $download-cell-padding; + display: table-cell; + text-align: center; + vertical-align: middle; + transition: background-color 0.2s ease; + background: transparent; + + &:first-child { + text-align: left; + border-radius: $radius-pill 0 0 $radius-pill; + } + + &:last-child { + text-align: right; + border-radius: 0 $radius-pill $radius-pill 0; + } + } + + &:hover td { + background: $download-row-bg-hover; + } + } + } + + .downloadTableOs { + text-align: left; + + .downloadTableOsContent { + @include download-cell-inner(flex-start); + gap: $download-cell-gap; + min-height: 100%; + height: 100%; + } + } + + .downloadTableIcon { + font-size: $download-icon-size; + width: $download-icon-size; + height: $download-icon-size; + color: $download-text-color; + } + + .tableOsIcon { + @include masked-icon("--table-os-icon-url"); + } + + .downloadTableDesc { + font-size: 14px; + color: $download-text-color; + + .downloadTableDescInner { + @include download-cell-inner(flex-start); + } + } + + .downloadTableAction { + text-align: right; + + .downloadTableActionInner { + @include download-cell-inner(flex-end); + } + } + + .downloadButton { + display: block; + } + + .downloadButtonIcon { + display: none; } } + } - .downloadTableOs { - display: flex; - align-items: center; - gap: 8px; + @media (max-width: 490px) { + .downloadContent .downloadTable { + .downloadButton { + display: none; + } + + .downloadButtonIcon { + display: block; + } } + } - .downloadTableIcon { - font-size: 20px; - color: $color-dark-primary; - opacity: 0.9; + @media (max-width: 405px) { + .downloadContent .downloadTable { + th:nth-child(2), + td:nth-child(2) { + display: none !important; + } + + .downloadButton { + display: block !important; + } + + .downloadButtonIcon { + display: none !important; + } } - - .downloadTableDesc { - font-size: 14px; - color: $color-dark-on-surface-variant; - } - - .downloadTableAction { - display: flex; - justify-content: flex-end; - } - - .downloadTableActionLink { - text-decoration: none; - } - } } @@ -341,18 +479,7 @@ $radius-pill: 9999px; } .menuCustomIcon { - display: inline-block; - width: 24px; - height: 24px; - background-color: $color-dark-on-surface-variant; - mask-image: var(--menu-custom-icon-url); - -webkit-mask-image: var(--menu-custom-icon-url); - mask-size: contain; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: contain; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; + @include masked-icon("--menu-custom-icon-url"); } // Keyframes for animations diff --git a/frontend/src/pages/home/os.ts b/frontend/src/pages/home/os.ts new file mode 100644 index 0000000..f5c1669 --- /dev/null +++ b/frontend/src/pages/home/os.ts @@ -0,0 +1,56 @@ +export type DownloadOs = "windows" | "linux" | "macos" | "android" | "ios"; + +export const ALL_OS: DownloadOs[] = [ + "windows", + "linux", + "macos", + "android", + "ios", +] as const; + +export interface OsInfo { + id: DownloadOs; + label: string; + description: string; + icon: string; +} + +export const OS_CONFIG: Record = { + windows: { id: "windows", label: "Windows", description: "ПК", icon: "computer" }, + linux: { id: "linux", label: "Linux", description: "ПК", icon: "computer" }, + macos: { id: "macos", label: "macOS", description: "Apple", icon: "computer" }, + android: { id: "android", label: "Android", description: "APK", icon: "android" }, + ios: { id: "ios", label: "iOS", description: "iPhone, iPad", icon: "phone_iphone" }, +}; + +export function detectOs(): DownloadOs { + if (typeof navigator === "undefined") { + return "android"; + } + + const ua = (navigator.userAgent || navigator.platform || "").toLowerCase(); + const platform = (navigator as any).userAgentData?.platform?.toLowerCase?.() ?? ""; + const haystack = `${ua} ${platform}`; + + if (haystack.includes("android")) { + return "android"; + } + + if (haystack.includes("iphone") || haystack.includes("ipad") || haystack.includes("ipod")) { + return "ios"; + } + + if (haystack.includes("win")) { + return "windows"; + } + + if (haystack.includes("mac")) { + return "macos"; + } + + if (haystack.includes("linux")) { + return "linux"; + } + + return "android"; +}