feat(player): PC 侧栏布局、顶栏与页内间距优化
Some checks failed
lotteryfront CI / build (push) Has been cancelled

- 桌面端侧栏 + 全局顶栏(Logo 在侧栏顶、激活态对齐后台红底白字)
- 移除各页重复页内顶栏,统一主区与卡片顶部留白
- 视口遮罩/弹层约束到主内容区,移动端保留底栏与顶栏 Logo
This commit is contained in:
2026-06-26 18:02:19 +08:00
parent e5622c58cb
commit 7d03ffa30b
24 changed files with 394 additions and 181 deletions

View File

@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { playerViewportPopoverMaxClass } from "@/lib/player-viewport";
import { cn } from "@/lib/utils";
type CurrencySwitcherProps = {
@@ -109,7 +110,8 @@ export function CurrencySwitcher({
side="bottom"
sideOffset={6}
className={cn(
"z-[200] w-auto min-w-[min(100vw-2rem,200px)] max-w-[min(100vw-2rem,280px)] p-1 text-gray-900",
"z-[200] w-auto min-w-[11rem] p-1 text-gray-900",
playerViewportPopoverMaxClass,
styles.dropdown,
)}
>

View File

@@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { normalizeLanguage, SUPPORTED_LANGUAGES, type AppLanguage } from "@/i18n";
import { playerViewportPopoverMaxClass } from "@/lib/player-viewport";
import { cn } from "@/lib/utils";
interface LanguageSwitcherProps {
@@ -104,7 +105,8 @@ export function LanguageSwitcher({
side="bottom"
sideOffset={6}
className={cn(
"z-[200] w-auto min-w-[min(100vw-2rem,220px)] max-w-[min(100vw-2rem,280px)] p-1 text-gray-900",
"z-[200] w-auto min-w-[12rem] p-1 text-gray-900",
playerViewportPopoverMaxClass,
styles.dropdown,
)}
>

View File

@@ -2,48 +2,65 @@
import type { ReactNode } from "react";
import { NetworkStatusBanner } from "@/components/network-status-banner";
import { PlayerDesktopHeader } from "@/components/layout/player-desktop-header";
import { PlayerBottomNav } from "@/components/layout/player-bottom-nav";
import { PlayerSidebar } from "@/components/layout/player-sidebar";
import { NetworkStatusBanner } from "@/components/network-status-banner";
import { PullToRefreshIndicator } from "@/components/pull-to-refresh-indicator";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
import { playerMainInset } from "@/lib/player-spacing";
type PlayerAppShellProps = {
children: ReactNode;
};
/**
* 玩家端外壳:顶栏(品牌 + 会话)+ 主体 + **底部 Tab 导航**H5
* 底部栏留白:{@link PlayerBottomNav} 作为 sticky 元素放在底部main 只需要基础 padding。
*
* 注意:全局离线横幅和服务器错误覆盖层已在 ErrorProvider 中处理
* 这里的 NetworkStatusBanner 仅用于 WebSocket 状态显示
* 玩家端外壳:
* - 移动:主体 + 底部 TabH5
* - 桌面:侧栏 + 顶栏 + 宽主内容区UI 设计稿)
*/
export function PlayerAppShell({ children }: PlayerAppShellProps): ReactNode {
const isMobile = useIsMobile();
const { pullDistance, isRefreshing } = usePullToRefresh({
onRefresh: () => {
window.dispatchEvent(new CustomEvent("lottery-hall-refresh"));
window.dispatchEvent(new CustomEvent("lottery-wallet-refresh"));
},
threshold: 70,
enabled: isMobile,
});
return (
<div className="flex h-full min-h-0 flex-1 flex-col overflow-hidden bg-white text-foreground">
<PullToRefreshIndicator
pullDistance={pullDistance}
isRefreshing={isRefreshing}
threshold={70}
/>
<NetworkStatusBanner />
<main
id="player-scroll-container"
className="flex w-full min-h-0 flex-1 flex-col overflow-y-auto overscroll-y-contain pb-3"
>
{children}
</main>
<div className="shrink-0 w-full relative z-50">
<PlayerBottomNav />
<div className="flex h-full min-h-0 flex-1 overflow-hidden bg-white text-foreground lg:flex-row">
<PlayerSidebar />
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{isMobile ? (
<PullToRefreshIndicator
pullDistance={pullDistance}
isRefreshing={isRefreshing}
threshold={70}
/>
) : null}
<NetworkStatusBanner />
<PlayerDesktopHeader />
<main
id="player-scroll-container"
className={cn(
"flex min-h-0 w-full flex-1 flex-col overflow-y-auto overscroll-y-contain bg-[#f8fafc] lg:bg-[#f0f2f6]",
playerMainInset,
)}
>
{children}
</main>
<div className="relative z-50 w-full shrink-0 lg:hidden">
<PlayerBottomNav />
</div>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,45 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { LanguageSwitcher } from "@/components/language-switcher";
import { PlayerNotificationBell } from "@/components/layout/player-notification-bell";
import { cn } from "@/lib/utils";
type PlayerDesktopHeaderProps = {
className?: string;
};
/** 全局顶栏:移动显示 Logo桌面 Logo 在侧栏顶部,此处仅语言 + 通知 */
export function PlayerDesktopHeader({ className }: PlayerDesktopHeaderProps) {
return (
<header
className={cn(
"flex h-12 shrink-0 items-center justify-between border-b border-[#e4ebf5] bg-white px-3 lg:h-14 lg:justify-end lg:px-6",
className,
)}
>
<Link href="/hall" className="inline-flex min-w-0 items-center lg:hidden" aria-label="N lotto">
<Image
src="/logo.png"
alt="N lotto"
width={243}
height={84}
className="h-7 w-auto max-w-[160px] object-contain object-left"
priority
/>
</Link>
<div className="flex shrink-0 items-center gap-2">
<LanguageSwitcher
variant="minimal"
menuAlign="end"
showFlag={false}
className="rounded-full border border-[#e4eaf4] bg-[#f8fafc] [&_button]:h-8 [&_button]:gap-1 [&_button]:px-2 [&_button]:text-xs lg:[&_button]:h-9 lg:[&_button]:gap-1.5 lg:[&_button]:px-3 lg:[&_button]:text-sm"
/>
<PlayerNotificationBell className="inline-flex size-9 items-center justify-center rounded-full border border-[#e4eaf4] bg-[#f8fafc] text-[#0b3f96]" />
</div>
</header>
);
}

View File

@@ -3,7 +3,8 @@ import type { ReactNode } from "react";
import { OfflineBanner } from "@/components/offline-banner";
import {
playerSafeAreaTopClass,
playerViewportColumnClass,
playerViewportOuterClass,
playerViewportRootClass,
} from "@/lib/player-viewport";
import { cn } from "@/lib/utils";
@@ -13,25 +14,21 @@ type PlayerMobileViewportProps = {
};
/**
* 玩家端根容器:真机全宽;桌面浏览器居中为固定宽度的 H5 画布,避免 PC 打开时 UI 横向拉满
* 玩家端根容器:移动居中 480px 画布;桌面全宽并配合侧栏壳层(见 UI 设计稿)
*/
export function PlayerMobileViewport({
children,
className,
}: PlayerMobileViewportProps): ReactNode {
return (
<div className="h-[100dvh] bg-[#e8ecf3] overflow-hidden">
<div className={playerViewportOuterClass}>
<div
className={cn(
playerViewportColumnClass,
playerSafeAreaTopClass,
"relative flex h-full min-h-0 flex-col bg-white shadow-none sm:shadow-[0_0_48px_rgba(15,23,42,0.1)]",
className,
)}
className={cn(playerViewportRootClass, playerSafeAreaTopClass, className)}
style={{ "--player-viewport-max": "480px" } as React.CSSProperties}
>
<OfflineBanner />
{children}
</div>
</div>
);
}
}

View File

@@ -1,12 +1,30 @@
"use client";
import { Bell } from "lucide-react";
import Link from "next/link";
import { type ReactElement } from "react";
import { cn } from "@/lib/utils";
type PlayerNotificationBellProps = {
className?: string;
};
/** 顶栏铃铛:暂隐藏,待接入通用通知中心后再开放 */
export function PlayerNotificationBell(_props: PlayerNotificationBellProps): ReactElement | null {
return null;
}
/** 顶栏铃铛:跳转通知页(完整通知中心待后续接入) */
export function PlayerNotificationBell({
className,
}: PlayerNotificationBellProps): ReactElement {
return (
<Link
href="/notifications"
className={cn("relative text-[#0b3f96]", className)}
aria-label="Notifications"
>
<Bell className="size-[1.125rem]" aria-hidden />
<span
className="absolute right-1 top-1 size-2 rounded-full bg-[#f10b32] ring-2 ring-white"
aria-hidden
/>
</Link>
);
}

View File

@@ -1,21 +1,14 @@
"use client";
import Link from "next/link";
import type { ReactNode } from "react";
import { ChevronLeft } from "lucide-react";
import { useTranslation } from "react-i18next";
import { LanguageSwitcher } from "@/components/language-switcher";
import { usePlayerStickyTopStyle } from "@/hooks/use-player-sticky-top-style";
import {
playerHeaderControl,
playerPageHeader,
} from "@/lib/player-spacing";
import { playerPageShellPadding } from "@/lib/player-spacing";
import { playerContentColumnClass } from "@/lib/player-viewport";
import { cn } from "@/lib/utils";
type PlayerPanelProps = {
title: string;
/** 保留供调用方语义标注,不再渲染页内顶栏(导航由外壳侧栏/顶栏承担) */
title?: string;
children: ReactNode;
backHref?: string;
backLabel?: string;
@@ -24,65 +17,21 @@ type PlayerPanelProps = {
};
export function PlayerPanel({
title,
children,
backHref = "/hall",
backLabel,
className,
containerClassName,
}: PlayerPanelProps) {
const { t: tp } = useTranslation("player");
const resolvedBackLabel = backLabel ?? tp("panel.home");
const stickyTopStyle = usePlayerStickyTopStyle();
return (
<div className={cn("mx-auto w-full max-w-[480px]", containerClassName)}>
<div className={cn(playerContentColumnClass, containerClassName)}>
<section
className={cn(
"bg-white text-slate-900",
"px-3 pb-6",
"bg-white text-slate-900 lg:rounded-2xl lg:border lg:border-[#e4ebf5] lg:shadow-sm",
playerPageShellPadding,
className,
)}
>
<header
className={cn(
playerPageHeader,
"sticky z-50 grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 bg-white/95 pt-2 pb-2 -mx-3 px-3 backdrop-blur",
)}
style={stickyTopStyle}
>
<div className="flex min-w-0 justify-start">
<Link
href={backHref}
className={cn(
playerHeaderControl,
"gap-0.5 rounded-full border border-[#e4eaf4] bg-[#f8fafc] px-2 text-xs font-bold text-[#0b3f96] hover:bg-[#f1f6ff]",
)}
aria-label={resolvedBackLabel}
>
<ChevronLeft className="size-4 shrink-0" aria-hidden />
<span className="max-w-[5.5rem] truncate">{resolvedBackLabel}</span>
</Link>
</div>
<h1 className="min-w-0 truncate px-1 text-center text-base font-black leading-tight text-[#0b3f96]">
{title}
</h1>
<div className="flex min-w-0 items-center justify-end gap-0.5">
<LanguageSwitcher
variant="minimal"
menuAlign="end"
showFlag={false}
className={cn(
playerHeaderControl,
"rounded-full border border-[#e4eaf4] bg-[#f8fafc] [&_button]:h-8 [&_button]:gap-1 [&_button]:px-2 [&_button]:py-0 [&_button]:text-xs",
)}
/>
</div>
</header>
{children}
</section>
</div>
);
}
}

View File

@@ -0,0 +1,118 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
BarChart3,
BookOpen,
ClipboardList,
Home,
Wallet,
} from "lucide-react";
import { useTranslation } from "react-i18next";
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
import { cn } from "@/lib/utils";
import { usePlayerSessionStore } from "@/stores/player-session-store";
const navItems = [
{
href: "/hall",
labelKey: "nav.home",
labelDefault: "首页",
icon: Home,
match: (p: string) => p === "/hall",
},
{
href: "/results",
labelKey: "nav.results",
labelDefault: "开奖结果",
icon: BarChart3,
match: (p: string) => p === "/results" || p.startsWith("/results/"),
},
{
href: "/orders",
labelKey: "nav.orders",
labelDefault: "我的注单",
icon: ClipboardList,
match: (p: string) => p === "/orders" || p.startsWith("/orders/"),
},
{
href: "/wallet",
labelKey: "nav.wallet",
creditLabelKey: "nav.credit",
labelDefault: "钱包",
creditLabelDefault: "信用",
icon: Wallet,
match: (p: string) => p === "/wallet" || p.startsWith("/wallet/"),
},
{
href: "/rules",
labelKey: "nav.rules",
labelDefault: "规则",
icon: BookOpen,
match: (p: string) => p === "/rules",
},
] as const;
/** 与 lotteryadmin `NAV_BTN` / `NAV_ACTIVE` 一致 */
const playerNavLinkClass =
"flex h-8 w-full items-center gap-2 rounded-md px-2.5 text-[13px] leading-snug font-normal text-sidebar-foreground/90 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0";
const playerNavLinkActiveClass =
"bg-sidebar-primary/90 font-medium text-sidebar-primary-foreground shadow-sm hover:bg-sidebar-primary/90 hover:text-sidebar-primary-foreground";
/** 桌面端左侧导航(激活态对齐后台侧栏) */
export function PlayerSidebar() {
const pathname = usePathname() ?? "";
const { t } = useTranslation("player");
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
return (
<aside
className="player-sidebar hidden h-full w-56 shrink-0 flex-col border-r border-[#e4ebf5] bg-white lg:flex"
aria-label={t("nav.aria")}
>
<div className="shrink-0 border-b border-[#e4ebf5] px-2 py-2">
<Link href="/hall" className="flex h-10 items-center px-1" aria-label="N lotto">
<Image
src="/logo.png"
alt="N lotto"
width={243}
height={84}
className="h-auto max-h-10 w-full object-contain object-left"
priority
/>
</Link>
</div>
<nav className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto px-1.5 py-1.5">
{navItems.map(({ href, labelKey, labelDefault, icon: Icon, match, ...item }) => {
const active = match(pathname);
const creditLabelKey = "creditLabelKey" in item ? item.creditLabelKey : undefined;
const creditLabelDefault =
"creditLabelDefault" in item ? item.creditLabelDefault : undefined;
const label =
creditMode && creditLabelKey
? t(creditLabelKey, { defaultValue: creditLabelDefault ?? labelDefault })
: t(labelKey, { defaultValue: labelDefault });
return (
<Link
key={href}
href={href}
prefetch
aria-current={active ? "page" : undefined}
className={cn(playerNavLinkClass, active && playerNavLinkActiveClass)}
>
<Icon aria-hidden />
<span className="truncate">{label}</span>
</Link>
);
})}
</nav>
</aside>
);
}

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { usePlayerBannerHeightRef } from "@/hooks/use-player-banner-height-ref";
import { useWebSocketManager } from "@/hooks/use-websocket-manager";
import { playerViewportColumnClass } from "@/lib/player-viewport";
import { playerContentColumnClass } from "@/lib/player-viewport";
import { useErrorStore } from "@/stores/error-store";
import { cn } from "@/lib/utils";
@@ -43,7 +43,7 @@ export function NetworkStatusBanner(): React.ReactElement | null {
<div ref={bannerRef} className="sticky top-0 z-50 w-full">
<div
className={cn(
playerViewportColumnClass,
playerContentColumnClass,
"flex items-center justify-center gap-2 px-4 py-2 text-sm",
"animate-in slide-in-from-top-2 duration-200",
)}

View File

@@ -5,7 +5,7 @@ import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { usePlayerBannerHeightRef } from "@/hooks/use-player-banner-height-ref";
import { playerViewportColumnClass } from "@/lib/player-viewport";
import { playerContentColumnClass } from "@/lib/player-viewport";
import { useErrorStore, ERROR_COLORS } from "@/stores/error-store";
import { cn } from "@/lib/utils";
@@ -35,7 +35,7 @@ export function OfflineBanner(): React.ReactElement | null {
<div ref={bannerRef} className="sticky top-0 z-[60] w-full">
<div
className={cn(
playerViewportColumnClass,
playerContentColumnClass,
"flex items-center justify-center gap-2 px-4 py-3 text-sm",
"animate-in slide-in-from-top-full duration-300 ease-out",
)}

View File

@@ -2,6 +2,7 @@
import { Loader2, RefreshCw } from "lucide-react";
import { playerViewportFixedBarClass } from "@/lib/player-viewport";
import { cn } from "@/lib/utils";
interface PullToRefreshIndicatorProps {
@@ -25,7 +26,10 @@ export function PullToRefreshIndicator({
return (
<div
className="pointer-events-none fixed left-1/2 z-[60] flex w-full max-w-[480px] -translate-x-1/2 items-center justify-center overflow-hidden transition-[height] duration-100 ease-out"
className={cn(
playerViewportFixedBarClass,
"pointer-events-none z-[60] flex items-center justify-center overflow-hidden transition-[height] duration-100 ease-out",
)}
style={{
top: "env(safe-area-inset-top, 0px)",
height: isRefreshing ? 40 : pullDistance,

View File

@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
import { useErrorStore, ERROR_COLORS } from "@/stores/error-store";
import { Button } from "@/components/ui/button";
import { playerViewportOverlayClass } from "@/lib/player-viewport";
import { cn } from "@/lib/utils";
/**
@@ -32,7 +33,12 @@ export function ServerError(): React.ReactElement | null {
}
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-background p-4">
<div
className={cn(
playerViewportOverlayClass,
"z-[100] flex items-center justify-center bg-background p-4",
)}
>
<div className="flex w-full max-w-md flex-col items-center text-center">
{/* 错误图标 */}
<div
@@ -109,7 +115,12 @@ export function ServerErrorModal(): React.ReactElement | null {
}
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm">
<div
className={cn(
playerViewportOverlayClass,
"z-[100] flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm",
)}
>
<div className="w-full max-w-sm rounded-xl bg-card p-6 shadow-lg">
<div className="flex flex-col items-center text-center">
{/* 错误图标 */}

View File

@@ -4,6 +4,10 @@ import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
import { cn } from "@/lib/utils"
import {
playerViewportModalClass,
playerViewportOverlayClass,
} from "@/lib/player-viewport"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
@@ -31,7 +35,8 @@ function DialogOverlay({
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
playerViewportOverlayClass,
"isolate bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
@@ -53,7 +58,8 @@ function DialogContent({
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
playerViewportModalClass,
"grid w-full max-w-[calc(100%-2rem)] gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}

View File

@@ -4,6 +4,7 @@ import * as React from "react"
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
import { cn } from "@/lib/utils"
import { playerViewportOverlayClass } from "@/lib/player-viewport"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
@@ -28,7 +29,8 @@ function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
playerViewportOverlayClass,
"bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
className
)}
{...props}
@@ -53,7 +55,7 @@ function SheetContent({
data-slot="sheet-content"
data-side={side}
className={cn(
"fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
"fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:bottom-0 data-[side=bottom]:left-[var(--player-sidebar-width)] data-[side=bottom]:right-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-[var(--player-sidebar-width)] data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:left-[var(--player-sidebar-width)] data-[side=top]:right-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
className
)}
{...props}

View File

@@ -33,13 +33,13 @@ const Toaster = ({ ...props }: ToasterProps) => {
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "calc(var(--radius) * 0.8)",
"--width": "min(280px, calc(100vw - 24px))",
"--width": "min(280px, calc(var(--player-viewport-max, 480px) - 24px))",
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast:
"cn-toast !min-h-0 !w-full !max-w-[min(280px,calc(100vw-24px))] !items-center !gap-2 !rounded-lg !border !px-3 !py-2 !text-xs !shadow-md",
"cn-toast !min-h-0 !w-full !max-w-[min(280px,calc(var(--player-viewport-max,480px)-24px))] !items-center !gap-2 !rounded-lg !border !px-3 !py-2 !text-xs !shadow-md",
title: "!text-xs !font-semibold !leading-snug",
description: "!text-[11px] !leading-snug !text-muted-foreground",
icon: "!mr-0 !size-3.5",