[IMP] website_sale_aplicoop: compact mobile layout for the Eskaera shop
Reorder cart/tags/category/search/products into one flex row with Bootstrap order utilities, so mobile shows cart, tags, category, search, then products, while desktop keeps its current layout. Product cards become a two-column grid of compact tiles from the smallest phones, with a 4:3 photo instead of a tall rectangle. Origin, supplier and tags move behind a per-product "i" toggle instead of always reserving their own row, which also drops the now-unused any_product_has_tags plumbing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
ef1283be7c
commit
6a59d9f6ad
6 changed files with 217 additions and 152 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -274,56 +274,15 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 eskaera-filters" id="realtimeSearch-filters">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-7">
|
||||
<div class="search-input-wrapper">
|
||||
<label class="visually-hidden" for="realtime-search-input">Search products</label>
|
||||
<input type="search" id="realtime-search-input" class="form-control eskaera-search-input" placeholder="Search products..." autocomplete="off" aria-describedby="realtime-search-status" />
|
||||
<button type="button" id="clear-search-btn" class="search-clear-btn" aria-label="Clear search" title="Clear search">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<p id="realtime-search-status" class="visually-hidden" aria-live="polite" aria-atomic="true" />
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<select name="category" id="realtime-category-select" class="form-select">
|
||||
<option value="">Browse Product Categories</option>
|
||||
<t t-call="website_sale_aplicoop.category_hierarchy_options">
|
||||
<t t-set="categories" t-value="category_hierarchy" />
|
||||
<t t-set="depth" t-value="0" />
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<t t-if="available_tags">
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div id="tag-filter-container" class="tag-filter-badges">
|
||||
<t t-foreach="available_tags" t-as="tag">
|
||||
<!-- Toggle buttons: aria-pressed carries the filter
|
||||
state, which realtime_search.js keeps in sync
|
||||
with the .is-selected / .is-dimmed classes.
|
||||
The record colour rides in a custom property so
|
||||
the stylesheet keeps owning the badge. -->
|
||||
<t t-if="tag['color']">
|
||||
<button type="button" class="badge tag-filter-badge" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" t-att-data-tag-color="tag['color']" t-attf-style="--ac-tag-color: {{ tag['color'] }};" data-toggle="tag-filter" aria-pressed="false">
|
||||
<span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
|
||||
</button>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<button type="button" class="badge tag-filter-badge tag-use-theme-color" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" data-tag-color="" data-toggle="tag-filter" aria-pressed="false">
|
||||
<span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-lg-3 mb-4 mb-lg-0">
|
||||
<!-- One flex row for cart/tags/category/search/products: Bootstrap's
|
||||
order utilities need every reordered item to be a sibling of the
|
||||
same flex container, so the four blocks that used to live in two
|
||||
separate rows (filters above, cart+products below) are now columns
|
||||
here. Unprefixed order-* puts the cart first on mobile (right below
|
||||
the header, above the filters); order-lg-* restores the desktop
|
||||
layout: search+category as one row, then tags, then cart+products. -->
|
||||
<div class="row g-2 eskaera-filters" id="realtimeSearch-filters">
|
||||
<div class="col-12 col-lg-3 order-1 order-lg-4 mb-4 mb-lg-0">
|
||||
<div class="card sticky-top cart-sticky-position" aria-label="Cart Summary">
|
||||
<div class="card-header d-flex justify-content-between align-items-center gap-1">
|
||||
<h6 class="mb-0 cart-title-sm" id="cart-title">My Cart</h6>
|
||||
|
|
@ -362,7 +321,49 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<t t-if="available_tags">
|
||||
<div class="col-12 order-2 order-lg-3">
|
||||
<div id="tag-filter-container" class="tag-filter-badges">
|
||||
<t t-foreach="available_tags" t-as="tag">
|
||||
<!-- Toggle buttons: aria-pressed carries the filter
|
||||
state, which realtime_search.js keeps in sync
|
||||
with the .is-selected / .is-dimmed classes.
|
||||
The record colour rides in a custom property so
|
||||
the stylesheet keeps owning the badge. -->
|
||||
<t t-if="tag['color']">
|
||||
<button type="button" class="badge tag-filter-badge" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" t-att-data-tag-color="tag['color']" t-attf-style="--ac-tag-color: {{ tag['color'] }};" data-toggle="tag-filter" aria-pressed="false">
|
||||
<span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
|
||||
</button>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<button type="button" class="badge tag-filter-badge tag-use-theme-color" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" data-tag-color="" data-toggle="tag-filter" aria-pressed="false">
|
||||
<span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<div class="col-12 col-lg-5 order-3 order-lg-2">
|
||||
<select name="category" id="realtime-category-select" class="form-select">
|
||||
<option value="">Browse Product Categories</option>
|
||||
<t t-call="website_sale_aplicoop.category_hierarchy_options">
|
||||
<t t-set="categories" t-value="category_hierarchy" />
|
||||
<t t-set="depth" t-value="0" />
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-lg-7 order-4 order-lg-1">
|
||||
<div class="search-input-wrapper">
|
||||
<label class="visually-hidden" for="realtime-search-input">Search products</label>
|
||||
<input type="search" id="realtime-search-input" class="form-control eskaera-search-input" placeholder="Search products..." autocomplete="off" aria-describedby="realtime-search-status" />
|
||||
<button type="button" id="clear-search-btn" class="search-clear-btn" aria-label="Clear search" title="Clear search">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<p id="realtime-search-status" class="visually-hidden" aria-live="polite" aria-atomic="true" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-9 order-5 order-lg-5">
|
||||
<div class="oe_structure oe_empty" data-name="Before Products Filter" />
|
||||
<t t-if="products">
|
||||
<div class="products-grid" id="products-grid">
|
||||
|
|
@ -763,16 +764,31 @@
|
|||
</template>
|
||||
<template id="eskaera_shop_products" name="Eskaera Shop Products">
|
||||
<t t-foreach="products" t-as="product">
|
||||
<t t-set="has_tags" t-value="bool(product.product_tag_ids)" />
|
||||
<t t-set="supplier_text" t-value="product_supplier_info.get(product.id)" />
|
||||
<t t-set="has_origin" t-value="bool(product.origin_text)" />
|
||||
<t t-set="has_info" t-value="has_tags or bool(supplier_text) or has_origin" />
|
||||
<div class="product-card-wrapper product-card" t-attf-data-product-name="{{ product.name }}" t-attf-data-category-id="{{ product.categ_id.id if product.categ_id else '' }}" t-attf-data-product-tags="{{ ','.join(str(t.id) for t in product.product_tag_ids) if product.product_tag_ids else '' }}">
|
||||
<div class="card h-100">
|
||||
<t t-if="product.image_128">
|
||||
<img t-attf-src="data:image/png;base64,{{ product.image_128.decode() }}" class="card-img-top product-img-cover" t-attf-alt="{{ product.name }}" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="card-img-top bg-light d-flex align-items-center justify-content-center product-img-placeholder">
|
||||
<i class="fa fa-image fa-3x text-muted" aria-hidden="true" />
|
||||
</div>
|
||||
</t>
|
||||
<div class="product-media">
|
||||
<t t-if="product.image_128">
|
||||
<img t-attf-src="data:image/png;base64,{{ product.image_128.decode() }}" class="card-img-top product-img-cover" t-attf-alt="{{ product.name }}" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="card-img-top bg-light d-flex align-items-center justify-content-center product-img-placeholder">
|
||||
<i class="fa fa-image fa-3x text-muted" aria-hidden="true" />
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="has_info">
|
||||
<!-- Origin/supplier/tags used to sit unconditionally in the card
|
||||
body; on the compact "cromo" tile there is no room to show
|
||||
them inline, so they move behind this toggle (see
|
||||
.product-info-panel below). -->
|
||||
<button type="button" class="product-info-toggle" aria-expanded="false" t-attf-aria-controls="product-info-{{ product.id }}" aria-label="Show origin, supplier and tags" title="Origin, supplier and tags">
|
||||
<i class="fa fa-info" aria-hidden="true" t-translation="off" />
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="product.dynamic_ribbon_id">
|
||||
<t t-set="ribbon" t-value="product.dynamic_ribbon_id" />
|
||||
<span t-attf-class="o_ribbon {{ ribbon._get_position_class() }} z-1" t-attf-style="color: {{ ribbon.text_color }}; background-color: {{ ribbon.bg_color }};" t-esc="ribbon.name" />
|
||||
|
|
@ -783,53 +799,36 @@
|
|||
</t>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h6 class="card-title" t-esc="product.name" />
|
||||
<!-- Tags/supplier/origin always occupy their slot (empty and
|
||||
aria-hidden when the product has no value) so that the price
|
||||
row below lines up across every card in the grid row,
|
||||
regardless of which optional fields a given product has. The
|
||||
tags row itself is only reserved when at least one product in
|
||||
this batch actually has a tag (any_product_has_tags, computed
|
||||
controller-side) — otherwise it would add a permanent empty
|
||||
gap to every card for a feature nobody in the list uses. -->
|
||||
<t t-if="any_product_has_tags">
|
||||
<t t-if="product.product_tag_ids">
|
||||
<div class="product-tags">
|
||||
<t t-foreach="filtered_product_tags.get(product.id, {}).get('published_tags', product.product_tag_ids)" t-as="tag">
|
||||
<t t-if="tag.color">
|
||||
<!-- Per-record colour: passed as a custom property so the
|
||||
stylesheet keeps owning the badge, no !important needed. -->
|
||||
<span class="badge badge-km" t-attf-style="--ac-tag-color: {{ tag.color }};" t-esc="tag.name" />
|
||||
<t t-if="has_info">
|
||||
<div t-attf-id="product-info-{{ product.id }}" class="product-info-panel" hidden="hidden">
|
||||
<t t-if="has_tags">
|
||||
<div class="product-tags">
|
||||
<t t-foreach="filtered_product_tags.get(product.id, {}).get('published_tags', product.product_tag_ids)" t-as="tag">
|
||||
<t t-if="tag.color">
|
||||
<!-- Per-record colour: passed as a custom property so the
|
||||
stylesheet keeps owning the badge, no !important needed. -->
|
||||
<span class="badge badge-km" t-attf-style="--ac-tag-color: {{ tag.color }};" t-esc="tag.name" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge badge-km tag-use-theme-color" t-esc="tag.name" />
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge badge-km tag-use-theme-color" t-esc="tag.name" />
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="product-tags" aria-hidden="true" />
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="product_supplier_info.get(product.id)">
|
||||
<p class="product-supplier">
|
||||
<small>
|
||||
<t t-esc="product_supplier_info[product.id]" />
|
||||
</small>
|
||||
</p>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<p class="product-supplier" aria-hidden="true" />
|
||||
</t>
|
||||
<t t-if="product.origin_text">
|
||||
<p class="product-origin">
|
||||
<small>
|
||||
<i class="fa fa-map-marker" aria-hidden="true" />
|
||||
<t t-out="product.origin_text" />
|
||||
</small>
|
||||
</p>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<p class="product-origin" aria-hidden="true" />
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="supplier_text">
|
||||
<p class="product-supplier">
|
||||
<small t-esc="supplier_text" />
|
||||
</p>
|
||||
</t>
|
||||
<t t-if="has_origin">
|
||||
<p class="product-origin">
|
||||
<small>
|
||||
<i class="fa fa-map-marker" aria-hidden="true" />
|
||||
<t t-out="product.origin_text" />
|
||||
</small>
|
||||
</p>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-set="price_info" t-value="product_price_info.get(product.id, {})" />
|
||||
<t t-set="display_price" t-value="product_display_info.get(product.id, {}).get('display_price', 0.0)" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue