Skip to content

Commit 1ce5233

Browse files
MohabMohieclaude
andauthored
Fix homepage P2 remainder from #898: dependency snippet, evidence-loop close, grid breakpoints/subgrid, contrast/motion/perf cleanup (#901)
Second P2 fast-follow to #899/#900. Covers the 9 P2 findings from #898's homepage audit those PRs explicitly deferred (7, 8, 11, 14, 16, 17, 19, 20, 21). Branched fresh from origin/master (post-#900) in an isolated worktree. Findings addressed: - 7: evidence loop read as a straight line, not a loop -- added a closing "Improve loops back to Execute" affordance with a rotate-left icon under the grid instead of silently terminating at the 5th card. - 8: no copyable dependency coordinate anywhere on the page -- added a <dependency> XML snippet (Docusaurus CodeBlock, built-in copy button) in the guide-paths intro, sourcing the version from the existing releases.json single source of truth (not hardcoded). - 11: .loopStep::after ('>') and .handledPanel li::before ('+') used bare CSS content strings, announced by screen readers -- switched both to the alt-text form (content: '>' / ''). - 14: composited-contrast coverage stopped short of the hero/final secondary buttons and .heroTrustLinks. Added a border-contrast measurement (WCAG 1.4.11, composites the border's own rgba over the sampled backdrop rather than sub-pixel-sampling the 1px stroke) plus a text-contrast case for the trust links. The border case caught a real borderline failure -- 0.42 alpha measured 2.94:1; raised to 0.48 (~3.4:1) per the issue's own guidance to adjust alpha, not text color. - 16: will-change: opacity, transform sat permanently on ~20 reveal elements for the page's whole lifetime -- removed; transitions still promote their own layer for the transition's duration. - 17: useHoverGlow forced a synchronous layout read on every pointermove across 28 elements -- now caches the rect once on pointerenter and reuses it in the pointermove handler. - 19: the 5-column path/loop/badge grids collapsed straight from 5 to 1 column at 980px, leaving ~150px-wide cards in the 981-1180px band -- added an intermediate 2-column step at 1180px. - 20: .audienceLane/.loopStep pinned content rows to fixed heights (2.1rem/4.4rem and 1.8rem/3rem), which a 3rd description line or a multi-word loop title would overflow -- replaced with CSS subgrid so cross-card row alignment now follows content instead of a guessed cap. - 21: height: 3.45rem on CTA buttons whose labels are explicitly allowed to wrap (white-space: normal) -- changed to min-height. Not in scope for this PR (per dispatch): P3 findings 23, 25-29, and finding 18 (scroll-reveal roll-back), which the issue itself frames as an owner decision, not a bug -- left untouched. Test impact / evidence (TDD): every new/changed assertion was written and watched RED against the pre-fix source first. - tests/homepage-performance.test.js: static assertions for all 9 findings (will-change absence, alt-text content strings, min-height, pointerenter caching, the 1180px breakpoint's presence, subgrid rows, the loop-return copy/testid, the dependency-snippet import/testid, and the still-single "Maven Central" occurrence). - tests/e2e/composited-contrast.spec.js: new border-contrast helper (compositeOver + measureBorderContrast) and two new tests (secondary CTA border, hero trust links text). - tests/e2e/homepage.spec.js: extended the existing mobile CTA-overflow check to also catch vertical overflow, added a dedicated 800px test for the previously-unguarded 761-900px wrap band, and a parametrized test at 1000/1100/1180px confirming the path/loop grids sit at 2-4 columns with no horizontal page overflow. - Recaptured test-screenshots/01-homepage-before-chat.png against the live built site (reduced-motion emulated so below-the-fold sections aren't mid-reveal) to keep it current. Test plan: - [x] node tests/homepage-performance.test.js -- watched RED per finding, then GREEN - [x] npx tsc --noEmit -p tsconfig.json -- clean - [x] npm run build -- clean production build - [x] npx playwright test tests/e2e/homepage.spec.js tests/e2e/composited-contrast.spec.js tests/e2e/hash-scroll-sync.spec.js -- 20/20 pass, headless, against docusaurus serve on the fresh build (includes the border-contrast test that caught the real 2.94:1 failure before the alpha fix) - [x] npm run test:docs (docs-loader, docs-quality, design-contrast 9/9 WCAG pairs, duplicate-check) -- pass, no regressions - [x] npm run test:release-template, npm run test:history -- pass - [x] Full-page + close-up screenshots captured against the live built site to visually confirm the dependency snippet, evidence-loop return note, 2-column grid at 1100px, and the more visible secondary-button border Ref #898 🤖 Generated with [Claude Code](https://claude.com/claude-code) Claude-Session: https://claude.ai/code/session_019wEoJduy4Q4jfHWejcizCe Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 976668a commit 1ce5233

6 files changed

Lines changed: 307 additions & 11 deletions

File tree

src/pages/index.module.css

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@
164164
}
165165

166166
.actions :global(.button) {
167-
height: 3.45rem;
167+
min-height: 3.45rem;
168168
min-width: 0;
169169
max-width: 100%;
170170
padding: 0.82rem 1.15rem !important;
@@ -214,7 +214,11 @@
214214

215215
.hero .actions :global(.button--secondary),
216216
.finalCta .actions :global(.button--secondary) {
217-
border-color: rgba(var(--site-color-muted-rgb), 0.42);
217+
/* 0.42 measured 2.94:1 against the hero backdrop -- under WCAG 1.4.11's
218+
3:1 floor for a UI component boundary (#898 finding 14). 0.48 clears it
219+
with headroom (~3.4:1); raising alpha per the issue's own guidance
220+
rather than the text color, which already passes on its own. */
221+
border-color: rgba(var(--site-color-muted-rgb), 0.48);
218222
background: rgba(var(--site-color-muted-rgb), 0.08);
219223
color: var(--site-color-on-dark);
220224
}
@@ -504,7 +508,7 @@
504508
color: var(--site-color-primary);
505509
font-weight: var(--site-font-weight-bold);
506510
transform: none;
507-
content: '+';
511+
content: '+' / '';
508512
}
509513

510514
.audienceSection {
@@ -517,12 +521,14 @@
517521
.audienceGrid {
518522
display: grid;
519523
grid-template-columns: repeat(2, minmax(0, 1fr));
524+
grid-template-rows: auto auto 1fr;
520525
gap: 1rem;
521526
}
522527

523528
.audienceLane {
524529
display: grid;
525-
grid-template-rows: 2.1rem 4.4rem 1fr;
530+
grid-row: span 3;
531+
grid-template-rows: subgrid;
526532
row-gap: 0.8rem;
527533
min-width: 0;
528534
padding: 1.35rem;
@@ -605,6 +611,22 @@
605611
color: var(--ifm-font-color-base);
606612
}
607613

614+
.dependencySnippet {
615+
max-width: 480px;
616+
margin: 0 auto 2rem;
617+
text-align: left;
618+
}
619+
620+
.dependencyLabel {
621+
margin: 0 0 0.5rem;
622+
color: var(--ifm-color-emphasis-800);
623+
font-family: var(--ifm-font-family-monospace);
624+
font-size: var(--site-font-small);
625+
font-weight: var(--site-font-weight-medium);
626+
letter-spacing: 0.1em;
627+
text-transform: uppercase;
628+
}
629+
608630
.pathGrid {
609631
display: grid;
610632
grid-template-columns: repeat(5, minmax(0, 1fr));
@@ -772,6 +794,7 @@
772794
.evidenceLoop {
773795
display: grid;
774796
grid-template-columns: repeat(5, minmax(0, 1fr));
797+
grid-template-rows: auto auto 1fr;
775798
gap: 0;
776799
border: 1px solid var(--ifm-color-emphasis-200);
777800
border-radius: 8px;
@@ -783,7 +806,8 @@
783806
.loopStep {
784807
position: relative;
785808
display: grid;
786-
grid-template-rows: 1.8rem 3rem 1fr;
809+
grid-row: span 3;
810+
grid-template-rows: subgrid;
787811
row-gap: 0.9rem;
788812
min-height: 210px;
789813
padding: 1.25rem;
@@ -807,7 +831,7 @@
807831
top: 1rem;
808832
right: 1rem;
809833
color: var(--site-color-primary);
810-
content: '>';
834+
content: '>' / '';
811835
font-weight: var(--site-font-weight-bold);
812836
}
813837

@@ -839,6 +863,23 @@
839863
color: var(--ifm-color-emphasis-800);
840864
}
841865

866+
.loopReturn {
867+
display: flex;
868+
align-items: center;
869+
justify-content: center;
870+
gap: 0.55rem;
871+
margin: 1rem 0 0;
872+
color: var(--ifm-color-emphasis-800);
873+
font-size: var(--site-font-label);
874+
font-weight: var(--site-font-weight-medium);
875+
}
876+
877+
.loopReturn svg {
878+
width: 0.85rem;
879+
height: 0.85rem;
880+
color: var(--site-color-primary);
881+
}
882+
842883
.featureLinks {
843884
margin-top: 1.5rem;
844885
}
@@ -977,7 +1018,6 @@
9771018
transition:
9781019
opacity 540ms ease var(--reveal-delay),
9791020
transform 540ms cubic-bezier(0.2, 0.8, 0.2, 1) var(--reveal-delay);
980-
will-change: opacity, transform;
9811021
}
9821022

9831023
:global(html[data-reveal-ready='true']) .revealVisible {
@@ -1023,6 +1063,29 @@
10231063
.heroGrid {
10241064
max-width: 920px;
10251065
}
1066+
1067+
.pathGrid,
1068+
.evidenceLoop,
1069+
.footerBadges {
1070+
grid-template-columns: repeat(2, minmax(0, 1fr));
1071+
}
1072+
1073+
.pathGrid,
1074+
.evidenceLoop {
1075+
gap: 1rem;
1076+
border: 0;
1077+
overflow: visible;
1078+
background: transparent;
1079+
box-shadow: none;
1080+
}
1081+
1082+
.pathCard,
1083+
.loopStep,
1084+
.loopStep:last-child {
1085+
border: 1px solid var(--ifm-color-emphasis-200);
1086+
border-radius: 8px;
1087+
box-shadow: 0 14px 34px rgba(var(--site-color-deep-alt-rgb), 0.06);
1088+
}
10261089
}
10271090

10281091
@media (max-width: 980px) {

src/pages/index.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import Layout from '@theme/Layout';
55
import Heading from '@theme/Heading';
66
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
77
import {faSlack} from '@fortawesome/free-brands-svg-icons';
8-
import {faBookOpen, faStar, faTerminal} from '@fortawesome/free-solid-svg-icons';
8+
import {faBookOpen, faRotateLeft, faStar, faTerminal} from '@fortawesome/free-solid-svg-icons';
9+
import CodeBlock from '@theme/CodeBlock';
910
import ParticleBackground from '@site/src/components/ParticleBackground';
11+
import releases from '@site/src/data/releases.json';
1012
import snippets from '@site/src/data/snippets.json';
1113
import styles from './index.module.css';
1214

@@ -222,20 +224,28 @@ function useHoverGlow(): void {
222224
}
223225

224226
const glowTargets = Array.from(document.querySelectorAll<HTMLElement>('[data-hover-glow]'));
227+
const rects = new WeakMap<HTMLElement, DOMRect>();
228+
229+
const cacheRect = (event: PointerEvent) => {
230+
const target = event.currentTarget as HTMLElement;
231+
rects.set(target, target.getBoundingClientRect());
232+
};
225233

226234
const updatePointer = (event: PointerEvent) => {
227235
const target = event.currentTarget as HTMLElement;
228-
const rect = target.getBoundingClientRect();
236+
const rect = rects.get(target) ?? target.getBoundingClientRect();
229237
target.style.setProperty('--hover-x', `${event.clientX - rect.left}px`);
230238
target.style.setProperty('--hover-y', `${event.clientY - rect.top}px`);
231239
};
232240

233241
glowTargets.forEach((target) => {
242+
target.addEventListener('pointerenter', cacheRect, { passive: true });
234243
target.addEventListener('pointermove', updatePointer, { passive: true });
235244
});
236245

237246
return () => {
238247
glowTargets.forEach((target) => {
248+
target.removeEventListener('pointerenter', cacheRect);
239249
target.removeEventListener('pointermove', updatePointer);
240250
});
241251
};
@@ -364,6 +374,14 @@ function GuidePathSection(): JSX.Element {
364374
<Heading as="h2" id="guide-paths-heading">Get started in minutes.</Heading>
365375
<p>Install, configure, write a readable test, run it headlessly, and review the evidence before starring the repository.</p>
366376
</div>
377+
<div className={styles.dependencySnippet} data-testid="landing-dependency-snippet">
378+
<p className={styles.dependencyLabel}>Add the dependency</p>
379+
<CodeBlock language="xml">{`<dependency>
380+
<groupId>io.github.shafthq</groupId>
381+
<artifactId>shaft-engine</artifactId>
382+
<version>${releases.engineVersion}</version>
383+
</dependency>`}</CodeBlock>
384+
</div>
367385
<div className={styles.pathGrid} role="group" aria-labelledby="guide-paths-heading">
368386
{guidePaths.map((path) => (
369387
<Link className={`${styles.pathCard} ${styles.reveal}`} to={path.to} key={path.title} data-reveal data-hover-glow>
@@ -493,6 +511,10 @@ function AgentSection(): JSX.Element {
493511
</div>
494512
))}
495513
</div>
514+
<p className={styles.loopReturn} data-testid="landing-evidence-loop-return">
515+
<FontAwesomeIcon icon={faRotateLeft} aria-hidden="true" />
516+
Improve loops back to Execute — every run repeats the cycle.
517+
</p>
496518
<div className={styles.featureLinks} data-testid="landing-agent-links">
497519
<Link data-testid="landing-agent-mcp-link" to="/docs/agentic/mcp">MCP setup and commands</Link>
498520
<Link to="/docs/agentic/doctor">Diagnose with Doctor</Link>
-136 KB
Loading

tests/e2e/composited-contrast.spec.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,81 @@ function interiorSampler(inset) {
194194
};
195195
}
196196

197+
// Composites `fg` (with alpha) over `bg` using the standard "over" formula.
198+
function compositeOver(fg, bg) {
199+
return {
200+
r: fg.r * fg.a + bg.r * (1 - fg.a),
201+
g: fg.g * fg.a + bg.g * (1 - fg.a),
202+
b: fg.b * fg.a + bg.b * (1 - fg.a),
203+
};
204+
}
205+
206+
// Regression guard for #898 finding 14. Measures the hero/final secondary
207+
// button's border -- a UI component boundary under WCAG 1.4.11 (>=3:1
208+
// against the adjacent surrounding color) -- without needing sub-pixel
209+
// screenshot sampling of the 1px stroke itself (the existing 6x6-block
210+
// averaging used elsewhere in this file would blur a 1px line into the
211+
// fill/background either side of it). Instead: read the border's own rgba
212+
// color (a flat, non-gradient value) via getComputedStyle, sample the real
213+
// composited backdrop just outside the button's box, then composite the
214+
// border color over that background analytically and measure contrast
215+
// against the same background -- i.e. the boundary color vs. its adjacent
216+
// color, which is exactly what 1.4.11 asks for.
217+
async function measureBorderContrast(page, selector) {
218+
const locator = page.locator(selector).first();
219+
await locator.scrollIntoViewIfNeeded();
220+
221+
const info = await locator.evaluate((el) => {
222+
const rect = el.getBoundingClientRect();
223+
const style = getComputedStyle(el);
224+
const borderMatch = style.borderTopColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
225+
return {
226+
rect: {x: rect.x, y: rect.y, width: rect.width, height: rect.height},
227+
border: {
228+
r: +borderMatch[1],
229+
g: +borderMatch[2],
230+
b: +borderMatch[3],
231+
a: borderMatch[4] !== undefined ? +borderMatch[4] : 1,
232+
},
233+
};
234+
});
235+
236+
// Sample the hero backdrop just above the button: clear of the button's
237+
// own fill/border/box-shadow and of any neighbouring button.
238+
const bgPoint = {x: info.rect.x + info.rect.width / 2, y: info.rect.y - 10};
239+
240+
const screenshot = (await page.screenshot()).toString('base64');
241+
const bg = await page.evaluate(async ({screenshot, point}) => {
242+
const img = new Image();
243+
img.src = `data:image/png;base64,${screenshot}`;
244+
await new Promise((resolve, reject) => {
245+
img.onload = resolve;
246+
img.onerror = reject;
247+
});
248+
const canvas = document.createElement('canvas');
249+
canvas.width = img.naturalWidth;
250+
canvas.height = img.naturalHeight;
251+
const ctx = canvas.getContext('2d');
252+
ctx.drawImage(img, 0, 0);
253+
const blockSize = 6;
254+
const d = ctx.getImageData(
255+
Math.round(point.x - blockSize / 2),
256+
Math.round(point.y - blockSize / 2),
257+
blockSize,
258+
blockSize,
259+
).data;
260+
let r = 0, g = 0, b = 0;
261+
const n = d.length / 4;
262+
for (let i = 0; i < d.length; i += 4) {
263+
r += d[i]; g += d[i + 1]; b += d[i + 2];
264+
}
265+
return {r: r / n, g: g / n, b: b / n};
266+
}, {screenshot, point: bgPoint});
267+
268+
const compositedBorder = compositeOver(info.border, bg);
269+
return {ratio: contrastRatio(compositedBorder, bg), border: info.border, bg};
270+
}
271+
197272
test.beforeEach(async ({page}) => {
198273
await page.setViewportSize({width: 1280, height: 900});
199274
await page.goto('/');
@@ -253,3 +328,16 @@ test('hero primary CTA clears WCAG AA contrast against its own button fill', asy
253328
interiorSampler(10),
254329
);
255330
});
331+
332+
test('hero secondary CTA border clears WCAG 1.4.11 non-text contrast (#898 finding 14)', async ({page}) => {
333+
const {ratio, border, bg} = await measureBorderContrast(page, '[data-testid="landing-hero-quickstart-cta"]');
334+
expect(
335+
ratio,
336+
`secondary button border rgba(${border.r},${border.g},${border.b},${border.a}) composited over measured bg ` +
337+
`rgb(${bg.r.toFixed(1)},${bg.g.toFixed(1)},${bg.b.toFixed(1)}) = ${ratio.toFixed(2)}:1, needs >= 3:1`,
338+
).toBeGreaterThanOrEqual(3);
339+
});
340+
341+
test('hero trust links clear WCAG AA text contrast against their real composited background (#898 finding 14)', async ({page}) => {
342+
await assertClearsContrast(page, '.heroTrustLinks a', '[class*="heroTrustLinks"] a', adjacentSampler);
343+
});

tests/e2e/homepage.spec.js

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,12 @@ test('landing page keeps mobile motion and CTAs inside the viewport', async ({pa
222222
rect.right > parentRect.right + 1 ||
223223
rect.left < -1 ||
224224
rect.right > viewportWidth + 1,
225+
// #898 finding 21: height: 3.45rem -> min-height must mean a
226+
// wrapped two-line label no longer overflows its own button box.
227+
overflowsVertically: link.scrollHeight > link.clientHeight + 1,
225228
};
226229
})
227-
.filter((button) => button.overflows || button.width > button.parentWidth + 1);
230+
.filter((button) => button.overflows || button.width > button.parentWidth + 1 || button.overflowsVertically);
228231
});
229232
expect(overflowingButtons).toEqual([]);
230233

@@ -251,3 +254,55 @@ test('landing page keeps mobile motion and CTAs inside the viewport', async ({pa
251254
}));
252255
}).toEqual({opacity: '0', state: 'rolled-back'});
253256
});
257+
258+
// Regression guard for #898 finding 21. The 761-900px band keeps the
259+
// 3-column-turned-2-column CTA grid active (single-column starts at 760px)
260+
// while each column is narrow enough to wrap a label like "Read quick
261+
// start" -- previously unguarded, since the mobile check above only runs at
262+
// 375px and only checked horizontal overflow.
263+
test('landing page CTA buttons do not overflow when their label wraps at 800px (#898 finding 21)', async ({page}) => {
264+
await page.setViewportSize({width: 800, height: 900});
265+
await page.goto('/');
266+
267+
const checkOverflow = async (testId) => page.evaluate((id) => {
268+
return Array.from(document.querySelectorAll(`[data-testid="${id}"] .button`))
269+
.map((button) => ({
270+
text: button.textContent.trim().replace(/\s+/g, ' '),
271+
overflowsVertically: button.scrollHeight > button.clientHeight + 1,
272+
}))
273+
.filter((button) => button.overflowsVertically);
274+
}, testId);
275+
276+
expect(await checkOverflow('landing-hero-actions')).toEqual([]);
277+
await page.getByTestId('landing-final').scrollIntoViewIfNeeded();
278+
await expect.poll(() => page.getByTestId('landing-final').evaluate((el) => getComputedStyle(el).opacity)).toBe('1');
279+
expect(await checkOverflow('landing-final')).toEqual([]);
280+
});
281+
282+
// Regression guard for #898 finding 19. The 5-column path/loop/badge grids
283+
// previously collapsed straight from 5 columns to 1 at 980px, with no
284+
// intermediate step -- leaving ~150px-wide cards in the 981-1180px band.
285+
// A 1180px breakpoint now gives them a 2-column step first.
286+
for (const width of [1000, 1100, 1180]) {
287+
test(`landing page path/loop grids use an intermediate column step at ${width}px (#898 finding 19)`, async ({page}) => {
288+
await page.setViewportSize({width, height: 900});
289+
await page.goto('/');
290+
291+
const countColumns = async (selector) => page.locator(selector).evaluate((el) => {
292+
return getComputedStyle(el).gridTemplateColumns.trim().split(/\s+/).length;
293+
});
294+
295+
const pathColumns = await countColumns('[data-testid="landing-pathfinder"] [aria-labelledby="guide-paths-heading"]');
296+
const loopColumns = await countColumns('[data-testid="landing-evidence-loop"]');
297+
298+
expect(pathColumns).toBeGreaterThanOrEqual(2);
299+
expect(pathColumns).toBeLessThan(5);
300+
expect(loopColumns).toBeGreaterThanOrEqual(2);
301+
expect(loopColumns).toBeLessThan(5);
302+
303+
const overflowsHorizontally = await page.evaluate(() => {
304+
return document.documentElement.scrollWidth > document.documentElement.clientWidth + 1;
305+
});
306+
expect(overflowsHorizontally).toBe(false);
307+
});
308+
}

0 commit comments

Comments
 (0)