From 242d79599a4fecaea055559d28b6c2a8e2b7c12e Mon Sep 17 00:00:00 2001 From: LucaP Date: Fri, 4 Sep 2026 21:02:44 +0300 Subject: [PATCH] fix: preserve journey geometry and mailto placement --- scripts/verify-artifacts.mjs | 9 +++++---- src/components/ToolkitGrid.vue | 2 +- src/styles/components.css | 10 ++++++++++ src/styles/layout.css | 9 +++------ tests/browser/semantic.spec.ts | 14 ++++++++++++++ tests/integration/verify-artifacts.test.ts | 10 ++++++++++ tests/unit/components.test.ts | 1 + 7 files changed, 44 insertions(+), 11 deletions(-) diff --git a/scripts/verify-artifacts.mjs b/scripts/verify-artifacts.mjs index 103663f..89aff1d 100644 --- a/scripts/verify-artifacts.mjs +++ b/scripts/verify-artifacts.mjs @@ -242,12 +242,13 @@ const assertSemanticHtml = (html, inputs, contract) => { for (const copy of requiredVisibleCopy) { if (!semanticText.includes(copy)) throw new Error(`Missing visible semantic evidence: ${copy}`); } - const mailLinks = [...appRoot.querySelectorAll('a[href^="mailto:"]')] - .filter((node) => isVisibleSemanticNode(node, appRoot)); - if (mailLinks.length !== 2 || mailLinks.some((link) => + const mailLinks = [...document.querySelectorAll('a[href^="mailto:"]')]; + const startMailLinks = [...document.querySelectorAll('#start a[href^="mailto:"]')]; + const contactMailLinks = [...document.querySelectorAll('#contact a[href^="mailto:"]')]; + if (mailLinks.length !== 2 || startMailLinks.length !== 1 || contactMailLinks.length !== 1 || mailLinks.some((link) => link.getAttribute('href') !== `mailto:${inputs.content.contact.email}` || link.textContent !== 'Email Luca' )) { - throw new Error('Visible semantic mailto links must be exactly the hero and contact Email Luca actions.'); + throw new Error('Mailto links must be exactly one Email Luca action beneath each hero and contact section.'); } const moduleScript = document.querySelector(`script[type="module"][src="/${contract.entryModule}"]`); diff --git a/src/components/ToolkitGrid.vue b/src/components/ToolkitGrid.vue index 9f130f2..db0e6e5 100644 --- a/src/components/ToolkitGrid.vue +++ b/src/components/ToolkitGrid.vue @@ -13,7 +13,7 @@ defineProps<{ groups: readonly ToolkitGroup[] }>(); diff --git a/src/styles/components.css b/src/styles/components.css index 03d92bc..067ee9f 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -21,12 +21,14 @@ max-inline-size: 40ch; color: var(--text-muted); font-size: var(--step-0); + line-height: 1.35; } .hero-journey__availability { max-inline-size: 52ch; color: var(--text-muted); font-size: var(--step--1); + line-height: 1.35; } .hero-journey__actions { @@ -36,6 +38,10 @@ align-items: center; } +.hero-journey__actions .hero-journey__action { + margin-block-start: 0; +} + .hero-journey__action, .project-link, .contact-link, @@ -584,6 +590,10 @@ font-size: var(--step--1); } +.toolkit-item__label--text-only { + grid-column: 1 / -1; +} + .brand-icon { inline-size: 1.05rem; block-size: 1.05rem; diff --git a/src/styles/layout.css b/src/styles/layout.css index fb81234..ad6e9f3 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -79,17 +79,14 @@ display: grid; min-block-size: calc(100svh - var(--site-navigation-block-size)); align-items: start; - padding: clamp(2rem, 8vh, 6rem) 4vw 16vh; + padding: clamp(3.5rem, 12vh, 8rem) 4vw 16vh; scroll-margin-block-start: var(--site-navigation-block-size); } .hero-journey__content { - position: relative; - z-index: 1; display: grid; - min-inline-size: 0; - inline-size: min(38rem, 50vw); - gap: 1rem; + inline-size: min(34rem, 39vw); + gap: 0.75rem; padding: clamp(1.4rem, 2.2vw, 2.1rem); border: 1px solid var(--line); border-inline-start: 3px solid var(--cyan); diff --git a/tests/browser/semantic.spec.ts b/tests/browser/semantic.spec.ts index d5e30ec..c6a1e1d 100644 --- a/tests/browser/semantic.spec.ts +++ b/tests/browser/semantic.spec.ts @@ -102,6 +102,20 @@ test('exposes the complete semantic portfolio from the generated document', asyn await expect(page.getByRole('link', { name: 'Explore work' })).toHaveAttribute('href', '#selected-builds'); await expect(page.getByRole('link', { name: 'Email Luca' })).toHaveCount(2); await expect(page.locator('[data-featured-operation]')).toContainText('without data loss'); + expect(await page.locator('[data-testid="toolkit-technology"]', { hasText: 'DNS' }).locator('span').evaluate( + (label) => getComputedStyle(label).gridColumn, + )).toBe('1 / -1'); + expect(await page.locator('.hero-journey').evaluate((hero) => { + const content = hero.querySelector('.hero-journey__content')!; + const heroStyle = getComputedStyle(hero); + const contentStyle = getComputedStyle(content); + return { + preservesPadding: Math.abs(Number.parseFloat(heroStyle.paddingTop) - innerHeight * 0.12) < 0.01, + preservesWidth: Math.abs(Number.parseFloat(contentStyle.width) - Math.min(544, innerWidth * 0.39)) < 0.1, + position: contentStyle.position, + zIndex: contentStyle.zIndex, + }; + })).toEqual({ preservesPadding: true, preservesWidth: true, position: 'static', zIndex: 'auto' }); expect( await page.locator('h1, h2, h3').evaluateAll((headings) => { const levels = headings.map((heading) => diff --git a/tests/integration/verify-artifacts.test.ts b/tests/integration/verify-artifacts.test.ts index cf149f9..7d10bdc 100644 --- a/tests/integration/verify-artifacts.test.ts +++ b/tests/integration/verify-artifacts.test.ts @@ -131,6 +131,16 @@ describe('artifact verification', () => { name: 'a mailto link with an address that differs from public content', mutate: (html: string) => html.replace('href="mailto:luca.pislaru@gmail.com">Email Luca', 'href="mailto:wrong@example.test">Email Luca'), }, + { + name: 'a hidden mailto link outside the app root', + mutate: (html: string) => html.replace('', ''), + }, + { + name: 'two mailto links placed beneath contact instead of one per section', + mutate: (html: string) => html + .replace('href="mailto:luca.pislaru@gmail.com">Email Luca', 'href="#selected-builds">Email Luca') + .replace('', 'Email Luca'), + }, ])('rejects $name', async ({ mutate }) => { const { directory, html, inputs, contract } = await writeValidArtifacts(); await writeFile(join(directory, 'index.html'), mutate(html)); diff --git a/tests/unit/components.test.ts b/tests/unit/components.test.ts index be408cf..9de00a8 100644 --- a/tests/unit/components.test.ts +++ b/tests/unit/components.test.ts @@ -171,6 +171,7 @@ describe("semantic section components", () => { expect(tile.text()).toBe("DNS"); expect(tile.find("svg").exists()).toBe(false); + expect(tile.get("span").classes()).toContain("toolkit-item__label--text-only"); }); it("renders a visible mailto contact and safe verified social links", () => {