Skip to content

Commit 40d796f

Browse files
author
DavidQ
committed
Refine Palette Manager harmony swatch display and names - PR_26140_061-refine-harmony-swatch-display-and-names
1 parent dc18ca6 commit 40d796f

5 files changed

Lines changed: 186 additions & 42 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Palette Harmony Swatch Display Report
2+
3+
PR: PR_26140_061-refine-harmony-swatch-display-and-names
4+
5+
## Summary
6+
7+
Refined Palette Manager V2 harmony results so generated harmony colors render as swatch-only color tiles. The visible tile no longer contains label text, while title, ARIA label, and DOM readout metadata retain the palette name, swatch name, harmony label, and hex value for hover/focus discovery.
8+
9+
## Changes
10+
11+
- Updated `PaletteHarmonyControl` so each harmony result button renders as a color-only swatch tile with no visible child text.
12+
- Increased harmony swatch tiles to 40px by 40px and added focus-visible styling.
13+
- Preserved metadata/readout behavior through:
14+
- `title` tooltip text
15+
- `aria-label`
16+
- `data-harmony-label`
17+
- `data-harmony-palette`
18+
- `data-harmony-swatch-name`
19+
- `data-harmony-hex`
20+
- Kept the no-selection/empty harmony message readable by spanning it across the harmony grid.
21+
- Updated add behavior for closest-match colors so added user-palette swatches use only the matched swatch color name.
22+
- Example: display metadata may be `Crayola008 - Black`, but the added user swatch name is `Black`.
23+
- Preserved calculated-mode names such as `Complementary - +180 deg`.
24+
25+
## Validation Coverage
26+
27+
The targeted Playwright test now confirms:
28+
29+
- Harmony swatches are 40px by 40px.
30+
- Harmony swatches have no visible text content.
31+
- Hover/focus metadata contains palette name, swatch name, and hex for matched colors.
32+
- Calculated mode keeps useful generated names.
33+
- Add Selected creates a matched `Black` swatch without a palette-name prefix.
34+
- Add All applies the same closest-match naming rule to all newly added matched colors.
35+
- Existing Add Selected/Add All duplicate behavior is preserved.
36+
37+
## Validation
38+
39+
Passed:
40+
41+
- `node --check tools/palette-manager-v2/controls/PaletteHarmonyControl.js; node --check tools/palette-manager-v2/modules/PaletteManagerApp.js; node --check tools/palette-manager-v2/modules/paletteHarmonyUtils.js; node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
42+
- `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "generates Palette Manager V2 harmony schemes"`
43+
- `npm run test:workspace-v2` (59 passed)
44+
- `git diff --check` (passed; Git reported line-ending normalization warnings for the modified Playwright spec and Palette Manager CSS only)
45+
46+
## Scope Notes
47+
48+
- Harmony calculations were not changed.
49+
- Closest-match calculation behavior was not changed.
50+
- No sample JSON was touched.
51+
- Full samples smoke test was not run, per PR instruction.
52+
- No commit was made by Codex.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8103,12 +8103,20 @@ test.describe("Workspace Manager V2 bootstrap", () => {
81038103

81048104
await page.locator("#harmonyMatchSourceSelect").selectOption("calculated");
81058105
await page.locator("#harmonySchemeSelect").selectOption("complementary");
8106-
await expect(page.locator("#harmonyColorList [data-harmony-index='0']")).toHaveAttribute("data-harmony-hex", "#00FFFF");
8107-
await expect(page.locator("#harmonyColorList [data-harmony-index='0'] .palette-manager-v2__harmony-text")).toHaveText("Complementary - +180 deg");
8108-
await expect(page.locator("#harmonyColorList [data-harmony-index='0'] .palette-manager-v2__harmony-meta")).toHaveText("#00FFFF");
8106+
const calculatedHarmonyTile = page.locator("#harmonyColorList [data-harmony-index='0']");
8107+
await expect(calculatedHarmonyTile).toHaveAttribute("data-harmony-hex", "#00FFFF");
8108+
await expect(calculatedHarmonyTile).toHaveAttribute("data-harmony-label", "Complementary - +180 deg");
8109+
await expect(calculatedHarmonyTile).toHaveAttribute("title", /Name: Complementary - \+180 deg[\s\S]*Hex: #00FFFF/);
8110+
await expect(calculatedHarmonyTile).toHaveText("");
8111+
const calculatedHarmonyTileBox = await calculatedHarmonyTile.boundingBox();
8112+
expect(Math.round(calculatedHarmonyTileBox.width)).toBe(40);
8113+
expect(Math.round(calculatedHarmonyTileBox.height)).toBe(40);
81098114
await page.locator("#addSelectedHarmonyButton").click();
81108115
await expect(page.locator("#userPaletteCount")).toHaveText("2 user swatches");
81118116
await expect(page.locator("#paletteStatus")).toHaveText(/OK Added selected harmony color/);
8117+
await expect.poll(async () => {
8118+
return page.evaluate(() => window.paletteManagerV2App.getPaletteValue().swatches.at(-1).name);
8119+
}).toBe("Complementary - +180 deg");
81128120

81138121
await page.locator('#userSwatchList [aria-label="Edit Harmony Base Red"]').click();
81148122
await expect(page.locator("#harmonyColorList [data-harmony-index='0']")).toHaveAttribute("data-harmony-hex", "#00FFFF");
@@ -8134,18 +8142,24 @@ test.describe("Workspace Manager V2 bootstrap", () => {
81348142
const harmonyHex = harmonyButton.dataset.harmonyHex;
81358143
const paletteName = harmonyButton.dataset.harmonyPalette;
81368144
const swatchName = harmonyButton.dataset.harmonySwatchName;
8137-
const label = harmonyButton.querySelector(".palette-manager-v2__harmony-text").textContent;
8138-
const meta = harmonyButton.querySelector(".palette-manager-v2__harmony-meta").textContent;
8145+
const label = harmonyButton.dataset.harmonyLabel;
8146+
const readout = harmonyButton.getAttribute("title");
8147+
const ariaLabel = harmonyButton.getAttribute("aria-label");
8148+
const textContent = harmonyButton.textContent;
81398149
const expectedPaletteName = formatPaletteName(window.paletteList.SOURCE_PALETTE_LABELS[sourceId] || sourceId);
81408150
const sourceMatch = (window.paletteList.SOURCE_PALETTES[sourceId] || [])
81418151
.find((swatch) => swatch.hex.toUpperCase() === harmonyHex && swatch.name === swatchName);
8142-
return { expectedPaletteName, harmonyHex, isFromCurrentSource: Boolean(sourceMatch), label, meta, paletteName, swatchName };
8152+
return { ariaLabel, expectedPaletteName, harmonyHex, isFromCurrentSource: Boolean(sourceMatch), label, paletteName, readout, swatchName, textContent };
81438153
});
81448154
expect(sourceMatchState.isFromCurrentSource).toBe(true);
81458155
expect(sourceMatchState.paletteName).toBe(sourceMatchState.expectedPaletteName);
81468156
expect(sourceMatchState.label).toBe(`${sourceMatchState.expectedPaletteName} - ${sourceMatchState.swatchName}`);
81478157
expect(sourceMatchState.label).not.toContain("Closest");
8148-
expect(sourceMatchState.meta).toBe(sourceMatchState.harmonyHex);
8158+
expect(sourceMatchState.textContent).toBe("");
8159+
expect(sourceMatchState.readout).toContain(`Palette: ${sourceMatchState.expectedPaletteName}`);
8160+
expect(sourceMatchState.readout).toContain(`Name: ${sourceMatchState.swatchName}`);
8161+
expect(sourceMatchState.readout).toContain(`Hex: ${sourceMatchState.harmonyHex}`);
8162+
expect(sourceMatchState.ariaLabel).toContain(sourceMatchState.harmonyHex);
81498163

81508164
await page.locator("#harmonyMatchSourceSelect").selectOption("all-palettes");
81518165
const allMatchState = await page.evaluate(() => {
@@ -8154,21 +8168,78 @@ test.describe("Workspace Manager V2 bootstrap", () => {
81548168
const harmonyHex = harmonyButton.dataset.harmonyHex;
81558169
const paletteName = harmonyButton.dataset.harmonyPalette;
81568170
const swatchName = harmonyButton.dataset.harmonySwatchName;
8157-
const label = harmonyButton.querySelector(".palette-manager-v2__harmony-text").textContent;
8158-
const meta = harmonyButton.querySelector(".palette-manager-v2__harmony-meta").textContent;
8171+
const label = harmonyButton.dataset.harmonyLabel;
8172+
const readout = harmonyButton.getAttribute("title");
8173+
const textContent = harmonyButton.textContent;
81598174
const allMatch = Object.entries(window.paletteList.SOURCE_PALETTES)
81608175
.flatMap(([sourceId, swatches]) => swatches.map((swatch) => ({ sourceId, swatch })))
81618176
.find(({ sourceId, swatch }) => {
81628177
return swatch.hex.toUpperCase() === harmonyHex
81638178
&& swatch.name === swatchName
81648179
&& formatPaletteName(window.paletteList.SOURCE_PALETTE_LABELS[sourceId] || sourceId) === paletteName;
81658180
});
8166-
return { harmonyHex, isFromAnySource: Boolean(allMatch), label, meta, paletteName, swatchName };
8181+
return { harmonyHex, isFromAnySource: Boolean(allMatch), label, paletteName, readout, swatchName, textContent };
81678182
});
81688183
expect(allMatchState.isFromAnySource).toBe(true);
81698184
expect(allMatchState.label).toBe(`${allMatchState.paletteName} - ${allMatchState.swatchName}`);
81708185
expect(allMatchState.label).not.toContain("Closest");
8171-
expect(allMatchState.meta).toBe(allMatchState.harmonyHex);
8186+
expect(allMatchState.textContent).toBe("");
8187+
expect(allMatchState.readout).toContain(`Palette: ${allMatchState.paletteName}`);
8188+
expect(allMatchState.readout).toContain(`Name: ${allMatchState.swatchName}`);
8189+
expect(allMatchState.readout).toContain(`Hex: ${allMatchState.harmonyHex}`);
8190+
8191+
await page.locator('#userSwatchList [aria-label="Edit Harmony Base Red"]').click();
8192+
await page.locator("#harmonyMatchSourceSelect").selectOption("source-palette");
8193+
await page.locator("#harmonySchemeSelect").selectOption("achromatic");
8194+
const matchedBlackTile = page.locator("#harmonyColorList [data-harmony-index='1']");
8195+
await expect(matchedBlackTile).toHaveAttribute("data-harmony-swatch-name", "Black");
8196+
await expect(matchedBlackTile).toHaveText("");
8197+
await expect(matchedBlackTile).toHaveAttribute("aria-label", /Palette: .*Name: Black.*Hex:/);
8198+
const matchedBlackTileBox = await matchedBlackTile.boundingBox();
8199+
expect(Math.round(matchedBlackTileBox.width)).toBe(40);
8200+
expect(Math.round(matchedBlackTileBox.height)).toBe(40);
8201+
await matchedBlackTile.focus();
8202+
await expect(matchedBlackTile).toBeFocused();
8203+
await matchedBlackTile.click();
8204+
const matchedBlackState = await matchedBlackTile.evaluate((button) => ({
8205+
hex: button.dataset.harmonyHex,
8206+
label: button.dataset.harmonyLabel,
8207+
paletteName: button.dataset.harmonyPalette,
8208+
swatchName: button.dataset.harmonySwatchName
8209+
}));
8210+
await page.locator("#addSelectedHarmonyButton").click();
8211+
await expect(page.locator("#userPaletteCount")).toHaveText("5 user swatches");
8212+
const addedBlackSwatch = await page.evaluate(() => window.paletteManagerV2App.getPaletteValue().swatches.at(-1));
8213+
expect(addedBlackSwatch.hex).toBe(matchedBlackState.hex);
8214+
expect(addedBlackSwatch.name).toBe("Black");
8215+
expect(addedBlackSwatch.name).toBe(matchedBlackState.swatchName);
8216+
expect(addedBlackSwatch.name).not.toContain(matchedBlackState.paletteName);
8217+
expect(matchedBlackState.label).toBe(`${matchedBlackState.paletteName} - Black`);
8218+
8219+
await page.locator('#userSwatchList [aria-label="Edit Harmony Base Red"]').click();
8220+
await page.locator("#harmonyMatchSourceSelect").selectOption("source-palette");
8221+
await page.locator("#harmonySchemeSelect").selectOption("achromatic");
8222+
const sourceAchromaticMatches = await page.evaluate(() => {
8223+
return Array.from(document.querySelectorAll("#harmonyColorList [data-harmony-index]"))
8224+
.map((button) => ({
8225+
hex: button.dataset.harmonyHex,
8226+
paletteName: button.dataset.harmonyPalette,
8227+
swatchName: button.dataset.harmonySwatchName
8228+
}));
8229+
});
8230+
const beforeAddAllCount = await page.evaluate(() => window.paletteManagerV2App.getPaletteValue().swatches.length);
8231+
await page.locator("#addAllHarmonyButton").click();
8232+
await expect(page.locator("#paletteStatus")).toHaveText(/OK Added \d+ harmony colors\. Skipped \d+ duplicates\./);
8233+
const addAllNamingState = await page.evaluate((beforeCount) => {
8234+
return window.paletteManagerV2App.getPaletteValue().swatches.slice(beforeCount);
8235+
}, beforeAddAllCount);
8236+
expect(addAllNamingState.length).toBeGreaterThan(0);
8237+
addAllNamingState.forEach((swatch) => {
8238+
const match = sourceAchromaticMatches.find((candidate) => candidate.hex === swatch.hex);
8239+
expect(match).toBeTruthy();
8240+
expect(swatch.name).toBe(match.swatchName);
8241+
expect(swatch.name).not.toContain(match.paletteName);
8242+
});
81728243
expect(pageErrors).toEqual([]);
81738244
} finally {
81748245
await workspaceV2CoverageReporter.stop(page);

tools/palette-manager-v2/controls/PaletteHarmonyControl.js

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
1-
import { cloneSwatch, normalizeHex } from "../modules/paletteUtils.js";
1+
import { normalizeHex } from "../modules/paletteUtils.js";
2+
3+
function getHarmonyDisplayName(color, hex) {
4+
return color.displayName || color.name || hex;
5+
}
6+
7+
function createHarmonyReadout(color) {
8+
const hex = normalizeHex(color.hex);
9+
const lines = [];
10+
if (color.paletteName) {
11+
lines.push(`Palette: ${color.paletteName}`);
12+
}
13+
lines.push(`Name: ${color.swatchName || color.name || color.displayName || hex}`);
14+
lines.push(`Hex: ${hex}`);
15+
if (color.displayName && color.displayName !== color.name && color.displayName !== color.swatchName) {
16+
lines.push(`Harmony: ${color.displayName}`);
17+
}
18+
return lines.join("\n");
19+
}
220

321
export class PaletteHarmonyControl {
422
constructor({ documentRef, refs, app }) {
@@ -101,36 +119,25 @@ export class PaletteHarmonyControl {
101119
}
102120

103121
createHarmonyColorButton(color, index) {
104-
const cleanSwatch = cloneSwatch(color.swatch || {});
122+
const hex = normalizeHex(color.hex);
123+
const displayName = getHarmonyDisplayName(color, hex);
124+
const readout = createHarmonyReadout(color);
105125
const button = this.document.createElement("button");
106126
button.type = "button";
107127
button.className = "palette-manager-v2__harmony-color";
108128
button.classList.toggle("is-selected", index === this.app.getSelectedHarmonyColorIndex());
109129
button.setAttribute("aria-pressed", String(index === this.app.getSelectedHarmonyColorIndex()));
130+
button.setAttribute("aria-label", readout.replace(/\n/g, ". "));
110131
button.dataset.harmonyIndex = String(index);
111-
button.dataset.harmonyHex = normalizeHex(color.hex);
112-
button.dataset.harmonyLabel = color.displayName || color.name || "";
132+
button.dataset.harmonyHex = hex;
133+
button.dataset.harmonyLabel = displayName;
113134
button.dataset.harmonyPalette = color.paletteName || "";
114135
button.dataset.harmonySwatchName = color.swatchName || "";
115-
button.title = `${color.displayName || color.name}: ${normalizeHex(color.hex)}`;
136+
button.title = readout;
137+
button.style.background = hex;
116138
button.addEventListener("click", () => {
117139
this.app.setSelectedHarmonyColorIndex(index);
118140
});
119-
120-
const chip = this.document.createElement("span");
121-
chip.className = "palette-manager-v2__harmony-chip";
122-
chip.style.background = normalizeHex(color.hex);
123-
chip.setAttribute("aria-hidden", "true");
124-
125-
const text = this.document.createElement("span");
126-
text.className = "palette-manager-v2__harmony-text";
127-
text.textContent = color.displayName || cleanSwatch.name || color.name || normalizeHex(color.hex);
128-
129-
const meta = this.document.createElement("span");
130-
meta.className = "palette-manager-v2__harmony-meta";
131-
meta.textContent = normalizeHex(color.hex);
132-
133-
button.append(chip, text, meta);
134141
return button;
135142
}
136143
}

tools/palette-manager-v2/modules/PaletteManagerApp.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -599,18 +599,23 @@ export class PaletteManagerApp {
599599
const displayName = matchedSwatch
600600
? `${paletteName} - ${swatchName}`
601601
: `${schemeLabel} - ${calculationLabel || cleanHex}`;
602+
const addName = matchedSwatch
603+
? (swatchName || cleanHex)
604+
: displayName;
602605
return {
603606
baseHex: normalizeHex(color.hex).slice(0, 7),
604607
hex: cleanHex,
605-
name: displayName,
608+
name: addName,
609+
addName,
610+
addNameIsExact: Boolean(matchedSwatch),
606611
displayName,
607612
paletteName,
608613
swatchName,
609614
source: matchedSwatch?.source || findHarmonyMatchSource(this.state.harmonyMatchSource).label,
610615
swatch: cloneSwatch({
611616
symbol: "",
612617
hex: cleanHex,
613-
name: displayName,
618+
name: addName,
614619
source: paletteName || findHarmonyMatchSource(this.state.harmonyMatchSource).label,
615620
tags: ["harmony"]
616621
})
@@ -1234,10 +1239,13 @@ export class PaletteManagerApp {
12341239
}
12351240

12361241
harmonyColorToSwatch(color) {
1242+
const colorName = color?.addNameIsExact
1243+
? sanitizeText(color?.addName || color?.name)
1244+
: this.getUniqueHarmonyName(color?.addName || color?.name);
12371245
return cloneSwatch({
12381246
symbol: nextHarmonySymbol(this.state.userSwatches),
12391247
hex: normalizeHex(color?.hex).slice(0, 7),
1240-
name: this.getUniqueHarmonyName(color?.name),
1248+
name: colorName,
12411249
source: sanitizeText(color?.source) || "Harmony",
12421250
tags: ["harmony"]
12431251
});

tools/palette-manager-v2/paletteManagerV2.css

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -745,34 +745,40 @@ body.tools-platform-surface :is(input, select, textarea):hover {
745745
.palette-manager-v2__harmony-list {
746746
min-height: 52px;
747747
display: grid;
748+
grid-template-columns: repeat(auto-fill, 40px);
748749
gap: 8px;
749750
border: 1px solid var(--pm-line);
750751
border-radius: 8px;
751752
background: var(--pm-surface-strong);
752753
padding: 8px;
753754
}
754755

756+
.palette-manager-v2__harmony-list > .palette-manager-v2__meta {
757+
grid-column: 1 / -1;
758+
}
759+
755760
.palette-manager-v2__harmony-color {
756-
width: 100%;
757-
min-width: 0;
758-
display: grid;
759-
grid-template-columns: 30px minmax(0, 1fr);
760-
gap: 4px 8px;
761-
align-items: center;
761+
width: 40px;
762+
min-width: 40px;
763+
height: 40px;
762764
border: 1px solid var(--pm-line);
763765
border-radius: 8px;
764766
background: rgba(0, 0, 0, 0.2);
765767
color: inherit;
766768
cursor: pointer;
767-
padding: 6px;
768-
text-align: left;
769+
padding: 0;
769770
}
770771

771772
.palette-manager-v2__harmony-color.is-selected {
772773
border-color: var(--pm-accent);
773774
box-shadow: inset 0 0 0 1px var(--pm-accent);
774775
}
775776

777+
.palette-manager-v2__harmony-color:focus-visible {
778+
outline: 2px solid var(--pm-accent);
779+
outline-offset: 2px;
780+
}
781+
776782
.palette-manager-v2__harmony-chip {
777783
grid-row: span 2;
778784
width: 30px;

0 commit comments

Comments
 (0)