From 32e8ed98bf185bf0219466ad88f35e0bab8bfa3a Mon Sep 17 00:00:00 2001 From: Nicholas Petrasek Date: Thu, 1 Oct 2026 10:07:36 +0200 Subject: [PATCH] feat: add homepage project support links --- e2e/support-cta.spec.ts | 84 +++++++++++++++++++++++++++++++++++++++++ website/src/main.tsx | 21 +++++++++++ website/src/site.css | 59 +++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 e2e/support-cta.spec.ts diff --git a/e2e/support-cta.spec.ts b/e2e/support-cta.spec.ts new file mode 100644 index 0000000..c96103c --- /dev/null +++ b/e2e/support-cta.spec.ts @@ -0,0 +1,84 @@ +import { expect, test } from '@playwright/test' + +for (const width of [1440, 390, 320]) { + test(`homepage support links remain usable at ${String(width)}px`, async ({ + page, + browserName, + }, testInfo) => { + await page.setViewportSize({ width, height: 900 }) + await page.goto('/') + const support = page.getByRole('region', { + name: 'Useful in your project?', + }) + await expect(support).toBeVisible() + await expect( + support.getByRole('heading', { + level: 2, + name: 'Useful in your project?', + }), + ).toBeVisible() + expect( + await support.evaluate((section) => + section.previousElementSibling?.matches('.hero'), + ), + ).toBe(true) + + const star = support.getByRole('link', { name: 'Star on GitHub' }) + const explore = support.getByRole('link', { + name: 'Explore NIPE Open Source', + }) + await expect(star).toHaveAttribute( + 'href', + 'https://github.com/NIPE-Solutions/react-pull-to-refresh', + ) + await expect(explore).toHaveAttribute( + 'href', + 'https://opensource.nipesolutions.com', + ) + await expect(support.locator('p')).not.toBeEmpty() + await support.scrollIntoViewIfNeeded() + for (const link of [star, explore]) { + await expect(link).toBeVisible() + const box = await link.boundingBox() + expect(box).not.toBeNull() + if (!box) throw new Error('Support link is not measurable') + expect(box.height).toBeGreaterThanOrEqual(44) + expect(box.width).toBeGreaterThanOrEqual(44) + expect(box.x).toBeGreaterThanOrEqual(0) + expect(box.x + box.width).toBeLessThanOrEqual(width) + } + expect( + await support.evaluate( + (section) => section.scrollWidth <= section.clientWidth, + ), + ).toBe(true) + + await star.focus() + const macWebKit = browserName === 'webkit' && process.platform === 'darwin' + const tab = macWebKit ? 'Alt+Tab' : 'Tab' + await page.keyboard.press(macWebKit ? 'Shift+Alt+Tab' : 'Shift+Tab') + await page.keyboard.press(tab) + await expect(star).toBeFocused() + const outline = await star.evaluate((link) => { + const style = getComputedStyle(link) + return { + width: parseFloat(style.outlineWidth), + style: style.outlineStyle, + } + }) + expect(outline.width).toBeGreaterThanOrEqual(2) + expect(outline.style).not.toBe('none') + await page.keyboard.press(tab) + await expect(explore).toBeFocused() + await expect(explore).toHaveCSS('outline-style', 'solid') + expect( + await explore.evaluate((link) => + parseFloat(getComputedStyle(link).outlineWidth), + ), + ).toBeGreaterThanOrEqual(2) + if (process.env.SUPPORT_CTA_SCREENSHOTS === '1') + await support.screenshot({ + path: testInfo.outputPath('support-cta.png'), + }) + }) +} diff --git a/website/src/main.tsx b/website/src/main.tsx index 8a15804..82c75ed 100644 --- a/website/src/main.tsx +++ b/website/src/main.tsx @@ -314,6 +314,27 @@ function App() { +
+
+

Useful in your project?

+

+ If React Pull to Refresh helps you build, a GitHub star supports + the project and helps others discover it. +

+
+
+ + Star on GitHub + + + Explore NIPE Open Source + +
+
+ diff --git a/website/src/site.css b/website/src/site.css index b38838d..5ceae47 100644 --- a/website/src/site.css +++ b/website/src/site.css @@ -420,6 +420,61 @@ footer a { font-size: 0.68rem; } +.support-cta { + display: flex; + max-width: 1320px; + margin: 0 auto; + padding: 48px 32px; + flex-wrap: wrap; + align-items: center; + gap: 32px; + border-bottom: 1px solid var(--line); +} + +.support-cta__copy { + flex: 1 1 480px; + min-width: 0; +} + +.support-cta h2 { + font-size: clamp(1.85rem, 3vw, 2.5rem); +} + +.support-cta p { + max-width: 64ch; + margin: 14px 0 0; + color: var(--ink-soft); + line-height: 1.6; +} + +.support-cta__actions { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.support-cta__actions a { + display: inline-flex; + min-height: 44px; + align-items: center; + justify-content: center; + padding: 12px 18px; + border: 1px solid var(--line); + font-weight: 700; + text-decoration: none; +} + +.support-cta__actions .support-cta__primary { + border-color: var(--ink); + color: var(--tension); + background: var(--ink); +} + +.support-cta__actions a:focus-visible { + outline: 3px solid var(--ink); + outline-offset: 4px; +} + .section-shell { max-width: 1320px; margin: 0 auto; @@ -952,6 +1007,10 @@ footer { } @media (max-width: 760px) { + .support-cta { + padding: 32px 22px; + } + .section-shell, .browser-section { padding: 82px 22px;