Site Design panel: Saved colours section + inline contrast on text/muted/link/primary-button
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
faaa8a87a0
commit
dcf9c92fee
2 files changed
+90
-3
No files matched your search
@@ -0,0 +1,62 @@
|
||||
import { describe, test, expect, afterEach } from 'vitest';
|
||||
import React from 'react';
|
||||
import { renderEditorHarness, EditorHarness } from '../../test-utils/editorHarness';
|
||||
import { EditorConfigProvider } from '../../state/EditorConfigContext';
|
||||
import { PageProvider } from '../../state/PageContext';
|
||||
import { SaveGuardProvider } from '../../state/SaveGuardContext';
|
||||
import { SiteDesignProvider, useSiteDesign, SiteDesignContextValue } from '../../state/SiteDesignContext';
|
||||
import { SiteDesignPanel } from './SiteDesignPanel';
|
||||
|
||||
let h: EditorHarness;
|
||||
let design: SiteDesignContextValue;
|
||||
const Probe: React.FC = () => { design = useSiteDesign(); return null; };
|
||||
afterEach(() => { h?.unmount(); document.body.innerHTML = ''; });
|
||||
|
||||
function mount() {
|
||||
h = renderEditorHarness();
|
||||
h.mountChild(
|
||||
<EditorConfigProvider config={{ user: 'u', apiUrl: '/api', csrfToken: 't', siteId: 1, siteDomain: 'example.com', siteName: 'E', backUrl: '/', isRoot: false }}>
|
||||
<SaveGuardProvider><PageProvider><SiteDesignProvider><Probe /><SiteDesignPanel /></SiteDesignProvider></PageProvider></SaveGuardProvider>
|
||||
</EditorConfigProvider>,
|
||||
);
|
||||
}
|
||||
const advanced = () => h.act(() => { Array.from(h.container.querySelectorAll('button')).find((b) => b.textContent?.trim() === 'advanced')!.click(); });
|
||||
const notes = () => Array.from(h.container.querySelectorAll('.contrast-note--fail')).map((n) => n.textContent ?? '');
|
||||
|
||||
describe('SiteDesignPanel colours', () => {
|
||||
test('has a Saved colours section with the pick buttons', () => {
|
||||
mount();
|
||||
expect(h.container.textContent).toContain('Saved colours');
|
||||
expect(Array.from(h.container.querySelectorAll('button')).some((b) => b.textContent === 'Palette from image')).toBe(true);
|
||||
});
|
||||
|
||||
test('defaults: only the genuinely failing default pairs (white on #3b82f6, #3b82f6 link on white = 3.68:1) are called out', () => {
|
||||
mount();
|
||||
advanced();
|
||||
const n = notes();
|
||||
expect(n.length).toBe(2);
|
||||
expect(n[0]).toContain('Button text on primary: Low contrast 3.68:1');
|
||||
expect(n[1]).toContain('Link on background: Low contrast 3.68:1');
|
||||
});
|
||||
|
||||
test('low-contrast text / muted / link / primary-button pairs are called out where picked', () => {
|
||||
mount();
|
||||
h.act(() => { design.updateDesign({ textColor: '#eeeeee', mutedTextColor: '#f0f0f0', linkColor: '#fafafa', primaryColor: '#ffffff' }); });
|
||||
advanced();
|
||||
const all = notes().join(' | ');
|
||||
expect(all).toContain('Text on background: Low contrast');
|
||||
expect(all).toContain('Muted text on background: Low contrast');
|
||||
expect(all).toContain('Link on background: Low contrast');
|
||||
expect(all).toContain('Button text on primary: Low contrast');
|
||||
});
|
||||
|
||||
test('"Use #xxxxxx" on the text colour applies the suggestion', () => {
|
||||
mount();
|
||||
h.act(() => { design.updateDesign({ textColor: '#eeeeee' }); });
|
||||
advanced();
|
||||
const use = h.container.querySelector<HTMLButtonElement>('.contrast-note__use')!;
|
||||
h.act(() => { use.click(); });
|
||||
expect(design.design.textColor).toMatch(/^#[0-9a-f]{6}$/);
|
||||
expect(design.design.textColor).not.toBe('#eeeeee');
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,8 @@ import { AssetPicker } from '../../ui/AssetPicker';
|
||||
import { inputStyle, ColorPickerField } from './styles/shared';
|
||||
import { openProfileWizard } from '../sitesmith/ProfileWizard';
|
||||
import { PALETTES, paletteUpdates, paletteMatches } from '../../constants/palettes';
|
||||
import { SwatchManager } from './SwatchManager';
|
||||
import { siteColorPairs } from '../../utils/contrast';
|
||||
|
||||
type DesignTab = 'basic' | 'advanced';
|
||||
|
||||
@@ -20,11 +22,26 @@ interface ColorFieldProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
/** Id of a site colour pair (utils/contrast siteColorPairs) this field belongs to. */
|
||||
pair?: 'text-bg' | 'muted-bg' | 'link-bg' | 'button-primary';
|
||||
}
|
||||
|
||||
const ColorField: React.FC<ColorFieldProps> = ({ label, value, onChange }) => (
|
||||
<ColorPickerField label={label} value={value} onChange={onChange} showTokens={false} />
|
||||
);
|
||||
const ColorField: React.FC<ColorFieldProps> = ({ label, value, onChange, pair }) => {
|
||||
const { design } = useSiteDesign();
|
||||
const p = pair ? siteColorPairs(design).find((x) => x.id === pair) : undefined;
|
||||
// The other side of the pair is a hex in the design (these fields are the token source).
|
||||
// Text/muted/link are the foreground against the background; the button pair is the
|
||||
// primary colour (the background of the button) against the page background (its text).
|
||||
const other = p ? design.backgroundColor : undefined;
|
||||
return (
|
||||
<ColorPickerField
|
||||
label={label} value={value} onChange={onChange} showTokens={false}
|
||||
contrastWith={other}
|
||||
contrastRole={pair === 'button-primary' ? 'bg' : 'fg'}
|
||||
contrastLabel={p?.label}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Try a palette ---------- */
|
||||
|
||||
@@ -299,6 +316,7 @@ export const SiteDesignPanel: React.FC = () => {
|
||||
</div>
|
||||
<ColorField
|
||||
label="Primary Color"
|
||||
pair="button-primary"
|
||||
value={design.primaryColor}
|
||||
onChange={(v) => updateDesign({ primaryColor: v })}
|
||||
/>
|
||||
@@ -324,6 +342,7 @@ export const SiteDesignPanel: React.FC = () => {
|
||||
/>
|
||||
<ColorField
|
||||
label="Link Color"
|
||||
pair="link-bg"
|
||||
value={design.linkColor}
|
||||
onChange={(v) => updateDesign({ linkColor: v })}
|
||||
/>
|
||||
@@ -335,6 +354,7 @@ export const SiteDesignPanel: React.FC = () => {
|
||||
<>
|
||||
<ColorField
|
||||
label="Primary Color"
|
||||
pair="button-primary"
|
||||
value={design.primaryColor}
|
||||
onChange={(v) => updateDesign({ primaryColor: v })}
|
||||
/>
|
||||
@@ -370,11 +390,13 @@ export const SiteDesignPanel: React.FC = () => {
|
||||
/>
|
||||
<ColorField
|
||||
label="Text Color"
|
||||
pair="text-bg"
|
||||
value={design.textColor}
|
||||
onChange={(v) => updateDesign({ textColor: v })}
|
||||
/>
|
||||
<ColorField
|
||||
label="Muted Text Color"
|
||||
pair="muted-bg"
|
||||
value={design.mutedTextColor}
|
||||
onChange={(v) => updateDesign({ mutedTextColor: v })}
|
||||
/>
|
||||
@@ -400,6 +422,7 @@ export const SiteDesignPanel: React.FC = () => {
|
||||
/>
|
||||
<ColorField
|
||||
label="Link Color"
|
||||
pair="link-bg"
|
||||
value={design.linkColor}
|
||||
onChange={(v) => updateDesign({ linkColor: v })}
|
||||
/>
|
||||
@@ -420,6 +443,8 @@ export const SiteDesignPanel: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<SwatchManager />
|
||||
|
||||
{/* Reset button */}
|
||||
<div style={{ marginTop: 16, paddingTop: 12, borderTop: '1px solid var(--color-border)' }}>
|
||||
<button
|
||||
|
||||
Reference in new issue
Block a user