diff --git a/.gitignore b/.gitignore index bb675ff1dc8..bc110b6487d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ ehthumbs.db Thumbs.db .shopify +node_modules +*.zip \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index f6480c2e7a5..00000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["shopify.theme-check-vscode", "esbenp.prettier-vscode"] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 90abaf1f399..00000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "editor.formatOnSave": false, - "[javascript]": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[css]": { - "editor.formatOnSave": true - }, - "[liquid]": { - "editor.defaultFormatter": "Shopify.theme-check-vscode", - "editor.formatOnSave": true - }, - "themeCheck.checkOnSave": true -} diff --git a/assets/base.css b/assets/base.css index c8c0d4cbde1..54d8c8d365a 100644 --- a/assets/base.css +++ b/assets/base.css @@ -1,3 +1,8 @@ +product-component, +collection-component { + display: block; +} + :root { --alpha-button-background: 1; --alpha-button-border: 1; @@ -146,6 +151,16 @@ body:has(.section-header .drawer-menu) .announcement-bar-section .page-width { z-index: 0; } +.content-for-layout { + flex: 1; + display: flex; + flex-direction: column; +} + +.content-for-layout > .shopify-section:last-child { + flex-grow: 1; +} + .section + .section { margin-top: var(--spacing-sections-mobile); } diff --git a/assets/cart-disclosure-modal.js b/assets/cart-disclosure-modal.js new file mode 100644 index 00000000000..1d58e70ea30 --- /dev/null +++ b/assets/cart-disclosure-modal.js @@ -0,0 +1,192 @@ +if (!customElements.get('cart-disclosure-modal')) { + class CartDisclosureModal extends HTMLElement { + constructor() { + super(); + + this.onOpenerClick = this.onOpenerClick.bind(this); + this.onCloseClick = this.onCloseClick.bind(this); + this.onModalClick = this.onModalClick.bind(this); + this.onKeyUp = this.onKeyUp.bind(this); + this.removeIfOpenerDisconnected = this.removeIfOpenerDisconnected.bind(this); + } + + connectedCallback() { + if (this.initialized) return; + + this.initialized = true; + this.dialog = this.querySelector('[role="dialog"]'); + const controlsId = this.dialog?.id; + this.opener = this.previousElementSibling?.classList.contains('cart-item__disclosure') + ? this.previousElementSibling + : controlsId + ? document.querySelector(`[aria-controls="${controlsId}"]`) + : null; + this.closeButton = this.querySelector('[data-cart-disclosure-close]'); + + let shouldRestoreOpen = false; + document.querySelectorAll('cart-disclosure-modal').forEach((modal) => { + if (modal === this || modal.id !== this.id) return; + + shouldRestoreOpen = shouldRestoreOpen || modal.hasAttribute('open'); + modal.remove(); + }); + + this.opener?.addEventListener('click', this.onOpenerClick); + this.closeButton?.addEventListener('click', this.onCloseClick); + this.addEventListener('click', this.onModalClick); + this.addEventListener('keyup', this.onKeyUp); + + if (this.parentElement !== document.body) { + this.isMoving = true; + document.body.appendChild(this); + this.isMoving = false; + } + + this.observeOpener(); + + if (shouldRestoreOpen) this.show(); + } + + disconnectedCallback() { + if (this.isMoving) return; + + this.openerObserver?.disconnect(); + this.opener?.removeEventListener('click', this.onOpenerClick); + this.closeButton?.removeEventListener('click', this.onCloseClick); + this.removeEventListener('click', this.onModalClick); + this.removeEventListener('keyup', this.onKeyUp); + + if (this.hasAttribute('open')) this.hide(false); + } + + observeOpener() { + this.openerObserver?.disconnect(); + + if (!this.opener) return; + + this.openerObserver = new MutationObserver(this.removeIfOpenerDisconnected); + this.openerObserver.observe(document.body, { childList: true, subtree: true }); + this.removeIfOpenerDisconnected(); + } + + removeIfOpenerDisconnected() { + if (this.isMoving || !this.isConnected || this.opener?.isConnected) return; + + this.remove(); + } + + onOpenerClick(event) { + event.preventDefault(); + event.stopPropagation(); + this.show(); + } + + onCloseClick(event) { + event.preventDefault(); + event.stopPropagation(); + this.hide(); + } + + onModalClick(event) { + event.stopPropagation(); + + if (event.target === this) this.hide(); + } + + onKeyUp(event) { + if (event.code.toUpperCase() !== 'ESCAPE') return; + + event.preventDefault(); + event.stopPropagation(); + this.hide(); + } + + show() { + if (this.hasAttribute('open')) return; + + document.querySelectorAll('cart-disclosure-modal[open]').forEach((modal) => { + if (modal !== this) modal.hide(false); + }); + + this.lockScroll(); + this.setAttribute('open', ''); + this.opener?.setAttribute('aria-expanded', 'true'); + + if (typeof trapFocus === 'function') { + trapFocus(this, this.dialog); + } else { + this.dialog?.focus(); + } + + window.pauseAllMedia(); + } + + hide(restoreFocus = true) { + if (!this.hasAttribute('open')) return; + + this.removeAttribute('open'); + this.opener?.setAttribute('aria-expanded', 'false'); + this.unlockScroll(); + + if (restoreFocus) { + this.restoreFocusTrap(); + } else if (typeof removeTrapFocus === 'function') { + removeTrapFocus(); + } + + window.pauseAllMedia(); + } + + lockScroll() { + this.bodyOverflowWasHidden = document.body.classList.contains('overflow-hidden'); + this.previousBodyPaddingRight = document.body.style.paddingRight; + + if (this.bodyOverflowWasHidden) return; + + const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; + + if (scrollbarWidth > 0) { + const currentPaddingRight = parseFloat(window.getComputedStyle(document.body).paddingRight) || 0; + document.body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`; + } + + document.body.classList.add('overflow-hidden'); + } + + unlockScroll() { + if (this.bodyOverflowWasHidden) return; + + document.body.classList.remove('overflow-hidden'); + document.body.style.paddingRight = this.previousBodyPaddingRight; + } + + restoreFocusTrap() { + const cartDrawer = this.opener?.closest('cart-drawer.active'); + const cartNotification = this.opener?.closest('cart-notification')?.querySelector('#cart-notification.active'); + + if (cartDrawer && typeof trapFocus === 'function') { + const containerToTrapFocusOn = cartDrawer.classList.contains('is-empty') + ? cartDrawer.querySelector('.drawer__inner-empty') + : document.getElementById('CartDrawer'); + + if (containerToTrapFocusOn) { + trapFocus(containerToTrapFocusOn, this.opener); + return; + } + } + + if (cartNotification && typeof trapFocus === 'function') { + trapFocus(cartNotification, this.opener); + return; + } + + if (typeof removeTrapFocus === 'function') { + removeTrapFocus(this.opener); + } else { + this.opener?.focus(); + } + } + } + + customElements.define('cart-disclosure-modal', CartDisclosureModal); +} diff --git a/assets/cart-disclosure-tooltip.js b/assets/cart-disclosure-tooltip.js new file mode 100644 index 00000000000..9000414fcab --- /dev/null +++ b/assets/cart-disclosure-tooltip.js @@ -0,0 +1,45 @@ +(function () { + // Keeps the cart disclosure hover tooltip within the viewport. + // On hover we measure the tooltip and set a `--cart-disclosure-tooltip-shift` + // custom property that nudges it back into view; + // the arrow is counter-shifted in CSS so it keeps pointing at the icon. + const VIEWPORT_MARGIN = 16; + + function positionTooltip(button) { + const tooltip = button.querySelector('.cart-item__disclosure-tooltip'); + if (!tooltip) return; + + // Reset before measuring so we read the natural, centered position. + tooltip.style.setProperty('--cart-disclosure-tooltip-shift', '0px'); + + const rect = tooltip.getBoundingClientRect(); + const viewportWidth = document.documentElement.clientWidth; + let shift = 0; + + if (rect.right > viewportWidth - VIEWPORT_MARGIN) { + shift = viewportWidth - VIEWPORT_MARGIN - rect.right; + } else if (rect.left < VIEWPORT_MARGIN) { + shift = VIEWPORT_MARGIN - rect.left; + } + + tooltip.style.setProperty('--cart-disclosure-tooltip-shift', `${Math.round(shift)}px`); + } + + function onPointerOver(event) { + const button = event.target.closest?.('.cart-item__disclosure'); + if (!button || button.dataset.tooltipPositioned === 'true') return; + + button.dataset.tooltipPositioned = 'true'; + positionTooltip(button); + } + + function onPointerOut(event) { + const button = event.target.closest?.('.cart-item__disclosure'); + if (button && !button.contains(event.relatedTarget)) { + delete button.dataset.tooltipPositioned; + } + } + + document.addEventListener('pointerover', onPointerOver); + document.addEventListener('pointerout', onPointerOut); +})(); diff --git a/assets/cart-drawer.js b/assets/cart-drawer.js index ad37f3cb871..fe8c2c02c15 100644 --- a/assets/cart-drawer.js +++ b/assets/cart-drawer.js @@ -26,6 +26,7 @@ class CartDrawer extends HTMLElement { } open(triggeredBy) { + if (this.classList.contains('active')) return; if (triggeredBy) this.setActiveElement(triggeredBy); const cartDrawerNote = this.querySelector('[id^="Details-"] summary'); if (cartDrawerNote && !cartDrawerNote.hasAttribute('role')) this.setSummaryAccessibility(cartDrawerNote); @@ -43,10 +44,15 @@ class CartDrawer extends HTMLElement { const focusElement = this.querySelector('.drawer__inner') || this.querySelector('.drawer__close'); trapFocus(containerToTrapFocusOn, focusElement); }, - { once: true } + { once: true }, ); document.body.classList.add('overflow-hidden'); + + // cart-drawer-items is a CartItems subclass that extends createViewEventElement. + // Its `view-event-trigger="manual"` skips auto-dispatch on connect; we fire + // it here when the drawer opens, with `context: 'dialog'` from the payload attribute. + this.querySelector('cart-drawer-items')?.dispatchViewEvent(); } close() { diff --git a/assets/cart-notification.js b/assets/cart-notification.js index 7e8a06cdf2e..af421bff88b 100644 --- a/assets/cart-notification.js +++ b/assets/cart-notification.js @@ -25,6 +25,35 @@ class CartNotification extends HTMLElement { ); document.body.addEventListener('click', this.onBodyClick); + + this.dispatchCartViewEvent(); + } + + // The notification's outer element is server-rendered once at page load, so + // its `cart` Liquid object reflects the pre-add state. The morphed children + // (inserted from the /cart/add.js sections response in renderContents) are + // post-add, but they don't expose the full cart shape we need for the event + // payload. So we keep an explicit /cart.json fetch on open. Migrating to the + // factory + filter would require re-rendering the notification element + // itself in sections, which is out of scope for this PR. + async dispatchCartViewEvent() { + const { CartViewEvent } = window.StandardEvents || {}; + if (!CartViewEvent) return; + + try { + const response = await fetch(`${routes.cart_url}.json`); + const cart = await response.json(); + if (!cart?.currency) return; + + this.dispatchEvent( + new CartViewEvent({ + context: 'dialog', + cart: CartViewEvent.createCartFromAjaxResponse(cart), + }) + ); + } catch (e) { + // cart:view is informational; swallow fetch errors + } } close() { diff --git a/assets/cart.js b/assets/cart.js index eb9b75f698e..a8d5df671f2 100644 --- a/assets/cart.js +++ b/assets/cart.js @@ -12,7 +12,7 @@ class CartRemoveButton extends HTMLElement { customElements.define('cart-remove-button', CartRemoveButton); -class CartItems extends HTMLElement { +class CartItems extends window.StandardEvents.createViewEventElement(HTMLElement) { constructor() { super(); this.lineItemStatusElement = @@ -27,15 +27,37 @@ class CartItems extends HTMLElement { cartUpdateUnsubscriber = undefined; + static pendingCartDataPromise = null; + connectedCallback() { + // The factory base class auto-dispatches cart:view from the + // `view-event-payload` attribute (Liquid filter output). The drawer + // sets `view-event-trigger="manual"` to skip auto-dispatch. + super.connectedCallback(); + this.cartUpdateUnsubscriber = subscribe(PUB_SUB_EVENTS.cartUpdate, (event) => { - if (event.source === 'cart-items') { - return; - } + if (event.source === 'cart-items') return; return this.onCartUpdate(); }); } + // Fetches the full cart shape (used to resolve the cart:lines-update event + // promise after /cart/add.js, which only returns the added line — not the + // post-mutation cart aggregates). De-duplicated across concurrent callers. + static fetchCartData() { + if (!CartItems.pendingCartDataPromise) { + const pendingCartDataPromise = fetch(`${routes.cart_url}.json`) + .then((response) => response.json()) + .catch(() => null) + .finally(() => { + if (CartItems.pendingCartDataPromise === pendingCartDataPromise) CartItems.pendingCartDataPromise = null; + }); + + CartItems.pendingCartDataPromise = pendingCartDataPromise; + } + return CartItems.pendingCartDataPromise; + } + disconnectedCallback() { if (this.cartUpdateUnsubscriber) { this.cartUpdateUnsubscriber(); @@ -150,10 +172,19 @@ class CartItems extends HTMLElement { this.enableLoading(line); + const action = quantity === 0 ? 'remove' : 'update'; + const quantityInput = this.querySelector(`#Quantity-${line}`) || this.querySelector(`#Drawer-quantity-${line}`); + const lineVariantId = variantId || quantityInput?.dataset.quantityVariantId; + const lineKey = quantityInput?.dataset.quantityLineKey; + const linesUpdateDeferred = this.createCartLinesUpdateEvent(action, lineVariantId, quantity, lineKey); + + // Cache sections before the fetch so we read dataset.id while elements still exist in the DOM + const sectionsToRender = this.getSectionsToRender(); + const body = JSON.stringify({ line, quantity, - sections: this.getSectionsToRender().map((section) => section.section), + sections: sectionsToRender.map((section) => section.section), sections_url: window.location.pathname, }); @@ -164,6 +195,13 @@ class CartItems extends HTMLElement { .then((state) => { const parsedState = JSON.parse(state); + if (parsedState.errors) { + this.dispatchCartErrorEvent(parsedState.errors, 'INVALID'); + linesUpdateDeferred?.reject(new Error(parsedState.errors)); + } else { + this.resolveCartLinesUpdate(linesUpdateDeferred, parsedState); + } + CartPerformance.measure(`${eventTarget}:paint-updated-sections`, () => { const quantityElement = document.getElementById(`Quantity-${line}`) || document.getElementById(`Drawer-quantity-${line}`); @@ -182,7 +220,7 @@ class CartItems extends HTMLElement { if (cartFooter) cartFooter.classList.toggle('is-empty', parsedState.item_count === 0); if (cartDrawerWrapper) cartDrawerWrapper.classList.toggle('is-empty', parsedState.item_count === 0); - this.getSectionsToRender().forEach((section) => { + sectionsToRender.forEach((section) => { const elementToReplace = document.getElementById(section.id).querySelector(section.selector) || document.getElementById(section.id); @@ -208,7 +246,7 @@ class CartItems extends HTMLElement { cartDrawerWrapper ? trapFocus(cartDrawerWrapper, lineItem.querySelector(`[name="${name}"]`)) : lineItem.querySelector(`[name="${name}"]`).focus(); - } else if (parsedState.item_count === 0 && cartDrawerWrapper) { + } else if (parsedState.item_count === 0 && cartDrawerWrapper?.querySelector('.drawer__inner-empty')) { trapFocus(cartDrawerWrapper.querySelector('.drawer__inner-empty'), cartDrawerWrapper.querySelector('a')); } else if (document.querySelector('.cart-item') && cartDrawerWrapper) { trapFocus(cartDrawerWrapper, document.querySelector('.cart-item__name')); @@ -217,10 +255,12 @@ class CartItems extends HTMLElement { publish(PUB_SUB_EVENTS.cartUpdate, { source: 'cart-items', cartData: parsedState, variantId: variantId }); }) - .catch(() => { + .catch((e) => { this.querySelectorAll('.loading__spinner').forEach((overlay) => overlay.classList.add('hidden')); const errors = document.getElementById('cart-errors') || document.getElementById('CartDrawer-CartErrors'); - errors.textContent = window.cartStrings.error; + if (errors) errors.textContent = window.cartStrings.error; + this.dispatchCartErrorEvent(window.cartStrings.error, 'SERVICE_UNAVAILABLE'); + linesUpdateDeferred?.reject(e); }) .finally(() => { this.disableLoading(line); @@ -228,6 +268,39 @@ class CartItems extends HTMLElement { }); } + createCartLinesUpdateEvent(action, variantId, quantity, lineKey) { + const { CartLinesUpdateEvent } = window.StandardEvents || {}; + if (!CartLinesUpdateEvent || !variantId) return null; + // No AJAX line key on the row — likely cached HTML rendered before this + // attribute landed. Skip dispatch rather than emit an event with id: ''. + if (!lineKey) return null; + + const deferred = CartLinesUpdateEvent.createPromise(); + this.dispatchEvent( + new CartLinesUpdateEvent({ + action, + context: 'cart', + lines: [{ id: lineKey, quantity }], + promise: deferred.promise, + }) + ); + return deferred; + } + + resolveCartLinesUpdate(deferred, parsedState) { + if (!deferred) return; + const { CartLinesUpdateEvent } = window.StandardEvents || {}; + if (!CartLinesUpdateEvent) return; + + deferred.resolve({ cart: CartLinesUpdateEvent.createCartFromAjaxResponse(parsedState) }); + } + + dispatchCartErrorEvent(message, code) { + const { CartErrorEvent } = window.StandardEvents || {}; + if (!CartErrorEvent) return; + this.dispatchEvent(new CartErrorEvent({ error: message, code })); + } + updateLiveRegions(line, message) { const lineItemError = document.getElementById(`Line-item-error-${line}`) || document.getElementById(`CartDrawer-LineItemError-${line}`); @@ -285,13 +358,58 @@ if (!customElements.get('cart-note')) { this.addEventListener( 'input', debounce((event) => { - const body = JSON.stringify({ note: event.target.value }); - fetch(`${routes.cart_update_url}`, { ...fetchConfig(), ...{ body } }).then(() => - CartPerformance.measureFromEvent('note-update:user-action', event) - ); + const newNote = event.target.value; + const noteDeferred = this.dispatchNoteUpdateEvent(newNote); + + const body = JSON.stringify({ note: newNote }); + fetch(`${routes.cart_update_url}`, { ...fetchConfig(), ...{ body } }) + .then((r) => r.json()) + .then((cart) => { + if (!cart || cart.errors) { + throw Object.assign(new Error(cart?.errors), { code: 'INVALID' }); + } + + if (noteDeferred) { + const { CartNoteUpdateEvent } = window.StandardEvents || {}; + if (CartNoteUpdateEvent) { + noteDeferred.resolve({ cart: CartNoteUpdateEvent.createCartFromAjaxResponse(cart) }); + } + } + CartPerformance.measureFromEvent('note-update:user-action', event); + }) + .catch((e) => { + noteDeferred?.reject(e); + const { CartErrorEvent } = window.StandardEvents || {}; + if (CartErrorEvent) { + this.dispatchEvent( + new CartErrorEvent({ + error: e.message || 'Note update failed', + code: e.code || 'SERVICE_UNAVAILABLE', + }) + ); + } + }); }, ON_CHANGE_DEBOUNCE_TIMER) ); } + + dispatchNoteUpdateEvent(newNote) { + const { CartNoteUpdateEvent } = window.StandardEvents || {}; + if (!CartNoteUpdateEvent) return null; + + const context = this.closest('dialog') || this.closest('cart-drawer') ? 'dialog' : 'cart'; + const deferred = CartNoteUpdateEvent.createPromise(); + + this.dispatchEvent( + new CartNoteUpdateEvent({ + context, + note: newNote, + promise: deferred.promise, + }) + ); + + return deferred; + } } ); } diff --git a/assets/component-cart-items.css b/assets/component-cart-items.css index c41d3a040b4..903b17bf685 100644 --- a/assets/component-cart-items.css +++ b/assets/component-cart-items.css @@ -89,6 +89,263 @@ cart-items .title-wrapper-with-link { display: block; } +.cart-item__title { + align-items: flex-start; + display: flex; + font-size: calc(var(--font-heading-scale) * 1.5rem); + gap: 0.6rem; + line-height: calc(1 + 0.3 / max(1, var(--font-heading-scale))); +} + +.cart-item__title .cart-item__name { + flex: 0 1 auto; + min-width: 0; +} + +.cart-item__disclosure { + appearance: none; + background: none; + border: 0; + color: inherit; + cursor: pointer; + display: inline-flex; + flex: 0 0 auto; + font: inherit; + line-height: 1; + margin: 0; + padding: 0; + position: relative; + top: 0.2rem; +} + +.cart-item__disclosure-indicator { + --icon-stroke-width: 1.2; + + color: rgb(var(--color-foreground)); + height: 1em; + width: 1em; +} + +.cart-item__disclosure-tooltip { + --cart-disclosure-tooltip-symbol-height: 1.3rem; + --cart-disclosure-tooltip-symbol-width: calc(var(--cart-disclosure-tooltip-symbol-height) * 40 / 31); + + background-color: rgb(var(--color-background)); + border: var(--popup-border-width) solid rgba(var(--color-foreground), var(--popup-border-opacity)); + bottom: calc(100% + 0.9rem); + box-shadow: var(--popup-shadow-horizontal-offset) var(--popup-shadow-vertical-offset) + var(--popup-shadow-blur-radius) rgba(var(--color-shadow), var(--popup-shadow-opacity)); + color: rgb(var(--color-foreground)); + display: flex; + flex-direction: column; + font-family: var(--font-body-family); + font-size: 1.2rem; + font-style: var(--font-body-style); + font-weight: var(--font-body-weight); + gap: 0.6rem; + left: 50%; + letter-spacing: 0; + line-height: 1.35; + max-width: min(36rem, calc(100vw - 4rem)); + opacity: 0; + padding: 0.8rem 1rem; + pointer-events: none; + position: absolute; + text-align: left; + transform: translateX(calc(-50% + var(--cart-disclosure-tooltip-shift, 0px))); + transition: opacity var(--duration-short) ease, visibility var(--duration-short) ease; + visibility: hidden; + width: max-content; + z-index: 3; +} + +.cart-item__disclosure-tooltip-row { + align-items: flex-start; + column-gap: 0.6rem; + display: grid; + grid-template-columns: var(--cart-disclosure-tooltip-symbol-width) minmax(0, 1fr); +} + +.cart-item__disclosure-tooltip-symbol-slot { + align-items: flex-start; + display: inline-flex; + height: var(--cart-disclosure-tooltip-symbol-height); + justify-content: center; + margin-top: 0.16rem; + width: var(--cart-disclosure-tooltip-symbol-width); +} + +.cart-item__disclosure-tooltip-symbol { + display: block; + height: 100%; + max-width: 100%; + object-fit: contain; + width: auto; +} + +.cart-item__disclosure-tooltip-title { + min-width: 0; + overflow-wrap: anywhere; + white-space: normal; +} + +@media (hover: hover) and (pointer: fine) { + .cart-item__disclosure:hover .cart-item__disclosure-tooltip { + opacity: 1; + visibility: visible; + } +} + +.cart-disclosure-modal { + align-items: center; + background: rgba(var(--color-foreground), 0.58); + box-sizing: border-box; + display: flex; + height: 100%; + justify-content: center; + left: 0; + opacity: 0; + overflow: auto; + padding: 2rem; + pointer-events: none; + position: fixed; + top: 0; + visibility: hidden; + width: 100%; + z-index: -1; +} + +.cart-disclosure-modal[open] { + opacity: 1; + pointer-events: auto; + visibility: visible; + z-index: 1001; +} + +.cart-disclosure-modal__content { + background-color: rgb(var(--color-background)); + border: var(--popup-border-width) solid rgba(var(--color-foreground), var(--popup-border-opacity)); + border-radius: var(--popup-corner-radius); + box-sizing: border-box; + box-shadow: var(--popup-shadow-horizontal-offset) var(--popup-shadow-vertical-offset) + var(--popup-shadow-blur-radius) rgba(var(--color-shadow), var(--popup-shadow-opacity)); + color: rgb(var(--color-foreground)); + margin: auto; + max-height: calc(100vh - 4rem); + max-width: min(100%, 56rem); + min-width: min(100%, 39rem); + overflow-y: auto; + padding: 2rem; + position: relative; + width: fit-content; +} + +.cart-disclosure-modal__close { + align-items: center; + appearance: none; + background: none; + border: 0; + color: rgba(var(--color-foreground), 0.65); + cursor: pointer; + display: flex; + height: 3.6rem; + justify-content: center; + margin: 0; + padding: 0; + position: absolute; + right: 0.8rem; + top: 0.8rem; + width: 3.6rem; +} + +.cart-disclosure-modal__close:hover { + color: rgb(var(--color-foreground)); +} + +.cart-disclosure-modal__close .icon { + height: 1.6rem; + width: 1.6rem; +} + +.cart-disclosure-modal__title { + font-family: var(--font-heading-family); + font-size: calc(var(--font-heading-scale) * 1.8rem); + font-weight: var(--font-body-weight-bold); + line-height: calc(1 + 0.3 / max(1, var(--font-heading-scale))); + margin: 0 4rem 1.8rem 0; +} + +.cart-disclosure-modal__item + .cart-disclosure-modal__item { + margin-top: 2.4rem; +} + +.cart-disclosure-modal__item-header { + --cart-disclosure-modal-symbol-height: 1.8rem; + --cart-disclosure-modal-symbol-width: calc(var(--cart-disclosure-modal-symbol-height) * 40 / 31); + --cart-disclosure-modal-title-font-size: 1.4rem; + --cart-disclosure-modal-title-line-height-ratio: calc(1 + 0.35 / var(--font-body-scale)); + --cart-disclosure-modal-title-line-height: calc( + var(--cart-disclosure-modal-title-font-size) * var(--cart-disclosure-modal-title-line-height-ratio) + ); + + align-items: flex-start; + display: grid; + gap: 0.9rem; + grid-template-columns: var(--cart-disclosure-modal-symbol-width) minmax(0, 1fr); + margin-bottom: 0.8rem; +} + +.cart-disclosure-modal__symbol-slot { + align-items: flex-start; + display: inline-flex; + height: var(--cart-disclosure-modal-symbol-height); + justify-content: center; + margin-top: calc( + (var(--cart-disclosure-modal-title-line-height) - var(--cart-disclosure-modal-symbol-height)) / 2 + ); + width: var(--cart-disclosure-modal-symbol-width); +} + +.cart-disclosure-modal__symbol { + display: block; + height: 100%; + max-width: 100%; + object-fit: contain; + width: auto; +} + +.cart-disclosure-modal__item-title { + font-size: var(--cart-disclosure-modal-title-font-size); + font-weight: var(--font-body-weight-bold); + line-height: var(--cart-disclosure-modal-title-line-height-ratio); + margin: 0; +} + +.cart-disclosure-modal__item-content { + font-size: 1.3rem; + line-height: calc(1 + 0.45 / var(--font-body-scale)); +} + +.cart-disclosure-modal__item-content p { + margin: 0; +} + +.cart-disclosure-modal__item-content p + p { + margin-top: 1rem; +} + +@media screen and (max-width: 749px) { + .cart-disclosure-modal { + padding: 1.6rem; + } + + .cart-disclosure-modal__content { + max-height: calc(100vh - 3.2rem); + max-width: 100%; + padding: 1.8rem; + } +} + .cart-item__name:hover { text-decoration: underline; text-underline-offset: 0.3rem; diff --git a/assets/component-cart-notification.css b/assets/component-cart-notification.css index 7af62a676ce..d89366150cc 100644 --- a/assets/component-cart-notification.css +++ b/assets/component-cart-notification.css @@ -117,7 +117,27 @@ content: none; } +.cart-notification-product__title { + align-items: flex-start; + display: flex; + gap: 0.6rem; + margin-bottom: 0.5rem; +} + +.cart-notification-product__title .cart-notification-product__name { + margin: 0; +} + +.cart-notification-product__title .cart-item__disclosure { + font-size: calc(var(--font-heading-scale) * 1.5rem); +} + .cart-notification-product__name { margin-bottom: 0.5rem; margin-top: 0; } + +.cart-notification .cart-item__disclosure-tooltip { + bottom: auto; + top: calc(100% + 0.9rem); +} diff --git a/assets/component-disclosures.css b/assets/component-disclosures.css new file mode 100644 index 00000000000..a227d7f3e49 --- /dev/null +++ b/assets/component-disclosures.css @@ -0,0 +1,165 @@ +.disclosures { + container-type: inline-size; + --disclosures-icon-height: 1.6rem; + } + + .disclosures__heading { + margin-bottom: 2rem; + } + + .disclosures__details { + border-top: 0.1rem solid rgba(var(--color-foreground), 0.08); + border-bottom: 0.1rem solid rgba(var(--color-foreground), 0.08); + } + + .disclosures__summary { + padding: 2rem 0; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + list-style: none; + } + + .disclosures__summary::-webkit-details-marker { + display: none; + } + + .disclosures__details[open] .disclosures__summary { + padding-bottom: 0rem; + padding-bottom: 0.5rem; + } + + .disclosures__summary-content { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 0; + flex: 1; + max-width: calc(100% - 3rem); + word-break: break-word; + } + + .disclosures-summary-item { + display: inline-flex; + align-items: flex-start; + gap: 0.5rem; + } + + .disclosures-summary-item__open-title { + display: none; + } + + .disclosures__details[open] .disclosures-summary-item__title { + display: none; + } + + .disclosures__details[open] .disclosures-summary-item__open-title { + display: inline; + font-weight: var(--font-body-weight-bold); + } + + .disclosures-separator { + padding: 0 1rem; + } + + .disclosures__icon { + display: block; + flex-shrink: 0; + width: auto; + height: var(--disclosures-icon-height); + } + + .disclosures-summary-item .disclosures__icon, + .disclosures-item__header .disclosures__icon { + margin-block-start: calc((1em * calc(1 + 0.8 / var(--font-body-scale)) - var(--disclosures-icon-height)) / 2); + } + + .disclosures__content { + padding-top: 0; + padding-bottom: 2rem; + cursor: pointer; + } + + .disclosures-item { + position: relative; + cursor: auto; + } + + .disclosures-item::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 50rem; + right: 0; + cursor: pointer; + } + + .disclosures-item + .disclosures-item { + padding-top: 2rem; + } + + .disclosures-item:last-child { + padding-bottom: 2rem; + } + + .disclosures-item__content { + max-width: 50rem; + cursor: auto; + } + + .disclosures-item__content p:first-child { + margin: 0; + } + + .disclosures-item__header { + display: flex; + align-items: flex-start; + gap: 0.5rem; + padding-bottom: 0.5rem; + } + + .disclosures-item__header span, + .disclosures-item__header img { + margin: 0; + cursor: auto; + } + + .disclosures-item__header span { + font-weight: var(--font-body-weight-bold); + } + + .disclosures__details[open] .disclosures__summary-content > *:not(:first-child) { + display: none; + } + + .disclosures__summary > .icon-caret { + position: absolute; + right: auto; + inset-inline-end: 1.5rem; + top: calc(50% - 0.75rem); + width: 1.5rem; + height: 1.5rem; + flex-shrink: 0; + transition: transform 0.2s ease; + } + + .disclosures__details[open] .disclosures__summary > .icon-caret { + transform: rotate(180deg); + } + + @container (max-width: 700px) { + .disclosures__summary-content { + flex-direction: column; + align-items: flex-start; + } + + .disclosures__summary > .icon-caret { + top: calc(2rem + (1em * calc(1 + 0.8 / var(--font-body-scale)) - 1.5rem) / 2); + } + + .disclosures-separator { + display: none; + } + } diff --git a/assets/disclosures.js b/assets/disclosures.js new file mode 100644 index 00000000000..db044acb880 --- /dev/null +++ b/assets/disclosures.js @@ -0,0 +1,26 @@ +class DisclosuresContent extends HTMLElement { + constructor() { + super(); + this.addEventListener('click', (event) => { + if (event.target.closest('.disclosures-item__content')) return; + if ( + event.target.closest('.disclosures-item__header') && + event.target !== event.target.closest('.disclosures-item__header') + ) + return; + + const item = event.target.closest('.disclosures-item'); + if (item) { + const content = item.querySelector('.disclosures-item__content'); + if (content && event.clientX <= content.getBoundingClientRect().right) return; + } + + const details = this.closest('details'); + if (details) details.removeAttribute('open'); + }); + } +} + +if (!customElements.get('disclosures-content')) { + customElements.define('disclosures-content', DisclosuresContent); +} diff --git a/assets/facets.js b/assets/facets.js index 9305e5f3c69..e6f6406bfb0 100644 --- a/assets/facets.js +++ b/assets/facets.js @@ -32,10 +32,11 @@ class FacetFiltersForm extends HTMLElement { static renderPage(searchParams, event, updateURLHash = true) { FacetFiltersForm.searchParamsPrev = searchParams; const sections = FacetFiltersForm.getSections(); + const updateEvent = FacetFiltersForm.startUpdateEvent(searchParams); const countContainer = document.getElementById('ProductCount'); const countContainerDesktop = document.getElementById('ProductCountDesktop'); const loadingSpinners = document.querySelectorAll( - '.facets-container .loading__spinner, facet-filters-form .loading__spinner' + '.facets-container .loading__spinner, facet-filters-form .loading__spinner', ); loadingSpinners.forEach((spinner) => spinner.classList.remove('hidden')); document.getElementById('ProductGridContainer').querySelector('.collection').classList.add('loading'); @@ -51,31 +52,87 @@ class FacetFiltersForm extends HTMLElement { const filterDataUrl = (element) => element.url === url; FacetFiltersForm.filterData.some(filterDataUrl) - ? FacetFiltersForm.renderSectionFromCache(filterDataUrl, event) - : FacetFiltersForm.renderSectionFromFetch(url, event); + ? FacetFiltersForm.renderSectionFromCache(filterDataUrl, event, updateEvent) + : FacetFiltersForm.renderSectionFromFetch(url, event, updateEvent); }); if (updateURLHash) FacetFiltersForm.updateURLHash(searchParams); } - static renderSectionFromFetch(url, event) { + static startUpdateEvent(searchParams) { + const { SearchUpdateEvent, CollectionUpdateEvent } = window.StandardEvents || {}; + if (!SearchUpdateEvent && !CollectionUpdateEvent) return; + + const facetsForm = document.querySelector('facet-filters-form'); + const facetsContainer = document.querySelector('.facets-container'); + const collectionId = facetsContainer?.dataset.collectionId || null; + const collectionHandle = facetsContainer?.dataset.collectionHandle || ''; + const currentCount = parseInt(document.getElementById('ProductCount')?.dataset.productCount) || 0; + const urlSearchParams = new URLSearchParams(searchParams); + const isSearchPage = facetsContainer?.dataset.template === 'search'; + const isCollectionPage = facetsContainer?.dataset.template === 'collection'; + const dispatchTarget = facetsForm || document; + + let deferred; + + if (isSearchPage) { + deferred = SearchUpdateEvent.createPromise(); + dispatchTarget.dispatchEvent( + new SearchUpdateEvent({ + search: { + query: urlSearchParams.get('q') || '', + productFilters: SearchUpdateEvent.parseProductFilters(urlSearchParams), + sortKey: SearchUpdateEvent.getSortKey(urlSearchParams), + }, + promise: deferred.promise, + }), + ); + } else if (isCollectionPage) { + // collectionId is null for auto-generated collections (/collections/all, + // /collections/vendors/, etc.) — the SSE schema accepts that. + deferred = CollectionUpdateEvent.createPromise(); + dispatchTarget.dispatchEvent( + new CollectionUpdateEvent({ + collection: { id: collectionId, handle: collectionHandle, productsCount: currentCount }, + productFilters: CollectionUpdateEvent.parseProductFilters(urlSearchParams), + sortKey: CollectionUpdateEvent.getSortKey(urlSearchParams), + promise: deferred.promise, + }), + ); + } + + if (!deferred) return; + + return { + resolve: (filteredCount) => { + deferred.resolve({ [isSearchPage ? 'totalCount' : 'productsCount']: filteredCount }); + }, + reject: deferred.reject, + }; + } + + static renderSectionFromFetch(url, event, updateEvent) { fetch(url) .then((response) => response.text()) - .then((responseText) => { - const html = responseText; + .then((html) => { FacetFiltersForm.filterData = [...FacetFiltersForm.filterData, { html, url }]; - FacetFiltersForm.renderFilters(html, event); - FacetFiltersForm.renderProductGridContainer(html); - FacetFiltersForm.renderProductCount(html); - if (typeof initializeScrollAnimationTrigger === 'function') initializeScrollAnimationTrigger(html.innerHTML); + FacetFiltersForm.renderSection(html, event, updateEvent); + }) + .catch((error) => { + console.error(error); + updateEvent?.reject(error); }); } - static renderSectionFromCache(filterDataUrl, event) { + static renderSectionFromCache(filterDataUrl, event, updateEvent) { const html = FacetFiltersForm.filterData.find(filterDataUrl).html; + FacetFiltersForm.renderSection(html, event, updateEvent); + } + + static renderSection(html, event, updateEvent) { FacetFiltersForm.renderFilters(html, event); FacetFiltersForm.renderProductGridContainer(html); - FacetFiltersForm.renderProductCount(html); + FacetFiltersForm.renderProductCount(html, updateEvent); if (typeof initializeScrollAnimationTrigger === 'function') initializeScrollAnimationTrigger(html.innerHTML); } @@ -92,29 +149,35 @@ class FacetFiltersForm extends HTMLElement { }); } - static renderProductCount(html) { - const count = new DOMParser().parseFromString(html, 'text/html').getElementById('ProductCount').innerHTML; + static renderProductCount(html, updateEvent) { + const parsedHtml = new DOMParser().parseFromString(html, 'text/html'); + const sourceCount = parsedHtml.getElementById('ProductCount'); + const count = sourceCount.innerHTML; const container = document.getElementById('ProductCount'); const containerDesktop = document.getElementById('ProductCountDesktop'); container.innerHTML = count; + container.dataset.productCount = sourceCount.dataset.productCount || ''; + container.dataset.totalCount = sourceCount.dataset.totalCount || ''; container.classList.remove('loading'); if (containerDesktop) { containerDesktop.innerHTML = count; containerDesktop.classList.remove('loading'); } const loadingSpinners = document.querySelectorAll( - '.facets-container .loading__spinner, facet-filters-form .loading__spinner' + '.facets-container .loading__spinner, facet-filters-form .loading__spinner', ); loadingSpinners.forEach((spinner) => spinner.classList.add('hidden')); + + updateEvent?.resolve(parseInt(sourceCount.dataset.productCount) || 0); } static renderFilters(html, event) { const parsedHTML = new DOMParser().parseFromString(html, 'text/html'); const facetDetailsElementsFromFetch = parsedHTML.querySelectorAll( - '#FacetFiltersForm .js-filter, #FacetFiltersFormMobile .js-filter, #FacetFiltersPillsForm .js-filter' + '#FacetFiltersForm .js-filter, #FacetFiltersFormMobile .js-filter, #FacetFiltersPillsForm .js-filter', ); const facetDetailsElementsFromDom = document.querySelectorAll( - '#FacetFiltersForm .js-filter, #FacetFiltersFormMobile .js-filter, #FacetFiltersPillsForm .js-filter' + '#FacetFiltersForm .js-filter, #FacetFiltersFormMobile .js-filter, #FacetFiltersPillsForm .js-filter', ); // Remove facets that are no longer returned from the server @@ -164,14 +227,28 @@ class FacetFiltersForm extends HTMLElement { FacetFiltersForm.renderMobileCounts(countsToRender, document.getElementById(closestJSFilterID)); const newFacetDetailsElement = document.getElementById(closestJSFilterID); - const newElementSelector = newFacetDetailsElement.classList.contains('mobile-facets__details') - ? `.mobile-facets__close-button` - : `.facets__summary`; - const newElementToActivate = newFacetDetailsElement.querySelector(newElementSelector); const isTextInput = event.target.getAttribute('type') === 'text'; - if (newElementToActivate && !isTextInput) newElementToActivate.focus(); + if (!isTextInput) { + // Try to return focus to the same checkbox the user just toggled, + // re-selecting it from the freshly rendered HTML by its id. + const originatingInputId = event.target.id; + const matchingInput = originatingInputId + ? newFacetDetailsElement.querySelector(`#${CSS.escape(originatingInputId)}`) + : null; + + if (matchingInput) { + matchingInput.focus(); + } else { + // Fallback to summary/close button if the checkbox can't be found + const fallbackSelector = newFacetDetailsElement.classList.contains('mobile-facets__details') + ? `.mobile-facets__close-button` + : `.facets__summary`; + const fallbackElement = newFacetDetailsElement.querySelector(fallbackSelector); + if (fallbackElement) fallbackElement.focus(); + } + } } } } diff --git a/assets/global.js b/assets/global.js index 4adf04def10..68dc6070e8a 100644 --- a/assets/global.js +++ b/assets/global.js @@ -1070,6 +1070,8 @@ class VariantSelects extends HTMLElement { const target = this.getInputForEventTarget(event.target); this.updateSelectionMetadata(event); + this.dispatchProductSelectEvent(); + publish(PUB_SUB_EVENTS.optionValueSelectionChange, { data: { event, @@ -1080,6 +1082,69 @@ class VariantSelects extends HTMLElement { }); } + getAllSelectedOptions() { + const options = []; + this.querySelectorAll('fieldset, .product-form__input--dropdown').forEach((group) => { + const checked = group.querySelector('input:checked') || group.querySelector('select option[selected]'); + if (checked) { + options.push({ name: checked.dataset.optionName || '', value: checked.value }); + } + }); + return options; + } + + dispatchProductSelectEvent() { + const { ProductSelectEvent } = window.StandardEvents || {}; + if (!ProductSelectEvent) return; + + const deferred = ProductSelectEvent.createPromise(); + this.pendingSelectPromise = deferred; + + this.dispatchEvent( + new ProductSelectEvent({ + product: { + id: this.dataset.productId, + title: this.dataset.productTitle, + handle: this.dataset.productHandle, + }, + selectedOptions: this.getAllSelectedOptions(), + promise: deferred.promise, + }) + ); + } + + takePendingSelectPromise() { + const deferred = this.pendingSelectPromise; + this.pendingSelectPromise = null; + return deferred; + } + + resolvePendingSelectPromise(variant, sourceVariantSelects = this) { + const deferred = this.takePendingSelectPromise(); + if (!deferred) return; + + if (variant) { + deferred.resolve({ + variant: { + id: variant.id, + title: variant.title, + availableForSale: variant.available, + price: { + amount: sourceVariantSelects?.dataset.selectedPriceAmount, + currencyCode: sourceVariantSelects?.dataset.currencyCode, + }, + selectedOptions: this.getAllSelectedOptions(), + }, + }); + } else { + deferred.resolve({ variant: null }); + } + } + + rejectPendingSelectPromise(error) { + this.takePendingSelectPromise()?.reject(error); + } + updateSelectionMetadata({ target }) { const { value, tagName } = target; @@ -1241,6 +1306,69 @@ class BulkAdd extends HTMLElement { return this._requestStarted; } + getCartQuantityForLine(id) { + const input = this.querySelector(`#Quantity-${id}`); + return parseInt(input?.dataset.cartQuantity || input?.getAttribute('value') || '0', 10) || 0; + } + + startCartLinesUpdate(items) { + const { CartLinesUpdateEvent } = window.StandardEvents || {}; + if (!CartLinesUpdateEvent) return; + + const linesByAction = Object.entries(items).reduce((groups, [variantId, quantity]) => { + const nextQuantity = parseInt(quantity, 10); + const currentQuantity = this.getCartQuantityForLine(variantId); + + if (Number.isNaN(nextQuantity) || currentQuantity === nextQuantity) return groups; + + const action = currentQuantity === 0 ? 'add' : nextQuantity === 0 ? 'remove' : 'update'; + let line; + if (action === 'add') { + line = { merchandiseId: variantId, quantity: nextQuantity }; + } else { + const lineKey = this.querySelector(`[data-quantity-variant-id="${variantId}"]`)?.dataset.quantityLineKey; + // No AJAX line key on the row — likely cached HTML rendered before this + // attribute landed. Skip rather than emit an event with id: ''. + if (!lineKey) return groups; + line = { id: lineKey, quantity: nextQuantity }; + } + + if (!groups[action]) groups[action] = []; + groups[action].push(line); + return groups; + }, {}); + + const deferreds = Object.entries(linesByAction).map(([action, lines]) => { + const deferred = CartLinesUpdateEvent.createPromise(); + this.dispatchEvent( + new CartLinesUpdateEvent({ + action, + context: 'product', + lines, + promise: deferred.promise, + }) + ); + return deferred; + }); + + return { + resolve: (parsedState) => { + const payload = { cart: CartLinesUpdateEvent.createCartFromAjaxResponse(parsedState) }; + deferreds.forEach((deferred) => deferred.resolve(payload)); + }, + reject: (error) => { + deferreds.forEach((deferred) => deferred.reject(error)); + }, + }; + } + + dispatchCartErrorEvent(message, code) { + const { CartErrorEvent } = window.StandardEvents || {}; + if (!CartErrorEvent) return; + + this.dispatchEvent(new CartErrorEvent({ error: message, code })); + } + resetQuantityInput(id) { const input = this.querySelector(`#Quantity-${id}`); input.value = input.getAttribute('value'); diff --git a/assets/icon-warning.svg b/assets/icon-warning.svg new file mode 100644 index 00000000000..7656be9a090 --- /dev/null +++ b/assets/icon-warning.svg @@ -0,0 +1,4 @@ + diff --git a/assets/predictive-search.js b/assets/predictive-search.js index b30210be21c..7a56bc25b18 100644 --- a/assets/predictive-search.js +++ b/assets/predictive-search.js @@ -173,9 +173,13 @@ class PredictiveSearch extends SearchForm { if (this.cachedResults[queryKey]) { this.renderSearchResults(this.cachedResults[queryKey]); + const searchDeferred = this.dispatchSearchUpdateEvent(searchTerm); + searchDeferred?.resolve({ totalCount: this.getTotalResultCount() }); return; } + const searchDeferred = this.dispatchSearchUpdateEvent(searchTerm); + fetch(`${routes.predictive_search_url}?q=${encodeURIComponent(searchTerm)}§ion_id=predictive-search`, { signal: this.abortController.signal, }) @@ -197,17 +201,39 @@ class PredictiveSearch extends SearchForm { predictiveSearchInstance.cachedResults[queryKey] = resultsMarkup; }); this.renderSearchResults(resultsMarkup); + + searchDeferred?.resolve({ totalCount: this.getTotalResultCount() }); }) .catch((error) => { if (error?.code === 20) { // Code 20 means the call was aborted + searchDeferred?.reject(error); return; } + searchDeferred?.reject(error); this.close(); throw error; }); } + getTotalResultCount() { + return parseInt(this.predictiveSearchResults.querySelector('[data-total-results]')?.dataset.totalResults) || 0; + } + + dispatchSearchUpdateEvent(query) { + const { SearchUpdateEvent } = window.StandardEvents || {}; + if (!SearchUpdateEvent) return null; + + const deferred = SearchUpdateEvent.createPromise(); + this.dispatchEvent( + new SearchUpdateEvent({ + search: { query }, + promise: deferred.promise, + }) + ); + return deferred; + } + setLiveRegionLoadingState() { this.statusElement = this.statusElement || this.querySelector('.predictive-search-status'); this.loadingText = this.loadingText || this.getAttribute('data-loading-text'); diff --git a/assets/product-form.js b/assets/product-form.js index 7e9b9225c4f..ac22efe9270 100644 --- a/assets/product-form.js +++ b/assets/product-form.js @@ -42,17 +42,23 @@ if (!customElements.get('product-form')) { } config.body = formData; + const variantId = formData.get('id'); + const quantity = parseInt(formData.get('quantity')) || 1; + const linesUpdateDeferred = this.createCartLinesUpdateEvent(variantId, quantity); + fetch(`${routes.cart_add_url}`, config) .then((response) => response.json()) .then((response) => { if (response.status) { publish(PUB_SUB_EVENTS.cartError, { source: 'product-form', - productVariantId: formData.get('id'), + productVariantId: variantId, errors: response.errors || response.description, message: response.message, }); this.handleErrorMessage(response.description); + this.dispatchCartErrorEvent(response.description || response.message, 'INVALID'); + linesUpdateDeferred?.reject(new Error(response.description || response.message)); const soldOutMessage = this.submitButton.querySelector('.sold-out-message'); if (!soldOutMessage) return; @@ -62,15 +68,18 @@ if (!customElements.get('product-form')) { this.error = true; return; } else if (!this.cart) { + this.resolveCartLinesUpdate(linesUpdateDeferred); window.location = window.routes.cart_url; return; } + this.resolveCartLinesUpdate(linesUpdateDeferred); + const startMarker = CartPerformance.createStartingMarker('add:wait-for-subscribers'); if (!this.error) publish(PUB_SUB_EVENTS.cartUpdate, { source: 'product-form', - productVariantId: formData.get('id'), + productVariantId: variantId, cartData: response, }).then(() => { CartPerformance.measureFromMarker('add:wait-for-subscribers', startMarker); @@ -98,6 +107,8 @@ if (!customElements.get('product-form')) { }) .catch((e) => { console.error(e); + this.dispatchCartErrorEvent(e.message || 'Network error', 'SERVICE_UNAVAILABLE'); + linesUpdateDeferred?.reject(e); }) .finally(() => { this.submitButton.classList.remove('loading'); @@ -134,6 +145,45 @@ if (!customElements.get('product-form')) { } } + createCartLinesUpdateEvent(variantId, quantity) { + const { CartLinesUpdateEvent } = window.StandardEvents || {}; + if (!CartLinesUpdateEvent) return null; + + const deferred = CartLinesUpdateEvent.createPromise(); + this.dispatchEvent( + new CartLinesUpdateEvent({ + action: 'add', + context: 'product', + lines: [{ merchandiseId: variantId, quantity }], + promise: deferred.promise, + }) + ); + return deferred; + } + + resolveCartLinesUpdate(deferred) { + if (!deferred) return; + const { CartLinesUpdateEvent } = window.StandardEvents || {}; + if (!CartLinesUpdateEvent) return; + + const pendingCartDataPromise = typeof CartItems !== 'undefined' + ? CartItems.fetchCartData() + : fetch(`${routes.cart_url}.json`).then((response) => response.json()); + + pendingCartDataPromise + .then((cart) => { + if (!cart?.currency) return deferred.reject(new Error('Missing currency in cart response')); + deferred.resolve({ cart: CartLinesUpdateEvent.createCartFromAjaxResponse(cart) }); + }) + .catch((e) => deferred.reject(e)); + } + + dispatchCartErrorEvent(message, code) { + const { CartErrorEvent } = window.StandardEvents || {}; + if (!CartErrorEvent) return; + this.dispatchEvent(new CartErrorEvent({ error: message, code })); + } + get variantIdInput() { return this.form.querySelector('[name=id]'); } diff --git a/assets/product-info.js b/assets/product-info.js index 0efef79b331..d3cbd8ce722 100644 --- a/assets/product-info.js +++ b/assets/product-info.js @@ -90,7 +90,9 @@ if (!customElements.get('product-info')) { this.productModal?.remove(); const selector = updateFullPage ? "product-info[id^='MainProduct']" : 'product-info'; - const variant = this.getSelectedVariant(html.querySelector(selector)); + const sourceProductInfo = html.querySelector(selector); + const variant = this.getSelectedVariant(sourceProductInfo); + this.variantSelectors?.resolvePendingSelectPromise(variant, this.getVariantSelects(sourceProductInfo)); this.updateURL(productUrl, variant?.id); if (updateFullPage) { @@ -123,8 +125,6 @@ if (!customElements.get('product-info')) { this.pendingRequestUrl = null; const html = new DOMParser().parseFromString(responseText, 'text/html'); callback(html); - }) - .then(() => { // set focus to last clicked option value document.querySelector(`#${targetId}`)?.focus(); }) @@ -134,12 +134,24 @@ if (!customElements.get('product-info')) { } else { console.error(error); } + this.variantSelectors?.rejectPendingSelectPromise(error); }); } + parseJsonScript(parent, selector) { + try { + return JSON.parse(parent?.querySelector(selector)?.textContent); + } catch { + return null; + } + } + + getVariantSelects(queryRoot) { + return queryRoot?.querySelector('variant-selects'); + } + getSelectedVariant(productInfoNode) { - const selectedVariant = productInfoNode.querySelector('variant-selects [data-selected-variant]')?.innerHTML; - return !!selectedVariant ? JSON.parse(selectedVariant) : null; + return this.parseJsonScript(this.getVariantSelects(productInfoNode), '[data-selected-variant]'); } buildRequestUrlWithParams(url, optionValues, shouldFetchFullPage = false) { @@ -163,8 +175,12 @@ if (!customElements.get('product-info')) { handleUpdateProductInfo(productUrl) { return (html) => { + const sourceVariantSelects = this.getVariantSelects(html); const variant = this.getSelectedVariant(html); + // Resolve product:select promise before updateOptionValues replaces the variant-selects DOM element + this.variantSelectors?.resolvePendingSelectPromise(variant, sourceVariantSelects); + this.pickupAvailability?.update(variant); this.updateOptionValues(html); this.updateURL(productUrl, variant?.id); diff --git a/assets/quick-add-bulk.js b/assets/quick-add-bulk.js index a3ec6582233..9cfc6e13b6e 100644 --- a/assets/quick-add-bulk.js +++ b/assets/quick-add-bulk.js @@ -115,6 +115,7 @@ if (!customElements.get('quick-add-bulk')) { this.selectProgressBar().classList.remove('hidden'); const ids = Object.keys(items); + const linesUpdate = this.startCartLinesUpdate(items); const body = JSON.stringify({ updates: items, sections: this.getSectionsToRender().map((section) => section.section), @@ -122,15 +123,23 @@ if (!customElements.get('quick-add-bulk')) { }); fetch(`${routes.cart_update_url}`, { ...fetchConfig(), ...{ body } }) - .then((response) => { - return response.text(); - }) - .then((state) => { - const parsedState = JSON.parse(state); + .then((response) => response.json()) + .then((parsedState) => { + if (parsedState.errors) { + throw Object.assign(new Error(parsedState.errors), { code: 'INVALID' }); + } + + linesUpdate?.resolve(parsedState); this.renderSections(parsedState, ids); publish(PUB_SUB_EVENTS.cartUpdate, { source: 'quick-add', cartData: parsedState }); }) - .catch(() => { + .catch((e) => { + if (e.code !== 'INVALID') console.error(e); + this.dispatchCartErrorEvent( + e.code === 'INVALID' ? e.message : window.cartStrings.error, + e.code || 'SERVICE_UNAVAILABLE' + ); + linesUpdate?.reject(e); // Commented out for now and will be fixed when BE issue is done https://github.com/Shopify/shopify/issues/440605 // e.target.setCustomValidity(error); // e.target.reportValidity(); diff --git a/assets/quick-add.js b/assets/quick-add.js index 5125a974c2f..5f7d59e5962 100644 --- a/assets/quick-add.js +++ b/assets/quick-add.js @@ -39,6 +39,8 @@ if (!customElements.get('quick-add-modal')) { } if (window.ProductModel) window.ProductModel.loadShopifyXR(); + this.modalContent.querySelector('product-component')?.dispatchViewEvent?.(); + super.show(opener); }) .finally(() => { diff --git a/assets/quick-order-list.js b/assets/quick-order-list.js index ab5eb56495e..c92868cc4a0 100644 --- a/assets/quick-order-list.js +++ b/assets/quick-order-list.js @@ -103,6 +103,13 @@ if (!customElements.get('quick-order-list')) { return JSON.parse(this.querySelector('[data-cart-contents]')?.innerHTML || '[]'); } + getCartQuantityForLine(id) { + const cartQuantity = super.getCartQuantityForLine(id); + if (cartQuantity > 0) return cartQuantity; + + return this.cartVariantsForProduct.includes(parseInt(id, 10)) ? 1 : 0; + } + onChange(event) { const inputValue = parseInt(event.target.value); this.cleanErrorMessageOnType(event); @@ -328,6 +335,7 @@ if (!customElements.get('quick-order-list')) { this.toggleLoading(true); const url = this.dataset.url || window.location.pathname; + const linesUpdate = this.startCartLinesUpdate(items); const body = JSON.stringify({ updates: items, @@ -339,9 +347,13 @@ if (!customElements.get('quick-order-list')) { this.setErrorMessage(); fetch(`${routes.cart_update_url}`, { ...fetchConfig(), ...{ body } }) - .then((response) => response.text()) - .then(async (state) => { - const parsedState = JSON.parse(state); + .then((response) => response.json()) + .then((parsedState) => { + if (parsedState.errors) { + throw Object.assign(new Error(parsedState.errors), { code: 'INVALID' }); + } + + linesUpdate?.resolve(parsedState); this.renderSections(parsedState); publish(PUB_SUB_EVENTS.cartUpdate, { source: this.id, @@ -349,8 +361,13 @@ if (!customElements.get('quick-order-list')) { }); }) .catch((e) => { - console.error(e); - this.setErrorMessage(window.cartStrings.error); + if (e.code !== 'INVALID') console.error(e); + this.dispatchCartErrorEvent( + e.code === 'INVALID' ? e.message : window.cartStrings.error, + e.code || 'SERVICE_UNAVAILABLE' + ); + linesUpdate?.reject(e); + this.setErrorMessage(e.code === 'INVALID' ? e.message : window.cartStrings.error); }) .finally(() => { this.queue.length === 0 && this.toggleLoading(false); diff --git a/assets/standard-actions-override.js b/assets/standard-actions-override.js new file mode 100644 index 00000000000..226124e2617 --- /dev/null +++ b/assets/standard-actions-override.js @@ -0,0 +1,157 @@ +/** + * Standard Actions configuration for Dawn. + * + * Storefront Renderer injects the Shopify Standard Actions bundle + * (`window.Shopify.actions.{updateCart,openCart,getCart,…}`). This file + * overrides the bundle's built-in Dawn refresh path with an explicit, + * in-theme version so forks that change Dawn's cart contract keep + * working. Remove this file and the built-in defaults take over. + * + * - openCart — opens ; falls back to /cart. + * - updateCart — after the Storefront API mutation, refreshes the + * affected cart sections and publishes `cart-update` so Dawn's + * pubsub subscribers react. + * - other actions (getCart, etc.) keep the default implementation. + */ + +// Cart custom elements that advertise sections via getSectionsToRender(). +// If Dawn adds a new cart custom element, add its tag here. +const DAWN_CART_TAGS = ['cart-drawer', 'cart-items', 'cart-drawer-items', 'cart-notification']; + +// Sections that Dawn's own pubsub subscribers refresh (cart.js's +// CartItems#onCartUpdate fetches and replaces these directly when +// cart-update fires; cart-drawer.js's renderContents handles the +// drawer body). We skip them here to avoid double-rendering. +// Format is ':'. +// If you change which sections those subscribers refresh, update this set. +const DAWN_PUBSUB_REFRESHED_SECTIONS = new Set([ + 'cart-drawer:cart-drawer', + 'cart-drawer-items:CartDrawer', + 'cart-items:main-cart-items', +]); + +// Walk every mounted Dawn cart custom element, collect the sections it +// wants rendered, and dedupe. Returns a Map keyed by section id, each +// entry pointing at the DOM mount and the selector used to extract the +// fresh fragment. +function collectCartSections() { + const sections = new Map(); + + for (const el of document.querySelectorAll(DAWN_CART_TAGS.join(','))) { + let entries; + try { + entries = el.getSectionsToRender?.(); + } catch { + continue; + } + + const tag = el.tagName.toLowerCase(); + for (const entry of entries ?? []) { + if (DAWN_PUBSUB_REFRESHED_SECTIONS.has(`${tag}:${entry.id}`)) continue; + + const sectionId = entry.section ?? entry.id; + if (!sectionId || sections.has(sectionId)) continue; + + // Two patterns coexist in getSectionsToRender(): + // - cart-items style: entry.section is the parent Liquid section + // id, entry.selector is a child node inside it. + // - cart-drawer / cart-notification style: entry.id IS the mount; + // there is no parent wrapper. + const root = entry.section ? document.getElementById(entry.id) : document; + if (!root) continue; + + const mount = entry.selector + ? (root.querySelector(entry.selector) ?? (entry.section ? root : null)) + : document.getElementById(entry.id); + if (!mount) continue; + + sections.set(sectionId, { + mount, + extractSelector: entry.selector || '.shopify-section', + }); + } + } + + return sections; +} + +// After a Storefront API mutation, refresh every Dawn cart section +// that isn't already refreshed by Dawn's own pubsub subscribers, then +// publish 'cart-update' so the subscribers run. +// +// We always fetch /cart.js (with sections= when we have any) so that +// `cartData` is defined for subscribers. quick-add-bulk.js reads +// `event.cartData.items` unconditionally — publishing without cartData +// makes it throw. +async function refreshDawnCartUI() { + const sections = collectCartSections(); + const sectionsQuery = sections.size + ? `?sections=${[...sections.keys()].join(',')}` + : ''; + // `routes` is a Dawn global, but don't assume it's defined. + const cartUrl = (typeof routes !== 'undefined' && routes?.cart_url) || '/cart'; + const url = `${cartUrl}.js${sectionsQuery}`; + const cartData = await fetch(url, { headers: { Accept: 'application/json' } }) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null); + + if (cartData?.sections) { + for (const [id, { mount, extractSelector }] of sections) { + const html = cartData.sections[id]; + if (!html) continue; + const source = new DOMParser() + .parseFromString(html, 'text/html') + .querySelector(extractSelector); + if (source) mount.replaceChildren(...source.childNodes); + } + } + + // Hand off to Dawn's existing subscribers. cartData is the full + // Cart Ajax payload (items, item_count, token, …) plus sections; + // quick-add-bulk.js and price-per-item.js read it directly. + publish(PUB_SUB_EVENTS.cartUpdate, { + source: 'external-refresh', + cartData: cartData ?? undefined, + }); +} + +function initStandardActions() { + const actions = window.Shopify?.actions; + if (!actions) return; + + actions.openCart.configure({ + async handler(defaultHandler) { + const drawer = document.querySelector('cart-drawer'); + if (drawer && typeof drawer.open === 'function') { + drawer.open(); + return; + } + return defaultHandler(); + }, + }); + + actions.updateCart.configure({ + // Dawn doesn't currently listen for shopify:cart:* events, but the + // bundle requires an eventTarget. document is the conventional root. + eventTarget: () => document, + async handler(defaultHandler) { + const result = await defaultHandler(); + try { + await refreshDawnCartUI(); + } catch (error) { + console.error('[Dawn] Standard Actions cart refresh failed; reloading.', error); + window.location.reload(); + } + return result; + }, + }); +} + +// Run immediately if the standard-actions bundle has already attached +// `Shopify.actions`; otherwise wait for DOMContentLoaded, which fires after +// all module scripts have executed regardless of document order. +if (window.Shopify?.actions) { + initStandardActions(); +} else { + document.addEventListener('DOMContentLoaded', initStandardActions, { once: true }); +} diff --git a/config/settings_data.json b/config/settings_data.json index 0f7a06db992..d6c15957e01 100644 --- a/config/settings_data.json +++ b/config/settings_data.json @@ -1,7 +1,7 @@ { - "current": "Default", + "current": "Dawn", "presets": { - "Default": { + "Dawn": { "logo_width": 90, "color_schemes": { "scheme-1": { @@ -68,6 +68,8 @@ "spacing_sections": 0, "spacing_grid_horizontal": 8, "spacing_grid_vertical": 8, + "animations_reveal_on_scroll": true, + "animations_hover_elements": "none", "buttons_border_thickness": 1, "buttons_border_opacity": 100, "buttons_radius": 0, @@ -151,8 +153,11 @@ "drawer_shadow_blur": 5, "badge_position": "bottom left", "badge_corner_radius": 40, - "sale_badge_color_scheme": "scheme-5", + "sale_badge_color_scheme": "scheme-4", "sold_out_badge_color_scheme": "scheme-3", + "brand_headline": "", + "brand_description": "

", + "brand_image_width": 100, "social_twitter_link": "", "social_facebook_link": "", "social_pinterest_link": "", @@ -170,6 +175,7 @@ "show_vendor": false, "show_cart_note": false, "cart_drawer_collection": "", + "cart_color_scheme": "scheme-1", "sections": { "main-password-header": { "type": "main-password-header", diff --git a/config/settings_schema.json b/config/settings_schema.json index bcaab5b5d2a..f6b985fdbb3 100644 --- a/config/settings_schema.json +++ b/config/settings_schema.json @@ -2,7 +2,7 @@ { "name": "theme_info", "theme_name": "Dawn", - "theme_version": "15.4.1", + "theme_version": "15.5.0", "theme_author": "Shopify", "theme_documentation_url": "https://help.shopify.com/manual/online-store/themes", "theme_support_url": "https://support.shopify.com/" diff --git a/layout/theme.liquid b/layout/theme.liquid index 70db635823b..43aeb6ddb6d 100644 --- a/layout/theme.liquid +++ b/layout/theme.liquid @@ -28,12 +28,36 @@ {% render 'meta-tags' %} + + + + + {%- if settings.animations_reveal_on_scroll -%} @@ -235,9 +259,8 @@ } body { - display: grid; - grid-template-rows: auto auto 1fr auto; - grid-template-columns: 100%; + display: flex; + flex-direction: column; min-height: 100%; margin: 0; font-size: 1.5rem; @@ -309,7 +332,13 @@ {% sections 'header-group' %} -
+
{{ content_for_layout }}
diff --git a/locales/bg.json b/locales/bg.json index 3151edfb2b0..753faac6716 100644 --- a/locales/bg.json +++ b/locales/bg.json @@ -281,6 +281,8 @@ "remove_title": "Премахване на {{ title }}", "checkout": "Преминаване към плащане", "empty": "Количката ви е празна.", + "product_disclosure": "Оповестяване за продукта", + "product_disclosures": "Оповестявания за продукта", "cart_error": "При актуализирането на количката ви възникна грешка. Опитайте отново.", "cart_quantity_error_html": "Можете да добавите само {{ quantity }} броя от този артикул в количката си.", "headings": { diff --git a/locales/cs.json b/locales/cs.json index f9b3c5269fd..facbbcca987 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -302,6 +302,8 @@ "note": "Zvláštní pokyny k objednávce", "checkout": "Pokladna", "empty": "Košík je prázdný", + "product_disclosure": "Povinná informace o produktu", + "product_disclosures": "Povinné informace o produktu", "cart_error": "Při aktualizaci vašeho košíku došlo k chybě. Zkuste to prosím znovu.", "cart_quantity_error_html": "Do košíku můžete přidat jen následující množství dané položky: {{ quantity }}.", "update": "Aktualizovat", diff --git a/locales/cs.schema.json b/locales/cs.schema.json index df77dbb3b47..e08c17240f0 100644 --- a/locales/cs.schema.json +++ b/locales/cs.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Seznam pro rychlé objednávky" } + }, + "disclosures": { + "name": "Prohlášení", + "settings": { + "paragraph": { + "content": "Povinná bezpečnostní varování a zákonná prohlášení pro tento produkt." + }, + "heading": { + "label": "Nadpis" + }, + "open_by_default": { + "label": "Otevřeno ve výchozím stavu" + } + } } } } diff --git a/locales/da.json b/locales/da.json index eb6db4bca16..9f160e589ac 100644 --- a/locales/da.json +++ b/locales/da.json @@ -282,6 +282,8 @@ "note": "Særlige instruktioner til ordre", "checkout": "Gå til betaling", "empty": "Din indkøbskurv er tom", + "product_disclosure": "Produktdeklaration", + "product_disclosures": "Produktdeklarationer", "cart_error": "Der opstod en fejl under opdatering af din indkøbskurv. Prøv igen.", "cart_quantity_error_html": "Du kan kun lægge {{ quantity }} af denne vare i indkøbskurven.", "headings": { diff --git a/locales/da.schema.json b/locales/da.schema.json index 6425ca6ac16..6a0b1fea331 100644 --- a/locales/da.schema.json +++ b/locales/da.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Hurtig ordreliste" } + }, + "disclosures": { + "name": "Oplysninger", + "settings": { + "paragraph": { + "content": "Obligatoriske sikkerhedsadvarsler og lovpligtige oplysninger for dette produkt." + }, + "heading": { + "label": "Overskrift" + }, + "open_by_default": { + "label": "Åben som standard" + } + } } } } diff --git a/locales/de.json b/locales/de.json index 3084491928f..c0dfd0872e0 100644 --- a/locales/de.json +++ b/locales/de.json @@ -282,6 +282,8 @@ "note": "Spezielle Bestellanweisungen", "checkout": "Auschecken", "empty": "Dein Warenkorb ist leer", + "product_disclosure": "Produktoffenlegung", + "product_disclosures": "Produktoffenlegungen", "cart_error": "Beim Aktualisieren deines Warenkorbs ist ein Fehler aufgetreten. Bitte versuche es erneut.", "cart_quantity_error_html": "Du kannst deinem Warenkorb nur {{ quantity }} Stück dieses Artikels hinzufügen.", "headings": { diff --git a/locales/de.schema.json b/locales/de.schema.json index ec02e67e90f..1a3daef16ac 100644 --- a/locales/de.schema.json +++ b/locales/de.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Schnelle Bestellliste" } + }, + "disclosures": { + "name": "Pflichtangaben", + "settings": { + "paragraph": { + "content": "Erforderliche Sicherheitshinweise und gesetzliche Pflichtangaben für dieses Produkt." + }, + "heading": { + "label": "Überschrift" + }, + "open_by_default": { + "label": "Standardmäßig geöffnet" + } + } } } } diff --git a/locales/el.json b/locales/el.json index 124dcee6232..9e0993c8991 100644 --- a/locales/el.json +++ b/locales/el.json @@ -282,6 +282,8 @@ "note": "Ειδικές οδηγίες παραγγελίας", "checkout": "Ολοκλήρωση αγοράς", "empty": "Το καλάθι σας είναι κενό", + "product_disclosure": "Γνωστοποίηση προϊόντος", + "product_disclosures": "Γνωστοποιήσεις προϊόντος", "cart_error": "Παρουσιάστηκε σφάλμα κατά την ενημέρωση του καλαθιού. Δοκιμάστε ξανά.", "cart_quantity_error_html": "Μπορείτε να προσθέσετε μόνο {{ quantity }} από αυτό το προϊόν στο καλάθι σας.", "headings": { diff --git a/locales/en.default.json b/locales/en.default.json index 6e008e7ebc7..a2dee4fc3f9 100644 --- a/locales/en.default.json +++ b/locales/en.default.json @@ -292,6 +292,8 @@ "note": "Order special instructions", "checkout": "Check out", "empty": "Your cart is empty", + "product_disclosure": "Product disclosure", + "product_disclosures": "Product disclosures", "cart_error": "There was an error while updating your cart. Please try again.", "cart_quantity_error_html": "You can only add {{ quantity }} of this item to your cart.", "duties_and_taxes_included_shipping_at_checkout_with_policy_html": "Duties and taxes included. Discounts and shipping calculated at checkout.", @@ -528,4 +530,4 @@ "send_on_label": "Send on (optional)" } } -} \ No newline at end of file +} diff --git a/locales/en.default.schema.json b/locales/en.default.schema.json index 8a0ca35854d..403dc1f9d27 100644 --- a/locales/en.default.schema.json +++ b/locales/en.default.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Collapsible content" } + }, + "disclosures": { + "name": "Disclosures", + "settings": { + "paragraph": { + "content": "Required safety warnings and regulatory disclosures for this product." + }, + "heading": { + "label": "Heading" + }, + "open_by_default": { + "label": "Open by default" + } + } } } } diff --git a/locales/es.json b/locales/es.json index efcef516b12..59b185e7f95 100644 --- a/locales/es.json +++ b/locales/es.json @@ -292,6 +292,8 @@ "note": "Instrucciones especiales del pedido", "checkout": "Pagar pedido", "empty": "Tu carrito esta vacío", + "product_disclosure": "Aviso sobre el producto", + "product_disclosures": "Avisos sobre el producto", "cart_error": "Hubo un error al actualizar tu carrito de compra. Inténtalo de nuevo.", "cart_quantity_error_html": "Solo puedes agregar {{ quantity }} de este artículo a tu carrito.", "headings": { diff --git a/locales/es.schema.json b/locales/es.schema.json index 4a526fc5b30..5be62fc27cf 100644 --- a/locales/es.schema.json +++ b/locales/es.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Lista de pedidos rápidos" } + }, + "disclosures": { + "name": "Divulgaciones", + "settings": { + "paragraph": { + "content": "Advertencias de seguridad y divulgaciones normativas obligatorias para este producto." + }, + "heading": { + "label": "Título" + }, + "open_by_default": { + "label": "Abrir de forma predeterminada" + } + } } } } diff --git a/locales/fi.json b/locales/fi.json index 759050e37a7..10e4c3ea584 100644 --- a/locales/fi.json +++ b/locales/fi.json @@ -282,6 +282,8 @@ "note": "Tilauksen erityisohjeet", "checkout": "Kassa", "empty": "Ostoskorisi on tyhjä", + "product_disclosure": "Tuoteilmoitus", + "product_disclosures": "Tuoteilmoitukset", "cart_error": "Ostoskorisi päivityksessä tapahtui virhe. Yritä uudelleen.", "cart_quantity_error_html": "Voit lisätä ostoskoriisi vain {{ quantity }} kappaletta tätä tuotetta.", "headings": { diff --git a/locales/fi.schema.json b/locales/fi.schema.json index 7e613ac9aec..ddfdff605db 100644 --- a/locales/fi.schema.json +++ b/locales/fi.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Pikatilausluettelo" } + }, + "disclosures": { + "name": "Ilmoitukset", + "settings": { + "paragraph": { + "content": "Tämän tuotteen pakolliset turvallisuusvaroitukset ja säädösten edellyttämät ilmoitukset." + }, + "heading": { + "label": "Otsikko" + }, + "open_by_default": { + "label": "Avoinna oletuksena" + } + } } } } diff --git a/locales/fr.json b/locales/fr.json index 1aa38a1cef2..0c749621d8d 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -292,6 +292,8 @@ "note": "Instructions spéciales concernant la commande", "checkout": "Procéder au paiement", "empty": "Votre panier est vide", + "product_disclosure": "Information sur le produit", + "product_disclosures": "Informations sur le produit", "cart_error": "Une erreur est survenue lors de l’actualisation de votre panier. Veuillez réessayer.", "cart_quantity_error_html": "Vous ne pouvez pas ajouter plus de {{ quantity }} de ce produit à votre panier.", "headings": { diff --git a/locales/fr.schema.json b/locales/fr.schema.json index 510c1665b72..f49f50fdaf4 100644 --- a/locales/fr.schema.json +++ b/locales/fr.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Liste rapide des commandes" } + }, + "disclosures": { + "name": "Divulgations", + "settings": { + "paragraph": { + "content": "Avertissements de sécurité obligatoires et informations réglementaires pour ce produit." + }, + "heading": { + "label": "Titre" + }, + "open_by_default": { + "label": "Ouvert par défaut" + } + } } } } diff --git a/locales/hr.json b/locales/hr.json index 4b84b681925..d56ad2ec53c 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -292,6 +292,8 @@ "note": "Ostale posebne upute", "checkout": "Završi kupnju", "empty": "Vaša je košarica prazna", + "product_disclosure": "Obavijest o proizvodu", + "product_disclosures": "Obavijesti o proizvodu", "cart_error": "Došlo je do pogreške prilikom ažuriranja košarice. Pokušajte ponovno.", "cart_quantity_error_html": "U svoju košaricu možete dodati {{ quantity }} kom ovog artikla.", "update": "Ažuriraj", diff --git a/locales/hu.json b/locales/hu.json index 92121966838..60c2da36e3a 100644 --- a/locales/hu.json +++ b/locales/hu.json @@ -282,6 +282,8 @@ "note": "Megjegyzések a rendeléssel kapcsolatban", "checkout": "Megrendelés", "empty": "A kosarad üres", + "product_disclosure": "Terméktájékoztató", + "product_disclosures": "Terméktájékoztatók", "cart_error": "Hiba történt a kosár frissítése közben. Próbálkozz újra.", "cart_quantity_error_html": "Ebből a termékből legfeljebb {{ quantity }} darabot rakhatsz a kosárba.", "headings": { diff --git a/locales/id.json b/locales/id.json index 5c566637953..2b7641f0549 100644 --- a/locales/id.json +++ b/locales/id.json @@ -282,6 +282,8 @@ "note": "Instruksi khusus pesanan", "checkout": "Check out", "empty": "Keranjang Anda kosong", + "product_disclosure": "Pengungkapan produk", + "product_disclosures": "Pengungkapan produk", "cart_error": "Terjadi kesalahan saat memperbarui keranjang. Silakan coba lagi.", "cart_quantity_error_html": "Hanya dapat menambahkan {{ quantity }} item ini ke keranjang Anda.", "headings": { diff --git a/locales/it.json b/locales/it.json index 2ee7ceaa28e..35109e6ce52 100644 --- a/locales/it.json +++ b/locales/it.json @@ -292,6 +292,8 @@ "note": "Istruzioni speciali per l'ordine", "checkout": "Check-out", "empty": "Il tuo carrello è vuoto", + "product_disclosure": "Informativa sul prodotto", + "product_disclosures": "Informative sul prodotto", "cart_error": "Si è verificato un errore durante l'aggiornamento del carrello. Riprova più tardi.", "cart_quantity_error_html": "Puoi aggiungere soltanto {{ quantity }} di questo articolo al tuo carrello.", "headings": { diff --git a/locales/it.schema.json b/locales/it.schema.json index 5d6a4b26c89..db4084871fc 100644 --- a/locales/it.schema.json +++ b/locales/it.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Elenco ordini rapido" } + }, + "disclosures": { + "name": "Informative", + "settings": { + "paragraph": { + "content": "Avvertenze di sicurezza e informative normative obbligatorie per questo prodotto." + }, + "heading": { + "label": "Titolo" + }, + "open_by_default": { + "label": "Aperto per impostazione predefinita" + } + } } } } diff --git a/locales/ja.json b/locales/ja.json index 62bc9733bb0..382eab57e0c 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -282,6 +282,8 @@ "note": "注文に関する特別な指示・備考", "checkout": "購入手続きに進む", "empty": "カートの中身が空です", + "product_disclosure": "商品の開示情報", + "product_disclosures": "商品の開示情報", "cart_error": "カートをアップデートするときにエラーが発生しました。もう一度お試しください。", "cart_quantity_error_html": "このアイテムは{{ quantity }}個しかカートに追加することができません。", "headings": { diff --git a/locales/ja.schema.json b/locales/ja.schema.json index 96e0599f96b..aaf875a1f2f 100644 --- a/locales/ja.schema.json +++ b/locales/ja.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "迅速な注文リスト" } + }, + "disclosures": { + "name": "開示情報", + "settings": { + "paragraph": { + "content": "この商品に必要な安全に関する警告および規制に基づく開示情報です。" + }, + "heading": { + "label": "見出し" + }, + "open_by_default": { + "label": "デフォルトで開く" + } + } } } } diff --git a/locales/ko.json b/locales/ko.json index 307080204aa..9e7c296f231 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -282,6 +282,8 @@ "note": "주문 특별 지침", "checkout": "결제", "empty": "카트가 비어 있습니다", + "product_disclosure": "제품 고지 사항", + "product_disclosures": "제품 고지 사항", "cart_error": "카트를 업데이트하는 중 오류가 발생했습니다. 다시 시도하십시오.", "cart_quantity_error_html": "카트에는 이 품목을 {{ quantity }}개만 추가할 수 있습니다.", "headings": { diff --git a/locales/ko.schema.json b/locales/ko.schema.json index d1531ba26f4..650f7e445bb 100644 --- a/locales/ko.schema.json +++ b/locales/ko.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "빠른 주문 목록" } + }, + "disclosures": { + "name": "고지 사항", + "settings": { + "paragraph": { + "content": "이 제품에 필수적인 안전 경고 및 규제 관련 고지 사항입니다." + }, + "heading": { + "label": "제목" + }, + "open_by_default": { + "label": "기본으로 열기" + } + } } } } diff --git a/locales/lt.json b/locales/lt.json index 035f2dfa7e4..066ff4523ad 100644 --- a/locales/lt.json +++ b/locales/lt.json @@ -302,6 +302,8 @@ "note": "Specialūs nurodymai dėl užsakymo", "checkout": "Atsiskaityti", "empty": "Jūsų krepšelis tuščias", + "product_disclosure": "Atskleidžiama produkto informacija", + "product_disclosures": "Atskleidžiama produkto informacija", "cart_error": "Atnaujinant krepšelį įvyko klaida. Bandykite dar kartą.", "cart_quantity_error_html": "Į krepšelį galite įdėti tik {{ quantity }}šios prekės vnt.", "update": "Atnaujinti", diff --git a/locales/nb.json b/locales/nb.json index d6afa35d78c..65c7007748e 100644 --- a/locales/nb.json +++ b/locales/nb.json @@ -282,6 +282,8 @@ "note": "Spesielle instruksjoner for bestillingen", "checkout": "Kasse", "empty": "Handlekurven din er tom", + "product_disclosure": "Produktopplysning", + "product_disclosures": "Produktopplysninger", "cart_error": "Det oppstod en feil under oppdateringen av handlekurven din. Prøv på nytt.", "cart_quantity_error_html": "Du kan bare legge {{ quantity }} av denne varen i handlekurven.", "headings": { diff --git a/locales/nb.schema.json b/locales/nb.schema.json index 275b54d0bde..b0980c2bace 100644 --- a/locales/nb.schema.json +++ b/locales/nb.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Hurtigordreliste" } + }, + "disclosures": { + "name": "Opplysninger", + "settings": { + "paragraph": { + "content": "Obligatoriske sikkerhetsadvarsler og lovpålagte opplysninger for dette produktet." + }, + "heading": { + "label": "Overskrift" + }, + "open_by_default": { + "label": "Åpen som standard" + } + } } } } diff --git a/locales/nl.json b/locales/nl.json index 307e9f4a1e4..d20a0c36ab8 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -282,6 +282,8 @@ "note": "Speciale instructies voor bestelling", "checkout": "Afrekenen", "empty": "Je winkelwagen is leeg", + "product_disclosure": "Productvermelding", + "product_disclosures": "Productvermeldingen", "cart_error": "Er is een fout opgetreden bij het bijwerken van je winkelwagen. Probeer het opnieuw.", "cart_quantity_error_html": "Je kunt maar {{ quantity }} van dit artikel toevoegen aan je winkelwagen.", "headings": { diff --git a/locales/nl.schema.json b/locales/nl.schema.json index f2d12e63ff1..abd93672231 100644 --- a/locales/nl.schema.json +++ b/locales/nl.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Snelle lijst met bestellingen" } + }, + "disclosures": { + "name": "Toelichtingen", + "settings": { + "paragraph": { + "content": "Verplichte veiligheidswaarschuwingen en wettelijke vermeldingen voor dit product." + }, + "heading": { + "label": "Opschrift" + }, + "open_by_default": { + "label": "Standaard geopend" + } + } } } } diff --git a/locales/pl.json b/locales/pl.json index b01383d12ba..9a7702a4830 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -302,6 +302,8 @@ "note": "Specjalne instrukcje do zamówienia", "checkout": "Realizuj zakup", "empty": "Twój koszyk jest pusty", + "product_disclosure": "Dodatkowa informacja o produkcie", + "product_disclosures": "Dodatkowe informacje o produkcie", "cart_error": "Wystąpił błąd podczas aktualizowania Twojego koszyka. Spróbuj ponownie.", "cart_quantity_error_html": "Możesz dodać do koszyka tylko {{ quantity }} sztuk(i) tej pozycji.", "update": "Aktualizuj", diff --git a/locales/pl.schema.json b/locales/pl.schema.json index 24f397847a9..c4c97da403e 100644 --- a/locales/pl.schema.json +++ b/locales/pl.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Lista szybkich zamówień" } + }, + "disclosures": { + "name": "Informacje prawne", + "settings": { + "paragraph": { + "content": "Wymagane ostrzeżenia dotyczące bezpieczeństwa i informacje prawne dla tego produktu." + }, + "heading": { + "label": "Nagłówek" + }, + "open_by_default": { + "label": "Domyślnie otwarte" + } + } } } } diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 68fc092d170..23fb8e5bc0c 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -292,6 +292,8 @@ "note": "Instruções especiais do pedido", "checkout": "Finalizar a compra", "empty": "O carrinho está vazio", + "product_disclosure": "Informação de divulgação do produto", + "product_disclosures": "Informações de divulgação do produto", "cart_error": "Ocorreu um erro ao atualizar o carrinho. Tente de novo.", "cart_quantity_error_html": "É possível adicionar apenas {{ quantity }} unidade(s) desse item ao carrinho.", "headings": { diff --git a/locales/pt-BR.schema.json b/locales/pt-BR.schema.json index a83635f1446..d9295ecd071 100644 --- a/locales/pt-BR.schema.json +++ b/locales/pt-BR.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Lista de pedidos rápidos" } + }, + "disclosures": { + "name": "Divulgações", + "settings": { + "paragraph": { + "content": "Avisos de segurança e divulgações regulatórias obrigatórios para este produto." + }, + "heading": { + "label": "Título" + }, + "open_by_default": { + "label": "Aberto por padrão" + } + } } } } diff --git a/locales/pt-PT.json b/locales/pt-PT.json index 08a061608de..5f5e1706904 100644 --- a/locales/pt-PT.json +++ b/locales/pt-PT.json @@ -292,6 +292,8 @@ "note": "Instruções especiais da encomenda", "checkout": "Finalizar a compra", "empty": "O seu carrinho está vazio", + "product_disclosure": "Divulgação do produto", + "product_disclosures": "Divulgações do produto", "cart_error": "Ocorreu um erro ao atualizar o seu carrinho. Tente novamente.", "cart_quantity_error_html": "É possível adicionar apenas {{ quantity }} unidade(s) deste item ao carrinho.", "headings": { diff --git a/locales/pt-PT.schema.json b/locales/pt-PT.schema.json index 44175fb7515..a7e172b7dc0 100644 --- a/locales/pt-PT.schema.json +++ b/locales/pt-PT.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Lista de encomendas rápida" } + }, + "disclosures": { + "name": "Divulgações", + "settings": { + "paragraph": { + "content": "Avisos de segurança e divulgações regulamentares obrigatórios para este produto." + }, + "heading": { + "label": "Título" + }, + "open_by_default": { + "label": "Aberto por predefinição" + } + } } } } diff --git a/locales/ro.json b/locales/ro.json index 9c4db9727d5..7f107cac56e 100644 --- a/locales/ro.json +++ b/locales/ro.json @@ -292,6 +292,8 @@ "note": "Instrucțiuni speciale legate de comandă", "checkout": "Finalizați comanda", "empty": "Coșul dvs. este gol", + "product_disclosure": "Mențiune referitoare la produs", + "product_disclosures": "Mențiuni referitoare la produs", "cart_error": "A apărut o eroare în timpul actualizării coșului. Încercați din nou.", "cart_quantity_error_html": "Cantitatea maximă pe care o poți adăuga în coș din acest articol: {{ quantity }}.", "update": "Actualizați", diff --git a/locales/ru.json b/locales/ru.json index c24894b5e32..3c1d3a91e7f 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -302,6 +302,8 @@ "note": "Получить специальные инструкции", "checkout": "Оформить заказ", "empty": "Корзина пуста.", + "product_disclosure": "Дополнительная информация о товаре", + "product_disclosures": "Дополнительная информация о товаре", "cart_error": "Не удалось обновить корзину. Повторите попытку.", "cart_quantity_error_html": "Максимально допустимое количество единиц этого товара в корзине: {{ quantity }}.", "update": "Обновить", diff --git a/locales/sk.json b/locales/sk.json index a5536e6387d..7552d7bf775 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -302,6 +302,8 @@ "note": "Špeciálne pokyny k objednávke", "checkout": "Platba", "empty": "Váš košík je prázdny", + "product_disclosure": "Zverejnená informácia o produkte", + "product_disclosures": "Zverejnené informácie o produkte", "cart_error": "Pri aktualizácii košíka sa vyskytla chyba. Skúste to znova.", "cart_quantity_error_html": "Do košíka môžete túto položku pridať len v počte {{ quantity }}.", "update": "Aktualizovať", diff --git a/locales/sl.json b/locales/sl.json index 9e2acb5ae4e..dbfb3647a56 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -302,6 +302,8 @@ "note": "Posebna navodila za naročilo", "checkout": "Zaključi nakup", "empty": "Vaša košarica je prazna", + "product_disclosure": "Razkritje o izdelku", + "product_disclosures": "Razkritja o izdelku", "cart_error": "Pri posodabljanju vaše košarice je prišlo do napake. Poskusite znova.", "cart_quantity_error_html": "V košarico lahko dodate največ toliko tovrstnih izdelkov: {{ quantity }}.", "update": "Posodobitev", diff --git a/locales/sv.json b/locales/sv.json index b834921f738..b877024a9d1 100644 --- a/locales/sv.json +++ b/locales/sv.json @@ -282,6 +282,8 @@ "note": "Beställ särskilda instruktioner", "checkout": "Gå till kassan", "empty": "Din varukorg är tom", + "product_disclosure": "Produktupplysning", + "product_disclosures": "Produktupplysningar", "cart_error": "Ett fel uppstod när du uppdaterade din varukorg. Försök igen.", "cart_quantity_error_html": "Du kan endast lägga till {{ quantity }} av denna artikel i din varukorg.", "headings": { diff --git a/locales/sv.schema.json b/locales/sv.schema.json index ee299701324..8da846c7a78 100644 --- a/locales/sv.schema.json +++ b/locales/sv.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Snabb orderlista" } + }, + "disclosures": { + "name": "Upplysningar", + "settings": { + "paragraph": { + "content": "Obligatoriska säkerhetsvarningar och lagstadgad information för denna produkt." + }, + "heading": { + "label": "Rubrik" + }, + "open_by_default": { + "label": "Öppen som standard" + } + } } } } diff --git a/locales/th.json b/locales/th.json index f242e80ad2b..0bd3cdfd6d8 100644 --- a/locales/th.json +++ b/locales/th.json @@ -282,6 +282,8 @@ "note": "คำแนะนำพิเศษสำหรับคำสั่งซื้อ", "checkout": "ชำระเงิน", "empty": "ตะกร้าสินค้าของคุณว่างอยู่", + "product_disclosure": "ข้อมูลชี้แจงสินค้า", + "product_disclosures": "ข้อมูลชี้แจงสินค้า", "cart_error": "เกิดข้อผิดพลาดระหว่างการอัปเดตตะกร้าสินค้าของคุณ โปรดลองอีกครั้ง", "cart_quantity_error_html": "คุณสามารถเพิ่มรายการนี้ {{ quantity }} รายการลงในตะกร้าสินค้าของคุณเท่านั้น", "headings": { diff --git a/locales/th.schema.json b/locales/th.schema.json index 2c88c86482a..170905c0816 100644 --- a/locales/th.schema.json +++ b/locales/th.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "รายการคำสั่งซื้อแบบด่วน" } + }, + "disclosures": { + "name": "การเปิดเผยข้อมูล", + "settings": { + "paragraph": { + "content": "คำเตือนด้านความปลอดภัยและข้อมูลตามระเบียบข้อบังคับที่ต้องเปิดเผยสำหรับสินค้านี้" + }, + "heading": { + "label": "หัวเรื่อง" + }, + "open_by_default": { + "label": "เปิดตามค่าเริ่มต้น" + } + } } } } diff --git a/locales/tr.json b/locales/tr.json index a4c30c10e34..1aeb07b05fc 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -282,6 +282,8 @@ "note": "Siparişe özel talimatlar", "checkout": "Ödeme", "empty": "Sepetiniz boş", + "product_disclosure": "Ürün bildirimi", + "product_disclosures": "Ürün bildirimleri", "cart_error": "Sepetiniz güncellenirken bir hata oluştu. Lütfen tekrar deneyin.", "cart_quantity_error_html": "Sepetinize bu üründen yalnızca {{ quantity }} adet ekleyebilirsiniz.", "headings": { diff --git a/locales/tr.schema.json b/locales/tr.schema.json index 8e5b5c1c455..629066b9b73 100644 --- a/locales/tr.schema.json +++ b/locales/tr.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "Hızlı sipariş listesi" } + }, + "disclosures": { + "name": "Bildirimler", + "settings": { + "paragraph": { + "content": "Bu ürün için zorunlu güvenlik uyarıları ve yasal bildirimler." + }, + "heading": { + "label": "Başlık" + }, + "open_by_default": { + "label": "Varsayılan olarak açık" + } + } } } } diff --git a/locales/vi.json b/locales/vi.json index 75147cf4249..536c7233a79 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -282,6 +282,8 @@ "note": "Hướng dẫn đặc biệt của đơn hàng", "checkout": "Thanh toán", "empty": "Giỏ hàng của bạn đang trống", + "product_disclosure": "Thông tin công bố sản phẩm", + "product_disclosures": "Thông tin công bố sản phẩm", "cart_error": "Đã xảy ra lỗi khi cập nhật giỏ hàng. Vui lòng thử lại.", "cart_quantity_error_html": "Bạn chỉ có thể thêm {{ quantity }} mặt hàng này vào giỏ hàng.", "headings": { diff --git a/locales/zh-CN.json b/locales/zh-CN.json index af68e9a2701..d2bbd792ba6 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -282,6 +282,8 @@ "note": "订单特殊说明", "checkout": "结账", "empty": "您的购物车为空", + "product_disclosure": "产品披露信息", + "product_disclosures": "产品披露信息", "cart_error": "更新购物车时出错。请重试。", "cart_quantity_error_html": "您只能向购物车添加 {{ quantity }} 件此商品。", "headings": { diff --git a/locales/zh-CN.schema.json b/locales/zh-CN.schema.json index c73d06e078f..b1c5bf19fb1 100644 --- a/locales/zh-CN.schema.json +++ b/locales/zh-CN.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "快速订单列表" } + }, + "disclosures": { + "name": "披露信息", + "settings": { + "paragraph": { + "content": "此产品所需的安全警告和监管披露信息。" + }, + "heading": { + "label": "标题" + }, + "open_by_default": { + "label": "默认展开" + } + } } } } diff --git a/locales/zh-TW.json b/locales/zh-TW.json index b094eda9f6c..4b1c235f30e 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -282,6 +282,8 @@ "note": "訂單特別指示", "checkout": "結帳", "empty": "您的購物車是空的", + "product_disclosure": "商品揭露資訊", + "product_disclosures": "商品揭露資訊", "cart_error": "更新購物車時發生錯誤。請再試一次。", "cart_quantity_error_html": "您只能將 {{ quantity }} 項商品加入您的購物車。", "headings": { diff --git a/locales/zh-TW.schema.json b/locales/zh-TW.schema.json index ca70438e735..3491cd6f1c4 100644 --- a/locales/zh-TW.schema.json +++ b/locales/zh-TW.schema.json @@ -3396,6 +3396,20 @@ "presets": { "name": "快速訂單清單" } + }, + "disclosures": { + "name": "資訊揭露", + "settings": { + "paragraph": { + "content": "此商品必備的安全警告與法規資訊揭露。" + }, + "heading": { + "label": "標題" + }, + "open_by_default": { + "label": "預設為開啟" + } + } } } } diff --git a/release-notes.md b/release-notes.md index ecc7e773149..64b697d892e 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1 +1,12 @@ -Dawn 15.4.1 introduces improvements for performance monitoring. \ No newline at end of file +Adds support for standard storefront events and a new section and block to display product disclosures. + +### Added + +- [Storefront Events & Actions] Added support for app, agent, and AI cart interactions without page reloads. +- [Product disclosures] New section and block to display product disclosures on product pages. + +### Fixes and improvements + +- [Accessibility] Fixed aria labels and focus management on search and filtering components +- [Performance] Changed body layout styles for improved cumulative layout shift (CLS) +- Fixed missing theme setting translation strings in "Icons with Text" section \ No newline at end of file diff --git a/sections/cart-notification-product.liquid b/sections/cart-notification-product.liquid index 6cd6259f19f..5262f2902d9 100644 --- a/sections/cart-notification-product.liquid +++ b/sections/cart-notification-product.liquid @@ -16,7 +16,13 @@ {%- if settings.show_vendor -%}

{{ item.product.vendor }}

{%- endif -%} -

{{ item.product.title | escape }}

+
+

+ {{- item.product.title | escape -}} +

+ {% assign modal_id = 'CartDisclosureModal-notification-' | append: item.key | replace: ':', '-' %} + {%- render 'cart-disclosure-indicator', product: item.product, modal_id: modal_id -%} +
{%- unless item.product.has_only_default_variant -%} {%- for option in item.options_with_values -%} diff --git a/sections/disclosures.liquid b/sections/disclosures.liquid new file mode 100644 index 00000000000..667030e9b8d --- /dev/null +++ b/sections/disclosures.liquid @@ -0,0 +1,113 @@ +{%- style -%} + .section-{{ section.id }}-padding { + padding-top: {{ section.settings.padding_top | times: 0.75 | round: 0 }}px; + padding-bottom: {{ section.settings.padding_bottom | times: 0.75 | round: 0 }}px; + } + + @media screen and (min-width: 750px) { + .section-{{ section.id }}-padding { + padding-top: {{ section.settings.padding_top }}px; + padding-bottom: {{ section.settings.padding_bottom }}px; + } + } +{%- endstyle -%} + +{%- capture disclosures_content -%} + {%- render 'product-disclosures', + product: product, + heading: section.settings.heading, + heading_size: section.settings.heading_size, + open_by_default: section.settings.open_by_default, + surface: 'product_page' + -%} +{%- endcapture -%} + +{%- unless disclosures_content == blank -%} +
+
+ {{ disclosures_content }} +
+
+{%- endunless -%} + +{% schema %} +{ + "name": "t:sections.disclosures.name", + "enabled_on": { + "templates": ["product"] + }, + "settings": [ + { + "type": "paragraph", + "content": "t:sections.disclosures.settings.paragraph.content" + }, + { + "type": "text", + "id": "heading", + "label": "t:sections.disclosures.settings.heading.label", + "default": "t:sections.disclosures.name" + }, + { + "type": "select", + "id": "heading_size", + "options": [ + { + "value": "h2", + "label": "t:sections.all.heading_size.options__1.label" + }, + { + "value": "h1", + "label": "t:sections.all.heading_size.options__2.label" + }, + { + "value": "h0", + "label": "t:sections.all.heading_size.options__3.label" + } + ], + "default": "h2", + "label": "t:sections.all.heading_size.label" + }, + { + "type": "checkbox", + "id": "open_by_default", + "label": "t:sections.disclosures.settings.open_by_default.label", + "default": false + }, + { + "type": "color_scheme", + "id": "color_scheme", + "label": "t:sections.all.colors.label", + "default": "scheme-1" + }, + { + "type": "header", + "content": "t:sections.all.padding.section_padding_heading" + }, + { + "type": "range", + "id": "padding_top", + "min": 0, + "max": 100, + "step": 4, + "unit": "px", + "label": "t:sections.all.padding.padding_top", + "default": 36 + }, + { + "type": "range", + "id": "padding_bottom", + "min": 0, + "max": 100, + "step": 4, + "unit": "px", + "label": "t:sections.all.padding.padding_bottom", + "default": 36 + } + ], + "presets": [ + { + "name": "t:sections.disclosures.name" + } + ] +} +{% endschema %} diff --git a/sections/featured-collection.liquid b/sections/featured-collection.liquid index 5468f8e3265..368b421fd2d 100644 --- a/sections/featured-collection.liquid +++ b/sections/featured-collection.liquid @@ -127,7 +127,8 @@ show_rating: section.settings.show_rating, skip_styles: skip_card_product_styles, section_id: section.id, - quick_add: section.settings.quick_add + quick_add: section.settings.quick_add, + product_view_context: 'collection' %} {%- assign skip_card_product_styles = true -%} diff --git a/sections/featured-product.liquid b/sections/featured-product.liquid index 1842a27acf4..e7c9456b492 100644 --- a/sections/featured-product.liquid +++ b/sections/featured-product.liquid @@ -68,6 +68,9 @@ {% assign variant_images = product.images | where: 'attached_to_variant?', true | map: 'src' %} + {%- unless placeholder -%} + + {%- endunless -%}
+ {%- unless placeholder -%} +
+ {%- endunless -%} {%- if section.settings.image_zoom == 'hover' -%} @@ -483,14 +498,6 @@ {%- endif -%} - {%- liquid - if product.selected_or_first_available_variant.featured_media - assign seo_media = product.selected_or_first_available_variant.featured_media - else - assign seo_media = product.featured_media - endif - -%} - @@ -707,6 +714,49 @@ } ] }, + { + "type": "disclosures", + "name": "t:sections.disclosures.name", + "limit": 1, + "settings": [ + { + "type": "paragraph", + "content": "t:sections.disclosures.settings.paragraph.content" + }, + { + "type": "text", + "id": "heading", + "label": "t:sections.disclosures.settings.heading.label", + "default": "t:sections.disclosures.name" + }, + { + "type": "select", + "id": "heading_size", + "options": [ + { + "value": "h2", + "label": "t:sections.all.heading_size.options__1.label" + }, + { + "value": "h1", + "label": "t:sections.all.heading_size.options__2.label" + }, + { + "value": "h0", + "label": "t:sections.all.heading_size.options__3.label" + } + ], + "default": "h2", + "label": "t:sections.all.heading_size.label" + }, + { + "type": "checkbox", + "id": "open_by_default", + "label": "t:sections.disclosures.settings.open_by_default.label", + "default": false + } + ] + }, { "type": "rating", "name": "t:sections.featured-product.blocks.rating.name", @@ -740,8 +790,8 @@ }, { "type": "header", - "content": "t:sections.main-product.blocks.icon_with_text.settings.content.label", - "info": "t:sections.main-product.blocks.icon_with_text.settings.content.info" + "content": "t:sections.main-product.blocks.icon_with_text.settings.pairing_1.label", + "info": "t:sections.main-product.blocks.icon_with_text.settings.pairing_1.info" }, { "type": "select", @@ -939,6 +989,10 @@ "label": "t:sections.main-product.blocks.icon_with_text.settings.heading_1.label", "info": "t:sections.main-product.blocks.icon_with_text.settings.heading.info" }, + { + "type": "header", + "content": "t:sections.main-product.blocks.icon_with_text.settings.pairing_2.label" + }, { "type": "select", "id": "icon_2", @@ -1135,6 +1189,10 @@ "label": "t:sections.main-product.blocks.icon_with_text.settings.heading_2.label", "info": "t:sections.main-product.blocks.icon_with_text.settings.heading.info" }, + { + "type": "header", + "content": "t:sections.main-product.blocks.icon_with_text.settings.pairing_3.label" + }, { "type": "select", "id": "icon_3", diff --git a/sections/footer-group.json b/sections/footer-group.json index a9263d9b541..6234f01624f 100644 --- a/sections/footer-group.json +++ b/sections/footer-group.json @@ -4,44 +4,19 @@ "sections": { "footer": { "type": "footer", - "blocks": { - "footer-0": { - "type": "link_list", - "settings": { - "heading": "Quick links", - "menu": "footer" - } - }, - "footer-1": { - "type": "link_list", - "settings": { - "heading": "Info", - "menu": "footer" - } - }, - "footer-2": { - "type": "text", - "settings": { - "heading": "Our mission", - "subtext": "

Share contact information, store details, and brand content with your customers.<\/p>" - } - } - }, - "block_order": [ - "footer-0", - "footer-1", - "footer-2" - ], + "blocks": {}, + "block_order": [], "settings": { "color_scheme": "scheme-1", "newsletter_enable": true, "newsletter_heading": "Subscribe to our emails", + "enable_follow_on_shop": true, "show_social": true, - "enable_country_selector": false, - "enable_language_selector": false, + "enable_country_selector": true, + "enable_language_selector": true, "payment_enable": true, - "show_policy": false, - "margin_top": 48, + "show_policy": true, + "margin_top": 0, "padding_top": 36, "padding_bottom": 36 } diff --git a/sections/header-group.json b/sections/header-group.json index a9ac3a08a0d..33c3876654c 100644 --- a/sections/header-group.json +++ b/sections/header-group.json @@ -4,6 +4,15 @@ "sections": { "announcement-bar": { "type": "announcement-bar", + "settings": { + "color_scheme": "scheme-1", + "show_line_separator": true, + "show_social": false, + "auto_rotate": false, + "change_slides_speed": 5, + "enable_country_selector": false, + "enable_language_selector": false + }, "blocks": { "announcement-bar-0": { "type": "announcement", @@ -23,6 +32,7 @@ "type": "header", "settings": { "color_scheme": "scheme-1", + "menu_color_scheme": "scheme-1", "logo_position": "middle-left", "menu": "main-menu", "menu_type_desktop": "dropdown", @@ -30,6 +40,7 @@ "show_line_separator": true, "enable_country_selector": true, "enable_language_selector": true, + "enable_customer_avatar": true, "mobile_logo_position": "center", "margin_bottom": 0, "padding_top": 20, diff --git a/sections/header.liquid b/sections/header.liquid index e3aa5eb5262..af5ff9ac238 100644 --- a/sections/header.liquid +++ b/sections/header.liquid @@ -1,7 +1,12 @@ - + {%- if settings.predictive_search_enabled -%} @@ -11,7 +16,6 @@ {%- endif -%} -