Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
141 changes: 76 additions & 65 deletions site/build-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ const SOCIAL_THUMBNAIL_PATH = `/assets/${SOCIAL_THUMBNAIL_FILENAME}`;
const SOCIAL_THUMBNAIL_ALT = "JudgmentKit. Before the UI.";
const HOMEPAGE_HERO_ART_FILENAME = "judgment-lens-hero.webp";
const HOMEPAGE_HERO_ART_PATH = `/assets/${HOMEPAGE_HERO_ART_FILENAME}`;
const MARKETING_PATTERN_HERO_FILENAME =
"marketing-surface-weeknight-meal-plan.webp";
const MARKETING_PATTERN_HERO_PATH =
`/assets/patterns/${MARKETING_PATTERN_HERO_FILENAME}`;
const VISUAL_COMPOSITION_RECORDING_FILENAME =
"judgmentkit-select-field-agent-demo.mp4";
const VISUAL_COMPOSITION_DARK_RECORDING_FILENAME =
Expand Down Expand Up @@ -3432,17 +3436,26 @@ pre {
}
.jk-surface-marketing-hero {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(260px, 0.9fr);
gap: 28px;
align-items: center;
min-height: clamp(430px, 50vw, 570px);
padding: clamp(24px, 5vw, 56px);
background: var(--jk-color-canvas, var(--bg));
background-color: var(--hero-art-bg);
background-image:
linear-gradient(90deg, color-mix(in srgb, var(--hero-art-bg) 96%, transparent) 0%, color-mix(in srgb, var(--hero-art-bg) 86%, transparent) 42%, color-mix(in srgb, var(--hero-art-bg) 42%, transparent) 70%, color-mix(in srgb, var(--hero-art-bg) 22%, transparent) 100%),
url("${MARKETING_PATTERN_HERO_PATH}");
background-position: 50% 48%;
background-size: cover;
color: var(--fixed-light-ink);
}
.jk-surface-marketing-offer {
display: grid;
gap: 14px;
align-content: center;
min-width: 0;
max-width: 650px;
}
.jk-surface-marketing-offer .jk-surface-kicker {
color: color-mix(in srgb, var(--fixed-light-ink) 78%, transparent);
}
.jk-surface-marketing-offer h4 {
max-width: 15ch;
Expand All @@ -3452,65 +3465,47 @@ pre {
}
.jk-surface-marketing-offer p:not(.jk-surface-kicker) {
max-width: 52ch;
color: var(--jk-color-muted, var(--muted));
color: color-mix(in srgb, var(--fixed-light-ink) 82%, transparent);
}
.jk-surface-marketing-actions {
display: flex;
flex-wrap: wrap;
gap: 10px 18px;
align-items: center;
}
.jk-surface-marketing-offer .jk-action-button {
width: max-content;
}
.jk-surface-marketing-offer small {
color: var(--jk-color-muted, var(--muted));
}
.jk-surface-marketing-proof {
display: grid;
gap: 14px;
min-width: 0;
padding: 18px;
border: 1px solid var(--jk-color-border, var(--line));
border-radius: var(--jk-radius-panel, 8px);
background: var(--jk-color-surface, var(--panel));
box-shadow: 0 16px 36px color-mix(in srgb, var(--jk-color-text, var(--ink)) 10%, transparent);
}
.jk-surface-plan-card {
display: grid;
gap: 2px;
.jk-surface-marketing-offer .jk-action-button:disabled {
border-color: var(--fixed-light-ink);
background: var(--fixed-light-ink);
color: var(--hero-art-bg);
opacity: 1;
}
.jk-surface-plan-card > div {
display: grid;
grid-template-columns: 40px minmax(0, 1fr);
gap: 2px 10px;
padding: 10px 0;
border-bottom: 1px solid var(--jk-color-border, var(--line));
.jk-surface-marketing-offer .jk-surface-text-link {
color: var(--fixed-light-ink);
}
.jk-surface-plan-card > div:last-child {
border-bottom: 0;
}
.jk-surface-plan-card span {
grid-row: 1 / 3;
color: var(--jk-color-focus, var(--accent));
font-size: 11px;
font-weight: 900;
.jk-surface-marketing-offer small {
color: color-mix(in srgb, var(--fixed-light-ink) 72%, transparent);
}
.jk-surface-plan-card small {
color: var(--jk-color-muted, var(--muted));
.jk-surface-marketing-proofline {
display: flex;
flex-wrap: wrap;
gap: 12px 26px;
max-width: 560px;
padding-top: 14px;
border-top: 1px solid color-mix(in srgb, var(--fixed-light-ink) 28%, transparent);
}
.jk-surface-proof-stats {
.jk-surface-marketing-proofline div {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
border-top: 1px solid var(--jk-color-border, var(--line));
border-bottom: 1px solid var(--jk-color-border, var(--line));
}
.jk-surface-proof-stats div {
padding: 10px;
}
.jk-surface-proof-stats div + div {
border-left: 1px solid var(--jk-color-border, var(--line));
gap: 1px;
}
.jk-surface-proof-stats dt {
font-size: 18px;
.jk-surface-marketing-proofline dt {
font-size: 17px;
font-weight: 900;
}
.jk-surface-proof-stats dd {
color: var(--jk-color-muted, var(--muted));
.jk-surface-marketing-proofline dd {
color: color-mix(in srgb, var(--fixed-light-ink) 74%, transparent);
font-size: 11px;
}
.jk-surface-marketing-next {
Expand Down Expand Up @@ -5627,15 +5622,21 @@ pre {
grid-template-columns: minmax(0, 1fr);
}
.jk-surface-marketing-hero {
gap: 20px;
padding: 22px 16px;
padding: clamp(210px, 38vw, 310px) 16px 24px;
background-image:
linear-gradient(180deg, transparent 0%, color-mix(in srgb, var(--hero-art-bg) 72%, transparent) 26%, var(--hero-art-bg) 34%, var(--hero-art-bg) 100%),
url("${MARKETING_PATTERN_HERO_PATH}");
background-position: center, center top;
background-repeat: no-repeat;
background-size: 100% 100%, 100% auto;
}
.jk-surface-marketing-offer h4 {
max-width: 18ch;
font-size: clamp(27px, 10vw, 40px);
}
.jk-surface-marketing-proof {
padding: 14px;
.jk-surface-marketing-actions {
align-items: flex-start;
flex-direction: column;
}
.jk-surface-marketing-next,
.jk-surface-review-receipt,
Expand Down Expand Up @@ -6981,21 +6982,16 @@ function renderMarketingSurface(contract, example) {
<p class="jk-surface-kicker">Five dinners. One calm week.</p>
<h4>${escapeHtml(example.regions.offer)}</h4>
<p>Flexible recipes, one organized grocery list, and simple swaps for nights when plans change.</p>
${renderPatternControl("primary call to action", 0, example.controls["primary call to action"])}
<small>No card required · Cancel any time</small>
</section>
<aside class="jk-surface-marketing-proof" id="meal-plan" data-pattern-region="proof" aria-label="This week's sample plan">
<div class="jk-surface-plan-card">
<div><span>Mon</span><strong>Lemon herb pasta</strong><small>25 min · family favorite</small></div>
<div><span>Wed</span><strong>Crispy tofu bowls</strong><small>30 min · easy swap</small></div>
<div><span>Fri</span><strong>Sheet-pan fajitas</strong><small>35 min · one pan</small></div>
<div class="jk-surface-marketing-actions">
${renderPatternControl("primary call to action", 0, example.controls["primary call to action"])}
${renderPatternStaticLink("secondary information path", example.controls["secondary information path"], "#meal-plan")}
</div>
<dl class="jk-surface-proof-stats">
<dl class="jk-surface-marketing-proofline" id="meal-plan" data-pattern-region="proof" aria-label="Meal plan member proof">
<div><dt>4.8/5</dt><dd>member rating</dd></div>
<div><dt>$34</dt><dd>average weekly savings</dd></div>
</dl>
Comment on lines +6989 to 6992

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore the sample-plan target

When a visitor follows “See a sample plan,” #meal-plan now contains only a member rating and savings statistic; the recipes, meal timings, and swap evidence were removed. This breaks the specimen’s declared activity and completion path, which require the shopper to inspect a sample plan before joining, and the generated background cannot replace that information for users who cannot perceive it. Restore meaningful sample-plan content at this target or change the interaction contract and control label accordingly.

AGENTS.md reference: AGENTS.md:L30-L36

Useful? React with 👍 / 👎.

${renderPatternStaticLink("secondary information path", example.controls["secondary information path"], "#meal-plan")}
</aside>
<small>No card required · Cancel any time</small>
</section>
</div>
<footer class="jk-surface-marketing-next" data-pattern-region="primary-next-step" data-pattern-completion>
<strong>See the whole week before joining.</strong>
Expand Down Expand Up @@ -11492,6 +11488,7 @@ export async function buildSite(
await fs.mkdir(path.join(outDir, "evals", "site-rebuild-log"), { recursive: true });
await fs.mkdir(path.join(outDir, "examples"), { recursive: true });
await fs.mkdir(path.join(outDir, "value"), { recursive: true });
await fs.mkdir(path.join(outDir, "assets", "patterns"), { recursive: true });
await fs.mkdir(path.join(outDir, "assets", "releases"), { recursive: true });

await fs.writeFile(path.join(outDir, "assets", "site.css"), stylesheet.trimStart());
Expand All @@ -11508,6 +11505,20 @@ export async function buildSite(
path.join(__dirname, "assets", HOMEPAGE_HERO_ART_FILENAME),
path.join(outDir, "assets", HOMEPAGE_HERO_ART_FILENAME),
);
await fs.copyFile(
path.join(
__dirname,
"assets",
"patterns",
MARKETING_PATTERN_HERO_FILENAME,
),
path.join(
outDir,
"assets",
"patterns",
MARKETING_PATTERN_HERO_FILENAME,
),
);
await fs.copyFile(
path.join(
__dirname,
Expand Down
80 changes: 80 additions & 0 deletions tests/patterns-browser.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ async function inspectPatternsPage(client, page, viewport) {
const rows = [...(index?.querySelectorAll("tbody tr") ?? [])];
const specimens = [...document.querySelectorAll("[data-pattern-specimen]")];
const surfaces = [...document.querySelectorAll("[data-pattern-surface]")];
const marketingHero = document.querySelector('[data-pattern-surface="marketing"] .jk-surface-marketing-hero');
const marketingOffer = marketingHero?.querySelector('.jk-surface-marketing-offer');
const marketingProof = marketingHero?.querySelector('[data-pattern-region="proof"]');
const marketingCompletion = document.querySelector('[data-pattern-surface="marketing"] [data-pattern-completion]');
const marketingAsset = new Image();
marketingAsset.src = "/assets/patterns/marketing-surface-weeknight-meal-plan.webp";
let marketingAssetLoaded = true;
try {
await marketingAsset.decode();
} catch {
marketingAssetLoaded = false;
}
const rectOf = (element) => {
const rect = element?.getBoundingClientRect();
return rect
? { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height }
: null;
};
const axe = await globalThis.axe.run(document, {
reporter: "v2",
resultTypes: ["violations"]
Expand Down Expand Up @@ -102,6 +120,24 @@ async function inspectPatternsPage(client, page, viewport) {
disabled: select.disabled,
ariaDisabled: select.getAttribute("aria-disabled"),
})),
marketing: {
heroRect: rectOf(marketingHero),
offerRect: rectOf(marketingOffer),
proofRect: rectOf(marketingProof),
completionRect: rectOf(marketingCompletion),
backgroundImage: marketingHero ? getComputedStyle(marketingHero).backgroundImage : "none",
backgroundPosition: marketingHero ? getComputedStyle(marketingHero).backgroundPosition : "none",
backgroundRepeat: marketingHero ? getComputedStyle(marketingHero).backgroundRepeat : "none",
backgroundSize: marketingHero ? getComputedStyle(marketingHero).backgroundSize : "none",
assetLoaded: marketingAssetLoaded,
assetWidth: marketingAsset.naturalWidth,
assetHeight: marketingAsset.naturalHeight,
proofInsideOffer: Boolean(marketingOffer?.contains(marketingProof)),
legacyRightWidgetPresent: Boolean(marketingHero?.querySelector(
".jk-surface-marketing-proof, .jk-surface-plan-card, .jk-surface-proof-stats"
)),
semanticImageCount: marketingHero?.querySelectorAll("img").length ?? 0,
},
axeViolations: axe.violations.map((violation) => ({
id: violation.id,
impact: violation.impact,
Expand All @@ -128,6 +164,50 @@ async function inspectPatternsPage(client, page, viewport) {
`${viewport.id}: a static select remains label-focusable or changeable`,
);
assert.deepEqual(observed.axeViolations, [], `${viewport.id}: page-level axe violations`);
assert.match(
observed.marketing.backgroundImage,
/marketing-surface-weeknight-meal-plan\.webp/,
`${viewport.id}: marketing hero background asset is missing`,
);
if (viewport.mobile) {
assert.equal(
observed.marketing.backgroundSize,
"100% 100%, 100%",
"mobile: the full generated image should remain visible above an opaque copy field",
);
assert.equal(observed.marketing.backgroundRepeat, "no-repeat, no-repeat");
} else {
assert.equal(
observed.marketing.backgroundSize.split(",").every((size) => size.trim() === "cover"),
true,
"desktop: every marketing hero background layer must cover its stage",
);
assert.equal(observed.marketing.backgroundPosition, "50% 48%, 50% 48%");
}
assert.equal(observed.marketing.assetLoaded, true, `${viewport.id}: marketing hero asset did not decode`);
assert.equal(observed.marketing.assetWidth, 1440, `${viewport.id}: marketing hero width drifted`);
assert.equal(observed.marketing.assetHeight, 900, `${viewport.id}: marketing hero height drifted`);
assert.equal(observed.marketing.proofInsideOffer, true, `${viewport.id}: proof returned to a right-side column`);
assert.equal(observed.marketing.legacyRightWidgetPresent, false, `${viewport.id}: legacy marketing widget returned`);
assert.equal(observed.marketing.semanticImageCount, 0, `${viewport.id}: decorative hero media should remain a CSS background`);
for (const [part, rect] of Object.entries({
hero: observed.marketing.heroRect,
offer: observed.marketing.offerRect,
proof: observed.marketing.proofRect,
completion: observed.marketing.completionRect,
})) {
assert.ok(rect && rect.width > 0 && rect.height > 0, `${viewport.id}: marketing ${part} is not visible`);
}
assert.ok(
observed.marketing.proofRect.bottom <= observed.marketing.heroRect.bottom + 1,
`${viewport.id}: inline marketing proof escapes the hero`,
);
if (viewport.mobile) {
assert.ok(
observed.marketing.offerRect.top - observed.marketing.heroRect.top >= 190,
"mobile: marketing copy should begin below the generated image focal strip",
);
}

for (const specimen of observed.specimens) {
assert.ok(specimen.rect.width > 0 && specimen.rect.height > 0, `${viewport.id}: ${specimen.contract} specimen has no visible geometry`);
Expand Down
12 changes: 12 additions & 0 deletions tests/site-local-server.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,18 @@ try {
assert.equal(await headResponse.text(), "");
}

await assertStaticGetAndHead(
url,
"/assets/patterns/marketing-surface-weeknight-meal-plan.webp",
"image/webp",
(body) => {
assert.equal(body.subarray(0, 4).toString("ascii"), "RIFF");
assert.equal(body.subarray(8, 12).toString("ascii"), "WEBP");
assert.ok(body.length > 0);
assert.ok(body.length < 250_000);
},
);

await assertStaticGetAndHead(url, "/assets/site.css", "text/css; charset=utf-8", (body) => {
const css = body.toString("utf8");
const controlsCss = cssRuleBody(css, ".homepage-film-controls");
Expand Down
40 changes: 39 additions & 1 deletion tests/site.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2506,7 +2506,11 @@ assert.equal(
const expectedPatternSurfaceStructures = {
marketing: {
layout: "landing-page",
markers: ['<div class="jk-surface-marketing-nav"', '<section class="jk-surface-marketing-offer"'],
markers: [
'<div class="jk-surface-marketing-nav"',
'<section class="jk-surface-marketing-offer"',
'<dl class="jk-surface-marketing-proofline"',
],
},
workbench: {
layout: "queue-workspace",
Expand Down Expand Up @@ -3332,6 +3336,21 @@ assert.doesNotMatch(
/<nav class="jk-surface-marketing-nav"/,
"Decorative marketing labels must not create an empty navigation landmark.",
);
assert.doesNotMatch(
marketingPatternHtml,
/class="jk-surface-marketing-proof(?:\s|")|class="jk-surface-plan-card(?:\s|")|class="jk-surface-proof-stats(?:\s|")/,
"The marketing specimen must not restore the right-side plan widget.",
);
assert.match(
marketingPatternHtml,
/<section class="jk-surface-marketing-offer"[^>]*>[\s\S]*<dl class="jk-surface-marketing-proofline"/,
"Marketing proof should remain inline with the offer instead of becoming a second hero column.",
);
assert.match(
siteCss,
/url\("\/assets\/patterns\/marketing-surface-weeknight-meal-plan\.webp"\)/,
"The marketing hero should reference its generated background asset.",
);
const formFlowPatternHtml = patternSpecimensExport.specimens.find(
(specimen) => specimen.contract_id === "form_flow",
)?.rendered_html ?? "";
Expand Down Expand Up @@ -4284,6 +4303,25 @@ assert.equal(homepageHeroArt.subarray(0, 4).toString("ascii"), "RIFF");
assert.equal(homepageHeroArt.subarray(8, 12).toString("ascii"), "WEBP");
assert.ok(homepageHeroArt.length > 0);
assert.ok(homepageHeroArt.length < 250_000);
const marketingPatternHeroArt = fs.readFileSync(
path.join(
tempDir,
"assets",
"patterns",
"marketing-surface-weeknight-meal-plan.webp",
),
);
const marketingPatternHeroArtSource = fs.readFileSync(
new URL(
"../site/assets/patterns/marketing-surface-weeknight-meal-plan.webp",
import.meta.url,
),
);
assert.deepEqual(marketingPatternHeroArt, marketingPatternHeroArtSource);
assert.equal(marketingPatternHeroArt.subarray(0, 4).toString("ascii"), "RIFF");
assert.equal(marketingPatternHeroArt.subarray(8, 12).toString("ascii"), "WEBP");
assert.ok(marketingPatternHeroArt.length > 0);
assert.ok(marketingPatternHeroArt.length < 250_000);

const visualCompositionFilmSource = fs.readFileSync(
new URL(
Expand Down
Loading