From 3326169a4dc7abe974f08d739c5d2fc8cad65c91 Mon Sep 17 00:00:00 2001 From: daguanren21 <1363917353@qq.com> Date: Mon, 21 Sep 2026 18:58:53 +0800 Subject: [PATCH 1/2] feat(web): refine project proof grid and scroll reveal --- apps/web/src/components/HomePage.astro | 58 ++++++++++- .../components/home/HomeProjectProof.astro | 95 ++++++++++++++++++- apps/web/src/styles/global.css | 33 +++++++ apps/web/tests/e2e/site.spec.ts | 51 +++++++++- 4 files changed, 232 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/HomePage.astro b/apps/web/src/components/HomePage.astro index 9ba0270..467d283 100644 --- a/apps/web/src/components/HomePage.astro +++ b/apps/web/src/components/HomePage.astro @@ -98,7 +98,7 @@ const projectStages: Record = locale === 'zh-CN'
-
+
{visualCopy.alt} @@ -139,6 +139,10 @@ const projectStages: Record = locale === 'zh-CN' ) })}
+
+

{locale === 'zh-CN' ? '构建透镜' : 'BUILD LENS'}

+

{copy.hero.visualLabel}

+
{homeProjects.map(project => )}
@@ -268,3 +272,55 @@ const projectStages: Record = locale === 'zh-CN'
+ diff --git a/apps/web/src/components/home/HomeProjectProof.astro b/apps/web/src/components/home/HomeProjectProof.astro index ec900d6..f7a563a 100644 --- a/apps/web/src/components/home/HomeProjectProof.astro +++ b/apps/web/src/components/home/HomeProjectProof.astro @@ -60,12 +60,101 @@ const planned = status === 'planned' diff --git a/apps/web/src/styles/global.css b/apps/web/src/styles/global.css index 2165a06..5270128 100644 --- a/apps/web/src/styles/global.css +++ b/apps/web/src/styles/global.css @@ -497,6 +497,7 @@ color: var(--accent); } + .home-project-visual-link { display: block; min-width: 0; @@ -525,6 +526,38 @@ box-shadow: none; } +/* The screenshot is the proof; perspective connects it to the project row as it enters view. */ +.home-project-visual-frame { + transform-origin: center bottom; +} +@media (prefers-reduced-motion: no-preference) { + html[data-project-proof-motion] [data-scroll-proof] { + opacity: var(--proof-opacity, 0.62); + transform: perspective(1400px) rotateX(var(--proof-rotate, 18deg)) translateY(var(--proof-lift, 3.5rem)) scale(var(--proof-scale, 0.84)); + } +} + +.toolchain-proof-heading { + margin-top: clamp(3rem, 7vw, 5.5rem); + margin-bottom: 1rem; +} +.toolchain-proof-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; + align-items: stretch; +} +@media (max-width: 980px) { + .toolchain-proof-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +@media (max-width: 560px) { + .toolchain-proof-grid { + grid-template-columns: minmax(0, 1fr); + } +} + .section-intro { max-width: 720px; margin-bottom: 2.25rem; diff --git a/apps/web/tests/e2e/site.spec.ts b/apps/web/tests/e2e/site.spec.ts index dcc89c3..cf32119 100644 --- a/apps/web/tests/e2e/site.spec.ts +++ b/apps/web/tests/e2e/site.spec.ts @@ -12,6 +12,7 @@ async function expectHomeVisuals(page: import('@playwright/test').Page, locale: await expect(page.getByRole('heading', { level: 1, name: 'weapp.dev' })).toBeAttached() await expect(page.locator('#home-hero-title')).toHaveText('weapp.dev') await expect(page.locator('.home-hero-screen')).toBeVisible() + await expect(page.locator('[data-scroll-proof]')).toHaveCount(3) await expect(page.locator('.home-hero-constellation .home-hero-tile')).toHaveCount(6) await expect(page.locator('.home-hero-constellation a.home-hero-tile').evaluateAll(links => links.map(link => ({ href: link.getAttribute('href'), @@ -266,7 +267,7 @@ test('hero planets stay between the wordmark and the first-screen edges', async test('reduced motion keeps content visible and product interactions stationary', async ({ page }) => { await page.emulateMedia({ reducedMotion: 'reduce' }) await page.goto('/') - const movingOrHidden = () => page.locator('[data-reveal], [data-hero-enter], [data-project-visual] img').evaluateAll(elements => elements.filter((element) => { + const movingOrHidden = () => page.locator('[data-reveal], [data-hero-enter], [data-project-visual] img, [data-scroll-proof]').evaluateAll(elements => elements.filter((element) => { const style = getComputedStyle(element) return style.opacity !== '1' || style.transform !== 'none' || style.animationName !== 'none' || style.transitionDuration !== '0s' }).map(element => element.tagName)) @@ -282,6 +283,54 @@ test('reduced motion keeps content visible and product interactions stationary', expect(await page.evaluate(() => document.getAnimations().length)).toBe(0) }) +test('disables proof motion when reduced motion changes at runtime', async ({ page }) => { + await page.emulateMedia({ reducedMotion: 'no-preference' }) + await page.goto('/') + await expect(page.locator('html')).toHaveAttribute('data-project-proof-motion', '') + await page.emulateMedia({ reducedMotion: 'reduce' }) + await expect.poll(() => page.locator('html').getAttribute('data-project-proof-motion')).toBeNull() + await expect.poll(() => page.locator('[data-scroll-proof]').first().evaluate(element => ({ + opacity: getComputedStyle(element).opacity, + transform: getComputedStyle(element).transform, + }))).toEqual({ opacity: '1', transform: 'none' }) +}) + +test('scrolls project proof frames into place', async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }) + await page.goto('/') + const proof = page.locator('[data-scroll-proof]').first() + await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' })) + await page.waitForTimeout(50) + const initial = await proof.evaluate(element => ({ + opacity: getComputedStyle(element).opacity, + transform: getComputedStyle(element).transform, + })) + await page.evaluate(() => window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' })) + await page.waitForTimeout(100) + const settled = await proof.evaluate(element => ({ + opacity: getComputedStyle(element).opacity, + transform: getComputedStyle(element).transform, + })) + expect(Number(settled.opacity)).toBeGreaterThan(Number(initial.opacity)) + expect(settled.transform).not.toBe(initial.transform) +}) + +test('keeps proof cards comparative across responsive widths', async ({ page }) => { + for (const viewport of [ + { width: 1440, columns: 3 }, + { width: 700, columns: 2 }, + { width: 390, columns: 1 }, + ]) { + await page.setViewportSize({ width: viewport.width, height: 900 }) + await page.goto('/') + const layout = await page.locator('.toolchain-proof-grid').evaluate(element => ({ + cards: element.querySelectorAll('.home-project-proof').length, + columns: getComputedStyle(element).gridTemplateColumns.split(' ').length, + })) + expect(layout, `${viewport.width}px proof grid`).toEqual({ cards: 5, columns: viewport.columns }) + } +}) + test('reveals content after the timeout when the observer never reports visibility', async ({ page }) => { await page.emulateMedia({ reducedMotion: 'no-preference' }) await page.clock.install() From 898251463086416ea93c687e4510a45a6da13d17 Mon Sep 17 00:00:00 2001 From: daguanren21 <1363917353@qq.com> Date: Tue, 22 Sep 2026 12:01:36 +0800 Subject: [PATCH 2/2] fix(web): satisfy CI stylesheet lint --- apps/web/src/styles/global.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/styles/global.css b/apps/web/src/styles/global.css index d674aaa..c7f6200 100644 --- a/apps/web/src/styles/global.css +++ b/apps/web/src/styles/global.css @@ -502,7 +502,6 @@ color: var(--accent); } - .home-project-visual-link { display: block; min-width: 0; @@ -537,7 +536,8 @@ @media (prefers-reduced-motion: no-preference) { html[data-project-proof-motion] [data-scroll-proof] { opacity: var(--proof-opacity, 0.62); - transform: perspective(1400px) rotateX(var(--proof-rotate, 18deg)) translateY(var(--proof-lift, 3.5rem)) scale(var(--proof-scale, 0.84)); + transform: perspective(1400px) rotateX(var(--proof-rotate, 18deg)) translateY(var(--proof-lift, 3.5rem)) + scale(var(--proof-scale, 0.84)); } }