diff --git a/404.html b/404.html
index 8e81b21d3..ef05c70bb 100644
--- a/404.html
+++ b/404.html
@@ -105,7 +105,6 @@
['dk', '/'],
['it', '/'],
['br', '/'],
- ['cn', '/'],
['de', '/'],
['es', '/'],
['fi', '/'],
@@ -119,7 +118,6 @@
['vn_en', '/'],
['vn_vi', '/'],
['za', '/'],
- ['zh-Hans-CN', '/cn/' ],
['zh-Hant-TW', '/tw/'],
]
);
diff --git a/express/code/blocks/color-blindness/color-blindness.js b/express/code/blocks/color-blindness/color-blindness.js
index 116a660ae..13b6c4fb2 100644
--- a/express/code/blocks/color-blindness/color-blindness.js
+++ b/express/code/blocks/color-blindness/color-blindness.js
@@ -86,6 +86,7 @@ export default async function decorate(block) {
layoutInstance = await createColorToolLayout(section, {
palette: initialPalette,
toolbar: {
+ daaLh: 'color-blindness',
variant: 'sticky-on-scroll',
showEdit: false,
showPaletteName: true,
@@ -101,6 +102,7 @@ export default async function decorate(block) {
activeId: 'color-blindness',
navLinks,
controls,
+ daaLh: 'color-blindness',
},
});
@@ -237,6 +239,7 @@ export default async function decorate(block) {
type: 'controls-only',
controls,
enableState: false,
+ daaLh: 'color-blindness',
onUndo: () => fullMenuEl?.querySelector('.undo-btn')?.click(),
onRedo: () => fullMenuEl?.querySelector('.redo-btn')?.click(),
});
diff --git a/express/code/blocks/color-contrast-checker/color-contrast-checker.js b/express/code/blocks/color-contrast-checker/color-contrast-checker.js
index 6268dd403..ca6fd05ea 100644
--- a/express/code/blocks/color-contrast-checker/color-contrast-checker.js
+++ b/express/code/blocks/color-contrast-checker/color-contrast-checker.js
@@ -167,6 +167,7 @@ export default async function decorate(block) {
name: initialPalette.name,
},
toolbar: {
+ daaLh: 'color-contrast-checker',
variant: 'sticky-on-scroll',
showEdit: false,
showPaletteName: true,
diff --git a/express/code/blocks/color-contrast-checker/utils/contrastConstants.js b/express/code/blocks/color-contrast-checker/utils/contrastConstants.js
index 02993a93b..f9e058239 100644
--- a/express/code/blocks/color-contrast-checker/utils/contrastConstants.js
+++ b/express/code/blocks/color-contrast-checker/utils/contrastConstants.js
@@ -17,6 +17,7 @@ export const WCAG_THRESHOLDS = {
export function createDefaultActionMenuConfig(placeholders = {}) {
return {
+ daaLh: 'color-contrast-checker',
navLinks: [
{ id: 'palette', href: '/create/color-wheel', label: placeholders.colorPaletteLabel || 'Color palette' },
{ id: 'contrast', href: '/create/color-contrast-analyzer', label: placeholders.contrastCheckerLabel || 'Contrast checker' },
diff --git a/express/code/blocks/color-extract/helpers/toolbar.js b/express/code/blocks/color-extract/helpers/toolbar.js
index 66d163fa4..8151349ee 100644
--- a/express/code/blocks/color-extract/helpers/toolbar.js
+++ b/express/code/blocks/color-extract/helpers/toolbar.js
@@ -1,6 +1,7 @@
import { createTag, getLibs } from '../../../scripts/utils.js';
import { createExpressTooltip } from '../../../scripts/color-shared/spectrum/components/express-tooltip.js';
import { UNDO_ICON, REDO_ICON } from '../../../scripts/color-shared/components/actionMenuIcons.js';
+import { decorateAnalyticsAttributes } from '../../../scripts/color-shared/utils/utilities.js';
/* S2 icons from Figma — Extract-toolbar (node 3582:110610) */
const ICON_EYEDROPPER = '';
@@ -120,6 +121,8 @@ export default async function createToolbar(options = {}) {
const undoBtn = await createIconButton(UNDO_ICON, 'Undo', options.onUndo);
const redoBtn = await createIconButton(REDO_ICON, 'Redo', options.onRedo);
+ decorateAnalyticsAttributes(undoBtn, { linkLabel: 'Undo' });
+ decorateAnalyticsAttributes(redoBtn, { linkLabel: 'Redo' });
undoBtn.setAttribute('aria-disabled', 'true');
redoBtn.setAttribute('aria-disabled', 'true');
historyGroup.append(undoBtn, redoBtn);
diff --git a/express/code/blocks/color-search-marquee/color-search-marquee.js b/express/code/blocks/color-search-marquee/color-search-marquee.js
index 0d26615b4..1c52613d8 100644
--- a/express/code/blocks/color-search-marquee/color-search-marquee.js
+++ b/express/code/blocks/color-search-marquee/color-search-marquee.js
@@ -135,7 +135,12 @@ export function setupStickyBounds(block, searchBar) {
endSentinel.dataset.searchBarEndSentinel = 'true';
endSentinel.setAttribute('aria-hidden', 'true');
endSentinel.style.cssText = 'display:block;inline-size:1px;block-size:1px;';
- colorExploreBlock.after(endSentinel);
+ const footer = document.querySelector('footer');
+ if (footer) {
+ footer.before(endSentinel);
+ } else {
+ colorExploreBlock.after(endSentinel);
+ }
let stickyEnabled = true;
diff --git a/express/code/blocks/color-wheel/color-wheel.js b/express/code/blocks/color-wheel/color-wheel.js
index 9291740a5..29dd974ab 100644
--- a/express/code/blocks/color-wheel/color-wheel.js
+++ b/express/code/blocks/color-wheel/color-wheel.js
@@ -213,6 +213,7 @@ function buildDefaultActionMenuConfig(strings) {
return {
id: ACTION_MENU_ID,
activeId: 'palette',
+ daaLh: 'color-wheel',
navLinks: [
{ id: 'palette', label: strings.createPalette, href: '/create/color-wheel' },
{ id: 'contrast', label: strings.contrastChecker, href: '/create/color-contrast-analyzer' },
@@ -234,6 +235,7 @@ let primaryColorAdapter = null;
let sidebarNaturalWidth = 0;
let sidebarTransitionCleanup = null;
let historyCleanup = null;
+let currentInitToken = 0;
function swatchHexListFromState(state) {
const swatches = state?.swatches || [];
@@ -475,7 +477,8 @@ function buildPrimaryColorContent(controller) {
primaryColorAdapter = null;
const state = controller.getState();
- const baseColor = swatchHexListFromState(state)[0];
+ const baseColorIndex = state.baseColorIndex ?? 0;
+ const baseColor = state.swatches?.[baseColorIndex]?.hex || '#FF0000';
const adapter = createBaseColorAdapter(
baseColor,
'HEX',
@@ -483,7 +486,6 @@ function buildPrimaryColorContent(controller) {
onColorChange: (detail) => {
if (!detail?.hex) return;
controller.setBaseColor(detail.hex);
- controller.setSwatchHex(0, detail.hex);
},
onColorChangeEnd: () => {
// eslint-disable-next-line no-underscore-dangle
@@ -493,10 +495,11 @@ function buildPrimaryColorContent(controller) {
const locked = detail?.locked;
const current = swatchRailController?.getState?.()?.lockedByIndex || new Set();
const next = new Set(current);
+ const currentBaseIndex = controller.getState().baseColorIndex ?? 0;
if (locked) {
- next.add(0);
+ next.add(currentBaseIndex);
} else {
- next.delete(0);
+ next.delete(currentBaseIndex);
}
swatchRailController?.setState?.({ lockedByIndex: next });
},
@@ -614,7 +617,12 @@ function createSwatchRailControllerBridge(controller) {
} else {
incoming = [];
}
+ const prevSize = lockedByIndex.size;
lockedByIndex = new Set(incoming.filter((index) => Number.isInteger(index) && index >= 0));
+ const lockAdded = lockedByIndex.size > prevSize;
+ if (lockAdded && controller.getState().harmonyRule !== 'CUSTOM') {
+ controller.setHarmonyRule('CUSTOM');
+ }
}
const current = controller.getState();
@@ -774,6 +782,11 @@ export default async function decorate(block) {
}
block.className = 'color-wheel';
+ // Each init() call claims a token. After every await, bail if a newer call has started.
+ // This prevents a stale concurrent init from appending duplicate tabs/layout to the DOM.
+ currentInitToken += 1;
+ const myToken = currentInitToken;
+
try {
const [strings, { getResolvedPalette, getResolvedPaletteName }] = await Promise.all([
loadPlaceholders(),
@@ -781,6 +794,8 @@ export default async function decorate(block) {
loadHeavyModules(),
]);
+ if (myToken !== currentInitToken) return;
+
// First load: authored content was preserved during the async wait; clear it now
if (!isReinit) {
cleanup();
@@ -809,6 +824,7 @@ export default async function decorate(block) {
layoutInstance = await createColorToolLayout(section, {
palette: initialPalette,
toolbar: {
+ daaLh: 'color-wheel',
variant: 'sticky-on-scroll',
showEdit: false,
showPalette: true,
@@ -830,6 +846,7 @@ export default async function decorate(block) {
// If no HISTORY_EVENT fires (e.g. all colors locked, palette unchanged),
// reset the flag so it doesn't corrupt the next undo/redo
queueMicrotask(() => { isGeneratingRandom = false; });
+ primaryColorAdapter?.element?.resetOriginalColor?.();
},
transformPalette: makeTransformPalette(
() => activeHarmonyRule,
@@ -878,6 +895,8 @@ export default async function decorate(block) {
},
});
+ if (myToken !== currentInitToken) return;
+
const stripHost = createTag('div', { class: 'color-wheel-strip-host' });
layoutInstance.slots.canvas.appendChild(stripHost);
@@ -885,10 +904,8 @@ export default async function decorate(block) {
const updateBaseColorBadge = () => {
const hide = activeTab !== 'color-wheel' || activeHarmonyRule === 'CUSTOM';
- const hideLock = activeTab === 'color-wheel' && activeHarmonyRule !== 'CUSTOM';
stripHost.querySelectorAll('color-swatch-rail').forEach((rail) => {
rail.hideBaseColorBadge = hide;
- rail.hideLock = hideLock;
});
};
@@ -907,6 +924,8 @@ export default async function decorate(block) {
}),
]);
+ if (myToken !== currentInitToken) return;
+
// Both resolved — wire up action menu history and append tabs
const actionMenuApi = layoutInstance.actionMenu;
let restoringFromHistory = false;
@@ -984,6 +1003,7 @@ export default async function decorate(block) {
onGenerateRandom: () => {
isGeneratingRandom = true;
queueMicrotask(() => { isGeneratingRandom = false; });
+ primaryColorAdapter?.element?.resetOriginalColor?.();
},
transformPalette: makeTransformPalette(
() => activeHarmonyRule,
@@ -1031,8 +1051,12 @@ export default async function decorate(block) {
const badgeRuleUnsubscribe = controller.subscribe((state) => {
const rule = state.harmonyRule || 'CUSTOM';
if (rule !== activeHarmonyRule) {
+ const wasCustom = activeHarmonyRule === 'CUSTOM';
activeHarmonyRule = rule;
updateBaseColorBadge();
+ if (wasCustom && rule !== 'CUSTOM') {
+ swatchRailController?.setState?.({ lockedByIndex: new Set() });
+ }
}
});
const prevHistoryCleanup = historyCleanup;
@@ -1070,11 +1094,12 @@ export default async function decorate(block) {
paletteUnsubscribe = controller.subscribe((state) => {
currentPalette = paletteFromThemeState(state);
layoutInstance?.context?.set('palette', currentPalette);
- const firstHex = swatchHexListFromState(state)[0];
+ const baseIdx = state.baseColorIndex ?? 0;
+ const baseHex = state.swatches?.[baseIdx]?.hex;
const currentColor = primaryColorAdapter?.element?.color;
- if (primaryColorAdapter?.setColor && firstHex
- && String(currentColor).toUpperCase() !== String(firstHex).toUpperCase()) {
- primaryColorAdapter.setColor(firstHex);
+ if (primaryColorAdapter?.setColor && baseHex
+ && String(currentColor).toUpperCase() !== String(baseHex).toUpperCase()) {
+ primaryColorAdapter.setColor(baseHex);
}
});
diff --git a/express/code/libs/color-components/components/color-swatch-rail/index.js b/express/code/libs/color-components/components/color-swatch-rail/index.js
index 50cc0cae2..d80ddafa2 100644
--- a/express/code/libs/color-components/components/color-swatch-rail/index.js
+++ b/express/code/libs/color-components/components/color-swatch-rail/index.js
@@ -1324,7 +1324,7 @@ export class ColorSwatchRail extends LitElement {
const stackedContent = html`
${showEdit ? html` this._onNativePickerChange(index, ev)} @change=${() => this._markNativePickerClosedSoon(50)} @blur=${() => this._markNativePickerClosedSoon(50)} />` : ''}
- ${f.hexCode ? ((showEdit || f.copyFromHex) ? html`` : html`${swatch.hex}`) : ''}
+ ${f.hexCode ? ((showEdit || f.copyFromHex) ? html`` : html`${swatch.hex}`) : ''}
${stackedIcons}
`;
diff --git a/express/code/libs/color-components/components/color-swatch-rail/styles.css.js b/express/code/libs/color-components/components/color-swatch-rail/styles.css.js
index 001f49b27..971108d7c 100644
--- a/express/code/libs/color-components/components/color-swatch-rail/styles.css.js
+++ b/express/code/libs/color-components/components/color-swatch-rail/styles.css.js
@@ -1021,11 +1021,17 @@ export const style = css`
height: 32px;
border-radius: var(--Corner-radius-corner-radius-100);
}
- .swatch-column[data-contrast="dark"] button.hex-code:hover,
+ @media (hover: hover) {
+ .swatch-column[data-contrast="dark"] button.hex-code:hover {
+ background-color: rgba(255, 255, 255, 0.12);
+ }
+ .swatch-column[data-contrast="light"] button.hex-code:hover {
+ background-color: rgba(0, 0, 0, 0.12);
+ }
+ }
.swatch-column[data-contrast="dark"] button.hex-code.hex-code--editor-open {
background-color: rgba(255, 255, 255, 0.12);
}
- .swatch-column[data-contrast="light"] button.hex-code:hover,
.swatch-column[data-contrast="light"] button.hex-code.hex-code--editor-open {
background-color: rgba(0, 0, 0, 0.12);
}
@@ -1063,11 +1069,13 @@ export const style = css`
- .swatch-column[data-contrast="dark"] .icon-button:hover {
- background-color: rgba(255, 255, 255, 0.12);
- }
- .swatch-column[data-contrast="light"] .icon-button:hover {
- background-color: rgba(0, 0, 0, 0.12);
+ @media (hover: hover) {
+ .swatch-column[data-contrast="dark"] .icon-button:hover {
+ background-color: rgba(255, 255, 255, 0.12);
+ }
+ .swatch-column[data-contrast="light"] .icon-button:hover {
+ background-color: rgba(0, 0, 0, 0.12);
+ }
}
diff --git a/express/code/scripts/color-shared/components/base-color/index.js b/express/code/scripts/color-shared/components/base-color/index.js
index 06b7dbb85..acff4a7e2 100644
--- a/express/code/scripts/color-shared/components/base-color/index.js
+++ b/express/code/scripts/color-shared/components/base-color/index.js
@@ -76,6 +76,7 @@ class BaseColor extends LitElement {
this._originalSaturation = 0;
this._originalBrightness = 0;
this._pickerInputEnabled = false;
+ this._keyboardActive = false;
}
get _rgb() {
@@ -463,10 +464,12 @@ class BaseColor extends LitElement {
_onPointerDown(e) {
this._lastPointerType = e.pointerType;
this._pickerInputEnabled = true;
+ this._keyboardActive = false;
}
_onPickerKeyDown() {
this._pickerInputEnabled = true;
+ this._keyboardActive = true;
}
_blurOnTouch(target) {
@@ -479,7 +482,7 @@ class BaseColor extends LitElement {
// --- Color area (Saturation/Brightness) ---
_snapColorAreaToOriginal(area, saturation, brightness) {
- if (!this._hasOriginal) return { saturation, brightness };
+ if (!this._hasOriginal || this._keyboardActive) return { saturation, brightness };
const ds = Math.abs(saturation - this._originalSaturation);
const db = Math.abs(brightness - this._originalBrightness);
if (ds > ORIGINAL_COLOR_AREA_SNAP_PCT || db > ORIGINAL_COLOR_AREA_SNAP_PCT) {
@@ -524,7 +527,7 @@ class BaseColor extends LitElement {
if (slider.value == null) return;
let hue = slider.value;
- if (this._hasOriginal) {
+ if (this._hasOriginal && !this._keyboardActive) {
const diff = Math.abs(hue - this._originalHue);
const wrappedDiff = Math.min(diff, 360 - diff);
if (wrappedDiff <= ORIGINAL_COLOR_HUE_SNAP_DEG) {
diff --git a/express/code/scripts/color-shared/components/color-wheel-express/index.js b/express/code/scripts/color-shared/components/color-wheel-express/index.js
index 2ceba8d29..3c42f971a 100644
--- a/express/code/scripts/color-shared/components/color-wheel-express/index.js
+++ b/express/code/scripts/color-shared/components/color-wheel-express/index.js
@@ -388,9 +388,10 @@ export class ColorWheelExpress extends ColorWheel {
this.getCanvasPosition();
this._dragIndex = index;
- const markerV = this.swatches[index]?.hsv?.v != null
+ const rawV = this.swatches[index]?.hsv?.v != null
? Number(this.swatches[index].hsv.v)
: this.wheelBrightness;
+ const markerV = rawV > 0 ? rawV : this.wheelBrightness;
const moveHandler = (e) => {
this._dragFixedBrightness = markerV;
@@ -441,10 +442,10 @@ export class ColorWheelExpress extends ColorWheel {
this.paint();
}
- if (this.swatches.length > 0 && active?.hsv != null) {
+ if (this.swatches.length > 0 && active?.hsv != null && this._dragIndex < 0) {
const activeV = Math.round(Number(active.hsv.v) ?? 100);
const v = Math.min(100, Math.max(0, activeV));
- if (this.wheelBrightness !== v) {
+ if (v > 0 && this.wheelBrightness !== v) {
this.wheelBrightness = v;
this.generateColorWheel();
}
diff --git a/express/code/scripts/color-shared/components/createActionMenuComponent.js b/express/code/scripts/color-shared/components/createActionMenuComponent.js
index a07677f24..d6a219a69 100644
--- a/express/code/scripts/color-shared/components/createActionMenuComponent.js
+++ b/express/code/scripts/color-shared/components/createActionMenuComponent.js
@@ -301,6 +301,7 @@ export async function createActionMenuComponent(options = {}) {
transformPalette,
getName,
enableState = true,
+ daaLh = null,
} = options;
if (!TYPES.includes(type)) {
@@ -340,6 +341,7 @@ export async function createActionMenuComponent(options = {}) {
}
const container = createTag('div', { class: `action-menu-${type}` });
+ if (daaLh) container.setAttribute('daa-lh', daaLh);
const buttonRefs = {};
const sections = [];
diff --git a/express/code/scripts/color-shared/modal/createGradientModalContent.js b/express/code/scripts/color-shared/modal/createGradientModalContent.js
index 15867d6db..f3a481272 100644
--- a/express/code/scripts/color-shared/modal/createGradientModalContent.js
+++ b/express/code/scripts/color-shared/modal/createGradientModalContent.js
@@ -84,7 +84,7 @@ export function createGradientModalContent(gradient, opts = {}) {
?? gradient?.creatorImageUrl ?? defaultCreatorImageUrl;
const tags = opts.tags || ['Color', 'Gradient'];
- const main = createTag('main', { class: 'modal-content' });
+ const main = createTag('main', { class: 'modal-content', 'daa-lh': 'color-gradient-modal' });
const containerSection = createTag('section', {
class: 'modal-palette-container',
@@ -156,10 +156,12 @@ export function createGradientModalContent(gradient, opts = {}) {
const paletteForToolbar = {
id: gradient?.id ?? '',
name: gradient?.name ?? 'Gradient',
+ angle: angle || 90,
colors: colorStops.map((s) => s.color),
};
initFloatingToolbar(toolbarMount, {
+ type: 'gradient',
palette: paletteForToolbar,
ctaText: 'Create with color palette',
showPaletteName: false,
diff --git a/express/code/scripts/color-shared/modal/createPaletteModalContent.js b/express/code/scripts/color-shared/modal/createPaletteModalContent.js
index e344ba5f7..a192b292f 100644
--- a/express/code/scripts/color-shared/modal/createPaletteModalContent.js
+++ b/express/code/scripts/color-shared/modal/createPaletteModalContent.js
@@ -367,7 +367,7 @@ export function createPaletteSwatchesModalContent(palette, options = {}) {
};
const colorCount = normalizedPalette.colors.length;
- const root = createTag('main', { class: 'modal-content' });
+ const root = createTag('main', { class: 'modal-content', 'daa-lh': 'color-palette-modal' });
const railSection = createTag('section', {
class: 'modal-palette-container modal-palette-container--color-rail',
diff --git a/express/code/scripts/color-shared/renderers/createStripContainerRenderer.js b/express/code/scripts/color-shared/renderers/createStripContainerRenderer.js
index 30b7d98a9..086695d44 100644
--- a/express/code/scripts/color-shared/renderers/createStripContainerRenderer.js
+++ b/express/code/scripts/color-shared/renderers/createStripContainerRenderer.js
@@ -567,6 +567,7 @@ export function createStripContainerRenderer(options) {
let listElement = null;
let activeColorEditor = null;
+ let isEditorOpening = false;
const cleanupHandlers = [];
function resolveAnchorRect(anchorElement, anchorRectFromDetail) {
@@ -576,15 +577,29 @@ export function createStripContainerRenderer(options) {
return anchorElement.getBoundingClientRect();
}
+ function getStickyHeaderBottom() {
+ const selectors = ['header.global-navigation', '.feds-localnav'];
+ return selectors.reduce((max, sel) => {
+ const el = document.querySelector(sel);
+ if (!el) return max;
+ return Math.max(max, el.getBoundingClientRect().bottom);
+ }, 0);
+ }
+
function positionPopover(popover, anchorRect, container) {
const gap = 4;
const popRect = popover.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
- let viewportTop = anchorRect.bottom + gap;
- if (viewportTop + popRect.height > window.innerHeight) {
- viewportTop = anchorRect.top - popRect.height - gap;
- }
+ const belowTop = anchorRect.bottom + gap;
+ const aboveTop = anchorRect.top - popRect.height - gap;
+ const headerBottom = getStickyHeaderBottom();
+
+ const fitsBelow = belowTop + popRect.height <= window.innerHeight;
+ const fitsAbove = aboveTop >= headerBottom;
+
+ let viewportTop = belowTop;
+ if (!fitsBelow && fitsAbove) viewportTop = aboveTop;
let viewportLeft = anchorRect.left;
if (viewportLeft + popRect.width > window.innerWidth - gap) {
@@ -601,29 +616,36 @@ export function createStripContainerRenderer(options) {
const {
adapter,
popover,
- mobile,
resizeObserver,
outsideHandler,
escapeHandler,
railElement: activeRailElement,
+ selectedIndex: editedIndex,
} = activeColorEditor;
+ activeColorEditor = null;
if (outsideHandler) {
document.removeEventListener('click', outsideHandler, true);
document.removeEventListener('touchend', outsideHandler, true);
}
if (escapeHandler) document.removeEventListener('keydown', escapeHandler, true);
resizeObserver?.disconnect?.();
- if (mobile) {
- try {
- adapter.hide?.();
- } catch (_err) {
- // no-op
- }
+ try {
+ adapter.hide?.();
+ } catch (_err) {
+ // no-op
}
adapter.destroy?.();
popover?.remove();
activeRailElement?.setActiveEditIndex?.(null);
- activeColorEditor = null;
+ requestAnimationFrame(() => {
+ const column = activeRailElement?.shadowRoot?.querySelector(
+ `.swatch-column[data-swatch-index="${editedIndex}"]`,
+ );
+ if (column) {
+ column.setAttribute('tabindex', '0');
+ column.focus();
+ }
+ });
}
function openColorEditorForRail(
@@ -636,8 +658,10 @@ export function createStripContainerRenderer(options) {
const alreadyOpen = activeColorEditor?.railElement === railElement
&& activeColorEditor?.selectedIndex === selectedIndex;
closeActiveColorEditor();
+ isEditorOpening = true;
if (alreadyOpen) return;
onEditOpen?.(selectedIndex);
+ isEditorOpening = false;
const state = controller?.getState?.() || {};
const palette = (state.swatches || []).map((swatch) => swatch?.hex).filter(Boolean);
@@ -678,7 +702,7 @@ export function createStripContainerRenderer(options) {
const editorElement = adapter.getElement?.() || adapter.element;
if (mobile) {
document.body.appendChild(editorElement);
- activeColorEditor = { adapter, mobile: true, railElement };
+ activeColorEditor = { adapter, mobile: true, railElement, selectedIndex };
requestAnimationFrame(async () => {
try {
await customElements.whenDefined('color-edit');
@@ -700,7 +724,8 @@ export function createStripContainerRenderer(options) {
popover.style.position = 'absolute';
popover.style.top = '0';
popover.style.left = '0';
- popover.style.visibility = 'hidden';
+ popover.style.opacity = '0';
+ popover.style.pointerEvents = 'none';
popover.style.zIndex = '2';
popover.appendChild(editorElement);
container.appendChild(popover);
@@ -709,7 +734,8 @@ export function createStripContainerRenderer(options) {
const { height } = entries[0].contentRect;
if (height > 0) {
positionPopover(popover, anchorRect, container);
- popover.style.visibility = 'visible';
+ popover.style.opacity = '1';
+ popover.style.pointerEvents = '';
}
});
observer.observe(popover);
@@ -723,7 +749,10 @@ export function createStripContainerRenderer(options) {
closeActiveColorEditor();
};
const escapeHandler = (evt) => {
- if (evt.key === 'Escape') closeActiveColorEditor();
+ if (evt.key === 'Escape') {
+ evt.stopPropagation();
+ closeActiveColorEditor();
+ }
};
document.addEventListener('click', outsideHandler, true);
@@ -823,7 +852,7 @@ export function createStripContainerRenderer(options) {
}
function update(newData) {
- if (!listElement) return;
+ if (!listElement || isEditorOpening) return;
closeActiveColorEditor();
cleanupHandlers.splice(0).forEach((fn) => fn());
listElement.innerHTML = '';
diff --git a/express/code/scripts/color-shared/shell/layouts/createColorToolLayout.js b/express/code/scripts/color-shared/shell/layouts/createColorToolLayout.js
index 5a1ef425f..8525393b8 100644
--- a/express/code/scripts/color-shared/shell/layouts/createColorToolLayout.js
+++ b/express/code/scripts/color-shared/shell/layouts/createColorToolLayout.js
@@ -160,6 +160,7 @@ async function mountActionMenu(topbarSlot, actionMenuConfig, modulePromise) {
transformPalette: actionMenuConfig.transformPalette,
getName: actionMenuConfig.getName,
enableState: actionMenuConfig.enableState !== false,
+ daaLh: actionMenuConfig.daaLh,
});
if (actionMenu?.element) {
diff --git a/express/code/scripts/color-shared/toolbar/createFloatingToolbar.js b/express/code/scripts/color-shared/toolbar/createFloatingToolbar.js
index d282961f2..71360ecce 100644
--- a/express/code/scripts/color-shared/toolbar/createFloatingToolbar.js
+++ b/express/code/scripts/color-shared/toolbar/createFloatingToolbar.js
@@ -220,6 +220,7 @@ export async function initFloatingToolbar(container, options = {}) {
standaloneAppearance = 'standalone',
palette: providedPalette = null,
deps = {},
+ daaLh = null,
} = options;
// 'raised' gives sticky visuals (band, shadow) without sticky positioning
@@ -232,6 +233,7 @@ export async function initFloatingToolbar(container, options = {}) {
if (!finalPalette) return null;
const wrapper = createTag('div', { class: 'color-floating-toolbar-container' });
+ if (daaLh) wrapper.setAttribute('daa-lh', daaLh);
const toolbar = createToolbar({
palette: finalPalette,
type,
@@ -361,6 +363,7 @@ export async function initFloatingToolbar(container, options = {}) {
const isVisible = entries[0].isIntersecting || entries[0].intersectionRatio > 0;
wrapper.classList.toggle('ax-toolbar-footer-hidden', isVisible);
if (isVisible) {
+ toolbar.closeDrawer?.();
wrapper.setAttribute('aria-hidden', 'true');
wrapper.setAttribute('inert', '');
} else {
diff --git a/express/code/scripts/color-shared/toolbar/createToolbarComponent.js b/express/code/scripts/color-shared/toolbar/createToolbarComponent.js
index 9ff7e5ad9..0b66c2f49 100644
--- a/express/code/scripts/color-shared/toolbar/createToolbarComponent.js
+++ b/express/code/scripts/color-shared/toolbar/createToolbarComponent.js
@@ -324,7 +324,7 @@ function buildCTAButton(getCTAText, onClick) {
ctaBtn.setAttribute('size', 'l');
ctaBtn.textContent = getCTAText();
ctaBtn.addEventListener('click', onClick);
- decorateAnalyticsAttributes(ctaBtn, { linkLabel: 'CTA' });
+ decorateAnalyticsAttributes(ctaBtn, { linkLabel: 'Create-with-palette-CTA' });
return ctaBtn;
}
@@ -580,6 +580,12 @@ export function createToolbar(options) {
nameInput.value = newName;
}
},
+ closeDrawer() {
+ if (activeDrawer?.isOpen) {
+ activeDrawer.close();
+ activeDrawer = null;
+ }
+ },
setVariant(nextVariant = 'standalone') {
const resolvedVariant = nextVariant === 'sticky' ? 'sticky' : 'standalone';
if (resolvedVariant === currentVariant) return;
diff --git a/express/code/scripts/color-shared/utils/harmony/HarmonyEngineExpress.js b/express/code/scripts/color-shared/utils/harmony/HarmonyEngineExpress.js
index 19c171db0..c39717294 100644
--- a/express/code/scripts/color-shared/utils/harmony/HarmonyEngineExpress.js
+++ b/express/code/scripts/color-shared/utils/harmony/HarmonyEngineExpress.js
@@ -28,6 +28,19 @@ import { hsvToAllSpacesDenormalized, valuesToAllSpaces } from '../../../../libs/
const MAX_NUMBER_OF_SWATCHES = 10;
+const MIN_SWATCHES_FOR_EACH_HARMONY = {
+ ANALOGOUS: 3,
+ COMPLEMENTARY: 2,
+ TRIAD: 3,
+ SQUARE: 4,
+ MONOCHROMATIC: 2,
+ SPLIT_COMPLEMENTARY: 3,
+ DOUBLE_SPLIT_COMPLEMENTARY: 4,
+ COMPOUND: 4,
+ SHADES: 2,
+ CUSTOM: 2,
+};
+
const polarPointCanonicalAngle0To360 = function (deg) {
const a = deg % 360;
@@ -1172,7 +1185,7 @@ Rule.COMPLEMENTARY = function () {
const _super = Rule;
_super.call(this);
- this._fName = 'COMPLEMENTARY 2';
+ this._fName = 'COMPLEMENTARY';
this.addSchemeToFormulaImpl = function (harmonyFormula) {
const colorScheme = new ColorScheme().setTo(30.0);
@@ -1185,41 +1198,31 @@ Rule.COMPLEMENTARY = function () {
return colorScheme;
};
- this.setSchemeToRuleImpl = function (colorScheme, regionLength, showTints) {
+ this.setSchemeToRuleImpl = function (colorScheme, regionLength) {
colorScheme.setRegionsToBaseOnly(180.0);
+ this._fRegionLength = regionLength;
- this.addDependentRegions(colorScheme);
+ this.addDependentRegions(colorScheme, regionLength);
};
- this.addDependentRegions = function (colorScheme) {
- // DA dR dH linkA theta fromC linkR linkH
- let newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 0.0, 0.1, -0.3, true, 0.0, false, false, false);
- // Darker shade of base
+ this.addDependentRegions = function (colorScheme, regionLength) {
+ let zeroAngle = 0, complementaryAngle = 180, hueRegions = MIN_SWATCHES_FOR_EACH_HARMONY.COMPLEMENTARY,
+ numOfColors = regionLength,
+ numColorsEachHueSide = Math.ceil(numOfColors / hueRegions),
+ zeroRadius = -parseFloat(1 / (numColorsEachHueSide + 1)), dZeroRadius = zeroRadius,
+ complementaryRadius = 0, dComplementaryRadius = -parseFloat(1 / numColorsEachHueSide),
+ totalRegions = 0;
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
- // Flips to lighter if base is dark
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 0.0, -0.1, 0.3, true, 0.0, false, false, false);
- // Lighter tint of base
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowScrunch));
- // Scrunches if base is light
- // (We don't want it to reflect
- // Because we already have a -0.3
- // Region that it would nearly
- // Coincide with.)
-
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 180.0, 0.2, -0.3, true, 0.0, false, false, false);
- // Darkened complement
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
- // Flips to lighter if base is dark
+ for (let region = 0; region <= numColorsEachHueSide; region++) {
+ totalRegions = addRegions(colorScheme, complementaryAngle, complementaryRadius, complementaryRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
- new RelativeColorRegion().setColorScheme5(colorScheme, 180.0, 0.0, 0.0, true, 0.0, false, false, false);
- // Pure complement
+ totalRegions = addRegions(colorScheme, zeroAngle, zeroRadius, zeroRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
- /*
- newRegion = new RelativeColorRegion().setColorScheme5( colorScheme, 170.0, -0.1, 0.2, true, 0.0, false, false, false );// lightened off-complement
- newRegion.setOnRadiusOverflow( new OverflowResponse( kOverflowNegate ) );
- newRegion.setOnHeightOverflow( new OverflowResponse( kOverflowNegate ) );
- */
+ zeroRadius += dZeroRadius;
+ complementaryRadius += dComplementaryRadius;
+ }
// Record which rule created the scheme. This must be done after all the regions are defined.
colorScheme.setCreatingRule(this._fName);
};
@@ -1233,49 +1236,8 @@ Rule.COMPOUND = function () {
const _super = Rule;
_super.call(this);
- this._fName = 'COMPOUND1';
+ this._fName = 'COMPOUND';
- this.addDependentRegions = function (colorScheme) {
- let newRegion;
-
- // DA dR dH linkA theta fromC linkR linkH
- // Clockwise hue shift, slightly brighter
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 30.0, 0.1, 0.2, true, 0.0, false, false, false);
- // Flips to less saturated if base is bright
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
- // Flips to darker if base is light
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
-
- // Lighter tint of clockwise hue
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 30.0, -0.4, 0.4, true, 0.0, false, false, false);
- // Flips to more saturated if base is desaturated
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
- // Flips to darker if base is light
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
-
- // Off-complement, desaturated
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 165, -0.25, 0.05, true, 0.0, false, false, false);
- // Flips to more saturated if base is desaturated
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
-
- // Off-complement, slightly brighter
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 150, 0.1, 0.2, true, 0.0, false, false, false);
- // Flips to less saturated if base is bright
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
- // Flips to darker if base is light
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
-
- /*
- // lighter tint of off-complement
- newRegion = new RelativeColorRegion().setColorScheme5( colorScheme, 150, -0.4, 0.4, true, 0.0, false, false, false );
- // flips to more saturated if base is desaturated
- newRegion.setOnRadiusOverflow( new OverflowResponse( kOverflowNegate ) );
- // flips to darker if base is light
- newRegion.setOnHeightOverflow( new OverflowResponse( kOverflowNegate ) );
- */
- // Record which rule created the scheme. This must be done after all the regions are defined.
- colorScheme.setCreatingRule(this._fName);
- };
this.addSchemeToFormulaImpl = function (harmonyFormula) {
const colorScheme = new ColorScheme().setTo(0);
@@ -1289,11 +1251,45 @@ Rule.COMPOUND = function () {
return colorScheme;
};
- this.setSchemeToRuleImpl = function (colorScheme, regionLength, showTints) {
- colorScheme.setRegionsToBaseOnly(30.0);
+ this.setSchemeToRuleImpl = function (colorScheme, regionLength) {
+ colorScheme.setRegionsToBaseOnly(90.0);
+ this._fRegionLength = regionLength;
this.addDependentRegions(colorScheme);
};
+
+ this.addDependentRegions = function (colorScheme) {
+ let zeroAngle = 0, complementaryAngle = 180, firstAngle = -30, secondAngle = -150, hueRegions = MIN_SWATCHES_FOR_EACH_HARMONY.COMPOUND,
+ numOfColors = this._fRegionLength,
+ numColorsEachHueSide = Math.ceil(numOfColors / hueRegions),
+ zeroRadius = -parseFloat(1 / (numColorsEachHueSide + 1)), dZeroRadius = zeroRadius,
+ firstAngleRadius = 0, dfirstAngleRadius = -parseFloat(1 / numColorsEachHueSide),
+ complementaryAngleRadius = 0, dcomplementaryAngleRadius = -parseFloat(1 / numColorsEachHueSide),
+ secondAngleRadius = 0, dsecondAngleRadius = -parseFloat(1 / numColorsEachHueSide);
+
+ let totalRegions = 0;
+
+ for (let region = 0; region <= numColorsEachHueSide; region++) {
+ totalRegions = addRegions(colorScheme, firstAngle, firstAngleRadius, firstAngleRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
+
+ totalRegions = addRegions(colorScheme, secondAngle, secondAngleRadius, secondAngleRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
+
+ totalRegions = addRegions(colorScheme, complementaryAngle, complementaryAngleRadius, complementaryAngleRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
+
+ totalRegions = addRegions(colorScheme, zeroAngle, zeroRadius, zeroRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
+
+ zeroRadius += dZeroRadius;
+ complementaryAngleRadius += dcomplementaryAngleRadius;
+ secondAngleRadius += dsecondAngleRadius;
+ firstAngleRadius += dfirstAngleRadius;
+ }
+ // Record which rule created the scheme. This must be done after all the regions are defined.
+ colorScheme.setCreatingRule(this._fName);
+ };
};
// Rule.COMPOUND.prototype = new Rule();
@@ -1303,14 +1299,14 @@ Rule.MONOCHROMATIC = function () {
const _super = Rule;
_super.call(this);
- this._fName = 'MONOCHROMATIC 2';
+ this._fName = 'MONOCHROMATIC';
this.addSchemeToFormulaImpl = function (harmonyFormula) {
const colorScheme = new ColorScheme().setTo(0.0);
colorScheme.setBaseColor(new CylindricalColor().setTo(0, 1, 1));
- new RelativeColorRegion().setColorScheme2(colorScheme, 0, 0, 0, false);
+ new RelativeColorRegion().setColorScheme2(colorScheme, 0, 0, 1, false);
this.addDependentRegions(colorScheme);
@@ -1318,25 +1314,20 @@ Rule.MONOCHROMATIC = function () {
return colorScheme;
};
- this.setSchemeToRuleImpl = function (colorScheme, regionLength, showTints) {
+ this.setSchemeToRuleImpl = function (colorScheme, regionLength) {
colorScheme.setRegionsToBaseOnly(0.0);
+ this._fRegionLength = regionLength;
this.addDependentRegions(colorScheme);
};
this.addDependentRegions = function (colorScheme) {
- let newRegion;
- // DA dR dH linkA theta fromC linkR linkH
+ let radius = -(parseFloat(1 / this._fRegionLength)), dR = radius;
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 0.0, 0.0, 0.3, true, 0.0, false, true, false);
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowWraparound));
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 0.0, -0.3, 0.1, true, 0.0, false, false, true);
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 0.0, -0.3, 0.3, true, 0.0, false, false, false);
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowWraparound));
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 0.0, 0.0, 0.6, true, 0.0, false, true, false);
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowWraparound));
+ for (let region = 0; region < (this._fRegionLength - 1); region++) {
+ addRegions(colorScheme, 0.0, radius, radius, true, 0.0, false, true, false);
+ radius += dR;
+ }
// Record which rule created the scheme. This must be done after all the regions are defined.
colorScheme.setCreatingRule(this._fName);
@@ -1357,9 +1348,9 @@ Rule.SHADES = function () {
this.addSchemeToFormulaImpl = function (harmonyFormula) {
const colorScheme = new ColorScheme().setTo(0.0);
- colorScheme.setBaseColor(new CylindricalColor().setTo(0, 1, 1));
+ colorScheme.setBaseColor(new CylindricalColor().setTo(0, 0.0, 1));
- new RelativeColorRegion().setColorScheme2(colorScheme, 0, 0, 0, false);
+ new RelativeColorRegion().setColorScheme2(colorScheme, 0, 0, 0.0, false);
this.addDependentRegions(colorScheme);
@@ -1369,26 +1360,38 @@ Rule.SHADES = function () {
this.setSchemeToRuleImpl = function (colorScheme, regionLength, showTints) {
colorScheme.setRegionsToBaseOnly(0.0);
+ this._fRegionLength = regionLength;
+ this._showTints = showTints;
- this.addDependentRegions(colorScheme);
+ this.addDependentRegions(colorScheme, showTints);
};
this.addDependentRegions = function (colorScheme) {
- let newRegion;
- // DA dR dH linkA theta fromC linkR linkH
+ let numberOfColorsOnEachSide = this._fRegionLength % 2 === 0 ? this._fRegionLength / 2 : (this._fRegionLength - 1) / 2,
+ totalRegions = 0, numOfColors = this._fRegionLength,
+ delta = 0.80 / this._fRegionLength,
+ height = 0.5 + (numberOfColorsOnEachSide - 1) * delta;
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 0.0, 0.0, -0.25, true, 0.0, false, true, false);
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowWraparound));
+ if (height > 0.9) {
+ height = 0.9;
+ }
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 0.0, 0.0, -0.50, true, 0.0, false, true, false);
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowWraparound));
+ for (let i = 0; i < this._fRegionLength - 1; i++) {
+ let newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 0, 0, height, true, 0.0, false, true, false);
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 0.0, 0.0, -0.75, true, 0.0, false, true, false);
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowWraparound));
+ newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowWraparound));
+ newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
+
+ totalRegions++;
+ if (totalRegions === numOfColors - 1) break;
+
+ height -= delta;
+
+ if (height < 0.1 && !this._showTints) {
+ height = 0.1;
+ }
+ }
- // Added by Ketan to Make 5
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 0.0, 0.0, -0.9, true, 0.0, false, true, false);
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowWraparound));
// Record which rule created the scheme. This must be done after all the regions are defined.
colorScheme.setCreatingRule(this._fName);
};
@@ -1403,7 +1406,7 @@ Rule.TRIAD = function () {
const _super = Rule;
_super.call(this);
- this._fName = 'TRIAD2';
+ this._fName = 'TRIAD';
this.addSchemeToFormulaImpl = function (harmonyFormula) {
const colorScheme = new ColorScheme().setTo(120.0);
@@ -1418,36 +1421,36 @@ Rule.TRIAD = function () {
return colorScheme;
};
- this.setSchemeToRuleImpl = function (colorScheme, regionLength, showTints) {
- colorScheme.setRegionsToBaseOnly(120.0);
+ this.setSchemeToRuleImpl = function (colorScheme, regionLength) {
+ colorScheme.setRegionsToBaseOnly(0.0);
+ this._fRegionLength = regionLength;
this.addDependentRegions(colorScheme);
};
this.addDependentRegions = function (colorScheme) {
- let newRegion;
+ let zeroAngle = 0, positiveTriadAngle = 120, negativeTriadAngle = -120, hueRegions = MIN_SWATCHES_FOR_EACH_HARMONY.TRIAD,
+ numOfColors = this._fRegionLength,
+ numColorsEachHueSide = Math.ceil(numOfColors / hueRegions),
+ zeroRadius = -parseFloat(1 / (numColorsEachHueSide + 1)), dZeroRadius = zeroRadius,
+ positiveTriadRadius = 0, dpositiveTriadRadius = -parseFloat(1 / numColorsEachHueSide),
+ negativeTriadRadius = 0, dnegativeTriadRadius = -parseFloat(1 / numColorsEachHueSide),
+ totalRegions = 0;
- // Darker shade of base
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 0.0, 0.1, -0.3, true, 0.0, false, false, false);
-
- // Flips to lighter if base is dark
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
+ for (let region = 0; region <= numColorsEachHueSide; region++) {
+ totalRegions = addRegions(colorScheme, negativeTriadAngle, negativeTriadRadius, negativeTriadRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
- // The positive fork, slightly desaturated
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 120.0, -0.1, 0.0, true, 0.0, false, false, false);
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
+ totalRegions = addRegions(colorScheme, positiveTriadAngle, positiveTriadRadius, positiveTriadRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
- // The negative fork, darker shade
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, -120.0, 0.1, 0.0, true, 0.0, false, false, false);
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
+ totalRegions = addRegions(colorScheme, zeroAngle, zeroRadius, zeroRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
- // The negative fork, lighter tint
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, -120.0, 0.05, -0.3, true, 0.0, false, false, false);
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
- // Note: since the thetaCoefficients are defaulted to zero, this is a "locked at 120 degrees" TRIAD2
+ zeroRadius += dZeroRadius;
+ positiveTriadRadius += dpositiveTriadRadius;
+ negativeTriadRadius += dnegativeTriadRadius;
+ }
// Record which rule created the scheme. This must be done after all the regions are defined.
colorScheme.setCreatingRule(this._fName);
@@ -1473,36 +1476,41 @@ Rule.SQUARE = function () {
return colorScheme;
};
- this.setSchemeToRuleImpl = function (colorScheme, regionLength, showTints) {
+ this.setSchemeToRuleImpl = function (colorScheme, regionLength) {
colorScheme.setRegionsToBaseOnly(90.0);
+ this._fRegionLength = regionLength;
this.addDependentRegions(colorScheme);
};
this.addDependentRegions = function (colorScheme) {
- let newRegion;
+ let zeroAngle = 0, rightAngle = 90, complementaryAngle = 180, negativeRightAngle = -90, hueRegions = MIN_SWATCHES_FOR_EACH_HARMONY.SQUARE,
+ numOfColors = this._fRegionLength,
+ numColorsEachHueSide = Math.ceil(numOfColors / hueRegions),
+ zeroRadius = -parseFloat(1 / (numColorsEachHueSide + 1)), dZeroRadius = zeroRadius,
+ rightAngleRadius = 0, drightAngleRadius = -parseFloat(1 / numColorsEachHueSide),
+ complementaryAngleRadius = 0, dcomplementaryAngleRadius = -parseFloat(1 / numColorsEachHueSide),
+ negativeRightAngleRadius = 0, dnegativeRightAngleRadius = -parseFloat(1 / numColorsEachHueSide),
+ totalRegions = 0;
- // Darker shade of base
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 0.0, 0.1, 0.0, true, 0.0, false, false, false);
+ for (let region = 0; region <= numColorsEachHueSide; region++) {
+ totalRegions = addRegions(colorScheme, negativeRightAngle, negativeRightAngleRadius, negativeRightAngleRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
- // Flips to lighter if base is dark
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
+ totalRegions = addRegions(colorScheme, complementaryAngle, complementaryAngleRadius, complementaryAngleRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
- // The positive fork, slightly desaturated
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 90.0, -0.1, 0.0, true, 0.0, false, false, false);
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
+ totalRegions = addRegions(colorScheme, rightAngle, rightAngleRadius, rightAngleRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
- // The negative fork, darker shade
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 180.0, 0.1, 0.0, true, 0.0, false, false, false);
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
+ totalRegions = addRegions(colorScheme, zeroAngle, zeroRadius, zeroRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
- // The negative fork, lighter tint
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, -90.0, 0.05, 0.0, true, 0.0, false, false, false);
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
- // Note: since the thetaCoefficients are defaulted to zero, this is a "locked at 120 degrees" TRIAD2
+ zeroRadius += dZeroRadius;
+ complementaryAngleRadius += dcomplementaryAngleRadius;
+ negativeRightAngleRadius += dnegativeRightAngleRadius;
+ rightAngleRadius += drightAngleRadius;
+ }
// Record which rule created the scheme. This must be done after all the regions are defined.
colorScheme.setCreatingRule(this._fName);
@@ -1517,7 +1525,7 @@ Rule.SPLIT_COMPLEMENTARY = function () {
this._fName = 'SPLIT_COMPLEMENTARY';
this.addSchemeToFormulaImpl = function (harmonyFormula) {
- const colorScheme = new ColorScheme().setTo(150.0);
+ const colorScheme = new ColorScheme().setTo(0.0);
colorScheme.setBaseColor(new CylindricalColor().setTo(0, 1, 1));
@@ -1529,38 +1537,37 @@ Rule.SPLIT_COMPLEMENTARY = function () {
return colorScheme;
};
- this.setSchemeToRuleImpl = function (colorScheme, regionLength, showTints) {
+ this.setSchemeToRuleImpl = function (colorScheme, regionLength) {
colorScheme.setRegionsToBaseOnly(0.0);
+ this._fRegionLength = regionLength;
this.addDependentRegions(colorScheme);
};
this.addDependentRegions = function (colorScheme) {
- let newRegion;
-
- // Darker shade of base DA dR dH linkA theta fromC linkR linkH
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 150.0, -0.1, 0.3, true, 0.5, true, false, false);
+ let zeroAngle = 0, secondAngle = 162, firstAngle = 198, hueRegions = MIN_SWATCHES_FOR_EACH_HARMONY.SPLIT_COMPLEMENTARY,
+ numOfColors = this._fRegionLength,
+ numColorsEachHueSide = Math.ceil(numOfColors / hueRegions),
+ zeroRadius = -parseFloat(1 / (numColorsEachHueSide)), dZeroRadius = zeroRadius,
+ firstAngleRadius = 0, dfirstAngleRadius = -parseFloat(1 / numColorsEachHueSide),
+ secondAngleRadius = 0, dsecondAngleRadius = -parseFloat(1 / numColorsEachHueSide),
+ totalRegions = 0;
- // Flips to lighter if base is dark
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
+ for (let region = 0; region <= numColorsEachHueSide; region++) {
+ totalRegions = addRegions(colorScheme, firstAngle, firstAngleRadius, firstAngleRadius, true, 0.0, true, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
- // The positive fork, slightly desaturated
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, 150.0, -0.05, 0, true, 0.5, true, false, false);
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
+ totalRegions = addRegions(colorScheme, secondAngle, secondAngleRadius, secondAngleRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
- // The negative fork, darker shade
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, -150.0, 0.1, 0.3, true, -0.5, true, false, false);
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
+ totalRegions = addRegions(colorScheme, zeroAngle, zeroRadius, zeroRadius, true, 0.0, false, false, false, totalRegions);
+ if (checkIfAllRegionsAreMapped(totalRegions, numOfColors)) break;
- // The negative fork, lighter tint
- newRegion = new RelativeColorRegion().setColorScheme5(colorScheme, -150.0, 0.05, 0, true, -0.5, true, false, false);
- newRegion.setOnRadiusOverflow(new OverflowResponse(kOverflowNegate));
- newRegion.setOnHeightOverflow(new OverflowResponse(kOverflowNegate));
- // Note: since the thetaCoefficients are defaulted to zero, this is a "locked at 120 degrees" TRIAD2
+ zeroRadius += dZeroRadius;
+ firstAngleRadius += dfirstAngleRadius;
+ secondAngleRadius += dsecondAngleRadius;
+ }
- // Record which rule created the scheme. This must be done after all the regions are defined.
colorScheme.setCreatingRule(this._fName);
};
};
@@ -1741,7 +1748,7 @@ function HarmonyAdapter(theme, setSwatch) {
function _updateBaseColor() {
if (_rule === null) { return; }
var baseColor = _colorSet.swatches[_colorSet.baseColorIndex];
- var baseCylindricalColor = new CylindricalColor(colorwheel.scientificToArtisticSmooth(baseColor.hsv.h), baseColor.hsv.s / 100, baseColor.hsv.v / 100);
+ var baseCylindricalColor = new CylindricalColor(colorwheel.scientificToArtisticSmooth(Math.round(baseColor.hsv.h)), Math.round(baseColor.hsv.s) / 100, Math.round(baseColor.hsv.v) / 100);
_harmonyController.setBasePoint(baseCylindricalColor);
if (_initFromColors) { _initFromColors = false; _resetFromColors(); }
@@ -1773,7 +1780,7 @@ function HarmonyAdapter(theme, setSwatch) {
scheme.clearRegionList();
var baseColor = _colorSet.swatches[_colorSet.baseColorIndex];
- var baseC = new CylindricalColor(colorwheel.scientificToArtisticSmooth(baseColor.hsv.h), baseColor.hsv.s / 100, baseColor.hsv.v / 100);
+ var baseC = new CylindricalColor(colorwheel.scientificToArtisticSmooth(Math.round(baseColor.hsv.h)), Math.round(baseColor.hsv.s) / 100, Math.round(baseColor.hsv.v) / 100);
var region = new RelativeColorRegion().setColorScheme2(scheme, 0, 0, 0, true);
scheme.setBaseColor(baseC);
diff --git a/express/code/scripts/scripts.js b/express/code/scripts/scripts.js
index 0206d13f2..e574c0196 100644
--- a/express/code/scripts/scripts.js
+++ b/express/code/scripts/scripts.js
@@ -445,9 +445,10 @@ async function loadPage() {
import('./instrument.js').then((mod) => { mod.default(); });
}
- /* region based redirect to homepage */
+ /* region based redirect to CN homepage */
+ const isAdobeOrigin = /^(www\.stage\.|www\.)adobe\.com$/.test(window.location.hostname);
import('./utils/location-utils.js').then(({ getCountry }) => getCountry()).then((country) => {
- if (country === 'cn') { window.location.href = '/cn'; }
+ if (country === 'cn' && isAdobeOrigin && !window.location.pathname.startsWith('/cn') && !window.isErrorPage) { window.location.href = '/cn'; }
});
document.head.querySelectorAll('meta').forEach((meta) => {
diff --git a/express/code/scripts/utils.js b/express/code/scripts/utils.js
index 1782898c8..a63c7dc24 100644
--- a/express/code/scripts/utils.js
+++ b/express/code/scripts/utils.js
@@ -20,6 +20,7 @@ export const [setLibs, getLibs] = (() => {
const { hostname, search } = location || window.location;
if (!['.aem.', '.hlx.', '.stage.', 'local', '.da.'].some((i) => hostname.includes(i))) return prodLibs;
const branch = new URLSearchParams(search).get('milolibs') || 'main';
+ if (!/^[a-zA-Z0-9_-]+$/.test(branch)) throw new Error('Invalid branch name.');
if (branch === 'local') return 'http://localhost:6456/libs';
if (branch === 'main' && hostname.includes('.stage.')) return '/libs';
return branch.includes('--') ? `https://${branch}.aem.live/libs` : `https://${branch}--milo--adobecom.aem.live/libs`;
diff --git a/express/code/scripts/utils/content-replace.js b/express/code/scripts/utils/content-replace.js
index a358cfb23..06fc543cb 100644
--- a/express/code/scripts/utils/content-replace.js
+++ b/express/code/scripts/utils/content-replace.js
@@ -205,6 +205,7 @@ async function getReplacementsFromSearch() {
phformat,
topics,
q,
+ ckgid,
} = params;
if (!tasks && !phformat) {
return null;
@@ -222,6 +223,7 @@ async function getReplacementsFromSearch() {
const sanitizedTasksx = tasksx?.match(exp) ? '' : tasksx;
const sanitizedTopics = topics?.match(exp) ? '' : topics;
const sanitizedQuery = q?.match(exp) ? '' : q;
+ const sanitizedCkgId = ckgid?.match(exp) ? '' : ckgid;
const tasksPair = Object.entries(categories).find((cat) => cat[1] === sanitizedTasks);
const xTasksPair = Object.entries(xCategories).find((cat) => cat[1] === sanitizedTasksx);
@@ -230,6 +232,11 @@ async function getReplacementsFromSearch() {
if (!translatedTasks) {
translatedTasks = tasksPair?.[1] ? tasksPair[0].toLowerCase() : sanitizedTasks;
}
+ // CKG searches already include a fully qualified query phrase in `topics`/`q`.
+ // Appending translated tasks duplicates terms in page title, marquee heading, and breadcrumbs.
+ if (sanitizedCkgId) {
+ translatedTasks = '';
+ }
return {
'{{queryTasks}}': sanitizedTasks || '',
'{{QueryTasks}}': titleCase(sanitizedTasks || ''),
diff --git a/test/scripts/color-shared/controllers/ColorThemeExpressController.test.js b/test/scripts/color-shared/controllers/ColorThemeExpressController.test.js
new file mode 100644
index 000000000..950d46158
--- /dev/null
+++ b/test/scripts/color-shared/controllers/ColorThemeExpressController.test.js
@@ -0,0 +1,158 @@
+import { expect } from '@esm-bundle/chai';
+import { setLibs } from '../../../../express/code/scripts/utils.js';
+import ColorThemeExpressController from '../../../../express/code/scripts/color-shared/controllers/ColorThemeExpressController.js';
+
+setLibs('/libs');
+
+function getPaletteHexes(baseColor, swatchCount, harmonyRule) {
+ const controller = new ColorThemeExpressController({
+ swatches: Array(swatchCount).fill(baseColor),
+ harmonyRule,
+ baseColorIndex: 0,
+ });
+ controller.setBaseColor(baseColor);
+ return controller.getState().swatches.map((s) => s.hex.toUpperCase());
+}
+
+const PALETTE_FIXTURES = [
+ {
+ label: '5-color palette, base #9E2BFC',
+ base: '#9E2BFC',
+ count: 5,
+ cases: [
+ ['ANALOGOUS', ['#9E2BFC', '#5A2BFC', '#E32BFC', '#2B40FC', '#FC2BAE']],
+ ['COMPLEMENTARY', ['#EDFC2B', '#8B4FBD', '#9E2BFC', '#A1A754', '#6A547D']],
+ ['SPLIT_COMPLEMENTARY', ['#9E2BFC', '#95FC2B', '#FCE82B', '#6A547D', '#697D54']],
+ ['TRIAD', ['#2BFC7B', '#FCAD2B', '#9E2BFC', '#8254A7', '#547D63']],
+ ['SQUARE', ['#2BFCF2', '#EDFC2B', '#9E2BFC', '#FC732B', '#8254A7']],
+ ['COMPOUND', ['#2C2BFC', '#5AFC2B', '#9E2BFC', '#EDFC2B', '#8254A7']],
+ ['SHADES', ['#8825D9', '#6E1EB0', '#9E2BFC', '#551787', '#3B105E']],
+ ['MONOCHROMATIC', ['#9E2BFC', '#904BC9', '#795696', '#594D63', '#2F2A33']],
+ ],
+ },
+ {
+ label: '2-color palette, base #FF763B',
+ base: '#FF763B',
+ count: 2,
+ cases: [
+ ['COMPLEMENTARY', ['#3BFFE5', '#FF763B']],
+ ['SHADES', ['#B35229', '#FF763B']],
+ ['MONOCHROMATIC', ['#80675D', '#FF763B']],
+ ],
+ },
+ {
+ label: '3-color palette, base #115E50',
+ base: '#115E50',
+ count: 3,
+ cases: [
+ ['ANALOGOUS', ['#115E50', '#115E36', '#11535E']],
+ ['COMPLEMENTARY', ['#115E50', '#5E2611', '#5CB3A3']],
+ ['SPLIT_COMPLEMENTARY', ['#115E50', '#5E3411', '#5E1911']],
+ ['TRIAD', ['#115E50', '#5E4D11', '#5C115E']],
+ ['SHADES', ['#115E50', '#28DEBC', '#1C9A83']],
+ ['MONOCHROMATIC', ['#115E50', '#5CB3A3', '#D8FFF8']],
+ ],
+ },
+ {
+ label: '4-color palette, base #FFCFCF',
+ base: '#FFCFCF',
+ count: 4,
+ cases: [
+ ['ANALOGOUS', ['#FFCFCF', '#803E3E', '#FFD7CF', '#FFCFEC']],
+ ['COMPLEMENTARY', ['#FFCFCF', '#3E8051', '#AA7070', '#CFFFDC']],
+ ['SPLIT_COMPLEMENTARY', ['#FFCFCF', '#803E3E', '#D2FFCF', '#CFFFED']],
+ ['TRIAD', ['#FFCFCF', '#AA7070', '#FFFECF', '#CFECFF']],
+ ['SQUARE', ['#FFCFCF', '#FFF5CF', '#CFFFDC', '#CFD3FF']],
+ ['COMPOUND', ['#FFCFCF', '#CFFFDC', '#CFFFF8', '#FFCFFF']],
+ ['SHADES', ['#7F6767', '#B39191', '#FFCFCF', '#E5BABA']],
+ ['MONOCHROMATIC', ['#400F0F', '#803E3E', '#FFCFCF', '#BF8E8E']],
+ ],
+ },
+ {
+ label: '6-color palette, base #FF1000',
+ base: '#FF1000',
+ count: 6,
+ cases: [
+ ['ANALOGOUS', ['#FF00F8', '#FF3E00', '#FF1100', '#FF005F', '#FF6055', '#FF6A00']],
+ ['COMPLEMENTARY', ['#39AA67', '#BF3930', '#FF1100', '#00FF69', '#395544', '#804440']],
+ ['SPLIT_COMPLEMENTARY', ['#804440', '#00FF13', '#FF1100', '#00FFC0', '#408044', '#408070']],
+ ['TRIAD', ['#AA4039', '#E2FF00', '#FF1100', '#007EFF', '#788040', '#405F80']],
+ ['SQUARE', ['#FFD400', '#00FF69', '#FF1100', '#0900FF', '#424080', '#AA4039']],
+ ['COMPOUND', ['#00FF69', '#00FFFA', '#FF1100', '#FF00C5', '#804071', '#AA4039']],
+ ['SHADES', ['#B30C00', '#D40E00', '#FF1100', '#F61000', '#6F0700', '#910A00']],
+ ['MONOCHROMATIC', ['#804440', '#AA4039', '#FF1100', '#D52F23', '#332B2B', '#553B39']],
+ ],
+ },
+ {
+ label: '7-color palette, base #FFF799',
+ base: '#FFF799',
+ count: 7,
+ cases: [
+ ['ANALOGOUS', ['#FFDE99', '#F2FF99', '#FFF799', '#FFEA99', '#FFFCDD', '#C7FF99', '#807859']],
+ ['COMPLEMENTARY', ['#AAA3BF', '#CCC9A3', '#FFF799', '#B499FF', '#635980', '#99967A', '#66633D']],
+ ['SPLIT_COMPLEMENTARY', ['#AAA893', '#999FFF', '#FFF799', '#D599FF', '#9395AA', '#A193AA', '#55522D']],
+ ['TRIAD', ['#BFBDA3', '#99EAFF', '#FFF799', '#FF99BC', '#93A5AA', '#AA939B', '#807C59']],
+ ['SQUARE', ['#99FFDC', '#B499FF', '#FFF799', '#FFAC99', '#806159', '#AAA893', '#635980']],
+ ['COMPOUND', ['#B499FF', '#EC99FF', '#FFF799', '#FFE299', '#807559', '#AAA893', '#785980']],
+ ['SHADES', ['#B3AD6B', '#D0C97D', '#FFF799', '#EDE58E', '#787448', '#95905A', '#5B5837']],
+ ['MONOCHROMATIC', ['#928F70', '#B6B4A1', '#FFF799', '#DBD6A2', '#494623', '#6D6A45', '#333011']],
+ ],
+ },
+ {
+ label: '8-color palette, base #1F3DA6',
+ base: '#1F3DA6',
+ count: 8,
+ cases: [
+ ['ANALOGOUS', ['#1F3DA6', '#1F67A6', '#2D1FA6', '#1F91A6', '#591FA6', '#5768A6', '#232C33', '#252333']],
+ ['COMPLEMENTARY', ['#1F3DA6', '#A6821F', '#2D3C73', '#66572D', '#262B40', '#332F23', '#282B33', '#33312C']],
+ ['SPLIT_COMPLEMENTARY', ['#1F3DA6', '#A6921F', '#A6721F', '#2A3351', '#514B2A', '#51422A', '#2C2D33', '#33322C']],
+ ['TRIAD', ['#1F3DA6', '#65A61F', '#A63D1F', '#2D3966', '#3E512A', '#51332A', '#232733', '#2F332C']],
+ ['SQUARE', ['#1F3DA6', '#1FA633', '#A6821F', '#A61F40', '#2A3351', '#233325', '#332F23', '#332327']],
+ ['COMPOUND', ['#1F3DA6', '#1F83A6', '#A69D1F', '#A6821F', '#2A3351', '#232F33', '#333223', '#332F23']],
+ ['SHADES', ['#1F3DA6', '#1F3DA6', '#1B338C', '#162A73', '#112159', '#0C1740', '#2E59F2', '#294FD9']],
+ ['MONOCHROMATIC', ['#1F3DA6', '#2A3E86', '#2D3966', '#282E46', '#232733', '#2A2C33', '#2C2D33', '#252833']],
+ ],
+ },
+ {
+ label: '9-color palette, base #33001E',
+ base: '#33001E',
+ count: 9,
+ cases: [
+ ['ANALOGOUS', ['#33001E', '#2E0033', '#330000', '#1D0033', '#330900', '#331125', '#AA59B3', '#B35A59', '#8C59B3']],
+ ['COMPLEMENTARY', ['#33001E', '#033300', '#5E103D', '#196614', '#882D62', '#43993D', '#B3598D', '#7FCC7A', '#DD93BE']],
+ ['SPLIT_COMPLEMENTARY', ['#33001E', '#00330F', '#183300', '#882D62', '#2D8848', '#59882D', '#DD93BE', '#93DDA9', '#B6DD93']],
+ ['TRIAD', ['#33001E', '#002F33', '#332C00', '#731D4F', '#2D8188', '#887C2D', '#B3598D', '#93D7DD', '#DDD393']],
+ ['SQUARE', ['#33001E', '#001433', '#033300', '#332200', '#731D4F', '#2D5188', '#33882D', '#886A2D', '#B3598D']],
+ ['COMPOUND', ['#33001E', '#230033', '#00331A', '#033300', '#731D4F', '#6B2D88', '#2D885C', '#33882D', '#B3598D']],
+ ['SHADES', ['#33001E', '#F60090', '#E00083', '#C90075', '#B30068', '#9C005B', '#85004E', '#6E0040', '#580033']],
+ ['MONOCHROMATIC', ['#33001E', '#4F0932', '#6C1849', '#882D62', '#A4497E', '#C16B9D', '#DD93BE', '#F9C2E2', '#FFE3F3']],
+ ],
+ },
+ {
+ label: '10-color palette, base #F0FFE0',
+ base: '#F0FFE0',
+ count: 10,
+ cases: [
+ ['ANALOGOUS', ['#F0FFE0', '#FDFFE0', '#E3FFE0', '#FFFCE0', '#E0FFE8', '#CCFF96', '#7B8036', '#3D8036', '#807836', '#368049']],
+ ['COMPLEMENTARY', ['#F0FFE0', '#FBE0FF', '#BBD5A0', '#C593CC', '#88AA64', '#8F5099', '#5C8036', '#5D2166', '#365516', '#2D0633']],
+ ['SPLIT_COMPLEMENTARY', ['#F0FFE0', '#FFE0F4', '#F1E0FF', '#A1BF80', '#BF80A9', '#A280BF', '#5C8036', '#803665', '#5E3680', '#26400B']],
+ ['TRIAD', ['#F0FFE0', '#FFE7E0', '#E0E7FF', '#B0CC93', '#BF8E80', '#808EBF', '#769950', '#804636', '#364580', '#446621']],
+ ['SQUARE', ['#F0FFE0', '#FFF0E0', '#FBE0FF', '#E0F7FF', '#A1BF80', '#AA8864', '#A164AA', '#6498AA', '#5C8036', '#553616']],
+ ['COMPOUND', ['#F0FFE0', '#FFFDE0', '#FFE0E8', '#FBE0FF', '#A1BF80', '#AAA664', '#AA6475', '#A164AA', '#5C8036', '#555116']],
+ ['SHADES', ['#F0FFE0', '#F0FFE0', '#E2F0D3', '#CFDBC1', '#BBC7AF', '#A8B39D', '#959E8B', '#828A79', '#6E7567', '#5B6155']],
+ ['MONOCHROMATIC', ['#F0FFE0', '#D2E6BC', '#B0CC93', '#92B36F', '#769950', '#5C8036', '#446621', '#304D11', '#1D3306', '#1B3301']],
+ ],
+ },
+];
+
+describe('ColorThemeExpressController harmony rules', () => {
+ PALETTE_FIXTURES.forEach(({ label, base, count, cases }) => {
+ describe(label, () => {
+ cases.forEach(([rule, expected]) => {
+ it(`creates ${rule} harmony palette`, () => {
+ expect(getPaletteHexes(base, count, rule)).to.have.members(expected);
+ });
+ });
+ });
+ });
+});