diff --git a/.eslintignore b/.eslintignore index 644bc2e..26f33aa 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,3 @@ -helix-importer-ui \ No newline at end of file +helix-importer-ui +tools/ +plugins/ \ No newline at end of file diff --git a/.stylelintrc.json b/.stylelintrc.json index 17c74ed..cf7e967 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,3 +1,7 @@ { - "extends": ["stylelint-config-standard"] + "extends": ["stylelint-config-standard"], + "rules": { + "selector-class-pattern": null, + "no-descending-specificity": null + } } \ No newline at end of file diff --git a/blocks/columns-stats/columns-stats.css b/blocks/columns-stats/columns-stats.css index fa08ea8..6ee0c0f 100644 --- a/blocks/columns-stats/columns-stats.css +++ b/blocks/columns-stats/columns-stats.css @@ -85,7 +85,9 @@ main > .section.columns-stats-container:first-of-type { } /* -------------------------------------------------------------------------- */ + /* USTA Foundation — pill metrics strip (scoped) */ + /* -------------------------------------------------------------------------- */ /* Horizontal inset comes from main > .section > div; avoid double padding here */ @@ -104,8 +106,7 @@ body.foundation .columns-stats { } body.foundation .columns-stats > div:first-child { - align-items: center; - justify-items: center; + place-items: center center; gap: clamp(24px, 4vw, 48px) clamp(16px, 3vw, 40px); } diff --git a/blocks/columns/columns.css b/blocks/columns/columns.css index 6597353..c1c7e7c 100644 --- a/blocks/columns/columns.css +++ b/blocks/columns/columns.css @@ -54,7 +54,7 @@ main > .section.columns-container:first-of-type { } /* Text column */ -.columns > div > div:not(.columns-img-col):not(.columns-gallery-col) { +.columns > div > div:not(.columns-img-col, .columns-gallery-col) { padding: 32px 24px; } @@ -85,7 +85,7 @@ main > .section.columns-container:first-of-type { order: unset; } - .columns > div > div:not(.columns-img-col):not(.columns-gallery-col) { + .columns > div > div:not(.columns-img-col, .columns-gallery-col) { display: flex; flex-direction: column; justify-content: center; @@ -93,7 +93,7 @@ main > .section.columns-container:first-of-type { box-sizing: border-box; } - .columns > div > div:not(.columns-img-col):not(.columns-gallery-col) h2 { + .columns > div > div:not(.columns-img-col, .columns-gallery-col) h2 { font-size: 72px; } @@ -114,7 +114,9 @@ main > .section.columns-container:first-of-type { } /* -------------------------------------------------------------------------- */ + /* Columns gallery — outer 1:1; .2-left-1-right = two 1:1 crops + one 1:2 crop */ + /* -------------------------------------------------------------------------- */ .columns.gallery > div > .columns-gallery--2-left-1-right { @@ -193,25 +195,27 @@ main > .section.columns-container:first-of-type { } /* -------------------------------------------------------------------------- */ + /* Foundation site — two-column copy + video card (.foundation ancestor) */ + /* -------------------------------------------------------------------------- */ .foundation .columns { overflow: visible; } -.foundation .columns > div > div:not(.columns-img-col):not(.columns-gallery-col) { +.foundation .columns > div > div:not(.columns-img-col, .columns-gallery-col) { text-align: left; align-items: flex-start; } -.foundation .columns > div > div:not(.columns-img-col):not(.columns-gallery-col) h2 { +.foundation .columns > div > div:not(.columns-img-col, .columns-gallery-col) h2 { font-size: clamp(26px, 3.2vw, 38px); line-height: 1.15; margin: 0 0 20px; } -.foundation .columns > div > div:not(.columns-img-col):not(.columns-gallery-col) p { +.foundation .columns > div > div:not(.columns-img-col, .columns-gallery-col) p { font-size: clamp(15px, 1.6vw, 18px); line-height: 1.65; color: var(--text-color); @@ -271,7 +275,7 @@ main > .section.columns-container:first-of-type { min-width: 0; } - .foundation .columns > div > div:not(.columns-img-col):not(.columns-gallery-col) { + .foundation .columns > div > div:not(.columns-img-col, .columns-gallery-col) { justify-content: flex-start; padding: 24px 0; } diff --git a/blocks/filter-cards/filter-cards.js b/blocks/filter-cards/filter-cards.js index 1a6bbfc..36bd7da 100644 --- a/blocks/filter-cards/filter-cards.js +++ b/blocks/filter-cards/filter-cards.js @@ -113,7 +113,8 @@ export default async function decorate(block) { const data = await fetchIndex(); allArticles = data - .filter((a) => a.path && a.title && a.path !== window.location.pathname) + .filter((a) => a.path && a.title && a.image && a.description + && a.path !== window.location.pathname) .sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0)); filteredArticles = [...allArticles]; diff --git a/blocks/form/form.css b/blocks/form/form.css index 8ede136..f93a18b 100644 --- a/blocks/form/form.css +++ b/blocks/form/form.css @@ -80,7 +80,7 @@ .form .form-field input { width: 100%; box-sizing: border-box; - padding: 14px 14px; + padding: 14px; border: 1px solid #000; border-radius: 0; font-size: 16px; @@ -151,6 +151,7 @@ .form-container .form-wrapper { padding: 64px 40px 72px; } + .form .form-heading h2 { font-size: 56px; margin-bottom: 12px; @@ -162,8 +163,7 @@ } .form .form-input-row { - flex-direction: row; - flex-wrap: wrap; + flex-flow: row wrap; gap: 20px; align-items: flex-end; margin-bottom: 32px; diff --git a/blocks/header/header.js b/blocks/header/header.js index 1a57da8..02eab8e 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -138,7 +138,16 @@ export default async function decorate(block) { trigger.addEventListener('click', (e) => { if (!isDesktop.matches) return; const href = trigger.getAttribute('href')?.trim() || ''; - const isPlaceholder = !href || href === '#' || href.startsWith('javascript:'); + let isJsProtocol = false; + if (href) { + try { + const proto = new URL(href, window.location.href).protocol.toLowerCase(); + isJsProtocol = proto === `${'java'}script:`; + } catch { + isJsProtocol = false; + } + } + const isPlaceholder = !href || href === '#' || isJsProtocol; if (!isPlaceholder) return; e.preventDefault(); diff --git a/blocks/hero/hero.css b/blocks/hero/hero.css index e8f4b42..8c17c9a 100644 --- a/blocks/hero/hero.css +++ b/blocks/hero/hero.css @@ -37,7 +37,9 @@ } /* -------------------------------------------------------------------------- */ + /* USTA Foundation home hero — scoped to body.foundation only */ + /* -------------------------------------------------------------------------- */ body.foundation .hero-container .hero-wrapper { diff --git a/blocks/player-profile/player-profile.css b/blocks/player-profile/player-profile.css index 3640d37..3834808 100644 --- a/blocks/player-profile/player-profile.css +++ b/blocks/player-profile/player-profile.css @@ -67,7 +67,7 @@ color: #fff; text-transform: none; margin: 0; - width: 58.33333333%; + width: 58.3333%; background: rgb(0 0 0 / 58%); padding: 20px 28px 24px; box-sizing: border-box; @@ -137,7 +137,7 @@ font-size: 18px; font-weight: 600; line-height: 23px; - color: rgb(0, 37, 193) !important; + color: rgb(0 37 193) !important; text-decoration: underline !important; text-underline-offset: 2px; letter-spacing: 0.2px; diff --git a/blocks/player-profile/player-profile.js b/blocks/player-profile/player-profile.js index baa159b..c7e3f27 100644 --- a/blocks/player-profile/player-profile.js +++ b/blocks/player-profile/player-profile.js @@ -28,8 +28,17 @@ async function fetchPlayerProfile() { function populateProfile(block, profile) { if (!profile) return; - const { firstName, gender, residencyDeclaration, uaid } = profile; - const { section, district, countryIso } = residencyDeclaration ?? {}; + const { + firstName, + gender, + residencyDeclaration, + uaid, + } = profile; + const { + section, + district, + countryIso, + } = residencyDeclaration ?? {}; const greet = block.querySelector('.player-profile-greeting'); if (greet && firstName) greet.textContent = `Welcome, ${firstName}!`; diff --git a/blocks/player-rankings/player-rankings.css b/blocks/player-rankings/player-rankings.css index b890974..16fac16 100644 --- a/blocks/player-rankings/player-rankings.css +++ b/blocks/player-rankings/player-rankings.css @@ -18,7 +18,7 @@ .pr-cat-tabs { width: 100%; background-color: #09b5f7; - margin: 0 0 2rem 0; + margin: 0 0 2rem; padding-top: 0; box-sizing: border-box; } @@ -127,7 +127,7 @@ transition: background-color 0.15s; white-space: nowrap; text-align: center; - color: rgb(9, 181, 247) !important; + color: rgb(9 181 247) !important; } .pr-header-link:hover { @@ -172,9 +172,7 @@ font-size: 16px; color: #1a1a1a; background-color: #fff; - min-width: 190px; box-sizing: border-box; - -webkit-appearance: none; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; @@ -224,7 +222,7 @@ letter-spacing: 0.55px; text-transform: uppercase; color: #1a1a1a; - margin: 0 0 1rem 0; + margin: 0 0 1rem; } .pr-sidebar-heading--filter { @@ -274,7 +272,7 @@ } .pr-filter-input--search[type='search']::-webkit-search-cancel-button { - -webkit-appearance: none; + appearance: none; } .pr-label-row { @@ -363,7 +361,7 @@ width: 100%; min-height: 52px; height: auto; - padding: 14px 14px; + padding: 14px; border-radius: 999px; background-color: #000; color: #fff; @@ -586,7 +584,7 @@ transition: background-color 0.12s, border-color 0.12s; } -.pr-page-btn:hover:not(.pr-page-btn--active):not(.pr-page-btn--disabled) { +.pr-page-btn:hover:not(.pr-page-btn--active, .pr-page-btn--disabled) { background-color: #f0f0f0; border-color: #aaa; } @@ -675,8 +673,7 @@ padding: 20px 16px 24px; border-right: none; border-bottom: 1px solid #e0e0e0; - flex-direction: column; - flex-wrap: nowrap; + flex-flow: column nowrap; gap: 20px; align-items: stretch; } @@ -851,7 +848,7 @@ min-width: 7rem; max-width: 9rem; background-color: #fff; - box-shadow: 4px 0 10px -4px rgba(0, 0, 0, 0.18); + box-shadow: 4px 0 10px -4px rgb(0 0 0 / 18%); } .pr-table .pr-row > .pr-td:nth-child(1) { @@ -885,7 +882,7 @@ min-width: 7rem; max-width: 9rem; background-color: #fff; - box-shadow: 4px 0 10px -4px rgba(0, 0, 0, 0.12); + box-shadow: 4px 0 10px -4px rgb(0 0 0 / 12%); } .pr-pagination { diff --git a/blocks/player-rankings/player-rankings.js b/blocks/player-rankings/player-rankings.js index 0d915f6..efcebab 100644 --- a/blocks/player-rankings/player-rankings.js +++ b/blocks/player-rankings/player-rankings.js @@ -25,7 +25,8 @@ * 2. On each filter change or page change → buildPayload() → fetchRankings() * 3. Response → renderTable() + renderPagination() * 4. Location hash is kept in sync (USTA-style), e.g. - * #tab=junior&junior-juniorListType=doubles&junior-rankListGender=M&junior-page=1&junior-sectionCode=S10 + * #tab=junior&junior-juniorListType=doubles&junior-rankListGender=M& + * junior-page=1&junior-sectionCode=S10 * (see usta.com/play/rankings for the same param names) * * Document authoring table shape (AEM/EDS table-block convention): @@ -125,8 +126,8 @@ const SECTIONS = [ function createState(defaults) { return { - listType: defaults.listType || 'doubles', // ← changed: was 'combined' - gender: defaults.gender || 'M', // ← changed: was 'F' + listType: defaults.listType || 'doubles', // ← changed: was 'combined' + gender: defaults.gender || 'M', // ← changed: was 'F' ageGroup: defaults.ageGroup || 'Y12', section: '', searchName: '', @@ -145,15 +146,12 @@ const SECTION_CODES = new Set(SECTIONS.map((s) => s.value).filter(Boolean)); /** True when the hash has at least one `junior-…` param (not `tab=junior` alone). */ function isJuniorUrlFragment(p) { - for (const k of p.keys()) { - if (k.startsWith('junior-')) return true; - } - return false; + return [...p.keys()].some((k) => k.startsWith('junior-')); } const DEFAULT_JUNIOR_STATE = { - listType: 'doubles', // ← changed: was 'combined' - gender: 'M', // ← changed: was 'F' + listType: 'doubles', // ← changed: was 'combined' + gender: 'M', // ← changed: was 'F' ageGroup: 'Y12', section: '', searchName: '', @@ -168,10 +166,10 @@ const DEFAULT_JUNIOR_STATE = { * @returns {{ filterState: null | object, hadPublishInHash: boolean, ignore: boolean }} */ function getPlayerRankingsStateFromHash() { - if (typeof location === 'undefined') { + if (typeof window.location === 'undefined') { return { filterState: null, hadPublishInHash: false, ignore: false }; } - const raw = location.hash?.replace(/^#/, '') ?? ''; + const raw = window.location.hash?.replace(/^#/, '') ?? ''; if (!raw.trim()) { return { filterState: null, hadPublishInHash: false, ignore: false }; } @@ -226,7 +224,7 @@ function getPlayerRankingsStateFromHash() { /** Writes #tab=junior&junior-…; uses replaceState (no history spam). */ function replaceHashFromPlayerRankingsState(state) { - if (typeof history === 'undefined' || typeof location === 'undefined') return; + if (typeof window.history === 'undefined' || typeof window.location === 'undefined') return; const next = new URLSearchParams(); next.set('tab', 'junior'); next.set('junior-juniorListType', state.listType); @@ -244,20 +242,20 @@ function replaceHashFromPlayerRankingsState(state) { next.set('junior-playerName', s); } const hash = `#${next.toString()}`; - if (location.hash === hash) return; - const u = new URL(location.href); + if (window.location.hash === hash) return; + const u = new URL(window.location.href); u.hash = hash; - history.replaceState(null, '', u); + window.history.replaceState(null, '', u); } /** If there is no fragment, set #tab=junior so the bar matches USTA before/without filters. */ function ensureTabJuniorInUrlWhenHashEmpty() { - if (typeof history === 'undefined' || typeof location === 'undefined') return; - const fr = (location.hash || '').replace(/^#/, '').trim(); + if (typeof window.history === 'undefined' || typeof window.location === 'undefined') return; + const fr = (window.location.hash || '').replace(/^#/, '').trim(); if (fr !== '') return; - const u = new URL(location.href); + const u = new URL(window.location.href); u.hash = 'tab=junior'; - history.replaceState(null, '', u); + window.history.replaceState(null, '', u); } /* ─── Date helper ────────────────────────────────────────────────────────── */ @@ -555,7 +553,9 @@ function buildSidebar(state) { sidebar.append(searchSection, filterSection, resetBtn); - return { sidebar, searchInput, dateInput, resetBtn }; + return { + sidebar, searchInput, dateInput, resetBtn, + }; } function buildTableSkeleton() { @@ -741,7 +741,9 @@ export default async function decorate(block) { block.setAttribute('aria-label', 'Junior Tournament Rankings'); const header = buildHeader(state); - const { sidebar, searchInput, dateInput, resetBtn } = buildSidebar(state); + const { + sidebar, searchInput, dateInput, resetBtn, + } = buildSidebar(state); const statusEl = buildStatusBar(); const tableWrap = buildTableSkeleton(); const tbody = tableWrap.querySelector('.pr-tbody'); @@ -783,6 +785,8 @@ export default async function decorate(block) { } } + // Mock / API payload uses _dateMismatch for “no rows for selected publish date”. + // eslint-disable-next-line no-underscore-dangle -- external payload shape const dateMismatch = Boolean(response._dateMismatch); const players = response.data || []; const total = response.pagination?.totalResults ?? players.length; @@ -862,7 +866,7 @@ export default async function decorate(block) { const inHash = getPlayerRankingsStateFromHash(); if (inHash.ignore) return; if (!inHash.filterState) { - if (!location.hash || location.hash === '' || location.hash === '#') { + if (!window.location.hash || window.location.hash === '' || window.location.hash === '#') { lastListKey = null; } return; @@ -879,8 +883,8 @@ export default async function decorate(block) { resetBtn.addEventListener('click', () => { Object.assign(state, { - listType: 'doubles', // ← changed: was 'combined' - gender: 'M', // ← changed: was 'F' + listType: 'doubles', // ← changed: was 'combined' + gender: 'M', // ← changed: was 'F' ageGroup: 'Y12', section: '', searchName: '', @@ -894,4 +898,4 @@ export default async function decorate(block) { // Initial load await load(); -} \ No newline at end of file +} diff --git a/scripts/scripts.js b/scripts/scripts.js index 125e7ef..b381bf9 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -17,6 +17,9 @@ import { toCamelCase, } from './aem.js'; +/** Base URL for NX experiment plugin (matches da.live paths used elsewhere in this file). */ +export const NX_ORIGIN = 'https://da.live/nx'; + /** * Builds hero block and prepends to main in a new section. * @param {Element} main The container element @@ -77,6 +80,7 @@ function mergeAdjacentButtonContainers(parent) { let child = parent.firstElementChild; while (child) { const next = child.nextElementSibling; + let merged = false; if ( next && child.classList.contains('button-container') @@ -89,11 +93,14 @@ function mergeAdjacentButtonContainers(parent) { if (a1 && a2 && child.children.length === 1 && next.children.length === 1) { while (next.firstChild) child.append(next.firstChild); next.remove(); - continue; + merged = true; } } - mergeAdjacentButtonContainers(child); - child = child.nextElementSibling; + if (!merged) { + mergeAdjacentButtonContainers(child); + child = child.nextElementSibling; + } + // After merge, keep same child so we can merge again with the new next sibling. } }