Compare commits
2 commits
158933e96e
...
5de7573fba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5de7573fba | ||
|
|
162ba0b570 |
40 changed files with 3054 additions and 2412 deletions
|
|
@ -10,7 +10,7 @@ metadata:
|
|||
## Style and conventions
|
||||
- ES6+ (`let`/`const`, arrow functions). Linting with ESLint (`.eslintrc.js` at root).
|
||||
- Odoo pattern: AMD modules (`odoo.define`).
|
||||
- **No business logic in JS, only UI and events.** Business logic lives in Python.
|
||||
- **No business logic in JS (pricing rules, stock/availability decisions, permission checks)** — these belong in Python. JS handles only rendering, DOM events, and calling Python endpoints via `ajax.jsonRpc`.
|
||||
- Comments to explain hacks or workarounds.
|
||||
|
||||
## Key patterns
|
||||
|
|
|
|||
182
.claude/skills/odoo-web-design/SKILL.md
Normal file
182
.claude/skills/odoo-web-design/SKILL.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
---
|
||||
name: odoo-web-design
|
||||
description: Professional finish for the Odoo frontend of this repo — visual polish, responsive layout and WCAG accessibility across CSS, QWeb/HTML and JS. Use when styling or reviewing website/portal pages (website_sale_aplicoop), adding UI components, or fixing responsive/a11y/design-consistency issues.
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
|
||||
# Web design & finish (Odoo 18 frontend)
|
||||
|
||||
Covers the three layers together: `static/src/css/`, `static/src/js/`, and the QWeb templates in
|
||||
`views/*.xml`. For language-level conventions see `odoo-qweb-html`, `odoo-javascript`,
|
||||
`odoo-xml-views`; this skill is about the **finish**: does it look designed, does it work at 320 px,
|
||||
does it work without a mouse.
|
||||
|
||||
## Inherited non-negotiables
|
||||
|
||||
- No logic in QWeb — prepare everything in the controller.
|
||||
- No business logic in JS — UI and events only.
|
||||
- **No inline `style="..."`** in templates. A style needs a class, and the class needs a token.
|
||||
- Every user-visible string is translatable (see §4).
|
||||
|
||||
## 1. Work order
|
||||
|
||||
Never start by editing a component. Always:
|
||||
|
||||
1. **Audit** with the commands in §7. Write down what is inconsistent before touching anything.
|
||||
2. **Tokens first** (`base/variables.css`). Every value a component needs must already exist as a token.
|
||||
3. **Component by component**, one file each, media queries co-located at the bottom of that file.
|
||||
4. **Accessibility pass** per template, top to bottom.
|
||||
5. **Verify** — lint, contrast, keyboard-only, 320 px, 200 % zoom.
|
||||
|
||||
Doing 3 before 2 is how a codebase ends up with 61 `!important` and three shades of the same blue.
|
||||
|
||||
## 2. Design tokens
|
||||
|
||||
Odoo 18 ships Bootstrap 5.3 with `$variable-prefix: ''`
|
||||
(`/media/snt/data/odoo/ocb/addons/web/static/src/scss/bootstrap_overridden.scss:51`), so Bootstrap's
|
||||
root custom properties are emitted **unprefixed** into `web.assets_frontend` (via
|
||||
`import_bootstrap.scss` → `@import "root"`). Available in plain CSS, no SCSS needed:
|
||||
|
||||
```
|
||||
--primary --secondary --success --danger --warning --info --light --dark
|
||||
--body-color --body-bg --emphasis-color --secondary-bg --tertiary-bg --border-color
|
||||
--border-radius --border-radius-sm --border-radius-lg --border-radius-xl --border-radius-pill
|
||||
--link-color --link-hover-color --focus-ring-color --focus-ring-width
|
||||
--box-shadow --box-shadow-sm --box-shadow-lg --font-sans-serif --gray-100 … --gray-900
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- **Alias, don't copy.** `--ac-color-primary: var(--primary, #007bff);` keeps the website theme
|
||||
editor in control of the palette. A hardcoded brand hex breaks theming silently.
|
||||
- **Hex literals live only in `base/variables.css`.** A hex anywhere else is a defect.
|
||||
- **Prefix new tokens** (`--ac-…`, aplicoop). Bare names like `--primary-color` sit in Bootstrap's
|
||||
unprefixed namespace and will collide as Odoo adds tokens.
|
||||
- **Never redefine a token inside a media query** to shrink it — use `clamp()` so it scales
|
||||
continuously instead of jumping at one width.
|
||||
- Space and radius come from the scale, never from a typed-in number. If you need a value the scale
|
||||
doesn't have, the scale is wrong — fix the scale.
|
||||
|
||||
## 3. Responsive
|
||||
|
||||
- **Mobile-first, `min-width` only.** Bootstrap 5 breakpoints exclusively: **576 / 768 / 992 / 1200 /
|
||||
1400**. No 480/720/1600 one-offs — a private breakpoint is a design decision nobody else knows about.
|
||||
- **One range per rule.** Pairing `min-width` and `max-width` to carve a band is almost always a
|
||||
symptom of desktop-first thinking leaking in.
|
||||
- **Media queries belong with their component**, at the bottom of its file. A central
|
||||
`responsive.css` drifts out of sync with the components it overrides and forces `!important`.
|
||||
Don't add to it; empty it as you touch each component.
|
||||
If a rule in `responsive.css` spans multiple components or has no clear owner, move it to the
|
||||
relevant layout partial (e.g. `layout/grid.css`) rather than duplicating it per component; note
|
||||
any such moves in the audit output.
|
||||
- **Prefer intrinsic layout to breakpoints**: `repeat(auto-fill, minmax(min(100%, 16rem), 1fr))`,
|
||||
`clamp()`, `flex-wrap`. Most grids need zero media queries.
|
||||
- `rem` for everything; `px` only for hairlines, borders and shadow offsets.
|
||||
- Touch targets: **24×24 px is the AA floor** (WCAG 2.2 §2.5.8); **44×44 px is §2.5.5, level AAA**,
|
||||
and the size a thumb actually needs — apply it under `@media (pointer: coarse)`. Quantity steppers
|
||||
and icon buttons are the usual offenders.
|
||||
- Must survive **320 px wide** and **200 % zoom** with no horizontal scroll.
|
||||
|
||||
Details and recipes: `references/responsive.md`.
|
||||
|
||||
## 4. Accessibility — WCAG 2.2 AA baseline
|
||||
|
||||
- **Semantics before ARIA**: `<button>`, `<nav>`, `<ul>`, `<label for>`. ARIA only when no element
|
||||
expresses the meaning. A `<div onclick>` is never acceptable.
|
||||
- **Contrast**: 4.5:1 body text, 3:1 for ≥24 px (or bold ≥19 px), 3:1 for form-control borders,
|
||||
icon-only buttons and focus rings.
|
||||
- **Focus**: never `outline: none` without a replacement. Style `:focus-visible`, not `:focus` — the
|
||||
latter also fires on mouse click and gets removed out of frustration.
|
||||
- Every input has a real `<label>`. A placeholder is not a label.
|
||||
- Icon-only control → translatable `aria-label`, or `.visually-hidden` text (Bootstrap ships the class).
|
||||
- Dynamic updates (cart total, quantity, result count) → `aria-live="polite"` region.
|
||||
- Images: meaningful `alt`, or `alt=""` when decorative. Never omit the attribute.
|
||||
- Status is never conveyed by colour alone — pair it with text or an icon.
|
||||
- Every animation you add needs a `@media (prefers-reduced-motion: reduce)` escape.
|
||||
|
||||
**Translatable QWeb attributes** (auto-extracted, `odoo/tools/translate.py:163`), plus their
|
||||
`t-attf-` forms:
|
||||
|
||||
```
|
||||
string help placeholder alt title label data-tooltip confirm
|
||||
aria-label aria-keyshortcuts aria-placeholder aria-roledescription aria-valuetext
|
||||
```
|
||||
|
||||
`aria-description` is **not** translated — use `aria-label` or visually-hidden text instead.
|
||||
Strings injected from JS go through `window.i18nManager.get(key)` (served by `/eskaera/i18n`), with
|
||||
the key added server-side — never a hardcoded literal, including in live-region announcements.
|
||||
|
||||
Per-component patterns: `references/accessibility.md`.
|
||||
|
||||
## 5. JS for finish (UI only)
|
||||
|
||||
- **Delegated events** — content is lazy-loaded and infinite-scrolled, direct listeners die.
|
||||
- After injecting nodes: move or restore focus deliberately, announce the new count in the live
|
||||
region, and set `aria-busy` on the container while fetching.
|
||||
- Check `matchMedia("(prefers-reduced-motion: reduce)")` before animating or smooth-scrolling.
|
||||
- Toggle **classes**, don't write `element.style.*` — inline styles from JS are the same defect as
|
||||
inline styles in templates, just later.
|
||||
- Debounce search input ≥ 200 ms and announce "N results" politely.
|
||||
- Batch DOM reads and writes; never read `offsetHeight` inside a loop that also writes.
|
||||
|
||||
## 6. Asset delivery
|
||||
|
||||
`website_sale.css` pulls its 17 partials with `@import`. Odoo rewrites those to absolute URLs and
|
||||
hoists them to the top of the bundle (`odoo/addons/base/models/assetsbundle.py:909` and `:525`), so
|
||||
they ship as ~17 extra render-blocking requests **outside** the minified bundle. For a production
|
||||
finish, prefer listing each file in the manifest's `web.assets_frontend` (order: variables →
|
||||
utilities → layout → components → sections). Only rename them to `.scss` if the maintainer
|
||||
explicitly requests SCSS compilation so Odoo inlines the imports at compile time.
|
||||
|
||||
## 7. Audit & verify
|
||||
|
||||
If `/media/snt/data/addons-cm/website_sale_aplicoop` does not exist, run the audit commands from the
|
||||
repository root of the current workspace instead, and report which path was used.
|
||||
|
||||
```bash
|
||||
cd /media/snt/data/addons-cm/website_sale_aplicoop
|
||||
|
||||
# Breakpoint sprawl — should only ever list 576/768/992/1200/1400, all min-width
|
||||
grep -rho "@media[^{]*" static/src/css | sort | uniq -c | sort -rn
|
||||
|
||||
# Hardcoded colours outside the token file (expect: none)
|
||||
grep -rn "#[0-9a-fA-F]\{3,8\}\b" static/src/css --include="*.css" | grep -v "base/variables.css"
|
||||
|
||||
# Specificity debt
|
||||
grep -rc "!important" static/src/css/*/*.css | grep -v ':0'
|
||||
|
||||
# Focus handling: :focus-visible should dominate, outline:none must be paired with a replacement
|
||||
grep -rn ":focus-visible\|outline: *none\|outline: *0" static/src/css
|
||||
|
||||
# Motion safety (expect at least one per animating component)
|
||||
grep -rn "prefers-reduced-motion" static/src/css
|
||||
|
||||
# Inline styles in templates (expect: none)
|
||||
grep -rn 'style="' views/*.xml
|
||||
|
||||
# Images without alt
|
||||
grep -o "<img[^>]*" views/*.xml | grep -v "alt="
|
||||
|
||||
# Interactive divs/spans (expect: none)
|
||||
grep -rn "<div[^>]*t-on-click\|<span[^>]*t-on-click\|<div[^>]*onclick" views/*.xml
|
||||
```
|
||||
|
||||
Then, always:
|
||||
|
||||
```bash
|
||||
pre-commit run --all-files # prettier (width 100, indent 4) over css/js/xml + eslint
|
||||
docker-compose up -d # http://localhost:8070 — check at 320/768/1280 and 200% zoom
|
||||
```
|
||||
|
||||
Manual passes that no grep replaces: **Tab through the whole page** (visible focus everywhere, order
|
||||
matches visual order, no trap), and check contrast on the actual rendered colours.
|
||||
|
||||
## 8. References
|
||||
|
||||
- `references/accessibility.md` — WCAG AA patterns as QWeb snippets, per component.
|
||||
- `references/responsive.md` — breakpoints, intrinsic layout, images, test matrix.
|
||||
- `references/css-architecture.md` — file layout, naming, specificity, killing `!important`.
|
||||
- `references/finish-checklist.md` — the polish pass: states, rhythm, empty/loading/error.
|
||||
|
||||
Repo docs: `docs/QWEB_BEST_PRACTICES.md`, `docs/LAZY_LOADING.md`.
|
||||
245
.claude/skills/odoo-web-design/references/accessibility.md
Normal file
245
.claude/skills/odoo-web-design/references/accessibility.md
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
# Accessibility patterns (QWeb + CSS + JS)
|
||||
|
||||
WCAG 2.2 AA. Snippets use this repo's conventions: 4-space indent, Bootstrap 5.3 utilities,
|
||||
translatable attributes, `window.i18nManager.get(key)` for JS strings.
|
||||
|
||||
## Page skeleton
|
||||
|
||||
```xml
|
||||
<t t-call="website.layout">
|
||||
<a href="#eskaera-main" class="visually-hidden-focusable btn btn-primary m-2">
|
||||
Skip to main content
|
||||
</a>
|
||||
<div id="wrap" class="eskaera-shop-page">
|
||||
<main id="eskaera-main" tabindex="-1">
|
||||
<h1 class="eskaera-page-title" t-esc="page_title"/>
|
||||
...
|
||||
</main>
|
||||
</div>
|
||||
</t>
|
||||
```
|
||||
|
||||
- Exactly one `<h1>` per page; never skip heading levels to get a font size — that's what classes are for.
|
||||
- `<main>`, `<nav aria-label="...">`, `<aside>` instead of anonymous `<div>`s.
|
||||
- `tabindex="-1"` on the skip-link target so focus actually lands there.
|
||||
|
||||
## Buttons vs links
|
||||
|
||||
| Intent | Element |
|
||||
| --- | --- |
|
||||
| Navigates to a URL | `<a href="...">` |
|
||||
| Changes state on this page | `<button type="button">` |
|
||||
| Submits a form | `<button type="submit">` |
|
||||
|
||||
Never `<div t-on-click>` / `<span onclick>` — no keyboard, no role, no focus.
|
||||
|
||||
Icon-only:
|
||||
|
||||
```xml
|
||||
<button type="button" class="btn btn-outline-secondary qty-decrease"
|
||||
aria-label="Decrease quantity">
|
||||
<i class="fa fa-minus" aria-hidden="true"/>
|
||||
</button>
|
||||
```
|
||||
|
||||
`aria-label` is auto-translated; `aria-hidden` on the icon stops the screen reader announcing
|
||||
"minus icon" twice. Alternative when the label is long: visible `<span class="visually-hidden">`.
|
||||
|
||||
## Quantity stepper (live-announced)
|
||||
|
||||
```xml
|
||||
<div class="qty-control" role="group" aria-label="Quantity">
|
||||
<button type="button" class="btn qty-decrease" aria-label="Decrease quantity">
|
||||
<i class="fa fa-minus" aria-hidden="true"/>
|
||||
</button>
|
||||
<input type="number" class="form-control qty-input"
|
||||
t-att-value="line.qty" min="0" step="1"
|
||||
aria-label="Quantity" inputmode="numeric"/>
|
||||
<button type="button" class="btn qty-increase" aria-label="Increase quantity">
|
||||
<i class="fa fa-plus" aria-hidden="true"/>
|
||||
</button>
|
||||
</div>
|
||||
<p class="visually-hidden" aria-live="polite" id="qty_status"/>
|
||||
```
|
||||
|
||||
```js
|
||||
// after the server confirms the new qty
|
||||
const status = document.getElementById("qty_status");
|
||||
status.textContent = window.i18nManager.get("qty_updated_announcement");
|
||||
```
|
||||
|
||||
Buttons must stay ≥ 24×24 px (WCAG 2.2 AA §2.5.8) everywhere, and ≥ 44×44 px under
|
||||
`@media (pointer: coarse)` — including at 320 px width.
|
||||
|
||||
## Forms and errors
|
||||
|
||||
```xml
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="delivery_note">Delivery note</label>
|
||||
<textarea id="delivery_note" name="delivery_note" class="form-control"
|
||||
t-att-aria-invalid="'true' if errors.get('delivery_note') else None"
|
||||
t-att-aria-describedby="'delivery_note_help delivery_note_error'
|
||||
if errors.get('delivery_note')
|
||||
else 'delivery_note_help'"/>
|
||||
<small id="delivery_note_help" class="form-text">Optional, visible to the group manager.</small>
|
||||
<p t-if="errors.get('delivery_note')" id="delivery_note_error"
|
||||
class="invalid-feedback d-block" t-esc="errors['delivery_note']"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
- The `for`/`id` pair is mandatory; wrapping alone is not enough for every AT.
|
||||
- Error text is referenced by `aria-describedby`, and never colour-only — keep the message.
|
||||
- The error summary at the top of a failed form should be focused on submit
|
||||
(`container.focus()` on a `tabindex="-1"` heading).
|
||||
- Required fields: `required` + a visible "(required)" or `*` with a legend explaining it.
|
||||
|
||||
## Product card — one link, no duplicates
|
||||
|
||||
```xml
|
||||
<article class="product-card">
|
||||
<img class="product-card-img" t-att-src="product['image_url']"
|
||||
t-att-alt="product['name']" loading="lazy" width="400" height="400"/>
|
||||
<h3 class="product-card-title">
|
||||
<a t-att-href="product['url']" class="stretched-link" t-esc="product['name']"/>
|
||||
</h3>
|
||||
<p class="product-card-price">
|
||||
<span class="visually-hidden">Price</span>
|
||||
<span t-esc="product['price_formatted']"/>
|
||||
</p>
|
||||
</article>
|
||||
```
|
||||
|
||||
- `stretched-link` makes the whole card clickable with a **single** link in the a11y tree — do not
|
||||
also wrap the image in an `<a>`, that produces two identical stops.
|
||||
- `width`/`height` reserve space (no layout shift); CSS `object-fit: cover` handles the ratio.
|
||||
- Decorative product placeholder → `alt=""`.
|
||||
|
||||
## Filters as toggle buttons
|
||||
|
||||
```xml
|
||||
<div class="tag-filter" role="group" aria-label="Filter by category">
|
||||
<button t-foreach="categories" t-as="cat" t-key="cat['id']"
|
||||
type="button" class="tag-filter-badge"
|
||||
t-att-aria-pressed="'true' if cat['active'] else 'false'"
|
||||
t-att-data-category-id="cat['id']">
|
||||
<span t-esc="cat['name']"/>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
JS flips `aria-pressed` together with the active class — the class alone announces nothing.
|
||||
If the filters are a true multi-select set, `<fieldset><legend>` + checkboxes is better still.
|
||||
|
||||
## Search with results announcement
|
||||
|
||||
```xml
|
||||
<label class="visually-hidden" for="product_search">Search products</label>
|
||||
<input id="product_search" type="search" class="form-control"
|
||||
placeholder="Search products…" autocomplete="off"
|
||||
aria-describedby="search_results_count"/>
|
||||
<p id="search_results_count" class="visually-hidden" aria-live="polite" aria-atomic="true"/>
|
||||
```
|
||||
|
||||
Debounce ≥ 200 ms, then set the text once per settled query — announcing on every keystroke makes
|
||||
the page unusable with a screen reader.
|
||||
|
||||
## Loading, infinite scroll, empty states
|
||||
|
||||
```xml
|
||||
<div id="products_grid" class="products-grid" aria-busy="false">…</div>
|
||||
<p class="visually-hidden" aria-live="polite" id="load_status"/>
|
||||
<button type="button" id="load_more" class="btn btn-outline-primary">Load more products</button>
|
||||
```
|
||||
|
||||
- Set `aria-busy="true"` on the grid while fetching, back to `false` when done.
|
||||
- Announce `"N more products loaded"` via `load_status`; don't move focus mid-scroll.
|
||||
- **Always keep a real "Load more" button** alongside scroll-triggered loading — infinite scroll
|
||||
alone is unreachable by keyboard and traps users away from the footer.
|
||||
- Empty state is content, not a blank div: heading + explanation + the action that fixes it.
|
||||
|
||||
## Modals
|
||||
|
||||
Use Bootstrap's modal (focus trap, Esc, `aria-modal` come free) and give it a label:
|
||||
|
||||
```xml
|
||||
<div class="modal fade" id="confirm_order_modal" tabindex="-1"
|
||||
aria-labelledby="confirm_order_title" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="confirm_order_title">Confirm your order</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"/>
|
||||
</div>
|
||||
…
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Return focus to the trigger on close (Bootstrap does this if the trigger is a real button).
|
||||
|
||||
## Order tables
|
||||
|
||||
```xml
|
||||
<table class="table checkout-summary-table">
|
||||
<caption class="visually-hidden">Order lines</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Product</th>
|
||||
<th scope="col" class="text-end">Quantity</th>
|
||||
<th scope="col" class="text-end">Subtotal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="lines" t-as="line" t-key="line['id']">
|
||||
<th scope="row" t-esc="line['name']"/>
|
||||
<td class="text-end" t-esc="line['qty']"/>
|
||||
<td class="text-end" t-esc="line['subtotal_formatted']"/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
```
|
||||
|
||||
Never use a table for layout, and never drop `<thead>`/`scope` when stacking to cards on mobile —
|
||||
restyle, don't restructure.
|
||||
|
||||
## Focus and motion in CSS
|
||||
|
||||
```css
|
||||
/* One ring, everywhere, on keyboard focus only. */
|
||||
.eskaera-page :focus-visible {
|
||||
outline: 2px solid var(--ac-color-focus, var(--focus-ring-color, #0d6efd));
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--ac-radius-sm);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is the one legitimate `!important` in the codebase — a user preference must beat every component.
|
||||
|
||||
## Multilingual
|
||||
|
||||
The addon runs es / eu / ca. `website.layout` sets `<html lang>`; when a string stays in another
|
||||
language inside a translated page (a supplier name, a product origin), mark it: `<span lang="eu">`.
|
||||
Never build a sentence by concatenating translated fragments — pass the whole string with `%s`
|
||||
placeholders so translators can reorder.
|
||||
|
||||
## Verification
|
||||
|
||||
1. **Keyboard only**: Tab through the page. Visible focus at every stop, order matches the visual
|
||||
order, no trap, every action reachable, Esc closes overlays.
|
||||
2. **Zoom 200 %** and **320 px width**: no horizontal scroll, nothing clipped.
|
||||
3. **Contrast**: check rendered colours, including disabled states and placeholder text.
|
||||
4. **Screen reader** smoke test on the cart flow (Orca on Linux, NVDA on Windows).
|
||||
5. Lighthouse/axe in DevTools catches the mechanical half — it does not catch focus order,
|
||||
announcement quality, or a label that lies.
|
||||
163
.claude/skills/odoo-web-design/references/css-architecture.md
Normal file
163
.claude/skills/odoo-web-design/references/css-architecture.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
|
||||
# CSS architecture
|
||||
|
||||
## File layout
|
||||
|
||||
```
|
||||
static/src/css/
|
||||
├── website_sale.css # index only: @import list, no rules of its own
|
||||
├── base/
|
||||
│ ├── variables.css # :root tokens — the ONLY file with hex literals
|
||||
│ └── utilities.css # tiny single-purpose helpers, when Bootstrap lacks one
|
||||
├── layout/
|
||||
│ ├── pages.css # page shells, containers, backgrounds
|
||||
│ └── header.css
|
||||
├── components/ # one file per reusable component
|
||||
│ ├── product-card.css
|
||||
│ ├── order-card.css
|
||||
│ ├── cart.css
|
||||
│ ├── buttons.css
|
||||
│ ├── quantity-control.css
|
||||
│ ├── forms.css
|
||||
│ ├── alerts.css
|
||||
│ └── tag-filter.css
|
||||
└── sections/ # page-specific composition only
|
||||
├── products-grid.css
|
||||
├── order-list.css
|
||||
├── checkout.css
|
||||
└── info-cards.css
|
||||
```
|
||||
|
||||
Rules of placement:
|
||||
|
||||
- A rule that could appear on two pages is a **component**, never a section.
|
||||
- A **section** file may only position and space components — if it restyles a component's internals,
|
||||
the component is missing a modifier.
|
||||
- Each component file owns its own media queries, at the bottom. There is no global responsive file
|
||||
in a healthy tree; a legacy `layout/responsive.css` should shrink to zero as components are touched.
|
||||
- `website_sale.css` contains imports and comments only.
|
||||
|
||||
## Naming
|
||||
|
||||
Prefix everything with `eskaera-` (the product name already used for page-level classes). Unprefixed
|
||||
names like `.product-card`, `.cart-header`, `.qty-control` sit in the same namespace as Bootstrap,
|
||||
Odoo core and every other installed addon — that collision is what forces `!important` later.
|
||||
|
||||
```
|
||||
.eskaera-product-card /* block */
|
||||
.eskaera-product-card__title /* element */
|
||||
.eskaera-product-card--out-of-stock /* modifier */
|
||||
.is-loading .is-active .has-error /* state, always paired with a block class */
|
||||
```
|
||||
|
||||
`js-` prefix for hooks JS queries, and never style a `js-` class — that way markup can be restyled
|
||||
without breaking behaviour, and behaviour can move without breaking styles.
|
||||
|
||||
When renaming existing classes, do it component by component: template + CSS + JS selectors + tests
|
||||
in the same commit, then grep for the old name across `views/`, `static/src/`, and `README*`.
|
||||
|
||||
## Specificity — how to stop writing `!important`
|
||||
|
||||
Overriding Bootstrap globally (`.list-group-item { … }`, `.alert-warning { … }`) is a fight you win
|
||||
only with `!important`, and it leaks into every other addon. Scope the override under the page root
|
||||
instead: equal specificity plus one class always wins, no escape hatch needed.
|
||||
|
||||
```css
|
||||
/* ❌ global override, needs !important to beat Bootstrap's own later rules */
|
||||
.list-group-item {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* ✅ scoped: higher specificity, no !important, no leakage */
|
||||
.eskaera-checkout-page .list-group-item {
|
||||
border-radius: 0;
|
||||
}
|
||||
```
|
||||
|
||||
Other rules:
|
||||
|
||||
- Max **3** levels of nesting in a selector. Deeper means the markup is doing the work the class
|
||||
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 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
|
||||
|
||||
```css
|
||||
.eskaera-product-card {
|
||||
/* 1. layout */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* 2. box */
|
||||
padding: var(--ac-space-md);
|
||||
border: 1px solid var(--ac-border-light);
|
||||
border-radius: var(--ac-radius-lg);
|
||||
/* 3. typography */
|
||||
font-size: var(--ac-text-base);
|
||||
color: var(--ac-text-primary);
|
||||
/* 4. paint & motion */
|
||||
background: var(--ac-surface);
|
||||
box-shadow: var(--ac-shadow-sm);
|
||||
transition: box-shadow var(--ac-transition-fast);
|
||||
}
|
||||
```
|
||||
|
||||
Then, in order: `:hover` (inside `@media (hover: hover)`), `:focus-visible`, `:active`, `:disabled`,
|
||||
state classes, modifiers, media queries.
|
||||
|
||||
## Tokens
|
||||
|
||||
Full rules in `SKILL.md` §2. In short:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* alias Odoo/Bootstrap's unprefixed custom properties so theming still works */
|
||||
--ac-color-primary: var(--primary, #007bff);
|
||||
--ac-color-danger: var(--danger, #dc3545);
|
||||
--ac-surface: var(--body-bg, #fff);
|
||||
--ac-text-primary: var(--body-color, #1a202c);
|
||||
--ac-border-light: var(--border-color, #e2e8f0);
|
||||
|
||||
/* own scales */
|
||||
--ac-space-xs: 0.25rem;
|
||||
--ac-space-sm: 0.5rem;
|
||||
--ac-space-md: 1rem;
|
||||
--ac-space-lg: 1.5rem;
|
||||
--ac-space-xl: 2rem;
|
||||
|
||||
--ac-radius-sm: 0.25rem;
|
||||
--ac-radius-md: 0.5rem;
|
||||
--ac-radius-lg: 0.75rem;
|
||||
|
||||
--ac-shadow-sm: 0 2px 8px rgb(0 0 0 / 8%);
|
||||
--ac-shadow-md: 0 4px 12px rgb(0 0 0 / 10%);
|
||||
|
||||
--ac-transition-fast: 200ms ease;
|
||||
}
|
||||
```
|
||||
|
||||
Migrating a legacy token set: add the `--ac-` token aliased to the old name, move consumers file by
|
||||
file, delete the old token when `grep -rn "\-\-old-name" static/src/` is empty. Don't rename all at once.
|
||||
|
||||
## Anti-patterns seen in Odoo addons
|
||||
|
||||
| Smell | Fix |
|
||||
| --- | --- |
|
||||
| `!important` to beat Bootstrap | scope under the page root class |
|
||||
| Same colour as `#28a745` in six files | one token in `variables.css` |
|
||||
| `responsive.css` overriding twelve components | move each query into its component |
|
||||
| `style="margin-top:20px"` in QWeb | utility class or component spacing |
|
||||
| `.card .body .row .col span` | give the span a class |
|
||||
| `px` for font sizes | `rem`, so browser zoom and user font size work |
|
||||
| Colour hardcoded instead of `var(--primary)` | breaks the website theme editor silently |
|
||||
| Both `@import` chain **and** manifest entries | double download; pick one (see SKILL.md §6) |
|
||||
|
||||
## Formatting
|
||||
|
||||
Prettier handles `.css` via pre-commit: `printWidth: 100`, `tabWidth: 4`. Run
|
||||
`pre-commit run --all-files` (or `make format`) before committing; never hand-align properties, it
|
||||
will be reformatted.
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
# The finish pass
|
||||
|
||||
What separates "it works" from "it looks designed". Go through this per screen, not per file.
|
||||
|
||||
## 1. Every interactive element has all its states
|
||||
|
||||
For each button, link, card, input, filter chip:
|
||||
|
||||
| State | Must be | Common miss |
|
||||
| --- | --- | --- |
|
||||
| default | on the token scale | — |
|
||||
| `:hover` | inside `@media (hover: hover)` | sticks after tap on touch |
|
||||
| `:focus-visible` | visible ring, 3:1 contrast | removed via `outline: none` |
|
||||
| `:active` | perceptible press | missing entirely |
|
||||
| `:disabled` | dimmed **and** `cursor: not-allowed`, still 4.5:1 if it carries text | unreadable grey |
|
||||
| loading | spinner + `aria-busy`, size unchanged | layout jumps |
|
||||
|
||||
The element must not resize between states — animate `box-shadow`, `background`, `transform`, never
|
||||
`padding`, `border-width` or `font-size`.
|
||||
|
||||
## 2. Vertical rhythm and spacing
|
||||
|
||||
- All spacing from `--ac-space-*`. A `margin: 13px` anywhere is a defect.
|
||||
- Space belongs to **one** side consistently (block-end margins, or `gap` — pick one per container).
|
||||
Prefer `gap` in flex/grid; it doesn't collapse and doesn't need `:last-child` cleanups.
|
||||
- Related things sit closer than unrelated things. If the label is as far from its input as from the
|
||||
previous field, the form reads as noise.
|
||||
- Cards in a grid must be equal height (`align-items: stretch` + the price pinned with `margin-top: auto`).
|
||||
|
||||
## 3. Typography
|
||||
|
||||
- One scale (`--ac-text-*`), max 3 weights, max 2 sizes per card.
|
||||
- Headings are structural (`h1`→`h2`→`h3`); size comes from a class, never from picking `h4` because
|
||||
it looks right.
|
||||
- `max-width: 65ch` on paragraphs of real prose.
|
||||
- Numbers in tables: `font-variant-numeric: tabular-nums` and right-aligned so columns line up.
|
||||
- Long product names truncate predictably: `text-overflow: ellipsis` with `title` attribute, or
|
||||
`-webkit-line-clamp: 2` with a fixed `min-height` so cards don't stagger.
|
||||
|
||||
## 4. Colour
|
||||
|
||||
- Semantic, not decorative: success/danger/warning always the same hue for the same meaning.
|
||||
- Status never colour-only — icon or text alongside (colour blindness, and grayscale printing).
|
||||
- Max ~2 accent colours per screen. Everything else is surface, border, text.
|
||||
- Check the rendered contrast, including: placeholder text, disabled buttons, badges on coloured
|
||||
backgrounds, white text on `--warning` (this one almost always fails).
|
||||
|
||||
## 5. The states people forget
|
||||
|
||||
Every list, grid and total needs four designs, not one:
|
||||
|
||||
1. **Loading** — skeletons matched to the real layout (not a centred spinner that collapses the page).
|
||||
2. **Empty** — heading, one sentence explaining why it's empty, and the button that fixes it.
|
||||
"No products found" alone is a dead end; offer "Clear filters".
|
||||
3. **Error** — what failed, in the user's language, and what to do now. Never a raw traceback or code.
|
||||
4. **Partial / stale** — an order past its cutoff, an out-of-stock line, a price that changed. Say it
|
||||
inline, next to the affected row, not only in a toast that disappears.
|
||||
|
||||
## 6. Feedback and motion
|
||||
|
||||
- Any action taken by the user gets a response within 100 ms — disable the button, show the spinner,
|
||||
optimistic-update the quantity. Silence reads as "broken", and users double-submit.
|
||||
- Toasts: `aria-live="polite"`, dismissible, never the only place an error is shown.
|
||||
- Motion is 150–250 ms and eases out. Anything above 400 ms feels slow; anything that moves the page
|
||||
under the cursor is a bug.
|
||||
- Everything animated respects `prefers-reduced-motion`.
|
||||
|
||||
## 7. Content and copy
|
||||
|
||||
- Sentence case for buttons and labels; imperative verbs ("Add to cart", not "Adding products").
|
||||
- Prices always formatted server-side with the currency and locale (`i18nManager.formatCurrency`),
|
||||
never string-concatenated in the template.
|
||||
- Dates: locale-formatted, and relative only when the absolute date is also available (`title`).
|
||||
- Nothing user-visible is a hardcoded English literal — templates get translatable text, JS gets
|
||||
`window.i18nManager.get(key)`.
|
||||
|
||||
## 8. Cross-cutting checks before "done"
|
||||
|
||||
```
|
||||
[ ] 320 px: no horizontal scroll on any page
|
||||
[ ] 200 % zoom: nothing clipped or overlapping
|
||||
[ ] Tab through the whole flow: visible focus, logical order, no trap
|
||||
[ ] All actions reachable without a mouse, Esc closes overlays
|
||||
[ ] No hex outside base/variables.css
|
||||
[ ] No inline style="" in views/*.xml
|
||||
[ ] No new !important (except the reduced-motion reset)
|
||||
[ ] Every <img> has alt and width/height
|
||||
[ ] Every icon-only control has aria-label
|
||||
[ ] Empty / loading / error states exist for each list
|
||||
[ ] es, eu and ca render without overflow (German-length strings break tight buttons)
|
||||
[ ] pre-commit run --all-files is clean
|
||||
[ ] Lighthouse a11y ≥ 95 on shop, cart, checkout
|
||||
```
|
||||
|
||||
The last three matter as much as the rest: a layout tuned only against Spanish copy usually breaks in
|
||||
Basque, and a11y regressions land silently.
|
||||
191
.claude/skills/odoo-web-design/references/responsive.md
Normal file
191
.claude/skills/odoo-web-design/references/responsive.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# Responsive layout
|
||||
|
||||
## Breakpoints — Bootstrap 5, and only these
|
||||
|
||||
| Name | `min-width` | Typical device |
|
||||
| --- | --- | --- |
|
||||
| (base) | — | phones, 320–575 px |
|
||||
| `sm` | 576px | large phones |
|
||||
| `md` | 768px | tablets |
|
||||
| `lg` | 992px | small laptops |
|
||||
| `xl` | 1200px | desktops |
|
||||
| `xxl` | 1400px | large desktops |
|
||||
|
||||
Mobile-first, `min-width` only:
|
||||
|
||||
```css
|
||||
/* ✅ base = mobile, enhance upwards */
|
||||
.products-grid {
|
||||
display: grid;
|
||||
gap: var(--ac-space-md);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.products-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.products-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* ❌ desktop-first bands: overlapping ranges, unclear cascade, private breakpoints */
|
||||
@media (max-width: 1599px) and (min-width: 1400px) { … }
|
||||
@media (max-width: 720px) { … }
|
||||
```
|
||||
|
||||
## Prefer intrinsic layout — most grids need no media query at all
|
||||
|
||||
```css
|
||||
.products-grid {
|
||||
display: grid;
|
||||
gap: var(--ac-space-md);
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
|
||||
}
|
||||
```
|
||||
|
||||
`min(100%, 16rem)` is what keeps it from overflowing at 320 px. Reach for a media query only when the
|
||||
layout must genuinely *change* (sidebar moves, table becomes cards), not merely resize.
|
||||
|
||||
## Fluid type and space
|
||||
|
||||
```css
|
||||
:root {
|
||||
--ac-text-sm: clamp(0.875rem, 0.85rem + 0.1vw, 0.9375rem);
|
||||
--ac-text-base: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
|
||||
--ac-text-lg: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
|
||||
--ac-text-xl: clamp(1.5rem, 1.3rem + 1vw, 2.25rem);
|
||||
--ac-space-md: clamp(1rem, 0.9rem + 0.5vw, 1.5rem);
|
||||
}
|
||||
```
|
||||
|
||||
One continuous scale beats three step-changes at breakpoints. Never redefine a token inside a media
|
||||
query to make it smaller.
|
||||
|
||||
Body copy: 1rem minimum, line-height ≥ 1.5, measure 45–75 characters (`max-width: 65ch`).
|
||||
|
||||
## Touch targets and pointer
|
||||
|
||||
WCAG 2.2 AA (§2.5.8 Target Size, Minimum) asks for **24×24 px**. The familiar **44×44 px** is §2.5.5,
|
||||
level **AAA** — treat it as the house rule for anything a thumb hits, applied by pointer type rather
|
||||
than by viewport width: a 1024 px tablet needs the big target, a 1024 px laptop doesn't.
|
||||
|
||||
```css
|
||||
.qty-control .btn,
|
||||
.tag-filter-badge {
|
||||
min-width: 2.25rem; /* 36px — comfortable with a mouse */
|
||||
min-height: 2.25rem;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.qty-control .btn,
|
||||
.tag-filter-badge {
|
||||
min-width: 2.75rem; /* 44px */
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.product-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--ac-shadow-md);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hover effects belong inside `@media (hover: hover)` — on touch they stick after a tap and look broken.
|
||||
|
||||
## Tables → cards, without restructuring
|
||||
|
||||
```css
|
||||
@media (max-width: 767.98px) {
|
||||
.checkout-summary-table thead {
|
||||
/* keep it in the a11y tree; hide visually only */
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
}
|
||||
.checkout-summary-table tr {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: var(--ac-space-xs);
|
||||
padding: var(--ac-space-sm) 0;
|
||||
border-bottom: 1px solid var(--ac-border-light);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is the one place a `max-width` query is honest: it's an override of the desktop table. Keep the
|
||||
markup semantic — never emit different HTML per viewport.
|
||||
|
||||
## Sticky bars (cart / checkout actions on mobile)
|
||||
|
||||
```css
|
||||
.checkout-actions {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: var(--ac-z-sticky);
|
||||
padding: var(--ac-space-sm);
|
||||
padding-bottom: calc(var(--ac-space-sm) + env(safe-area-inset-bottom, 0px));
|
||||
background: var(--ac-surface, #fff);
|
||||
box-shadow: 0 -2px 8px rgb(0 0 0 / 8%);
|
||||
}
|
||||
```
|
||||
|
||||
`env(safe-area-inset-bottom)` keeps the button above the iOS home indicator. Sticky elements must not
|
||||
eat more than ~20 % of a 568 px-tall viewport.
|
||||
|
||||
## Images
|
||||
|
||||
```xml
|
||||
<img class="product-card-img" t-att-src="product['image_url']" t-att-alt="product['name']"
|
||||
loading="lazy" decoding="async" width="400" height="400"/>
|
||||
```
|
||||
|
||||
- `width`/`height` always — they reserve the box and prevent layout shift (CLS).
|
||||
- `loading="lazy"` for everything below the fold; **never** on the LCP image (first product row,
|
||||
hero) — that delays the largest paint.
|
||||
- CSS: `max-width: 100%; height: auto;` plus `object-fit: cover` with a fixed `aspect-ratio` when
|
||||
cards must align.
|
||||
- Odoo serves resized variants: `/web/image/product.product/<id>/image_256` etc. Ask for the size you
|
||||
render, not `image_1920` scaled down in CSS.
|
||||
|
||||
## Container and page rhythm
|
||||
|
||||
```css
|
||||
.eskaera-page {
|
||||
width: 100%;
|
||||
max-width: 75rem;
|
||||
margin-inline: auto;
|
||||
padding-inline: clamp(1rem, 4vw, 2rem);
|
||||
}
|
||||
```
|
||||
|
||||
Use logical properties (`margin-inline`, `padding-block`, `inset-inline-start`) — Odoo auto-generates
|
||||
an RTL stylesheet, and logical properties survive the flip without a second rule.
|
||||
|
||||
## Test matrix
|
||||
|
||||
| Width | Why |
|
||||
| --- | --- |
|
||||
| 320 px | smallest supported; the one that breaks first |
|
||||
| 360 / 390 px | real Android / iPhone |
|
||||
| 768 px | tablet portrait, first breakpoint |
|
||||
| 1024 px | tablet landscape / small laptop |
|
||||
| 1280 / 1440 px | desktop |
|
||||
| 1280 px @ 200 % zoom | WCAG 1.4.4 reflow — equivalent to 640 px |
|
||||
|
||||
Run the stack (`docker-compose up -d`, http://localhost:8070) and check, per viewport: no horizontal
|
||||
scroll, no clipped text, touch targets still ≥ 44 px on coarse pointers, sticky bars not covering
|
||||
content, images not stretched.
|
||||
|
||||
DevTools device toolbar is enough for layout; check touch targets by actually tapping on a phone once
|
||||
before calling it done.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -288,6 +288,7 @@ msgstr "Nombre de productes disponibles"
|
|||
#. module: website_sale_aplicoop
|
||||
#. odoo-python
|
||||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout
|
||||
msgid "Back to Cart"
|
||||
msgstr "Torna al carret"
|
||||
|
||||
|
|
@ -416,7 +417,9 @@ msgid "Clear search"
|
|||
msgstr "Neteja la cerca"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#. odoo-python
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form
|
||||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
msgid "Close"
|
||||
msgstr "Tanca"
|
||||
|
||||
|
|
@ -1899,6 +1902,7 @@ msgstr "Dissabte"
|
|||
#. module: website_sale_aplicoop
|
||||
#. odoo-python
|
||||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
|
||||
msgid "Save Cart"
|
||||
msgstr "Desa el Carret"
|
||||
|
||||
|
|
@ -2298,3 +2302,63 @@ msgstr "Desa l'Esborrany"
|
|||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
msgid "Save order as draft"
|
||||
msgstr "Desa la comanda com a esborrany"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
|
||||
msgid "Search products"
|
||||
msgstr "Cerca productes"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary
|
||||
msgid "Order summary"
|
||||
msgstr "Resum de la comanda"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary
|
||||
msgid "Products in this order, with quantity, price and subtotal"
|
||||
msgstr "Productes d'aquesta comanda, amb quantitat, preu i subtotal"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
|
||||
msgid "Loading your order…"
|
||||
msgstr "Carregant la teva comanda…"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
|
||||
msgid "This page needs JavaScript to load the order into your cart."
|
||||
msgstr ""
|
||||
"Aquesta pàgina necessita JavaScript per carregar la comanda al teu carret."
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
|
||||
msgid "Continue to the group order"
|
||||
msgstr "Continua a la comanda de grup"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#. odoo-python
|
||||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
msgid "products found"
|
||||
msgstr "productes trobats"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
|
||||
msgid "<i class=\"fa fa-download me-2\" aria-hidden=\"true\"/>Load More Products"
|
||||
msgstr "<i class=\"fa fa-download me-2\" aria-hidden=\"true\"/>Carregar més productes"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta
|
||||
msgid ""
|
||||
"<span class=\"badge special-order-badge\"><i class=\"fa fa-star\" aria-"
|
||||
"hidden=\"true\"/> Special Order</span>"
|
||||
msgstr ""
|
||||
"<span class=\"badge special-order-badge\"><i class=\"fa fa-star\" aria-"
|
||||
"hidden=\"true\"/> Comanda especial</span>"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta
|
||||
msgid ""
|
||||
"<span class=\"badge text-bg-success\"><i class=\"fa fa-truck\" aria-"
|
||||
"hidden=\"true\"/> Home Delivery</span>"
|
||||
msgstr ""
|
||||
"<span class=\"badge text-bg-success\"><i class=\"fa fa-truck\" aria-"
|
||||
"hidden=\"true\"/> Lliurament a domicili</span>"
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@ msgstr "Cantidad de Productos Disponibles"
|
|||
#. module: website_sale_aplicoop
|
||||
#. odoo-python
|
||||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout
|
||||
msgid "Back to Cart"
|
||||
msgstr "Volver al Carrito"
|
||||
|
||||
|
|
@ -418,7 +419,9 @@ msgid "Clear search"
|
|||
msgstr "Limpiar búsqueda"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#. odoo-python
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form
|
||||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
msgid "Close"
|
||||
msgstr "Cerrar"
|
||||
|
||||
|
|
@ -1899,6 +1902,7 @@ msgstr "Sábado"
|
|||
#. module: website_sale_aplicoop
|
||||
#. odoo-python
|
||||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
|
||||
msgid "Save Cart"
|
||||
msgstr "Guardar Carrito"
|
||||
|
||||
|
|
@ -2295,3 +2299,62 @@ msgstr "Grabar pedido"
|
|||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
msgid "Save order as draft"
|
||||
msgstr "Grabar pedido"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
|
||||
msgid "Search products"
|
||||
msgstr "Buscar productos"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary
|
||||
msgid "Order summary"
|
||||
msgstr "Resumen del pedido"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary
|
||||
msgid "Products in this order, with quantity, price and subtotal"
|
||||
msgstr "Productos de este pedido, con cantidad, precio y subtotal"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
|
||||
msgid "Loading your order…"
|
||||
msgstr "Cargando tu pedido…"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
|
||||
msgid "This page needs JavaScript to load the order into your cart."
|
||||
msgstr "Esta página necesita JavaScript para cargar el pedido en tu carrito."
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
|
||||
msgid "Continue to the group order"
|
||||
msgstr "Continuar al pedido de grupo"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#. odoo-python
|
||||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
msgid "products found"
|
||||
msgstr "productos encontrados"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
|
||||
msgid "<i class=\"fa fa-download me-2\" aria-hidden=\"true\"/>Load More Products"
|
||||
msgstr "<i class=\"fa fa-download me-2\" aria-hidden=\"true\"/>Cargar más productos"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta
|
||||
msgid ""
|
||||
"<span class=\"badge special-order-badge\"><i class=\"fa fa-star\" aria-"
|
||||
"hidden=\"true\"/> Special Order</span>"
|
||||
msgstr ""
|
||||
"<span class=\"badge special-order-badge\"><i class=\"fa fa-star\" aria-"
|
||||
"hidden=\"true\"/> Pedido Especial</span>"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta
|
||||
msgid ""
|
||||
"<span class=\"badge text-bg-success\"><i class=\"fa fa-truck\" aria-"
|
||||
"hidden=\"true\"/> Home Delivery</span>"
|
||||
msgstr ""
|
||||
"<span class=\"badge text-bg-success\"><i class=\"fa fa-truck\" aria-"
|
||||
"hidden=\"true\"/> Entrega a Casa</span>"
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ msgstr "Eskuragarri Dauden Produktuen Kopurua"
|
|||
#. module: website_sale_aplicoop
|
||||
#. odoo-python
|
||||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout
|
||||
msgid "Back to Cart"
|
||||
msgstr "Sarera Itzuli"
|
||||
|
||||
|
|
@ -419,7 +420,9 @@ msgid "Clear search"
|
|||
msgstr "Bilaketa garbitu"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#. odoo-python
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.view_group_order_form
|
||||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
msgid "Close"
|
||||
msgstr "Itxi"
|
||||
|
||||
|
|
@ -1895,6 +1898,7 @@ msgstr "Larunbata"
|
|||
#. module: website_sale_aplicoop
|
||||
#. odoo-python
|
||||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
|
||||
msgid "Save Cart"
|
||||
msgstr "Saskia Gorde"
|
||||
|
||||
|
|
@ -2293,3 +2297,64 @@ msgstr "Eskaera gorde"
|
|||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
msgid "Save order as draft"
|
||||
msgstr "Eskaera gorde"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
|
||||
msgid "Search products"
|
||||
msgstr "Produktuak bilatu"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary
|
||||
msgid "Order summary"
|
||||
msgstr "Eskaera laburpena"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_checkout_summary
|
||||
msgid "Products in this order, with quantity, price and subtotal"
|
||||
msgstr "Eskaera honetako produktuak, kantitatea, prezioa eta azpitotala"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
|
||||
msgid "Loading your order…"
|
||||
msgstr "Zure eskaera kargatzen…"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
|
||||
msgid "This page needs JavaScript to load the order into your cart."
|
||||
msgstr "Orri honek JavaScript behar du eskaera zure saskira kargatzeko."
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_load_from_history
|
||||
msgid "Continue to the group order"
|
||||
msgstr "Jarraitu taldeko eskaerara"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#. odoo-python
|
||||
#: code:addons/website_sale_aplicoop/models/js_translations.py:0
|
||||
msgid "products found"
|
||||
msgstr "produktu aurkitu dira"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_shop
|
||||
msgid "<i class=\"fa fa-download me-2\" aria-hidden=\"true\"/>Load More Products"
|
||||
msgstr ""
|
||||
"<i class=\"fa fa-download me-2\" aria-hidden=\"true\"/>Produktu Gehiago "
|
||||
"Kargatu"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta
|
||||
msgid ""
|
||||
"<span class=\"badge special-order-badge\"><i class=\"fa fa-star\" aria-"
|
||||
"hidden=\"true\"/> Special Order</span>"
|
||||
msgstr ""
|
||||
"<span class=\"badge special-order-badge\"><i class=\"fa fa-star\" aria-"
|
||||
"hidden=\"true\"/> Eskaera Berezia</span>"
|
||||
|
||||
#. module: website_sale_aplicoop
|
||||
#: model_terms:ir.ui.view,arch_db:website_sale_aplicoop.eskaera_order_card_meta
|
||||
msgid ""
|
||||
"<span class=\"badge text-bg-success\"><i class=\"fa fa-truck\" aria-"
|
||||
"hidden=\"true\"/> Home Delivery</span>"
|
||||
msgstr ""
|
||||
"<span class=\"badge text-bg-success\"><i class=\"fa fa-truck\" aria-"
|
||||
"hidden=\"true\"/> Etxerako Entrega</span>"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# ========================
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +1,228 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/base/variables.css */
|
||||
|
||||
/**
|
||||
* CSS Custom Properties (Variables)
|
||||
* Colores, tipografía, espaciados centralizados
|
||||
* Design tokens (CSS custom properties)
|
||||
*
|
||||
* The ONLY file in this addon allowed to contain colour literals.
|
||||
*
|
||||
* Odoo 18 ships Bootstrap 5.3 with `$variable-prefix: ''`
|
||||
* (web/static/src/scss/bootstrap_overridden.scss), so Bootstrap's root custom
|
||||
* properties are exposed UNPREFIXED: --primary, --body-bg, --border-color, …
|
||||
* Brand tokens alias them so the website theme editor keeps control of the palette;
|
||||
* the literal is only the fallback when the bundle is loaded outside a themed page.
|
||||
*
|
||||
* Contrast ratios noted below are against --ac-surface (#fff) and are the reason
|
||||
* a given literal was chosen. Do not "tidy" them without re-checking WCAG AA.
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ========== COLORS ========== */
|
||||
--primary-color: var(--primary, #007bff);
|
||||
--primary-dark: var(--primary-dark, #0056b3);
|
||||
--secondary-color: var(--secondary, #6c757d);
|
||||
--success-color: var(--success, #28a745);
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #17a2b8;
|
||||
--light-color: #f8f9fa;
|
||||
/* ========== BRAND ========== */
|
||||
--ac-color-primary: var(--primary, #007bff);
|
||||
/* White text on --ac-color-primary is only 3.98:1 (fails AA for body text).
|
||||
Use -strong whenever text sits ON the colour: 7.04:1. */
|
||||
--ac-color-primary-strong: #0056b3;
|
||||
--ac-color-secondary: var(--secondary, #6c757d);
|
||||
--ac-color-accent: #7c3aed; /* white on it: 5.70:1 */
|
||||
--ac-color-accent-strong: #6d28d9; /* white on it: 7.10:1 */
|
||||
|
||||
/* Order cards carry their own indigo accent, and special orders an amber one. */
|
||||
--ac-color-order: #5a67d8; /* white on it: 4.81:1 */
|
||||
--ac-color-order-strong: #4c57bd; /* 6.17:1 */
|
||||
--ac-color-order-deep: #3d4898; /* 8.14:1 */
|
||||
--ac-color-order-border: rgb(90 103 216 / 12%);
|
||||
--ac-color-order-border-hover: rgb(90 103 216 / 25%);
|
||||
--ac-surface-order: linear-gradient(180deg, #fff 0%, #fbfdff 100%);
|
||||
--ac-surface-order-hover: linear-gradient(180deg, #fff 0%, #f7faff 100%);
|
||||
|
||||
--ac-color-special: #f59e0b;
|
||||
--ac-color-special-strong: #d97706;
|
||||
--ac-color-special-border: rgb(245 158 11 / 35%);
|
||||
--ac-color-special-border-hover: rgb(245 158 11 / 50%);
|
||||
--ac-surface-special: linear-gradient(180deg, #fffdf0 0%, #fffbea 100%);
|
||||
--ac-surface-special-hover: linear-gradient(180deg, #fffdf0 0%, #fff8db 100%);
|
||||
/* White on amber is 2.15:1. Amber always takes dark text: 7.60:1. */
|
||||
--ac-text-on-special: #1a202c;
|
||||
|
||||
/* ========== FEEDBACK ========== */
|
||||
--ac-color-success: var(--success, #28a745);
|
||||
/* #28a745 is only 3.13:1 on white — fine as a fill, not as text. */
|
||||
--ac-color-success-strong: #1e7e34; /* 5.14:1 */
|
||||
--ac-color-success-deep: #1a6e2e; /* hover/active on -strong */
|
||||
|
||||
/* Warning alert: amber surface with its own readable foreground (6.30:1). */
|
||||
--ac-surface-warning: #fef3c7;
|
||||
--ac-border-warning: #fcd34d;
|
||||
--ac-text-warning: #92400e;
|
||||
--ac-color-danger: var(--danger, #dc3545);
|
||||
--ac-color-warning: var(--warning, #ffc107);
|
||||
--ac-color-info: var(--info, #17a2b8);
|
||||
|
||||
/* ========== SURFACES ========== */
|
||||
--ac-surface: var(--body-bg, #fff);
|
||||
--ac-surface-sunken: var(--light, #f8f9fa);
|
||||
--ac-surface-muted: #f3f3f3;
|
||||
--ac-surface-tint: rgb(0 123 255 / 6%);
|
||||
--ac-surface-tint-hover: rgb(108 117 125 / 10%);
|
||||
|
||||
/* ========== TEXT (all ≥ 4.5:1 on --ac-surface) ========== */
|
||||
--ac-text-primary: var(--body-color, #1a202c); /* 16.32:1 */
|
||||
--ac-text-secondary: #4a5568; /* 7.53:1 */
|
||||
--ac-text-muted: #6b7280; /* 4.83:1 */
|
||||
--ac-text-on-fill: #fff;
|
||||
|
||||
/* ========== BORDERS ========== */
|
||||
/* Decorative separators and card outlines. */
|
||||
--ac-border: var(--border-color, #e2e8f0);
|
||||
--ac-border-subtle: #e0e0e0;
|
||||
/* Boundaries of interactive controls (inputs, steppers): WCAG 1.4.11 needs
|
||||
≥ 3:1 against the adjacent surface. #8a919a is 3.18:1. */
|
||||
--ac-border-strong: #8a919a;
|
||||
|
||||
/* ========== FOCUS RING ========== */
|
||||
--ac-focus-ring-color: var(--primary, #007bff); /* 3.98:1 — ok for a focus indicator */
|
||||
--ac-focus-ring-width: 2px;
|
||||
--ac-focus-ring-offset: 2px;
|
||||
|
||||
/* ========== TYPOGRAPHY ========== */
|
||||
--ac-font-family: var(
|
||||
--font-sans-serif,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
"Helvetica Neue",
|
||||
Arial,
|
||||
sans-serif
|
||||
);
|
||||
|
||||
/* Fluid scale: one continuous ramp instead of per-breakpoint overrides.
|
||||
Always rem + vw, never pure vw, so browser zoom keeps working. */
|
||||
--ac-text-2xs: clamp(0.6875rem, 0.66rem + 0.1vw, 0.75rem);
|
||||
--ac-text-xs: clamp(0.75rem, 0.72rem + 0.12vw, 0.8125rem);
|
||||
--ac-text-sm: clamp(0.8125rem, 0.79rem + 0.14vw, 0.875rem);
|
||||
--ac-text-base: clamp(0.875rem, 0.85rem + 0.18vw, 1rem);
|
||||
--ac-text-md: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
|
||||
--ac-text-lg: clamp(1.125rem, 1.06rem + 0.3vw, 1.375rem);
|
||||
--ac-text-xl: clamp(1.375rem, 1.25rem + 0.6vw, 1.75rem);
|
||||
--ac-text-2xl: clamp(1.75rem, 1.5rem + 1vw, 2.25rem);
|
||||
|
||||
--ac-leading-tight: 1.2;
|
||||
--ac-leading-snug: 1.35;
|
||||
--ac-leading-normal: 1.5;
|
||||
|
||||
--ac-weight-normal: 400;
|
||||
--ac-weight-medium: 500;
|
||||
--ac-weight-semibold: 600;
|
||||
--ac-weight-bold: 700;
|
||||
|
||||
/* ========== SPACING ========== */
|
||||
--ac-space-3xs: 0.125rem;
|
||||
--ac-space-2xs: 0.25rem;
|
||||
--ac-space-xs: 0.375rem;
|
||||
--ac-space-sm: 0.5rem;
|
||||
--ac-space-md: 1rem;
|
||||
--ac-space-lg: 1.5rem;
|
||||
--ac-space-xl: 2rem;
|
||||
--ac-space-2xl: 3rem;
|
||||
|
||||
/* ========== RADIUS ========== */
|
||||
--ac-radius-sm: 0.25rem;
|
||||
--ac-radius-md: 0.5rem;
|
||||
--ac-radius-lg: 0.75rem;
|
||||
--ac-radius-xl: 1rem;
|
||||
--ac-radius-pill: 50rem;
|
||||
|
||||
/* ========== ELEVATION ========== */
|
||||
--ac-shadow-sm: 0 2px 8px rgb(0 0 0 / 8%);
|
||||
--ac-shadow-md: 0 4px 12px rgb(0 0 0 / 10%);
|
||||
--ac-shadow-lg: 0 10px 25px rgb(0 0 0 / 12%);
|
||||
|
||||
/* ========== MOTION ========== */
|
||||
--ac-transition-fast: 150ms ease-out;
|
||||
--ac-transition-normal: 220ms cubic-bezier(0.2, 0.9, 0.2, 1);
|
||||
--ac-transition-slow: 400ms ease;
|
||||
|
||||
/* ========== CONTROLS ========== */
|
||||
/* Fluid control size: replaces six per-breakpoint width/height overrides. */
|
||||
--ac-control-size: clamp(2.25rem, 2.05rem + 0.7vw, 2.5rem);
|
||||
/* 44px. WCAG 2.2 AA (2.5.8) only requires 24px, but 44px is the AAA figure
|
||||
(2.5.5) and the practical minimum for touch. Applied on coarse pointers. */
|
||||
--ac-touch-target: 2.75rem;
|
||||
--ac-control-border-width: 1px;
|
||||
|
||||
/* ========== PRODUCT CARD MEDIA ========== */
|
||||
--ac-card-media-ratio: 4 / 3;
|
||||
--ac-card-media-height: clamp(5rem, 4rem + 6vw, 8.5rem);
|
||||
|
||||
/* ========== Z-INDEX ========== */
|
||||
--ac-z-dropdown: 1000;
|
||||
--ac-z-sticky: 1020;
|
||||
--ac-z-fixed: 1030;
|
||||
--ac-z-modal: 1040;
|
||||
--ac-z-popover: 1050;
|
||||
--ac-z-tooltip: 1060;
|
||||
--ac-z-notification: 9999;
|
||||
|
||||
/* ==========================================================================
|
||||
LEGACY ALIASES — do not use in new code.
|
||||
Kept so the not-yet-migrated partials keep rendering. Delete each one once
|
||||
`grep -rn "var(--<name>)" static/src/` comes back empty.
|
||||
========================================================================== */
|
||||
--primary-color: var(--ac-color-primary);
|
||||
/* Was `var(--primary-dark, #0056b3)`: a custom property cannot reference
|
||||
itself — that made the declaration invalid at computed-value time, so the
|
||||
five consumers in buttons.css / order-card.css silently rendered with no
|
||||
colour at all. */
|
||||
--primary-dark: var(--ac-color-primary-strong);
|
||||
--secondary-color: var(--ac-color-secondary);
|
||||
--success-color: var(--ac-color-success);
|
||||
--danger-color: var(--ac-color-danger);
|
||||
--warning-color: var(--ac-color-warning);
|
||||
--info-color: var(--ac-color-info);
|
||||
--light-color: var(--ac-surface-sunken);
|
||||
--dark-color: #2d3748;
|
||||
|
||||
/* Text colors */
|
||||
--text-primary: #1a202c;
|
||||
--text-secondary: #4a5568;
|
||||
--text-muted: #6b7280;
|
||||
--text-primary: var(--ac-text-primary);
|
||||
--text-secondary: var(--ac-text-secondary);
|
||||
--text-muted: var(--ac-text-muted);
|
||||
|
||||
/* Border colors */
|
||||
--border-light: #e2e8f0;
|
||||
--border-light: var(--ac-border);
|
||||
--border-medium: #cbd5e0;
|
||||
--border-dark: #718096;
|
||||
|
||||
/* ========== TYPOGRAPHY ========== */
|
||||
--font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
|
||||
Arial, sans-serif;
|
||||
--font-family-base: var(--ac-font-family);
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-normal: var(--ac-weight-normal);
|
||||
--font-weight-medium: var(--ac-weight-medium);
|
||||
--font-weight-semibold: var(--ac-weight-semibold);
|
||||
--font-weight-bold: var(--ac-weight-bold);
|
||||
--font-weight-extrabold: 800;
|
||||
|
||||
/* ========== SPACING ========== */
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
--spacing-2xl: 3rem;
|
||||
--spacing-xs: var(--ac-space-2xs);
|
||||
--spacing-sm: var(--ac-space-sm);
|
||||
--spacing-md: var(--ac-space-md);
|
||||
--spacing-lg: var(--ac-space-lg);
|
||||
--spacing-xl: var(--ac-space-xl);
|
||||
--spacing-2xl: var(--ac-space-2xl);
|
||||
|
||||
/* ========== BORDER RADIUS ========== */
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-sm: var(--ac-radius-sm);
|
||||
--radius-md: var(--ac-radius-md);
|
||||
--radius-lg: var(--ac-radius-lg);
|
||||
--radius-xl: var(--ac-radius-xl);
|
||||
|
||||
/* ========== SHADOWS ========== */
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.12);
|
||||
--shadow-sm: var(--ac-shadow-sm);
|
||||
--shadow-md: var(--ac-shadow-md);
|
||||
--shadow-lg: var(--ac-shadow-lg);
|
||||
|
||||
/* ========== TRANSITIONS ========== */
|
||||
--transition-fast: 200ms ease;
|
||||
--transition-normal: 320ms cubic-bezier(0.2, 0.9, 0.2, 1);
|
||||
--transition-normal: var(--ac-transition-normal);
|
||||
--transition-slow: 500ms ease;
|
||||
|
||||
/* ========== Z-INDEX ========== */
|
||||
--z-dropdown: 1000;
|
||||
--z-sticky: 1020;
|
||||
--z-fixed: 1030;
|
||||
--z-modal: 1040;
|
||||
--z-popover: 1050;
|
||||
--z-tooltip: 1060;
|
||||
--z-notification: 9999;
|
||||
--z-dropdown: var(--ac-z-dropdown);
|
||||
--z-sticky: var(--ac-z-sticky);
|
||||
--z-fixed: var(--ac-z-fixed);
|
||||
--z-modal: var(--ac-z-modal);
|
||||
--z-popover: var(--ac-z-popover);
|
||||
--z-tooltip: var(--ac-z-tooltip);
|
||||
--z-notification: var(--ac-z-notification);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,83 +7,142 @@
|
|||
/* Unified header for both shop and checkout pages */
|
||||
.eskaera-order-header,
|
||||
.checkout-header {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
padding: clamp(var(--ac-space-md), 2vw, var(--ac-space-xl));
|
||||
border: 1px solid var(--ac-border-subtle);
|
||||
border-radius: var(--ac-radius-md);
|
||||
background: var(--ac-surface);
|
||||
box-shadow: var(--ac-shadow-sm);
|
||||
}
|
||||
|
||||
.eskaera-order-header h1,
|
||||
.checkout-header h1 {
|
||||
color: #2d3748;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 3px solid var(--primary-color, #007bff);
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: var(--ac-space-md);
|
||||
padding-bottom: var(--ac-space-md);
|
||||
border-bottom: 3px solid var(--ac-color-primary);
|
||||
font-size: var(--ac-text-2xl);
|
||||
font-weight: var(--ac-weight-bold);
|
||||
color: var(--ac-text-primary);
|
||||
}
|
||||
|
||||
.order-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 700;
|
||||
color: #2d3748;
|
||||
font-size: 1.05rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.info-value,
|
||||
.info-date {
|
||||
font-size: 1.35rem;
|
||||
color: #1a202c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-date {
|
||||
display: block;
|
||||
font-size: 1.25rem;
|
||||
color: #2d3748;
|
||||
font-weight: 500;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Title styling for both pages */
|
||||
.checkout-title,
|
||||
.eskaera-order-header h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
margin-bottom: 1rem;
|
||||
.checkout-title {
|
||||
margin-bottom: var(--ac-space-md);
|
||||
font-size: var(--ac-text-2xl);
|
||||
font-weight: var(--ac-weight-bold);
|
||||
color: var(--ac-text-primary);
|
||||
}
|
||||
|
||||
.checkout-order-name {
|
||||
font-size: 1.5rem;
|
||||
color: #2d3748;
|
||||
font-weight: 600;
|
||||
font-size: var(--ac-text-lg);
|
||||
font-weight: var(--ac-weight-semibold);
|
||||
color: var(--ac-text-secondary);
|
||||
}
|
||||
|
||||
/* Info value styling */
|
||||
.info-value {
|
||||
font-size: 1.1rem;
|
||||
/* ==========================================================================
|
||||
Order info: label / value pairs
|
||||
Rendered as <dl><dt class="info-label"><dd class="info-value">.
|
||||
Column count follows the viewport: 4 (label, value, label, value) on
|
||||
desktop, 2 (one pair per row) from sm, stacked below that. Cells carry the
|
||||
rules themselves and column-gap stays 0, so each row reads as one
|
||||
uninterrupted line instead of a dashed one.
|
||||
========================================================================== */
|
||||
|
||||
.order-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
align-items: baseline;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.order-info-grid > .info-label {
|
||||
padding-block: var(--ac-space-sm) 0;
|
||||
/* Small, muted and spaced out, so the label never competes with its value.
|
||||
--ac-text-secondary keeps it at 7.53:1 despite the smaller size. */
|
||||
font-size: var(--ac-text-xs);
|
||||
font-weight: var(--ac-weight-semibold);
|
||||
line-height: var(--ac-leading-snug);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--ac-text-secondary);
|
||||
}
|
||||
|
||||
.order-info-grid > .info-value {
|
||||
margin: 0;
|
||||
padding-block: var(--ac-space-2xs) var(--ac-space-sm);
|
||||
border-bottom: 1px solid var(--ac-border);
|
||||
font-size: var(--ac-text-md);
|
||||
font-weight: var(--ac-weight-semibold);
|
||||
line-height: var(--ac-leading-snug);
|
||||
color: var(--ac-text-primary);
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Multi-line values (pickup slots) keep their own rhythm. */
|
||||
.order-info-grid .info-value-line + .info-value-line {
|
||||
margin-top: var(--ac-space-2xs);
|
||||
}
|
||||
|
||||
/* Two columns: label and value side by side, one pair per row. */
|
||||
@media (min-width: 576px) {
|
||||
.order-info-grid {
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.order-info-grid > .info-label {
|
||||
padding-block: var(--ac-space-sm);
|
||||
padding-inline-end: var(--ac-space-md);
|
||||
border-bottom: 1px solid var(--ac-border);
|
||||
}
|
||||
|
||||
.order-info-grid > .info-value {
|
||||
padding-block: var(--ac-space-sm);
|
||||
padding-inline-end: var(--ac-space-md);
|
||||
}
|
||||
}
|
||||
|
||||
/* Four columns: two pairs per row, split by a vertical rule. The second pair
|
||||
always starts on an even <dt>, because grid places the cells in source
|
||||
order. */
|
||||
@media (min-width: 992px) {
|
||||
.order-info-grid {
|
||||
grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.order-info-grid > .info-label:nth-of-type(even) {
|
||||
padding-inline-start: var(--ac-space-lg);
|
||||
border-inline-start: 1px solid var(--ac-border);
|
||||
}
|
||||
}
|
||||
|
||||
/* Single label/value pair, used in the checkout info row where each pair sits in
|
||||
its own Bootstrap column. */
|
||||
.info-pair {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-pair > .info-label {
|
||||
margin-bottom: var(--ac-space-3xs);
|
||||
font-size: var(--ac-text-xs);
|
||||
font-weight: var(--ac-weight-semibold);
|
||||
line-height: var(--ac-leading-snug);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--ac-text-secondary);
|
||||
}
|
||||
|
||||
.info-pair > .info-value {
|
||||
margin: 0;
|
||||
font-size: var(--ac-text-md);
|
||||
font-weight: var(--ac-weight-semibold);
|
||||
line-height: var(--ac-leading-snug);
|
||||
color: var(--ac-text-primary);
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Standalone date line used by the checkout header. */
|
||||
.info-date {
|
||||
font-size: 1rem;
|
||||
display: block;
|
||||
margin-top: var(--ac-space-2xs);
|
||||
font-size: var(--ac-text-base);
|
||||
font-weight: var(--ac-weight-medium);
|
||||
color: var(--ac-text-secondary);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,106 +1,67 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/layout/pages.css */
|
||||
|
||||
/**
|
||||
* Page backgrounds and main layout structures
|
||||
* Page shells: backgrounds and the container stacking above them.
|
||||
*
|
||||
* These styles used to reach far outside the addon:
|
||||
* - `html, body { background: transparent !important }` stripped the
|
||||
* background from EVERY page of the website, addon or not.
|
||||
* - `body.website_published { … !important }` was meant to put it back, but
|
||||
* no page carries that class — checked against the rendered HTML of the
|
||||
* orders, shop and checkout pages, whose <body> has no class at all. Both
|
||||
* rules were therefore dead weight on top of a site-wide side effect.
|
||||
* - `#wrap { padding: 2rem 0 }` styled Odoo's own page wrapper everywhere.
|
||||
* On these pages #wrap *is* the Eskaera root, so the padding moved onto the
|
||||
* root classes and the ID selector is gone.
|
||||
*
|
||||
* The body reset now applies only to pages that actually contain an Eskaera
|
||||
* root, which is enough specificity to win without !important.
|
||||
*/
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: transparent !important;
|
||||
background: transparent !important;
|
||||
body:has(.eskaera-page),
|
||||
body:has(.eskaera-shop-page),
|
||||
body:has(.eskaera-generic-page),
|
||||
body:has(.eskaera-checkout-page) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
body.website_published {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--primary-color) 30%, white),
|
||||
color-mix(in srgb, var(--primary-color) 60%, black)
|
||||
) !important;
|
||||
}
|
||||
|
||||
body.website_published .eskaera-shop-page,
|
||||
body.website_published .eskaera-checkout-page {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Generic page background mixin */
|
||||
.eskaera-page,
|
||||
.eskaera-shop-page,
|
||||
.eskaera-generic-page,
|
||||
.eskaera-checkout-page {
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
/* dvh tracks the mobile browser chrome; vh stays as the fallback. */
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
padding: var(--ac-space-xl) 0;
|
||||
}
|
||||
|
||||
.eskaera-page,
|
||||
.eskaera-generic-page {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--primary-color) 10%, white),
|
||||
color-mix(in srgb, var(--primary-color) 70%, black)
|
||||
) !important;
|
||||
color-mix(in srgb, var(--ac-color-primary) 10%, white),
|
||||
color-mix(in srgb, var(--ac-color-primary) 70%, black)
|
||||
);
|
||||
}
|
||||
|
||||
.eskaera-shop-page {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--primary-color) 10%, white),
|
||||
color-mix(in srgb, var(--primary-color) 10%, rgb(135, 135, 135))
|
||||
) !important;
|
||||
color-mix(in srgb, var(--ac-color-primary) 10%, white),
|
||||
color-mix(in srgb, var(--ac-color-primary) 10%, rgb(135, 135, 135))
|
||||
);
|
||||
}
|
||||
|
||||
.eskaera-checkout-page {
|
||||
background: linear-gradient(
|
||||
-135deg,
|
||||
color-mix(in srgb, var(--primary-color) 0%, white),
|
||||
color-mix(in srgb, var(--primary-color) 60%, black)
|
||||
) !important;
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,10 +194,10 @@
|
|||
'<th scope="col" class="col-qty text-center">' +
|
||||
escapeHtml(labels.quantity) +
|
||||
"</th>" +
|
||||
'<th scope="col" class="col-price text-right">' +
|
||||
'<th scope="col" class="col-price text-end">' +
|
||||
escapeHtml(labels.price) +
|
||||
"</th>" +
|
||||
'<th scope="col" class="col-subtotal text-right">' +
|
||||
'<th scope="col" class="col-subtotal text-end">' +
|
||||
escapeHtml(labels.subtotal) +
|
||||
"</th>" +
|
||||
'</tr></thead><tbody id="checkout-summary-tbody"></tbody></table>' +
|
||||
|
|
@ -280,10 +280,10 @@
|
|||
'<td class="text-center">' +
|
||||
qty.toFixed(2).replace(/\.?0+$/, "") +
|
||||
"</td>" +
|
||||
'<td class="text-right">€' +
|
||||
'<td class="text-end">€' +
|
||||
price.toFixed(2) +
|
||||
"</td>" +
|
||||
'<td class="text-right">€' +
|
||||
'<td class="text-end">€' +
|
||||
subtotal.toFixed(2) +
|
||||
"</td>";
|
||||
tbody.appendChild(row);
|
||||
|
|
@ -307,10 +307,10 @@
|
|||
'<td class="text-center">' +
|
||||
qty.toFixed(2).replace(/\.?0+$/, "") +
|
||||
"</td>" +
|
||||
'<td class="text-right">€' +
|
||||
'<td class="text-end">€' +
|
||||
price.toFixed(2) +
|
||||
"</td>" +
|
||||
'<td class="text-right">€' +
|
||||
'<td class="text-end">€' +
|
||||
subtotal.toFixed(2) +
|
||||
"</td>";
|
||||
tbody.appendChild(row);
|
||||
|
|
|
|||
|
|
@ -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 = "";
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -691,12 +691,16 @@
|
|||
|
||||
var notification = document.createElement("div");
|
||||
notification.className =
|
||||
"alert alert-" + type + " alert-dismissible fade show position-fixed";
|
||||
notification.style.cssText =
|
||||
"top: 80px; right: 20px; z-index: 9999; min-width: 300px; max-width: 500px; font-size: 18px; padding: 1.5rem; transition: opacity 0.3s ease-out;";
|
||||
"alert alert-" + type + " alert-dismissible fade show eskaera-notification";
|
||||
// Announce it: errors and warnings are the whole point of this helper,
|
||||
// and without a live role a screen reader never hears them.
|
||||
notification.setAttribute("role", type === "error" ? "alert" : "status");
|
||||
notification.setAttribute("aria-live", type === "error" ? "assertive" : "polite");
|
||||
notification.innerHTML =
|
||||
message +
|
||||
'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>';
|
||||
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="' +
|
||||
((window.i18nManager && window.i18nManager.get("close")) || "Close") +
|
||||
'"></button>';
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
|
|
@ -1253,13 +1257,17 @@
|
|||
// Create toast element
|
||||
var toast = document.createElement("div");
|
||||
toast.className = "toast-notification";
|
||||
toast.setAttribute("role", "status");
|
||||
|
||||
// Get translated "added to cart" label from server
|
||||
var labels = this._getLabels();
|
||||
var addedMsg = labels.added_to_cart || "added to cart";
|
||||
|
||||
toast.innerHTML =
|
||||
'<i class="fa fa-check"></i> <strong>' + productName + "</strong> " + addedMsg;
|
||||
'<i class="fa fa-check" aria-hidden="true"></i> <strong>' +
|
||||
this.escapeHtml(productName) +
|
||||
"</strong> " +
|
||||
addedMsg;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
|
|
@ -1551,7 +1559,7 @@
|
|||
// Simple approach: if response is very short, assume no more products
|
||||
if (html.trim().length < 100) {
|
||||
console.log("[LAZY_LOADING] No more products, hiding load-more button");
|
||||
btn.style.display = "none";
|
||||
btn.classList.add("d-none");
|
||||
} else {
|
||||
// Re-enable button for next page
|
||||
btn.disabled = false;
|
||||
|
|
@ -1595,7 +1603,7 @@
|
|||
|
||||
if (Object.keys(this.cart).length === 0) {
|
||||
cartContainer.innerHTML =
|
||||
'<p class="text-muted" style="font-size: 1.1rem;">' +
|
||||
'<p class="text-muted cart-empty-message">' +
|
||||
(labels.empty_cart || "This order's cart is empty.") +
|
||||
"</p>";
|
||||
return;
|
||||
|
|
@ -1612,20 +1620,20 @@
|
|||
total += subtotal;
|
||||
|
||||
html +=
|
||||
'<div class="list-group-item" style="font-size: 1.05rem;">' +
|
||||
'<div class="list-group-item">' +
|
||||
'<div class="mb-2">' +
|
||||
'<h6 class="mb-0" style="font-size: 1.1rem;">' +
|
||||
'<h6 class="mb-0">' +
|
||||
self.escapeHtml(item.name) +
|
||||
"</h6>" +
|
||||
"</div>" +
|
||||
'<div class="d-flex justify-content-between align-items-center">' +
|
||||
'<small class="text-muted" style="font-size: 1rem;">' +
|
||||
'<small class="text-muted">' +
|
||||
parseFloat(item.qty).toFixed(1) +
|
||||
" x €" +
|
||||
item.price.toFixed(2) +
|
||||
"</small>" +
|
||||
'<div class="d-flex align-items-center gap-2">' +
|
||||
'<strong style="font-size: 1.1rem;">€' +
|
||||
"<strong>€" +
|
||||
subtotal.toFixed(2) +
|
||||
"</strong>" +
|
||||
'<button class="btn btn-sm btn-danger remove-from-cart" ' +
|
||||
|
|
@ -1644,11 +1652,7 @@
|
|||
});
|
||||
|
||||
html += "</div>";
|
||||
html +=
|
||||
'<div class="cart-total mt-3 pt-3" style="font-size: 1.2rem; font-weight: bold;">' +
|
||||
"Total: €" +
|
||||
total.toFixed(2) +
|
||||
"</div>";
|
||||
html += '<div class="cart-total mt-3 pt-3">' + "Total: €" + total.toFixed(2) + "</div>";
|
||||
|
||||
cartContainer.innerHTML = html;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,45 @@
|
|||
<data>
|
||||
<!-- Template to load items from history and redirect to group order -->
|
||||
<template id="eskaera_load_from_history" name="Load Order from History">
|
||||
<html>
|
||||
<html t-att-lang="page_lang or 'en-US'">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Loading Order...</title>
|
||||
<!-- Standalone document: it does not go through website.layout, so
|
||||
web.assets_frontend is not loaded and there is no design token to
|
||||
reach for. Hence the local style block. -->
|
||||
<style>
|
||||
.eskaera-redirect-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
padding: 2rem 1rem;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, sans-serif;
|
||||
text-align: center;
|
||||
color: #1a202c;
|
||||
}
|
||||
.eskaera-redirect-message {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- This page only stages the cart in sessionStorage and redirects, but it is
|
||||
still a page a person can land on: give it something to read while that
|
||||
happens, announced politely, and a way out if the script never runs. -->
|
||||
<main class="eskaera-redirect-page">
|
||||
<p class="eskaera-redirect-message" role="status">Loading your order…</p>
|
||||
<noscript>
|
||||
<p>This page needs JavaScript to load the order into your cart.</p>
|
||||
<p>
|
||||
<a t-att-href="group_order_url">Continue to the group order</a>
|
||||
</p>
|
||||
</noscript>
|
||||
</main>
|
||||
<script type="text/javascript">
|
||||
// Items are embedded directly in the script (pre-serialized JSON from controller)
|
||||
var itemsJson = <t t-raw="items_json"/>; // This is a JSON array/string
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
<data>
|
||||
<!-- Extend portal_my_orders to add group order name and pickup day columns -->
|
||||
<template id="portal_my_orders_extend" inherit_id="sale.portal_my_orders" name="My Orders - Add Group Order Info">
|
||||
<!-- Add column headers for new info -->
|
||||
<!-- Add column headers for new info.
|
||||
There is no "Actions" header here: the matching <td> below is
|
||||
commented out, so declaring it left every row one cell short of
|
||||
its header row and pushed the columns out of alignment. -->
|
||||
<xpath expr="//tr[hasclass('active')]//th[last()]" position="after">
|
||||
<th>Group Order</th>
|
||||
<th>Pickup Day</th>
|
||||
<th class="text-center">Actions</th>
|
||||
<th scope="col">Group Order</th>
|
||||
<th scope="col">Pickup Day</th>
|
||||
</xpath>
|
||||
|
||||
<!-- Add data cells for each order row -->
|
||||
|
|
@ -75,25 +77,30 @@
|
|||
<!-- Group Order Info -->
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<!-- text-bg-primary, not bg-primary: a badge's default white on
|
||||
the primary blue is 3.98:1, under AA. The utility picks a
|
||||
contrasting foreground for whatever the theme colour is.
|
||||
d-block on the notes so their mt-2 actually applies — margins
|
||||
do nothing on an inline span. -->
|
||||
<div class="card-header bg-light">
|
||||
<strong><t t-esc="sale_order.group_order_id.name"/></strong>
|
||||
<t t-if="sale_order.home_delivery">
|
||||
<span class="badge bg-primary">
|
||||
<span class="badge text-bg-primary">
|
||||
<t t-set="day_idx" t-value="(int(sale_order.group_order_id.pickup_day) + 1) % 7 if sale_order.group_order_id.pickup_day else 0"/>
|
||||
<t t-esc="day_names[day_idx] if day_names else ''"/>,
|
||||
<t t-esc="sale_order.group_order_id.delivery_date.strftime('%d/%m/%Y') if sale_order.group_order_id.delivery_date else ''"/>
|
||||
</span>
|
||||
<span class="mt-2 text-muted small">
|
||||
<span class="d-block mt-2 text-muted small">
|
||||
<t t-esc="sale_order.group_order_id.delivery_notice"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge bg-primary">
|
||||
<span class="badge text-bg-primary">
|
||||
<t t-set="day_idx" t-value="int(sale_order.group_order_id.pickup_day) % 7 if sale_order.group_order_id.pickup_day else 0"/>
|
||||
<t t-esc="day_names[day_idx] if day_names else ''"/>,
|
||||
<t t-esc="sale_order.pickup_date.strftime('%d/%m/%Y')"/>
|
||||
</span>
|
||||
<span class="mt-2 small">
|
||||
<span class="d-block mt-2 small">
|
||||
<t t-foreach="sale_order.group_order_id.group_ids" t-as="group">
|
||||
<t t-esc="group.name"/>
|
||||
<t t-if="group.street">
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@
|
|||
<div t-attf-class="eskaera-order-card{{ ' eskaera-order-card--special' if order.type == 'special' else '' }}">
|
||||
<div class="card-body">
|
||||
<div class="position-absolute order-badge-position">
|
||||
<span class="badge bg-primary d-flex align-items-center gap-2 order-badge-custom">
|
||||
<i class="fa fa-shopping-bag" />
|
||||
<span class="badge text-bg-primary d-flex align-items-center gap-2 order-badge-custom">
|
||||
<i class="fa fa-shopping-bag" aria-hidden="true" />
|
||||
<strong>
|
||||
<t t-esc="order.available_products_count" />
|
||||
</strong>
|
||||
|
|
@ -147,22 +147,24 @@
|
|||
</span>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="order.home_delivery and order.delivery_date">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Delivery</span>
|
||||
<span class="meta-value">
|
||||
<t t-esc="day_names[order.delivery_date.weekday()]" /> <t t-esc="order.delivery_date.strftime('%d/%m')" />
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="order.home_delivery">
|
||||
<div class="meta-item">
|
||||
<span class="badge bg-success"><i class="fa fa-truck" /> Home Delivery</span>
|
||||
<!-- Badge first, then the date, centred as a pair. Source order
|
||||
matches reading order, so no CSS reordering is involved.
|
||||
No "Delivery" label: the badge already names the thing.
|
||||
text-bg-* instead of bg-*: Bootstrap picks a contrasting
|
||||
foreground. A badge's default white on green is 3.13:1. -->
|
||||
<div class="meta-item meta-item--delivery">
|
||||
<span class="badge text-bg-success"><i class="fa fa-truck" aria-hidden="true" /> Home Delivery</span>
|
||||
<t t-if="order.delivery_date">
|
||||
<span class="meta-value">
|
||||
<t t-esc="day_names[order.delivery_date.weekday()]" /> <t t-esc="order.delivery_date.strftime('%d/%m')" />
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="order.type == 'special'">
|
||||
<div class="meta-item meta-item--full">
|
||||
<span class="badge special-order-badge"><i class="fa fa-star" /> Special Order</span>
|
||||
<span class="badge special-order-badge"><i class="fa fa-star" aria-hidden="true" /> Special Order</span>
|
||||
</div>
|
||||
<t t-if="order.start_date">
|
||||
<div class="meta-item">
|
||||
|
|
@ -182,7 +184,10 @@
|
|||
</template>
|
||||
<template id="order_header" name="Order Header">
|
||||
<div t-att-class="header_class or 'eskaera-order-header'">
|
||||
<div class="d-flex gap-5 align-items-center mb-4">
|
||||
<!-- Responsive Bootstrap utilities rather than a CSS override: the
|
||||
spacing/alignment utilities are !important, so fighting them from
|
||||
a stylesheet would need !important too. -->
|
||||
<div class="d-flex flex-column flex-md-row gap-3 gap-md-5 align-items-start align-items-md-center mb-4">
|
||||
<t t-set="image_to_show" t-value="group_order.image or (group_order.group_ids[0].image_1920 if group_order.group_ids else False)" />
|
||||
<t t-if="image_to_show">
|
||||
<img t-att-src="image_data_uri(image_to_show)" alt="Order image" class="order-thumbnail-md" />
|
||||
|
|
@ -210,79 +215,70 @@
|
|||
<t t-set="header_class" t-value="'eskaera-order-header'" />
|
||||
</t>
|
||||
<div class="eskaera-order-header">
|
||||
<div class="order-info-grid">
|
||||
<div class="info-item">
|
||||
<span t-att-class="'info-label'">Consumer Groups</span>
|
||||
<span class="info-value">
|
||||
<t t-esc="', '.join(group_order.group_ids.mapped('name'))" />
|
||||
</span>
|
||||
</div>
|
||||
<!-- Name/value pairs: a description list, so assistive tech
|
||||
announces each value with its label. The 4 → 2 → 1 column
|
||||
layout lives in layout/header.css. -->
|
||||
<dl class="order-info-grid">
|
||||
<dt class="info-label">Consumer Groups</dt>
|
||||
<dd class="info-value">
|
||||
<t t-esc="', '.join(group_order.group_ids.mapped('name'))" />
|
||||
</dd>
|
||||
<t t-if="group_order.cutoff_day">
|
||||
<div class="info-item">
|
||||
<span t-att-class="'info-label'">Cutoff Day</span>
|
||||
<span class="info-value">
|
||||
<t t-esc="day_names[int(group_order.cutoff_day) % 7]" /> (<t t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')" />)</span>
|
||||
</div>
|
||||
<dt class="info-label">Cutoff Day</dt>
|
||||
<dd class="info-value">
|
||||
<t t-esc="day_names[int(group_order.cutoff_day) % 7]" /> (<t t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')" />)</dd>
|
||||
</t>
|
||||
<t t-if="group_order.pickup_slot_ids">
|
||||
<div class="info-item">
|
||||
<span t-att-class="'info-label'">Store Pickup Slots</span>
|
||||
<span class="info-value">
|
||||
<t t-foreach="group_order.pickup_slot_ids" t-as="slot">
|
||||
<div>
|
||||
<t t-esc="day_names[int(slot.weekday) % 7]" />
|
||||
 
|
||||
<t t-esc="('%02d:%02d–%02d:%02d' % (int(slot.start_hour or 0), int(((slot.start_hour or 0) % 1) * 60), int(slot.end_hour or 0), int(((slot.end_hour or 0) % 1) * 60)))" />
|
||||
<t t-if="slot.label"> (<t t-esc="slot.label" />)</t>
|
||||
</div>
|
||||
</t>
|
||||
</span>
|
||||
</div>
|
||||
<dt class="info-label">Store Pickup Slots</dt>
|
||||
<dd class="info-value">
|
||||
<t t-foreach="group_order.pickup_slot_ids" t-as="slot">
|
||||
<div class="info-value-line">
|
||||
<t t-esc="day_names[int(slot.weekday) % 7]" />
|
||||
 
|
||||
<t t-esc="('%02d:%02d–%02d:%02d' % (int(slot.start_hour or 0), int(((slot.start_hour or 0) % 1) * 60), int(slot.end_hour or 0), int(((slot.end_hour or 0) % 1) * 60)))" />
|
||||
<t t-if="slot.label"> (<t t-esc="slot.label" />)</t>
|
||||
</div>
|
||||
</t>
|
||||
</dd>
|
||||
</t>
|
||||
<t t-elif="group_order.pickup_day">
|
||||
<div class="info-item">
|
||||
<span t-att-class="'info-label'">Store Pickup Day</span>
|
||||
<span class="info-value">
|
||||
<t t-esc="day_names[int(group_order.pickup_day) % 7]" /> (<t t-esc="group_order.pickup_date.strftime('%d/%m/%Y')" />)
|
||||
</span>
|
||||
</div>
|
||||
<dt class="info-label">Store Pickup Day</dt>
|
||||
<dd class="info-value">
|
||||
<t t-esc="day_names[int(group_order.pickup_day) % 7]" /> (<t t-esc="group_order.pickup_date.strftime('%d/%m/%Y')" />)
|
||||
</dd>
|
||||
</t>
|
||||
<t t-if="group_order.delivery_date and group_order.home_delivery">
|
||||
<div class="info-item">
|
||||
<span t-att-class="'info-label'">Home Delivery Day</span>
|
||||
<span class="info-value">
|
||||
<t t-esc="day_names[group_order.delivery_date.weekday()]" /> (<t t-esc="group_order.delivery_date.strftime('%d/%m/%Y')" />)</span>
|
||||
</div>
|
||||
<dt class="info-label">Home Delivery Day</dt>
|
||||
<dd class="info-value">
|
||||
<t t-esc="day_names[group_order.delivery_date.weekday()]" /> (<t t-esc="group_order.delivery_date.strftime('%d/%m/%Y')" />)</dd>
|
||||
</t>
|
||||
<t t-if="group_order.start_date">
|
||||
<div class="info-item">
|
||||
<span t-att-class="'info-label'">Open From</span>
|
||||
<span class="info-value">
|
||||
<t t-esc="group_order.start_date.strftime('%d/%m/%Y')" />
|
||||
</span>
|
||||
</div>
|
||||
<dt class="info-label">Open From</dt>
|
||||
<dd class="info-value">
|
||||
<t t-esc="group_order.start_date.strftime('%d/%m/%Y')" />
|
||||
</dd>
|
||||
</t>
|
||||
<t t-if="group_order.end_date">
|
||||
<div class="info-item">
|
||||
<span t-att-class="'info-label'">Open Until</span>
|
||||
<span class="info-value">
|
||||
<t t-esc="group_order.end_date.strftime('%d/%m/%Y')" />
|
||||
</span>
|
||||
</div>
|
||||
<dt class="info-label">Open Until</dt>
|
||||
<dd class="info-value">
|
||||
<t t-esc="group_order.end_date.strftime('%d/%m/%Y')" />
|
||||
</dd>
|
||||
</t>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3" id="realtimeSearch-filters">
|
||||
<div class="mb-3 eskaera-filters" id="realtimeSearch-filters">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-7">
|
||||
<div style="position: relative;">
|
||||
<input type="text" id="realtime-search-input" class="form-control realtime-search-box search-input-styled" placeholder="Search products..." autocomplete="off" style="padding-right: 40px;" />
|
||||
<button type="button" id="clear-search-btn" class="btn btn-link" style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%); padding: 0; width: 30px; height: 30px; display: none; color: #6c757d; text-decoration: none; font-size: 1.5rem; line-height: 1;" aria-label="Clear search" title="Clear search">
|
||||
×
|
||||
</button>
|
||||
<div class="search-input-wrapper">
|
||||
<label class="visually-hidden" for="realtime-search-input">Search products</label>
|
||||
<input type="search" id="realtime-search-input" class="form-control eskaera-search-input" placeholder="Search products..." autocomplete="off" aria-describedby="realtime-search-status" />
|
||||
<button type="button" id="clear-search-btn" class="search-clear-btn" aria-label="Clear search" title="Clear search">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<p id="realtime-search-status" class="visually-hidden" aria-live="polite" aria-atomic="true" />
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<select name="category" id="realtime-category-select" class="form-select">
|
||||
|
|
@ -299,13 +295,18 @@
|
|||
<div class="col-12">
|
||||
<div id="tag-filter-container" class="tag-filter-badges">
|
||||
<t t-foreach="available_tags" t-as="tag">
|
||||
<!-- Toggle buttons: aria-pressed carries the filter
|
||||
state, which realtime_search.js keeps in sync
|
||||
with the .is-selected / .is-dimmed classes.
|
||||
The record colour rides in a custom property so
|
||||
the stylesheet keeps owning the badge. -->
|
||||
<t t-if="tag['color']">
|
||||
<button type="button" class="badge tag-filter-badge" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" t-att-data-tag-color="tag['color']" t-attf-style="background-color: {{ tag['color'] }} !important; border-color: {{ tag['color'] }} !important; color: #ffffff !important;" data-toggle="tag-filter">
|
||||
<button type="button" class="badge tag-filter-badge" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" t-att-data-tag-color="tag['color']" t-attf-style="--ac-tag-color: {{ tag['color'] }};" data-toggle="tag-filter" aria-pressed="false">
|
||||
<span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
|
||||
</button>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<button type="button" class="badge tag-filter-badge tag-use-theme-color" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" data-tag-color="" data-toggle="tag-filter">
|
||||
<button type="button" class="badge tag-filter-badge tag-use-theme-color" t-att-data-tag-id="tag['id']" t-att-data-tag-name="tag['name']" data-tag-color="" data-toggle="tag-filter" aria-pressed="false">
|
||||
<span t-esc="tag['name']" /> (<span class="tag-count" t-esc="tag['count']" />)
|
||||
</button>
|
||||
</t>
|
||||
|
|
@ -321,30 +322,32 @@
|
|||
<div class="card-header d-flex justify-content-between align-items-center gap-1">
|
||||
<h6 class="mb-0 cart-title-sm" id="cart-title">My Cart</h6>
|
||||
<div class="btn-group cart-btn-group gap-0" role="group">
|
||||
<button type="button" class="btn btn-primary cart-btn-compact" id="save-cart-btn" t-attf-data-order-id="{{ group_order.id }}" data-bs-title="Save Cart" data-bs-toggle="tooltip">
|
||||
<i class="fa fa-save cart-icon-size" />
|
||||
<!-- data-bs-title feeds the tooltip, it is not an
|
||||
accessible name: this icon-only button had none. -->
|
||||
<button type="button" class="btn btn-primary cart-btn-compact" id="save-cart-btn" t-attf-data-order-id="{{ group_order.id }}" aria-label="Save Cart" title="Save Cart" data-bs-toggle="tooltip">
|
||||
<i class="fa fa-save cart-icon-size" aria-hidden="true" />
|
||||
</button>
|
||||
<t t-if="group_order.home_delivery and delivery_product_id">
|
||||
<button type="button" class="btn btn-outline-warning cart-btn-compact" id="home-delivery-btn" t-att-aria-label="labels.get('toggle_home_delivery', 'Toggle home delivery')" t-att-data-bs-title="labels.get('home_delivery', 'Home Delivery')" data-bs-toggle="tooltip">
|
||||
<i class="fa fa-truck cart-icon-size" aria-hidden="true" />
|
||||
</button>
|
||||
</t>
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-success cart-btn-compact" aria-label="Proceed to checkout" data-bs-title="Proceed to Checkout" data-bs-toggle="tooltip">
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-success cart-btn-compact" aria-label="Proceed to Checkout" title="Proceed to Checkout" data-bs-toggle="tooltip">
|
||||
<i class="fa fa-check cart-icon-size" aria-hidden="true" />
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger cart-btn-compact" id="clear-cart-btn" t-attf-data-order-id="{{ group_order.id }}" data-bs-title="Clear Cart" data-bs-toggle="tooltip" aria-label="Clear Cart">
|
||||
<button type="button" class="btn btn-outline-danger cart-btn-compact" id="clear-cart-btn" t-attf-data-order-id="{{ group_order.id }}" title="Clear Cart" data-bs-toggle="tooltip" aria-label="Clear Cart">
|
||||
<i class="fa fa-trash cart-icon-size" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body cart-body-lg" id="cart-items-container" t-attf-data-order-id="{{ group_order.id }}" t-attf-data-build-version="{{ module_version }}" aria-labelledby="cart-title" aria-live="polite" aria-relevant="additions removals">
|
||||
<div class="card-body eskaera-cart-items" id="cart-items-container" t-attf-data-order-id="{{ group_order.id }}" t-attf-data-build-version="{{ module_version }}" aria-labelledby="cart-title" aria-live="polite" aria-relevant="additions removals">
|
||||
<p class="text-muted">This order's cart is empty</p>
|
||||
</div>
|
||||
<div class="card-footer bg-white d-flex justify-content-between align-items-center gap-2">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" id="clear-cart-btn-footer" t-attf-data-order-id="{{ group_order.id }}" data-bs-title="Clear Cart" data-bs-toggle="tooltip" aria-label="Clear Cart">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" id="clear-cart-btn-footer" t-attf-data-order-id="{{ group_order.id }}" title="Clear Cart" data-bs-toggle="tooltip" aria-label="Clear Cart">
|
||||
<i class="fa fa-trash me-1" aria-hidden="true" />Clear Cart
|
||||
</button>
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-success checkout-btn-lg" data-bs-title="Proceed to Checkout" data-bs-toggle="tooltip">
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}/checkout" class="btn btn-success checkout-btn-lg" title="Proceed to Checkout" data-bs-toggle="tooltip">
|
||||
Proceed to Checkout
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -361,14 +364,14 @@
|
|||
<t t-if="lazy_loading_enabled and has_next">
|
||||
<div id="infinite-scroll-container" class="row mt-4">
|
||||
<div class="col-12 text-center">
|
||||
<div id="loading-spinner" class="d-none" style="padding: 20px;">
|
||||
<div id="loading-spinner" class="d-none p-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2">Loading more products...</p>
|
||||
</div>
|
||||
<button id="load-more-btn" class="btn btn-primary btn-lg d-none" t-attf-data-page="{{ current_page + 1 }}" t-attf-data-order-id="{{ group_order.id }}" t-attf-data-search="{{ search_query }}" t-attf-data-category="{{ selected_category }}" t-attf-data-per-page="{{ per_page }}" aria-label="Load more products" style="display: none;">
|
||||
<i class="fa fa-download me-2" />Load More Products
|
||||
<button id="load-more-btn" class="btn btn-primary btn-lg d-none" t-attf-data-page="{{ current_page + 1 }}" t-attf-data-order-id="{{ group_order.id }}" t-attf-data-search="{{ search_query }}" t-attf-data-category="{{ selected_category }}" t-attf-data-per-page="{{ per_page }}" aria-label="Load more products">
|
||||
<i class="fa fa-download me-2" aria-hidden="true" />Load More Products
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -396,14 +399,13 @@
|
|||
|
||||
var successCount = 0;
|
||||
tooltipElements.forEach(function(element) {
|
||||
// Static labels now live in the title attribute, which QWeb
|
||||
// translates; data-bs-title (which it does not translate) is
|
||||
// only left where the text comes from the i18n endpoint.
|
||||
var title = element.getAttribute('data-bs-title');
|
||||
if (title) {
|
||||
// Set native title attribute for browser-native tooltip
|
||||
element.setAttribute('title', title);
|
||||
successCount++;
|
||||
console.log('[TOOLTIP] ✅ Set title for', element.id || element.className, ':', title);
|
||||
} else {
|
||||
console.warn('[TOOLTIP] ⚠️ No data-bs-title found for element:', element.id || element.className);
|
||||
}
|
||||
});
|
||||
console.log('[TOOLTIP] Tooltip initialization complete:', successCount, 'elements updated');
|
||||
|
|
@ -421,20 +423,23 @@
|
|||
</t>
|
||||
</template>
|
||||
<template id="eskaera_checkout_summary" name="Checkout Order Summary">
|
||||
<div class="checkout-summary-container">
|
||||
<!-- The table keeps its width and scrolls inside this container; tabindex
|
||||
makes that scroll reachable from the keyboard (WCAG 2.1.1). -->
|
||||
<div class="checkout-summary-container" role="region" tabindex="0" aria-label="Order summary">
|
||||
<table class="table table-hover checkout-summary-table" id="checkout-summary-table">
|
||||
<caption class="visually-hidden">Products in this order, with quantity, price and subtotal</caption>
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th class="col-name">Product</th>
|
||||
<th class="col-qty text-center">Quantity</th>
|
||||
<th class="col-price text-right">Price</th>
|
||||
<th class="col-subtotal text-right">Subtotal</th>
|
||||
<th scope="col" class="col-name">Product</th>
|
||||
<th scope="col" class="col-qty text-center">Quantity</th>
|
||||
<th scope="col" class="col-price text-end">Price</th>
|
||||
<th scope="col" class="col-subtotal text-end">Subtotal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="checkout-summary-tbody">
|
||||
<tr id="checkout-empty-row" class="empty-message">
|
||||
<td colspan="4" class="text-center text-muted py-4">
|
||||
<i class="fa fa-inbox fa-2x mb-2" />
|
||||
<i class="fa fa-inbox fa-2x mb-2" aria-hidden="true" />
|
||||
<p>This order's cart is empty</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -464,55 +469,60 @@
|
|||
</div>
|
||||
<div class="order-info-card card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<!-- Name/value pairs, same treatment as the shop header:
|
||||
a <label> without a form control to point at labels
|
||||
nothing, while <dt>/<dd> ties value to name. -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="info-item">
|
||||
<label t-att-class="'info-label'">Cutoff Day</label>
|
||||
<dl class="info-pair">
|
||||
<dt class="info-label">Cutoff Day</dt>
|
||||
<t t-if="group_order.cutoff_day and group_order.cutoff_date">
|
||||
<span class="info-value">
|
||||
<dd class="info-value">
|
||||
<t t-esc="day_names[int(group_order.cutoff_day) % 7]" />
|
||||
<span class="info-date">(<t t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')" />)</span>
|
||||
</span>
|
||||
</dd>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-att-class="'text-muted small'">Not configured</span>
|
||||
<dd class="info-value text-muted small">Not configured</dd>
|
||||
</t>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="info-item">
|
||||
<t t-if="group_order.pickup_slot_ids">
|
||||
<label t-att-class="'info-label'">Store Pickup Slots</label>
|
||||
<span class="info-value">
|
||||
<t t-if="group_order.pickup_slot_ids">
|
||||
<dl class="info-pair">
|
||||
<dt class="info-label">Store Pickup Slots</dt>
|
||||
<dd class="info-value">
|
||||
<t t-foreach="group_order.pickup_slot_ids" t-as="slot">
|
||||
<div>
|
||||
<div class="info-value-line">
|
||||
<t t-esc="day_names[int(slot.weekday) % 7]" />
|
||||
 
|
||||
<t t-esc="('%02d:%02d–%02d:%02d' % (int(slot.start_hour or 0), int(((slot.start_hour or 0) % 1) * 60), int(slot.end_hour or 0), int(((slot.end_hour or 0) % 1) * 60)))" />
|
||||
<t t-if="slot.label"> (<t t-esc="slot.label" />)</t>
|
||||
</div>
|
||||
</t>
|
||||
</span>
|
||||
</t>
|
||||
<t t-elif="group_order.pickup_day and group_order.pickup_date">
|
||||
<label t-att-class="'info-label'">Store Pickup Day</label>
|
||||
<span class="info-value" t-attf-data-pickup-date="{{ group_order.pickup_date }}">
|
||||
</dd>
|
||||
</dl>
|
||||
</t>
|
||||
<t t-elif="group_order.pickup_day and group_order.pickup_date">
|
||||
<dl class="info-pair">
|
||||
<dt class="info-label">Store Pickup Day</dt>
|
||||
<dd class="info-value" t-attf-data-pickup-date="{{ group_order.pickup_date }}">
|
||||
<t t-esc="day_names[int(group_order.pickup_day) % 7]" />
|
||||
<span class="info-date">(<t t-esc="group_order.pickup_date.strftime('%d/%m/%Y')" />)</span>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="info-item">
|
||||
<t t-if="group_order.delivery_date and group_order.home_delivery">
|
||||
<label t-att-class="'info-label'">Home Delivery Day</label>
|
||||
<span class="info-value">
|
||||
<t t-if="group_order.delivery_date and group_order.home_delivery">
|
||||
<dl class="info-pair">
|
||||
<dt class="info-label">Home Delivery Day</dt>
|
||||
<dd class="info-value">
|
||||
<t t-esc="day_names[group_order.delivery_date.weekday()]" />
|
||||
<span class="info-date">(<t t-esc="group_order.delivery_date.strftime('%d/%m/%Y')" />)</span>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-2" />
|
||||
|
|
@ -529,11 +539,11 @@
|
|||
<t t-if="group_order.home_delivery">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="form-check">
|
||||
<div class="form-check eskaera-delivery-check">
|
||||
<input type="checkbox" class="form-check-input" id="home-delivery-checkbox" name="home_delivery" />
|
||||
<label class="form-check-label fw-bold" for="home-delivery-checkbox">Home Delivery</label>
|
||||
</div>
|
||||
<div id="delivery-info-alert" class="alert alert-info mt-3 d-none">
|
||||
<div id="delivery-info-alert" class="alert alert-info mt-3 d-none eskaera-delivery-notice">
|
||||
<p class="mb-2">
|
||||
<i class="fa fa-truck" aria-hidden="true" t-translation="off" />
|
||||
<t t-if="group_order.delivery_date">
|
||||
|
|
@ -555,7 +565,7 @@
|
|||
<i class="fa fa-save" aria-hidden="true" t-translation="off" />
|
||||
<span t-esc="labels.get('save_draft', 'Save Draft')" />
|
||||
</button>
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}" class="btn btn-outline-secondary btn-lg" aria-label="Back to cart page" data-bs-title="Back to Cart" data-bs-toggle="tooltip">
|
||||
<a t-attf-href="/eskaera/{{ group_order.slug }}" class="btn btn-outline-secondary btn-lg" aria-label="Back to cart page" title="Back to Cart" data-bs-toggle="tooltip">
|
||||
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
|
||||
<span>Back to Cart</span>
|
||||
</a>
|
||||
|
|
@ -635,7 +645,7 @@
|
|||
</t>
|
||||
<t t-else="">
|
||||
<div class="card-img-top bg-light d-flex align-items-center justify-content-center product-img-placeholder">
|
||||
<i class="fa fa-image fa-3x text-muted" />
|
||||
<i class="fa fa-image fa-3x text-muted" aria-hidden="true" />
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="product.dynamic_ribbon_id">
|
||||
|
|
@ -649,10 +659,12 @@
|
|||
<div class="card-body d-flex flex-column">
|
||||
<h6 class="card-title" t-esc="product.name" />
|
||||
<t t-if="product.product_tag_ids">
|
||||
<div class="product-tags mb-2">
|
||||
<div class="product-tags">
|
||||
<t t-foreach="filtered_product_tags.get(product.id, {}).get('published_tags', product.product_tag_ids)" t-as="tag">
|
||||
<t t-if="tag.color">
|
||||
<span class="badge badge-km" t-attf-style="background-color: {{ tag.color }} !important; border-color: {{ tag.color }} !important; color: #ffffff !important;" t-esc="tag.name" />
|
||||
<!-- Per-record colour: passed as a custom property so the
|
||||
stylesheet keeps owning the badge, no !important needed. -->
|
||||
<span class="badge badge-km" t-attf-style="--ac-tag-color: {{ tag.color }};" t-esc="tag.name" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="badge badge-km tag-use-theme-color" t-esc="tag.name" />
|
||||
|
|
@ -661,14 +673,14 @@
|
|||
</div>
|
||||
</t>
|
||||
<t t-if="product_supplier_info.get(product.id)">
|
||||
<p class="product-supplier mb-2">
|
||||
<p class="product-supplier">
|
||||
<small>
|
||||
<t t-esc="product_supplier_info[product.id]" />
|
||||
</small>
|
||||
</p>
|
||||
</t>
|
||||
<t t-if="product.origin_text">
|
||||
<p class="product-origin mb-2">
|
||||
<p class="product-origin">
|
||||
<small>
|
||||
<i class="fa fa-map-marker" aria-hidden="true" />
|
||||
<t t-out="product.origin_text" />
|
||||
|
|
@ -695,7 +707,7 @@
|
|||
</h6>
|
||||
<t t-set="tax_incl_base_unit_price" t-value="product_display_info.get(product.id, {}).get('base_unit_price', 0.0)" />
|
||||
<t t-if="tax_incl_base_unit_price and product.base_unit_name">
|
||||
<p class="product-unit-price text-muted" style="font-size: 0.85rem; margin-top: 0.25rem; margin-bottom: 0;">
|
||||
<p class="product-unit-price text-muted">
|
||||
<t t-esc="'%.2f' % tax_incl_base_unit_price" /> € / <t t-esc="product.base_unit_name" />
|
||||
</p>
|
||||
</t>
|
||||
|
|
@ -716,19 +728,24 @@
|
|||
t-att-data-out-of-stock="'true' if product.is_out_of_stock else 'false'"
|
||||
>
|
||||
<div class="qty-control">
|
||||
<label t-attf-for="qty_{{ product.id }}" class="sr-only">Quantity of <t t-esc="product.name" />
|
||||
<label t-attf-for="qty_{{ product.id }}" class="visually-hidden">Quantity of <t t-esc="product.name" />
|
||||
</label>
|
||||
<button class="qty-decrease" type="button" t-attf-data-product-id="{{ product.id }}" aria-label="Decrease quantity">
|
||||
<i class="fa fa-minus" />
|
||||
<i class="fa fa-minus" aria-hidden="true" />
|
||||
</button>
|
||||
<input type="number" t-attf-id="qty_{{ product.id }}" class="product-qty" name="quantity" t-attf-value="1" t-attf-min="{{ quantity_step }}" t-attf-step="{{ quantity_step }}" t-att-max="product_max_qty.get(product.id) if product_max_qty else None" t-att-data-max-qty="product_max_qty.get(product.id) if product_max_qty else None" />
|
||||
<button class="qty-increase" type="button" t-attf-data-product-id="{{ product.id }}" aria-label="Increase quantity">
|
||||
<i class="fa fa-plus" />
|
||||
<i class="fa fa-plus" aria-hidden="true" />
|
||||
</button>
|
||||
<!-- Out of stock stays focusable and clickable on purpose: the handler
|
||||
explains why instead of the button silently doing nothing.
|
||||
aria-disabled announces the state without removing it from the
|
||||
tab order (a real `disabled` would suppress the click event). -->
|
||||
<button
|
||||
class="add-to-cart-btn"
|
||||
type="button"
|
||||
t-att-data-out-of-stock="'true' if product.is_out_of_stock else 'false'"
|
||||
t-att-aria-disabled="'true' if product.is_out_of_stock else None"
|
||||
t-att-aria-label="out_of_stock_label if product.is_out_of_stock else add_to_cart_label"
|
||||
t-att-title="out_of_stock_label if product.is_out_of_stock else add_to_cart_label"
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue