mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Chore(ui): device-side switch for page transitions
`?vt=off` turns the View Transitions off, `?vt=unnamed` keeps them but leaves the Nav un-named (it slides with the page), `?vt=default` restores the normal behaviour; the choice lives in localStorage. WebKit cannot run in the sandbox and the Nav's animations stall on iOS after a transition, so this is how the trigger gets bisected on the device. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
This commit is contained in:
4 files changed
+71
-4
No files matched your search
@@ -192,6 +192,8 @@ Every "business state → color" mapping goes through `use-status-tone.js`
|
||||
`view-transition-name` (only while one runs); nothing else gets one, and
|
||||
the animations inside those named elements are restarted when a transition
|
||||
ends — WebKit leaves them stalled after a capture (`restartAnimations`).
|
||||
`?vt=off` / `?vt=unnamed` / `?vt=default` is a device-side diagnostic
|
||||
switch (kept in localStorage) for problems only WebKit shows.
|
||||
- **Responsive hide** — `.hidden` is `!important` (`style/style.css`), so
|
||||
`hidden sm:flex` never shows: write `max-sm:hidden`.
|
||||
- **Drawer vs Sheet** — bottom Drawer only for a full-bleed expansion of an
|
||||
|
||||
@@ -284,10 +284,10 @@ footer {
|
||||
animations inside a captured element are restarted when the transition
|
||||
ends (utils/page-transition.js, NAMED_ELEMENTS_SELECTOR — keep the
|
||||
selector and these classes in step). */
|
||||
html[data-page-transition] .jn-site-nav {
|
||||
html[data-page-transition]:not([data-page-transition-names="off"]) .jn-site-nav {
|
||||
view-transition-name: site-nav;
|
||||
}
|
||||
html[data-page-transition] .jn-site-status-bar {
|
||||
html[data-page-transition]:not([data-page-transition-names="off"]) .jn-site-status-bar {
|
||||
view-transition-name: site-status-bar;
|
||||
}
|
||||
::view-transition-old(site-nav),
|
||||
|
||||
@@ -52,6 +52,30 @@ export const NAMED_ELEMENTS_SELECTOR = '.jn-site-nav, .jn-site-status-bar';
|
||||
// Cancel + play recreates each running animation on the compositor; a paused
|
||||
// or finished one is left as it is. An infinite spin or ping restarting from
|
||||
// its first frame is not visible.
|
||||
// Device-side diagnostic switch for a rendering problem that only shows on
|
||||
// iOS (WebKit cannot run in the sandbox): `?vt=off` turns page transitions
|
||||
// off, `?vt=unnamed` keeps them but leaves the Nav un-named (it slides with
|
||||
// the page), `?vt=default` restores the normal behaviour. The choice is kept
|
||||
// in localStorage so it survives the reloads a test needs.
|
||||
export const TRANSITION_MODE_KEY = 'jn-page-transitions';
|
||||
const TRANSITION_MODES = new Set(['off', 'unnamed', 'default']);
|
||||
|
||||
export const rememberTransitionMode = (win) => {
|
||||
let mode = null;
|
||||
try {
|
||||
const asked = new URLSearchParams(win?.location?.search ?? '').get('vt');
|
||||
if (TRANSITION_MODES.has(asked)) {
|
||||
win.localStorage?.setItem(TRANSITION_MODE_KEY, asked);
|
||||
mode = asked;
|
||||
} else {
|
||||
mode = win?.localStorage?.getItem(TRANSITION_MODE_KEY) ?? null;
|
||||
}
|
||||
} catch {
|
||||
/* storage disabled: the default behaviour */
|
||||
}
|
||||
return TRANSITION_MODES.has(mode) ? mode : 'default';
|
||||
};
|
||||
|
||||
export const restartAnimations = (element) => {
|
||||
const animations = element?.getAnimations?.({ subtree: true }) ?? [];
|
||||
for (const animation of animations) {
|
||||
@@ -73,6 +97,8 @@ export const installPageTransitions = (router, {
|
||||
} = {}) => {
|
||||
if (!doc?.documentElement) return;
|
||||
const root = doc.documentElement;
|
||||
const mode = rememberTransitionMode(win);
|
||||
if (mode === 'unnamed') root.dataset.pageTransitionNames = 'off';
|
||||
let active = null; // the ViewTransition until its `finished` settles
|
||||
let endUpdate = null; // resolves the active transition's update callback
|
||||
let uaTransition = false; // the pending history step was animated by the browser
|
||||
@@ -99,7 +125,7 @@ export const installPageTransitions = (router, {
|
||||
if (inFlight && direction) active.skipTransition?.();
|
||||
const animate = shouldAnimate({
|
||||
direction,
|
||||
supported: typeof doc.startViewTransition === 'function',
|
||||
supported: mode !== 'off' && typeof doc.startViewTransition === 'function',
|
||||
reducedMotion: Boolean(win?.matchMedia?.(REDUCED_MOTION_QUERY)?.matches),
|
||||
inFlight,
|
||||
uaTransition: viaBrowser,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { describe, it } from 'node:test';
|
||||
import {
|
||||
transitionDirection,
|
||||
shouldAnimate,
|
||||
rememberTransitionMode,
|
||||
restartAnimations,
|
||||
installPageTransitions,
|
||||
} from '../frontend/utils/page-transition.js';
|
||||
@@ -22,6 +23,34 @@ const fakeAnimated = (states) => {
|
||||
return { log, getAnimations: () => animations };
|
||||
};
|
||||
|
||||
describe('rememberTransitionMode()', () => {
|
||||
const fakeWin = (search, stored = null) => {
|
||||
const store = new Map(stored ? [[ 'jn-page-transitions', stored ]] : []);
|
||||
return {
|
||||
location: { search },
|
||||
localStorage: { getItem: (k) => store.get(k) ?? null, setItem: (k, v) => store.set(k, v) },
|
||||
store,
|
||||
};
|
||||
};
|
||||
|
||||
it('defaults without a query or a stored choice', () => {
|
||||
assert.equal(rememberTransitionMode(fakeWin('')), 'default');
|
||||
assert.equal(rememberTransitionMode(undefined), 'default');
|
||||
});
|
||||
|
||||
it('stores a valid ?vt= choice and reads it back without the query', () => {
|
||||
const win = fakeWin('?vt=off');
|
||||
assert.equal(rememberTransitionMode(win), 'off');
|
||||
assert.equal(win.store.get('jn-page-transitions'), 'off');
|
||||
assert.equal(rememberTransitionMode(fakeWin('', 'unnamed')), 'unnamed');
|
||||
});
|
||||
|
||||
it('ignores an unknown value and a stored garbage value', () => {
|
||||
assert.equal(rememberTransitionMode(fakeWin('?vt=sideways', 'unnamed')), 'unnamed');
|
||||
assert.equal(rememberTransitionMode(fakeWin('', 'garbage')), 'default');
|
||||
});
|
||||
});
|
||||
|
||||
describe('restartAnimations()', () => {
|
||||
it('cancels and replays the running animations, leaves the others alone', () => {
|
||||
const el = fakeAnimated(['running', 'paused', 'finished', 'running']);
|
||||
@@ -96,7 +125,7 @@ describe('shouldAnimate()', () => {
|
||||
// A router stand-in recording the hooks, and a document whose
|
||||
// startViewTransition runs the update callback on the next task like a browser
|
||||
// does after snapshotting the old page.
|
||||
const setup = ({ supported = true, reducedMotion = false, timeoutMs = 1000 } = {}) => {
|
||||
const setup = ({ supported = true, reducedMotion = false, timeoutMs = 1000, search = '' } = {}) => {
|
||||
const hooks = { beforeResolve: null, afterEach: null, onError: null };
|
||||
const router = {
|
||||
beforeResolve: (fn) => { hooks.beforeResolve = fn; },
|
||||
@@ -128,6 +157,7 @@ const setup = ({ supported = true, reducedMotion = false, timeoutMs = 1000 } = {
|
||||
const win = {
|
||||
matchMedia: () => ({ matches: reducedMotion }),
|
||||
addEventListener: (type, fn) => { if (type === 'popstate') popstate = fn; },
|
||||
location: { search },
|
||||
};
|
||||
installPageTransitions(router, { doc, win, nextTick: () => Promise.resolve(), timeoutMs });
|
||||
return { hooks, calls, doc, nav, popstate: (event) => popstate(event) };
|
||||
@@ -155,6 +185,15 @@ describe('installPageTransitions()', () => {
|
||||
assert.equal(doc.documentElement.dataset.pageTransition, undefined);
|
||||
});
|
||||
|
||||
it('honours the diagnostic switch: off never animates, unnamed un-names the Nav', () => {
|
||||
const off = setup({ search: '?vt=off' });
|
||||
assert.equal(off.hooks.beforeResolve(whois, home), undefined);
|
||||
assert.equal(off.doc.documentElement.dataset.pageTransitionNames, undefined);
|
||||
const unnamed = setup({ search: '?vt=unnamed' });
|
||||
assert.equal(unnamed.doc.documentElement.dataset.pageTransitionNames, 'off');
|
||||
assert.ok(unnamed.hooks.beforeResolve(whois, home) instanceof Promise);
|
||||
});
|
||||
|
||||
it('restarts the named elements\' animations once the transition has finished', async () => {
|
||||
const { hooks, calls, nav } = setup();
|
||||
await hooks.beforeResolve(whois, home);
|
||||
|
||||
Reference in new issue
Block a user