TokenSwatchRow: saved colours after the site tokens (brand = live ring + token ref)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9c61f41079
commit
150c84065b
3 files changed
+96
No files matched your search
@@ -100,4 +100,62 @@ describe('TokenSwatchRow', () => {
|
||||
act(() => { ctx.current!.updateDesign({ primaryColor: '#ff0000' }); });
|
||||
expect(swatches()[0].style.background).toBe('rgb(255, 0, 0)');
|
||||
});
|
||||
|
||||
describe('saved colours', () => {
|
||||
const saved = [
|
||||
{ id: 'a', label: 'Logo blue', hex: '#15202c', brand: true, slot: 2 },
|
||||
{ id: 'b', label: 'Sand', hex: '#d2b48c', brand: false },
|
||||
];
|
||||
const withSaved = (value: string | undefined, onSelect: (v: string) => void) => {
|
||||
const ctx: { current: SiteDesignContextValue | null } = { current: null };
|
||||
render(
|
||||
<SiteDesignProvider>
|
||||
<DesignProbe out={ctx} />
|
||||
<TokenSwatchRow value={value} onSelect={onSelect} />
|
||||
</SiteDesignProvider>,
|
||||
);
|
||||
act(() => { ctx.current!.updateDesign({ swatches: saved }); });
|
||||
};
|
||||
const savedBtns = () => Array.from(container!.querySelectorAll<HTMLButtonElement>('button[data-swatch]'));
|
||||
|
||||
test('shown after the 11 token swatches; title = label + hex; brand shows a live ring', () => {
|
||||
withSaved(undefined, () => {});
|
||||
const all = swatches();
|
||||
expect(all.length).toBe(COLOR_TOKENS.length + 2);
|
||||
expect(all.slice(0, COLOR_TOKENS.length).every((b) => b.dataset.token)).toBe(true);
|
||||
const [brand, plain] = savedBtns();
|
||||
expect(brand.title).toContain('Logo blue');
|
||||
expect(brand.title).toContain('#15202c');
|
||||
expect(brand.title).toContain('var(--wsb-brand-2)');
|
||||
expect(brand.classList.contains('token-swatch--live')).toBe(true);
|
||||
expect(brand.dataset.live).toBe('true');
|
||||
expect(plain.title).toBe('Sand #d2b48c');
|
||||
expect(plain.classList.contains('token-swatch--live')).toBe(false);
|
||||
expect(brand.style.background).toBe('rgb(21, 32, 44)');
|
||||
});
|
||||
|
||||
test('brand swatch selects var(--wsb-brand-N); plain selects its hex', () => {
|
||||
const onSelect = vi.fn();
|
||||
withSaved(undefined, onSelect);
|
||||
const [brand, plain] = savedBtns();
|
||||
act(() => { brand.click(); });
|
||||
act(() => { plain.click(); });
|
||||
expect(onSelect.mock.calls.map((c) => c[0])).toEqual(['var(--wsb-brand-2)', '#d2b48c']);
|
||||
});
|
||||
|
||||
test('pressed when the value equals what the swatch stores', () => {
|
||||
withSaved('var(--wsb-brand-2)', () => {});
|
||||
expect(savedBtns().map((b) => b.getAttribute('aria-pressed'))).toEqual(['true', 'false']);
|
||||
});
|
||||
|
||||
test('plain hex swatch pressed on a case-insensitive hex match', () => {
|
||||
withSaved('#D2B48C', () => {});
|
||||
expect(savedBtns().map((b) => b.getAttribute('aria-pressed'))).toEqual(['false', 'true']);
|
||||
});
|
||||
|
||||
test('the group is labelled', () => {
|
||||
withSaved(undefined, () => {});
|
||||
expect(container!.querySelector('[role="group"]')!.getAttribute('aria-label')).toBe('Site colors');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useSiteDesign } from '../../../state/SiteDesignContext';
|
||||
import { COLOR_TOKENS, TokenKey, tokenRef, resolveTokenValue } from '../../../utils/tokens';
|
||||
import { swatchValue, normalizeHex, type SavedSwatch } from '../../../utils/swatches';
|
||||
|
||||
/** Hover/aria text for a saved swatch: label + hex (+ the live token for brand colours). */
|
||||
export function savedSwatchTitle(s: SavedSwatch): string {
|
||||
const base = s.label.trim() ? `${s.label.trim()} ${s.hex}` : s.hex;
|
||||
const ref = swatchValue(s);
|
||||
return ref !== s.hex ? `${base} (live brand colour, ${ref})` : base;
|
||||
}
|
||||
|
||||
export interface TokenSwatchRowProps {
|
||||
/** The control's current value; the swatch whose `var(--wsb-*)` ref equals it is shown pressed. */
|
||||
@@ -25,6 +33,8 @@ export const TokenSwatchRow: React.FC<TokenSwatchRowProps> = ({
|
||||
}) => {
|
||||
const { design } = useSiteDesign();
|
||||
const current = value?.trim();
|
||||
const saved: SavedSwatch[] = Array.isArray(design.swatches) ? design.swatches : [];
|
||||
const currentHex = current ? normalizeHex(current) : null;
|
||||
return (
|
||||
<div className="token-swatch-row" role="group" aria-label={label}>
|
||||
<div className="token-swatch-label">{label}</div>
|
||||
@@ -46,6 +56,26 @@ export const TokenSwatchRow: React.FC<TokenSwatchRowProps> = ({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{saved.map((sw) => {
|
||||
const stored = swatchValue(sw);
|
||||
const live = stored !== sw.hex;
|
||||
const pressed = live ? current === stored : !!currentHex && currentHex === sw.hex.toLowerCase();
|
||||
const title = savedSwatchTitle(sw);
|
||||
return (
|
||||
<button
|
||||
key={sw.id}
|
||||
type="button"
|
||||
className={`token-swatch token-swatch--saved${live ? ' token-swatch--live' : ''}${pressed ? ' active' : ''}`}
|
||||
style={{ background: sw.hex }}
|
||||
title={title}
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
data-swatch={sw.id}
|
||||
data-live={live ? 'true' : undefined}
|
||||
onClick={() => onSelect(stored)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3382,3 +3382,11 @@ body {
|
||||
.guided-regenerate-btn:hover { background: var(--color-bg-hover); }
|
||||
.guided-regenerate-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }
|
||||
/* /#followups */
|
||||
|
||||
/* Saved colours: a "live" brand swatch gets a double ring */
|
||||
.token-swatch--live {
|
||||
box-shadow: 0 0 0 2px var(--color-bg-surface, #18181b), 0 0 0 3px var(--color-accent);
|
||||
}
|
||||
.token-swatch--live.active {
|
||||
box-shadow: 0 0 0 2px var(--color-bg-surface, #18181b), 0 0 0 4px var(--color-accent);
|
||||
}
|
||||
Reference in new issue
Block a user