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";
+}