Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to Previewer are listed here. The format follows

## [Unreleased]

### Added

- The mouse pointer is a fingertip inside the phone, like DevTools' touch pointer, and darkens while you press.

## [1.0.0] - 2026-10-30

### Added
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ controlled" banner on every page:

- `navigator.userAgent` in JavaScript still reports desktop Chrome. Server-side detection works; client-side sniffing
doesn't.
- Touch: `(hover: hover)` and `(pointer: fine)` still match, and you scroll with a mouse or trackpad.
- Touch: inside the phone the mouse pointer becomes a fingertip, but it is still a mouse. Hover effects still work,
`(hover: hover)` and `(pointer: fine)` still match, and you scroll with a mouse wheel or trackpad.
- The rendering engine is Chrome's Blink, not Safari's WebKit. Previewer shows layout, not WebKit-specific bugs.

If a link inside the phone leads to a different website that refuses to be shown in a frame, Chrome shows its own
Expand Down
19 changes: 16 additions & 3 deletions extension/overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@
tablet: `<span>9:41</span><span class="icons">${WIFI}100%${BATTERY}</span>`,
};

// A fingertip like DevTools' touch pointer: grey with a light and a dark ring so
// it shows on any background, darker while pressed. Hotspot in the centre.
const touchCursor = (fill, r) => {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><circle cx="12" cy="12" r="${r + 1}" fill="none" stroke="rgba(0,0,0,.28)"/><circle cx="12" cy="12" r="${r}" fill="${fill}" stroke="rgba(255,255,255,.9)" stroke-width="1.5"/></svg>`;
return `url("data:image/svg+xml,${encodeURIComponent(svg)}") 12 12, default`;
};
const FRAME_CSS = `
html { scrollbar-width: none; }
::-webkit-scrollbar { display: none; }
*, *::before, *::after { cursor: ${touchCursor('rgba(70,74,82,.38)', 10)} !important; }
*:active, *:active::before, *:active::after { cursor: ${touchCursor('rgba(70,74,82,.7)', 9)} !important; }`;

let ui = null; // everything that exists while the preview is open

function open({ devices, settings, css }) {
Expand Down Expand Up @@ -265,13 +277,14 @@
}

// Phones overlay their scrollbars; hide desktop scrollbars so the page gets
// its full width (Windows otherwise takes ~15px of a 402px viewport).
// its full width (Windows otherwise takes ~15px of a 402px viewport). Show a
// fingertip instead of the mouse pointer, so it looks and feels like touch.
try {
const sheet = new doc.defaultView.CSSStyleSheet();
sheet.replaceSync('html { scrollbar-width: none; } ::-webkit-scrollbar { display: none; }');
sheet.replaceSync(FRAME_CSS);
doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, sheet];
} catch {
// Not critical: the page just keeps its scrollbar.
// Not critical: the page keeps its scrollbar and mouse pointer.
}

doc.addEventListener('keydown', onKeydown, true);
Expand Down
1 change: 1 addition & 0 deletions test/e2e/preview.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ test('shows the page at the phone width with the phone user agent', async () =>
assert.equal(await frame.evaluate(() => innerWidth), 402);
assert.match(await frame.evaluate(() => navigator.userAgent), /Chrome/, 'JS user agent is not overridden (documented)');
assert.match(seenUserAgents.at(-1), /iPhone/);
assert.match(await frame.evaluate(() => getComputedStyle(document.querySelector('.hero')).cursor), /^url\("data:image\/svg\+xml/, 'touch pointer');
await page.waitForTimeout(300);
await shoot(page, 'iphone-18-pro');

Expand Down
Loading