fix: resolve navigation overlap and smooth animations
This commit is contained in:
1 parent
731f57da78
commit
41a529e50e
6 files changed
+230
-85
No files matched your search
@@ -8,6 +8,7 @@
|
||||
"build": "tsc -b && vite build",
|
||||
"typecheck": "tsc -b --pretty false",
|
||||
"visual-check": "node scripts/visual-check.mjs",
|
||||
"navigation-motion-check": "node scripts/navigation-motion-check.mjs",
|
||||
"sidebar-resize-check": "node scripts/sidebar-resize-check.mjs",
|
||||
"viewer-resize-check": "node scripts/viewer-resize-check.mjs",
|
||||
"viewer-similar-check": "node scripts/viewer-similar-check.mjs"
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { chromium } from "playwright-core";
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE
|
||||
?? "/root/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome",
|
||||
headless: true,
|
||||
args: ["--no-sandbox", "--disable-dev-shm-usage"],
|
||||
});
|
||||
|
||||
async function traceNavigation(page, toggles) {
|
||||
return page.evaluate(async (times) => {
|
||||
const menu = document.querySelector(".gallery-sidebar-toggle");
|
||||
const toolbar = document.querySelector(".topbar");
|
||||
const sidebar = () => document.querySelector(innerWidth <= 720 ? ".mobile-sidebar" : ".desktop-sidebar");
|
||||
const right = () => sidebar()?.getBoundingClientRect().right ?? 0;
|
||||
const controls = [...toolbar.querySelectorAll("button")].filter(button => !button.closest("[inert]"));
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
const result = { frames: 0, reversals: [], leaks: [], coveredSidebar: 0, coveredControls: 0, menuDrift: 0, partialFrames: 0 };
|
||||
const started = performance.now();
|
||||
let index = 0;
|
||||
while (performance.now() - started < times.at(-1) + 440) {
|
||||
await new Promise(requestAnimationFrame);
|
||||
if (index < times.length && performance.now() - started >= times[index]) {
|
||||
const before = right();
|
||||
menu.click();
|
||||
// Flush the component update without advancing the animation timeline.
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
result.reversals.push(Math.abs(right() - before));
|
||||
index++;
|
||||
}
|
||||
result.frames++;
|
||||
const panel = sidebar();
|
||||
const panelRect = panel?.getBoundingClientRect();
|
||||
const topbarRect = toolbar.getBoundingClientRect();
|
||||
const rect = menu.getBoundingClientRect();
|
||||
result.menuDrift = Math.max(result.menuDrift, Math.hypot(rect.x - menuRect.x, rect.y - menuRect.y));
|
||||
if (panelRect && panelRect.right > 1 && panelRect.right < panelRect.width - 1) result.partialFrames++;
|
||||
for (const button of controls) {
|
||||
const box = button.getBoundingClientRect();
|
||||
if (!button.contains(document.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2))) result.coveredControls++;
|
||||
}
|
||||
for (const y of [topbarRect.top + 1, topbarRect.top + topbarRect.height / 2, topbarRect.bottom - 1]) {
|
||||
for (let x = 2; x < innerWidth; x += 16) {
|
||||
const hit = document.elementFromPoint(x, y);
|
||||
if (!hit || hit.closest(".content")) result.leaks.push({ x, y });
|
||||
if (
|
||||
panelRect && x < panelRect.right - 1 && !panel.closest("[inert]")
|
||||
&& !panel.contains(hit) && !toolbar.contains(hit)
|
||||
) result.coveredSidebar++;
|
||||
}
|
||||
}
|
||||
}
|
||||
result.open = menu.getAttribute("aria-expanded");
|
||||
result.drawerMounted = Boolean(document.querySelector(".mobile-nav-layer"));
|
||||
result.bodyLocked = document.body.style.overflow === "hidden";
|
||||
result.overflow = document.documentElement.scrollWidth > innerWidth;
|
||||
return result;
|
||||
}, toggles);
|
||||
}
|
||||
|
||||
function checkTrace(trace, reduced = false) {
|
||||
assert.deepEqual(trace.leaks, [], "the header lets clicks reach gallery cards during a transition");
|
||||
assert.equal(trace.coveredSidebar, 0, "the header surface covers the visible sidebar");
|
||||
assert.equal(trace.coveredControls, 0, "navigation covers a toolbar control");
|
||||
assert.ok(trace.menuDrift < 1, `the menu button moved during navigation: ${trace.menuDrift}px`);
|
||||
assert.equal(trace.overflow, false);
|
||||
if (!reduced) {
|
||||
assert.ok(trace.partialFrames > 1, "the sidebar did not animate");
|
||||
assert.ok(Math.max(...trace.reversals) < 2, `navigation jumped when reversed: ${trace.reversals}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (const width of [1440, 1000, 721, 720, 390, 320]) {
|
||||
const mobile = width <= 720;
|
||||
const page = await browser.newPage({ viewport: { width, height: 844 }, hasTouch: mobile });
|
||||
const errors = [];
|
||||
page.on("pageerror", error => errors.push(error.message));
|
||||
await page.addInitScript(() => localStorage.setItem("pixhelf.sidebar-collapsed", "true"));
|
||||
await page.goto(process.env.PIXHELF_URL ?? "http://127.0.0.1:3002");
|
||||
await page.locator(".image-card").nth(24).waitFor();
|
||||
// Put real, clickable cards behind the header, including its left edge.
|
||||
await page.evaluate(() => window.scrollTo(0, 320));
|
||||
await page.waitForTimeout(250);
|
||||
const opened = await traceNavigation(page, [0]);
|
||||
checkTrace(opened);
|
||||
assert.equal(opened.open, "true");
|
||||
const panel = mobile ? ".mobile-sidebar" : ".desktop-sidebar";
|
||||
const brandClickable = await page.locator(`${panel} .brand-lockup`).evaluate(brand =>
|
||||
[...brand.querySelectorAll(".brand-mark, .brand-name, .brand-version")].every(element => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return element.closest("a").contains(document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2));
|
||||
}),
|
||||
);
|
||||
assert.equal(brandClickable, true, "the sidebar brand is obscured by the header");
|
||||
await page.screenshot({ path: `/tmp/pixhelf-navigation-${width}.png` });
|
||||
const closed = await traceNavigation(page, [0]);
|
||||
checkTrace(closed);
|
||||
assert.equal(closed.open, "false");
|
||||
assert.equal(closed.drawerMounted || closed.bodyLocked, false);
|
||||
const rapid = await traceNavigation(page, [0, 70, 140, 210, 280, 350]);
|
||||
checkTrace(rapid);
|
||||
assert.equal(rapid.drawerMounted || rapid.bodyLocked, false);
|
||||
// A genuine pointer click in the blank header must not open the card below it.
|
||||
await page.mouse.click(Math.min(100, width / 2), 25);
|
||||
assert.equal(await page.locator(".image-viewer").count(), 0);
|
||||
await page.locator(".gallery-sidebar-toggle").click();
|
||||
await page.locator(`${panel} .brand-name`).click();
|
||||
await page.waitForFunction(() => document.querySelector(".content").getAttribute("aria-busy") === "false");
|
||||
await page.locator(".search-toggle").click();
|
||||
await page.locator(".search-popover-panel input").fill("navigation-check");
|
||||
assert.equal(await page.locator(".search-popover-panel input").inputValue(), "navigation-check");
|
||||
await page.locator(".clear-search").click();
|
||||
await page.locator(".search-toggle").click();
|
||||
if (!mobile) await page.locator(".gallery-sidebar-toggle").click();
|
||||
await page.waitForTimeout(360);
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
const reduced = await traceNavigation(page, [0, 90]);
|
||||
checkTrace(reduced, true);
|
||||
assert.equal(reduced.drawerMounted || reduced.bodyLocked, false);
|
||||
assert.equal(reduced.partialFrames, 0, "reduced motion still animates the sidebar");
|
||||
if (width === 390) {
|
||||
await page.evaluate(() => window.scrollTo(0, 320));
|
||||
await page.waitForTimeout(200);
|
||||
await page.evaluate(() => document.documentElement.style.setProperty("--visual-viewport-top", "24px"));
|
||||
const offset = await traceNavigation(page, [0, 90]);
|
||||
checkTrace(offset, true);
|
||||
await page.evaluate(() => document.documentElement.style.removeProperty("--visual-viewport-top"));
|
||||
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||||
await page.locator(".gallery-sidebar-toggle").click();
|
||||
await page.locator(".mobile-sidebar .brand-name").waitFor();
|
||||
await page.setViewportSize({ width: 1000, height: 844 });
|
||||
await page.waitForFunction(() => !document.querySelector(".mobile-nav-layer") && document.body.style.overflow !== "hidden");
|
||||
assert.equal(await page.locator(".gallery-sidebar-toggle").getAttribute("aria-controls"), "desktop-album-navigation");
|
||||
await page.setViewportSize({ width, height: 844 });
|
||||
await page.waitForFunction(() => document.querySelector(".gallery-sidebar-toggle").getAttribute("aria-controls") === "mobile-album-navigation");
|
||||
assert.equal(await page.locator(".gallery-sidebar-toggle").getAttribute("aria-expanded"), "false");
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(JSON.stringify({ width, frames: opened.frames + closed.frames + rapid.frames, maxReversalJump: Math.max(...rapid.reversals), brandClickable, reducedMotion: true }));
|
||||
await page.close();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -172,7 +172,7 @@ try {
|
||||
topbarPosition: topbar ? getComputedStyle(topbar).position : "",
|
||||
topbarTop: topbarRect?.top ?? -1,
|
||||
topbarHeight: topbarRect?.height ?? -1,
|
||||
topbarSurfaceLeft: topbar ? Number.parseFloat(getComputedStyle(topbar, "::before").left) : -1,
|
||||
topbarSurfaceLeft: document.querySelector(".topbar-surface")?.getBoundingClientRect().left ?? -1,
|
||||
sidebarTop: document.querySelector(".desktop-sidebar")?.getBoundingClientRect().top ?? -1,
|
||||
topbarSearches: document.querySelectorAll(
|
||||
".topbar-actions > .topbar-search",
|
||||
@@ -1561,7 +1561,7 @@ try {
|
||||
progressPanels: document.querySelectorAll(".sidebar-progress").length,
|
||||
inlineToggles: document.querySelectorAll(".desktop-sidebar-inline-toggle").length,
|
||||
galleryToggles: document.querySelectorAll(".gallery-sidebar-toggle").length,
|
||||
topbarSurfaceLeft: Number.parseFloat(getComputedStyle(document.querySelector(".topbar"), "::before").left),
|
||||
topbarSurfaceLeft: document.querySelector(".topbar-surface")?.getBoundingClientRect().left ?? -1,
|
||||
brandVisibility: getComputedStyle(document.querySelector(".desktop-sidebar .brand-lockup")).visibility,
|
||||
brandInert: Boolean(document.querySelector(".desktop-sidebar .brand")?.closest("[inert]")),
|
||||
}));
|
||||
@@ -1655,8 +1655,8 @@ try {
|
||||
layerTopGap: navigationLayer && topbar
|
||||
? navigationLayer.y - topbar.y
|
||||
: -1,
|
||||
topbarSurfaceLeft: await page.locator(".topbar").evaluate((header) =>
|
||||
Number.parseFloat(getComputedStyle(header, "::before").left)),
|
||||
topbarSurfaceLeft: await page.locator(".topbar-surface").evaluate((surface) =>
|
||||
surface.getBoundingClientRect().left),
|
||||
toggleWidth: navigationToggleBefore?.width ?? -1,
|
||||
toggleHeight: navigationToggleBefore?.height ?? -1,
|
||||
expandedToggleWidth: navigationToggleOpen?.width ?? -1,
|
||||
@@ -1696,9 +1696,9 @@ try {
|
||||
for (const width of [320, 360, 375, target.width]) {
|
||||
await page.setViewportSize({ width, height: target.height });
|
||||
await page.waitForFunction(() => {
|
||||
const header = document.querySelector(".topbar");
|
||||
const drawer = document.querySelector(".mobile-sidebar").getBoundingClientRect();
|
||||
return Math.abs(Number.parseFloat(getComputedStyle(header, "::before").left) - drawer.right) < 1;
|
||||
// The gallery's ResizeObserver commits on the next animation frame.
|
||||
return Math.abs(drawer.left) < 1 && document.documentElement.scrollWidth <= innerWidth;
|
||||
});
|
||||
const dimensions = await page.evaluate(() => {
|
||||
const drawer = document.querySelector(".mobile-sidebar").getBoundingClientRect();
|
||||
@@ -2006,7 +2006,7 @@ try {
|
||||
desktopSidebar.toggleHeaderInsetY < 5 ||
|
||||
desktopSidebar.toggleHeaderInsetY > 9 ||
|
||||
Math.abs(layout.sidebarTop) > 1 ||
|
||||
Math.abs(layout.topbarSurfaceLeft - desktopSidebar.contentBefore) > 1 ||
|
||||
Math.abs(layout.topbarSurfaceLeft) > 1 ||
|
||||
Math.abs(desktopSidebar.topbarSurfaceLeft) > 1 ||
|
||||
desktopSidebar.stationaryToggleDelta < 0 ||
|
||||
desktopSidebar.stationaryToggleDelta > 1 ||
|
||||
@@ -2035,7 +2035,7 @@ try {
|
||||
mobileNavigation.metaRows !== 0 ||
|
||||
Math.abs(mobileNavigation.layerTopGap) > 1 ||
|
||||
Math.abs(layout.topbarSurfaceLeft) > 1 ||
|
||||
Math.abs(mobileNavigation.topbarSurfaceLeft - mobileNavigation.drawerWidth) > 1 ||
|
||||
Math.abs(mobileNavigation.topbarSurfaceLeft) > 1 ||
|
||||
Math.abs(mobileNavigation.toggleWidth - 42) > 1 ||
|
||||
Math.abs(mobileNavigation.toggleHeight - 42) > 1 ||
|
||||
Math.abs(mobileNavigation.expandedToggleWidth - 42) > 1 ||
|
||||
|
||||
+15
-5
@@ -285,7 +285,7 @@ function useDebounced<T>(value: T, delay: number): T {
|
||||
return debounced;
|
||||
}
|
||||
|
||||
function useDelayedUnmount(visible: boolean, delay: number): boolean {
|
||||
function useTransitionPresence(visible: boolean, duration: number) {
|
||||
const [mounted, setMounted] = useState(visible);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -295,11 +295,16 @@ function useDelayedUnmount(visible: boolean, delay: number): boolean {
|
||||
}
|
||||
if (!mounted) return;
|
||||
|
||||
const timer = window.setTimeout(() => setMounted(false), delay);
|
||||
// The transition event normally removes the drawer. Cover hidden tabs and breakpoint changes.
|
||||
const timer = window.setTimeout(() => setMounted(false), duration + 100);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [delay, mounted, visible]);
|
||||
}, [duration, mounted, visible]);
|
||||
|
||||
return mounted;
|
||||
const finishExit = useCallback(() => {
|
||||
if (!visible) setMounted(false);
|
||||
}, [visible]);
|
||||
|
||||
return { mounted: visible || mounted, finishExit };
|
||||
}
|
||||
|
||||
function errorMessage(reason: unknown, fallback: string): string {
|
||||
@@ -363,7 +368,10 @@ function App() {
|
||||
const [viewerReturnRequest, setViewerReturnRequest] = useState<ViewerReturnRequest | null>(null);
|
||||
const [reloadToken, setReloadToken] = useState(0);
|
||||
const compactLayout = useMediaQuery("(max-width: 720px)");
|
||||
const mobileNavMounted = useDelayedUnmount(mobileNavOpen, MOBILE_NAV_EXIT_MS);
|
||||
const { mounted: mobileNavMounted, finishExit: finishMobileNavExit } = useTransitionPresence(
|
||||
mobileNavOpen,
|
||||
MOBILE_NAV_EXIT_MS,
|
||||
);
|
||||
const textSearchReady = Boolean(
|
||||
status?.backgroundComplete
|
||||
&& status.textSearch.enabled
|
||||
@@ -1164,6 +1172,7 @@ function App() {
|
||||
data-viewer-returning={Boolean(viewerReturnRequest)}
|
||||
data-viewer-return-image-id={viewerReturnRequest?.imageId}
|
||||
>
|
||||
<div className="topbar-surface" aria-hidden="true" />
|
||||
<Header
|
||||
search={search}
|
||||
searchOpen={searchOpen}
|
||||
@@ -1203,6 +1212,7 @@ function App() {
|
||||
onHome={goHome}
|
||||
mobileOpen={mobileNavOpen}
|
||||
mobileMounted={mobileNavMounted}
|
||||
onMobileExited={finishMobileNavExit}
|
||||
onClose={() => setMobileNavOpen(false)}
|
||||
desktopCollapsed={sidebarCollapsed}
|
||||
/>
|
||||
|
||||
@@ -301,6 +301,7 @@ export function Sidebar({
|
||||
onHome,
|
||||
mobileOpen,
|
||||
mobileMounted,
|
||||
onMobileExited,
|
||||
onClose,
|
||||
desktopCollapsed,
|
||||
}: {
|
||||
@@ -311,6 +312,7 @@ export function Sidebar({
|
||||
onHome: () => void;
|
||||
mobileOpen: boolean;
|
||||
mobileMounted: boolean;
|
||||
onMobileExited: () => void;
|
||||
onClose: () => void;
|
||||
desktopCollapsed: boolean;
|
||||
}) {
|
||||
@@ -370,6 +372,13 @@ export function Sidebar({
|
||||
className="sidebar mobile-sidebar"
|
||||
role="dialog"
|
||||
aria-label="相册导航"
|
||||
onTransitionEnd={(event) => {
|
||||
if (
|
||||
event.target === event.currentTarget
|
||||
&& event.propertyName === "transform"
|
||||
&& !mobileOpen
|
||||
) onMobileExited();
|
||||
}}
|
||||
>
|
||||
<SidebarBrand onHome={onHome} />
|
||||
{navigation}
|
||||
|
||||
+50
-72
@@ -87,22 +87,21 @@ select:focus-visible,
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0 18px;
|
||||
/* Keep the sidebar brand beneath the transparent header area clickable. */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.topbar::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: 0 0 0 var(--sidebar-layout-width);
|
||||
content: "";
|
||||
.topbar-surface {
|
||||
/* Block gallery clicks below both sidebars; toolbar controls stay above them. */
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
inset: 0 0 auto;
|
||||
height: var(--topbar-height);
|
||||
pointer-events: auto;
|
||||
background: rgba(252, 253, 252, 0.94);
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: 0 4px 18px rgba(25, 35, 31, 0.035);
|
||||
backdrop-filter: blur(16px) saturate(1.08);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(1.08);
|
||||
transition: left var(--motion-medium) var(--ease-standard);
|
||||
}
|
||||
|
||||
.topbar-leading > *,
|
||||
@@ -229,17 +228,15 @@ select:focus-visible,
|
||||
|
||||
.request-progress {
|
||||
position: fixed;
|
||||
z-index: 31;
|
||||
z-index: 11;
|
||||
top: calc(var(--visual-viewport-top) + var(--topbar-height) - 2px);
|
||||
right: 0;
|
||||
left: var(--sidebar-layout-width);
|
||||
left: 0;
|
||||
height: 2px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
opacity 180ms ease,
|
||||
left var(--motion-medium) var(--ease-standard);
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
|
||||
.request-progress[data-visible="true"] { opacity: 1; }
|
||||
@@ -250,6 +247,9 @@ select:focus-visible,
|
||||
display: block;
|
||||
background: linear-gradient(90deg, transparent 0%, #2f8d82 28%, #78bab2 58%, transparent 100%);
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
.request-progress[data-visible="true"] span {
|
||||
animation: request-progress 1.1s var(--ease-standard) infinite;
|
||||
}
|
||||
|
||||
@@ -810,7 +810,7 @@ select:focus-visible,
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #e5e8e6;
|
||||
background: #e3e7e5;
|
||||
border: 1px solid rgba(26, 35, 31, 0.08);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 1px 1px rgba(21, 31, 27, 0.05);
|
||||
@@ -823,16 +823,16 @@ select:focus-visible,
|
||||
}
|
||||
|
||||
.image-card::before,
|
||||
.viewer-similar-card::before {
|
||||
.viewer-similar-card::before,
|
||||
.skeleton-grid span::before,
|
||||
.viewer-similar-skeleton span::before {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
inset: 0;
|
||||
content: "";
|
||||
background:
|
||||
linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, .48) 47%, transparent 64%),
|
||||
#e3e7e5;
|
||||
background-size: 220% 100%, 100% 100%;
|
||||
background: linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, .48) 47%, transparent 64%);
|
||||
opacity: 1;
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
transition: opacity var(--motion-medium) ease;
|
||||
animation: none;
|
||||
}
|
||||
@@ -844,7 +844,9 @@ select:focus-visible,
|
||||
}
|
||||
|
||||
.image-card[data-loading="true"]::before,
|
||||
.viewer-similar-card[data-loading="true"]::before {
|
||||
.viewer-similar-card[data-loading="true"]::before,
|
||||
.skeleton-grid span::before,
|
||||
.viewer-similar-skeleton span::before {
|
||||
animation: card-shimmer 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -924,8 +926,8 @@ select:focus-visible,
|
||||
}
|
||||
|
||||
@keyframes card-shimmer {
|
||||
from { background-position: 130% 0, 0 0; }
|
||||
to { background-position: -120% 0, 0 0; }
|
||||
from { transform: translate3d(-100%, 0, 0); }
|
||||
to { transform: translate3d(100%, 0, 0); }
|
||||
}
|
||||
|
||||
.skeleton-grid span,
|
||||
@@ -933,19 +935,11 @@ select:focus-visible,
|
||||
position: absolute;
|
||||
display: block;
|
||||
min-height: 64px;
|
||||
background:
|
||||
linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, .48) 47%, transparent 64%),
|
||||
#e3e7e5;
|
||||
background-size: 220% 100%, 100% 100%;
|
||||
overflow: hidden;
|
||||
background: #e3e7e5;
|
||||
border: 1px solid rgba(26, 35, 31, .08);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 1px 1px rgba(21, 31, 27, .05);
|
||||
animation: card-shimmer 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
from { background-position: 130% 0; }
|
||||
to { background-position: -120% 0; }
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -1451,11 +1445,8 @@ select:focus-visible,
|
||||
}
|
||||
|
||||
.viewer-similar-card::before,
|
||||
.viewer-similar-skeleton span {
|
||||
background:
|
||||
linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, .07) 47%, transparent 64%),
|
||||
#1b211e;
|
||||
background-size: 220% 100%, 100% 100%;
|
||||
.viewer-similar-skeleton span::before {
|
||||
background: linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, .07) 47%, transparent 64%);
|
||||
}
|
||||
|
||||
.viewer-similar-card:focus-visible {
|
||||
@@ -1683,9 +1674,6 @@ select:focus-visible,
|
||||
/* Reserve three 42px tools, their gaps and the drawer clearance. */
|
||||
--mobile-drawer-width: min(286px, calc(100vw - 144px - max(12px, env(safe-area-inset-right))));
|
||||
}
|
||||
.app-shell[data-mobile-navigation-open="true"] {
|
||||
--sidebar-layout-width: var(--mobile-drawer-width);
|
||||
}
|
||||
.topbar {
|
||||
position: fixed;
|
||||
top: var(--visual-viewport-top);
|
||||
@@ -1697,20 +1685,11 @@ select:focus-visible,
|
||||
max(12px, env(safe-area-inset-left));
|
||||
gap: 8px;
|
||||
}
|
||||
.topbar::before {
|
||||
.topbar-surface {
|
||||
top: var(--visual-viewport-top);
|
||||
background: #fcfdfc;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
transition: left 240ms cubic-bezier(.55, .05, .8, .45);
|
||||
}
|
||||
.request-progress {
|
||||
transition:
|
||||
opacity 180ms ease,
|
||||
left 240ms cubic-bezier(.55, .05, .8, .45);
|
||||
}
|
||||
.app-shell[data-mobile-navigation-open="true"] .topbar::before,
|
||||
.app-shell[data-mobile-navigation-open="true"] .request-progress {
|
||||
transition-timing-function: var(--ease-standard);
|
||||
}
|
||||
.gallery-sidebar-toggle.sidebar-toggle-button {
|
||||
width: 42px;
|
||||
@@ -1812,11 +1791,13 @@ select:focus-visible,
|
||||
.mobile-nav-layer[data-state="closing"] { pointer-events: none; }
|
||||
.mobile-nav-scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
inset: var(--topbar-height) 0 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
background: rgba(18, 25, 22, .32);
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
.mobile-sidebar {
|
||||
position: absolute;
|
||||
@@ -1824,36 +1805,31 @@ select:focus-visible,
|
||||
width: var(--mobile-drawer-width);
|
||||
overflow: hidden;
|
||||
box-shadow: 10px 0 24px rgba(13, 21, 17, .13);
|
||||
will-change: transform;
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
transition: transform 240ms var(--ease-standard);
|
||||
}
|
||||
.mobile-sidebar .sidebar-brand {
|
||||
transform: translateX(var(--mobile-drawer-width));
|
||||
transition: transform 240ms var(--ease-standard);
|
||||
}
|
||||
.mobile-nav-layer[data-state="open"] .mobile-nav-scrim {
|
||||
animation: mobile-scrim-in 180ms ease-out both;
|
||||
opacity: 1;
|
||||
}
|
||||
.mobile-nav-layer[data-state="open"] .mobile-sidebar {
|
||||
animation: mobile-drawer-in 240ms var(--ease-standard) both;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
.mobile-nav-layer[data-state="open"] .sidebar-brand {
|
||||
animation: mobile-brand-in 240ms var(--ease-standard) both;
|
||||
transform: translateX(0);
|
||||
}
|
||||
.mobile-nav-layer[data-state="closing"] .mobile-nav-scrim {
|
||||
animation: mobile-scrim-out 180ms ease-in both;
|
||||
}
|
||||
.mobile-nav-layer[data-state="closing"] .mobile-sidebar {
|
||||
animation: mobile-drawer-out 240ms cubic-bezier(.55, .05, .8, .45) both;
|
||||
}
|
||||
.mobile-nav-layer[data-state="closing"] .sidebar-brand {
|
||||
animation: mobile-brand-out 240ms cubic-bezier(.55, .05, .8, .45) both;
|
||||
@starting-style {
|
||||
.mobile-nav-layer[data-state="open"] .mobile-nav-scrim { opacity: 0; }
|
||||
.mobile-nav-layer[data-state="open"] .mobile-sidebar { transform: translate3d(-100%, 0, 0); }
|
||||
.mobile-nav-layer[data-state="open"] .sidebar-brand { transform: translateX(var(--mobile-drawer-width)); }
|
||||
}
|
||||
.mobile-sidebar .sidebar-status {
|
||||
min-height: calc(50px + env(safe-area-inset-bottom));
|
||||
padding-bottom: calc(11px + env(safe-area-inset-bottom));
|
||||
}
|
||||
@keyframes mobile-scrim-in { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes mobile-scrim-out { from { opacity: 1; } to { opacity: 0; } }
|
||||
@keyframes mobile-drawer-in { from { transform: translateX(-100%); } to { transform: translateX(0); } }
|
||||
@keyframes mobile-drawer-out { from { transform: translateX(0); } to { transform: translateX(-100%); } }
|
||||
@keyframes mobile-brand-in { from { transform: translateX(var(--mobile-drawer-width)); } to { transform: translateX(0); } }
|
||||
@keyframes mobile-brand-out { from { transform: translateX(0); } to { transform: translateX(var(--mobile-drawer-width)); } }
|
||||
}
|
||||
|
||||
@media (max-width: 340px) {
|
||||
@@ -1928,8 +1904,10 @@ select:focus-visible,
|
||||
}
|
||||
|
||||
.image-card::before,
|
||||
.skeleton-grid span,
|
||||
.viewer-similar-skeleton span {
|
||||
background-position: 0 0;
|
||||
.viewer-similar-card::before,
|
||||
.skeleton-grid span::before,
|
||||
.viewer-similar-skeleton span::before {
|
||||
animation: none !important;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user