diff --git a/index.html b/index.html index d117229..0234c86 100644 --- a/index.html +++ b/index.html @@ -28,6 +28,13 @@ {{t:format.audio}} {{t:nav.star}} +
+ + +
`, + ); + return ``; +} diff --git a/src/style.css b/src/style.css index 83bef89..dc157e0 100644 --- a/src/style.css +++ b/src/style.css @@ -234,7 +234,7 @@ main { align-items: center; gap: 2px; } -.site-header nav a { +.site-header nav > a { padding: 6px 10px; border-radius: 8px; color: var(--muted); @@ -243,7 +243,7 @@ main { text-decoration: none; transition: color 0.15s, background-color 0.15s; } -.site-header nav a:hover { +.site-header nav > a:hover { color: var(--text); background: var(--surface-2); } @@ -345,7 +345,7 @@ main { } @media (max-width: 900px) { - .site-header nav a:not(.star-btn) { + .site-header nav > a:not(.star-btn) { display: none; } } @@ -355,6 +355,29 @@ main { display: none; } } +/* Room for the share button next to the theme switch on narrow phones. */ +@media (max-width: 420px) { + .star-btn > svg:first-child { + display: none; + } + .site-header .lang select { + max-width: 84px; + } +} +/* The smallest phones: the theme already follows the device, so its switch gives way to sharing. */ +@media (max-width: 380px) { + .site-header .theme-toggle:not(.share-btn) { + display: none; + } +} +@media (max-width: 340px) { + .site-header .bar { + gap: 8px; + } + .site-header .lang select { + max-width: 70px; + } +} /* ---------- Badges & chips ---------- */ @@ -1776,3 +1799,71 @@ dialog p { color: var(--muted); font-size: 0.8rem; } + +/* Share with a friend: header menu, the box under the star call-out, and the line after a result. */ +.share { + position: relative; +} +.share-pop { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 40; + width: min(300px, calc(100vw - 32px)); + padding: 14px; + border: 1px solid var(--line); + border-radius: 16px; + background: var(--surface); + box-shadow: var(--shadow-md); +} +.share-pop p { + margin: 0 0 10px; + font-size: 0.9rem; + color: var(--muted); +} +.share-links { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.share-link { + display: inline-grid; + place-items: center; + width: 40px; + height: 40px; + padding: 0; + border: 1px solid var(--line); + border-radius: 50%; + background: var(--surface-2, var(--surface)); + color: var(--text); + cursor: pointer; + transition: transform 0.15s, border-color 0.15s; +} +.share-link svg { + width: 18px; + height: 18px; + fill: currentColor; +} +.share-link:hover, +.share-link:focus-visible { + transform: translateY(-2px); + border-color: var(--line-strong); +} +.share-link.done { + border-color: #1d9a5b; + color: #1d9a5b; +} +.share-cta { + flex-basis: 100%; + padding-top: 20px; + border-top: 1px solid var(--line); +} +.share-cta p, +.share-nudge p { + margin: 0 0 10px; + color: var(--muted); + font-size: 0.92rem; +} +.share-nudge { + margin-top: 14px; +} diff --git a/tests/app.e2e.ts b/tests/app.e2e.ts index 8c79134..21d61ba 100644 --- a/tests/app.e2e.ts +++ b/tests/app.e2e.ts @@ -1026,3 +1026,36 @@ test('publishes sitemap, robots.txt and llms.txt', async ({ request }) => { expect(await (await request.get('/robots.txt')).text()).toContain('User-agent: *\nAllow: /'); expect((await request.get('/og.png')).headers()['content-type']).toBe('image/png'); }); + +test('shares Fizzdoc: the phone’s share sheet, or links to the big networks and a copy button', async ({ browser }) => { + const context = await browser.newContext({ permissions: ['clipboard-read', 'clipboard-write'] }); + const page = await context.newPage(); + await page.goto('/merge-pdf/'); + await page.getByRole('button', { name: 'Share Fizzdoc' }).click(); + const menu = page.locator('#share-pop'); + await expect(menu).toBeVisible(); + const whatsapp = menu.getByRole('link', { name: 'Share on WhatsApp' }); + await expect(whatsapp).toHaveAttribute('href', /^https:\/\/wa\.me\/\?text=Merge%20PDF.*fizzdoc\.com%2Fmerge-pdf%2F$/); + await expect(menu.getByRole('link', { name: 'Share on LinkedIn' })).toHaveAttribute('href', 'https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Ffizzdoc.com%2Fmerge-pdf%2F'); + await menu.getByRole('button', { name: 'Copy link' }).click(); + await expect(menu.getByRole('button', { name: 'Link copied' })).toBeVisible(); + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe('https://fizzdoc.com/merge-pdf/'); + await page.keyboard.press('Escape'); + await expect(menu).toBeHidden(); + // The same links sit under the star call-out on every page. + await expect(page.locator('.share-cta .share-link')).toHaveCount(8); + await context.close(); + + // On a phone the system share sheet opens instead of the menu. + const phone = await browser.newContext({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true }); + const mobile = await phone.newPage(); + await mobile.addInitScript(() => { + Object.defineProperty(navigator, 'share', { value: async (data: ShareData) => ((window as unknown as { shared: ShareData }).shared = data) }); + }); + await mobile.goto('/hi/'); + await mobile.locator('#share-btn').tap(); + expect(await mobile.evaluate(() => (window as unknown as { shared: ShareData }).shared.url)).toBe('https://fizzdoc.com/hi/'); + await expect(mobile.locator('#share-pop')).toBeHidden(); + expect(await mobile.evaluate(() => document.documentElement.scrollWidth)).toBe(390); + await phone.close(); +});