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); + }); + }); + }); + }); +});