From fb5a4714b4d6f5fc74d229e7e69550a02209417c Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 30 Jul 2025 20:52:38 +0000 Subject: [PATCH] Horizon v2.0.3 Upstream-SHA: 9a4d0f8397f8d9fd36c1ce64f4540ba6f22f0666 --- .cursor/prompts/fix-accesibility-issue.md | 90 + .cursor/prompts/fix-breaking-changes.md | 246 + .cursor/prompts/get-breaking-changes.md | 116 + .cursor/prompts/update-schema-translations.md | 94 + .cursor/prompts/update-translations.md | 94 + .cursor/rules/accordion-accessibility.mdc | 218 + .cursor/rules/assets.mdc | 12 + .cursor/rules/blocks.mdc | 344 ++ .cursor/rules/breadcrumb-accessibility.mdc | 129 + .cursor/rules/carousel-accessibility.mdc | 427 ++ .cursor/rules/cart-drawer-accessibility.mdc | 206 + .../rules/color-contrast-accessibility.mdc | 329 ++ .cursor/rules/color-swatch-accessibility.mdc | 216 + .cursor/rules/combobox-accessibility.mdc | 242 + .cursor/rules/css-standards.mdc | 918 ++++ .cursor/rules/disclosure-accessibility.mdc | 150 + .../dropdown-navigation-accessibility.mdc | 442 ++ .../rules/examples/block-example-group.liquid | 105 + .../rules/examples/block-example-text.liquid | 61 + .cursor/rules/examples/section-example.liquid | 64 + .cursor/rules/examples/snippet-example.liquid | 83 + .cursor/rules/html-standards.mdc | 299 ++ .cursor/rules/javascript-standards.mdc | 950 ++++ .cursor/rules/liquid.mdc | 166 + .cursor/rules/locales.mdc | 63 + .cursor/rules/localization.mdc | 67 + .cursor/rules/modal-accessibility.mdc | 142 + .cursor/rules/product-card-accessibility.mdc | 345 ++ .../rules/product-filter-accessibility.mdc | 625 +++ .cursor/rules/prompts-and-references.mdc | 50 + .cursor/rules/sale-price-accessibility.mdc | 192 + .cursor/rules/schemas.mdc | 137 + .cursor/rules/sections.mdc | 83 + .cursor/rules/slider-accessibility.mdc | 206 + .cursor/rules/snippets.mdc | 132 + .cursor/rules/switch-accessibility.mdc | 167 + .cursor/rules/tab-accessibility.mdc | 208 + .cursor/rules/templates.mdc | 154 + .cursor/rules/theme-settings.mdc | 51 + .cursor/rules/tooltip-accessibility.mdc | 151 + LICENSE.md | 9 + README.md | 100 +- assets/accordion-custom.js | 106 + assets/account-login-actions.js | 34 + assets/anchored-popover.js | 80 + assets/announcement-bar.js | 130 + assets/auto-close-details.js | 15 + assets/base.css | 4249 +++++++++++++++++ assets/blog-posts-list.js | 10 + assets/cart-discount.js | 203 + assets/cart-drawer.js | 46 + assets/cart-icon.js | 108 + assets/cart-note.js | 46 + assets/collection-links.js | 195 + assets/component-cart-items.js | 259 + assets/component-quantity-selector.js | 100 + assets/component.js | 331 ++ assets/copy-to-clipboard.js | 26 + assets/critical.js | 480 ++ assets/dialog.js | 183 + assets/drag-zoom-wrapper.js | 352 ++ assets/events.js | 285 ++ assets/facets.js | 910 ++++ assets/floating-panel.js | 60 + assets/focus.js | 104 + assets/global.d.ts | 72 + assets/header-drawer.js | 155 + assets/header-menu.js | 211 + assets/header.js | 246 + assets/icon-account.svg | 1 + assets/icon-add-to-cart.svg | 1 + assets/icon-arrow.svg | 1 + assets/icon-available.svg | 3 + assets/icon-caret.svg | 3 + assets/icon-cart.svg | 1 + assets/icon-checkmark.svg | 3 + assets/icon-close.svg | 4 + assets/icon-delete.svg | 4 + assets/icon-discount.svg | 4 + assets/icon-error.svg | 6 + assets/icon-external.svg | 1 + assets/icon-filter.svg | 1 + assets/icon-filters-close.svg | 6 + assets/icon-grid-default.svg | 6 + assets/icon-grid-dense.svg | 11 + assets/icon-inventory.svg | 5 + assets/icon-menu.svg | 4 + assets/icon-minus.svg | 3 + assets/icon-one-col-mobile.svg | 3 + assets/icon-orders.svg | 4 + assets/icon-pause.svg | 4 + assets/icon-play.svg | 3 + assets/icon-plus.svg | 4 + assets/icon-reset.svg | 1 + assets/icon-search.svg | 1 + assets/icon-shopify.svg | 1 + assets/icon-unavailable.svg | 3 + assets/jsconfig.json | 14 + assets/jumbo-text.js | 152 + assets/local-pickup.js | 79 + assets/localization.js | 548 +++ assets/marquee.js | 221 + assets/media-gallery.js | 84 + assets/media.js | 248 + assets/morph.js | 469 ++ assets/overflow-list.css | 58 + assets/paginated-list-aspect-ratio.js | 171 + assets/paginated-list.js | 355 ++ assets/performance.js | 75 + assets/predictive-search.js | 464 ++ assets/product-card-link.js | 74 + assets/product-card.js | 516 ++ assets/product-form.js | 448 ++ assets/product-inventory.js | 36 + assets/product-price.js | 47 + assets/product-recommendations.js | 144 + assets/product-title-truncation.js | 86 + assets/qr-code-generator.js | 1665 +++++++ assets/qr-code-image.js | 33 + assets/quick-add.js | 229 + assets/recently-viewed-products.js | 35 + assets/results-list.js | 78 + assets/rte-formatter.js | 26 + assets/scrolling.js | 404 ++ assets/search-page-input.js | 58 + assets/section-renderer.js | 180 + assets/show-more.js | 162 + assets/slideshow.js | 750 +++ assets/template-giftcard.css | 137 + assets/theme-editor.js | 268 ++ assets/utilities.js | 603 +++ assets/variant-picker.js | 296 ++ assets/video-background.js | 32 + assets/view-transitions.js | 87 + assets/zoom-dialog.js | 276 ++ blocks/_accordion-row.liquid | 349 ++ blocks/_announcement.liquid | 328 ++ blocks/_blog-post-card.liquid | 116 + blocks/_blog-post-content.liquid | 24 + blocks/_blog-post-description.liquid | 356 ++ blocks/_blog-post-featured-image.liquid | 292 ++ blocks/_blog-post-image.liquid | 125 + blocks/_blog-post-info-text.liquid | 154 + blocks/_cart-products.liquid | 115 + blocks/_cart-summary.liquid | 247 + blocks/_cart-title.liquid | 169 + blocks/_collection-card-image.liquid | 265 + blocks/_collection-card.liquid | 194 + blocks/_collection-image.liquid | 166 + blocks/_collection-info.liquid | 165 + blocks/_collection-link.liquid | 164 + blocks/_content-without-appearance.liquid | 115 + blocks/_content.liquid | 160 + blocks/_divider.liquid | 77 + blocks/_featured-product-gallery.liquid | 59 + blocks/_featured-product-price.liquid | 107 + blocks/_featured-product.liquid | 39 + blocks/_footer-copyright.liquid | 80 + blocks/_footer-policy-list.liquid | 94 + blocks/_footer-social-icons.liquid | 32 + blocks/_header-logo.liquid | 231 + blocks/_header-menu.liquid | 994 ++++ blocks/_heading.liquid | 337 ++ blocks/_image.liquid | 168 + blocks/_inline-collection-title.liquid | 164 + blocks/_inline-text.liquid | 161 + blocks/_marquee.liquid | 210 + blocks/_media-without-appearance.liquid | 104 + blocks/_media.liquid | 134 + blocks/_product-card-gallery.liquid | 175 + blocks/_product-card-group.liquid | 515 ++ blocks/_product-card.liquid | 172 + blocks/_product-details.liquid | 706 +++ blocks/_product-list-button.liquid | 119 + blocks/_product-list-content.liquid | 454 ++ blocks/_product-list-text.liquid | 398 ++ blocks/_product-media-gallery.liquid | 907 ++++ blocks/_search-input.liquid | 247 + blocks/_slide.liquid | 498 ++ blocks/_social-link.liquid | 113 + blocks/accelerated-checkout.liquid | 63 + blocks/accordion.liquid | 310 ++ blocks/add-to-cart.liquid | 50 + blocks/button.liquid | 114 + blocks/buy-buttons.liquid | 474 ++ blocks/collection-card.liquid | 218 + blocks/collection-title.liquid | 397 ++ blocks/contact-form-submit-button.liquid | 104 + blocks/contact-form.liquid | 131 + blocks/custom-liquid.liquid | 24 + blocks/email-signup.liquid | 567 +++ blocks/featured-collection.liquid | 29 + blocks/filters.liquid | 1235 +++++ blocks/follow-on-shop.liquid | 72 + blocks/footer-utilities.liquid | 122 + blocks/group.liquid | 503 ++ blocks/icon.liquid | 325 ++ blocks/image.liquid | 335 ++ blocks/jumbo-text.liquid | 135 + blocks/logo.liquid | 289 ++ blocks/menu.liquid | 339 ++ blocks/page-content.liquid | 10 + blocks/page.liquid | 140 + blocks/payment-icons.liquid | 138 + blocks/popup-link.liquid | 263 + blocks/price.liquid | 436 ++ blocks/product-card.liquid | 245 + blocks/product-description.liquid | 403 ++ blocks/product-inventory.liquid | 182 + blocks/product-recommendations.liquid | 474 ++ blocks/product-title.liquid | 404 ++ blocks/quantity.liquid | 8 + blocks/review.liquid | 267 ++ blocks/spacer.liquid | 182 + blocks/swatches.liquid | 213 + blocks/text.liquid | 402 ++ blocks/variant-picker.liquid | 103 + blocks/video.liquid | 252 + config/settings_data.json | 420 ++ config/settings_schema.json | 2317 +++++++++ layout/password.liquid | 167 + layout/theme.liquid | 70 + locales/bg.json | 271 ++ locales/cs.json | 283 ++ locales/cs.schema.json | 931 ++++ locales/da.json | 271 ++ locales/da.schema.json | 931 ++++ locales/de.json | 271 ++ locales/de.schema.json | 931 ++++ locales/el.json | 271 ++ locales/en.default.json | 281 ++ locales/en.default.schema.json | 941 ++++ locales/es.json | 277 ++ locales/es.schema.json | 931 ++++ locales/fi.json | 271 ++ locales/fi.schema.json | 930 ++++ locales/fr.json | 277 ++ locales/fr.schema.json | 931 ++++ locales/hr.json | 277 ++ locales/hu.json | 271 ++ locales/id.json | 271 ++ locales/it.json | 277 ++ locales/it.schema.json | 930 ++++ locales/ja.json | 271 ++ locales/ja.schema.json | 931 ++++ locales/ko.json | 271 ++ locales/ko.schema.json | 930 ++++ locales/lt.json | 283 ++ locales/nb.json | 271 ++ locales/nb.schema.json | 931 ++++ locales/nl.json | 271 ++ locales/nl.schema.json | 931 ++++ locales/pl.json | 283 ++ locales/pl.schema.json | 931 ++++ locales/pt-BR.json | 277 ++ locales/pt-BR.schema.json | 931 ++++ locales/pt-PT.json | 277 ++ locales/pt-PT.schema.json | 931 ++++ locales/ro.json | 277 ++ locales/ru.json | 283 ++ locales/sk.json | 283 ++ locales/sl.json | 283 ++ locales/sv.json | 271 ++ locales/sv.schema.json | 930 ++++ locales/th.json | 271 ++ locales/th.schema.json | 931 ++++ locales/tr.json | 271 ++ locales/tr.schema.json | 930 ++++ locales/vi.json | 271 ++ locales/zh-CN.json | 271 ++ locales/zh-CN.schema.json | 931 ++++ locales/zh-TW.json | 271 ++ locales/zh-TW.schema.json | 931 ++++ release-notes.md | 22 + sections/_blocks.liquid | 425 ++ sections/collection-links.liquid | 369 ++ sections/collection-list.liquid | 968 ++++ sections/custom-liquid.liquid | 73 + sections/divider.liquid | 130 + sections/featured-product.liquid | 191 + sections/footer-group.json | 246 + sections/footer.liquid | 372 ++ sections/header-announcements.liquid | 217 + sections/header-group.json | 93 + sections/header.liquid | 1054 ++++ sections/hero.liquid | 938 ++++ sections/main-404.liquid | 186 + sections/main-blog-post.liquid | 171 + sections/main-blog.liquid | 204 + sections/main-cart.liquid | 198 + sections/main-collection-list.liquid | 403 ++ sections/main-collection.liquid | 307 ++ sections/main-page.liquid | 94 + sections/marquee.liquid | 190 + sections/media-with-content.liquid | 432 ++ sections/password.liquid | 526 ++ sections/predictive-search-empty.liquid | 9 + sections/predictive-search.liquid | 1222 +++++ sections/product-information.liquid | 388 ++ sections/product-list.liquid | 941 ++++ sections/product-recommendations.liquid | 560 +++ sections/search-header.liquid | 81 + sections/search-results.liquid | 219 + .../section-rendering-product-card.liquid | 61 + sections/section.liquid | 1324 +++++ sections/slideshow.liquid | 304 ++ snippets/account-actions.liquid | 194 + snippets/account-button.liquid | 90 + snippets/account-drawer.liquid | 103 + snippets/account-popover.liquid | 75 + snippets/add-to-cart-button.liquid | 118 + snippets/background-image.liquid | 40 + snippets/background-media.liquid | 7 + snippets/background-video.liquid | 57 + snippets/bento-grid.liquid | 210 + snippets/blog-comment-form.liquid | 206 + snippets/border-override.liquid | 7 + snippets/button.liquid | 43 + snippets/card-gallery.liquid | 403 ++ snippets/cart-bubble.liquid | 40 + snippets/cart-discount.liquid | 232 + snippets/cart-drawer.liquid | 171 + snippets/cart-icon-component.liquid | 32 + snippets/cart-note.liquid | 38 + snippets/cart-products.liquid | 753 +++ snippets/cart-summary.liquid | 126 + snippets/checkbox.liquid | 50 + snippets/collection-card.liquid | 191 + snippets/color-schemes.liquid | 99 + snippets/contact-form.liquid | 170 + snippets/divider.liquid | 54 + snippets/drawer-localization.liquid | 125 + snippets/dropdown-localization.liquid | 92 + snippets/editorial-collection-grid.liquid | 117 + snippets/editorial-product-grid.liquid | 124 + snippets/facets-actions.liquid | 220 + snippets/filter-remove-buttons.liquid | 178 + snippets/filters-toggle.liquid | 149 + snippets/fonts.liquid | 49 + snippets/gap-style.liquid | 25 + snippets/grid-density-controls.liquid | 179 + snippets/group.liquid | 90 + snippets/header-actions.liquid | 280 ++ snippets/header-drawer.liquid | 1128 +++++ snippets/header-menu.liquid | 90 + snippets/header-row.liquid | 78 + snippets/icon-or-image.liquid | 40 + snippets/icon.liquid | 399 ++ snippets/image.liquid | 32 + snippets/jumbo-text.liquid | 186 + snippets/layout-panel-style.liquid | 33 + snippets/link-featured-image.liquid | 47 + snippets/list-filter.liquid | 783 +++ snippets/localization-form.liquid | 817 ++++ snippets/media.liquid | 116 + snippets/mega-menu-list.liquid | 322 ++ snippets/mega-menu.liquid | 29 + snippets/menu-font-styles.liquid | 23 + snippets/meta-tags.liquid | 121 + snippets/overflow-list.liquid | 63 + snippets/overlay.liquid | 38 + snippets/predictive-search-empty-state.liquid | 39 + .../predictive-search-products-list.liquid | 112 + ...predictive-search-resource-carousel.liquid | 65 + snippets/predictive-search.liquid | 135 + snippets/price-filter.liquid | 239 + snippets/price.liquid | 79 + snippets/product-card-badges.liquid | 77 + snippets/product-card-variant-url.liquid | 27 + snippets/product-card.liquid | 220 + snippets/product-grid.liquid | 216 + snippets/product-media.liquid | 208 + snippets/quantity-selector.liquid | 74 + snippets/quick-add-modal.liquid | 442 ++ snippets/quick-add.liquid | 324 ++ snippets/resource-card.liquid | 275 ++ snippets/resource-list-carousel.liquid | 59 + snippets/scripts.liquid | 258 + snippets/search-modal.liquid | 108 + snippets/search.liquid | 52 + snippets/section.liquid | 96 + snippets/size-style.liquid | 33 + snippets/skip-to-content-link.liquid | 16 + snippets/slideshow-arrow.liquid | 49 + snippets/slideshow-arrows.liquid | 29 + snippets/slideshow-controls.liquid | 180 + snippets/slideshow-slide.liquid | 42 + snippets/slideshow.liquid | 77 + snippets/sorting.liquid | 361 ++ snippets/spacing-padding.liquid | 11 + snippets/spacing-style.liquid | 49 + snippets/strikethrough-variant.liquid | 11 + snippets/stylesheets.liquid | 2 + snippets/submenu-font-styles.liquid | 48 + snippets/swatch.liquid | 41 + snippets/tax-info.liquid | 84 + snippets/text.liquid | 216 + snippets/theme-editor.liquid | 4 + snippets/theme-styles-variables.liquid | 556 +++ snippets/timeline-scope.liquid | 11 + snippets/typography-style.liquid | 75 + snippets/unit-price.liquid | 16 + snippets/util-autofill-img-size-attr.liquid | 75 + snippets/util-mega-menu-img-sizes-attr.liquid | 87 + snippets/util-product-grid-card-size.liquid | 45 + snippets/util-product-media-sizes-attr.liquid | 141 + snippets/variant-main-picker.liquid | 477 ++ snippets/variant-quick-add.liquid | 101 + snippets/variant-swatches.liquid | 145 + snippets/video.liquid | 215 + templates/404.json | 318 ++ templates/article.json | 53 + templates/blog.json | 125 + templates/cart.json | 162 + templates/collection.json | 232 + templates/gift_card.liquid | 232 + templates/index.json | 1883 ++++++++ templates/list-collections.json | 169 + templates/page.contact.json | 149 + templates/page.json | 43 + templates/password.json | 72 + templates/product.json | 440 ++ templates/search.json | 211 + 423 files changed, 111249 insertions(+), 2 deletions(-) create mode 100644 .cursor/prompts/fix-accesibility-issue.md create mode 100644 .cursor/prompts/fix-breaking-changes.md create mode 100644 .cursor/prompts/get-breaking-changes.md create mode 100644 .cursor/prompts/update-schema-translations.md create mode 100644 .cursor/prompts/update-translations.md create mode 100644 .cursor/rules/accordion-accessibility.mdc create mode 100644 .cursor/rules/assets.mdc create mode 100644 .cursor/rules/blocks.mdc create mode 100644 .cursor/rules/breadcrumb-accessibility.mdc create mode 100644 .cursor/rules/carousel-accessibility.mdc create mode 100644 .cursor/rules/cart-drawer-accessibility.mdc create mode 100644 .cursor/rules/color-contrast-accessibility.mdc create mode 100644 .cursor/rules/color-swatch-accessibility.mdc create mode 100644 .cursor/rules/combobox-accessibility.mdc create mode 100644 .cursor/rules/css-standards.mdc create mode 100644 .cursor/rules/disclosure-accessibility.mdc create mode 100644 .cursor/rules/dropdown-navigation-accessibility.mdc create mode 100644 .cursor/rules/examples/block-example-group.liquid create mode 100644 .cursor/rules/examples/block-example-text.liquid create mode 100644 .cursor/rules/examples/section-example.liquid create mode 100644 .cursor/rules/examples/snippet-example.liquid create mode 100644 .cursor/rules/html-standards.mdc create mode 100644 .cursor/rules/javascript-standards.mdc create mode 100644 .cursor/rules/liquid.mdc create mode 100644 .cursor/rules/locales.mdc create mode 100644 .cursor/rules/localization.mdc create mode 100644 .cursor/rules/modal-accessibility.mdc create mode 100644 .cursor/rules/product-card-accessibility.mdc create mode 100644 .cursor/rules/product-filter-accessibility.mdc create mode 100644 .cursor/rules/prompts-and-references.mdc create mode 100644 .cursor/rules/sale-price-accessibility.mdc create mode 100644 .cursor/rules/schemas.mdc create mode 100644 .cursor/rules/sections.mdc create mode 100644 .cursor/rules/slider-accessibility.mdc create mode 100644 .cursor/rules/snippets.mdc create mode 100644 .cursor/rules/switch-accessibility.mdc create mode 100644 .cursor/rules/tab-accessibility.mdc create mode 100644 .cursor/rules/templates.mdc create mode 100644 .cursor/rules/theme-settings.mdc create mode 100644 .cursor/rules/tooltip-accessibility.mdc create mode 100644 LICENSE.md create mode 100644 assets/accordion-custom.js create mode 100644 assets/account-login-actions.js create mode 100644 assets/anchored-popover.js create mode 100644 assets/announcement-bar.js create mode 100644 assets/auto-close-details.js create mode 100644 assets/base.css create mode 100644 assets/blog-posts-list.js create mode 100644 assets/cart-discount.js create mode 100644 assets/cart-drawer.js create mode 100644 assets/cart-icon.js create mode 100644 assets/cart-note.js create mode 100644 assets/collection-links.js create mode 100644 assets/component-cart-items.js create mode 100644 assets/component-quantity-selector.js create mode 100644 assets/component.js create mode 100644 assets/copy-to-clipboard.js create mode 100644 assets/critical.js create mode 100644 assets/dialog.js create mode 100644 assets/drag-zoom-wrapper.js create mode 100644 assets/events.js create mode 100644 assets/facets.js create mode 100644 assets/floating-panel.js create mode 100644 assets/focus.js create mode 100644 assets/global.d.ts create mode 100644 assets/header-drawer.js create mode 100644 assets/header-menu.js create mode 100644 assets/header.js create mode 100644 assets/icon-account.svg create mode 100644 assets/icon-add-to-cart.svg create mode 100644 assets/icon-arrow.svg create mode 100644 assets/icon-available.svg create mode 100644 assets/icon-caret.svg create mode 100644 assets/icon-cart.svg create mode 100644 assets/icon-checkmark.svg create mode 100644 assets/icon-close.svg create mode 100644 assets/icon-delete.svg create mode 100644 assets/icon-discount.svg create mode 100644 assets/icon-error.svg create mode 100644 assets/icon-external.svg create mode 100644 assets/icon-filter.svg create mode 100644 assets/icon-filters-close.svg create mode 100644 assets/icon-grid-default.svg create mode 100644 assets/icon-grid-dense.svg create mode 100644 assets/icon-inventory.svg create mode 100644 assets/icon-menu.svg create mode 100644 assets/icon-minus.svg create mode 100644 assets/icon-one-col-mobile.svg create mode 100644 assets/icon-orders.svg create mode 100644 assets/icon-pause.svg create mode 100644 assets/icon-play.svg create mode 100644 assets/icon-plus.svg create mode 100644 assets/icon-reset.svg create mode 100644 assets/icon-search.svg create mode 100644 assets/icon-shopify.svg create mode 100644 assets/icon-unavailable.svg create mode 100644 assets/jsconfig.json create mode 100644 assets/jumbo-text.js create mode 100644 assets/local-pickup.js create mode 100644 assets/localization.js create mode 100644 assets/marquee.js create mode 100644 assets/media-gallery.js create mode 100644 assets/media.js create mode 100644 assets/morph.js create mode 100644 assets/overflow-list.css create mode 100644 assets/paginated-list-aspect-ratio.js create mode 100644 assets/paginated-list.js create mode 100644 assets/performance.js create mode 100644 assets/predictive-search.js create mode 100644 assets/product-card-link.js create mode 100644 assets/product-card.js create mode 100644 assets/product-form.js create mode 100644 assets/product-inventory.js create mode 100644 assets/product-price.js create mode 100644 assets/product-recommendations.js create mode 100644 assets/product-title-truncation.js create mode 100644 assets/qr-code-generator.js create mode 100644 assets/qr-code-image.js create mode 100644 assets/quick-add.js create mode 100644 assets/recently-viewed-products.js create mode 100644 assets/results-list.js create mode 100644 assets/rte-formatter.js create mode 100644 assets/scrolling.js create mode 100644 assets/search-page-input.js create mode 100644 assets/section-renderer.js create mode 100644 assets/show-more.js create mode 100644 assets/slideshow.js create mode 100644 assets/template-giftcard.css create mode 100644 assets/theme-editor.js create mode 100644 assets/utilities.js create mode 100644 assets/variant-picker.js create mode 100644 assets/video-background.js create mode 100644 assets/view-transitions.js create mode 100644 assets/zoom-dialog.js create mode 100644 blocks/_accordion-row.liquid create mode 100644 blocks/_announcement.liquid create mode 100644 blocks/_blog-post-card.liquid create mode 100644 blocks/_blog-post-content.liquid create mode 100644 blocks/_blog-post-description.liquid create mode 100644 blocks/_blog-post-featured-image.liquid create mode 100644 blocks/_blog-post-image.liquid create mode 100644 blocks/_blog-post-info-text.liquid create mode 100644 blocks/_cart-products.liquid create mode 100644 blocks/_cart-summary.liquid create mode 100644 blocks/_cart-title.liquid create mode 100644 blocks/_collection-card-image.liquid create mode 100644 blocks/_collection-card.liquid create mode 100644 blocks/_collection-image.liquid create mode 100644 blocks/_collection-info.liquid create mode 100644 blocks/_collection-link.liquid create mode 100644 blocks/_content-without-appearance.liquid create mode 100644 blocks/_content.liquid create mode 100644 blocks/_divider.liquid create mode 100644 blocks/_featured-product-gallery.liquid create mode 100644 blocks/_featured-product-price.liquid create mode 100644 blocks/_featured-product.liquid create mode 100644 blocks/_footer-copyright.liquid create mode 100644 blocks/_footer-policy-list.liquid create mode 100644 blocks/_footer-social-icons.liquid create mode 100644 blocks/_header-logo.liquid create mode 100644 blocks/_header-menu.liquid create mode 100644 blocks/_heading.liquid create mode 100644 blocks/_image.liquid create mode 100644 blocks/_inline-collection-title.liquid create mode 100644 blocks/_inline-text.liquid create mode 100644 blocks/_marquee.liquid create mode 100644 blocks/_media-without-appearance.liquid create mode 100644 blocks/_media.liquid create mode 100644 blocks/_product-card-gallery.liquid create mode 100644 blocks/_product-card-group.liquid create mode 100644 blocks/_product-card.liquid create mode 100644 blocks/_product-details.liquid create mode 100644 blocks/_product-list-button.liquid create mode 100644 blocks/_product-list-content.liquid create mode 100644 blocks/_product-list-text.liquid create mode 100644 blocks/_product-media-gallery.liquid create mode 100644 blocks/_search-input.liquid create mode 100644 blocks/_slide.liquid create mode 100644 blocks/_social-link.liquid create mode 100644 blocks/accelerated-checkout.liquid create mode 100644 blocks/accordion.liquid create mode 100644 blocks/add-to-cart.liquid create mode 100644 blocks/button.liquid create mode 100644 blocks/buy-buttons.liquid create mode 100644 blocks/collection-card.liquid create mode 100644 blocks/collection-title.liquid create mode 100644 blocks/contact-form-submit-button.liquid create mode 100644 blocks/contact-form.liquid create mode 100644 blocks/custom-liquid.liquid create mode 100644 blocks/email-signup.liquid create mode 100644 blocks/featured-collection.liquid create mode 100644 blocks/filters.liquid create mode 100644 blocks/follow-on-shop.liquid create mode 100644 blocks/footer-utilities.liquid create mode 100644 blocks/group.liquid create mode 100644 blocks/icon.liquid create mode 100644 blocks/image.liquid create mode 100644 blocks/jumbo-text.liquid create mode 100644 blocks/logo.liquid create mode 100644 blocks/menu.liquid create mode 100644 blocks/page-content.liquid create mode 100644 blocks/page.liquid create mode 100644 blocks/payment-icons.liquid create mode 100644 blocks/popup-link.liquid create mode 100644 blocks/price.liquid create mode 100644 blocks/product-card.liquid create mode 100644 blocks/product-description.liquid create mode 100644 blocks/product-inventory.liquid create mode 100644 blocks/product-recommendations.liquid create mode 100644 blocks/product-title.liquid create mode 100644 blocks/quantity.liquid create mode 100644 blocks/review.liquid create mode 100644 blocks/spacer.liquid create mode 100644 blocks/swatches.liquid create mode 100644 blocks/text.liquid create mode 100644 blocks/variant-picker.liquid create mode 100644 blocks/video.liquid create mode 100644 config/settings_data.json create mode 100644 config/settings_schema.json create mode 100644 layout/password.liquid create mode 100644 layout/theme.liquid create mode 100644 locales/bg.json create mode 100644 locales/cs.json create mode 100644 locales/cs.schema.json create mode 100644 locales/da.json create mode 100644 locales/da.schema.json create mode 100644 locales/de.json create mode 100644 locales/de.schema.json create mode 100644 locales/el.json create mode 100644 locales/en.default.json create mode 100644 locales/en.default.schema.json create mode 100644 locales/es.json create mode 100644 locales/es.schema.json create mode 100644 locales/fi.json create mode 100644 locales/fi.schema.json create mode 100644 locales/fr.json create mode 100644 locales/fr.schema.json create mode 100644 locales/hr.json create mode 100644 locales/hu.json create mode 100644 locales/id.json create mode 100644 locales/it.json create mode 100644 locales/it.schema.json create mode 100644 locales/ja.json create mode 100644 locales/ja.schema.json create mode 100644 locales/ko.json create mode 100644 locales/ko.schema.json create mode 100644 locales/lt.json create mode 100644 locales/nb.json create mode 100644 locales/nb.schema.json create mode 100644 locales/nl.json create mode 100644 locales/nl.schema.json create mode 100644 locales/pl.json create mode 100644 locales/pl.schema.json create mode 100644 locales/pt-BR.json create mode 100644 locales/pt-BR.schema.json create mode 100644 locales/pt-PT.json create mode 100644 locales/pt-PT.schema.json create mode 100644 locales/ro.json create mode 100644 locales/ru.json create mode 100644 locales/sk.json create mode 100644 locales/sl.json create mode 100644 locales/sv.json create mode 100644 locales/sv.schema.json create mode 100644 locales/th.json create mode 100644 locales/th.schema.json create mode 100644 locales/tr.json create mode 100644 locales/tr.schema.json create mode 100644 locales/vi.json create mode 100644 locales/zh-CN.json create mode 100644 locales/zh-CN.schema.json create mode 100644 locales/zh-TW.json create mode 100644 locales/zh-TW.schema.json create mode 100644 release-notes.md create mode 100644 sections/_blocks.liquid create mode 100644 sections/collection-links.liquid create mode 100644 sections/collection-list.liquid create mode 100644 sections/custom-liquid.liquid create mode 100644 sections/divider.liquid create mode 100644 sections/featured-product.liquid create mode 100644 sections/footer-group.json create mode 100644 sections/footer.liquid create mode 100644 sections/header-announcements.liquid create mode 100644 sections/header-group.json create mode 100644 sections/header.liquid create mode 100644 sections/hero.liquid create mode 100644 sections/main-404.liquid create mode 100644 sections/main-blog-post.liquid create mode 100644 sections/main-blog.liquid create mode 100644 sections/main-cart.liquid create mode 100644 sections/main-collection-list.liquid create mode 100644 sections/main-collection.liquid create mode 100644 sections/main-page.liquid create mode 100644 sections/marquee.liquid create mode 100644 sections/media-with-content.liquid create mode 100644 sections/password.liquid create mode 100644 sections/predictive-search-empty.liquid create mode 100644 sections/predictive-search.liquid create mode 100644 sections/product-information.liquid create mode 100644 sections/product-list.liquid create mode 100644 sections/product-recommendations.liquid create mode 100644 sections/search-header.liquid create mode 100644 sections/search-results.liquid create mode 100644 sections/section-rendering-product-card.liquid create mode 100644 sections/section.liquid create mode 100644 sections/slideshow.liquid create mode 100644 snippets/account-actions.liquid create mode 100644 snippets/account-button.liquid create mode 100644 snippets/account-drawer.liquid create mode 100644 snippets/account-popover.liquid create mode 100644 snippets/add-to-cart-button.liquid create mode 100644 snippets/background-image.liquid create mode 100644 snippets/background-media.liquid create mode 100644 snippets/background-video.liquid create mode 100644 snippets/bento-grid.liquid create mode 100644 snippets/blog-comment-form.liquid create mode 100644 snippets/border-override.liquid create mode 100644 snippets/button.liquid create mode 100644 snippets/card-gallery.liquid create mode 100644 snippets/cart-bubble.liquid create mode 100644 snippets/cart-discount.liquid create mode 100644 snippets/cart-drawer.liquid create mode 100644 snippets/cart-icon-component.liquid create mode 100644 snippets/cart-note.liquid create mode 100644 snippets/cart-products.liquid create mode 100644 snippets/cart-summary.liquid create mode 100644 snippets/checkbox.liquid create mode 100644 snippets/collection-card.liquid create mode 100644 snippets/color-schemes.liquid create mode 100644 snippets/contact-form.liquid create mode 100644 snippets/divider.liquid create mode 100644 snippets/drawer-localization.liquid create mode 100644 snippets/dropdown-localization.liquid create mode 100644 snippets/editorial-collection-grid.liquid create mode 100644 snippets/editorial-product-grid.liquid create mode 100644 snippets/facets-actions.liquid create mode 100644 snippets/filter-remove-buttons.liquid create mode 100644 snippets/filters-toggle.liquid create mode 100644 snippets/fonts.liquid create mode 100644 snippets/gap-style.liquid create mode 100644 snippets/grid-density-controls.liquid create mode 100644 snippets/group.liquid create mode 100644 snippets/header-actions.liquid create mode 100644 snippets/header-drawer.liquid create mode 100644 snippets/header-menu.liquid create mode 100644 snippets/header-row.liquid create mode 100644 snippets/icon-or-image.liquid create mode 100644 snippets/icon.liquid create mode 100644 snippets/image.liquid create mode 100644 snippets/jumbo-text.liquid create mode 100644 snippets/layout-panel-style.liquid create mode 100644 snippets/link-featured-image.liquid create mode 100644 snippets/list-filter.liquid create mode 100644 snippets/localization-form.liquid create mode 100644 snippets/media.liquid create mode 100644 snippets/mega-menu-list.liquid create mode 100644 snippets/mega-menu.liquid create mode 100644 snippets/menu-font-styles.liquid create mode 100644 snippets/meta-tags.liquid create mode 100644 snippets/overflow-list.liquid create mode 100644 snippets/overlay.liquid create mode 100644 snippets/predictive-search-empty-state.liquid create mode 100644 snippets/predictive-search-products-list.liquid create mode 100644 snippets/predictive-search-resource-carousel.liquid create mode 100644 snippets/predictive-search.liquid create mode 100644 snippets/price-filter.liquid create mode 100644 snippets/price.liquid create mode 100644 snippets/product-card-badges.liquid create mode 100644 snippets/product-card-variant-url.liquid create mode 100644 snippets/product-card.liquid create mode 100644 snippets/product-grid.liquid create mode 100644 snippets/product-media.liquid create mode 100644 snippets/quantity-selector.liquid create mode 100644 snippets/quick-add-modal.liquid create mode 100644 snippets/quick-add.liquid create mode 100644 snippets/resource-card.liquid create mode 100644 snippets/resource-list-carousel.liquid create mode 100644 snippets/scripts.liquid create mode 100644 snippets/search-modal.liquid create mode 100644 snippets/search.liquid create mode 100644 snippets/section.liquid create mode 100644 snippets/size-style.liquid create mode 100644 snippets/skip-to-content-link.liquid create mode 100644 snippets/slideshow-arrow.liquid create mode 100644 snippets/slideshow-arrows.liquid create mode 100644 snippets/slideshow-controls.liquid create mode 100644 snippets/slideshow-slide.liquid create mode 100644 snippets/slideshow.liquid create mode 100644 snippets/sorting.liquid create mode 100644 snippets/spacing-padding.liquid create mode 100644 snippets/spacing-style.liquid create mode 100644 snippets/strikethrough-variant.liquid create mode 100644 snippets/stylesheets.liquid create mode 100644 snippets/submenu-font-styles.liquid create mode 100644 snippets/swatch.liquid create mode 100644 snippets/tax-info.liquid create mode 100644 snippets/text.liquid create mode 100644 snippets/theme-editor.liquid create mode 100644 snippets/theme-styles-variables.liquid create mode 100644 snippets/timeline-scope.liquid create mode 100644 snippets/typography-style.liquid create mode 100644 snippets/unit-price.liquid create mode 100644 snippets/util-autofill-img-size-attr.liquid create mode 100644 snippets/util-mega-menu-img-sizes-attr.liquid create mode 100644 snippets/util-product-grid-card-size.liquid create mode 100644 snippets/util-product-media-sizes-attr.liquid create mode 100644 snippets/variant-main-picker.liquid create mode 100644 snippets/variant-quick-add.liquid create mode 100644 snippets/variant-swatches.liquid create mode 100644 snippets/video.liquid create mode 100644 templates/404.json create mode 100644 templates/article.json create mode 100644 templates/blog.json create mode 100644 templates/cart.json create mode 100644 templates/collection.json create mode 100644 templates/gift_card.liquid create mode 100644 templates/index.json create mode 100644 templates/list-collections.json create mode 100644 templates/page.contact.json create mode 100644 templates/page.json create mode 100644 templates/password.json create mode 100644 templates/product.json create mode 100644 templates/search.json diff --git a/.cursor/prompts/fix-accesibility-issue.md b/.cursor/prompts/fix-accesibility-issue.md new file mode 100644 index 000000000..b732cff2e --- /dev/null +++ b/.cursor/prompts/fix-accesibility-issue.md @@ -0,0 +1,90 @@ +# Fix Accessibility Issue + +Analyze and fix accessibility issues in web applications, following the standard GitHub issue workflow with accessibility-specific considerations. + +## Usage + +``` +Fix accessibility issue in [COMPONENT_NAME] or Fix GitHub accessibility issue #[NUMBER] +``` + +## Accessibility Rules Reference + +**IMPORTANT**: Use the `fetch_rules` tool to access specific accessibility rules for components you're working on: + +- `accordion-accessibility`: Accordion component ARIA patterns +- `breadcrumb-accessibility`: Breadcrumb navigation patterns +- `carousel-accessibility`: Carousel/slider ARIA compliance +- `cart-drawer-accessibility`: Cart drawer accessibility patterns +- `color-swatch-accessibility`: Color swatch component patterns +- `combobox-accessibility`: Combobox/dropdown ARIA patterns +- `disclosure-accessibility`: Disclosure/collapsible content patterns +- `dropdown-navigation-accessibility`: Navigation dropdown patterns +- `modal-accessibility`: Modal/dialog ARIA Dialog Pattern +- `product-card-accessibility`: Product card accessibility patterns +- `product-filter-accessibility`: Product filtering interface patterns +- `sale-price-accessibility`: Sale price display patterns +- `slider-accessibility`: Slider/range input patterns +- `switch-accessibility`: Toggle switch patterns +- `tab-accessibility`: Tab interface patterns +- `tooltip-accessibility`: Tooltip accessibility patterns + +Always fetch and follow the relevant rule(s) for the component you're fixing. + +## Base Workflow + +**Follow the complete workflow from `fix-github-issue` command**, including: + +- Testing requirements (full test suite with `--reporter=line`) +- Commit standards (one-liner messages only) +- Test failure handling and selector updates +- Force push safety with `--force-with-lease` +- Write down learnings as a last step + +## Accessibility-Specific Principles + +### Critical Implementation Rules + +- **Role must be on the element that contains the items** - not the wrapper +- **Screen readers need direct parent-child relationship** between role and items +- **Test with actual screen readers**, not just markup validation +- **Fetch specific component rules** before implementing fixes + +### Testing Requirements + +In addition to standard testing requirements: + +- **Update page object models** when changing roles (e.g., `navigation` → `menubar`) +- **Test with screen readers** to verify actual behavior, not just markup +- **Verify individual items are recognized**, not just containers +- **Test focus states thoroughly** by navigating away and back to components + +### Focus Management Best Practices + +- **Consistent focus behavior** across all interaction methods (keyboard vs mouse) +- **Focus state bugs are subtle** - may look correct but behave wrong on subsequent interactions +- **Reset focus state properly** when closing dropdowns/menus with ESC vs selection +- **Centralize focus management logic** to avoid duplication and inconsistency + +### Performance Considerations + +- **Complex keyboard navigation can introduce lag** - balance functionality with performance +- **Test on slower devices** to ensure accessibility JavaScript doesn't impact UX +- **Consider simpler solutions first** before implementing custom keyboard handling + +### Implementation Guidelines + +- **Fetch and follow existing rules** for the component type you're working on +- **Search for existing ARIA patterns** in the codebase first +- **Make minimal changes** that improve accessibility +- **Focus on semantic correctness** over visual changes +- **Ensure backward compatibility** +- **Don't over-engineer** - native browser behavior often suffices +- **Use `aria-labelledby`** when referencing existing visible text instead of duplicating with `aria-label` + +### Code Quality Standards + +- **Avoid duplicate logic** between keyboard and mouse interaction handlers +- **Single responsibility principle** - separate ARIA state management from focus management +- **Centralize common patterns** like focus reset and state management +- **Refactor when you find redundancy** in accessibility implementations diff --git a/.cursor/prompts/fix-breaking-changes.md b/.cursor/prompts/fix-breaking-changes.md new file mode 100644 index 000000000..ab77bc12a --- /dev/null +++ b/.cursor/prompts/fix-breaking-changes.md @@ -0,0 +1,246 @@ +# Breaking Changes Fix Command + +## Command: Fix Shopify Theme Breaking Changes + +### Prerequisites + +- `breaking-changes.md` file exists in project root with fix instructions +- Modify files in the `templates/` folder and `config/settings_data.json` +- Use scripts when possible, clean up temporary files afterward + +### Step-by-Step Process + +1. **Analyze Breaking Changes Documentation** + + - Read the `breaking-changes.md` file to understand what needs to be fixed + - Identify the specific changes required based on the documentation + + **Look for:** + + - Settings that need to be removed + - Block types that need to be changed + - Property values that need to be updated + - Context changes (e.g., `closest.product` vs explicit settings) + +2. **Create Adaptable Fix Script Template** + Create `scripts/fix-breaking-changes.js` and customize the `applyFixes` function based on breaking-changes.md: + + ```javascript + #!/usr/bin/env node + const fs = require('fs'); + const path = require('path'); + const templatesDir = './templates'; + const configFile = './config/settings_data.json'; + + // ======================================== + // CUSTOMIZE THIS SECTION BASED ON breaking-changes.md + // ======================================== + function applyFixes(obj) { + if (typeof obj !== 'object' || obj === null) return obj; + if (Array.isArray(obj)) return obj.map(applyFixes); + + const result = { ...obj }; + + // ADD YOUR BREAKING CHANGE FIXES HERE + // Examples based on common patterns: + + // 1. Remove specific settings (adapt key/value as needed) + if (result.settings) { + // Example: Remove closest.product/collection references + // if (result.settings.product === "{{ closest.product }}") delete result.settings.product; + // if (result.settings.collection === "{{ closest.collection }}") delete result.settings.collection; + // Example: Remove deprecated settings + // delete result.settings.deprecated_setting; + } + + // 2. Update block types (adapt old/new types as needed) + // Example: Change block types in specific contexts + // if (result.type === 'old-block-type') result.type = 'new-block-type'; + + // 3. Update setting values (adapt property/old/new values as needed) + // if (result.settings && result.settings.some_property === 'old-value') { + // result.settings.some_property = 'new-value'; + // } + + // 4. Rename properties (adapt old/new property names as needed) + // if (result.old_property_name) { + // result.new_property_name = result.old_property_name; + // delete result.old_property_name; + // } + + // Recursively process nested objects + for (const key in result) { + if (typeof result[key] === 'object' && result[key] !== null) { + result[key] = applyFixes(result[key]); + } + } + + return result; + } + // ======================================== + // END CUSTOMIZATION SECTION + // ======================================== + + function processTemplateFile(filePath) { + try { + console.log(`Processing ${filePath}...`); + const content = fs.readFileSync(filePath, 'utf8'); + + // Preserve comment headers + const commentMatch = content.match(/^(\/\*[\s\S]*?\*\/)\s*/); + const comment = commentMatch ? commentMatch[1] : ''; + const jsonContent = commentMatch ? content.slice(commentMatch[0].length) : content; + + // Parse and apply fixes + const data = JSON.parse(jsonContent); + const processedData = applyFixes(data); + + // Write back with preserved formatting + const updatedJsonContent = JSON.stringify(processedData, null, 2); + const updatedContent = comment ? comment + '\n' + updatedJsonContent : updatedJsonContent; + + fs.writeFileSync(filePath, updatedContent); + console.log(`✓ Updated ${filePath}`); + } catch (error) { + console.error(`Error processing ${filePath}:`, error.message); + } + } + + function processConfigFile(filePath) { + try { + console.log(`Processing ${filePath}...`); + const content = fs.readFileSync(filePath, 'utf8'); + const data = JSON.parse(content); + const processedData = applyFixes(data); + + const updatedContent = JSON.stringify(processedData, null, 2); + fs.writeFileSync(filePath, updatedContent); + console.log(`✓ Updated ${filePath}`); + } catch (error) { + console.error(`Error processing ${filePath}:`, error.message); + } + } + + function main() { + console.log('🔧 Fixing breaking changes in template files and config...\n'); + + // Process template files + const files = fs.readdirSync(templatesDir); + const jsonFiles = files.filter((file) => file.endsWith('.json')); + + if (jsonFiles.length > 0) { + console.log(`Found ${jsonFiles.length} template files to process:\n`); + jsonFiles.forEach((file) => { + const filePath = path.join(templatesDir, file); + processTemplateFile(filePath); + }); + } else { + console.log('No JSON template files found.'); + } + + // Process config file + if (fs.existsSync(configFile)) { + console.log(`\nProcessing config file: ${configFile}`); + processConfigFile(configFile); + } else { + console.log(`\nConfig file not found: ${configFile}`); + } + + console.log('\n✅ All template files and config have been processed!'); + console.log('Next: Run theme check to verify fixes'); + } + + if (require.main === module) main(); + ``` + +3. **Customize the Fix Script** + Based on your `breaking-changes.md` analysis, uncomment and modify the relevant fix patterns in the `applyFixes` function. + + **Common Fix Patterns:** + + - **Settings Removal**: `if (result.settings.key === 'value') delete result.settings.key;` + - **Block Type Changes**: `if (result.type === 'old-type') result.type = 'new-type';` + - **Value Updates**: `if (result.settings.prop === 'old') result.settings.prop = 'new';` + - **Property Renaming**: `result.newName = result.oldName; delete result.oldName;` + +4. **Execute Fix Script** + + ```bash + node fix-breaking-changes.js + ``` + +5. **Verify Fixes** + + ```bash + shopify theme check --fail-level error + ``` + +6. **Review and Iterate** + If theme check still shows errors: + + - Analyze remaining issues + - Update the `applyFixes` function + - Re-run the script + - Verify again + +7. **Handle Issues Outside Templates and Config** + If theme check still fails after template and config fixes, some issues may require changes outside the `templates/` folder and `config/settings_data.json`. + + **STOP HERE** and: + + - Run theme check again to capture remaining errors + - Analyze which files outside `templates/` and `config/` need changes + - Identify what specific changes are required + + **Create a summary report:** + + - List which files outside `templates/` and `config/` need changes + - Document what specific changes are required + - Note that these fixes require manual intervention + - Examples of files that might need fixes: + - `sections/*.liquid` + - `blocks/*.liquid` + - `snippets/*.liquid` + - `assets/*.js` or `assets/*.css` + - Schema files in `schemas/` + + **Report format:** + + 🚫 BREAKING CHANGES REQUIRING MANUAL FIXES OUTSIDE TEMPLATES/ AND CONFIG/ + + The following issues cannot be resolved by modifying templates/ and config/ only: + + 1. [File path] - [Description of required change] + 2. [File path] - [Description of required change] + + These require manual review and fixes in non-template and non-config files. + +8. **Clean Up** + + ```bash + rm scripts/fix-breaking-changes.js + # Keep remaining-issues.txt if there are unresolved issues + ``` + +### Expected Outcome + +- All breaking changes in template files and config are fixed +- Theme check passes with no errors +- Template files and config follow new architecture requirements +- Temporary files are cleaned up + +### Framework Benefits + +- **Adaptable**: Easily customizable for any breaking change type +- **Reusable**: Same process works for future releases +- **Safe**: Preserves JSON formatting and comment headers +- **Comprehensive**: Handles nested objects and arrays recursively +- **Targeted**: Modifies both templates and config as required + +### Notes for Future Use + +1. **Always start** by thoroughly reading `breaking-changes.md` +2. **Identify patterns** in the breaking changes (settings removal, type changes, etc.) +3. **Use the template** and customize the `applyFixes` function accordingly +4. **Test incrementally** - run theme check after each fix type +5. **Document your fixes** in the script comments for future reference diff --git a/.cursor/prompts/get-breaking-changes.md b/.cursor/prompts/get-breaking-changes.md new file mode 100644 index 000000000..72f88b573 --- /dev/null +++ b/.cursor/prompts/get-breaking-changes.md @@ -0,0 +1,116 @@ +# Get Breaking Changes + +This command helps identify confirmed breaking changes merged since the last breaking version in a repository. + +**Modified Approach**: Instead of searching through commits, this process focuses on pull requests that have been explicitly labeled with "Breaking changes" to identify confirmed breaking changes. + +## Command Steps: + +### 1. Get All SEMVER Tags + +```bash +# Using GitHub CLI to get all SEMVER tags +gh api repos/:owner/:repo/git/refs/tags --jq '.[].ref' | sed 's|refs/tags/||' | grep -E "^[0-9]+\.[0-9]+\.[0-9]+$" | sort -V + +# Alternative using git +git show-ref --tags | grep -E "refs/tags/[0-9]+\.[0-9]+\.[0-9]+$" +``` + +### 2. Identify Last Breaking Version + +- In SEMVER, breaking changes occur when major version increases +- Find the first version in the current major version series (e.g., 1.0.1 for 1.x.x series) +- This represents the last breaking change + +### 3. Get Breaking Version Commit Date + +```bash +# Get commit date for the breaking version tag +git show --format="%ci" --no-patch +``` + +### 4. Find Pull Requests with Breaking Changes Label + +```bash +# Search for merged PRs with "Breaking changes" label merged to main since the breaking version date +gh pr list --label "Breaking changes" --state merged --search "merged:>YYYY-MM-DD base:main" --json number,title,mergedAt,url + +# Example: Find breaking changes PRs merged to main after 2025-05-22 +gh pr list --label "Breaking changes" --state merged --search "merged:>2025-05-22 base:main" --json number,title,mergedAt,url +``` + +### 5. Get PR Details + +Use `fetch_pull_request` tool to get full details of each breaking change PR: + +``` +fetch_pull_request(pullNumberOrCommitHash: "PR_NUMBER") +``` + +### 6. Classify Breaking Changes + +Review each PR and classify by impact type: + +- **API Changes**: Removal or modification of public APIs +- **Configuration Changes**: Removal of settings or configuration options +- **Behavior Changes**: Changes that could break existing functionality +- **Schema Changes**: Changes to data structures or interfaces +- **Removal of Features**: Deprecated or removed functionality + +Save the analysis into the breaking-changes.md file. If the file already contains text, replace it. + +### 7. Analyze Solution Patterns + +Examine the template file changes in each breaking change PR to identify actionable patterns for solving the breaking changes: + +- Look at `/templates` directory changes in the PR diffs +- Identify common patterns in how configurations were updated (settings removal, block type changes, etc.) +- Focus on specific find/replace patterns that can be applied systematically +- Add these patterns to the breaking-changes.md file as shown in the example output format + +## Example Output Format: + +``` +## Breaking Changes Since Last Breaking Version (X.Y.Z) + +### Confirmed Breaking Changes: +1. **PR #XXXX** - "Title" (Merged: DATE) + - **Type**: API Changes/Configuration Changes/Behavior Changes/etc. + - **Impact**: Description of what breaks and how it affects users + +2. **PR #YYYY** - "Title" (Merged: DATE) + - **Type**: Schema Changes + - **Impact**: Description of breaking changes + +### Potential Breaking Changes: +1. **PR #ZZZZ** - "Title" (Merged: DATE) + - **Needs Review**: Description of potential impact that requires further analysis + +## Summary: +X confirmed breaking changes found since version X.Y.Z (DATE) + +**Key Areas of Impact:** +- Brief summary of main breaking change categories +- Notes on merchant/user impact + +## Solving breaking changes + +Based on the template file changes in the breaking change PRs, here are the key patterns to follow when updating themes: + +### Breaking Change Name (PR #XXXX): +- Specific find/replace pattern or configuration change needed +- Another configuration change with before/after example +- Remove/add specific settings or block types + +### Another Breaking Change (PR #YYYY): +- Different set of patterns for this breaking change +- Specific block type replacements needed +- Configuration updates required +``` + +## Usage Notes: + +- Focus on changes that would require users to modify their code/configuration +- Internal refactoring may not be breaking unless it affects public APIs +- Consider both technical breaking changes and UX breaking changes +- Document the rationale for why each change is considered breaking diff --git a/.cursor/prompts/update-schema-translations.md b/.cursor/prompts/update-schema-translations.md new file mode 100644 index 000000000..0d4986180 --- /dev/null +++ b/.cursor/prompts/update-schema-translations.md @@ -0,0 +1,94 @@ +# Update Schema Translations + +## Instructions + +This guide helps you translate English strings in theme schema JSON files into multiple languages. + +## Usage + +1. Create the translation data structure +2. Run the translation script with the schema flag + +## Step 1: Create Translation Data Structure + +You must translate English strings in a JSON object into multiple languages. +Use the following structure to create the file `scripts/translation-data.json`: + +```json +{ + "sourceStructure": { + "categories": { + "banners": "Banners", + "decorative": "Decorative", + "storytelling": "Storytelling" + }, + "content": { + "advanced": "Advanced", + "some_key": { + "child_key_1": "Child key 1" + } + } + }, + "wordTranslations": {} +} +``` + +Inside the `wordTranslations` key, match the structure in `sourceStructure`, but add a translation for each language. + +## Getting Language Codes + +You can use the following command to get the list of language codes: + +```bash +ls locales/*.schema.json | grep -v en.default.schema.json | sed 's|locales/||g' | sed 's|\.schema\.json||g' +``` + +## Constraints + +- Match the sourceStructure in the wordTranslations +- Have each leaf key's value be an object that includes translations for each of the languages +- Do not read any other files +- Only add translations for the keys in sourceStructure +- Never add any other keys + +## Step 2: Run the Translation Script + +After creating the `scripts/translation-data.json` file, run: + +```bash +node scripts/update-translations.js --schema +``` + +This will update all schema translation files with the new translations. + +## Example + +For a `sourceStructure` with: + +```json +{ + "categories": { + "banners": "Banners" + } +} +``` + +The `wordTranslations` would look like: + +```json +{ + "categories": { + "banners": { + "fr": "Bannières", + "es": "Banners", + "de": "Banner" + // ... other languages + } + } +} +``` + +## Tips + +- Use CMD+SHIFT+V to paste the JSON structure into cursor as plain text +- The script will automatically delete the `translation-data.json` file after completion diff --git a/.cursor/prompts/update-translations.md b/.cursor/prompts/update-translations.md new file mode 100644 index 000000000..7af0f1d80 --- /dev/null +++ b/.cursor/prompts/update-translations.md @@ -0,0 +1,94 @@ +# Update Storefront Translations + +## Instructions + +This guide helps you translate English strings in theme locale JSON files into multiple languages for storefront-facing content. + +## Usage + +1. Create the translation data structure +2. Run the translation script + +## Step 1: Create Translation Data Structure + +You must translate English strings in a JSON object into multiple languages. +Use the following structure to create the file `scripts/translation-data.json`: + +```json +{ + "sourceStructure": { + "actions": { + "add": "Add", + "add_to_cart": "Add to cart" + }, + "blocks": { + "contact_form": { + "name": "Name", + "email": "Email" + } + } + }, + "wordTranslations": {} +} +``` + +Inside the `wordTranslations` key, match the structure in `sourceStructure`, but add a translation for each language. + +## Getting Language Codes + +You can use the following command to get the list of language codes: + +```bash +ls locales/*.json | grep -v schema | grep -v en.default.json | sed 's|locales/||g' | sed 's|\.json||g' +``` + +## Constraints + +- Match the sourceStructure in the wordTranslations +- Have each leaf key's value be an object that includes translations for each of the languages +- Do not read any other files +- Only add translations for the keys in sourceStructure +- Never add any other keys + +## Step 2: Run the Translation Script + +After creating the `scripts/translation-data.json` file, run: + +```bash +node scripts/update-translations.js +``` + +This will update all locale translation files with the new translations. + +## Example + +For a `sourceStructure` with: + +```json +{ + "actions": { + "add": "Add" + } +} +``` + +The `wordTranslations` would look like: + +```json +{ + "actions": { + "add": { + "fr": "Ajouter", + "es": "Añadir", + "de": "Hinzufügen" + // ... other languages + } + } +} +``` + +## Tips + +- Use CMD+SHIFT+V to paste the JSON structure into cursor as plain text +- The script will automatically delete the `translation-data.json` file after completion +- This is for storefront translations (customer-facing text), not schema translations diff --git a/.cursor/rules/accordion-accessibility.mdc b/.cursor/rules/accordion-accessibility.mdc new file mode 100644 index 000000000..2c2bc9e84 --- /dev/null +++ b/.cursor/rules/accordion-accessibility.mdc @@ -0,0 +1,218 @@ +--- +description: Accordion component accessibility compliance and WAI-ARIA Accordion Pattern +globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid +alwaysApply: false +--- + +# Accordion Component Accessibility Standards + +Ensures accordion components follow WCAG compliance and WAI-ARIA Accordion Pattern specifications. + + +name: accordion_accessibility_standards +description: Enforce accordion component accessibility standards and WAI-ARIA Accordion Pattern compliance +filters: + - type: file_extension + pattern: "\\.(vue|jsx|tsx|html|php|js|ts|liquid)$" + +actions: + +- type: enforce + conditions: + + # Accordion header button role requirement + + - pattern: "(?i)]_(?:accordion|expand|collapse)[^>]_>" + pattern_negate: "role=\"button\"" + message: "Accordion header buttons should have role='button' (or use native button element which has implicit role)." + + # Accordion header missing aria-expanded + + - pattern: "(?i)]_(?:accordion|expand|collapse)[^>]_>" + pattern_negate: "aria-expanded=\"(true|false)\"" + message: "Accordion header buttons must have aria-expanded attribute set to 'true' or 'false'." + + # Accordion header missing aria-controls + + - pattern: "(?i)]_(?:accordion|expand|collapse)[^>]_>" + pattern_negate: "aria-controls=\"[^\"]+\"" + message: "Accordion header buttons must have aria-controls attribute referencing the ID of the associated panel." + + # Heading wrapper missing role + + - pattern: "(?i)<(div|section)[^>]*(?:accordion.*header|header._accordion)[^>]_>" + pattern_negate: "role=\"heading\"" + message: "Accordion header wrappers should have role='heading' or use native heading elements (h1-h6)." + + # Heading role missing aria-level + + - pattern: "(?i)<[^>]_role=\"heading\"[^>]_>" + pattern_negate: "aria-level=\"[1-6]\"" + message: "Elements with role='heading' must have aria-level attribute set to appropriate level (1-6)." + + # Panel missing proper identification + + - pattern: "(?i)<(div|section)[^>]*(?:accordion.*panel|panel._accordion)[^>]_>" + pattern_negate: "id=\"[^\"]+\"" + message: "Accordion panels must have unique ID attributes for aria-controls reference." + + # Panel with region role missing aria-labelledby + + - pattern: "(?i)<[^>]_role=\"region\"[^>]_>" + pattern_negate: "aria-labelledby=\"[^\"]+\"" + message: "Accordion panels with role='region' must have aria-labelledby referencing the controlling button." + + # Missing keyboard event handlers + + - pattern: "(?i)]_(?:accordion|expand|collapse)[^>]_>" + pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)" + message: "Accordion header buttons should handle keyboard events (Enter, Space, optionally Arrow keys)." + + # Missing Escape key support for accordion content + + - pattern: "(?i)<(div|section)[^>]*(?:accordion.*panel|panel._accordion)[^>]_>" + pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)" + message: "Accordion panels should handle Escape key to close panel and return focus to header." + +- type: suggest + message: | + **Accordion Component Accessibility Best Practices:** + + **Required ARIA Attributes:** + + - **role='button':** Set on accordion header elements (or use native button) + - **role='heading':** Set on accordion header container with aria-level + - **aria-expanded:** 'true' if panel is visible, 'false' if collapsed + - **aria-controls:** Reference to the ID of the associated panel content + - **aria-level:** Appropriate heading level (1-6) for information architecture + - **aria-disabled:** 'true' if panel cannot be collapsed (optional) + + **Optional ARIA Attributes:** + + - **role='region':** On panel content containers (avoid with >6 panels) + - **aria-labelledby:** On panels with role='region', referencing the header button + + **Keyboard Interaction Requirements:** + + - **Enter/Space:** Toggle panel expansion/collapse + - **Tab/Shift+Tab:** Move through all focusable elements in page order + - **Down/Up Arrow:** (Optional) Navigate between accordion headers + - **Home/End:** (Optional) Jump to first/last accordion header + - **Escape:** Close open panel and return focus to header button + + **Structure Requirements:** + + - Header button must be the only element inside heading container + - Each panel must have unique ID for aria-controls reference + - Use native heading elements (h1-h6) when possible instead of role='heading' + - Avoid role='region' on panels when many accordions exist (>6 panels) + + **Implementation Patterns:** + + **Single Accordion Item:** + + ```html +
+

+ +

+ +
+ ``` + + **JavaScript for Accordion with Escape Support:** + + ```javascript + function toggleAccordion(button) { + const isExpanded = button.getAttribute('aria-expanded') === 'true'; + const panel = document.getElementById(button.getAttribute('aria-controls')); + + button.setAttribute('aria-expanded', !isExpanded); + panel.hidden = isExpanded; + + if (!isExpanded) { + // Add escape key listener to panel + panel.addEventListener('keydown', handleAccordionEscapeKey); + } else { + // Remove escape key listener + panel.removeEventListener('keydown', handleAccordionEscapeKey); + } + } + + function handleAccordionEscapeKey(event) { + if (event.key === 'Escape') { + const panel = event.target.closest('[hidden]'); + if (panel) { + const button = document.querySelector(`[aria-controls="${panel.id}"]`); + if (button) { + button.setAttribute('aria-expanded', 'false'); + panel.hidden = true; + button.focus(); // Return focus to header + panel.removeEventListener('keydown', handleAccordionEscapeKey); + } + } + } + } + ``` + + **Using Native Heading:** + + ```html +
+

+ +

+
+

Panel content...

+
+
+ ``` + + **JavaScript Considerations:** + + - Implement Enter and Space key handlers for expansion/collapse + - Optionally implement Arrow key navigation between headers + - Update aria-expanded state when panels toggle + - Consider implementing single-expand vs multi-expand behavior + - Use hidden attribute or CSS to show/hide panels (note: CSS visibility property can be animated) + - Ensure smooth keyboard navigation flow + - Implement Escape key handler to close open panel and return focus to header + - Add/remove event listeners when panels open/close to manage Escape key support + + **Accessibility Notes:** + + - Role 'region' helps screen readers understand panel structure + - Avoid role='region' proliferation with many simultaneous panels + - Button should be direct child of heading element + - Consider aria-disabled='true' for panels that cannot be collapsed + - Test with screen readers to ensure proper announcement + +metadata: +priority: high +version: 1.0 +
diff --git a/.cursor/rules/assets.mdc b/.cursor/rules/assets.mdc new file mode 100644 index 000000000..eabf02702 --- /dev/null +++ b/.cursor/rules/assets.mdc @@ -0,0 +1,12 @@ +--- +description: +globs: assets/* +alwaysApply: false +--- +# Assets + +The assets directory contains any assets that need to be referenced within a `.liquid` file, usually using the [asset_url](mdc:https:/shopify.dev/docs/api/liquid/filters/asset_url) Liquid filter. + +Assets is a flat directory, it may not contain subdirectories. + +Any images that are required in the code, including icons, may be stored within assets. Icons can be used in `.liquid` files via the [inline_asset_content](mdc:https:/shopify.dev/docs/api/liquid/filters/inline_asset_content) Liquid filter. diff --git a/.cursor/rules/blocks.mdc b/.cursor/rules/blocks.mdc new file mode 100644 index 000000000..2fc9fb9cd --- /dev/null +++ b/.cursor/rules/blocks.mdc @@ -0,0 +1,344 @@ +--- +description: +globs: blocks/*.liquid +alwaysApply: false +--- +# Theme Blocks Development Standards + +Follow [Shopify's theme blocks documentation](mdc:https:/shopify.dev/docs/storefronts/themes/architecture/blocks/theme-blocks/quick-start?framework=liquid.txt). + +## Theme Block Fundamentals + +Theme blocks are reusable components defined at the theme level that can be: +- Nested under sections and blocks +- Configured using settings in the theme editor +- Given presets and added by merchants +- Used as [static blocks](mdc:https:/shopify.dev/docs/storefronts/themes/architecture/blocks/theme-blocks/static-blocks#statically-vs-dynamically-rendered-theme-blocks) by theme developers + +Blocks render in the editor and storefront when they are referenced in [template files](mdc:.cursor/rules/templates.mdc). + +### Basic Block Structure +```liquid +{% doc %} + Block description and usage examples + + @example + {% content_for 'block', type: 'block-name', id: 'unique-id' %} +{% enddoc %} + +
+ +
+ +{% stylesheet %} + /* + Scoped CSS for this block + + Use BEM structure + CSS written in here should be for components that are exclusively in this block. If the CSS will be used elsewhere, it should instead be written in [assets/base.css](mdc:@assets/base.css) + */ +{% endstylesheet %} + +{% schema %} +{ + "name": "Block Name", + "settings": [], + "presets": [] +} +{% endschema %} +``` + +### Static Block Usage + +Static blocks are theme blocks that are rendered directly in Liquid templates by developers, rather than being dynamically added through the theme editor. This allows for predetermined block placement with optional default settings. + +**Basic Static Block Syntax:** +```liquid +{% content_for 'block', type: 'text', id: 'header-announcement' %} +``` + +**Example: Product Template with Mixed Static and Dynamic Blocks** +```liquid + +
+ {% comment %} Static breadcrumb block {% endcomment %} + {% content_for 'block', type: 'breadcrumb', id: 'product-breadcrumb' %} + +
+
+ {% comment %} Static product gallery block {% endcomment %} + {% content_for 'block', type: 'product-gallery', id: 'main-gallery', settings: { + enable_zoom: true, + thumbnails_position: "bottom" + } %} +
+ +
+ {% comment %} Static product info blocks {% endcomment %} + {% content_for 'block', type: 'product-title', id: 'product-title' %} + {% content_for 'block', type: 'product-price', id: 'product-price' %} + {% content_for 'block', type: 'product-form', id: 'product-form' %} + + {% comment %} Dynamic blocks area for additional content {% endcomment %} +
+ {% content_for 'blocks' %} +
+
+
+ + {% comment %} Static related products block {% endcomment %} + {% content_for 'block', type: 'related-products', id: 'related-products', settings: { + heading: "You might also like", + limit: 4 + } %} +
+``` + +**Key Points about Static Blocks:** +- They have a fixed `id` that makes them identifiable in the theme editor +- Settings can be overridden in the theme editor despite having defaults +- They appear in the theme editor as locked blocks that can't be removed or reordered +- Useful for consistent layout elements that should always be present +- Can be mixed with dynamic block areas using `{% content_for 'blocks' %}` + +## Schema Configuration + +See [schemas.mdc](mdc:.cursor/rules/schemas.mdc) for rules on schemas + +### Advanced Schema Features + +#### Exclude wrapper + +```json +{ + "tag": null // No wrapper - must include {{ block.shopify_attributes }} for proper editor function +} +``` + +## Block Implementation Patterns + +### Accessing Block Data + +**Block Settings:** +```liquid +{{ block.settings.text }} +{{ block.settings.heading | escape }} +{{ block.settings.image | image_url: width: 800 }} +``` + +**Block Properties:** +```liquid +{{ block.id }} // Unique block identifier +{{ block.type }} // Block type name +{{ block.shopify_attributes }} // Required for theme editor +``` + +**Section Context:** +```liquid +{{ section.id }} // Parent section ID +{{ section.settings.heading | escape }} +{{ section.settings.image | image_url: width: 800 }} +``` + +## Nested Blocks Implementation + +### Rendering Nested Blocks +```liquid +
+

{{ block.settings.heading | escape }}

+ +
+ {% content_for 'blocks' %} +
+
+``` + +### Nesting with Layout Control +```liquid +
+ {% content_for 'blocks' %} +
+``` + +### Presets with Nested Blocks +```json +{ + "presets": [ + { + "name": "t:names.two_column_layout", + "category": "Layout", + "settings": { + "layout_direction": "horizontal" + }, + "blocks": [ + { + "type": "text", + "settings": { + "text": "Column 1 content" + } + }, + { + "type": "text", + "settings": { + "text": "Column 2 content" + } + } + ] + } + ] +} +``` + +## CSS and Styling + +See [css-standards.mdc](mdc:.cursor/rules/css-standards.mdc) for rules on writing CSS + +### Scoped Styles +```liquid +{% stylesheet %} +.block-name { + padding: var(--block-padding, 1rem); + background: var(--block-background, transparent); +} + +.block-name__title { + font-size: var(--title-size, 1.5rem); + color: var(--title-color, inherit); +} + +.block-name--primary { + background-color: var(--color-primary); +} + +.block-name--secondary { + background-color: var(--color-secondary); +} +{% endstylesheet %} +``` + +### Dynamic CSS Variables +```liquid +
+``` + +## Block Targeting + +### Section Schema for Theme Blocks +```json +{ + "blocks": [ + { "type": "@theme" }, // Accept all theme blocks + { "type": "@app" } // Accept app blocks + ] +} +``` + +### Restricted Block Targeting +```json +{ + "blocks": [ + { + "type": "text", + "name": "Text Content" + }, + { + "type": "image", + "name": "Image Content" + } + ] +} +``` + +## Common Block Patterns + +### Content Block +```liquid +
+ {% if block.settings.heading != blank %} +

{{ block.settings.heading | escape }}

+ {% endif %} + + {% if block.settings.text != blank %} +
{{ block.settings.text }}
+ {% endif %} + + {% if block.settings.button_text != blank %} + + {{ block.settings.button_text | escape }} + + {% endif %} +
+``` + +### Media Block +```liquid +
+ {% if block.settings.image %} +
+ {{ block.settings.image | image_url: width: 800 | image_tag: + alt: block.settings.image.alt | default: block.settings.alt_text + }} +
+ {% endif %} + + {% if block.settings.video %} +
+ {{ block.settings.video | video_tag: controls: true }} +
+ {% endif %} +
+``` + +### Layout Block (Container) +```liquid +
+ {% content_for 'blocks' %} +
+``` + +## Performance Best Practices + + +### Conditional Rendering +```liquid +{% liquid + assign has_content = false + if block.settings.heading != blank or block.settings.text != blank + assign has_content = true + endif +%} + +{% if has_content %} +
+ +
+{% endif %} +``` + + +## Examples Referenced + +[text.liquid](mdc:.cursor/rules/blocks/examples/text.liquid) - Basic content block from existing project +[group.liquid](mdc:.cursor/rules/blocks/examples/group.liquid) - Container with nested blocks from existing project +[button.liquid](mdc:.cursor/rules/blocks/examples/button.liquid) - Advanced button block with multiple styles +[card.liquid](mdc:.cursor/rules/blocks/examples/card.liquid) - Flexible card container demonstrating complex nesting +[hero-banner.liquid](mdc:.cursor/rules/blocks/examples/hero-banner.liquid) - Hero banner with targeting and static features diff --git a/.cursor/rules/breadcrumb-accessibility.mdc b/.cursor/rules/breadcrumb-accessibility.mdc new file mode 100644 index 000000000..4da15e627 --- /dev/null +++ b/.cursor/rules/breadcrumb-accessibility.mdc @@ -0,0 +1,129 @@ +--- +description: Breadcrumb component accessibility compliance pattern +globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid +alwaysApply: false +--- + +# Breadcrumb Accessibility + +Ensures breadcrumb components follow WCAG compliance and WAI-ARIA Breadcrumb Pattern specifications. + + +name: breadcrumb_accessibility_standards +description: Enforce breadcrumb component accessibility standards and WAI-ARIA Breadcrumb Pattern compliance +filters: + - type: file_extension + pattern: "\\.(vue|jsx|tsx|html|php|js|ts|liquid)$" + +actions: + +- type: enforce + conditions: + + # Navigation landmark requirement + + - pattern: "(?i)]_(?:breadcrumb|navigation)[^>]_>" + pattern_negate: "(aria-label|aria-labelledby)=\"[^\"]+\"" + message: "Breadcrumb navigation must have aria-label or aria-labelledby attribute." + + # Current page aria-current requirement + + - pattern: "(?i)<[^>]*(?:breadcrumb.*current|current._breadcrumb)[^>]_>" + pattern_negate: "aria-current=\"page\"" + message: "Current page in breadcrumb must have aria-current='page' attribute." + + # List structure requirement + + - pattern: "(?i)]_(?:breadcrumb|navigation)[^>]_>" + pattern_negate: "]\*>" + message: "Breadcrumb navigation should use ordered list (ol) for proper structure." + +- type: suggest + message: | + **Breadcrumb Component Accessibility Best Practices:** + + **Required ARIA Attributes:** + + - **aria-label/aria-labelledby:** On navigation element to describe the breadcrumb trail + - **aria-current="page":** On the current page link or element + - **role="navigation":** Implicit on nav element, but can be explicit if needed + + **Structure Requirements:** + + - Use `
+ + + {% if blog.moderated? %} +

+ {{- 'blogs.comment_form.moderated' | t -}} +

+ {% endif %} + + + +{% endform %} + +{% stylesheet %} + .blog-post-comments__form-container { + --comment-form-gap: var(--gap-md); + + width: 100%; + max-width: var(--normal-content-width); + margin: var(--margin-4xl) auto 0; + } + + .blog-post-comments__form { + display: grid; + grid-template-columns: 1fr; + gap: var(--comment-form-gap); + + @media screen and (min-width: 750px) { + grid-template-columns: 1fr 1fr; + } + } + + .blog-post-comments__form-input { + padding: var(--padding-lg) var(--padding-xl); + border: var(--style-border-width-inputs) solid var(--color-input-border); + } + + .blog-post-comments__form-input--textarea { + resize: vertical; + min-height: var(--input-textarea-min-height); + } + + .blog-post-comments__form-message { + display: flex; + align-items: center; + gap: var(--gap-xs); + } + + .blog-post-comments__form-body { + grid-column: 1 / -1; + } + + .blog-post-comments__form-input:focus-visible { + outline: var(--focus-outline-width) solid currentcolor; + outline-offset: var(--focus-outline-offset); + } + + .blog-post-comments__form-moderated { + font-size: var(--font-size--xs); + } + + .blog-post-comments__form-submit { + margin-block-start: var(--comment-form-gap); + } +{% endstylesheet %} diff --git a/snippets/border-override.liquid b/snippets/border-override.liquid new file mode 100644 index 000000000..94d8888f0 --- /dev/null +++ b/snippets/border-override.liquid @@ -0,0 +1,7 @@ +{%- comment -%} + Renders border override CSS +{%- endcomment -%} + +--border-width: {{ settings.border_width }}px; --border-style: {{ settings.border }}; --border-color: +rgb(var(--color-border-rgb) / {{ settings.border_opacity | divided_by: 100.0 }}); --border-radius: +{{ settings.border_radius }}px; {% if settings.border_radius > 0 %} overflow: hidden; {% endif %} diff --git a/snippets/button.liquid b/snippets/button.liquid new file mode 100644 index 000000000..c8ae53790 --- /dev/null +++ b/snippets/button.liquid @@ -0,0 +1,43 @@ +{%- doc -%} + Intended for use in a block similar to the button block. + + @param {string} link - link to render + @param {object} [block] - The block + + @example + {% render 'button', link: '/collections/all' %} +{%- enddoc -%} + + + {{ block.settings.label }} + + +{% stylesheet %} + .link { + text-decoration: none; + text-decoration-color: currentcolor; + + &:hover { + color: var(--color-primary-hover); + text-decoration-color: transparent; + } + } +{% endstylesheet %} diff --git a/snippets/card-gallery.liquid b/snippets/card-gallery.liquid new file mode 100644 index 000000000..7727cbb70 --- /dev/null +++ b/snippets/card-gallery.liquid @@ -0,0 +1,403 @@ +{%- doc -%} + Displays product images in a carousel. + Settings allow for a full slideshow, showing only the first image, or showing the second image when hovering. + Note: When the product card itself is in a carousel layout, the card-gallery's carousel is disabled with JavaScript. + + @param [children] - Additional content rendered below the card gallery + @param [section] - The section object the snippet is rendered in + @param [block] - The block object the snippet is rendered in + @param [has_applied_colour_filter] - Whether there's an applied colour filter +{%- enddoc -%} + +{% liquid + assign image_sizes = '(min-width: 750px) 50vw, 100vw' + # if the card-gallery has a section.settings.product_card_size: + # assume grid-template autofill(card-size, 1fr) and calculate the sizes attribute based on the minimum card size + + # if section has section.settings.columns: + # assume grid-template repeat(column-count, 1fr) and calculate the sizes attribute based on the number of columns +%} + +{% if section.settings.product_card_size %} + {% capture card_size %} + {% render 'util-product-grid-card-size' section: section %} + {% endcapture %} + {% assign card_size = card_size | strip | replace: 'px', '' | plus: 0 %} + {% capture sizes_attribute %} + {% render 'util-autofill-img-size-attr' card_size: card_size, card_gap: section.settings.columns_gap_horizontal %} + {% endcapture %} + {% assign image_sizes = sizes_attribute | strip %} +{% elsif section.settings.columns and section.settings.layout_type != 'editorial' %} + {% assign viewport_width = 100.0 | divided_by: section.settings.columns %} + {% assign sizes_attribute = '(min-width: 750px) [viewport_width]vw, 100vw' + | replace: '[viewport_width]', viewport_width + %} + {% assign image_sizes = sizes_attribute | strip %} +{% endif %} + +{% liquid + assign product = closest.product + + assign lazy_image_sizes = 'auto, ' | append: image_sizes + + assign image_ratio_setting = block.settings.image_ratio + assign temp_ratio = 1 + + if image_ratio_setting == 'landscape' + assign temp_ratio = '16 / 9' + elsif image_ratio_setting == 'portrait' + assign temp_ratio = '4 / 5' + elsif image_ratio_setting == 'square' + assign temp_ratio = '1' + elsif image_ratio_setting == 'adapt' + if product != blank + assign current_featured_image_for_ratio = product.featured_image + if current_featured_image_for_ratio == blank and product.featured_media.preview_image != blank + assign current_featured_image_for_ratio = product.featured_media.preview_image + endif + + if current_featured_image_for_ratio != blank and current_featured_image_for_ratio.aspect_ratio != null and current_featured_image_for_ratio.aspect_ratio > 0 + assign temp_ratio = current_featured_image_for_ratio.aspect_ratio + endif + endif + endif + + if temp_ratio != blank and temp_ratio != 0 and temp_ratio != '' + assign ratio = temp_ratio + else + assign ratio = 1 + endif + + assign variant_images = product.images | where: 'attached_to_variant?', true | map: 'src' + assign selected_variant_image = product.selected_or_first_available_variant.image.src + + assign hover_behavior = 'carousel' + + if block.settings.image_ratio == 'adapt' and block.settings.constrain_to_viewport + if block.settings.constrain_to_viewport != '' + assign constrain_to_viewport = true + assign media_fit = block.settings.constrain_to_viewport + endif + endif +%} + + + +{% stylesheet %} + .card-gallery { + overflow: hidden; + container-type: inline-size; /* Make card-gallery a container */ + container-name: card-gallery-container; /* Optional: name the container */ + } + + .card-gallery__placeholder svg { + height: 100%; + width: 100%; + } + + .card-gallery svg { + aspect-ratio: var(--gallery-aspect-ratio, var(--ratio)); + } + + .product-card-gallery__title-placeholder { + padding: var(--padding-md); + font-size: var(--font-size--2xl); + line-height: var(--line-height--display-loose); + word-break: break-word; + color: var(--color-foreground); + background-color: rgb(var(--color-foreground-rgb) / var(--opacity-5)); + aspect-ratio: var(--gallery-aspect-ratio); + border-radius: var(--product-corner-radius); + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + } + + .product-card-gallery__title-placeholder a { + color: var(--color-foreground); + } + + @media screen and (min-width: 750px) { + .product-grid[data-product-card-size='extra-large'] .product-card-gallery__title-placeholder { + padding: var(--padding-3xl); + font-size: var(--font-size--3xl); + } + + .product-grid[data-product-card-size='large'] .product-card-gallery__title-placeholder { + padding: var(--padding-2xl); + font-size: var(--font-size--2xl); + } + + .product-grid[data-product-card-size='medium'] .product-card-gallery__title-placeholder { + padding: var(--padding-xl); + font-size: var(--font-size--xl); + } + + .product-grid[data-product-card-size='small'] .product-card-gallery__title-placeholder { + padding: var(--padding-sm); + font-size: var(--font-size--lg); + } + + .product-grid[data-product-card-size='extra-large'] + .card-gallery:has(.product-badges--top-right .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-right: calc(var(--padding-3xl) + 50px); + } + + .product-grid[data-product-card-size='large'] + .card-gallery:has(.product-badges--top-right .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-right: calc(var(--padding-2xl) + 50px); + } + + .product-grid[data-product-card-size='medium'] + .card-gallery:has(.product-badges--top-right .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-right: calc(var(--padding-xl) + 50px); + } + + .product-grid[data-product-card-size='small'] + .card-gallery:has(.product-badges--top-right .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-right: calc(var(--padding-sm) + 50px); + } + + .product-grid[data-product-card-size='extra-large'] + .card-gallery:has(.product-badges--top-left .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-top: calc(var(--padding-3xl) + 40px); + } + + .product-grid[data-product-card-size='large'] + .card-gallery:has(.product-badges--top-left .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-top: calc(var(--padding-2xl) + 40px); + } + + .product-grid[data-product-card-size='medium'] + .card-gallery:has(.product-badges--top-left .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-top: calc(var(--padding-xl) + 40px); + } + + .product-grid[data-product-card-size='small'] + .card-gallery:has(.product-badges--top-left .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-top: calc(var(--padding-sm) + 40px); + } + + .product-grid[data-product-card-size='extra-large'] + .card-gallery:has(.product-badges--bottom-left .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-bottom: calc(var(--padding-3xl) + 40px); + } + + .product-grid[data-product-card-size='large'] + .card-gallery:has(.product-badges--bottom-left .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-bottom: calc(var(--padding-2xl) + 40px); + } + + .product-grid[data-product-card-size='medium'] + .card-gallery:has(.product-badges--bottom-left .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-bottom: calc(var(--padding-xl) + 40px); + } + + .product-grid[data-product-card-size='small'] + .card-gallery:has(.product-badges--bottom-left .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-bottom: calc(var(--padding-sm) + 40px); + } + } + + @media screen and (max-width: 749px) { + .product-card-gallery__title-placeholder { + font-size: var(--font-size--xl); + padding: var(--padding-md); + } + + .product-grid[data-product-card-size] + .card-gallery:has(.product-badges--top-right .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-right: calc(var(--padding-sm) + 50px); + } + + .product-grid[data-product-card-size] + .card-gallery:has(.product-badges--top-left .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-top: calc(var(--padding-sm) + 40px); + } + + .product-grid[data-product-card-size] + .card-gallery:has(.product-badges--bottom-left .product-badges__badge) + .product-card-gallery__title-placeholder { + padding-bottom: calc(var(--padding-sm) + 40px); + } + } + + [product-grid-view='zoom-out'] .card-gallery .product-card-gallery__title-placeholder { + padding: var(--padding-xs) !important; + font-size: var(--font-size--xs); + } +{% endstylesheet %} diff --git a/snippets/cart-bubble.liquid b/snippets/cart-bubble.liquid new file mode 100644 index 000000000..1954e41af --- /dev/null +++ b/snippets/cart-bubble.liquid @@ -0,0 +1,40 @@ +{%- doc -%} + @param [limit] - {number} + @param [live_region] - {boolean} + + The maximum number of items in the cart to display. If the number of items in the cart is greater than this limit, the + count will be displayed as "99+". +{%- enddoc -%} + +
+ + + + {{- 'accessibility.cart_count' | t -}} + : {{ cart.item_count }} + + + +
diff --git a/snippets/cart-discount.liquid b/snippets/cart-discount.liquid new file mode 100644 index 000000000..91bd285d4 --- /dev/null +++ b/snippets/cart-discount.liquid @@ -0,0 +1,232 @@ +{%- doc -%} + Renders a cart discount form. + + @param {string} section_id - The section ID +{%- enddoc -%} + +{% liquid + assign discount_codes = cart.cart_level_discount_applications | where: 'type', 'discount_code' | map: 'title' + for item in cart.items + for allocation in item.line_level_discount_allocations + if allocation.discount_application.type == 'discount_code' + assign discount_codes = item.line_level_discount_allocations | slice: forloop.index0 | map: 'discount_application' | map: 'title' | concat: discount_codes + endif + endfor + endfor + + assign discount_codes = discount_codes | uniq +%} + + + +
0 %} + open + {% endif %} + declarative-open + > + + {{ 'content.discount' | t }} + + + {{- 'icon-plus.svg' | inline_asset_content -}} + + + +
+
+ + + + + +
+ +
    + {% for discount_code in discount_codes %} +
  • +

    + {{ discount_code }} +

    + +
  • + {% endfor %} +
+
+
+
+
+ +{% stylesheet %} + .cart-discount__input { + background-color: var(--color-input-background); + color: var(--color-input-text); + border-width: var(--style-border-width-inputs); + border-color: var(--color-input-border); + border-style: solid; + border-radius: var(--style-border-radius-inputs); + padding: var(--padding-sm) var(--padding-md); + height: 100%; + flex-grow: 1; + min-width: 0; + } + + .cart-discount__input::placeholder { + color: rgb(var(--color-input-text-rgb) / var(--opacity-subdued-text)); + } + + .cart-discount__label { + display: flex; + align-items: flex-start; + gap: var(--gap-2xs); + font-size: var(--cart-font-size--sm); + } + + .cart-discount__pill-code { + overflow: hidden; + max-width: 100px; + text-overflow: ellipsis; + white-space: nowrap; + margin: 0; + } + + .cart-discount { + width: 100%; + } + + .cart-discount__summary { + display: flex; + align-items: center; + justify-content: space-between; + } + + .cart-discount__summary:hover { + color: rgb(var(--color-foreground-rgb) / var(--opacity-subdued-text)); + } + + .cart-discount__codes { + display: none; + gap: var(--padding-xs); + flex-wrap: wrap; + list-style: none; + padding-inline: 0; + margin: 0; + } + + .cart-discount__codes:has(.cart-discount__pill) { + display: flex; + } + + .cart-discount__button { + height: 100%; + } + + .cart-discount__content { + height: calc(var(--button-size) + var(--padding-2xs) + var(--padding-sm)); + } + + .cart-discount__pill { + display: flex; + color: var(--color-foreground); + gap: var(--padding-xs); + align-items: center; + padding: var(--padding-xs) var(--padding-sm); + border-radius: var(--style-border-radius-pills); + background-color: var(--color-input-background); + text-transform: uppercase; + } + + .cart-discount__form { + display: flex; + gap: var(--padding-md); + align-items: center; + height: 100%; + padding-block: var(--padding-2xs) var(--padding-sm); + } + + :is(.cart-discount__pill-remove, .cart-discount__pill-remove:hover) { + --close-icon-opacity: 0.4; + + color: var(--color-foreground); + background-color: transparent; + pointer-events: all; + cursor: pointer; + height: 100%; + } + + .cart-discount__error { + display: flex; + align-items: center; + width: 100%; + padding-block: var(--padding-2xs) var(--padding-sm); + } + + .cart-discount__error .svg-wrapper { + flex-shrink: 0; + width: var(--icon-size-xs); + height: var(--icon-size-xs); + margin-inline: var(--margin-3xs) var(--margin-xs); + } + + .cart-discount__error-text { + margin-block-start: var(--margin-3xs); + } + + cart-discount-component { + display: flex; + } +{% endstylesheet %} diff --git a/snippets/cart-drawer.liquid b/snippets/cart-drawer.liquid new file mode 100644 index 000000000..01ed3481a --- /dev/null +++ b/snippets/cart-drawer.liquid @@ -0,0 +1,171 @@ +{%- doc -%} + Renders the cart drawer, a slide-out panel that displays the contents of the cart. It includes the cart icon that acts as a trigger. + + @param {object} [settings] - An object containing theme settings. + + @param {boolean} [settings.auto_open_cart_drawer] - If `true`, the cart drawer opens automatically after an item is + added. + @param {string} [settings.drawer_color_scheme] - The color scheme for the drawer. +{%- enddoc -%} + + + + + + + +
+ + {%- if cart.empty? -%} +
+ +
+ +
+ + {{ 'content.your_cart_is_empty' | t }} + + +
+ {% render 'cart-products' %} +
+
+ {%- else -%} +
+ + {{ 'content.cart_title' | t }} + {% render 'cart-bubble' %} + + + +
+ +
+ + {% render 'cart-products' %} + + +
+ {% render 'cart-summary' %} +
+
+ {%- endif -%} +
+
+
+
+ +{% stylesheet %} + .cart-items-component { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + } + + .cart-drawer__heading .cart-bubble { + width: fit-content; + border-radius: var(--style-border-radius-buttons-primary); + aspect-ratio: auto; + padding: var(--cart-padding); + } + + .cart-drawer__heading .cart-bubble[data-maintain-ratio] { + aspect-ratio: 1; + min-width: 26px; + } + + .cart-drawer__header { + background-color: var(--color-background); + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: var(--cart-drawer-padding); + border-bottom: var(--style-border-width) solid none; + position: sticky; + top: 0; + z-index: 1; + + @media screen and (min-width: 750px) { + padding: var(--cart-drawer-padding-desktop); + } + } + + .cart-drawer__dialog { + overflow: hidden; + } + + .cart-drawer__inner { + height: 100%; + overflow: hidden; + } + + .cart-drawer__content { + height: calc(100% - var(--header-height)); + display: flex; + flex-direction: column; + } + + .cart-drawer__summary { + background-color: var(--color-background); + position: sticky; + bottom: 0; + z-index: 1; + } +{% endstylesheet %} diff --git a/snippets/cart-icon-component.liquid b/snippets/cart-icon-component.liquid new file mode 100644 index 000000000..a17f7c0bc --- /dev/null +++ b/snippets/cart-icon-component.liquid @@ -0,0 +1,32 @@ +{%- doc -%} + Renders the cart icon, which displays the number of items in the cart via a bubble. +{%- enddoc -%} + + + + {% render 'cart-bubble', limit: 100, live_region: true, test_id: test_id %} + + +{% stylesheet %} + cart-icon:has(.cart-bubble__text-count:empty) { + --cart-bubble-size: 10px; + --cart-bubble-top: 9px; + --cart-bubble-right: 9px; + + .svg-wrapper { + --cart-bubble-top: 4px; + --cart-bubble-right: 4px; + } + } +{% endstylesheet %} diff --git a/snippets/cart-note.liquid b/snippets/cart-note.liquid new file mode 100644 index 000000000..1f2e85c1a --- /dev/null +++ b/snippets/cart-note.liquid @@ -0,0 +1,38 @@ + + + + +
+ + + {{ 'content.seller_note' | t }} + + + + {{- 'icon-plus.svg' | inline_asset_content -}} + + + +
+ + +
+
+
+
diff --git a/snippets/cart-products.liquid b/snippets/cart-products.liquid new file mode 100644 index 000000000..73b31b589 --- /dev/null +++ b/snippets/cart-products.liquid @@ -0,0 +1,753 @@ + + +
+ {% if cart.empty? %} + {%- if shop.customer_accounts_enabled and customer == null -%} +

+ {{ 'actions.log_in_html' | t: link: routes.account_login_url }} +

+ {%- endif -%} + + + {{ 'actions.continue_shopping' | t }} + + {%- else -%} + +
+
+ + + + + + + + + + + + + + {% for item in cart.items %} + + + + + + + + {% endfor %} + +
+ {{ 'content.cart_total' | t }} + {{ cart.total_price | money_with_currency }} +
+ {{ 'content.product_image' | t }} + + {{ 'content.product_information' | t }} + + {{ 'content.quantity' | t }} + + {{ 'content.product_total' | t }} +
+ {% if item.image -%} + {% liquid + assign ratio = 1 + assign border_opacity = settings.cart_thumbnail_border_opacity | divided_by: 100.0 + assign border_override = '--border-width: [cart_thumbnail_border_width]px; --border-style: [cart_thumbnail_border_style]; --border-color: rgb(var(--color-border-rgb) / [cart_thumbnail_border_opacity]); --border-radius: [cart_thumbnail_border_radius]px;' | replace: '[cart_thumbnail_border_width]', settings.cart_thumbnail_border_width | replace: '[cart_thumbnail_border_style]', settings.cart_thumbnail_border | replace: '[cart_thumbnail_border_opacity]', border_opacity | replace: '[cart_thumbnail_border_radius]', settings.cart_thumbnail_border_radius + + if settings.cart_thumbnail_border_radius > 0 + assign border_override = border_override | append: ' overflow: hidden;' + endif + if block.settings.image_ratio == 'portrait' + assign ratio = 0.8 + elsif block.settings.image_ratio == 'adapt' + assign ratio = item.image.aspect_ratio + endif + %} + + {%- liquid + echo item.image | image_url: width: 250 | image_tag: class: 'cart-items__media-image border-style', style: border_override + -%} + + {%- endif %} + +

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

+ {% if item.product.vendor and block.settings.vendor %} +

+ {{ item.product.vendor }} +

+ {% endif %} + + {%- if item.item_components.size != 0 -%} +
    + {%- for component in item.item_components -%} +
  • + {{- component.title -}} + {%- if component.quantity > 1 -%} + × {{ component.quantity }} + {%- endif -%} +
  • + {%- endfor -%} +
+ {%- endif -%} + + {%- if item.product.has_only_default_variant == false + or item.properties.size != 0 + or item.selling_plan_allocation != null + -%} +
+ {%- if item.product.has_only_default_variant == false and item.item_components.size == 0 -%} + {%- for option in item.options_with_values -%} +
+
{{ option.name }}:
+
+ {{- option.value -}} + {%- if forloop.last != true %}, {% endif -%} +
+
+ {%- endfor -%} + {%- endif -%} + + {%- for property in item.properties -%} + {%- assign property_first_char = property.first | slice: 0 -%} + {%- if property.last != blank and property_first_char != '_' -%} +
+
{{ property.first }}:
+
+ {%- if property.last contains '/uploads/' -%} + {{ property.last | split: '/' | last }} + {%- else -%} + {{ property.last }} + {%- endif -%} +
+
+ {%- endif -%} + {%- endfor -%} +
+ + {% if item.selling_plan_allocation %} +

{{ item.selling_plan_allocation.selling_plan.name }}

+ {% endif %} + {%- endif -%} + + {% if item.line_level_discount_allocations.size > 0 %} +
    + {%- for discount in item.line_level_discount_allocations -%} +
  • {{ discount.discount_application.title | escape }}
  • + {%- endfor -%} +
+ {% endif %} + +
+ {% if item.original_price != item.final_price %} + {{ 'content.price_sale' | t }} + {{ item.final_price | money }} + {{ 'content.price_regular' | t }} + + {% if item.variant.compare_at_price > item.original_price %} + {{ item.variant.compare_at_price | money }} + {% else %} + {{ item.original_price | money }} + {% endif %} + + {% else %} + {% if item.variant.compare_at_price > item.original_price %} + {{ 'content.price_sale' | t }} + {% else %} + {{ 'content.price' | t }} + {% endif %} + + {{ item.original_price | money }} + + {% if item.variant.compare_at_price > item.original_price %} + {{ 'content.price_regular' | t }} + {{ item.variant.compare_at_price | money }} + {% endif %} + {% endif %} +
+
+ {% # Here I want to pass some arguments to the quantity block so it knows which value should the input be set to. Though quantity block could be a snippet instead %} + {% assign can_update_quantity = item.instructions.can_update_quantity + | default: true, allow_false: true + %} + {% render 'quantity-selector', + product: item.product, + in_cart_quantity: item.quantity, + line_index: item.index, + min: 0, + class: 'cart-primary-typography', + can_update_quantity: can_update_quantity + %} + + + + {%- liquid + if settings.currency_code_enabled_cart_items + assign price = item.final_line_price | money_with_currency + assign unit_price = item.unit_price | money_with_currency + else + assign price = item.final_line_price | money + assign unit_price = item.unit_price | money + endif + -%} + {{ price }} + {%- if item.unit_price_measurement -%} +
+ {% render 'unit-price', price: unit_price, measurement: item.unit_price_measurement %} +
+ {%- endif -%} +
+
+
+ {%- endif -%} +
+ +{% stylesheet %} + .cart-items { + --cart-item-media-width-min: 2.5rem; + --cart-item-media-width-max: 7.5rem; + + container-name: cart-items; + container-type: inline-size; + width: 100%; + } + + .cart-items-disabled { + pointer-events: none; + } + + .cart-items__table { + width: 100%; + } + + .cart-items__table * { + margin: 0; + } + + .cart-items__table-row { + --cart-item-price-width: 6rem; + + display: grid; + grid-template-columns: clamp(2.5rem, 15cqi, 7.5rem) minmax(0, 1fr) minmax(var(--cart-item-price-width), auto); + grid-template-areas: + 'media details price' + 'media quantity price' + 'media error error'; + column-gap: var(--gap-md); + align-items: start; + padding-bottom: var(--cart-items-gap); + margin-bottom: var(--margin-lg); + } + + .cart-items__table-row.cart-items__nested-line td:first-child { + width: 60%; + justify-self: right; + } + + html:active-view-transition-type(page-navigation) .cart-items__table-row { + view-transition-name: none !important; + } + + .cart-items__table-row.removing { + overflow: hidden; + animation: removeRow calc(var(--animation-speed) * 2) var(--animation-easing) forwards; + animation-delay: var(--animation-speed); + } + + @keyframes removeRow { + 0% { + height: var(--row-height); + } + + 100% { + opacity: 0; + height: 0; + padding-bottom: 0; + margin-bottom: 0; + border-color: transparent; + } + } + + .cart-items__table-row:last-child { + padding-bottom: 0; + } + + .cart-items--dividers .cart-items__table-row { + border-bottom: 1px solid var(--color-border); + margin-bottom: var(--cart-items-gap); + } + + .cart-items--dividers .cart-items__table-row:has(+ .cart-items__nested-line) { + border-bottom: none; + margin-bottom: 0; + } + + .cart-items--dividers .cart-items__table-row:last-child { + border-block-end: none; + padding-block-end: 0; + margin-bottom: 0; + } + + .cart-items__details { + grid-area: details; + color: rgb(var(--color-foreground-rgb) / var(--opacity-70)); + } + + .cart-items__details > * + *, + .cart-items__bundle li { + margin-block-start: var(--margin-2xs); + } + + .cart-items__details * { + font-size: var(--cart-font-size--sm); + } + + .cart-items__details a { + text-decoration: none; + } + + .cart-items__title { + font-size: var(--cart-font-size--md); + color: var(--color-foreground); + text-transform: var(--product-title-case); + } + + .cart-items__variant { + display: inline-block; + } + + .cart-items__quantity { + grid-area: quantity; + margin-block-start: var(--margin-xs); + display: flex; + align-items: center; + justify-content: flex-start; + gap: var(--gap-xs); + width: fit-content; + } + + .cart-items__quantity .quantity-selector { + display: inline-flex; + flex: 0 1 var(--quantity-selector-width); + font-size: var(--cart-font-size--sm); + height: auto; + } + + .cart-items__remove { + background-color: transparent; + color: var(--color-foreground); + width: var(--minimum-touch-target); + height: var(--minimum-touch-target); + justify-content: center; + box-shadow: none; + padding: 0; + } + + .cart-items__media { + grid-area: media; + padding: 0; + } + + .cart-items__price { + grid-area: price; + min-height: unset; + min-width: var(--cart-item-price-width); + text-align: end; + display: block; + font-size: var(--cart-font-size--md); + } + + .cart-items__price-unit { + font-size: var(--cart-font-size--xs); + } + + .cart-items__media-container { + display: flex; + aspect-ratio: var(--ratio); + position: relative; + width: 100%; + overflow: hidden; + } + + .cart-items__media-image { + aspect-ratio: inherit; + object-fit: cover; + object-position: center center; + width: 100%; + height: auto; + } + + .cart-items__empty-button { + margin-top: var(--margin-md); + padding-inline: var(--padding-4xl); + padding-block: var(--padding-lg); + } + + /* Error message */ + .cart-items__error { + display: flex; + align-items: flex-start; + width: 100%; + grid-area: error; + margin-block-start: var(--margin-xs); + opacity: 1; + overflow: hidden; + transform: translateY(0); + transition: opacity var(--drawer-animation-speed) var(--animation-easing), + transform var(--drawer-animation-speed) var(--animation-easing); + + @starting-style { + opacity: 0; + transform: translateY(-0.5rem); + } + } + + .cart-item__error { + display: flex; + align-items: flex-start; + width: 100%; + font-size: var(--cart-font-size--sm); + padding-block: var(--padding-2xs); + } + + .cart-item__error .svg-wrapper { + flex-shrink: 0; + width: var(--icon-size-xs); + height: var(--icon-size-xs); + margin-inline: var(--margin-3xs) var(--margin-xs); + margin-block-start: var(--margin-3xs); + } + + @container cart-items (min-width: 720px) { + .cart-items__table-row { + --cart-item-price-width: 6rem; + + grid-template-columns: 7.5rem 1fr 1fr minmax(var(--cart-item-price-width), auto); + grid-template-rows: min-content 1fr; + grid-template-areas: + 'media details quantity price' + 'media details error error'; + } + + .cart-items__quantity, + .cart-items__price { + grid-area: initial; + } + + .cart-items__quantity { + margin-top: 0; + } + + .cart-items__price { + min-height: var(--minimum-touch-target); + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: center; + } + } + + .cart__original-total-container, + .cart__total-container { + display: flex; + flex-direction: column; + } + + .cart__total-container { + row-gap: var(--gap-2xs); + + &:has(.cart__installments) { + row-gap: var(--gap-xs); + } + } + + .cart__original-total-container:empty { + display: none; + } + + .cart__summary-totals { + display: flex; + flex-direction: column; + gap: var(--gap-xl); + width: 100%; + border-block-start: none; + + &:has(> :first-child:not(.cart__original-total-container, .cart__total-container)) { + padding-block-start: 0; + border-block-start: none; + } + + @media screen and (min-width: 750px) { + padding-block-start: 0; + } + } + + .cart__original-total-container, + .cart__original-total-container * { + font-size: var(--cart-font-size--sm); + } + + .cart__total { + font-weight: var(--font-weight-bold); + } + + .cart__total-label { + font-size: var(--cart-font-size--sm); + } + + .cart__total-value { + font-size: var(--cart-font-size--2xl); + } + + .cart-primary-typography { + font-family: var(--cart-primary-font-family); + font-style: var(--cart-primary-font-style); + font-weight: var(--cart-primary-font-weight); + } + + .cart-secondary-typography { + font-family: var(--cart-secondary-font-family); + font-style: var(--cart-secondary-font-style); + font-weight: var(--cart-secondary-font-weight); + } + + .cart__ctas { + width: 100%; + display: grid; + gap: var(--checkout-button-gap); + grid-auto-flow: row; + grid-template-columns: 1fr; + } + + .cart__additional-checkout-buttons { + width: 100%; + } + + .cart__ctas .cart__checkout-button { + width: 100%; + height: clamp(25px, var(--height-buy-buttons), 55px); + padding-inline: var(--padding-4xl); + } + + shopify-accelerated-checkout-cart { + --shopify-accelerated-checkout-inline-alignment: center; + --shopify-accelerated-checkout-button-border-radius: var(--style-border-radius-buttons-primary); + } + + .cart-note { + width: 100%; + } + + .cart-note__inner { + padding-block: var(--padding-2xs) var(--padding-sm); + } + + .cart-note__summary { + display: flex; + align-items: center; + justify-content: space-between; + } + + .cart-note__summary:hover { + color: rgb(var(--color-foreground-rgb) / var(--opacity-subdued-text)); + } + + .cart-note__label { + display: flex; + align-items: flex-start; + gap: var(--gap-2xs); + font-size: var(--cart-font-size--sm); + } + + .cart-note__instructions { + color: var(--color-input-text); + background-color: var(--color-input-background); + border-width: var(--style-border-width-inputs); + border-color: var(--color-input-border); + transition: box-shadow var(--animation-speed) ease; + box-shadow: var(--input-box-shadow); + min-height: 5.5rem; + min-width: 100%; + max-width: 100%; + font-size: var(--cart-font-size--sm); + padding: max(4px, calc(var(--style-border-radius-inputs) * (1 - cos(45deg)))); + } + + .cart-note .svg-wrapper { + height: var(--icon-size-sm); + width: var(--icon-size-sm); + margin: 0; + } + + .cart-note .icon-plus { + height: var(--icon-size-xs); + width: var(--icon-size-xs); + } + + /* Remove animation */ + .remove-icon-bottom, + .remove-icon-top { + transition: transform var(--animation-speed) var(--animation-easing); + } + + .cart-items__remove:hover .remove-icon-top { + transform: translate(calc(-1 * var(--icon-stroke-width)), var(--icon-stroke-width)) rotate(-15deg); + } + + .cart-items__remove:is(:hover, :active) .remove-icon-bottom { + transform: translateY(var(--icon-stroke-width)); + } + + .cart-items__table-row.removing .remove-icon-bottom { + transform: translateY(0); + } + + .cart-items__table-row.removing .remove-icon-top { + animation: removeButtonClickedIconTop var(--animation-speed) var(--animation-easing) forwards; + } + + @keyframes removeButtonClickedIconTop { + 50% { + transform: translate(0, calc(-1 * var(--icon-stroke-width))); + } + + 100% { + transform: translate(0, 0); + } + } + + .cart-items__properties { + display: block; + margin-block-start: var(--margin-2xs); + } + + .cart-items__properties dt, + .cart-items__properties dd { + display: inline; + } +{% endstylesheet %} diff --git a/snippets/cart-summary.liquid b/snippets/cart-summary.liquid new file mode 100644 index 000000000..fe5e37cc8 --- /dev/null +++ b/snippets/cart-summary.liquid @@ -0,0 +1,126 @@ +{%- doc -%} + Renders the cart summary totals. +{%- enddoc -%} + +
+ {% # We need to keep this node in place to allow morphing to work properly # %} +
+ {%- if cart.cart_level_discount_applications.size > 0 -%} + + {{ 'content.cart_subtotal' | t }} + + {{- cart.original_total_price | money -}} + + +
+
    + {%- for discount in cart.cart_level_discount_applications -%} +
  • + + {{- 'icon-discount.svg' | inline_asset_content -}} + {{ discount.title | escape }} + + -{{ discount.total_allocated_amount | money -}} + +
  • + {%- endfor -%} +
+
+ {%- endif -%} +
+ + {% if settings.show_cart_note or settings.show_add_discount_code %} +
+ {% if settings.show_cart_note %} + {% render 'cart-note' %} + {% endif %} + + {% if settings.show_add_discount_code %} + {% render 'cart-discount', section_id: section.id %} + {% endif %} +
+ {% endif %} + + {%- liquid + if settings.currency_code_enabled_cart_total + assign total_price = cart.total_price | money_with_currency + else + assign total_price = cart.total_price | money + endif + -%} + +
+ + {{ 'content.cart_estimated_total' | t }} + + {{ total_price }} + + + {% if settings.show_installments %} + + {% form 'cart', cart %} + {{ form | payment_terms }} + {% endform %} + + {% endif %} +
+ {% render 'tax-info', has_discounts_enabled: settings.show_add_discount_code %} +
+
+
+ +
+ + + {% if additional_checkout_buttons and settings.show_accelerated_checkout_buttons %} +
+ {{ content_for_additional_checkout_buttons }} +
+ {% endif %} +
+ +{% stylesheet %} + .cart-actions { + display: flex; + flex-direction: column; + gap: var(--gap-2xs); + border-block: 1px solid var(--color-border); + padding-block: var(--padding-sm); + margin-block-start: var(--margin-3xs); + } + + .cart__summary-totals:not(:has(.cart-actions)) { + margin-block-start: var(--margin-3xs); + border-block-start: 1px solid var(--color-border); + padding-block-start: var(--margin-xl); + } + + .cart__installments { + color: var(--color-foreground); + } +{% endstylesheet %} diff --git a/snippets/checkbox.liquid b/snippets/checkbox.liquid new file mode 100644 index 000000000..0cd00456a --- /dev/null +++ b/snippets/checkbox.liquid @@ -0,0 +1,50 @@ +{%- doc -%} + Renders a checkbox input and label + + @param {string} id - input id attribute + @param {string} name - input name attribute + @param {string} value - input value attribute + @param {string} label - label text + @param {boolean} checked - whether the input is checked + @param {string} events - event attributes for the input, e.g. 'on:click="/action"' + @param {boolean} disabled - whether the input is disabled + @param {string} [inputRef] - input ref attribute for use with component framework + @param {string} [labelRef] - label ref attribute for use with component framework + @param {boolean} [autofocus] - whether the input should be autofocused +{%- enddoc -%} +
+ + +
diff --git a/snippets/collection-card.liquid b/snippets/collection-card.liquid new file mode 100644 index 000000000..bb55d0937 --- /dev/null +++ b/snippets/collection-card.liquid @@ -0,0 +1,191 @@ +{%- doc -%} + This snippet is used to render a collection card. + To be used inside a block to inherit the block object settings. + + @param {string} card_image - The image to display in the collection card + @param {string} children - The content to render inside the collection card + @param {object} collection - The collection to render the card for + @param {object} block - The block object + @param {object} section - The section object + + @example + {% render 'collection-card', + card_image: card_image, + children: children, + block: block, + collection: collection, + section: section + %} +{%- enddoc -%} + +{% liquid + assign onboarding = false + + if collection == blank + assign onboarding = true + endif +%} + +
+ + {{ collection.title }} + +
+ {{ card_image }} +
+ {{ children }} +
+
+
+ +{% stylesheet %} + .collection-card { + --fixed-card-height: var(--height-small); + + width: 100%; + position: relative; + } + + .collection-card > svg { + height: 100%; + width: 100%; + aspect-ratio: var(--ratio); + } + + .collection-card__inner { + width: 100%; + overflow: hidden; + position: relative; + gap: var(--collection-card-gap); + display: flex; + flex-direction: column; + } + + .collection-card--image-bg .collection-card__inner { + height: 100%; + } + + .collection-card__inner { + z-index: var(--layer-flat); + pointer-events: none; + + a, + button { + /* only allow interactive elements to be clickable separate from .collection-card__link */ + pointer-events: auto; + } + } + + /* allow all blocks to be selectable in editor preview */ + .shopify-design-mode .collection-card__content * { + pointer-events: auto; + } + + .collection-card__content { + position: relative; + display: flex; + height: 100%; + width: 100%; + max-width: 100%; + gap: var(--collection-card-gap); + flex-direction: column; + align-items: var(--horizontal-alignment); + justify-content: var(--vertical-alignment); + } + + .collection-card__link { + position: absolute; + inset: 0; + + /* allows focus outline to have radius in supported browsers */ + border-radius: var(--border-radius); + } + + /* Nested image block rules */ + + .collection-card.collection-card--image-bg { + aspect-ratio: var(--ratio); + } + + .collection-card.collection-card--image-bg .collection-card__content { + padding: var(--padding-lg); + } + + /* Bento layout rules */ + .collection-card--image-height-fixed .collection-card__image { + height: var(--fixed-card-height); + width: 100%; + } + + .collection-card--image-height-fixed.collection-card--image-bg { + height: var(--fixed-card-height); + aspect-ratio: unset; + } + + .collection-card__image .image-block__image { + object-fit: cover; + width: 100%; + height: 100%; + max-width: 100%; + } + + .collection-card--image-bg .collection-card__image { + position: absolute; + width: 100%; + height: 100%; + } + + .collection-card__image svg { + height: 100%; + width: 100%; + } + + .resource-list:not(.hidden--desktop) .collection-card--flexible-aspect-ratio { + &.collection-card.collection-card--image-bg, + &.collection-card .placeholder-svg { + aspect-ratio: 99; + } + + .collection-card__image { + aspect-ratio: 99; + height: 100%; + } + + .collection-card__inner { + display: flex; + flex-direction: column; + height: 100%; + } + + .collection-card__content { + flex-shrink: 0; + } + + &:not(.collection-card--image-bg) .collection-card__content { + height: auto; + } + } +{% endstylesheet %} diff --git a/snippets/color-schemes.liquid b/snippets/color-schemes.liquid new file mode 100644 index 000000000..cfaacdac5 --- /dev/null +++ b/snippets/color-schemes.liquid @@ -0,0 +1,99 @@ +{% style %} + {% for scheme in settings.color_schemes -%} + {% assign scheme_classes = scheme_classes | append: ', .color-' | append: scheme.id %} + {% if forloop.index == 1 %} + :root, + {% endif %} + {% assign background_brightness = scheme.settings.background | color_brightness %} + {% if background_brightness < 64 %} + {% assign opacity_5_15 = 0.15 %} + {% assign opacity_10_25 = 0.25 %} + {% assign opacity_35_55 = 0.55 %} + {% assign opacity_40_60 = 0.60 %} + {% assign opacity_30_60 = 0.60 %} + {% else %} + {% assign opacity_5_15 = 0.05 %} + {% assign opacity_10_25 = 0.1 %} + {% assign opacity_35_55 = 0.35 %} + {% assign opacity_40_60 = 0.40 %} + {% assign opacity_30_60 = 0.30 %} + {% endif %} + .color-{{ scheme.id }} { + --color-background: rgb({{ scheme.settings.background.rgba }}); + /* RGB values only to apply different opacities - Relative color values are not supported in iOS < 16.4 */ + --color-background-rgb: {{ scheme.settings.background.rgb }}; + --opacity-5-15: {{ opacity_5_15 }}; + --opacity-10-25: {{ opacity_10_25 }}; + --opacity-35-55: {{ opacity_35_55 }}; + --opacity-40-60: {{ opacity_40_60 }}; + --opacity-30-60: {{ opacity_30_60 }}; + --color-foreground: rgb({{ scheme.settings.foreground.rgba }}); + --color-foreground-rgb: {{ scheme.settings.foreground.rgb }}; + --color-foreground-heading: rgb({{ scheme.settings.foreground_heading.rgba }}); + --color-foreground-heading-rgb: {{ scheme.settings.foreground_heading.rgb }}; + --color-primary: rgb({{ scheme.settings.primary.rgba }}); + --color-primary-rgb: {{ scheme.settings.primary.rgb }}; + --color-primary-hover: rgb({{ scheme.settings.primary_hover.rgba }}); + --color-primary-hover-rgb: {{ scheme.settings.primary_hover.rgb }}; + --color-border: rgb({{ scheme.settings.border.rgba }}); + --color-border-rgb: {{ scheme.settings.border.rgb }}; + --color-shadow: rgb({{ scheme.settings.shadow.rgba }}); + --color-shadow-rgb: {{ scheme.settings.shadow.rgb }}; + --color-primary-button-text: rgb({{ scheme.settings.primary_button_text.rgba }}); + --color-primary-button-background: rgb({{ scheme.settings.primary_button_background.rgba }}); + --color-primary-button-border: rgb({{ scheme.settings.primary_button_border.rgba }}); + --color-primary-button-hover-text: rgb({{ scheme.settings.primary_button_hover_text.rgba }}); + --color-primary-button-hover-background: rgb({{ scheme.settings.primary_button_hover_background.rgba }}); + --color-primary-button-hover-border: rgb({{ scheme.settings.primary_button_hover_border.rgba }}); + --color-secondary-button-text: rgb({{ scheme.settings.secondary_button_text.rgba }}); + --color-secondary-button-background: rgb({{ scheme.settings.secondary_button_background.rgba }}); + --color-secondary-button-border: rgb({{ scheme.settings.secondary_button_border.rgba }}); + --color-secondary-button-hover-text: rgb({{ scheme.settings.secondary_button_hover_text.rgba }}); + --color-secondary-button-hover-background: rgb({{ scheme.settings.secondary_button_hover_background.rgba }}); + --color-secondary-button-hover-border: rgb({{ scheme.settings.secondary_button_hover_border.rgba }}); + --color-input-background: rgb({{ scheme.settings.input_background.rgba }}); + --color-input-text: rgb({{ scheme.settings.input_text_color.rgba }}); + --color-input-text-rgb: {{ scheme.settings.input_text_color.rgb }}; + --color-input-border: rgb({{ scheme.settings.input_border_color.rgba }}); + --color-input-hover-background: rgb({{ scheme.settings.input_hover_background.rgba }}); + --color-variant-background: rgb({{ scheme.settings.variant_background_color.rgba }}); + --color-variant-border: rgb({{ scheme.settings.variant_border_color.rgba }}); + --color-variant-text: rgb({{ scheme.settings.variant_text_color.rgba }}); + --color-variant-text-rgb: {{ scheme.settings.variant_text_color.rgb }}; + --color-variant-hover-background: rgb({{ scheme.settings.variant_hover_background_color.rgba }}); + --color-variant-hover-text: rgb({{ scheme.settings.variant_hover_text_color.rgba }}); + --color-variant-hover-border: rgb({{ scheme.settings.variant_hover_border_color.rgba }}); + --color-selected-variant-background: rgb({{ scheme.settings.selected_variant_background_color.rgba }}); + --color-selected-variant-border: rgb({{ scheme.settings.selected_variant_border_color.rgba }}); + --color-selected-variant-text: rgb({{ scheme.settings.selected_variant_text_color.rgba }}); + --color-selected-variant-hover-background: rgb({{ scheme.settings.selected_variant_hover_background_color.rgba }}); + --color-selected-variant-hover-text: rgb({{ scheme.settings.selected_variant_hover_text_color.rgba }}); + --color-selected-variant-hover-border: rgb({{ scheme.settings.selected_variant_hover_border_color.rgba }}); + + --input-disabled-background-color: rgb(var(--color-foreground-rgb) / var(--opacity-10)); + --input-disabled-border-color: rgb(var(--color-foreground-rgb) / var(--opacity-5-15)); + --input-disabled-text-color: rgb(var(--color-foreground-rgb) / var(--opacity-50)); + --color-foreground-muted: rgb(var(--color-foreground-rgb) / var(--opacity-60)); + --font-h1--color: var(--color-foreground-heading); + --font-h2--color: var(--color-foreground-heading); + --font-h3--color: var(--color-foreground-heading); + --font-h4--color: var(--color-foreground-heading); + --font-h5--color: var(--color-foreground-heading); + --font-h6--color: var(--color-foreground-heading); + + /* Shadows */ + {% if settings.drawer_drop_shadow %} + --shadow-drawer: 0px 4px 20px rgb(var(--color-shadow-rgb) / var(--opacity-15)); + {% endif %} + {% if settings.popover_drop_shadow %} + --shadow-blur: 20px; + --shadow-popover: 0px 4px 20px rgb(var(--color-shadow-rgb) / var(--opacity-15)); + {% endif %} + } + {% endfor %} + + {{ scheme_classes | prepend: 'body' }} { + color: var(--color-foreground); + background-color: var(--color-background); + } +{% endstyle %} diff --git a/snippets/contact-form.liquid b/snippets/contact-form.liquid new file mode 100644 index 000000000..27fc2f646 --- /dev/null +++ b/snippets/contact-form.liquid @@ -0,0 +1,170 @@ +{%- doc -%} + Renders a contact form with name, email, phone, and comment fields. + + @param {object} settings - Block settings, required for the 'spacing-style' and 'size-style' snippets. + @param {string} submit_button - HTML for the submit button, rendered via a `content_for` block. + + @example + {% render 'contact-form', settings: block.settings, submit_button: content_for_submit_button %} +{%- enddoc -%} + +
+ {% assign form_id = block.id | default: section.id | prepend: 'ContactForm-' %} + + {%- form 'contact', id: form_id, class: 'contact-form__form' -%} + {%- if form.errors -%} +
+ {{- 'icon-error.svg' | inline_asset_content -}} + + {{- form.errors.translated_fields.email | capitalize }} + {{ form.errors.messages.email -}} +
+ {%- endif -%} + + {%- if form.posted_successfully? -%} +
+ {{- 'icon-checkmark.svg' | inline_asset_content -}} + {{- 'blocks.contact_form.post_success' | t -}} +
+ {%- endif -%} + +
+ + + + + +
+ + + + + + + + {{ submit_button }} + {%- endform -%} +
+ +{% stylesheet %} + .contact-form__form { + display: flex; + flex-direction: column; + gap: var(--gap-md); + } + + .contact-form__form-row { + display: flex; + flex-direction: column; + gap: var(--gap-md); + + @media screen and (min-width: 750px) { + flex-direction: row; + align-items: center; + } + } + + .contact-form__input { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + color: var(--color-foreground); + background-color: var(--color-input-background); + padding: var(--padding-lg) var(--padding-xl); + border-radius: var(--style-border-radius-inputs); + border: var(--style-border-width-inputs) solid var(--color-input-border); + -webkit-font-smoothing: antialiased; + } + + .contact-form__input--textarea { + resize: vertical; + min-height: var(--input-textarea-min-height); + } + + .contact-form__error, + .contact-form__success { + display: flex; + align-items: center; + gap: var(--gap-xs); + } +{% endstylesheet %} diff --git a/snippets/divider.liquid b/snippets/divider.liquid new file mode 100644 index 000000000..27edd0d65 --- /dev/null +++ b/snippets/divider.liquid @@ -0,0 +1,54 @@ +{%- doc -%} + Renders a divider line, used to visually separate content. + + @param {string} id - A unique ID for the divider, linking it to a block or section. + @param {object} settings - An object containing style settings for the divider. + + @param {string} [settings.alignment_horizontal] - The horizontal alignment of the divider ('left', 'center', or + 'right'). Defaults to 'center'. + @param {number} [settings.thickness] - The thickness of the divider line in pixels. + @param {string} [settings.corner_radius] - The corner radius of the divider, e.g., 'rounded'. + @param {number} [settings.width_percent] - The width of the divider as a percentage of its container. + + @param {boolean} [full_width] - When `true`, the divider spans the full width of its container. + @param {boolean} [attributes] - When `true`, render block.shopify_attributes on the divider container. +{%- enddoc -%} + +
+ +
+ +{% stylesheet %} + .divider { + align-self: stretch; + display: flex; + align-items: center; + justify-content: var(--divider-justify-content); + } + + .divider__line { + border-bottom: var(--divider-border-thickness) solid var(--color-border); + border-right: var(--divider-border-thickness) solid var(--color-border); + border-radius: calc(var(--style-border-radius-sm) * var(--divider-border-rounded)); + flex-basis: var(--divider-flex-basis); + min-height: var(--divider-flex-basis); + } +{% endstylesheet %} diff --git a/snippets/drawer-localization.liquid b/snippets/drawer-localization.liquid new file mode 100644 index 000000000..bf24e2f68 --- /dev/null +++ b/snippets/drawer-localization.liquid @@ -0,0 +1,125 @@ +{%- doc -%} + Renders a localization component for the drawer. + The component must be used inside a component. It relies on event bindings from the parent component. + + @param {boolean} [show_country] - Whether to show the country selector + @param {boolean} [show_language] - Whether to show the language selector + @param {boolean} [country_style] - Whether to show the country flag + + @example + {% render 'drawer-localization', country_style: true %} +{%- enddoc -%} + +{% liquid + assign background_brightness = block.settings.color_scheme.settings.background | color_brightness + if background_brightness < 64 + assign flag_shadow_size = 4 + else + assign flag_shadow_size = 2 + endif + + assign localization_font = '--menu-localization-font: var(--font-[localization_font]--family); ' | replace: '[localization_font]', section.settings.localization_font + assign color_shadow = '--color-shadow: rgb(var(--color-foreground-rgb) / var(--opacity-10-25));' + assign form_style = localization_font | append: color_shadow +%} + +{% if show_language and show_country == false %} + +{% else %} + +
+ + + +
+
+{% endif %} diff --git a/snippets/dropdown-localization.liquid b/snippets/dropdown-localization.liquid new file mode 100644 index 000000000..e24383e38 --- /dev/null +++ b/snippets/dropdown-localization.liquid @@ -0,0 +1,92 @@ +{%- doc -%} + Determines whether to wrap the localization-form in a dropdown-component and passes variables to it. + + @param {boolean} [show_country] - Whether to show the country selector. + @param {boolean} [show_language] - Whether to show the language selector. + @param {string} [country_style] - The style of the country selector. + @param {string} localization_position - { 'right' | 'left' } The position of the localization picker. +{%- enddoc -%} + +{% liquid + assign background_brightness = section.settings.color_scheme.settings.background | color_brightness + if background_brightness < 64 + assign shadow_size = 4 + else + assign shadow_size = 2 + endif + + assign localization_font = '--menu-localization-font: var(--font-[localization_font]--family); ' | replace: '[localization_font]', section.settings.localization_font + assign localization_font_size = '--menu-localization-font-size: [localization_font_size]; ' | replace: '[localization_font_size]', section.settings.localization_font_size + assign color_shadow = '--color-shadow: rgb(var(--color-foreground-rgb) / var(--opacity-10-25));' + assign form_style = localization_font | append: localization_font_size | append: color_shadow +%} + +{% if show_language and show_country == false %} + +{% elsif show_country %} + + + + + +{% endif %} diff --git a/snippets/editorial-collection-grid.liquid b/snippets/editorial-collection-grid.liquid new file mode 100644 index 000000000..65e8f215e --- /dev/null +++ b/snippets/editorial-collection-grid.liquid @@ -0,0 +1,117 @@ +{%- doc -%} + Renders a grid and places items inside of it using an editorial layout. + + @param {object} items - An array of HTML strings for the collection list items + + @example + {% render 'editorial-collection-grid', items: items %} +{%- enddoc -%} + +
+
+ + {% for item in items %} + {% liquid + assign current_grid_index = forloop.index0 | divided_by: 8 + assign current_item_index = forloop.index0 | modulo: 8 + + case current_item_index + when 0 + assign grid_column = '2 / span 4' + assign grid_row = 1 + assign grid_row_span = 5 + when 1 + assign grid_column = '7 / span 5' + assign grid_row = 3 + assign grid_row_span = 5 + when 2 + assign grid_column = '1 / span 8' + assign grid_row = 9 + assign grid_row_span = 6 + when 3 + assign grid_column = '3 / span 8' + assign grid_row = 16 + assign grid_row_span = 6 + when 4 + assign grid_column = '7 / span 5' + assign grid_row = 23 + assign grid_row_span = 5 + when 5 + assign grid_column = '2 / span 4' + assign grid_row = 25 + assign grid_row_span = 5 + when 6 + assign grid_column = '5 / span 8' + assign grid_row = 31 + assign grid_row_span = 6 + when 7 + assign grid_column = '2 / span 8' + assign grid_row = 38 + assign grid_row_span = 6 + endcase + + assign full_grid_rows = current_grid_index | times: 44 + assign grid_row = grid_row | plus: full_grid_rows + %} +
+ {{ item }} +
+ {% endfor %} +
+ +{% stylesheet %} + .editorial-collection__grid { + display: grid; + grid-template-columns: repeat(12, 1fr); + grid-auto-rows: 1fr; + gap: var(--gap-xl); + + .resource-list__item, + .collection-card { + height: 100%; + } + } + + .editorial-collection__spacer { + aspect-ratio: 1; + } + + @media (max-width: 768px) { + .editorial-collection__grid { + display: flex; + flex-direction: column; + gap: var(--gap-2xl); + } + + .editorial-collection__spacer { + display: none; + } + + .editorial-collection__item-0 { + width: 66%; + align-self: flex-start; + aspect-ratio: 4 / 5; + } + + .editorial-collection__item-1 { + width: 83%; + align-self: flex-end; + aspect-ratio: 5 / 5; + } + + .editorial-collection__item-2 { + width: 83%; + align-self: flex-start; + aspect-ratio: 8 / 6; + } + + .editorial-collection__item-3 { + width: 100%; + align-self: center; + aspect-ratio: 8 / 6; + } + } +{% endstylesheet %} diff --git a/snippets/editorial-product-grid.liquid b/snippets/editorial-product-grid.liquid new file mode 100644 index 000000000..470917dd8 --- /dev/null +++ b/snippets/editorial-product-grid.liquid @@ -0,0 +1,124 @@ +{%- doc -%} + Renders a grid and places items inside of it using an editorial layout. + + @param {object} items - An array of HTML strings for the product list items + + @example + {% render 'editorial-product-grid', items: items %} +{%- enddoc -%} + +
+
+ + {% for item in items %} + {% liquid + assign current_grid_index = forloop.index0 | divided_by: 8 + assign current_item_index = forloop.index0 | modulo: 8 + + case current_item_index + when 0 + assign grid_column = '1 / span 7' + assign grid_row = 1 + assign grid_row_span = 6 + when 1 + assign grid_column = '9 / span 4' + assign grid_row = 5 + assign grid_row_span = 5 + when 2 + assign grid_column = '2 / span 5' + assign grid_row = 8 + assign grid_row_span = 5 + when 3 + assign grid_column = '5 / span 8' + assign grid_row = 14 + assign grid_row_span = 6 + when 4 + assign grid_column = '1 / span 7' + assign grid_row = 21 + assign grid_row_span = 6 + when 5 + assign grid_column = '9 / span 4' + assign grid_row = 25 + assign grid_row_span = 5 + when 6 + assign grid_column = '2 / span 5' + assign grid_row = 28 + assign grid_row_span = 5 + when 7 + assign grid_column = '3 / span 8' + assign grid_row = 34 + assign grid_row_span = 6 + endcase + + assign full_grid_rows = current_grid_index | times: 40 + assign grid_row = grid_row | plus: full_grid_rows + %} +
+ {{ item }} +
+ {% endfor %} +
+ +{% stylesheet %} + .editorial-product__grid { + display: grid; + grid-template-columns: repeat(12, 1fr); + grid-auto-rows: 1fr; + gap: var(--gap-xl); + + /* Make the aspect ratio super high on width, then increase the height of + * slideshow containers until they fill all the available space */ + .card-gallery { + --gallery-aspect-ratio: 99 !important; + } + + .card-gallery, + slideshow-component, + slideshow-container, + slideshow-slides { + height: 100%; + } + } + + .editorial-product__spacer { + aspect-ratio: 1; + } + + @media (max-width: 768px) { + .editorial-product__grid { + display: flex; + flex-direction: column; + gap: var(--gap-2xl); + } + + .editorial-product__spacer { + display: none; + } + + .editorial-product__item-0 { + width: 83%; + align-self: flex-start; + aspect-ratio: 7 / 6; + } + + .editorial-product__item-1 { + width: 83%; + align-self: flex-end; + aspect-ratio: 4 / 5; + } + + .editorial-product__item-2 { + width: 66%; + align-self: flex-start; + aspect-ratio: 5 / 5; + } + + .editorial-product__item-3 { + width: 100%; + aspect-ratio: 8 / 6; + } + } +{% endstylesheet %} diff --git a/snippets/facets-actions.liquid b/snippets/facets-actions.liquid new file mode 100644 index 000000000..702064ad4 --- /dev/null +++ b/snippets/facets-actions.liquid @@ -0,0 +1,220 @@ +{%- doc -%} + Renders the facets actions + + @param {string} results_url - the url to remove the filters + @param {boolean} is_active - whether the clear all button is active + @param {number} products_count - the number of products in the results + @param {string} [form_component] - the form component to use for the clear all button + @param {boolean} [should_show_clear_all] - whether to show the clear all button + @param {number} [shadow_opacity] - the opacity of the shadow for the sticky action bar +{%- enddoc -%} + +
+ {% if should_show_clear_all %} + + + + {% endif %} + + {% if products_count > 0 %} + + {% endif %} +
+ +{% stylesheet %} + /* Facets - Actions */ + .facets__actions { + --to-top-gradient-background: linear-gradient( + to top, + rgb(var(--color-background-rgb) / var(--opacity-90)), + rgb(var(--color-background-rgb) / var(--opacity-80)), + rgb(var(--color-background-rgb) / var(--opacity-40)), + transparent + ); + + order: 1; + position: sticky; + bottom: 0; + display: flex; + justify-content: space-between; + align-items: stretch; + gap: var(--gap-sm); + background-image: var(--to-top-gradient-background); + z-index: var(--facets-sticky-z-index); + padding-block-start: var(--padding-xs); + padding-block-end: var(--padding-md); + padding-inline: var(--padding-lg); + margin-top: auto; + } + + .facets:not(.facets--drawer) .facets__actions { + @media screen and (min-width: 750px) { + position: static; + } + } + + .facets--vertical .facets__actions { + padding-inline: 0; + justify-content: center; + } + + .facets--horizontal .facets__actions { + @media screen and (min-width: 750px) { + order: 0; + bottom: auto; + position: static; + padding: 0; + z-index: var(--layer-flat); + flex-shrink: 0; + align-items: center; + margin-top: initial; + background-image: none; + } + } + + .facets--horizontal .facets__actions--active::before { + @media screen and (min-width: 750px) { + content: ''; + border-inline-start: var(--style-border-width) solid var(--color-border); + height: var(--font-paragraph--size); + position: absolute; + } + } + + /* Clear button */ + .facets__clear { + display: none; + } + + .facets--horizontal .facets__clear { + @media screen and (min-width: 750px) { + width: 100%; + justify-content: flex-end; + padding: 0 var(--facets-clear-padding) var(--facets-clear-padding) 0; + cursor: pointer; + } + } + + .facets__clear--active { + @media screen and (min-width: 750px) { + display: flex; + } + } + + .clear-filter:hover { + text-decoration: underline; + background-color: transparent; + } + + /* Clear all button */ + .facets__clear-all { + display: none; + cursor: pointer; + min-width: var(--facets-clear-all-min-width); + transition: transform var(--animation-values), opacity var(--animation-values); + opacity: 0; + transform: translateY(100%); + flex-grow: 1; + padding-block: var(--padding-lg); + } + + .facets:not(.facets--drawer) .facets__clear-all { + box-shadow: none; + } + + .facets--horizontal .facets__clear-all { + @media screen and (min-width: 750px) { + --facets-clear-all-min-width: var(--minimum-touch-target); + --button-color: var(--color-primary); + + text-decoration: underline transparent 0.075em; + text-underline-offset: 0.125em; + width: auto; + transform: none; + opacity: 1; + height: var(--minimum-touch-target); + align-items: center; + flex-grow: 0; + transition: text-decoration-color var(--animation-speed) var(--animation-easing); + } + } + + .facets--horizontal .facets__clear-all:hover { + @media screen and (min-width: 750px) { + --button-color: var(--color-primary-hover); + } + } + + @starting-style { + .facets__clear-all { + opacity: 1; + transform: translateY(0); + } + } + + .facets__clear-all.active { + transform: translateY(0); + opacity: 1; + display: grid; + } + + .facets--horizontal .facets__clear-all.active { + @media screen and (min-width: 750px) { + padding-block: 0; + padding-inline: var(--facets-form-horizontal-gap); + background-color: transparent; + position: static; + transform: none; + } + } + + @starting-style { + .facets__clear-all.active { + opacity: 0; + transform: translateY(100%); + } + + .facets--horizontal .facets__clear-all.active { + @media screen and (min-width: 750px) { + opacity: 1; + transform: none; + } + } + } + + .facets__see-results { + min-width: var(--facets-see-results-min-width); + flex-grow: 1; + padding-block: var(--padding-lg); + } + + .facets:not(.facets--drawer) .facets__see-results { + @media screen and (min-width: 750px) { + display: none; + } + } +{% endstylesheet %} diff --git a/snippets/filter-remove-buttons.liquid b/snippets/filter-remove-buttons.liquid new file mode 100644 index 000000000..196270670 --- /dev/null +++ b/snippets/filter-remove-buttons.liquid @@ -0,0 +1,178 @@ +{%- doc -%} + Renders filter remove buttons. + + Accepts: + + @param {object} filters - The filters to render + @param {boolean} show_filter_label - Whether to show the filter label + @param {string} results_url - The results URL + @param {boolean} should_show_clear_all - Whether to show the clear all button +{%- enddoc -%} + +
+ {%- for filter in filters -%} + {%- liquid + assign is_first_filter = forloop.first + -%} + {% if filter.type == 'price_range' and filter.min_value.value != null or filter.max_value.value != null %} + {%- liquid + assign is_active = true + -%} + + {%- if filter.min_value.value != null and filter.max_value.value != null %} + {{- filter.min_value.value | money -}} + – + {{- filter.max_value.value | money -}} + {%- elsif filter.min_value.value != null -%} + {{ filter.min_value.value | money }}–{{ filter.range_max | money }} + {%- elsif filter.max_value.value != null -%} + {{- 0 | money -}} + – + {{- filter.max_value.value | money -}} + {%- endif -%} + + {{- 'icon-filters-close.svg' | inline_asset_content -}} + + {{ 'actions.remove' | t }} + + {% else %} + {%- for value in filter.active_values -%} + {%- liquid + assign is_active = true + -%} + + {% if value.swatch %} + {% render 'swatch', swatch: value.swatch, mode: 'pill' %} + {% endif %} + + {% if filter.type == 'boolean' or show_filter_label %} + {{ filter.label | escape }}: {{ value.label | escape }} + {% else %} + {{ value.label | escape }} + {% endif %} + + + {{- 'icon-filters-close.svg' | inline_asset_content -}} + + {{ 'actions.remove' | t }} + + {%- endfor -%} + {% endif %} + {%- endfor -%} + {% if should_show_clear_all and is_active %} + + + + {% endif %} +
+ +{% stylesheet %} + /* Facets - Remove buttons */ + .facets-remove { + --variant-picker-swatch-width: 20px; + --variant-picker-swatch-height: 20px; + + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--gap-xs); + padding: 0 var(--drawer-padding); + margin-block-start: var(--margin-2xs); + margin-block-end: var(--margin-md); + + @media screen and (min-width: 750px) { + --variant-picker-swatch-width: 16px; + --variant-picker-swatch-height: 16px; + + gap: var(--gap-2xs); + } + } + + .facets__clear-all-link { + --button-color: var(--color-primary); + + border: none; + background-color: transparent; + padding: var(--padding-xs); + min-width: fit-content; + color: var(--button-color); + transition: text-decoration-color var(--animation-speed) var(--animation-easing), + color var(--animation-speed) var(--animation-easing); + } + + .facets__clear-all-link:hover { + --button-color: var(--color-primary-hover); + + color: var(--button-color); + text-decoration-color: var(--button-color); + } + + .facets:not(.facets--drawer) .facets-remove--mobile-and-vertical { + @media screen and (min-width: 750px) { + padding: 0; + } + } + + .facets--horizontal .facets-remove--mobile-and-vertical { + @media screen and (min-width: 750px) { + display: none; + } + } + + .facets-remove:not(:has(facet-remove-component)) { + display: none; + margin: 0; + } + + .facets-remove__pill { + .svg-wrapper, + .swatch { + flex-shrink: 0; + } + } + + .facets--horizontal .facets-remove { + @media screen and (min-width: 750px) { + display: none; + } + } +{% endstylesheet %} diff --git a/snippets/filters-toggle.liquid b/snippets/filters-toggle.liquid new file mode 100644 index 000000000..8bbdfa3cd --- /dev/null +++ b/snippets/filters-toggle.liquid @@ -0,0 +1,149 @@ +{%- doc -%} + Renders the sorting component. + + @param {boolean} enable_filtering - Whether to enable filtering + @param {number} padding-block-start - The padding-block-start value + @param {number} padding-block-end - The padding-block-end value + @param {number} total_active_values - The total number of active values + @param {string} section_id - The section ID + @param {object} results - The results of the search + @param {string} sort_by - The current sort by +{%- enddoc -%} + +
+ {% if enable_filtering %} +
+ +
+ {% endif %} + +
+ {% if block.settings.enable_filtering == false and block.settings.enable_sorting %} + {% render 'sorting', + results: results, + sort_by: sort_by, + filter_style: block.settings.filter_style, + suffix: 'mobile', + section_id: section_id, + should_use_select_on_mobile: false + %} + {% endif %} + {% if block.settings.enable_grid_density %} + {% render 'grid-density-controls', viewport: 'mobile' %} + {% endif %} +
+
+ +{% stylesheet %} + /* Facets - Toggle */ + .facets-toggle { + --icon-offset: -3px; + + display: flex; + justify-content: space-between; + align-items: center; + height: var(--minimum-touch-target); + margin: var(--facets-margin); + padding-block: var(--facets-inner-padding-block); + padding-inline: var(--facets-inner-padding-inline); + + @media screen and (min-width: 750px) { + display: none; + } + } + + .facets-toggle__wrapper { + margin-left: var(--icon-offset); + } + + .facets-toggle__button { + box-shadow: none; + + @media screen and (min-width: 750px) { + display: none; + } + } + + /* Filter count */ + .filter-count-bubble { + position: relative; + width: 20px; + aspect-ratio: 1; + border-radius: 50%; + display: grid; + line-height: normal; + place-content: center; + color: var(--color-foreground); + border: var(--icon-stroke-width) solid var(--color-background); + } + + .facets-mobile__title-wrapper .h3 { + margin-block-end: 0; + display: inline-flex; + align-items: center; + gap: var(--gap-xs); + } + + .facets-mobile__title-wrapper .filter-count-bubble { + width: 22px; + height: 22px; + } + + .facets-mobile__title-wrapper .filter-count-bubble__text { + font-size: var(--font-size--xs); + } + + .filter-count-bubble__background { + position: absolute; + inset: 0; + background-color: rgb(var(--color-foreground-rgb) / var(--opacity-10-25)); + border-radius: var(--style-border-radius-50); + } + + .filter-count-bubble__text { + font-size: 11px; + font-weight: var(--font-paragraph--weight); + aspect-ratio: 1 / 1; + } + + .facets-toggle--no-filters { + @media screen and (max-width: 749px) { + justify-content: unset !important; + + & > .facets-mobile-wrapper { + width: 100%; + } + } + } +{% endstylesheet %} diff --git a/snippets/fonts.liquid b/snippets/fonts.liquid new file mode 100644 index 000000000..e48f6a991 --- /dev/null +++ b/snippets/fonts.liquid @@ -0,0 +1,49 @@ +{%- unless settings.type_body_font.system? + and settings.type_subheading_font.system? + and settings.type_heading_font.system? +-%} + +{%- endunless -%} + +{% # theme-check-disable %} +{%- unless settings.type_body_font.system? -%} + +{%- endunless -%} +{%- unless settings.type_subheading_font.system? -%} + +{%- endunless -%} +{%- unless settings.type_heading_font.system? -%} + +{%- endunless -%} +{%- unless settings.type_accent_font.system? -%} + +{%- endunless -%} +{% # theme-check-enable %} diff --git a/snippets/gap-style.liquid b/snippets/gap-style.liquid new file mode 100644 index 000000000..99b91ea4f --- /dev/null +++ b/snippets/gap-style.liquid @@ -0,0 +1,25 @@ +{%- doc -%} + Renders the CSS variables for the `gap` styles needed for responsive scaling. + Intended for use with the `gap-style` class. + + @param {number} value - The base or desktop gap value to use, in pixels. + @param {string} [name] - The name of the CSS variable to set. Default: 'gap' + @param {number} [scale_min] - Value above which gap scaling will be applied. Default: 20 + @param {boolean} [disable_scaling] - If true, disables scaling and outputs the original value. + + @example +
+{%- enddoc -%} + +{%- liquid + assign min = scale_min | default: 24 + assign name = name | default: 'gap' +-%} + +{%- if value != blank -%} + {%- if disable_scaling != true and value > min -%} + --{{ name }}: max({{ min }}px, calc(var(--gap-scale, 1.0) * {{ value }}px)); + {%- else -%} + --{{ name }}: {{ value }}px; + {%- endif -%} +{%- endif -%} diff --git a/snippets/grid-density-controls.liquid b/snippets/grid-density-controls.liquid new file mode 100644 index 000000000..2dbc5f169 --- /dev/null +++ b/snippets/grid-density-controls.liquid @@ -0,0 +1,179 @@ +{%- doc -%} + Renders the grid density controls. + + @param {string} viewport - The viewport to render the controls for, either 'mobile' or 'desktop'. + + @example + {% render 'grid-density-controls', viewport: 'desktop' %} +{%- enddoc -%} + +
+
+ + {{ 'content.grid_view.grid_fieldset' | t }} + + + {% if viewport == 'mobile' %} + + + + + {% elsif viewport == 'desktop' %} + + + + {% endif %} +
+
+ +{% stylesheet %} + .column-options-wrapper { + --icon-offset: -3px; + + display: flex; + gap: var(--gap-sm); + min-width: fit-content; + justify-content: flex-end; + height: var(--minimum-touch-target); + align-items: center; + margin-right: var(--icon-offset); + } + + .column-options-wrapper:only-child { + margin-left: auto; + } + + .facets__form-wrapper > .column-options-wrapper:first-child { + margin-left: auto; + } + + .facets .column-options-wrapper { + display: none; + + @media screen and (min-width: 750px) { + display: flex; + } + } + + .column-options { + display: flex; + flex-wrap: wrap; + gap: var(--gap-xs); + margin: 0; + padding: 0; + border: none; + + @media screen and (min-width: 750px) { + gap: var(--gap-2xs); + } + } + + .column-options__option { + display: none; + position: relative; + } + + .column-options__option:has(.column-picker-mobile--single), + .column-options__option:has(.column-picker-mobile--double) { + @media screen and (max-width: 749px) { + display: flex; + } + } + + .column-options__option:has(.column-picker--default), + .column-options__option:has(.column-picker--zoom-out) { + @media screen and (min-width: 750px) { + display: flex; + } + } + + .column-options__legend { + padding: 0; + margin: 0; + } + + .column-options__option-input { + /* this is a repeating pattern a bit with the variant picker buttons */ + + /* remove the checkbox from the page flow */ + position: absolute; + + /* set the dimensions to match those of the label */ + inset: 0; + + /* hide it */ + opacity: 0; + margin: 0; + cursor: pointer; + } + + .column-picker { + color: rgb(var(--color-foreground-rgb) / var(--opacity-50)); + padding: var(--padding-2xs); + border-radius: var(--style-border-radius-xs); + transition: background-color var(--animation-speed) ease, color var(--animation-speed) ease; + } + + .column-options__option:hover .column-picker { + background-color: rgb(var(--color-foreground-rgb) / var(--opacity-5)); + } + + .column-options__option-input:checked ~ .column-picker { + color: rgb(var(--color-foreground-rgb)); + background-color: rgb(var(--color-foreground-rgb) / var(--opacity-5)); + } +{% endstylesheet %} diff --git a/snippets/group.liquid b/snippets/group.liquid new file mode 100644 index 000000000..716a43aa6 --- /dev/null +++ b/snippets/group.liquid @@ -0,0 +1,90 @@ +{%- doc -%} + Renders block content for all blocks that extend the group block. + + @param {string} children - The DOM content of the group block. + @param {object} settings - The settings of the group block. + @param {string} shopify_attributes - String with Shopify attributes for the editor. + @param {string} [class] - Custom classes for the group block. + @param {string} [style] - Custom inline styles for the group block. + + @example + {% render 'group', children: children, settings: block.settings, shopify_attributes: block.shopify_attributes %} +{%- enddoc -%} + +
+ {%- if settings.link != blank -%} + + {%- endif -%} + +
+ {% render 'background-media', + background_media: settings.background_media, + background_video: settings.video, + background_video_position: settings.video_position, + background_image: settings.background_image, + background_image_position: settings.background_image_position, + placeholder: settings.placeholder + %} + {% if settings.toggle_overlay %} + {% render 'overlay', settings: settings, layer: '0' %} + {% endif %} +
+ +
+ {{- children -}} +
+
+ +{% stylesheet %} + .group-block__link { + position: absolute; + inset: 0; + } + + .group-block__link ~ :is(.group-block-content, .group-block__media-wrapper) { + pointer-events: none; + + :is(a, button, input, textarea, select) { + pointer-events: auto; + } + } + + /* Needs the .group-block__link ~ to be specific enough to take effect. */ + .group-block__link ~ .group-block-content--design-mode { + pointer-events: auto; + } +{% endstylesheet %} diff --git a/snippets/header-actions.liquid b/snippets/header-actions.liquid new file mode 100644 index 000000000..248106014 --- /dev/null +++ b/snippets/header-actions.liquid @@ -0,0 +1,280 @@ + + + + {% if shop.customer_accounts_enabled %} + {% render 'account-popover' %} + {% render 'account-drawer' %} + {% endif %} + + {% if settings.cart_type == 'drawer' and template.name != 'cart' %} + {% render 'cart-drawer' %} + {% else %} + + {% render 'cart-icon-component' %} + + {% endif %} + + +{% stylesheet %} + .cart-drawer { + --cart-drawer-padding: var(--padding-lg) var(--padding-xl); + --cart-drawer-padding-desktop: var(--padding-xl) var(--padding-2xl); + --cart-font-size--2xs: var(--font-size--2xs); + --cart-font-size--xs: var(--font-size--xs); + --cart-font-size--sm: var(--font-size--sm); + --cart-font-size--md: var(--font-size--md); + --cart-font-size--2xl: var(--font-size--2xl); + } + + .cart-drawer__dialog { + position: fixed; + border-radius: 0; + width: var(--sidebar-width); + max-width: 95vw; + height: 100%; + margin: 0 0 0 auto; + padding: 0; + border-left: var(--style-border-drawer); + box-shadow: var(--shadow-drawer); + background-color: var(--color-background); + } + + /* Needed to ensure the drawer is full height */ + .cart-drawer__dialog:modal { + max-height: 100dvh; + overflow-y: hidden; + } + + .cart-drawer__inner { + height: 100%; + } + + .cart-drawer__content { + padding: 0; + background-color: var(--color-background); + display: flex; + flex-direction: column; + flex-grow: 1; + overflow-y: auto; + } + + .cart-drawer__heading { + display: flex; + align-items: center; + gap: var(--gap-xs); + } + + .cart-drawer__close-button { + width: var(--minimum-touch-target); + height: var(--minimum-touch-target); + display: flex; + align-items: center; + justify-content: center; + margin-right: calc(var(--padding-sm) * -1); + } + + .cart-drawer__close-button svg { + width: var(--icon-size-xs); + height: var(--icon-size-xs); + } + + .cart-drawer--empty .cart-drawer__content { + text-align: center; + min-height: auto; + } + + .cart-drawer--empty .cart-drawer__heading { + margin-bottom: var(--margin-md); + } + + .cart-drawer__items .cart-items__table-row { + padding-bottom: var(--gap-xl); + border-bottom: var(--style-border-width) solid var(--color-border); + margin-bottom: var(--gap-xl); + } + + .cart-drawer__items .cart-items__table-row:has(+ .cart-items__nested-line) { + border-bottom: none; + margin-bottom: 0; + } + + .cart-drawer__items .cart-items__table-row:last-child { + border-bottom: none; + } + + .cart-drawer__summary { + --cart-drawer-summary-padding: var(--padding-lg); + + display: flex; + flex-direction: column; + align-items: center; + gap: var(--gap-2xl); + padding: var(--cart-drawer-summary-padding); + margin-top: auto; + background-color: var(--color-background); + /* stylelint-disable-next-line color-named */ + mask-image: linear-gradient(to bottom, transparent, black var(--cart-drawer-summary-padding)); + + @media screen and (min-width: 750px) { + --cart-drawer-summary-padding: var(--padding-2xl); + } + } + + .cart-drawer__summary .cart__summary-totals:not(:has(.cart__original-total-container:empty)) { + border-block-start: var(--style-border-width) solid var(--color-border); + padding-block-start: var(--padding-2xl); + } + + .cart-drawer__summary .cart-note { + @media screen and (min-width: 750px) { + margin-block-start: var(--margin-3xs); + } + } + + .cart-drawer__heading--empty { + display: flex; + justify-content: center; + } + + .cart-drawer__items { + display: flex; + flex-direction: column; + padding-inline: var(--cart-drawer-padding); + overflow-y: auto; + + @media screen and (min-width: 750px) { + padding-inline: var(--cart-drawer-padding-desktop); + } + } + + .cart-drawer__items .cart-items__table-row { + padding-bottom: var(--gap-xl); + border-bottom: var(--style-border-width) solid var(--color-border); + margin-bottom: var(--gap-xl); + } + + .cart-drawer__items .cart-items__table-row:last-child { + border-bottom: none; + padding-block-end: 0; + margin-block-end: 0; + } + + .cart-drawer--empty .cart-drawer__inner { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100dvh; + margin-top: 0; + } + + .cart-drawer:not(:has(.cart-form)) .cart-drawer__content { + justify-content: center; + } + + .cart-drawer--empty .cart-drawer__header { + justify-content: right; + border-bottom: none; + padding-bottom: 0; + } + + .cart-drawer--empty .cart-drawer__heading { + text-align: center; + } + + .cart-drawer:not(:has(.cart-form)) .cart-items__wrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + } + + header-actions { + display: flex; + + @media screen and (max-width: 749px) { + justify-self: flex-end; + } + } + + .header__column--right header-actions { + margin-inline-start: calc(var(--gap-md) * -1); + } + + .header-actions__action { + --button-color: var(--color-foreground); + + cursor: pointer; + display: flex; + justify-content: center; + } + + .header-actions__action .svg-wrapper { + height: var(--button-size); + width: var(--button-size); + } + + .header-actions__action svg { + width: var(--icon-size-md); + height: var(--icon-size-md); + } + + .header-actions__cart-icon { + --cart-bubble-size: 20px; + --cart-bubble-top: 4.5px; + --cart-bubble-right: 2.5px; + + position: relative; + } + + .header-actions__cart-icon .cart-bubble { + position: absolute; + width: var(--cart-bubble-size, 20px); + top: var(--cart-bubble-top); + right: var(--cart-bubble-right); + } + + .header-actions__cart-icon .cart-bubble__text, + .cart-drawer__heading .cart-bubble__text { + font-family: var(--font-paragraph--family); + font-weight: var(--font-paragraph--weight); + } + + .header-actions__cart-icon.header-actions__cart-icon--has-cart svg { + /* Create donut mask where the cart bubble sits */ + mask: radial-gradient( + calc(var(--cart-bubble-size) + 2px) at calc(100% - var(--cart-bubble-right)) var(--cart-bubble-top), + transparent 45.45%, + white 45.45%, + white 100% + ); + } + + .cart-drawer__heading .cart-bubble__background { + background-color: rgb(var(--color-foreground-rgb) / var(--opacity-10-25)); + } + + .cart-drawer__heading .cart-bubble__text { + color: var(--color-foreground); + font-size: var(--font-size--xs); + } + + .cart-bubble--animating .cart-bubble__background { + animation: grow var(--animation-speed) var(--animation-easing); + } + + .cart-bubble--animating .cart-bubble__text { + animation: cartBubbleSlideIn var(--animation-speed) var(--animation-easing); + } +{% endstylesheet %} diff --git a/snippets/header-drawer.liquid b/snippets/header-drawer.liquid new file mode 100644 index 000000000..ecc2668f3 --- /dev/null +++ b/snippets/header-drawer.liquid @@ -0,0 +1,1128 @@ +{%- doc -%} + Renders a header drawer menu triggered by the top details element. + + @param {object} linklist - The linklist to render + @param {string} [class] - Additional classes to add to the drawer + @param {string} [data_header_drawer_type] - The type of header drawer to render + @param {object} [block] - The block that can be used to provide settings + @param {object} [section] - The section that can be used to provide settings + + @example + {% render 'header-drawer', linklist: section.settings.menu, class: 'header-drawer--mobile' %} +{%- enddoc -%} + +{% liquid + assign max_featured_items = 4 + assign image_border_radius = block.settings.image_corner_radius + + if block.settings.menu_style == 'featured_collections' + assign ratio = block.settings.featured_collections_aspect_ratio + elsif block.settings.menu_style == 'featured_products' + assign ratio = block.settings.featured_products_aspect_ratio + endif +%} + + + + + + + +{% stylesheet %} + .header__icon--menu { + position: initial; + } + + @media screen and (min-width: 750px) { + .header--desktop header-menu + .header__drawer header-drawer { + display: none; + } + } + + .menu-drawer-container .header__icon--summary { + color: var(--color-foreground); + display: flex; + justify-content: center; + align-items: center; + padding: var(--padding-lg); + } + + .header__icon--summary .header-drawer-icon { + margin: auto; + width: var(--icon-size-xs); + height: var(--icon-size-xs); + } + + .header__drawer { + display: flex; + min-height: 60px; + align-items: center; + + @media screen and (min-width: 750px) { + min-height: 0; + } + } + + .header--compact .header__drawer { + min-height: var(--minimum-touch-target); + } + + .menu-drawer__navigation { + padding: 0; + + @media screen and (min-width: 750px) { + margin-top: var(--drawer-header-desktop-top); + } + } + + details:not([open]) .header__icon--menu .header-drawer-icon--close { + display: none; + } + + details[open] .header__icon--menu .header-drawer-icon--close { + @media screen and (min-width: 750px) { + display: none; + } + } + + details[open] .header__icon--menu .header-drawer-icon--open { + display: none; + + @media screen and (min-width: 750px) { + display: flex; + } + } + + .menu-drawer { + position: fixed; + transform: translateX(-100%); + visibility: hidden; + height: var(--drawer-height); + width: var(--drawer-width); + max-width: var(--drawer-max-width); + z-index: var(--layer-menu-drawer); + left: 0; + top: 0; + padding: 0; + background-color: var(--color-background); + overflow: auto; + display: flex; + border-right: var(--style-border-drawer); + box-shadow: var(--shadow-drawer); + flex-direction: column; + + @media screen and (min-width: 750px) { + width: 25rem; + } + + .header__drawer--desktop & { + height: 100vh; + } + } + + .menu-drawer:has(details[open]) { + overflow: initial; + } + + .menu-drawer__backdrop { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100dvh; + backdrop-filter: brightness(0.75); + z-index: var(--layer-heightened); + opacity: 0; + transition: opacity var(--drawer-animation-speed) ease; + + .menu-open & { + opacity: 1; + } + } + + .menu-drawer, + details[open] > .menu-drawer__submenu { + transition: transform var(--drawer-animation-speed) ease, visibility var(--drawer-animation-speed) ease, + opacity var(--drawer-animation-speed) ease; + } + + .menu-open > .menu-drawer, + .menu-open > .menu-drawer__submenu:not(.menu-drawer__menu--childlist) { + transform: translateX(0); + visibility: visible; + opacity: 1; + display: flex; + flex-direction: column; + will-change: transform; + } + + .menu-drawer__inner-container { + position: relative; + height: 100%; + } + + .menu-drawer__navigation-container { + display: grid; + grid-template-rows: 1fr auto; + align-content: space-between; + overflow-y: auto; + height: 100%; + } + + .menu-drawer__inner-submenu { + display: flex; + flex-direction: column; + height: 100%; + overflow-y: auto; + + @media screen and (min-width: 750px) { + margin-top: var(--drawer-header-desktop-top); + } + } + + .menu-drawer__nav-buttons { + display: flex; + justify-content: space-between; + align-items: center; + } + + .menu-drawer__menu { + --menu-drawer-inline-padding: calc(var(--padding-sm) + 7px); + + list-style: none; + padding-inline: var(--drawer-padding); + margin-inline: 0; + margin-block-start: 0; + } + + .menu-drawer__menu--grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--padding-sm); + padding-inline-end: var(--menu-drawer-inline-padding); + padding-block-start: var(--padding-xs); + } + + .menu-drawer__menu--childlist:not(.menu-drawer__menu--grid) { + flex-grow: 1; + } + + .menu-drawer__menu.has-submenu, + .menu-drawer__menu--childlist:not(:has(.menu-drawer__animated-element)) { + margin-block-end: var(--margin-xs); + + @media screen and (min-width: 750px) { + margin-block-end: 2.5rem; + } + } + + .menu-drawer__list-item--divider { + border-block-end: 1px solid var(--color-border); + } + + .menu-drawer__list-item--deep:not(.menu-drawer__list-item--divider) .menu-drawer__menu { + margin-block-start: -0.3rem; + } + + .menu-drawer__list-item--flat.menu-drawer__list-item--divider .menu-drawer__menu { + margin-block-start: -0.4rem; + } + + .menu-drawer__menu-container--divider { + border-block-end: 1px solid var(--color-border); + } + + .menu-drawer__menu > .menu-drawer__list-item { + display: flex; + min-height: calc(2 * var(--padding-lg) + var(--icon-size-xs)); + } + + .menu-drawer__list-item--deep .menu-drawer__list-item, + .menu-drawer__list-item--flat .menu-drawer__list-item { + min-height: auto; + } + + .menu-drawer__menu .menu-drawer__list-item--flat { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-block-end: var(--margin-md); + } + + .menu-drawer__menu--childlist .menu-drawer__list-item--flat { + margin-block-end: var(--margin-sm); + + @media screen and (min-width: 750px) { + margin-block-end: var(--margin-lg); + } + } + + .menu-drawer__menu--childlist .menu-drawer__list-item--flat.menu-drawer__list-item--divider { + margin-block-end: 0; + } + + .menu-drawer__list-item--flat .menu-drawer__menu--childlist { + width: 100%; + padding-inline-start: 0; + } + + .menu-drawer-container[open] .menu-drawer__animated-element { + animation: menu-drawer-nav-open var(--drawer-animation-speed) ease-in-out; + animation-delay: calc(var(--drawer-animation-speed) + (var(--menu-drawer-animation-index) - 1) * 0.1s); + animation-fill-mode: backwards; + } + + .menu-drawer__menu details, + .menu-drawer__menu-item, + .menu-drawer__menu accordion-custom { + width: 100%; + } + + .menu-drawer__list-item--divider .menu-drawer__menu-item:not(.menu-drawer__menu-item--child) { + min-height: calc(2 * var(--padding-lg) + var(--icon-size-xs)); + } + + .menu-drawer__menu-item--mainlist { + min-height: calc(2 * var(--padding-lg) + var(--icon-size-xs)); + font-family: var(--menu-top-level-font-family); + font-style: var(--menu-top-level-font-style); + font-weight: var(--menu-top-level-font-weight); + font-size: var(--menu-top-level-font-size); + line-height: var(--menu-top-level-font-line-height); + text-transform: var(--menu-top-level-font-case); + color: var(--menu-top-level-font-color); + justify-content: space-between; + + &:hover { + color: var(--menu-top-level-font-color); + } + } + + .menu-drawer__menu-item--parent { + font-family: var(--menu-parent-font-family); + font-style: var(--menu-parent-font-style); + font-weight: var(--menu-parent-font-weight); + font-size: var(--menu-parent-font-size); + line-height: var(--menu-parent-font-line-height); + text-transform: var(--menu-parent-font-case); + color: var(--menu-parent-font-color); + + &:hover { + color: var(--menu-parent-font-color); + } + } + + .menu-drawer__menu-item--child { + font-family: var(--menu-child-font-family); + font-style: var(--menu-child-font-style); + font-weight: var(--menu-child-font-weight); + font-size: var(--menu-child-font-size); + line-height: var(--menu-child-font-line-height); + text-transform: var(--menu-child-font-case); + color: var(--menu-child-font-color); + + &:hover { + color: var(--menu-child-font-color); + } + } + + .menu-drawer__menu--childlist summary.menu-drawer__menu-item { + display: flex; + width: 100%; + padding-inline-end: 0; + } + + .menu-drawer__list-item--deep .menu-drawer__menu, + .menu-drawer__menu--grandchildlist { + padding-inline-start: 0; + } + + .menu-drawer__list-item--deep .menu-drawer__menu { + padding-block-end: 0.5rem; + } + + .menu-drawer__list-item--deep.menu-drawer__list-item--divider .menu-drawer__menu { + padding-block-end: 0.3rem; + } + + .menu-drawer__list-item--flat.menu-drawer__list-item--divider .menu-drawer__menu--grandchildlist { + padding-block-end: 0.5rem; + } + + .menu-drawer__menu-item { + display: flex; + padding: var(--padding-2xs) 0; + position: relative; + text-decoration: none; + justify-content: space-between; + align-items: center; + } + + .menu-drawer__menu-item:has(> .menu-drawer__link-image) { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + row-gap: var(--padding-3xs); + padding: 0; + } + + .menu-drawer__link-image { + width: 100%; + position: relative; + aspect-ratio: 16 / 9; + object-fit: cover; + } + + /* Fix alignment for collection image mode links without images in drawer */ + /* Target menu items in grids that have images */ + .menu-drawer__menu--grid:has(.menu-drawer__link-image) .menu-drawer__menu-item:not(:has(> .menu-drawer__link-image)) { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + row-gap: var(--padding-3xs); + padding: 0; + } + + .menu-drawer__menu--grid:has(.menu-drawer__link-image) + .menu-drawer__menu-item:not(:has(> .menu-drawer__link-image))::before { + content: ''; + display: block; + width: 100%; + aspect-ratio: 16 / 9; + background-color: var(--color-foreground-muted); + opacity: 0.1; + border-radius: var(--menu-image-border-radius); + } + + .menu-drawer__close-button { + background-color: transparent; + color: var(--color-foreground); + padding: var(--padding-xl); + box-shadow: none; + will-change: transform; + } + + .menu-drawer__close-button .svg-wrapper, + .menu-drawer__close-button svg { + width: var(--icon-size-xs); + height: var(--icon-size-xs); + } + + .menu-drawer__back-button { + display: flex; + width: 100%; + padding: var(--padding-md) var(--padding-xl); + border: none; + align-items: center; + color: var(--color-foreground); + background-color: transparent; + text-align: left; + text-decoration: none; + white-space: nowrap; + overflow-x: hidden; + line-height: 1.2; + box-shadow: none; + } + + .menu-drawer__menu-item-text { + overflow: hidden; + text-overflow: ellipsis; + } + + /** Styles when the country selector is hidden */ + .menu-drawer .language-selector:not(.menu-drawer__submenu *) { + width: fit-content; + padding-inline-start: 0; + + .localization-form__select { + text-align: left; + } + } + + .menu-drawer__menu-item > .svg-wrapper { + width: fit-content; + height: fit-content; + margin: 0; + padding-block: var(--padding-lg); + padding-inline-start: var(--padding-xl); + flex-shrink: 0; + } + + .menu-drawer__list-item--divider .menu-drawer__menu-item > .svg-wrapper { + padding-block: var(--padding-md); + } + + .menu-drawer svg { + width: var(--icon-size-xs); + height: var(--icon-size-xs); + } + + .menu-drawer__submenu { + position: absolute; + width: 100%; + top: 0; + height: 100dvh; + left: 0; + background-color: var(--color-background); + z-index: var(--layer-flat); + transform: translateX(-5%); + visibility: hidden; + overflow-y: auto; + opacity: 0; + } + + .menu-drawer__back-button > .svg-wrapper { + margin-right: var(--padding-md); + width: var(--icon-size-xs); + height: var(--icon-size-xs); + } + + .menu-drawer__utility-links { + display: flex; + flex-direction: column; + padding: 0; + margin-block: auto var(--padding-sm); + margin-inline-start: var(--padding-xl); + background-color: rgb(var(--color-foreground) 0.03); + } + + .menu-drawer__account { + display: inline-flex; + align-items: center; + gap: var(--gap-xs); + text-decoration: none; + height: 44px; + font-size: 1.4rem; + color: rgb(var(--color-foreground)); + } + + .menu-drawer__account svg { + height: var(--icon-size-sm); + width: var(--icon-size-sm); + } + + .menu-drawer__account shop-user-avatar { + --shop-avatar-size: 2.4rem; + + margin-right: 0.55rem; + margin-left: -0.45rem; + } + + .menu-drawer__link-image, + .menu-drawer__featured-product-image, + .menu-drawer__featured-collection-image, + .menu-drawer__featured-collection-link::before { + border-radius: var(--menu-image-border-radius); + } + + @keyframes menu-drawer-nav-open { + 0% { + visibility: hidden; + opacity: 0; + transform: translateX(-0.5rem); + } + + 100% { + visibility: visible; + opacity: 1; + transform: translateX(0); + } + } + + @keyframes menu-drawer-subnav-open { + 0% { + visibility: visible; + opacity: 1; + transform: translateX(0); + } + + 100% { + visibility: hidden; + opacity: 0; + transform: translateX(-1rem); + } + } +{% endstylesheet %} diff --git a/snippets/header-menu.liquid b/snippets/header-menu.liquid new file mode 100644 index 000000000..bc9f9e491 --- /dev/null +++ b/snippets/header-menu.liquid @@ -0,0 +1,90 @@ +{% liquid + assign menu_content_type = block.settings.menu_style | default: 'text' + assign image_border_radius = block.settings.image_border_radius + assign color_scheme_classes = '' + assign color_scheme_setting_id = 'color_scheme_' | append: section.settings.menu_row + assign current_color_scheme = block.settings.color_scheme + assign parent_color_scheme = section.settings[color_scheme_setting_id] + + if parent_color_scheme.id != current_color_scheme.id + assign color_scheme_classes = ' color-' | append: current_color_scheme + endif + + # Check if header and menu colors match. This is used to apply different padding styles in css + if parent_color_scheme.settings.background.rgb == current_color_scheme.settings.background.rgb + assign color_scheme_classes = color_scheme_classes | append: ' color-scheme-matches-parent' + endif + + if block.settings.menu_style == 'featured_collections' + assign ratio = block.settings.featured_collections_aspect_ratio + elsif block.settings.menu_style == 'featured_products' + assign ratio = block.settings.featured_products_aspect_ratio + endif +%} + +{% capture children %} + {% for link in block.settings.menu.links %} + + {% endfor %} + +{% endcapture %} + + diff --git a/snippets/header-row.liquid b/snippets/header-row.liquid new file mode 100644 index 000000000..134390046 --- /dev/null +++ b/snippets/header-row.liquid @@ -0,0 +1,78 @@ +{%- liquid + assign order = order | split: ',' + assign left = '' + assign center = '' + assign right = '' + + if first != blank + assign left = 'first ' + endif + + for item in order + assign column_key = item | append: '_position' + assign row_key = item | append: '_row' + assign item_row = settings[row_key] | default: 'top' + assign item_column = settings[column_key] | default: 'left' + + case item + when 'actions': + assign item_column = 'right' + endcase + + if item_row == row + case item_column + when 'left' + assign left = left | append: item | append: ' ' + when 'center' + assign center = center | append: item | append: ' ' + else + assign right = right | append: item | append: ' ' + endcase + endif + endfor + + assign columns = 'left,center,right' | split: ',' +-%} + +{%- for column in columns -%} + {%- capture items_for_column -%} + {% case column %} + {% when 'left' %} + {{ left }} + {% when 'center' %} + {{ center }} + {% else %} + {{ right }} + {% endcase %} + {%- endcapture -%} + + {%- assign items_array = items_for_column | strip | split: ' ' | compact -%} + + {%- if items_array.size > 0 -%} +
+ {% for key in items_array %} + {% unless key == blank %} + {% case key %} + {% when 'first' %} + {{ first }} + {% when 'logo' %} + {{ logo }} + {% when 'menu' %} + {{ menu }} + {% when 'localization' %} + {{ localization }} + {% when 'search' %} + {{ search }} + {% when 'mobile_search' %} + {{ mobile_search }} + {% when 'actions' %} + {{ actions }} + {% endcase %} + {% endunless %} + {% endfor %} +
+ {%- endif -%} +{%- endfor -%} diff --git a/snippets/icon-or-image.liquid b/snippets/icon-or-image.liquid new file mode 100644 index 000000000..3cd6735fd --- /dev/null +++ b/snippets/icon-or-image.liquid @@ -0,0 +1,40 @@ +{%- doc -%} + Renders either an SVG icon or an uploaded image based on block settings. + + @param {string} icon - The icon name from block.settings.icon + @param {object} image_upload - The uploaded image from block.settings.image_upload + @param {number} width - The width setting from block.settings.width + @param {string} class_name - CSS class name for the rendered element + @param {object} [attributes] - Additional HTML attributes to add to the element +{%- enddoc -%} + +{%- if icon != 'none' and image_upload == blank -%} + +{%- elsif image_upload != blank -%} + {% liquid + assign media_width_desktop = '100vw' + assign media_width_mobile = '100vw' + assign sizes = '(min-width: 1024px) 1024px, ' | append: media_width_desktop | append: ', ' | append: media_width_mobile + assign widths = '300, 375, 450, 525, 600, 675, 750, 768, 850, 900, 1024' + %} + + {% assign image_style = 'width: ' | append: width | append: 'px;' %} + {{ + image_upload + | image_url: width: 1024 + | image_tag: widths: widths, class: class_name, style: image_style, sizes: sizes + }} +{%- endif -%} diff --git a/snippets/icon.liquid b/snippets/icon.liquid new file mode 100644 index 000000000..3ebf7200f --- /dev/null +++ b/snippets/icon.liquid @@ -0,0 +1,399 @@ +{% # To be removed when we can use the icon static block instead %} + +{%- case icon -%} + {%- when 'apple' -%} + + + + {%- when 'banana' -%} + + + {%- when 'bottle' -%} + + + + {%- when 'bluesky' %} + + {%- when 'box' -%} + + {%- when 'caret' -%} + + {%- when 'double-sided-caret' -%} + + + + + + {%- when 'carrot' -%} + + + + + + + {%- when 'chat_bubble' -%} + + + + + {%- when 'check_box' -%} + + + {%- when 'clipboard' -%} + + + + + + {%- when 'dairy' -%} + + + + + + {%- when 'dairy_free' -%} + + + + + + + + + + {%- when 'discord' -%} + + {%- when 'dryer' -%} + + + {%- when 'error' -%} + + + {%- when 'eye' -%} + + + {%- when 'fire' -%} + + + {%- when 'gluten_free' -%} + + + + + + + + + + + + {%- when 'heart' -%} + + {%- when 'iron' -%} + + + + + {%- when 'leaf' -%} + + + + + {%- when 'leather' -%} + + {%- when 'lightning_bolt' -%} + + {%- when 'linkedin' -%} + + {%- when 'lipstick' -%} + + + + {%- when 'lock' -%} + + + + + {%- when 'map_pin' -%} + + + {%- when 'nut_free' -%} + + + + + {%- when 'pants' -%} + + + {%- when 'paw_print' -%} + + + + + + {%- when 'pepper' -%} + + {%- when 'perfume' -%} + + + + + {%- when 'plane' -%} + + {%- when 'plant' -%} + + + + + {%- when 'price_tag' -%} + + + {%- when 'question_mark' -%} + + + {%- when 'recycle' -%} + + + + {%- when 'return' -%} + + + {%- when 'ruler' -%} + + + + + + {%- when 'serving_dish' -%} + + + + + {%- when 'shirt' -%} + + {%- when 'shoe' -%} + + + {%- when 'silhouette' -%} + + + {%- when 'snowflake' -%} + + {%- when 'star' -%} + + {%- when 'stopwatch' -%} + + + + + + + {%- when 'truck' -%} + + + + + {%- when 'washing' -%} + + + {%- when 'arrow' -%} + + + {%- when 'tiktok' -%} + + {%- when 'youtube' -%} + + {%- when 'instagram' -%} + + {%- when 'x' -%} + + {%- when 'twitter' -%} + + {%- when 'facebook' -%} + + {%- when 'pinterest' -%} + + {%- when 'tumblr' -%} + + {%- when 'vimeo' -%} + + {%- when 'snapchat' -%} + + {%- when 'spotify' -%} + + {%- when 'next' -%} + + {%- when 'previous' -%} + + {%- when 'threads' -%} + + {%- when 'whatsapp' -%} + + + {%- when '3d-model' -%} + + {%- when 'mastodon' -%} + + {%- when 'reddit' -%} + + {%- when 'telegram' -%} + + {%- when 'twitch' -%} + +{%- endcase -%} diff --git a/snippets/image.liquid b/snippets/image.liquid new file mode 100644 index 000000000..2f220188f --- /dev/null +++ b/snippets/image.liquid @@ -0,0 +1,32 @@ +{%- doc -%} + Renders the element using provided image object + + @param {object} image - image object + @param {number} [height] - custom image height + @param {string} [class] - additional classes + @param {string} [text_fallback] - text to display if image is blank + @param {boolean} [unset_image_tag] - if true, ignores the image focal point + @param {string} [style] - additional styles + + @example + {% render 'image', image: product.featured_image, height: 300, class: 'product-image' %} +{%- enddoc -%} +{% if image != blank %} + {% assign image_height = height | default: image.height %} + {% assign image_height_2x = height | default: image_height | times: 2 %} + {% assign image_height_3x = height | default: image_height | times: 3 %} + + {% capture image_srcset -%} + {{ image | image_url: height: image_height }} 1x, {{ image | image_url: height: image_height_2x }} 2x, {{ image | image_url: height: image_height_3x }} 3x + {%- endcapture %} + + {% assign style_value = style | default: '' %} + + {% if unset_image_tag %} + {% assign style_value = style_value | append: 'object-position: inherit;' %} + {% endif %} + + {{ image | image_url: height: image_height | image_tag: class: class, srcset: image_srcset, style: style_value }} +{% elsif text_fallback %} + {{ text_fallback }} +{% endif %} diff --git a/snippets/jumbo-text.liquid b/snippets/jumbo-text.liquid new file mode 100644 index 000000000..b24bee9df --- /dev/null +++ b/snippets/jumbo-text.liquid @@ -0,0 +1,186 @@ +{%- doc -%} + Renders text that stretches to fit the full width of its container. + + @param {string} [text] - The text to be rendered. + + @example + {% render 'jumbo-text', text: block.settings.text %} +{%- enddoc -%} + +{% liquid + assign shown_text = text | default: block.settings.text + assign descenders = 'alphabetic' + assign trim = 'trim-both' + + unless block.settings.case == 'uppercase' + if shown_text contains 'g' or shown_text contains 'j' or shown_text contains 'p' or shown_text contains 'q' or shown_text contains 'y' + assign descenders = 'text' + endif + endunless + + assign text_trim = trim | append: ' cap ' | append: descenders + assign shown_text_with_line_breaks = shown_text | newline_to_br + assign text_with_lines = shown_text_with_line_breaks | split: '
' + assign nudge = '-0.04em' +%} + +{% capture attributes %} + style=" + --font-family: var(--font-{{ block.settings.font | default: 'accent'}}--family); + --font-weight: var(--font-{{ block.settings.font | default: 'accent'}}--weight); + {% if block.settings.font == 'body' %} + --color: var(--color-foreground); + {% else %} + --color: var(--color-foreground-heading); + {% endif %} + --text-align: {{ block.settings.alignment | default: 'left' }}; + {% if block.settings.alignment == "left" %} + --margin-left-nudge: {{nudge}}; + {% elsif block.settings.alignment == "right" %} + --margin-right-nudge: {{nudge}}; + {% endif %} + --line-height: {{ block.settings.line_height | default: '1' }}; + --letter-spacing: {{ block.settings.letter_spacing | default: '-0.03em' }}; + --text-transform: {{ block.settings.case | default: 'none' }}; + --text-trim: {{text_trim}}; + " + {{ block.shopify_attributes }} +{% endcapture %} + +{% comment %} + If the jumbo text is not wrapped inside its own container, the overflow calculation does not always work correctly + (looks like some weird off-by-one error when comparing sizes). +{% endcomment %} +
+ {% if text != blank %} + {{ text }} + {% else %} + {{ shown_text }} + + {%- assign char_index = 0 -%} + {%- for line in text_with_lines -%} + {%- if forloop.index > 1 -%} +
+ {%- endif -%} + + {%- endfor -%} +
+ {% endif %} +
+ + + +{% stylesheet %} + .jumbo-text__container { + width: 100%; + } + + jumbo-text { + display: block; + font-family: var(--font-family, inherit); + font-style: var(--font-style, normal); + color: var(--color, inherit); + font-weight: var(--font-weight, inherit); + letter-spacing: var(--letter-spacing, -0.02em); + line-height: var(--line-height, 1); + opacity: 0; + text-align: var(--text-align); + text-box: var(--text-trim, trim-end cap text); + text-transform: var(--text-transform, none); + transition: opacity 0.3s ease; + white-space: pre; + width: 100%; + will-change: font-size; + margin-left: var(--margin-left-nudge, 0); + margin-right: var(--margin-right-nudge, 0); + overflow: visible; + } + + jumbo-text.ready { + opacity: 1; + } + + jumbo-text[data-cap-text='true'] { + text-box-edge: cap text; + } + + .jumbo-text-space { + display: inline-flex; + width: 0.5ch; + } + + :is(.jumbo-text-char, .jumbo-text-line) { + display: inline-flex; + } + + @media (prefers-reduced-motion: no-preference) { + /* Blur effect */ + [data-text-effect='blur'] { + filter: blur(20px); + opacity: 0.5; + scale: 1.05; + transition: filter 1.6s var(--animation-timing-fade-in), opacity 1.3s var(--animation-timing-fade-in), + scale 1.6s var(--animation-timing-fade-in); + } + + .jumbo-text-visible[data-text-effect='blur'] { + filter: blur(0); + opacity: 1; + scale: 1; + } + + /* Reveal effect */ + .ready[data-text-effect='reveal'], + .ready[data-text-effect='reveal'] .jumbo-text-line { + overflow: hidden; + } + + .ready[data-text-effect='reveal'] .jumbo-text-char { + transform: translateY(100%); + } + + .jumbo-text-visible[data-text-effect='reveal'] .jumbo-text-char { + transition: transform 0.5s var(--animation-timing-fade-in) calc(var(--line-index) * 0.05s); + transform: translateY(0); + } + + .jumbo-text-visible[data-text-effect='reveal'], + .jumbo-text-visible[data-text-effect='reveal'] .jumbo-text-line { + overflow: visible; + transition: overflow 0s linear 0.75s; + } + } +{% endstylesheet %} diff --git a/snippets/layout-panel-style.liquid b/snippets/layout-panel-style.liquid new file mode 100644 index 000000000..d8311f78e --- /dev/null +++ b/snippets/layout-panel-style.liquid @@ -0,0 +1,33 @@ +{%- liquid + comment + Intended for blocks and sections that provide values for all the referenced settings. + + Accepts: + settings: {block.settings || section.settings} + endcomment + + assign horizontal_alignment = settings.horizontal_alignment + + assign vertical_alignment = settings.vertical_alignment + if settings.align_baseline and vertical_alignment == 'flex-end' + assign vertical_alignment = 'last baseline' + endif + + unless settings.content_direction == 'row' + assign horizontal_alignment = settings.horizontal_alignment_flex_direction_column + assign vertical_alignment = settings.vertical_alignment_flex_direction_column + endunless + + assign vertical_alignment_mobile = vertical_alignment + + if settings.vertical_on_mobile and vertical_alignment == 'last baseline' + assign vertical_alignment_mobile = 'flex-end' + endif +-%} + +--flex-direction: {{ settings.content_direction | default: 'column' }}; --flex-wrap: nowrap; + +{% render 'gap-style', value: settings.gap %} + +--horizontal-alignment: {{ horizontal_alignment }}; --vertical-alignment: {{ vertical_alignment }}; +--vertical-alignment-mobile: {{ vertical_alignment_mobile }}; diff --git a/snippets/link-featured-image.liquid b/snippets/link-featured-image.liquid new file mode 100644 index 000000000..22b44d70f --- /dev/null +++ b/snippets/link-featured-image.liquid @@ -0,0 +1,47 @@ +{%- doc -%} + Renders the featured image for a menu item. + + @param {object} link - The link to render + @param {string} [class] - The class to apply to the image + @param {string} [sizes] - The sizes to apply to the image + + @example + {% render 'menu-featured-image', link: link %} +{%- enddoc -%} + +{% assign image_sizes = sizes | default: 'auto' %} + +{% if link.type == 'collection_link' %} + {% if link.object.featured_image %} + {{ + link.object.featured_image + | image_url: width: 800 + | image_tag: loading: 'lazy', class: class, sizes: image_sizes + }} + {% elsif link.object.products.size > 0 %} + {% assign product_object = link.object.products | where: 'featured_image' | first %} + {% if product_object.featured_image %} + {{ + product_object.featured_image + | image_url: width: 800 + | image_tag: loading: 'lazy', class: class, sizes: image_sizes + }} + {% endif %} + {% endif %} +{% elsif link.type == 'collections_link' %} + {% assign collection_object = collections | where: 'featured_image' | first %} + {% if collection_object.featured_image %} + {{ + collection_object.featured_image + | image_url: width: 800 + | image_tag: loading: 'lazy', class: class, sizes: image_sizes + }} + {% endif %} +{% elsif link.type == 'catalog_link' %} + {% assign product_object = collections.all.products | where: 'featured_image' | first %} + {{ + product_object.featured_image + | image_url: width: 800 + | image_tag: loading: 'lazy', class: class, sizes: image_sizes + }} +{% endif %} diff --git a/snippets/list-filter.liquid b/snippets/list-filter.liquid new file mode 100644 index 000000000..7452dc042 --- /dev/null +++ b/snippets/list-filter.liquid @@ -0,0 +1,783 @@ +{%- doc -%} + Renders a list or swatch filter. + + @param {object} filter - The filter to render + @param {string} filter_style - The filter style ('horizontal' | 'vertical') + @param {number} active_value_count - The number of active values + @param {number} sectionId - The section ID + @param {boolean} [autofocus] - Whether to autofocus the filter + @param {boolean} [should_render_clear] - Whether to render the clear button + @param {boolean} [show_swatch_label] - Whether to show the swatch label + @param {boolean} [in_drawer] - Whether the filter is in a drawer +{%- enddoc -%} + +{% liquid + assign is_swatch = false + assign swatch_index = filter.values | find_index: 'swatch' + + if swatch_index != null + assign is_swatch = true + endif + + assign is_image = false + if filter.presentation == 'image' + assign is_image = true + endif +%} + + +
+ + {{ filter.label }} + +
+ {% if is_swatch %} + + + {%- liquid + if active_value_count > 3 + echo active_value_count + elsif active_value_count > 0 and active_value_count <= 3 + for value in filter.active_values + render 'swatch', swatch: value.swatch, mode: 'filter' + endfor + endif + -%} + + + {% else %} + + 1 %} + class="bubble facets__bubble" + {% endif %} + hide-when-empty + ref="facetStatus" + > + {%- liquid + if active_value_count == 1 + echo filter.active_values[0].label + elsif active_value_count > 1 + echo active_value_count + endif + -%} + + + {% endif %} + + {{- 'icon-caret.svg' | inline_asset_content -}} + +
+
+ + + {% liquid + assign has_active_values = false + assign inital_visible_values = 10 + if is_swatch + assign inital_visible_values = 22 + endif + if is_image + assign inital_visible_values = 6 + endif + assign max_visible_values = inital_visible_values | plus: 1 + assign render_show_more = false + assign should_render_for_swatch = is_swatch + if is_swatch and show_swatch_label + assign should_render_for_swatch = false + endif + if filter.values.size > max_visible_values and should_render_for_swatch == false + assign render_show_more = true + endif + %} + {% liquid + if render_show_more + if filter_style == 'horizontal' + echo '' + else + echo '' + endif + endif + assign should_use_pills = true + + for value in filter.values + if value.label.size > 3 + assign should_use_pills = false + break + endif + endfor + + if filter.type == 'boolean' + assign should_use_pills = false + endif + %} + +
+ {% liquid + if is_swatch + assign swatch_columns = filter.values.size + + if swatch_columns > 4 + assign swatch_columns = 4 + + # Balance the number of columns based on the number of values, i.e. try to avoid one or two items in + # the last row if the number of values is (almost) divisible by 3. + assign mod4 = filter.values.size | modulo: 4 + assign mod3 = filter.values.size | modulo: 3 + if mod4 != 0 and mod4 != 3 + if mod3 == 0 or mod3 == 2 + assign swatch_columns = 3 + endif + endif + endif + endif + + if is_image + assign image_columns = 3 + if filter.values.size < 3 + assign image_columns = filter.values.size + endif + endif + %} +
    + {%- for value in filter.values -%} + {% liquid + assign input_id = 'Filter-' | append: filter.param_name | escape | append: '-' | append: forloop.index | replace: '.', '-' | append: '-' | append: filter_style | append: '-' | append: in_drawer + assign is_disabled = false + if value.count == 0 and value.active == false + assign is_disabled = true + endif + assign hidden_class = null + if forloop.index > inital_visible_values and render_show_more + assign hidden_class = 'hidden' + if filter_style == 'horizontal' + assign hidden_class = 'mobile:hidden' + endif + endif + %} +
  • + {% if value.active %} + {% assign has_active_values = true %} + {% endif %} + {% if is_image %} +
    +
    + {% if value.image %} + {{ value.image | image_url: width: 300 | image_tag: alt: value.alt }} + {% endif %} + {% if is_disabled %} + + {% endif %} +
    + + +
    + {% elsif is_swatch %} +
    + +
    + {% else %} + {% if should_use_pills %} +
    + + +
    + {% else %} + {% render 'checkbox', + name: value.param_name, + value: value.value, + label: value.label, + checked: value.active, + id: input_id, + disabled: is_disabled, + inputRef: 'facetInputs[]', + events: 'on:pointerenter="/prefetchPage" on:pointerleave="/cancelPrefetchPage"', + autofocus: autofocus + %} + {% endif %} + {% endif %} +
  • + {%- endfor -%} +
+
+ {% if render_show_more %} + + {% echo '
' %} + {% endif %} + + {% if should_render_clear %} + +
+ +
+
+ {% endif %} +
+
+
+
+ +{% stylesheet %} + .facets input:checked + label { + font-weight: 500; + } + + .facets .checkbox .icon-checkmark { + transition: border-color 0.2s ease, background-color 0.2s ease; + } + + .facets .checkbox:not(:has(.checkbox__input:disabled)):hover .icon-checkmark { + border-color: rgb(var(--color-foreground-rgb) / var(--opacity-40-60)); + background-color: rgb(var(--color-foreground-rgb) / var(--opacity-5)); + } + + .facets .checkbox:has(.checkbox__input:checked):not(:has(.checkbox__input:disabled)):hover .icon-checkmark { + background-color: rgb(var(--color-foreground-rgb) / var(--opacity-85)); + } + + .facets .checkbox:not(:has(.checkbox__input:disabled)):hover .checkbox__label-text { + color: rgb(var(--color-foreground-rgb) / var(--opacity-90)); + } + + .facets .checkbox .checkbox__label-text { + transition: color 0.2s ease, font-weight 0.2s ease; + } + + /* Pill style */ + .facets__pill-label { + --pill-label-padding-inline: var(--padding-xs); + --pill-label-border-radius: var(--style-border-radius-md); + --pill-label-border-width: var(--variant-picker-button-border-width); + --pill-label-height: var(--button-size-md); + --pill-label-focus-outline-color: var(--color-foreground); + --pill-label-color: var(--color-foreground); + --pill-label-color-rgb: var(--color-foreground-rgb); + --pill-label-background-color: var(--color-background); + --pill-label-background-color-rgb: var(--color-background-rgb); + --pill-label-border-opacity: var(--facets-low-opacity); + + display: inline-flex; + position: relative; + align-items: center; + justify-content: center; + box-shadow: inset 0 0 0 var(--pill-label-border-width) rgb(var(--pill-label-color-rgb) / var(--opacity-10-25)); + border-radius: var(--pill-label-border-radius); + height: var(--pill-label-height); + width: 100%; + padding-inline: var(--pill-label-padding-inline); + color: rgb(var(---pill-label-color-rgb)); + background-color: rgb(var(--pill-label-background-color-rgb)); + cursor: pointer; + transition: color var(--animation-speed) var(--animation-easing), + background-color var(--animation-speed) var(--animation-easing); + outline-color: var(--pill-label-focus-outline-color); + + &:hover { + --pill-label-border-opacity: 100%; + } + } + + .facets__pill-input { + &:checked + .facets__pill-label { + --pill-label-color: var(--color-background); + --pill-label-background-color: var(--color-foreground); + --pill-label-border-opacity: 0; + + font-weight: 500; + } + + &:disabled + .facets__pill-label { + opacity: var(--disabled-opacity); + cursor: not-allowed; + + &:hover { + --pill-label-border-opacity: var(--facets-low-opacity); + } + } + } + + .facets__status-wrapper { + display: flex; + align-items: center; + } + + .facets--drawer .facets__status-wrapper { + @media screen and (max-width: 749px) { + gap: var(--gap-3xs); + } + } + + .facets--vertical .facets__status-wrapper { + gap: var(--gap-xs); + } + + .facets--horizontal .facets__status-wrapper { + gap: 0; + } + + .facets__pill-input:disabled + .facets__pill-label svg { + position: absolute; + top: 0; + left: 0; + border-radius: var(--style-border-radius-md); + } + + .facets__pill-label svg line { + stroke-width: 1.5px; + stroke: rgb(var(--color-foreground-rgb) / var(--facets-low-opacity)); + } + + .facets__pill-wrapper { + position: relative; + } + + .facets__pill-input { + position: absolute; + inset: 0; + margin: 0; + opacity: 0; + cursor: pointer; + } + + /* Swatches */ + .facets__status--swatches { + display: none; + } + + .facets__swatch-wrapper { + display: flex; + } + + .facets__inputs-list--swatches .variant-option__button-label { + --color-variant-text: var(--color-foreground); + } + + .facets__inputs-list--swatches { + --variant-picker-swatch-width: 32px; + --variant-picker-swatch-height: 32px; + + @media screen and (min-width: 750px) { + --variant-picker-swatch-width: 26px; + --variant-picker-swatch-height: 26px; + } + } + + .facets--vertical .facets__inputs-wrapper .facets__inputs-list--swatches-grid { + gap: var(--gap-sm); + } + + .facets--vertical .facets__inputs-list--swatches .facets__inputs-list-item { + display: flex; + } + + .facets__inputs-wrapper .facets__inputs-list--swatches-grid { + --columns: 2; + + display: grid; + grid-template-columns: repeat(var(--columns), 1fr); + } + + .facets__inputs-wrapper .facets__inputs-list--swatches-grid .variant-option--swatches { + cursor: pointer; + overflow: visible; + + &.variant-option--swatches-disabled, + &:has(input:disabled) { + cursor: not-allowed; + } + } + + .facets__inputs-wrapper .facets__inputs-list--swatches-grid label { + cursor: pointer; + word-break: break-word; + white-space: normal; + + .variant-option--swatches-disabled &, + .variant-option--swatches:has(input:disabled) & { + cursor: not-allowed; + } + } + + .facets__inputs-wrapper .facets__inputs-list--swatches-grid .variant-option__button-label--has-swatch { + align-items: center; + overflow: visible; + justify-content: flex-start; + display: flex; + width: 100%; + flex-basis: unset; + gap: var(--gap-sm); + } + + .facets__inputs-wrapper .facets__inputs-list--swatches-grid .variant-option__button-label:has(:checked) { + color: rgb(var(--color-foreground-rgb)); + background-color: rgb(var(--color-background-rgb)); + font-weight: 500; + transition: font-weight 0.2s ease; + } + + .facets .variant-option--swatches { + --options-border-radius: var(--variant-picker-swatch-radius); + + width: auto; + } + + .facets--horizontal .facets__status--swatches { + @media screen and (min-width: 750px) { + display: flex; + } + } + + .facets--horizontal .sorting-filter .facets__status { + @media screen and (min-width: 750px) { + display: none; + } + } + + .facets__status--swatches .swatch { + width: calc(var(--variant-picker-swatch-width) / 1.5); + height: calc(var(--variant-picker-swatch-height) / 1.5); + } + + .facets__status--swatches .swatch + .swatch { + margin-left: calc(var(--variant-picker-swatch-width) / -3); + outline: 1px solid rgb(var(--color-background-rgb)); + } + + .variant-option--images { + position: relative; + } + + .variant-option--images { + --image-facet-border-width: var(--variant-picker-button-border-width); + --image-facet-border-opacity: var(--facets-low-opacity); + --image-facet-border-radius: var(--style-border-radius-xs); + + border-radius: var(--image-facet-border-radius); + box-shadow: inset 0 0 0 var(--image-facet-border-width) + rgb(var(--color-foreground-rgb) / var(--image-facet-border-opacity)); + + &:hover:not(:has(input:disabled)), + &:has(input:checked) { + --image-facet-border-opacity: 100%; + } + + &:has(input:checked) { + font-weight: 500; + transition: font-weight 0.2s ease; + } + + &:has(input:checked):hover { + --image-facet-border-width: calc(var(--variant-picker-button-border-width) + 0.5px); + } + + &:has(input:focus-visible) { + outline: var(--focus-outline-width) solid currentcolor; + outline-offset: var(--focus-outline-offset); + } + + &:has(input:disabled), + &:has(input:disabled):hover { + --image-facet-border-opacity: 0; + + opacity: var(--disabled-opacity); + cursor: not-allowed; + + img { + opacity: var(--disabled-opacity); + } + + input, + label, + .facets__image-label { + cursor: not-allowed; + } + + .facets__image-wrapper { + border: var(--style-border-width) solid rgb(var(--color-foreground-rgb) / var(--opacity-30)); + border-radius: var(--image-facet-border-radius); + } + } + } + + .facets__inputs-wrapper .facets__inputs-list--images { + display: grid; + grid-template-columns: repeat(var(--image-columns), 125px); + gap: var(--gap-sm); + } + + .facets--drawer .facets__inputs-wrapper .facets__inputs-list--images { + grid-template-columns: repeat(3, 1fr); + + @media screen and (min-width: 750px) { + grid-template-columns: repeat(4, 1fr); + } + } + + .facets--vertical .facets__inputs-wrapper .facets__inputs-list--images { + grid-template-columns: repeat(2, 1fr); + } + + .facets--drawer .facets__inputs-list--images { + padding-top: var(--padding-xs); + } + + .facets__image-wrapper { + aspect-ratio: 1/1; + width: 100%; + padding: var(--padding-xs); + position: relative; + overflow: hidden; + } + + .facets__image-wrapper img { + height: 100%; + width: 100%; + object-fit: contain; + border-radius: calc(var(--border-radius) / 2); + } + + /* Position disabled-svg */ + .variant-option--images svg { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + width: 100%; + height: 100%; + stroke-width: var(--border-width); + stroke: rgb(var(--color-foreground-rgb) / var(--opacity-5)); + } + + /* Position label text and handle overflow */ + .facets__inputs-list-item, + .variant-option--images { + min-width: 0; + } + + .facets__image-label { + width: 100%; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-block-end: var(--padding-xs); + cursor: pointer; + + .variant-option--images:has(input:disabled) & { + cursor: not-allowed; + } + } + + .facets__inputs-list--swatches .variant-option__button-label:hover:not(:has(input:disabled)) { + font-weight: 500; + } + + .variant-option--images:not(:has(input:disabled)) .facets__image-label:hover { + font-weight: 500; + } +{% endstylesheet %} diff --git a/snippets/localization-form.liquid b/snippets/localization-form.liquid new file mode 100644 index 000000000..234119d73 --- /dev/null +++ b/snippets/localization-form.liquid @@ -0,0 +1,817 @@ +{%- doc -%} + Renders either a country selector, language selector, or both. + + @param {boolean} show_country - Whether to show the country selector + @param {boolean} show_language - Whether to show the language selector + @param {string} block_id - The block ID + @param {string} [form_style] - The style tag string to be applied to the form + @param {string} [localization_style] - The style of the localization form + + @example + {% render 'localization-form', show_country: true, show_language: true, block_id: block.id %} +{%- enddoc -%} + +{%- liquid + comment + From Tyler in July: Noting here that it might make sense for us to just be able to get localization.available_currencies and localization.popular_countries instead of needing this esoteric logic. + endcomment + assign currencies = localization.available_countries | map: 'currency' | map: 'iso_code' | uniq + assign popular_countries = localization.available_countries | where: 'popular?' | sort: 'name' + + assign show_country_filter = false + if localization.available_countries.size > 9 + assign show_country_filter = true + endif + + assign show_popular_countries = false + if localization.available_countries.size > 9 and popular_countries.size > 1 + assign show_popular_countries = true + endif + + assign show_currencies = false + if currencies.size > 1 + assign show_currencies = true + endif + + assign aliases_us = 'us,usa,america,united states of america' + assign aliases_uk = 'uk,gb,great britain' +%} + + {% assign localization_label = 'content.localization_region_and_language' | t %} + + {%- form 'localization', + id: 'LocalizationForm', + class: 'localization-form', + ref: 'form', + aria-label: localization_label + -%} + {% if show_country %} + {% if show_country_filter %} +
+
+
+ + {{ 'icon-search.svg' | inline_asset_content }} + +
+ + + +
+
+ {% endif %} +
+

+ Country/Region +

+ {% if show_country_filter %} +
+ {% endif %} +
+ {% if show_popular_countries %} + + {% endif %} +
    + + {%- for country in localization.available_countries -%} + {% liquid + assign aliases = '' + case country.iso_code + when 'US' + assign aliases = aliases_us + when 'GB' + assign aliases = aliases_uk + endcase + %} +
  • + + {{- 'icon-checkmark.svg' | inline_asset_content -}} + + {{- country.name }} + +
  • + {%- endfor -%} +
+
+
+ +
+ {% endif %} + + {% if show_language %} +
+

+ {{ 'content.language' | t }} +

+ {% if show_country == true %} + {{ 'content.language' | t }} + {% endif %} + + + {{- 'icon-caret.svg' | inline_asset_content -}} + +
+ {% endif %} + {%- endform -%} +
+ +{% stylesheet %} + /* Localization */ + localization-form-component { + display: flex; + width: var(--width, auto); + + @media screen and (min-width: 750px) { + position: relative; + } + } + + localization-form-component[data-show-filter='false'] .country-selector-form__wrapper { + padding-block-start: var(--padding-xs); + } + + .localization-form { + width: 100%; + } + + localization-form-component .button:is(:not(.country-filter__reset-button)) { + --button-color: var(--color-primary); + --button-background-color: var(--language-button-background-color, var(--color-background)); + --button-border-color: var(--language-button-border-color, var(--color-border)); + + text-decoration-color: transparent; + text-decoration-thickness: 0.075em; + text-underline-offset: 0.125em; + transition: text-decoration-color var(--animation-speed) var(--animation-easing); + } + + localization-form-component .button:is(:not(.country-filter__reset-button)):hover, + .localization-form__list-item:hover, + .localization-form__list-item:focus { + --button-color: var(--color-primary-hover); + + background-color: rgb(var(--color-primary-hover-rgb) / var(--opacity-8)); + } + + .localization-form__list-item[aria-current='true'] { + --button-color: var(--color-primary-active); + + background-color: rgb(var(--color-primary-hover-rgb) / var(--opacity-10)); + } + + .localization-form__list-item-disabled { + pointer-events: none; + } + + .localization-form__list-item:focus-visible { + outline: none; + } + + localization-form-component .localization-selector { + display: flex; + align-items: center; + gap: var(--margin-2xs); + } + + localization-form-component .country-filter__search-icon { + left: 8px; + right: auto; + color: var(--color-foreground-muted); + pointer-events: none; + } + + .country-filter__search-icon .svg-wrapper svg { + width: var(--icon-size-sm); + height: var(--icon-size-sm); + } + + .disclosure { + width: 100%; + } + + .dropdown-localization__button { + display: flex; + position: relative; + align-items: center; + gap: 4px; + font-family: var(--menu-localization-font); + font-size: var(--menu-localization-font-size); + font-weight: var(--menu-top-level-font-weight); + padding-inline: var(--padding-2xs); + margin-inline: calc(-1 * var(--padding-2xs)); + } + + .dropdown-localization__button .icon-caret { + height: var(--icon-size-xs); + width: var(--icon-size-xs); + right: var(--margin-xs); + top: calc(50% - var(--padding-2xs)); + flex-shrink: 0; + transition: transform var(--animation-speed) var(--animation-easing); + } + + .drawer-localization__button .icon-flag, + .dropdown-localization__button .icon-flag { + width: var(--menu-localization-font-size, var(--icon-size-sm)); + height: var(--menu-localization-font-size, var(--icon-size-sm)); + clip-path: circle(50%); /* stylelint-disable-line */ + background-position: center; + background-size: cover; + margin-inline-end: 4px; + position: relative; + } + + .icon-flag::after { + content: ''; + position: absolute; + inset: 0; + box-shadow: inset 0 0 var(--size-shadow) var(--color-shadow); + border-radius: 50%; + } + + .dropdown-localization__button[aria-expanded='true'] .icon-caret svg { + transform: rotate(180deg); + } + + .dropdown-localization__button, + .dropdown-localization__button:hover { + box-shadow: none; + background-color: transparent; + border-color: transparent; + color: var(--color-foreground); + } + + .localization-form__list { + position: relative; + width: 100%; + padding-block: 0 var(--padding-xs); + font-size: var(--font-size-lg); + scroll-padding: var(--padding-xs) 0; + overflow-y: auto; + white-space: nowrap; + + /* Hide scrollbar which would cause extra right padding in Safari */ + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + } + + dropdown-localization-component .localization-form__list { + max-height: 20.5rem; + } + + .localization-wrapper { + position: fixed; + z-index: var(--layer-raised); + border-radius: var(--style-border-radius-popover); + transition-property: display, opacity, translate; + transition-duration: 0.3s; + transition-timing-function: var(--ease-out-quad); + transition-behavior: allow-discrete; + translate: 0 20px; + opacity: 0; + } + + .localization-wrapper:not([hidden]) { + translate: 0 0; + opacity: 1; + } + @starting-style { + .localization-wrapper:not([hidden]) { + translate: 0 20px; + opacity: 0; + } + } + + .localization-form__list-item:not([hidden]) { + margin-block-end: var(--margin-3xs); + display: flex; + gap: var(--margin-sm); + padding: 8px; + border-radius: 8px; + line-height: var(--font-line-height-md); + align-items: center; + text-align: start; + cursor: pointer; + transition: background-color var(--animation-speed) var(--animation-easing); + + .country { + flex: 1; + color: var(--color-foreground); + } + + &:hover { + background-color: rgb(var(--color-foreground-rgb) / var(--opacity-8)); + } + + &[aria-current='true'] { + .country { + font-weight: 500; + } + } + } + + .localization-form__list-item#no-results-message { + grid-template-columns: 1fr; + text-align: center; + color: rgb(var(--color-foreground-rgb) / var(--opacity-subdued-text)); + } + + .is-searching .localization-form__list-item .country { + color: rgb(var(--color-foreground-rgb) / var(--opacity-80)); + } + + .localization-form__list-item .country mark { + font-weight: 500; + background: none; + color: var(--color-foreground); + } + + .country-filter { + position: relative; + padding: var(--padding-xs); + border-bottom: var(--style-border-width) solid transparent; + transition: border-color var(--animation-values); + } + + .country-filter.is-scrolled { + border-color: var(--color-border); + } + + .drawer-localization .country-filter { + padding-block: 8px; + } + + dropdown-localization-component .country-filter { + position: relative; + padding: 8px; + } + + .country-selector-form__wrapper { + overflow-y: auto; + max-height: 100%; + flex-grow: 1; + } + + .language-selector { + display: flex; + gap: var(--gap-xs); + padding: var(--padding-md) var(--padding-lg); + position: relative; + align-items: center; + justify-content: space-between; + width: 100%; + } + + .language-selector__label { + flex-shrink: 0; + color: rgb(var(--color-foreground-rgb) / var(--opacity-subdued-text)); + } + + .localization-form__select { + border: none; + color: var(--color-foreground); + appearance: none; + background-color: var(--color-input-background); + padding-block: var(--padding-3xs); + padding-inline: var(--padding-xs) calc(var(--icon-size-xs) + var(--padding-xs)); + text-align: right; + cursor: pointer; + max-width: 40vw; + text-overflow: ellipsis; + + &:focus-visible { + outline: var(--focus-outline-width) solid currentcolor; + } + + &:focus { + outline: none; + } + } + + #header-component[transparent] localization-form-component .localization-form .localization-form__select { + background-color: transparent; + } + + .localization-form__select option { + background-color: var(--color-input-background); + color: var(--color-input-text); + } + + dropdown-localization-component .localization-form__select:hover { + background-color: rgb(var(--color-primary-hover-rgb) / var(--opacity-8)); + } + + .language-selector .svg-wrapper.icon-caret { + width: var(--icon-size-xs); + height: var(--icon-size-xs); + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + } + + .language-selector--collapse-space { + padding-inline-end: var(--padding-2xs); + } + + .language-selector--collapse-space .localization-form__select { + padding-inline-end: var(--icon-size-xs); + } + + .language-selector--collapse-space .svg-wrapper.icon-caret { + right: 0; + } + + .localization-form .icon-checkmark { + width: var(--icon-size-xs); + height: var(--icon-size-xs); + } + + .localization-form .svg-wrapper.icon-checkmark { + visibility: hidden; + } + + .localization-form__list-item[aria-current='true'] .svg-wrapper.icon-checkmark { + visibility: visible; + } + + .country-filter__input { + width: 100%; + height: 44px; + font-size: var(--font-size-lg); + padding: var(--padding-md) var(--padding-lg) var(--padding-md) calc(var(--margin-md) + var(--padding-xl)); + border: 1px solid var(--color-foreground); + color: var(--color-input-text); + background-color: var(--color-input-background); + outline-offset: -1px; + + @media screen and (min-width: 750px) { + height: 36px; + } + } + + .country-filter__input::placeholder { + color: inherit; + } + + .country-filter .field { + position: relative; + } + + .country-filter .field__label { + font-size: var(--font-size-lg); + left: var(--margin-2xl); + top: var(--margin-xl); + pointer-events: none; + position: absolute; + } + + .country-filter__input:focus ~ .field__label, + .country-filter__input:not(:placeholder-shown) ~ .field__label, + .country-filter__input:-webkit-autofill ~ .field__label { + font-size: var(--font-size-xs); + top: var(--margin-xs); + } + + .country-filter .field__button:not([hidden]) { + display: flex; + height: fit-content; + position: absolute; + padding: 0; + right: 8px; + top: 50%; + transform: translateY(-50%); + align-items: center; + background-color: transparent; + color: var(--color-input-text); + border: 0; + } + + input[type='search']::-webkit-search-cancel-button { + appearance: none; + } + + .country-selector__close-button { + display: none; + } + + .drawer-localization .drawer-localization__button { + display: flex; + padding: 0; + position: relative; + text-decoration: none; + height: 44px; + + &:hover { + color: var(--color-foreground); + } + } + + .drawer-localization .drawer-localization__button .icon-caret { + width: fit-content; + height: fit-content; + margin: 0; + padding: var(--padding-xl) var(--padding-xl) var(--padding-xl) var(--padding-xs); + } + + dropdown-localization-component { + position: relative; + background-color: transparent; + } + + dropdown-localization-component .country-filter__input { + border: none; + } + + dropdown-localization-component .localization-form__list-item { + margin-inline: 8px; + } + + dropdown-localization-component .localization-wrapper { + box-shadow: var(--shadow-popover); + border: var(--style-border-popover); + background-color: var(--color-background); + max-height: 27.5rem; + position: absolute; + top: calc(100% + 10px); + z-index: calc(var(--layer-header-menu) + 1); + } + + dropdown-localization-component .localization-wrapper.right-bound { + right: 0; + left: unset; + } + + dropdown-localization-component .localization-wrapper.left-bound { + left: -8px; + right: unset; + } + + /* Additional specificity due to dropdown-localization-component getting a low score */ + dropdown-localization-component .language-selector.language-selector { + padding: 10px 8px 10px 16px; + } + + dropdown-localization-component .localization-form__currency { + width: max-content; + opacity: 0; + visibility: hidden; + transition: none; + } + + dropdown-localization-component + :is( + .localization-form__list-item:hover, + .localization-form__list-item[aria-selected='true'], + .localization-form__list-item[aria-current='true'] + ) + .localization-form__currency { + opacity: 1; + color: var(--color-foreground-muted); + transition: opacity var(--animation-speed-slow) var(--animation-easing); + visibility: visible; + } + + .dropdown-localization .language-selector:where(:not(.top-shadow)) { + font-weight: var(--menu-top-level-font-weight); + } + + .dropdown-localization:not(dropdown-localization-component) .language-selector, + .menu-drawer__localization:not(drawer-localization-component) .language-selector { + font-family: var(--menu-localization-font); + font-size: var(--menu-localization-font-size); + } + + .menu-drawer__localization .language-selector.h5 { + padding-inline-start: 0; + } + + .header__column .localization-form__select { + background-color: var(--header-bg-color); + } + + .drawer-localization { + display: contents; + color: var(--color-foreground); + } + + .drawer-localization localization-form-component { + position: relative; + height: 100%; + } + + .drawer-localization .mobile-localization, + .drawer-localization .drawer-localization__button--label { + display: flex; + gap: var(--gap-xs); + margin-block: 0; + align-items: center; + } + + .drawer-localization__button--label.h6 { + font-family: var(--menu-localization-font); + } + + .drawer-localization img { + width: var(--icon-size-sm); + } + + .drawer-localization .localization-button__icon, + .drawer-localization .localization-button__icon svg { + width: var(--icon-size-xs); + height: var(--icon-size-xs); + } + + .drawer-localization summary.is-disabled { + pointer-events: none; + } + + .drawer-localization .localization-wrapper { + width: 100%; + } + + .drawer-localization .localization-form { + display: flex; + flex-direction: column; + position: absolute; + inset: 0; + width: 100%; + height: 100%; + } + + .drawer-localization .localization-form > * { + padding-inline: var(--padding-xl); + } + + .drawer-localization .language-selector .svg-wrapper.icon-caret { + transform: translateY(-50%) rotate(0deg); + } + + .drawer-localization .language-selector .svg-wrapper.icon-caret svg { + transform: none; + } +{% endstylesheet %} diff --git a/snippets/media.liquid b/snippets/media.liquid new file mode 100644 index 000000000..f99f946ef --- /dev/null +++ b/snippets/media.liquid @@ -0,0 +1,116 @@ +{%- doc -%} + Renders media block contents (used in _media and _media-without-appearance blocks) + + @param {string} section_id - The section ID + @param {object} [block] - The block object + @param {boolean} [unset_image_tag] - if true, ignores the image focal point in the image +{%- enddoc -%} + +{% liquid + assign show_image = false + assign show_video = false + + if block.settings.media_type == 'image' + assign show_image = true + endif + + if block.settings.media_type == 'video' + assign show_video = true + endif +%} + +
+ {%- if show_image -%} + {% capture image_tag %} + {% if block.settings.image != blank %} + {% render 'image', + image: block.settings.image, + class: 'media-block__media border-style', + unset_image_tag: unset_image_tag + %} + {% else %} +
+ {{ 'detailed-apparel-1' | placeholder_svg_tag: 'hero__image' }} +
+ {% endif %} + {% endcapture %} + + {% if block.settings.link != blank %} + + {{ image_tag }} + + {% else %} + {{ image_tag }} + {% endif %} + {%- elsif show_video -%} + {% render 'video', + video: block.settings.video, + video_autoplay: block.settings.video_autoplay, + video_loop: block.settings.video_loop, + video_class: 'media-block__media media-block__media--video border-style', + section_id: section_id + %} + {%- else -%} +
+ {{ 'hero-apparel-3' | placeholder_svg_tag }} +
+ {%- endif -%} +
+ +{% stylesheet %} + .media-block { + overflow: hidden; + position: relative; + + @media screen and (min-width: 750px) { + min-height: var(--media-height); + } + } + + .media-block__media { + height: var(--media-height-mobile, auto); + object-fit: var(--image-position, 'cover'); + object-position: center center; + width: 100%; + + @media screen and (min-width: 750px) { + height: 100%; + position: absolute; + } + } + + deferred-media[class].media-block__media + :is(.deferred-media__poster-button img, .deferred-media__poster-button ~ video) { + object-fit: var(--video-position, 'cover'); + } + + /* This is to support corner radius on video and align the video to the center of the block */ + .media-block__media--video { + display: flex; + align-items: center; + justify-content: center; + + @media screen and (max-width: 749px) { + --media-height-mobile: auto; + } + } +{% endstylesheet %} diff --git a/snippets/mega-menu-list.liquid b/snippets/mega-menu-list.liquid new file mode 100644 index 000000000..762327540 --- /dev/null +++ b/snippets/mega-menu-list.liquid @@ -0,0 +1,322 @@ +{%- doc -%} + Renders mega menu list markup and optional featured content. + + @param {object} parent_link - The linklist to render. + @param {string} id - Unique ID to assign to the `
    ` element. + @param {number} [grid_columns_count] - Number of grid columns for the mega menu. + @param {number} [grid_columns_count_tablet] - Number of grid columns for the mega menu on tablets. + @param {number} [grid_columns_count_collection_images] - Number of grid columns when `menu_content_type` is 'collection_images'. + @param {string} [menu_content_type] - Type of content: 'featured_products', 'featured_collections', 'collection_images', or 'text'. + @param {number} [content_aspect_ratio] - Aspect ratio for content images. + @param {number} [image_border_radius] - Border radius for content images. + + @example + {% render 'mega-menu-list', parent_link: link, id: 'MegaMenuList-1', grid_columns_count: 6, menu_content_type: 'featured_products' %} +{%- enddoc -%} + +{% liquid + comment + open_column_span tracks when a vertical column in the mega menu is open. Links will be stacked + in the column until the code closes the column span. + endcomment + assign open_column_span = false + assign column_count = 0 + assign links_before_wrap = 10 + assign max_menu_columns = grid_columns_count | default: 6 + assign max_menu_columns_tablet = grid_columns_count_tablet | default: 4 + + if menu_content_type == 'collection_images' + assign collection_links = parent_link.links | where: 'type', 'collection_link' + assign catalog_links = parent_link.links | where: 'type', 'catalog_link' + assign collection_list_links = parent_link.links | where: 'type', 'collections_link' + if collection_links.size == 0 and catalog_links.size == 0 and collection_list_links.size == 0 + assign menu_content_type = 'text' + endif + endif + + if menu_content_type == 'featured_collections' + if parent_link.type == 'collection_link' + assign collection_handles = parent_link.object.handle | append: ',' + elsif parent_link.type == 'catalog_link' or parent_link.type == 'collections_link' + assign collection_handles = 'all' | append: ',' + endif + assign collection_links = parent_link.links | where: 'type', 'collection_link' + for collection_link in collection_links + assign collection_handles = collection_handles | append: collection_link.object.handle | append: ',' + endfor + endif + + if menu_content_type == 'featured_products' + if parent_link.type == 'collection_link' + assign collection_object = parent_link.object + elsif parent_link.type == 'catalog_link' or parent_link.type == 'collections_link' + assign collection_object = collections.all + else + assign menu_content_type = 'text' + endif + endif +%} + + +{% liquid + # decide how many grid columns are needed for the menu list, and how many columns are needed for the featured content + # prioritize a minimum of 1 featured_collection (2 columns), and minimum 2 featured_products (2 columns) + assign min_products = 2 + assign max_products = 3 + assign min_products_tablet = 1 + assign max_products_tablet = 3 + assign min_collections = 1 + + if menu_content_type == 'featured_products' + # desktop breakpoint + assign temp_column_count = column_count | plus: min_products + + if temp_column_count > max_menu_columns + assign max_product_columns = 2 + else + assign max_product_columns = max_menu_columns | minus: column_count | at_most: max_products + endif + + assign max_product_columns = max_product_columns | at_most: collection_object.products.size + + assign max_featured_products = max_product_columns + assign max_menu_columns = max_menu_columns | minus: max_product_columns + + # tablet breakpoint + assign temp_column_count = column_count | plus: min_products_tablet + + if temp_column_count > max_menu_columns_tablet + assign max_product_columns_tablet = 1 + else + assign max_product_columns_tablet = max_menu_columns_tablet | minus: column_count | at_most: max_products_tablet + endif + + assign max_product_columns_tablet = max_product_columns_tablet | at_most: collection_object.products.size + + assign max_featured_products_tablet = max_product_columns_tablet + assign max_menu_columns_tablet = max_menu_columns_tablet | minus: max_product_columns_tablet + endif + + if menu_content_type == 'featured_collections' + # desktop breakpoint + assign min_featured_collection_columns = min_collections | times: 2 + assign temp_column_count = column_count | plus: min_featured_collection_columns + + if temp_column_count > max_menu_columns + assign max_collection_columns = 2 + else + assign max_collection_columns = max_menu_columns | minus: column_count + endif + + assign max_featured_collections = max_collection_columns | divided_by: 2 | floor + assign max_menu_columns = max_menu_columns | minus: max_collection_columns + + # tablet breakpoint + assign max_collection_columns_tablet = 2 + assign max_featured_collections_tablet = 1 + assign max_menu_columns_tablet = max_menu_columns_tablet | minus: max_collection_columns_tablet + endif +%} + +{% style %} + [data-menu-grid-id="{{ id }}"] { + {% if menu_content_type == 'collection_images' and parent_link.links.size < 5 %} + --menu-columns-desktop: {{ grid_columns_count_collection_images }}; + --menu-columns-tablet: {{ grid_columns_count_tablet }}; + {% else %} + --menu-columns-desktop: {{ grid_columns_count }}; + --menu-columns-tablet: {{ grid_columns_count_tablet }}; + {% endif %} + } + + [data-menu-list-id="{{ id }}"] { + {% if menu_content_type == 'collection_images' and parent_link.links.size < 5 %} + --menu-columns-desktop: {{ grid_columns_count_collection_images }}; + --menu-columns-tablet: {{ max_menu_columns_tablet }}; + {% else %} + --menu-columns-desktop: {{ max_menu_columns }}; + --menu-columns-tablet: {{ max_menu_columns_tablet }}; + {% endif %} + } +{% endstyle %} + +{% case menu_content_type %} + {% when 'featured_products' %} + {%- capture image_sizes -%} + {%- render 'util-mega-menu-img-sizes-attr', + menu_content_type: 'featured_products', + settings: settings, + grid_columns: grid_columns_count, + grid_columns_tablet: grid_columns_count_tablet + -%} + {%- endcapture -%} + + +
      + {% paginate collection_object.products by max_featured_products %} + {% for item in collection_object.products %} + + {% endfor %} + {% endpaginate %} +
    +
    + {% when 'featured_collections' %} + {% assign collection_handles = collection_handles | split: ',' | uniq %} + {%- capture image_sizes -%} + {%- render 'util-mega-menu-img-sizes-attr', + menu_content_type: 'featured_collections', + settings: settings + -%} + {%- endcapture -%} + + {% if collection_handles.size == 1 %} + {% assign max_featured_collections = 1 %} + {% endif %} + + +
      + {% for handle in collection_handles limit: max_featured_collections %} + {% if handle == 'all' %} + {% assign collection_object = collections.all %} + {% else %} + {% assign collection_object = collections[handle] %} + {% endif %} + + {% endfor %} +
    +
    +{% endcase %} diff --git a/snippets/mega-menu.liquid b/snippets/mega-menu.liquid new file mode 100644 index 000000000..3267722cc --- /dev/null +++ b/snippets/mega-menu.liquid @@ -0,0 +1,29 @@ +{%- doc -%} + Renders a mega menu list and optional "more" menu links. + When more menu links are present a duplicate instance of each submenu is also rendered. + + @param {object} [section] - The section object. + @param {linklist} [parent_link] - The linklist to render + @param {string} [id] - Unique ID to assign ul in markup + @param {string} [menu_content_type] - The type of content to render, options: ['featured_products', 'featured_collections', 'collection_images', 'text'] (default: 'text') + @param {number} [content_aspect_ratio] - The aspect ratio to display the content if applicable + @param {number} [image_border_radius] - The border radius used for the content images +{%- enddoc -%} + +
    +
    + {% render 'mega-menu-list', + parent_link: parent_link, + id: id, + grid_columns_count: 6, + grid_columns_count_tablet: 4, + grid_columns_count_collection_images: 8, + menu_content_type: menu_content_type, + content_aspect_ratio: content_aspect_ratio, + image_border_radius: image_border_radius + %} +
    +
    diff --git a/snippets/menu-font-styles.liquid b/snippets/menu-font-styles.liquid new file mode 100644 index 000000000..21015a907 --- /dev/null +++ b/snippets/menu-font-styles.liquid @@ -0,0 +1,23 @@ +{%- comment -%} + Derives CSS variables from the menu typography settings for 1st level/main menu items. + Accepts: + settings {block.settings} + menu_type {string}: 'drawer' or 'mega_menu' +{%- endcomment -%} + +--menu-top-level-font-family: var(--font-{{ settings.type_font_primary_link }}--family); +--menu-top-level-font-size-desktop: {{ settings.type_font_primary_size }}; --menu-top-level-font-style: var(--font- +{{- settings.type_font_primary_link -}} +--style); --menu-top-level-font-weight: var(--font- +{{- settings.type_font_primary_link -}} +--weight); --menu-top-level-font-case: +{%- if settings.type_case_primary_link == 'uppercase' %}uppercase{% else %}none{% endif -%} +; +{% if menu_type == 'drawer' %} + --menu-top-level-font-size: var(--menu-font-2xl--size); --menu-top-level-font-line-height: + var(--menu-font-2xl--line-height); +{% else %} + --menu-top-level-font-size: var(--menu-font-sm--size); --menu-top-level-font-line-height: + var(--menu-font-sm--line-height); +{% endif %} +--menu-top-level-font-color: var(--color-foreground); --menu-top-level-font-color-rgb: var(--color-foreground-rgb); diff --git a/snippets/meta-tags.liquid b/snippets/meta-tags.liquid new file mode 100644 index 000000000..e4bd0f0a2 --- /dev/null +++ b/snippets/meta-tags.liquid @@ -0,0 +1,121 @@ + + + + + + +{%- liquid + assign og_title = page_title | default: shop.name + assign og_url = canonical_url | default: request.origin + assign og_type = 'website' + assign og_description = page_description | default: shop.description | default: shop.name + + if request.page_type == 'product' + assign og_type = 'product' + elsif request.page_type == 'article' + assign og_type = 'article' + elsif request.page_type == 'password' + assign og_url = request.origin + endif +%} + + + + + + + +{%- if page_image -%} + + + + +{%- endif -%} + +{%- if request.page_type == 'product' -%} + + +{%- endif -%} + +{%- if settings.social_twitter_link != blank -%} + +{%- endif -%} + + + + + + {{ page_title }} + {%- if current_tags %} – tagged "{{ current_tags | join: ', ' }}"{% endif -%} + {%- if current_page != 1 %} – Page {{ current_page }}{% endif -%} + {%- unless page_title contains shop.name %} – {{ shop.name }}{% endunless -%} + + + + +{% if page_description %} + +{% endif %} diff --git a/snippets/overflow-list.liquid b/snippets/overflow-list.liquid new file mode 100644 index 000000000..2bb5b6fcc --- /dev/null +++ b/snippets/overflow-list.liquid @@ -0,0 +1,63 @@ +{%- doc -%} + @param {string} children - The children of the overflow list. + @param {string} [class] - The class that is applied on the overflow-list element. + @param {boolean} [defer] - Whether to defer the loading of the overflow list. + @param {number} [minimum-items] - The minimum number of items to show in the overflow list. + @param {string} [more-attributes] - The attributes that are applied on the more button. + @param {string} [ref] - The ref that is set on the overflow-list element. +{%- enddoc -%} + + + + + {{ children }} + diff --git a/snippets/overlay.liquid b/snippets/overlay.liquid new file mode 100644 index 000000000..02d3d49d3 --- /dev/null +++ b/snippets/overlay.liquid @@ -0,0 +1,38 @@ +{%- doc -%} + Renders a full-bleed overlay. + + @param {object} settings - Block or section settings, expecting `overlay_color`, `overlay_style`, and `gradient_direction`. + @param {string} [layer] - The z-index layer for the overlay, defaults to `var(--layer-flat)`. + + @example + {% render 'overlay', settings: section.settings, layer: 'var(--layer-raised)' %} +{%- enddoc -%} + +
    + +{% stylesheet %} + .overlay { + position: absolute; + inset: 0; + z-index: var(--overlay-layer); + pointer-events: none; + border-radius: var(--overlay-border-radius, 0); + } + + .overlay--solid { + background: var(--overlay-color); + } + + .overlay--gradient { + background: linear-gradient(var(--overlay-direction), var(--overlay-color), var(--overlay-color--end)); + } +{% endstylesheet %} diff --git a/snippets/predictive-search-empty-state.liquid b/snippets/predictive-search-empty-state.liquid new file mode 100644 index 000000000..9b0183d13 --- /dev/null +++ b/snippets/predictive-search-empty-state.liquid @@ -0,0 +1,39 @@ +{% doc %} + Renders the predictive search empty state + + @param {object} empty_state_collection - collection used to in empty state + @param {number} shadow_opacity - shadow opacity for the empty state container shadow + @param {string} products_test_id - a playwright test id, used to differentiate empty state from 'real' search results + + @example + {% render 'predictive-search-empty-state', shadow_opacity: 0.1, products_test_id: 'empty-state' %} +{% enddoc %} + +
    +
    + {% liquid + assign products = settings.empty_state_collection.products | default: collections.all.products + assign default_title = 'content.search_results_resource_products' | t + assign title = settings.empty_state_collection.title | default: default_title + %} + {% comment %} Only show products section if there are products to display {% endcomment %} + {% if products.size > 0 %} + {% render 'predictive-search-products-list', + products_test_id: products_test_id, + title: title, + products: products, + limit: 4 + %} + {% else %} +
    +

    {{ 'content.no_products_found' | t }}

    +
    + {% endif %} +
    +
    diff --git a/snippets/predictive-search-products-list.liquid b/snippets/predictive-search-products-list.liquid new file mode 100644 index 000000000..1b5fafe45 --- /dev/null +++ b/snippets/predictive-search-products-list.liquid @@ -0,0 +1,112 @@ +{%- doc -%} + Renders the predictive search products list for empty state and recently viewed products + + @param {string} title - title of the result list + @param {object[]} products - array of products + @param {string[]} [order_ids] - array of product ids + @param {number} [limit] - limit of products to display + @param {string} [products_test_id] - a playwright test id, used to differentiate empty state from 'real' search results +{%- enddoc -%} +{%- liquid + assign recently_viewed_title_text = 'content.recently_viewed_products' | t +-%} +
    + {% if title == recently_viewed_title_text %} +
    +

    + {{ title }} + +

    +
      + {% liquid + assign limit = limit | default: 8 + %} + {% comment %} + If we're searching for recently viewed products by id, we need to reorder the products. + The order here comes from the search terms, and we display the products in the order of the ids. + {% endcomment %} + {% if order_ids != blank %} + {% for _id in order_ids %} + {% assign int_id = _id | times: 1 %} + {% assign product = products | find: 'id', int_id %} +
    • + {% render 'resource-card', + resource_type: 'product', + resource: product, + image_width: 500, + image_hover: true, + image_aspect_ratio: '4 / 5' + %} +
    • + {% endfor %} + {% else %} + {% for product in products limit: limit %} +
    • + {% render 'resource-card', + resource_type: 'product', + resource: product, + image_width: 500, + image_hover: true, + image_aspect_ratio: '4 / 5' + %} +
    • + {% endfor %} + {% endif %} +
    +
    + {% else %} +

    + {{ title }} +

    +
      + {% liquid + assign limit = limit | default: 8 + %} + {% for product in products limit: limit %} +
    • + {% render 'resource-card', + resource_type: 'product', + resource: product, + image_width: 500, + image_hover: true, + image_aspect_ratio: '4 / 5' + %} +
    • + {% endfor %} +
    + {% endif %} +
    diff --git a/snippets/predictive-search-resource-carousel.liquid b/snippets/predictive-search-resource-carousel.liquid new file mode 100644 index 000000000..93701e716 --- /dev/null +++ b/snippets/predictive-search-resource-carousel.liquid @@ -0,0 +1,65 @@ +{%- doc -%} + Renders a carousel of predictive search results cards. + + @param {string} title - The title of the carousel. + @param {object} resources - The resources to display. + @param {string} resource_type - The type of resource to display. +{%- enddoc -%} + +{% liquid + capture slides + for resource in resources + capture children + render 'resource-card', resource_type: resource_type, resource: resource, image_aspect_ratio: '4 / 5', collection_thumbnails: 'multiple' + endcapture + render 'slideshow-slide', index: forloop.index0, children: children, class: 'predictive-search-results__card' + endfor + endcapture +%} + +{% capture header %} +
    +

    + {{ title }} +

    + + {% if resources.size >= 4 %} + {%- render 'slideshow-controls', + show_arrows: true, + icon_style: 'chevron', + shape: 'none' + -%} + {% endif %} +
    +{% endcapture %} + +{% assign slideshow_class = 'predictive-search-results__list predictive-search-results__wrapper list-unstyled slideshow--single-media' %} +{% if resources.size >= 4 %} + {% assign slideshow_class = 'predictive-search-results__list predictive-search-results__wrapper list-unstyled' %} +{% endif %} + +{% render 'slideshow', + class: slideshow_class, + header: header, + infinite: false, + slides: slides, + slide_count: resources.size, + icon_style: 'chevron', + slideshow_gutters: 'start end' +%} + +{% stylesheet %} + .predictive-search-results__wrapper slideshow-slides { + /* Add padding to prevent hover animations from being clipped in slideshow + 15px accommodates: + - Scale effect (9px on each side from 1.03 scale) + - Lift effect (4px upward movement) + - Shadow (15px spread with -5px offset) + Using 16px for better alignment with our spacing scale */ + padding-block: var(--padding-xl); + margin-block: calc(-1 * var(--padding-xl)); + } +{% endstylesheet %} diff --git a/snippets/predictive-search.liquid b/snippets/predictive-search.liquid new file mode 100644 index 000000000..e42058db1 --- /dev/null +++ b/snippets/predictive-search.liquid @@ -0,0 +1,135 @@ +{%- doc -%} + Renders the predictive search input and results. + + @param {string} [class] - Additional classes to add to the component. + @param {string} [search_position] - Lateral position of the search component in the header ('left' or 'right'). + @param {string} [search_test_id] - A Playwright test ID for differentiating search components. + @param {string} [input_id] - The ID for the search input element. + @param {string} [products_test_id] - A Playwright test ID for differentiating products. + + @example + {% render 'predictive-search', search_position: 'left', search_test_id: 'header-search' %} +{%- enddoc -%} + + + + + + + +{% stylesheet %} + predictive-search-component { + &:has([data-search-results]):not(:has(.predictive-search-results__no-results)) { + .predictive-search-form__footer { + display: block; + } + } + } + + .predictive-search-form__footer { + display: none; + } +{% endstylesheet %} diff --git a/snippets/price-filter.liquid b/snippets/price-filter.liquid new file mode 100644 index 000000000..d772538c1 --- /dev/null +++ b/snippets/price-filter.liquid @@ -0,0 +1,239 @@ +{%- doc -%} + Renders a price filter. + + @param {object} filter - The filter object to render. + @param {string} filter_style - The filter style, can be 'horizontal' or 'vertical'. + @param {boolean} [autofocus] - Whether to autofocus the filter. + @param {boolean} [should_render_clear] - Whether to render the clear button. + + @example + {% render 'price-filter', filter: filter, filter_style: 'vertical' %} +{%- enddoc -%} + + +
    + + {{ filter.label }} + + + + + {%- if filter.min_value.value != null or filter.max_value.value != null %} + {%- if filter.min_value.value != null and filter.max_value.value != null %} + {{- filter.min_value.value | money | strip_html -}} + – + {{- filter.max_value.value | money | strip_html -}} + {%- elsif filter.min_value.value != null -%} + {{ filter.min_value.value | money | strip_html }}–{{ filter.range_max | money | strip_html }} + + {%- elsif filter.max_value.value != null -%} + {{- 0 | money | strip_html -}} + – + {{- filter.max_value.value | money | strip_html -}} + {%- endif -%} + {%- endif -%} + + + + {{- 'icon-caret.svg' | inline_asset_content -}} + + + + {% assign min_input_max_value = filter.max_value.value | default: filter.range_max %} + {% assign max_input_min_value = filter.min_value.value | default: 0 %} + + +
    +
    + + +
    + +
    {{ 'fields.separator' | t }}
    + +
    + + +
    +
    + +
    + {%- assign formatted_highest_price = filter.range_max | money -%} + {{ 'content.price_filter_html' | t: price: formatted_highest_price }} +
    + + {% if filter.min_value.value != null or filter.max_value.value != null %} + {% assign has_active_values = true %} + {% endif %} + + {% if should_render_clear %} + +
    + {{- 'actions.clear' | t -}} +
    +
    + {% endif %} +
    +
    +
    +
    + +{% stylesheet %} + /* Price filter */ + .price-facet { + container-type: inline-size; + display: flex; + flex-direction: column; + } + + .facets__inputs-wrapper.price-facet__inputs-wrapper { + flex-wrap: nowrap; + } + + .price-facet__field { + width: 50%; + flex-grow: 0; + } + + @container (max-width: 199px) { + .facets__inputs-wrapper.price-facet__inputs-wrapper { + flex-wrap: wrap; + width: 100%; + } + + .price-facet__inputs-wrapper .price-facet__field { + width: 100%; + } + } + + .facets .facets__inputs-wrapper.price-facet__inputs-wrapper { + padding: var(--style-border-width-inputs); + gap: calc(var(--gap-sm) + (var(--style-border-width-inputs) * 2)); + } + + .facets--horizontal .facets__panel-content:has(.price-facet) { + min-width: 360px; + } + + .facets--horizontal .facets__inputs-wrapper.price-facet__inputs-wrapper { + @media screen and (min-width: 750px) { + padding: calc(var(--padding-md) + var(--style-border-width-inputs)); + } + } + + .price-facet__input { + width: 100%; + text-align: right; + padding-left: calc(2.5 * var(--input-padding-x)); + } + + .price-facet__input::placeholder { + color: var(--facets-input-label-color); + } + + .price-facet__separator { + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-paragraph--size); + } + + .price-facet__highest-price { + padding: var(--padding-xs) 0 var(--padding-sm); + } + + .facets--horizontal .price-facet__highest-price { + padding: 0 var(--padding-md) var(--padding-xs); + } + + .field__label.price-facet__label { + top: 0; + left: 0; + color: var(--facets-input-label-color); + padding: var(--input-padding-y) var(--input-padding-x); + transform: none; + } +{% endstylesheet %} diff --git a/snippets/price.liquid b/snippets/price.liquid new file mode 100644 index 000000000..7369b79ff --- /dev/null +++ b/snippets/price.liquid @@ -0,0 +1,79 @@ +{%- doc -%} + This snippet is used to render a product card. + It is used in the product block,featured product block, and the product card block. + + @param {product} product_resource - The product to render + @param {boolean} [show_unit_price] - Whether to show the unit price + @param {boolean} [show_sale_price_first] - Whether to show the sale price first +{%- enddoc -%} + +{%- liquid + assign show_unit_price = show_unit_price | default: false + assign show_sale_price_first = show_sale_price_first | default: false + assign selected_variant = product_resource.selected_or_first_available_variant + assign price = selected_variant.price + assign compare_at_price = selected_variant.compare_at_price + + assign show_compare_price = false + if compare_at_price > price + assign show_compare_price = true + endif + + if product_resource == blank + assign price = 1999 + endif + + # Checks if product handle matches the closest product's handle (i.e. product page) + # and if the currency code is enabled for product pages + if product.handle == closest.product.handle and settings.currency_code_enabled_product_pages + assign price = price | money_with_currency + assign compare_at_price = compare_at_price | money_with_currency + + # Checks if product handle does not match the closest product's handle (i.e. product card) + # and if the currency code is enabled for product cards + elsif product.handle != closest.product.handle and settings.currency_code_enabled_product_cards + assign price = price | money_with_currency + assign compare_at_price = compare_at_price | money_with_currency + + else + assign price = price | money + assign compare_at_price = compare_at_price | money + endif +-%} + +
    + {% if show_sale_price_first == false and show_compare_price %} + + {{ 'content.price_regular' | t }}  + {{- compare_at_price -}} + + {% endif %} + + {% if show_compare_price %} + + {{ 'content.price_sale' | t }}  + {{ price | default: ' ' }} + + {% else %} + {{ price | default: ' ' }} + {% endif %} + + {% if show_sale_price_first == true and show_compare_price %} + + {{ 'content.price_regular' | t }}  + {{- compare_at_price -}} + + {% endif %} + {%- if selected_variant.unit_price and show_unit_price %} + {%- liquid + if product.handle == closest.product.handle and settings.currency_code_enabled_product_pages + assign unit_price = selected_variant.unit_price | money_with_currency + elsif product.handle != closest.product.handle and settings.currency_code_enabled_product_cards + assign unit_price = selected_variant.unit_price | money_with_currency + else + assign unit_price = selected_variant.unit_price | money + endif + -%} + {% render 'unit-price', price: unit_price, measurement: selected_variant.unit_price_measurement %} + {%- endif -%} +
    diff --git a/snippets/product-card-badges.liquid b/snippets/product-card-badges.liquid new file mode 100644 index 000000000..62b7b1856 --- /dev/null +++ b/snippets/product-card-badges.liquid @@ -0,0 +1,77 @@ +{%- doc -%} + Renders product badges for the product card. + + @param {object} product - The product object. + @param {object} settings - The theme settings object. + + @example + {% render 'product-card-badges', product: product, settings: settings %} +{%- enddoc -%} + +
    + {%- if product.available == false or product.compare_at_price > product.price and product.available -%} +
    + {%- if product.available == false -%} + {{ 'content.product_badge_sold_out' | t }} + {%- elsif product.compare_at_price > product.price -%} + {{ 'content.product_badge_sale' | t }} + {%- endif -%} +
    + {%- endif -%} +
    + +{% stylesheet %} + .product-badges { + --badge-inset: max(var(--padding-xs), calc((var(--border-radius) + var(--padding-xs)) * (1 - cos(45deg)))); + + position: absolute; + z-index: var(--layer-flat); + } + + .product-badges--bottom-left { + bottom: calc(var(--badge-inset) + var(--padding-block-start)); + left: calc(var(--badge-inset) + var(--padding-inline-start)); + } + + .product-badges--top-left { + top: calc(var(--badge-inset) + var(--padding-block-start)); + left: calc(var(--badge-inset) + var(--padding-inline-start)); + } + + .product-badges--top-right { + top: calc(var(--badge-inset) + var(--padding-block-start)); + right: calc(var(--badge-inset) + var(--padding-inline-start)); + } + + .product-badges__badge { + --badge-font-size: var(--font-size--xs); + + display: flex; + align-items: center; + justify-content: center; + text-align: center; + color: var(--color-foreground); + background: var(--color-background); + font-size: var(--badge-font-size); + font-family: var(--badge-font-family); + font-weight: var(--badge-font-weight); + text-transform: var(--badge-text-transform); + border-radius: var(--badge-border-radius); + } + + .product-badges__badge--rectangle { + padding-block: var(--badge-rectangle-padding-block); + padding-inline: var(--badge-rectangle-padding-inline); + } +{% endstylesheet %} diff --git a/snippets/product-card-variant-url.liquid b/snippets/product-card-variant-url.liquid new file mode 100644 index 000000000..9609fc439 --- /dev/null +++ b/snippets/product-card-variant-url.liquid @@ -0,0 +1,27 @@ +{%- doc -%} + Logic to determine which variant URL to use (matching swatch selection logic) + + @param {object} product - The product object + + @example + {% render 'product-card-variant-url', product: product %} +{%- enddoc -%} + +{% liquid + assign variant_to_link = product.selected_or_first_available_variant + assign combined_listing_count = product.options_with_values | map: 'values' | map: 'product_url' | compact | size + + # For now if it's combined listing, we don't need to do anything. + unless combined_listing_count > 0 + # Simple direct check: which variant owns the featured image? + if product.featured_media + for variant in product.variants + if variant.featured_media.id == product.featured_media.id + assign variant_to_link = variant + break + endif + endfor + endif + endunless +%} +{{- variant_to_link.url -}} diff --git a/snippets/product-card.liquid b/snippets/product-card.liquid new file mode 100644 index 000000000..6b8c9c4f8 --- /dev/null +++ b/snippets/product-card.liquid @@ -0,0 +1,220 @@ +{%- doc -%} + This snippet is used to render a product card. + It is used in the product block, featured product block, and the product card block. + The product object is null or when placeholders are rendered. + + @param {object} product - The product object + @param {object} children - The children of the product card + @param {object} [block] - The block object + @param {number} [product_card_gap] - The gap between the product card children (overrides block settings) +{%- enddoc -%} + +{% style %} + {% if request.visual_preview_mode %} + product-card-link { + width: 100%; + min-width: 250px; + } + {% endif %} +{% endstyle %} + +{% liquid + assign has_quick_add = false + if settings.quick_add and product.available + assign has_quick_add = true + endif + + assign has_mobile_quick_add = false + if has_quick_add and settings.mobile_quick_add + assign has_mobile_quick_add = true + endif + + assign product_card_id = 'product-card-link-' | append: block.id | append: '-' | append: product.id + assign product_card_gap_value = product_card_gap | default: block.settings.product_card_gap + + # Logic to determine which variant URL to use (matching swatch selection logic) + assign variant_to_link = product.selected_or_first_available_variant + assign combined_listing_count = product.options_with_values | map: 'values' | map: 'product_url' | compact | size + assign no_swatch_selected = null + + unless combined_listing_count > 0 + # Simple direct check: which variant owns the featured image? + if product.featured_media + assign found_variant = false + + for variant in product.variants + if variant.featured_media.id == product.featured_media.id + assign variant_to_link = variant + assign found_variant = true + break + endif + endfor + + # Check if we need to set no_swatch_selected + unless found_variant + # Featured image is not a variant image + # Check if product has swatches + for option in product.options_with_values + assign swatch_count = option.values | map: 'swatch' | compact | size + if swatch_count > 1 + # Multiple swatches exist but featured image doesn't match any + assign no_swatch_selected = true + break + endif + endfor + endunless + endif + endunless + + if settings.transition_to_main_product + assign featured_image = variant_to_link.featured_image + if featured_image == blank + assign featured_image = product.featured_media.preview_image + endif + + if featured_image != blank + assign featured_media_url = featured_image | image_url: width: 500 + endif + endif + + assign onboarding = false + if product.id == empty + assign onboarding = true + endif +%} + +{%- if settings.transition_to_main_product -%} + +{%- endif -%} + + + + {{ product.title }} + + +
    + {{ children }} +
    +
    +{%- if settings.transition_to_main_product -%} +
    +{%- endif -%} + +{% stylesheet %} + product-card-link, + :not(product-card-link) product-card { + width: 100%; + } + + .product-card__placeholder-image svg { + height: 100%; + } + + @media screen and (max-width: 749px) { + .product-card slideshow-arrows .slideshow-control { + display: none; + } + } + + /* Hide the variant swatches for product cards that show a swatches variant picker */ + :is(.product-card):has(swatches-variant-picker-component) .quick-add .variant-option--swatches { + display: none; + } + + /* Hide "Add" button for single option product cards that show a swatches variant picker */ + :is(.product-card:not([data-no-swatch-selected])):has(.quick-add__product-form-component--single-option):has( + swatches-variant-picker-component + ) + .quick-add__button--choose { + display: none; + } + + /* Hide "Add" button for single option product cards that show a swatches variant picker */ + :is(.product-card[data-no-swatch-selected]):has(.quick-add__product-form-component--single-option):has( + swatches-variant-picker-component + ) + add-to-cart-component { + display: none; + } + + /* Hide "add" button for multi-variant product cards that don't show a swatches variant picker */ + :is(.product-card):has(.quick-add__product-form-component--multi-variant):not(:has(swatches-variant-picker-component)) + .quick-add__button--add { + display: none; + } + + /* Hover effect for single variant product cards and product blocks */ + + /* stylelint-disable selector-max-specificity */ + :is(.product-card):has(.quick-add__product-form-component--single-variant) .card-gallery:hover { + & .quick-add__button--choose { + display: none; + } + + & .quick-add__button--add { + display: grid; + } + } + + .product-card[data-no-swatch-selected] slideshow-component[data-generic-media-size='1'] slideshow-arrows { + display: none; + } + + .product-card[data-no-swatch-selected] + slideshow-component[data-generic-media-size='1'] + slideshow-arrows:has(+ slideshow-slides slideshow-slide[variant-image]:not([hidden])) { + display: flex; + } + + .product-card .variant-option__swatch svg { + display: none; + } + + .product-card [data-available-count='0'] ~ svg { + display: block; + } +{% endstylesheet %} diff --git a/snippets/product-grid.liquid b/snippets/product-grid.liquid new file mode 100644 index 000000000..1150b05e5 --- /dev/null +++ b/snippets/product-grid.liquid @@ -0,0 +1,216 @@ +{%- doc -%} + This snippet is used to render the product grid on collection and search pages. + + @param {object} section - The section object + @param {object} paginate - Pagination object + @param {object} products - Array of product objects + @param {string} [title] - Header of the collection or search results + @param {string} children - List or grid of product cards +{%- enddoc -%} + +{% capture product_card_size %} + {% render 'util-product-grid-card-size' section: section %} +{% endcapture %} +{% assign product_card_size = product_card_size | strip %} + +{% style %} + @media (min-width: 750px) { + {% case section.settings.layout_type %} + {% when 'grid' %} + .product-grid--{{ section.id }}:is(.product-grid--grid) { + --product-grid-columns-desktop: repeat(auto-fill, minmax({{ product_card_size }}, 1fr)); + } + {% when 'organic' %} + {% assign large_span = 2 %} + {% assign row_cycle = 3 %} + {% assign product_cycle = row_cycle | times: 2 %} + {% assign right_large_start_col = 3 %} + .product-grid--{{ section.id }}:not([product-grid-view='zoom-out']):is(.product-grid--organic) .product-grid__item:nth-child({{ product_cycle }}n + 1) { + grid-column: 1 / span {{ large_span }}; + } + + .product-grid--{{ section.id }}:not([product-grid-view='zoom-out']):is(.product-grid--organic) .product-grid__item:nth-child({{ product_cycle }}n + 2), + .product-grid--{{ section.id }}:not([product-grid-view='zoom-out']):is(.product-grid--organic) .product-grid__item:nth-child({{ product_cycle }}n + 5) { + align-self: end; + } + + .product-grid--{{ section.id }}:not([product-grid-view='zoom-out']):is(.product-grid--organic) .product-grid__item:nth-child({{ product_cycle }}n + {{ product_cycle }}) { + grid-column: {{ right_large_start_col }} / span {{ large_span }}; + } + + .product-grid--{{ section.id }}:not([product-grid-view='zoom-out']):is(.product-grid--organic) { + --product-grid-columns-desktop: repeat(4, 1fr); + } + {% endcase %} + + /* This logic helps prevent displaying one column for an large or extra-large product card size on a small screen. We want it to display at least two columns. */ + {% case section.settings.product_card_size %} + {% when 'extra-large' or 'large' %} + @container product-grid (max-width: calc({{ product_card_size }} * 3 + {{ section.settings.columns_gap_horizontal }}px * 2)) { + .product-grid--{{ section.id }}:is(.product-grid--grid) { + --product-grid-columns-desktop: repeat(2, 1fr); + } + } + {% endcase %} + + /* When zoomed out, fit as many 100px-wide columns as possible */ + .product-grid--{{ section.id }}:is([product-grid-view='zoom-out']) { + --product-grid-columns-desktop: repeat(auto-fill, minmax(6.25rem, 1fr)); + } + + .product-grid--{{ section.id }}:is([product-grid-view='zoom-out']) .product-grid-view-zoom-out--details { + display: block; + } + + .product-grid--{{ section.id }}:is([product-grid-view='zoom-out']) .product-grid__card { + padding-inline-start: var(--zoom-out-padding-inline-start, 0); + padding-inline-end: var(--zoom-out-padding-inline-end, 0); + padding-block-start: var(--zoom-out-padding-block-start, 0); + padding-block-end: var(--zoom-out-padding-block-end, 0); + } + } +{% endstyle %} + +
    +
    + {% if products.size == 0 %} +
    +

    + {{ 'content.no_products_found' | t }} +

    +

    + {{ 'content.use_fewer_filters_html' | t: link: collection.url, class: 'main-collection-grid__empty-link' }} +

    +
    + {% else %} + + + {% if title %} +

    {{ title }}

    + {% endif %} +
      + {{ children }} +
    + + {% endif %} +
    +
    + +{% comment %} + This script is used to set the grid view on the product grid stored in sessionStorage. Keeping it here helps us prevent seeing the default state. +{% endcomment %} +{% unless request.design_mode %} + +{% endunless %} + +{% stylesheet %} + .product-grid { + --product-grid-gap: var(--product-grid-gap-mobile); + + isolation: isolate; + + @media screen and (min-width: 750px) { + --product-grid-gap: var(--product-grid-gap-desktop); + } + } + + .product-grid slideshow-arrows .slideshow-control { + display: none; + + @media screen and (min-width: 750px) { + display: grid; + } + } + + /* This triggers iOS < 16.4 */ + @supports not (background-color: rgb(from red 150 g b / alpha)) { + /* Force aspect ratio to auto for iOS < 16.4 since it's not compatible with the infinite pagination */ + .product-grid .product-media, + .product-grid .product-media-container { + aspect-ratio: auto; + } + } + + .main-collection-grid { + padding: var(--grid--margin--mobile); + + @media screen and (min-width: 750px) { + padding: var(--padding-block-start) var(--padding-inline-end) var(--padding-block-end) var(--padding-inline-start); + } + } + + .main-collection-grid__empty { + padding-block: var(--padding-6xl); + padding-inline: var(--page-margin); + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: var(--padding-sm); + } + + .main-collection-grid__empty-title { + margin: 0; + } + + .collection-wrapper--full-width .main-collection-grid__title { + margin-left: var(--page-margin); + } + + .collection-wrapper--full-width-on-mobile .main-collection-grid__title { + @media screen and (max-width: 749px) { + margin-left: var(--page-margin); + } + } +{% endstylesheet %} diff --git a/snippets/product-media.liquid b/snippets/product-media.liquid new file mode 100644 index 000000000..637ae50d4 --- /dev/null +++ b/snippets/product-media.liquid @@ -0,0 +1,208 @@ +{%- doc -%} + Renders a product media component. + + @param {object} media - The product media object. + @param {boolean} [preview_image_only] - Renders only the preview image without controls. + @param {string} [widths] - Image widths for responsive images. + @param {string} [sizes] - Image sizes for responsive images. + @param {string} [loading] - The loading attribute for the image. + @param {object} [block] - The block object. + @param {object} [section] - The section object. + @param {object} [selected_product] - The currently selected product. + @param {boolean} [first_3d_model] - Indicates if this is the first 3D model. + @param {boolean} [is_main_product_media] - Indicates if this is the main product image. + + @example + {% render 'product-media', media: media, preview_image_only: false, loading: 'lazy' %} +{%- enddoc -%} + +{% liquid + assign widths = widths | default: '400, 800, 1200, 1600, 2000' +%} + +
    + {% liquid + assign high_res_url = media.preview_image | image_url: width: 3840 + assign fetch_priority = 'auto' + if is_main_product_media + assign fetch_priority = 'high' + endif + %} + {{ + media.preview_image + | image_url: width: 2000 + | image_tag: + widths: widths, + alt: media.alt, + sizes: sizes, + loading: loading, + class: 'product-media__image', + transitionToProduct: settings.transition_to_main_product, + data_max_resolution: high_res_url, + fetchpriority: fetch_priority + }} + + {% unless preview_image_only %} + {%- case media.media_type -%} + {% when 'model' %} + + + + + + + + {%- if first_3d_model -%} + + {%- endif -%} + {% when 'video', 'external_video' %} + {%- render 'video', + video: media, + video_loop: block.settings.video_loop, + widths: widths, + sizes: sizes, + loading: loading, + disable_controls: true, + section_id: section.id + -%} + {% endcase %} + {% endunless %} +
    + +{% stylesheet %} + .product-media { + aspect-ratio: var(--gallery-aspect-ratio, var(--ratio)); + min-height: 0; + min-width: 0; + } + + /*** Media border-radius feature ****/ + @media screen and (min-width: 750px) { + .media-gallery--carousel slideshow-container, + .media-gallery--grid .product-media > * { + border-radius: var(--media-radius, 0); + overflow: hidden; + } + + /* When the CAROUSEL is on the LEFT side */ + .product-information:not(.product-information--media-right) + .media-gallery--carousel.media-gallery--extend + slideshow-container { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + /* When the CAROUSEL is on the RIGHT side */ + .product-information.product-information--media-right + .media-gallery--carousel.media-gallery--extend + slideshow-container { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + /* When the GRID is on the LEFT side */ + .product-information:not(.product-information--media-right) { + /* One column */ + .media-gallery--grid.media-gallery--extend:not(.media-gallery--two-column) .product-media > *, + /* Two column, small first image */ + .media-gallery--grid.media-gallery--extend.media-gallery--two-column:not(.media-gallery--large-first-image) + .product-media-container:nth-of-type(odd) + .product-media + > *, + /* Two column, large first image */ + .media-gallery--grid.media-gallery--extend.media-gallery--two-column.media-gallery--large-first-image + .product-media-container:is(:first-of-type, :nth-of-type(even)) + .product-media + > * { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + + /* When the GRID is on the RIGHT side */ + .product-information.product-information--media-right { + /* One column */ + .media-gallery--grid.media-gallery--extend:not(.media-gallery--two-column) .product-media > *, + /* Two column, small first image */ + .media-gallery--grid.media-gallery--extend.media-gallery--two-column:not(.media-gallery--large-first-image) + .product-media-container:nth-of-type(even) + .product-media + > *, + /* Two column, large first image */ + .media-gallery--grid.media-gallery--extend.media-gallery--two-column.media-gallery--large-first-image + .product-media-container:is(:first-of-type, :nth-of-type(odd)) + .product-media + > * { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + } + } + + ::view-transition-old(gallery-item), + ::view-transition-new(gallery-item) { + animation-duration: 0ms; + } +{% endstylesheet %} diff --git a/snippets/quantity-selector.liquid b/snippets/quantity-selector.liquid new file mode 100644 index 000000000..b42c87a9d --- /dev/null +++ b/snippets/quantity-selector.liquid @@ -0,0 +1,74 @@ +{%- doc -%} + This snippet is used to render the quantity selector for a product. + It is used in the product page and the cart page. + + @param {object} product - the product to render the quantity selector for + @param {number} [in_cart_quantity] - the quantity in the cart to set the input value + @param {number} [line_index] - the index of the forloop representing the line on which the quantity selector is rendered + @param {number} [min] - the minimum quantity the input supports + @param {string} [class] - custom class for the quantity selector, optional + @param {boolean} [can_update_quantity] - whether the quantity can be updated, defaults to true +{%- enddoc -%} + +{% liquid + assign variant = product.selected_or_first_available_variant +%} + + + + + + diff --git a/snippets/quick-add-modal.liquid b/snippets/quick-add-modal.liquid new file mode 100644 index 000000000..cf231d849 --- /dev/null +++ b/snippets/quick-add-modal.liquid @@ -0,0 +1,442 @@ + + + +
    +
    +
    + +{% stylesheet %} + .quick-add-modal { + padding: 0; + border: var(--style-border-popover); + height: fit-content; + overflow: hidden; + min-height: 500px; + box-shadow: 0 5px 30px rgb(0 0 0 / var(--opacity-15)); + + @media screen and (max-width: 750px) { + position: fixed; + display: block; + margin: auto 0 0 0; + min-height: unset; + max-width: 100%; + border-radius: 0; + } + } + + .quick-add-modal[open] { + @media screen and (min-width: 750px) { + display: flex; + } + } + + .quick-add-modal .view-more-details__wrapper { + @media screen and (max-width: 750px) { + display: none; + } + } + + .quick-add-modal[open] { + animation: modalSlideInTop var(--animation-speed) var(--animation-easing) forwards; + } + + .quick-add-modal.dialog-closing { + animation: modalSlideOutTop var(--animation-speed) var(--animation-easing) forwards; + } + + .quick-add-modal__close { + position: absolute; + top: var(--margin-2xs); + right: var(--margin-2xs); + width: var(--minimum-touch-target); + height: var(--minimum-touch-target); + color: var(--color-foreground); + background-color: var(--color-background); + display: flex; + align-items: center; + justify-content: center; + padding: 0; + z-index: 2; + transition: transform 0.15s var(--animation-timing-bounce), opacity 0.15s var(--animation-easing); + } + + .quick-add-modal__close:active { + transform: scale(0.8); + } + + .quick-add-modal__close svg { + width: var(--icon-size-xs); + height: var(--icon-size-xs); + } + + .quick-add-modal__content { + display: grid; + grid-template-columns: repeat(7, 1fr); + grid-template-rows: 100% 1fr; + position: relative; + max-width: var(--wide-content-width); + overflow-y: auto; + max-height: 100vh; + + @media screen and (max-width: 750px) { + grid-template-columns: repeat(4, 1fr); + grid-template-rows: auto; + padding-inline: var(--padding-xl); + padding-block: var(--padding-xl); + gap: var(--gap-lg); + flex: 1; + min-height: 0; + overflow-y: auto; + height: auto; /* Prevent a bug in Safari where height:fit-content is not respected */ + max-height: 100vh; + } + } + + .quick-add-modal__content .media-gallery--grid .media-gallery__grid { + grid-template-columns: 1fr; + } + + .quick-add-modal__content .media-gallery--grid.media-gallery--two-column .product-media-container:first-child { + grid-column: auto; + } + + .quick-add-modal__content { + /* One column */ + .media-gallery--grid:not(.media-gallery--two-column) .product-media > *, + /* Two column, small first image */ + .media-gallery--grid.media-gallery--two-column:not(.media-gallery--large-first-image) + .product-media-container:nth-of-type(odd) + .product-media > *, + /* Two column, large first image */ + .media-gallery--grid.media-gallery--two-column.media-gallery--large-first-image + .product-media-container:is(:first-of-type, :nth-of-type(even)) + .product-media > *, + /* Carousel */ + .media-gallery--carousel slideshow-container { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + + .quick-add-modal__content .view-more-details__wrapper { + display: flex; + justify-content: flex-start; + width: 100%; + } + + .view-more-details__wrapper .view-more-details { + display: flex; + align-items: center; + width: fit-content; + } + + .quick-add-modal__content .product-header { + @media screen and (max-width: 750px) { + display: flex; + flex-direction: column; + grid-column: 2 / -1; + grid-row: 1; + padding-right: var(--padding-2xl); + } + } + + .quick-add-modal__content .product-header a:not(product-price *) { + @media screen and (max-width: 749px) { + font-size: var(--font-size--md); + font-weight: 500; + color: inherit; + width: fit-content; + } + } + + .quick-add-modal__content variant-picker, + .quick-add-modal__content product-form-component { + @media screen and (max-width: 750px) { + grid-column: 1 / -1; + } + } + + .quick-add-modal__content .variant-picker__form { + display: block; + } + + .quick-add-modal__content fieldset { + margin-top: var(--padding-lg); + } + + .quick-add-modal__content .product-media-container__zoom-button { + cursor: default; + } + + .quick-add-modal__content .product-details { + grid-column: 4 / -1; + grid-row: 1 / span 2; + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + + dialog[open] & { + animation: fadeSlideIn 0.3s var(--animation-timing-fade-in) both; + animation-delay: 0.1s; + } + + @media screen and (max-width: 750px) { + grid-column: 2 / span 2; + grid-row: span 1; + overflow-y: auto; + max-height: 100%; + height: 100%; + } + } + + .quick-add-modal__content > * { + min-height: 0; + } + + .quick-add-modal__content .product-details :is(.view-product-title, .buy-buttons-block) { + flex: 0 0 auto; + } + + .quick-add-modal__content .product-details .variant-picker { + --product-swatches-padding-block-end: 0px; + flex: 1 1 auto; + overflow-y: auto; + min-height: 0; + padding-block-end: calc( + var(--product-swatches-padding-block-end) + var(--focus-outline-offset) + var(--focus-outline-width) + ); + } + + .quick-add-modal__content .variant-option--swatches { + padding-inline-start: var(--padding-2xs); + } + + .quick-add-modal__content .variant-option--swatches legend { + margin-inline-start: calc(-1 * var(--padding-2xs)); + } + + .quick-add-modal__content:not(:has(.product-information__media)) .product-details { + grid-column: 1 / -1; + } + + .quick-add-modal__content .view-product-title { + padding: 0; + } + + .quick-add-modal__content .view-product-title a { + color: inherit; + text-decoration: none; + text-align: left; + font-size: var(--font-size--2xl); + font-weight: 600; + line-height: 1.2; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.2s var(--animation-easing); + } + + .quick-add-modal__content .view-product-title { + display: flex; + } + + .quick-add-modal__content + .product-details + *:not( + .group-block, + .group-block-content, + .buy-buttons-block, + .buy-buttons-block *, + .view-product-title, + .view-product-title *, + variant-picker, + variant-picker *, + product-price, + product-price *, + product-inventory, + product-inventory *, + .view-more-details__wrapper, + .view-more-details__wrapper * + ) { + @media screen and (min-width: 750px) { + display: none !important; + } + } + + .quick-add-modal__content + .group-block:not( + :has( + .buy-buttons-block, + .buy-buttons-block *, + .view-product-title, + .view-product-title *, + variant-picker, + variant-picker *, + product-price, + product-price *, + product-inventory, + product-inventory *, + .view-more-details__wrapper, + .view-more-details__wrapper * + ), + .buy-buttons-block + ) { + display: none; + } + + @media screen and (min-width: 750px) { + .quick-add-modal__content .group-block-content { + gap: min(var(--gap-2xl), var(--gap)); + } + + .quick-add-modal__content .media-gallery__grid { + gap: min(var(--gap-2xs), var(--image-gap)); + border-radius: var(--style-border-radius-popover, 0); + } + + .quick-add-modal__content .media-gallery--grid .product-media img { + border-radius: 0; + } + + .quick-add-modal__content .media-gallery--grid .product-media-container:first-child { + border-top-right-radius: var(--style-border-radius-popover, 0); + } + + .quick-add-modal__content .media-gallery--grid .product-media-container:last-child { + border-bottom-right-radius: var(--style-border-radius-popover, 0); + } + } + + .quick-add-modal__content .product-details > .group-block { + padding: var(--padding-2xl); + max-height: 100%; + } + + .quick-add-modal__content slideshow-slide:not([aria-hidden='false']) { + content-visibility: auto; + } + + .quick-add-modal__content .product-information__media { + width: 100%; + grid-column: 1 / span 1; + grid-row: 1; + position: relative; + top: 0; + animation: fadeIn 0.4s var(--animation-timing-fade-in) both; + + @media screen and (min-width: 750px) { + position: sticky; + grid-column: 1 / 4; + overflow-y: auto; + -ms-overflow-style: none; + scrollbar-width: none; + } + + &::-webkit-scrollbar { + display: none; + } + } + + .quick-add-modal__content .product-information__media media-gallery { + pointer-events: none; + + @media screen and (min-width: 750px) { + position: absolute; + inset: 0; + } + } + + .quick-add-modal media-gallery { + padding: 0; + } + + .quick-add-modal__content .product-information__media slideshow-arrows { + display: none; + } + + .quick-add-modal__content .product-information__media slideshow-container { + display: block; + } + + .quick-add-modal__content .product-information__media slideshow-slides { + display: flex; + flex-direction: column; + gap: var(--gap-2xs); + overflow: visible; + scroll-snap-type: none; + } + + .quick-add-modal__content .product-information__media slideshow-slide { + width: 100%; + flex: none; + scroll-snap-align: unset; + position: relative; + transform: none; + opacity: 1; + visibility: visible; + transition: opacity 0.3s var(--animation-easing); + } + + .quick-add-modal__content .product-information__media slideshow-slide[aria-hidden='true'] { + @media screen and (max-width: 750px) { + display: none; + } + } + + .quick-add-modal__content .product-information__media slideshow-slide:nth-child(1) { + animation: fadeSlideIn 0.3s var(--animation-timing-fade-in) both; + } + + .quick-add-modal__content .product-information__media slideshow-slide:nth-child(2) { + animation: fadeSlideIn 0.3s var(--animation-timing-fade-in) both; + animation-delay: 0.05s; + } + + .quick-add-modal__content .product-information__media slideshow-slide:nth-child(3) { + animation: fadeSlideIn 0.3s var(--animation-timing-fade-in) both; + animation-delay: 0.1s; + } + + .quick-add-modal__content .product-information__media slideshow-controls { + display: none; + } + + .quick-add-modal__content .sticky-content, + .quick-add-modal__content .sticky-content--desktop { + top: 0; + } + + .quick-add-modal__content .text-block.rte:not(product-price), + .quick-add-modal__content .view-more-details__wrapper { + display: none; + } + + @keyframes fadeSlideIn { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } + } +{% endstylesheet %} diff --git a/snippets/quick-add.liquid b/snippets/quick-add.liquid new file mode 100644 index 000000000..dddff3652 --- /dev/null +++ b/snippets/quick-add.liquid @@ -0,0 +1,324 @@ +{%- doc -%} + Renders a quick add component. + + @param {object} product - The product object + @param {string} section_id - The section ID + @param {object} [block] - The block object +{%- enddoc -%} + +{% liquid + assign product_form_id = 'QuickAdd-ProductForm-' | append: product.id | append: '-' | append: block.id + assign add_to_cart_text = 'actions.add' | t + + # Logic to determine which variant to use (matching swatch selection logic from product-card) + assign variant_to_use = product.selected_or_first_available_variant + assign combined_listing_count = product.options_with_values | map: 'values' | map: 'product_url' | compact | size + + unless combined_listing_count > 0 + assign first_image = product.media.first + assign variant_images = product.images | where: 'attached_to_variant?', true + # Get swatchable options (options that have swatch values) + assign swatch_variant_picker = null + for option in product.options_with_values + assign swatch_count = option.values | map: 'swatch' | compact | size + if swatch_count > 0 + assign swatch_variant_picker = option + break + endif + endfor + + if swatch_variant_picker + assign swatch_count = swatch_variant_picker.values | map: 'swatch' | compact | size + + if swatch_count == 1 + # Single swatch: use that variant + assign variant_to_use = swatch_variant_picker.values.first.variant + elsif swatch_count > 1 + if first_image and variant_images contains first_image + # First image is a variant image - find which variant it belongs to + for option_value in swatch_variant_picker.values + if option_value.variant.featured_media.id == first_image.id + assign variant_to_use = option_value.variant + break + endif + endfor + elsif variant_images.size == 0 + # No variants have images - use first swatch variant + assign variant_to_use = swatch_variant_picker.values.first.variant + endif + # else: First image is NOT a variant image - keep default (selected_or_first_available_variant) + endif + endif + endunless + + if variant_to_use.available + assign can_add_to_cart = true + else + assign can_add_to_cart = false + endif +%} + + + +
    + {%- form 'product', product, id: product_form_id, novalidate: 'novalidate', data-type: 'add-to-cart-form' -%} + + + {% comment %} If there is one variant option but it's swatches or if it's a single variant product, then use add to cart button {% endcomment %} + {%- if product.variants.size == 1 or product.options.size == 1 -%} + {% render 'add-to-cart-button', + add_to_cart_text: add_to_cart_text, + class: 'button quick-add__button quick-add__button--add', + can_add_to_cart: can_add_to_cart, + icon_only_on_mobile: true, + product: product + %} + {%- endif -%} + {%- if product.variants.size > 1 -%} + + {%- endif -%} + {%- endform -%} +
    +
    + +{% stylesheet %} + /* Quick Add */ + .quick-add { + --quick-add-offset: var(--padding-sm); + --quick-add-top: calc(var(--quick-add-offset) + var(--padding-block-start)); + --quick-add-right: calc(var(--quick-add-offset) + var(--padding-inline-end)); + --quick-add-bottom: calc(var(--quick-add-offset) + var(--padding-block-end)); + --quick-add-left: calc(var(--quick-add-offset) + var(--padding-inline-end)); + + position: absolute; + display: var(--quick-add-mobile-display, none); + flex-direction: column; + justify-content: flex-end; + inset: max(var(--quick-add-top), calc((var(--border-radius) + var(--quick-add-top)) * (1 - cos(45deg)))) + max(var(--quick-add-right), calc((var(--border-radius) + var(--quick-add-right)) * (1 - cos(45deg)))) + max(var(--quick-add-bottom), calc((var(--border-radius) + var(--quick-add-bottom)) * (1 - cos(45deg)))) + max(var(--quick-add-left), calc((var(--border-radius) + var(--quick-add-left)) * (1 - cos(45deg)))); + width: auto; + height: auto; + z-index: var(--layer-raised); + cursor: default; + pointer-events: none; + + @media screen and (min-width: 750px) { + --quick-add-offset: var(--padding-md); + + display: var(--quick-add-display, flex); + } + } + + .quick-add .variant-option__button-label input[data-option-available='false'] { + cursor: not-allowed; + } + + .quick-add[class*='color-scheme-'] { + background-color: transparent; + } + + .quick-add__button { + display: grid; + padding: var(--padding-xs); + align-items: center; + background-color: var(--color-background); + color: var(--color-foreground); + border-color: transparent; + box-shadow: var(--shadow-popover); + pointer-events: all; + position: relative; + overflow: hidden; + border-radius: 100px; + + @media screen and (min-width: 750px) { + display: none; + padding: var(--padding-xs) var(--padding-sm); + } + + .quick-add[stay-visible] & { + display: grid; + } + } + + .quick-add__button .add-to-cart-text { + gap: 0; + line-height: 1; + grid-row: 1 / span 1; + grid-column: 1 / span 1; + animation: none; + + @media screen and (min-width: 750px) { + /* offset button padding to show a round button in a collapsed state */ + margin-inline: calc(var(--padding-sm) * -1); + padding-inline: var(--padding-xs); + } + } + + .quick-add__button .add-to-cart-text--added { + position: relative; + grid-row: 1 / span 1; + grid-column: 1 / span 1; + justify-self: end; + line-height: 1; + + @media screen and (min-width: 750px) { + width: 0; + } + } + + .quick-add__button .add-to-cart-text__content { + width: 0; + opacity: 0; + transform: translateX(1em); + transition: width var(--animation-speed) ease-in-out, opacity var(--animation-speed) ease-in-out, + transform var(--animation-speed) ease-in-out; + interpolate-size: allow-keywords; + will-change: width, opacity, transform; + } + + @container (min-width: 99px) { + .quick-add[stay-visible] .add-to-cart-text, + .quick-add__button:is(:focus, :hover) .add-to-cart-text { + gap: var(--gap-2xs); + + @media screen and (min-width: 750px) { + /* offset button padding to show a round button in a collapsed state */ + margin-inline: 0; + padding-inline: 0; + } + } + + .quick-add[stay-visible] .add-to-cart-text__content, + .quick-add__button:is(:focus, :hover) .add-to-cart-text__content { + width: fit-content; + opacity: 1; + transform: translateX(0); + } + } + + .quick-add__button.atc-added .add-to-cart-text { + opacity: 0; + } + + .quick-add__button.atc-added .add-to-cart-text--added { + opacity: 1; + width: auto; + + @supports (width: calc-size(auto, size)) { + width: calc-size(auto, size); + } + } + + .quick-add__button.atc-added .add-to-cart-text { + animation-name: atc-fade-out; + } + + .quick-add__button.atc-added .add-to-cart-text--added { + animation-name: atc-fade-in; + } + + .quick-add__product-form-component { + height: 100%; + } + + .quick-add__product-form-component .shopify-product-form { + display: flex; + justify-content: flex-end; + align-items: flex-end; + container-type: inline-size; + height: 100%; + } + + .quick-add-modal .product-media { + width: 100%; + height: 100%; + } + + .quick-add-modal deferred-media { + display: none; + } + + .quick-add-modal .media-gallery--carousel slideshow-component { + --cursor: default; + } + + @keyframes atc-fade-in { + from { + opacity: 0; + transform: translateX(1em); + position: absolute; + } + + to { + opacity: 1; + transform: translateX(0); + position: inherit; + } + } + + @keyframes atc-fade-out { + from { + opacity: 1; + transform: translateX(0); + position: inherit; + } + + to { + opacity: 0; + transform: translateX(-1em); + position: absolute; + } + } +{% endstylesheet %} diff --git a/snippets/resource-card.liquid b/snippets/resource-card.liquid new file mode 100644 index 000000000..13e1bd9a7 --- /dev/null +++ b/snippets/resource-card.liquid @@ -0,0 +1,275 @@ +{%- doc -%} + Renders a card for displaying various resource types (products, collections, articles, pages). + + @param {object} resource - The product or collection resource to render + @param {string} resource_type - The type of resource to render. + @param {string} [collection_thumbnails] - The style of the collection card. Can be 'single' or 'multiple'. Defaults to 'single' + @param {string} [style] - The style of the card. Can be 'default' or 'overlay' + @param {number} [image_width] - The maximum width of the image, value influences the srcset attribute. Defaults to 1200px. + @param {string} [image_aspect_ratio] - The aspect ratio to display the image. Defaults to image's natural ratio + @param {boolean} [image_hover] - Whether to show a secondary image on hover and focus + @param {string} [image_sizes] - The sizes attribute for responsive images. Defaults to 'auto' +{%- enddoc -%} + +{% liquid + if image_aspect_ratio == blank or image_aspect_ratio == 'adapt' + assign ratio = resource.featured_image.aspect_ratio + else + assign ratio = image_aspect_ratio + endif + assign image_width = image_width | default: 1200 + assign widths = '240, 352, 832, 1200' + assign image_sizes = image_sizes | default: 'auto' + assign single_thumbnail_collection = false + if resource_type == 'collection' and collection_thumbnails != 'multiple' + assign single_thumbnail_collection = true + endif + + if resource_type == 'product' and settings.transition_to_main_product + assign featured_media_url = resource.selected_or_first_available_variant.featured_image | image_url: width: image_width + if featured_media_url == blank + assign featured_media_url = resource.featured_media.preview_image | image_url: width: image_width + endif + endif +%} + +{%- if resource_type == 'product' and settings.transition_to_main_product -%} + +{%- endif -%} +
    + + + {{ resource.title }} + + +
    + {%- if resource_type == 'product' or single_thumbnail_collection -%} + {% assign featured_image = resource.featured_image | default: resource.featured_media.preview_image %} + + {%- if featured_image != blank -%} + {{ + featured_image + | image_url: width: image_width + | image_tag: + loading: 'lazy', + class: 'resource-card__image', + widths: widths, + sizes: image_sizes, + transitionToProduct: settings.transition_to_main_product, + data-media-id: featured_image.id + }} + {%- if image_hover and resource.media.size > 1 -%} + {{ + resource.media[1] + | image_url: width: image_width + | image_tag: + loading: 'lazy', + class: 'resource-card__image resource-card__image--secondary', + widths: widths, + sizes: image_sizes, + transitionToProduct: settings.transition_to_main_product, + data-media-id: resource.media[1].id + }} + {%- endif -%} + {%- else -%} + + {%- endif -%} + {%- elsif resource_type == 'collection' -%} + {%- if resource.products.size > 0 -%} +
    + {% assign resource_products = resource.products | where: 'featured_image' %} + {% for product in resource_products limit: 4 %} + {{ + product.featured_image + | image_url: width: image_width + | image_tag: loading: 'lazy', class: 'resource-card__collection-image', sizes: image_sizes, widths: widths + }} + {% endfor %} +
    + {%- endif -%} + {%- endif -%} +
    + +
    +

    + {{- resource.title -}} +

    + + {% if resource_type == 'product' %} + {% render 'price', product_resource: resource, show_unit_price: true %} + {% elsif resource_type == 'collection' and single_thumbnail_collection == false %} +

    + {{- 'content.search_results_resource_products_count' | t: count: resource.all_products_count -}} +

    + {% else %} +

    + {{- resource.excerpt | default: resource.content | strip_html | truncate: 65 -}} +

    + {% endif %} +
    +
    +{%- if resource_type == 'product' and settings.transition_to_main_product -%} +
    +{%- endif -%} + +{% stylesheet %} + .resource-card { + --resource-card-secondary-image-opacity: 0; + --resource-card-primary-image-opacity: calc(1 - var(--resource-card-secondary-image-opacity)); + + display: flex; + flex-direction: column; + row-gap: var(--padding-xs); + position: relative; + text-decoration: none; + height: 100%; + opacity: 0; + animation: fadeIn var(--animation-speed-medium) var(--animation-timing-fade-in) forwards; + } + + .resource-card__link { + position: absolute; + inset: 0; + z-index: 1; + } + + .resource-card__content { + display: flex; + flex-direction: column; + color: var(--color-foreground); + gap: var(--padding-3xs); + + .price { + font-weight: 500; + } + } + + .resource-card[data-resource-type='article'] .resource-card__content, + .resource-card[data-resource-type='page'] .resource-card__content { + gap: var(--padding-xs); + } + + .resource-card__image { + aspect-ratio: var(--resource-card-aspect-ratio, auto); + object-fit: cover; + border-radius: var(--resource-card-corner-radius); + opacity: var(--resource-card-primary-image-opacity); + } + + .resource-card__image--secondary { + position: absolute; + top: 0; + opacity: var(--resource-card-secondary-image-opacity); + border-radius: var(--resource-card-corner-radius); + } + + .resource-card__media:empty { + display: none; + } + + .resource-card__image-placeholder { + padding: var(--padding-sm); + font-size: var(--font-size--lg); + line-height: var(--line-height--display-loose); + word-break: break-word; + background-color: rgb(var(--color-foreground-rgb) / var(--opacity-5)); + aspect-ratio: var(--resource-card-aspect-ratio, auto); + border-radius: var(--resource-card-corner-radius); + color: var(--color-foreground); + } + + .resource-card__title { + margin-block: 0; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + line-height: 1.3; + } + + .resource-card__title.paragraph { + line-height: 1.3; + } + + .resource-card--overlay { + height: 100%; + + &::before { + content: ''; + position: absolute; + inset: 50% 0 0; + background: var(--gradient-image-overlay); + border-radius: var(--resource-card-corner-radius); + pointer-events: none; + z-index: var(--layer-flat); + } + } + + .resource-card--overlay .resource-card__image { + height: 100%; + } + + .resource-card--overlay .resource-card__content { + position: absolute; + inset: auto 0 0; + padding: var(--padding-lg) var(--padding-lg) var(--padding-sm); + z-index: var(--layer-raised); + } + + .resource-card--overlay .resource-card__title { + color: var(--color-white); + } + + /* Collection images */ + .resource-card__image-wrapper { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--gap-2xs); + } + + .resource-card__collection-image { + aspect-ratio: 1 / 1; + object-fit: cover; + border-radius: calc(var(--card-corner-radius) - (var(--padding-xs) / 2)); + } + + .resource-card__subtext { + color: rgb(var(--color-foreground-rgb) / var(--opacity-subdued-text)); + margin-block-start: 0; + } + + .resource-card__subtext.paragraph { + font-size: var(--font-size--body-sm); + line-height: var(--line-height--body-tight); + color: rgb(var(--color-foreground-rgb) / var(--opacity-subdued-text)); + } + + .resource-card:has(.resource-card__image--secondary) { + &:hover, + &:focus { + --resource-card-secondary-image-opacity: 1; + } + } +{% endstylesheet %} diff --git a/snippets/resource-list-carousel.liquid b/snippets/resource-list-carousel.liquid new file mode 100644 index 000000000..547d8efd5 --- /dev/null +++ b/snippets/resource-list-carousel.liquid @@ -0,0 +1,59 @@ +{%- doc -%} + Renders a carousel of predictive search results cards + + @param {string} ref - The ref of the slideshow + @param {object} slides - An array of HTML for the slides to display in the carousel + @param {number} slide_count - The number of slides to display in the carousel + @param {object} settings - The block or sections settings from the parent block/section. + @param {string} [slide_width_max] - The maximum width of the slides in the carousel. + + @example + {% render 'resource-list-carousel', slides: slides, slide_count: slide_count, settings: block.settings %} +{%- enddoc -%} + +{% liquid + assign slideshow_ref = ref | default: 'resourceListCarousel' + if settings.section_width == 'page-width' + assign slideshow_gutters = 'start end' + else + assign slideshow_gutters = null + endif +%} + +{% capture slides %} + {% for item in slides limit: slides.size %} + {% render 'slideshow-slide' + index : forloop.index0, + children : item, + class : 'resource-list__slide' + %} + {% endfor %} +{% endcapture %} + +{% capture slideshow_arrows %} + {% render 'slideshow-arrows', icon_style: settings.icons_style, icon_shape: settings.icons_shape %} +{% endcapture %} + + diff --git a/snippets/scripts.liquid b/snippets/scripts.liquid new file mode 100644 index 000000000..82a88a92b --- /dev/null +++ b/snippets/scripts.liquid @@ -0,0 +1,258 @@ + + +{% if settings.transition_to_main_product %} + {% # theme-check-disable ParserBlockingScript %} + + {% # theme-check-enable %} +{% endif %} + + + + + + + + +{% if template.name == 'collection' or template.name == 'search' %} + + + + +{% endif %} + + + + + + +{% if settings.show_add_discount_code %} + +{% endif %} + + + + + + + + + + + + + + + + + + +{% if localization.available_countries.size > 1 or localization.available_languages.size > 1 %} + +{% endif %} + +{% if template == 'product' %} + +{% endif %} + +{% if settings.transition_to_main_product %} + +{% endif %} + + + + diff --git a/snippets/search-modal.liquid b/snippets/search-modal.liquid new file mode 100644 index 000000000..d0b66c884 --- /dev/null +++ b/snippets/search-modal.liquid @@ -0,0 +1,108 @@ + + + + + {% render 'predictive-search', + input_id: 'cmdk-input', + search_test_id: 'search-component--modal', + products_test_id: 'products-list-default--modal' + %} + + + +{% stylesheet %} + /* Search modal style */ + .search-modal { + --search-border-radius: var(--style-border-radius-popover); + --search-border-width: var(--style-border-width); + } + + .search-modal__button { + display: flex; + align-items: center; + justify-content: center; + } + + .search-modal__content { + /* Approx set the top so when the content is at max height, the modal is centered */ + --modal-top-margin: calc(50dvh - var(--modal-max-height) / 2 - 2rem); + --modal-width: 66dvw; + + padding: 0; + + @media screen and (min-width: 750px) { + width: var(--modal-width); + margin-block-start: var(--modal-top-margin); + overflow: hidden; + } + } + + /* Hide the default dialog backdrop on small screens */ + @media screen and (max-width: 749px) { + .search-modal__content::backdrop { + display: none; + } + } + + .dialog-modal[open].search-modal__content { + transform-origin: bottom center; + animation: search-element-slide-in-bottom 300ms var(--ease-out-quad) forwards; + border-radius: var(--search-border-radius); + box-shadow: var(--shadow-popover); + + @media screen and (max-width: 749px) { + border-radius: 0; + } + } + + .dialog-modal.search-modal__content.dialog-closing { + animation: search-element-slide-out-bottom 200ms var(--ease-out-quad) forwards; + } + + .search-modal__content[open] { + display: flex; + } + + .search-modal__content :is(.predictive-search-dropdown, .predictive-search-form__content-wrapper) { + position: relative; + } + + /* Predictive search header tweaks for small screens */ + @media screen and (max-width: 749px) { + .dialog-modal + .predictive-search-form__header:has( + .predictive-search__reset-button:not(.predictive-search__reset-button[hidden]) + )::before { + content: ''; + position: absolute; + right: calc(var(--padding-sm) + var(--minimum-touch-target)); + top: 0; + bottom: 0; + width: var(--border-width-sm); + background-color: var(--color-border); + } + + .dialog-modal + .predictive-search-form__header:has( + .predictive-search__reset-button:not(.predictive-search__reset-button[hidden]) + ) + > .predictive-search__close-modal-button { + &::before { + content: none; + } + } + } +{% endstylesheet %} diff --git a/snippets/search.liquid b/snippets/search.liquid new file mode 100644 index 000000000..271471c15 --- /dev/null +++ b/snippets/search.liquid @@ -0,0 +1,52 @@ +{%- doc -%} + Renders the search action button. + + @param {string} [style] - The style of the search action. + @param {string} [class] - Additional classes for the search action. + + @example + {% render 'search', style: 'default', class: 'custom-class' %} +{%- enddoc -%} + +{% unless style == 'none' %} + + + +{% endunless %} + +{% stylesheet %} + .search-action { + --search-border-radius: var(--style-border-radius-inputs); + --search-border-width: var(--style-border-width-inputs); + + display: flex; + } + + .header__column--center .search-action { + width: auto; + flex-grow: 1; + } + + :is(.header__column--left, .header__column--center) .search-action { + @media screen and (min-width: 750px) { + margin-inline: calc(var(--padding-lg) * -1); + } + } + + .header__column--right .search-action { + @media screen and (min-width: 750px) { + margin-inline: calc(var(--gap-md) * -1) calc(var(--gap-xs) * -1); + } + } +{% endstylesheet %} diff --git a/snippets/section.liquid b/snippets/section.liquid new file mode 100644 index 000000000..2f3e42cce --- /dev/null +++ b/snippets/section.liquid @@ -0,0 +1,96 @@ +{%- doc -%} + Renders a wrapper section + + @param {section} section - The section object + @param {string} children - The children of the section +{%- enddoc -%} + +
    +
    +
    + {% render 'background-media', + background_media: section.settings.background_media, + background_video: section.settings.video, + background_video_position: section.settings.video_position, + background_image: section.settings.background_image, + background_image_position: section.settings.background_image_position + %} +
    + +
    + {% if section.settings.toggle_overlay %} + {% render 'overlay', settings: section.settings, layer: '0' %} + {% endif %} + +
    + {{ children }} +
    +
    +
    + +{% stylesheet %} + .section-wrapper { + --section-height-offset: 0px; + } + + .section[data-shopify-visual-preview] { + min-height: var(--section-preview-height); + padding-top: 0; + } + + .section[data-shopify-visual-preview] .custom-section-background { + display: none; + } + + body:has(> #header-group > .header-section > #header-component[transparent]):not( + :has(> #header-group > .header-section + .shopify-section) + ) + > main + > .section-wrapper:first-child { + --section-height-offset: var(--header-group-height, 0); + } + + .custom-section-background { + grid-column: 1 / -1; + } + + .custom-section-content { + z-index: var(--layer-flat); + } +{% endstylesheet %} diff --git a/snippets/size-style.liquid b/snippets/size-style.liquid new file mode 100644 index 000000000..1b863451a --- /dev/null +++ b/snippets/size-style.liquid @@ -0,0 +1,33 @@ +{%- comment -%} + Intended for blocks and sections that provide values for all the referenced settings. + Accepts: + settings: {block.settings || section.settings} +{%- endcomment -%} + +{%- if settings.width == 'custom' -%} + --size-style-width: + {{- settings.custom_width }}%; +{%- elsif settings.width == 'fill' -%} + --size-style-width: 100%; +{%- else -%} + --size-style-width: {{ settings.width }}; +{%- endif -%} + +{%- if settings.height == 'custom' -%} + --size-style-height: + {{- settings.custom_height }}%; +{%- elsif settings.height == 'fill' -%} + --size-style-height: 100%; +{%- else -%} + --size-style-height: {{ settings.height }}; +{%- endif -%} + +{% if settings.width_mobile == 'custom' %} + --size-style-width-mobile: + {{- settings.custom_width_mobile }}%; --size-style-width-mobile-min: + {{- settings.custom_width_mobile }}%; +{%- elsif settings.width_mobile == 'fill' -%} + --size-style-width-mobile: 100%; --size-style-width-mobile-min: 5rem; +{%- elsif settings.width_mobile == 'fit-content' -%} + --size-style-width-mobile: {{ settings.width_mobile }}; --size-style-width-mobile-min: {{ settings.width_mobile }}; +{%- endif -%} diff --git a/snippets/skip-to-content-link.liquid b/snippets/skip-to-content-link.liquid new file mode 100644 index 000000000..15d8e34e5 --- /dev/null +++ b/snippets/skip-to-content-link.liquid @@ -0,0 +1,16 @@ +{%- doc -%} + Renders a skip to content link, visible on focus only. + Parent element must have position: relative to ensure proper positioning. + + @param {string} href - The URL to skip to, usually an id like "#MainContent". + @param {string} text - The text to display, in the form of a translation key for a locale file. + + @example + {% render 'skip-to-content-link', href: '#MainContent', text: 'Skip to main content' %} +{%- enddoc -%} + + {{ text | t }} + diff --git a/snippets/slideshow-arrow.liquid b/snippets/slideshow-arrow.liquid new file mode 100644 index 000000000..70c511a9e --- /dev/null +++ b/snippets/slideshow-arrow.liquid @@ -0,0 +1,49 @@ +{%- doc -%} + Renders a slideshow arrow control + + @param {string} action - { 'previous' | 'next' } The action to perform when the arrow is clicked. + @param {string} [icon_style] - { 'arrow' | 'chevron' } The style of the icon, defaults to 'arrow'. + @param {string} [icon_shape] - { 'none' | 'circle' | 'square' } The shape of the icon background, defaults to 'none'. + @param {string} [icon_size] - { 'small' | 'medium' | 'large' } The size of the icon, defaults to 'medium'. + + @example + {%- render 'slideshow-arrow', action: 'previous' -%} +{%- enddoc -%} + +{%- liquid + assign icon_name = 'arrow' + assign style = icon_style | default: 'arrow' + assign shape = icon_shape | default: 'none' + + if icon_style contains 'chevron' + assign icon_name = 'caret' + endif + + if icon_style contains 'large' + assign icon_size = 'large' + endif +-%} + + diff --git a/snippets/slideshow-arrows.liquid b/snippets/slideshow-arrows.liquid new file mode 100644 index 000000000..5f32be972 --- /dev/null +++ b/snippets/slideshow-arrows.liquid @@ -0,0 +1,29 @@ +{%- doc -%} + Renders arrow controls for a slideshow component. + Assumes arrows are placed on top of media. + When icon shape is 'none', component uses mixed-blend-mode to ensure visibility. + + @param {string} [class] - The class name to apply to the slideshow-arrows component + @param {string} [icon_style] - The style of the icon, defaults to 'arrow' + @param {string} [icon_shape] - The shape of the icon background (none, circle, square), defaults to 'none' + @param {string} [arrows_position] - { 'left' | 'center' | 'right' } The position of the arrows, defaults to 'center' + + @example + {%- render 'slideshow-arrows' -%} +{%- enddoc -%} + +{%- liquid + if arrows_position == null + assign arrows_position = 'center' + endif +-%} + + + {%- render 'slideshow-arrow', action: 'previous', icon_style: icon_style, icon_shape: icon_shape -%} + {%- render 'slideshow-arrow', action: 'next', icon_style: icon_style, icon_shape: icon_shape -%} + diff --git a/snippets/slideshow-controls.liquid b/snippets/slideshow-controls.liquid new file mode 100644 index 000000000..2e3509acf --- /dev/null +++ b/snippets/slideshow-controls.liquid @@ -0,0 +1,180 @@ +{%- doc -%} + Renders controls for a slider component + + @param {string} [style] - { 'dots' | 'counter' | 'thumbnails' | 'none' } The display style of the controls + @param {boolean} [autoplay] - Whether the controls will display an autoplay option + @param {number} [item_count] - The total number of slides + @param {boolean} [show_arrows] - Whether the controls will display arrows + @param {boolean} [arrows_on_media] - Whether the controls will display as floating icons on the media + @param {boolean} [controls_on_media] - Whether the controls will display as floating controls on the media + @param {media[]} [thumbnails] - Array of media to be displayed as thumbnails, sorted. + @param {string} [pagination_position] - { 'left' | 'center' | 'right' } Sets the pagination position, defaults to 'center' if none passed + @param {string} [icon_style] - The style of the icon, defaults to 'arrow' + @param {string} [shape] - The shape of the control, defaults to 'square' + @param {string} [aspect_ratio] - The aspect ratio of thumbnails, if applicable. defaults to 'adapt' + @param {string} [class] - Additional classes to apply to the controls + @param {boolean} [secondary] - Whether the controls are secondary + + @example + {%- render 'slideshow-controls', style: 'dots', item_count: 10 -%} +{%- enddoc -%} + +{%- liquid + if aspect_ratio == null + assign aspect_ratio = 'adapt' + endif + + if pagination_position == null + assign pagination_position = 'center' + endif + + assign show_arrows_separately = false + if style == 'thumbnails' and arrows_on_media == false + assign show_arrows_separately = true + # Specific case - we want to show the arrows with the thumbnails if everything is off media and centered. + if controls_on_media == false and pagination_position == 'center' + assign show_arrows_separately = false + endif + elsif controls_on_media == true and arrows_on_media == false + assign show_arrows_separately = true + endif + + if pagination_position == 'left' or pagination_position == 'right' + assign scroll_mode = 'vertical' + else + assign scroll_mode = 'horizontal' + endif +-%} + + + {% if show_arrows_separately == false and show_arrows and pagination_position != 'left' %} +
    + {%- render 'slideshow-arrow', action: 'previous', icon_style: icon_style, icon_shape: shape -%} + + {% if pagination_position == 'right' %} + {%- render 'slideshow-arrow', action: 'next', icon_style: icon_style, icon_shape: shape -%} + {% endif %} +
    + {% endif %} + + {% if autoplay %} + + + {% endif %} + + {% case style %} + {% when 'thumbnails' %} + +
    + {% for media in thumbnails %} + + {% endfor %} +
    +
    + {% when 'counter' %} +
    + 1/{{ item_count -}} +
    + {% when 'dots' %} +
      + {% for i in (1..item_count) %} +
    1. + +
    2. + {% endfor %} +
    + {% endcase %} + + {% if show_arrows_separately == false and show_arrows and pagination_position != 'right' %} +
    + {% if pagination_position == 'left' %} + {%- render 'slideshow-arrow', action: 'previous', icon_style: icon_style, icon_shape: shape -%} + {% endif %} + + {%- render 'slideshow-arrow', action: 'next', icon_style: icon_style, icon_shape: shape -%} +
    + {% endif %} +
    + +{% if show_arrows_separately and show_arrows %} +
    + {%- render 'slideshow-arrow', action: 'previous', icon_style: icon_style, icon_shape: shape -%} + {%- render 'slideshow-arrow', action: 'next', icon_style: icon_style, icon_shape: shape -%} +
    +{% endif %} diff --git a/snippets/slideshow-slide.liquid b/snippets/slideshow-slide.liquid new file mode 100644 index 000000000..c6f7f1f7a --- /dev/null +++ b/snippets/slideshow-slide.liquid @@ -0,0 +1,42 @@ +{%- doc -%} + Renders a slideshow slide component. + + @param {number} index - the index of the slide + @param {string} [children] - The content of the slideshow slide + @param {string} [class] - HTML class attribute of the slideshow slide + @param {string} [style] - HTML style attribute of the slideshow slide + @param {string} [attributes] - Additional HTML attributes to add to the slideshow slide + @param {boolean} [hidden] - Hidden slides will not be shown in the slideshow + @param {string} [slide_id] - The unique id assigned to the slide amongst all slides in the slideshow + @param {string} [media_fit] - { 'cover', 'contain' } - CSS property for how the media should be fit in the slide + + @example + {% render 'slideshow-slide', index: 0, children: imageElement, slide_id: 'slide-1', hidden: false, media_fit: 'cover' %} +{%- enddoc -%} + +{%- liquid + assign class = class | strip | strip_newlines + assign style = style | strip | strip_newlines +-%} + + diff --git a/snippets/slideshow.liquid b/snippets/slideshow.liquid new file mode 100644 index 000000000..28aa284e7 --- /dev/null +++ b/snippets/slideshow.liquid @@ -0,0 +1,77 @@ +{%- doc -%} + Renders a slideshow component. + Condiional component and slideshow controls. + + @param {object[]} slides - the slides of the slideshow + @param {string} [ref] - the ref of the slideshow component + @param {string} [class] - HTML class attribute of the slideshow component + @param {string} [controls] - the controls of the slideshow component + @param {string} [style] - HTML style attribute of the slideshow component + @param {boolean} [autoplay] - whether the slideshow will autoplay + @param {number} [autoplay_speed] - the speed of the slideshow autoplay + @param {boolean} [auto_hide_controls] - whether to hide slideshow-controls when the scroller is smaller than the viewport + @param {boolean} [infinite] - whether the slideshow will loop + @param {number} [initial_slide] - the 0-based index of the initial slide, defaults to 0 + @param {string} [slideshow_gutters] - the gutter positions to render. Set width with CSS variables --gutter-slide-width + @param {number} [slide_count] - the total number of slides + @param {string} [slide_size] - the height of the slides + @param {boolean} [show_arrows] - whether the slideshow will render a slideshow-arrows component + @param {string} [slideshow_arrows] - a custom slideshow-arrows component to render instead of the default + @param {string} [icon_style] - The style of the icon, defaults to 'arrow' + @param {string} [arrows_position] - The position of the arrows, defaults to 'bottom' + @param {string} [attributes] - Additional attributes to add to the slideshow component + @param {boolean} [header] - The title of the slideshow + + @example + {% render 'slideshow', slides: slides, slide_count: collection.products.size, ref: 'mobileSlideshow' %} +{%- enddoc -%} + +{% assign class = class | strip %} +{% assign style = style | strip %} + + + {% if header != blank %} + {{ header }} + {% endif %} + + {% if show_arrows and disabled != true %} + {% render 'slideshow-arrows', icon_style: icon_style, arrows_position: arrows_position %} + {% endif %} + {% if slideshow_arrows and disabled != true %} + {{ slideshow_arrows }} + {% endif %} + + {{ slides }} + + + {{ controls }} + diff --git a/snippets/sorting.liquid b/snippets/sorting.liquid new file mode 100644 index 000000000..6f9bb46ce --- /dev/null +++ b/snippets/sorting.liquid @@ -0,0 +1,361 @@ +{%- doc -%} + Renders the sorting component. + + @param {object} results - The results of the search + @param {string} sort_by - The current sort by + @param {string} filter_style - The filter style + @param {string} [suffix] - { 'desktop' | 'mobile' | 'overflow' } Used on `form` attributes to connect inputs to a form[id] + @param {string} [sort_position] - { 'desktop' | 'mobile' } Used in a data-testID selector for automated testing + @param {boolean} should_use_select_on_mobile - Whether to use a select element for the sorting component on mobile + @param {string} section_id - The section ID +{%- enddoc -%} + + + {% liquid + assign default_sort_by = results.default_sort_by + for option in results.sort_options + if option.value == sort_by + assign status = option.name + endif + endfor + %} + + + + +
    + + + {% for option in results.sort_options %} + + {% endfor %} + +
    +
    +
    + +{% stylesheet %} + .sorting-filter__container { + display: flex; + align-items: center; + justify-content: space-between; + padding-inline: var(--drawer-padding) 0; + padding-block: var(--padding-sm); + margin-inline-end: var(--margin-md); + position: relative; + } + + .sorting-filter__container .facets__label { + font-size: var(--font-h4--size); + } + + .sorting-filter__select-wrapper { + display: flex; + position: relative; + border-radius: var(--variant-picker-button-radius); + align-items: center; + overflow: clip; + padding: var(--padding-2xs) var(--padding-xs); + } + + .sorting-filter__select-wrapper:has(:focus-visible) { + outline: var(--focus-outline-width) solid currentcolor; + outline-offset: var(--focus-outline-offset); + } + + .sorting-filter__select-wrapper:has(:focus-visible) .sorting-filter__select { + outline: none; + } + + .sorting-filter__container .sorting-filter__select { + appearance: none; + border: 0; + margin: 0; + cursor: pointer; + width: 100%; + padding-inline-end: var(--icon-size-2xs); + text-align: right; + + /* Needed for Safari */ + text-align-last: right; + } + + .sorting-filter__select .icon { + position: absolute; + right: var(--padding-md); + top: 50%; + transform: translateY(-50%); + width: var(--icon-size-2xs); + height: var(--icon-size-2xs); + pointer-events: none; + } + + .sorting-filter { + @media screen and (min-width: 750px) { + z-index: var(--facets-upper-z-index); + } + } + + .sorting-filter__options { + display: flex; + right: 0; + flex-direction: column; + gap: var(--margin-3xs); + padding: calc(var(--drawer-padding) / 2); + color: var(--color-foreground); + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } + } + + .sorting-filter__option { + cursor: pointer; + display: grid; + grid-template-columns: var(--icon-size-sm) 1fr; + gap: var(--margin-2xs); + min-width: 180px; + padding: var(--padding-2xs) calc(var(--drawer-padding) / 2) var(--padding-2xs) var(--padding-2xs); + + &:hover { + border-radius: calc(var(--style-border-radius-popover) / 2); + background-color: rgb(var(--color-foreground-rgb) / var(--opacity-8)); + } + + &:focus { + border-radius: calc(var(--style-border-radius-popover) / 2); + } + } + + .sorting-filter__input { + display: none; + + &:checked + .sorting-filter__checkmark + .sorting-filter__label { + font-weight: 500; + } + } + + .sorting-filter__checkmark { + visibility: hidden; + } + + *:checked ~ .sorting-filter__checkmark { + visibility: visible; + } + + .sorting-filter__label { + cursor: pointer; + pointer-events: none; + } + + .facets-toggle--no-filters .sorting-filter__select-wrapper { + @media screen and (max-width: 749px) { + padding-inline-start: 0; + } + } + + .facets-mobile-wrapper .sorting-filter .facets__panel { + padding-inline: 0; + position: relative; + } + + .facets-mobile-wrapper .sorting-filter .facets__status { + display: none; + } + + .facets-mobile-wrapper:has(> :nth-child(2)) .sorting-filter .sorting-filter__options { + left: 0; + right: unset; + } + + .facets-mobile-wrapper .sorting-filter .facets__label { + margin-inline-end: var(--margin-2xs); + font-size: var(--font-paragraph--size); + color: var(--color-foreground-muted); + } + + .facets-mobile-wrapper .sorting-filter__options { + border-radius: var(--style-border-radius-popover); + position: absolute; + top: 0; + right: 0; + width: max-content; + min-width: var(--facets-panel-min-width); + max-width: var(--facets-panel-width); + max-height: var(--facets-panel-height); + z-index: var(--facets-upper-z-index); + box-shadow: var(--shadow-popover); + border: var(--style-border-popover); + background-color: var(--color-background); + overflow-y: hidden; + padding: var(--padding-sm); + gap: var(--gap-sm); + } + + .facets-toggle .sorting-filter__container { + @media screen and (max-width: 749px) { + padding: 0; + } + } + + .facets-toggle .sorting-filter__container .facets__label { + @media screen and (max-width: 749px) { + display: none; + } + } + + .facets-toggle .sorting-filter::before { + @media screen and (max-width: 749px) { + display: none; + } + } + + .facets--drawer .sorting-filter { + @media screen and (min-width: 750px) { + display: none; + } + } + + .sorting-filter__options { + block-size: 0; + overflow-y: clip; + opacity: 0; + interpolate-size: allow-keywords; + transition: content-visibility var(--animation-speed-slow) allow-discrete, + padding-block var(--animation-speed-slow) var(--animation-easing), + opacity var(--animation-speed-slow) var(--animation-easing), + block-size var(--animation-speed-slow) var(--animation-easing); + } + + details[open] .sorting-filter__options { + opacity: 1; + block-size: auto; + + @starting-style { + block-size: 0; + opacity: 0; + overflow-y: clip; + } + + &:focus-within { + overflow-y: visible; + } + } +{% endstylesheet %} diff --git a/snippets/spacing-padding.liquid b/snippets/spacing-padding.liquid new file mode 100644 index 000000000..5e00f2319 --- /dev/null +++ b/snippets/spacing-padding.liquid @@ -0,0 +1,11 @@ +{%- comment -%} + Intended for blocks and sections that provide values for all the referenced settings. + +
    + + Accepts: + settings: {block.settings || section.settings} +{%- endcomment -%} + +--padding-block-start: {{ settings.padding-block-start | default: 0 }}px; --padding-block-end:{{- settings.padding-block-end | default: 0 -}}px; +--padding-inline-start:{{ settings.padding-inline-start | default: 0 }}px; --padding-inline-end:{{- settings.padding-inline-end | default: 0 -}}px; diff --git a/snippets/spacing-style.liquid b/snippets/spacing-style.liquid new file mode 100644 index 000000000..70d5289af --- /dev/null +++ b/snippets/spacing-style.liquid @@ -0,0 +1,49 @@ +{%- comment -%} + Intended for blocks and sections that provide values for all the referenced settings. + +
    + + Accepts: + settings: {block.settings || section.settings} + suffix: {string} + scale_min {number}: Value above which spacing scaling will be applied. Default: 20 + disable_scaling {boolean}: Disable scaling. Default: false +{%- endcomment -%} +{%- liquid + assign properties = 'padding,margin' | split: ',' + assign directions = 'block,inline' | split: ',' + assign edges = ',start,end' | split: ',' + assign min = scale_min | default: 20 +-%} +{%- capture variables -%} + {%- for property in properties -%} + {%- for direction in directions -%} + {%- for edge in edges -%} + {%-liquid + assign name = property | append: '-' | append: direction + + if edge != blank + assign name = name | append: '-' | append: edge + endif + + assign setting_id = name + + if suffix != blank + assign setting_id = setting_id | append: '-' | append: suffix + endif + + assign value = settings[setting_id] + -%} + + {%- if value != blank -%} + {%- if disable_scaling != true and value > min -%} + --{{ name }}: max({{ min }}px, calc(var(--spacing-scale) * {{ value }}px)); + {%- else -%} + --{{ name }}: {{ value }}px; + {%- endif -%} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + {%- endfor -%} +{%- endcapture -%} +{{- variables | strip | strip_newlines -}} diff --git a/snippets/strikethrough-variant.liquid b/snippets/strikethrough-variant.liquid new file mode 100644 index 000000000..f5e85ee2a --- /dev/null +++ b/snippets/strikethrough-variant.liquid @@ -0,0 +1,11 @@ +{% unless product_option.available %} + + {% # 25deg %} + + +{% endunless %} diff --git a/snippets/stylesheets.liquid b/snippets/stylesheets.liquid new file mode 100644 index 000000000..88668ee40 --- /dev/null +++ b/snippets/stylesheets.liquid @@ -0,0 +1,2 @@ +{{ 'overflow-list.css' | asset_url | preload_tag: as: 'style' }} +{{ 'base.css' | asset_url | stylesheet_tag: preload: true }} diff --git a/snippets/submenu-font-styles.liquid b/snippets/submenu-font-styles.liquid new file mode 100644 index 000000000..411ca3e43 --- /dev/null +++ b/snippets/submenu-font-styles.liquid @@ -0,0 +1,48 @@ +{%- comment -%} + Derives CSS variables from the menu typography settings for 2nd and 3rd level menu items. + Accepts: + settings: {block.settings} +{%- endcomment -%} + +--menu-parent-font-family: var(--font-{{ settings.type_font_tertiary_link }}--family); --menu-parent-font-style: +var(--font- +{{- settings.type_font_tertiary_link -}} +--style); --menu-parent-font-weight: var(--font- +{{- settings.type_font_tertiary_link -}} +--weight); --menu-parent-font-case: +{% if settings.type_case_tertiary_link == 'uppercase' %}uppercase{% else %}none{% endif %}; +{% case settings.menu_font_style %} + {% when 'regular' %} + --menu-parent-font-size: var(--menu-font-md--size); --menu-parent-font-line-height: + var(--menu-font-md--line-height); --menu-parent-font-color: var(--color-foreground); + --menu-parent-active-font-color: rgb(var(--color-foreground-rgb) / var(--opacity-subdued-text)); + {% when 'inverse' %} + --menu-parent-font-size: var(--menu-font-sm--size); --menu-parent-font-line-height: + var(--menu-font-sm--line-height); --menu-parent-font-color: rgb(var(--color-foreground-rgb) / + var(--opacity-subdued-text)); --menu-parent-active-font-color: var(--color-foreground); + {% when 'inverse_large' %} + --menu-parent-font-size: var(--menu-font-sm--size); --menu-parent-font-line-height: + var(--menu-font-sm--line-height); --menu-parent-font-color: rgb(var(--color-foreground-rgb) / + var(--opacity-subdued-text)); --menu-parent-active-font-color: var(--color-foreground); +{% endcase %} +--menu-child-font-family: var(--font-{{ settings.type_font_secondary_link }}--family); --menu-child-font-style: +var(--font- +{{- settings.type_font_secondary_link -}} +--style); --menu-child-font-weight: var(--font- +{{- settings.type_font_secondary_link -}} +--weight); --menu-child-font-case: +{% if settings.type_case_secondary_link == 'uppercase' %}uppercase{% else %}none{% endif %}; +{% case settings.menu_font_style %} + {% when 'regular' %} + --menu-child-font-size: var(--menu-font-sm--size); --menu-child-font-line-height: var(--menu-font-sm--line-height); + --menu-child-font-color: rgb(var(--color-foreground-rgb) / var(--opacity-subdued-text)); + --menu-child-active-font-color: var(--color-foreground); + {% when 'inverse' %} + --menu-child-font-size: var(--menu-font-md--size); --menu-child-font-line-height: var(--menu-font-md--line-height); + --menu-child-font-color: var(--color-foreground); --menu-child-active-font-color: rgb(var(--color-foreground-rgb) / + var(--opacity-subdued-text)); + {% when 'inverse_large' %} + --menu-child-font-size: var(--menu-font-xl--size); --menu-child-font-line-height: var(--menu-font-xl--line-height); + --menu-child-font-color: var(--color-foreground); --menu-child-active-font-color: rgb(var(--color-foreground-rgb) / + var(--opacity-subdued-text)); +{% endcase %} diff --git a/snippets/swatch.liquid b/snippets/swatch.liquid new file mode 100644 index 000000000..1bcaffa91 --- /dev/null +++ b/snippets/swatch.liquid @@ -0,0 +1,41 @@ +{%- doc -%} + Renders a swatch + + @param {object} swatch - a swatch object + @param {object} [variant_image] - an alternate image + @param {string} [mode] - one of 'unscaled' or 'filter' + + @example + {% render 'swatch', swatch: swatch, variant_image: variant_image, mode: 'unscaled' %} +{%- enddoc -%} + +{% liquid + assign swatch_value = null + if settings.show_variant_image and variant_image + assign swatch_image_width = settings.variant_swatch_width | times: 2 + assign swatch_image_url = variant_image | image_url: width: swatch_image_width + assign swatch_value = 'url(' | append: swatch_image_url | append: ')' + elsif swatch.image + assign swatch_image_url = swatch.image | image_url: width: 80 + assign swatch_value = 'url(' | append: swatch_image_url | append: ')' + elsif swatch.color + assign swatch_value = 'rgb(' | append: swatch.color.rgb | append: ')' + endif + assign classes = + case mode + when 'unscaled' + assign extra_classes = ' swatch--unscaled' + when 'filter' + assign extra_classes = ' swatch--filter' + when 'pill' + assign extra_classes = ' swatch--pill' + else + assign extra_classes = '' + endcase +%} + + + diff --git a/snippets/tax-info.liquid b/snippets/tax-info.liquid new file mode 100644 index 000000000..3c16eaee0 --- /dev/null +++ b/snippets/tax-info.liquid @@ -0,0 +1,84 @@ +{%- comment -%} + Intended for use in a block similar to the text block. + + Accepts: + has_discounts_enabled: {boolean} - whether discounts are enabled +{%- endcomment -%} + + + {%- if cart.duties_included and cart.taxes_included -%} + {%- if shop.shipping_policy.body == blank -%} + {%- if has_discounts_enabled -%} + {{ 'content.duties_and_taxes_included_shipping_at_checkout_without_policy_without_discounts' | t }} + {%- else -%} + {{ 'content.duties_and_taxes_included_shipping_at_checkout_without_policy' | t }} + {%- endif -%} + {%- else -%} + {%- if has_discounts_enabled -%} + {{ + 'content.duties_and_taxes_included_shipping_at_checkout_with_policy_without_discounts_html' + | t: link: shop.shipping_policy.url + }} + {%- else -%} + {{ + 'content.duties_and_taxes_included_shipping_at_checkout_with_policy_html' + | t: link: shop.shipping_policy.url + }} + {%- endif -%} + {%- endif -%} + {%- elsif cart.duties_included == false and cart.taxes_included -%} + {%- if shop.shipping_policy.body == blank -%} + {%- if has_discounts_enabled -%} + {{ 'content.duties_and_taxes_included_shipping_at_checkout_without_policy_without_discounts' | t }} + {%- else -%} + {{ 'content.taxes_included_shipping_at_checkout_without_policy' | t }} + {%- endif -%} + {%- else -%} + {%- if has_discounts_enabled -%} + {{ + 'content.duties_and_taxes_included_shipping_at_checkout_with_policy_without_discounts_html' + | t: link: shop.shipping_policy.url + }} + {%- else -%} + {{ 'content.taxes_included_shipping_at_checkout_with_policy_html' | t: link: shop.shipping_policy.url }} + {%- endif -%} + {%- endif -%} + {%- elsif cart.duties_included and cart.taxes_included == false -%} + {%- if shop.shipping_policy.body == blank -%} + {%- if has_discounts_enabled -%} + {{ 'content.duties_included_taxes_at_checkout_shipping_at_checkout_without_policy_without_discounts' | t }} + {%- else -%} + {{ 'content.duties_included_taxes_at_checkout_shipping_at_checkout_without_policy' | t }} + {%- endif -%} + {%- else -%} + {%- if has_discounts_enabled -%} + {{ + 'content.duties_included_taxes_at_checkout_shipping_at_checkout_with_policy_without_discounts_html' + | t: link: shop.shipping_policy.url + }} + {%- else -%} + {{ + 'content.duties_included_taxes_at_checkout_shipping_at_checkout_with_policy_html' + | t: link: shop.shipping_policy.url + }} + {%- endif -%} + {%- endif -%} + {%- elsif cart.duties_included == false and cart.taxes_included == false -%} + {%- if shop.shipping_policy.body == blank -%} + {%- if has_discounts_enabled -%} + {{ 'content.taxes_at_checkout_shipping_at_checkout_without_policy_without_discounts' | t }} + {%- else -%} + {{ 'content.taxes_at_checkout_shipping_at_checkout_without_policy' | t }} + {%- endif -%} + {%- else -%} + {%- if has_discounts_enabled -%} + {{ + 'content.taxes_at_checkout_shipping_at_checkout_with_policy_without_discounts_html' + | t: link: shop.shipping_policy.url + }} + {%- else -%} + {{ 'content.taxes_at_checkout_shipping_at_checkout_with_policy_html' | t: link: shop.shipping_policy.url }} + {%- endif -%} + {%- endif -%} + {%- endif -%} + diff --git a/snippets/text.liquid b/snippets/text.liquid new file mode 100644 index 000000000..cfab851ed --- /dev/null +++ b/snippets/text.liquid @@ -0,0 +1,216 @@ +{%- doc -%} + Intended for use in a block similar to the text block. + + @param {string} [class] - custom class to define in addition to text-block classes + @param {string} [fallback_text] - fallback text if settings.text does not exist + @param {string} [width] - width of the text block +{%- enddoc -%} + +{% liquid + assign plain_text = block.settings.text | strip_newlines | strip_html | strip + assign text_width = width | default: block.settings.width + + if block.settings.font_size contains 'heading-lg' or block.settings.font_size contains 'heading-xl' + assign type = 'display' + elsif block.settings.font_size contains 'heading' + assign type = 'heading' + else + assign type = 'body' + endif + if block.settings.type_preset == 'rte' or block.settings.type_preset == 'paragraph' + assign is_rte = true + endif + + capture text_block_classes + if text_width == '100%' + echo 'text-block--align-' | append: block.settings.alignment + if block.settings.max_width == 'none' + echo ' text-block--full-width ' + endif + endif + if block.settings.type_preset == 'custom' + echo ' custom-typography ' + if block.settings.font_size != '' + echo ' custom-font-size ' + endif + if block.settings.color != '' + echo ' custom-color ' + endif + endif + if block.settings.background + echo ' text-block--background ' + endif + if is_rte + echo ' rte ' + endif + endcapture +%} + +{% capture attributes %} + class="{{ class }} spacing-style text-block text-block--{{ block.id }} {{ block.settings.type_preset }} + {{ text_block_classes }} + " + + style=" + {% render 'spacing-padding', settings: block.settings %} + {% render 'typography-style', settings: block.settings %} + --width: {{ text_width }}; + --max-width: var(--max-width--{{ type }}-{{ block.settings.max_width }}); + {% if text_width == "100%" %} + --text-align: {{ block.settings.alignment }}; + {% endif %} + {% if block.settings.background %} + --text-background-color: {{ block.settings.background_color | default: 'rgb(255 255 255 / 1.0)' }}; + --text-corner-radius: {{ block.settings.corner_radius }}px; + --text-padding: max(var(--padding-2xs), calc((var(--text-corner-radius) + var(--padding-xs)) * (1 - cos(45deg)))); + {% endif %} + " + + {{ block.shopify_attributes }} +{% endcapture %} +{% liquid + # {{ attributes }} must be on the immediate HTML parent of the text to preserve + # the click-to-edit connection in the theme editor. Any break between the text, + # including if-statements, will break the connection. + + assign element = 'div' + if is_rte + assign element = 'rte-formatter' + endif +%} + +{% if fallback_text != blank and plain_text == blank %} +
    + {{ fallback_text }} +
    +{% elsif plain_text != blank %} + <{{ element }} {{ attributes }}> + {{ block.settings.text }} + +{% endif %} + +{% stylesheet %} + :root { + --text-align-default: left; + } + + [style*='--horizontal-alignment: center'] .text-block { + --text-align-default: center; + } + + [style*='--horizontal-alignment: flex-end'] .text-block { + --text-align-default: right; + } + + [style*='--horizontal-alignment: flex-start'] > .text-block { + --text-align-default: left; + } + + [style*='--horizontal-alignment: center'] > .text-block { + --text-align-default: center; + } + + [style*='--horizontal-alignment: flex-end'] > .text-block { + --text-align-default: right; + } + + .text-block { + width: var(--width); + max-width: 100%; + display: flex; + flex-direction: column; + align-items: var(--horizontal-alignment); + } + + .text-block > * { + width: var(--width); + max-width: var(--max-width, 100%); + text-align: var(--text-align, var(--text-align-default)); + text-wrap: var(--text-wrap); + } + + .text-block:not(.text-block--full-width).rte, + .text-block:not(.text-block--full-width).paragraph { + /* Safari doesn't support pretty, so fallback to balance */ + text-wrap: balance; + text-wrap: pretty; + } + + .text-block:not(.text-block--full-width):is(.h1, .h2, .h3, .h4, .h5, .h6) { + text-wrap: balance; + } + + /* Hide underline unless text is using paragraph styles. */ + .text-block:is(.h1, .h2, .h3, .h4, .h5, .h6) a { + text-decoration-color: transparent; + } + + .text-block h1, + .text-block.h1 > * { + margin-block: var(--font-h1--spacing); + } + + .text-block h2, + .text-block.h2 > * { + margin-block: var(--font-h2--spacing); + } + + .text-block h3, + .text-block.h3 > * { + margin-block: var(--font-h3--spacing); + } + + .text-block h4, + .text-block.h4 > * { + margin-block: var(--font-h4--spacing); + } + + .text-block h5, + .text-block.h5 > * { + margin-block: var(--font-h5--spacing); + } + + .text-block h6, + .text-block.h6 > * { + margin-block: var(--font-h6--spacing); + } + + .text-block p, + .text-block.p > * { + margin-block: var(--font-paragraph--spacing); + } + + .text-block > *:first-child { + margin-block-start: 0; + } + + .text-block > *:last-child { + margin-block-end: 0; + } + + .text-block--align-center, + .text-block--align-center > * { + margin-inline: auto; + } + + .text-block--align-right, + .text-block--align-right > * { + margin-inline-start: auto; + } + + .text-block--background { + background-color: var(--text-background-color); + border-radius: var(--text-corner-radius); + + /* To avoid text being cropped when using a border radius we add a minimum padding. */ + padding-block-start: max(var(--text-padding), var(--padding-block-start, 0)); + padding-block-end: max(var(--text-padding), var(--padding-block-end, 0)); + padding-inline-start: max(var(--text-padding), var(--padding-inline-start, 0)); + padding-inline-end: max(var(--text-padding), var(--padding-inline-end, 0)); + } + + .custom-color, + .custom-color > :is(h1, h2, h3, h4, h5, h6, p, *) { + color: var(--color); + } +{% endstylesheet %} diff --git a/snippets/theme-editor.liquid b/snippets/theme-editor.liquid new file mode 100644 index 000000000..efa9f5f72 --- /dev/null +++ b/snippets/theme-editor.liquid @@ -0,0 +1,4 @@ + diff --git a/snippets/theme-styles-variables.liquid b/snippets/theme-styles-variables.liquid new file mode 100644 index 000000000..84e74728f --- /dev/null +++ b/snippets/theme-styles-variables.liquid @@ -0,0 +1,556 @@ +{%- liquid + assign primary_font_bold = settings.type_body_font | font_modify: 'weight', 'bold' + assign primary_font_italic = settings.type_body_font | font_modify: 'style', 'italic' + assign primary_font_bold_italic = primary_font_bold | font_modify: 'style', 'italic' + + assign secondary_font_bold = settings.type_subheading_font | font_modify: 'weight', 'bold' + assign secondary_font_italic = settings.type_subheading_font | font_modify: 'style', 'italic' + assign secondary_font_bold_italic = secondary_font_bold | font_modify: 'style', 'italic' + + assign tertiary_font_bold = settings.type_heading_font | font_modify: 'weight', 'bold' + assign tertiary_font_italic = settings.type_heading_font | font_modify: 'style', 'italic' + assign tertiary_font_bold_italic = tertiary_font_bold | font_modify: 'style', 'italic' + + assign accent_font_bold = settings.type_accent_font | font_modify: 'weight', 'bold' + assign accent_font_italic = settings.type_accent_font | font_modify: 'style', 'italic' + assign accent_font_bold_italic = accent_font_bold | font_modify: 'style', 'italic' +%} + +{% style %} + {{ settings.type_body_font | font_face: font_display: 'swap' }} + {{ primary_font_bold | font_face: font_display: 'swap' }} + {{ primary_font_italic | font_face: font_display: 'swap' }} + {{ primary_font_bold_italic | font_face: font_display: 'swap' }} + + {{ settings.type_subheading_font | font_face: font_display: 'swap' }} + {{ secondary_font_bold | font_face: font_display: 'swap' }} + {{ secondary_font_italic | font_face: font_display: 'swap' }} + {{ secondary_font_bold_italic | font_face: font_display: 'swap' }} + + {{ settings.type_heading_font | font_face: font_display: 'swap' }} + {{ tertiary_font_bold | font_face: font_display: 'swap' }} + {{ tertiary_font_italic | font_face: font_display: 'swap' }} + {{ tertiary_font_bold_italic | font_face: font_display: 'swap' }} + + {{ settings.type_accent_font | font_face: font_display: 'swap' }} + {{ accent_font_bold | font_face: font_display: 'swap' }} + {{ accent_font_italic | font_face: font_display: 'swap' }} + {{ accent_font_bold_italic | font_face: font_display: 'swap' }} + + :root { + /* Page Layout */ + --sidebar-width: 25rem; + --narrow-content-width: 36rem; + --normal-content-width: 42rem; + --wide-content-width: 46rem; + --narrow-page-width: 90rem; + --normal-page-width: 120rem; + --wide-page-width: 150rem; + + /* Section Heights */ + --section-height-small: 15rem; + --section-height-medium: 25rem; + --section-height-large: 35rem; + + @media screen and (min-width: 40em) { + --section-height-small: 40svh; + --section-height-medium: 55svh; + --section-height-large: 70svh; + } + + @media screen and (min-width: 60em) { + --section-height-small: 50svh; + --section-height-medium: 65svh; + --section-height-large: 80svh; + } + + /* Letter spacing */ + --letter-spacing-sm: 0.06em; + --letter-spacing-md: 0.13em; + + /* Font families */ + --font-body--family: {{ settings.type_body_font.family }}, {{ settings.type_body_font.fallback_families }}; + --font-body--style: {{ settings.type_body_font.style }}; + --font-body--weight: {{ settings.type_body_font.weight }}; + --font-subheading--family: {{ settings.type_subheading_font.family }}, {{ settings.type_subheading_font.fallback_families }}; + --font-subheading--style: {{ settings.type_subheading_font.style }}; + --font-subheading--weight: {{ settings.type_subheading_font.weight }}; + --font-heading--family: {{ settings.type_heading_font.family }}, {{ settings.type_heading_font.fallback_families }}; + --font-heading--style: {{ settings.type_heading_font.style }}; + --font-heading--weight: {{ settings.type_heading_font.weight }}; + --font-accent--family: {{ settings.type_accent_font.family }}, {{ settings.type_accent_font.fallback_families }}; + --font-accent--style: {{ settings.type_accent_font.style }}; + --font-accent--weight: {{ settings.type_accent_font.weight }}; + + /* Margin sizes */ + --font-h1--spacing: 0.25em; + --font-h2--spacing: 0.25em; + --font-h3--spacing: 0.25em; + --font-h4--spacing: 0.25em; + --font-h5--spacing: 0.25em; + --font-h6--spacing: 0.25em; + --font-paragraph--spacing: 0.5em; + + /* Heading colors */ + --font-h1--color: var(--color-foreground-heading); + --font-h2--color: var(--color-foreground-heading); + --font-h3--color: var(--color-foreground-heading); + --font-h4--color: var(--color-foreground-heading); + --font-h5--color: var(--color-foreground-heading); + --font-h6--color: var(--color-foreground-heading); + + /** Z-Index / Layering */ + --layer-section-background: -2; + --layer-lowest: -1; + --layer-base: 0; + --layer-flat: 1; + --layer-raised: 2; + --layer-heightened: 4; + --layer-sticky: 8; + --layer-window-overlay: 10; + --layer-header-menu: 12; + --layer-overlay: 16; + --layer-menu-drawer: 18; + --layer-temporary: 20; + + /* Max-width / Measure */ + --max-width--body-normal: 50ch; + --max-width--body-narrow: 35ch; + + --max-width--heading-normal: 50ch; + --max-width--heading-narrow: 30ch; + + --max-width--display-normal: 20ch; + --max-width--display-narrow: 15ch; + --max-width--display-tight: 5ch; + + /* Letter-spacing / Tracking */ + --letter-spacing--display-tight: -0.03em; + --letter-spacing--display-normal: 0em; + --letter-spacing--display-loose: 0.03em; + + --letter-spacing--heading-tight: -0.03em; + --letter-spacing--heading-normal: 0em; + --letter-spacing--heading-loose: 0.03em; + + --letter-spacing--body-tight: -0.03em; + --letter-spacing--body-normal: 0em; + --letter-spacing--body-loose: 0.03em; + + /* Line height / Leading */ + --line-height: 1; + + --line-height--display-tight: 1; + --line-height--display-normal: 1.1; + --line-height--display-loose: 1.2; + + --line-height--heading-tight: 1.15; + --line-height--heading-normal: 1.25; + --line-height--heading-loose: 1.35; + + --line-height--body-tight: 1.2; + --line-height--body-normal: 1.4; + --line-height--body-loose: 1.6; + + /* Typography presets */ + {% liquid + assign font_sizes = "paragraph, h1, h2, h3, h4, h5, h6" | split: ", " + assign fluid_size_cutoff = 48 + assign absolute_font_size_min = 10 + + comment + Build an array of font sizes and sort it + endcomment + assign font_size_values = '' + for font_size in font_sizes + assign size_setting = 'type_size_[font_size]' | replace: '[font_size]', font_size + assign size_setting_value = settings[size_setting] | times: 1 + + comment + If the font size is less than 100, pad it with a 0 + This is because we end up with an array of strings, which | sort filter can't order "correctly") + endcomment + if size_setting_value < 100 + assign size_setting_value = '0[size_setting_value]' | replace: '[size_setting_value]', size_setting_value + endif + + assign font_size_values = font_size_values | append: '[size_setting_value],' | replace: '[size_setting_value]', size_setting_value + endfor + + assign font_size_values = font_size_values | split: ',' | uniq | sort_natural + + comment + For each font size S, find the next smaller size S-1, and determine the minimum for S + The calculation depends on the size of S-1 (over or under the cutoff) + endcomment + for font_size in font_sizes + assign size_setting = 'type_size_[font_size]' | replace: '[font_size]', font_size + assign font_size_value = settings[size_setting] | times: 1 + assign font_size_string = '[font_size_value]' | replace: '[font_size_value]', font_size_value + assign index = font_size_values | find_index: font_size_string + + if font_size_value >= fluid_size_cutoff + + comment + Calculate the minimum size for each font size + endcomment + assign fluid_font_size_min = font_size_value + + if index == 0 + assign fluid_font_size_min = absolute_font_size_min + else + assign next_font_size_index = index | minus: 1 + assign next_font_size_value = font_size_values[next_font_size_index] + assign next_font_size_value_number = next_font_size_value | times: 1 + + comment + If the next bigger font size under the fluid cutoff, we use keep a 4px buffer + endcomment + if next_font_size_value_number < fluid_size_cutoff + assign fluid_font_size_min = next_font_size_value_number | plus: 4 + if font_size_value < fluid_font_size_min + assign fluid_font_size_min = font_size_value + endif + else + assign fluid_font_size_min = next_font_size_value | times: 1 + endif + endif + + comment + Calculate the fluid and maximum size for each font size + endcomment + assign fluid_size_min_rem = fluid_font_size_min | divided_by: 16.0 + assign fluid_size = font_size_value | times: 0.1 + assign fluid_size_max_rem = font_size_value | divided_by: 16.0 + + echo '--font-size--[font_size]: clamp([fluid_size_min_rem]rem, [fluid_size]vw, [fluid_size_max_rem]rem);' | replace: '[font_size]', font_size | replace: '[fluid_size_min_rem]', fluid_size_min_rem | replace: '[fluid_size]', fluid_size | replace: '[fluid_size_max_rem]', fluid_size_max_rem + else + assign fluid_size_rem = font_size_value | divided_by: 16.0 + echo '--font-size--[font_size]: [fluid_size_rem]rem;' | replace: '[font_size]', font_size | replace: '[fluid_size_rem]', fluid_size_rem + endif + endfor + + assign type_presets = "paragraph, h1, h2, h3, h4, h5, h6" | split: ", " + + for preset_name in type_presets + assign preset_size = '--font-size--[preset_name]' | replace: '[preset_name]', preset_name + assign preset_line_height = 'type_line_height_[preset_name]' | replace: '[preset_name]', preset_name + + if preset_name == 'paragraph' + assign preset_font = '--font-body--family' + assign preset_style = '--font-body--style' + assign preset_weight = '400' + assign preset_case = '--font-body--case' + assign preset_letter_spacing = 'body-normal' + + echo '--font-[preset_name]--weight: [preset_weight];' | replace: '[preset_name]', preset_name | replace: '[preset_weight]', preset_weight + echo '--font-[preset_name]--letter-spacing: var(--letter-spacing--[preset_letter_spacing]);' | replace: '[preset_name]', preset_name | replace: '[preset_letter_spacing]', preset_letter_spacing + else + assign preset_font_id = 'type_font_[preset_name]' | replace: '[preset_name]', preset_name + assign preset_font = '--font-[preset_font]--family' | replace: '[preset_font]', settings[preset_font_id] + assign preset_style = '--font-[preset_font]--style' | replace: '[preset_font]', settings[preset_font_id] + assign preset_weight = '--font-[preset_font]--weight' | replace: '[preset_font]', settings[preset_font_id] + assign preset_case = 'type_case_[preset_name]' | replace: '[preset_name]', preset_name + assign preset_letter_spacing = 'type_letter_spacing_[preset_name]' | replace: '[preset_name]', preset_name + + echo '--font-[preset_name]--weight: var([preset_weight]);' | replace: '[preset_name]', preset_name | replace: '[preset_weight]', preset_weight + echo '--font-[preset_name]--letter-spacing: var(--letter-spacing--[preset_letter_spacing]);' | replace: '[preset_name]', preset_name | replace: '[preset_letter_spacing]', settings[preset_letter_spacing] + endif + + echo '--font-[preset_name]--size: var([preset_size]);' | replace: '[preset_name]', preset_name | replace: '[preset_size]', preset_size + echo '--font-[preset_name]--family: var([preset_font]);' | replace: '[preset_name]', preset_name | replace: '[preset_font]', preset_font + echo '--font-[preset_name]--style: var([preset_style]);' | replace: '[preset_name]', preset_name | replace: '[preset_style]', preset_style + echo '--font-[preset_name]--case: [preset_case];' | replace: '[preset_name]', preset_name | replace: '[preset_case]', settings[preset_case] + echo '--font-[preset_name]--line-height: var(--line-height--[preset_line_height]);' | replace: '[preset_name]', preset_name | replace: '[preset_line_height]', settings[preset_line_height] + endfor + %} + + /* Hardcoded font sizes */ + --font-size--2xs: 0.625rem; + --font-size--xs: 0.8125rem; + --font-size--sm: 0.875rem; + --font-size--md: 1rem; + --font-size--lg: 1.125rem; + --font-size--xl: 1.25rem; + --font-size--2xl: 1.5rem; + --font-size--3xl: 2rem; + --font-size--4xl: 2.5rem; + --font-size--5xl: 3rem; + --font-size--6xl: 3.5rem; + + /* Menu font sizes */ + --menu-font-sm--size: 0.875rem; + --menu-font-sm--line-height: calc(1.1 + 0.5 * min(16 / 14)); + --menu-font-md--size: 1rem; + --menu-font-md--line-height: calc(1.1 + 0.5 * min(16 / 16)); + --menu-font-lg--size: 1.125rem; + --menu-font-lg--line-height: calc(1.1 + 0.5 * min(16 / 18)); + --menu-font-xl--size: 1.25rem; + --menu-font-xl--line-height: calc(1.1 + 0.5 * min(16 / 20)); + --menu-font-2xl--size: 1.75rem; + --menu-font-2xl--line-height: calc(1.1 + 0.5 * min(16 / 28)); + + /* Colors */ + --color-error: #8B0000; + --color-success: #006400; + --color-white: #FFFFFF; + --color-white-rgb: 255 255 255; + --color-black: #000000; + --color-instock: #3ED660; + --color-lowstock: #EE9441; + --color-outofstock: #C8C8C8; + + /* Opacity */ + --opacity-5: 0.05; + --opacity-8: 0.08; + --opacity-10: 0.1; + --opacity-15: 0.15; + --opacity-20: 0.2; + --opacity-25: 0.25; + --opacity-30: 0.3; + --opacity-40: 0.4; + --opacity-50: 0.5; + --opacity-60: 0.6; + --opacity-70: 0.7; + --opacity-80: 0.8; + --opacity-85: 0.85; + --opacity-90: 0.9; + --opacity-subdued-text: var(--opacity-70); + + --shadow-button: 0 2px 3px rgb(0 0 0 / 20%); + --gradient-image-overlay: linear-gradient(to top, rgb(0 0 0 / 0.5), transparent); + + /* Spacing */ + --margin-3xs: 0.125rem; + --margin-2xs: 0.3rem; + --margin-xs: 0.5rem; + --margin-sm: 0.7rem; + --margin-md: 0.8rem; + --margin-lg: 1rem; + --margin-xl: 1.25rem; + --margin-2xl: 1.5rem; + --margin-3xl: 1.75rem; + --margin-4xl: 2rem; + --margin-5xl: 3rem; + --margin-6xl: 5rem; + + --scroll-margin: 50px; + + --padding-3xs: 0.125rem; + --padding-2xs: 0.25rem; + --padding-xs: 0.5rem; + --padding-sm: 0.7rem; + --padding-md: 0.8rem; + --padding-lg: 1rem; + --padding-xl: 1.25rem; + --padding-2xl: 1.5rem; + --padding-3xl: 1.75rem; + --padding-4xl: 2rem; + --padding-5xl: 3rem; + --padding-6xl: 4rem; + + --gap-3xs: 0.125rem; + --gap-2xs: 0.3rem; + --gap-xs: 0.5rem; + --gap-sm: 0.7rem; + --gap-md: 0.9rem; + --gap-lg: 1rem; + --gap-xl: 1.25rem; + --gap-2xl: 2rem; + --gap-3xl: 3rem; + + --spacing-scale-sm: 0.6; + --spacing-scale-md: 0.7; + --spacing-scale-default: 1.0; + + /* Checkout buttons gap */ + --checkout-button-gap: 8px; + + /* Borders */ + --style-border-width: 1px; + --style-border-radius-xs: 0.2rem; + --style-border-radius-sm: 0.6rem; + --style-border-radius-md: 0.8rem; + --style-border-radius-50: 50%; + --style-border-radius-lg: 1rem; + --style-border-radius-pills: {{ settings.pills_border_radius }}px; + --style-border-radius-inputs: {{ settings.inputs_border_radius }}px; + --style-border-radius-buttons-primary: {{ settings.button_border_radius_primary }}px; + --style-border-radius-buttons-secondary: {{ settings.button_border_radius_secondary }}px; + --style-border-width-primary: {{ settings.primary_button_border_width }}px; + --style-border-width-secondary: {{ settings.secondary_button_border_width }}px; + --style-border-width-inputs: {{ settings.input_border_width }}px; + --style-border-radius-popover: {{ settings.popover_border_radius }}px; + --style-border-popover: {{ settings.popover_border_width }}px {{ settings.popover_border }} rgb(var(--color-border-rgb) / {{ settings.popover_border_opacity }}%); + --style-border-drawer: {{ settings.drawer_border_width }}px {{ settings.drawer_border }} rgb(var(--color-border-rgb) / {{ settings.drawer_border_opacity }}%); + --style-border-swatch-opacity: {{ settings.variant_swatch_border_opacity }}%; + --style-border-swatch-width: {{ settings.variant_swatch_border_width }}px; + --style-border-swatch-style: {{ settings.variant_swatch_border_style }}; + + /* Animation */ + --ease-out-cubic: cubic-bezier(0.33, 1, 0.68, 1); + --ease-out-quad: cubic-bezier(0.32, 0.72, 0, 1); + --animation-speed-fast: 0.0625s; + --animation-speed: 0.125s; + --animation-speed-slow: 0.2s; + --animation-speed-medium: 0.15s; + --animation-easing: ease-in-out; + --animation-slideshow-easing: cubic-bezier(0.4, 0, 0.2, 1); + --drawer-animation-speed: 0.2s; + --animation-values-slow: var(--animation-speed-slow) var(--animation-easing); + --animation-values: var(--animation-speed) var(--animation-easing); + --animation-values-fast: var(--animation-speed-fast) var(--animation-easing); + --animation-values-allow-discrete: var(--animation-speed) var(--animation-easing) allow-discrete; + --animation-timing-hover: cubic-bezier(0.25, 0.46, 0.45, 0.94); + --animation-timing-active: cubic-bezier(0.5, 0, 0.75, 0); + --animation-timing-bounce: cubic-bezier(0.34, 1.56, 0.64, 1); + --animation-timing-default: cubic-bezier(0, 0, 0.2, 1); + --animation-timing-fade-in: cubic-bezier(0.16, 1, 0.3, 1); + --animation-timing-fade-out: cubic-bezier(0.4, 0, 0.2, 1); + + /* View transitions */ + /* View transition old */ + --view-transition-old-main-content: var(--animation-speed) var(--animation-easing) both fadeOut; + + /* View transition new */ + --view-transition-new-main-content: var(--animation-speed) var(--animation-easing) both fadeIn, var(--animation-speed) var(--animation-easing) both slideInTopViewTransition; + + /* Focus */ + --focus-outline-width: 0.09375rem; + --focus-outline-offset: 0.2em; + + /* Badges */ + --badge-blob-padding-block: 1px; + --badge-blob-padding-inline: 12px 8px; + --badge-rectangle-padding-block: 1px; + --badge-rectangle-padding-inline: 6px; + @media screen and (min-width: 750px) { + --badge-blob-padding-block: 4px; + --badge-blob-padding-inline: 16px 12px; + --badge-rectangle-padding-block: 4px; + --badge-rectangle-padding-inline: 10px; + } + + /* Icons */ + --icon-size-2xs: 0.6rem; + --icon-size-xs: 0.85rem; + --icon-size-sm: 1.25rem; + --icon-size-md: 1.375rem; + --icon-size-lg: 1.5rem; + --icon-stroke-width: {% if settings.icon_stroke == 'thin' %}1px{% elsif settings.icon_stroke == 'heavy' %}2px{% else %}1.5px{% endif %}; + + /* Input */ + --input-email-min-width: 200px; + --input-search-max-width: 650px; + --input-padding-y: 0.8rem; + --input-padding-x: 0.8rem; + --input-padding: var(--input-padding-y) var(--input-padding-x); + --input-box-shadow-width: var(--style-border-width-inputs); + --input-box-shadow: 0 0 0 var(--input-box-shadow-width) var(--color-input-border); + --input-box-shadow-focus: 0 0 0 calc(var(--input-box-shadow-width) + 0.5px) var(--color-input-border); + --input-disabled-background-color: rgb(var(--color-foreground-rgb) / var(--opacity-10)); + --input-disabled-border-color: rgb(var(--color-foreground-rgb) / var(--opacity-5)); + --input-disabled-text-color: rgb(var(--color-foreground-rgb) / var(--opacity-50)); + --input-textarea-min-height: 55px; + + /* Button size */ + --button-size-sm: 30px; + --button-size-md: 36px; + --button-size: var(--minimum-touch-target); + --button-padding-inline: 24px; + --button-padding-block: 16px; + + /* Button font-family */ + --button-font-family-primary: var(--font-{{ settings.type_font_button_primary }}--family); + --button-font-family-secondary: var(--font-{{ settings.type_font_button_secondary }}--family); + + /* Button font-weight */ + --button-font-weight-primary: {{ settings.button_font_weight_primary }}; + --button-font-weight-secondary: {{ settings.button_font_weight_secondary }}; + + /* Button text case */ + --button-text-case: {{ settings.button_text_case }}; + --button-text-case-primary: {{ settings.button_text_case_primary }}; + --button-text-case-secondary: {{ settings.button_text_case_secondary }}; + + /* Borders */ + --border-color: rgb(var(--color-border-rgb) / var(--opacity-50)); + --border-width-sm: 1px; + --border-width-md: 2px; + --border-width-lg: 5px; + + /* Drawers */ + --drawer-inline-padding: 25px; + --drawer-menu-inline-padding: 2.5rem; + --drawer-header-block-padding: 20px; + --drawer-content-block-padding: 10px; + --drawer-header-desktop-top: 0rem; + --drawer-padding: calc(var(--padding-sm) + 7px); + --drawer-height: 100dvh; + --drawer-width: 95vw; + --drawer-max-width: 500px; + + /* Variant Picker Swatches */ + --variant-picker-swatch-width-unitless: {{ settings.variant_swatch_width }}; + --variant-picker-swatch-height-unitless: {{ settings.variant_swatch_height }}; + --variant-picker-swatch-width: {{ settings.variant_swatch_width | append: 'px' }}; + --variant-picker-swatch-height: {{ settings.variant_swatch_height | append: 'px' }}; + --variant-picker-swatch-radius: {{ settings.variant_swatch_radius | append: 'px' }}; + --variant-picker-border-width: {{ settings.variant_swatch_border_width | append: 'px' }}; + --variant-picker-border-style: {{ settings.variant_swatch_border_style }}; + --variant-picker-border-opacity: {{ settings.variant_swatch_border_opacity | append: '%' }}; + + /* Variant Picker Buttons */ + --variant-picker-button-radius: {{ settings.variant_button_radius | append: 'px' }}; + --variant-picker-button-border-width: {{ settings.variant_button_border_width | append: 'px' }}; + + /* Slideshow */ + --slideshow-controls-size: 3.5rem; + --slideshow-controls-icon: 2rem; + --peek-next-slide-size: 3rem; + + /* Utilities */ + --backdrop-opacity: 0.15; + --backdrop-color-rgb: var(--color-shadow-rgb); + --minimum-touch-target: 44px; + --disabled-opacity: 0.5; + --skeleton-opacity: 0.025; + + /* Shapes */ + --shape--circle: circle(50% at center); + --shape--sunburst: polygon(100% 50%,94.62% 55.87%,98.3% 62.94%,91.57% 67.22%,93.3% 75%,85.7% 77.39%,85.36% 85.36%,77.39% 85.7%,75% 93.3%,67.22% 91.57%,62.94% 98.3%,55.87% 94.62%,50% 100%,44.13% 94.62%,37.06% 98.3%,32.78% 91.57%,25% 93.3%,22.61% 85.7%,14.64% 85.36%,14.3% 77.39%,6.7% 75%,8.43% 67.22%,1.7% 62.94%,5.38% 55.87%,0% 50%,5.38% 44.13%,1.7% 37.06%,8.43% 32.78%,6.7% 25%,14.3% 22.61%,14.64% 14.64%,22.61% 14.3%,25% 6.7%,32.78% 8.43%,37.06% 1.7%,44.13% 5.38%,50% 0%,55.87% 5.38%,62.94% 1.7%,67.22% 8.43%,75% 6.7%,77.39% 14.3%,85.36% 14.64%,85.7% 22.61%,93.3% 25%,91.57% 32.78%,98.3% 37.06%,94.62% 44.13%); + --shape--diamond: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); + --shape--blob: polygon(85.349% 11.712%, 87.382% 13.587%, 89.228% 15.647%, 90.886% 17.862%, 92.359% 20.204%, 93.657% 22.647%, 94.795% 25.169%, 95.786% 27.752%, 96.645% 30.382%, 97.387% 33.048%, 98.025% 35.740%, 98.564% 38.454%, 99.007% 41.186%, 99.358% 43.931%, 99.622% 46.685%, 99.808% 49.446%, 99.926% 52.210%, 99.986% 54.977%, 99.999% 57.744%, 99.975% 60.511%, 99.923% 63.278%, 99.821% 66.043%, 99.671% 68.806%, 99.453% 71.565%, 99.145% 74.314%, 98.724% 77.049%, 98.164% 79.759%, 97.433% 82.427%, 96.495% 85.030%, 95.311% 87.529%, 93.841% 89.872%, 92.062% 91.988%, 89.972% 93.796%, 87.635% 95.273%, 85.135% 96.456%, 82.532% 97.393%, 79.864% 98.127%, 77.156% 98.695%, 74.424% 99.129%, 71.676% 99.452%, 68.918% 99.685%, 66.156% 99.844%, 63.390% 99.942%, 60.624% 99.990%, 57.856% 99.999%, 55.089% 99.978%, 52.323% 99.929%, 49.557% 99.847%, 46.792% 99.723%, 44.031% 99.549%, 41.273% 99.317%, 38.522% 99.017%, 35.781% 98.639%, 33.054% 98.170%, 30.347% 97.599%, 27.667% 96.911%, 25.024% 96.091%, 22.432% 95.123%, 19.907% 93.994%, 17.466% 92.690%, 15.126% 91.216%, 12.902% 89.569%, 10.808% 87.761%, 8.854% 85.803%, 7.053% 83.703%, 5.418% 81.471%, 3.962% 79.119%, 2.702% 76.656%, 1.656% 74.095%, 0.846% 71.450%, 0.294% 68.740%, 0.024% 65.987%, 0.050% 63.221%, 0.343% 60.471%, 0.858% 57.752%, 1.548% 55.073%, 2.370% 52.431%, 3.283% 49.819%, 4.253% 47.227%, 5.249% 44.646%, 6.244% 42.063%, 7.211% 39.471%, 8.124% 36.858%, 8.958% 34.220%, 9.711% 31.558%, 10.409% 28.880%, 11.083% 26.196%, 11.760% 23.513%, 12.474% 20.839%, 13.259% 18.186%, 14.156% 15.569%, 15.214% 13.012%, 16.485% 10.556%, 18.028% 8.261%, 19.883% 6.211%, 22.041% 4.484%, 24.440% 3.110%, 26.998% 2.057%, 29.651% 1.275%, 32.360% 0.714%, 35.101% 0.337%, 37.859% 0.110%, 40.624% 0.009%, 43.391% 0.016%, 46.156% 0.113%, 48.918% 0.289%, 51.674% 0.533%, 54.425% 0.837%, 57.166% 1.215%, 59.898% 1.654%, 62.618% 2.163%, 65.322% 2.750%, 68.006% 3.424%, 70.662% 4.197%, 73.284% 5.081%, 75.860% 6.091%, 78.376% 7.242%, 80.813% 8.551%, 83.148% 10.036%, 85.349% 11.712%); + + /* Buy buttons */ + --height-buy-buttons: calc(var(--padding-lg) * 2 + var(--icon-size-sm)); + + /* Card image height variables */ + --height-small: 10rem; + --height-medium: 11.5rem; + --height-large: 13rem; + --height-full: 100vh; + + @media screen and (min-width: 750px) { + --height-small: 17.5rem; + --height-medium: 21.25rem; + --height-large: 25rem; + } + + /* Modal */ + --modal-max-height: 65dvh; + + /* Card styles for search */ + --card-bg-hover: rgb(var(--color-foreground-rgb) / var(--opacity-5)); + --card-border-hover: rgb(var(--color-foreground-rgb) / var(--opacity-30)); + --card-border-focus: rgb(var(--color-foreground-rgb) / var(--opacity-10)); + + /* Cart */ + --cart-primary-font-family: var(--font-body--family); + --cart-primary-font-style: var(--font-body--style); + --cart-primary-font-weight: var(--font-body--weight); + --cart-secondary-font-family: var(--font-{{ settings.cart_price_font }}--family); + --cart-secondary-font-style: var(--font-{{ settings.cart_price_font }}--style); + --cart-secondary-font-weight: var(--font-{{ settings.cart_price_font }}--weight); + } +{% endstyle %} diff --git a/snippets/timeline-scope.liquid b/snippets/timeline-scope.liquid new file mode 100644 index 000000000..67c1cfe02 --- /dev/null +++ b/snippets/timeline-scope.liquid @@ -0,0 +1,11 @@ +{%- liquid + assign timeline_scope = '' + + for index in (1..count) + assign scope = '--prefix-index, ' | replace: 'prefix', prefix | replace: 'index', index + assign timeline_scope = timeline_scope | append: scope + endfor + + assign timeline_scope = timeline_scope | strip | split: ',' | compact | join: ',' +-%} +{{- timeline_scope -}} diff --git a/snippets/typography-style.liquid b/snippets/typography-style.liquid new file mode 100644 index 000000000..e89876d63 --- /dev/null +++ b/snippets/typography-style.liquid @@ -0,0 +1,75 @@ +{%- comment -%} + Intended for blocks and sections that provide values for all the referenced settings. + +
    + + Accepts: + settings: {settings || section.settings} +{%- endcomment -%} + +{% assign preset = preset | default: settings.type_preset %} + +{%- capture variables -%} + {%- if preset != 'rte' and settings.color != "" -%} + --color: {{ settings.color }}; + {%- endif -%} + {%- if preset == 'custom' -%} + {% liquid + unless type + comment + When choosing to customize the font, picking a specific font size + determines the type of text block. + endcomment + if settings.font_size != '' + assign font_size_value = settings.font_size | split: 'rem' | first | times: 1.0 + + if font_size_value > 4.5 + assign type = 'display' + elsif font_size_value > 3.5 + assign type = 'heading' + else + assign type = 'body' + endif + endif + endunless + %} + {%- if settings.font_size != blank -%} + {%- liquid + assign font_size_rem = settings.font_size | split: 'rem' | first | times: 1.0 + assign fluid_size_cutoff_rem = 3.0 + + if font_size_rem >= fluid_size_cutoff_rem + assign target_viewport = 1400 + assign vw_value = font_size_rem | times: 16 | divided_by: target_viewport | times: 100 + + assign scale_factor = font_size_rem | divided_by: fluid_size_cutoff_rem + assign base_min_rem = 3.0 + assign scaling_bonus_rem = scale_factor | minus: 1 | times: 0.25 + assign dynamic_min_rem = base_min_rem | plus: scaling_bonus_rem + endif + -%} + {%- if font_size_rem >= fluid_size_cutoff_rem -%} + --font-size: clamp({{ dynamic_min_rem }}rem, {{ vw_value }}vw, {{ settings.font_size }}); + {%- else -%} + --font-size: {{ settings.font_size }}; + {%- endif -%} + {%- endif -%} + {%- if settings.weight != blank -%} + --font-weight: {{ settings.weight }}; + {% else %} + --font-weight: {{ settings.font | replace: 'family', 'weight' }}; + {%- endif -%} + --font-family: {{ settings.font }}; + --text-transform: {{ settings.case }}; + --text-wrap: {{ settings.wrap }}; + {% if settings.type_preset == 'custom' and settings.font_size == blank %} + --line-height--display: var(--line-height--display-{{ settings.line_height }}); + --line-height--heading: var(--line-height--heading-{{ settings.line_height }}); + --line-height--body: var(--line-height--body-{{ settings.line_height }}); + {% else %} + --line-height: var(--line-height--{{ type }}-{{ settings.line_height }}); + {% endif %} + --letter-spacing: var(--letter-spacing--{{ type }}-{{ settings.letter_spacing }}); + {%- endif -%} +{%- endcapture -%} +{{- variables | strip | strip_newlines -}} diff --git a/snippets/unit-price.liquid b/snippets/unit-price.liquid new file mode 100644 index 000000000..6d9ba4d59 --- /dev/null +++ b/snippets/unit-price.liquid @@ -0,0 +1,16 @@ +{%- doc -%} + Renders the unit price, including its measurement. + + @param {object} price - The unit price (money or string). + @param {object} measurement - The unit_price_measurement object. + + @example + {% render 'unit-price', price: variant.unit_price, measurement: variant.unit_price_measurement %} + + @example + {% render 'unit-price', price: line_item.unit_price | money_with_currency, measurement: line_item.unit_price_measurement } +{%- enddoc -%} + + {{ 'accessibility.unit_price' | t }} + {{ price | unit_price_with_measurement: measurement }} + diff --git a/snippets/util-autofill-img-size-attr.liquid b/snippets/util-autofill-img-size-attr.liquid new file mode 100644 index 000000000..79dbf1e40 --- /dev/null +++ b/snippets/util-autofill-img-size-attr.liquid @@ -0,0 +1,75 @@ +{%- doc -%} + Echo a sizes attribute for an tag based on a minimum image size. + + @param {number} card_size - The minimum pixel-width of the product card. + @param {number} [card_gap] - The pixel-width of the gap between product cards. + @param {number} [max_breakpoint] - The maximum pixel-width to calculate breakpoints for. + @param {number} [min_breakpoint] - The minimum pixel-width before defaulting to 50vw. + + @example + {% capture size_attribute %} + {% render 'util-autofill-img-size-attr' card_size: 400 %} + {% endcapture %} + {% assign size_attribute = size_attribute | strip %} + {{ image_url | image_tag: sizes: size_attribute }} +{%- enddoc -%} + +{% liquid + # Defense: ensure card_size and card_gap are numbers + assign card_size = card_size | strip | replace: 'px', '' | plus: 0 + + if card_gap + assign card_gap = card_gap | strip | replace: 'px', '' | plus: 0 + else + assign card_gap = 0 + endif + + assign card_size_with_gap = card_size | plus: card_gap + + assign max_breakpoint = max_breakpoint | default: 2000 + + assign min_breakpoint = min_breakpoint | default: 750 + + # Calculate maximum number of columns at max width + assign max_cols = max_breakpoint | divided_by: card_size_with_gap | floor + + assign sizes_attr = '' + + # Calculate breakpoints dynamically based on card size + # Start with max columns and work down + for i in (1..max_cols) + # Current number of columns we're calculating for + assign current_cols = max_cols | minus: i | plus: 1 + + # Skip if we're down to 1 column + if current_cols < 2 + break + endif + + # Calculate the minimum width needed for this many columns + assign min_width_needed = current_cols | times: card_size_with_gap + + if min_width_needed < min_breakpoint + break + endif + + assign percentage = 100 | divided_by: current_cols + + # Build up the sizes attribute + if sizes_attr != '' + assign sizes_attr = sizes_attr | append: ', ' + endif + assign sizes_attr = sizes_attr | append: '(min-width: ' | append: min_width_needed | append: 'px) ' | append: percentage | append: 'vw' + endfor + + # Add tablet size (50vw) and mobile size (100vw) fallbacks + if sizes_attr != '' + assign sizes_attr = sizes_attr | append: ', ' + endif + assign sizes_attr = sizes_attr | append: '(min-width: ' | append: min_breakpoint | append: 'px) 50vw' + + assign sizes_attr = sizes_attr | append: ', 100vw' + + # Echo the sizes attribute + echo sizes_attr +%} diff --git a/snippets/util-mega-menu-img-sizes-attr.liquid b/snippets/util-mega-menu-img-sizes-attr.liquid new file mode 100644 index 000000000..a8ce818a5 --- /dev/null +++ b/snippets/util-mega-menu-img-sizes-attr.liquid @@ -0,0 +1,87 @@ +{%- doc -%} + Calculate the sizes attribute for mega menu images based on menu type and grid configuration. + + @param {string} menu_content_type - Type of menu: 'collection_images', 'featured_products', or 'featured_collections' + @param {object} settings - Theme settings object containing page width configuration + @param {number} [grid_columns] - Number of grid columns for the mega menu + @param {number} [grid_columns_tablet] - Number of grid columns for tablet view + @param {number} [grid_columns_collection_images] - Grid columns when menu_content_type is 'collection_images' with < 5 items + @param {number} [parent_links_size] - Number of parent links (for collection images special case) + @param {number} [columns_per_item] - Columns each item occupies (2 for collection images, 1 for products) + + @example + {% capture image_sizes %} + {% render 'util-mega-menu-img-sizes-attr', + menu_content_type: 'collection_images', + settings: settings, + grid_columns: 8, + grid_columns_tablet: 4, + columns_per_item: 2 + %} + {% endcapture %} + + {{ image | image_url: width: 1024 | image_tag: sizes: image_sizes }} +{%- enddoc -%} + +{% liquid + # Early exit for featured collections + if menu_content_type == 'featured_collections' + echo '300px' + break + endif + + # Define breakpoints and max widths based on page width setting + case settings.page_width + when 'narrow' + assign page_max_width = '90rem' + assign breakpoint = '95rem' + when 'normal' + assign page_max_width = '120rem' + assign breakpoint = '125rem' + when 'wide' + assign page_max_width = '150rem' + assign breakpoint = '155rem' + endcase + + # Common values + # Gap between items in pixels (numeric for calculations) + # Page margins (with unit for direct use in calc()) + assign gap = 20 + assign margins = '80px' + assign grid_tablet = grid_columns_tablet | default: 4 + + # Set up grid configuration based on menu type + case menu_content_type + when 'collection_images' + assign cols_per_item = columns_per_item | default: 2 + assign grid_desktop = grid_columns | default: 8 + + # Special case: fewer than 5 collection images + if parent_links_size < 5 + assign grid_desktop = grid_columns_collection_images | default: grid_desktop + endif + + when 'featured_products' + assign cols_per_item = 1 + assign grid_desktop = grid_columns | default: 6 + endcase + + # Calculate gaps for each breakpoint + assign items_desktop = grid_desktop | divided_by: cols_per_item + assign items_tablet = grid_tablet | divided_by: cols_per_item + assign gaps_desktop_px = items_desktop | minus: 1 | times: gap | append: 'px' + assign gaps_tablet_px = items_tablet | minus: 1 | times: gap | append: 'px' +%} + +{%- capture sizes -%} + {%- comment -%} Large viewports with fixed page width {%- endcomment -%} + (min-width: {{ breakpoint }}) calc(({{ page_max_width }} - {{ margins }} - {{ gaps_desktop_px }}) * {{ cols_per_item }} / {{ grid_desktop }}), + + {%- comment -%} Desktop viewports {%- endcomment -%} + (min-width: 990px) calc((100vw - {{ margins }} - {{ gaps_desktop_px }}) * {{ cols_per_item }} / {{ grid_desktop }}), + + {%- comment -%} Tablet {%- endcomment -%} + calc((100vw - {{ margins }} - {{ gaps_tablet_px }}) / {{ grid_tablet }}) +{%- endcapture -%} + +{{ sizes | strip_newlines | strip }} diff --git a/snippets/util-product-grid-card-size.liquid b/snippets/util-product-grid-card-size.liquid new file mode 100644 index 000000000..fdbcd0f42 --- /dev/null +++ b/snippets/util-product-grid-card-size.liquid @@ -0,0 +1,45 @@ +{%- doc -%} + Output the minimum product card size for cards in a product grid (main collection and search results). + + @param {object} section - Section object that contains the product card block. + + @example + {% capture product_card_size %} + {% render 'util-product-grid-card-size' section: section %} + {% endcapture %} +{%- enddoc -%} + +{% liquid + if section.settings.layout_type == 'organic' + if section.settings.product_grid_width == 'centered' + assign product_card_size = '250px' + else + assign product_card_size = '260px' + endif + elsif section.settings.product_grid_width == 'centered' + # Hardcoded values for product card size when width set to 'centered' + case section.settings.product_card_size + when 'small' + assign product_card_size = '165px' + when 'medium' + assign product_card_size = '250px' + when 'large' + assign product_card_size = '340px' + when 'extra-large' + assign product_card_size = '480px' + endcase + else + # Hardcoded values for product card size when width set to 'full-width' + case section.settings.product_card_size + when 'small' + assign product_card_size = '180px' + when 'medium' + assign product_card_size = '260px' + when 'large' + assign product_card_size = '365px' + when 'extra-large' + assign product_card_size = '530px' + endcase + endif + echo product_card_size +%} diff --git a/snippets/util-product-media-sizes-attr.liquid b/snippets/util-product-media-sizes-attr.liquid new file mode 100644 index 000000000..50478898d --- /dev/null +++ b/snippets/util-product-media-sizes-attr.liquid @@ -0,0 +1,141 @@ +{%- doc -%} + Calculate the sizes attribute for product media images in the product media gallery. + + @param {object} block - Block object containing media gallery settings + @param {object} section - Section object containing layout settings + @param {object} settings - Theme settings object containing page width configuration + @param {boolean} [is_first_image] - Whether this is the first image (for large_first_image mode) + @param {boolean} [is_single_column] - Whether the layout is single column (carousel or one-column grid) + @param {boolean} [needs_both_sizes] - Whether we need to calculate different sizes for first and other images + + @example + {% capture media_sizes %} + {% render 'util-product-media-sizes-attr', block: block, section: section, settings: settings, is_single_column: is_single_column %} + {% endcapture %} + {% assign media_sizes = media_sizes | strip %} + {{ media | image_url: width: 800 | image_tag: sizes: media_sizes }} +{%- enddoc -%} + +{%- liquid + # Constants + assign page_margin = '40px' + # Section gap divided by 2 (used for single column layouts) + assign gap_half = section.settings.gap | divided_by: 2 | append: 'px' + # Section gap divided by 4 (used for two column layouts where each column gets half of the half gap) + assign gap_quarter = section.settings.gap | divided_by: 4 | append: 'px' + # Image gap divided by 2 (space between images in grid) + assign image_gap_half = block.settings.image_gap | divided_by: 2 | append: 'px' + + assign is_single_column = is_single_column | default: false + assign needs_both_sizes = needs_both_sizes | default: false + + # Determine which size calculation to use + assign calculate_single_column = false + assign calculate_grid_column = false + + if needs_both_sizes + if is_first_image + assign calculate_single_column = true + else + assign calculate_grid_column = true + endif + elsif is_single_column + assign calculate_single_column = true + else + assign calculate_grid_column = true + endif + + # Set up default sizes + if calculate_single_column + # Default for carousel or single column grid (or first image in large_first_image mode) + if section.settings.equal_columns == false + assign default_sizes = '(min-width: 750px) calc(100vw - 25rem - [gap_half]), 100vw' | replace: '[gap_half]', gap_half + else + assign default_sizes = '(min-width: 750px) calc(50vw - [gap_half]), 100vw' | replace: '[gap_half]', gap_half + endif + else + # Default for two column grid - includes image gap and quarter section gap + if section.settings.equal_columns == false + assign default_sizes = '(min-width: 750px) calc((100vw - 25rem) / 2 - [gap_quarter] - [image_gap_half]), 100vw' | replace: '[gap_quarter]', gap_quarter | replace: '[image_gap_half]', image_gap_half + else + assign default_sizes = '(min-width: 750px) calc(50vw / 2 - [gap_quarter] - [image_gap_half]), 100vw' | replace: '[gap_quarter]', gap_quarter | replace: '[image_gap_half]', image_gap_half + endif + endif + + # Override for center-aligned content + if section.settings.content_width == 'content-center-aligned' + # Define breakpoints and base sizes based on page width + # Breakpoints are the page width setting + margin (where 2 x margin is 80px = 5rem) + case settings.page_width + when 'narrow' + assign breakpoint = '95rem' + assign media_base_size_equal_columns = '45rem' + assign media_base_size_unequal_columns = '65rem' + when 'normal' + assign breakpoint = '125rem' + assign media_base_size_equal_columns = '60rem' + assign media_base_size_unequal_columns = '95rem' + when 'wide' + assign breakpoint = '155rem' + assign media_base_size_equal_columns = '75rem' + assign media_base_size_unequal_columns = '125rem' + endcase + + # Select the appropriate base size + if section.settings.equal_columns + assign media_base_size = media_base_size_equal_columns + else + assign media_base_size = media_base_size_unequal_columns + endif + + # Calculate large screen size base + if block.settings.extend_media + assign large_size_base = '[media_base_size] + (100vw - [breakpoint])' | replace: '[media_base_size]', media_base_size | replace: '[breakpoint]', breakpoint + else + assign large_size_base = media_base_size + endif + + # Calculate medium screen size + if section.settings.equal_columns + assign medium_base = '50vw' + else + assign medium_base = '100vw - 25rem' + endif + + # Build calculation based on column type + if calculate_grid_column + # Grid column calculation - includes image gap + assign medium_base = '([medium_base]) / 2' | replace: '[medium_base]', medium_base + # Build the complete large size expression + assign large_size_expr = '([large_size_base]) / 2 - [image_gap_half]' | replace: '[large_size_base]', large_size_base | replace: '[image_gap_half]', image_gap_half + assign large_size = 'calc([large_size_expr])' | replace: '[large_size_expr]', large_size_expr + + if block.settings.extend_media + assign medium_size = 'calc([medium_base] - [page_margin] - [gap_quarter] - [image_gap_half] + [page_margin])' | replace: '[medium_base]', medium_base | replace: '[page_margin]', page_margin | replace: '[gap_quarter]', gap_quarter | replace: '[image_gap_half]', image_gap_half + else + assign medium_size = 'calc([medium_base] - [page_margin] - [gap_quarter] - [image_gap_half])' | replace: '[medium_base]', medium_base | replace: '[page_margin]', page_margin | replace: '[gap_quarter]', gap_quarter | replace: '[image_gap_half]', image_gap_half + endif + assign sizes = '(min-width: [breakpoint]) [large_size], (min-width: 750px) [medium_size], 100vw' | replace: '[breakpoint]', breakpoint | replace: '[large_size]', large_size | replace: '[medium_size]', medium_size + else + # Single column calculation + if block.settings.extend_media + assign large_size = 'calc([large_size_base])' | replace: '[large_size_base]', large_size_base + else + assign large_size = large_size_base + endif + + if block.settings.extend_media + assign medium_size = 'calc([medium_base] - [page_margin] - [gap_half])' | replace: '[medium_base]', medium_base | replace: '[page_margin]', page_margin | replace: '[gap_half]', gap_half + else + assign medium_size = 'calc([medium_base] - [page_margin] - [gap_half] - [page_margin])' | replace: '[medium_base]', medium_base | replace: '[page_margin]', page_margin | replace: '[gap_half]', gap_half + endif + assign sizes = '(min-width: [breakpoint]) [large_size], (min-width: 750px) [medium_size], 100vw' | replace: '[breakpoint]', breakpoint | replace: '[large_size]', large_size | replace: '[medium_size]', medium_size + endif + else + # Use default sizes + assign sizes = default_sizes + endif + + # Echo the sizes attribute + echo sizes +-%} diff --git a/snippets/variant-main-picker.liquid b/snippets/variant-main-picker.liquid new file mode 100644 index 000000000..2970e04bf --- /dev/null +++ b/snippets/variant-main-picker.liquid @@ -0,0 +1,477 @@ +{%- doc -%} + Renders a default variant picker, used to display the variant picker in the variants block. + + @param {object} product_resource - The product object. +{%- enddoc -%} + +{% unless product_resource.has_only_default_variant %} + {% liquid + assign button_background_brightness = section.settings.color_scheme.settings.foreground | color_brightness + if button_background_brightness < 105 + assign strikethrough_color_mix = '#000' + else + assign strikethrough_color_mix = '#fff' + endif + %} + +
    + {%- for product_option in product_resource.options_with_values -%} + {%- liquid + assign swatch_count = product_option.values | map: 'swatch' | compact | size + assign variant_style = block.settings.variant_style + + if swatch_count > 0 and block.settings.show_swatches + if block.settings.variant_style == 'dropdown' + assign variant_style = 'swatch_dropdown' + else + assign variant_style = 'swatch' + endif + endif + + if variant_style == 'buttons' and settings.variant_button_width == 'equal-width-buttons' + assign fieldset_id = section.id | append: '-' | append: product_resource.id | append: '-' | append: product_option.name | handleize + assign option_id_attribute = 'data-option-id="' | append: fieldset_id | append: '"' + assign longest_value = 0 + endif + -%} + + {%- if variant_style == 'swatch' or block.settings.variant_style == 'buttons' -%} +
    + + {{ product_option.name | escape -}} + {%- if variant_style == 'swatch' -%} + {{ product_option.selected_value }} + {%- endif %} + + {%- for product_option_value in product_option.values -%} + {% if product_option_value.size > longest_value and option_id_attribute %} + {% assign longest_value = product_option_value.size %} + {% endif %} + + {%- endfor -%} + {% if option_id_attribute %} + {% style %} + [data-option-id="{{ fieldset_id }}"] { + --variant-ch: {{ longest_value }}ch; + } + {% endstyle %} + {% endif %} +
    + {%- elsif block.settings.variant_style == 'dropdowns' -%} + {% + # There is an opportunity to build a custom select component that will allow us to style the select element further (animation for dropdown, swatches shown in the dropdown options, etc) + # It's too bad as it mean rebuilding baked in behaviours but I think we've already done that for the locale selectors + # in dawn. So it might mean more time spent in setting it up but worth it for future updates/styling. + %} + {% liquid + assign property_being_updated = false + if settings.variant_swatch_width != settings.variant_swatch_height + assign property_being_updated = true + # (original width / original height) x new height (20px at the moment) = new width + assign new_width = settings.variant_swatch_width | times: 1.0 | divided_by: settings.variant_swatch_height | times: 20 + endif + %} + +
    + +
    + + +
    +
    + {%- endif -%} + {%- endfor -%} + + +
    +
    +{% endunless %} + +{% stylesheet %} + .variant-picker { + width: 100%; + } + + .variant-picker__form { + display: flex; + flex-direction: column; + gap: var(--padding-lg); + width: 100%; + } + + .variant-picker[data-shopify-visual-preview] { + min-width: 300px; + padding-inline-start: max(4px, var(--padding-inline-start)); + } + + .variant-option { + --options-border-radius: var(--variant-picker-button-radius); + --options-border-width: var(--variant-picker-button-border-width); + --variant-option-padding-inline: var(--padding-md); + } + + .variant-option--swatches { + --options-border-radius: var(--variant-picker-swatch-radius); + + width: 100%; + } + + .variant-option--swatches-disabled { + pointer-events: none; + cursor: not-allowed; + } + + .variant-option--swatches > overflow-list { + justify-content: var(--product-swatches-alignment); + + @media (max-width: 749px) { + justify-content: var(--product-swatches-alignment-mobile); + } + } + + .variant-option--buttons { + display: flex; + flex-wrap: wrap; + gap: var(--gap-sm); + margin: 0; + padding: 0; + border: none; + } + + .variant-option--buttons legend { + padding: 0; + margin-block-end: var(--margin-xs); + } + + .variant-option__swatch-value { + padding-inline-start: var(--padding-xs); + color: rgb(var(--color-foreground-rgb) / var(--opacity-70)); + } + + .variant-option__button-label { + --variant-picker-stroke-color: var(--color-variant-border); + + display: flex; + flex: 0 0 calc(3ch + 1.3em); + align-items: center; + position: relative; + padding-block: var(--padding-sm); + padding-inline: var(--padding-lg); + border: var(--style-border-width) solid var(--color-variant-border); + border-radius: var(--options-border-radius); + border-width: var(--options-border-width); + overflow: clip; + justify-content: center; + min-height: calc(3ch + 1.3em); + min-width: fit-content; + white-space: nowrap; + background-color: var(--color-variant-background); + color: var(--color-variant-text); + transition: background-color var(--animation-speed) var(--animation-easing), + border-color var(--animation-speed) var(--animation-easing); + + &:hover { + background-color: var(--color-variant-hover-background); + border-color: var(--color-variant-hover-border); + color: var(--color-variant-hover-text); + } + + @media screen and (min-width: 750px) { + padding: var(--padding-xs) var(--variant-option-padding-inline); + } + } + + .variant-option__button-label__text { + text-align: left; + text-wrap: auto; + } + + .variant-option--equal-width-buttons { + --variant-min-width: clamp(44px, calc(var(--variant-option-padding-inline) * 2 + var(--variant-ch)), 100%); + + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--variant-min-width), 1fr)); + + .variant-option__button-label { + min-width: var(--variant-min-width); + } + + .variant-option__button-label__text { + text-align: center; + text-wrap: balance; + } + } + + .variant-option__button-label:has(:focus-visible) { + --variant-picker-stroke-color: var(--color-foreground); + + border-color: var(--color-foreground); + outline: var(--focus-outline-width) solid var(--color-foreground); + outline-offset: var(--focus-outline-offset); + } + + .variant-option__button-label--has-swatch { + padding: 0; + border: none; + display: block; + flex-basis: auto; + min-height: auto; + } + + .variant-option__button-label:has(:checked) { + color: var(--color-selected-variant-text); + background-color: var(--color-selected-variant-background); + border-color: var(--color-selected-variant-border); + transition: background-color var(--animation-speed) var(--animation-easing), + border-color var(--animation-speed) var(--animation-easing); + + &:hover { + background-color: var(--color-selected-variant-hover-background); + border-color: var(--color-selected-variant-hover-border); + color: var(--color-selected-variant-hover-text); + } + } + + .variant-option__button-label:has([data-option-available='false']) { + color: rgb(var(--color-variant-text-rgb) / var(--opacity-60)); + } + + .facets__inputs-list--swatches-grid .variant-option__button-label--has-swatch:hover .swatch { + outline: var(--focus-outline-width) solid rgb(var(--color-foreground-rgb) / var(--opacity-35-55)); + outline-offset: var(--focus-outline-offset); + } + + .facets__inputs-list--swatches-grid .variant-option__button-label:has(:focus-visible) .swatch { + outline: var(--focus-outline-width) solid currentcolor; + outline-offset: var(--focus-outline-offset); + } + + .facets__inputs-list--swatches-grid .variant-option__button-label:has(:focus-visible) { + outline: none; + } + + .facets__inputs-list--swatches-grid .variant-option__button-label--has-swatch:hover { + outline: none; + } + + .variant-option__button-label--has-swatch:hover { + outline: var(--focus-outline-width) solid rgb(var(--color-foreground-rgb) / var(--opacity-35-55)); + outline-offset: var(--focus-outline-offset); + } + + .facets__inputs-list--swatches-grid .variant-option__button-label--has-swatch:has(:checked) { + outline: none; + } + + .facets__inputs-list--swatches-grid .variant-option__button-label--has-swatch:has(:checked) .swatch { + outline: var(--focus-outline-width) solid var(--color-foreground); + outline-offset: var(--focus-outline-offset); + } + + .variant-option__button-label--has-swatch:has(:checked) { + outline: var(--focus-outline-width) solid var(--color-foreground); + outline-offset: var(--focus-outline-offset); + } + + .variant-option__button-label:has([data-option-available='false']):has(:checked) { + --variant-picker-stroke-color: rgb(var(--color-variant-text-rgb) / var(--opacity-60)); + + background-color: inherit; + color: rgb(var(--color-variant-text-rgb) / var(--opacity-60)); + border-color: var(--color-selected-variant-border); + } + + .variant-option__button-label input, + .variant-option--images input { + /* remove the checkbox from the page flow */ + position: absolute; + + /* set the dimensions to match those of the label */ + inset: 0; + + /* hide it */ + opacity: 0; + margin: 0; + cursor: pointer; + width: 100%; + height: 100%; + } + + .variant-option__button-label svg { + position: absolute; + top: 0; + left: 0; + cursor: pointer; + pointer-events: none; + stroke-width: var(--style-border-width); + stroke: var(--variant-picker-stroke-color); + } + + .variant-option__select-wrapper { + display: flex; + position: relative; + border: var(--style-border-width-inputs) solid var(--color-border); + border-radius: var(--style-border-radius-inputs); + align-items: center; + margin-top: var(--margin-2xs); + overflow: clip; + transition: background-color var(--animation-speed) var(--animation-easing), + border-color var(--animation-speed) var(--animation-easing); + } + + .variant-option__select-wrapper:has(.swatch) { + --variant-picker-swatch-width: 20px; + --variant-picker-swatch-height: 20px; + } + + .variant-option__select-wrapper:hover { + border-color: var(--color-variant-hover-border); + } + + .variant-option__select:focus-visible { + outline: var(--focus-outline-width) solid currentcolor; + outline-offset: var(--focus-outline-offset); + } + + .variant-option__select { + padding-block: var(--padding-md); + padding-inline: var(--padding-lg) calc(var(--padding-lg) + var(--icon-size-2xs)); + appearance: none; + border: 0; + width: 100%; + margin: 0; + cursor: pointer; + } + + .variant-option__select-wrapper .icon { + position: absolute; + right: var(--padding-md); + top: 50%; + transform: translateY(-50%); + width: var(--icon-size-2xs); + height: var(--icon-size-2xs); + pointer-events: none; + } + + .variant-option__select--has-swatch { + padding-inline-start: calc((2 * var(--padding-sm)) + var(--variant-picker-swatch-width)); + } + + .variant-option__select-wrapper .swatch { + position: absolute; + top: 50%; + left: var(--padding-md); + transform: translateY(-50%); + } + + .variant-picker--center, + .variant-picker--center .variant-option { + text-align: center; + align-items: center; + justify-content: center; + width: 100%; + } + + .variant-picker--right, + .variant-picker--right .variant-option { + text-align: right; + justify-content: right; + } +{% endstylesheet %} diff --git a/snippets/variant-quick-add.liquid b/snippets/variant-quick-add.liquid new file mode 100644 index 000000000..1a368b9bd --- /dev/null +++ b/snippets/variant-quick-add.liquid @@ -0,0 +1,101 @@ +{%- doc -%} + Renders a quick-add variant picker, used to display the variant picker in the quick-add modal. + + @param {object} product_resource - The product object. +{%- enddoc -%} + + + {%- for product_option in product_resource.options_with_values -%} + {%- liquid + assign swatch_count = product_option.values | map: 'swatch' | compact | size + assign variant_style = '' + + if swatch_count > 0 + assign variant_style = 'swatch' + else + assign fieldset_id = section.id | append: '-' | append: product_resource.id | append: '-' | append: product_option.name | handleize + assign option_id_attribute = 'data-option-id="' | append: fieldset_id | append: '"' + assign longest_value = 0 + endif + -%} + +
    + + {{ product_option.name | escape -}} + {%- if variant_style == 'swatch' and product_resource.options_with_values.size > 1 -%} + {{ product_option.selected_value }} + {%- endif %} + + {%- for product_option_value in product_option.values -%} + {% if product_option_value.size > longest_value and option_id_attribute %} + {% assign longest_value = product_option_value.size %} + {% endif %} + + {% liquid + assign featured_media = product_option_value.variant.featured_media + + # If the variant has no featured media, and we have a combined listing product, then fall back to using the + # featured media of the child product that is linked to this option value. + if featured_media == blank and product_option_value.product_url + assign featured_media = product_option_value.variant.product.featured_media + endif + %} + + + {%- endfor -%} + {% if option_id_attribute %} + {% style %} + [data-option-id="{{ fieldset_id }}"] { + --variant-ch: {{ longest_value }}ch; + } + {% endstyle %} + {% endif %} +
    + {%- endfor -%} + + +
    diff --git a/snippets/variant-swatches.liquid b/snippets/variant-swatches.liquid new file mode 100644 index 000000000..1ae7ff2f8 --- /dev/null +++ b/snippets/variant-swatches.liquid @@ -0,0 +1,145 @@ +{%- doc -%} + Renders a swatches variant picker, used within the product-swatches block. + + @param {object} product_resource - The product object, which contains variants and options. + @param {boolean} has_option_selected - Whether an option is already selected. + + @example + {% render 'variant-swatches', product_resource: product %} +{%- enddoc -%} + + +
    + {%- for product_option in product_resource.options_with_values -%} + {%- liquid + assign swatch_count = product_option.values | map: 'swatch' | compact | size + -%} + + {% if swatch_count == 0 %} + {% continue %} + {% endif %} + + {%- liquid + assign product_has_combined_listing = closest.product.options_with_values | map: 'values' | map: 'product_url' | compact | size + + # Only apply our custom logic if nothing is selected (initial page load) + if has_option_selected != true and product_has_combined_listing == 0 + # Logic to determine which swatch should be pre-selected + assign first_image = product_resource.media.first + assign variant_images = product_resource.images | where: 'attached_to_variant?', true + assign swatch_to_preselect = null + if swatch_count == 1 + # Single swatch: Always pre-select it + assign swatch_to_preselect = product_option.values.first + elsif swatch_count > 1 + if first_image and variant_images contains first_image + # First image is a variant image - find which variant it belongs to + for option_value in product_option.values + if option_value.variant.featured_media.id == first_image.id + assign swatch_to_preselect = option_value + break + endif + endfor + elsif variant_images.size == 0 + # No variants have images - pre-select first swatch + assign swatch_to_preselect = product_option.values.first + else + assign none_checked = true + endif + # else: First image is NOT a variant image - don't pre-select any swatch + endif + endif + + # Identify which option position this swatch option is + # Use product_option.position which is the actual position among ALL options + assign swatch_option_position = product_option.position + assign swatch_option_key = 'option' | append: swatch_option_position + -%} + +
    + {% capture children %} + {%- for product_option_value in product_option.values -%} + {% liquid + assign featured_media = product_option_value.variant.featured_media + + # If the variant has no featured media, and we have a combined listing product, then fall back to using the + # featured media of the child product that is linked to this option value. + if featured_media == blank and product_option_value.product_url + assign featured_media = product_option_value.variant.product.featured_media + endif + + if has_option_selected != true and product_has_combined_listing == 0 + # Determine if this swatch should be checked + assign is_checked = false + if swatch_to_preselect != nil and swatch_to_preselect.id == product_option_value.id + assign is_checked = true + assign none_checked = false + endif + endif + + # Use Liquid filters to check if any variant with this swatch option value is available + # swatch_option_key was set in the parent loop (e.g., 'option1', 'option2', etc.) + assign available_variants = product_resource.variants | where: swatch_option_key, product_option_value | where: 'available', true + assign first_available_variant = available_variants | first + assign available_count = available_variants | size + %} + +
  • + +
  • + {%- endfor -%} +
  • + +
  • + {% endcapture %} + + {% render 'overflow-list', children: children, ref: 'overflowList', defer: true %} +
    + {%- endfor -%} + +
    +
    diff --git a/snippets/video.liquid b/snippets/video.liquid new file mode 100644 index 000000000..ef87051c1 --- /dev/null +++ b/snippets/video.liquid @@ -0,0 +1,215 @@ +{%- doc -%} + Renders a video element, from a video object (