refactor(z-index): name the stacking layers as a token scale
Replace ~48 magic z-index values (z-10..z-[9999] + 2 inline zIndex) with named tailwind zIndex tokens (base/menu/chrome/paneltab/panel/rail/overlay/popover/ tooltip/tuner). Values are UNCHANGED — a rename, not a renumber — so layering is identical; the names give one source of truth for the stack.
This commit is contained in:
@@ -130,7 +130,7 @@ export default function AddToPlaylist({
|
||||
<div
|
||||
ref={panelRef}
|
||||
style={{ position: "fixed", top: coords.top, left: coords.left, width: 240 }}
|
||||
className="z-50 glass-menu rounded-xl p-2 shadow-2xl"
|
||||
className="z-overlay glass-menu rounded-xl p-2 shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="text-xs text-muted px-1.5 pb-1.5">
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function BackToTop({ dep, threshold = 600 }: { dep?: unknown; thr
|
||||
return createPortal(
|
||||
<button
|
||||
onClick={toTop}
|
||||
className={`fixed bottom-5 right-5 z-30 inline-flex items-center justify-center w-16 h-16 rounded-full glass-card glass-hover text-fg shadow-lg hover:text-accent transition-all duration-200 ${
|
||||
className={`fixed bottom-5 right-5 z-chrome inline-flex items-center justify-center w-16 h-16 rounded-full glass-card glass-hover text-fg shadow-lg hover:text-accent transition-all duration-200 ${
|
||||
show ? "opacity-100 translate-y-0" : "opacity-0 translate-y-3 pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -260,7 +260,7 @@ export default function ChannelPage({
|
||||
<button
|
||||
data-testid="channel-back"
|
||||
onClick={closeChannel}
|
||||
className="absolute top-5 left-7 sm:left-9 z-10 inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm bg-black/45 text-white hover:bg-black/65 backdrop-blur-sm"
|
||||
className="absolute top-5 left-7 sm:left-9 z-base inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm bg-black/45 text-white hover:bg-black/65 backdrop-blur-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
{t("channel.back")}
|
||||
@@ -268,9 +268,9 @@ export default function ChannelPage({
|
||||
</div>
|
||||
|
||||
<div className="px-4 sm:px-6">
|
||||
{/* Identity + actions. relative+z-10 so the avatar that overlaps up into the banner
|
||||
{/* Identity + actions. relative+z-base so the avatar that overlaps up into the banner
|
||||
paints ABOVE it (the banner's positioned container would otherwise cover it). */}
|
||||
<div className="relative z-10 flex items-end gap-4 -mt-8">
|
||||
<div className="relative z-base flex items-end gap-4 -mt-8">
|
||||
<Avatar
|
||||
src={ch?.thumbnail_url ?? null}
|
||||
fallback={name}
|
||||
|
||||
@@ -742,7 +742,7 @@ function TagsCell({
|
||||
<div
|
||||
ref={fade.ref}
|
||||
style={fade.style}
|
||||
className={`glass-menu absolute left-0 z-30 w-44 max-h-[264px] overflow-y-auto no-scrollbar rounded-xl p-1.5 animate-[popIn_0.16s_ease] ${
|
||||
className={`glass-menu absolute left-0 z-chrome w-44 max-h-[264px] overflow-y-auto no-scrollbar rounded-xl p-1.5 animate-[popIn_0.16s_ease] ${
|
||||
openUp ? "bottom-full mb-1" : "top-full mt-1"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -208,7 +208,7 @@ export default function DataTable<T>({
|
||||
return (
|
||||
<div
|
||||
ref={popRef}
|
||||
className="glass-menu absolute left-0 top-full mt-1 z-30 w-52 rounded-xl p-2 animate-[popIn_0.16s_ease]"
|
||||
className="glass-menu absolute left-0 top-full mt-1 z-chrome w-52 rounded-xl p-2 animate-[popIn_0.16s_ease]"
|
||||
>
|
||||
{f.kind === "text" && (
|
||||
<input
|
||||
@@ -373,7 +373,7 @@ export default function DataTable<T>({
|
||||
away with the collapsed table box in some browsers. The `::after` paints a short
|
||||
fade just BELOW the header so rows dissolve as they slide under it (the header itself
|
||||
stays crisp — the page scroller's own top fade is off on these pages). */}
|
||||
<thead className="sticky top-0 z-10 after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-4 after:bg-[linear-gradient(to_bottom,var(--bg),transparent)] after:content-['']">
|
||||
<thead className="sticky top-0 z-base after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-4 after:bg-[linear-gradient(to_bottom,var(--bg),transparent)] after:content-['']">
|
||||
<tr className="text-muted [&>th]:shadow-[inset_0_-1px_0_var(--border)]">
|
||||
{columns.map((col) => {
|
||||
const sorted_ = sort?.key === col.key;
|
||||
|
||||
@@ -447,7 +447,7 @@ function QuotaUserPicker({ onPick }: { onPick: (u: { id: number; email: string }
|
||||
className={inputCls}
|
||||
/>
|
||||
{open && filtered.length > 0 && (
|
||||
<div className="absolute z-10 mt-1 w-full rounded-lg glass-menu shadow-xl overflow-hidden">
|
||||
<div className="absolute z-base mt-1 w-full rounded-lg glass-menu shadow-xl overflow-hidden">
|
||||
{filtered.map((u) => (
|
||||
<button
|
||||
key={u.id}
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function Header({
|
||||
// Absolutely positioned within the (relative) content column, so its left edge tracks the
|
||||
// actual left zone: it sits just inside the content, reclaiming the space when the nav rail is
|
||||
// slim and/or no filter sidebar is present — no hard-coded offset.
|
||||
<div className="pointer-events-none absolute top-3 left-3 right-[10%] z-30 flex items-center gap-3">
|
||||
<div className="pointer-events-none absolute top-3 left-3 right-[10%] z-chrome flex items-center gap-3">
|
||||
<ModuleName
|
||||
label={t(moduleLabelKey[page])}
|
||||
labels={moduleLabels}
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function LanguageSwitcher({
|
||||
</button>
|
||||
{open &&
|
||||
createPortal(
|
||||
<div style={{ position: "fixed", left: pos.left, bottom: pos.bottom, zIndex: 40 }}>
|
||||
<div className="z-rail" style={{ position: "fixed", left: pos.left, bottom: pos.bottom }}>
|
||||
{menu}
|
||||
</div>,
|
||||
document.body
|
||||
@@ -127,7 +127,7 @@ export default function LanguageSwitcher({
|
||||
<div
|
||||
className={`glass-menu absolute ${
|
||||
align === "right" ? "right-0" : "left-0"
|
||||
} mt-1 w-40 rounded-xl p-1.5 z-40 animate-[popIn_0.16s_ease]`}
|
||||
} mt-1 w-40 rounded-xl p-1.5 z-rail animate-[popIn_0.16s_ease]`}
|
||||
>
|
||||
{LANGUAGES.map((l) => (
|
||||
<button
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function Modal({
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/70 backdrop-blur-sm"
|
||||
className="fixed inset-0 z-overlay flex items-center justify-center p-4 sm:p-6 bg-black/70 backdrop-blur-sm"
|
||||
onMouseDown={(e) => {
|
||||
pressedOnBackdrop.current = e.target === e.currentTarget;
|
||||
}}
|
||||
|
||||
@@ -278,7 +278,7 @@ export default function NavSidebar({
|
||||
// margin, and a hover zone out there would fire from the dead space next to the rail —
|
||||
// leaving the side panel to the left would pop the rail open with nothing under the cursor.
|
||||
{...peek.handlers}
|
||||
className={`glass absolute top-0 bottom-0 left-0 z-40 rounded-2xl flex flex-col py-3 m-3 transition-[width] ${
|
||||
className={`glass absolute top-0 bottom-0 left-0 z-rail rounded-2xl flex flex-col py-3 m-3 transition-[width] ${
|
||||
slim ? "w-[56px] px-2" : "w-52 px-3"
|
||||
}`}
|
||||
aria-label={t("nav.primary")}
|
||||
@@ -390,7 +390,7 @@ export default function NavSidebar({
|
||||
<div
|
||||
ref={acctPanelRef}
|
||||
style={{ position: "fixed", left: acctPos.left, bottom: acctPos.bottom }}
|
||||
className="glass w-60 rounded-xl p-3 z-50 animate-[popIn_0.16s_ease]"
|
||||
className="glass w-60 rounded-xl p-3 z-overlay animate-[popIn_0.16s_ease]"
|
||||
>
|
||||
<div className="flex items-center gap-3 pb-3 border-b border-border">
|
||||
<AvatarImg
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function PanelGroup({
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`glass-card rounded-xl ${
|
||||
isDragging ? "relative z-10 shadow-2xl ring-1 ring-accent" : ""
|
||||
isDragging ? "relative z-base shadow-2xl ring-1 ring-accent" : ""
|
||||
} ${hidden && editing ? "opacity-50" : ""}`}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 px-3 py-2">
|
||||
|
||||
@@ -585,14 +585,14 @@ export default function PlayerModal({
|
||||
|
||||
// Portaled to <body>, like every other full-screen overlay here. A fixed, full-viewport modal
|
||||
// must not render inside the page scroller: once that scroller is a stacking context (its
|
||||
// edge-fade mask makes it one) this z-50 is scoped inside it, and the nav rail (z-40) and the
|
||||
// header (z-30) paint over the modal — worst at browser zoom, where the centered card is smaller
|
||||
// edge-fade mask makes it one) this z-overlay is scoped inside it, and the nav rail (z-rail) and the
|
||||
// header (z-chrome) paint over the modal — worst at browser zoom, where the centered card is smaller
|
||||
// relative to the rail/panel. (Same class as PlexPlayer; this one was the miss in that sweep.)
|
||||
return createPortal(
|
||||
<div
|
||||
ref={dialogRef}
|
||||
data-testid="player-modal"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/80 backdrop-blur-sm"
|
||||
className="fixed inset-0 z-overlay flex items-center justify-center p-4 sm:p-6 bg-black/80 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@@ -642,21 +642,21 @@ export default function PlayerModal({
|
||||
}}
|
||||
title={t("player.shortcutsHint")}
|
||||
aria-label={t("player.shortcutsHint")}
|
||||
className={`absolute inset-x-0 top-[12%] bottom-[22%] z-10 cursor-pointer ${
|
||||
className={`absolute inset-x-0 top-[12%] bottom-[22%] z-base cursor-pointer ${
|
||||
nativeControls ? "pointer-events-none" : ""
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
{/* Discreet badge while YouTube's own controls have taken over (overlay yielded). */}
|
||||
{playerError == null && nativeControls && (
|
||||
<div className="pointer-events-none absolute top-3 left-3 z-20 flex items-center gap-1.5 rounded-full bg-black/60 px-2.5 py-1 text-[11px] text-white/90 shadow-lg backdrop-blur-sm">
|
||||
<div className="pointer-events-none absolute top-3 left-3 z-menu flex items-center gap-1.5 rounded-full bg-black/60 px-2.5 py-1 text-[11px] text-white/90 shadow-lg backdrop-blur-sm">
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
{t("player.nativeControls")}
|
||||
</div>
|
||||
)}
|
||||
{/* Volume level flash (auto-fades). pointer-events-none so it never blocks the video. */}
|
||||
{volumeUi != null && (
|
||||
<div className="pointer-events-none absolute top-3 left-1/2 -translate-x-1/2 z-20 flex items-center gap-2 rounded-full bg-black/70 px-3 py-1.5 text-xs text-white shadow-lg">
|
||||
<div className="pointer-events-none absolute top-3 left-1/2 -translate-x-1/2 z-menu flex items-center gap-2 rounded-full bg-black/70 px-3 py-1.5 text-xs text-white shadow-lg">
|
||||
{volumeUi === 0 ? <VolumeX className="h-4 w-4" /> : <Volume2 className="h-4 w-4" />}
|
||||
<div className="h-1.5 w-28 overflow-hidden rounded-full bg-white/25">
|
||||
<div className="h-full rounded-full bg-white transition-all" style={{ width: `${volumeUi}%` }} />
|
||||
@@ -768,7 +768,7 @@ export default function PlayerModal({
|
||||
descRect &&
|
||||
createPortal(
|
||||
<div
|
||||
className="fixed z-[60] bg-surface border border-border rounded-xl shadow-2xl p-4"
|
||||
className="fixed z-popover bg-surface border border-border rounded-xl shadow-2xl p-4"
|
||||
style={{
|
||||
left: descRect.left,
|
||||
bottom: descRect.bottom,
|
||||
|
||||
@@ -231,7 +231,7 @@ export default function PlexBrowse({
|
||||
// The fallback is portaled for the same reason the player itself is: it is a fixed
|
||||
// full-viewport overlay, and this tree renders inside the page scroller.
|
||||
return (
|
||||
<Suspense fallback={createPortal(<div className="fixed inset-0 z-50 bg-black" />, document.body)}>
|
||||
<Suspense fallback={createPortal(<div className="fixed inset-0 z-overlay bg-black" />, document.body)}>
|
||||
<PlexPlayer itemId={sub.view.id} queue={sub.view.queue} onClose={sub.back} />
|
||||
</Suspense>
|
||||
);
|
||||
@@ -924,7 +924,7 @@ function PlexShowView({
|
||||
<>
|
||||
{/* Hero */}
|
||||
<div className="glass relative rounded-2xl p-4 sm:p-5 mb-8 flex gap-4 sm:gap-5">
|
||||
<div className="absolute right-3 top-3 z-10">
|
||||
<div className="absolute right-3 top-3 z-base">
|
||||
<DetailCustomizeMenu
|
||||
artBg={artBg}
|
||||
onToggleArtBg={toggleArtBg}
|
||||
@@ -1121,7 +1121,7 @@ function PlexSeasonView({
|
||||
) : (
|
||||
<>
|
||||
<div className="glass relative rounded-2xl p-4 sm:p-5 mb-8 flex gap-4 sm:gap-5">
|
||||
<div className="absolute right-3 top-3 z-10">
|
||||
<div className="absolute right-3 top-3 z-base">
|
||||
<DetailCustomizeMenu
|
||||
artBg={artBg}
|
||||
onToggleArtBg={toggleArtBg}
|
||||
|
||||
@@ -948,12 +948,12 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
// Portaled to <body>, like every other full-screen overlay here (Modal, PlayerModal, ChatDock,
|
||||
// BackToTop). A fixed, full-viewport player must not be painted inside the page scroller: the
|
||||
// moment that scroller gets a mask/filter/transform/contain it becomes a stacking context, and
|
||||
// this z-50 is then scoped inside it — the rail, the side panel and the header paint straight
|
||||
// this z-overlay is then scoped inside it — the rail, the side panel and the header paint straight
|
||||
// over the player. That is exactly what the scroller's edge-fade did before this moved out.
|
||||
return createPortal(
|
||||
<div
|
||||
ref={wrapRef}
|
||||
className="fixed left-0 top-0 z-50 bg-black flex items-center justify-center select-none"
|
||||
className="fixed left-0 top-0 z-overlay bg-black flex items-center justify-center select-none"
|
||||
onMouseMove={wake}
|
||||
onClick={wake}
|
||||
onWheel={onWheel}
|
||||
@@ -1546,7 +1546,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
{/* Keyboard-shortcut cheat sheet (toggle with "h" or the keyboard button). */}
|
||||
{helpOpen && (
|
||||
<div
|
||||
className="absolute inset-0 z-20 grid place-items-center bg-black/60 p-4"
|
||||
className="absolute inset-0 z-menu grid place-items-center bg-black/60 p-4"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setHelpOpen(false);
|
||||
@@ -1609,7 +1609,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
{/* Rich info overlay (poster/cast/ratings) — "i" or the info button; video keeps playing. */}
|
||||
{infoOpen && detail && (
|
||||
<div
|
||||
className="absolute inset-0 z-20 grid place-items-center overflow-y-auto bg-black/70 p-4"
|
||||
className="absolute inset-0 z-menu grid place-items-center overflow-y-auto bg-black/70 p-4"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setInfoOpen(false);
|
||||
|
||||
@@ -56,7 +56,7 @@ function UserShare({ job }: { job: DownloadJob }) {
|
||||
className={inputCls}
|
||||
/>
|
||||
{open && filtered.length > 0 && (
|
||||
<div className="absolute z-10 mt-1 w-full rounded-lg glass-menu shadow-xl overflow-hidden">
|
||||
<div className="absolute z-base mt-1 w-full rounded-lg glass-menu shadow-xl overflow-hidden">
|
||||
{filtered.map((u) => (
|
||||
<button
|
||||
key={u.id}
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function SidePanel({
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
aria-label={pinned ? t("sidebar.unpin") : t("sidebar.pin")}
|
||||
className={`glass glass-hover absolute top-3 left-0 z-[34] flex flex-col items-center gap-2 px-1.5 py-3 rounded-l-none rounded-r-xl transition-opacity ${
|
||||
className={`glass glass-hover absolute top-3 left-0 z-paneltab flex flex-col items-center gap-2 px-1.5 py-3 rounded-l-none rounded-r-xl transition-opacity ${
|
||||
expanded ? "opacity-0" : ""
|
||||
} ${count > 0 ? "ring-1 ring-accent/40" : ""}`}
|
||||
>
|
||||
@@ -79,7 +79,7 @@ export default function SidePanel({
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<aside className="glass absolute top-3 bottom-3 left-0 z-[35] w-64 rounded-2xl flex flex-col overflow-hidden">
|
||||
<aside className="glass absolute top-3 bottom-3 left-0 z-panel w-64 rounded-2xl flex flex-col overflow-hidden">
|
||||
{/* Row 1 — identity only: pin, icon, title (flexes + truncates last), count, customize.
|
||||
Module actions live on their own row below so the title always has room. */}
|
||||
<div className="flex items-center gap-1.5 px-3 py-2.5 border-b border-border/60 flex-none">
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function SyncStatus({
|
||||
<div
|
||||
ref={popRef}
|
||||
style={{ position: "fixed", top: pos.top, right: pos.right }}
|
||||
className="glass-menu w-64 rounded-xl p-3 z-50 text-xs text-muted animate-[popIn_0.16s_ease]"
|
||||
className="glass-menu w-64 rounded-xl p-3 z-overlay text-xs text-muted animate-[popIn_0.16s_ease]"
|
||||
>
|
||||
<div
|
||||
title={t("header.sync.countTooltip")}
|
||||
|
||||
@@ -82,8 +82,8 @@ function TagRow({
|
||||
ref={popFade.ref}
|
||||
onMouseEnter={openPop}
|
||||
onMouseLeave={closeSoon}
|
||||
style={{ position: "fixed", left: pos.left, top: pos.top, zIndex: 60, ...popFade.style }}
|
||||
className="glass-menu w-56 max-h-72 overflow-y-auto no-scrollbar rounded-xl p-1.5 animate-[popIn_0.16s_ease]"
|
||||
style={{ position: "fixed", left: pos.left, top: pos.top, ...popFade.style }}
|
||||
className="z-popover glass-menu w-56 max-h-72 overflow-y-auto no-scrollbar rounded-xl p-1.5 animate-[popIn_0.16s_ease]"
|
||||
>
|
||||
<div className="text-[11px] uppercase tracking-wide text-muted px-2 py-1">
|
||||
{t("tagManager.onChannels")}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function Toaster() {
|
||||
const toasts = useSyncExternalStore(subscribe, getActiveToasts, getActiveToasts);
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-4 left-4 z-50 flex flex-col gap-2 w-80 max-w-[calc(100vw-2rem)]">
|
||||
<div className="absolute bottom-4 left-4 z-overlay flex flex-col gap-2 w-80 max-w-[calc(100vw-2rem)]">
|
||||
{toasts.map((toast) => {
|
||||
const { icon: Icon, color, bar } = LEVEL_STYLE[toast.level];
|
||||
return (
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function Tooltip({
|
||||
top: coords.top,
|
||||
transform: `translateX(-50%) translateY(${coords.placement === "top" ? "-100%" : "0"})`,
|
||||
}}
|
||||
className="glass pointer-events-none z-[100] w-max max-w-[240px] px-2.5 py-1.5 rounded-lg text-xs leading-snug text-fg font-normal normal-case tracking-normal animate-[fadeIn_0.12s_ease]"
|
||||
className="glass pointer-events-none z-tooltip w-max max-w-[240px] px-2.5 py-1.5 rounded-lg text-xs leading-snug text-fg font-normal normal-case tracking-normal animate-[fadeIn_0.12s_ease]"
|
||||
>
|
||||
{hint}
|
||||
</div>,
|
||||
|
||||
@@ -405,7 +405,7 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
))}
|
||||
{/* draggable boundary markers */}
|
||||
{boundaries.map((b, bi) => (
|
||||
<div key={bi} className="absolute inset-y-0 -ml-1.5 w-3 flex items-center justify-center z-10" style={{ left: pct(b) }}>
|
||||
<div key={bi} className="absolute inset-y-0 -ml-1.5 w-3 flex items-center justify-center z-base" style={{ left: pct(b) }}>
|
||||
<div
|
||||
className="w-1 h-full bg-white/90 cursor-ew-resize rounded"
|
||||
onPointerDown={(e) => {
|
||||
@@ -426,13 +426,13 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
|
||||
</div>
|
||||
))}
|
||||
{/* playhead */}
|
||||
<div className="absolute inset-y-0 w-0.5 bg-white pointer-events-none z-20" style={{ left: pct(current) }} />
|
||||
<div className="absolute inset-y-0 w-0.5 bg-white pointer-events-none z-menu" style={{ left: pct(current) }} />
|
||||
</div>
|
||||
|
||||
{/* hover-scrub thumbnail */}
|
||||
{strip && hoverTime != null && (
|
||||
<div
|
||||
className="absolute bottom-full mb-2 pointer-events-none z-30 rounded-md overflow-hidden border border-border shadow-xl bg-black"
|
||||
className="absolute bottom-full mb-2 pointer-events-none z-chrome rounded-md overflow-hidden border border-border shadow-xl bg-black"
|
||||
style={{ left: hoverLeft, width: HOVER_W }}
|
||||
>
|
||||
<div style={{ width: HOVER_W, height: HOVER_W / aspect, ...tileStyle(Math.floor(hoverTime / (strip.interval_s || 1))) }} />
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function ViewSwitcher<T extends string>({
|
||||
role="menu"
|
||||
aria-label={label}
|
||||
onKeyDown={onMenuKeyDown}
|
||||
className="glass-menu absolute right-0 top-full mt-1 z-30 min-w-44 p-1.5 rounded-xl animate-[popIn_0.16s_ease]"
|
||||
className="glass-menu absolute right-0 top-full mt-1 z-chrome min-w-44 p-1.5 rounded-xl animate-[popIn_0.16s_ease]"
|
||||
>
|
||||
{/* The modes are one exclusive choice, so the radio items need a group to scope their
|
||||
checked-ness to — otherwise they read as independent checkable items. */}
|
||||
|
||||
@@ -228,7 +228,7 @@ function Lightbox({ src, label, onClose }: { src: string; label: string; onClose
|
||||
}, [onClose]);
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[60] grid place-items-center bg-black/80 backdrop-blur-sm p-4 sm:p-8"
|
||||
className="fixed inset-0 z-popover grid place-items-center bg-black/80 backdrop-blur-sm p-4 sm:p-8"
|
||||
onClick={onClose}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
|
||||
@@ -37,11 +37,11 @@ export function DraftSaveBar({
|
||||
const saving = state === "saving";
|
||||
const floating = variant === "floating";
|
||||
const wrap = floating
|
||||
? "fixed bottom-4 left-1/2 -translate-x-1/2 z-40 glass rounded-2xl shadow-lg flex items-center justify-between gap-4 px-4 py-3 w-[min(48rem,calc(100%-2rem))]"
|
||||
? "fixed bottom-4 left-1/2 -translate-x-1/2 z-rail glass rounded-2xl shadow-lg flex items-center justify-between gap-4 px-4 py-3 w-[min(48rem,calc(100%-2rem))]"
|
||||
: "flex items-center justify-between gap-3 border-t border-border/60 px-4 py-3 bg-card/40";
|
||||
// The floating variant is fixed to the viewport but renders from a page inside the page
|
||||
// scroller, so it goes to <body> — otherwise the scroller's stacking context (its edge-fade
|
||||
// mask) scopes this z-40 and fades the bar's bottom edge. The inline variant is in normal flow
|
||||
// mask) scopes this z-rail and fades the bar's bottom edge. The inline variant is in normal flow
|
||||
// and stays put.
|
||||
const bar = (
|
||||
<div data-testid="draft-save-bar" className={wrap}>
|
||||
|
||||
@@ -141,7 +141,7 @@ export function DetailCustomizeMenu({
|
||||
{open && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="glass-menu absolute right-0 top-full z-20 mt-1 w-52 rounded-xl p-1.5 text-sm"
|
||||
className="glass-menu absolute right-0 top-full z-menu mt-1 w-52 rounded-xl p-1.5 text-sm"
|
||||
style={{ background: "color-mix(in srgb, var(--surface) 58%, transparent)" }}
|
||||
>
|
||||
<PrefToggle label={t("plex.info.prefArtBg")} on={artBg} onClick={onToggleArtBg} />
|
||||
|
||||
@@ -16,6 +16,31 @@ export default {
|
||||
fontFamily: {
|
||||
sans: ["Inter", "system-ui", "-apple-system", "Segoe UI", "Roboto", "sans-serif"],
|
||||
},
|
||||
// Named z-index layers — the single source of truth for stacking order. Values are
|
||||
// deliberately UNCHANGED from the magic numbers they replaced (this was a rename, not a
|
||||
// renumber), so the visual layering is identical; the names just make the stack legible and
|
||||
// give future changes one place to edit. Low → high:
|
||||
// base(10) in-content decorations: sticky table head, card badges, in-flow dropdowns
|
||||
// menu(20) menus/overlays sitting just over content
|
||||
// chrome(30) floating chrome: the header band, back-to-top, content filter/sort menus
|
||||
// paneltab / panel (34/35) the collapsed tab vs the expanded side panel (aside over tab)
|
||||
// rail(40) full-height rails + docked bars: nav rail, chat dock, floating save bar
|
||||
// overlay(50) full-screen modals/players, toaster, onboarding, popovers over content
|
||||
// popover(60) a popover that must sit over an overlay (player scrub, tag menu) + the login gate
|
||||
// tooltip(100) tooltips, above everything interactive
|
||||
// tuner(9999) the dev/lab glass tuner, deliberately on top of all
|
||||
zIndex: {
|
||||
base: "10",
|
||||
menu: "20",
|
||||
chrome: "30",
|
||||
paneltab: "34",
|
||||
panel: "35",
|
||||
rail: "40",
|
||||
overlay: "50",
|
||||
popover: "60",
|
||||
tooltip: "100",
|
||||
tuner: "9999",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
||||
Reference in New Issue
Block a user