Skip to content

Commit dc18ca6

Browse files
author
DavidQ
committed
Improve Palette Manager harmony swatch labeling - PR_26140_060-improve-harmony-swatch-labeling
1 parent e74f206 commit dc18ca6

5 files changed

Lines changed: 134 additions & 20 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Palette Harmony Labeling Report
2+
3+
PR: PR_26140_060-improve-harmony-swatch-labeling
4+
5+
## Summary
6+
7+
Improved Palette Manager V2 harmony result labels without changing harmony calculation or add behavior. Closest-match harmony cards now display the matched palette and swatch color name as the primary label, with the hex value on the next line.
8+
9+
## Changes
10+
11+
- Updated harmony closest-match metadata so palette id/name survives through `closestPaletteMatch`.
12+
- Added Palette Manager V2 display-name formatting for source palette ids, so `crayola008` is shown as `Crayola008` in harmony cards.
13+
- Updated harmony result construction:
14+
- Calculated mode now displays clean generated labels such as `Complementary - +180 deg` with `#00FFFF` on the next line.
15+
- Source Palette Closest Match now displays `<Palette Name> - <Swatch Name>` from the current source palette.
16+
- All Palettes Closest Match now displays `<Palette Name> - <Swatch Name>` from the matched palette across all loaded palettes.
17+
- Updated harmony cards to expose display metadata in DOM data attributes for validation and keep the hex value as the second line.
18+
- Updated targeted Playwright coverage to confirm palette name, swatch name, hex display, calculated mode labeling, and removal of `Closest ...` display text.
19+
20+
## Scope Notes
21+
22+
- Add Selected and Add All behavior is preserved.
23+
- Harmony calculation behavior is unchanged.
24+
- No sample JSON was touched.
25+
- No full samples smoke test was run, per PR instruction.
26+
27+
## Validation
28+
29+
Passed:
30+
31+
- `node --check tools/palette-manager-v2/modules/paletteHarmonyUtils.js; node --check tools/palette-manager-v2/controls/PaletteHarmonyControl.js; node --check tools/palette-manager-v2/modules/PaletteManagerApp.js; node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
32+
- `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "generates Palette Manager V2 harmony schemes"`
33+
- `npm run test:workspace-v2` (59 passed)
34+
- `git diff --check` (passed; Git reported line-ending normalization warning for the modified Playwright spec only)
35+
36+
## Manual Test Notes
37+
38+
1. Open Palette Manager V2.
39+
2. Select or add a swatch and open Color Harmony Schemes.
40+
3. In Calculated mode, confirm cards use a clear harmony label and show the hex on the next line.
41+
4. In Source Palette Closest Match mode, confirm cards show the current source palette name and matched swatch name, with the hex below.
42+
5. In All Palettes Closest Match mode, confirm cards show the matched palette name and matched swatch name, with the hex below.
43+
6. Confirm Add Selected and Add All still add colors without duplicate hex entries.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8104,6 +8104,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
81048104
await page.locator("#harmonyMatchSourceSelect").selectOption("calculated");
81058105
await page.locator("#harmonySchemeSelect").selectOption("complementary");
81068106
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");
81078109
await page.locator("#addSelectedHarmonyButton").click();
81088110
await expect(page.locator("#userPaletteCount")).toHaveText("2 user swatches");
81098111
await expect(page.locator("#paletteStatus")).toHaveText(/OK Added selected harmony color/);
@@ -8126,21 +8128,47 @@ test.describe("Workspace Manager V2 bootstrap", () => {
81268128
await page.locator("#harmonySchemeSelect").selectOption("complementary");
81278129
await page.locator("#harmonyMatchSourceSelect").selectOption("source-palette");
81288130
const sourceMatchState = await page.evaluate(() => {
8131+
const formatPaletteName = (value) => String(value || "").trim().replace(/^([a-z])/, (letter) => letter.toUpperCase());
81298132
const sourceId = document.querySelector("#sourcePaletteSelect").value;
8130-
const harmonyHex = document.querySelector("#harmonyColorList [data-harmony-index='0']").dataset.harmonyHex;
8131-
const sourceHexes = (window.paletteList.SOURCE_PALETTES[sourceId] || []).map((swatch) => swatch.hex.toUpperCase());
8132-
return { harmonyHex, isFromCurrentSource: sourceHexes.includes(harmonyHex) };
8133+
const harmonyButton = document.querySelector("#harmonyColorList [data-harmony-index='0']");
8134+
const harmonyHex = harmonyButton.dataset.harmonyHex;
8135+
const paletteName = harmonyButton.dataset.harmonyPalette;
8136+
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;
8139+
const expectedPaletteName = formatPaletteName(window.paletteList.SOURCE_PALETTE_LABELS[sourceId] || sourceId);
8140+
const sourceMatch = (window.paletteList.SOURCE_PALETTES[sourceId] || [])
8141+
.find((swatch) => swatch.hex.toUpperCase() === harmonyHex && swatch.name === swatchName);
8142+
return { expectedPaletteName, harmonyHex, isFromCurrentSource: Boolean(sourceMatch), label, meta, paletteName, swatchName };
81338143
});
81348144
expect(sourceMatchState.isFromCurrentSource).toBe(true);
8145+
expect(sourceMatchState.paletteName).toBe(sourceMatchState.expectedPaletteName);
8146+
expect(sourceMatchState.label).toBe(`${sourceMatchState.expectedPaletteName} - ${sourceMatchState.swatchName}`);
8147+
expect(sourceMatchState.label).not.toContain("Closest");
8148+
expect(sourceMatchState.meta).toBe(sourceMatchState.harmonyHex);
81358149

81368150
await page.locator("#harmonyMatchSourceSelect").selectOption("all-palettes");
81378151
const allMatchState = await page.evaluate(() => {
8138-
const harmonyHex = document.querySelector("#harmonyColorList [data-harmony-index='0']").dataset.harmonyHex;
8139-
const allHexes = Object.values(window.paletteList.SOURCE_PALETTES)
8140-
.flatMap((swatches) => swatches.map((swatch) => swatch.hex.toUpperCase()));
8141-
return { harmonyHex, isFromAnySource: allHexes.includes(harmonyHex) };
8152+
const formatPaletteName = (value) => String(value || "").trim().replace(/^([a-z])/, (letter) => letter.toUpperCase());
8153+
const harmonyButton = document.querySelector("#harmonyColorList [data-harmony-index='0']");
8154+
const harmonyHex = harmonyButton.dataset.harmonyHex;
8155+
const paletteName = harmonyButton.dataset.harmonyPalette;
8156+
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;
8159+
const allMatch = Object.entries(window.paletteList.SOURCE_PALETTES)
8160+
.flatMap(([sourceId, swatches]) => swatches.map((swatch) => ({ sourceId, swatch })))
8161+
.find(({ sourceId, swatch }) => {
8162+
return swatch.hex.toUpperCase() === harmonyHex
8163+
&& swatch.name === swatchName
8164+
&& formatPaletteName(window.paletteList.SOURCE_PALETTE_LABELS[sourceId] || sourceId) === paletteName;
8165+
});
8166+
return { harmonyHex, isFromAnySource: Boolean(allMatch), label, meta, paletteName, swatchName };
81428167
});
81438168
expect(allMatchState.isFromAnySource).toBe(true);
8169+
expect(allMatchState.label).toBe(`${allMatchState.paletteName} - ${allMatchState.swatchName}`);
8170+
expect(allMatchState.label).not.toContain("Closest");
8171+
expect(allMatchState.meta).toBe(allMatchState.harmonyHex);
81448172
expect(pageErrors).toEqual([]);
81458173
} finally {
81468174
await workspaceV2CoverageReporter.stop(page);

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,10 @@ export class PaletteHarmonyControl {
109109
button.setAttribute("aria-pressed", String(index === this.app.getSelectedHarmonyColorIndex()));
110110
button.dataset.harmonyIndex = String(index);
111111
button.dataset.harmonyHex = normalizeHex(color.hex);
112-
button.title = `${color.name}: ${normalizeHex(color.hex)}`;
112+
button.dataset.harmonyLabel = color.displayName || color.name || "";
113+
button.dataset.harmonyPalette = color.paletteName || "";
114+
button.dataset.harmonySwatchName = color.swatchName || "";
115+
button.title = `${color.displayName || color.name}: ${normalizeHex(color.hex)}`;
113116
button.addEventListener("click", () => {
114117
this.app.setSelectedHarmonyColorIndex(index);
115118
});
@@ -121,7 +124,7 @@ export class PaletteHarmonyControl {
121124

122125
const text = this.document.createElement("span");
123126
text.className = "palette-manager-v2__harmony-text";
124-
text.textContent = cleanSwatch.name || color.name || normalizeHex(color.hex);
127+
text.textContent = color.displayName || cleanSwatch.name || color.name || normalizeHex(color.hex);
125128

126129
const meta = this.document.createElement("span");
127130
meta.className = "palette-manager-v2__harmony-meta";

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

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ function getRgbHexKey(hex) {
118118
return normalizeHex(hex).slice(0, 7);
119119
}
120120

121+
function formatSourcePaletteName(value) {
122+
const cleanValue = sanitizeText(value);
123+
if (!cleanValue) {
124+
return "";
125+
}
126+
return cleanValue.replace(/^([a-z])/, (letter) => letter.toUpperCase());
127+
}
128+
121129
function formatDuplicateUserSwatchMessage(duplicateFields) {
122130
const parts = [];
123131
if (duplicateFields.symbol) {
@@ -538,22 +546,37 @@ export class PaletteManagerApp {
538546
}
539547

540548
getCurrentSourcePaletteSwatches() {
541-
return (this.sourcePalettes[this.state.sourcePaletteId] || [])
549+
const sourceId = this.state.sourcePaletteId;
550+
const paletteName = this.getHarmonySourcePaletteName(sourceId);
551+
return (this.sourcePalettes[sourceId] || [])
542552
.map((swatch) => cloneSwatch({
543553
...swatch,
544-
source: swatch.source || this.state.sourcePaletteId
554+
source: swatch.source || sourceId
555+
}))
556+
.map((swatch) => ({
557+
...swatch,
558+
paletteId: sourceId,
559+
paletteName
545560
}));
546561
}
547562

548563
getAllSourcePaletteSwatches() {
549564
return Object.entries(this.sourcePalettes)
550565
.flatMap(([sourceId, swatches]) => (Array.isArray(swatches) ? swatches : [])
551-
.map((swatch) => cloneSwatch({
552-
...swatch,
553-
source: swatch.source || sourceId
566+
.map((swatch) => ({
567+
...cloneSwatch({
568+
...swatch,
569+
source: swatch.source || sourceId
570+
}),
571+
paletteId: sourceId,
572+
paletteName: this.getHarmonySourcePaletteName(sourceId)
554573
})));
555574
}
556575

576+
getHarmonySourcePaletteName(sourceId) {
577+
return formatSourcePaletteName(this.getSourcePaletteLabel(sourceId) || sourceId);
578+
}
579+
557580
getHarmonyColors() {
558581
const selectedSwatch = this.getSelectedSwatch();
559582
if (!selectedSwatch || !this.hexColorPattern.test(normalizeHex(selectedSwatch.hex))) {
@@ -564,21 +587,31 @@ export class PaletteManagerApp {
564587
const matchPalette = matchSource === "source-palette"
565588
? this.getCurrentSourcePaletteSwatches()
566589
: (matchSource === "all-palettes" ? this.getAllSourcePaletteSwatches() : []);
567-
return calculatedColors.map((color, index) => {
590+
return calculatedColors.map((color) => {
568591
const matchedSwatch = matchPalette.length ? closestPaletteMatch(color.hex, matchPalette) : null;
569592
const cleanHex = normalizeHex(matchedSwatch?.hex || color.hex).slice(0, 7);
570593
const schemeLabel = findHarmonyScheme(this.state.harmonyScheme).label;
571-
const matchLabel = matchedSwatch ? `Closest ${matchedSwatch.name}` : color.label;
594+
const paletteName = matchedSwatch
595+
? (sanitizeText(matchedSwatch.paletteName) || this.getHarmonySourcePaletteName(matchedSwatch.paletteId || matchedSwatch.source))
596+
: "";
597+
const swatchName = sanitizeText(matchedSwatch?.name);
598+
const calculationLabel = sanitizeText(color.label);
599+
const displayName = matchedSwatch
600+
? `${paletteName} - ${swatchName}`
601+
: `${schemeLabel} - ${calculationLabel || cleanHex}`;
572602
return {
573603
baseHex: normalizeHex(color.hex).slice(0, 7),
574604
hex: cleanHex,
575-
name: `${schemeLabel} ${index + 1} - ${matchLabel}`,
605+
name: displayName,
606+
displayName,
607+
paletteName,
608+
swatchName,
576609
source: matchedSwatch?.source || findHarmonyMatchSource(this.state.harmonyMatchSource).label,
577610
swatch: cloneSwatch({
578611
symbol: "",
579612
hex: cleanHex,
580-
name: `${schemeLabel} ${index + 1} - ${matchLabel}`,
581-
source: matchedSwatch?.source || findHarmonyMatchSource(this.state.harmonyMatchSource).label,
613+
name: displayName,
614+
source: paletteName || findHarmonyMatchSource(this.state.harmonyMatchSource).label,
582615
tags: ["harmony"]
583616
})
584617
};

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,14 @@ export function closestPaletteMatch(targetHex, swatches) {
246246
+ ((target.g - rgb.g) ** 2)
247247
+ ((target.b - rgb.b) ** 2)
248248
);
249-
return { distance, swatch: cloneSwatch(swatch) };
249+
return {
250+
distance,
251+
swatch: {
252+
...cloneSwatch(swatch),
253+
paletteId: sanitizeText(swatch?.paletteId),
254+
paletteName: sanitizeText(swatch?.paletteName)
255+
}
256+
};
250257
})
251258
.filter(Boolean)
252259
.sort((left, right) => left.distance - right.distance)[0]?.swatch || null;

0 commit comments

Comments
 (0)