mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement SplitButton from Material 3 Expressive
This commit is contained in:
@@ -0,0 +1,113 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { StyledDialog } from "./StyledDialog";
|
||||||
|
import { MaterialButton, MaterialIcon } from "@/utils/material";
|
||||||
|
import { OS_CONFIG, type DownloadOs } from "@/core/downloads/os";
|
||||||
|
import styles from "./css/download-dialog.module.scss";
|
||||||
|
|
||||||
|
interface DownloadDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
os: DownloadOs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AndroidInstructions(): ReactNode {
|
||||||
|
return (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 className={styles.sectionTitle}>Установка на Android</h3>
|
||||||
|
<p className={styles.text}>
|
||||||
|
Вы скачали APK-файл FromChat. Чтобы установить приложение:
|
||||||
|
</p>
|
||||||
|
<ul className={styles.list}>
|
||||||
|
<li>Откройте загруженный APK-файл из шторки уведомлений или файлового менеджера.</li>
|
||||||
|
<li>
|
||||||
|
Если появится запрос "Разрешить установку из неизвестных источников" — дайте
|
||||||
|
разрешение для браузера, из которого вы скачивали APK.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Google Play Protect может предупредить о неизвестном приложении. Если вы доверяете FromChat,
|
||||||
|
нажмите "Подробнее" → "Всё равно установить" (или аналогичную кнопку).
|
||||||
|
</li>
|
||||||
|
<li>Дождитесь завершения установки и откройте FromChat из списка приложений.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function IosInstructions(): ReactNode {
|
||||||
|
return (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 className={styles.sectionTitle}>Установка на iOS</h3>
|
||||||
|
<p className={styles.text}>
|
||||||
|
Эта сборка не распространяется через App Store или TestFlight. Чтобы установить FromChat на iPhone
|
||||||
|
или iPad, потребуется один из вариантов сторонней установки:
|
||||||
|
</p>
|
||||||
|
<ul className={styles.list}>
|
||||||
|
<li>
|
||||||
|
<strong>TrollStore</strong>: постоянная установка приложений из IPA-файлов. Требуется поддерживаемая
|
||||||
|
версия iOS и настройка TrollStore на устройстве.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Джейлбрейк</strong>: установка через менеджер пакетов (Sileo, Cydia и т.п.) или напрямую
|
||||||
|
из файлового менеджера, если у вас уже есть джейлбрейк.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Другие сервисы сайдлоада</strong>: сторонние инструменты, которые подписывают IPA-файл
|
||||||
|
вашим сертификатом разработчика или временным сертификатом.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p className={styles.text}>
|
||||||
|
К сожалению, простого и официально поддерживаемого пути установки для iOS здесь нет — именно поэтому я
|
||||||
|
бы сам iPhone не покупал 😄
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInstructions(os: DownloadOs): ReactNode {
|
||||||
|
if (os === "android") {
|
||||||
|
return <AndroidInstructions />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (os === "ios") {
|
||||||
|
return <IosInstructions />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DownloadDialog({ open, onOpenChange, os }: DownloadDialogProps) {
|
||||||
|
const osInfo = OS_CONFIG[os];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledDialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
className={styles.downloadDialog}
|
||||||
|
contentClassName={styles.downloadDialogContent}
|
||||||
|
afterChildren={
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<MaterialButton variant="filled" onClick={() => onOpenChange(false)}>
|
||||||
|
Закрыть
|
||||||
|
</MaterialButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.body}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.iconWrapper}>
|
||||||
|
<MaterialIcon name="download" className={styles.icon} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.titleBlock}>
|
||||||
|
<h2 className={styles.title}>Thanks for downloading</h2>
|
||||||
|
<p className={styles.subtitle}>
|
||||||
|
FromChat для
|
||||||
|
<span className={styles.osName}>{osInfo.label}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{renderInstructions(os)}
|
||||||
|
</div>
|
||||||
|
</StyledDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState, type MouseEvent, type ReactNode } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import { MaterialIcon, MaterialRipple, useRippleHandlers } from "@/utils/material";
|
||||||
|
import useWindowSize from "@/core/hooks/useWindowSize";
|
||||||
|
import styles from "./css/split-button.module.scss";
|
||||||
|
|
||||||
|
export type SplitButtonVariant = "filled" | "tonal" | "outlined" | "elevated";
|
||||||
|
|
||||||
|
interface SplitButtonProps {
|
||||||
|
text: ReactNode;
|
||||||
|
icon?: ReactNode | string;
|
||||||
|
menu: ReactNode;
|
||||||
|
menuOpen: boolean;
|
||||||
|
onMenuOpen: (open: boolean) => void;
|
||||||
|
onPrimaryClick?: () => void;
|
||||||
|
variant?: SplitButtonVariant;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
menuAriaLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SplitButton({
|
||||||
|
text,
|
||||||
|
icon,
|
||||||
|
menu,
|
||||||
|
menuOpen: open,
|
||||||
|
onMenuOpen,
|
||||||
|
onPrimaryClick,
|
||||||
|
variant = "filled",
|
||||||
|
disabled = false,
|
||||||
|
className = "",
|
||||||
|
menuAriaLabel,
|
||||||
|
}: SplitButtonProps) {
|
||||||
|
const [isExiting, setIsExiting] = useState(false);
|
||||||
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const menuSegmentRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [menuPosition, setMenuPosition] = useState<{
|
||||||
|
top?: number;
|
||||||
|
bottom?: number;
|
||||||
|
left: number;
|
||||||
|
transform: string;
|
||||||
|
maxHeight: number;
|
||||||
|
} | null>(null);
|
||||||
|
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
||||||
|
const primaryRipple = useRippleHandlers(disabled);
|
||||||
|
const menuRipple = useRippleHandlers(disabled);
|
||||||
|
|
||||||
|
const MENU_GAP = 16;
|
||||||
|
const EDGE_PAD = 16;
|
||||||
|
|
||||||
|
const updateMenuPosition = useCallback(() => {
|
||||||
|
const anchor = menuSegmentRef.current;
|
||||||
|
if (!anchor) return;
|
||||||
|
const rect = anchor.getBoundingClientRect();
|
||||||
|
const menuEl = menuRef.current;
|
||||||
|
const menuWidth = menuEl?.offsetWidth ?? 220;
|
||||||
|
const menuHeight = menuEl?.offsetHeight ?? 320;
|
||||||
|
|
||||||
|
const anchorX = rect.left + rect.width / 2;
|
||||||
|
let left: number;
|
||||||
|
let transform: string;
|
||||||
|
let top: number | undefined;
|
||||||
|
let bottom: number | undefined;
|
||||||
|
let maxHeight: number;
|
||||||
|
|
||||||
|
const vw = window.innerWidth;
|
||||||
|
const vh = window.innerHeight;
|
||||||
|
|
||||||
|
const availableBelow = vh - rect.bottom - MENU_GAP - EDGE_PAD;
|
||||||
|
const availableAbove = rect.top - MENU_GAP - EDGE_PAD;
|
||||||
|
const fitsBelow = menuHeight <= availableBelow;
|
||||||
|
const fitsAbove = menuHeight <= availableAbove;
|
||||||
|
const placeAbove = !fitsBelow && (fitsAbove || availableAbove > availableBelow);
|
||||||
|
|
||||||
|
if (placeAbove) {
|
||||||
|
bottom = vh - (rect.top - MENU_GAP);
|
||||||
|
maxHeight = Math.max(100, availableAbove);
|
||||||
|
} else {
|
||||||
|
top = rect.bottom + MENU_GAP;
|
||||||
|
maxHeight = Math.max(100, availableBelow);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchorX - menuWidth / 2 < EDGE_PAD) {
|
||||||
|
left = EDGE_PAD;
|
||||||
|
transform = "none";
|
||||||
|
} else if (anchorX + menuWidth / 2 > vw - EDGE_PAD) {
|
||||||
|
left = vw - menuWidth - EDGE_PAD;
|
||||||
|
transform = "none";
|
||||||
|
} else {
|
||||||
|
left = anchorX;
|
||||||
|
transform = "translateX(-50%)";
|
||||||
|
}
|
||||||
|
|
||||||
|
setMenuPosition({ top, bottom, left, transform, maxHeight });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeMenu = useCallback(() => {
|
||||||
|
onMenuOpen(false);
|
||||||
|
setIsExiting(true);
|
||||||
|
}, [onMenuOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open && !isExiting) {
|
||||||
|
setMenuPosition(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
updateMenuPosition();
|
||||||
|
window.addEventListener("scroll", updateMenuPosition, true);
|
||||||
|
|
||||||
|
function handleDocumentClick(event: MouseEvent | globalThis.MouseEvent) {
|
||||||
|
const target = event.target as Node | null;
|
||||||
|
if (!target) return;
|
||||||
|
if (rootRef.current?.contains(target)) return;
|
||||||
|
if (menuRef.current?.contains(target)) return;
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Escape") closeMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleDocumentClick as unknown as EventListener);
|
||||||
|
document.addEventListener("touchstart", handleDocumentClick as unknown as EventListener);
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", updateMenuPosition, true);
|
||||||
|
document.removeEventListener("mousedown", handleDocumentClick as unknown as EventListener);
|
||||||
|
document.removeEventListener("touchstart", handleDocumentClick as unknown as EventListener);
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [open, isExiting, closeMenu, updateMenuPosition, windowWidth, windowHeight]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) setIsExiting(true);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (open && menuRef.current) {
|
||||||
|
updateMenuPosition();
|
||||||
|
}
|
||||||
|
}, [open, updateMenuPosition]);
|
||||||
|
|
||||||
|
const handlePrimaryClick = () => {
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onPrimaryClick?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuToggle = () => {
|
||||||
|
if (disabled) return;
|
||||||
|
if (open) closeMenu();
|
||||||
|
else onMenuOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const variantClass =
|
||||||
|
variant === "tonal"
|
||||||
|
? styles.variantTonal
|
||||||
|
: variant === "outlined"
|
||||||
|
? styles.variantOutlined
|
||||||
|
: variant === "elevated"
|
||||||
|
? styles.variantElevated
|
||||||
|
: styles.variantFilled;
|
||||||
|
|
||||||
|
const renderIcon = () => {
|
||||||
|
if (!icon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof icon === "string") {
|
||||||
|
return <MaterialIcon name={icon} className={styles.leadingIconIcon} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span className={styles.leadingIconIcon}>{icon}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rootClasses = [
|
||||||
|
styles.splitButton,
|
||||||
|
variantClass,
|
||||||
|
disabled ? styles.disabled : "",
|
||||||
|
className,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={rootRef}
|
||||||
|
className={rootClasses}
|
||||||
|
data-open={open ? "true" : "false"}
|
||||||
|
aria-disabled={disabled ? "true" : "false"}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.primarySegment}
|
||||||
|
onClick={handlePrimaryClick}
|
||||||
|
onPointerDown={primaryRipple.onPointerDown}
|
||||||
|
onPointerEnter={primaryRipple.onPointerEnter}
|
||||||
|
onPointerLeave={primaryRipple.onPointerLeave}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<MaterialRipple ref={primaryRipple.rippleRef} />
|
||||||
|
<span className={styles.primaryContent}>
|
||||||
|
{icon && <span className={styles.leadingIcon}>{renderIcon()}</span>}
|
||||||
|
<span className={styles.label}>{text}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
ref={menuSegmentRef}
|
||||||
|
type="button"
|
||||||
|
className={styles.menuSegment}
|
||||||
|
onClick={handleMenuToggle}
|
||||||
|
onPointerDown={menuRipple.onPointerDown}
|
||||||
|
onPointerEnter={menuRipple.onPointerEnter}
|
||||||
|
onPointerLeave={menuRipple.onPointerLeave}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-label={menuAriaLabel}
|
||||||
|
>
|
||||||
|
<MaterialRipple ref={menuRipple.rippleRef} />
|
||||||
|
<span className={styles.menuIcon}>
|
||||||
|
<MaterialIcon name="expand_more" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{(open || isExiting) &&
|
||||||
|
menuPosition &&
|
||||||
|
createPortal(
|
||||||
|
<AnimatePresence onExitComplete={() => setIsExiting(false)}>
|
||||||
|
{open && (
|
||||||
|
<motion.div
|
||||||
|
key="menu"
|
||||||
|
ref={menuRef}
|
||||||
|
className={styles.menu}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
...(menuPosition.bottom != null
|
||||||
|
? { bottom: menuPosition.bottom }
|
||||||
|
: { top: menuPosition.top }),
|
||||||
|
left: menuPosition.left,
|
||||||
|
transform: menuPosition.transform,
|
||||||
|
maxHeight: menuPosition.maxHeight,
|
||||||
|
overflowY: "auto",
|
||||||
|
}}
|
||||||
|
initial={{ opacity: 0, y: -4 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -4 }}
|
||||||
|
transition={{ duration: 0.16, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
{menu}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
@use "../../../css/material" as *;
|
||||||
|
@use "../../../css/colors" as *;
|
||||||
|
@use "sass:color";
|
||||||
|
|
||||||
|
.downloadDialog {
|
||||||
|
color: $color-dark-on-surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadDialogContent {
|
||||||
|
padding: 24px 24px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconWrapper {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: $color-dark-primary-container;
|
||||||
|
color: $color-dark-on-primary-container;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleBlock {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $color-dark-on-surface-variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
.osName {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: $color-dark-on-surface-variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: $color-dark-on-surface-variant;
|
||||||
|
|
||||||
|
li + li {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 24px 20px;
|
||||||
|
border-top: 1px solid rgba($color-dark-outline-variant, 0.4);
|
||||||
|
background-color: color.mix($color-dark-surface-container, $color-dark-surface-container-low, 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.downloadDialogContent {
|
||||||
|
padding: 20px 16px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
padding-inline: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
@use "../../../css/material" as *;
|
||||||
|
|
||||||
|
$height: 40px;
|
||||||
|
$trailing-width: 48px; // 12 + 22 + 14 per spec
|
||||||
|
$between-space: 2px;
|
||||||
|
$outer-radius: calc(#{$height} / 2); // 20px
|
||||||
|
$inner-radius: 4px;
|
||||||
|
$inner-radius-hovered: 12px;
|
||||||
|
|
||||||
|
.splitButton {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: stretch;
|
||||||
|
position: relative;
|
||||||
|
border-radius: $outer-radius;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: $color-dark-on-primary;
|
||||||
|
isolation: isolate;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.38;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primarySegment,
|
||||||
|
.menuSegment {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
min-height: $height;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid $color-dark-primary;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
mdui-ripple {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.primarySegment {
|
||||||
|
border-top-left-radius: $outer-radius;
|
||||||
|
border-bottom-left-radius: $outer-radius;
|
||||||
|
border-top-right-radius: $inner-radius;
|
||||||
|
border-bottom-right-radius: $inner-radius;
|
||||||
|
padding-inline: 16px 12px;
|
||||||
|
transition: border-top-right-radius 0.18s ease-out, border-bottom-right-radius 0.18s ease-out;
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:hover {
|
||||||
|
border-top-right-radius: $inner-radius-hovered;
|
||||||
|
border-bottom-right-radius: $inner-radius-hovered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border-top-right-radius: $inner-radius-hovered;
|
||||||
|
border-bottom-right-radius: $inner-radius-hovered;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primaryContent {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.leadingIcon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.leadingIconIcon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuSegment {
|
||||||
|
width: $trailing-width;
|
||||||
|
border-top-right-radius: $outer-radius;
|
||||||
|
border-bottom-right-radius: $outer-radius;
|
||||||
|
border-top-left-radius: $inner-radius;
|
||||||
|
border-bottom-left-radius: $inner-radius;
|
||||||
|
margin-left: $between-space;
|
||||||
|
padding-inline: 12px 14px;
|
||||||
|
transition:
|
||||||
|
border-top-left-radius 0.18s ease-out,
|
||||||
|
border-bottom-left-radius 0.18s ease-out,
|
||||||
|
padding-inline 0.18s ease-out;
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:hover {
|
||||||
|
border-top-left-radius: $inner-radius-hovered;
|
||||||
|
border-bottom-left-radius: $inner-radius-hovered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border-top-left-radius: $inner-radius-hovered;
|
||||||
|
border-bottom-left-radius: $inner-radius-hovered;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuIcon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
font-size: 22px;
|
||||||
|
transition: transform 0.18s ease-out;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
mdui-icon {
|
||||||
|
width: inherit;
|
||||||
|
height: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-open="true"] .menuSegment {
|
||||||
|
$size: calc($trailing-width / 2);
|
||||||
|
border-radius: $size;
|
||||||
|
padding-inline: 13px 13px;
|
||||||
|
|
||||||
|
.menuIcon {
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.variantFilled {
|
||||||
|
.primarySegment, .menuSegment {
|
||||||
|
background-color: $color-dark-primary;
|
||||||
|
color: $color-dark-on-primary;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.variantTonal {
|
||||||
|
.primarySegment, .menuSegment {
|
||||||
|
background-color: $color-dark-primary-container;
|
||||||
|
color: $color-dark-on-primary-container;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.variantOutlined {
|
||||||
|
.primarySegment, .menuSegment {
|
||||||
|
border: 1px solid rgba($color-dark-outline, 0.8);
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: $color-dark-on-surface;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.variantElevated {
|
||||||
|
.primarySegment, .menuSegment {
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.3),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.15);
|
||||||
|
background-color: $color-dark-surface-container-low;
|
||||||
|
color: $color-dark-on-surface;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$menu-padding: 8px;
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
padding: $menu-padding;
|
||||||
|
min-width: 220px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: rgba($color-dark-surface-container-high, 0.4);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
z-index: 100000000;
|
||||||
|
|
||||||
|
// Custom slim semi-transparent scrollbar
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba($color-dark-on-surface, 0.25);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba($color-dark-on-surface, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba($color-dark-on-surface, 0.25) transparent;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="256" height="256" viewBox="0 0 256 256">
|
||||||
|
<line x1="1.407" y1="1.353" x2="1.407" y2="1.46" style="stroke: none; stroke-width: 2.81; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0, 0, 0); fill-rule: nonzero; opacity: 1;"/>
|
||||||
|
<g transform="matrix(1.329817, 0, 0, 1.329817, -42.024433, -38.371166)" style="">
|
||||||
|
<g transform="matrix(1, 0, 0, 1, -0.000001, 0.000008)">
|
||||||
|
<path d="M 187.533 167.601 C 184.614 169.278 180.394 175.015 176.028 176.522 L 173.703 176.879 C 173.692 176.878 173.682 176.878 173.671 176.877 C 170.911 176.954 167.048 175.793 164.888 173.621 C 164.62 173.351 164.378 173.064 164.167 172.763 C 163.081 170.76 162.591 168.959 162.078 167.519 C 163.681 138.029 142.006 97.682 141.345 89.031 L 140.535 89.081 L 143.379 87.758 C 146.633 86.246 146.577 81.601 143.289 80.165 L 126.072 72.64 C 124.802 72.084 123.338 72.196 122.169 72.941 L 109.131 81.222 C 106.644 82.801 106.537 86.395 108.928 88.118 L 120.16 96.21 C 121.382 97.093 122.987 97.244 124.353 96.607 L 131.993 93.054 L 124.35 96.609 C 122.984 97.244 121.38 97.093 120.157 96.213 L 112.629 90.787 L 112.182 90.815 C 112.182 96.766 92.754 126.354 85.61 153.184 C 82.2 144.438 85.83 134.593 92.372 122.051 C 83.667 131.338 75.779 146.462 83.605 158.699 C 87.499 164.679 93.454 168.284 99.138 171.235 C 103.255 173.904 113.854 176.441 110.918 182.885 C 110.925 182.876 110.931 182.867 110.938 182.859 C 110.931 182.869 110.925 182.88 110.918 182.89 C 108.679 187.681 104.866 190.521 100.012 192.077 C 98.585 189.825 96.894 187.456 94.937 184.967 L 90.627 179.198 C 89.739 177.993 88.66 176.155 87.266 173.682 C 85.934 171.212 84.729 169.31 83.779 167.978 C 82.956 166.519 81.624 165.061 79.912 163.603 C 79.401 163.15 78.861 162.743 78.299 162.386 C 75.967 160.899 72.963 160.587 70.538 161.919 C 69.819 162.313 69.262 162.771 68.883 163.288 C 67.998 164.493 67.489 165.825 67.363 167.219 C 67.172 168.551 66.792 169.439 66.157 169.883 C 66.152 169.886 66.141 169.891 66.135 169.894 C 65.64 163.51 67.408 157.038 70.867 151.489 C 73.595 147.111 75.664 142.699 76.428 138.228 C 76.779 130.993 78.71 123.934 82.113 117.038 C 86.159 107.861 95.837 101.976 98.644 92.659 C 102.474 80.66 83.964 28.579 126.067 28.984 C 150.53 28.984 159.32 50.152 159.32 84.644 C 159.32 107.391 201.316 122.18 187.533 167.601 Z M 151.486 211.429 C 151.801 212.025 152.146 212.581 152.52 213.101 C 136.094 207.361 120.468 206.548 104.498 212.435 C 105.326 211.043 105.778 209.435 105.778 207.781 C 105.778 207.779 105.778 207.778 105.778 207.776 C 105.778 207.761 105.778 207.746 105.778 207.731 C 105.778 207.729 105.778 207.727 105.778 207.725 C 105.778 204.739 104.88 201.359 103.085 197.582 C 107.83 198.93 113.536 199.677 120.393 199.677 C 136.171 200.012 146.507 196.345 152.958 190.163 C 151.666 200.295 149.855 207.938 151.486 211.429 Z M 168.963 155.718 C 167.694 156.465 166.457 157.278 165.336 158.247 C 164.063 159.306 162.925 160.573 162.122 161.99 C 164.732 160.051 167.618 158.932 170.555 157.952 L 170.855 157.851 C 172.058 156.221 173.112 154.49 173.983 152.649 C 179.496 140.288 172.1 126.454 162.265 118.749 C 168.908 130.233 175.535 143.047 168.963 155.718 Z M 110.325 80.46 L 121.571 73.32 C 121.829 72.168 121.972 70.94 121.972 69.664 C 121.972 63.111 118.283 57.8 113.731 57.8 C 109.179 57.8 105.489 63.111 105.489 69.664 C 105.489 74.469 107.476 78.594 110.325 80.46 Z M 140.237 78.831 C 142.075 76.656 143.244 73.357 143.244 69.664 C 143.244 63.111 139.555 57.8 135.002 57.8 C 130.45 57.8 126.761 63.111 126.761 69.664 C 126.761 70.859 126.887 72.008 127.115 73.095 Z" style="stroke: none; stroke-width: 2.81; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1; fill: rgb(255, 255, 255);" stroke-linecap="round"/>
|
||||||
|
<path d="M 97.708 188.673 C 97.691 188.648 97.674 188.626 97.658 188.6 C 97.677 188.626 97.691 188.651 97.708 188.673 Z" style="stroke: none; stroke-width: 2.81; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1; fill: rgb(255, 255, 255);" stroke-linecap="round"/>
|
||||||
|
<path d="M 178.569 117.583 L 162.265 118.749 C 173.339 131.799 177.532 144.523 169.998 156.693 C 166.446 158.264 163.822 160.028 162.122 161.987 C 162.189 163.875 162.178 165.735 162.08 167.551 C 161.65 166.34 161.203 165.379 160.377 164.766 C 160.374 164.774 160.368 164.786 160.363 164.794 C 161.832 165.882 162.13 168.073 163.274 170.855 C 163.316 170.956 163.353 171.052 163.397 171.156 C 163.619 171.676 163.867 172.21 164.167 172.763 C 166.07 175.489 170.571 176.97 173.676 176.883 C 174.039 176.905 174.404 176.88 174.767 176.829 C 174.882 176.812 175 176.781 175.115 176.759 C 175.365 176.708 175.615 176.649 175.863 176.571 C 175.995 176.529 176.124 176.481 176.256 176.43 C 176.495 176.34 176.734 176.236 176.973 176.124 C 177.099 176.065 177.223 176.006 177.349 175.941 C 177.616 175.801 177.883 175.646 178.147 175.48 C 178.237 175.424 178.327 175.376 178.414 175.317 C 178.768 175.087 179.122 174.84 179.471 174.576 C 179.532 174.531 179.591 174.48 179.651 174.432 C 179.943 174.208 180.229 173.977 180.516 173.738 C 180.628 173.646 180.738 173.55 180.848 173.454 C 181.086 173.249 181.325 173.041 181.558 172.831 C 181.668 172.732 181.778 172.631 181.887 172.533 C 182.14 172.302 182.39 172.069 182.638 171.836 C 182.713 171.766 182.789 171.693 182.865 171.622 C 183.202 171.302 183.537 170.987 183.863 170.675 C 184.141 170.411 184.413 170.153 184.68 169.903 C 184.773 169.818 184.86 169.737 184.95 169.655 C 185.155 169.467 185.358 169.282 185.557 169.107 C 185.658 169.017 185.759 168.933 185.861 168.849 C 186.043 168.694 186.223 168.545 186.397 168.408 C 186.496 168.329 186.594 168.253 186.69 168.18 C 186.869 168.045 187.046 167.924 187.218 167.812 C 187.297 167.761 187.378 167.705 187.454 167.66 C 187.482 167.643 187.513 167.621 187.538 167.604 C 194.434 144.871 187.358 129.81 178.569 117.583 Z" style="stroke: none; stroke-width: 2.81; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1; fill: rgb(255, 255, 255);" stroke-linecap="round"/>
|
||||||
|
<path d="M 97.708 188.676 C 97.708 188.676 97.708 188.676 97.708 188.676 C 97.711 188.679 97.711 188.682 97.714 188.685 C 97.714 188.685 97.714 188.685 97.714 188.685 C 98.54 189.845 99.296 190.975 99.995 192.079 C 104.857 190.525 108.676 187.684 110.918 182.888 C 119.916 168.72 59.028 169.675 92.372 122.051 L 80.221 121.306 C 79.123 124.043 78.257 126.806 77.622 129.593 C 77.611 129.638 77.6 129.683 77.589 129.728 C 77.299 131.021 77.069 132.322 76.883 133.628 C 76.849 133.864 76.821 134.103 76.79 134.339 C 76.625 135.629 76.493 136.924 76.431 138.228 C 75.666 142.699 73.598 147.111 70.87 151.489 C 70.46 152.149 70.075 152.823 69.715 153.506 C 69.414 154.08 69.136 154.661 68.872 155.248 C 68.827 155.344 68.776 155.44 68.734 155.535 C 68.127 156.915 67.616 158.328 67.208 159.767 C 66.273 163.066 65.876 166.488 66.141 169.894 C 66.146 169.891 66.157 169.886 66.163 169.883 C 66.798 169.439 67.177 168.551 67.369 167.219 C 67.495 165.825 68.004 164.493 68.889 163.288 C 69.268 162.771 69.824 162.313 70.544 161.919 C 72.969 160.587 75.973 160.899 78.305 162.386 C 78.867 162.743 79.407 163.15 79.918 163.603 C 81.629 165.061 82.961 166.519 83.785 167.978 C 84.734 169.31 85.94 171.212 87.272 173.682 C 88.666 176.155 89.745 177.993 90.633 179.198 L 94.943 184.967 C 95.688 185.914 96.373 186.836 97.037 187.746 C 97.261 188.058 97.492 188.37 97.708 188.676 Z" style="stroke: none; stroke-width: 2.81; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1; fill: rgb(255, 255, 255);" stroke-linecap="round"/>
|
||||||
|
<path d="M 122.169 72.941 C 118.834 75.093 115.509 77.26 112.241 79.513 L 109.8 81.219 C 107.29 82.543 107.108 86.283 109.592 87.601 C 110.213 88.014 120.685 94.772 120.823 94.921 C 121.686 95.556 122.672 95.629 123.597 95.235 C 123.597 95.235 128.913 92.698 128.913 92.698 C 132.521 91.046 136.14 89.416 139.799 87.879 C 140.951 87.263 143.674 86.679 144.295 85.44 C 145.102 84.229 144.885 82.292 143.618 81.301 C 140.94 79.665 136.031 77.493 133.153 75.984 C 133.153 75.984 127.767 73.449 127.767 73.449 C 126.033 72.567 123.979 71.716 122.169 72.941 Z M 122.169 72.941 C 123.948 71.676 126.072 72.486 127.817 73.337 C 127.817 73.337 133.339 75.568 133.339 75.568 C 136.525 76.889 141.575 78.485 144.585 79.985 C 147.94 82.014 147.237 87.637 143.626 89.039 C 139.26 91.341 134.738 93.535 130.282 95.634 C 127.727 96.584 123.675 99.481 120.826 98.191 C 118.682 97.396 110.668 90.321 108.647 88.91 C 105.613 86.881 106.132 81.958 109.417 80.618 C 109.417 80.618 112 79.134 112 79.134 C 115.428 77.13 118.803 75.043 122.169 72.941 Z" style="stroke: none; stroke-width: 2.81; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1; fill: rgb(255, 255, 255);" stroke-linecap="round"/>
|
||||||
|
<path d="M 137.309 83.658 C 129.815 90.793 116.833 92.487 108.451 85.83 C 118.092 89.511 128.081 88.126 137.309 83.658 Z" style="stroke: none; stroke-width: 2.81; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1; fill: rgb(255, 255, 255);" stroke-linecap="round"/>
|
||||||
|
<path d="M 135.177 76.619 C 136.199 75.787 136.995 74.354 137.234 72.634 C 137.638 69.737 136.337 67.158 134.322 66.877 C 132.31 66.596 130.349 68.717 129.942 71.614 C 129.801 72.612 129.877 73.562 130.102 74.402 L 135.177 76.619 Z" style="stroke: none; stroke-width: 2.81; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1; fill: rgb(255, 255, 255);" stroke-linecap="round"/>
|
||||||
|
<path d="M 115.226 77.35 L 118.477 75.284 C 118.94 74.267 119.14 72.98 118.949 71.612 C 118.544 68.715 116.583 66.593 114.568 66.874 C 112.556 67.155 111.252 69.735 111.657 72.632 C 112.008 75.138 113.523 77.055 115.226 77.35 Z" style="stroke: none; stroke-width: 2.81; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1; fill: rgb(255, 255, 255);" stroke-linecap="round"/>
|
||||||
|
<path d="M 59.565 208.06 C 67.843 208.624 75.959 211.078 83.813 213.494 C 87.536 214.444 91.254 216.223 95.12 215.916 C 110.114 213.418 101.209 197.528 95.48 190.025 C 91.948 185.386 87.606 179.844 84.661 174.59 C 82.773 171.676 80.913 167.472 77.864 165.353 C 76.043 163.476 71.974 162.855 70.552 165.308 C 69.26 167.528 70.055 170.085 67.534 171.853 C 66.008 172.746 64.775 172.898 63.246 173.241 C 63.03 173.227 61.69 173.283 61.453 173.275 C 58.157 173.502 53.65 172.238 52.563 176.27 C 51.548 180.165 53.532 184.234 53.389 188.345 C 53.512 192.045 51.677 195.729 49.702 198.539 C 48.876 199.939 48.05 201.462 47.923 202.836 C 47.923 202.836 47.895 202.625 47.895 202.625 C 48.39 204.522 51 205.533 52.799 206.213 C 54.976 206.989 57.264 207.565 59.565 208.043 C 57.219 207.877 54.858 207.683 52.526 207.127 C 50.104 206.433 47.33 205.812 46.257 203.049 C 46.176 198.978 48.98 195.946 49.725 192.984 C 51.425 187.937 48.719 183.45 48.387 178.296 C 48.379 169.56 54.704 168.554 61.099 168.652 C 61.099 168.652 62.988 168.554 62.988 168.554 L 62.49 168.607 C 63.215 168.411 64.415 168.155 64.924 167.826 C 64.362 168.315 64.946 167.716 64.949 166.739 C 65.267 162.259 69.116 158.446 73.638 158.494 C 76.42 158.37 79.069 159.514 81.166 161.259 C 84.762 163.752 87.005 168.211 89.362 171.945 C 94.294 181.041 102.184 188.401 106.051 198.258 C 110.586 208.352 107.428 218.445 95.457 219.623 C 91.046 219.825 86.937 217.515 82.888 216.127 C 75.248 212.887 67.68 209.875 59.565 208.06 Z" style="stroke: none; stroke-width: 2.81; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1; fill: rgb(255, 255, 255);" stroke-linecap="round"/>
|
||||||
|
<path d="M 173.676 176.883 C 170.04 177.085 166.101 176.093 163.628 173.137 C 162.616 171.504 161.905 169.658 161.242 167.975 C 160.756 166.418 159.579 165.201 158.46 165.297 C 154.203 166.106 156.179 175.222 155.81 178.734 C 155.754 184.413 155.22 190.07 154.523 195.628 C 154.133 200.068 152.424 206.837 153.947 210.561 C 155.895 214.048 159.787 216.161 163.743 215.908 C 166.519 215.635 168.883 213.823 171.541 212.008 C 178.285 206.981 184.38 201.231 191.84 196.904 C 194.948 195.108 198.368 193.566 201.861 192.501 C 203.075 192.231 204.952 190.952 205.103 190.143 C 205.053 189.705 204.471 189.193 204.145 188.879 C 201.195 186.423 195.49 184.627 193.473 182.365 C 190.983 180.314 189.73 176.486 189.986 173.477 C 189.927 170.948 189.772 167.112 188.134 168.394 C 183.601 170.858 179.6 177.209 173.676 176.883 C 176.63 176.759 178.805 174.629 180.834 172.746 C 183.436 170.431 189.337 161.785 191.947 168.818 C 193.237 172.586 192.152 177.414 195.718 179.766 C 196.949 180.974 200.987 182.446 202.777 183.326 C 209.203 185.979 212.499 191.309 205.674 195.839 C 201.861 197.685 197.972 198.767 194.414 201.184 C 187.519 205.522 181.325 211.283 174.736 216.332 C 171.822 218.443 168.059 221.036 163.892 221.219 C 157.221 221.539 149.907 217.024 148.541 210.28 C 147.889 205.092 149.28 199.972 149.974 194.959 C 150.904 189.491 151.747 184.065 152.079 178.594 C 152.748 173.682 151.132 163.049 158.578 162.858 C 163.322 163.372 162.987 169.245 164.743 172.446 C 166.393 174.955 170.386 176.7 173.676 176.883 Z" style="stroke: none; stroke-width: 2.81; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1; fill: rgb(255, 255, 255);" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="200px" height="200px" viewBox="0 0 200 200">
|
||||||
|
<path d="M 176.226 68.183 C 175.066 69.083 154.581 80.625 154.581 106.291 C 154.581 135.977 180.646 146.48 181.427 146.739 C 181.307 147.38 177.286 161.122 167.684 175.125 C 159.122 187.448 150.18 199.75 136.577 199.75 C 122.974 199.75 119.474 191.849 103.771 191.849 C 88.467 191.849 83.027 200.011 70.583 200.011 C 58.141 200.011 49.46 188.608 39.478 174.605 C 27.915 158.162 18.573 132.617 18.573 108.372 C 18.573 69.484 43.858 48.859 68.744 48.859 C 81.966 48.859 92.988 57.541 101.29 57.541 C 109.191 57.541 121.514 48.339 136.557 48.339 C 142.259 48.339 162.743 48.859 176.226 68.183 Z M 129.416 31.876 C 135.637 24.494 140.038 14.252 140.038 4.01 C 140.038 2.59 139.918 1.149 139.658 -0.011 C 129.536 0.369 117.493 6.73 110.232 15.152 C 104.531 21.634 99.209 31.876 99.209 42.257 C 99.209 43.818 99.47 45.379 99.59 45.879 C 100.23 45.998 101.27 46.139 102.31 46.139 C 111.393 46.139 122.815 40.057 129.416 31.876 Z" style="fill: rgb(255, 255, 255);" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0" y="0" width="95" height="95" fill="#ffffff"/>
|
||||||
|
<rect x="105" y="0" width="95" height="95" fill="#ffffff"/>
|
||||||
|
<rect x="0" y="105" width="95" height="95" fill="#ffffff"/>
|
||||||
|
<rect x="105" y="105" width="95" height="95" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 318 B |
@@ -15,7 +15,7 @@ export function HomeFooter() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.footerLinks}>
|
<div className={styles.footerLinks}>
|
||||||
<div className={styles.footerSection}>
|
<div className={styles.footerSection}>
|
||||||
<Link to="/download" className={styles.footerLink}>
|
<Link to="/#download" className={styles.footerLink}>
|
||||||
<MaterialIcon name="download" className={styles.footerLinkIcon} />
|
<MaterialIcon name="download" className={styles.footerLinkIcon} />
|
||||||
Скачать приложение
|
Скачать приложение
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import type { ReactNode } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
import styles from "./home.module.scss";
|
import styles from "./home.module.scss";
|
||||||
import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen";
|
import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen";
|
||||||
import { MaterialButton, MaterialIcon } from "@/utils/material";
|
import { MaterialButton, MaterialIcon, MaterialList, MaterialListItem } from "@/utils/material";
|
||||||
import generalChatScreenshot from "../../images/screenshots/general-chat.png";
|
import generalChatScreenshot from "../../images/screenshots/general-chat.png";
|
||||||
import dmScreenshot from "../../images/screenshots/dm.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 { HomeHeader } from "./HomeHeader";
|
||||||
import { HomeFooter } from "./HomeFooter";
|
import { HomeFooter } from "./HomeFooter";
|
||||||
import { OS_CONFIG, ALL_OS } from "@/core/downloads/os";
|
import { SplitButton } from "@/core/components/SplitButton";
|
||||||
|
import { DownloadDialog } from "@/core/components/DownloadDialog";
|
||||||
|
import { OS_CONFIG, ALL_OS, detectOs, type DownloadOs } from "@/core/downloads/os";
|
||||||
|
|
||||||
interface FeatureSectionProps {
|
interface FeatureSectionProps {
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
@@ -47,6 +52,28 @@ export default function HomePage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isMobile } = useDownloadAppScreen();
|
const { isMobile } = useDownloadAppScreen();
|
||||||
|
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [dialogOs, setDialogOs] = useState<DownloadOs>(() => detectOs());
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const triggerDownload = (os: DownloadOs): boolean => {
|
||||||
|
if (typeof document === "undefined") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDialogOs(os);
|
||||||
|
setDialogOpen(true);
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = `/api/download/${os}`;
|
||||||
|
link.download = "";
|
||||||
|
link.style.display = "none";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.homepage}>
|
<div className={styles.homepage}>
|
||||||
<HomeHeader />
|
<HomeHeader />
|
||||||
@@ -70,13 +97,39 @@ export default function HomePage() {
|
|||||||
Открыть веб-версию
|
Открыть веб-версию
|
||||||
</MaterialButton>
|
</MaterialButton>
|
||||||
)}
|
)}
|
||||||
<MaterialButton
|
<SplitButton
|
||||||
variant={isMobile ? "filled" : "outlined"}
|
variant={isMobile ? "filled" : "tonal"}
|
||||||
onClick={() => navigate("/download-app")}
|
text="Скачать приложение"
|
||||||
icon="download"
|
icon="download"
|
||||||
|
onPrimaryClick={() => triggerDownload(detectOs())}
|
||||||
|
menuOpen={menuOpen}
|
||||||
|
onMenuOpen={setMenuOpen}
|
||||||
|
menu={(
|
||||||
|
<MaterialList>
|
||||||
|
{ALL_OS.map((os) => (
|
||||||
|
<MaterialListItem
|
||||||
|
key={os}
|
||||||
|
icon={["windows", "linux", "macos"].includes(os) ? undefined : OS_CONFIG[os].icon}
|
||||||
|
headline={OS_CONFIG[os].label}
|
||||||
|
rounded
|
||||||
|
onClick={() => {
|
||||||
|
if (triggerDownload(os)) setMenuOpen(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Скачать приложение
|
{["windows", "linux", "macos"].includes(os) && (
|
||||||
</MaterialButton>
|
<span
|
||||||
|
slot="icon"
|
||||||
|
className={styles.menuCustomIcon}
|
||||||
|
style={{
|
||||||
|
"--menu-custom-icon-url": `url("${os === "windows" ? windowsIcon : os === "linux" ? linuxIcon : macIcon}")`,
|
||||||
|
} as React.CSSProperties}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</MaterialListItem>
|
||||||
|
))}
|
||||||
|
</MaterialList>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -95,7 +148,7 @@ export default function HomePage() {
|
|||||||
</FeatureSection>
|
</FeatureSection>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className={styles.download}>
|
<section id="download" className={styles.download}>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.downloadContent}>
|
<div className={styles.downloadContent}>
|
||||||
<h3>Скачайте приложение</h3>
|
<h3>Скачайте приложение</h3>
|
||||||
@@ -122,17 +175,13 @@ export default function HomePage() {
|
|||||||
{OS_CONFIG[os].description}
|
{OS_CONFIG[os].description}
|
||||||
</span>
|
</span>
|
||||||
<div className={styles.downloadTableAction}>
|
<div className={styles.downloadTableAction}>
|
||||||
<a
|
|
||||||
href={`/api/download/${os}`}
|
|
||||||
className={styles.downloadTableActionLink}
|
|
||||||
>
|
|
||||||
<MaterialButton
|
<MaterialButton
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
icon="download"
|
icon="download"
|
||||||
|
onClick={() => triggerDownload(os)}
|
||||||
>
|
>
|
||||||
Скачать
|
Скачать
|
||||||
</MaterialButton>
|
</MaterialButton>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -174,6 +223,7 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<DownloadDialog open={dialogOpen} onOpenChange={setDialogOpen} os={dialogOs} />
|
||||||
<HomeFooter />
|
<HomeFooter />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -339,6 +339,21 @@ $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;
|
||||||
|
}
|
||||||
|
|
||||||
// Keyframes for animations
|
// Keyframes for animations
|
||||||
@keyframes neonGlow {
|
@keyframes neonGlow {
|
||||||
0% {
|
0% {
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ import 'mdui/components/badge';
|
|||||||
import "mdui/mdui.css";
|
import "mdui/mdui.css";
|
||||||
import 'mdui/components/circular-progress';
|
import 'mdui/components/circular-progress';
|
||||||
|
|
||||||
|
import { useCallback, useRef } from "react";
|
||||||
import { setColorScheme } from 'mdui/functions/setColorScheme';
|
import { setColorScheme } from 'mdui/functions/setColorScheme';
|
||||||
import type { ChangeEventHandler, ComponentProps, ComponentPropsWithoutRef, FormEventHandler, Ref } from 'react';
|
import type { ChangeEventHandler, ComponentProps, ComponentPropsWithoutRef, FormEventHandler, Ref, RefObject } from 'react';
|
||||||
import type { TextField } from 'mdui/components/text-field';
|
import type { TextField } from 'mdui/components/text-field';
|
||||||
import type { Switch } from 'mdui/components/switch';
|
import type { Switch } from 'mdui/components/switch';
|
||||||
import type { Override } from '@/core/types';
|
import type { Override } from '@/core/types';
|
||||||
@@ -145,8 +146,67 @@ export function MaterialBottomAppBar(props: MaterialBottomAppBarProps) {
|
|||||||
return <mdui-bottom-app-bar {...props as ComponentProps<"mdui-bottom-app-bar">} />
|
return <mdui-bottom-app-bar {...props as ComponentProps<"mdui-bottom-app-bar">} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MaterialRippleProps = BasePropCustomization<"div", MDUIRipple>;
|
export type MaterialRippleProps = NoChildren<BasePropCustomization<"mdui-ripple", MDUIRipple>>;
|
||||||
export function MaterialRipple(props: MaterialRippleProps) {
|
export function MaterialRipple(props: MaterialRippleProps) {
|
||||||
// Wrapper component for future custom ripple usage; MDUI buttons already include real ripple.
|
return <mdui-ripple {...props as ComponentProps<"mdui-ripple">} />
|
||||||
return <div {...props as ComponentProps<"div">} />;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns pointer handlers that forward press and hover events to an mdui-ripple element.
|
||||||
|
* Pass the returned ref to MaterialRipple and spread the handlers onto the container (e.g. button).
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { rippleRef, ...rippleHandlers } = useRippleHandlers(disabled);
|
||||||
|
* <button {...rippleHandlers}>
|
||||||
|
* <MaterialRipple ref={rippleRef} />
|
||||||
|
* ...
|
||||||
|
* </button>
|
||||||
|
*/
|
||||||
|
export function useRippleHandlers(
|
||||||
|
disabled = false
|
||||||
|
): {
|
||||||
|
rippleRef: RefObject<MDUIRipple | null>;
|
||||||
|
onPointerDown: (e: React.PointerEvent) => void;
|
||||||
|
onPointerEnter: (e: React.PointerEvent) => void;
|
||||||
|
onPointerLeave: (e: React.PointerEvent) => void;
|
||||||
|
} {
|
||||||
|
const rippleRef = useRef<MDUIRipple | null>(null);
|
||||||
|
|
||||||
|
const onPointerDown = useCallback(
|
||||||
|
(e: React.PointerEvent) => {
|
||||||
|
if (disabled || e.button !== 0) return;
|
||||||
|
const ripple = rippleRef.current;
|
||||||
|
if (!ripple?.startPress) return;
|
||||||
|
ripple.startPress(e.nativeEvent);
|
||||||
|
const btn = e.currentTarget as HTMLElement;
|
||||||
|
const endPress = () => {
|
||||||
|
ripple.endPress?.();
|
||||||
|
btn.removeEventListener("pointerup", endPress);
|
||||||
|
btn.removeEventListener("pointercancel", endPress);
|
||||||
|
btn.removeEventListener("pointerleave", endPress);
|
||||||
|
};
|
||||||
|
btn.addEventListener("pointerup", endPress);
|
||||||
|
btn.addEventListener("pointercancel", endPress);
|
||||||
|
btn.addEventListener("pointerleave", endPress);
|
||||||
|
},
|
||||||
|
[disabled]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onPointerEnter = useCallback(
|
||||||
|
(e: React.PointerEvent) => {
|
||||||
|
if (disabled || e.pointerType !== "mouse") return;
|
||||||
|
rippleRef.current?.startHover?.();
|
||||||
|
},
|
||||||
|
[disabled]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onPointerLeave = useCallback(
|
||||||
|
(e: React.PointerEvent) => {
|
||||||
|
if (disabled || e.pointerType !== "mouse") return;
|
||||||
|
rippleRef.current?.endHover?.();
|
||||||
|
},
|
||||||
|
[disabled]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { rippleRef, onPointerDown, onPointerEnter, onPointerLeave };
|
||||||
}
|
}
|
||||||
Vendored
+6
@@ -8,6 +8,12 @@ declare global {
|
|||||||
interface SyntheticEvent<T = Element, E = Event> {
|
interface SyntheticEvent<T = Element, E = Event> {
|
||||||
target: EventTarget & T;
|
target: EventTarget & T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
"mdui-ripple": DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Augment DOM event listeners to provide typed target for ALL elements
|
// Augment DOM event listeners to provide typed target for ALL elements
|
||||||
|
|||||||
Reference in New Issue
Block a user