diff --git a/public/styles.css b/public/styles.css index 276f238..9d4fdd7 100644 --- a/public/styles.css +++ b/public/styles.css @@ -50,62 +50,97 @@ textarea, transform: scale(0.90) translateY(-4px); } +/* keep the page tappable mid-transition (rapid tab switching) */ +::view-transition { + pointer-events: none; +} + +/* Theme texture overlays (scanlines, rays, grain) paint ABOVE all content; a + real element with its own transition group keeps them on top and static + mid-transition — a body::before overlay sinks into the root snapshot and + blinks off the content for the duration of every transition. */ +.flxn-theme-overlay { + position: fixed; + pointer-events: none; + view-transition-name: theme-overlay; +} + +/* Root (wallpaper + tab bar) is identical across routes — swap it instantly + instead of crossfading; Safari's plus-lighter blend of the pair dims the + whole backdrop mid-fade. Same explicit-state treatment for the header and + theme overlay. */ ::view-transition-old(root), -::view-transition-new(root), -::view-transition-old(page), -::view-transition-new(page), ::view-transition-old(app-header), -::view-transition-new(app-header) { +::view-transition-old(theme-overlay) { + animation: none; + opacity: 0; +} +::view-transition-new(root), +::view-transition-new(app-header), +::view-transition-new(theme-overlay) { + animation: none; + opacity: 1; + mix-blend-mode: normal; +} +::view-transition-image-pair(root), +::view-transition-image-pair(app-header), +::view-transition-image-pair(theme-overlay), +::view-transition-image-pair(page) { + isolation: auto; +} + +::view-transition-old(page), +::view-transition-new(page) { animation-duration: 180ms; animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1); + mix-blend-mode: normal; } +/* content pushes edge-to-edge over the static theme background; panes stay + transparent so the background never appears to move. The header is not in + these rules on purpose: it crossfades in place (its own group's default) — + sliding its chrome reads as a band appearing on continuous-bg themes */ html[data-nav-direction="forward"]::view-transition-old(page) { - animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-under-out; + animation: 250ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-out-left; } html[data-nav-direction="forward"]::view-transition-new(page) { - animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-over-in; + animation: 250ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-in-right; } html[data-nav-direction="back"]::view-transition-old(page) { - animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-over-out; - z-index: 1; + animation: 250ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-out-right; } html[data-nav-direction="back"]::view-transition-new(page) { - animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-under-in; + animation: 250ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-in-left; } +/* a header appearing or disappearing rides with its page instead of popping; + opacity overrides the pinned instant-swap defaults above */ html[data-nav-direction="forward"][data-header-vt="exit"]::view-transition-old(app-header) { - animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-header-out; + animation: 250ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-out-left; + opacity: 1; } html[data-nav-direction="back"][data-header-vt="exit"]::view-transition-old(app-header) { - animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-over-out; + animation: 250ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-out-right; + opacity: 1; } html[data-nav-direction="forward"][data-header-vt="enter"]::view-transition-new(app-header) { - animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-over-in; + animation: 250ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-in-right; } html[data-nav-direction="back"][data-header-vt="enter"]::view-transition-new(app-header) { - animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-header-in; + animation: 250ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-in-left; } -@keyframes flxn-page-over-in { +@keyframes flxn-page-out-left { + to { transform: translateX(-100%); } +} +@keyframes flxn-page-in-right { from { transform: translateX(100%); } } -@keyframes flxn-page-over-out { +@keyframes flxn-page-out-right { to { transform: translateX(100%); } } -@keyframes flxn-page-under-out { - to { transform: translateX(-25%); opacity: 0.8; } -} -@keyframes flxn-page-under-in { - from { transform: translateX(-25%); opacity: 0.8; } -} - -@keyframes flxn-header-out { - 45% { opacity: 0; } - to { transform: translateX(-25%); opacity: 0; } -} -@keyframes flxn-header-in { - from { transform: translateX(-25%); opacity: 0; } +@keyframes flxn-page-in-left { + from { transform: translateX(-100%); } } @keyframes flxn-vt-fade-out { @@ -130,6 +165,15 @@ html[data-nav-direction="back"][data-header-vt="enter"]::view-transition-new(app animation: 120ms ease both flxn-vt-fade-in !important; } + ::view-transition-old(root), + ::view-transition-old(app-header), + ::view-transition-old(theme-overlay), + ::view-transition-new(root), + ::view-transition-new(app-header), + ::view-transition-new(theme-overlay) { + animation: none !important; + } + .app, [data-drawer-level] { transition: none; diff --git a/src/app/routes/__root.tsx b/src/app/routes/__root.tsx index a81eb22..677f57d 100644 --- a/src/app/routes/__root.tsx +++ b/src/app/routes/__root.tsx @@ -183,7 +183,19 @@ function RootComponent() { ensureSuperTokensFrontend(); initTelemetry(router); if (import.meta.env.PROD && 'serviceWorker' in navigator) { - navigator.serviceWorker.register('/sw.js').catch(() => track('sw_register_failed')); + // first install claims the page too; only reload on real updates + const hadController = !!navigator.serviceWorker.controller; + let reloading = false; + navigator.serviceWorker.addEventListener('controllerchange', () => { + if (!hadController || reloading) return; + reloading = true; + window.location.reload(); + }); + navigator.serviceWorker.register('/sw.js').then((registration) => { + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') registration.update().catch(() => {}); + }); + }).catch(() => track('sw_register_failed')); } }, [router]); @@ -243,6 +255,7 @@ function RootDocument({ children }: { children: React.ReactNode }) { }} >
{children}
+