[IMP] website_sale_aplicoop: accessibility and responsive polish — phase 8

Quantity control layout: changed from wrapping flex to deliberate two-row
grid so the add-to-cart button sits full-width underneath the stepper,
avoiding accidental line breaks and giving the primary action more pulsing
area. Also removed the /Kg suffix's redundant font rules.

Order card delivery row: reordered to badge-then-date (visually centred),
removed the "Delivery" label, and swapped bg-primary to text-bg-primary for
proper contrast on the home-delivery badge (3.13:1 minimum).

Tooltip translations: moved hardcoded static tooltips from data-bs-title
(untranslatable) to title (QWeb-translatable). Handled the edge case of
"Save Cart" and "Back to Cart" which had translations but no model_terms
reference in the POT, causing the merge to discard them — added the view
reference so translations now apply.

Load-from-history page: added accessibility: a visible status message
("Loading your order…"), a <noscript> fallback with link to the group
order, lang and viewport meta tags, and localised strings for all three
languages. Gave the page a minimal inline style block since it does not
inherit the token system (no website.layout).

Translations: 11 new entries (es/eu/ca) for new/reworded UI strings, plus
two code catalogue entries (products found, Close) that needed the
#. odoo-python comment for _() resolution. Documented the silent-failure
pattern in docs/TRANSLATIONS.md: the POT merge, untranslatable attributes,
and missing code comments.

Tests: 246 passing. Pre-commit: clean.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
GitHub Copilot 2026-08-16 11:34:38 +02:00
parent 162ba0b570
commit 5de7573fba
35 changed files with 2181 additions and 2413 deletions

View file

@ -1,3 +1,4 @@
# CSS architecture
## File layout
@ -79,8 +80,10 @@ Other rules:
should do.
- No ID selectors for styling; IDs are for anchors and `aria-*` references.
- No element selectors outside `base/` (`div.product-card` locks the markup).
- `!important` is allowed in exactly two places: the `prefers-reduced-motion` reset, and a documented
override of a core rule you cannot scope — with a comment saying which rule and why.
- `!important` is allowed in exactly three places, each with a comment saying why: the
`prefers-reduced-motion` reset; a genuine **utility** that must beat whatever component it lands on
(`.hidden-product`, Bootstrap's own utility layer works this way); and a documented override of a
core rule you cannot scope. Anything else means the selector is wrong.
## Ordering inside a file

View file

@ -40,7 +40,6 @@ Los siguientes addons custom han sido actualizados con la estructura OCA:
Todos los addons incluyen correctamente:
- **Autor**: Criptomart (en `__manifest__.py` y `README.rst`)
- **Financiador**: Elika Bilbo (en `readme/CREDITS.rst`)
## Logo de CriptoMart

View file

@ -106,10 +106,51 @@ PEDIR AL USUARIO QUE GENERE EL POT DESDE LA UI DE ODOO
docker-compose cp odoo:/tmp/addon_name.pot ./addon_name/i18n/
```
#### El `.pot` no es opcional: Odoo fusiona los `.po` contra él
El `.pot` es un artefacto **generado** y está en `.gitignore` (`*.pot`), pero no es
prescindible: al importar, Odoo fusiona cada `.po` contra el `.pot` del módulo antes
de leerlo.
```python
# odoo/tools/translate.py, PoFileReader.__init__
if pot_path:
self.pofile.merge(polib.pofile(pot_path))
```
`merge()` de polib **sustituye las ocurrencias de cada entrada por las del POT**, y
marca como obsoletas las entradas que el POT no tiene (el lector se salta las
obsoletas). Consecuencias prácticas:
- Añadir una ocurrencia `model_terms:ir.ui.view,...` solo al `.po` **no sirve de
nada**: el merge la descarta y la traducción nunca llega a la vista.
- Una cadena nueva que no esté en el POT se ignora entera, por muy traducida que
esté en los tres idiomas.
- Por eso, tras mover una cadena a un atributo traducible o añadir una nueva, hay
que **regenerar el POT** antes de que la traducción surta efecto.
⚠️ Al exportar contra una BD **con datos demo**, el POT se lleva los nombres de los
registros demo (productos, impuestos, nombres de pedidos). Genera el POT contra una
BD limpia.
### 2. Actualizar Archivos .po Existentes
no usar msmerge, corrompe el po. Usa polib.
Al crear entradas con polib hay dos detalles que fallan **en silencio**:
```python
entry = polib.POEntry(
msgid="products found",
msgstr="produktu aurkitu dira",
# 1. Sin `module:` el lector revienta; sin `odoo-python` la cadena se lee
# del .po y se descarta acto seguido (ver Troubleshooting).
comment="module: website_sale_aplicoop\nodoo-python",
# 2. El número de línea es obligatorio en ocurrencias `code:`.
occurrences=[("code:addons/website_sale_aplicoop/models/js_translations.py", "0")],
)
```
### 3. Traducir Términos Nuevos
Editar los archivos `.po` y completar las traducciones:
@ -245,6 +286,58 @@ name = fields.Char(string="Name")
4. **Usuario con idioma incorrecto**: Verificar preferencias de usuario
5. **Cache**: Reiniciar Odoo: `docker-compose restart odoo`
### La cadena SÍ está traducida en el .po y sigue saliendo en inglés
Los tres casos siguientes fallan sin dar ningún error, ni en el log ni al importar.
**a) El atributo no es traducible.** QWeb solo traduce los de esta lista
(`odoo/tools/translate.py`, `TRANSLATED_ATTRS`), más sus variantes `t-attf-`:
```text
string help placeholder alt title label data-tooltip confirm
aria-label aria-keyshortcuts aria-placeholder aria-roledescription aria-valuetext
```
`data-bs-title` **no está**: un tooltip escrito ahí nunca se traduce. Usa `title`
(Bootstrap lo respeta igual) o pasa el texto desde el endpoint i18n. Tampoco están
`aria-description` ni `data-bs-*` en general.
**b) Falta la referencia a la vista en el POT.** Para que una traducción se aplique
a una vista, su entrada necesita una ocurrencia
`model_terms:ir.ui.view,arch_db:modulo.xmlid` **en el POT** (ver el apartado del
merge más arriba). Síntoma típico: dos botones contiguos, uno traducido y el otro
no, con las dos cadenas presentes en el `.po`. Se comprueba así:
```bash
grep -B 4 '^msgid "Save Cart"$' addon/i18n/addon.pot # ¿tiene model_terms?
```
**c) Falta el comentario `#. odoo-python`.** Las cadenas de `_()` se leen del `.po`
directamente (no de la BD), y `CodeTranslations._load_python_translations` filtra
por ese comentario:
```python
def filter_func(row):
return row.get('value') and PYTHON_TRANSLATION_COMMENT in row['comments']
```
Sin él la entrada se lee y se descarta. La entrada tiene que quedar así:
```po
#. module: website_sale_aplicoop
#. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "products found"
msgstr "produktu aurkitu dira"
```
Y ojo con el `:0` final: el lector hace `int(line_number)` sobre las ocurrencias
`code:`, así que una sin número de línea lanza `ValueError` y **tumba la carga del
catálogo entero**, no solo la de esa entrada.
Estas cadenas se cachean por proceso, así que después de tocarlas hay que
reiniciar Odoo (`docker-compose restart odoo`), no basta con `-u`.
### Caracteres Especiales
Usar escape correcto en .po:

View file

@ -2249,6 +2249,9 @@ class AplicoopWebsiteSale(WebsiteSale):
"website_sale_aplicoop.eskaera_load_from_history",
{
"group_order_id": group_order_id,
# This template renders a bare HTML document instead of
# website.layout, so it has to declare its own language.
"page_lang": (request.env.lang or "en_US").replace("_", "-"),
# sessionStorage keys stay keyed by id; the redirect at
# the end of the template needs the public URL instead.
"group_order_url": self._eskaera_url(group_order),

View file

@ -129,6 +129,11 @@ def _get_translated_labels(self, lang=None, request_obj=None):
"delivery_info_template": tr(
"Delivery Information: Your order will be delivered at {pickup_day} {pickup_date}"
),
# Announced in the search results live region (realtime_search.js)
"no_results": tr("No products found"),
"products_found": tr("products found"),
# Accessible name of the notification dismiss button (website_sale.js)
"close": tr("Close"),
"important": tr("Important"),
"confirm_order_warning": tr(
"Once you confirm this order, you will not be able to modify it. Please review carefully before confirming."

View file

@ -12,7 +12,7 @@ This module has complete translation support for **3 languages**:
## Translated Content
Each `.po` file contains **378 translations** for:
Each `.po` file contains **388 translations** for:
- **Selection Field Options** (Days of week, Recurrence periods)
- Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
@ -41,11 +41,26 @@ When users switch their Odoo interface language to any of the supported language
To add or update translations:
1. Edit the corresponding `.po` file
1. Edit the corresponding `.po` file **with `polib`** (never `msgmerge`, it corrupts them)
2. Update the `msgstr` values (keep `msgid` unchanged)
3. Save and reload the module in Odoo
4. Translations apply immediately
### Adding a *new* string is not just a `.po` edit
`website_sale_aplicoop.pot` is generated by Odoo and is gitignored, but it is not
optional: Odoo merges every `.po` against it before importing, and polib's `merge()`
drops entries the POT does not have and replaces their occurrences with the POT's.
A brand new string, or an existing one that just became a view term, needs the POT
regenerated (from the Odoo UI, against a database **without demo data**) before its
translation applies anywhere.
Three failure modes that produce no error at all — an untranslatable attribute
(`data-bs-title` is not on QWeb's list, `title` is), a missing
`model_terms:ir.ui.view` reference in the POT, and a missing `#. odoo-python`
comment on `_()` strings — are written up in
[`docs/TRANSLATIONS.md`](../../docs/TRANSLATIONS.md), under *Troubleshooting*.
Example entry in a `.po` file:
```po
#. module: website_sale_aplicoop
@ -57,7 +72,7 @@ msgstr "Pedido de Grupo" # Spanish translation
- All `.po` files were generated and tested: **40/40 tests passing**
- Translation coverage: **100%** for all supported languages
- Last updated: August 13, 2026
- Last updated: August 16, 2026
---

View file

@ -288,6 +288,7 @@ msgstr "Nombre de productes disponibles"
#. module: website_sale_aplicoop
#. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout
msgid "Back to Cart"
msgstr "Torna al carret"
@ -416,7 +417,9 @@ msgid "Clear search"
msgstr "Neteja la cerca"
#. module: website_sale_aplicoop
#. odoo-python
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "Close"
msgstr "Tanca"
@ -1899,6 +1902,7 @@ msgstr "Dissabte"
#. module: website_sale_aplicoop
#. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
msgid "Save Cart"
msgstr "Desa el Carret"
@ -2298,3 +2302,63 @@ msgstr "Desa l'Esborrany"
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "Save order as draft"
msgstr "Desa la comanda com a esborrany"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
msgid "Search products"
msgstr "Cerca productes"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary
msgid "Order summary"
msgstr "Resum de la comanda"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary
msgid "Products in this order, with quantity, price and subtotal"
msgstr "Productes d'aquesta comanda, amb quantitat, preu i subtotal"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
msgid "Loading your order…"
msgstr "Carregant la teva comanda…"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
msgid "This page needs JavaScript to load the order into your cart."
msgstr ""
"Aquesta pàgina necessita JavaScript per carregar la comanda al teu carret."
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
msgid "Continue to the group order"
msgstr "Continua a la comanda de grup"
#. module: website_sale_aplicoop
#. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "products found"
msgstr "productes trobats"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
msgid "<i class=\"fa fa-download me-2\" aria-hidden=\"true\"/>Load More Products"
msgstr "<i class=\"fa fa-download me-2\" aria-hidden=\"true\"/>Carregar més productes"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta
msgid ""
"<span class=\"badge special-order-badge\"><i class=\"fa fa-star\" aria-"
"hidden=\"true\"/> Special Order</span>"
msgstr ""
"<span class=\"badge special-order-badge\"><i class=\"fa fa-star\" aria-"
"hidden=\"true\"/> Comanda especial</span>"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta
msgid ""
"<span class=\"badge text-bg-success\"><i class=\"fa fa-truck\" aria-"
"hidden=\"true\"/> Home Delivery</span>"
msgstr ""
"<span class=\"badge text-bg-success\"><i class=\"fa fa-truck\" aria-"
"hidden=\"true\"/> Lliurament a domicili</span>"

View file

@ -290,6 +290,7 @@ msgstr "Cantidad de Productos Disponibles"
#. module: website_sale_aplicoop
#. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout
msgid "Back to Cart"
msgstr "Volver al Carrito"
@ -418,7 +419,9 @@ msgid "Clear search"
msgstr "Limpiar búsqueda"
#. module: website_sale_aplicoop
#. odoo-python
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "Close"
msgstr "Cerrar"
@ -1899,6 +1902,7 @@ msgstr "Sábado"
#. module: website_sale_aplicoop
#. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
msgid "Save Cart"
msgstr "Guardar Carrito"
@ -2295,3 +2299,62 @@ msgstr "Grabar pedido"
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "Save order as draft"
msgstr "Grabar pedido"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
msgid "Search products"
msgstr "Buscar productos"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary
msgid "Order summary"
msgstr "Resumen del pedido"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary
msgid "Products in this order, with quantity, price and subtotal"
msgstr "Productos de este pedido, con cantidad, precio y subtotal"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
msgid "Loading your order…"
msgstr "Cargando tu pedido…"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
msgid "This page needs JavaScript to load the order into your cart."
msgstr "Esta página necesita JavaScript para cargar el pedido en tu carrito."
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
msgid "Continue to the group order"
msgstr "Continuar al pedido de grupo"
#. module: website_sale_aplicoop
#. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "products found"
msgstr "productos encontrados"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
msgid "<i class=\"fa fa-download me-2\" aria-hidden=\"true\"/>Load More Products"
msgstr "<i class=\"fa fa-download me-2\" aria-hidden=\"true\"/>Cargar más productos"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta
msgid ""
"<span class=\"badge special-order-badge\"><i class=\"fa fa-star\" aria-"
"hidden=\"true\"/> Special Order</span>"
msgstr ""
"<span class=\"badge special-order-badge\"><i class=\"fa fa-star\" aria-"
"hidden=\"true\"/> Pedido Especial</span>"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta
msgid ""
"<span class=\"badge text-bg-success\"><i class=\"fa fa-truck\" aria-"
"hidden=\"true\"/> Home Delivery</span>"
msgstr ""
"<span class=\"badge text-bg-success\"><i class=\"fa fa-truck\" aria-"
"hidden=\"true\"/> Entrega a Casa</span>"

View file

@ -291,6 +291,7 @@ msgstr "Eskuragarri Dauden Produktuen Kopurua"
#. module: website_sale_aplicoop
#. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout
msgid "Back to Cart"
msgstr "Sarera Itzuli"
@ -419,7 +420,9 @@ msgid "Clear search"
msgstr "Bilaketa garbitu"
#. module: website_sale_aplicoop
#. odoo-python
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "Close"
msgstr "Itxi"
@ -1895,6 +1898,7 @@ msgstr "Larunbata"
#. module: website_sale_aplicoop
#. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
msgid "Save Cart"
msgstr "Saskia Gorde"
@ -2293,3 +2297,64 @@ msgstr "Eskaera gorde"
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "Save order as draft"
msgstr "Eskaera gorde"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
msgid "Search products"
msgstr "Produktuak bilatu"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary
msgid "Order summary"
msgstr "Eskaera laburpena"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary
msgid "Products in this order, with quantity, price and subtotal"
msgstr "Eskaera honetako produktuak, kantitatea, prezioa eta azpitotala"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
msgid "Loading your order…"
msgstr "Zure eskaera kargatzen…"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
msgid "This page needs JavaScript to load the order into your cart."
msgstr "Orri honek JavaScript behar du eskaera zure saskira kargatzeko."
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
msgid "Continue to the group order"
msgstr "Jarraitu taldeko eskaerara"
#. module: website_sale_aplicoop
#. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "products found"
msgstr "produktu aurkitu dira"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
msgid "<i class=\"fa fa-download me-2\" aria-hidden=\"true\"/>Load More Products"
msgstr ""
"<i class=\"fa fa-download me-2\" aria-hidden=\"true\"/>Produktu Gehiago "
"Kargatu"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta
msgid ""
"<span class=\"badge special-order-badge\"><i class=\"fa fa-star\" aria-"
"hidden=\"true\"/> Special Order</span>"
msgstr ""
"<span class=\"badge special-order-badge\"><i class=\"fa fa-star\" aria-"
"hidden=\"true\"/> Eskaera Berezia</span>"
#. module: website_sale_aplicoop
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta
msgid ""
"<span class=\"badge text-bg-success\"><i class=\"fa fa-truck\" aria-"
"hidden=\"true\"/> Home Delivery</span>"
msgstr ""
"<span class=\"badge text-bg-success\"><i class=\"fa fa-truck\" aria-"
"hidden=\"true\"/> Etxerako Entrega</span>"

View file

@ -140,9 +140,17 @@ def _register_translations():
_("Search")
_("Search products...")
_("No products found")
# Announced with a count in the search results live region, e.g. "7 products found"
_("products found")
_("Categories")
_("All categories")
# ========================
# Accessible Names
# ========================
# Dismiss button of the floating notification (website_sale.js)
_("Close")
# ========================
# Category Labels
# ========================

View file

@ -16,7 +16,7 @@ website_sale_aplicoop/static/src/css/
├── base/
│ ├── variables.css ← Variables CSS globales (colores, tipografía, espaciados)
│ └── utilities.css ← Clases utilitarias (.sr-only, .text-muted, etc)
│ └── utilities.css ← Clases utilitarias (.text-muted, .word-wrap-break, etc)
├── layout/
│ ├── pages.css ← Fondos y layouts de páginas (eskaera-page, etc)
@ -62,7 +62,8 @@ website_sale_aplicoop/static/src/css/
- **variables.css** (~80 líneas)
Colores, tipografía, espaciados, sombras, transiciones, z-index
- **utilities.css** (~15 líneas)
Clases utilitarias reutilizables (.sr-only, .text-muted, etc)
Clases utilitarias reutilizables (.text-muted, .word-wrap-break, etc).
Para texto solo-lector-de-pantalla usa `.visually-hidden` de Bootstrap.
### **layout/** - Estructura Global
- **pages.css** (~70 líneas)

View file

@ -1,34 +1,26 @@
/* filepath: website_sale_aplicoop/static/src/css/base/utilities.css */
/**
* Utility classes used throughout the project
* Utility classes
*
* Screen-reader-only text: use Bootstrap's own `.visually-hidden` /
* `.visually-hidden-focusable`, shipped in web.assets_frontend. The local
* `.sr-only` copy (the Bootstrap 4 name, dropped in 5.x) is gone, as are
* `.word-wrap-break` and `.text-xs`, which no template ever used.
*/
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.text-muted {
color: var(--text-secondary) !important;
}
.word-wrap-break {
word-wrap: break-word;
overflow-wrap: break-word;
}
.text-xs {
font-size: 0.85rem;
/* Scoped to the addon's pages: unscoped this redefined Bootstrap's .text-muted
for the whole frontend, and needed !important to do it. Two classes beat
Bootstrap's one, so the flag is no longer necessary. */
:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) .text-muted {
color: var(--ac-text-secondary);
}
/* Set by realtime_search.js to filter the grid. A utility has to beat the
component it lands on .product-card declares display:flex at the same
specificity and loads later so this is the one place the flag is the
mechanism rather than a workaround. Bootstrap's own utility layer does the
same for every one of its classes. */
.hidden-product {
display: none !important;
}

View file

@ -1,72 +1,228 @@
/* filepath: website_sale_aplicoop/static/src/css/base/variables.css */
/**
* CSS Custom Properties (Variables)
* Colores, tipografía, espaciados centralizados
* Design tokens (CSS custom properties)
*
* The ONLY file in this addon allowed to contain colour literals.
*
* Odoo 18 ships Bootstrap 5.3 with `$variable-prefix: ''`
* (web/static/src/scss/bootstrap_overridden.scss), so Bootstrap's root custom
* properties are exposed UNPREFIXED: --primary, --body-bg, --border-color,
* Brand tokens alias them so the website theme editor keeps control of the palette;
* the literal is only the fallback when the bundle is loaded outside a themed page.
*
* Contrast ratios noted below are against --ac-surface (#fff) and are the reason
* a given literal was chosen. Do not "tidy" them without re-checking WCAG AA.
*/
:root {
/* ========== COLORS ========== */
--primary-color: var(--primary, #007bff);
--primary-dark: var(--primary-dark, #0056b3);
--secondary-color: var(--secondary, #6c757d);
--success-color: var(--success, #28a745);
--danger-color: #dc3545;
--warning-color: #ffc107;
--info-color: #17a2b8;
--light-color: #f8f9fa;
/* ========== BRAND ========== */
--ac-color-primary: var(--primary, #007bff);
/* White text on --ac-color-primary is only 3.98:1 (fails AA for body text).
Use -strong whenever text sits ON the colour: 7.04:1. */
--ac-color-primary-strong: #0056b3;
--ac-color-secondary: var(--secondary, #6c757d);
--ac-color-accent: #7c3aed; /* white on it: 5.70:1 */
--ac-color-accent-strong: #6d28d9; /* white on it: 7.10:1 */
/* Order cards carry their own indigo accent, and special orders an amber one. */
--ac-color-order: #5a67d8; /* white on it: 4.81:1 */
--ac-color-order-strong: #4c57bd; /* 6.17:1 */
--ac-color-order-deep: #3d4898; /* 8.14:1 */
--ac-color-order-border: rgb(90 103 216 / 12%);
--ac-color-order-border-hover: rgb(90 103 216 / 25%);
--ac-surface-order: linear-gradient(180deg, #fff 0%, #fbfdff 100%);
--ac-surface-order-hover: linear-gradient(180deg, #fff 0%, #f7faff 100%);
--ac-color-special: #f59e0b;
--ac-color-special-strong: #d97706;
--ac-color-special-border: rgb(245 158 11 / 35%);
--ac-color-special-border-hover: rgb(245 158 11 / 50%);
--ac-surface-special: linear-gradient(180deg, #fffdf0 0%, #fffbea 100%);
--ac-surface-special-hover: linear-gradient(180deg, #fffdf0 0%, #fff8db 100%);
/* White on amber is 2.15:1. Amber always takes dark text: 7.60:1. */
--ac-text-on-special: #1a202c;
/* ========== FEEDBACK ========== */
--ac-color-success: var(--success, #28a745);
/* #28a745 is only 3.13:1 on white — fine as a fill, not as text. */
--ac-color-success-strong: #1e7e34; /* 5.14:1 */
--ac-color-success-deep: #1a6e2e; /* hover/active on -strong */
/* Warning alert: amber surface with its own readable foreground (6.30:1). */
--ac-surface-warning: #fef3c7;
--ac-border-warning: #fcd34d;
--ac-text-warning: #92400e;
--ac-color-danger: var(--danger, #dc3545);
--ac-color-warning: var(--warning, #ffc107);
--ac-color-info: var(--info, #17a2b8);
/* ========== SURFACES ========== */
--ac-surface: var(--body-bg, #fff);
--ac-surface-sunken: var(--light, #f8f9fa);
--ac-surface-muted: #f3f3f3;
--ac-surface-tint: rgb(0 123 255 / 6%);
--ac-surface-tint-hover: rgb(108 117 125 / 10%);
/* ========== TEXT (all ≥ 4.5:1 on --ac-surface) ========== */
--ac-text-primary: var(--body-color, #1a202c); /* 16.32:1 */
--ac-text-secondary: #4a5568; /* 7.53:1 */
--ac-text-muted: #6b7280; /* 4.83:1 */
--ac-text-on-fill: #fff;
/* ========== BORDERS ========== */
/* Decorative separators and card outlines. */
--ac-border: var(--border-color, #e2e8f0);
--ac-border-subtle: #e0e0e0;
/* Boundaries of interactive controls (inputs, steppers): WCAG 1.4.11 needs
3:1 against the adjacent surface. #8a919a is 3.18:1. */
--ac-border-strong: #8a919a;
/* ========== FOCUS RING ========== */
--ac-focus-ring-color: var(--primary, #007bff); /* 3.98:1 — ok for a focus indicator */
--ac-focus-ring-width: 2px;
--ac-focus-ring-offset: 2px;
/* ========== TYPOGRAPHY ========== */
--ac-font-family: var(
--font-sans-serif,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
sans-serif
);
/* Fluid scale: one continuous ramp instead of per-breakpoint overrides.
Always rem + vw, never pure vw, so browser zoom keeps working. */
--ac-text-2xs: clamp(0.6875rem, 0.66rem + 0.1vw, 0.75rem);
--ac-text-xs: clamp(0.75rem, 0.72rem + 0.12vw, 0.8125rem);
--ac-text-sm: clamp(0.8125rem, 0.79rem + 0.14vw, 0.875rem);
--ac-text-base: clamp(0.875rem, 0.85rem + 0.18vw, 1rem);
--ac-text-md: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
--ac-text-lg: clamp(1.125rem, 1.06rem + 0.3vw, 1.375rem);
--ac-text-xl: clamp(1.375rem, 1.25rem + 0.6vw, 1.75rem);
--ac-text-2xl: clamp(1.75rem, 1.5rem + 1vw, 2.25rem);
--ac-leading-tight: 1.2;
--ac-leading-snug: 1.35;
--ac-leading-normal: 1.5;
--ac-weight-normal: 400;
--ac-weight-medium: 500;
--ac-weight-semibold: 600;
--ac-weight-bold: 700;
/* ========== SPACING ========== */
--ac-space-3xs: 0.125rem;
--ac-space-2xs: 0.25rem;
--ac-space-xs: 0.375rem;
--ac-space-sm: 0.5rem;
--ac-space-md: 1rem;
--ac-space-lg: 1.5rem;
--ac-space-xl: 2rem;
--ac-space-2xl: 3rem;
/* ========== RADIUS ========== */
--ac-radius-sm: 0.25rem;
--ac-radius-md: 0.5rem;
--ac-radius-lg: 0.75rem;
--ac-radius-xl: 1rem;
--ac-radius-pill: 50rem;
/* ========== ELEVATION ========== */
--ac-shadow-sm: 0 2px 8px rgb(0 0 0 / 8%);
--ac-shadow-md: 0 4px 12px rgb(0 0 0 / 10%);
--ac-shadow-lg: 0 10px 25px rgb(0 0 0 / 12%);
/* ========== MOTION ========== */
--ac-transition-fast: 150ms ease-out;
--ac-transition-normal: 220ms cubic-bezier(0.2, 0.9, 0.2, 1);
--ac-transition-slow: 400ms ease;
/* ========== CONTROLS ========== */
/* Fluid control size: replaces six per-breakpoint width/height overrides. */
--ac-control-size: clamp(2.25rem, 2.05rem + 0.7vw, 2.5rem);
/* 44px. WCAG 2.2 AA (2.5.8) only requires 24px, but 44px is the AAA figure
(2.5.5) and the practical minimum for touch. Applied on coarse pointers. */
--ac-touch-target: 2.75rem;
--ac-control-border-width: 1px;
/* ========== 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;
--ac-z-sticky: 1020;
--ac-z-fixed: 1030;
--ac-z-modal: 1040;
--ac-z-popover: 1050;
--ac-z-tooltip: 1060;
--ac-z-notification: 9999;
/* ==========================================================================
LEGACY ALIASES do not use in new code.
Kept so the not-yet-migrated partials keep rendering. Delete each one once
`grep -rn "var(--<name>)" static/src/` comes back empty.
========================================================================== */
--primary-color: var(--ac-color-primary);
/* Was `var(--primary-dark, #0056b3)`: a custom property cannot reference
itself that made the declaration invalid at computed-value time, so the
five consumers in buttons.css / order-card.css silently rendered with no
colour at all. */
--primary-dark: var(--ac-color-primary-strong);
--secondary-color: var(--ac-color-secondary);
--success-color: var(--ac-color-success);
--danger-color: var(--ac-color-danger);
--warning-color: var(--ac-color-warning);
--info-color: var(--ac-color-info);
--light-color: var(--ac-surface-sunken);
--dark-color: #2d3748;
/* Text colors */
--text-primary: #1a202c;
--text-secondary: #4a5568;
--text-muted: #6b7280;
--text-primary: var(--ac-text-primary);
--text-secondary: var(--ac-text-secondary);
--text-muted: var(--ac-text-muted);
/* Border colors */
--border-light: #e2e8f0;
--border-light: var(--ac-border);
--border-medium: #cbd5e0;
--border-dark: #718096;
/* ========== TYPOGRAPHY ========== */
--font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
--font-family-base: var(--ac-font-family);
--font-weight-light: 300;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-normal: var(--ac-weight-normal);
--font-weight-medium: var(--ac-weight-medium);
--font-weight-semibold: var(--ac-weight-semibold);
--font-weight-bold: var(--ac-weight-bold);
--font-weight-extrabold: 800;
/* ========== SPACING ========== */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
--spacing-xs: var(--ac-space-2xs);
--spacing-sm: var(--ac-space-sm);
--spacing-md: var(--ac-space-md);
--spacing-lg: var(--ac-space-lg);
--spacing-xl: var(--ac-space-xl);
--spacing-2xl: var(--ac-space-2xl);
/* ========== BORDER RADIUS ========== */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-sm: var(--ac-radius-sm);
--radius-md: var(--ac-radius-md);
--radius-lg: var(--ac-radius-lg);
--radius-xl: var(--ac-radius-xl);
/* ========== SHADOWS ========== */
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.12);
--shadow-sm: var(--ac-shadow-sm);
--shadow-md: var(--ac-shadow-md);
--shadow-lg: var(--ac-shadow-lg);
/* ========== TRANSITIONS ========== */
--transition-fast: 200ms ease;
--transition-normal: 320ms cubic-bezier(0.2, 0.9, 0.2, 1);
--transition-normal: var(--ac-transition-normal);
--transition-slow: 500ms ease;
/* ========== Z-INDEX ========== */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal: 1040;
--z-popover: 1050;
--z-tooltip: 1060;
--z-notification: 9999;
--z-dropdown: var(--ac-z-dropdown);
--z-sticky: var(--ac-z-sticky);
--z-fixed: var(--ac-z-fixed);
--z-modal: var(--ac-z-modal);
--z-popover: var(--ac-z-popover);
--z-tooltip: var(--ac-z-tooltip);
--z-notification: var(--ac-z-notification);
}

View file

@ -1,77 +1,68 @@
/* filepath: website_sale_aplicoop/static/src/css/components/alerts.css */
/**
* Alert and notification component styles
* Alerts, toasts and notifications
*
* `.alert-warning` is scoped to the addon's pages: unscoped it redefined
* Bootstrap's own alert for the whole frontend. `.group-order-alert` and its
* .info/.warning variants were dead no template emits them.
*/
.group-order-alert {
background-color: #e7f3ff;
border-left: 4px solid var(--primary-color);
color: #004085;
padding: 1rem;
border-radius: 0.375rem;
}
.group-order-alert.info {
background-color: #d1ecf1;
border-left-color: #17a2b8;
color: #0c5460;
}
.group-order-alert.warning {
background-color: #fff3cd;
border-left-color: #ffc107;
color: #856404;
}
:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
.alert-warning {
background-color: #fef3c7;
border-color: #fcd34d;
color: #92400e;
margin-bottom: 2rem;
border-radius: 0.5rem;
padding: 1.25rem;
--alert-bg: var(--ac-surface-warning);
--alert-border-color: var(--ac-border-warning);
--alert-color: var(--ac-text-warning);
margin-bottom: var(--ac-space-xl);
padding: var(--ac-space-md);
border-radius: var(--ac-radius-md);
}
.alert-warning i {
margin-right: 0.75rem;
font-size: 1.1rem;
:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
.alert-warning
i {
margin-right: var(--ac-space-xs);
font-size: var(--ac-text-md);
}
.alert-warning strong {
font-weight: 700;
:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
.alert-warning
strong {
font-weight: var(--ac-weight-bold);
}
#delivery-info-alert {
margin-top: 1rem;
border-left: 4px solid #0dcaf0;
.eskaera-delivery-notice {
margin-top: var(--ac-space-md);
border-left: 4px solid var(--ac-color-info);
}
#delivery-info-alert .fa-truck {
margin-right: 0.5rem;
color: #0dcaf0;
.eskaera-delivery-notice .fa-truck {
margin-right: var(--ac-space-sm);
color: var(--ac-color-info);
}
/* Toast Notification Animation */
/* ---------- Toast (added to cart) ---------- */
.toast-notification {
position: fixed;
bottom: 30px;
right: 30px;
background-color: #28a745;
color: white;
padding: 1rem 1.5rem;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
inset: auto var(--ac-space-lg) var(--ac-space-lg) auto;
z-index: var(--ac-z-notification);
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 500;
z-index: 9999;
gap: var(--ac-space-xs);
max-width: 25rem;
padding: var(--ac-space-md) var(--ac-space-lg);
padding-bottom: calc(var(--ac-space-md) + env(safe-area-inset-bottom, 0px));
border-radius: var(--ac-radius-sm);
/* White on --ac-color-success is 3.13:1; the strong tone is 5.14:1. */
background-color: var(--ac-color-success-strong);
color: var(--ac-text-on-fill);
box-shadow: var(--ac-shadow-lg);
font-weight: var(--ac-weight-medium);
word-break: break-word;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.3s ease, transform 0.3s ease;
max-width: 400px;
word-break: break-word;
transition: opacity var(--ac-transition-normal), transform var(--ac-transition-normal);
}
.toast-notification.show {
@ -80,6 +71,41 @@
}
.toast-notification i {
font-size: 1.2rem;
min-width: 20px;
min-width: 1.25rem;
font-size: var(--ac-text-md);
}
/* ---------- Floating notification (_showNotification) ---------- */
/* Was a 200-character inline style written from JS on every call. */
.eskaera-notification {
position: fixed;
top: 5rem;
right: var(--ac-space-md);
z-index: var(--ac-z-notification);
min-width: 18.75rem;
max-width: 31.25rem;
padding: var(--ac-space-lg);
font-size: var(--ac-text-md);
box-shadow: var(--ac-shadow-lg);
transition: opacity var(--ac-transition-normal);
}
@media (max-width: 576px) {
.eskaera-notification {
left: var(--ac-space-md);
right: var(--ac-space-md);
min-width: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.toast-notification,
.eskaera-notification {
transition: none;
}
.toast-notification {
transform: none;
}
}

View file

@ -1,103 +1,102 @@
/* filepath: website_sale_aplicoop/static/src/css/components/buttons.css */
/**
* Button and action component styles
* Buttons
*
* Colours are set through Bootstrap 5.3's own --btn-* custom properties, which
* Odoo exposes unprefixed. That wins on the cascade without !important and
* keeps every state (hover, active, disabled) consistent, instead of
* overriding background-color and leaving the others behind.
*
* `.btn-add-to-cart` and `.btn-checkout` used to live here; no template or
* script emits either name the real ones are `.add-to-cart-btn` (see
* components/quantity-control.css) and `.checkout-btn-lg`.
*/
.btn-add-to-cart {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
:is(.btn-primary, .btn-success) {
font-weight: var(--ac-weight-semibold);
}
.btn-add-to-cart:focus {
outline: 3px solid var(--primary-color);
outline-offset: 2px;
}
/* ---------- Checkout actions ---------- */
.btn-add-to-cart:hover {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
}
.btn-checkout {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
border: none;
color: white;
font-weight: 600;
}
.btn-checkout:focus {
outline: 3px solid #667eea;
outline-offset: 2px;
}
.btn-checkout:hover {
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-dark) 100%);
color: white;
}
.btn-primary,
.btn-success {
font-weight: 600;
}
.btn-primary:disabled,
.btn-success:disabled {
opacity: 0.65;
}
/* Checkout action buttons */
.checkout-actions {
margin-top: 2rem;
margin-top: var(--ac-space-xl);
}
.checkout-actions .btn {
font-size: 1.1rem;
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
transition: all 0.3s ease;
--btn-border-radius: var(--ac-radius-md);
--btn-font-size: var(--ac-text-md);
--btn-font-weight: var(--ac-weight-semibold);
--btn-padding-y: var(--ac-space-xs);
--btn-padding-x: var(--ac-space-lg);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.checkout-actions .btn-success {
background-color: var(--success-color);
border-color: var(--success-color);
}
.checkout-actions .btn-success:hover {
background-color: #218838;
border-color: #218838;
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
transform: translateY(-2px);
}
.checkout-actions .btn-outline-secondary {
color: #ebeef0;
border-color: #cad2d8;
}
.checkout-actions .btn-outline-secondary:hover {
color: white;
background-color: #6c757d;
border-color: #6c757d;
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
letter-spacing: 0.05em;
transition: background-color var(--ac-transition-fast), border-color var(--ac-transition-fast),
box-shadow var(--ac-transition-fast), transform var(--ac-transition-fast);
}
.checkout-actions .btn i {
margin-right: 0.5rem;
margin-right: var(--ac-space-sm);
}
.checkout-actions .btn-success {
--btn-bg: var(--ac-color-success-strong);
--btn-border-color: var(--ac-color-success-strong);
--btn-color: var(--ac-text-on-fill);
--btn-hover-bg: var(--ac-color-success-deep);
--btn-hover-border-color: var(--ac-color-success-deep);
--btn-hover-color: var(--ac-text-on-fill);
}
/* "Back to cart". The previous #ebeef0 text sat at 1.17:1 on the white
checkout card the label was invisible until hover. */
.checkout-actions .btn-outline-secondary {
--btn-color: var(--ac-text-secondary);
--btn-border-color: var(--ac-border-strong);
--btn-hover-color: var(--ac-text-on-fill);
--btn-hover-bg: var(--ac-color-secondary);
--btn-hover-border-color: var(--ac-color-secondary);
}
/* ---------- Long-label buttons ---------- */
.save-order-btn,
.save-order-btn-styled,
.checkout-btn-lg {
padding: var(--ac-space-sm) var(--ac-space-md);
font-size: var(--ac-text-md);
white-space: nowrap;
font-size: 1.1rem;
padding: 0.6rem 1.2rem;
}
.save-icon-size {
font-size: 1.2rem;
/* ---------- States ---------- */
@media (hover: hover) and (pointer: fine) {
.checkout-actions .btn-success:hover,
.checkout-actions .btn-outline-secondary:hover {
transform: translateY(-2px);
box-shadow: var(--ac-shadow-md);
}
}
.checkout-actions .btn:focus-visible,
.save-order-btn:focus-visible,
.checkout-btn-lg:focus-visible {
outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color);
outline-offset: var(--ac-focus-ring-offset);
}
@media (pointer: coarse) {
.checkout-actions .btn,
.save-order-btn,
.checkout-btn-lg {
min-height: var(--ac-touch-target);
}
}
@media (prefers-reduced-motion: reduce) {
.checkout-actions .btn,
.checkout-actions .btn:hover {
transform: none;
transition: none;
}
}

View file

@ -1,236 +1,177 @@
/* filepath: website_sale_aplicoop/static/src/css/components/cart.css */
/**
* Shopping cart component styles
* Cart sidebar
*
* Markup (eskaera_shop):
* .card.sticky-top.cart-sticky-position
* .card-header #cart-title + .btn-group.cart-btn-group (4 icon buttons)
* .card-body.eskaera-cart-items[aria-live=polite]
* injected by website_sale.js: .list-group > .list-group-item, .cart-total
* .card-footer clear cart + checkout
*
* The .list-group-item rules are scoped to the container: unscoped, they hit
* every Bootstrap list group on the site, which is what forced three rounds of
* re-declaration in layout/responsive.css.
*/
.sticky-cart {
top: 20px;
.cart-sticky-position {
top: var(--ac-space-md);
}
.cart-header {
background-color: var(--primary-color);
color: white;
padding: 0.25rem;
border-radius: 0.25rem 0.25rem 0 0;
}
.cart-header h5 {
color: white;
margin: 0;
}
.cart-header .btn {
color: white;
}
.cart-items {
max-height: 400px;
/* Cap the scroll area against the viewport, not a fixed pixel height, so it
still fits when text is enlarged. */
.cart-items,
.eskaera-cart-items {
max-height: min(60vh, 25rem);
overflow-y: auto;
}
#cart-items-container {
padding: 0.2rem 0.4rem;
.eskaera-cart-items {
padding: var(--ac-space-2xs) var(--ac-space-xs);
font-size: var(--ac-text-base);
}
#cart-items-container .list-group {
.eskaera-cart-items .list-group {
margin-bottom: 0;
}
#cart-items-container p.text-muted {
margin: 0.4rem 0;
.eskaera-cart-items p.text-muted {
margin: var(--ac-space-2xs) 0;
}
.list-group-item {
padding: 0.4rem 0.6rem;
border-bottom: 1px solid #e0e0e0;
.cart-empty-message {
font-size: var(--ac-text-base);
}
.list-group-item.d-flex {
gap: 0.5rem;
/* ---------- Cart lines ---------- */
.eskaera-cart-items .list-group-item {
display: flex;
flex-direction: column;
gap: var(--ac-space-2xs);
padding: var(--ac-space-xs) var(--ac-space-sm);
border-bottom: 1px solid var(--ac-border);
font-size: var(--ac-text-base);
}
.list-group-item h6 {
margin: 0;
margin-bottom: 0.2rem;
font-weight: 600;
.eskaera-cart-items .list-group-item h6 {
margin: 0 0 var(--ac-space-3xs);
font-size: var(--ac-text-base);
font-weight: var(--ac-weight-semibold);
line-height: var(--ac-leading-snug);
overflow-wrap: break-word;
word-break: break-word;
font-size: 0.875rem;
line-height: 1.3;
}
.list-group-item small {
.eskaera-cart-items .list-group-item small {
display: block;
font-size: 0.75rem;
color: #6c757d;
font-size: var(--ac-text-xs);
color: var(--ac-text-muted);
}
.list-group-item .d-flex {
min-width: auto;
.eskaera-cart-items .list-group-item .d-flex {
justify-content: space-between;
align-items: center;
gap: 0.5rem;
gap: var(--ac-space-sm);
flex-wrap: wrap;
}
.list-group-item .remove-from-cart {
flex-shrink: 0;
min-width: 24px;
min-height: 24px;
/* Line subtotal. The previous #667eea was 3.42:1 at this size — under AA. */
.eskaera-cart-items .list-group-item strong {
min-width: 3.75rem;
font-size: var(--ac-text-base);
text-align: right;
white-space: nowrap;
color: var(--ac-color-primary-strong);
}
.eskaera-cart-items .list-group-item > div:first-child {
flex: 1 1 auto;
min-width: 0;
}
/* Colours come from Bootstrap's .btn-danger, already on the element. */
.remove-from-cart {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.15rem 0.25rem;
font-size: 0.7rem;
flex-shrink: 0;
/* 24px is the WCAG 2.2 AA floor (2.5.8); touch gets the full target below. */
min-width: 1.5rem;
min-height: 1.5rem;
padding: var(--ac-space-3xs) var(--ac-space-2xs);
font-size: var(--ac-text-2xs);
cursor: pointer;
background-color: #dc3545;
color: #ffffff;
border: 1px solid #dc3545;
border-radius: 3px;
transition: all 0.2s ease;
}
.list-group-item .remove-from-cart:hover {
background-color: #bb2d3b;
border-color: #bb2d3b;
transform: scale(1.05);
}
.list-group-item .remove-from-cart i {
font-size: 0.85rem;
color: #ffffff;
}
.list-group-item strong {
min-width: 60px;
text-align: right;
white-space: nowrap;
font-size: 0.875rem;
color: #667eea;
}
.list-group-item.d-flex > div:first-child {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
}
.list-group-item.d-flex > div:first-child h6 {
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
transition: background-color var(--ac-transition-fast), transform var(--ac-transition-fast);
}
.cart-total {
padding: 1rem;
background-color: #f8f9fa;
font-size: 1.5rem;
font-weight: 700;
border-top: 2px solid var(--primary-color);
padding: var(--ac-space-md);
border-top: 2px solid var(--ac-color-primary);
background-color: var(--ac-surface-sunken);
font-size: var(--ac-text-lg);
font-weight: var(--ac-weight-bold);
text-align: right;
color: var(--primary-color);
color: var(--ac-color-primary-strong);
}
.cart-item-remove {
cursor: pointer;
color: #dc3545;
font-size: 0.7rem;
}
/* ---------- Header actions ---------- */
.cart-item-remove:hover {
text-decoration: underline;
}
/* Cart header buttons styling */
#save-cart-btn,
#reload-cart-btn {
font-weight: 600;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-right: 0.25rem;
color: white;
}
#save-cart-btn {
background-color: #007bff;
border-color: #0056b3;
}
#save-cart-btn:hover {
background-color: #0056b3;
border-color: #004085;
box-shadow: 0 4px 8px rgba(0, 86, 179, 0.3);
}
#save-cart-btn:active {
transform: scale(0.95);
box-shadow: 0 2px 4px rgba(0, 86, 179, 0.3);
}
#reload-cart-btn {
background-color: #17a2b8;
border-color: #117a8b;
}
#reload-cart-btn:hover {
background-color: #117a8b;
border-color: #0c5460;
box-shadow: 0 4px 8px rgba(17, 122, 139, 0.3);
}
#reload-cart-btn:active {
transform: scale(0.95);
box-shadow: 0 2px 4px rgba(17, 122, 139, 0.3);
}
.card-header .btn-group {
flex-shrink: 0;
}
#cart-title {
font-size: 1.2rem;
}
.cart-btn-group-nowrap,
.card-header .btn-group,
.cart-btn-group {
flex-shrink: 0;
flex-wrap: nowrap;
}
.cart-header-btn {
padding: 0.25rem;
/* These four icon buttons were 0.5rem type with 0.1rem padding roughly an
18px target, under the 24px AA minimum, for the cart's primary actions. */
.cart-btn-compact {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2rem;
min-height: 2rem;
padding: var(--ac-space-2xs);
line-height: 1;
}
.cart-icon-size {
font-size: 0.9rem;
}
.cart-body-text {
font-size: 1.1rem;
}
.cart-body-lg {
font-size: 1.1rem;
}
.cart-title-lg {
font-size: 1.5rem;
font-size: var(--ac-text-base);
}
.cart-title-sm {
font-size: 1.1rem;
font-size: var(--ac-text-md);
}
.cart-btn-compact {
padding: 0.1rem;
min-width: auto;
font-size: 0.5rem;
line-height: 0.5;
height: auto;
/* ---------- States ---------- */
@media (hover: hover) and (pointer: fine) {
.remove-from-cart:hover {
transform: scale(1.05);
}
}
.cart-sticky-position {
top: 20px;
.remove-from-cart:focus-visible,
.cart-btn-compact:focus-visible {
outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color);
outline-offset: var(--ac-focus-ring-offset);
position: relative;
z-index: 1;
}
@media (pointer: coarse) {
.cart-btn-compact,
.remove-from-cart {
min-width: var(--ac-touch-target);
min-height: var(--ac-touch-target);
}
}
@media (prefers-reduced-motion: reduce) {
.remove-from-cart,
.remove-from-cart:hover {
transform: none;
transition: none;
}
}

View file

@ -1,99 +1,129 @@
/* filepath: website_sale_aplicoop/static/src/css/components/forms.css */
/**
* Form and input component styles
* Form controls
*
* Everything here is scoped to an Eskaera page root. The previous version
* styled bare `label`, `input[type="text"]`, `input[type="number"]` and
* `select`, which reaches every form in web.assets_frontend the website
* login, the contact form, any other module's frontend widgets not just this
* addon's pages.
*/
label {
font-weight: 600;
color: #2d3748;
:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) label {
font-weight: var(--ac-weight-semibold);
color: var(--ac-text-secondary);
}
.form-control,
.form-select {
border-color: #cbd5e0;
font-size: 1rem;
:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
:is(.form-control, .form-select, input[type="text"], input[type="number"], select) {
/* WCAG 1.4.11: an input's border is what identifies it, so ≥ 3:1. */
border: 1px solid var(--ac-border-strong);
font-size: var(--ac-text-base);
color: var(--ac-text-primary);
background-color: var(--ac-surface);
}
.form-control:focus,
.form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
/* One focus treatment for every control on these pages, keyboard only. */
:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
:is(.form-control, .form-select, .form-check-input, input, select, textarea):focus-visible {
outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color);
outline-offset: var(--ac-focus-ring-offset);
border-color: var(--ac-color-primary);
}
input[type="number"],
input[type="text"],
select {
background-color: #ffffff;
color: #212529;
border: 1px solid #ced4da;
/* Bootstrap draws its own ring through a box-shadow on :focus; keep it in
token colours so mouse focus still reads, without a second outline. */
:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
:is(.form-control, .form-select):focus {
border-color: var(--ac-color-primary);
box-shadow: 0 0 0 0.2rem rgb(0 123 255 / 20%);
}
input[type="number"]:focus,
input[type="text"]:focus,
select:focus {
outline: 2px solid #667eea;
outline-offset: 0;
border-color: var(--primary-color);
/* ---------- Search and category filter ---------- */
.eskaera-filters :is(.form-control, .form-select) {
height: auto;
padding: var(--ac-space-xs) var(--ac-space-sm);
font-size: var(--ac-text-base);
}
.form-check {
/* The clear-search button sits inside the input; give it a real target. */
.search-clear-btn {
position: absolute;
inset-block-start: 50%;
inset-inline-end: var(--ac-space-2xs);
transform: translateY(-50%);
display: none;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
border: none;
background: none;
font-size: var(--ac-text-lg);
line-height: 1;
color: var(--ac-text-secondary);
text-decoration: none;
}
.search-clear-btn.is-visible {
display: flex;
}
.search-input-wrapper {
position: relative;
}
.search-input-wrapper .form-control {
padding-inline-end: 2.5rem;
}
/* ---------- Home delivery checkbox ---------- */
.eskaera-delivery-check {
display: flex;
align-items: center;
padding: 1rem 1.25rem;
margin-left: 0;
gap: var(--ac-space-sm);
padding: var(--ac-space-md);
}
.form-check-input {
margin-right: 0.75rem;
.eskaera-delivery-check .form-check-input {
flex-shrink: 0;
}
/* Compact search and filter inputs */
#realtime-search-input,
#realtime-category-select {
font-size: 0.95rem;
padding: 0.375rem 0.75rem;
height: auto;
}
#realtimeSearch-filters .form-control,
#realtimeSearch-filters .form-select {
padding: 0.375rem 0.75rem;
font-size: 0.95rem;
}
.form-check-label {
margin: 0;
}
#home-delivery-checkbox {
width: 1.5rem;
height: 1.5rem;
margin: 0;
border: 2px solid var(--ac-color-primary);
border-radius: var(--ac-radius-sm);
cursor: pointer;
border: 2px solid #0d6efd;
border-radius: 0.25rem;
margin-right: 0.75rem;
}
#home-delivery-checkbox:checked {
background-color: #0d6efd;
border-color: #0d6efd;
.eskaera-delivery-check .form-check-input:checked {
border-color: var(--ac-color-primary);
background-color: var(--ac-color-primary);
}
#home-delivery-checkbox:focus {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.form-check-label[for="home-delivery-checkbox"] {
font-size: 1.1rem;
font-weight: 600;
color: #212529;
cursor: pointer;
.eskaera-delivery-check .form-check-label {
display: flex;
align-items: center;
margin: 0;
font-size: var(--ac-text-md);
font-weight: var(--ac-weight-semibold);
color: var(--ac-text-primary);
cursor: pointer;
}
.help-text-sm {
font-size: 0.85rem;
/* ---------- Touch ---------- */
@media (pointer: coarse) {
:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
:is(.form-control, .form-select) {
min-height: var(--ac-touch-target);
}
.search-clear-btn {
width: var(--ac-touch-target);
height: var(--ac-touch-target);
}
}

View file

@ -1,30 +1,43 @@
/* filepath: website_sale_aplicoop/static/src/css/components/order-card.css */
/**
* Order card (Eskaera) component styles
* Order card (Eskaera)
*
* Markup (eskaera_page):
* .eskaera-order-card-wrapper
* .eskaera-order-card[.eskaera-order-card--special]
* .card-body
* .order-badge-position (product count)
* a.eskaera-order-card-link .card-header-top (image, title, description)
* .card-meta-compact > .meta-grid (dates, badges)
* a.btn (Browse products)
*
* Note the link only wraps the header, so the whole card is NOT clickable
* the hover/focus treatment below is applied to the card while the pointer is
* anywhere over it, but `cursor: pointer` is left to the two real links.
*/
.eskaera-order-card-link {
text-decoration: none;
color: inherit;
display: block;
width: 100%;
color: inherit;
text-decoration: none;
}
.eskaera-order-card {
position: relative;
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
border: 1px solid rgba(90, 103, 216, 0.12);
border-radius: 0.75rem;
box-shadow: 0 6px 18px rgba(28, 37, 80, 0.06);
transition: transform 320ms cubic-bezier(0.2, 0.9, 0.2, 1), box-shadow 320ms, border-color 320ms,
background 320ms;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 290px;
/* min-height in rem, not px: at 200% zoom a px floor clips the content. */
min-height: 18rem;
height: 100%;
cursor: pointer;
border: 1px solid var(--ac-color-order-border);
border-radius: var(--ac-radius-lg);
background: var(--ac-surface-order);
box-shadow: var(--ac-shadow-md);
overflow: hidden;
transition: transform var(--ac-transition-normal), box-shadow var(--ac-transition-normal),
border-color var(--ac-transition-normal), background var(--ac-transition-normal);
animation: slideInUp 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@ -33,371 +46,281 @@
opacity: 0;
transform: translateY(30px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* Accent bar along the top edge. */
.eskaera-order-card::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
inset: 0 0 auto;
height: 6px;
background: linear-gradient(90deg, var(--primary-color), var(--primary-dark));
background: linear-gradient(90deg, var(--ac-color-order), var(--ac-color-order-strong));
}
.eskaera-order-card-link:hover .eskaera-order-card {
transform: translateY(-8px) scale(1.01);
box-shadow: 0 20px 50px rgba(90, 103, 216, 0.15), 0 0 30px rgba(90, 103, 216, 0.1);
border: 1px solid rgba(90, 103, 216, 0.25);
background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);
}
.eskaera-order-card-link:hover .eskaera-order-card::before {
animation: shimmer 0.6s ease-in-out;
}
@keyframes shimmer {
0% {
left: 0;
}
50% {
left: 100%;
}
100% {
left: 0;
}
}
.eskaera-order-card .card-body {
padding: 0.6rem 0.8rem 0 0.8rem;
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.eskaera-order-card .card-title {
font-size: 0.95rem;
font-weight: 700;
color: #1f2937;
margin: 0;
line-height: 1.15;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.order-desc-text {
font-size: 0.8rem;
color: #6b7280;
margin: 0;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.order-desc-sm {
font-size: 0.875rem;
line-height: 1.4;
}
.order-desc-md {
font-size: 0.95rem;
line-height: 1.5;
}
.eskaera-order-card .btn {
margin-top: auto;
margin-left: auto;
margin-right: auto;
margin-bottom: 0.6rem;
padding: 0.6rem 1.2rem;
font-weight: 600;
border-radius: 6px;
border: none !important;
font-size: 0.85rem;
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
background: linear-gradient(135deg, #5a67d8, #4c57bd) !important;
color: white !important;
box-shadow: 0 4px 12px rgba(90, 103, 216, 0.2) !important;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none !important;
position: relative;
overflow: hidden;
text-align: center;
width: auto;
}
.eskaera-order-card .btn::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.5s, height 0.5s;
}
.eskaera-order-card .btn:active::before {
width: 300px;
height: 300px;
}
.eskaera-order-card .btn:hover {
box-shadow: 0 8px 24px rgba(90, 103, 216, 0.4), inset 0 0 20px rgba(255, 255, 255, 0.2) !important;
transform: translateY(-3px) scale(1.03);
background: linear-gradient(135deg, #4c57bd, #3d4898) !important;
}
/* Center button within card body */
.eskaera-order-card .card-body > .btn {
margin-left: auto;
margin-right: auto;
}
/* Order card header spacing */
.order-card-header-spacing {
margin-top: 0.9rem;
}
/* Order thumbnail small */
.order-thumbnail-sm {
width: 90px;
height: 90px;
border-radius: 8px;
object-fit: cover;
flex-shrink: 0;
border: 2px solid rgba(90, 103, 216, 0.1);
}
/* Order thumbnail medium */
.order-thumbnail-md {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: 8px;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Order thumbnail checkout */
.order-thumbnail-checkout,
.checkout-thumbnail {
width: 90px;
height: 90px;
object-fit: cover;
border-radius: 6px;
flex-shrink: 0;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
/* ---------- Header ---------- */
.card-header-top {
display: flex;
gap: 0.6rem;
gap: var(--ac-space-sm);
align-items: flex-start;
margin-bottom: 0.3rem;
height: 100px;
overflow: hidden;
margin-bottom: var(--ac-space-2xs);
}
.card-header-top > div:last-child {
text-align: left;
flex: 1;
text-align: left;
}
.card-header-top .card-title {
margin: 0;
}
.card-badges .badge {
display: inline-flex;
align-items: center;
gap: 0.2rem;
.eskaera-order-card .card-body {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: var(--ac-space-2xs);
padding: var(--ac-space-sm) var(--ac-space-sm) 0;
}
.eskaera-order-card .card-title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
margin: 0;
overflow: hidden;
font-size: var(--ac-text-base);
font-weight: var(--ac-weight-bold);
line-height: var(--ac-leading-tight);
color: var(--ac-text-primary);
text-overflow: ellipsis;
}
.order-desc-text {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
margin: 0;
overflow: hidden;
font-size: var(--ac-text-xs);
line-height: var(--ac-leading-snug);
color: var(--ac-text-muted);
text-overflow: ellipsis;
}
/* ---------- Thumbnails ---------- */
.order-thumbnail-sm {
flex-shrink: 0;
width: 5.625rem;
height: 5.625rem;
border: 2px solid var(--ac-color-order-border);
border-radius: var(--ac-radius-md);
object-fit: cover;
}
.order-thumbnail-md {
flex-shrink: 0;
width: 7.5rem;
height: 7.5rem;
border-radius: var(--ac-radius-md);
box-shadow: var(--ac-shadow-sm);
object-fit: cover;
}
/* ---------- Product-count badge ---------- */
.order-badge-position {
position: absolute;
top: var(--ac-space-md);
right: var(--ac-space-md);
z-index: 1;
display: flex;
}
.order-badge-custom {
display: flex;
align-items: center;
gap: var(--ac-space-sm);
padding: var(--ac-space-sm) var(--ac-space-xs);
border-radius: var(--ac-radius-sm);
font-size: var(--ac-text-sm);
font-weight: var(--ac-weight-bold);
box-shadow: var(--ac-shadow-md);
}
/* ---------- Meta block ---------- */
.card-meta-compact {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
margin: 0.2rem auto 0 auto;
padding: 0.6rem 0.8rem;
border-top: 1px solid rgba(90, 103, 216, 0.08);
background: rgba(245, 247, 255, 0.4);
border-radius: 0 0 0.75rem 0.75rem;
width: 100%;
margin: var(--ac-space-2xs) auto 0;
padding: var(--ac-space-sm);
border-top: 1px solid var(--ac-color-order-border);
border-radius: 0 0 var(--ac-radius-lg) var(--ac-radius-lg);
background: var(--ac-surface-sunken);
}
.card-meta-compact .card-meta-list {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
/* Meta table styles for clean date display */
.meta-table {
width: auto;
border-collapse: collapse;
font-size: 0.8rem;
margin: 0 auto;
text-align: left;
min-height: 160px;
display: table;
}
.meta-table tbody {
display: table-row-group;
}
.meta-row {
display: table-row;
font-size: 0.8rem;
margin-bottom: 0;
}
.meta-label-cell {
display: table-cell;
padding: 0.25rem 0.6rem 0.25rem 0;
font-weight: 600;
color: #374151;
text-align: right;
vertical-align: top;
white-space: nowrap;
}
.meta-label-cell span {
display: inline-block;
}
.meta-value-cell {
display: table-cell;
padding: 0.25rem 0;
color: #6b7280;
text-align: left;
vertical-align: middle;
}
.meta-value-cell .badge {
display: inline-block;
vertical-align: middle;
font-size: 0.7rem;
padding: 0.2rem 0.5rem;
}
.meta-label {
font-weight: 600;
color: #374151;
min-width: 85px;
}
.meta-value {
color: #6b7280;
flex: 1;
}
.order-badge-position {
position: absolute;
top: 1.25rem;
right: 1.25rem;
z-index: 10;
display: flex;
}
.order-badge-custom {
font-weight: 700;
padding: 0.5rem 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
box-shadow: 0 4px 12px rgba(90, 103, 216, 0.3);
display: flex !important;
align-items: center;
gap: 0.5rem;
}
/* Meta grid - compact horizontal layout for order card metadata */
.meta-grid {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1rem;
justify-content: center;
gap: var(--ac-space-sm) var(--ac-space-md);
align-items: center;
justify-content: center;
}
.meta-grid .meta-item {
display: flex;
align-items: center;
gap: 0.35rem;
font-size: 0.8rem;
gap: var(--ac-space-2xs);
font-size: var(--ac-text-xs);
}
.meta-grid .meta-label {
font-weight: 600;
color: #374151;
min-width: auto;
font-weight: var(--ac-weight-semibold);
color: var(--ac-text-secondary);
}
.meta-grid .meta-value {
color: #6b7280;
color: var(--ac-text-muted);
}
.meta-grid .badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.meta-grid .badge i {
font-size: 0.7rem;
}
/* Special order card */
.eskaera-order-card--special {
background: linear-gradient(180deg, #fffdf0 0%, #fffbea 100%);
border: 1px solid rgba(245, 158, 11, 0.35);
box-shadow: 0 6px 18px rgba(245, 158, 11, 0.12);
}
.eskaera-order-card--special::before {
background: linear-gradient(90deg, #f59e0b, #d97706);
}
.eskaera-order-card-link:hover .eskaera-order-card--special {
box-shadow: 0 20px 50px rgba(245, 158, 11, 0.2), 0 0 30px rgba(245, 158, 11, 0.12);
border-color: rgba(245, 158, 11, 0.5);
background: linear-gradient(180deg, #fffdf0 0%, #fff8db 100%);
}
.special-order-badge {
background: linear-gradient(135deg, #f59e0b, #d97706) !important;
color: #fff !important;
font-size: 0.75rem;
padding: 0.25rem 0.6rem;
display: inline-flex;
align-items: center;
gap: 0.3rem;
border-radius: 4px;
font-weight: 600;
box-shadow: 0 2px 6px rgba(245, 158, 11, 0.3);
gap: var(--ac-space-2xs);
padding: var(--ac-space-2xs) var(--ac-space-xs);
font-size: var(--ac-text-2xs);
}
.meta-item--full {
width: 100%;
justify-content: center;
}
/* Home delivery: its own row, badge then date, centred as a pair so it stays
centred whether or not the date is there. */
.meta-grid .meta-item--delivery {
width: 100%;
justify-content: center;
gap: var(--ac-space-sm);
}
/* ---------- Call to action ---------- */
/* Bootstrap 5.3 drives .btn from unprefixed custom properties, so the card's
own colours are set by redefining those instead of piling up !important. */
.eskaera-order-card .btn {
--btn-color: var(--ac-text-on-fill);
--btn-bg: var(--ac-color-order);
--btn-border-color: transparent;
--btn-hover-color: var(--ac-text-on-fill);
--btn-hover-bg: var(--ac-color-order-strong);
--btn-hover-border-color: transparent;
--btn-active-color: var(--ac-text-on-fill);
--btn-active-bg: var(--ac-color-order-deep);
--btn-active-border-color: transparent;
--btn-border-radius: var(--ac-radius-sm);
--btn-font-size: var(--ac-text-sm);
--btn-font-weight: var(--ac-weight-semibold);
--btn-padding-y: var(--ac-space-sm);
--btn-padding-x: var(--ac-space-md);
display: inline-flex;
align-items: center;
gap: var(--ac-space-sm);
margin: auto auto var(--ac-space-sm);
box-shadow: var(--ac-shadow-sm);
text-align: center;
text-decoration: none;
transition: background-color var(--ac-transition-fast), box-shadow var(--ac-transition-fast),
transform var(--ac-transition-fast);
}
/* ---------- Special orders ---------- */
.eskaera-order-card--special {
border-color: var(--ac-color-special-border);
background: var(--ac-surface-special);
}
.eskaera-order-card--special::before {
background: linear-gradient(90deg, var(--ac-color-special), var(--ac-color-special-strong));
}
.special-order-badge {
/* Amber needs dark text — white on it is 2.15:1. */
--badge-color: var(--ac-text-on-special);
--badge-font-size: var(--ac-text-2xs);
--badge-border-radius: var(--ac-radius-sm);
--badge-padding-x: var(--ac-space-xs);
--badge-padding-y: var(--ac-space-2xs);
display: inline-flex;
align-items: center;
gap: var(--ac-space-2xs);
background: linear-gradient(135deg, var(--ac-color-special), var(--ac-color-special-strong));
font-weight: var(--ac-weight-semibold);
box-shadow: var(--ac-shadow-sm);
}
/* ---------- States ---------- */
@media (hover: hover) and (pointer: fine) {
.eskaera-order-card:hover {
transform: translateY(-8px);
border-color: var(--ac-color-order-border-hover);
background: var(--ac-surface-order-hover);
box-shadow: var(--ac-shadow-lg);
}
.eskaera-order-card--special:hover {
border-color: var(--ac-color-special-border-hover);
background: var(--ac-surface-special-hover);
}
.eskaera-order-card .btn:hover {
transform: translateY(-2px);
box-shadow: var(--ac-shadow-md);
}
}
/* Keyboard users get the same emphasis as a hover, plus a ring on the card
that holds the focused link. */
.eskaera-order-card:focus-within {
border-color: var(--ac-color-order-border-hover);
background: var(--ac-surface-order-hover);
box-shadow: var(--ac-shadow-lg);
}
.eskaera-order-card-link:focus-visible,
.eskaera-order-card .btn:focus-visible {
outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color);
outline-offset: var(--ac-focus-ring-offset);
border-radius: var(--ac-radius-sm);
}
/* The entry animation is decorative; motion sensitivity outranks it, and the
hover lift has to stop moving too. */
@media (prefers-reduced-motion: reduce) {
.eskaera-order-card {
animation: none;
transition: none;
}
.eskaera-order-card:hover,
.eskaera-order-card .btn,
.eskaera-order-card .btn:hover {
transform: none;
transition: none;
}
}

View file

@ -1,238 +1,204 @@
/* filepath: website_sale_aplicoop/static/src/css/components/product-card.css */
/**
* Product card component styles
* Product card
*
* Markup (eskaera_shop_products):
* .product-card-wrapper.product-card
* .card
* img.product-img-cover | .product-img-placeholder
* .card-body (title, tags, supplier, origin, 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.
*/
.product-card {
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.3s, transform 0.2s;
display: flex;
flex-direction: column;
padding: 0.5rem;
height: 100%;
padding: var(--ac-space-2xs);
border: 1px solid var(--ac-border-subtle);
border-radius: var(--ac-radius-lg);
background-color: var(--ac-surface);
box-shadow: var(--ac-shadow-sm);
overflow: hidden;
outline: none;
transition: box-shadow var(--ac-transition-normal), transform var(--ac-transition-normal);
}
.product-card:hover,
.product-card:focus-within {
transform: translateY(-4px) scale(1.01);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.13);
outline: 2px solid var(--primary-color, #007bff);
outline-offset: 2px;
/* Bootstrap 5.3 drives .card through unprefixed custom properties, so the inner
card can be flattened without !important and without touching the markup. */
.product-card > .card {
--card-bg: transparent;
--card-border-width: 0;
--card-border-radius: 0;
display: flex;
flex-direction: column;
height: 100%;
}
.product-card .product-image {
height: 120px;
width: 100%;
object-fit: cover;
border-radius: 8px 8px 0 0;
background: #f3f3f3;
display: block;
}
/* ---------- Media ---------- */
.product-img-cover {
height: 120px;
width: 100%;
object-fit: cover;
border-radius: 8px 8px 0 0;
box-shadow: 0 2px 8px rgba(40, 39, 39, 0.09);
background: #f3f3f3;
.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);
border-radius: var(--ac-radius-md) var(--ac-radius-md) 0 0;
object-fit: cover;
background-color: var(--ac-surface-muted);
}
.product-img-placeholder {
display: flex;
align-items: center;
justify-content: center;
color: var(--ac-text-muted);
}
/* ---------- Body ---------- */
.product-card .card-body {
display: flex;
flex-direction: column;
height: 100%;
flex-grow: 1;
padding: 0.4rem 0.55rem 0.5rem;
position: relative;
background: linear-gradient(135deg, rgba(0, 123, 255, 0.07) 0%, rgba(0, 123, 255, 0.04) 100%);
transition: background 0.3s;
}
.product-card:hover .card-body,
.product-card:focus-within .card-body {
background: linear-gradient(
135deg,
rgba(108, 117, 125, 0.13) 0%,
rgba(108, 117, 125, 0.09) 100%
);
/* Sized to its own content, not stretched: the add-to-cart form below is
what aligns across the row (see quantity-control.css), so the text/price
block stays compact instead of ballooning to match a taller sibling. */
gap: var(--ac-space-3xs);
padding: var(--ac-space-2xs) var(--ac-space-xs) var(--ac-space-2xs);
background: linear-gradient(135deg, var(--ac-surface-tint) 0%, transparent 100%);
transition: background var(--ac-transition-normal);
}
.product-card .card-title {
flex-grow: 1;
margin: 0 0 0.1rem 0;
min-height: auto;
display: block;
word-wrap: break-word;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
/* Reserve both lines so single-line titles don't stagger the grid. */
min-height: calc(2em * var(--ac-leading-tight));
margin: 0;
overflow: hidden;
overflow-wrap: break-word;
font-size: 1rem !important;
line-height: 1.15;
text-align: center;
font-weight: 600;
color: #1a202c;
font-size: var(--ac-text-base);
font-weight: var(--ac-weight-semibold);
line-height: var(--ac-leading-tight);
letter-spacing: 0.01em;
text-align: center;
color: var(--ac-text-primary);
}
.product-card .card-text {
margin-bottom: 0.08rem;
margin-bottom: 0;
font-size: var(--ac-text-sm);
text-align: center;
font-size: 0.95rem;
}
.product-card .card-text strong {
display: block;
margin-bottom: 0.1rem;
font-size: 1.15rem;
color: #3b82f6;
}
.product-card .product-supplier {
text-align: center;
color: #4a5568;
font-weight: 400;
margin-bottom: 0.08rem;
font-size: 0.85rem !important;
}
/* ---------- Tags ---------- */
.product-tags {
text-align: center;
display: flex;
flex-wrap: wrap;
gap: 0.12rem;
gap: var(--ac-space-3xs);
justify-content: center;
font-weight: 400;
font-size: 1rem !important;
margin: 0 0 0.08rem 0;
margin: 0;
padding: 0;
}
/* The per-tag colour is a record value, so it arrives as the --ac-tag-color
custom property set inline on the element (see eskaera_shop_products). */
.badge-km {
background-color: var(--primary-color, #007bff) !important;
color: #fff !important;
font-weight: 600 !important;
padding: 0.18rem 0.32rem !important;
font-size: 0.68rem !important;
border-radius: 0.22rem;
display: inline-block;
border: 1px solid #007bff;
--badge-padding-x: var(--ac-space-2xs);
--badge-padding-y: var(--ac-space-3xs);
--badge-font-size: var(--ac-text-2xs);
--badge-font-weight: var(--ac-weight-semibold);
--badge-color: var(--ac-text-on-fill);
--badge-border-radius: var(--ac-radius-sm);
border: 1px solid var(--ac-tag-color, var(--ac-color-primary-strong));
background-color: var(--ac-tag-color, var(--ac-color-primary-strong));
white-space: nowrap;
margin-right: 0.08rem;
margin-bottom: 0.08rem;
}
.card-body p.card-text {
/* ---------- Supplier & origin ---------- */
.product-card .product-supplier,
.product-card .product-origin {
margin: 0;
font-size: var(--ac-text-xs);
font-weight: var(--ac-weight-normal);
text-align: center;
margin-bottom: 0.35rem;
min-height: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--primary-color, #007bff);
color: #fff;
border-radius: 0.18rem;
font-size: 0.98rem;
color: var(--ac-text-secondary);
}
.card-body p.card-text strong {
display: inline;
font-size: 1.18rem !important;
color: var(--primary-color, #007bff);
margin-bottom: 0;
.product-card .product-origin .fa {
margin-right: var(--ac-space-3xs);
}
/* ---------- Price ---------- */
.product-card .product-price-display {
display: flex;
align-items: baseline;
justify-content: center;
flex-wrap: wrap;
gap: var(--ac-space-2xs);
margin: 0;
}
.product-card .product-price-main {
font-size: var(--ac-text-lg);
font-weight: var(--ac-weight-bold);
line-height: var(--ac-leading-tight);
white-space: nowrap;
color: var(--ac-color-primary-strong);
}
.product-img-fixed {
object-fit: cover;
height: 120px;
width: 100%;
border-radius: 8px 8px 0 0;
background: #f3f3f3;
display: block;
.product-card .product-unit-price {
margin: 0;
font-size: var(--ac-text-xs);
text-align: center;
color: var(--ac-text-secondary);
}
.product-img-placeholder {
height: 120px;
width: 100%;
border-radius: 8px 8px 0 0;
background: #f3f3f3;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: #ccc;
/* ---------- States ---------- */
/* Hover effects only where a pointer can actually hover on touch they stick
after a tap and read as a stuck selection. */
@media (hover: hover) and (pointer: fine) {
.product-card:hover {
transform: translateY(-4px);
box-shadow: var(--ac-shadow-lg);
}
/* Responsive: tablet (≤768px) — compresión moderada */
@media (max-width: 768px) {
.product-card .product-image,
.product-img-cover,
.product-img-fixed,
.product-img-placeholder {
height: 90px;
.product-card:hover .card-body {
background: linear-gradient(135deg, var(--ac-surface-tint-hover) 0%, transparent 100%);
}
}
/* Keyboard focus only: a ring, and no transform moving the card under the
caret while tabbing is disorienting. */
.product-card:focus-visible,
.product-card:focus-within {
outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color);
outline-offset: var(--ac-focus-ring-offset);
}
.product-card:focus-within .card-body {
background: linear-gradient(135deg, var(--ac-surface-tint-hover) 0%, transparent 100%);
}
@media (prefers-reduced-motion: reduce) {
.product-card,
.product-card .card-body {
padding: 0.35rem 0.45rem 0.4rem;
}
.card-body p.card-text {
margin-bottom: 0.25rem;
min-height: 1.4rem;
}
transition: none;
}
/* Responsive: móvil (≤480px) — compresión máxima */
@media (max-width: 480px) {
.product-card {
padding: 0.15rem;
border-radius: 6px;
}
.product-card .product-image,
.product-img-cover,
.product-img-fixed,
.product-img-placeholder {
height: 60px;
border-radius: 5px 5px 0 0;
}
.product-card .card-body {
padding: 0.25rem 0.3rem 0.3rem;
}
.product-card .card-title {
font-size: 0.82rem !important;
line-height: 1.15;
margin-bottom: 0.05rem;
}
.product-card .card-text {
font-size: 0.8rem;
}
.badge-km {
font-size: 0.55rem !important;
padding: 0.1rem 0.18rem !important;
}
.product-tags {
font-size: 0.82rem !important;
gap: 0.08rem;
margin-bottom: 0.05rem;
}
.card-body p.card-text {
min-height: 1rem;
font-size: 0.85rem;
margin-bottom: 0.2rem;
}
.product-card .product-supplier {
font-size: 0.72rem !important;
margin-bottom: 0.05rem;
.product-card:hover {
transform: none;
}
}
/* Accesibilidad: focus visible */
.product-card:focus-visible {
outline: 2.5px solid var(--primary-color, #007bff);
outline-offset: 2px;
}

View file

@ -1,405 +1,220 @@
/* filepath: website_sale_aplicoop/static/src/css/components/quantity-control.css */
/**
* Quantity control (+ - input) component styles
* Quantity control stepper ( input +) plus the add-to-cart button.
*
* Markup (eskaera_shop_products):
* form.add-to-cart-form
* .qty-control
* label.visually-hidden
* button.qty-decrease
* input.product-qty
* button.qty-increase
* button.add-to-cart-btn
*
* Sizing is fluid (--ac-control-size), which replaces the six per-breakpoint
* bands this component used to carry. The only query left is a pointer query:
* what actually changes between a phone and a desktop is the input device, not
* the viewport width.
*/
/* Formulario agregar al carrito */
/* margin-top: auto pins this to the bottom of .product-card > .card (which is
stretched to the grid row's tallest sibling) so every card's button ends up
at the same height, without ballooning the text/price block above it see
.product-card .card-body in product-card.css. */
.add-to-cart-form {
width: 100%;
gap: 0;
padding: 0.5rem;
margin-top: 0.25rem;
background-color: #f8f9fa;
border-radius: 4px;
display: flex;
border: 1px solid #e9ecef;
align-content: center;
justify-content: center;
width: 100%;
margin-top: auto;
padding: var(--ac-space-3xs);
border: 1px solid var(--ac-border);
border-radius: var(--ac-radius-sm);
background-color: var(--ac-surface-sunken);
}
.add-to-cart-form .input-group {
/* A grid, not a wrapping flex row: four 44px controls never fit across a card
that is ~150px wide, so the row used to wrap and drop "+" onto its own line
next to the cart button. Two deliberate rows instead the stepper across the
top, the primary action full width underneath, where it is easier to hit. */
.qty-control {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
width: 100%;
gap: 0;
padding: 0;
}
/* ---------- Stepper ---------- */
.qty-control .qty-decrease,
.qty-control .qty-increase {
display: flex;
align-items: center;
justify-content: center;
height: 36px;
flex-shrink: 0;
width: var(--ac-control-size);
height: var(--ac-control-size);
padding: 0;
/* WCAG 1.4.11: the boundary is what identifies the control, so it needs
3:1 against the surface behind it. */
border: var(--ac-control-border-width) solid var(--ac-border-strong);
font-size: var(--ac-text-sm);
color: var(--ac-text-secondary);
background-color: var(--ac-surface);
cursor: pointer;
transition: color var(--ac-transition-fast), border-color var(--ac-transition-fast),
background-color var(--ac-transition-fast);
}
.qty-control .qty-decrease {
border-start-start-radius: var(--ac-radius-sm);
border-end-start-radius: var(--ac-radius-sm);
border-start-end-radius: 0;
border-end-end-radius: 0;
}
.qty-control .qty-increase {
border-start-end-radius: var(--ac-radius-sm);
border-end-end-radius: var(--ac-radius-sm);
border-start-start-radius: 0;
border-end-start-radius: 0;
}
.add-to-cart-form .product-qty {
flex: 1 1 auto;
text-align: center;
font-weight: 700;
font-size: 1rem;
padding: 0.3rem;
min-width: 32px;
max-width: 70px;
border: 2px solid #dee2e6;
/* Fills the middle grid column; min-width 0 lets it shrink on a narrow card
instead of pushing the "+" button out of the row. */
width: 100%;
min-width: 0;
height: var(--ac-control-size);
padding: 0 var(--ac-space-2xs);
/* Collapse the shared edge with the two stepper buttons. */
border: var(--ac-control-border-width) solid var(--ac-border-strong);
border-inline-width: 0;
border-radius: 0;
background-color: #ffffff;
transition: all 0.2s ease;
font-size: var(--ac-text-base);
font-weight: var(--ac-weight-bold);
text-align: center;
color: var(--ac-text-primary);
background-color: var(--ac-surface);
appearance: textfield;
-moz-appearance: textfield;
height: 36px;
line-height: 30px;
transition: border-color var(--ac-transition-fast), box-shadow var(--ac-transition-fast);
}
.add-to-cart-form .product-qty::-webkit-outer-spin-button,
.add-to-cart-form .product-qty::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
-webkit-appearance: none;
}
.add-to-cart-form .product-qty:focus {
outline: none;
border-color: var(--primary-color, #007bff);
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
background-color: #ffffff;
}
/* ---------- Add to cart ---------- */
/* Contenedor de control de cantidad */
.qty-control {
.add-to-cart-form .add-to-cart-btn {
/* Full width on its own row, under the stepper. */
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: center;
gap: 0;
padding: 0;
width: 100%;
height: 44px;
}
/* Botones de cantidad + y - */
.qty-control .qty-decrease,
.qty-control .qty-increase {
min-width: 36px;
width: 36px;
height: 36px;
height: var(--ac-control-size);
margin-top: var(--ac-space-3xs);
padding: 0;
gap: 0;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #dee2e6;
background-color: #ffffff;
color: #495057;
transition: all 0.2s ease;
border: none;
border-radius: var(--ac-radius-sm);
font-size: var(--ac-text-sm);
color: var(--ac-text-on-fill);
background-color: var(--ac-color-accent);
cursor: pointer;
flex-shrink: 0;
transition: background-color var(--ac-transition-fast), transform var(--ac-transition-fast);
}
.add-to-cart-form .add-to-cart-btn .fa {
font-size: var(--ac-text-md);
}
/* Out of stock stays focusable and clickable on purpose: the handler answers
with an explanation instead of the control silently doing nothing. It is
marked aria-disabled in the template, so the styling has to read as
unavailable without relying on colour alone (the icon changes to fa-ban). */
.add-to-cart-form .add-to-cart-btn[data-out-of-stock="true"] {
color: var(--ac-text-secondary);
background-color: var(--ac-surface-muted);
cursor: not-allowed;
}
/* ---------- States ---------- */
@media (hover: hover) and (pointer: fine) {
.qty-control .qty-decrease:hover,
.qty-control .qty-increase:hover {
border-color: var(--primary-color, #007bff);
color: var(--primary-color, #007bff);
background-color: #f8f9fa;
border-color: var(--ac-color-primary);
color: var(--ac-color-primary-strong);
background-color: var(--ac-surface-sunken);
}
.add-to-cart-form .add-to-cart-btn:hover {
background-color: var(--ac-color-accent-strong);
transform: scale(1.05);
}
.add-to-cart-form .add-to-cart-btn[data-out-of-stock="true"]:hover {
background-color: var(--ac-surface-muted);
transform: none;
}
}
.qty-control .qty-decrease:active,
.qty-control .qty-increase:active {
background-color: #e9ecef;
}
.add-to-cart-form .add-to-cart-btn {
white-space: nowrap;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
margin: 0;
font-size: 0.8rem;
height: 36px;
width: 36px;
min-width: 36px;
max-width: 36px;
min-height: 36px;
max-height: 36px;
flex-shrink: 0;
border: none;
background-color: #7c3aed !important;
color: #ffffff !important;
cursor: pointer;
transition: all 0.2s ease;
}
.add-to-cart-form .add-to-cart-btn:hover {
background-color: #6d28d9 !important;
transform: scale(1.05);
background-color: var(--ac-surface-sunken);
}
.add-to-cart-form .add-to-cart-btn:active {
background-color: #5b21b6 !important;
background-color: var(--ac-color-accent-strong);
transform: scale(0.98);
}
.add-to-cart-form .add-to-cart-btn i {
font-size: 1rem;
.qty-control .qty-decrease:focus-visible,
.qty-control .qty-increase:focus-visible,
.add-to-cart-form .add-to-cart-btn:focus-visible,
.add-to-cart-form .product-qty:focus-visible {
outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color);
outline-offset: var(--ac-focus-ring-offset);
/* Raise the focused control so the ring is not clipped by its neighbours. */
position: relative;
z-index: 1;
}
/* Pantallas muy grandes: 1600px+ (6 columnas) */
@media (min-width: 1600px) {
.add-to-cart-form {
padding: 0.35rem;
gap: 0.08rem;
.add-to-cart-form .product-qty:focus-visible {
border-color: var(--ac-color-primary);
}
.add-to-cart-form .input-group {
height: 32px;
gap: 0.06rem;
}
.add-to-cart-form .product-qty {
font-size: 1rem;
min-width: 28px;
max-width: 60px;
height: 32px;
padding: 0.2rem;
}
/* ---------- Input device ---------- */
/* Touch: grow every target to 44px. WCAG 2.2 AA (2.5.8) only asks for 24px,
but 44px is the AAA figure (2.5.5) and what a thumb actually needs. */
@media (pointer: coarse) {
.qty-control .qty-decrease,
.qty-control .qty-increase {
width: 32px;
height: 32px;
font-size: 0.9rem;
width: var(--ac-touch-target);
height: var(--ac-touch-target);
}
.add-to-cart-form .product-qty,
.add-to-cart-form .add-to-cart-btn {
height: 32px;
width: 32px;
min-width: 32px;
max-width: 32px;
font-size: 0.75rem;
height: var(--ac-touch-target);
}
}
/* Pantallas grandes: 1400-1599px (5 columnas) */
@media (max-width: 1599px) and (min-width: 1400px) {
.add-to-cart-form {
padding: 0.36rem;
gap: 0.07rem;
}
.add-to-cart-form .input-group {
height: 32px;
gap: 0.05rem;
}
.add-to-cart-form .product-qty {
font-size: 1rem;
min-width: 28px;
max-width: 62px;
height: 32px;
padding: 0.2rem;
}
@media (prefers-reduced-motion: reduce) {
.qty-control .qty-decrease,
.qty-control .qty-increase {
width: 32px;
height: 32px;
font-size: 0.9rem;
}
.qty-control .qty-increase,
.add-to-cart-form .product-qty,
.add-to-cart-form .add-to-cart-btn {
height: 32px;
width: 32px;
min-width: 32px;
font-size: 0.75rem;
}
transition: none;
}
/* Pantallas medianas: 1200-1399px (4 columnas) */
@media (max-width: 1399px) and (min-width: 1200px) {
.add-to-cart-form {
padding: 0.34rem;
gap: 0.06rem;
}
.add-to-cart-form .input-group {
height: 32px;
gap: 0.05rem;
}
.add-to-cart-form .product-qty {
font-size: 0.95rem;
min-width: 28px;
max-width: 60px;
height: 32px;
padding: 0.2rem;
}
.qty-control .qty-decrease,
.qty-control .qty-increase {
width: 32px;
height: 32px;
font-size: 0.88rem;
}
.add-to-cart-form .add-to-cart-btn {
height: 32px;
width: 32px;
min-width: 32px;
font-size: 0.74rem;
}
}
/* Pantallas tablet grandes: 992-1199px (3 columnas) */
@media (max-width: 1199px) and (min-width: 992px) {
.add-to-cart-form {
padding: 0.32rem;
gap: 0.04rem;
}
.add-to-cart-form .input-group {
height: 32px;
gap: 0.04rem;
}
.add-to-cart-form .product-qty {
font-size: 0.9rem;
min-width: 28px;
max-width: 58px;
height: 32px;
padding: 0.2rem;
}
.qty-control .qty-decrease,
.qty-control .qty-increase {
width: 32px;
height: 32px;
font-size: 0.85rem;
border-width: 1px;
}
.add-to-cart-form .add-to-cart-btn {
height: 32px;
width: 32px;
min-width: 32px;
font-size: 0.72rem;
}
}
/* Pantallas tablet: 768-991px (2-3 columnas) */
@media (max-width: 991px) and (min-width: 768px) {
.add-to-cart-form {
padding: 0.42rem;
gap: 0.03rem;
}
.add-to-cart-form .input-group {
height: 36px;
gap: 0.04rem;
}
.add-to-cart-form .product-qty {
font-size: 1rem;
min-width: 32px;
max-width: 68px;
height: 36px;
padding: 0.3rem;
}
.qty-control .qty-decrease,
.qty-control .qty-increase {
width: 36px;
height: 36px;
font-size: 0.95rem;
border-width: 1px;
}
.add-to-cart-form .add-to-cart-btn {
height: 36px;
width: 36px;
min-width: 36px;
font-size: 0.82rem;
}
}
/* Móvil grande: 576-767px (2 columnas) */
@media (max-width: 767px) and (min-width: 576px) {
.add-to-cart-form {
padding: 0.35rem;
gap: 0.1rem;
}
.add-to-cart-form .input-group {
height: 34px;
gap: 0.08rem;
}
.add-to-cart-form .product-qty {
font-size: 0.9rem;
min-width: 30px;
max-width: 64px;
height: 34px;
padding: 0.25rem;
}
.qty-control .qty-decrease,
.qty-control .qty-increase {
width: 34px;
height: 34px;
font-size: 0.9rem;
border-width: 1px;
}
.add-to-cart-form .add-to-cart-btn {
height: 34px;
width: 34px;
min-width: 34px;
font-size: 0.8rem;
}
}
/* Móvil pequeño: < 576px (1 columna) */
@media (max-width: 575px) {
.add-to-cart-form {
padding: 0.3rem;
gap: 0.08rem;
flex-wrap: wrap;
width: 100%;
}
.add-to-cart-form .input-group {
height: 32px;
gap: 0.06rem;
flex: 1 1 100%;
}
.add-to-cart-form .product-qty {
font-size: 0.8rem;
min-width: 28px;
max-width: 60px;
height: 32px;
padding: 0.2rem;
line-height: 28px;
}
.qty-control .qty-decrease,
.qty-control .qty-increase {
width: 32px;
height: 32px;
font-size: 0.8rem;
border-width: 1px;
min-width: 32px;
}
.qty-control {
width: auto;
gap: 0;
}
.add-to-cart-form .add-to-cart-btn {
height: 32px;
width: 32px;
min-width: 32px;
max-width: 32px;
font-size: 0.75rem;
flex: 0 0 auto;
}
.add-to-cart-form .add-to-cart-btn i {
font-size: 0.8rem;
.add-to-cart-form .add-to-cart-btn:hover,
.add-to-cart-form .add-to-cart-btn:active {
transform: none;
}
}

View file

@ -4,70 +4,103 @@
*/
/**
* Tag Filter Badges Component
* Tag filter badges
*
* Styles for interactive tag filter badges in the product search/filter bar.
* Badges toggle between secondary (unselected) and primary (selected) states.
* Toggle buttons above the product grid. Three states:
* (default) nothing selected each badge wears its own tag colour
* .is-selected this tag is filtering
* .is-dimmed another tag is filtering, this one is not
*
* realtime_search.js used to paint all three by writing nine
* style.setProperty(..., "important") calls per click. It now only toggles
* these classes and aria-pressed; the appearance lives here.
*
* The per-tag colour arrives as --ac-tag-color, set inline from the record.
*/
/* Container for all tag filter badges */
.tag-filter-badges {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 0;
justify-content: center;
gap: var(--ac-space-sm);
padding: var(--ac-space-xs) 0;
}
/* Individual tag filter badge button */
.tag-filter-badge {
cursor: pointer;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
display: inline-flex;
align-items: center;
/* 24px is the AA floor (WCAG 2.2 §2.5.8); coarse pointers get 44 below. */
min-height: 1.5rem;
padding: var(--ac-space-sm) var(--ac-space-xs);
border: 1px solid var(--ac-tag-color, var(--ac-color-secondary));
border-radius: var(--ac-radius-sm);
background-color: var(--ac-tag-color, var(--ac-color-secondary));
font-size: var(--ac-text-sm);
font-weight: var(--ac-weight-medium);
color: var(--ac-text-on-fill);
white-space: nowrap;
transition: all 0.2s ease-in-out;
cursor: pointer;
user-select: none;
display: inline-block;
border: 1px solid;
color: #ffffff !important;
transition: background-color var(--ac-transition-fast), border-color var(--ac-transition-fast),
box-shadow var(--ac-transition-fast), transform var(--ac-transition-fast);
}
/* Tags sin color definido en Odoo: usar color secundario del tema */
.tag-filter-badge.tag-use-theme-color {
background-color: var(--bs-secondary, #6c757d);
border-color: var(--bs-secondary, #6c757d);
}
/* Product card tags (badge-km) sin color definido: usar color del tema */
/* Tags with no colour of their own fall back to the theme's secondary. The old
var(--bs-secondary) never resolved: Odoo emits Bootstrap's custom properties
unprefixed, so that lookup always landed on its hardcoded fallback. */
.tag-filter-badge.tag-use-theme-color,
.badge-km.tag-use-theme-color {
background-color: var(--bs-secondary, #6c757d);
border-color: var(--bs-secondary, #6c757d);
color: #ffffff !important;
border-color: var(--ac-color-secondary);
background-color: var(--ac-color-secondary);
color: var(--ac-text-on-fill);
}
/* Active filter: the strong primary, which also carries white text at 7.04:1. */
.tag-filter-badge.is-selected {
border-color: var(--ac-color-primary-strong);
background-color: var(--ac-color-primary-strong);
box-shadow: var(--ac-shadow-sm);
}
/* Not part of the current filter while others are. */
.tag-filter-badge.is-dimmed {
border-color: var(--ac-color-secondary);
background-color: var(--ac-color-secondary);
}
.tag-filter-badge .tag-count {
margin-left: var(--ac-space-2xs);
font-weight: var(--ac-weight-semibold);
font-variant-numeric: tabular-nums;
}
/* ---------- States ---------- */
@media (hover: hover) and (pointer: fine) {
.tag-filter-badge:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
box-shadow: var(--ac-shadow-md);
filter: brightness(0.9);
}
/* Counter text inside badge */
.tag-filter-badge .tag-count {
font-weight: 600;
margin-left: 0.25rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.tag-filter-badges {
gap: 0.375rem;
.tag-filter-badge:focus-visible {
outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color);
outline-offset: var(--ac-focus-ring-offset);
}
@media (pointer: coarse) {
.tag-filter-badge {
padding: 0.375rem 0.625rem;
font-size: 0.8125rem;
min-height: var(--ac-touch-target);
padding-inline: var(--ac-space-md);
}
}
@media (prefers-reduced-motion: reduce) {
.tag-filter-badge,
.tag-filter-badge:hover {
transform: none;
transition: none;
}
}

View file

@ -7,83 +7,142 @@
/* Unified header for both shop and checkout pages */
.eskaera-order-header,
.checkout-header {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: clamp(var(--ac-space-md), 2vw, var(--ac-space-xl));
border: 1px solid var(--ac-border-subtle);
border-radius: var(--ac-radius-md);
background: var(--ac-surface);
box-shadow: var(--ac-shadow-sm);
}
.eskaera-order-header h1,
.checkout-header h1 {
color: #2d3748;
font-weight: 600;
margin-bottom: 1.5rem;
border-bottom: 3px solid var(--primary-color, #007bff);
padding-bottom: 1rem;
margin-bottom: var(--ac-space-md);
padding-bottom: var(--ac-space-md);
border-bottom: 3px solid var(--ac-color-primary);
font-size: var(--ac-text-2xl);
font-weight: var(--ac-weight-bold);
color: var(--ac-text-primary);
}
.order-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
}
.info-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-label {
font-weight: 700;
color: #2d3748;
font-size: 1.05rem;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.info-value,
.info-date {
font-size: 1.35rem;
color: #1a202c;
font-weight: 600;
}
.info-date {
display: block;
font-size: 1.25rem;
color: #2d3748;
font-weight: 500;
margin-top: 0.25rem;
}
/* Title styling for both pages */
.checkout-title,
.eskaera-order-header h1 {
font-size: 2rem;
font-weight: 700;
color: #1a202c;
margin-bottom: 1rem;
.checkout-title {
margin-bottom: var(--ac-space-md);
font-size: var(--ac-text-2xl);
font-weight: var(--ac-weight-bold);
color: var(--ac-text-primary);
}
.checkout-order-name {
font-size: 1.5rem;
color: #2d3748;
font-weight: 600;
font-size: var(--ac-text-lg);
font-weight: var(--ac-weight-semibold);
color: var(--ac-text-secondary);
}
/* Info value styling */
.info-value {
font-size: 1.1rem;
/* ==========================================================================
Order info: label / value pairs
Rendered as <dl><dt class="info-label"><dd class="info-value">.
Column count follows the viewport: 4 (label, value, label, value) on
desktop, 2 (one pair per row) from sm, stacked below that. Cells carry the
rules themselves and column-gap stays 0, so each row reads as one
uninterrupted line instead of a dashed one.
========================================================================== */
.order-info-grid {
display: grid;
grid-template-columns: 1fr;
align-items: baseline;
margin: 0;
}
.order-info-grid > .info-label {
padding-block: var(--ac-space-sm) 0;
/* Small, muted and spaced out, so the label never competes with its value.
--ac-text-secondary keeps it at 7.53:1 despite the smaller size. */
font-size: var(--ac-text-xs);
font-weight: var(--ac-weight-semibold);
line-height: var(--ac-leading-snug);
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ac-text-secondary);
}
.order-info-grid > .info-value {
margin: 0;
padding-block: var(--ac-space-2xs) var(--ac-space-sm);
border-bottom: 1px solid var(--ac-border);
font-size: var(--ac-text-md);
font-weight: var(--ac-weight-semibold);
line-height: var(--ac-leading-snug);
color: var(--ac-text-primary);
overflow-wrap: break-word;
}
/* Multi-line values (pickup slots) keep their own rhythm. */
.order-info-grid .info-value-line + .info-value-line {
margin-top: var(--ac-space-2xs);
}
/* Two columns: label and value side by side, one pair per row. */
@media (min-width: 576px) {
.order-info-grid {
grid-template-columns: auto 1fr;
}
.order-info-grid > .info-label {
padding-block: var(--ac-space-sm);
padding-inline-end: var(--ac-space-md);
border-bottom: 1px solid var(--ac-border);
}
.order-info-grid > .info-value {
padding-block: var(--ac-space-sm);
padding-inline-end: var(--ac-space-md);
}
}
/* Four columns: two pairs per row, split by a vertical rule. The second pair
always starts on an even <dt>, because grid places the cells in source
order. */
@media (min-width: 992px) {
.order-info-grid {
grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr);
}
.order-info-grid > .info-label:nth-of-type(even) {
padding-inline-start: var(--ac-space-lg);
border-inline-start: 1px solid var(--ac-border);
}
}
/* Single label/value pair, used in the checkout info row where each pair sits in
its own Bootstrap column. */
.info-pair {
margin: 0;
}
.info-pair > .info-label {
margin-bottom: var(--ac-space-3xs);
font-size: var(--ac-text-xs);
font-weight: var(--ac-weight-semibold);
line-height: var(--ac-leading-snug);
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ac-text-secondary);
}
.info-pair > .info-value {
margin: 0;
font-size: var(--ac-text-md);
font-weight: var(--ac-weight-semibold);
line-height: var(--ac-leading-snug);
color: var(--ac-text-primary);
overflow-wrap: break-word;
}
/* Standalone date line used by the checkout header. */
.info-date {
font-size: 1rem;
display: block;
margin-top: var(--ac-space-2xs);
font-size: var(--ac-text-base);
font-weight: var(--ac-weight-medium);
color: var(--ac-text-secondary);
}

View file

@ -1,106 +1,67 @@
/* filepath: website_sale_aplicoop/static/src/css/layout/pages.css */
/**
* Page backgrounds and main layout structures
* Page shells: backgrounds and the container stacking above them.
*
* These styles used to reach far outside the addon:
* - `html, body { background: transparent !important }` stripped the
* background from EVERY page of the website, addon or not.
* - `body.website_published { !important }` was meant to put it back, but
* no page carries that class checked against the rendered HTML of the
* orders, shop and checkout pages, whose <body> has no class at all. Both
* rules were therefore dead weight on top of a site-wide side effect.
* - `#wrap { padding: 2rem 0 }` styled Odoo's own page wrapper everywhere.
* On these pages #wrap *is* the Eskaera root, so the padding moved onto the
* root classes and the ID selector is gone.
*
* The body reset now applies only to pages that actually contain an Eskaera
* root, which is enough specificity to win without !important.
*/
html,
body {
background-color: transparent !important;
background: transparent !important;
body:has(.eskaera-page),
body:has(.eskaera-shop-page),
body:has(.eskaera-generic-page),
body:has(.eskaera-checkout-page) {
background: transparent;
}
body.website_published {
background: linear-gradient(
135deg,
color-mix(in srgb, var(--primary-color) 30%, white),
color-mix(in srgb, var(--primary-color) 60%, black)
) !important;
}
body.website_published .eskaera-shop-page,
body.website_published .eskaera-checkout-page {
background: transparent !important;
}
/* Generic page background mixin */
.eskaera-page,
.eskaera-shop-page,
.eskaera-generic-page,
.eskaera-checkout-page {
min-height: 100vh;
position: relative;
/* dvh tracks the mobile browser chrome; vh stays as the fallback. */
min-height: 100vh;
min-height: 100dvh;
padding: var(--ac-space-xl) 0;
}
.eskaera-page,
.eskaera-generic-page {
background: linear-gradient(
180deg,
color-mix(in srgb, var(--primary-color) 10%, white),
color-mix(in srgb, var(--primary-color) 70%, black)
) !important;
color-mix(in srgb, var(--ac-color-primary) 10%, white),
color-mix(in srgb, var(--ac-color-primary) 70%, black)
);
}
.eskaera-shop-page {
background: linear-gradient(
135deg,
color-mix(in srgb, var(--primary-color) 10%, white),
color-mix(in srgb, var(--primary-color) 10%, rgb(135, 135, 135))
) !important;
color-mix(in srgb, var(--ac-color-primary) 10%, white),
color-mix(in srgb, var(--ac-color-primary) 10%, rgb(135, 135, 135))
);
}
.eskaera-checkout-page {
background: linear-gradient(
-135deg,
color-mix(in srgb, var(--primary-color) 0%, white),
color-mix(in srgb, var(--primary-color) 60%, black)
) !important;
}
.eskaera-page::before,
.eskaera-generic-page::before {
background-image: radial-gradient(
circle at 20% 50%,
color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 80%,
color-mix(in srgb, var(--primary-color) 25%, transparent) 0%,
transparent 50%
),
radial-gradient(
circle at 40% 20%,
color-mix(in srgb, var(--primary-color, white) 15%, transparent) 0%,
transparent 50%
white,
color-mix(in srgb, var(--ac-color-primary) 60%, black)
);
}
.eskaera-shop-page::before {
background-image: radial-gradient(
circle at 15% 30%,
color-mix(in srgb, var(--primary-color, white) 18%, transparent) 0%,
transparent 50%
),
radial-gradient(
circle at 85% 70%,
color-mix(in srgb, var(--primary-color) 22%, transparent) 0%,
transparent 50%
);
}
.eskaera-checkout-page::before {
background-image: radial-gradient(
circle at 20% 50%,
color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 80%,
color-mix(in srgb, var(--primary-color) 25%, transparent) 0%,
transparent 50%
);
}
/* ---------- Decorative glow overlay ---------- */
.eskaera-page::before,
.eskaera-shop-page::before,
@ -108,28 +69,61 @@ body.website_published .eskaera-checkout-page {
.eskaera-checkout-page::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
inset: 0;
z-index: 0;
pointer-events: none;
}
.eskaera-page::before,
.eskaera-generic-page::before {
background-image: radial-gradient(
circle at 20% 50%,
color-mix(in srgb, var(--ac-color-primary) 20%, transparent) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 80%,
color-mix(in srgb, var(--ac-color-primary) 25%, transparent) 0%,
transparent 50%
),
radial-gradient(
circle at 40% 20%,
color-mix(in srgb, var(--ac-color-primary) 15%, transparent) 0%,
transparent 50%
);
}
.eskaera-shop-page::before {
background-image: radial-gradient(
circle at 15% 30%,
color-mix(in srgb, var(--ac-color-primary) 18%, transparent) 0%,
transparent 50%
),
radial-gradient(
circle at 85% 70%,
color-mix(in srgb, var(--ac-color-primary) 22%, transparent) 0%,
transparent 50%
);
}
.eskaera-checkout-page::before {
background-image: radial-gradient(
circle at 20% 50%,
color-mix(in srgb, var(--ac-color-primary) 20%, transparent) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 80%,
color-mix(in srgb, var(--ac-color-primary) 25%, transparent) 0%,
transparent 50%
);
}
/* Content sits above the overlay. */
.eskaera-page > .container,
.eskaera-shop-page > .container,
.eskaera-generic-page > div,
.eskaera-checkout-page > .container {
position: relative;
z-index: 1;
padding-top: 2rem;
padding-bottom: 2rem;
}
#wrap {
padding: 2rem 0;
}
.group-order-shop {
background-color: transparent;
}

View file

@ -1,103 +1,21 @@
/* filepath: website_sale_aplicoop/static/src/css/layout/responsive.css */
/**
* Responsive design breakpoints and adjustments
* All media queries centralized here for easier maintenance
* NOTE: products-grid.css has its own breakpoints and should NOT be overridden here
* Responsive adjustments do not add to it.
*
* Centralising every media query here was the original plan; in practice this
* file drifted out of sync with the components it overrode and needed
* !important to win twenty of them at its peak. Each component now owns its
* queries at the bottom of its own file, and most need none at all once sizing
* is fluid. What is left here is thumbnail geometry and table density, which
* belong to no single component.
*/
@media (max-width: 992px) {
/* Cart sidebar */
.sticky-cart {
position: static !important;
margin-top: 2rem;
width: 100%;
}
.cart-items {
max-height: 400px;
}
#cart-items-container {
width: 100%;
padding: 0.75rem;
}
.list-group-item {
padding: 0.75rem;
}
.list-group-item h6 {
font-size: 0.95rem;
}
.list-group-item strong {
min-width: 70px;
}
.cart-header {
padding: 1rem;
width: 100%;
}
.cart-header h5 {
font-size: 1.25rem;
}
.cart-title-lg {
font-size: 1.25rem;
}
#save-cart-btn,
#reload-cart-btn {
padding: 0.5rem 0.75rem;
font-size: 0.9rem;
margin-right: 0.25rem;
}
.card-header .btn-group {
gap: 0.5rem !important;
}
/* Order list grid */
.eskaera-orders,
.eskaera-orders-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
}
@media (max-width: 768px) {
/* Header (shared between eskaera and checkout) */
.eskaera-order-header,
.checkout-header {
padding: 1rem;
}
.eskaera-order-header h1,
.checkout-header h1 {
font-size: 1.5rem;
margin-bottom: 1rem;
word-wrap: break-word;
overflow-wrap: break-word;
}
.checkout-title,
.eskaera-order-header h1 {
font-size: 1.5rem;
}
.checkout-order-name {
font-size: 1.2rem;
}
/* Fix header flex layout for mobile */
.eskaera-order-header .d-flex,
.checkout-header .d-flex {
flex-direction: column !important;
gap: 1rem !important;
align-items: flex-start !important;
}
/* Header type and the info grid scale from layout/header.css; the title row
stacks through Bootstrap's flex-column/flex-md-row utilities. Order cards,
order list, product card and quantity control all size themselves from
their own files. */
.order-thumbnail-md {
max-width: 100%;
@ -105,413 +23,54 @@
height: auto;
}
/* Order info grid */
.order-info-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
/* Eskaera page headings */
.eskaera-page h1 {
font-size: 2rem;
}
.eskaera-page > .container > .row:first-child p {
font-size: 1.1rem !important;
}
/* Order cards */
.eskaera-order-card {
min-height: 250px;
}
.eskaera-order-card .card-body {
padding: 0.5rem 0.6rem 0 0.6rem;
gap: 0.15rem;
}
.eskaera-order-card .card-title {
font-size: 0.9rem;
-webkit-line-clamp: 3;
}
.order-desc-text {
font-size: 0.75rem;
-webkit-line-clamp: 2;
}
.eskaera-order-card .btn {
padding: 0.5rem 1rem;
font-size: 0.8rem;
margin-bottom: 0.5rem;
}
.order-thumbnail-sm {
width: 70px;
height: 70px;
}
.order-thumbnail-md {
width: 100px;
height: 100px;
}
/* Header with image and text */
.eskaera-order-card .d-flex {
gap: 0.75rem !important;
}
.eskaera-order-card .card-footer {
padding: 0.75rem 1rem;
}
/* Order list */
.eskaera-orders,
.eskaera-orders-grid {
grid-template-columns: 1fr !important;
gap: 1.25rem;
}
/* Product grid */
.card-grid {
grid-template-columns: 1fr;
}
/* Product quantity controls */
.add-to-cart-form .product-qty {
font-size: 1rem;
min-width: 50px;
height: 32px;
padding: 0.25rem 0.35rem;
}
.qty-control .qty-decrease,
.qty-control .qty-increase {
height: 32px;
width: 32px;
font-size: 0.9rem;
}
.add-to-cart-form .add-to-cart-btn {
height: 36px;
min-height: 36px;
padding: 0.35rem 0.5rem;
font-size: 1rem;
min-width: 44px;
}
.add-to-cart-form .input-group {
gap: 0.25rem;
}
/* Checkout summary */
.checkout-summary-container {
padding: 1rem;
overflow-x: auto;
}
.checkout-summary-table {
font-size: 0.95rem;
min-width: 500px; /* Prevent table collapse */
}
.checkout-summary-table thead th {
padding: 0.75rem 0.5rem;
font-size: 0.85rem;
}
.checkout-summary-table tbody td {
padding: 0.75rem 0.5rem;
}
.total-amount {
font-size: 1.5rem;
}
.summary-heading {
font-size: 1.5rem;
}
.checkout-actions .btn {
font-size: 1rem;
padding: 0.6rem 1.2rem;
width: 4.375rem;
height: 4.375rem;
}
.group-order-header {
padding: 1rem 0;
margin-bottom: 1rem;
}
.product-card {
margin-bottom: 1rem;
}
.product-card .product-image {
height: 150px;
}
.checkout-container {
padding: 1rem;
padding: var(--ac-space-md) 0;
margin-bottom: var(--ac-space-md);
}
}
@media (max-width: 576px) {
/* Cart items */
.list-group-item.d-flex {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.list-group-item h6 {
font-size: 0.85rem;
word-break: break-word;
}
.list-group-item small {
font-size: 0.75rem;
}
.list-group-item .d-flex {
width: 100%;
justify-content: space-between;
align-items: center;
}
.list-group-item strong {
min-width: auto;
font-size: 0.95rem;
}
.list-group-item .remove-from-cart {
min-width: 28px;
min-height: 28px;
}
.cart-items {
max-height: 250px;
}
.sticky-cart {
margin-top: 1.5rem;
}
/* Checkout type scales fluidly from sections/checkout.css; only the table's
density genuinely changes here, and the container's padding with it. */
.toast-notification {
bottom: 20px;
right: 20px;
left: 20px;
inset: auto var(--ac-space-md) var(--ac-space-md);
max-width: none;
}
#save-cart-btn,
#reload-cart-btn {
padding: 0.3rem 0.4rem;
font-size: 0.8rem;
}
.card-header .d-flex {
flex-wrap: wrap;
gap: 0.5rem !important;
}
.card-header .btn-group {
gap: 0.25rem !important;
}
.card-header .btn-group .btn {
padding: 0.3rem 0.4rem;
font-size: 0.8rem;
}
.form-check {
padding: 1rem 1.5rem !important;
margin-left: 0 !important;
}
.form-check-input {
margin-left: 0 !important;
margin-right: 1rem !important;
flex-shrink: 0;
}
.form-check-label[for="home-delivery-checkbox"] {
font-size: 1rem !important;
margin-left: 0 !important;
}
/* Header */
.eskaera-order-header,
.checkout-header {
padding: 0.75rem;
}
.eskaera-order-header h1,
.checkout-header h1 {
font-size: 1.25rem;
padding-bottom: 0.5rem;
}
.checkout-title,
.eskaera-order-header h1 {
font-size: 1.25rem;
}
.checkout-order-name {
font-size: 1rem;
}
.info-label {
font-size: 0.9rem;
}
/* Eskaera page */
.eskaera-page h1 {
font-size: 1.5rem;
margin-bottom: 0.75rem;
}
.eskaera-page > .container > .row:first-child {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
}
.eskaera-page > .container > .row:first-child p {
font-size: 1rem !important;
}
/* Order cards */
.eskaera-order-card {
min-height: 220px;
}
.eskaera-order-card .card-body {
padding: 0.4rem 0.5rem 0 0.5rem;
}
.eskaera-order-card .card-title {
font-size: 0.85rem;
}
.order-desc-text {
font-size: 0.7rem;
}
.eskaera-order-card .btn {
padding: 0.45rem 0.85rem;
font-size: 0.75rem;
margin-bottom: 0.4rem;
}
.order-thumbnail-sm {
width: 60px;
height: 60px;
width: 3.75rem;
height: 3.75rem;
}
.order-thumbnail-md {
width: 80px;
height: 80px;
}
/* Order list */
.eskaera-orders,
.eskaera-orders-grid {
grid-template-columns: 1fr !important;
gap: 1rem;
width: 5rem;
height: 5rem;
}
.eskaera-empty-state {
padding: 2rem 0.5rem;
padding: var(--ac-space-xl) var(--ac-space-sm);
}
.eskaera-empty-state .alert {
font-size: 0.95rem;
padding: 1rem;
}
/* Checkout */
.checkout-summary-container {
padding: 0.75rem;
padding: var(--ac-space-sm);
}
.checkout-summary-table {
font-size: 0.85rem;
min-width: 450px;
}
.checkout-summary-table thead th {
padding: 0.5rem 0.35rem;
font-size: 0.75rem;
min-width: 28rem;
}
.checkout-summary-table thead th,
.checkout-summary-table tbody td {
padding: 0.5rem 0.35rem;
font-size: 0.85rem;
}
.total-amount {
font-size: 1.3rem;
}
.total-label {
font-size: 1rem;
}
.currency {
font-size: 1rem;
}
.summary-heading {
font-size: 1.25rem;
padding-left: 0.75rem;
}
.checkout-actions .btn {
font-size: 0.9rem;
padding: 0.6rem 1rem;
}
}
/* Product card typography responsive scaling */
@media screen and (min-width: 1600px) {
.product-tags {
font-size: 1.1rem !important;
}
/* Scale down quantity input for 6-column layout */
.add-to-cart-form .product-qty {
font-size: 0.85rem;
max-width: 55px;
}
.add-to-cart-form .qty-decrease,
.add-to-cart-form .qty-increase {
font-size: 0.75rem;
min-width: 28px;
min-height: 28px;
}
}
@media screen and (min-width: 1400px) and (max-width: 1599px) {
.product-tags {
font-size: 1.25rem !important;
}
/* Scale down quantity input for 5-column layout */
.add-to-cart-form .product-qty {
font-size: 0.9rem;
max-width: 60px;
}
.add-to-cart-form .qty-decrease,
.add-to-cart-form .qty-increase {
font-size: 0.8rem;
min-width: 30px;
min-height: 30px;
}
}
@media (max-width: 720px) {
.card-grid {
grid-template-columns: 1fr;
padding: var(--ac-space-sm) var(--ac-space-2xs);
}
}

View file

@ -1,64 +1,67 @@
/* filepath: website_sale_aplicoop/static/src/css/sections/checkout.css */
/**
* Checkout page section styles
* Checkout page
*
* The summary table keeps a min-width and scrolls inside its own container
* rather than reflowing into cards: the four columns (product, qty, price,
* subtotal) are read across, and stacking them loses that. The container is
* focusable so the scroll is reachable without a pointer.
*/
.checkout-container {
background-color: white;
padding: 2rem;
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.checkout-summary {
background-color: #f8f9fa;
padding: 1.5rem;
border-radius: 0.5rem;
margin-bottom: 2rem;
margin-bottom: var(--ac-space-xl);
padding: var(--ac-space-lg);
border-radius: var(--ac-radius-md);
background-color: var(--ac-surface-sunken);
}
.checkout-summary-container {
background: white;
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: 1.5rem;
margin-bottom: 2rem;
margin-bottom: var(--ac-space-xl);
padding: var(--ac-space-lg);
border-radius: var(--ac-radius-lg);
background: var(--ac-surface);
box-shadow: var(--ac-shadow-sm);
overflow-x: auto;
}
.checkout-summary-container:focus-visible {
outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color);
outline-offset: var(--ac-focus-ring-offset);
}
.checkout-summary-table {
margin-bottom: 0;
font-size: 1.15rem;
}
.checkout-summary-table thead {
background-color: #2d3748;
color: white;
min-width: 30rem;
font-size: var(--ac-text-md);
}
.checkout-summary-table thead th {
font-weight: 700;
padding: 1rem 0.75rem;
text-transform: uppercase;
font-size: 1rem;
letter-spacing: 0.5px;
padding: var(--ac-space-md) var(--ac-space-xs);
border: none;
font-size: var(--ac-text-base);
font-weight: var(--ac-weight-bold);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.checkout-summary-table tbody tr {
border-bottom: 1px solid #e2e8f0;
transition: background-color 0.2s ease;
}
.checkout-summary-table tbody tr:hover {
background-color: #f7fafc;
border-bottom: 1px solid var(--ac-border);
transition: background-color var(--ac-transition-fast);
}
.checkout-summary-table tbody td {
padding: 1rem 0.75rem;
padding: var(--ac-space-md) var(--ac-space-xs);
vertical-align: middle;
}
/* Figures line up column-wise instead of drifting with glyph widths. */
.checkout-summary-table .col-price,
.checkout-summary-table .col-subtotal,
.checkout-summary-table td.text-end {
font-variant-numeric: tabular-nums;
}
.checkout-summary-table .col-name {
width: 50%;
}
@ -76,52 +79,68 @@
}
.checkout-summary-table .empty-message {
background-color: #f7fafc;
background-color: var(--ac-surface-sunken);
}
/* ---------- Total ---------- */
.checkout-total-section {
border-top: 2px solid #e2e8f0;
padding-top: 1.5rem;
margin-top: 1rem;
margin-top: var(--ac-space-md);
padding-top: var(--ac-space-lg);
border-top: 2px solid var(--ac-border);
}
.total-row {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 1rem;
align-items: baseline;
gap: var(--ac-space-sm);
}
.total-label {
font-weight: 700;
font-size: 1.2rem;
color: #1a202c;
font-size: var(--ac-text-md);
font-weight: var(--ac-weight-bold);
color: var(--ac-text-primary);
}
.total-amount {
font-size: 2rem;
font-weight: 700;
color: var(--success-color);
min-width: 120px;
min-width: 7.5rem;
font-size: var(--ac-text-2xl);
font-weight: var(--ac-weight-bold);
font-variant-numeric: tabular-nums;
text-align: right;
/* --ac-color-success is 3.13:1 on white: fine for the 2xl total, but the
darker tone keeps it readable if the type scale ever shrinks. */
color: var(--ac-color-success-strong);
}
.currency {
font-size: 1.2rem;
color: #2d3748;
font-weight: 600;
font-size: var(--ac-text-md);
font-weight: var(--ac-weight-semibold);
color: var(--ac-text-secondary);
}
.summary-heading {
font-size: 1.75rem;
font-weight: 700;
color: #1a202c;
border-left: 4px solid var(--success-color);
padding-left: 1rem;
padding-left: var(--ac-space-md);
border-left: 4px solid var(--ac-color-success-strong);
font-size: var(--ac-text-xl);
font-weight: var(--ac-weight-bold);
color: var(--ac-text-primary);
}
.checkout-order-desc {
word-wrap: break-word;
color: var(--ac-text-secondary);
overflow-wrap: break-word;
color: #666;
}
@media (hover: hover) and (pointer: fine) {
.checkout-summary-table tbody tr:hover {
background-color: var(--ac-surface-sunken);
}
}
@media (prefers-reduced-motion: reduce) {
.checkout-summary-table tbody tr {
transition: none;
}
}

View file

@ -1,63 +1,21 @@
/* filepath: website_sale_aplicoop/static/src/css/sections/info-cards.css */
/**
* Info cards and grid section styles
* Info cards
*
* The meta block that used to be duplicated here now lives with its component
* (components/order-card.css): `.card-meta-compact`, `.meta-label` and
* `.meta-value` were declared in both files, and this one loading last won,
* silently overriding the component. `.card-grid`, `.card-col`, `.card-meta-list`
* and `.meta-row` were dead: no template ever emitted them.
*/
.order-info-card {
background: white;
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border-radius: var(--ac-radius-lg);
background: var(--ac-surface);
box-shadow: var(--ac-shadow-sm);
}
.order-info-card .card-body {
padding: 1rem;
}
.card-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
align-items: start;
margin-top: 0.5rem;
}
.card-meta-compact {
padding: 0.5rem 0;
}
.card-meta-compact .card-meta-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.meta-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0;
}
.meta-label {
font-weight: 700;
color: #2d3748;
min-width: fit-content;
flex-shrink: 0;
}
.meta-value {
font-weight: 400;
color: #27292c;
word-break: break-word;
flex-grow: 1;
}
.card-col {
min-width: 0;
}
.card-col .card-text strong {
font-weight: 700;
color: #111827;
padding: var(--ac-space-md);
}

View file

@ -1,48 +1,54 @@
/* filepath: website_sale_aplicoop/static/src/css/sections/order-list.css */
/**
* Order list and Eskaera page section styles
* Order list (Eskaera index page)
*
* The grid derived its column count from three breakpoints (3 / 2 / 1) that
* each had to out-specify the previous one the mobile rule needed
* !important. Deriving it from the available width removes all three.
*/
.eskaera-page h1 {
font-size: 2.5rem;
font-weight: 700;
color: #2d3748;
margin-bottom: 0.5rem;
margin-bottom: var(--ac-space-sm);
font-size: var(--ac-text-2xl);
font-weight: var(--ac-weight-bold);
text-align: center;
color: var(--ac-text-primary);
}
.eskaera-page > .container > .row:first-child {
margin-bottom: 2rem;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 1.5rem;
margin-bottom: var(--ac-space-xl);
padding-bottom: var(--ac-space-lg);
border-bottom: 2px solid var(--ac-border);
}
.eskaera-page > .container > .row:first-child p {
font-size: 1.3rem !important;
color: #4a5568;
text-align: center;
margin: 0;
font-weight: 500;
font-size: var(--ac-text-md);
font-weight: var(--ac-weight-medium);
text-align: center;
color: var(--ac-text-secondary);
}
.eskaera-orders,
.eskaera-orders-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
/* 20rem keeps three across a full-width container and drops to one on a
phone, without any breakpoint doing the counting. */
grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
gap: var(--ac-space-lg);
margin: 0 auto;
max-width: 100%;
}
.eskaera-empty-state {
padding: var(--ac-space-2xl) var(--ac-space-md);
text-align: center;
padding: 3rem 1rem;
}
.eskaera-empty-state .alert {
max-width: 500px;
max-width: 31.25rem;
margin: 0 auto;
font-size: 1.05rem;
border-radius: 0.5rem;
border-radius: var(--ac-radius-md);
font-size: var(--ac-text-base);
}

View file

@ -1,14 +1,29 @@
/* filepath: website_sale_aplicoop/static/src/css/sections/products-grid.css */
/**
* Products grid section styles
* Products grid
*
* 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.
*
* 9rem is the card's minimum width; at the widest container (1320px minus the
* category sidebar) it still resolves to six columns, as before.
*/
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 1.2rem;
margin-bottom: 2rem;
grid-template-columns: 1fr;
gap: var(--ac-space-md);
margin-bottom: var(--ac-space-xl);
}
@media (min-width: 576px) {
.products-grid {
grid-template-columns: repeat(auto-fill, minmax(min(100%, 9rem), 1fr));
}
}
.product-card-wrapper {
@ -17,62 +32,7 @@
}
.product-search-highlight {
border: 2px solid #007bff;
padding: 10px;
}
/* Pantallas muy grandes: 1600px+ (6 columnas) */
@media screen and (min-width: 1600px) {
.products-grid {
grid-template-columns: repeat(6, 1fr);
gap: 1.2rem;
}
}
/* Pantallas grandes: 1400px-1599px (5 columnas) */
@media screen and (min-width: 1400px) and (max-width: 1599px) {
.products-grid {
grid-template-columns: repeat(5, 1fr);
gap: 1.15rem;
}
}
/* Pantallas medianas: 1200px-1399px (4 columnas) */
@media screen and (min-width: 1200px) and (max-width: 1399px) {
.products-grid {
grid-template-columns: repeat(4, 1fr);
gap: 1.1rem;
}
}
/* Pantallas tablet grandes: 992px-1199px (3 columnas) */
@media screen and (min-width: 992px) and (max-width: 1199px) {
.products-grid {
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
}
/* Pantallas tablet: 768px-991px (3 columnas en tablet grande, 2 en tablet pequeña) */
@media screen and (min-width: 768px) and (max-width: 991px) {
.products-grid {
grid-template-columns: repeat(3, 1fr);
gap: 0.95rem;
}
}
/* Pantallas móvil grande: 576px-767px (2 columnas) */
@media screen and (min-width: 576px) and (max-width: 767px) {
.products-grid {
grid-template-columns: repeat(2, 1fr);
gap: 0.8rem;
}
}
/* Pantallas móvil pequeño: 1 columna */
@media (max-width: 575px) {
.products-grid {
grid-template-columns: 1fr;
gap: 0.5rem;
}
padding: var(--ac-space-sm);
border: 2px solid var(--ac-color-primary);
border-radius: var(--ac-radius-md);
}

View file

@ -194,10 +194,10 @@
'<th scope="col" class="col-qty text-center">' +
escapeHtml(labels.quantity) +
"</th>" +
'<th scope="col" class="col-price text-right">' +
'<th scope="col" class="col-price text-end">' +
escapeHtml(labels.price) +
"</th>" +
'<th scope="col" class="col-subtotal text-right">' +
'<th scope="col" class="col-subtotal text-end">' +
escapeHtml(labels.subtotal) +
"</th>" +
'</tr></thead><tbody id="checkout-summary-tbody"></tbody></table>' +
@ -280,10 +280,10 @@
'<td class="text-center">' +
qty.toFixed(2).replace(/\.?0+$/, "") +
"</td>" +
'<td class="text-right">€' +
'<td class="text-end">€' +
price.toFixed(2) +
"</td>" +
'<td class="text-right">€' +
'<td class="text-end">€' +
subtotal.toFixed(2) +
"</td>";
tbody.appendChild(row);
@ -307,10 +307,10 @@
'<td class="text-center">' +
qty.toFixed(2).replace(/\.?0+$/, "") +
"</td>" +
'<td class="text-right">€' +
'<td class="text-end">€' +
price.toFixed(2) +
"</td>" +
'<td class="text-right">€' +
'<td class="text-end">€' +
subtotal.toFixed(2) +
"</td>";
tbody.appendChild(row);

View file

@ -459,8 +459,9 @@ console.log("[INFINITE_SCROLL] Script loaded!");
// Show fallback button
var btn = document.getElementById("load-more-btn");
if (btn) {
// d-none is the only thing hiding it; the button no longer
// carries an inline display:none to clear.
btn.classList.remove("d-none");
btn.style.display = "";
}
});
},

View file

@ -141,9 +141,6 @@
// Initialize available tags from DOM
self._initializeAvailableTags();
// Store original colors for each tag badge
self.originalTagColors = {}; // Maps tag ID to original color
// Store last values at instance level so polling can access them
// Initialize to current values to avoid triggering reset on first poll
self.lastSearchValue = self.searchInput.value.trim();
@ -165,9 +162,9 @@
// This listener is separate from the filtering listener
self.searchInput.addEventListener("input", function () {
if (self.searchInput.value.trim().length > 0) {
self.clearSearchBtn.style.display = "block";
self.clearSearchBtn.classList.add("is-visible");
} else {
self.clearSearchBtn.style.display = "none";
self.clearSearchBtn.classList.remove("is-visible");
}
});
@ -179,7 +176,7 @@
// Clear the input
self.searchInput.value = "";
self.clearSearchBtn.style.display = "none";
self.clearSearchBtn.classList.remove("is-visible");
// Update last stored value to prevent polling from detecting "change"
self.lastSearchValue = "";
@ -204,7 +201,7 @@
// Initial check - don't show if empty
if (self.searchInput.value.trim().length > 0) {
self.clearSearchBtn.style.display = "block";
self.clearSearchBtn.classList.add("is-visible");
}
}
@ -283,114 +280,21 @@
var tagBadges = document.querySelectorAll('[data-toggle="tag-filter"]');
console.log("[realtimeSearch] Found " + tagBadges.length + " tag filter badges");
// Get theme colors from CSS variables
var rootStyles = getComputedStyle(document.documentElement);
var primaryColor =
rootStyles.getPropertyValue("--bs-primary").trim() ||
rootStyles.getPropertyValue("--primary").trim() ||
"#0d6efd";
var secondaryColor =
rootStyles.getPropertyValue("--bs-secondary").trim() ||
rootStyles.getPropertyValue("--secondary").trim() ||
"#6c757d";
console.log(
"[realtimeSearch] Theme colors - Primary:",
primaryColor,
"Secondary:",
secondaryColor
);
// Store original colors for each badge BEFORE adding event listeners
tagBadges.forEach(function (badge) {
var tagId = parseInt(badge.getAttribute("data-tag-id"), 10);
var tagColor = badge.getAttribute("data-tag-color");
// Store the original color (either from data-tag-color or use secondary for tags without color)
if (tagColor) {
self.originalTagColors[tagId] = tagColor;
console.log(
"[realtimeSearch] Stored original color for tag " + tagId + ": " + tagColor
);
} else {
self.originalTagColors[tagId] = "var(--bs-secondary, " + secondaryColor + ")";
console.log("[realtimeSearch] Tag " + tagId + " has no color, using secondary");
}
});
tagBadges.forEach(function (badge) {
badge.addEventListener("click", function (e) {
e.preventDefault();
var tagId = parseInt(badge.getAttribute("data-tag-id"), 10);
var originalColor = self.originalTagColors[tagId];
console.log(
"[realtimeSearch] Tag badge clicked: " +
tagId +
" (original color: " +
originalColor +
")"
);
// Toggle tag selection
if (self.selectedTags.has(tagId)) {
// Deselect
self.selectedTags.delete(tagId);
console.log("[realtimeSearch] Tag " + tagId + " deselected");
} else {
// Select
self.selectedTags.add(tagId);
console.log("[realtimeSearch] Tag " + tagId + " selected");
}
// Update colors for ALL badges based on selection state
tagBadges.forEach(function (badge) {
var id = parseInt(badge.getAttribute("data-tag-id"), 10);
if (self.selectedTags.size === 0) {
// No tags selected: restore all to original colors
var originalColor = self.originalTagColors[id];
badge.style.setProperty("background-color", originalColor, "important");
badge.style.setProperty("border-color", originalColor, "important");
badge.style.setProperty("color", "#ffffff", "important");
console.log(
"[realtimeSearch] Badge " +
id +
" reset to original color (no selection)"
);
} else if (self.selectedTags.has(id)) {
// Selected: primary color
badge.style.setProperty(
"background-color",
"var(--bs-primary, " + primaryColor + ")",
"important"
);
badge.style.setProperty(
"border-color",
"var(--bs-primary, " + primaryColor + ")",
"important"
);
badge.style.setProperty("color", "#ffffff", "important");
console.log(
"[realtimeSearch] Badge " + id + " set to primary (selected)"
);
} else {
// Not selected but others are: secondary color
badge.style.setProperty(
"background-color",
"var(--bs-secondary, " + secondaryColor + ")",
"important"
);
badge.style.setProperty(
"border-color",
"var(--bs-secondary, " + secondaryColor + ")",
"important"
);
badge.style.setProperty("color", "#ffffff", "important");
console.log(
"[realtimeSearch] Badge " + id + " set to secondary (not selected)"
);
}
});
self._syncTagBadgeStates(tagBadges);
// Filter products (independent of search/category state)
// Skip during initialization
@ -647,6 +551,10 @@
")"
);
// Announce the outcome to screen readers, which otherwise get no
// signal that the grid changed under them.
self._announceResults(visibleCount);
// Auto-load more products if too few are visible
self._autoLoadMoreIfNeeded(visibleCount);
} catch (error) {
@ -655,6 +563,49 @@
}
},
/**
* Reflect the current selection on every tag badge: aria-pressed for
* assistive tech, two state classes for the eye. Colours belong to
* components/tag-filter.css this only says which state each badge is in.
*
* @param {NodeList} tagBadges - All badges in the filter bar
*/
_syncTagBadgeStates: function (tagBadges) {
var self = this;
var hasSelection = self.selectedTags.size > 0;
tagBadges.forEach(function (badge) {
var id = parseInt(badge.getAttribute("data-tag-id"), 10);
var isSelected = self.selectedTags.has(id);
badge.classList.toggle("is-selected", isSelected);
badge.classList.toggle("is-dimmed", hasSelection && !isSelected);
badge.setAttribute("aria-pressed", isSelected ? "true" : "false");
});
},
/**
* Write the result count into the polite live region next to the search
* box. Labels come from i18nManager, never from a hardcoded literal.
*
* @param {number} visibleCount - Number of currently visible products
*/
_announceResults: function (visibleCount) {
var status = document.getElementById("realtime-search-status");
if (!status) {
return;
}
var i18n = window.i18nManager;
var message;
if (visibleCount === 0) {
message = (i18n && i18n.get("no_results")) || "No products found";
} else {
message =
visibleCount + " " + ((i18n && i18n.get("products_found")) || "products found");
}
status.textContent = message;
},
_autoLoadMoreIfNeeded: function (visibleCount) {
/**
* Automatically load more products if there are too few visible on screen.

View file

@ -691,12 +691,16 @@
var notification = document.createElement("div");
notification.className =
"alert alert-" + type + " alert-dismissible fade show position-fixed";
notification.style.cssText =
"top: 80px; right: 20px; z-index: 9999; min-width: 300px; max-width: 500px; font-size: 18px; padding: 1.5rem; transition: opacity 0.3s ease-out;";
"alert alert-" + type + " alert-dismissible fade show eskaera-notification";
// Announce it: errors and warnings are the whole point of this helper,
// and without a live role a screen reader never hears them.
notification.setAttribute("role", type === "error" ? "alert" : "status");
notification.setAttribute("aria-live", type === "error" ? "assertive" : "polite");
notification.innerHTML =
message +
'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>';
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="' +
((window.i18nManager && window.i18nManager.get("close")) || "Close") +
'"></button>';
document.body.appendChild(notification);
@ -1253,13 +1257,17 @@
// Create toast element
var toast = document.createElement("div");
toast.className = "toast-notification";
toast.setAttribute("role", "status");
// Get translated "added to cart" label from server
var labels = this._getLabels();
var addedMsg = labels.added_to_cart || "added to cart";
toast.innerHTML =
'<i class="fa fa-check"></i> <strong>' + productName + "</strong> " + addedMsg;
'<i class="fa fa-check" aria-hidden="true"></i> <strong>' +
this.escapeHtml(productName) +
"</strong> " +
addedMsg;
document.body.appendChild(toast);
@ -1551,7 +1559,7 @@
// Simple approach: if response is very short, assume no more products
if (html.trim().length < 100) {
console.log("[LAZY_LOADING] No more products, hiding load-more button");
btn.style.display = "none";
btn.classList.add("d-none");
} else {
// Re-enable button for next page
btn.disabled = false;
@ -1595,7 +1603,7 @@
if (Object.keys(this.cart).length === 0) {
cartContainer.innerHTML =
'<p class="text-muted" style="font-size: 1.1rem;">' +
'<p class="text-muted cart-empty-message">' +
(labels.empty_cart || "This order's cart is empty.") +
"</p>";
return;
@ -1612,20 +1620,20 @@
total += subtotal;
html +=
'<div class="list-group-item" style="font-size: 1.05rem;">' +
'<div class="list-group-item">' +
'<div class="mb-2">' +
'<h6 class="mb-0" style="font-size: 1.1rem;">' +
'<h6 class="mb-0">' +
self.escapeHtml(item.name) +
"</h6>" +
"</div>" +
'<div class="d-flex justify-content-between align-items-center">' +
'<small class="text-muted" style="font-size: 1rem;">' +
'<small class="text-muted">' +
parseFloat(item.qty).toFixed(1) +
" x €" +
item.price.toFixed(2) +
"</small>" +
'<div class="d-flex align-items-center gap-2">' +
'<strong style="font-size: 1.1rem;">€' +
"<strong>€" +
subtotal.toFixed(2) +
"</strong>" +
'<button class="btn btn-sm btn-danger remove-from-cart" ' +
@ -1644,11 +1652,7 @@
});
html += "</div>";
html +=
'<div class="cart-total mt-3 pt-3" style="font-size: 1.2rem; font-weight: bold;">' +
"Total: €" +
total.toFixed(2) +
"</div>";
html += '<div class="cart-total mt-3 pt-3">' + "Total: €" + total.toFixed(2) + "</div>";
cartContainer.innerHTML = html;

View file

@ -3,12 +3,45 @@
<data>
<!-- Template to load items from history and redirect to group order -->
<template id="eskaera_load_from_history" name="Load Order from History">
<html>
<html t-att-lang="page_lang or 'en-US'">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Loading Order...</title>
<!-- Standalone document: it does not go through website.layout, so
web.assets_frontend is not loaded and there is no design token to
reach for. Hence the local style block. -->
<style>
.eskaera-redirect-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
padding: 2rem 1rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
text-align: center;
color: #1a202c;
}
.eskaera-redirect-message {
font-size: 1.125rem;
}
</style>
</head>
<body>
<!-- This page only stages the cart in sessionStorage and redirects, but it is
still a page a person can land on: give it something to read while that
happens, announced politely, and a way out if the script never runs. -->
<main class="eskaera-redirect-page">
<p class="eskaera-redirect-message" role="status">Loading your order…</p>
<noscript>
<p>This page needs JavaScript to load the order into your cart.</p>
<p>
<a t-att-href="group_order_url">Continue to the group order</a>
</p>
</noscript>
</main>
<script type="text/javascript">
// Items are embedded directly in the script (pre-serialized JSON from controller)
var itemsJson = <t t-raw="items_json"/>; // This is a JSON array/string

View file

@ -3,11 +3,13 @@
<data>
<!-- Extend portal_my_orders to add group order name and pickup day columns -->
<template id="portal_my_orders_extend" inherit_id="sale.portal_my_orders" name="My Orders - Add Group Order Info">
<!-- Add column headers for new info -->
<!-- Add column headers for new info.
There is no "Actions" header here: the matching <td> below is
commented out, so declaring it left every row one cell short of
its header row and pushed the columns out of alignment. -->
<xpath expr="//tr[hasclass('active')]//th[last()]" position="after">
<th>Group Order</th>
<th>Pickup Day</th>
<th class="text-center">Actions</th>
<th scope="col">Group Order</th>
<th scope="col">Pickup Day</th>
</xpath>
<!-- Add data cells for each order row -->
@ -75,25 +77,30 @@
<!-- Group Order Info -->
<div class="col-lg-12">
<div class="card">
<!-- text-bg-primary, not bg-primary: a badge's default white on
the primary blue is 3.98:1, under AA. The utility picks a
contrasting foreground for whatever the theme colour is.
d-block on the notes so their mt-2 actually applies — margins
do nothing on an inline span. -->
<div class="card-header bg-light">
<strong><t t-esc="sale_order.group_order_id.name"/></strong>
<t t-if="sale_order.home_delivery">
<span class="badge bg-primary">
<span class="badge text-bg-primary">
<t t-set="day_idx" t-value="(int(sale_order.group_order_id.pickup_day) + 1) % 7 if sale_order.group_order_id.pickup_day else 0"/>
<t t-esc="day_names[day_idx] if day_names else ''"/>,
<t t-esc="sale_order.group_order_id.delivery_date.strftime('%d/%m/%Y') if sale_order.group_order_id.delivery_date else ''"/>
</span>
<span class="mt-2 text-muted small">
<span class="d-block mt-2 text-muted small">
<t t-esc="sale_order.group_order_id.delivery_notice"/>
</span>
</t>
<t t-else="">
<span class="badge bg-primary">
<span class="badge text-bg-primary">
<t t-set="day_idx" t-value="int(sale_order.group_order_id.pickup_day) % 7 if sale_order.group_order_id.pickup_day else 0"/>
<t t-esc="day_names[day_idx] if day_names else ''"/>,
<t t-esc="sale_order.pickup_date.strftime('%d/%m/%Y')"/>
</span>
<span class="mt-2 small">
<span class="d-block mt-2 small">
<t t-foreach="sale_order.group_order_id.group_ids" t-as="group">
<t t-esc="group.name"/>
<t t-if="group.street">

View file

@ -21,8 +21,8 @@
<div t-attf-class="eskaera-order-card{{ ' eskaera-order-card--special' if order.type == 'special' else '' }}">
<div class="card-body">
<div class="position-absolute order-badge-position">
<span class="badge bg-primary d-flex align-items-center gap-2 order-badge-custom">
<i class="fa fa-shopping-bag" />
<span class="badge text-bg-primary d-flex align-items-center gap-2 order-badge-custom">
<i class="fa fa-shopping-bag" aria-hidden="true" />
<strong>
<t t-esc="order.available_products_count" />
</strong>
@ -147,22 +147,24 @@
</span>
</div>
</t>
<t t-if="order.home_delivery and order.delivery_date">
<div class="meta-item">
<span class="meta-label">Delivery</span>
<t t-if="order.home_delivery">
<!-- Badge first, then the date, centred as a pair. Source order
matches reading order, so no CSS reordering is involved.
No "Delivery" label: the badge already names the thing.
text-bg-* instead of bg-*: Bootstrap picks a contrasting
foreground. A badge's default white on green is 3.13:1. -->
<div class="meta-item meta-item--delivery">
<span class="badge text-bg-success"><i class="fa fa-truck" aria-hidden="true" /> Home Delivery</span>
<t t-if="order.delivery_date">
<span class="meta-value">
<t t-esc="day_names[order.delivery_date.weekday()]" /> <t t-esc="order.delivery_date.strftime('%d/%m')" />
</span>
</div>
</t>
<t t-if="order.home_delivery">
<div class="meta-item">
<span class="badge bg-success"><i class="fa fa-truck" /> Home Delivery</span>
</div>
</t>
<t t-if="order.type == 'special'">
<div class="meta-item meta-item--full">
<span class="badge special-order-badge"><i class="fa fa-star" /> Special Order</span>
<span class="badge special-order-badge"><i class="fa fa-star" aria-hidden="true" /> Special Order</span>
</div>
<t t-if="order.start_date">
<div class="meta-item">
@ -182,7 +184,10 @@
</template>
<template id="order_header" name="Order Header">
<div t-att-class="header_class or 'eskaera-order-header'">
<div class="d-flex gap-5 align-items-center mb-4">
<!-- Responsive Bootstrap utilities rather than a CSS override: the
spacing/alignment utilities are !important, so fighting them from
a stylesheet would need !important too. -->
<div class="d-flex flex-column flex-md-row gap-3 gap-md-5 align-items-start align-items-md-center mb-4">
<t t-set="image_to_show" t-value="group_order.image or (group_order.group_ids[0].image_1920 if group_order.group_ids else False)" />
<t t-if="image_to_show">
<img t-att-src="image_data_uri(image_to_show)" alt="Order image" class="order-thumbnail-md" />
@ -210,79 +215,70 @@
<t t-set="header_class" t-value="'eskaera-order-header'" />
</t>
<div class="eskaera-order-header">
<div class="order-info-grid">
<div class="info-item">
<span t-att-class="'info-label'">Consumer Groups</span>
<span class="info-value">
<!-- Name/value pairs: a description list, so assistive tech
announces each value with its label. The 4 → 2 → 1 column
layout lives in layout/header.css. -->
<dl class="order-info-grid">
<dt class="info-label">Consumer Groups</dt>
<dd class="info-value">
<t t-esc="', '.join(group_order.group_ids.mapped('name'))" />
</span>
</div>
</dd>
<t t-if="group_order.cutoff_day">
<div class="info-item">
<span t-att-class="'info-label'">Cutoff Day</span>
<span class="info-value">
<t t-esc="day_names[int(group_order.cutoff_day) % 7]" /> (<t t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')" />)</span>
</div>
<dt class="info-label">Cutoff Day</dt>
<dd class="info-value">
<t t-esc="day_names[int(group_order.cutoff_day) % 7]" /> (<t t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')" />)</dd>
</t>
<t t-if="group_order.pickup_slot_ids">
<div class="info-item">
<span t-att-class="'info-label'">Store Pickup Slots</span>
<span class="info-value">
<dt class="info-label">Store Pickup Slots</dt>
<dd class="info-value">
<t t-foreach="group_order.pickup_slot_ids" t-as="slot">
<div>
<div class="info-value-line">
<t t-esc="day_names[int(slot.weekday) % 7]" />
&#160;
<t t-esc="('%02d:%02d%02d:%02d' % (int(slot.start_hour or 0), int(((slot.start_hour or 0) % 1) * 60), int(slot.end_hour or 0), int(((slot.end_hour or 0) % 1) * 60)))" />
<t t-if="slot.label">&#160;(<t t-esc="slot.label" />)</t>
</div>
</t>
</span>
</div>
</dd>
</t>
<t t-elif="group_order.pickup_day">
<div class="info-item">
<span t-att-class="'info-label'">Store Pickup Day</span>
<span class="info-value">
<dt class="info-label">Store Pickup Day</dt>
<dd class="info-value">
<t t-esc="day_names[int(group_order.pickup_day) % 7]" /> (<t t-esc="group_order.pickup_date.strftime('%d/%m/%Y')" />)
</span>
</div>
</dd>
</t>
<t t-if="group_order.delivery_date and group_order.home_delivery">
<div class="info-item">
<span t-att-class="'info-label'">Home Delivery Day</span>
<span class="info-value">
<t t-esc="day_names[group_order.delivery_date.weekday()]" /> (<t t-esc="group_order.delivery_date.strftime('%d/%m/%Y')" />)</span>
</div>
<dt class="info-label">Home Delivery Day</dt>
<dd class="info-value">
<t t-esc="day_names[group_order.delivery_date.weekday()]" /> (<t t-esc="group_order.delivery_date.strftime('%d/%m/%Y')" />)</dd>
</t>
<t t-if="group_order.start_date">
<div class="info-item">
<span t-att-class="'info-label'">Open From</span>
<span class="info-value">
<dt class="info-label">Open From</dt>
<dd class="info-value">
<t t-esc="group_order.start_date.strftime('%d/%m/%Y')" />
</span>
</div>
</dd>
</t>
<t t-if="group_order.end_date">
<div class="info-item">
<span t-att-class="'info-label'">Open Until</span>
<span class="info-value">
<dt class="info-label">Open Until</dt>
<dd class="info-value">
<t t-esc="group_order.end_date.strftime('%d/%m/%Y')" />
</span>
</div>
</dd>
</t>
</dl>
</div>
</div>
</div>
</div>
<div class="mb-3" id="realtimeSearch-filters">
<div class="mb-3 eskaera-filters" id="realtimeSearch-filters">
<div class="row g-2">
<div class="col-md-7">
<div style="position: relative;">
<input type="text" id="realtime-search-input" class="form-control realtime-search-box search-input-styled" placeholder="Search products..." autocomplete="off" style="padding-right: 40px;" />
<button type="button" id="clear-search-btn" class="btn btn-link" style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%); padding: 0; width: 30px; height: 30px; display: none; color: #6c757d; text-decoration: none; font-size: 1.5rem; line-height: 1;" aria-label="Clear search" title="Clear search">
<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">
@ -299,13 +295,18 @@
<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="background-color: {{ tag['color'] }} !important; border-color: {{ tag['color'] }} !important; color: #ffffff !important;" data-toggle="tag-filter">
<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">
<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>
@ -321,30 +322,32 @@
<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>
<div class="btn-group cart-btn-group gap-0" role="group">
<button type="button" class="btn btn-primary cart-btn-compact" id="save-cart-btn" t-attf-data-order-id="{{ group_order.id }}" data-bs-title="Save Cart" data-bs-toggle="tooltip">
<i class="fa fa-save cart-icon-size" />
<!-- data-bs-title feeds the tooltip, it is not an
accessible name: this icon-only button had none. -->
<button type="button" class="btn btn-primary cart-btn-compact" id="save-cart-btn" t-attf-data-order-id="{{ group_order.id }}" aria-label="Save Cart" title="Save Cart" data-bs-toggle="tooltip">
<i class="fa fa-save cart-icon-size" aria-hidden="true" />
</button>
<t t-if="group_order.home_delivery and delivery_product_id">
<button type="button" class="btn btn-outline-warning cart-btn-compact" id="home-delivery-btn" t-att-aria-label="labels.get('toggle_home_delivery', 'Toggle home delivery')" t-att-data-bs-title="labels.get('home_delivery', 'Home Delivery')" data-bs-toggle="tooltip">
<i class="fa fa-truck cart-icon-size" aria-hidden="true" />
</button>
</t>
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-success cart-btn-compact" aria-label="Proceed to checkout" data-bs-title="Proceed to Checkout" data-bs-toggle="tooltip">
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-success cart-btn-compact" aria-label="Proceed to Checkout" title="Proceed to Checkout" data-bs-toggle="tooltip">
<i class="fa fa-check cart-icon-size" aria-hidden="true" />
</a>
<button type="button" class="btn btn-outline-danger cart-btn-compact" id="clear-cart-btn" t-attf-data-order-id="{{ group_order.id }}" data-bs-title="Clear Cart" data-bs-toggle="tooltip" aria-label="Clear Cart">
<button type="button" class="btn btn-outline-danger cart-btn-compact" id="clear-cart-btn" t-attf-data-order-id="{{ group_order.id }}" title="Clear Cart" data-bs-toggle="tooltip" aria-label="Clear Cart">
<i class="fa fa-trash cart-icon-size" aria-hidden="true" />
</button>
</div>
</div>
<div class="card-body cart-body-lg" id="cart-items-container" t-attf-data-order-id="{{ group_order.id }}" t-attf-data-build-version="{{ module_version }}" aria-labelledby="cart-title" aria-live="polite" aria-relevant="additions removals">
<div class="card-body eskaera-cart-items" id="cart-items-container" t-attf-data-order-id="{{ group_order.id }}" t-attf-data-build-version="{{ module_version }}" aria-labelledby="cart-title" aria-live="polite" aria-relevant="additions removals">
<p class="text-muted">This order's cart is empty</p>
</div>
<div class="card-footer bg-white d-flex justify-content-between align-items-center gap-2">
<button type="button" class="btn btn-outline-danger btn-sm" id="clear-cart-btn-footer" t-attf-data-order-id="{{ group_order.id }}" data-bs-title="Clear Cart" data-bs-toggle="tooltip" aria-label="Clear Cart">
<button type="button" class="btn btn-outline-danger btn-sm" id="clear-cart-btn-footer" t-attf-data-order-id="{{ group_order.id }}" title="Clear Cart" data-bs-toggle="tooltip" aria-label="Clear Cart">
<i class="fa fa-trash me-1" aria-hidden="true" />Clear Cart
</button>
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-success checkout-btn-lg" data-bs-title="Proceed to Checkout" data-bs-toggle="tooltip">
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-success checkout-btn-lg" title="Proceed to Checkout" data-bs-toggle="tooltip">
Proceed to Checkout
</a>
</div>
@ -361,14 +364,14 @@
<t t-if="lazy_loading_enabled and has_next">
<div id="infinite-scroll-container" class="row mt-4">
<div class="col-12 text-center">
<div id="loading-spinner" class="d-none" style="padding: 20px;">
<div id="loading-spinner" class="d-none p-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading more products...</p>
</div>
<button id="load-more-btn" class="btn btn-primary btn-lg d-none" t-attf-data-page="{{ current_page + 1 }}" t-attf-data-order-id="{{ group_order.id }}" t-attf-data-search="{{ search_query }}" t-attf-data-category="{{ selected_category }}" t-attf-data-per-page="{{ per_page }}" aria-label="Load more products" style="display: none;">
<i class="fa fa-download me-2" />Load More Products
<button id="load-more-btn" class="btn btn-primary btn-lg d-none" t-attf-data-page="{{ current_page + 1 }}" t-attf-data-order-id="{{ group_order.id }}" t-attf-data-search="{{ search_query }}" t-attf-data-category="{{ selected_category }}" t-attf-data-per-page="{{ per_page }}" aria-label="Load more products">
<i class="fa fa-download me-2" aria-hidden="true" />Load More Products
</button>
</div>
</div>
@ -396,14 +399,13 @@
var successCount = 0;
tooltipElements.forEach(function(element) {
// Static labels now live in the title attribute, which QWeb
// translates; data-bs-title (which it does not translate) is
// only left where the text comes from the i18n endpoint.
var title = element.getAttribute('data-bs-title');
if (title) {
// Set native title attribute for browser-native tooltip
element.setAttribute('title', title);
successCount++;
console.log('[TOOLTIP] ✅ Set title for', element.id || element.className, ':', title);
} else {
console.warn('[TOOLTIP] ⚠️ No data-bs-title found for element:', element.id || element.className);
}
});
console.log('[TOOLTIP] Tooltip initialization complete:', successCount, 'elements updated');
@ -421,20 +423,23 @@
</t>
</template>
<template id="eskaera_checkout_summary" name="Checkout Order Summary">
<div class="checkout-summary-container">
<!-- The table keeps its width and scrolls inside this container; tabindex
makes that scroll reachable from the keyboard (WCAG 2.1.1). -->
<div class="checkout-summary-container" role="region" tabindex="0" aria-label="Order summary">
<table class="table table-hover checkout-summary-table" id="checkout-summary-table">
<caption class="visually-hidden">Products in this order, with quantity, price and subtotal</caption>
<thead class="table-dark">
<tr>
<th class="col-name">Product</th>
<th class="col-qty text-center">Quantity</th>
<th class="col-price text-right">Price</th>
<th class="col-subtotal text-right">Subtotal</th>
<th scope="col" class="col-name">Product</th>
<th scope="col" class="col-qty text-center">Quantity</th>
<th scope="col" class="col-price text-end">Price</th>
<th scope="col" class="col-subtotal text-end">Subtotal</th>
</tr>
</thead>
<tbody id="checkout-summary-tbody">
<tr id="checkout-empty-row" class="empty-message">
<td colspan="4" class="text-center text-muted py-4">
<i class="fa fa-inbox fa-2x mb-2" />
<i class="fa fa-inbox fa-2x mb-2" aria-hidden="true" />
<p>This order's cart is empty</p>
</td>
</tr>
@ -464,57 +469,62 @@
</div>
<div class="order-info-card card border-0 shadow-sm mb-4">
<div class="card-body">
<!-- Name/value pairs, same treatment as the shop header:
a <label> without a form control to point at labels
nothing, while <dt>/<dd> ties value to name. -->
<div class="row mb-4">
<div class="col-md-4">
<div class="info-item">
<label t-att-class="'info-label'">Cutoff Day</label>
<dl class="info-pair">
<dt class="info-label">Cutoff Day</dt>
<t t-if="group_order.cutoff_day and group_order.cutoff_date">
<span class="info-value">
<dd class="info-value">
<t t-esc="day_names[int(group_order.cutoff_day) % 7]" />
<span class="info-date">(<t t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')" />)</span>
</span>
</dd>
</t>
<t t-else="">
<span t-att-class="'text-muted small'">Not configured</span>
<dd class="info-value text-muted small">Not configured</dd>
</t>
</div>
</dl>
</div>
<div class="col-md-4">
<div class="info-item">
<t t-if="group_order.pickup_slot_ids">
<label t-att-class="'info-label'">Store Pickup Slots</label>
<span class="info-value">
<dl class="info-pair">
<dt class="info-label">Store Pickup Slots</dt>
<dd class="info-value">
<t t-foreach="group_order.pickup_slot_ids" t-as="slot">
<div>
<div class="info-value-line">
<t t-esc="day_names[int(slot.weekday) % 7]" />
&#160;
<t t-esc="('%02d:%02d%02d:%02d' % (int(slot.start_hour or 0), int(((slot.start_hour or 0) % 1) * 60), int(slot.end_hour or 0), int(((slot.end_hour or 0) % 1) * 60)))" />
<t t-if="slot.label">&#160;(<t t-esc="slot.label" />)</t>
</div>
</t>
</span>
</dd>
</dl>
</t>
<t t-elif="group_order.pickup_day and group_order.pickup_date">
<label t-att-class="'info-label'">Store Pickup Day</label>
<span class="info-value" t-attf-data-pickup-date="{{ group_order.pickup_date }}">
<dl class="info-pair">
<dt class="info-label">Store Pickup Day</dt>
<dd class="info-value" t-attf-data-pickup-date="{{ group_order.pickup_date }}">
<t t-esc="day_names[int(group_order.pickup_day) % 7]" />
<span class="info-date">(<t t-esc="group_order.pickup_date.strftime('%d/%m/%Y')" />)</span>
</span>
</dd>
</dl>
</t>
</div>
</div>
<div class="col-md-4">
<div class="info-item">
<t t-if="group_order.delivery_date and group_order.home_delivery">
<label t-att-class="'info-label'">Home Delivery Day</label>
<span class="info-value">
<dl class="info-pair">
<dt class="info-label">Home Delivery Day</dt>
<dd class="info-value">
<t t-esc="day_names[group_order.delivery_date.weekday()]" />
<span class="info-date">(<t t-esc="group_order.delivery_date.strftime('%d/%m/%Y')" />)</span>
</span>
</dd>
</dl>
</t>
</div>
</div>
</div>
<hr class="my-2" />
</div>
</div>
@ -529,11 +539,11 @@
<t t-if="group_order.home_delivery">
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<div class="form-check">
<div class="form-check eskaera-delivery-check">
<input type="checkbox" class="form-check-input" id="home-delivery-checkbox" name="home_delivery" />
<label class="form-check-label fw-bold" for="home-delivery-checkbox">Home Delivery</label>
</div>
<div id="delivery-info-alert" class="alert alert-info mt-3 d-none">
<div id="delivery-info-alert" class="alert alert-info mt-3 d-none eskaera-delivery-notice">
<p class="mb-2">
<i class="fa fa-truck" aria-hidden="true" t-translation="off" />
<t t-if="group_order.delivery_date">
@ -555,7 +565,7 @@
<i class="fa fa-save" aria-hidden="true" t-translation="off" />
<span t-esc="labels.get('save_draft', 'Save Draft')" />
</button>
<a t-attf-href="/eskaera/{{ group_order.slug }}" class="btn btn-outline-secondary btn-lg" aria-label="Back to cart page" data-bs-title="Back to Cart" data-bs-toggle="tooltip">
<a t-attf-href="/eskaera/{{ group_order.slug }}" class="btn btn-outline-secondary btn-lg" aria-label="Back to cart page" title="Back to Cart" data-bs-toggle="tooltip">
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
<span>Back to Cart</span>
</a>
@ -635,7 +645,7 @@
</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" />
<i class="fa fa-image fa-3x text-muted" aria-hidden="true" />
</div>
</t>
<t t-if="product.dynamic_ribbon_id">
@ -649,10 +659,12 @@
<div class="card-body d-flex flex-column">
<h6 class="card-title" t-esc="product.name" />
<t t-if="product.product_tag_ids">
<div class="product-tags mb-2">
<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">
<span class="badge badge-km" t-attf-style="background-color: {{ tag.color }} !important; border-color: {{ tag.color }} !important; color: #ffffff !important;" t-esc="tag.name" />
<!-- 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" />
@ -661,14 +673,14 @@
</div>
</t>
<t t-if="product_supplier_info.get(product.id)">
<p class="product-supplier mb-2">
<p class="product-supplier">
<small>
<t t-esc="product_supplier_info[product.id]" />
</small>
</p>
</t>
<t t-if="product.origin_text">
<p class="product-origin mb-2">
<p class="product-origin">
<small>
<i class="fa fa-map-marker" aria-hidden="true" />
<t t-out="product.origin_text" />
@ -695,7 +707,7 @@
</h6>
<t t-set="tax_incl_base_unit_price" t-value="product_display_info.get(product.id, {}).get('base_unit_price', 0.0)" />
<t t-if="tax_incl_base_unit_price and product.base_unit_name">
<p class="product-unit-price text-muted" style="font-size: 0.85rem; margin-top: 0.25rem; margin-bottom: 0;">
<p class="product-unit-price text-muted">
<t t-esc="'%.2f' % tax_incl_base_unit_price" /> € / <t t-esc="product.base_unit_name" />
</p>
</t>
@ -716,19 +728,24 @@
t-att-data-out-of-stock="'true' if product.is_out_of_stock else 'false'"
>
<div class="qty-control">
<label t-attf-for="qty_{{ product.id }}" class="sr-only">Quantity of <t t-esc="product.name" />
<label t-attf-for="qty_{{ product.id }}" class="visually-hidden">Quantity of <t t-esc="product.name" />
</label>
<button class="qty-decrease" type="button" t-attf-data-product-id="{{ product.id }}" aria-label="Decrease quantity">
<i class="fa fa-minus" />
<i class="fa fa-minus" aria-hidden="true" />
</button>
<input type="number" t-attf-id="qty_{{ product.id }}" class="product-qty" name="quantity" t-attf-value="1" t-attf-min="{{ quantity_step }}" t-attf-step="{{ quantity_step }}" t-att-max="product_max_qty.get(product.id) if product_max_qty else None" t-att-data-max-qty="product_max_qty.get(product.id) if product_max_qty else None" />
<button class="qty-increase" type="button" t-attf-data-product-id="{{ product.id }}" aria-label="Increase quantity">
<i class="fa fa-plus" />
<i class="fa fa-plus" aria-hidden="true" />
</button>
<!-- Out of stock stays focusable and clickable on purpose: the handler
explains why instead of the button silently doing nothing.
aria-disabled announces the state without removing it from the
tab order (a real `disabled` would suppress the click event). -->
<button
class="add-to-cart-btn"
type="button"
t-att-data-out-of-stock="'true' if product.is_out_of_stock else 'false'"
t-att-aria-disabled="'true' if product.is_out_of_stock else None"
t-att-aria-label="out_of_stock_label if product.is_out_of_stock else add_to_cart_label"
t-att-title="out_of_stock_label if product.is_out_of_stock else add_to_cart_label"
>