diff --git a/craft/src/utils/contrast-repair.test.ts b/craft/src/utils/contrast-repair.test.ts index 782decd..2179a6a 100644 --- a/craft/src/utils/contrast-repair.test.ts +++ b/craft/src/utils/contrast-repair.test.ts @@ -385,15 +385,60 @@ describe('color-mix() surfaces and ContactForm inputs (eval 2026-11-01: placehol }); test('ContactForm label colour is checked against the section it sits on', () => { const resp = replace(n('Section', { style: { backgroundColor: '#0d0b14' } }, [ - n('ContactForm', { labelColor: 'var(--wsb-text)', fields: [] }), + n('ContactForm', { style: {}, labelColor: 'var(--wsb-text)', fields: [] }), // explicit style without a background = transparent form ])); const out = repairResponseContrast(resp, D); const f = firstTree(out.response).nodes![0].props as any; expect(f.labelColor).not.toBe('var(--wsb-text)'); }); test('ContactForm with the default (token) input surface is left alone', () => { - const resp = replace(n('Section', { style: { backgroundColor: '#0d0b14' } }, [n('ContactForm', { labelColor: '#ffffff', fields: [] })])); + const resp = replace(n('Section', { style: { backgroundColor: '#0d0b14' } }, [n('ContactForm', { style: {}, labelColor: '#ffffff', fields: [] })])); const out = repairResponseContrast(resp, D); expect(firstTree(out.response).nodes![0].props.inputTextColor).toBeUndefined(); }); }); + +describe('ContactForm labels/inputs are judged against the FORM card, not the section (eval r3 case 6 ttm-likes)', () => { + // Real shape: dark gradient section; ContactForm with NO style prop, so the component's default card + // (style.backgroundColor = var(--wsb-bg), light) applies; labelColor/inputTextColor = var(--wsb-bg) (white). + const darkSection = (form: SerializedTreeNode) => replace(n('Section', { + style: { background: 'linear-gradient(135deg, #0d0b14 30%, color-mix(in srgb, var(--wsb-secondary) 40%, #0d0b14) 100%)' }, + }, [n('Container', { style: { padding: '0px' } }, [form])])); + const formOf = (r: SitesmithResponse) => firstTree(r).nodes![0].nodes![0].props as any; + + test('case-6: white labels on the default (light) form card inside a dark section are repaired to read on the card', () => { + const out = repairResponseContrast(darkSection(n('ContactForm', { + fields: [], labelColor: 'var(--wsb-bg)', inputTextColor: 'var(--wsb-bg)', + inputBg: 'color-mix(in srgb, var(--wsb-bg) 8%, #0d0b14)', + })), D); + const f = formOf(out.response); + expect(ratio(f.labelColor, 'var(--wsb-bg)')).toBeGreaterThanOrEqual(4.5); + expect(out.adjustments.some((a) => a.node === 'ContactForm' && a.from === 'var(--wsb-bg)')).toBe(true); + // typed text stays on the dark input surface (unchanged: white on near-black passes) + expect(f.inputTextColor).toBe('var(--wsb-bg)'); + }); + test('an explicit light form card via style.backgroundColor literal is honoured too', () => { + const out = repairResponseContrast(darkSection(n('ContactForm', { + fields: [], style: { backgroundColor: '#fafafa' }, labelColor: '#ffffff', + })), D); + expect(ratio(formOf(out.response).labelColor, '#fafafa')).toBeGreaterThanOrEqual(4.5); + }); + test('a form card that is dark (token surface) keeps light labels untouched', () => { + const out = repairResponseContrast(darkSection(n('ContactForm', { + fields: [], style: { backgroundColor: '#111827' }, labelColor: '#ffffff', + })), D); + expect(formOf(out.response).labelColor).toBe('#ffffff'); + expect(out.adjustments).toEqual([]); + }); + test('no regression: a transparent form (explicit style without a background) is still judged against the dark section', () => { + const out = repairResponseContrast(darkSection(n('ContactForm', { + fields: [], style: { padding: '32px' }, labelColor: 'var(--wsb-text)', + })), D); + expect(formOf(out.response).labelColor).not.toBe('var(--wsb-text)'); + }); + test('labels already readable on the (default light) form card are not changed', () => { + const out = repairResponseContrast(darkSection(n('ContactForm', { fields: [], labelColor: 'var(--wsb-text)' })), D); + expect(formOf(out.response).labelColor).toBe('var(--wsb-text)'); + expect(out.adjustments).toEqual([]); + }); +}); diff --git a/craft/src/utils/contrast-repair.ts b/craft/src/utils/contrast-repair.ts index 03c46e6..2e8cc0e 100644 --- a/craft/src/utils/contrast-repair.ts +++ b/craft/src/utils/contrast-repair.ts @@ -311,6 +311,10 @@ function ownBg(name: string, props: Record, design: SiteDesign, nam if (b !== 'none') return b; } if (BGCOLOR_DEFAULTS_TO_TEXT.has(name)) return cssBg(tokenRef('text'), design); + // ContactForm paints its own card: with no `style` prop the component default applies (style.backgroundColor = + // the bg token, ContactForm.tsx craft.props), so labels sit on THAT, not on the section behind the card. + // An explicit `style` without a background is a transparent form and falls through to the section. + if (name === 'ContactForm' && props.style === undefined) return cssBg(tokenRef('bg'), design); return 'none'; }