Add CSS Grid Lanes support with toggleable shop setting

Implement CSS Grid Lanes (masonry layout) for product grids with graceful
fallback to standard CSS Grid for browsers that don't support it yet.

- Add grid_lanes_enabled column to Shop model (default: True)
- Add toggle in Shop Settings under Announcement Ribbon Settings
- Update product grid templates to conditionally apply grid-lanes-enabled class
- CSS uses @supports (display: grid-lanes) for progressive enhancement
- Mobile (160px min) and desktop (240px min) breakpoints supported

When enabled and browser supports Grid Lanes, products flow into a
waterfall/masonry layout where items fill the shortest column first,
creating a Pinterest-style layout that handles varying image heights.
This commit is contained in:
Russell Ballestrini 2025-12-19 20:33:28 -05:00
parent 3bf65966c5
commit b3270eb369
8 changed files with 107 additions and 6 deletions

View file

@ -60,6 +60,9 @@ class Shop(RBase, Base):
ribbon_color_1 = Column(Unicode(32), nullable=True, default="")
ribbon_color_2 = Column(Unicode(32), nullable=True, default="")
# CSS Grid Lanes (masonry layout) - enabled by default
grid_lanes_enabled = Column(Boolean, default=True)
# example: "UA-XX31XX60-3".
google_analytics_id = Column(Unicode(32), nullable=True, default="")
plausible_domain_name = Column(Unicode(256), nullable=True, default="")

View file

@ -0,0 +1,30 @@
"""add grid_lanes_enabled to shop
Revision ID: a7c3e8f1d2b4
Revises: b090f873502e
Create Date: 2025-12-19 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "a7c3e8f1d2b4"
down_revision = "b090f873502e"
branch_labels = None
depends_on = None
def upgrade():
# Add grid_lanes_enabled column to mps_shop table with default True (enabled)
op.add_column(
"mps_shop",
sa.Column("grid_lanes_enabled", sa.Boolean(), nullable=False, server_default="1"),
)
def downgrade():
# Remove grid_lanes_enabled column from mps_shop table
op.drop_column("mps_shop", "grid_lanes_enabled")

View file

@ -987,12 +987,36 @@ button.cart-public-button span.lock {
background-color: #a3c765;
}
/*
* CSS Grid Lanes: Masonry-style layout with graceful fallback
*
* Grid Lanes is a new CSS feature that creates true masonry layouts natively.
* We use @supports to detect browser support and fall back to regular CSS Grid.
* Shop owners can toggle this via the "grid-lanes-enabled" class in settings.
*
* Fallback: Standard CSS Grid with auto-fit columns (items in rows)
* Enhanced: CSS Grid Lanes (items flow into shortest column, waterfall style)
*/
/* Default: Standard CSS Grid layout */
section.serp {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1.00fr));
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
grid-auto-columns: max-content;
grid-auto-flow: dense;
grid-gap: 4px;
gap: 4px;
}
/* Grid Lanes: Only apply when shop has it enabled AND browser supports it */
@supports (display: grid-lanes) {
section.serp.grid-lanes-enabled {
display: grid-lanes;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 4px;
/* item-tolerance controls sensitivity when deciding item placement */
/* Prevents layout shuffling from minor size variations */
item-tolerance: 1em;
}
}
div.serp-item {
@ -1403,7 +1427,7 @@ textarea.markup-editor-textarea {
/* The SERP page grid should have bigger items since it has more room.
* mobile -> desktop
* 140px -> 240px */
* 160px -> 240px */
section.serp {
grid-template-columns: repeat(auto-fit, minmax(240px, .25fr));
}
@ -1538,6 +1562,24 @@ textarea.markup-editor-textarea {
padding: 20px;
}
}
/*
* Desktop: Progressive enhancement for CSS Grid Lanes
* This uses nested @supports inside @media to only apply when both
* the viewport is desktop-sized AND the browser supports grid-lanes
* AND the shop has grid-lanes-enabled class set
*/
@media (min-width: 960px) {
@supports (display: grid-lanes) {
section.serp.grid-lanes-enabled {
display: grid-lanes;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 4px;
item-tolerance: 1em;
}
}
}
div.message-ribbon {
color: #ffffff;
background-color: #5871ad;

View file

@ -36,7 +36,7 @@
{% else %}
{% if products %}
<section class="serp">
<section class="serp{% if request.shop.grid_lanes_enabled %} grid-lanes-enabled{% endif %}">
{% for product in products %}
{% if product.is_ready %}

View file

@ -2,7 +2,7 @@
{% block content -%}
<section class="serp">
<section class="serp{% if request.shop.grid_lanes_enabled %} grid-lanes-enabled{% endif %}">
{% if products %}
{% for product in products %}

View file

@ -527,6 +527,16 @@
<br />
<br />
<label>Product Layout</label>
<br />
<input type="checkbox" name="grid-lanes-checkbox" id="grid-lanes-checkbox" {% if request.shop.grid_lanes_enabled %}checked{% endif %} />
<label for="grid-lanes-checkbox" class="inline-label"><b>Enable Masonry Layout (Grid Lanes)</b></label>
<br />
<small class="note-text">When enabled, products flow into a waterfall/masonry layout where items fill the shortest column first. This creates a Pinterest-style layout that better handles varying product image heights. Requires browser support for CSS Grid Lanes.</small>
<br />
<br />
<input type="submit" name="submit" class="mps-submit" value="Save Settings" />
<br />

View file

@ -4,7 +4,7 @@
<section class="two-column">
<div class="products-left">
<h2>My Purchases</h2>
<section class="serp">
<section class="serp{% if request.shop and request.shop.grid_lanes_enabled %} grid-lanes-enabled{% endif %}">
{% if products %}
{% for product in products %}
<div class="serp-item">

View file

@ -385,6 +385,9 @@ def shop_settings(request):
# Theme settings
default_theme = request.params.get("default_theme", shop.default_theme)
# Grid lanes setting (keep current value by default)
grid_lanes_enabled = shop.grid_lanes_enabled
google_analytics_id = request.params.get(
"google_analytics_id", shop.google_analytics_id
)
@ -564,6 +567,19 @@ def shop_settings(request):
)
request.session.flash(msg)
# Handle grid lanes setting
grid_lanes_checkbox = request.params.get("grid-lanes-checkbox", "off")
grid_lanes_enabled = checkbox_to_bool(grid_lanes_checkbox)
if shop.grid_lanes_enabled != grid_lanes_enabled:
shop.grid_lanes_enabled = grid_lanes_enabled
status = "enabled" if grid_lanes_enabled else "disabled"
request.session.flash(
(
f"Masonry layout (Grid Lanes) {status}",
"success",
)
)
# Handle stripe settings form
if form_section == "stripe-settings":
# Handle disable action