feat: finish portfolio title, navigation, and destination effects

Add the approved name animation, rounded docking navigation, localized keyboard focus cue, unified builds card, and destination copy hierarchy.

Introduce lightweight one-shot meteors and a sun ring that repeats every three seconds at Contact. Open the email action in a separate browsing context. Preserve reduced-motion behavior and keep private handoff and visual-check artifacts out of version control.

Validation: 649 tests passed; all 20 failures in world-models and SceneCanvas reproduce on baseline 103427f. Typecheck, artifact verification, and transfer budgets passed. Client/SSR preview build passed before this commit. Existing baseline test failures remain unchanged.
This commit is contained in:
2026-09-05 22:16:42 +03:00
parent 103427f95b
commit f46131723b
31 changed files with 948 additions and 51 deletions

View File

@@ -1,6 +1,40 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '../helpers/serve-dist';
test('builds heading shares the first project surface and contact keeps a clear copy hierarchy', async ({ page, distUrl }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto(distUrl);
const joined = await page.locator('#selected-builds-heading').evaluate(heading => {
const surface = heading.closest('.project-stage__lead');
return Boolean(surface?.querySelector('.project-case-study') &&
getComputedStyle(surface).backgroundColor !== 'rgba(0, 0, 0, 0)');
});
expect(joined).toBe(true);
const hierarchy = await page.locator('#contact').evaluate(section => {
const lead = section.querySelector('.contact-destination__copy');
const availability = section.querySelector('.contact-destination__availability');
return Boolean(lead && availability &&
parseFloat(getComputedStyle(lead).fontSize) > parseFloat(getComputedStyle(availability).fontSize));
});
expect(hierarchy).toBe(true);
});
test('the live reading mask measures the inset builds heading', async ({ page, distUrl }) => {
await page.goto(distUrl);
await expect(page.locator('html')).toHaveAttribute('data-expressway', 'live');
const geometry = await page.locator('#selected-builds').evaluate(section => ({
offset: parseFloat((section as HTMLElement).style.getPropertyValue('--chapter-heading-offset')) || 0,
actual: section.querySelector('h2')!.getBoundingClientRect().top - section.getBoundingClientRect().top,
}));
expect(geometry.offset).toBeGreaterThan(20);
expect(geometry.offset).toBeCloseTo(geometry.actual, 1);
const disclosure = page.locator('.site-navigation summary');
if (await disclosure.isVisible()) await disclosure.click();
await page.locator('.site-navigation a[href="#selected-builds"]').click();
await expect.poll(() => page.locator('#selected-builds-heading').evaluate(heading =>
Number(getComputedStyle(heading).opacity))).toBe(1);
});
test('start navigation, contact identity, and toolkit icons use the approved content', async ({ page, distUrl }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
const response = await page.goto(distUrl);
@@ -11,6 +45,10 @@ test('start navigation, contact identity, and toolkit icons use the approved con
await page.locator('.hero-journey__action--primary').click();
await expect(page).toHaveURL(/#contact$/);
await expect(page.locator('#contact')).toBeInViewport();
await expect(page.locator('.journey-world')).toHaveAttribute('data-arrived', 'true');
await expect(page.locator('.journey-world')).toHaveAttribute('data-static-fallback', 'true');
await expect(page.locator('.journey-canvas')).toHaveCount(0);
expect(await page.locator('.journey-poster').evaluate(poster => getComputedStyle(poster).filter)).not.toBe('none');
await expect(page.locator('.contact-avatar')).toBeVisible();
await expect(page.locator('.contact-social-link')).toHaveText(['LinkedIn', 'GitHub', 'GitLab']);
const missing = await page.locator('[data-testid="toolkit-technology"]').evaluateAll(items => items.filter(item => !item.querySelector('svg')).map(item => item.textContent));