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

@@ -0,0 +1,143 @@
import { expect, test } from '../helpers/serve-dist';
test('desktop navigation docks without stretching labels or shifting the document', async ({ page, distUrl }, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium');
await page.setViewportSize({ width: 1920, height: 1080 });
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto(distUrl, { waitUntil: 'networkidle' });
await page.evaluate(() => document.fonts.ready);
const nav = page.locator('.site-navigation');
await expect(nav).toHaveAttribute('data-enhanced', 'true');
const surfaceScale = () => nav.evaluate(el => new DOMMatrixReadOnly(getComputedStyle(el, '::before').transform).a);
expect(await surfaceScale()).toBeLessThan(1);
const label = nav.locator('a').first();
const initialFont = await label.evaluate(el => getComputedStyle(el).fontSize);
// Measure the content beside this fixed header, not unrelated lazy media
// farther down the page that can settle independently of navigation.
const heroGeometry = () => page.locator('#start').evaluate(el => ({
top: el.getBoundingClientRect().top + scrollY,
height: el.getBoundingClientRect().height,
}));
const initialHero = await heroGeometry();
await page.evaluate(() => scrollTo(0, 100));
await expect.poll(async () => (await nav.boundingBox())!.y).toBeLessThan(1);
await expect.poll(async () => (await nav.boundingBox())!.width).toBe(1920);
await expect.poll(surfaceScale).toBe(1);
expect(await label.evaluate(el => getComputedStyle(el).fontSize)).toBe(initialFont);
expect(await heroGeometry()).toEqual(initialHero);
await page.evaluate(() => scrollTo(0, 0));
await expect.poll(async () => (await nav.boundingBox())!.y).toBeGreaterThan(10);
});
test('selected navigation has rounded inset highlights', async ({ page, distUrl }) => {
await page.goto(distUrl);
const disclosure = page.locator('.site-navigation summary');
if (await disclosure.isVisible()) await disclosure.click();
const selected = page.locator('.site-navigation a[aria-current="page"]');
expect(await selected.evaluate(el => parseFloat(getComputedStyle(el).borderTopLeftRadius))).toBeGreaterThanOrEqual(5);
await selected.focus();
await page.keyboard.press('Shift');
expect(await selected.evaluate(el => getComputedStyle(el).outlineStyle)).not.toBe('none');
});
test('skip link uses a localized visible cue and Tab continues into content', async ({ page, distUrl }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto(distUrl);
await page.keyboard.press('Tab');
await expect(page.locator('.skip-link')).toBeFocused();
await page.keyboard.press('Enter');
const main = page.locator('#portfolio-content');
await expect(main).toBeFocused();
expect(await main.evaluate(el => getComputedStyle(el).boxShadow)).toBe('none');
expect(await main.evaluate(el => getComputedStyle(el).outlineStyle)).toBe('none');
await expect(page.locator('.content-focus-cue')).toBeVisible();
const cue = await page.locator('.content-focus-cue').boundingBox();
expect(cue!.width).toBeLessThan(240);
expect(cue!.height).toBeLessThan(60);
await page.keyboard.press('Tab');
await expect(page.locator('.hero-journey__action--primary')).toBeFocused();
expect(await page.locator('.hero-journey__action--primary').evaluate(el => getComputedStyle(el).outlineStyle)).not.toBe('none');
await expect(page.locator('.content-focus-cue')).not.toBeVisible();
});
test('Shift cannot draw a page-wide focus frame in the live reading stage', async ({ page, distUrl }) => {
await page.goto(distUrl);
await page.locator('main').evaluate(el => (el as HTMLElement).focus({ preventScroll: true }));
await page.keyboard.press('Shift');
expect(await page.locator('main').evaluate(el => ({ outline: getComputedStyle(el).outlineStyle, shadow: getComputedStyle(el).boxShadow }))).toEqual({ outline: 'none', shadow: 'none' });
await page.locator('.hero-journey__action--primary').focus();
await page.keyboard.press('Shift');
expect(await page.locator('.hero-journey__action--primary').evaluate(el => getComputedStyle(el).outlineStyle)).not.toBe('none');
});
test('name decoration pauses offscreen and leaves one readable heading', async ({ page, distUrl }) => {
await page.goto(distUrl);
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Luca Pîslaru');
await page.evaluate(() => document.fonts.ready);
const title = await page.locator('.hero-name').evaluate(el => ({
height: el.getBoundingClientRect().height,
lineHeight: parseFloat(getComputedStyle(el).lineHeight),
}));
expect(title.height).toBeLessThanOrEqual(title.lineHeight * 2.1);
const particles = page.locator('.hero-name-particles');
await expect(particles).toHaveAttribute('aria-hidden', 'true');
await expect(particles).toHaveAttribute('data-active', 'true');
const canvas = particles.locator('canvas');
await expect(canvas).toHaveCount(1);
const nameBox = await particles.boundingBox();
const effectBox = await canvas.boundingBox();
expect(effectBox!.x).toBeCloseTo(nameBox!.x - 12, 0);
expect(effectBox!.width).toBeCloseTo(nameBox!.width + 24, 0);
const roleBox = await page.locator('.hero-journey__role').boundingBox();
// The complete drawing area (including every wave crest) clears the role.
expect(roleBox!.y - (effectBox!.y + effectBox!.height)).toBeGreaterThanOrEqual(4);
await expect.poll(() => canvas.evaluate(el => {
const context = (el as HTMLCanvasElement).getContext('2d')!;
const { width, height } = el as HTMLCanvasElement;
// Sparks occupy the name area, while the waves sit below it.
return [
context.getImageData(0, 0, width, Math.floor(height * 0.75)).data.some(value => value > 0),
context.getImageData(0, Math.floor(height * 0.92), width, Math.max(1, Math.floor(height * 0.08))).data.some(value => value > 0),
];
})).toEqual([true, true]);
expect(await particles.evaluate(el => getComputedStyle(el, '::after').animationName)).toBe('name-sheen');
expect(await particles.evaluate(el => getComputedStyle(el, '::after').animationPlayState)).toBe('running');
const firstFrame = await canvas.evaluate(el => (el as HTMLCanvasElement).toDataURL());
await expect.poll(() => canvas.evaluate(el => (el as HTMLCanvasElement).toDataURL())).not.toBe(firstFrame);
await page.locator('#contact').scrollIntoViewIfNeeded();
await expect(particles).toHaveAttribute('data-active', 'false');
expect(await particles.evaluate(el => getComputedStyle(el, '::after').animationPlayState)).toBe('paused');
const stoppedFrame = await canvas.evaluate(el => (el as HTMLCanvasElement).toDataURL());
await page.waitForTimeout(150);
expect(await canvas.evaluate(el => (el as HTMLCanvasElement).toDataURL())).toBe(stoppedFrame);
});
test('wave animation clears the role text across desktop, short screens, and narrow phones', async ({ page, distUrl }, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium');
await page.goto(distUrl);
for (const [width, height] of [[1920, 1080], [1024, 768], [820, 600], [768, 520], [393, 851], [320, 640]]) {
await page.setViewportSize({ width: width!, height: height! });
await page.evaluate(() => document.fonts.ready);
await expect.poll(async () => {
const effect = await page.locator('.hero-name-particles canvas').boundingBox();
const role = await page.locator('.hero-journey__role').boundingBox();
return role!.y - (effect!.y + effect!.height);
}, { message: `Wave clearance at ${width} × ${height}` }).toBeGreaterThanOrEqual(4);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
}
});
test('reduced motion and no-JavaScript retain the name and native skip link', async ({ browser, page, distUrl }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto(distUrl);
await expect(page.locator('.hero-name-particles')).toHaveAttribute('data-active', 'false');
const context = await browser.newContext({ javaScriptEnabled: false });
const native = await context.newPage();
await native.goto(distUrl);
await expect(native.getByRole('heading', { level: 1 })).toHaveText('Luca Pîslaru');
await native.keyboard.press('Tab');
await native.keyboard.press('Enter');
await expect(native.locator('main')).toBeFocused();
await expect(native.locator('.content-focus-cue')).toBeVisible();
await context.close();
});