mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-23 03:25:07 +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;
|
||||
}
|
||||
Reference in New Issue
Block a user