[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 # CSS architecture
## File layout ## File layout
@ -79,8 +80,10 @@ Other rules:
should do. should do.
- No ID selectors for styling; IDs are for anchors and `aria-*` references. - No ID selectors for styling; IDs are for anchors and `aria-*` references.
- No element selectors outside `base/` (`div.product-card` locks the markup). - 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 - `!important` is allowed in exactly three places, each with a comment saying why: the
override of a core rule you cannot scope — with a comment saying which rule and why. `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 ## 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: Todos los addons incluyen correctamente:
- **Autor**: Criptomart (en `__manifest__.py` y `README.rst`) - **Autor**: Criptomart (en `__manifest__.py` y `README.rst`)
- **Financiador**: Elika Bilbo (en `readme/CREDITS.rst`)
## Logo de CriptoMart ## 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/ 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 ### 2. Actualizar Archivos .po Existentes
no usar msmerge, corrompe el po. Usa polib. 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 ### 3. Traducir Términos Nuevos
Editar los archivos `.po` y completar las traducciones: 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 4. **Usuario con idioma incorrecto**: Verificar preferencias de usuario
5. **Cache**: Reiniciar Odoo: `docker-compose restart odoo` 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 ### Caracteres Especiales
Usar escape correcto en .po: Usar escape correcto en .po:

View file

@ -2249,6 +2249,9 @@ class AplicoopWebsiteSale(WebsiteSale):
"website_sale_aplicoop.eskaera_load_from_history", "website_sale_aplicoop.eskaera_load_from_history",
{ {
"group_order_id": group_order_id, "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 # sessionStorage keys stay keyed by id; the redirect at
# the end of the template needs the public URL instead. # the end of the template needs the public URL instead.
"group_order_url": self._eskaera_url(group_order), "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_info_template": tr(
"Delivery Information: Your order will be delivered at {pickup_day} {pickup_date}" "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"), "important": tr("Important"),
"confirm_order_warning": tr( "confirm_order_warning": tr(
"Once you confirm this order, you will not be able to modify it. Please review carefully before confirming." "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 ## Translated Content
Each `.po` file contains **378 translations** for: Each `.po` file contains **388 translations** for:
- **Selection Field Options** (Days of week, Recurrence periods) - **Selection Field Options** (Days of week, Recurrence periods)
- Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday - 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: 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) 2. Update the `msgstr` values (keep `msgid` unchanged)
3. Save and reload the module in Odoo 3. Save and reload the module in Odoo
4. Translations apply immediately 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: Example entry in a `.po` file:
```po ```po
#. module: website_sale_aplicoop #. 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** - All `.po` files were generated and tested: **40/40 tests passing**
- Translation coverage: **100%** for all supported languages - 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 #. module: website_sale_aplicoop
#. odoo-python #. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0 #: 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" msgid "Back to Cart"
msgstr "Torna al carret" msgstr "Torna al carret"
@ -416,7 +417,9 @@ msgid "Clear search"
msgstr "Neteja la cerca" msgstr "Neteja la cerca"
#. module: website_sale_aplicoop #. module: website_sale_aplicoop
#. odoo-python
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form #: 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" msgid "Close"
msgstr "Tanca" msgstr "Tanca"
@ -1899,6 +1902,7 @@ msgstr "Dissabte"
#. module: website_sale_aplicoop #. module: website_sale_aplicoop
#. odoo-python #. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0 #: 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" msgid "Save Cart"
msgstr "Desa el Carret" msgstr "Desa el Carret"
@ -2298,3 +2302,63 @@ msgstr "Desa l'Esborrany"
#: code:addons/website_sale_aplicoop/models/js_translations.py:0 #: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "Save order as draft" msgid "Save order as draft"
msgstr "Desa la comanda com a esborrany" 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 #. module: website_sale_aplicoop
#. odoo-python #. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0 #: 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" msgid "Back to Cart"
msgstr "Volver al Carrito" msgstr "Volver al Carrito"
@ -418,7 +419,9 @@ msgid "Clear search"
msgstr "Limpiar búsqueda" msgstr "Limpiar búsqueda"
#. module: website_sale_aplicoop #. module: website_sale_aplicoop
#. odoo-python
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form #: 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" msgid "Close"
msgstr "Cerrar" msgstr "Cerrar"
@ -1899,6 +1902,7 @@ msgstr "Sábado"
#. module: website_sale_aplicoop #. module: website_sale_aplicoop
#. odoo-python #. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0 #: 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" msgid "Save Cart"
msgstr "Guardar Carrito" msgstr "Guardar Carrito"
@ -2295,3 +2299,62 @@ msgstr "Grabar pedido"
#: code:addons/website_sale_aplicoop/models/js_translations.py:0 #: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "Save order as draft" msgid "Save order as draft"
msgstr "Grabar pedido" 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 #. module: website_sale_aplicoop
#. odoo-python #. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0 #: 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" msgid "Back to Cart"
msgstr "Sarera Itzuli" msgstr "Sarera Itzuli"
@ -419,7 +420,9 @@ msgid "Clear search"
msgstr "Bilaketa garbitu" msgstr "Bilaketa garbitu"
#. module: website_sale_aplicoop #. module: website_sale_aplicoop
#. odoo-python
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form #: 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" msgid "Close"
msgstr "Itxi" msgstr "Itxi"
@ -1895,6 +1898,7 @@ msgstr "Larunbata"
#. module: website_sale_aplicoop #. module: website_sale_aplicoop
#. odoo-python #. odoo-python
#: code:addons/website_sale_aplicoop/models/js_translations.py:0 #: 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" msgid "Save Cart"
msgstr "Saskia Gorde" msgstr "Saskia Gorde"
@ -2293,3 +2297,64 @@ msgstr "Eskaera gorde"
#: code:addons/website_sale_aplicoop/models/js_translations.py:0 #: code:addons/website_sale_aplicoop/models/js_translations.py:0
msgid "Save order as draft" msgid "Save order as draft"
msgstr "Eskaera gorde" 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")
_("Search products...") _("Search products...")
_("No products found") _("No products found")
# Announced with a count in the search results live region, e.g. "7 products found"
_("products found")
_("Categories") _("Categories")
_("All categories") _("All categories")
# ========================
# Accessible Names
# ========================
# Dismiss button of the floating notification (website_sale.js)
_("Close")
# ======================== # ========================
# Category Labels # Category Labels
# ======================== # ========================

View file

@ -16,7 +16,7 @@ website_sale_aplicoop/static/src/css/
├── base/ ├── base/
│ ├── variables.css ← Variables CSS globales (colores, tipografía, espaciados) │ ├── 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/ ├── layout/
│ ├── pages.css ← Fondos y layouts de páginas (eskaera-page, etc) │ ├── 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) - **variables.css** (~80 líneas)
Colores, tipografía, espaciados, sombras, transiciones, z-index Colores, tipografía, espaciados, sombras, transiciones, z-index
- **utilities.css** (~15 líneas) - **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 ### **layout/** - Estructura Global
- **pages.css** (~70 líneas) - **pages.css** (~70 líneas)

View file

@ -1,34 +1,26 @@
/* filepath: website_sale_aplicoop/static/src/css/base/utilities.css */ /* 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 { /* Scoped to the addon's pages: unscoped this redefined Bootstrap's .text-muted
position: absolute; for the whole frontend, and needed !important to do it. Two classes beat
width: 1px; Bootstrap's one, so the flag is no longer necessary. */
height: 1px; :is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) .text-muted {
padding: 0; color: var(--ac-text-secondary);
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;
} }
/* 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 { .hidden-product {
display: none !important; display: none !important;
} }

View file

@ -1,72 +1,228 @@
/* filepath: website_sale_aplicoop/static/src/css/base/variables.css */ /* filepath: website_sale_aplicoop/static/src/css/base/variables.css */
/** /**
* CSS Custom Properties (Variables) * Design tokens (CSS custom properties)
* Colores, tipografía, espaciados centralizados *
* 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 { :root {
/* ========== COLORS ========== */ /* ========== BRAND ========== */
--primary-color: var(--primary, #007bff); --ac-color-primary: var(--primary, #007bff);
--primary-dark: var(--primary-dark, #0056b3); /* White text on --ac-color-primary is only 3.98:1 (fails AA for body text).
--secondary-color: var(--secondary, #6c757d); Use -strong whenever text sits ON the colour: 7.04:1. */
--success-color: var(--success, #28a745); --ac-color-primary-strong: #0056b3;
--danger-color: #dc3545; --ac-color-secondary: var(--secondary, #6c757d);
--warning-color: #ffc107; --ac-color-accent: #7c3aed; /* white on it: 5.70:1 */
--info-color: #17a2b8; --ac-color-accent-strong: #6d28d9; /* white on it: 7.10:1 */
--light-color: #f8f9fa;
/* 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; --dark-color: #2d3748;
/* Text colors */ --text-primary: var(--ac-text-primary);
--text-primary: #1a202c; --text-secondary: var(--ac-text-secondary);
--text-secondary: #4a5568; --text-muted: var(--ac-text-muted);
--text-muted: #6b7280;
/* Border colors */ --border-light: var(--ac-border);
--border-light: #e2e8f0;
--border-medium: #cbd5e0; --border-medium: #cbd5e0;
--border-dark: #718096; --border-dark: #718096;
/* ========== TYPOGRAPHY ========== */ --font-family-base: var(--ac-font-family);
--font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
--font-weight-light: 300; --font-weight-light: 300;
--font-weight-normal: 400; --font-weight-normal: var(--ac-weight-normal);
--font-weight-medium: 500; --font-weight-medium: var(--ac-weight-medium);
--font-weight-semibold: 600; --font-weight-semibold: var(--ac-weight-semibold);
--font-weight-bold: 700; --font-weight-bold: var(--ac-weight-bold);
--font-weight-extrabold: 800; --font-weight-extrabold: 800;
/* ========== SPACING ========== */ --spacing-xs: var(--ac-space-2xs);
--spacing-xs: 0.25rem; --spacing-sm: var(--ac-space-sm);
--spacing-sm: 0.5rem; --spacing-md: var(--ac-space-md);
--spacing-md: 1rem; --spacing-lg: var(--ac-space-lg);
--spacing-lg: 1.5rem; --spacing-xl: var(--ac-space-xl);
--spacing-xl: 2rem; --spacing-2xl: var(--ac-space-2xl);
--spacing-2xl: 3rem;
/* ========== BORDER RADIUS ========== */ --radius-sm: var(--ac-radius-sm);
--radius-sm: 0.25rem; --radius-md: var(--ac-radius-md);
--radius-md: 0.5rem; --radius-lg: var(--ac-radius-lg);
--radius-lg: 0.75rem; --radius-xl: var(--ac-radius-xl);
--radius-xl: 1rem;
/* ========== SHADOWS ========== */ --shadow-sm: var(--ac-shadow-sm);
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08); --shadow-md: var(--ac-shadow-md);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1); --shadow-lg: var(--ac-shadow-lg);
--shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.12);
/* ========== TRANSITIONS ========== */
--transition-fast: 200ms ease; --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; --transition-slow: 500ms ease;
/* ========== Z-INDEX ========== */ --z-dropdown: var(--ac-z-dropdown);
--z-dropdown: 1000; --z-sticky: var(--ac-z-sticky);
--z-sticky: 1020; --z-fixed: var(--ac-z-fixed);
--z-fixed: 1030; --z-modal: var(--ac-z-modal);
--z-modal: 1040; --z-popover: var(--ac-z-popover);
--z-popover: 1050; --z-tooltip: var(--ac-z-tooltip);
--z-tooltip: 1060; --z-notification: var(--ac-z-notification);
--z-notification: 9999;
} }

View file

@ -1,77 +1,68 @@
/* filepath: website_sale_aplicoop/static/src/css/components/alerts.css */ /* 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 { :is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
background-color: #e7f3ff; .alert-warning {
border-left: 4px solid var(--primary-color); --alert-bg: var(--ac-surface-warning);
color: #004085; --alert-border-color: var(--ac-border-warning);
padding: 1rem; --alert-color: var(--ac-text-warning);
border-radius: 0.375rem; margin-bottom: var(--ac-space-xl);
padding: var(--ac-space-md);
border-radius: var(--ac-radius-md);
} }
.group-order-alert.info { :is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
background-color: #d1ecf1; .alert-warning
border-left-color: #17a2b8; i {
color: #0c5460; margin-right: var(--ac-space-xs);
font-size: var(--ac-text-md);
} }
.group-order-alert.warning { :is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
background-color: #fff3cd; .alert-warning
border-left-color: #ffc107; strong {
color: #856404; font-weight: var(--ac-weight-bold);
} }
.alert-warning { .eskaera-delivery-notice {
background-color: #fef3c7; margin-top: var(--ac-space-md);
border-color: #fcd34d; border-left: 4px solid var(--ac-color-info);
color: #92400e;
margin-bottom: 2rem;
border-radius: 0.5rem;
padding: 1.25rem;
} }
.alert-warning i { .eskaera-delivery-notice .fa-truck {
margin-right: 0.75rem; margin-right: var(--ac-space-sm);
font-size: 1.1rem; color: var(--ac-color-info);
} }
.alert-warning strong { /* ---------- Toast (added to cart) ---------- */
font-weight: 700;
}
#delivery-info-alert {
margin-top: 1rem;
border-left: 4px solid #0dcaf0;
}
#delivery-info-alert .fa-truck {
margin-right: 0.5rem;
color: #0dcaf0;
}
/* Toast Notification Animation */
.toast-notification { .toast-notification {
position: fixed; position: fixed;
bottom: 30px; inset: auto var(--ac-space-lg) var(--ac-space-lg) auto;
right: 30px; z-index: var(--ac-z-notification);
background-color: #28a745;
color: white;
padding: 1rem 1.5rem;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: var(--ac-space-xs);
font-weight: 500; max-width: 25rem;
z-index: 9999; 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; opacity: 0;
transform: translateY(20px); transform: translateY(20px);
transition: opacity 0.3s ease, transform 0.3s ease; transition: opacity var(--ac-transition-normal), transform var(--ac-transition-normal);
max-width: 400px;
word-break: break-word;
} }
.toast-notification.show { .toast-notification.show {
@ -80,6 +71,41 @@
} }
.toast-notification i { .toast-notification i {
font-size: 1.2rem; min-width: 1.25rem;
min-width: 20px; 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 */ /* 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 { :is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
background-color: var(--primary-color); :is(.btn-primary, .btn-success) {
border-color: var(--primary-color); font-weight: var(--ac-weight-semibold);
color: white;
} }
.btn-add-to-cart:focus { /* ---------- Checkout actions ---------- */
outline: 3px solid var(--primary-color);
outline-offset: 2px;
}
.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 { .checkout-actions {
margin-top: 2rem; margin-top: var(--ac-space-xl);
} }
.checkout-actions .btn { .checkout-actions .btn {
font-size: 1.1rem; --btn-border-radius: var(--ac-radius-md);
font-weight: 600; --btn-font-size: var(--ac-text-md);
padding: 0.75rem 1.5rem; --btn-font-weight: var(--ac-weight-semibold);
border-radius: 0.5rem; --btn-padding-y: var(--ac-space-xs);
transition: all 0.3s ease; --btn-padding-x: var(--ac-space-lg);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; 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-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);
} }
.checkout-actions .btn i { .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,
.save-order-btn-styled,
.checkout-btn-lg { .checkout-btn-lg {
padding: var(--ac-space-sm) var(--ac-space-md);
font-size: var(--ac-text-md);
white-space: nowrap; white-space: nowrap;
font-size: 1.1rem;
padding: 0.6rem 1.2rem;
} }
.save-icon-size { /* ---------- States ---------- */
font-size: 1.2rem;
@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 */ /* 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 { .cart-sticky-position {
top: 20px; top: var(--ac-space-md);
} }
.cart-header { /* Cap the scroll area against the viewport, not a fixed pixel height, so it
background-color: var(--primary-color); still fits when text is enlarged. */
color: white; .cart-items,
padding: 0.25rem; .eskaera-cart-items {
border-radius: 0.25rem 0.25rem 0 0; max-height: min(60vh, 25rem);
}
.cart-header h5 {
color: white;
margin: 0;
}
.cart-header .btn {
color: white;
}
.cart-items {
max-height: 400px;
overflow-y: auto; overflow-y: auto;
} }
#cart-items-container { .eskaera-cart-items {
padding: 0.2rem 0.4rem; 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; margin-bottom: 0;
} }
#cart-items-container p.text-muted { .eskaera-cart-items p.text-muted {
margin: 0.4rem 0; margin: var(--ac-space-2xs) 0;
} }
.list-group-item { .cart-empty-message {
padding: 0.4rem 0.6rem; font-size: var(--ac-text-base);
border-bottom: 1px solid #e0e0e0;
} }
.list-group-item.d-flex { /* ---------- Cart lines ---------- */
gap: 0.5rem;
.eskaera-cart-items .list-group-item {
display: flex;
flex-direction: column; 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 { .eskaera-cart-items .list-group-item h6 {
margin: 0; margin: 0 0 var(--ac-space-3xs);
margin-bottom: 0.2rem; font-size: var(--ac-text-base);
font-weight: 600; font-weight: var(--ac-weight-semibold);
line-height: var(--ac-leading-snug);
overflow-wrap: break-word;
word-break: 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; display: block;
font-size: 0.75rem; font-size: var(--ac-text-xs);
color: #6c757d; color: var(--ac-text-muted);
} }
.list-group-item .d-flex { .eskaera-cart-items .list-group-item .d-flex {
min-width: auto;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 0.5rem; gap: var(--ac-space-sm);
flex-wrap: wrap; flex-wrap: wrap;
} }
.list-group-item .remove-from-cart { /* Line subtotal. The previous #667eea was 3.42:1 at this size — under AA. */
flex-shrink: 0; .eskaera-cart-items .list-group-item strong {
min-width: 24px; min-width: 3.75rem;
min-height: 24px; 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; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0.15rem 0.25rem; flex-shrink: 0;
font-size: 0.7rem; /* 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; cursor: pointer;
background-color: #dc3545; transition: background-color var(--ac-transition-fast), transform var(--ac-transition-fast);
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;
} }
.cart-total { .cart-total {
padding: 1rem; padding: var(--ac-space-md);
background-color: #f8f9fa; border-top: 2px solid var(--ac-color-primary);
font-size: 1.5rem; background-color: var(--ac-surface-sunken);
font-weight: 700; font-size: var(--ac-text-lg);
border-top: 2px solid var(--primary-color); font-weight: var(--ac-weight-bold);
text-align: right; text-align: right;
color: var(--primary-color); color: var(--ac-color-primary-strong);
} }
.cart-item-remove { /* ---------- Header actions ---------- */
cursor: pointer;
color: #dc3545;
font-size: 0.7rem;
}
.cart-item-remove:hover { .card-header .btn-group,
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,
.cart-btn-group { .cart-btn-group {
flex-shrink: 0;
flex-wrap: nowrap; flex-wrap: nowrap;
} }
.cart-header-btn { /* These four icon buttons were 0.5rem type with 0.1rem padding roughly an
padding: 0.25rem; 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 { .cart-icon-size {
font-size: 0.9rem; font-size: var(--ac-text-base);
}
.cart-body-text {
font-size: 1.1rem;
}
.cart-body-lg {
font-size: 1.1rem;
}
.cart-title-lg {
font-size: 1.5rem;
} }
.cart-title-sm { .cart-title-sm {
font-size: 1.1rem; font-size: var(--ac-text-md);
} }
.cart-btn-compact { /* ---------- States ---------- */
padding: 0.1rem;
min-width: auto; @media (hover: hover) and (pointer: fine) {
font-size: 0.5rem; .remove-from-cart:hover {
line-height: 0.5; transform: scale(1.05);
height: auto; }
} }
.cart-sticky-position { .remove-from-cart:focus-visible,
top: 20px; .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 */ /* 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 { :is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) label {
font-weight: 600; font-weight: var(--ac-weight-semibold);
color: #2d3748; color: var(--ac-text-secondary);
} }
.form-control, :is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
.form-select { :is(.form-control, .form-select, input[type="text"], input[type="number"], select) {
border-color: #cbd5e0; /* WCAG 1.4.11: an input's border is what identifies it, so ≥ 3:1. */
font-size: 1rem; 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, /* One focus treatment for every control on these pages, keyboard only. */
.form-select:focus { :is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
border-color: var(--primary-color); :is(.form-control, .form-select, .form-check-input, input, select, textarea):focus-visible {
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); 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"], /* Bootstrap draws its own ring through a box-shadow on :focus; keep it in
input[type="text"], token colours so mouse focus still reads, without a second outline. */
select { :is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page)
background-color: #ffffff; :is(.form-control, .form-select):focus {
color: #212529; border-color: var(--ac-color-primary);
border: 1px solid #ced4da; box-shadow: 0 0 0 0.2rem rgb(0 123 255 / 20%);
} }
input[type="number"]:focus, /* ---------- Search and category filter ---------- */
input[type="text"]:focus,
select:focus { .eskaera-filters :is(.form-control, .form-select) {
outline: 2px solid #667eea; height: auto;
outline-offset: 0; padding: var(--ac-space-xs) var(--ac-space-sm);
border-color: var(--primary-color); 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; display: flex;
align-items: center; align-items: center;
padding: 1rem 1.25rem; gap: var(--ac-space-sm);
margin-left: 0; padding: var(--ac-space-md);
} }
.form-check-input { .eskaera-delivery-check .form-check-input {
margin-right: 0.75rem;
flex-shrink: 0; 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; width: 1.5rem;
height: 1.5rem; height: 1.5rem;
margin: 0;
border: 2px solid var(--ac-color-primary);
border-radius: var(--ac-radius-sm);
cursor: pointer; cursor: pointer;
border: 2px solid #0d6efd;
border-radius: 0.25rem;
margin-right: 0.75rem;
} }
#home-delivery-checkbox:checked { .eskaera-delivery-check .form-check-input:checked {
background-color: #0d6efd; border-color: var(--ac-color-primary);
border-color: #0d6efd; background-color: var(--ac-color-primary);
} }
#home-delivery-checkbox:focus { .eskaera-delivery-check .form-check-label {
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;
display: flex; display: flex;
align-items: center; 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 { /* ---------- Touch ---------- */
font-size: 0.85rem;
@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 */ /* 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 { .eskaera-order-card-link {
text-decoration: none;
color: inherit;
display: block; display: block;
width: 100%; width: 100%;
color: inherit;
text-decoration: none;
} }
.eskaera-order-card { .eskaera-order-card {
position: relative; 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; display: flex;
flex-direction: column; 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%; 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; animation: slideInUp 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both;
} }
@ -33,371 +46,281 @@
opacity: 0; opacity: 0;
transform: translateY(30px); transform: translateY(30px);
} }
100% { 100% {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
} }
/* Accent bar along the top edge. */
.eskaera-order-card::before { .eskaera-order-card::before {
content: ""; content: "";
position: absolute; position: absolute;
left: 0; inset: 0 0 auto;
right: 0;
top: 0;
height: 6px; 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 { /* ---------- Header ---------- */
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);
}
.card-header-top { .card-header-top {
display: flex; display: flex;
gap: 0.6rem; gap: var(--ac-space-sm);
align-items: flex-start; align-items: flex-start;
margin-bottom: 0.3rem; margin-bottom: var(--ac-space-2xs);
height: 100px;
overflow: hidden;
} }
.card-header-top > div:last-child { .card-header-top > div:last-child {
text-align: left;
flex: 1; flex: 1;
text-align: left;
} }
.card-header-top .card-title { .card-header-top .card-title {
margin: 0; margin: 0;
} }
.card-badges .badge { .eskaera-order-card .card-body {
display: inline-flex; display: flex;
align-items: center; flex-direction: column;
gap: 0.2rem; 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 { .card-meta-compact {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: 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%; 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 { .meta-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5rem 1rem; gap: var(--ac-space-sm) var(--ac-space-md);
justify-content: center;
align-items: center; align-items: center;
justify-content: center;
} }
.meta-grid .meta-item { .meta-grid .meta-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.35rem; gap: var(--ac-space-2xs);
font-size: 0.8rem; font-size: var(--ac-text-xs);
} }
.meta-grid .meta-label { .meta-grid .meta-label {
font-weight: 600;
color: #374151;
min-width: auto; min-width: auto;
font-weight: var(--ac-weight-semibold);
color: var(--ac-text-secondary);
} }
.meta-grid .meta-value { .meta-grid .meta-value {
color: #6b7280; color: var(--ac-text-muted);
} }
.meta-grid .badge { .meta-grid .badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.25rem; gap: var(--ac-space-2xs);
} padding: var(--ac-space-2xs) var(--ac-space-xs);
font-size: var(--ac-text-2xs);
.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);
} }
.meta-item--full { .meta-item--full {
width: 100%; width: 100%;
justify-content: center; 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 */ /* 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 { .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; display: flex;
flex-direction: column; flex-direction: column;
padding: 0.5rem;
height: 100%; 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; overflow: hidden;
outline: none; transition: box-shadow var(--ac-transition-normal), transform var(--ac-transition-normal);
} }
.product-card:hover, /* Bootstrap 5.3 drives .card through unprefixed custom properties, so the inner
.product-card:focus-within { card can be flattened without !important and without touching the markup. */
transform: translateY(-4px) scale(1.01); .product-card > .card {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.13); --card-bg: transparent;
outline: 2px solid var(--primary-color, #007bff); --card-border-width: 0;
outline-offset: 2px; --card-border-radius: 0;
display: flex;
flex-direction: column;
height: 100%;
} }
.product-card .product-image { /* ---------- Media ---------- */
height: 120px;
width: 100%;
object-fit: cover;
border-radius: 8px 8px 0 0;
background: #f3f3f3;
display: block;
}
.product-img-cover { .product-card .product-image,
height: 120px; .product-img-cover,
width: 100%; .product-img-fixed,
object-fit: cover; .product-img-placeholder {
border-radius: 8px 8px 0 0;
box-shadow: 0 2px 8px rgba(40, 39, 39, 0.09);
background: #f3f3f3;
display: block; display: block;
flex-shrink: 0; 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 { .product-card .card-body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; /* Sized to its own content, not stretched: the add-to-cart form below is
flex-grow: 1; what aligns across the row (see quantity-control.css), so the text/price
padding: 0.4rem 0.55rem 0.5rem; block stays compact instead of ballooning to match a taller sibling. */
position: relative; gap: var(--ac-space-3xs);
background: linear-gradient(135deg, rgba(0, 123, 255, 0.07) 0%, rgba(0, 123, 255, 0.04) 100%); padding: var(--ac-space-2xs) var(--ac-space-xs) var(--ac-space-2xs);
transition: background 0.3s; background: linear-gradient(135deg, var(--ac-surface-tint) 0%, transparent 100%);
} transition: background var(--ac-transition-normal);
.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%
);
} }
.product-card .card-title { .product-card .card-title {
flex-grow: 1; display: -webkit-box;
margin: 0 0 0.1rem 0; -webkit-box-orient: vertical;
min-height: auto; -webkit-line-clamp: 2;
display: block; line-clamp: 2;
word-wrap: break-word; /* 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; overflow-wrap: break-word;
font-size: 1rem !important; font-size: var(--ac-text-base);
line-height: 1.15; font-weight: var(--ac-weight-semibold);
text-align: center; line-height: var(--ac-leading-tight);
font-weight: 600;
color: #1a202c;
letter-spacing: 0.01em; letter-spacing: 0.01em;
text-align: center;
color: var(--ac-text-primary);
} }
.product-card .card-text { .product-card .card-text {
margin-bottom: 0.08rem; margin-bottom: 0;
font-size: var(--ac-text-sm);
text-align: center; text-align: center;
font-size: 0.95rem;
} }
.product-card .card-text strong { /* ---------- Tags ---------- */
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;
}
.product-tags { .product-tags {
text-align: center;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.12rem; gap: var(--ac-space-3xs);
justify-content: center; justify-content: center;
font-weight: 400; margin: 0;
font-size: 1rem !important;
margin: 0 0 0.08rem 0;
padding: 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 { .badge-km {
background-color: var(--primary-color, #007bff) !important; --badge-padding-x: var(--ac-space-2xs);
color: #fff !important; --badge-padding-y: var(--ac-space-3xs);
font-weight: 600 !important; --badge-font-size: var(--ac-text-2xs);
padding: 0.18rem 0.32rem !important; --badge-font-weight: var(--ac-weight-semibold);
font-size: 0.68rem !important; --badge-color: var(--ac-text-on-fill);
border-radius: 0.22rem; --badge-border-radius: var(--ac-radius-sm);
display: inline-block; border: 1px solid var(--ac-tag-color, var(--ac-color-primary-strong));
border: 1px solid #007bff; background-color: var(--ac-tag-color, var(--ac-color-primary-strong));
white-space: nowrap; 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; text-align: center;
margin-bottom: 0.35rem; color: var(--ac-text-secondary);
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;
} }
.card-body p.card-text strong { .product-card .product-origin .fa {
display: inline; margin-right: var(--ac-space-3xs);
font-size: 1.18rem !important; }
color: var(--primary-color, #007bff);
margin-bottom: 0; /* ---------- 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; white-space: nowrap;
color: var(--ac-color-primary-strong);
} }
.product-img-fixed { .product-card .product-unit-price {
object-fit: cover; margin: 0;
height: 120px; font-size: var(--ac-text-xs);
width: 100%; text-align: center;
border-radius: 8px 8px 0 0; color: var(--ac-text-secondary);
background: #f3f3f3;
display: block;
} }
.product-img-placeholder { /* ---------- States ---------- */
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;
}
/* Responsive: tablet (≤768px) — compresión moderada */ /* Hover effects only where a pointer can actually hover on touch they stick
@media (max-width: 768px) { after a tap and read as a stuck selection. */
.product-card .product-image, @media (hover: hover) and (pointer: fine) {
.product-img-cover, .product-card:hover {
.product-img-fixed, transform: translateY(-4px);
.product-img-placeholder { box-shadow: var(--ac-shadow-lg);
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 { .product-card .card-body {
padding: 0.35rem 0.45rem 0.4rem; transition: none;
} }
.card-body p.card-text {
margin-bottom: 0.25rem;
min-height: 1.4rem;
}
}
/* Responsive: móvil (≤480px) — compresión máxima */ .product-card:hover {
@media (max-width: 480px) { transform: none;
.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;
} }
} }
/* 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 */ /* 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 { .add-to-cart-form {
width: 100%;
gap: 0;
padding: 0.5rem;
margin-top: 0.25rem;
background-color: #f8f9fa;
border-radius: 4px;
display: flex; display: flex;
border: 1px solid #e9ecef;
align-content: center;
justify-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%; width: 100%;
gap: 0; }
padding: 0;
/* ---------- Stepper ---------- */
.qty-control .qty-decrease,
.qty-control .qty-increase {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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 { .add-to-cart-form .product-qty {
flex: 1 1 auto; /* Fills the middle grid column; min-width 0 lets it shrink on a narrow card
text-align: center; instead of pushing the "+" button out of the row. */
font-weight: 700; width: 100%;
font-size: 1rem; min-width: 0;
padding: 0.3rem; height: var(--ac-control-size);
min-width: 32px; padding: 0 var(--ac-space-2xs);
max-width: 70px; /* Collapse the shared edge with the two stepper buttons. */
border: 2px solid #dee2e6; border: var(--ac-control-border-width) solid var(--ac-border-strong);
border-inline-width: 0;
border-radius: 0; border-radius: 0;
background-color: #ffffff; font-size: var(--ac-text-base);
transition: all 0.2s ease; font-weight: var(--ac-weight-bold);
text-align: center;
color: var(--ac-text-primary);
background-color: var(--ac-surface);
appearance: textfield;
-moz-appearance: textfield; -moz-appearance: textfield;
height: 36px; transition: border-color var(--ac-transition-fast), box-shadow var(--ac-transition-fast);
line-height: 30px;
} }
.add-to-cart-form .product-qty::-webkit-outer-spin-button, .add-to-cart-form .product-qty::-webkit-outer-spin-button,
.add-to-cart-form .product-qty::-webkit-inner-spin-button { .add-to-cart-form .product-qty::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0; margin: 0;
-webkit-appearance: none;
} }
.add-to-cart-form .product-qty:focus { /* ---------- Add to cart ---------- */
outline: none;
border-color: var(--primary-color, #007bff);
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
background-color: #ffffff;
}
/* Contenedor de control de cantidad */ .add-to-cart-form .add-to-cart-btn {
.qty-control { /* Full width on its own row, under the stepper. */
grid-column: 1 / -1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0;
padding: 0;
width: 100%; width: 100%;
height: 44px; height: var(--ac-control-size);
} margin-top: var(--ac-space-3xs);
/* Botones de cantidad + y - */
.qty-control .qty-decrease,
.qty-control .qty-increase {
min-width: 36px;
width: 36px;
height: 36px;
padding: 0; padding: 0;
gap: 0; border: none;
font-size: 1rem; border-radius: var(--ac-radius-sm);
display: flex; font-size: var(--ac-text-sm);
align-items: center; color: var(--ac-text-on-fill);
justify-content: center; background-color: var(--ac-color-accent);
border: 2px solid #dee2e6;
background-color: #ffffff;
color: #495057;
transition: all 0.2s ease;
cursor: pointer; cursor: pointer;
flex-shrink: 0; transition: background-color var(--ac-transition-fast), transform var(--ac-transition-fast);
} }
.qty-control .qty-decrease:hover, .add-to-cart-form .add-to-cart-btn .fa {
.qty-control .qty-increase:hover { font-size: var(--ac-text-md);
border-color: var(--primary-color, #007bff); }
color: var(--primary-color, #007bff);
background-color: #f8f9fa; /* 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(--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-decrease:active,
.qty-control .qty-increase:active { .qty-control .qty-increase:active {
background-color: #e9ecef; background-color: var(--ac-surface-sunken);
}
.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);
} }
.add-to-cart-form .add-to-cart-btn:active { .add-to-cart-form .add-to-cart-btn:active {
background-color: #5b21b6 !important; background-color: var(--ac-color-accent-strong);
transform: scale(0.98); transform: scale(0.98);
} }
.add-to-cart-form .add-to-cart-btn i { .qty-control .qty-decrease:focus-visible,
font-size: 1rem; .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) */ .add-to-cart-form .product-qty:focus-visible {
@media (min-width: 1600px) { border-color: var(--ac-color-primary);
.add-to-cart-form { }
padding: 0.35rem;
gap: 0.08rem;
}
.add-to-cart-form .input-group { /* ---------- Input device ---------- */
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;
}
/* 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-decrease,
.qty-control .qty-increase { .qty-control .qty-increase {
width: 32px; width: var(--ac-touch-target);
height: 32px; height: var(--ac-touch-target);
font-size: 0.9rem;
} }
.add-to-cart-form .product-qty,
.add-to-cart-form .add-to-cart-btn { .add-to-cart-form .add-to-cart-btn {
height: 32px; height: var(--ac-touch-target);
width: 32px;
min-width: 32px;
max-width: 32px;
font-size: 0.75rem;
} }
} }
/* Pantallas grandes: 1400-1599px (5 columnas) */ @media (prefers-reduced-motion: reduce) {
@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;
}
.qty-control .qty-decrease, .qty-control .qty-decrease,
.qty-control .qty-increase { .qty-control .qty-increase,
width: 32px; .add-to-cart-form .product-qty,
height: 32px; .add-to-cart-form .add-to-cart-btn {
font-size: 0.9rem; transition: none;
} }
.add-to-cart-form .add-to-cart-btn { .add-to-cart-form .add-to-cart-btn:hover,
height: 32px; .add-to-cart-form .add-to-cart-btn:active {
width: 32px; transform: none;
min-width: 32px;
font-size: 0.75rem;
}
}
/* 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;
} }
} }

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. * Toggle buttons above the product grid. Three states:
* Badges toggle between secondary (unselected) and primary (selected) 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 { .tag-filter-badges {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center;
align-items: center; align-items: center;
gap: 0.5rem; justify-content: center;
padding: 0.75rem 0; gap: var(--ac-space-sm);
padding: var(--ac-space-xs) 0;
} }
/* Individual tag filter badge button */
.tag-filter-badge { .tag-filter-badge {
cursor: pointer; display: inline-flex;
padding: 0.5rem 0.75rem; align-items: center;
border-radius: 0.375rem; /* 24px is the AA floor (WCAG 2.2 §2.5.8); coarse pointers get 44 below. */
font-size: 0.875rem; min-height: 1.5rem;
font-weight: 500; 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; white-space: nowrap;
transition: all 0.2s ease-in-out; cursor: pointer;
user-select: none; user-select: none;
display: inline-block; transition: background-color var(--ac-transition-fast), border-color var(--ac-transition-fast),
border: 1px solid; box-shadow var(--ac-transition-fast), transform var(--ac-transition-fast);
color: #ffffff !important;
} }
/* Tags sin color definido en Odoo: usar color secundario del tema */ /* Tags with no colour of their own fall back to the theme's secondary. The old
.tag-filter-badge.tag-use-theme-color { var(--bs-secondary) never resolved: Odoo emits Bootstrap's custom properties
background-color: var(--bs-secondary, #6c757d); unprefixed, so that lookup always landed on its hardcoded fallback. */
border-color: var(--bs-secondary, #6c757d); .tag-filter-badge.tag-use-theme-color,
}
/* Product card tags (badge-km) sin color definido: usar color del tema */
.badge-km.tag-use-theme-color { .badge-km.tag-use-theme-color {
background-color: var(--bs-secondary, #6c757d); border-color: var(--ac-color-secondary);
border-color: var(--bs-secondary, #6c757d); background-color: var(--ac-color-secondary);
color: #ffffff !important; color: var(--ac-text-on-fill);
} }
.tag-filter-badge:hover { /* Active filter: the strong primary, which also carries white text at 7.04:1. */
transform: translateY(-2px); .tag-filter-badge.is-selected {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); border-color: var(--ac-color-primary-strong);
filter: brightness(0.9); 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);
} }
/* Counter text inside badge */
.tag-filter-badge .tag-count { .tag-filter-badge .tag-count {
font-weight: 600; margin-left: var(--ac-space-2xs);
margin-left: 0.25rem; font-weight: var(--ac-weight-semibold);
font-variant-numeric: tabular-nums;
} }
/* Responsive adjustments */ /* ---------- States ---------- */
@media (max-width: 768px) {
.tag-filter-badges {
gap: 0.375rem;
}
@media (hover: hover) and (pointer: fine) {
.tag-filter-badge:hover {
transform: translateY(-2px);
box-shadow: var(--ac-shadow-md);
filter: brightness(0.9);
}
}
.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 { .tag-filter-badge {
padding: 0.375rem 0.625rem; min-height: var(--ac-touch-target);
font-size: 0.8125rem; 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 */ /* Unified header for both shop and checkout pages */
.eskaera-order-header, .eskaera-order-header,
.checkout-header { .checkout-header {
background: white; padding: clamp(var(--ac-space-md), 2vw, var(--ac-space-xl));
border: 1px solid #e0e0e0; border: 1px solid var(--ac-border-subtle);
border-radius: 8px; border-radius: var(--ac-radius-md);
padding: 2rem; background: var(--ac-surface);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); box-shadow: var(--ac-shadow-sm);
} }
.eskaera-order-header h1, .eskaera-order-header h1,
.checkout-header h1 { .checkout-header h1 {
color: #2d3748; margin-bottom: var(--ac-space-md);
font-weight: 600; padding-bottom: var(--ac-space-md);
margin-bottom: 1.5rem; border-bottom: 3px solid var(--ac-color-primary);
border-bottom: 3px solid var(--primary-color, #007bff); font-size: var(--ac-text-2xl);
padding-bottom: 1rem; font-weight: var(--ac-weight-bold);
color: var(--ac-text-primary);
} }
.order-info-grid { .checkout-title {
display: grid; margin-bottom: var(--ac-space-md);
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); font-size: var(--ac-text-2xl);
gap: 2rem; font-weight: var(--ac-weight-bold);
} color: var(--ac-text-primary);
.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-order-name { .checkout-order-name {
font-size: 1.5rem; font-size: var(--ac-text-lg);
color: #2d3748; font-weight: var(--ac-weight-semibold);
font-weight: 600; color: var(--ac-text-secondary);
} }
/* Info value styling */ /* ==========================================================================
.info-value { Order info: label / value pairs
font-size: 1.1rem; 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 { .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 */ /* 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:has(.eskaera-page),
body { body:has(.eskaera-shop-page),
background-color: transparent !important; body:has(.eskaera-generic-page),
background: transparent !important; 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-page,
.eskaera-shop-page, .eskaera-shop-page,
.eskaera-generic-page, .eskaera-generic-page,
.eskaera-checkout-page { .eskaera-checkout-page {
min-height: 100vh;
position: relative; 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-page,
.eskaera-generic-page { .eskaera-generic-page {
background: linear-gradient( background: linear-gradient(
180deg, 180deg,
color-mix(in srgb, var(--primary-color) 10%, white), color-mix(in srgb, var(--ac-color-primary) 10%, white),
color-mix(in srgb, var(--primary-color) 70%, black) color-mix(in srgb, var(--ac-color-primary) 70%, black)
) !important; );
} }
.eskaera-shop-page { .eskaera-shop-page {
background: linear-gradient( background: linear-gradient(
135deg, 135deg,
color-mix(in srgb, var(--primary-color) 10%, white), color-mix(in srgb, var(--ac-color-primary) 10%, white),
color-mix(in srgb, var(--primary-color) 10%, rgb(135, 135, 135)) color-mix(in srgb, var(--ac-color-primary) 10%, rgb(135, 135, 135))
) !important; );
} }
.eskaera-checkout-page { .eskaera-checkout-page {
background: linear-gradient( background: linear-gradient(
-135deg, -135deg,
color-mix(in srgb, var(--primary-color) 0%, white), white,
color-mix(in srgb, var(--primary-color) 60%, black) color-mix(in srgb, var(--ac-color-primary) 60%, black)
) !important; );
} }
.eskaera-page::before, /* ---------- Decorative glow overlay ---------- */
.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%
);
}
.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%
);
}
.eskaera-page::before, .eskaera-page::before,
.eskaera-shop-page::before, .eskaera-shop-page::before,
@ -108,28 +69,61 @@ body.website_published .eskaera-checkout-page {
.eskaera-checkout-page::before { .eskaera-checkout-page::before {
content: ""; content: "";
position: absolute; position: absolute;
top: 0; inset: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 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-page > .container,
.eskaera-shop-page > .container, .eskaera-shop-page > .container,
.eskaera-generic-page > div, .eskaera-generic-page > div,
.eskaera-checkout-page > .container { .eskaera-checkout-page > .container {
position: relative; position: relative;
z-index: 1; 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 */ /* filepath: website_sale_aplicoop/static/src/css/layout/responsive.css */
/** /**
* Responsive design breakpoints and adjustments * Responsive adjustments do not add to it.
* All media queries centralized here for easier maintenance *
* NOTE: products-grid.css has its own breakpoints and should NOT be overridden here * 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) { @media (max-width: 768px) {
/* Header (shared between eskaera and checkout) */ /* Header type and the info grid scale from layout/header.css; the title row
.eskaera-order-header, stacks through Bootstrap's flex-column/flex-md-row utilities. Order cards,
.checkout-header { order list, product card and quantity control all size themselves from
padding: 1rem; their own files. */
}
.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;
}
.order-thumbnail-md { .order-thumbnail-md {
max-width: 100%; max-width: 100%;
@ -105,413 +23,54 @@
height: auto; 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 { .order-thumbnail-sm {
width: 70px; width: 4.375rem;
height: 70px; height: 4.375rem;
}
.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;
} }
.group-order-header { .group-order-header {
padding: 1rem 0; padding: var(--ac-space-md) 0;
margin-bottom: 1rem; margin-bottom: var(--ac-space-md);
}
.product-card {
margin-bottom: 1rem;
}
.product-card .product-image {
height: 150px;
}
.checkout-container {
padding: 1rem;
} }
} }
@media (max-width: 576px) { @media (max-width: 576px) {
/* Cart items */ /* Checkout type scales fluidly from sections/checkout.css; only the table's
.list-group-item.d-flex { density genuinely changes here, and the container's padding with it. */
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;
}
.toast-notification { .toast-notification {
bottom: 20px; inset: auto var(--ac-space-md) var(--ac-space-md);
right: 20px;
left: 20px;
max-width: none; max-width: none;
} }
#save-cart-btn,
#reload-cart-btn {
padding: 0.3rem 0.4rem;
font-size: 0.8rem;
}
.card-header .d-flex { .card-header .d-flex {
flex-wrap: wrap; 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 { .order-thumbnail-sm {
width: 60px; width: 3.75rem;
height: 60px; height: 3.75rem;
} }
.order-thumbnail-md { .order-thumbnail-md {
width: 80px; width: 5rem;
height: 80px; height: 5rem;
}
/* Order list */
.eskaera-orders,
.eskaera-orders-grid {
grid-template-columns: 1fr !important;
gap: 1rem;
} }
.eskaera-empty-state { .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 { .checkout-summary-container {
padding: 0.75rem; padding: var(--ac-space-sm);
} }
.checkout-summary-table { .checkout-summary-table {
font-size: 0.85rem; min-width: 28rem;
min-width: 450px;
}
.checkout-summary-table thead th {
padding: 0.5rem 0.35rem;
font-size: 0.75rem;
} }
.checkout-summary-table thead th,
.checkout-summary-table tbody td { .checkout-summary-table tbody td {
padding: 0.5rem 0.35rem; padding: var(--ac-space-sm) var(--ac-space-2xs);
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;
} }
} }

View file

@ -1,64 +1,67 @@
/* filepath: website_sale_aplicoop/static/src/css/sections/checkout.css */ /* 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 { .checkout-summary {
background-color: #f8f9fa; margin-bottom: var(--ac-space-xl);
padding: 1.5rem; padding: var(--ac-space-lg);
border-radius: 0.5rem; border-radius: var(--ac-radius-md);
margin-bottom: 2rem; background-color: var(--ac-surface-sunken);
} }
.checkout-summary-container { .checkout-summary-container {
background: white; margin-bottom: var(--ac-space-xl);
border-radius: 0.75rem; padding: var(--ac-space-lg);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); border-radius: var(--ac-radius-lg);
padding: 1.5rem; background: var(--ac-surface);
margin-bottom: 2rem; 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 { .checkout-summary-table {
margin-bottom: 0; margin-bottom: 0;
font-size: 1.15rem; min-width: 30rem;
} font-size: var(--ac-text-md);
.checkout-summary-table thead {
background-color: #2d3748;
color: white;
} }
.checkout-summary-table thead th { .checkout-summary-table thead th {
font-weight: 700; padding: var(--ac-space-md) var(--ac-space-xs);
padding: 1rem 0.75rem;
text-transform: uppercase;
font-size: 1rem;
letter-spacing: 0.5px;
border: none; 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 { .checkout-summary-table tbody tr {
border-bottom: 1px solid #e2e8f0; border-bottom: 1px solid var(--ac-border);
transition: background-color 0.2s ease; transition: background-color var(--ac-transition-fast);
}
.checkout-summary-table tbody tr:hover {
background-color: #f7fafc;
} }
.checkout-summary-table tbody td { .checkout-summary-table tbody td {
padding: 1rem 0.75rem; padding: var(--ac-space-md) var(--ac-space-xs);
vertical-align: middle; 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 { .checkout-summary-table .col-name {
width: 50%; width: 50%;
} }
@ -76,52 +79,68 @@
} }
.checkout-summary-table .empty-message { .checkout-summary-table .empty-message {
background-color: #f7fafc; background-color: var(--ac-surface-sunken);
} }
/* ---------- Total ---------- */
.checkout-total-section { .checkout-total-section {
border-top: 2px solid #e2e8f0; margin-top: var(--ac-space-md);
padding-top: 1.5rem; padding-top: var(--ac-space-lg);
margin-top: 1rem; border-top: 2px solid var(--ac-border);
} }
.total-row { .total-row {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: baseline;
gap: 1rem; gap: var(--ac-space-sm);
} }
.total-label { .total-label {
font-weight: 700; font-size: var(--ac-text-md);
font-size: 1.2rem; font-weight: var(--ac-weight-bold);
color: #1a202c; color: var(--ac-text-primary);
} }
.total-amount { .total-amount {
font-size: 2rem; min-width: 7.5rem;
font-weight: 700; font-size: var(--ac-text-2xl);
color: var(--success-color); font-weight: var(--ac-weight-bold);
min-width: 120px; font-variant-numeric: tabular-nums;
text-align: right; 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 { .currency {
font-size: 1.2rem; font-size: var(--ac-text-md);
color: #2d3748; font-weight: var(--ac-weight-semibold);
font-weight: 600; color: var(--ac-text-secondary);
} }
.summary-heading { .summary-heading {
font-size: 1.75rem; padding-left: var(--ac-space-md);
font-weight: 700; border-left: 4px solid var(--ac-color-success-strong);
color: #1a202c; font-size: var(--ac-text-xl);
border-left: 4px solid var(--success-color); font-weight: var(--ac-weight-bold);
padding-left: 1rem; color: var(--ac-text-primary);
} }
.checkout-order-desc { .checkout-order-desc {
word-wrap: break-word; color: var(--ac-text-secondary);
overflow-wrap: break-word; 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 */ /* 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 { .order-info-card {
background: white; border-radius: var(--ac-radius-lg);
border-radius: 0.75rem; background: var(--ac-surface);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); box-shadow: var(--ac-shadow-sm);
} }
.order-info-card .card-body { .order-info-card .card-body {
padding: 1rem; padding: var(--ac-space-md);
}
.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;
} }

View file

@ -1,48 +1,54 @@
/* filepath: website_sale_aplicoop/static/src/css/sections/order-list.css */ /* 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 { .eskaera-page h1 {
font-size: 2.5rem; margin-bottom: var(--ac-space-sm);
font-weight: 700; font-size: var(--ac-text-2xl);
color: #2d3748; font-weight: var(--ac-weight-bold);
margin-bottom: 0.5rem;
text-align: center; text-align: center;
color: var(--ac-text-primary);
} }
.eskaera-page > .container > .row:first-child { .eskaera-page > .container > .row:first-child {
margin-bottom: 2rem; margin-bottom: var(--ac-space-xl);
border-bottom: 2px solid #e2e8f0; padding-bottom: var(--ac-space-lg);
padding-bottom: 1.5rem; border-bottom: 2px solid var(--ac-border);
} }
.eskaera-page > .container > .row:first-child p { .eskaera-page > .container > .row:first-child p {
font-size: 1.3rem !important;
color: #4a5568;
text-align: center;
margin: 0; 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,
.eskaera-orders-grid { .eskaera-orders-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); /* 20rem keeps three across a full-width container and drops to one on a
gap: 2rem; 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; margin: 0 auto;
max-width: 100%; max-width: 100%;
} }
.eskaera-empty-state { .eskaera-empty-state {
padding: var(--ac-space-2xl) var(--ac-space-md);
text-align: center; text-align: center;
padding: 3rem 1rem;
} }
.eskaera-empty-state .alert { .eskaera-empty-state .alert {
max-width: 500px; max-width: 31.25rem;
margin: 0 auto; margin: 0 auto;
font-size: 1.05rem; border-radius: var(--ac-radius-md);
border-radius: 0.5rem; font-size: var(--ac-text-base);
} }

View file

@ -1,14 +1,29 @@
/* filepath: website_sale_aplicoop/static/src/css/sections/products-grid.css */ /* 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 { .products-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-template-columns: 1fr;
gap: 1.2rem; gap: var(--ac-space-md);
margin-bottom: 2rem; 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 { .product-card-wrapper {
@ -17,62 +32,7 @@
} }
.product-search-highlight { .product-search-highlight {
border: 2px solid #007bff; padding: var(--ac-space-sm);
padding: 10px; border: 2px solid var(--ac-color-primary);
} border-radius: var(--ac-radius-md);
/* 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;
}
} }

View file

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

View file

@ -459,8 +459,9 @@ console.log("[INFINITE_SCROLL] Script loaded!");
// Show fallback button // Show fallback button
var btn = document.getElementById("load-more-btn"); var btn = document.getElementById("load-more-btn");
if (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.classList.remove("d-none");
btn.style.display = "";
} }
}); });
}, },

View file

@ -141,9 +141,6 @@
// Initialize available tags from DOM // Initialize available tags from DOM
self._initializeAvailableTags(); 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 // Store last values at instance level so polling can access them
// Initialize to current values to avoid triggering reset on first poll // Initialize to current values to avoid triggering reset on first poll
self.lastSearchValue = self.searchInput.value.trim(); self.lastSearchValue = self.searchInput.value.trim();
@ -165,9 +162,9 @@
// This listener is separate from the filtering listener // This listener is separate from the filtering listener
self.searchInput.addEventListener("input", function () { self.searchInput.addEventListener("input", function () {
if (self.searchInput.value.trim().length > 0) { if (self.searchInput.value.trim().length > 0) {
self.clearSearchBtn.style.display = "block"; self.clearSearchBtn.classList.add("is-visible");
} else { } else {
self.clearSearchBtn.style.display = "none"; self.clearSearchBtn.classList.remove("is-visible");
} }
}); });
@ -179,7 +176,7 @@
// Clear the input // Clear the input
self.searchInput.value = ""; self.searchInput.value = "";
self.clearSearchBtn.style.display = "none"; self.clearSearchBtn.classList.remove("is-visible");
// Update last stored value to prevent polling from detecting "change" // Update last stored value to prevent polling from detecting "change"
self.lastSearchValue = ""; self.lastSearchValue = "";
@ -204,7 +201,7 @@
// Initial check - don't show if empty // Initial check - don't show if empty
if (self.searchInput.value.trim().length > 0) { 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"]'); var tagBadges = document.querySelectorAll('[data-toggle="tag-filter"]');
console.log("[realtimeSearch] Found " + tagBadges.length + " tag filter badges"); 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) { tagBadges.forEach(function (badge) {
badge.addEventListener("click", function (e) { badge.addEventListener("click", function (e) {
e.preventDefault(); e.preventDefault();
var tagId = parseInt(badge.getAttribute("data-tag-id"), 10); 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 // Toggle tag selection
if (self.selectedTags.has(tagId)) { if (self.selectedTags.has(tagId)) {
// Deselect
self.selectedTags.delete(tagId); self.selectedTags.delete(tagId);
console.log("[realtimeSearch] Tag " + tagId + " deselected"); console.log("[realtimeSearch] Tag " + tagId + " deselected");
} else { } else {
// Select
self.selectedTags.add(tagId); self.selectedTags.add(tagId);
console.log("[realtimeSearch] Tag " + tagId + " selected"); console.log("[realtimeSearch] Tag " + tagId + " selected");
} }
// Update colors for ALL badges based on selection state self._syncTagBadgeStates(tagBadges);
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)"
);
}
});
// Filter products (independent of search/category state) // Filter products (independent of search/category state)
// Skip during initialization // 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 // Auto-load more products if too few are visible
self._autoLoadMoreIfNeeded(visibleCount); self._autoLoadMoreIfNeeded(visibleCount);
} catch (error) { } 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) { _autoLoadMoreIfNeeded: function (visibleCount) {
/** /**
* Automatically load more products if there are too few visible on screen. * Automatically load more products if there are too few visible on screen.

View file

@ -691,12 +691,16 @@
var notification = document.createElement("div"); var notification = document.createElement("div");
notification.className = notification.className =
"alert alert-" + type + " alert-dismissible fade show position-fixed"; "alert alert-" + type + " alert-dismissible fade show eskaera-notification";
notification.style.cssText = // Announce it: errors and warnings are the whole point of this helper,
"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;"; // 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 = notification.innerHTML =
message + 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); document.body.appendChild(notification);
@ -1253,13 +1257,17 @@
// Create toast element // Create toast element
var toast = document.createElement("div"); var toast = document.createElement("div");
toast.className = "toast-notification"; toast.className = "toast-notification";
toast.setAttribute("role", "status");
// Get translated "added to cart" label from server // Get translated "added to cart" label from server
var labels = this._getLabels(); var labels = this._getLabels();
var addedMsg = labels.added_to_cart || "added to cart"; var addedMsg = labels.added_to_cart || "added to cart";
toast.innerHTML = 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); document.body.appendChild(toast);
@ -1551,7 +1559,7 @@
// Simple approach: if response is very short, assume no more products // Simple approach: if response is very short, assume no more products
if (html.trim().length < 100) { if (html.trim().length < 100) {
console.log("[LAZY_LOADING] No more products, hiding load-more button"); console.log("[LAZY_LOADING] No more products, hiding load-more button");
btn.style.display = "none"; btn.classList.add("d-none");
} else { } else {
// Re-enable button for next page // Re-enable button for next page
btn.disabled = false; btn.disabled = false;
@ -1595,7 +1603,7 @@
if (Object.keys(this.cart).length === 0) { if (Object.keys(this.cart).length === 0) {
cartContainer.innerHTML = 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.") + (labels.empty_cart || "This order's cart is empty.") +
"</p>"; "</p>";
return; return;
@ -1612,20 +1620,20 @@
total += subtotal; total += subtotal;
html += html +=
'<div class="list-group-item" style="font-size: 1.05rem;">' + '<div class="list-group-item">' +
'<div class="mb-2">' + '<div class="mb-2">' +
'<h6 class="mb-0" style="font-size: 1.1rem;">' + '<h6 class="mb-0">' +
self.escapeHtml(item.name) + self.escapeHtml(item.name) +
"</h6>" + "</h6>" +
"</div>" + "</div>" +
'<div class="d-flex justify-content-between align-items-center">' + '<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) + parseFloat(item.qty).toFixed(1) +
" x €" + " x €" +
item.price.toFixed(2) + item.price.toFixed(2) +
"</small>" + "</small>" +
'<div class="d-flex align-items-center gap-2">' + '<div class="d-flex align-items-center gap-2">' +
'<strong style="font-size: 1.1rem;">€' + "<strong>€" +
subtotal.toFixed(2) + subtotal.toFixed(2) +
"</strong>" + "</strong>" +
'<button class="btn btn-sm btn-danger remove-from-cart" ' + '<button class="btn btn-sm btn-danger remove-from-cart" ' +
@ -1644,11 +1652,7 @@
}); });
html += "</div>"; html += "</div>";
html += html += '<div class="cart-total mt-3 pt-3">' + "Total: €" + total.toFixed(2) + "</div>";
'<div class="cart-total mt-3 pt-3" style="font-size: 1.2rem; font-weight: bold;">' +
"Total: €" +
total.toFixed(2) +
"</div>";
cartContainer.innerHTML = html; cartContainer.innerHTML = html;

View file

@ -3,12 +3,45 @@
<data> <data>
<!-- Template to load items from history and redirect to group order --> <!-- Template to load items from history and redirect to group order -->
<template id="eskaera_load_from_history" name="Load Order from History"> <template id="eskaera_load_from_history" name="Load Order from History">
<html> <html t-att-lang="page_lang or 'en-US'">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Loading Order...</title> <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> </head>
<body> <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"> <script type="text/javascript">
// Items are embedded directly in the script (pre-serialized JSON from controller) // 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 var itemsJson = <t t-raw="items_json"/>; // This is a JSON array/string

View file

@ -3,11 +3,13 @@
<data> <data>
<!-- Extend portal_my_orders to add group order name and pickup day columns --> <!-- 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"> <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"> <xpath expr="//tr[hasclass('active')]//th[last()]" position="after">
<th>Group Order</th> <th scope="col">Group Order</th>
<th>Pickup Day</th> <th scope="col">Pickup Day</th>
<th class="text-center">Actions</th>
</xpath> </xpath>
<!-- Add data cells for each order row --> <!-- Add data cells for each order row -->
@ -75,25 +77,30 @@
<!-- Group Order Info --> <!-- Group Order Info -->
<div class="col-lg-12"> <div class="col-lg-12">
<div class="card"> <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"> <div class="card-header bg-light">
<strong><t t-esc="sale_order.group_order_id.name"/></strong> <strong><t t-esc="sale_order.group_order_id.name"/></strong>
<t t-if="sale_order.home_delivery"> <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-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="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 ''"/> <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>
<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"/> <t t-esc="sale_order.group_order_id.delivery_notice"/>
</span> </span>
</t> </t>
<t t-else=""> <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-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="day_names[day_idx] if day_names else ''"/>,
<t t-esc="sale_order.pickup_date.strftime('%d/%m/%Y')"/> <t t-esc="sale_order.pickup_date.strftime('%d/%m/%Y')"/>
</span> </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-foreach="sale_order.group_order_id.group_ids" t-as="group">
<t t-esc="group.name"/> <t t-esc="group.name"/>
<t t-if="group.street"> <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 t-attf-class="eskaera-order-card{{ ' eskaera-order-card--special' if order.type == 'special' else '' }}">
<div class="card-body"> <div class="card-body">
<div class="position-absolute order-badge-position"> <div class="position-absolute order-badge-position">
<span class="badge bg-primary d-flex align-items-center gap-2 order-badge-custom"> <span class="badge text-bg-primary d-flex align-items-center gap-2 order-badge-custom">
<i class="fa fa-shopping-bag" /> <i class="fa fa-shopping-bag" aria-hidden="true" />
<strong> <strong>
<t t-esc="order.available_products_count" /> <t t-esc="order.available_products_count" />
</strong> </strong>
@ -147,22 +147,24 @@
</span> </span>
</div> </div>
</t> </t>
<t t-if="order.home_delivery and order.delivery_date">
<div class="meta-item">
<span class="meta-label">Delivery</span>
<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"> <t t-if="order.home_delivery">
<div class="meta-item"> <!-- Badge first, then the date, centred as a pair. Source order
<span class="badge bg-success"><i class="fa fa-truck" /> Home Delivery</span> 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>
</t>
</div> </div>
</t> </t>
<t t-if="order.type == 'special'"> <t t-if="order.type == 'special'">
<div class="meta-item meta-item--full"> <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> </div>
<t t-if="order.start_date"> <t t-if="order.start_date">
<div class="meta-item"> <div class="meta-item">
@ -182,7 +184,10 @@
</template> </template>
<template id="order_header" name="Order Header"> <template id="order_header" name="Order Header">
<div t-att-class="header_class or 'eskaera-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-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"> <t t-if="image_to_show">
<img t-att-src="image_data_uri(image_to_show)" alt="Order image" class="order-thumbnail-md" /> <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 t-set="header_class" t-value="'eskaera-order-header'" />
</t> </t>
<div class="eskaera-order-header"> <div class="eskaera-order-header">
<div class="order-info-grid"> <!-- Name/value pairs: a description list, so assistive tech
<div class="info-item"> announces each value with its label. The 4 → 2 → 1 column
<span t-att-class="'info-label'">Consumer Groups</span> layout lives in layout/header.css. -->
<span class="info-value"> <dl class="order-info-grid">
<t t-esc="', '.join(group_order.group_ids.mapped('name'))" /> <dt class="info-label">Consumer Groups</dt>
</span> <dd class="info-value">
</div> <t t-esc="', '.join(group_order.group_ids.mapped('name'))" />
</dd>
<t t-if="group_order.cutoff_day"> <t t-if="group_order.cutoff_day">
<div class="info-item"> <dt class="info-label">Cutoff Day</dt>
<span t-att-class="'info-label'">Cutoff Day</span> <dd class="info-value">
<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')" />)</dd>
<t t-esc="day_names[int(group_order.cutoff_day) % 7]" /> (<t t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')" />)</span>
</div>
</t> </t>
<t t-if="group_order.pickup_slot_ids"> <t t-if="group_order.pickup_slot_ids">
<div class="info-item"> <dt class="info-label">Store Pickup Slots</dt>
<span t-att-class="'info-label'">Store Pickup Slots</span> <dd class="info-value">
<span class="info-value"> <t t-foreach="group_order.pickup_slot_ids" t-as="slot">
<t t-foreach="group_order.pickup_slot_ids" t-as="slot"> <div class="info-value-line">
<div> <t t-esc="day_names[int(slot.weekday) % 7]" />
<t t-esc="day_names[int(slot.weekday) % 7]" /> &#160;
&#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-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>
<t t-if="slot.label">&#160;(<t t-esc="slot.label" />)</t> </div>
</div> </t>
</t> </dd>
</span>
</div>
</t> </t>
<t t-elif="group_order.pickup_day"> <t t-elif="group_order.pickup_day">
<div class="info-item"> <dt class="info-label">Store Pickup Day</dt>
<span t-att-class="'info-label'">Store Pickup Day</span> <dd class="info-value">
<span 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')" />)
<t t-esc="day_names[int(group_order.pickup_day) % 7]" /> (<t t-esc="group_order.pickup_date.strftime('%d/%m/%Y')" />) </dd>
</span>
</div>
</t> </t>
<t t-if="group_order.delivery_date and group_order.home_delivery"> <t t-if="group_order.delivery_date and group_order.home_delivery">
<div class="info-item"> <dt class="info-label">Home Delivery Day</dt>
<span t-att-class="'info-label'">Home Delivery Day</span> <dd class="info-value">
<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')" />)</dd>
<t t-esc="day_names[group_order.delivery_date.weekday()]" /> (<t t-esc="group_order.delivery_date.strftime('%d/%m/%Y')" />)</span>
</div>
</t> </t>
<t t-if="group_order.start_date"> <t t-if="group_order.start_date">
<div class="info-item"> <dt class="info-label">Open From</dt>
<span t-att-class="'info-label'">Open From</span> <dd class="info-value">
<span class="info-value"> <t t-esc="group_order.start_date.strftime('%d/%m/%Y')" />
<t t-esc="group_order.start_date.strftime('%d/%m/%Y')" /> </dd>
</span>
</div>
</t> </t>
<t t-if="group_order.end_date"> <t t-if="group_order.end_date">
<div class="info-item"> <dt class="info-label">Open Until</dt>
<span t-att-class="'info-label'">Open Until</span> <dd class="info-value">
<span class="info-value"> <t t-esc="group_order.end_date.strftime('%d/%m/%Y')" />
<t t-esc="group_order.end_date.strftime('%d/%m/%Y')" /> </dd>
</span>
</div>
</t> </t>
</div> </dl>
</div> </div>
</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="row g-2">
<div class="col-md-7"> <div class="col-md-7">
<div style="position: relative;"> <div class="search-input-wrapper">
<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;" /> <label class="visually-hidden" for="realtime-search-input">Search products</label>
<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"> <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> ×
</button>
</div> </div>
<p id="realtime-search-status" class="visually-hidden" aria-live="polite" aria-atomic="true" />
</div> </div>
<div class="col-md-5"> <div class="col-md-5">
<select name="category" id="realtime-category-select" class="form-select"> <select name="category" id="realtime-category-select" class="form-select">
@ -299,13 +295,18 @@
<div class="col-12"> <div class="col-12">
<div id="tag-filter-container" class="tag-filter-badges"> <div id="tag-filter-container" class="tag-filter-badges">
<t t-foreach="available_tags" t-as="tag"> <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']"> <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']" />) <span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
</button> </button>
</t> </t>
<t t-else=""> <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']" />) <span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
</button> </button>
</t> </t>
@ -321,30 +322,32 @@
<div class="card-header d-flex justify-content-between align-items-center gap-1"> <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> <h6 class="mb-0 cart-title-sm" id="cart-title">My Cart</h6>
<div class="btn-group cart-btn-group gap-0" role="group"> <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"> <!-- data-bs-title feeds the tooltip, it is not an
<i class="fa fa-save cart-icon-size" /> 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> </button>
<t t-if="group_order.home_delivery and delivery_product_id"> <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"> <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" /> <i class="fa fa-truck cart-icon-size" aria-hidden="true" />
</button> </button>
</t> </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" /> <i class="fa fa-check cart-icon-size" aria-hidden="true" />
</a> </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" /> <i class="fa fa-trash cart-icon-size" aria-hidden="true" />
</button> </button>
</div> </div>
</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> <p class="text-muted">This order's cart is empty</p>
</div> </div>
<div class="card-footer bg-white d-flex justify-content-between align-items-center gap-2"> <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 <i class="fa fa-trash me-1" aria-hidden="true" />Clear Cart
</button> </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 Proceed to Checkout
</a> </a>
</div> </div>
@ -361,14 +364,14 @@
<t t-if="lazy_loading_enabled and has_next"> <t t-if="lazy_loading_enabled and has_next">
<div id="infinite-scroll-container" class="row mt-4"> <div id="infinite-scroll-container" class="row mt-4">
<div class="col-12 text-center"> <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"> <div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
<p class="mt-2">Loading more products...</p> <p class="mt-2">Loading more products...</p>
</div> </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;"> <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" />Load More Products <i class="fa fa-download me-2" aria-hidden="true" />Load More Products
</button> </button>
</div> </div>
</div> </div>
@ -396,14 +399,13 @@
var successCount = 0; var successCount = 0;
tooltipElements.forEach(function(element) { 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'); var title = element.getAttribute('data-bs-title');
if (title) { if (title) {
// Set native title attribute for browser-native tooltip
element.setAttribute('title', title); element.setAttribute('title', title);
successCount++; 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'); console.log('[TOOLTIP] Tooltip initialization complete:', successCount, 'elements updated');
@ -421,20 +423,23 @@
</t> </t>
</template> </template>
<template id="eskaera_checkout_summary" name="Checkout Order Summary"> <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"> <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"> <thead class="table-dark">
<tr> <tr>
<th class="col-name">Product</th> <th scope="col" class="col-name">Product</th>
<th class="col-qty text-center">Quantity</th> <th scope="col" class="col-qty text-center">Quantity</th>
<th class="col-price text-right">Price</th> <th scope="col" class="col-price text-end">Price</th>
<th class="col-subtotal text-right">Subtotal</th> <th scope="col" class="col-subtotal text-end">Subtotal</th>
</tr> </tr>
</thead> </thead>
<tbody id="checkout-summary-tbody"> <tbody id="checkout-summary-tbody">
<tr id="checkout-empty-row" class="empty-message"> <tr id="checkout-empty-row" class="empty-message">
<td colspan="4" class="text-center text-muted py-4"> <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> <p>This order's cart is empty</p>
</td> </td>
</tr> </tr>
@ -464,55 +469,60 @@
</div> </div>
<div class="order-info-card card border-0 shadow-sm mb-4"> <div class="order-info-card card border-0 shadow-sm mb-4">
<div class="card-body"> <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="row mb-4">
<div class="col-md-4"> <div class="col-md-4">
<div class="info-item"> <dl class="info-pair">
<label t-att-class="'info-label'">Cutoff Day</label> <dt class="info-label">Cutoff Day</dt>
<t t-if="group_order.cutoff_day and group_order.cutoff_date"> <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]" /> <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 class="info-date">(<t t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')" />)</span>
</span> </dd>
</t> </t>
<t t-else=""> <t t-else="">
<span t-att-class="'text-muted small'">Not configured</span> <dd class="info-value text-muted small">Not configured</dd>
</t> </t>
</div> </dl>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="info-item"> <t t-if="group_order.pickup_slot_ids">
<t t-if="group_order.pickup_slot_ids"> <dl class="info-pair">
<label t-att-class="'info-label'">Store Pickup Slots</label> <dt class="info-label">Store Pickup Slots</dt>
<span class="info-value"> <dd class="info-value">
<t t-foreach="group_order.pickup_slot_ids" t-as="slot"> <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]" /> <t t-esc="day_names[int(slot.weekday) % 7]" />
&#160; &#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-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> <t t-if="slot.label">&#160;(<t t-esc="slot.label" />)</t>
</div> </div>
</t> </t>
</span> </dd>
</t> </dl>
<t t-elif="group_order.pickup_day and group_order.pickup_date"> </t>
<label t-att-class="'info-label'">Store Pickup Day</label> <t t-elif="group_order.pickup_day and group_order.pickup_date">
<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]" /> <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 class="info-date">(<t t-esc="group_order.pickup_date.strftime('%d/%m/%Y')" />)</span>
</span> </dd>
</t> </dl>
</div> </t>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="info-item"> <t t-if="group_order.delivery_date and group_order.home_delivery">
<t t-if="group_order.delivery_date and group_order.home_delivery"> <dl class="info-pair">
<label t-att-class="'info-label'">Home Delivery Day</label> <dt class="info-label">Home Delivery Day</dt>
<span class="info-value"> <dd class="info-value">
<t t-esc="day_names[group_order.delivery_date.weekday()]" /> <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 class="info-date">(<t t-esc="group_order.delivery_date.strftime('%d/%m/%Y')" />)</span>
</span> </dd>
</t> </dl>
</div> </t>
</div> </div>
</div> </div>
<hr class="my-2" /> <hr class="my-2" />
@ -529,11 +539,11 @@
<t t-if="group_order.home_delivery"> <t t-if="group_order.home_delivery">
<div class="card border-0 shadow-sm mb-4"> <div class="card border-0 shadow-sm mb-4">
<div class="card-body"> <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" /> <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> <label class="form-check-label fw-bold" for="home-delivery-checkbox">Home Delivery</label>
</div> </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"> <p class="mb-2">
<i class="fa fa-truck" aria-hidden="true" t-translation="off" /> <i class="fa fa-truck" aria-hidden="true" t-translation="off" />
<t t-if="group_order.delivery_date"> <t t-if="group_order.delivery_date">
@ -555,7 +565,7 @@
<i class="fa fa-save" aria-hidden="true" t-translation="off" /> <i class="fa fa-save" aria-hidden="true" t-translation="off" />
<span t-esc="labels.get('save_draft', 'Save Draft')" /> <span t-esc="labels.get('save_draft', 'Save Draft')" />
</button> </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" /> <i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
<span>Back to Cart</span> <span>Back to Cart</span>
</a> </a>
@ -635,7 +645,7 @@
</t> </t>
<t t-else=""> <t t-else="">
<div class="card-img-top bg-light d-flex align-items-center justify-content-center product-img-placeholder"> <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> </div>
</t> </t>
<t t-if="product.dynamic_ribbon_id"> <t t-if="product.dynamic_ribbon_id">
@ -649,10 +659,12 @@
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
<h6 class="card-title" t-esc="product.name" /> <h6 class="card-title" t-esc="product.name" />
<t t-if="product.product_tag_ids"> <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-foreach="filtered_product_tags.get(product.id, {}).get('published_tags', product.product_tag_ids)" t-as="tag">
<t t-if="tag.color"> <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 t-else=""> <t t-else="">
<span class="badge badge-km tag-use-theme-color" t-esc="tag.name" /> <span class="badge badge-km tag-use-theme-color" t-esc="tag.name" />
@ -661,14 +673,14 @@
</div> </div>
</t> </t>
<t t-if="product_supplier_info.get(product.id)"> <t t-if="product_supplier_info.get(product.id)">
<p class="product-supplier mb-2"> <p class="product-supplier">
<small> <small>
<t t-esc="product_supplier_info[product.id]" /> <t t-esc="product_supplier_info[product.id]" />
</small> </small>
</p> </p>
</t> </t>
<t t-if="product.origin_text"> <t t-if="product.origin_text">
<p class="product-origin mb-2"> <p class="product-origin">
<small> <small>
<i class="fa fa-map-marker" aria-hidden="true" /> <i class="fa fa-map-marker" aria-hidden="true" />
<t t-out="product.origin_text" /> <t t-out="product.origin_text" />
@ -695,7 +707,7 @@
</h6> </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-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"> <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" /> <t t-esc="'%.2f' % tax_incl_base_unit_price" /> € / <t t-esc="product.base_unit_name" />
</p> </p>
</t> </t>
@ -716,19 +728,24 @@
t-att-data-out-of-stock="'true' if product.is_out_of_stock else 'false'" t-att-data-out-of-stock="'true' if product.is_out_of_stock else 'false'"
> >
<div class="qty-control"> <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> </label>
<button class="qty-decrease" type="button" t-attf-data-product-id="{{ product.id }}" aria-label="Decrease quantity"> <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> </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" /> <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"> <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> </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 <button
class="add-to-cart-btn" class="add-to-cart-btn"
type="button" type="button"
t-att-data-out-of-stock="'true' if product.is_out_of_stock else 'false'" 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-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" t-att-title="out_of_stock_label if product.is_out_of_stock else add_to_cart_label"
> >