diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index 5254423..f0a55a6 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -890,13 +890,6 @@ class AplicoopWebsiteSale(WebsiteSale): product_display_info, filtered_products_dict, ) = self._prepare_products_maps(products, pricelist) - # Whether the tags row needs to be reserved on every card: if nothing in - # this batch has a published tag, the row is skipped entirely instead of - # leaving an empty aria-hidden gap on every product (see product-card.css). - any_product_has_tags = any( - v["published_tags"] for v in filtered_products_dict.values() - ) - # Inject draft sale demand context so ribbons/is_out_of_stock use forecasted net qty products, product_max_qty = self._prepare_draft_stock_data(products) @@ -941,7 +934,6 @@ class AplicoopWebsiteSale(WebsiteSale): "placed_order_url": placed_order_url, "products": products, "filtered_product_tags": filtered_products_dict, - "any_product_has_tags": any_product_has_tags, "cart": cart, "available_categories": available_categories, "category_hierarchy": category_hierarchy, @@ -1053,11 +1045,6 @@ class AplicoopWebsiteSale(WebsiteSale): "product": product, "published_tags": published_tags, } - # See eskaera_shop for why this decides whether to reserve the tags row. - any_product_has_tags = any( - v["published_tags"] for v in filtered_products_dict.values() - ) - product_display_info = {} for product in products_page: product_display_info[product.id] = self._prepare_product_display_info( @@ -1075,7 +1062,6 @@ class AplicoopWebsiteSale(WebsiteSale): "group_order": group_order, "products": products_page, "filtered_product_tags": filtered_products_dict, - "any_product_has_tags": any_product_has_tags, "product_supplier_info": product_supplier_info, "product_price_info": product_price_info, "product_display_info": product_display_info, @@ -1185,11 +1171,6 @@ class AplicoopWebsiteSale(WebsiteSale): } for product in products_page } - # See eskaera_shop for why this decides whether to reserve the tags row. - any_product_has_tags = any( - v["published_tags"] for v in filtered_products_dict.values() - ) - # Inject draft demand context for ribbons/stock flags products_page, product_max_qty = self._prepare_draft_stock_data(products_page) @@ -1203,7 +1184,6 @@ class AplicoopWebsiteSale(WebsiteSale): "group_order": group_order, "products": products_page, "filtered_product_tags": filtered_products_dict, - "any_product_has_tags": any_product_has_tags, "product_supplier_info": product_supplier_info, "product_price_info": product_price_info, "product_display_info": product_display_info, diff --git a/website_sale_aplicoop/static/src/css/base/variables.css b/website_sale_aplicoop/static/src/css/base/variables.css index 4073997..009f0ce 100644 --- a/website_sale_aplicoop/static/src/css/base/variables.css +++ b/website_sale_aplicoop/static/src/css/base/variables.css @@ -152,7 +152,6 @@ /* ========== PRODUCT CARD MEDIA ========== */ --ac-card-media-ratio: 4 / 3; - --ac-card-media-height: clamp(5rem, 4rem + 6vw, 8.5rem); /* ========== Z-INDEX ========== */ --ac-z-dropdown: 1000; diff --git a/website_sale_aplicoop/static/src/css/components/product-card.css b/website_sale_aplicoop/static/src/css/components/product-card.css index 852270b..6c648aa 100644 --- a/website_sale_aplicoop/static/src/css/components/product-card.css +++ b/website_sale_aplicoop/static/src/css/components/product-card.css @@ -6,13 +6,23 @@ * Markup (eskaera_shop_products): * .product-card-wrapper.product-card * └ .card - * ├ img.product-img-cover | .product-img-placeholder - * ├ .card-body (title, tags, supplier, origin, price) + * ├ .product-media + * │ ├ img.product-img-cover | .product-img-placeholder + * │ └ button.product-info-toggle (only when the product has a + * │ tag, a supplier or an origin to show) + * ├ .card-body (title, .product-info-panel, price) * └ form.add-to-cart-form → components/quantity-control.css * * The wrapper carries the chrome (border, radius, elevation); the inner * Bootstrap .card is neutralised through its own custom properties so the two * boxes stop drawing a double frame. + * + * Tags/supplier/origin used to sit unconditionally in the card body so every + * card's price row lined up regardless of which fields a product had. The + * compact grid has no room for that: they now live in .product-info-panel, + * collapsed by default and revealed by .product-info-toggle (see + * website_sale.js for the click handler, delegated on #products-grid same as + * the quantity stepper). */ .product-card { @@ -41,14 +51,23 @@ /* ---------- Media ---------- */ +.product-media { + position: relative; + flex-shrink: 0; +} + .product-card .product-image, .product-img-cover, .product-img-fixed, .product-img-placeholder { display: block; - flex-shrink: 0; width: 100%; - height: var(--ac-card-media-height); + /* 4:3, not square: a square photo ate too much vertical space on a + two-up mobile grid. aspect-ratio keeps it fluid across card widths + instead of a fixed height that over- or under-crops depending on how + many columns fit. */ + aspect-ratio: var(--ac-card-media-ratio); + height: auto; border-radius: var(--ac-radius-md) var(--ac-radius-md) 0 0; object-fit: cover; background-color: var(--ac-surface-muted); @@ -61,6 +80,68 @@ color: var(--ac-text-muted); } +/* ---------- Info toggle & panel ---------- */ + +.product-info-toggle { + position: absolute; + top: var(--ac-space-2xs); + right: var(--ac-space-2xs); + display: flex; + align-items: center; + justify-content: center; + width: 1.375rem; + height: 1.375rem; + padding: 0; + border: 1px solid var(--ac-border-strong); + border-radius: var(--ac-radius-pill); + font-size: var(--ac-text-2xs); + color: var(--ac-text-secondary); + background-color: rgb(255 255 255 / 90%); + cursor: pointer; + transition: background-color var(--ac-transition-fast), color var(--ac-transition-fast); +} + +.product-info-toggle[aria-expanded="true"] { + color: var(--ac-text-on-fill); + background-color: var(--ac-color-primary-strong); + border-color: var(--ac-color-primary-strong); +} + +.product-info-panel { + display: flex; + flex-direction: column; + gap: var(--ac-space-3xs); + margin-bottom: var(--ac-space-3xs); + padding: var(--ac-space-2xs); + border: 1px solid var(--ac-border); + border-radius: var(--ac-radius-sm); + background-color: var(--ac-surface-sunken); +} + +.product-info-panel[hidden] { + display: none; +} + +@media (hover: hover) and (pointer: fine) { + .product-info-toggle:hover { + color: var(--ac-text-on-fill); + background-color: var(--ac-color-primary); + border-color: var(--ac-color-primary); + } +} + +.product-info-toggle:focus-visible { + outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color); + outline-offset: var(--ac-focus-ring-offset); +} + +@media (pointer: coarse) { + .product-info-toggle { + width: var(--ac-touch-target); + height: var(--ac-touch-target); + } +} + /* ---------- Body ---------- */ .product-card .card-body { @@ -107,10 +188,6 @@ align-content: center; gap: var(--ac-space-3xs); justify-content: center; - /* Reserved even when a product has no tags (rendered empty, aria-hidden — - see eskaera_shop_products), so the supplier/origin/price rows below line - up across every card in the grid row. Matches one row of .badge-km. */ - min-height: calc(var(--ac-text-2xs) * var(--ac-leading-tight) + 2 * var(--ac-space-3xs) + 2px); margin: 0; padding: 0; } @@ -131,14 +208,12 @@ /* ---------- Supplier & origin ---------- */ -/* Both rows are reserved even when empty (rendered aria-hidden — see - eskaera_shop_products) and clamped to a fixed line count, so the price row - below lines up across every card in the grid row regardless of which - products actually have a supplier name or an origin. */ +/* Only ever rendered inside .product-info-panel now (see + eskaera_shop_products), so there is no longer a cross-card alignment to + protect — no reserved space, no forced line clamp. */ .product-card .product-supplier, .product-card .product-origin { margin: 0; - overflow: hidden; font-size: var(--ac-text-xs); font-weight: var(--ac-weight-normal); text-align: center; @@ -146,17 +221,11 @@ } .product-card .product-supplier { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - line-clamp: 2; - min-height: calc(2em * var(--ac-leading-tight)); overflow-wrap: break-word; } .product-card .product-origin { - min-height: calc(1em * var(--ac-leading-tight)); - white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; } diff --git a/website_sale_aplicoop/static/src/css/sections/products-grid.css b/website_sale_aplicoop/static/src/css/sections/products-grid.css index 5151ca3..d994649 100644 --- a/website_sale_aplicoop/static/src/css/sections/products-grid.css +++ b/website_sale_aplicoop/static/src/css/sections/products-grid.css @@ -5,17 +5,18 @@ * * The grid used to pin an exact column count at seven breakpoints (1 → 6). It * now derives the count from the space available, which keeps the card width - * stable no matter how wide the sidebar or the container is. The single query - * left is the deliberate design decision: one card per row on phones, where a - * two-up grid would shrink names and controls below comfortable size. + * stable no matter how wide the sidebar or the container is. * - * 9rem is the card's minimum width; at the widest container (1320px minus the - * category sidebar) it still resolves to six columns, as before. + * Two columns from the smallest phones: with the compact "cromo" card (4:3 + * photo, no inline tags/supplier/origin — see product-card.css) a single + * full-width column read as a tall row rather than a browsable grid. 9rem is + * the card's minimum width from 576px up; at the widest container (1320px + * minus the category sidebar) it still resolves to six columns, as before. */ .products-grid { display: grid; - grid-template-columns: 1fr; + grid-template-columns: repeat(2, 1fr); gap: var(--ac-space-md); margin-bottom: var(--ac-space-xl); } diff --git a/website_sale_aplicoop/static/src/js/website_sale.js b/website_sale_aplicoop/static/src/js/website_sale.js index 6926f18..10eb073 100644 --- a/website_sale_aplicoop/static/src/js/website_sale.js +++ b/website_sale_aplicoop/static/src/js/website_sale.js @@ -1246,6 +1246,23 @@ } }); + // Product info toggle: shows/hides the origin/supplier/tags panel + // that the compact card no longer displays inline (via event + // delegation, same reasoning as the stepper above). + productsGrid.addEventListener("click", function (e) { + var infoBtn = e.target.closest(".product-info-toggle"); + if (!infoBtn) return; + + e.preventDefault(); + var panelId = infoBtn.getAttribute("aria-controls"); + var panel = panelId ? document.getElementById(panelId) : null; + if (!panel) return; + + var isOpen = infoBtn.getAttribute("aria-expanded") === "true"; + infoBtn.setAttribute("aria-expanded", isOpen ? "false" : "true"); + panel.hidden = isOpen; + }); + // Add to cart button (via event delegation on grid) productsGrid.addEventListener("click", handleAddToCart); diff --git a/website_sale_aplicoop/views/website_templates.xml b/website_sale_aplicoop/views/website_templates.xml index e92e9de..0b89f4a 100644 --- a/website_sale_aplicoop/views/website_templates.xml +++ b/website_sale_aplicoop/views/website_templates.xml @@ -274,56 +274,15 @@ -
-
-
-
- - - -
-

-

-
- -
-
- -
-
-
- - - - - - - - - -
-
-
-
-
-
-
+ +
+
My Cart
@@ -362,7 +321,49 @@
-
+ +
+
+ + + + + + + + + +
+
+
+
+ +
+
+
+ + + +
+

+

+
@@ -763,16 +764,31 @@