spree_storefront 5.2.5 → 5.3.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +5 -3
  3. data/app/controllers/concerns/spree/cart_methods.rb +4 -3
  4. data/app/controllers/concerns/spree/storefront/pagination_concern.rb +23 -0
  5. data/app/controllers/spree/account/gift_cards_controller.rb +1 -1
  6. data/app/controllers/spree/account/orders_controller.rb +2 -2
  7. data/app/controllers/spree/account/store_credits_controller.rb +4 -4
  8. data/app/controllers/spree/addresses_controller.rb +2 -2
  9. data/app/controllers/spree/checkout_controller.rb +14 -8
  10. data/app/controllers/spree/line_items_controller.rb +3 -3
  11. data/app/controllers/spree/order_status_controller.rb +1 -1
  12. data/app/controllers/spree/orders_controller.rb +3 -3
  13. data/app/controllers/spree/posts_controller.rb +1 -1
  14. data/app/controllers/spree/products_controller.rb +1 -0
  15. data/app/controllers/spree/search_controller.rb +1 -1
  16. data/app/controllers/spree/store_controller.rb +28 -33
  17. data/app/helpers/spree/canonical_url_helper.rb +27 -0
  18. data/app/helpers/spree/checkout_analytics_helper.rb +1 -4
  19. data/app/helpers/spree/checkout_helper.rb +3 -1
  20. data/app/helpers/spree/page_helper.rb +2 -4
  21. data/app/helpers/spree/products_helper.rb +31 -8
  22. data/app/helpers/spree/storefront_helper.rb +2 -1
  23. data/app/helpers/spree/taxons_helper.rb +3 -1
  24. data/app/helpers/spree/theme_helper.rb +1 -1
  25. data/app/views/devise/sessions/new.html.erb +1 -1
  26. data/app/views/spree/checkout/_address.html.erb +2 -2
  27. data/app/views/spree/checkout/_coupon_code.html.erb +2 -2
  28. data/app/views/spree/checkout/_delivery.html.erb +2 -2
  29. data/app/views/spree/checkout/_delivery_shipping_rate.html.erb +1 -1
  30. data/app/views/spree/checkout/_line_items.html.erb +3 -5
  31. data/app/views/spree/checkout/_payment_methods.html.erb +1 -1
  32. data/app/views/spree/products/index.html.erb +1 -1
  33. data/app/views/spree/search/show.html.erb +1 -1
  34. data/app/views/spree/shared/_load_more_products.turbo_stream.erb +1 -1
  35. data/app/views/spree/shared/_product_listing_page.html.erb +2 -2
  36. data/app/views/themes/default/spree/page_sections/_featured_product.html.erb +4 -4
  37. data/app/views/themes/default/spree/page_sections/_featured_taxons.html.erb +4 -4
  38. data/app/views/themes/default/spree/page_sections/_footer.html.erb +6 -6
  39. data/app/views/themes/default/spree/page_sections/_header.html.erb +5 -2
  40. data/app/views/themes/default/spree/page_sections/_post_grid.html.erb +1 -1
  41. data/app/views/themes/default/spree/page_sections/_product_details.html.erb +2 -2
  42. data/app/views/themes/default/spree/posts/_pagination.html.erb +1 -1
  43. data/app/views/themes/default/spree/products/_add_to_cart_button.html.erb +16 -3
  44. data/app/views/themes/default/spree/products/_color_swatches.html.erb +1 -1
  45. data/app/views/themes/default/spree/products/_featured_image.html.erb +2 -1
  46. data/app/views/themes/default/spree/products/_json_ld.html.erb +4 -4
  47. data/app/views/themes/default/spree/products/_price.html.erb +3 -1
  48. data/app/views/themes/default/spree/products/_product.html.erb +3 -3
  49. data/app/views/themes/default/spree/products/_quantity_selector.html.erb +6 -4
  50. data/app/views/themes/default/spree/products/_show_more_button.html.erb +3 -3
  51. data/app/views/themes/default/spree/products/_swiper.html.erb +2 -2
  52. data/app/views/themes/default/spree/products/_variant_options.html.erb +6 -2
  53. data/app/views/themes/default/spree/products/_variant_picker.html.erb +5 -3
  54. data/app/views/themes/default/spree/shared/_custom_head.html.erb +7 -0
  55. data/app/views/themes/default/spree/shared/_line_item_options.html.erb +1 -1
  56. data/app/views/themes/default/spree/shared/_pagination.html.erb +7 -0
  57. data/app/views/themes/default/spree/shared/_search.html.erb +1 -1
  58. data/config/locales/en.yml +2 -0
  59. data/lib/generators/spree/storefront/install/install_generator.rb +12 -4
  60. data/lib/generators/spree/storefront/install/templates/application.css +45 -4
  61. data/lib/spree/storefront/configuration.rb +3 -0
  62. data/lib/spree/storefront.rb +3 -2
  63. metadata +33 -16
@@ -3,5 +3,5 @@
3
3
  <% end %>
4
4
 
5
5
  <%= turbo_stream.append "products" do %>
6
- <%= render 'spree/shared/products', products: storefront_products.records %>
6
+ <%= render 'spree/shared/products', products: storefront_products %>
7
7
  <% end %>
@@ -1,11 +1,11 @@
1
1
  <% if turbo_frame_request? && current_theme_preview.nil? %>
2
- <%= render 'spree/page_sections/product_grid', products: storefront_products.records, section: current_page.sections.find_by(type: 'Spree::PageSections::ProductGrid') %>
2
+ <%= render 'spree/page_sections/product_grid', products: storefront_products, section: current_page.sections.find_by(type: 'Spree::PageSections::ProductGrid') %>
3
3
  <% else %>
4
4
  <%= render_page(
5
5
  current_page,
6
6
  taxon: @taxon,
7
7
  collection: @collection,
8
- products: storefront_products.records,
8
+ products: storefront_products,
9
9
  ) %>
10
10
  <% end %>
11
11
 
@@ -47,15 +47,15 @@
47
47
  <div <%= block_attributes(block) %>>
48
48
  <% case block.type %>
49
49
  <% when 'Spree::PageBlocks::Products::Brand' %>
50
- <% if presenter.product&.brand&.page_builder_url %>
51
- <%= link_to spree_storefront_resource_url(presenter.product&.brand), title: strip_tags(presenter.product&.brand&.name).strip, data: { turbo_frame: '_top' } do %>
50
+ <% if presenter.product&.brand_taxon&.page_builder_url %>
51
+ <%= link_to spree_storefront_resource_url(presenter.product&.brand_taxon), title: strip_tags(presenter.product&.brand_taxon&.name).strip, data: { turbo_frame: '_top' } do %>
52
52
  <h3 class="text-sm uppercase font-semibold">
53
- <%= presenter.product&.brand&.name %>
53
+ <%= presenter.product&.brand_taxon&.name %>
54
54
  </h3>
55
55
  <% end %>
56
56
  <% else %>
57
57
  <h3 class="text-sm uppercase font-semibold">
58
- <%= presenter.product&.brand&.name %>
58
+ <%= presenter.product&.brand_taxon&.name %>
59
59
  </h3>
60
60
  <% end %>
61
61
  <% when 'Spree::PageBlocks::Products::Title' %>
@@ -14,11 +14,11 @@
14
14
  <%= page_builder_link_to link, title: link.label, class: 'group block overflow-hidden', target: link.open_in_new_tab.presence && '_blank' do %>
15
15
  <% if link.linkable.page_builder_image&.attached? && link.linkable.page_builder_image&.variable? %>
16
16
  <div class="flex space-y-2 flex-col">
17
- <%= spree_image_tag(link.linkable.page_builder_image, height: 300, width: 300, class: 'h-full w-full object-cover object-center group-hover:opacity-75 rounded-md bg-gray-200', loading: :lazy, alt: link.label) %>
17
+ <%= spree_image_tag(link.linkable.page_builder_image, height: 300, width: 300, class: 'h-full w-full object-cover object-center group-hover:opacity-75 rounded-md bg-border', loading: :lazy, alt: link.label) %>
18
18
  <span><%= link.label %></span>
19
19
  </div>
20
20
  <% else %>
21
- <div class="aspect-1 w-full group-hover:bg-gray-100 bg-gray-200 rounded-md flex items-center justify-center relative">
21
+ <div class="aspect-1 w-full group-hover:bg-gray-100 bg-border rounded-md flex items-center justify-center relative">
22
22
  <span><%= link.label %></span>
23
23
  </div>
24
24
  <% end %>
@@ -49,10 +49,10 @@
49
49
  <%= page_builder_link_to link, title: link.label, class: 'group block overflow-hidden', target: link.open_in_new_tab.presence && '_blank' do %>
50
50
  <% if link.linkable.page_builder_image.attached? %>
51
51
  <div class="flex space-y-2 flex-col">
52
- <%= spree_image_tag(link.linkable.page_builder_image, height: 200, width: 200, class: 'h-full w-full object-cover object-center group-hover:opacity-75 rounded-md bg-gray-200', loading: :lazy, alt: link.label) %>
52
+ <%= spree_image_tag(link.linkable.page_builder_image, height: 200, width: 200, class: 'h-full w-full object-cover object-center group-hover:opacity-75 rounded-md bg-border', loading: :lazy, alt: link.label) %>
53
53
  </div>
54
54
  <% else %>
55
- <div class="aspect-1 w-full group-hover:bg-gray-100 bg-gray-200 flex items-center justify-center relative">
55
+ <div class="aspect-1 w-full group-hover:bg-gray-100 bg-border flex items-center justify-center relative">
56
56
  <span><%= link.label %></span>
57
57
  </div>
58
58
  <% end %>
@@ -12,9 +12,9 @@
12
12
  </div>
13
13
  <div class="grid grid-cols-1 lg:grid-cols-4 grow w-full">
14
14
  <% section.blocks.includes(:links).each do |block| %>
15
- <div class="flex-grow gap-1 flex flex-col py-6 md:py-0 border-b md:border-none border-default lg:mb-6">
15
+ <nav class="flex-grow gap-1 flex flex-col py-6 md:py-0 border-b md:border-none border-default lg:mb-6" aria-label="<%= block.preferred_label %>">
16
16
  <div <%= block_attributes(block) %>>
17
- <h3 class="text-sm py-2"><%= block.preferred_label %></h3>
17
+ <div class="text-sm py-2"><%= block.preferred_label %></div>
18
18
  <ul class="uppercase text-sm tracking-wider !leading-4 flex flex-col gap-1">
19
19
  <% block.links.each do |link| %>
20
20
  <li class='py-2'>
@@ -25,12 +25,12 @@
25
25
  <% end %>
26
26
  </ul>
27
27
  </div>
28
- </div>
28
+ </nav>
29
29
  <% end %>
30
30
  </div>
31
- <div class="flex-grow gap-4 flex flex-col justify-between lg:w-60">
31
+ <nav class="flex-grow gap-4 flex flex-col justify-between lg:w-60" aria-label="<%= Spree.t(:follow_us) %>">
32
32
  <div class="gap-1 flex flex-col py-6 md:py-0">
33
- <h3 class="text-sm py-2"><%= Spree.t(:follow_us) %></h3>
33
+ <div class="text-sm py-2"><%= Spree.t(:follow_us) %></div>
34
34
  <div class="flex flex-wrap gap-2 max-w-48 py-2">
35
35
  <% cache spree_storefront_base_cache_scope.call(current_store) do %>
36
36
  <% Spree::Store::SUPPORTED_SOCIAL_NETWORKS.each do |media| %>
@@ -44,7 +44,7 @@
44
44
  <% end %>
45
45
  </div>
46
46
  </div>
47
- </div>
47
+ </nav>
48
48
  </div>
49
49
  <div class="flex justify-end w-full"></div>
50
50
  </div>
@@ -103,11 +103,12 @@
103
103
  <% if show_account_pane? %>
104
104
  <button
105
105
  data-action='click->slideover-account#toggle click@window->slideover-account#hide click->toggle-menu#hide touch->toggle-menu#hide'
106
+ aria-label='Open account panel'
106
107
  >
107
108
  <%= render 'spree/shared/icons/account', color: section.preferred_text_color, section: section %>
108
109
  </button>
109
110
  <% else %>
110
- <%= link_to spree.account_path do %>
111
+ <%= link_to spree.account_path, 'aria-label': 'Open account panel' do %>
111
112
  <%= render 'spree/shared/icons/account', color: section.preferred_text_color, section: section %>
112
113
  <% end %>
113
114
  <% end %>
@@ -117,12 +118,13 @@
117
118
  <button
118
119
  type="submit"
119
120
  id="wishlist-icon"
121
+ aria-label='Open wishlist'
120
122
  class="flex items-end">
121
123
  <%= render 'spree/shared/wishlist_icon', wishlist: current_wishlist, background_color: section.preferred_background_color %>
122
124
  </button>
123
125
  <% end %>
124
126
  <% else %>
125
- <%= link_to spree.account_wishlist_path, id: "wishlist-icon" do %>
127
+ <%= link_to spree.account_wishlist_path, id: "wishlist-icon", 'aria-label': 'Open wishlist' do %>
126
128
  <%= render 'spree/shared/wishlist_icon', background_color: section.preferred_background_color %>
127
129
  <% end %>
128
130
  <% end %>
@@ -141,6 +143,7 @@
141
143
  data-toggle-menu-target='toggleable'
142
144
  role='dialog'
143
145
  aria-modal='true'
146
+ aria-label='Mobile menu'
144
147
  >
145
148
  <div
146
149
  class='flex justify-between flex-col lg:hidden w-full transition-transform has-[.currency-and-locale-modal:not(.hidden)]:transform-none body header--mobile-menu'
@@ -5,7 +5,7 @@
5
5
  <%= render partial: 'spree/posts/post', collection: @posts, cached: true %>
6
6
  </div>
7
7
 
8
- <%= paginate @posts, theme: 'storefront', outer_window: 1, inner_window: 2 %>
8
+ <%= render 'spree/shared/pagination' %>
9
9
  </div>
10
10
  <% end %>
11
11
  </div>
@@ -47,9 +47,9 @@
47
47
  </h1>
48
48
  <% when 'Spree::PageBlocks::Products::Brand' %>
49
49
  <% if product.brand_taxon %>
50
- <%= link_to spree.nested_taxons_path(product.brand_taxon), title: product.brand_name do %>
50
+ <%= link_to spree.nested_taxons_path(product.brand_taxon), title: product.brand_taxon.name do %>
51
51
  <h3 class="text-sm lg:mt-0 inline-block mb-1">
52
- <%= product.brand_name %>
52
+ <%= product.brand_taxon.name %>
53
53
  </h3>
54
54
  <% end %>
55
55
  <% end %>
@@ -1 +1 @@
1
- <%= paginate @posts, theme: 'storefront', outer_window: 1, inner_window: 2 %>
1
+ <%= render 'spree/shared/pagination' %>
@@ -5,11 +5,24 @@ options_param_name ||= :options
5
5
  not_selected_options = product_not_selected_options(product, selected_variant, options_param_name: options_param_name)
6
6
  all_options_selected = (params[options_param_name].present? || @variant_from_options.present?) && not_selected_options.empty?
7
7
  not_all_options_selected = !all_options_selected && product.any_variant_available?(current_currency)
8
+
9
+ # Build pricing context for selected variant
10
+ if selected_variant
11
+ variant_pricing_context = pricing_context_for_variant(selected_variant)
12
+ variant_price = selected_variant.price_for(variant_pricing_context)
13
+ else
14
+ variant_price = nil
15
+ end
16
+
8
17
  variant_not_available = selected_variant.nil? ||
9
18
  product.discontinued? ||
10
19
  selected_variant.discontinued? ||
11
20
  !selected_variant.purchasable? ||
12
- selected_variant.price_in(current_currency).amount.nil?
21
+ variant_price&.amount.nil?
22
+
23
+ # Build pricing context for product
24
+ product_pricing_context = pricing_context_for_variant(product)
25
+ product_price = product.default_variant.price_for(product_pricing_context)
13
26
  %>
14
27
 
15
28
  <div class='<%= sticky_button_classes %> bottom-0 flex flex-col gap-4 z-10' data-sticky-button-target='stickyButton'>
@@ -22,7 +35,7 @@ variant_not_available = selected_variant.nil? ||
22
35
  ].compact.join(' '),
23
36
  product_form_target: 'submit'
24
37
  },
25
- disabled: product.price_in(current_currency).zero? do %>
38
+ disabled: product_price.zero? do %>
26
39
  <% if not_selected_options.size == 1 %>
27
40
  <span><%= Spree.t('storefront.variant_picker.please_choose', option_type: not_selected_options[0].presentation) %></span>
28
41
  <% elsif not_all_options_selected %>
@@ -47,7 +60,7 @@ variant_not_available = selected_variant.nil? ||
47
60
  ].compact.join(' '),
48
61
  product_form_target: 'submit'
49
62
  },
50
- disabled: product.price_in(current_currency).zero? do %>
63
+ disabled: product_price.zero? do %>
51
64
  <% if not_selected_options.size == 1 %>
52
65
  <span><%= Spree.t('storefront.variant_picker.please_choose', option_type: not_selected_options[0].presentation) %></span>
53
66
  <% elsif not_all_options_selected %>
@@ -31,7 +31,7 @@
31
31
  color_preview_container_class: "lg:w-[28px] lg:h-[28px]",
32
32
  color_preview_class: "lg:top-[2px] lg:left-[2px]" %>
33
33
  <% end %>
34
- <% if selected_variant.default_image %>
34
+ <% if selected_variant.has_images? || selected_variant.default_image.present? %>
35
35
  <template data-featured-image-template>
36
36
  <%= render 'spree/products/featured_image',
37
37
  object: selected_variant %>
@@ -1,5 +1,6 @@
1
1
  <% with_cover_image ||= true %>
2
2
  <% object ||= product %>
3
+
3
4
  <% if object.default_image.present? && object.default_image.attached? %>
4
5
  <% height ||= theme_setting('product_listing_image_height') %>
5
6
  <% width ||= theme_setting('product_listing_image_width') %>
@@ -13,7 +14,7 @@
13
14
  "w-full group-hover:opacity-0 transition-opacity duration-500 z-10 relative h-full bg-background product-card !max-h-full #{object_class}" :
14
15
  "w-full h-full !max-h-full #{object_class}" %>
15
16
  <%= spree_image_tag(object.default_image, width: width, height: height, variant: :medium, loading: :lazy, alt: "#{object.name} #{Spree.t('storefront.products.primary_image', default: 'primary image')}", class: image_hover_class) %>
16
- <% if object.secondary_image.present? && object.secondary_image.attached? %>
17
+ <% if object.has_images? && object.image_count > 1 && object.secondary_image.present? && object.secondary_image.attached? %>
17
18
  <% secondary_image_class = "w-full absolute top-0 left-0 opacity-100 h-full !max-h-full #{object_class}" %>
18
19
  <%= spree_image_tag(object.secondary_image, width: width, height: height, variant: :medium, loading: :lazy, alt: "#{object.name} #{Spree.t('storefront.products.secondary_image', default: 'secondary image')}", class: secondary_image_class) %>
19
20
  <% end %>
@@ -5,9 +5,9 @@
5
5
  "@type": "Product",
6
6
  "name": <%= product.name.to_json.html_safe %>,
7
7
  "url": <%= spree.product_url(product, host: current_store.url_or_custom_domain).to_json.html_safe %>,
8
- <% if product.featured_image %>
8
+ <% if product.has_images? %>
9
9
  "image": [
10
- <%= spree_image_url(product.featured_image, variant: :large).to_json.html_safe %>
10
+ <%= spree_image_url(product.default_image, variant: :large).to_json.html_safe %>
11
11
  ],
12
12
  <% end %>
13
13
  <% if product.description.present? %>
@@ -18,10 +18,10 @@
18
18
  <% elsif selected_variant %>
19
19
  <% if selected_variant.sku.present? %>"sku": <%= selected_variant.sku.to_json.html_safe %>,<% end %>
20
20
  <% end %>
21
- <% if product.brand_name %>
21
+ <% if product.brand_taxon %>
22
22
  "brand": {
23
23
  "@type": "Brand",
24
- "name": <%= product.brand_name.to_json.html_safe %>
24
+ "name": <%= product.brand_taxon.name.to_json.html_safe %>
25
25
  },
26
26
  <% end %>
27
27
  "offers": [
@@ -20,7 +20,9 @@
20
20
 
21
21
  target ||= product
22
22
 
23
- price = target.price_in(current_currency)
23
+ variant_for_context = target.is_a?(Spree::Product) ? target.default_variant : target
24
+ pricing_context = pricing_context_for_variant(variant_for_context)
25
+ price = variant_for_context.price_for(pricing_context)
24
26
  money_price = price.display_amount
25
27
 
26
28
  if target.is_a?(Spree::Product) && !use_variant && product.price_varies?(current_currency)
@@ -24,9 +24,9 @@
24
24
  <%= render 'spree/products/label', product: product %>
25
25
  </div>
26
26
  <div class="product-card-inner">
27
- <% if product.brand_name.present? %>
27
+ <% if product.brand_taxon.present? %>
28
28
  <h3 class="text-xs font-semibold uppercase inline-block product-card-brand">
29
- <%= product.brand_name %>
29
+ <%= product.brand_taxon.name %>
30
30
  </h3>
31
31
  <% end %>
32
32
  <h3 class="line-clamp-1 product-card-title">
@@ -55,7 +55,7 @@
55
55
  </div>
56
56
  <% end %>
57
57
  <div class="absolute right-1 top-1 z-10" data-plp-variant-picker-target="addToWishlist">
58
- <%= render 'spree/products/add_to_wishlist', variant: product.first_or_default_variant(current_currency) if product %>
58
+ <%= render 'spree/products/add_to_wishlist', variant: selected_variant %>
59
59
  </div>
60
60
  <%= render 'spree/products/color_swatches', product: product if product && (defined?(show_variant_picker) ? show_variant_picker : true) %>
61
61
  </div>
@@ -5,19 +5,21 @@
5
5
  <%= button_tag class: 'decrease-quantity',
6
6
  type: 'button',
7
7
  data: { action: 'click->quantity-picker#decrease',
8
- 'quantity-picker-target': 'decrease' } do %>
8
+ 'quantity-picker-target': 'decrease' },
9
+ aria: { label: Spree.t('storefront.products.decrease_quantity') } do %>
9
10
  <%= render 'spree/shared/icons/minus' %>
10
11
  <% end %>
11
12
  <%= number_field_tag :quantity, 1,
12
13
  min: 1,
13
14
  max: selected_variant&.backorderable? ? nil : selected_variant&.total_on_hand,
14
15
  class: 'quantity-input',
15
- 'aria-label': 'Quantity',
16
- data: { 'quantity-picker-target': 'quantity' } %>
16
+ data: { 'quantity-picker-target': 'quantity' },
17
+ aria: { label: Spree.t(:quantity) } %>
17
18
  <%= button_tag class: 'increase-quantity',
18
19
  type: 'button',
19
20
  data: { action: 'click->quantity-picker#increase',
20
- 'quantity-picker-target': 'increase' } do %>
21
+ 'quantity-picker-target': 'increase' },
22
+ aria: { label: Spree.t('storefront.products.increase_quantity') } do %>
21
23
  <%= render 'spree/shared/icons/plus' %>
22
24
  <% end %>
23
25
  </div>
@@ -1,7 +1,7 @@
1
- <% unless storefront_products.last_page? %>
2
- <%= turbo_frame_tag "next_page", src: url_for(params.to_unsafe_h.merge(page: storefront_products.next_page, format: :turbo_stream)), class: "block relative w-full", data: { controller: "infinite-scroll", infinite_scroll_offset_value: "1350px" }, loading: "lazy" do %>
1
+ <% if storefront_pagy&.next %>
2
+ <%= turbo_frame_tag "next_page", src: url_for(params.to_unsafe_h.merge(page: storefront_pagy.next, format: :turbo_stream)), class: "block relative w-full", data: { controller: "infinite-scroll", infinite_scroll_offset_value: "1350px" }, loading: "lazy" do %>
3
3
  <span class="flex justify-center gap-2 items-center py-4 left-0 w-full h-full">
4
- <%= render 'spree/shared/icons/spinner' %>
4
+ <%= render 'spree/shared/icons/spinner' %>
5
5
  <%= Spree.t(:loading) %>...
6
6
  </span>
7
7
  <% end %>
@@ -75,10 +75,10 @@
75
75
  <% end %>
76
76
  </div>
77
77
  </div>
78
- <%= button_tag class: "absolute p-2 bg-white rounded-full z-10 border border-accent left-0 disabled:hidden hover:border-primary ml-2 lg:ml-0 swiper-custom-button-prev-#{section.id} block #{arrows_on_top ? '' : 'md:hidden'} top-(--top) lg:top-(--desktop-top)", aria: { hidden: true }, style: "--top: calc(#{theme_setting('product_listing_image_height_mobile')}px/2); --desktop-top: calc(#{theme_setting('product_listing_image_height')}px/2); transform: translate(-50%, -50%);" do %>
78
+ <%= button_tag class: "absolute p-2 bg-white rounded-full z-10 border border-accent left-0 disabled:hidden hover:border-primary ml-2 lg:ml-0 swiper-custom-button-prev-#{section.id} block #{arrows_on_top ? '' : 'md:hidden'} top-(--top) lg:top-(--desktop-top)", style: "--top: calc(#{theme_setting('product_listing_image_height_mobile')}px/2); --desktop-top: calc(#{theme_setting('product_listing_image_height')}px/2); transform: translate(-50%, -50%);" do %>
79
79
  <%= render 'spree/shared/icons/chevron' %>
80
80
  <% end %>
81
- <%= button_tag class: "absolute p-2 bg-white rounded-full z-10 border border-accent right-0 disabled:hidden hover:border-primary mr-2 lg:mr-0 swiper-custom-button-next-#{section.id} block #{arrows_on_top ? '' : 'md:hidden'} top-(--top) lg:top-(--desktop-top)", aria: { hidden: true }, style: "--top: calc(#{theme_setting('product_listing_image_height_mobile')}px/2); --desktop-top: calc(#{theme_setting('product_listing_image_height')}px/2); transform: translate(50%, -50%);" do %>
81
+ <%= button_tag class: "absolute p-2 bg-white rounded-full z-10 border border-accent right-0 disabled:hidden hover:border-primary mr-2 lg:mr-0 swiper-custom-button-next-#{section.id} block #{arrows_on_top ? '' : 'md:hidden'} top-(--top) lg:top-(--desktop-top)", style: "--top: calc(#{theme_setting('product_listing_image_height_mobile')}px/2); --desktop-top: calc(#{theme_setting('product_listing_image_height')}px/2); transform: translate(50%, -50%);" do %>
82
82
  <%= render 'spree/shared/icons/chevron_right' %>
83
83
  <% end %>
84
84
  </div>
@@ -32,8 +32,8 @@
32
32
  <% end %>
33
33
  <% end %>
34
34
  <% if option_type.color? %>
35
- <li>
36
- <label class="cursor-pointer">
35
+ <li role="option" aria-label="<%= value.presentation %>">
36
+ <label class="cursor-pointer" aria-label="<%= value.presentation %>">
37
37
  <%= radio_button_tag option_type.presentation, value.name, selected_option == value,
38
38
  name: option_type.presentation,
39
39
  id: "product-option-#{product.id}-#{position}-#{index}",
@@ -56,10 +56,14 @@
56
56
  option_id: option_type.id
57
57
  },
58
58
  name: option_type.presentation,
59
+ role: 'menuitemradio',
60
+ aria: { label: value.presentation },
59
61
  id: "product-option-#{product.id}-#{position}-#{index}"
60
62
  %>
61
63
  <label
62
64
  for='product-option-<%= product.id %>-<%= position %>-<%= index %>'
65
+ role="menuitem"
66
+ aria-label="<%= value.presentation %>"
63
67
  class='text-sm cursor-pointer flex items-center justify-between px-4 py-2.5 hover:bg-accent focus:outline-hidden focus:bg-accent transition duration-150 ease-in-out <%= option_disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-accent" %>'
64
68
  >
65
69
  <p><%=h value.presentation %></p>
@@ -7,19 +7,20 @@
7
7
  <%= option_type_colors_preview_styles(option_type).html_safe %>
8
8
  <fieldset data-option-id="<%= option_type.id %>" class="flex flex-col gap-y-2">
9
9
  <span class="text-sm leading-4 uppercase tracking-widest"><%= option_type.presentation %>: <%= selected_option.presentation %></span>
10
- <ul class="flex items-center flex-wrap gap-1">
10
+ <ul class="flex items-center flex-wrap gap-1" role="listbox" aria-label="<%= option_type.presentation %>">
11
11
  <%= render 'spree/products/variant_options', product: product, option_type: option_type, position: index, selected_variant: selected_variant, options_param_name: options_param_name %>
12
12
  </ul>
13
13
  </fieldset>
14
14
  <% else %>
15
15
  <fieldset data-option-id="<%= option_type.id %>">
16
- <div data-controller="dropdown" class="relative mb-2">
16
+ <div data-controller="dropdown" class="relative mb-2" role="group" aria-label="<%= option_type.presentation %>">
17
17
  <div class="flex items-center justify-between">
18
18
  <button
19
19
  data-action="click->dropdown#toggle click@window->dropdown#hide"
20
20
  type='button'
21
21
  class='text-sm uppercase tracking-widest flex gap-2 items-center border border-default py-2 px-4 rounded-input dropdown-button'
22
- data-dropdown-target="button">
22
+ data-dropdown-target="button"
23
+ aria-label="<%= selected_option ? option_type.presentation+":"+selected_option.presentation : Spree.t('storefront.variant_picker.please_choose', option_type: option_type.presentation) %>">
23
24
  <legend class="mr-2" id="option-<%= option_type.id %>-value">
24
25
  <% if selected_option %>
25
26
  <%= option_type.presentation %>: <span class="option-value-text"><%= selected_option.presentation %></span>
@@ -37,6 +38,7 @@
37
38
  data-transition-leave="transition ease-in"
38
39
  data-transition-leave-from="opacity-100 translate-y-0"
39
40
  data-transition-leave-to="opacity-0 translate-y-1"
41
+ role="menu"
40
42
  class="hidden absolute top-11 left-0 z-[9999] flex w-screen max-w-max shadow-xs">
41
43
  <div class="bg-background border-default border overflow-hidden w-72">
42
44
  <%= render 'spree/products/variant_options', product: product, option_type: option_type, position: index, selected_variant: selected_variant, options_param_name: options_param_name %>
@@ -0,0 +1,7 @@
1
+ <link rel="preconnect" href="https://esm.sh" crossorigin>
2
+ <link rel="dns-prefetch" href="https://esm.sh">
3
+
4
+ <link rel="preload" href="https://esm.sh/swiper@11.2.2/swiper-bundle.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
5
+ <noscript><link rel="stylesheet" href="https://esm.sh/swiper@11.2.2/swiper-bundle.min.css"></noscript>
6
+ <link rel="preload" href="https://esm.sh/flag-icons@7.3.2/css/flag-icons.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
7
+ <noscript><link rel="stylesheet" href="https://esm.sh/flag-icons@7.3.2/css/flag-icons.min.css"></noscript>
@@ -1,4 +1,4 @@
1
- <% options = line_item.variant.option_values.includes(:option_type) %>
1
+ <% options = line_item.variant.option_values %>
2
2
 
3
3
  <% options.select { |ov| ov.option_type.color? }.each do |option| %>
4
4
  <div>
@@ -0,0 +1,7 @@
1
+ <%# Renders pagination - supports both Pagy and Kaminari %>
2
+ <% if Spree::Storefront::Config[:use_kaminari_pagination] %>
3
+ <%= paginate @posts || @orders || @gift_cards || @store_credit_events || @storefront_products,
4
+ theme: 'storefront', outer_window: 1, inner_window: 2 %>
5
+ <% elsif @pagy && @pagy.pages > 1 %>
6
+ <%== @pagy.series_nav(aria_label: 'Pages') %>
7
+ <% end %>
@@ -1,4 +1,4 @@
1
- <div id="search-suggestions" role="dialog" aria-modal="true">
1
+ <div id="search-suggestions" role="dialog" aria-label="Search suggestions" aria-modal="true">
2
2
  <div class='w-full flex flex-col border-b border-default' data-controller="search-suggestions" data-show-class="h-screen min-h-screen" data-search-suggestions-url-value="<%= spree.search_suggestions_path %>">
3
3
  <div class='hidden lg:flex justify-center w-full mt-4 mb-3' id="header-logo">
4
4
  <%= render 'spree/shared/logo', logo: logo, height: logo_height %>
@@ -59,6 +59,8 @@ en:
59
59
  join: Email me about new products, sales, and more. You can unsubscribe at any time.
60
60
  status: You are currently %{status} to the newsletters.
61
61
  products:
62
+ decrease_quantity: Decrease quantity
63
+ increase_quantity: Increase quantity
62
64
  pinch_to_zoom_html: Pinch to<br>zoom
63
65
  primary_image: primary image
64
66
  secondary_image: secondary image
@@ -6,6 +6,8 @@ module Spree
6
6
  class InstallGenerator < Rails::Generators::Base
7
7
  desc 'Installs Spree Storefront'
8
8
 
9
+ class_option :migrate, type: :boolean, default: true, banner: 'Run migrations'
10
+
9
11
  def self.source_paths
10
12
  [
11
13
  File.expand_path('templates', __dir__),
@@ -14,10 +16,16 @@ module Spree
14
16
  ]
15
17
  end
16
18
 
19
+ def install_page_builder
20
+ say_status :installing, 'page builder'
21
+ migrate_option = options[:migrate] == false ? ' --migrate=false' : ''
22
+ generate "spree:page_builder:install --force#{migrate_option}"
23
+ end
24
+
17
25
  def install
18
26
  empty_directory Rails.root.join('app/assets/tailwind') if Rails.root && !Rails.root.join('app/assets/tailwind').exist?
19
- template 'application.css', 'app/assets/tailwind/application.css'
20
- template 'tailwind.config.js', 'config/tailwind.config.js'
27
+ template 'application.css', 'app/assets/tailwind/application.css', force: options[:force]
28
+ template 'tailwind.config.js', 'config/tailwind.config.js', force: options[:force]
21
29
 
22
30
  if Rails.root && Rails.root.join("Procfile.dev").exist?
23
31
  append_to_file 'Procfile.dev', "\nstorefront_css: bin/rails tailwindcss:watch" unless File.read('Procfile.dev').include?('storefront_css:')
@@ -40,8 +48,8 @@ module Spree
40
48
  append_to_file 'app/assets/config/manifest.js', "\n//= link_tree ../builds" unless File.read('app/assets/config/manifest.js').include?('//= link_tree ../builds')
41
49
  end
42
50
 
43
- # remove static robots.txt as we use robots.txt.erb
44
- remove_file Rails.root.join('public/robots.txt') if Rails.root && Rails.root.join('public/robots.txt').exist?
51
+ # remove static robots.txt as storefront serves it dynamically via seo#robots
52
+ remove_file 'public/robots.txt'
45
53
  end
46
54
  end
47
55
  end
@@ -1,6 +1,3 @@
1
- @import url('https://esm.sh/swiper@11.2.2/swiper-bundle.min.css');
2
- @import url('https://esm.sh/flag-icons@7.3.2/css/flag-icons.min.css');
3
-
4
1
  @import 'tailwindcss';
5
2
  @plugin "@tailwindcss/typography";
6
3
  @plugin "@tailwindcss/forms";
@@ -135,7 +132,7 @@ html {
135
132
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
136
133
  }
137
134
 
138
- body .section-header {
135
+ body:not(.inside-page-builder) > div.section-header {
139
136
  @apply sticky top-0 z-50;
140
137
  }
141
138
 
@@ -1853,3 +1850,47 @@ body:has(.currency-and-locale-modal:not(.hidden)) .page-contents {
1853
1850
  aspect-ratio: 1.77;
1854
1851
  }
1855
1852
  }
1853
+
1854
+ /* Pagy pagination styles */
1855
+ .pagy.series-nav {
1856
+ display: flex;
1857
+ align-items: center;
1858
+ justify-content: center;
1859
+ gap: 0.25rem;
1860
+ margin-top: 4rem;
1861
+ }
1862
+
1863
+ .pagy.series-nav a {
1864
+ display: inline-flex;
1865
+ justify-content: center;
1866
+ align-items: center;
1867
+ width: 2rem;
1868
+ text-align: center;
1869
+ padding: 0.25rem;
1870
+ border-radius: 0.25rem;
1871
+ }
1872
+
1873
+ .pagy.series-nav a[rel="prev"],
1874
+ .pagy.series-nav a[rel="next"] {
1875
+ width: auto;
1876
+ padding-left: 1rem;
1877
+ padding-right: 1rem;
1878
+ }
1879
+
1880
+ .pagy.series-nav a[aria-disabled="true"] {
1881
+ opacity: 0.5;
1882
+ cursor: default;
1883
+ }
1884
+
1885
+ .pagy.series-nav a[aria-current="page"] {
1886
+ font-weight: 600;
1887
+ text-decoration: underline;
1888
+ }
1889
+
1890
+ .pagy.series-nav a:not([aria-disabled="true"]):hover {
1891
+ background-color: rgba(0, 0, 0, 0.05);
1892
+ }
1893
+
1894
+ .pagy.series-nav a[role="separator"] {
1895
+ width: 2rem;
1896
+ }
@@ -9,6 +9,9 @@ module Spree
9
9
  preference :products_per_page, :integer, default: 20
10
10
 
11
11
  preference :search_min_query_length, :integer, default: 2
12
+
13
+ # Pagination preference - set to true to use legacy Kaminari pagination
14
+ preference :use_kaminari_pagination, :boolean, default: false
12
15
  end
13
16
  end
14
17
  end
@@ -1,7 +1,7 @@
1
- require 'spree_core'
1
+ require 'spree'
2
+ require 'spree_page_builder'
2
3
 
3
4
  require 'active_link_to'
4
- require 'canonical-rails'
5
5
  require 'heroicon'
6
6
  require 'importmap-rails'
7
7
  require 'local_time'
@@ -10,6 +10,7 @@ require 'stimulus-rails'
10
10
  require 'tailwindcss-rails'
11
11
  require 'turbo-rails'
12
12
  require 'inline_svg'
13
+ require 'pagy'
13
14
 
14
15
  require 'spree/storefront/engine'
15
16
  require 'spree/core/partials'