diff --git a/.claude/skills/odoo-web-design/references/css-architecture.md b/.claude/skills/odoo-web-design/references/css-architecture.md index 3083f06..77b9a3d 100644 --- a/.claude/skills/odoo-web-design/references/css-architecture.md +++ b/.claude/skills/odoo-web-design/references/css-architecture.md @@ -1,3 +1,4 @@ + # CSS architecture ## File layout @@ -79,8 +80,10 @@ Other rules: should do. - No ID selectors for styling; IDs are for anchors and `aria-*` references. - No element selectors outside `base/` (`div.product-card` locks the markup). -- `!important` is allowed in exactly two places: the `prefers-reduced-motion` reset, and a documented - override of a core rule you cannot scope — with a comment saying which rule and why. +- `!important` is allowed in exactly three places, each with a comment saying why: the + `prefers-reduced-motion` reset; a genuine **utility** that must beat whatever component it lands on + (`.hidden-product`, Bootstrap's own utility layer works this way); and a documented override of a + core rule you cannot scope. Anything else means the selector is wrong. ## Ordering inside a file diff --git a/docs/OCA_DOCUMENTATION.md b/docs/OCA_DOCUMENTATION.md index 3dad8b6..d5182a3 100644 --- a/docs/OCA_DOCUMENTATION.md +++ b/docs/OCA_DOCUMENTATION.md @@ -40,7 +40,6 @@ Los siguientes addons custom han sido actualizados con la estructura OCA: Todos los addons incluyen correctamente: - **Autor**: Criptomart (en `__manifest__.py` y `README.rst`) -- **Financiador**: Elika Bilbo (en `readme/CREDITS.rst`) ## Logo de CriptoMart diff --git a/docs/TRANSLATIONS.md b/docs/TRANSLATIONS.md index 0902d88..b9733c4 100644 --- a/docs/TRANSLATIONS.md +++ b/docs/TRANSLATIONS.md @@ -106,10 +106,51 @@ PEDIR AL USUARIO QUE GENERE EL POT DESDE LA UI DE ODOO docker-compose cp odoo:/tmp/addon_name.pot ./addon_name/i18n/ ``` +#### El `.pot` no es opcional: Odoo fusiona los `.po` contra él + +El `.pot` es un artefacto **generado** y está en `.gitignore` (`*.pot`), pero no es +prescindible: al importar, Odoo fusiona cada `.po` contra el `.pot` del módulo antes +de leerlo. + +```python +# odoo/tools/translate.py, PoFileReader.__init__ +if pot_path: + self.pofile.merge(polib.pofile(pot_path)) +``` + +`merge()` de polib **sustituye las ocurrencias de cada entrada por las del POT**, y +marca como obsoletas las entradas que el POT no tiene (el lector se salta las +obsoletas). Consecuencias prácticas: + +- Añadir una ocurrencia `model_terms:ir.ui.view,...` solo al `.po` **no sirve de + nada**: el merge la descarta y la traducción nunca llega a la vista. +- Una cadena nueva que no esté en el POT se ignora entera, por muy traducida que + esté en los tres idiomas. +- Por eso, tras mover una cadena a un atributo traducible o añadir una nueva, hay + que **regenerar el POT** antes de que la traducción surta efecto. + +⚠️ Al exportar contra una BD **con datos demo**, el POT se lleva los nombres de los +registros demo (productos, impuestos, nombres de pedidos). Genera el POT contra una +BD limpia. + ### 2. Actualizar Archivos .po Existentes no usar msmerge, corrompe el po. Usa polib. +Al crear entradas con polib hay dos detalles que fallan **en silencio**: + +```python +entry = polib.POEntry( + msgid="products found", + msgstr="produktu aurkitu dira", + # 1. Sin `module:` el lector revienta; sin `odoo-python` la cadena se lee + # del .po y se descarta acto seguido (ver Troubleshooting). + comment="module: website_sale_aplicoop\nodoo-python", + # 2. El número de línea es obligatorio en ocurrencias `code:`. + occurrences=[("code:addons/website_sale_aplicoop/models/js_translations.py", "0")], +) +``` + ### 3. Traducir Términos Nuevos Editar los archivos `.po` y completar las traducciones: @@ -245,6 +286,58 @@ name = fields.Char(string="Name") 4. **Usuario con idioma incorrecto**: Verificar preferencias de usuario 5. **Cache**: Reiniciar Odoo: `docker-compose restart odoo` +### La cadena SÍ está traducida en el .po y sigue saliendo en inglés + +Los tres casos siguientes fallan sin dar ningún error, ni en el log ni al importar. + +**a) El atributo no es traducible.** QWeb solo traduce los de esta lista +(`odoo/tools/translate.py`, `TRANSLATED_ATTRS`), más sus variantes `t-attf-`: + +```text +string help placeholder alt title label data-tooltip confirm +aria-label aria-keyshortcuts aria-placeholder aria-roledescription aria-valuetext +``` + +`data-bs-title` **no está**: un tooltip escrito ahí nunca se traduce. Usa `title` +(Bootstrap lo respeta igual) o pasa el texto desde el endpoint i18n. Tampoco están +`aria-description` ni `data-bs-*` en general. + +**b) Falta la referencia a la vista en el POT.** Para que una traducción se aplique +a una vista, su entrada necesita una ocurrencia +`model_terms:ir.ui.view,arch_db:modulo.xmlid` **en el POT** (ver el apartado del +merge más arriba). Síntoma típico: dos botones contiguos, uno traducido y el otro +no, con las dos cadenas presentes en el `.po`. Se comprueba así: + +```bash +grep -B 4 '^msgid "Save Cart"$' addon/i18n/addon.pot # ¿tiene model_terms? +``` + +**c) Falta el comentario `#. odoo-python`.** Las cadenas de `_()` se leen del `.po` +directamente (no de la BD), y `CodeTranslations._load_python_translations` filtra +por ese comentario: + +```python +def filter_func(row): + return row.get('value') and PYTHON_TRANSLATION_COMMENT in row['comments'] +``` + +Sin él la entrada se lee y se descarta. La entrada tiene que quedar así: + +```po +#. module: website_sale_aplicoop +#. odoo-python +#: code:addons/website_sale_aplicoop/models/js_translations.py:0 +msgid "products found" +msgstr "produktu aurkitu dira" +``` + +Y ojo con el `:0` final: el lector hace `int(line_number)` sobre las ocurrencias +`code:`, así que una sin número de línea lanza `ValueError` y **tumba la carga del +catálogo entero**, no solo la de esa entrada. + +Estas cadenas se cachean por proceso, así que después de tocarlas hay que +reiniciar Odoo (`docker-compose restart odoo`), no basta con `-u`. + ### Caracteres Especiales Usar escape correcto en .po: diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index 4925b6a..058472f 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -2249,6 +2249,9 @@ class AplicoopWebsiteSale(WebsiteSale): "website_sale_aplicoop.eskaera_load_from_history", { "group_order_id": group_order_id, + # This template renders a bare HTML document instead of + # website.layout, so it has to declare its own language. + "page_lang": (request.env.lang or "en_US").replace("_", "-"), # sessionStorage keys stay keyed by id; the redirect at # the end of the template needs the public URL instead. "group_order_url": self._eskaera_url(group_order), diff --git a/website_sale_aplicoop/controllers/website_sale_i18n.py b/website_sale_aplicoop/controllers/website_sale_i18n.py index e1bc924..21eeb05 100644 --- a/website_sale_aplicoop/controllers/website_sale_i18n.py +++ b/website_sale_aplicoop/controllers/website_sale_i18n.py @@ -129,6 +129,11 @@ def _get_translated_labels(self, lang=None, request_obj=None): "delivery_info_template": tr( "Delivery Information: Your order will be delivered at {pickup_day} {pickup_date}" ), + # Announced in the search results live region (realtime_search.js) + "no_results": tr("No products found"), + "products_found": tr("products found"), + # Accessible name of the notification dismiss button (website_sale.js) + "close": tr("Close"), "important": tr("Important"), "confirm_order_warning": tr( "Once you confirm this order, you will not be able to modify it. Please review carefully before confirming." diff --git a/website_sale_aplicoop/i18n/README.md b/website_sale_aplicoop/i18n/README.md index 0afa148..89badb2 100644 --- a/website_sale_aplicoop/i18n/README.md +++ b/website_sale_aplicoop/i18n/README.md @@ -12,7 +12,7 @@ This module has complete translation support for **3 languages**: ## Translated Content -Each `.po` file contains **378 translations** for: +Each `.po` file contains **388 translations** for: - **Selection Field Options** (Days of week, Recurrence periods) - Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday @@ -41,11 +41,26 @@ When users switch their Odoo interface language to any of the supported language To add or update translations: -1. Edit the corresponding `.po` file +1. Edit the corresponding `.po` file **with `polib`** (never `msgmerge`, it corrupts them) 2. Update the `msgstr` values (keep `msgid` unchanged) 3. Save and reload the module in Odoo 4. Translations apply immediately +### Adding a *new* string is not just a `.po` edit + +`website_sale_aplicoop.pot` is generated by Odoo and is gitignored, but it is not +optional: Odoo merges every `.po` against it before importing, and polib's `merge()` +drops entries the POT does not have and replaces their occurrences with the POT's. +A brand new string, or an existing one that just became a view term, needs the POT +regenerated (from the Odoo UI, against a database **without demo data**) before its +translation applies anywhere. + +Three failure modes that produce no error at all — an untranslatable attribute +(`data-bs-title` is not on QWeb's list, `title` is), a missing +`model_terms:ir.ui.view` reference in the POT, and a missing `#. odoo-python` +comment on `_()` strings — are written up in +[`docs/TRANSLATIONS.md`](../../docs/TRANSLATIONS.md), under *Troubleshooting*. + Example entry in a `.po` file: ```po #. module: website_sale_aplicoop @@ -57,7 +72,7 @@ msgstr "Pedido de Grupo" # Spanish translation - All `.po` files were generated and tested: **40/40 tests passing** - Translation coverage: **100%** for all supported languages -- Last updated: August 13, 2026 +- Last updated: August 16, 2026 --- diff --git a/website_sale_aplicoop/i18n/ca.po b/website_sale_aplicoop/i18n/ca.po index 9177a82..8938572 100644 --- a/website_sale_aplicoop/i18n/ca.po +++ b/website_sale_aplicoop/i18n/ca.po @@ -288,6 +288,7 @@ msgstr "Nombre de productes disponibles" #. module: website_sale_aplicoop #. odoo-python #: code:addons/website_sale_aplicoop/models/js_translations.py:0 +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout msgid "Back to Cart" msgstr "Torna al carret" @@ -416,7 +417,9 @@ msgid "Clear search" msgstr "Neteja la cerca" #. module: website_sale_aplicoop +#. odoo-python #: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form +#: code:addons/website_sale_aplicoop/models/js_translations.py:0 msgid "Close" msgstr "Tanca" @@ -1899,6 +1902,7 @@ msgstr "Dissabte" #. module: website_sale_aplicoop #. odoo-python #: code:addons/website_sale_aplicoop/models/js_translations.py:0 +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop msgid "Save Cart" msgstr "Desa el Carret" @@ -2298,3 +2302,63 @@ msgstr "Desa l'Esborrany" #: code:addons/website_sale_aplicoop/models/js_translations.py:0 msgid "Save order as draft" msgstr "Desa la comanda com a esborrany" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop +msgid "Search products" +msgstr "Cerca productes" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary +msgid "Order summary" +msgstr "Resum de la comanda" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary +msgid "Products in this order, with quantity, price and subtotal" +msgstr "Productes d'aquesta comanda, amb quantitat, preu i subtotal" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history +msgid "Loading your order…" +msgstr "Carregant la teva comanda…" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history +msgid "This page needs JavaScript to load the order into your cart." +msgstr "" +"Aquesta pàgina necessita JavaScript per carregar la comanda al teu carret." + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history +msgid "Continue to the group order" +msgstr "Continua a la comanda de grup" + +#. module: website_sale_aplicoop +#. odoo-python +#: code:addons/website_sale_aplicoop/models/js_translations.py:0 +msgid "products found" +msgstr "productes trobats" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop +msgid "Load More Products" +msgstr "Carregar més productes" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta +msgid "" +" Special Order" +msgstr "" +" Comanda especial" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta +msgid "" +" Home Delivery" +msgstr "" +" Lliurament a domicili" diff --git a/website_sale_aplicoop/i18n/es.po b/website_sale_aplicoop/i18n/es.po index e904244..c102d72 100644 --- a/website_sale_aplicoop/i18n/es.po +++ b/website_sale_aplicoop/i18n/es.po @@ -290,6 +290,7 @@ msgstr "Cantidad de Productos Disponibles" #. module: website_sale_aplicoop #. odoo-python #: code:addons/website_sale_aplicoop/models/js_translations.py:0 +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout msgid "Back to Cart" msgstr "Volver al Carrito" @@ -418,7 +419,9 @@ msgid "Clear search" msgstr "Limpiar búsqueda" #. module: website_sale_aplicoop +#. odoo-python #: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form +#: code:addons/website_sale_aplicoop/models/js_translations.py:0 msgid "Close" msgstr "Cerrar" @@ -1899,6 +1902,7 @@ msgstr "Sábado" #. module: website_sale_aplicoop #. odoo-python #: code:addons/website_sale_aplicoop/models/js_translations.py:0 +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop msgid "Save Cart" msgstr "Guardar Carrito" @@ -2295,3 +2299,62 @@ msgstr "Grabar pedido" #: code:addons/website_sale_aplicoop/models/js_translations.py:0 msgid "Save order as draft" msgstr "Grabar pedido" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop +msgid "Search products" +msgstr "Buscar productos" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary +msgid "Order summary" +msgstr "Resumen del pedido" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary +msgid "Products in this order, with quantity, price and subtotal" +msgstr "Productos de este pedido, con cantidad, precio y subtotal" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history +msgid "Loading your order…" +msgstr "Cargando tu pedido…" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history +msgid "This page needs JavaScript to load the order into your cart." +msgstr "Esta página necesita JavaScript para cargar el pedido en tu carrito." + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history +msgid "Continue to the group order" +msgstr "Continuar al pedido de grupo" + +#. module: website_sale_aplicoop +#. odoo-python +#: code:addons/website_sale_aplicoop/models/js_translations.py:0 +msgid "products found" +msgstr "productos encontrados" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop +msgid "Load More Products" +msgstr "Cargar más productos" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta +msgid "" +" Special Order" +msgstr "" +" Pedido Especial" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta +msgid "" +" Home Delivery" +msgstr "" +" Entrega a Casa" diff --git a/website_sale_aplicoop/i18n/eu.po b/website_sale_aplicoop/i18n/eu.po index bda4097..e0cec95 100644 --- a/website_sale_aplicoop/i18n/eu.po +++ b/website_sale_aplicoop/i18n/eu.po @@ -291,6 +291,7 @@ msgstr "Eskuragarri Dauden Produktuen Kopurua" #. module: website_sale_aplicoop #. odoo-python #: code:addons/website_sale_aplicoop/models/js_translations.py:0 +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout msgid "Back to Cart" msgstr "Sarera Itzuli" @@ -419,7 +420,9 @@ msgid "Clear search" msgstr "Bilaketa garbitu" #. module: website_sale_aplicoop +#. odoo-python #: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form +#: code:addons/website_sale_aplicoop/models/js_translations.py:0 msgid "Close" msgstr "Itxi" @@ -1895,6 +1898,7 @@ msgstr "Larunbata" #. module: website_sale_aplicoop #. odoo-python #: code:addons/website_sale_aplicoop/models/js_translations.py:0 +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop msgid "Save Cart" msgstr "Saskia Gorde" @@ -2293,3 +2297,64 @@ msgstr "Eskaera gorde" #: code:addons/website_sale_aplicoop/models/js_translations.py:0 msgid "Save order as draft" msgstr "Eskaera gorde" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop +msgid "Search products" +msgstr "Produktuak bilatu" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary +msgid "Order summary" +msgstr "Eskaera laburpena" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary +msgid "Products in this order, with quantity, price and subtotal" +msgstr "Eskaera honetako produktuak, kantitatea, prezioa eta azpitotala" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history +msgid "Loading your order…" +msgstr "Zure eskaera kargatzen…" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history +msgid "This page needs JavaScript to load the order into your cart." +msgstr "Orri honek JavaScript behar du eskaera zure saskira kargatzeko." + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history +msgid "Continue to the group order" +msgstr "Jarraitu taldeko eskaerara" + +#. module: website_sale_aplicoop +#. odoo-python +#: code:addons/website_sale_aplicoop/models/js_translations.py:0 +msgid "products found" +msgstr "produktu aurkitu dira" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop +msgid "Load More Products" +msgstr "" +"Produktu Gehiago " +"Kargatu" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta +msgid "" +" Special Order" +msgstr "" +" Eskaera Berezia" + +#. module: website_sale_aplicoop +#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta +msgid "" +" Home Delivery" +msgstr "" +" Etxerako Entrega" diff --git a/website_sale_aplicoop/models/js_translations.py b/website_sale_aplicoop/models/js_translations.py index 83d0058..681f96b 100644 --- a/website_sale_aplicoop/models/js_translations.py +++ b/website_sale_aplicoop/models/js_translations.py @@ -140,9 +140,17 @@ def _register_translations(): _("Search") _("Search products...") _("No products found") + # Announced with a count in the search results live region, e.g. "7 products found" + _("products found") _("Categories") _("All categories") + # ======================== + # Accessible Names + # ======================== + # Dismiss button of the floating notification (website_sale.js) + _("Close") + # ======================== # Category Labels # ======================== diff --git a/website_sale_aplicoop/static/src/css/README.md b/website_sale_aplicoop/static/src/css/README.md index 938b4a0..e2625bf 100644 --- a/website_sale_aplicoop/static/src/css/README.md +++ b/website_sale_aplicoop/static/src/css/README.md @@ -16,7 +16,7 @@ website_sale_aplicoop/static/src/css/ │ ├── base/ │ ├── variables.css ← Variables CSS globales (colores, tipografía, espaciados) -│ └── utilities.css ← Clases utilitarias (.sr-only, .text-muted, etc) +│ └── utilities.css ← Clases utilitarias (.text-muted, .word-wrap-break, etc) │ ├── layout/ │ ├── pages.css ← Fondos y layouts de páginas (eskaera-page, etc) @@ -62,7 +62,8 @@ website_sale_aplicoop/static/src/css/ - **variables.css** (~80 líneas) Colores, tipografía, espaciados, sombras, transiciones, z-index - **utilities.css** (~15 líneas) - Clases utilitarias reutilizables (.sr-only, .text-muted, etc) + Clases utilitarias reutilizables (.text-muted, .word-wrap-break, etc). + Para texto solo-lector-de-pantalla usa `.visually-hidden` de Bootstrap. ### **layout/** - Estructura Global - **pages.css** (~70 líneas) diff --git a/website_sale_aplicoop/static/src/css/base/utilities.css b/website_sale_aplicoop/static/src/css/base/utilities.css index 20613a4..4417aee 100644 --- a/website_sale_aplicoop/static/src/css/base/utilities.css +++ b/website_sale_aplicoop/static/src/css/base/utilities.css @@ -1,34 +1,26 @@ /* filepath: website_sale_aplicoop/static/src/css/base/utilities.css */ /** - * Utility classes used throughout the project + * Utility classes + * + * Screen-reader-only text: use Bootstrap's own `.visually-hidden` / + * `.visually-hidden-focusable`, shipped in web.assets_frontend. The local + * `.sr-only` copy (the Bootstrap 4 name, dropped in 5.x) is gone, as are + * `.word-wrap-break` and `.text-xs`, which no template ever used. */ -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -.text-muted { - color: var(--text-secondary) !important; -} - -.word-wrap-break { - word-wrap: break-word; - overflow-wrap: break-word; -} - -.text-xs { - font-size: 0.85rem; +/* Scoped to the addon's pages: unscoped this redefined Bootstrap's .text-muted + for the whole frontend, and needed !important to do it. Two classes beat + Bootstrap's one, so the flag is no longer necessary. */ +:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) .text-muted { + color: var(--ac-text-secondary); } +/* Set by realtime_search.js to filter the grid. A utility has to beat the + component it lands on — .product-card declares display:flex at the same + specificity and loads later — so this is the one place the flag is the + mechanism rather than a workaround. Bootstrap's own utility layer does the + same for every one of its classes. */ .hidden-product { display: none !important; } diff --git a/website_sale_aplicoop/static/src/css/base/variables.css b/website_sale_aplicoop/static/src/css/base/variables.css index 9152e76..4073997 100644 --- a/website_sale_aplicoop/static/src/css/base/variables.css +++ b/website_sale_aplicoop/static/src/css/base/variables.css @@ -1,72 +1,228 @@ /* filepath: website_sale_aplicoop/static/src/css/base/variables.css */ /** - * CSS Custom Properties (Variables) - * Colores, tipografía, espaciados centralizados + * Design tokens (CSS custom properties) + * + * The ONLY file in this addon allowed to contain colour literals. + * + * Odoo 18 ships Bootstrap 5.3 with `$variable-prefix: ''` + * (web/static/src/scss/bootstrap_overridden.scss), so Bootstrap's root custom + * properties are exposed UNPREFIXED: --primary, --body-bg, --border-color, … + * Brand tokens alias them so the website theme editor keeps control of the palette; + * the literal is only the fallback when the bundle is loaded outside a themed page. + * + * Contrast ratios noted below are against --ac-surface (#fff) and are the reason + * a given literal was chosen. Do not "tidy" them without re-checking WCAG AA. */ :root { - /* ========== COLORS ========== */ - --primary-color: var(--primary, #007bff); - --primary-dark: var(--primary-dark, #0056b3); - --secondary-color: var(--secondary, #6c757d); - --success-color: var(--success, #28a745); - --danger-color: #dc3545; - --warning-color: #ffc107; - --info-color: #17a2b8; - --light-color: #f8f9fa; + /* ========== BRAND ========== */ + --ac-color-primary: var(--primary, #007bff); + /* White text on --ac-color-primary is only 3.98:1 (fails AA for body text). + Use -strong whenever text sits ON the colour: 7.04:1. */ + --ac-color-primary-strong: #0056b3; + --ac-color-secondary: var(--secondary, #6c757d); + --ac-color-accent: #7c3aed; /* white on it: 5.70:1 */ + --ac-color-accent-strong: #6d28d9; /* white on it: 7.10:1 */ + + /* Order cards carry their own indigo accent, and special orders an amber one. */ + --ac-color-order: #5a67d8; /* white on it: 4.81:1 */ + --ac-color-order-strong: #4c57bd; /* 6.17:1 */ + --ac-color-order-deep: #3d4898; /* 8.14:1 */ + --ac-color-order-border: rgb(90 103 216 / 12%); + --ac-color-order-border-hover: rgb(90 103 216 / 25%); + --ac-surface-order: linear-gradient(180deg, #fff 0%, #fbfdff 100%); + --ac-surface-order-hover: linear-gradient(180deg, #fff 0%, #f7faff 100%); + + --ac-color-special: #f59e0b; + --ac-color-special-strong: #d97706; + --ac-color-special-border: rgb(245 158 11 / 35%); + --ac-color-special-border-hover: rgb(245 158 11 / 50%); + --ac-surface-special: linear-gradient(180deg, #fffdf0 0%, #fffbea 100%); + --ac-surface-special-hover: linear-gradient(180deg, #fffdf0 0%, #fff8db 100%); + /* White on amber is 2.15:1. Amber always takes dark text: 7.60:1. */ + --ac-text-on-special: #1a202c; + + /* ========== FEEDBACK ========== */ + --ac-color-success: var(--success, #28a745); + /* #28a745 is only 3.13:1 on white — fine as a fill, not as text. */ + --ac-color-success-strong: #1e7e34; /* 5.14:1 */ + --ac-color-success-deep: #1a6e2e; /* hover/active on -strong */ + + /* Warning alert: amber surface with its own readable foreground (6.30:1). */ + --ac-surface-warning: #fef3c7; + --ac-border-warning: #fcd34d; + --ac-text-warning: #92400e; + --ac-color-danger: var(--danger, #dc3545); + --ac-color-warning: var(--warning, #ffc107); + --ac-color-info: var(--info, #17a2b8); + + /* ========== SURFACES ========== */ + --ac-surface: var(--body-bg, #fff); + --ac-surface-sunken: var(--light, #f8f9fa); + --ac-surface-muted: #f3f3f3; + --ac-surface-tint: rgb(0 123 255 / 6%); + --ac-surface-tint-hover: rgb(108 117 125 / 10%); + + /* ========== TEXT (all ≥ 4.5:1 on --ac-surface) ========== */ + --ac-text-primary: var(--body-color, #1a202c); /* 16.32:1 */ + --ac-text-secondary: #4a5568; /* 7.53:1 */ + --ac-text-muted: #6b7280; /* 4.83:1 */ + --ac-text-on-fill: #fff; + + /* ========== BORDERS ========== */ + /* Decorative separators and card outlines. */ + --ac-border: var(--border-color, #e2e8f0); + --ac-border-subtle: #e0e0e0; + /* Boundaries of interactive controls (inputs, steppers): WCAG 1.4.11 needs + ≥ 3:1 against the adjacent surface. #8a919a is 3.18:1. */ + --ac-border-strong: #8a919a; + + /* ========== FOCUS RING ========== */ + --ac-focus-ring-color: var(--primary, #007bff); /* 3.98:1 — ok for a focus indicator */ + --ac-focus-ring-width: 2px; + --ac-focus-ring-offset: 2px; + + /* ========== TYPOGRAPHY ========== */ + --ac-font-family: var( + --font-sans-serif, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + "Helvetica Neue", + Arial, + sans-serif + ); + + /* Fluid scale: one continuous ramp instead of per-breakpoint overrides. + Always rem + vw, never pure vw, so browser zoom keeps working. */ + --ac-text-2xs: clamp(0.6875rem, 0.66rem + 0.1vw, 0.75rem); + --ac-text-xs: clamp(0.75rem, 0.72rem + 0.12vw, 0.8125rem); + --ac-text-sm: clamp(0.8125rem, 0.79rem + 0.14vw, 0.875rem); + --ac-text-base: clamp(0.875rem, 0.85rem + 0.18vw, 1rem); + --ac-text-md: clamp(1rem, 0.96rem + 0.2vw, 1.125rem); + --ac-text-lg: clamp(1.125rem, 1.06rem + 0.3vw, 1.375rem); + --ac-text-xl: clamp(1.375rem, 1.25rem + 0.6vw, 1.75rem); + --ac-text-2xl: clamp(1.75rem, 1.5rem + 1vw, 2.25rem); + + --ac-leading-tight: 1.2; + --ac-leading-snug: 1.35; + --ac-leading-normal: 1.5; + + --ac-weight-normal: 400; + --ac-weight-medium: 500; + --ac-weight-semibold: 600; + --ac-weight-bold: 700; + + /* ========== SPACING ========== */ + --ac-space-3xs: 0.125rem; + --ac-space-2xs: 0.25rem; + --ac-space-xs: 0.375rem; + --ac-space-sm: 0.5rem; + --ac-space-md: 1rem; + --ac-space-lg: 1.5rem; + --ac-space-xl: 2rem; + --ac-space-2xl: 3rem; + + /* ========== RADIUS ========== */ + --ac-radius-sm: 0.25rem; + --ac-radius-md: 0.5rem; + --ac-radius-lg: 0.75rem; + --ac-radius-xl: 1rem; + --ac-radius-pill: 50rem; + + /* ========== ELEVATION ========== */ + --ac-shadow-sm: 0 2px 8px rgb(0 0 0 / 8%); + --ac-shadow-md: 0 4px 12px rgb(0 0 0 / 10%); + --ac-shadow-lg: 0 10px 25px rgb(0 0 0 / 12%); + + /* ========== MOTION ========== */ + --ac-transition-fast: 150ms ease-out; + --ac-transition-normal: 220ms cubic-bezier(0.2, 0.9, 0.2, 1); + --ac-transition-slow: 400ms ease; + + /* ========== CONTROLS ========== */ + /* Fluid control size: replaces six per-breakpoint width/height overrides. */ + --ac-control-size: clamp(2.25rem, 2.05rem + 0.7vw, 2.5rem); + /* 44px. WCAG 2.2 AA (2.5.8) only requires 24px, but 44px is the AAA figure + (2.5.5) and the practical minimum for touch. Applied on coarse pointers. */ + --ac-touch-target: 2.75rem; + --ac-control-border-width: 1px; + + /* ========== PRODUCT CARD MEDIA ========== */ + --ac-card-media-ratio: 4 / 3; + --ac-card-media-height: clamp(5rem, 4rem + 6vw, 8.5rem); + + /* ========== Z-INDEX ========== */ + --ac-z-dropdown: 1000; + --ac-z-sticky: 1020; + --ac-z-fixed: 1030; + --ac-z-modal: 1040; + --ac-z-popover: 1050; + --ac-z-tooltip: 1060; + --ac-z-notification: 9999; + + /* ========================================================================== + LEGACY ALIASES — do not use in new code. + Kept so the not-yet-migrated partials keep rendering. Delete each one once + `grep -rn "var(--)" static/src/` comes back empty. + ========================================================================== */ + --primary-color: var(--ac-color-primary); + /* Was `var(--primary-dark, #0056b3)`: a custom property cannot reference + itself — that made the declaration invalid at computed-value time, so the + five consumers in buttons.css / order-card.css silently rendered with no + colour at all. */ + --primary-dark: var(--ac-color-primary-strong); + --secondary-color: var(--ac-color-secondary); + --success-color: var(--ac-color-success); + --danger-color: var(--ac-color-danger); + --warning-color: var(--ac-color-warning); + --info-color: var(--ac-color-info); + --light-color: var(--ac-surface-sunken); --dark-color: #2d3748; - /* Text colors */ - --text-primary: #1a202c; - --text-secondary: #4a5568; - --text-muted: #6b7280; + --text-primary: var(--ac-text-primary); + --text-secondary: var(--ac-text-secondary); + --text-muted: var(--ac-text-muted); - /* Border colors */ - --border-light: #e2e8f0; + --border-light: var(--ac-border); --border-medium: #cbd5e0; --border-dark: #718096; - /* ========== TYPOGRAPHY ========== */ - --font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", - Arial, sans-serif; + --font-family-base: var(--ac-font-family); --font-weight-light: 300; - --font-weight-normal: 400; - --font-weight-medium: 500; - --font-weight-semibold: 600; - --font-weight-bold: 700; + --font-weight-normal: var(--ac-weight-normal); + --font-weight-medium: var(--ac-weight-medium); + --font-weight-semibold: var(--ac-weight-semibold); + --font-weight-bold: var(--ac-weight-bold); --font-weight-extrabold: 800; - /* ========== SPACING ========== */ - --spacing-xs: 0.25rem; - --spacing-sm: 0.5rem; - --spacing-md: 1rem; - --spacing-lg: 1.5rem; - --spacing-xl: 2rem; - --spacing-2xl: 3rem; + --spacing-xs: var(--ac-space-2xs); + --spacing-sm: var(--ac-space-sm); + --spacing-md: var(--ac-space-md); + --spacing-lg: var(--ac-space-lg); + --spacing-xl: var(--ac-space-xl); + --spacing-2xl: var(--ac-space-2xl); - /* ========== BORDER RADIUS ========== */ - --radius-sm: 0.25rem; - --radius-md: 0.5rem; - --radius-lg: 0.75rem; - --radius-xl: 1rem; + --radius-sm: var(--ac-radius-sm); + --radius-md: var(--ac-radius-md); + --radius-lg: var(--ac-radius-lg); + --radius-xl: var(--ac-radius-xl); - /* ========== SHADOWS ========== */ - --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1); - --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.12); + --shadow-sm: var(--ac-shadow-sm); + --shadow-md: var(--ac-shadow-md); + --shadow-lg: var(--ac-shadow-lg); - /* ========== TRANSITIONS ========== */ --transition-fast: 200ms ease; - --transition-normal: 320ms cubic-bezier(0.2, 0.9, 0.2, 1); + --transition-normal: var(--ac-transition-normal); --transition-slow: 500ms ease; - /* ========== Z-INDEX ========== */ - --z-dropdown: 1000; - --z-sticky: 1020; - --z-fixed: 1030; - --z-modal: 1040; - --z-popover: 1050; - --z-tooltip: 1060; - --z-notification: 9999; + --z-dropdown: var(--ac-z-dropdown); + --z-sticky: var(--ac-z-sticky); + --z-fixed: var(--ac-z-fixed); + --z-modal: var(--ac-z-modal); + --z-popover: var(--ac-z-popover); + --z-tooltip: var(--ac-z-tooltip); + --z-notification: var(--ac-z-notification); } diff --git a/website_sale_aplicoop/static/src/css/components/alerts.css b/website_sale_aplicoop/static/src/css/components/alerts.css index 11f4812..bb11925 100644 --- a/website_sale_aplicoop/static/src/css/components/alerts.css +++ b/website_sale_aplicoop/static/src/css/components/alerts.css @@ -1,77 +1,68 @@ /* filepath: website_sale_aplicoop/static/src/css/components/alerts.css */ /** - * Alert and notification component styles + * Alerts, toasts and notifications + * + * `.alert-warning` is scoped to the addon's pages: unscoped it redefined + * Bootstrap's own alert for the whole frontend. `.group-order-alert` and its + * .info/.warning variants were dead — no template emits them. */ -.group-order-alert { - background-color: #e7f3ff; - border-left: 4px solid var(--primary-color); - color: #004085; - padding: 1rem; - border-radius: 0.375rem; +:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) + .alert-warning { + --alert-bg: var(--ac-surface-warning); + --alert-border-color: var(--ac-border-warning); + --alert-color: var(--ac-text-warning); + margin-bottom: var(--ac-space-xl); + padding: var(--ac-space-md); + border-radius: var(--ac-radius-md); } -.group-order-alert.info { - background-color: #d1ecf1; - border-left-color: #17a2b8; - color: #0c5460; +:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) + .alert-warning + i { + margin-right: var(--ac-space-xs); + font-size: var(--ac-text-md); } -.group-order-alert.warning { - background-color: #fff3cd; - border-left-color: #ffc107; - color: #856404; +:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) + .alert-warning + strong { + font-weight: var(--ac-weight-bold); } -.alert-warning { - background-color: #fef3c7; - border-color: #fcd34d; - color: #92400e; - margin-bottom: 2rem; - border-radius: 0.5rem; - padding: 1.25rem; +.eskaera-delivery-notice { + margin-top: var(--ac-space-md); + border-left: 4px solid var(--ac-color-info); } -.alert-warning i { - margin-right: 0.75rem; - font-size: 1.1rem; +.eskaera-delivery-notice .fa-truck { + margin-right: var(--ac-space-sm); + color: var(--ac-color-info); } -.alert-warning strong { - font-weight: 700; -} +/* ---------- Toast (added to cart) ---------- */ -#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 { position: fixed; - bottom: 30px; - right: 30px; - background-color: #28a745; - color: white; - padding: 1rem 1.5rem; - border-radius: 4px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + inset: auto var(--ac-space-lg) var(--ac-space-lg) auto; + z-index: var(--ac-z-notification); display: flex; align-items: center; - gap: 0.75rem; - font-weight: 500; - z-index: 9999; + gap: var(--ac-space-xs); + max-width: 25rem; + padding: var(--ac-space-md) var(--ac-space-lg); + padding-bottom: calc(var(--ac-space-md) + env(safe-area-inset-bottom, 0px)); + border-radius: var(--ac-radius-sm); + /* White on --ac-color-success is 3.13:1; the strong tone is 5.14:1. */ + background-color: var(--ac-color-success-strong); + color: var(--ac-text-on-fill); + box-shadow: var(--ac-shadow-lg); + font-weight: var(--ac-weight-medium); + word-break: break-word; opacity: 0; transform: translateY(20px); - transition: opacity 0.3s ease, transform 0.3s ease; - max-width: 400px; - word-break: break-word; + transition: opacity var(--ac-transition-normal), transform var(--ac-transition-normal); } .toast-notification.show { @@ -80,6 +71,41 @@ } .toast-notification i { - font-size: 1.2rem; - min-width: 20px; + min-width: 1.25rem; + font-size: var(--ac-text-md); +} + +/* ---------- Floating notification (_showNotification) ---------- */ + +/* Was a 200-character inline style written from JS on every call. */ +.eskaera-notification { + position: fixed; + top: 5rem; + right: var(--ac-space-md); + z-index: var(--ac-z-notification); + min-width: 18.75rem; + max-width: 31.25rem; + padding: var(--ac-space-lg); + font-size: var(--ac-text-md); + box-shadow: var(--ac-shadow-lg); + transition: opacity var(--ac-transition-normal); +} + +@media (max-width: 576px) { + .eskaera-notification { + left: var(--ac-space-md); + right: var(--ac-space-md); + min-width: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + .toast-notification, + .eskaera-notification { + transition: none; + } + + .toast-notification { + transform: none; + } } diff --git a/website_sale_aplicoop/static/src/css/components/buttons.css b/website_sale_aplicoop/static/src/css/components/buttons.css index c593f29..130f629 100644 --- a/website_sale_aplicoop/static/src/css/components/buttons.css +++ b/website_sale_aplicoop/static/src/css/components/buttons.css @@ -1,103 +1,102 @@ /* filepath: website_sale_aplicoop/static/src/css/components/buttons.css */ /** - * Button and action component styles + * Buttons + * + * Colours are set through Bootstrap 5.3's own --btn-* custom properties, which + * Odoo exposes unprefixed. That wins on the cascade without !important and + * keeps every state (hover, active, disabled) consistent, instead of + * overriding background-color and leaving the others behind. + * + * `.btn-add-to-cart` and `.btn-checkout` used to live here; no template or + * script emits either name — the real ones are `.add-to-cart-btn` (see + * components/quantity-control.css) and `.checkout-btn-lg`. */ -.btn-add-to-cart { - background-color: var(--primary-color); - border-color: var(--primary-color); - color: white; +:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) + :is(.btn-primary, .btn-success) { + font-weight: var(--ac-weight-semibold); } -.btn-add-to-cart:focus { - outline: 3px solid var(--primary-color); - outline-offset: 2px; -} +/* ---------- Checkout actions ---------- */ -.btn-add-to-cart:hover { - background-color: var(--primary-dark); - border-color: var(--primary-dark); -} - -.btn-checkout { - background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%); - border: none; - color: white; - font-weight: 600; -} - -.btn-checkout:focus { - outline: 3px solid #667eea; - outline-offset: 2px; -} - -.btn-checkout:hover { - background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-dark) 100%); - color: white; -} - -.btn-primary, -.btn-success { - font-weight: 600; -} - -.btn-primary:disabled, -.btn-success:disabled { - opacity: 0.65; -} - -/* Checkout action buttons */ .checkout-actions { - margin-top: 2rem; + margin-top: var(--ac-space-xl); } .checkout-actions .btn { - font-size: 1.1rem; - font-weight: 600; - padding: 0.75rem 1.5rem; - border-radius: 0.5rem; - transition: all 0.3s ease; + --btn-border-radius: var(--ac-radius-md); + --btn-font-size: var(--ac-text-md); + --btn-font-weight: var(--ac-weight-semibold); + --btn-padding-y: var(--ac-space-xs); + --btn-padding-x: var(--ac-space-lg); text-transform: uppercase; - letter-spacing: 0.5px; -} - -.checkout-actions .btn-success { - background-color: var(--success-color); - border-color: var(--success-color); -} - -.checkout-actions .btn-success:hover { - background-color: #218838; - border-color: #218838; - box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3); - transform: translateY(-2px); -} - -.checkout-actions .btn-outline-secondary { - color: #ebeef0; - border-color: #cad2d8; -} - -.checkout-actions .btn-outline-secondary:hover { - color: white; - background-color: #6c757d; - border-color: #6c757d; - box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3); + letter-spacing: 0.05em; + transition: background-color var(--ac-transition-fast), border-color var(--ac-transition-fast), + box-shadow var(--ac-transition-fast), transform var(--ac-transition-fast); } .checkout-actions .btn i { - margin-right: 0.5rem; + margin-right: var(--ac-space-sm); } +.checkout-actions .btn-success { + --btn-bg: var(--ac-color-success-strong); + --btn-border-color: var(--ac-color-success-strong); + --btn-color: var(--ac-text-on-fill); + --btn-hover-bg: var(--ac-color-success-deep); + --btn-hover-border-color: var(--ac-color-success-deep); + --btn-hover-color: var(--ac-text-on-fill); +} + +/* "Back to cart". The previous #ebeef0 text sat at 1.17:1 on the white + checkout card — the label was invisible until hover. */ +.checkout-actions .btn-outline-secondary { + --btn-color: var(--ac-text-secondary); + --btn-border-color: var(--ac-border-strong); + --btn-hover-color: var(--ac-text-on-fill); + --btn-hover-bg: var(--ac-color-secondary); + --btn-hover-border-color: var(--ac-color-secondary); +} + +/* ---------- Long-label buttons ---------- */ + .save-order-btn, -.save-order-btn-styled, .checkout-btn-lg { + padding: var(--ac-space-sm) var(--ac-space-md); + font-size: var(--ac-text-md); white-space: nowrap; - font-size: 1.1rem; - padding: 0.6rem 1.2rem; } -.save-icon-size { - font-size: 1.2rem; +/* ---------- States ---------- */ + +@media (hover: hover) and (pointer: fine) { + .checkout-actions .btn-success:hover, + .checkout-actions .btn-outline-secondary:hover { + transform: translateY(-2px); + box-shadow: var(--ac-shadow-md); + } +} + +.checkout-actions .btn:focus-visible, +.save-order-btn:focus-visible, +.checkout-btn-lg:focus-visible { + outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color); + outline-offset: var(--ac-focus-ring-offset); +} + +@media (pointer: coarse) { + .checkout-actions .btn, + .save-order-btn, + .checkout-btn-lg { + min-height: var(--ac-touch-target); + } +} + +@media (prefers-reduced-motion: reduce) { + .checkout-actions .btn, + .checkout-actions .btn:hover { + transform: none; + transition: none; + } } diff --git a/website_sale_aplicoop/static/src/css/components/cart.css b/website_sale_aplicoop/static/src/css/components/cart.css index 7ddab7e..2d208f1 100644 --- a/website_sale_aplicoop/static/src/css/components/cart.css +++ b/website_sale_aplicoop/static/src/css/components/cart.css @@ -1,236 +1,177 @@ /* filepath: website_sale_aplicoop/static/src/css/components/cart.css */ /** - * Shopping cart component styles + * Cart sidebar + * + * Markup (eskaera_shop): + * .card.sticky-top.cart-sticky-position + * ├ .card-header → #cart-title + .btn-group.cart-btn-group (4 icon buttons) + * ├ .card-body.eskaera-cart-items[aria-live=polite] + * │ └ injected by website_sale.js: .list-group > .list-group-item, .cart-total + * └ .card-footer → clear cart + checkout + * + * The .list-group-item rules are scoped to the container: unscoped, they hit + * every Bootstrap list group on the site, which is what forced three rounds of + * re-declaration in layout/responsive.css. */ -.sticky-cart { - top: 20px; +.cart-sticky-position { + top: var(--ac-space-md); } -.cart-header { - background-color: var(--primary-color); - color: white; - padding: 0.25rem; - border-radius: 0.25rem 0.25rem 0 0; -} - -.cart-header h5 { - color: white; - margin: 0; -} - -.cart-header .btn { - color: white; -} - -.cart-items { - max-height: 400px; +/* Cap the scroll area against the viewport, not a fixed pixel height, so it + still fits when text is enlarged. */ +.cart-items, +.eskaera-cart-items { + max-height: min(60vh, 25rem); overflow-y: auto; } -#cart-items-container { - padding: 0.2rem 0.4rem; +.eskaera-cart-items { + padding: var(--ac-space-2xs) var(--ac-space-xs); + font-size: var(--ac-text-base); } -#cart-items-container .list-group { +.eskaera-cart-items .list-group { margin-bottom: 0; } -#cart-items-container p.text-muted { - margin: 0.4rem 0; +.eskaera-cart-items p.text-muted { + margin: var(--ac-space-2xs) 0; } -.list-group-item { - padding: 0.4rem 0.6rem; - border-bottom: 1px solid #e0e0e0; +.cart-empty-message { + font-size: var(--ac-text-base); } -.list-group-item.d-flex { - gap: 0.5rem; +/* ---------- Cart lines ---------- */ + +.eskaera-cart-items .list-group-item { + display: flex; flex-direction: column; + gap: var(--ac-space-2xs); + padding: var(--ac-space-xs) var(--ac-space-sm); + border-bottom: 1px solid var(--ac-border); + font-size: var(--ac-text-base); } -.list-group-item h6 { - margin: 0; - margin-bottom: 0.2rem; - font-weight: 600; +.eskaera-cart-items .list-group-item h6 { + margin: 0 0 var(--ac-space-3xs); + font-size: var(--ac-text-base); + font-weight: var(--ac-weight-semibold); + line-height: var(--ac-leading-snug); + overflow-wrap: break-word; word-break: break-word; - font-size: 0.875rem; - line-height: 1.3; } -.list-group-item small { +.eskaera-cart-items .list-group-item small { display: block; - font-size: 0.75rem; - color: #6c757d; + font-size: var(--ac-text-xs); + color: var(--ac-text-muted); } -.list-group-item .d-flex { - min-width: auto; +.eskaera-cart-items .list-group-item .d-flex { justify-content: space-between; align-items: center; - gap: 0.5rem; + gap: var(--ac-space-sm); flex-wrap: wrap; } -.list-group-item .remove-from-cart { - flex-shrink: 0; - min-width: 24px; - min-height: 24px; +/* Line subtotal. The previous #667eea was 3.42:1 at this size — under AA. */ +.eskaera-cart-items .list-group-item strong { + min-width: 3.75rem; + font-size: var(--ac-text-base); + text-align: right; + white-space: nowrap; + color: var(--ac-color-primary-strong); +} + +.eskaera-cart-items .list-group-item > div:first-child { + flex: 1 1 auto; + min-width: 0; +} + +/* Colours come from Bootstrap's .btn-danger, already on the element. */ +.remove-from-cart { display: inline-flex; align-items: center; justify-content: center; - padding: 0.15rem 0.25rem; - font-size: 0.7rem; + flex-shrink: 0; + /* 24px is the WCAG 2.2 AA floor (2.5.8); touch gets the full target below. */ + min-width: 1.5rem; + min-height: 1.5rem; + padding: var(--ac-space-3xs) var(--ac-space-2xs); + font-size: var(--ac-text-2xs); cursor: pointer; - background-color: #dc3545; - color: #ffffff; - border: 1px solid #dc3545; - border-radius: 3px; - transition: all 0.2s ease; -} - -.list-group-item .remove-from-cart:hover { - background-color: #bb2d3b; - border-color: #bb2d3b; - transform: scale(1.05); -} - -.list-group-item .remove-from-cart i { - font-size: 0.85rem; - color: #ffffff; -} - -.list-group-item strong { - min-width: 60px; - text-align: right; - white-space: nowrap; - font-size: 0.875rem; - color: #667eea; -} - -.list-group-item.d-flex > div:first-child { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; -} - -.list-group-item.d-flex > div:first-child h6 { - word-wrap: break-word; - overflow-wrap: break-word; - white-space: normal; + transition: background-color var(--ac-transition-fast), transform var(--ac-transition-fast); } .cart-total { - padding: 1rem; - background-color: #f8f9fa; - font-size: 1.5rem; - font-weight: 700; - border-top: 2px solid var(--primary-color); + padding: var(--ac-space-md); + border-top: 2px solid var(--ac-color-primary); + background-color: var(--ac-surface-sunken); + font-size: var(--ac-text-lg); + font-weight: var(--ac-weight-bold); text-align: right; - color: var(--primary-color); + color: var(--ac-color-primary-strong); } -.cart-item-remove { - cursor: pointer; - color: #dc3545; - font-size: 0.7rem; -} +/* ---------- Header actions ---------- */ -.cart-item-remove:hover { - text-decoration: underline; -} - -/* Cart header buttons styling */ -#save-cart-btn, -#reload-cart-btn { - font-weight: 600; - padding: 0.5rem 0.75rem; - border-radius: 0.375rem; - transition: all 0.2s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - margin-right: 0.25rem; - color: white; -} - -#save-cart-btn { - background-color: #007bff; - border-color: #0056b3; -} - -#save-cart-btn:hover { - background-color: #0056b3; - border-color: #004085; - box-shadow: 0 4px 8px rgba(0, 86, 179, 0.3); -} - -#save-cart-btn:active { - transform: scale(0.95); - box-shadow: 0 2px 4px rgba(0, 86, 179, 0.3); -} - -#reload-cart-btn { - background-color: #17a2b8; - border-color: #117a8b; -} - -#reload-cart-btn:hover { - background-color: #117a8b; - border-color: #0c5460; - box-shadow: 0 4px 8px rgba(17, 122, 139, 0.3); -} - -#reload-cart-btn:active { - transform: scale(0.95); - box-shadow: 0 2px 4px rgba(17, 122, 139, 0.3); -} - -.card-header .btn-group { - flex-shrink: 0; -} - -#cart-title { - font-size: 1.2rem; -} - -.cart-btn-group-nowrap, +.card-header .btn-group, .cart-btn-group { + flex-shrink: 0; flex-wrap: nowrap; } -.cart-header-btn { - padding: 0.25rem; +/* These four icon buttons were 0.5rem type with 0.1rem padding — roughly an + 18px target, under the 24px AA minimum, for the cart's primary actions. */ +.cart-btn-compact { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2rem; + min-height: 2rem; + padding: var(--ac-space-2xs); + line-height: 1; } .cart-icon-size { - font-size: 0.9rem; -} - -.cart-body-text { - font-size: 1.1rem; -} - -.cart-body-lg { - font-size: 1.1rem; -} - -.cart-title-lg { - font-size: 1.5rem; + font-size: var(--ac-text-base); } .cart-title-sm { - font-size: 1.1rem; + font-size: var(--ac-text-md); } -.cart-btn-compact { - padding: 0.1rem; - min-width: auto; - font-size: 0.5rem; - line-height: 0.5; - height: auto; +/* ---------- States ---------- */ + +@media (hover: hover) and (pointer: fine) { + .remove-from-cart:hover { + transform: scale(1.05); + } } -.cart-sticky-position { - top: 20px; +.remove-from-cart:focus-visible, +.cart-btn-compact:focus-visible { + outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color); + outline-offset: var(--ac-focus-ring-offset); + position: relative; + z-index: 1; +} + +@media (pointer: coarse) { + .cart-btn-compact, + .remove-from-cart { + min-width: var(--ac-touch-target); + min-height: var(--ac-touch-target); + } +} + +@media (prefers-reduced-motion: reduce) { + .remove-from-cart, + .remove-from-cart:hover { + transform: none; + transition: none; + } } diff --git a/website_sale_aplicoop/static/src/css/components/forms.css b/website_sale_aplicoop/static/src/css/components/forms.css index 17635bc..156f327 100644 --- a/website_sale_aplicoop/static/src/css/components/forms.css +++ b/website_sale_aplicoop/static/src/css/components/forms.css @@ -1,99 +1,129 @@ /* filepath: website_sale_aplicoop/static/src/css/components/forms.css */ /** - * Form and input component styles + * Form controls + * + * Everything here is scoped to an Eskaera page root. The previous version + * styled bare `label`, `input[type="text"]`, `input[type="number"]` and + * `select`, which reaches every form in web.assets_frontend — the website + * login, the contact form, any other module's frontend widgets — not just this + * addon's pages. */ -label { - font-weight: 600; - color: #2d3748; +:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) label { + font-weight: var(--ac-weight-semibold); + color: var(--ac-text-secondary); } -.form-control, -.form-select { - border-color: #cbd5e0; - font-size: 1rem; +:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) + :is(.form-control, .form-select, input[type="text"], input[type="number"], select) { + /* WCAG 1.4.11: an input's border is what identifies it, so ≥ 3:1. */ + border: 1px solid var(--ac-border-strong); + font-size: var(--ac-text-base); + color: var(--ac-text-primary); + background-color: var(--ac-surface); } -.form-control:focus, -.form-select:focus { - border-color: var(--primary-color); - box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +/* One focus treatment for every control on these pages, keyboard only. */ +:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) + :is(.form-control, .form-select, .form-check-input, input, select, textarea):focus-visible { + outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color); + outline-offset: var(--ac-focus-ring-offset); + border-color: var(--ac-color-primary); } -input[type="number"], -input[type="text"], -select { - background-color: #ffffff; - color: #212529; - border: 1px solid #ced4da; +/* Bootstrap draws its own ring through a box-shadow on :focus; keep it in + token colours so mouse focus still reads, without a second outline. */ +:is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) + :is(.form-control, .form-select):focus { + border-color: var(--ac-color-primary); + box-shadow: 0 0 0 0.2rem rgb(0 123 255 / 20%); } -input[type="number"]:focus, -input[type="text"]:focus, -select:focus { - outline: 2px solid #667eea; - outline-offset: 0; - border-color: var(--primary-color); +/* ---------- Search and category filter ---------- */ + +.eskaera-filters :is(.form-control, .form-select) { + height: auto; + padding: var(--ac-space-xs) var(--ac-space-sm); + font-size: var(--ac-text-base); } -.form-check { +/* The clear-search button sits inside the input; give it a real target. */ +.search-clear-btn { + position: absolute; + inset-block-start: 50%; + inset-inline-end: var(--ac-space-2xs); + transform: translateY(-50%); + display: none; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + padding: 0; + border: none; + background: none; + font-size: var(--ac-text-lg); + line-height: 1; + color: var(--ac-text-secondary); + text-decoration: none; +} + +.search-clear-btn.is-visible { + display: flex; +} + +.search-input-wrapper { + position: relative; +} + +.search-input-wrapper .form-control { + padding-inline-end: 2.5rem; +} + +/* ---------- Home delivery checkbox ---------- */ + +.eskaera-delivery-check { display: flex; align-items: center; - padding: 1rem 1.25rem; - margin-left: 0; + gap: var(--ac-space-sm); + padding: var(--ac-space-md); } -.form-check-input { - margin-right: 0.75rem; +.eskaera-delivery-check .form-check-input { flex-shrink: 0; -} - -/* Compact search and filter inputs */ -#realtime-search-input, -#realtime-category-select { - font-size: 0.95rem; - padding: 0.375rem 0.75rem; - height: auto; -} - -#realtimeSearch-filters .form-control, -#realtimeSearch-filters .form-select { - padding: 0.375rem 0.75rem; - font-size: 0.95rem; -} - -.form-check-label { - margin: 0; -} - -#home-delivery-checkbox { width: 1.5rem; height: 1.5rem; + margin: 0; + border: 2px solid var(--ac-color-primary); + border-radius: var(--ac-radius-sm); cursor: pointer; - border: 2px solid #0d6efd; - border-radius: 0.25rem; - margin-right: 0.75rem; } -#home-delivery-checkbox:checked { - background-color: #0d6efd; - border-color: #0d6efd; +.eskaera-delivery-check .form-check-input:checked { + border-color: var(--ac-color-primary); + background-color: var(--ac-color-primary); } -#home-delivery-checkbox:focus { - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); -} - -.form-check-label[for="home-delivery-checkbox"] { - font-size: 1.1rem; - font-weight: 600; - color: #212529; - cursor: pointer; +.eskaera-delivery-check .form-check-label { display: flex; align-items: center; + margin: 0; + font-size: var(--ac-text-md); + font-weight: var(--ac-weight-semibold); + color: var(--ac-text-primary); + cursor: pointer; } -.help-text-sm { - font-size: 0.85rem; +/* ---------- Touch ---------- */ + +@media (pointer: coarse) { + :is(.eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page) + :is(.form-control, .form-select) { + min-height: var(--ac-touch-target); + } + + .search-clear-btn { + width: var(--ac-touch-target); + height: var(--ac-touch-target); + } } diff --git a/website_sale_aplicoop/static/src/css/components/order-card.css b/website_sale_aplicoop/static/src/css/components/order-card.css index 72e51c7..e9a3633 100644 --- a/website_sale_aplicoop/static/src/css/components/order-card.css +++ b/website_sale_aplicoop/static/src/css/components/order-card.css @@ -1,30 +1,43 @@ /* filepath: website_sale_aplicoop/static/src/css/components/order-card.css */ /** - * Order card (Eskaera) component styles + * Order card (Eskaera) + * + * Markup (eskaera_page): + * .eskaera-order-card-wrapper + * └ .eskaera-order-card[.eskaera-order-card--special] + * └ .card-body + * ├ .order-badge-position (product count) + * ├ a.eskaera-order-card-link → .card-header-top (image, title, description) + * ├ .card-meta-compact > .meta-grid (dates, badges) + * └ a.btn (Browse products) + * + * Note the link only wraps the header, so the whole card is NOT clickable — + * the hover/focus treatment below is applied to the card while the pointer is + * anywhere over it, but `cursor: pointer` is left to the two real links. */ .eskaera-order-card-link { - text-decoration: none; - color: inherit; display: block; width: 100%; + color: inherit; + text-decoration: none; } .eskaera-order-card { position: relative; - background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%); - border: 1px solid rgba(90, 103, 216, 0.12); - border-radius: 0.75rem; - box-shadow: 0 6px 18px rgba(28, 37, 80, 0.06); - transition: transform 320ms cubic-bezier(0.2, 0.9, 0.2, 1), box-shadow 320ms, border-color 320ms, - background 320ms; - overflow: hidden; display: flex; flex-direction: column; - min-height: 290px; + /* min-height in rem, not px: at 200% zoom a px floor clips the content. */ + min-height: 18rem; height: 100%; - cursor: pointer; + border: 1px solid var(--ac-color-order-border); + border-radius: var(--ac-radius-lg); + background: var(--ac-surface-order); + box-shadow: var(--ac-shadow-md); + overflow: hidden; + transition: transform var(--ac-transition-normal), box-shadow var(--ac-transition-normal), + border-color var(--ac-transition-normal), background var(--ac-transition-normal); animation: slideInUp 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both; } @@ -33,371 +46,281 @@ opacity: 0; transform: translateY(30px); } + 100% { opacity: 1; transform: translateY(0); } } +/* Accent bar along the top edge. */ .eskaera-order-card::before { content: ""; position: absolute; - left: 0; - right: 0; - top: 0; + inset: 0 0 auto; height: 6px; - background: linear-gradient(90deg, var(--primary-color), var(--primary-dark)); + background: linear-gradient(90deg, var(--ac-color-order), var(--ac-color-order-strong)); } -.eskaera-order-card-link:hover .eskaera-order-card { - transform: translateY(-8px) scale(1.01); - box-shadow: 0 20px 50px rgba(90, 103, 216, 0.15), 0 0 30px rgba(90, 103, 216, 0.1); - border: 1px solid rgba(90, 103, 216, 0.25); - background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%); -} - -.eskaera-order-card-link:hover .eskaera-order-card::before { - animation: shimmer 0.6s ease-in-out; -} - -@keyframes shimmer { - 0% { - left: 0; - } - 50% { - left: 100%; - } - 100% { - left: 0; - } -} - -.eskaera-order-card .card-body { - padding: 0.6rem 0.8rem 0 0.8rem; - flex-grow: 1; - display: flex; - flex-direction: column; - gap: 0.2rem; -} - -.eskaera-order-card .card-title { - font-size: 0.95rem; - font-weight: 700; - color: #1f2937; - margin: 0; - line-height: 1.15; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} - -.order-desc-text { - font-size: 0.8rem; - color: #6b7280; - margin: 0; - line-height: 1.3; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} - -.order-desc-sm { - font-size: 0.875rem; - line-height: 1.4; -} - -.order-desc-md { - font-size: 0.95rem; - line-height: 1.5; -} - -.eskaera-order-card .btn { - margin-top: auto; - margin-left: auto; - margin-right: auto; - margin-bottom: 0.6rem; - padding: 0.6rem 1.2rem; - font-weight: 600; - border-radius: 6px; - border: none !important; - font-size: 0.85rem; - transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1); - background: linear-gradient(135deg, #5a67d8, #4c57bd) !important; - color: white !important; - box-shadow: 0 4px 12px rgba(90, 103, 216, 0.2) !important; - display: inline-flex; - align-items: center; - gap: 0.5rem; - text-decoration: none !important; - position: relative; - overflow: hidden; - text-align: center; - width: auto; -} - -.eskaera-order-card .btn::before { - content: ""; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - border-radius: 50%; - background: rgba(255, 255, 255, 0.3); - transform: translate(-50%, -50%); - transition: width 0.5s, height 0.5s; -} - -.eskaera-order-card .btn:active::before { - width: 300px; - height: 300px; -} - -.eskaera-order-card .btn:hover { - box-shadow: 0 8px 24px rgba(90, 103, 216, 0.4), inset 0 0 20px rgba(255, 255, 255, 0.2) !important; - transform: translateY(-3px) scale(1.03); - background: linear-gradient(135deg, #4c57bd, #3d4898) !important; -} - -/* Center button within card body */ -.eskaera-order-card .card-body > .btn { - margin-left: auto; - margin-right: auto; -} - -/* Order card header spacing */ -.order-card-header-spacing { - margin-top: 0.9rem; -} - -/* Order thumbnail small */ -.order-thumbnail-sm { - width: 90px; - height: 90px; - border-radius: 8px; - object-fit: cover; - flex-shrink: 0; - border: 2px solid rgba(90, 103, 216, 0.1); -} - -/* Order thumbnail medium */ -.order-thumbnail-md { - width: 120px; - height: 120px; - object-fit: cover; - border-radius: 8px; - flex-shrink: 0; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -/* Order thumbnail checkout */ -.order-thumbnail-checkout, -.checkout-thumbnail { - width: 90px; - height: 90px; - object-fit: cover; - border-radius: 6px; - flex-shrink: 0; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); -} +/* ---------- Header ---------- */ .card-header-top { display: flex; - gap: 0.6rem; + gap: var(--ac-space-sm); align-items: flex-start; - margin-bottom: 0.3rem; - height: 100px; - overflow: hidden; + margin-bottom: var(--ac-space-2xs); } .card-header-top > div:last-child { - text-align: left; flex: 1; + text-align: left; } .card-header-top .card-title { margin: 0; } -.card-badges .badge { - display: inline-flex; - align-items: center; - gap: 0.2rem; +.eskaera-order-card .card-body { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: var(--ac-space-2xs); + padding: var(--ac-space-sm) var(--ac-space-sm) 0; } +.eskaera-order-card .card-title { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + margin: 0; + overflow: hidden; + font-size: var(--ac-text-base); + font-weight: var(--ac-weight-bold); + line-height: var(--ac-leading-tight); + color: var(--ac-text-primary); + text-overflow: ellipsis; +} + +.order-desc-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + margin: 0; + overflow: hidden; + font-size: var(--ac-text-xs); + line-height: var(--ac-leading-snug); + color: var(--ac-text-muted); + text-overflow: ellipsis; +} + +/* ---------- Thumbnails ---------- */ + +.order-thumbnail-sm { + flex-shrink: 0; + width: 5.625rem; + height: 5.625rem; + border: 2px solid var(--ac-color-order-border); + border-radius: var(--ac-radius-md); + object-fit: cover; +} + +.order-thumbnail-md { + flex-shrink: 0; + width: 7.5rem; + height: 7.5rem; + border-radius: var(--ac-radius-md); + box-shadow: var(--ac-shadow-sm); + object-fit: cover; +} + +/* ---------- Product-count badge ---------- */ + +.order-badge-position { + position: absolute; + top: var(--ac-space-md); + right: var(--ac-space-md); + z-index: 1; + display: flex; +} + +.order-badge-custom { + display: flex; + align-items: center; + gap: var(--ac-space-sm); + padding: var(--ac-space-sm) var(--ac-space-xs); + border-radius: var(--ac-radius-sm); + font-size: var(--ac-text-sm); + font-weight: var(--ac-weight-bold); + box-shadow: var(--ac-shadow-md); +} + +/* ---------- Meta block ---------- */ + .card-meta-compact { display: flex; flex-direction: column; align-items: center; justify-content: center; - gap: 0; - margin: 0.2rem auto 0 auto; - padding: 0.6rem 0.8rem; - border-top: 1px solid rgba(90, 103, 216, 0.08); - background: rgba(245, 247, 255, 0.4); - border-radius: 0 0 0.75rem 0.75rem; width: 100%; + margin: var(--ac-space-2xs) auto 0; + padding: var(--ac-space-sm); + border-top: 1px solid var(--ac-color-order-border); + border-radius: 0 0 var(--ac-radius-lg) var(--ac-radius-lg); + background: var(--ac-surface-sunken); } -.card-meta-compact .card-meta-list { - display: flex; - flex-direction: column; - gap: 0.4rem; -} - -/* Meta table styles for clean date display */ -.meta-table { - width: auto; - border-collapse: collapse; - font-size: 0.8rem; - margin: 0 auto; - text-align: left; - min-height: 160px; - display: table; -} - -.meta-table tbody { - display: table-row-group; -} - -.meta-row { - display: table-row; - font-size: 0.8rem; - margin-bottom: 0; -} - -.meta-label-cell { - display: table-cell; - padding: 0.25rem 0.6rem 0.25rem 0; - font-weight: 600; - color: #374151; - text-align: right; - vertical-align: top; - white-space: nowrap; -} - -.meta-label-cell span { - display: inline-block; -} - -.meta-value-cell { - display: table-cell; - padding: 0.25rem 0; - color: #6b7280; - text-align: left; - vertical-align: middle; -} - -.meta-value-cell .badge { - display: inline-block; - vertical-align: middle; - font-size: 0.7rem; - padding: 0.2rem 0.5rem; -} - -.meta-label { - font-weight: 600; - color: #374151; - min-width: 85px; -} - -.meta-value { - color: #6b7280; - flex: 1; -} - -.order-badge-position { - position: absolute; - top: 1.25rem; - right: 1.25rem; - z-index: 10; - display: flex; -} - -.order-badge-custom { - font-weight: 700; - padding: 0.5rem 0.75rem; - border-radius: 6px; - font-size: 0.85rem; - box-shadow: 0 4px 12px rgba(90, 103, 216, 0.3); - display: flex !important; - align-items: center; - gap: 0.5rem; -} - -/* Meta grid - compact horizontal layout for order card metadata */ .meta-grid { display: flex; flex-wrap: wrap; - gap: 0.5rem 1rem; - justify-content: center; + gap: var(--ac-space-sm) var(--ac-space-md); align-items: center; + justify-content: center; } .meta-grid .meta-item { display: flex; align-items: center; - gap: 0.35rem; - font-size: 0.8rem; + gap: var(--ac-space-2xs); + font-size: var(--ac-text-xs); } .meta-grid .meta-label { - font-weight: 600; - color: #374151; min-width: auto; + font-weight: var(--ac-weight-semibold); + color: var(--ac-text-secondary); } .meta-grid .meta-value { - color: #6b7280; + color: var(--ac-text-muted); } .meta-grid .badge { - font-size: 0.75rem; - padding: 0.25rem 0.5rem; display: inline-flex; align-items: center; - gap: 0.25rem; -} - -.meta-grid .badge i { - font-size: 0.7rem; -} - -/* Special order card */ -.eskaera-order-card--special { - background: linear-gradient(180deg, #fffdf0 0%, #fffbea 100%); - border: 1px solid rgba(245, 158, 11, 0.35); - box-shadow: 0 6px 18px rgba(245, 158, 11, 0.12); -} - -.eskaera-order-card--special::before { - background: linear-gradient(90deg, #f59e0b, #d97706); -} - -.eskaera-order-card-link:hover .eskaera-order-card--special { - box-shadow: 0 20px 50px rgba(245, 158, 11, 0.2), 0 0 30px rgba(245, 158, 11, 0.12); - border-color: rgba(245, 158, 11, 0.5); - background: linear-gradient(180deg, #fffdf0 0%, #fff8db 100%); -} - -.special-order-badge { - background: linear-gradient(135deg, #f59e0b, #d97706) !important; - color: #fff !important; - font-size: 0.75rem; - padding: 0.25rem 0.6rem; - display: inline-flex; - align-items: center; - gap: 0.3rem; - border-radius: 4px; - font-weight: 600; - box-shadow: 0 2px 6px rgba(245, 158, 11, 0.3); + gap: var(--ac-space-2xs); + padding: var(--ac-space-2xs) var(--ac-space-xs); + font-size: var(--ac-text-2xs); } .meta-item--full { width: 100%; justify-content: center; } + +/* Home delivery: its own row, badge then date, centred as a pair — so it stays + centred whether or not the date is there. */ +.meta-grid .meta-item--delivery { + width: 100%; + justify-content: center; + gap: var(--ac-space-sm); +} + +/* ---------- Call to action ---------- */ + +/* Bootstrap 5.3 drives .btn from unprefixed custom properties, so the card's + own colours are set by redefining those instead of piling up !important. */ +.eskaera-order-card .btn { + --btn-color: var(--ac-text-on-fill); + --btn-bg: var(--ac-color-order); + --btn-border-color: transparent; + --btn-hover-color: var(--ac-text-on-fill); + --btn-hover-bg: var(--ac-color-order-strong); + --btn-hover-border-color: transparent; + --btn-active-color: var(--ac-text-on-fill); + --btn-active-bg: var(--ac-color-order-deep); + --btn-active-border-color: transparent; + --btn-border-radius: var(--ac-radius-sm); + --btn-font-size: var(--ac-text-sm); + --btn-font-weight: var(--ac-weight-semibold); + --btn-padding-y: var(--ac-space-sm); + --btn-padding-x: var(--ac-space-md); + display: inline-flex; + align-items: center; + gap: var(--ac-space-sm); + margin: auto auto var(--ac-space-sm); + box-shadow: var(--ac-shadow-sm); + text-align: center; + text-decoration: none; + transition: background-color var(--ac-transition-fast), box-shadow var(--ac-transition-fast), + transform var(--ac-transition-fast); +} + +/* ---------- Special orders ---------- */ + +.eskaera-order-card--special { + border-color: var(--ac-color-special-border); + background: var(--ac-surface-special); +} + +.eskaera-order-card--special::before { + background: linear-gradient(90deg, var(--ac-color-special), var(--ac-color-special-strong)); +} + +.special-order-badge { + /* Amber needs dark text — white on it is 2.15:1. */ + --badge-color: var(--ac-text-on-special); + --badge-font-size: var(--ac-text-2xs); + --badge-border-radius: var(--ac-radius-sm); + --badge-padding-x: var(--ac-space-xs); + --badge-padding-y: var(--ac-space-2xs); + display: inline-flex; + align-items: center; + gap: var(--ac-space-2xs); + background: linear-gradient(135deg, var(--ac-color-special), var(--ac-color-special-strong)); + font-weight: var(--ac-weight-semibold); + box-shadow: var(--ac-shadow-sm); +} + +/* ---------- States ---------- */ + +@media (hover: hover) and (pointer: fine) { + .eskaera-order-card:hover { + transform: translateY(-8px); + border-color: var(--ac-color-order-border-hover); + background: var(--ac-surface-order-hover); + box-shadow: var(--ac-shadow-lg); + } + + .eskaera-order-card--special:hover { + border-color: var(--ac-color-special-border-hover); + background: var(--ac-surface-special-hover); + } + + .eskaera-order-card .btn:hover { + transform: translateY(-2px); + box-shadow: var(--ac-shadow-md); + } +} + +/* Keyboard users get the same emphasis as a hover, plus a ring on the card + that holds the focused link. */ +.eskaera-order-card:focus-within { + border-color: var(--ac-color-order-border-hover); + background: var(--ac-surface-order-hover); + box-shadow: var(--ac-shadow-lg); +} + +.eskaera-order-card-link:focus-visible, +.eskaera-order-card .btn:focus-visible { + outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color); + outline-offset: var(--ac-focus-ring-offset); + border-radius: var(--ac-radius-sm); +} + +/* The entry animation is decorative; motion sensitivity outranks it, and the + hover lift has to stop moving too. */ +@media (prefers-reduced-motion: reduce) { + .eskaera-order-card { + animation: none; + transition: none; + } + + .eskaera-order-card:hover, + .eskaera-order-card .btn, + .eskaera-order-card .btn:hover { + transform: none; + transition: none; + } +} diff --git a/website_sale_aplicoop/static/src/css/components/product-card.css b/website_sale_aplicoop/static/src/css/components/product-card.css index d3d6671..ca0ff82 100644 --- a/website_sale_aplicoop/static/src/css/components/product-card.css +++ b/website_sale_aplicoop/static/src/css/components/product-card.css @@ -1,238 +1,204 @@ /* filepath: website_sale_aplicoop/static/src/css/components/product-card.css */ /** - * Product card component styles + * Product card + * + * Markup (eskaera_shop_products): + * .product-card-wrapper.product-card + * └ .card + * ├ img.product-img-cover | .product-img-placeholder + * ├ .card-body (title, tags, supplier, origin, price) + * └ form.add-to-cart-form → components/quantity-control.css + * + * The wrapper carries the chrome (border, radius, elevation); the inner + * Bootstrap .card is neutralised through its own custom properties so the two + * boxes stop drawing a double frame. */ .product-card { - background-color: #fff; - border: 1px solid #e0e0e0; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - transition: box-shadow 0.3s, transform 0.2s; display: flex; flex-direction: column; - padding: 0.5rem; height: 100%; + padding: var(--ac-space-2xs); + border: 1px solid var(--ac-border-subtle); + border-radius: var(--ac-radius-lg); + background-color: var(--ac-surface); + box-shadow: var(--ac-shadow-sm); overflow: hidden; - outline: none; + transition: box-shadow var(--ac-transition-normal), transform var(--ac-transition-normal); } -.product-card:hover, -.product-card:focus-within { - transform: translateY(-4px) scale(1.01); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.13); - outline: 2px solid var(--primary-color, #007bff); - outline-offset: 2px; +/* Bootstrap 5.3 drives .card through unprefixed custom properties, so the inner + card can be flattened without !important and without touching the markup. */ +.product-card > .card { + --card-bg: transparent; + --card-border-width: 0; + --card-border-radius: 0; + display: flex; + flex-direction: column; + height: 100%; } -.product-card .product-image { - height: 120px; - width: 100%; - object-fit: cover; - border-radius: 8px 8px 0 0; - background: #f3f3f3; - display: block; -} +/* ---------- Media ---------- */ -.product-img-cover { - height: 120px; - width: 100%; - object-fit: cover; - border-radius: 8px 8px 0 0; - box-shadow: 0 2px 8px rgba(40, 39, 39, 0.09); - background: #f3f3f3; +.product-card .product-image, +.product-img-cover, +.product-img-fixed, +.product-img-placeholder { display: block; flex-shrink: 0; + width: 100%; + height: var(--ac-card-media-height); + border-radius: var(--ac-radius-md) var(--ac-radius-md) 0 0; + object-fit: cover; + background-color: var(--ac-surface-muted); } +.product-img-placeholder { + display: flex; + align-items: center; + justify-content: center; + color: var(--ac-text-muted); +} + +/* ---------- Body ---------- */ + .product-card .card-body { display: flex; flex-direction: column; - height: 100%; - flex-grow: 1; - padding: 0.4rem 0.55rem 0.5rem; - position: relative; - background: linear-gradient(135deg, rgba(0, 123, 255, 0.07) 0%, rgba(0, 123, 255, 0.04) 100%); - transition: background 0.3s; -} - -.product-card:hover .card-body, -.product-card:focus-within .card-body { - background: linear-gradient( - 135deg, - rgba(108, 117, 125, 0.13) 0%, - rgba(108, 117, 125, 0.09) 100% - ); + /* Sized to its own content, not stretched: the add-to-cart form below is + what aligns across the row (see quantity-control.css), so the text/price + block stays compact instead of ballooning to match a taller sibling. */ + gap: var(--ac-space-3xs); + padding: var(--ac-space-2xs) var(--ac-space-xs) var(--ac-space-2xs); + background: linear-gradient(135deg, var(--ac-surface-tint) 0%, transparent 100%); + transition: background var(--ac-transition-normal); } .product-card .card-title { - flex-grow: 1; - margin: 0 0 0.1rem 0; - min-height: auto; - display: block; - word-wrap: break-word; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + /* Reserve both lines so single-line titles don't stagger the grid. */ + min-height: calc(2em * var(--ac-leading-tight)); + margin: 0; + overflow: hidden; overflow-wrap: break-word; - font-size: 1rem !important; - line-height: 1.15; - text-align: center; - font-weight: 600; - color: #1a202c; + font-size: var(--ac-text-base); + font-weight: var(--ac-weight-semibold); + line-height: var(--ac-leading-tight); letter-spacing: 0.01em; + text-align: center; + color: var(--ac-text-primary); } .product-card .card-text { - margin-bottom: 0.08rem; + margin-bottom: 0; + font-size: var(--ac-text-sm); text-align: center; - font-size: 0.95rem; } -.product-card .card-text strong { - display: block; - margin-bottom: 0.1rem; - font-size: 1.15rem; - color: #3b82f6; -} - -.product-card .product-supplier { - text-align: center; - color: #4a5568; - font-weight: 400; - margin-bottom: 0.08rem; - font-size: 0.85rem !important; -} +/* ---------- Tags ---------- */ .product-tags { - text-align: center; display: flex; flex-wrap: wrap; - gap: 0.12rem; + gap: var(--ac-space-3xs); justify-content: center; - font-weight: 400; - font-size: 1rem !important; - margin: 0 0 0.08rem 0; + margin: 0; padding: 0; } +/* The per-tag colour is a record value, so it arrives as the --ac-tag-color + custom property set inline on the element (see eskaera_shop_products). */ .badge-km { - background-color: var(--primary-color, #007bff) !important; - color: #fff !important; - font-weight: 600 !important; - padding: 0.18rem 0.32rem !important; - font-size: 0.68rem !important; - border-radius: 0.22rem; - display: inline-block; - border: 1px solid #007bff; + --badge-padding-x: var(--ac-space-2xs); + --badge-padding-y: var(--ac-space-3xs); + --badge-font-size: var(--ac-text-2xs); + --badge-font-weight: var(--ac-weight-semibold); + --badge-color: var(--ac-text-on-fill); + --badge-border-radius: var(--ac-radius-sm); + border: 1px solid var(--ac-tag-color, var(--ac-color-primary-strong)); + background-color: var(--ac-tag-color, var(--ac-color-primary-strong)); white-space: nowrap; - margin-right: 0.08rem; - margin-bottom: 0.08rem; } -.card-body p.card-text { +/* ---------- Supplier & origin ---------- */ + +.product-card .product-supplier, +.product-card .product-origin { + margin: 0; + font-size: var(--ac-text-xs); + font-weight: var(--ac-weight-normal); text-align: center; - margin-bottom: 0.35rem; - min-height: 1.5rem; - display: flex; - align-items: center; - justify-content: center; - background-color: var(--primary-color, #007bff); - color: #fff; - border-radius: 0.18rem; - font-size: 0.98rem; + color: var(--ac-text-secondary); } -.card-body p.card-text strong { - display: inline; - font-size: 1.18rem !important; - color: var(--primary-color, #007bff); - margin-bottom: 0; +.product-card .product-origin .fa { + margin-right: var(--ac-space-3xs); +} + +/* ---------- Price ---------- */ + +.product-card .product-price-display { + display: flex; + align-items: baseline; + justify-content: center; + flex-wrap: wrap; + gap: var(--ac-space-2xs); + margin: 0; +} + +.product-card .product-price-main { + font-size: var(--ac-text-lg); + font-weight: var(--ac-weight-bold); + line-height: var(--ac-leading-tight); white-space: nowrap; + color: var(--ac-color-primary-strong); } -.product-img-fixed { - object-fit: cover; - height: 120px; - width: 100%; - border-radius: 8px 8px 0 0; - background: #f3f3f3; - display: block; +.product-card .product-unit-price { + margin: 0; + font-size: var(--ac-text-xs); + text-align: center; + color: var(--ac-text-secondary); } -.product-img-placeholder { - height: 120px; - width: 100%; - border-radius: 8px 8px 0 0; - background: #f3f3f3; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - color: #ccc; -} +/* ---------- States ---------- */ -/* Responsive: tablet (≤768px) — compresión moderada */ -@media (max-width: 768px) { - .product-card .product-image, - .product-img-cover, - .product-img-fixed, - .product-img-placeholder { - height: 90px; +/* Hover effects only where a pointer can actually hover — on touch they stick + after a tap and read as a stuck selection. */ +@media (hover: hover) and (pointer: fine) { + .product-card:hover { + transform: translateY(-4px); + box-shadow: var(--ac-shadow-lg); } + + .product-card:hover .card-body { + background: linear-gradient(135deg, var(--ac-surface-tint-hover) 0%, transparent 100%); + } +} + +/* Keyboard focus only: a ring, and no transform — moving the card under the + caret while tabbing is disorienting. */ +.product-card:focus-visible, +.product-card:focus-within { + outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color); + outline-offset: var(--ac-focus-ring-offset); +} + +.product-card:focus-within .card-body { + background: linear-gradient(135deg, var(--ac-surface-tint-hover) 0%, transparent 100%); +} + +@media (prefers-reduced-motion: reduce) { + .product-card, .product-card .card-body { - padding: 0.35rem 0.45rem 0.4rem; + transition: none; } - .card-body p.card-text { - margin-bottom: 0.25rem; - min-height: 1.4rem; - } -} -/* Responsive: móvil (≤480px) — compresión máxima */ -@media (max-width: 480px) { - .product-card { - padding: 0.15rem; - border-radius: 6px; - } - .product-card .product-image, - .product-img-cover, - .product-img-fixed, - .product-img-placeholder { - height: 60px; - border-radius: 5px 5px 0 0; - } - .product-card .card-body { - padding: 0.25rem 0.3rem 0.3rem; - } - .product-card .card-title { - font-size: 0.82rem !important; - line-height: 1.15; - margin-bottom: 0.05rem; - } - .product-card .card-text { - font-size: 0.8rem; - } - .badge-km { - font-size: 0.55rem !important; - padding: 0.1rem 0.18rem !important; - } - .product-tags { - font-size: 0.82rem !important; - gap: 0.08rem; - margin-bottom: 0.05rem; - } - .card-body p.card-text { - min-height: 1rem; - font-size: 0.85rem; - margin-bottom: 0.2rem; - } - .product-card .product-supplier { - font-size: 0.72rem !important; - margin-bottom: 0.05rem; + .product-card:hover { + transform: none; } } - -/* Accesibilidad: focus visible */ -.product-card:focus-visible { - outline: 2.5px solid var(--primary-color, #007bff); - outline-offset: 2px; -} diff --git a/website_sale_aplicoop/static/src/css/components/quantity-control.css b/website_sale_aplicoop/static/src/css/components/quantity-control.css index 64a0087..ef8806c 100644 --- a/website_sale_aplicoop/static/src/css/components/quantity-control.css +++ b/website_sale_aplicoop/static/src/css/components/quantity-control.css @@ -1,405 +1,220 @@ /* filepath: website_sale_aplicoop/static/src/css/components/quantity-control.css */ /** - * Quantity control (+ - input) component styles + * Quantity control — stepper (− input +) plus the add-to-cart button. + * + * Markup (eskaera_shop_products): + * form.add-to-cart-form + * └ .qty-control + * ├ label.visually-hidden + * ├ button.qty-decrease + * ├ input.product-qty + * ├ button.qty-increase + * └ button.add-to-cart-btn + * + * Sizing is fluid (--ac-control-size), which replaces the six per-breakpoint + * bands this component used to carry. The only query left is a pointer query: + * what actually changes between a phone and a desktop is the input device, not + * the viewport width. */ -/* Formulario agregar al carrito */ +/* margin-top: auto pins this to the bottom of .product-card > .card (which is + stretched to the grid row's tallest sibling) so every card's button ends up + at the same height, without ballooning the text/price block above it — see + .product-card .card-body in product-card.css. */ .add-to-cart-form { - width: 100%; - gap: 0; - padding: 0.5rem; - margin-top: 0.25rem; - background-color: #f8f9fa; - border-radius: 4px; display: flex; - border: 1px solid #e9ecef; - align-content: center; justify-content: center; + width: 100%; + margin-top: auto; + padding: var(--ac-space-3xs); + border: 1px solid var(--ac-border); + border-radius: var(--ac-radius-sm); + background-color: var(--ac-surface-sunken); } -.add-to-cart-form .input-group { +/* A grid, not a wrapping flex row: four 44px controls never fit across a card + that is ~150px wide, so the row used to wrap and drop "+" onto its own line + next to the cart button. Two deliberate rows instead — the stepper across the + top, the primary action full width underneath, where it is easier to hit. */ +.qty-control { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; width: 100%; - gap: 0; - padding: 0; +} + +/* ---------- Stepper ---------- */ + +.qty-control .qty-decrease, +.qty-control .qty-increase { display: flex; align-items: center; justify-content: center; - height: 36px; + flex-shrink: 0; + width: var(--ac-control-size); + height: var(--ac-control-size); + padding: 0; + /* WCAG 1.4.11: the boundary is what identifies the control, so it needs + ≥ 3:1 against the surface behind it. */ + border: var(--ac-control-border-width) solid var(--ac-border-strong); + font-size: var(--ac-text-sm); + color: var(--ac-text-secondary); + background-color: var(--ac-surface); + cursor: pointer; + transition: color var(--ac-transition-fast), border-color var(--ac-transition-fast), + background-color var(--ac-transition-fast); +} + +.qty-control .qty-decrease { + border-start-start-radius: var(--ac-radius-sm); + border-end-start-radius: var(--ac-radius-sm); + border-start-end-radius: 0; + border-end-end-radius: 0; +} + +.qty-control .qty-increase { + border-start-end-radius: var(--ac-radius-sm); + border-end-end-radius: var(--ac-radius-sm); + border-start-start-radius: 0; + border-end-start-radius: 0; } .add-to-cart-form .product-qty { - flex: 1 1 auto; - text-align: center; - font-weight: 700; - font-size: 1rem; - padding: 0.3rem; - min-width: 32px; - max-width: 70px; - border: 2px solid #dee2e6; + /* Fills the middle grid column; min-width 0 lets it shrink on a narrow card + instead of pushing the "+" button out of the row. */ + width: 100%; + min-width: 0; + height: var(--ac-control-size); + padding: 0 var(--ac-space-2xs); + /* Collapse the shared edge with the two stepper buttons. */ + border: var(--ac-control-border-width) solid var(--ac-border-strong); + border-inline-width: 0; border-radius: 0; - background-color: #ffffff; - transition: all 0.2s ease; + font-size: var(--ac-text-base); + font-weight: var(--ac-weight-bold); + text-align: center; + color: var(--ac-text-primary); + background-color: var(--ac-surface); + appearance: textfield; -moz-appearance: textfield; - height: 36px; - line-height: 30px; + transition: border-color var(--ac-transition-fast), box-shadow var(--ac-transition-fast); } .add-to-cart-form .product-qty::-webkit-outer-spin-button, .add-to-cart-form .product-qty::-webkit-inner-spin-button { - -webkit-appearance: none; margin: 0; + -webkit-appearance: none; } -.add-to-cart-form .product-qty:focus { - outline: none; - border-color: var(--primary-color, #007bff); - box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); - background-color: #ffffff; -} +/* ---------- Add to cart ---------- */ -/* Contenedor de control de cantidad */ -.qty-control { +.add-to-cart-form .add-to-cart-btn { + /* Full width on its own row, under the stepper. */ + grid-column: 1 / -1; display: flex; align-items: center; justify-content: center; - gap: 0; - padding: 0; width: 100%; - height: 44px; -} - -/* Botones de cantidad + y - */ -.qty-control .qty-decrease, -.qty-control .qty-increase { - min-width: 36px; - width: 36px; - height: 36px; + height: var(--ac-control-size); + margin-top: var(--ac-space-3xs); padding: 0; - gap: 0; - font-size: 1rem; - display: flex; - align-items: center; - justify-content: center; - border: 2px solid #dee2e6; - background-color: #ffffff; - color: #495057; - transition: all 0.2s ease; + border: none; + border-radius: var(--ac-radius-sm); + font-size: var(--ac-text-sm); + color: var(--ac-text-on-fill); + background-color: var(--ac-color-accent); cursor: pointer; - flex-shrink: 0; + transition: background-color var(--ac-transition-fast), transform var(--ac-transition-fast); } -.qty-control .qty-decrease:hover, -.qty-control .qty-increase:hover { - border-color: var(--primary-color, #007bff); - color: var(--primary-color, #007bff); - background-color: #f8f9fa; +.add-to-cart-form .add-to-cart-btn .fa { + font-size: var(--ac-text-md); +} + +/* Out of stock stays focusable and clickable on purpose: the handler answers + with an explanation instead of the control silently doing nothing. It is + marked aria-disabled in the template, so the styling has to read as + unavailable without relying on colour alone (the icon changes to fa-ban). */ +.add-to-cart-form .add-to-cart-btn[data-out-of-stock="true"] { + color: var(--ac-text-secondary); + background-color: var(--ac-surface-muted); + cursor: not-allowed; +} + +/* ---------- States ---------- */ + +@media (hover: hover) and (pointer: fine) { + .qty-control .qty-decrease:hover, + .qty-control .qty-increase:hover { + border-color: var(--ac-color-primary); + color: var(--ac-color-primary-strong); + background-color: var(--ac-surface-sunken); + } + + .add-to-cart-form .add-to-cart-btn:hover { + background-color: var(--ac-color-accent-strong); + transform: scale(1.05); + } + + .add-to-cart-form .add-to-cart-btn[data-out-of-stock="true"]:hover { + background-color: var(--ac-surface-muted); + transform: none; + } } .qty-control .qty-decrease:active, .qty-control .qty-increase:active { - background-color: #e9ecef; -} - -.add-to-cart-form .add-to-cart-btn { - white-space: nowrap; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - margin: 0; - font-size: 0.8rem; - height: 36px; - width: 36px; - min-width: 36px; - max-width: 36px; - min-height: 36px; - max-height: 36px; - flex-shrink: 0; - border: none; - background-color: #7c3aed !important; - color: #ffffff !important; - cursor: pointer; - transition: all 0.2s ease; -} - -.add-to-cart-form .add-to-cart-btn:hover { - background-color: #6d28d9 !important; - transform: scale(1.05); + background-color: var(--ac-surface-sunken); } .add-to-cart-form .add-to-cart-btn:active { - background-color: #5b21b6 !important; + background-color: var(--ac-color-accent-strong); transform: scale(0.98); } -.add-to-cart-form .add-to-cart-btn i { - font-size: 1rem; +.qty-control .qty-decrease:focus-visible, +.qty-control .qty-increase:focus-visible, +.add-to-cart-form .add-to-cart-btn:focus-visible, +.add-to-cart-form .product-qty:focus-visible { + outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color); + outline-offset: var(--ac-focus-ring-offset); + /* Raise the focused control so the ring is not clipped by its neighbours. */ + position: relative; + z-index: 1; } -/* Pantallas muy grandes: 1600px+ (6 columnas) */ -@media (min-width: 1600px) { - .add-to-cart-form { - padding: 0.35rem; - gap: 0.08rem; - } +.add-to-cart-form .product-qty:focus-visible { + border-color: var(--ac-color-primary); +} - .add-to-cart-form .input-group { - height: 32px; - gap: 0.06rem; - } - - .add-to-cart-form .product-qty { - font-size: 1rem; - min-width: 28px; - max-width: 60px; - height: 32px; - padding: 0.2rem; - } +/* ---------- Input device ---------- */ +/* Touch: grow every target to 44px. WCAG 2.2 AA (2.5.8) only asks for 24px, + but 44px is the AAA figure (2.5.5) and what a thumb actually needs. */ +@media (pointer: coarse) { .qty-control .qty-decrease, .qty-control .qty-increase { - width: 32px; - height: 32px; - font-size: 0.9rem; + width: var(--ac-touch-target); + height: var(--ac-touch-target); } + .add-to-cart-form .product-qty, .add-to-cart-form .add-to-cart-btn { - height: 32px; - width: 32px; - min-width: 32px; - max-width: 32px; - font-size: 0.75rem; + height: var(--ac-touch-target); } } -/* Pantallas grandes: 1400-1599px (5 columnas) */ -@media (max-width: 1599px) and (min-width: 1400px) { - .add-to-cart-form { - padding: 0.36rem; - gap: 0.07rem; - } - - .add-to-cart-form .input-group { - height: 32px; - gap: 0.05rem; - } - - .add-to-cart-form .product-qty { - font-size: 1rem; - min-width: 28px; - max-width: 62px; - height: 32px; - padding: 0.2rem; - } - +@media (prefers-reduced-motion: reduce) { .qty-control .qty-decrease, - .qty-control .qty-increase { - width: 32px; - height: 32px; - font-size: 0.9rem; + .qty-control .qty-increase, + .add-to-cart-form .product-qty, + .add-to-cart-form .add-to-cart-btn { + transition: none; } - .add-to-cart-form .add-to-cart-btn { - height: 32px; - width: 32px; - 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; + .add-to-cart-form .add-to-cart-btn:hover, + .add-to-cart-form .add-to-cart-btn:active { + transform: none; } } diff --git a/website_sale_aplicoop/static/src/css/components/tag-filter.css b/website_sale_aplicoop/static/src/css/components/tag-filter.css index 45022d5..839200b 100644 --- a/website_sale_aplicoop/static/src/css/components/tag-filter.css +++ b/website_sale_aplicoop/static/src/css/components/tag-filter.css @@ -4,70 +4,103 @@ */ /** - * Tag Filter Badges Component + * Tag filter badges * - * Styles for interactive tag filter badges in the product search/filter bar. - * Badges toggle between secondary (unselected) and primary (selected) states. + * Toggle buttons above the product grid. Three states: + * (default) nothing selected — each badge wears its own tag colour + * .is-selected this tag is filtering + * .is-dimmed another tag is filtering, this one is not + * + * realtime_search.js used to paint all three by writing nine + * style.setProperty(..., "important") calls per click. It now only toggles + * these classes and aria-pressed; the appearance lives here. + * + * The per-tag colour arrives as --ac-tag-color, set inline from the record. */ -/* Container for all tag filter badges */ .tag-filter-badges { display: flex; flex-wrap: wrap; - justify-content: center; align-items: center; - gap: 0.5rem; - padding: 0.75rem 0; + justify-content: center; + gap: var(--ac-space-sm); + padding: var(--ac-space-xs) 0; } -/* Individual tag filter badge button */ .tag-filter-badge { - cursor: pointer; - padding: 0.5rem 0.75rem; - border-radius: 0.375rem; - font-size: 0.875rem; - font-weight: 500; + display: inline-flex; + align-items: center; + /* 24px is the AA floor (WCAG 2.2 §2.5.8); coarse pointers get 44 below. */ + min-height: 1.5rem; + padding: var(--ac-space-sm) var(--ac-space-xs); + border: 1px solid var(--ac-tag-color, var(--ac-color-secondary)); + border-radius: var(--ac-radius-sm); + background-color: var(--ac-tag-color, var(--ac-color-secondary)); + font-size: var(--ac-text-sm); + font-weight: var(--ac-weight-medium); + color: var(--ac-text-on-fill); white-space: nowrap; - transition: all 0.2s ease-in-out; + cursor: pointer; user-select: none; - display: inline-block; - border: 1px solid; - color: #ffffff !important; + transition: background-color var(--ac-transition-fast), border-color var(--ac-transition-fast), + box-shadow var(--ac-transition-fast), transform var(--ac-transition-fast); } -/* Tags sin color definido en Odoo: usar color secundario del tema */ -.tag-filter-badge.tag-use-theme-color { - background-color: var(--bs-secondary, #6c757d); - border-color: var(--bs-secondary, #6c757d); -} - -/* Product card tags (badge-km) sin color definido: usar color del tema */ +/* Tags with no colour of their own fall back to the theme's secondary. The old + var(--bs-secondary) never resolved: Odoo emits Bootstrap's custom properties + unprefixed, so that lookup always landed on its hardcoded fallback. */ +.tag-filter-badge.tag-use-theme-color, .badge-km.tag-use-theme-color { - background-color: var(--bs-secondary, #6c757d); - border-color: var(--bs-secondary, #6c757d); - color: #ffffff !important; + border-color: var(--ac-color-secondary); + background-color: var(--ac-color-secondary); + color: var(--ac-text-on-fill); } -.tag-filter-badge:hover { - transform: translateY(-2px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - filter: brightness(0.9); +/* Active filter: the strong primary, which also carries white text at 7.04:1. */ +.tag-filter-badge.is-selected { + border-color: var(--ac-color-primary-strong); + background-color: var(--ac-color-primary-strong); + box-shadow: var(--ac-shadow-sm); +} + +/* Not part of the current filter while others are. */ +.tag-filter-badge.is-dimmed { + border-color: var(--ac-color-secondary); + background-color: var(--ac-color-secondary); } -/* Counter text inside badge */ .tag-filter-badge .tag-count { - font-weight: 600; - margin-left: 0.25rem; + margin-left: var(--ac-space-2xs); + font-weight: var(--ac-weight-semibold); + font-variant-numeric: tabular-nums; } -/* Responsive adjustments */ -@media (max-width: 768px) { - .tag-filter-badges { - gap: 0.375rem; - } +/* ---------- States ---------- */ +@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 { - padding: 0.375rem 0.625rem; - font-size: 0.8125rem; + min-height: var(--ac-touch-target); + padding-inline: var(--ac-space-md); + } +} + +@media (prefers-reduced-motion: reduce) { + .tag-filter-badge, + .tag-filter-badge:hover { + transform: none; + transition: none; } } diff --git a/website_sale_aplicoop/static/src/css/layout/header.css b/website_sale_aplicoop/static/src/css/layout/header.css index e4980c6..69a47ff 100644 --- a/website_sale_aplicoop/static/src/css/layout/header.css +++ b/website_sale_aplicoop/static/src/css/layout/header.css @@ -7,83 +7,142 @@ /* Unified header for both shop and checkout pages */ .eskaera-order-header, .checkout-header { - background: white; - border: 1px solid #e0e0e0; - border-radius: 8px; - padding: 2rem; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + padding: clamp(var(--ac-space-md), 2vw, var(--ac-space-xl)); + border: 1px solid var(--ac-border-subtle); + border-radius: var(--ac-radius-md); + background: var(--ac-surface); + box-shadow: var(--ac-shadow-sm); } .eskaera-order-header h1, .checkout-header h1 { - color: #2d3748; - font-weight: 600; - margin-bottom: 1.5rem; - border-bottom: 3px solid var(--primary-color, #007bff); - padding-bottom: 1rem; + margin-bottom: var(--ac-space-md); + padding-bottom: var(--ac-space-md); + border-bottom: 3px solid var(--ac-color-primary); + font-size: var(--ac-text-2xl); + font-weight: var(--ac-weight-bold); + color: var(--ac-text-primary); } -.order-info-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 2rem; -} - -.info-item { - display: flex; - flex-direction: column; - gap: 0.5rem; - margin-bottom: 1rem; -} - -.info-item:last-child { - margin-bottom: 0; -} - -.info-label { - font-weight: 700; - color: #2d3748; - font-size: 1.05rem; - margin-bottom: 0.5rem; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.info-value, -.info-date { - font-size: 1.35rem; - color: #1a202c; - font-weight: 600; -} - -.info-date { - display: block; - font-size: 1.25rem; - color: #2d3748; - font-weight: 500; - margin-top: 0.25rem; -} - -/* Title styling for both pages */ -.checkout-title, -.eskaera-order-header h1 { - font-size: 2rem; - font-weight: 700; - color: #1a202c; - margin-bottom: 1rem; +.checkout-title { + margin-bottom: var(--ac-space-md); + font-size: var(--ac-text-2xl); + font-weight: var(--ac-weight-bold); + color: var(--ac-text-primary); } .checkout-order-name { - font-size: 1.5rem; - color: #2d3748; - font-weight: 600; + font-size: var(--ac-text-lg); + font-weight: var(--ac-weight-semibold); + color: var(--ac-text-secondary); } -/* Info value styling */ -.info-value { - font-size: 1.1rem; +/* ========================================================================== + Order info: label / value pairs + Rendered as
. + 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
, because grid places the cells in source + order. */ +@media (min-width: 992px) { + .order-info-grid { + grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr); + } + + .order-info-grid > .info-label:nth-of-type(even) { + padding-inline-start: var(--ac-space-lg); + border-inline-start: 1px solid var(--ac-border); + } +} + +/* Single label/value pair, used in the checkout info row where each pair sits in + its own Bootstrap column. */ +.info-pair { + margin: 0; +} + +.info-pair > .info-label { + margin-bottom: var(--ac-space-3xs); + font-size: var(--ac-text-xs); + font-weight: var(--ac-weight-semibold); + line-height: var(--ac-leading-snug); + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--ac-text-secondary); +} + +.info-pair > .info-value { + margin: 0; + font-size: var(--ac-text-md); + font-weight: var(--ac-weight-semibold); + line-height: var(--ac-leading-snug); + color: var(--ac-text-primary); + overflow-wrap: break-word; +} + +/* Standalone date line used by the checkout header. */ .info-date { - font-size: 1rem; + display: block; + margin-top: var(--ac-space-2xs); + font-size: var(--ac-text-base); + font-weight: var(--ac-weight-medium); + color: var(--ac-text-secondary); } diff --git a/website_sale_aplicoop/static/src/css/layout/pages.css b/website_sale_aplicoop/static/src/css/layout/pages.css index 267e560..2216bd2 100644 --- a/website_sale_aplicoop/static/src/css/layout/pages.css +++ b/website_sale_aplicoop/static/src/css/layout/pages.css @@ -1,106 +1,67 @@ /* filepath: website_sale_aplicoop/static/src/css/layout/pages.css */ /** - * Page backgrounds and main layout structures + * Page shells: backgrounds and the container stacking above them. + * + * These styles used to reach far outside the addon: + * - `html, body { background: transparent !important }` stripped the + * background from EVERY page of the website, addon or not. + * - `body.website_published { … !important }` was meant to put it back, but + * no page carries that class — checked against the rendered HTML of the + * orders, shop and checkout pages, whose has no class at all. Both + * rules were therefore dead weight on top of a site-wide side effect. + * - `#wrap { padding: 2rem 0 }` styled Odoo's own page wrapper everywhere. + * On these pages #wrap *is* the Eskaera root, so the padding moved onto the + * root classes and the ID selector is gone. + * + * The body reset now applies only to pages that actually contain an Eskaera + * root, which is enough specificity to win without !important. */ -html, -body { - background-color: transparent !important; - background: transparent !important; +body:has(.eskaera-page), +body:has(.eskaera-shop-page), +body:has(.eskaera-generic-page), +body:has(.eskaera-checkout-page) { + background: transparent; } -body.website_published { - background: linear-gradient( - 135deg, - color-mix(in srgb, var(--primary-color) 30%, white), - color-mix(in srgb, var(--primary-color) 60%, black) - ) !important; -} - -body.website_published .eskaera-shop-page, -body.website_published .eskaera-checkout-page { - background: transparent !important; -} - -/* Generic page background mixin */ .eskaera-page, .eskaera-shop-page, .eskaera-generic-page, .eskaera-checkout-page { - min-height: 100vh; position: relative; + /* dvh tracks the mobile browser chrome; vh stays as the fallback. */ + min-height: 100vh; + min-height: 100dvh; + padding: var(--ac-space-xl) 0; } .eskaera-page, .eskaera-generic-page { background: linear-gradient( 180deg, - color-mix(in srgb, var(--primary-color) 10%, white), - color-mix(in srgb, var(--primary-color) 70%, black) - ) !important; + color-mix(in srgb, var(--ac-color-primary) 10%, white), + color-mix(in srgb, var(--ac-color-primary) 70%, black) + ); } .eskaera-shop-page { background: linear-gradient( 135deg, - color-mix(in srgb, var(--primary-color) 10%, white), - color-mix(in srgb, var(--primary-color) 10%, rgb(135, 135, 135)) - ) !important; + color-mix(in srgb, var(--ac-color-primary) 10%, white), + color-mix(in srgb, var(--ac-color-primary) 10%, rgb(135, 135, 135)) + ); } .eskaera-checkout-page { background: linear-gradient( -135deg, - color-mix(in srgb, var(--primary-color) 0%, white), - color-mix(in srgb, var(--primary-color) 60%, black) - ) !important; + white, + color-mix(in srgb, var(--ac-color-primary) 60%, black) + ); } -.eskaera-page::before, -.eskaera-generic-page::before { - background-image: radial-gradient( - circle at 20% 50%, - color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%, - transparent 50% - ), - radial-gradient( - circle at 80% 80%, - color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, - transparent 50% - ), - radial-gradient( - circle at 40% 20%, - color-mix(in srgb, var(--primary-color, white) 15%, transparent) 0%, - transparent 50% - ); -} - -.eskaera-shop-page::before { - background-image: radial-gradient( - circle at 15% 30%, - color-mix(in srgb, var(--primary-color, white) 18%, transparent) 0%, - transparent 50% - ), - radial-gradient( - circle at 85% 70%, - color-mix(in srgb, var(--primary-color) 22%, transparent) 0%, - transparent 50% - ); -} - -.eskaera-checkout-page::before { - background-image: radial-gradient( - circle at 20% 50%, - color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%, - transparent 50% - ), - radial-gradient( - circle at 80% 80%, - color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, - transparent 50% - ); -} +/* ---------- Decorative glow overlay ---------- */ .eskaera-page::before, .eskaera-shop-page::before, @@ -108,28 +69,61 @@ body.website_published .eskaera-checkout-page { .eskaera-checkout-page::before { content: ""; position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; + inset: 0; z-index: 0; + pointer-events: none; } +.eskaera-page::before, +.eskaera-generic-page::before { + background-image: radial-gradient( + circle at 20% 50%, + color-mix(in srgb, var(--ac-color-primary) 20%, transparent) 0%, + transparent 50% + ), + radial-gradient( + circle at 80% 80%, + color-mix(in srgb, var(--ac-color-primary) 25%, transparent) 0%, + transparent 50% + ), + radial-gradient( + circle at 40% 20%, + color-mix(in srgb, var(--ac-color-primary) 15%, transparent) 0%, + transparent 50% + ); +} + +.eskaera-shop-page::before { + background-image: radial-gradient( + circle at 15% 30%, + color-mix(in srgb, var(--ac-color-primary) 18%, transparent) 0%, + transparent 50% + ), + radial-gradient( + circle at 85% 70%, + color-mix(in srgb, var(--ac-color-primary) 22%, transparent) 0%, + transparent 50% + ); +} + +.eskaera-checkout-page::before { + background-image: radial-gradient( + circle at 20% 50%, + color-mix(in srgb, var(--ac-color-primary) 20%, transparent) 0%, + transparent 50% + ), + radial-gradient( + circle at 80% 80%, + color-mix(in srgb, var(--ac-color-primary) 25%, transparent) 0%, + transparent 50% + ); +} + +/* Content sits above the overlay. */ .eskaera-page > .container, .eskaera-shop-page > .container, .eskaera-generic-page > div, .eskaera-checkout-page > .container { position: relative; z-index: 1; - padding-top: 2rem; - padding-bottom: 2rem; -} - -#wrap { - padding: 2rem 0; -} - -.group-order-shop { - background-color: transparent; } diff --git a/website_sale_aplicoop/static/src/css/layout/responsive.css b/website_sale_aplicoop/static/src/css/layout/responsive.css index 1e62b6e..a47213d 100644 --- a/website_sale_aplicoop/static/src/css/layout/responsive.css +++ b/website_sale_aplicoop/static/src/css/layout/responsive.css @@ -1,103 +1,21 @@ /* filepath: website_sale_aplicoop/static/src/css/layout/responsive.css */ /** - * Responsive design breakpoints and adjustments - * All media queries centralized here for easier maintenance - * NOTE: products-grid.css has its own breakpoints and should NOT be overridden here + * Responsive adjustments — do not add to it. + * + * Centralising every media query here was the original plan; in practice this + * file drifted out of sync with the components it overrode and needed + * !important to win — twenty of them at its peak. Each component now owns its + * queries at the bottom of its own file, and most need none at all once sizing + * is fluid. What is left here is thumbnail geometry and table density, which + * belong to no single component. */ -@media (max-width: 992px) { - /* Cart sidebar */ - .sticky-cart { - position: static !important; - margin-top: 2rem; - width: 100%; - } - - .cart-items { - max-height: 400px; - } - - #cart-items-container { - width: 100%; - padding: 0.75rem; - } - - .list-group-item { - padding: 0.75rem; - } - - .list-group-item h6 { - font-size: 0.95rem; - } - - .list-group-item strong { - min-width: 70px; - } - - .cart-header { - padding: 1rem; - width: 100%; - } - - .cart-header h5 { - font-size: 1.25rem; - } - - .cart-title-lg { - font-size: 1.25rem; - } - - #save-cart-btn, - #reload-cart-btn { - padding: 0.5rem 0.75rem; - font-size: 0.9rem; - margin-right: 0.25rem; - } - - .card-header .btn-group { - gap: 0.5rem !important; - } - - /* Order list grid */ - .eskaera-orders, - .eskaera-orders-grid { - grid-template-columns: repeat(2, 1fr); - gap: 1.5rem; - } -} - @media (max-width: 768px) { - /* Header (shared between eskaera and checkout) */ - .eskaera-order-header, - .checkout-header { - padding: 1rem; - } - - .eskaera-order-header h1, - .checkout-header h1 { - font-size: 1.5rem; - margin-bottom: 1rem; - word-wrap: break-word; - overflow-wrap: break-word; - } - - .checkout-title, - .eskaera-order-header h1 { - font-size: 1.5rem; - } - - .checkout-order-name { - font-size: 1.2rem; - } - - /* Fix header flex layout for mobile */ - .eskaera-order-header .d-flex, - .checkout-header .d-flex { - flex-direction: column !important; - gap: 1rem !important; - align-items: flex-start !important; - } + /* Header type and the info grid scale from layout/header.css; the title row + stacks through Bootstrap's flex-column/flex-md-row utilities. Order cards, + order list, product card and quantity control all size themselves from + their own files. */ .order-thumbnail-md { max-width: 100%; @@ -105,413 +23,54 @@ height: auto; } - /* Order info grid */ - .order-info-grid { - grid-template-columns: 1fr; - gap: 1rem; - } - - /* Eskaera page headings */ - .eskaera-page h1 { - font-size: 2rem; - } - - .eskaera-page > .container > .row:first-child p { - font-size: 1.1rem !important; - } - - /* Order cards */ - .eskaera-order-card { - min-height: 250px; - } - - .eskaera-order-card .card-body { - padding: 0.5rem 0.6rem 0 0.6rem; - gap: 0.15rem; - } - - .eskaera-order-card .card-title { - font-size: 0.9rem; - -webkit-line-clamp: 3; - } - - .order-desc-text { - font-size: 0.75rem; - -webkit-line-clamp: 2; - } - - .eskaera-order-card .btn { - padding: 0.5rem 1rem; - font-size: 0.8rem; - margin-bottom: 0.5rem; - } - .order-thumbnail-sm { - width: 70px; - height: 70px; - } - - .order-thumbnail-md { - width: 100px; - height: 100px; - } - - /* Header with image and text */ - .eskaera-order-card .d-flex { - gap: 0.75rem !important; - } - - .eskaera-order-card .card-footer { - padding: 0.75rem 1rem; - } - - /* Order list */ - .eskaera-orders, - .eskaera-orders-grid { - grid-template-columns: 1fr !important; - gap: 1.25rem; - } - - /* Product grid */ - .card-grid { - grid-template-columns: 1fr; - } - - /* Product quantity controls */ - .add-to-cart-form .product-qty { - font-size: 1rem; - min-width: 50px; - height: 32px; - padding: 0.25rem 0.35rem; - } - - .qty-control .qty-decrease, - .qty-control .qty-increase { - height: 32px; - width: 32px; - font-size: 0.9rem; - } - - .add-to-cart-form .add-to-cart-btn { - height: 36px; - min-height: 36px; - padding: 0.35rem 0.5rem; - font-size: 1rem; - min-width: 44px; - } - - .add-to-cart-form .input-group { - gap: 0.25rem; - } - - /* Checkout summary */ - .checkout-summary-container { - padding: 1rem; - overflow-x: auto; - } - - .checkout-summary-table { - font-size: 0.95rem; - min-width: 500px; /* Prevent table collapse */ - } - - .checkout-summary-table thead th { - padding: 0.75rem 0.5rem; - font-size: 0.85rem; - } - - .checkout-summary-table tbody td { - padding: 0.75rem 0.5rem; - } - - .total-amount { - font-size: 1.5rem; - } - - .summary-heading { - font-size: 1.5rem; - } - - .checkout-actions .btn { - font-size: 1rem; - padding: 0.6rem 1.2rem; + width: 4.375rem; + height: 4.375rem; } .group-order-header { - padding: 1rem 0; - margin-bottom: 1rem; - } - - .product-card { - margin-bottom: 1rem; - } - - .product-card .product-image { - height: 150px; - } - - .checkout-container { - padding: 1rem; + padding: var(--ac-space-md) 0; + margin-bottom: var(--ac-space-md); } } @media (max-width: 576px) { - /* Cart items */ - .list-group-item.d-flex { - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - } - - .list-group-item h6 { - font-size: 0.85rem; - word-break: break-word; - } - - .list-group-item small { - font-size: 0.75rem; - } - - .list-group-item .d-flex { - width: 100%; - justify-content: space-between; - align-items: center; - } - - .list-group-item strong { - min-width: auto; - font-size: 0.95rem; - } - - .list-group-item .remove-from-cart { - min-width: 28px; - min-height: 28px; - } - - .cart-items { - max-height: 250px; - } - - .sticky-cart { - margin-top: 1.5rem; - } + /* Checkout type scales fluidly from sections/checkout.css; only the table's + density genuinely changes here, and the container's padding with it. */ .toast-notification { - bottom: 20px; - right: 20px; - left: 20px; + inset: auto var(--ac-space-md) var(--ac-space-md); max-width: none; } - #save-cart-btn, - #reload-cart-btn { - padding: 0.3rem 0.4rem; - font-size: 0.8rem; - } - .card-header .d-flex { flex-wrap: wrap; - gap: 0.5rem !important; - } - - .card-header .btn-group { - gap: 0.25rem !important; - } - - .card-header .btn-group .btn { - padding: 0.3rem 0.4rem; - font-size: 0.8rem; - } - - .form-check { - padding: 1rem 1.5rem !important; - margin-left: 0 !important; - } - - .form-check-input { - margin-left: 0 !important; - margin-right: 1rem !important; - flex-shrink: 0; - } - - .form-check-label[for="home-delivery-checkbox"] { - font-size: 1rem !important; - margin-left: 0 !important; - } - - /* Header */ - .eskaera-order-header, - .checkout-header { - padding: 0.75rem; - } - - .eskaera-order-header h1, - .checkout-header h1 { - font-size: 1.25rem; - padding-bottom: 0.5rem; - } - - .checkout-title, - .eskaera-order-header h1 { - font-size: 1.25rem; - } - - .checkout-order-name { - font-size: 1rem; - } - - .info-label { - font-size: 0.9rem; - } - - /* Eskaera page */ - .eskaera-page h1 { - font-size: 1.5rem; - margin-bottom: 0.75rem; - } - - .eskaera-page > .container > .row:first-child { - margin-bottom: 1.5rem; - padding-bottom: 1rem; - } - - .eskaera-page > .container > .row:first-child p { - font-size: 1rem !important; - } - - /* Order cards */ - .eskaera-order-card { - min-height: 220px; - } - - .eskaera-order-card .card-body { - padding: 0.4rem 0.5rem 0 0.5rem; - } - - .eskaera-order-card .card-title { - font-size: 0.85rem; - } - - .order-desc-text { - font-size: 0.7rem; - } - - .eskaera-order-card .btn { - padding: 0.45rem 0.85rem; - font-size: 0.75rem; - margin-bottom: 0.4rem; } .order-thumbnail-sm { - width: 60px; - height: 60px; + width: 3.75rem; + height: 3.75rem; } .order-thumbnail-md { - width: 80px; - height: 80px; - } - - /* Order list */ - .eskaera-orders, - .eskaera-orders-grid { - grid-template-columns: 1fr !important; - gap: 1rem; + width: 5rem; + height: 5rem; } .eskaera-empty-state { - padding: 2rem 0.5rem; + padding: var(--ac-space-xl) var(--ac-space-sm); } - .eskaera-empty-state .alert { - font-size: 0.95rem; - padding: 1rem; - } - - /* Checkout */ .checkout-summary-container { - padding: 0.75rem; + padding: var(--ac-space-sm); } .checkout-summary-table { - font-size: 0.85rem; - min-width: 450px; - } - - .checkout-summary-table thead th { - padding: 0.5rem 0.35rem; - font-size: 0.75rem; + min-width: 28rem; } + .checkout-summary-table thead th, .checkout-summary-table tbody td { - padding: 0.5rem 0.35rem; - font-size: 0.85rem; - } - - .total-amount { - font-size: 1.3rem; - } - - .total-label { - font-size: 1rem; - } - - .currency { - font-size: 1rem; - } - - .summary-heading { - font-size: 1.25rem; - padding-left: 0.75rem; - } - - .checkout-actions .btn { - font-size: 0.9rem; - padding: 0.6rem 1rem; - } -} - -/* Product card typography responsive scaling */ -@media screen and (min-width: 1600px) { - .product-tags { - font-size: 1.1rem !important; - } - - /* Scale down quantity input for 6-column layout */ - .add-to-cart-form .product-qty { - font-size: 0.85rem; - max-width: 55px; - } - - .add-to-cart-form .qty-decrease, - .add-to-cart-form .qty-increase { - font-size: 0.75rem; - min-width: 28px; - min-height: 28px; - } -} - -@media screen and (min-width: 1400px) and (max-width: 1599px) { - .product-tags { - font-size: 1.25rem !important; - } - - /* Scale down quantity input for 5-column layout */ - .add-to-cart-form .product-qty { - font-size: 0.9rem; - max-width: 60px; - } - - .add-to-cart-form .qty-decrease, - .add-to-cart-form .qty-increase { - font-size: 0.8rem; - min-width: 30px; - min-height: 30px; - } -} - -@media (max-width: 720px) { - .card-grid { - grid-template-columns: 1fr; + padding: var(--ac-space-sm) var(--ac-space-2xs); } } diff --git a/website_sale_aplicoop/static/src/css/sections/checkout.css b/website_sale_aplicoop/static/src/css/sections/checkout.css index 7418215..724ecb6 100644 --- a/website_sale_aplicoop/static/src/css/sections/checkout.css +++ b/website_sale_aplicoop/static/src/css/sections/checkout.css @@ -1,64 +1,67 @@ /* filepath: website_sale_aplicoop/static/src/css/sections/checkout.css */ /** - * Checkout page section styles + * Checkout page + * + * The summary table keeps a min-width and scrolls inside its own container + * rather than reflowing into cards: the four columns (product, qty, price, + * subtotal) are read across, and stacking them loses that. The container is + * focusable so the scroll is reachable without a pointer. */ -.checkout-container { - background-color: white; - padding: 2rem; - border-radius: 0.5rem; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); -} - .checkout-summary { - background-color: #f8f9fa; - padding: 1.5rem; - border-radius: 0.5rem; - margin-bottom: 2rem; + margin-bottom: var(--ac-space-xl); + padding: var(--ac-space-lg); + border-radius: var(--ac-radius-md); + background-color: var(--ac-surface-sunken); } .checkout-summary-container { - background: white; - border-radius: 0.75rem; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - padding: 1.5rem; - margin-bottom: 2rem; + margin-bottom: var(--ac-space-xl); + padding: var(--ac-space-lg); + border-radius: var(--ac-radius-lg); + background: var(--ac-surface); + box-shadow: var(--ac-shadow-sm); + overflow-x: auto; +} + +.checkout-summary-container:focus-visible { + outline: var(--ac-focus-ring-width) solid var(--ac-focus-ring-color); + outline-offset: var(--ac-focus-ring-offset); } .checkout-summary-table { margin-bottom: 0; - font-size: 1.15rem; -} - -.checkout-summary-table thead { - background-color: #2d3748; - color: white; + min-width: 30rem; + font-size: var(--ac-text-md); } .checkout-summary-table thead th { - font-weight: 700; - padding: 1rem 0.75rem; - text-transform: uppercase; - font-size: 1rem; - letter-spacing: 0.5px; + padding: var(--ac-space-md) var(--ac-space-xs); border: none; + font-size: var(--ac-text-base); + font-weight: var(--ac-weight-bold); + text-transform: uppercase; + letter-spacing: 0.05em; } .checkout-summary-table tbody tr { - border-bottom: 1px solid #e2e8f0; - transition: background-color 0.2s ease; -} - -.checkout-summary-table tbody tr:hover { - background-color: #f7fafc; + border-bottom: 1px solid var(--ac-border); + transition: background-color var(--ac-transition-fast); } .checkout-summary-table tbody td { - padding: 1rem 0.75rem; + padding: var(--ac-space-md) var(--ac-space-xs); vertical-align: middle; } +/* Figures line up column-wise instead of drifting with glyph widths. */ +.checkout-summary-table .col-price, +.checkout-summary-table .col-subtotal, +.checkout-summary-table td.text-end { + font-variant-numeric: tabular-nums; +} + .checkout-summary-table .col-name { width: 50%; } @@ -76,52 +79,68 @@ } .checkout-summary-table .empty-message { - background-color: #f7fafc; + background-color: var(--ac-surface-sunken); } +/* ---------- Total ---------- */ + .checkout-total-section { - border-top: 2px solid #e2e8f0; - padding-top: 1.5rem; - margin-top: 1rem; + margin-top: var(--ac-space-md); + padding-top: var(--ac-space-lg); + border-top: 2px solid var(--ac-border); } .total-row { display: flex; justify-content: flex-end; - align-items: center; - gap: 1rem; + align-items: baseline; + gap: var(--ac-space-sm); } .total-label { - font-weight: 700; - font-size: 1.2rem; - color: #1a202c; + font-size: var(--ac-text-md); + font-weight: var(--ac-weight-bold); + color: var(--ac-text-primary); } .total-amount { - font-size: 2rem; - font-weight: 700; - color: var(--success-color); - min-width: 120px; + min-width: 7.5rem; + font-size: var(--ac-text-2xl); + font-weight: var(--ac-weight-bold); + font-variant-numeric: tabular-nums; text-align: right; + /* --ac-color-success is 3.13:1 on white: fine for the 2xl total, but the + darker tone keeps it readable if the type scale ever shrinks. */ + color: var(--ac-color-success-strong); } .currency { - font-size: 1.2rem; - color: #2d3748; - font-weight: 600; + font-size: var(--ac-text-md); + font-weight: var(--ac-weight-semibold); + color: var(--ac-text-secondary); } .summary-heading { - font-size: 1.75rem; - font-weight: 700; - color: #1a202c; - border-left: 4px solid var(--success-color); - padding-left: 1rem; + padding-left: var(--ac-space-md); + border-left: 4px solid var(--ac-color-success-strong); + font-size: var(--ac-text-xl); + font-weight: var(--ac-weight-bold); + color: var(--ac-text-primary); } .checkout-order-desc { - word-wrap: break-word; + color: var(--ac-text-secondary); overflow-wrap: break-word; - color: #666; +} + +@media (hover: hover) and (pointer: fine) { + .checkout-summary-table tbody tr:hover { + background-color: var(--ac-surface-sunken); + } +} + +@media (prefers-reduced-motion: reduce) { + .checkout-summary-table tbody tr { + transition: none; + } } diff --git a/website_sale_aplicoop/static/src/css/sections/info-cards.css b/website_sale_aplicoop/static/src/css/sections/info-cards.css index 92927d2..c9cb1f4 100644 --- a/website_sale_aplicoop/static/src/css/sections/info-cards.css +++ b/website_sale_aplicoop/static/src/css/sections/info-cards.css @@ -1,63 +1,21 @@ /* filepath: website_sale_aplicoop/static/src/css/sections/info-cards.css */ /** - * Info cards and grid section styles + * Info cards + * + * The meta block that used to be duplicated here now lives with its component + * (components/order-card.css): `.card-meta-compact`, `.meta-label` and + * `.meta-value` were declared in both files, and this one — loading last — won, + * silently overriding the component. `.card-grid`, `.card-col`, `.card-meta-list` + * and `.meta-row` were dead: no template ever emitted them. */ .order-info-card { - background: white; - border-radius: 0.75rem; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + border-radius: var(--ac-radius-lg); + background: var(--ac-surface); + box-shadow: var(--ac-shadow-sm); } .order-info-card .card-body { - padding: 1rem; -} - -.card-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0.5rem; - align-items: start; - margin-top: 0.5rem; -} - -.card-meta-compact { - padding: 0.5rem 0; -} - -.card-meta-compact .card-meta-list { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.meta-row { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.25rem 0; -} - -.meta-label { - font-weight: 700; - color: #2d3748; - min-width: fit-content; - flex-shrink: 0; -} - -.meta-value { - font-weight: 400; - color: #27292c; - word-break: break-word; - flex-grow: 1; -} - -.card-col { - min-width: 0; -} - -.card-col .card-text strong { - font-weight: 700; - color: #111827; + padding: var(--ac-space-md); } diff --git a/website_sale_aplicoop/static/src/css/sections/order-list.css b/website_sale_aplicoop/static/src/css/sections/order-list.css index 69515b1..8ed3004 100644 --- a/website_sale_aplicoop/static/src/css/sections/order-list.css +++ b/website_sale_aplicoop/static/src/css/sections/order-list.css @@ -1,48 +1,54 @@ /* filepath: website_sale_aplicoop/static/src/css/sections/order-list.css */ /** - * Order list and Eskaera page section styles + * Order list (Eskaera index page) + * + * The grid derived its column count from three breakpoints (3 / 2 / 1) that + * each had to out-specify the previous one — the mobile rule needed + * !important. Deriving it from the available width removes all three. */ .eskaera-page h1 { - font-size: 2.5rem; - font-weight: 700; - color: #2d3748; - margin-bottom: 0.5rem; + margin-bottom: var(--ac-space-sm); + font-size: var(--ac-text-2xl); + font-weight: var(--ac-weight-bold); text-align: center; + color: var(--ac-text-primary); } .eskaera-page > .container > .row:first-child { - margin-bottom: 2rem; - border-bottom: 2px solid #e2e8f0; - padding-bottom: 1.5rem; + margin-bottom: var(--ac-space-xl); + padding-bottom: var(--ac-space-lg); + border-bottom: 2px solid var(--ac-border); } .eskaera-page > .container > .row:first-child p { - font-size: 1.3rem !important; - color: #4a5568; - text-align: center; margin: 0; - font-weight: 500; + font-size: var(--ac-text-md); + font-weight: var(--ac-weight-medium); + text-align: center; + color: var(--ac-text-secondary); } .eskaera-orders, .eskaera-orders-grid { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 2rem; + /* 20rem keeps three across a full-width container and drops to one on a + phone, without any breakpoint doing the counting. */ + grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr)); + gap: var(--ac-space-lg); margin: 0 auto; max-width: 100%; } .eskaera-empty-state { + padding: var(--ac-space-2xl) var(--ac-space-md); text-align: center; - padding: 3rem 1rem; } .eskaera-empty-state .alert { - max-width: 500px; + max-width: 31.25rem; margin: 0 auto; - font-size: 1.05rem; - border-radius: 0.5rem; + border-radius: var(--ac-radius-md); + font-size: var(--ac-text-base); } diff --git a/website_sale_aplicoop/static/src/css/sections/products-grid.css b/website_sale_aplicoop/static/src/css/sections/products-grid.css index be646d3..5151ca3 100644 --- a/website_sale_aplicoop/static/src/css/sections/products-grid.css +++ b/website_sale_aplicoop/static/src/css/sections/products-grid.css @@ -1,14 +1,29 @@ /* filepath: website_sale_aplicoop/static/src/css/sections/products-grid.css */ /** - * Products grid section styles + * Products grid + * + * The grid used to pin an exact column count at seven breakpoints (1 → 6). It + * now derives the count from the space available, which keeps the card width + * stable no matter how wide the sidebar or the container is. The single query + * left is the deliberate design decision: one card per row on phones, where a + * two-up grid would shrink names and controls below comfortable size. + * + * 9rem is the card's minimum width; at the widest container (1320px minus the + * category sidebar) it still resolves to six columns, as before. */ .products-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 1.2rem; - margin-bottom: 2rem; + grid-template-columns: 1fr; + gap: var(--ac-space-md); + margin-bottom: var(--ac-space-xl); +} + +@media (min-width: 576px) { + .products-grid { + grid-template-columns: repeat(auto-fill, minmax(min(100%, 9rem), 1fr)); + } } .product-card-wrapper { @@ -17,62 +32,7 @@ } .product-search-highlight { - border: 2px solid #007bff; - padding: 10px; -} - -/* Pantallas muy grandes: 1600px+ (6 columnas) */ -@media screen and (min-width: 1600px) { - .products-grid { - grid-template-columns: repeat(6, 1fr); - gap: 1.2rem; - } -} - -/* Pantallas grandes: 1400px-1599px (5 columnas) */ -@media screen and (min-width: 1400px) and (max-width: 1599px) { - .products-grid { - grid-template-columns: repeat(5, 1fr); - gap: 1.15rem; - } -} - -/* Pantallas medianas: 1200px-1399px (4 columnas) */ -@media screen and (min-width: 1200px) and (max-width: 1399px) { - .products-grid { - grid-template-columns: repeat(4, 1fr); - gap: 1.1rem; - } -} - -/* Pantallas tablet grandes: 992px-1199px (3 columnas) */ -@media screen and (min-width: 992px) and (max-width: 1199px) { - .products-grid { - grid-template-columns: repeat(3, 1fr); - gap: 1rem; - } -} - -/* Pantallas tablet: 768px-991px (3 columnas en tablet grande, 2 en tablet pequeña) */ -@media screen and (min-width: 768px) and (max-width: 991px) { - .products-grid { - grid-template-columns: repeat(3, 1fr); - gap: 0.95rem; - } -} - -/* Pantallas móvil grande: 576px-767px (2 columnas) */ -@media screen and (min-width: 576px) and (max-width: 767px) { - .products-grid { - grid-template-columns: repeat(2, 1fr); - gap: 0.8rem; - } -} - -/* Pantallas móvil pequeño: 1 columna */ -@media (max-width: 575px) { - .products-grid { - grid-template-columns: 1fr; - gap: 0.5rem; - } + padding: var(--ac-space-sm); + border: 2px solid var(--ac-color-primary); + border-radius: var(--ac-radius-md); } diff --git a/website_sale_aplicoop/static/src/js/checkout_labels.js b/website_sale_aplicoop/static/src/js/checkout_labels.js index b2537db..3971e74 100644 --- a/website_sale_aplicoop/static/src/js/checkout_labels.js +++ b/website_sale_aplicoop/static/src/js/checkout_labels.js @@ -194,10 +194,10 @@ '' + escapeHtml(labels.quantity) + "" + - '' + + '' + escapeHtml(labels.price) + "" + - '' + + '' + escapeHtml(labels.subtotal) + "" + '' + @@ -280,10 +280,10 @@ '' + qty.toFixed(2).replace(/\.?0+$/, "") + "" + - '€' + + '€' + price.toFixed(2) + "" + - '€' + + '€' + subtotal.toFixed(2) + ""; tbody.appendChild(row); @@ -307,10 +307,10 @@ '' + qty.toFixed(2).replace(/\.?0+$/, "") + "" + - '€' + + '€' + price.toFixed(2) + "" + - '€' + + '€' + subtotal.toFixed(2) + ""; tbody.appendChild(row); diff --git a/website_sale_aplicoop/static/src/js/infinite_scroll.js b/website_sale_aplicoop/static/src/js/infinite_scroll.js index 966d1d0..61735fc 100644 --- a/website_sale_aplicoop/static/src/js/infinite_scroll.js +++ b/website_sale_aplicoop/static/src/js/infinite_scroll.js @@ -459,8 +459,9 @@ console.log("[INFINITE_SCROLL] Script loaded!"); // Show fallback button var btn = document.getElementById("load-more-btn"); if (btn) { + // d-none is the only thing hiding it; the button no longer + // carries an inline display:none to clear. btn.classList.remove("d-none"); - btn.style.display = ""; } }); }, diff --git a/website_sale_aplicoop/static/src/js/realtime_search.js b/website_sale_aplicoop/static/src/js/realtime_search.js index 2eef78c..a6bb805 100644 --- a/website_sale_aplicoop/static/src/js/realtime_search.js +++ b/website_sale_aplicoop/static/src/js/realtime_search.js @@ -141,9 +141,6 @@ // Initialize available tags from DOM self._initializeAvailableTags(); - // Store original colors for each tag badge - self.originalTagColors = {}; // Maps tag ID to original color - // Store last values at instance level so polling can access them // Initialize to current values to avoid triggering reset on first poll self.lastSearchValue = self.searchInput.value.trim(); @@ -165,9 +162,9 @@ // This listener is separate from the filtering listener self.searchInput.addEventListener("input", function () { if (self.searchInput.value.trim().length > 0) { - self.clearSearchBtn.style.display = "block"; + self.clearSearchBtn.classList.add("is-visible"); } else { - self.clearSearchBtn.style.display = "none"; + self.clearSearchBtn.classList.remove("is-visible"); } }); @@ -179,7 +176,7 @@ // Clear the input self.searchInput.value = ""; - self.clearSearchBtn.style.display = "none"; + self.clearSearchBtn.classList.remove("is-visible"); // Update last stored value to prevent polling from detecting "change" self.lastSearchValue = ""; @@ -204,7 +201,7 @@ // Initial check - don't show if empty if (self.searchInput.value.trim().length > 0) { - self.clearSearchBtn.style.display = "block"; + self.clearSearchBtn.classList.add("is-visible"); } } @@ -283,114 +280,21 @@ var tagBadges = document.querySelectorAll('[data-toggle="tag-filter"]'); console.log("[realtimeSearch] Found " + tagBadges.length + " tag filter badges"); - // Get theme colors from CSS variables - var rootStyles = getComputedStyle(document.documentElement); - var primaryColor = - rootStyles.getPropertyValue("--bs-primary").trim() || - rootStyles.getPropertyValue("--primary").trim() || - "#0d6efd"; - var secondaryColor = - rootStyles.getPropertyValue("--bs-secondary").trim() || - rootStyles.getPropertyValue("--secondary").trim() || - "#6c757d"; - - console.log( - "[realtimeSearch] Theme colors - Primary:", - primaryColor, - "Secondary:", - secondaryColor - ); - - // Store original colors for each badge BEFORE adding event listeners - tagBadges.forEach(function (badge) { - var tagId = parseInt(badge.getAttribute("data-tag-id"), 10); - var tagColor = badge.getAttribute("data-tag-color"); - - // Store the original color (either from data-tag-color or use secondary for tags without color) - if (tagColor) { - self.originalTagColors[tagId] = tagColor; - console.log( - "[realtimeSearch] Stored original color for tag " + tagId + ": " + tagColor - ); - } else { - self.originalTagColors[tagId] = "var(--bs-secondary, " + secondaryColor + ")"; - console.log("[realtimeSearch] Tag " + tagId + " has no color, using secondary"); - } - }); - tagBadges.forEach(function (badge) { badge.addEventListener("click", function (e) { e.preventDefault(); var tagId = parseInt(badge.getAttribute("data-tag-id"), 10); - var originalColor = self.originalTagColors[tagId]; - console.log( - "[realtimeSearch] Tag badge clicked: " + - tagId + - " (original color: " + - originalColor + - ")" - ); // Toggle tag selection if (self.selectedTags.has(tagId)) { - // Deselect self.selectedTags.delete(tagId); console.log("[realtimeSearch] Tag " + tagId + " deselected"); } else { - // Select self.selectedTags.add(tagId); console.log("[realtimeSearch] Tag " + tagId + " selected"); } - // Update colors for ALL badges based on selection state - tagBadges.forEach(function (badge) { - var id = parseInt(badge.getAttribute("data-tag-id"), 10); - - if (self.selectedTags.size === 0) { - // No tags selected: restore all to original colors - var originalColor = self.originalTagColors[id]; - badge.style.setProperty("background-color", originalColor, "important"); - badge.style.setProperty("border-color", originalColor, "important"); - badge.style.setProperty("color", "#ffffff", "important"); - console.log( - "[realtimeSearch] Badge " + - id + - " reset to original color (no selection)" - ); - } else if (self.selectedTags.has(id)) { - // Selected: primary color - badge.style.setProperty( - "background-color", - "var(--bs-primary, " + primaryColor + ")", - "important" - ); - badge.style.setProperty( - "border-color", - "var(--bs-primary, " + primaryColor + ")", - "important" - ); - badge.style.setProperty("color", "#ffffff", "important"); - console.log( - "[realtimeSearch] Badge " + id + " set to primary (selected)" - ); - } else { - // Not selected but others are: secondary color - badge.style.setProperty( - "background-color", - "var(--bs-secondary, " + secondaryColor + ")", - "important" - ); - badge.style.setProperty( - "border-color", - "var(--bs-secondary, " + secondaryColor + ")", - "important" - ); - badge.style.setProperty("color", "#ffffff", "important"); - console.log( - "[realtimeSearch] Badge " + id + " set to secondary (not selected)" - ); - } - }); + self._syncTagBadgeStates(tagBadges); // Filter products (independent of search/category state) // Skip during initialization @@ -647,6 +551,10 @@ ")" ); + // Announce the outcome to screen readers, which otherwise get no + // signal that the grid changed under them. + self._announceResults(visibleCount); + // Auto-load more products if too few are visible self._autoLoadMoreIfNeeded(visibleCount); } catch (error) { @@ -655,6 +563,49 @@ } }, + /** + * Reflect the current selection on every tag badge: aria-pressed for + * assistive tech, two state classes for the eye. Colours belong to + * components/tag-filter.css — this only says which state each badge is in. + * + * @param {NodeList} tagBadges - All badges in the filter bar + */ + _syncTagBadgeStates: function (tagBadges) { + var self = this; + var hasSelection = self.selectedTags.size > 0; + + tagBadges.forEach(function (badge) { + var id = parseInt(badge.getAttribute("data-tag-id"), 10); + var isSelected = self.selectedTags.has(id); + + badge.classList.toggle("is-selected", isSelected); + badge.classList.toggle("is-dimmed", hasSelection && !isSelected); + badge.setAttribute("aria-pressed", isSelected ? "true" : "false"); + }); + }, + + /** + * Write the result count into the polite live region next to the search + * box. Labels come from i18nManager, never from a hardcoded literal. + * + * @param {number} visibleCount - Number of currently visible products + */ + _announceResults: function (visibleCount) { + var status = document.getElementById("realtime-search-status"); + if (!status) { + return; + } + var i18n = window.i18nManager; + var message; + if (visibleCount === 0) { + message = (i18n && i18n.get("no_results")) || "No products found"; + } else { + message = + visibleCount + " " + ((i18n && i18n.get("products_found")) || "products found"); + } + status.textContent = message; + }, + _autoLoadMoreIfNeeded: function (visibleCount) { /** * Automatically load more products if there are too few visible on screen. diff --git a/website_sale_aplicoop/static/src/js/website_sale.js b/website_sale_aplicoop/static/src/js/website_sale.js index 1cc0cee..4edee32 100644 --- a/website_sale_aplicoop/static/src/js/website_sale.js +++ b/website_sale_aplicoop/static/src/js/website_sale.js @@ -691,12 +691,16 @@ var notification = document.createElement("div"); notification.className = - "alert alert-" + type + " alert-dismissible fade show position-fixed"; - notification.style.cssText = - "top: 80px; right: 20px; z-index: 9999; min-width: 300px; max-width: 500px; font-size: 18px; padding: 1.5rem; transition: opacity 0.3s ease-out;"; + "alert alert-" + type + " alert-dismissible fade show eskaera-notification"; + // Announce it: errors and warnings are the whole point of this helper, + // and without a live role a screen reader never hears them. + notification.setAttribute("role", type === "error" ? "alert" : "status"); + notification.setAttribute("aria-live", type === "error" ? "assertive" : "polite"); notification.innerHTML = message + - ''; + ''; document.body.appendChild(notification); @@ -1253,13 +1257,17 @@ // Create toast element var toast = document.createElement("div"); toast.className = "toast-notification"; + toast.setAttribute("role", "status"); // Get translated "added to cart" label from server var labels = this._getLabels(); var addedMsg = labels.added_to_cart || "added to cart"; toast.innerHTML = - ' ' + productName + " " + addedMsg; + ' ' + + this.escapeHtml(productName) + + " " + + addedMsg; document.body.appendChild(toast); @@ -1551,7 +1559,7 @@ // Simple approach: if response is very short, assume no more products if (html.trim().length < 100) { console.log("[LAZY_LOADING] No more products, hiding load-more button"); - btn.style.display = "none"; + btn.classList.add("d-none"); } else { // Re-enable button for next page btn.disabled = false; @@ -1595,7 +1603,7 @@ if (Object.keys(this.cart).length === 0) { cartContainer.innerHTML = - '

' + + '

' + (labels.empty_cart || "This order's cart is empty.") + "

"; return; @@ -1612,20 +1620,20 @@ total += subtotal; html += - '
' + + '
' + '
' + - '
' + + '
' + self.escapeHtml(item.name) + "
" + "
" + '
' + - '' + + '' + parseFloat(item.qty).toFixed(1) + " x €" + item.price.toFixed(2) + "" + '
' + - '€' + + "€" + subtotal.toFixed(2) + "" + '
"; + html += '
' + "Total: €" + total.toFixed(2) + "
"; cartContainer.innerHTML = html; diff --git a/website_sale_aplicoop/views/load_from_history_templates.xml b/website_sale_aplicoop/views/load_from_history_templates.xml index 833b818..1888d1c 100644 --- a/website_sale_aplicoop/views/load_from_history_templates.xml +++ b/website_sale_aplicoop/views/load_from_history_templates.xml @@ -3,12 +3,45 @@