Fix 10 known bugs: catalog, single product, and account pages (v0.1.5)

Catalog: page title via woocommerce_page_title(), breadcrumbs, category
template rename (underscore), 3-column grid, single chevron on sort.

Single product: variable form data attributes + disabled CSS class fix
(WC JS only toggles CSS classes, not HTML disabled attribute), dark mode
select specificity (0,5,1) to beat WC's (0,4,3) background shorthand,
gallery main image in thumbnail strip with empty URL guard, related/
upsells setup_postdata for correct global $product, grouped product
loop logic rewrite.

Account: downloads via wc_get_customer_available_downloads().

New: product-gallery.js, sanitize_title filter, wc_setup_product_data()
and wp_reset_postdata() Twig functions, product-thumbnails.html.twig
suppressor. Removed obsolete PLAN.md and SETUP.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 03:33:31 +01:00
parent 98359d4cfb
commit aaadac73c9
23 changed files with 340 additions and 922 deletions

1
.gitignore vendored
View File

@@ -34,3 +34,4 @@ releases/
# Docker runtime # Docker runtime
.env .env
KNOWN_BUGS.md

View File

@@ -2,6 +2,35 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.1.5] - 2026-03-01
### Fixed
- **Empty page title on catalog pages** (`header.html.twig`): Replaced `page_title` context variable (never passed by WC) with direct `fn('woocommerce_page_title', false)` call
- **Missing breadcrumbs on catalog pages** (`archive-product.php`): Added `woocommerce_breadcrumb()` call before shop loop header
- **Missing product categories on catalog pages** (`content-product-cat.html.twig`): Renamed template from hyphen (`content-product-cat`) to underscore (`content-product_cat`) to match WC's `wc_get_template()` filename convention
- **Product grid 4 columns instead of 3** (`functions.php`, `loop-start.html.twig`): Changed default columns from 4 to 3 for better card proportions with sidebar
- **Double chevron on sort dropdown** (`wc-bootstrap.css`): Removed conflicting `appearance: auto` rule; set `appearance: none` to let Bootstrap's `form-select` class handle the dropdown arrow exclusively
- **Variable product add-to-cart button stays disabled** (`variable.html.twig`, `variation-add-to-cart-button.html.twig`): Added missing `data-product_id` and `data-product_variations` attributes to form; replaced HTML `disabled` attribute on button with CSS classes `disabled wc-variation-selection-needed` (WC JS only toggles CSS classes, never removes the HTML attribute)
- **Variable product select white background in dark mode** (`wc-bootstrap.css`): Increased dark mode override specificity to `(0,5,1)` to beat WC's `.woocommerce div.product form.cart .variations select` at `(0,4,3)` which uses `background` shorthand; also overrides `background-image` for Bootstrap's dark-mode-aware chevron SVG
- **Product gallery missing main image in thumbnail strip** (`product-image.html.twig`): Prepend main image ID to gallery IDs using `[post_thumbnail_id]|merge(gallery_image_ids)` with active state on first thumbnail; added `{% if thumb_url %}` guard to skip invalid attachment IDs
- **Related/upsells products show same product repeated** (`related.html.twig`, `up-sells.html.twig`): Added `wc_setup_product_data()` call before each product render and `wp_reset_postdata()` after loop to set global `$product` correctly for WC hooks
- **Grouped product add-to-cart button/pricing broken** (`grouped.html.twig`): Rewrote template to compute `quantites_required` and `show_add_to_cart_button` in loop (matching WC PHP logic); moved hidden `add-to-cart` input outside conditional; added `has_options()` and `is_sold_individually()` checks
- **Downloads page empty** (`downloads.html.twig`): Replaced fragile `fn('WC').customer.get_downloadable_products()` chain with direct `fn('wc_get_customer_available_downloads', get_current_user_id())` call
### Added
- **Product gallery JS** (`product-gallery.js`): Vanilla JS click handler for thumbnail-to-main-image swap with active state highlighting and gallery fade-in
- **`wc_setup_product_data()` Twig function** (`WooCommerceExtension.php`): Sets `$GLOBALS['product']` and calls `setup_postdata()` for correct product context in Twig loops
- **`wp_reset_postdata` Twig function** (`WooCommerceExtension.php`): Restores global post state after product loops
- **`sanitize_title` Twig filter** (`WooCommerceExtension.php`): Matches WC PHP's lowercase attribute name handling for variation form data attributes
- **Product thumbnails suppressor** (`product-thumbnails.html.twig`): Empty template override to prevent WC's default full-size gallery images rendering below custom thumbnail row
### Changed
- **Whitelisted functions** (`WooCommerceExtension.php`): Added `woocommerce_page_title` and `wc_get_customer_available_downloads` to `ALLOWED_FUNCTIONS`
- **Removed obsolete files**: Deleted `PLAN.md` and `SETUP.md` (superseded by CLAUDE.md)
## [0.1.4] - 2026-03-01 ## [0.1.4] - 2026-03-01
### Security ### Security

View File

@@ -237,6 +237,24 @@ Recurring bugs and non-obvious behaviours discovered across sessions. **Read thi
- **WooCommerce float layout fights Bootstrap grid** -- `div.product div.images/summary` have `float:left/right; width:48%` in `woocommerce-layout.css`. Override with `float: none; width: 100%`. - **WooCommerce float layout fights Bootstrap grid** -- `div.product div.images/summary` have `float:left/right; width:48%` in `woocommerce-layout.css`. Override with `float: none; width: 100%`.
- **Bootstrap `g-*` gutters add negative top margin** -- `g-4` sets both `--bs-gutter-x` and `--bs-gutter-y`; the `.row` gets `margin-top: calc(-1 * var(--bs-gutter-y))` pulling it upward. Use `gx-*` for horizontal-only gutters when vertical gap isn't desired. - **Bootstrap `g-*` gutters add negative top margin** -- `g-4` sets both `--bs-gutter-x` and `--bs-gutter-y`; the `.row` gets `margin-top: calc(-1 * var(--bs-gutter-y))` pulling it upward. Use `gx-*` for horizontal-only gutters when vertical gap isn't desired.
### WooCommerce Variation JS
- **NEVER use HTML `disabled` attribute on the add-to-cart button.** WC's `add-to-cart-variation.js` only manages CSS classes (`disabled`, `wc-variation-selection-needed`) via jQuery `.addClass()`/`.removeClass()`. It never removes the HTML `disabled` attribute. The HTML attribute prevents click events from firing, making the button permanently unclickable.
- Use CSS classes `disabled wc-variation-selection-needed` instead. WC's `onAddToCart` handler checks `$button.is('.disabled')` (CSS class), not the HTML property.
- WC variation form requires `data-product_id` and `data-product_variations` on `<form class="variations_form">`. Without these, the JS doesn't initialize.
- Attribute select `name` and `data-attribute_name` must use `sanitize_title()` on the attribute name (lowercase, hyphens) to match the keys in the variation JSON data.
### WooCommerce Variation Select CSS Specificity
- WC's `.woocommerce div.product form.cart .variations select` at specificity `(0,4,3)` uses `background` shorthand — this resets BOTH `background-color` and `background-image`. A dark mode override must exceed `(0,4,3)` AND override `background-image` with `var(--bs-form-select-bg-img)` for Bootstrap's dark-mode-aware chevron SVG.
- Previous selector `[data-bs-theme="dark"] .variations .form-select` at `(0,3,0)` lost the specificity battle.
### WooCommerce Gallery Data Integrity
- `_product_image_gallery` post meta may contain IDs pointing to product variations or other non-attachment posts instead of image attachments. `wp_get_attachment_url()` returns empty/false for these.
- Always guard thumbnail rendering with `{% if thumb_url %}` after calling `wp_get_attachment_url(image_id)`.
- The main product image should be prepended to the gallery strip so users can switch back to it after viewing gallery images.
### Double Heading Prevention ### Double Heading Prevention
- Parent theme (`wp-bootstrap`) conditionally skips its `<h1>` when `post.title` is empty. - Parent theme (`wp-bootstrap`) conditionally skips its `<h1>` when `post.title` is empty.
@@ -327,10 +345,50 @@ The child theme inherits from `wp-bootstrap` via WordPress `Template: wp-bootstr
## Version History ## Version History
Current version: **v0.1.4** Current version: **v0.1.5**
## Session History ## Session History
### 2026-03-01 — v0.1.5 Fix 10 Known Bugs
**Scope:** Fixed all 10 bugs from KNOWN_BUGS.md — catalog page features (title, breadcrumbs, categories, filters, sort dropdown), single product fixes (variable form, gallery, related products, grouped products), and downloads page.
**Files changed (17):**
- `inc/Twig/WooCommerceExtension.php` — Added `woocommerce_page_title` and `wc_get_customer_available_downloads` to `ALLOWED_FUNCTIONS`; added `wc_setup_product_data()` method, `wp_reset_postdata` Twig function, and `sanitize_title` Twig filter
- `templates/single-product/add-to-cart/variable.html.twig` — Added `data-product_id` and `data-product_variations` attributes to form; applied `sanitize_title` filter on attribute names for variation matching
- `templates/single-product/add-to-cart/variation-add-to-cart-button.html.twig` — Removed HTML `disabled` attribute, replaced with CSS classes `disabled wc-variation-selection-needed` (WC JS only manages CSS classes, never the HTML attribute)
- `templates/single-product/product-image.html.twig` — Prepend main image to gallery thumbnail strip; added `{% if thumb_url %}` guard for invalid attachment IDs
- `templates/single-product/product-thumbnails.html.twig` — New empty override to suppress WC's default full-size gallery images
- `templates/single-product/related.html.twig` — Added `wc_setup_product_data()` before each product render + `wp_reset_postdata()` after loop
- `templates/single-product/up-sells.html.twig` — Same setup_postdata fix as related.html.twig
- `templates/single-product/add-to-cart/grouped.html.twig` — Rewrote to compute `quantites_required`/`show_add_to_cart_button` in loop (matching WC PHP); moved hidden input outside conditional; added `has_options()` and `is_sold_individually()` checks
- `templates/loop/header.html.twig` — Replaced `page_title` context variable with direct `fn('woocommerce_page_title', false)` call
- `woocommerce/archive-product.php` — Added `woocommerce_breadcrumb()` call before shop loop header
- `functions.php` — Changed loop columns from 4 to 3; added product gallery JS conditional enqueue
- `templates/loop/loop-start.html.twig` — Changed default columns from 4 to 3
- `assets/css/wc-bootstrap.css` — Replaced duplicate ordering select rules with `appearance: none`; increased dark mode variation select specificity to `(0,5,1)` to beat WC's `(0,4,3)` background shorthand
- `templates/content-product-cat.html.twig` — Renamed to `content-product_cat.html.twig` (WC uses underscore)
- `assets/js/product-gallery.js` — New: thumbnail click-to-swap, gallery fade-in, active state highlighting
- `templates/myaccount/downloads.html.twig` — Replaced `fn('WC').customer.get_downloadable_products()` with direct `fn('wc_get_customer_available_downloads', get_current_user_id())`
- `style.css` — Version bump 0.1.4 → 0.1.5
**Key decisions:**
- **`wc_setup_product_data()` over PHP bridge:** Adding a Twig function that sets `$GLOBALS['product']` + `setup_postdata()` is simpler and more maintainable than creating PHP bridge files for related/upsells rendering
- **`json_encode|esc_attr` over `wc_esc_json`:** Avoids adding a custom filter; `esc_attr` performs the same HTML entity encoding as `wc_esc_json`
- **3 columns default:** With sidebar taking `col-lg-3`, 3 product columns in `col-lg-9` gives better card proportions than 4
- **Vanilla JS gallery:** Lightweight click-to-swap handler instead of WC's built-in flexslider/photoswipe (which requires specific PHP setup for `wp_get_attachment_image_src` data attributes)
- **CSS class `disabled` over HTML `disabled` attribute:** WC's `add-to-cart-variation.js` `onShow`/`onHide` only toggle CSS classes (`disabled`, `wc-variation-selection-needed`). The `onAddToCart` handler checks `.is('.disabled')`. Using the HTML `disabled` attribute prevents click events entirely and WC JS never removes it.
- **CSS specificity `(0,5,1)` for dark mode selects:** WC's `.woocommerce div.product form.cart .variations select` at `(0,4,3)` uses `background` shorthand which resets `background-color` and `background-image`. Must exceed this specificity AND override `background-image` for Bootstrap's dark-mode SVG chevron.
**Key findings (WC variation JS):**
- WC's `add-to-cart-variation.js` event flow: `onChange``check_variations``findMatchingVariations``found_variation``onFoundVariation` (300ms setTimeout) → `show_variation``onShow` (removes CSS class only)
- `onShow` calls `$button.removeClass('disabled wc-variation-selection-needed')` — never touches the HTML `disabled` attribute/property
- `onAddToCart` checks `$button.is('.disabled')` — CSS class, not HTML attribute
- Gallery `_product_image_gallery` meta may contain IDs of product variations or posts instead of image attachments — always guard with `{% if thumb_url %}` after `wp_get_attachment_url()`
### 2026-03-01 — v0.1.4 Security Audit & Performance Fixes ### 2026-03-01 — v0.1.4 Security Audit & Performance Fixes
**Scope:** Cross-theme security audit (12 findings), all fixed. Covers fn() whitelist, notice data escaping, search query escaping, per-request context caching, shared render helper, and unused constant removal. **Scope:** Cross-theme security audit (12 findings), all fixed. Covers fn() whitelist, notice data escaping, search query escaping, per-request context caching, shared render helper, and unused constant removal.

652
PLAN.md
View File

@@ -1,652 +0,0 @@
# WooCommerce to Bootstrap 5 -- Template Conversion Plan
This document outlines the full plan for converting WooCommerce's default templates into Bootstrap 5 themed Twig overrides within the `wc-bootstrap` child theme.
## Scope
**Source:** 233 WooCommerce PHP/HTML templates in `wp-content/plugins/woocommerce/templates/`
**Target:** Twig template overrides in `wp-content/themes/wc-bootstrap/templates/`
**Design system:** Bootstrap 5.3 (loaded via parent theme `wp-bootstrap`)
The plugin is **read-only** -- we never modify WooCommerce source. All customisation happens through Twig template overrides via `TemplateOverride::prependPath()` and WordPress hooks in `functions.php`.
---
## Existing Scaffold (v0.1.0)
Already in place:
| File | Purpose |
| ---- | ------- |
| `templates/base.html.twig` | Conditional wrapper (`_theme_wrapped` detection) |
| `templates/layouts/account.html.twig` | My Account layout shell |
| `templates/layouts/archive.html.twig` | Product archive / search layout |
| `templates/layouts/form.html.twig` | Centered form layout (card + shadow) |
| `templates/layouts/page.html.twig` | Generic content page |
| `templates/layouts/single.html.twig` | Detail page (8+4 grid) |
| `templates/components/card.html.twig` | Reusable product/post card |
| `templates/components/pagination.html.twig` | Bootstrap pagination nav |
| `inc/TemplateOverride.php` | Twig loader hook |
| `functions.php` | Style chain, render filter, wrapping signal |
| `assets/css/wc-bootstrap.css` | Button/alert/dark-mode overrides |
---
## Conversion Phases
### Phase 1 -- Global & Notices (Foundation)
Templates that appear on every WooCommerce page. Must be done first because all other templates depend on them.
#### 1.1 Global Templates
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `global/wrapper-start.php` | `global/wrapper-start.html.twig` | `<div class="container my-4">` (or skip if `_theme_wrapped`) |
| `global/wrapper-end.php` | `global/wrapper-end.html.twig` | Closing `</div>` |
| `global/breadcrumb.php` | `global/breadcrumb.html.twig` | `nav[aria-label] > ol.breadcrumb > li.breadcrumb-item` |
| `global/sidebar.php` | `global/sidebar.html.twig` | `aside.col-lg-3` with `offcanvas-lg` for mobile |
| `global/quantity-input.php` | `global/quantity-input.html.twig` | `input-group` with `btn-outline-secondary` +/- buttons |
| `global/form-login.php` | `global/form-login.html.twig` | Extend `layouts/form.html.twig`, `form-control`, `form-label` |
#### 1.2 Notice Templates
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `notices/notice.php` | `notices/notice.html.twig` | `alert alert-info alert-dismissible fade show` |
| `notices/error.php` | `notices/error.html.twig` | `alert alert-danger alert-dismissible fade show` |
| `notices/success.php` | `notices/success.html.twig` | `alert alert-success alert-dismissible fade show` |
**Notes:**
- Notices must support multiple messages (WooCommerce passes arrays)
- Include `btn-close` with `data-bs-dismiss="alert"`
- Respect dark mode via Bootstrap's adaptive colour utilities
---
### Phase 2 -- Product Archive & Shop Loop (Storefront)
The shop page, category pages, and product grid. This is the main entry point for customers.
#### 2.1 Archive Container
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `archive-product.php` | `archive-product.html.twig` | Extend `layouts/archive.html.twig`; 3+9 grid (filter sidebar + products) |
| `content-product.php` | `content-product.html.twig` | Extend `components/card.html.twig`; product card in grid |
| `content-product-cat.php` | `content-product-cat.html.twig` | Category card with thumbnail |
| `product-searchform.php` | `product-searchform.html.twig` | `input-group` with `form-control` + `btn btn-outline-primary` |
#### 2.2 Loop Components
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `loop/loop-start.php` | `loop/loop-start.html.twig` | `<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">` |
| `loop/loop-end.php` | `loop/loop-end.html.twig` | `</div>` |
| `loop/header.php` | `loop/header.html.twig` | Flex row: result-count left, orderby right |
| `loop/result-count.php` | `loop/result-count.html.twig` | `<p class="text-body-secondary mb-0">` |
| `loop/orderby.php` | `loop/orderby.html.twig` | `form-select form-select-sm` in flex container |
| `loop/pagination.php` | `loop/pagination.html.twig` | Delegate to `components/pagination.html.twig` |
| `loop/no-products-found.php` | `loop/no-products-found.html.twig` | `alert alert-info` with icon |
| `loop/add-to-cart.php` | `loop/add-to-cart.html.twig` | `btn btn-primary btn-sm w-100` |
| `loop/price.php` | `loop/price.html.twig` | `<span class="fs-5 fw-semibold">` (sale: `text-decoration-line-through text-body-secondary` + `text-danger`) |
| `loop/rating.php` | `loop/rating.html.twig` | Bootstrap Icons stars (`bi-star-fill`, `bi-star`) |
| `loop/sale-flash.php` | `loop/sale-flash.html.twig` | `badge bg-danger position-absolute top-0 start-0 m-2` |
---
### Phase 3 -- Single Product Page
The product detail page, including gallery, variations, tabs, and related products.
#### 3.1 Product Layout
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `single-product/product-image.php` | `single-product/product-image.html.twig` | Main image + thumbnail row; `img-fluid rounded`; lightbox via PhotoSwipe or Bootstrap modal |
| `single-product/title.php` | `single-product/title.html.twig` | `<h1 class="mb-2">` |
| `single-product/price.php` | `single-product/price.html.twig` | `fs-3 fw-bold`; sale styling as in loop |
| `single-product/short-description.php` | `single-product/short-description.html.twig` | `<div class="lead text-body-secondary mb-3">` |
| `single-product/meta.php` | `single-product/meta.html.twig` | `<dl class="row mb-3">` with `col-sm-3`/`col-sm-9` pairs |
| `single-product/rating.php` | `single-product/rating.html.twig` | Stars + link to reviews tab |
| `single-product/stock.php` | `single-product/stock.html.twig` | `badge bg-success` (in stock) / `badge bg-danger` (out of stock) |
| `single-product/sale-flash.php` | `single-product/sale-flash.html.twig` | `badge bg-danger fs-6` |
| `single-product/share.php` | `single-product/share.html.twig` | `btn-group` with Bootstrap Icon buttons |
| `single-product/product-attributes.php` | `single-product/product-attributes.html.twig` | `table table-sm table-striped` |
| `single-product/related.php` | `single-product/related.html.twig` | Section heading + `row row-cols-2 row-cols-lg-4 g-4` of cards |
| `single-product/up-sells.php` | `single-product/up-sells.html.twig` | Same grid as related |
#### 3.2 Add to Cart Forms
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `single-product/add-to-cart/simple.php` | `single-product/add-to-cart/simple.html.twig` | `input-group` (quantity) + `btn btn-primary btn-lg` |
| `single-product/add-to-cart/variable.php` | `single-product/add-to-cart/variable.html.twig` | `form-select` per attribute + quantity + button |
| `single-product/add-to-cart/grouped.php` | `single-product/add-to-cart/grouped.html.twig` | `table table-borderless` with quantity inputs per item |
| `single-product/add-to-cart/external.php` | `single-product/add-to-cart/external.html.twig` | `btn btn-outline-primary btn-lg` with external link icon |
| `single-product/add-to-cart/variation.php` | `single-product/add-to-cart/variation.html.twig` | Variation data container (hidden, JS-driven) |
| `single-product/add-to-cart/variation-add-to-cart-button.php` | `single-product/add-to-cart/variation-add-to-cart-button.html.twig` | Same as simple but variation-aware |
#### 3.3 Product Tabs
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `single-product/tabs/tabs.php` | `single-product/tabs/tabs.html.twig` | `nav nav-tabs` + `tab-content` with `tab-pane fade` |
| `single-product/tabs/description.php` | `single-product/tabs/description.html.twig` | `tab-pane` with prose content |
| `single-product/tabs/additional-information.php` | `single-product/tabs/additional-information.html.twig` | `tab-pane` with attributes table |
---
### Phase 4 -- Cart
The cart page with item management, shipping calculator, and totals.
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `cart/cart.php` | `cart/cart.html.twig` | `row`: `col-lg-8` (items table) + `col-lg-4` (totals sidebar) |
| `cart/cart-empty.php` | `cart/cart-empty.html.twig` | Centred `alert alert-info` + `btn btn-primary` to shop |
| `cart/cart-item-data.php` | `cart/cart-item-data.html.twig` | `<dl class="small text-body-secondary mb-0">` |
| `cart/cart-totals.php` | `cart/cart-totals.html.twig` | `card` with `list-group list-group-flush` rows |
| `cart/cart-shipping.php` | `cart/cart-shipping.html.twig` | `form-check` radio buttons per shipping method |
| `cart/cross-sells.php` | `cart/cross-sells.html.twig` | Section below cart; `row row-cols-2 row-cols-md-4 g-3` of small cards |
| `cart/mini-cart.php` | `cart/mini-cart.html.twig` | `offcanvas offcanvas-end` slide-in panel |
| `cart/proceed-to-checkout-button.php` | `cart/proceed-to-checkout-button.html.twig` | `btn btn-primary btn-lg w-100` |
| `cart/shipping-calculator.php` | `cart/shipping-calculator.html.twig` | Collapsible form (`collapse`) with `form-select` for country/state |
**Cart Table Structure:**
```html
<div class="table-responsive">
<table class="table align-middle">
<thead class="table-light">
<tr>
<th>Product</th>
<th>Price</th>
<th>Quantity</th>
<th>Subtotal</th>
<th></th><!-- Remove -->
</tr>
</thead>
<tbody><!-- cart items --></tbody>
</table>
</div>
```
---
### Phase 5 -- Checkout
The checkout flow including billing/shipping forms, order review, and payment.
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `checkout/form-checkout.php` | `checkout/form-checkout.html.twig` | `row`: `col-lg-7` (forms) + `col-lg-5` (order review, sticky) |
| `checkout/form-billing.php` | `checkout/form-billing.html.twig` | `card mb-4` with `form-control`, `form-label`, `form-select` fields |
| `checkout/form-shipping.php` | `checkout/form-shipping.html.twig` | Same card style; `form-check` for "ship to different address" toggle |
| `checkout/form-coupon.php` | `checkout/form-coupon.html.twig` | `input-group` inline: `form-control` + `btn btn-outline-secondary` |
| `checkout/form-login.php` | `checkout/form-login.html.twig` | Collapsible (`collapse`) login form above checkout |
| `checkout/review-order.php` | `checkout/review-order.html.twig` | `card` with `table table-sm` for items + `list-group` for totals |
| `checkout/payment.php` | `checkout/payment.html.twig` | `list-group` of `form-check` radio items per gateway; `btn btn-primary btn-lg w-100` place order |
| `checkout/payment-method.php` | `checkout/payment-method.html.twig` | `list-group-item` with radio + label + description collapse |
| `checkout/terms.php` | `checkout/terms.html.twig` | `form-check` checkbox with link to terms page |
| `checkout/thankyou.php` | `checkout/thankyou.html.twig` | `alert alert-success` + order details card |
| `checkout/order-received.php` | `checkout/order-received.html.twig` | Confirmation message with order summary |
| `checkout/cart-errors.php` | `checkout/cart-errors.html.twig` | `alert alert-danger` list |
---
### Phase 6 -- My Account Dashboard
The customer account area with orders, addresses, and account management.
#### 6.1 Account Shell
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `myaccount/my-account.php` | `myaccount/my-account.html.twig` | Extend `layouts/account.html.twig`; 3+9 grid (nav + content) |
| `myaccount/navigation.php` | `myaccount/navigation.html.twig` | `list-group` in sticky sidebar (desktop) / `nav nav-pills` (mobile) |
| `myaccount/dashboard.php` | `myaccount/dashboard.html.twig` | Welcome card + quick links as `row row-cols-1 row-cols-md-3 g-3` |
#### 6.2 Orders
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `myaccount/orders.php` | `myaccount/orders.html.twig` | `table-responsive` > `table table-hover` with status badges |
| `myaccount/view-order.php` | `myaccount/view-order.html.twig` | Order details card + address cards row |
#### 6.3 Addresses & Account
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `myaccount/my-address.php` | `myaccount/my-address.html.twig` | `row row-cols-1 row-cols-md-2 g-4`; billing + shipping cards |
| `myaccount/form-edit-address.php` | `myaccount/form-edit-address.html.twig` | `card` form with `form-control`, `form-select` |
| `myaccount/form-edit-account.php` | `myaccount/form-edit-account.html.twig` | `card` form for name, email, password change |
| `myaccount/downloads.php` | `myaccount/downloads.html.twig` | `table table-striped` with download links |
| `myaccount/payment-methods.php` | `myaccount/payment-methods.html.twig` | `list-group` of saved methods with delete button |
| `myaccount/form-add-payment-method.php` | `myaccount/form-add-payment-method.html.twig` | `card` form |
#### 6.4 Authentication
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `myaccount/form-login.php` | `myaccount/form-login.html.twig` | `row row-cols-1 row-cols-md-2 g-4`: login card + register card side-by-side |
| `myaccount/form-lost-password.php` | `myaccount/form-lost-password.html.twig` | Extend `layouts/form.html.twig`; centered card |
| `myaccount/form-reset-password.php` | `myaccount/form-reset-password.html.twig` | Extend `layouts/form.html.twig`; centered card |
| `myaccount/lost-password-confirmation.php` | `myaccount/lost-password-confirmation.html.twig` | `alert alert-success` |
---
### Phase 7 -- Order Details
Order display used on thank-you page, view-order, and emails.
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `order/order-details.php` | `order/order-details.html.twig` | `card` > `table-responsive` > `table` |
| `order/order-details-item.php` | `order/order-details-item.html.twig` | `<tr>` with product image thumbnail, name, quantity, price |
| `order/order-details-customer.php` | `order/order-details-customer.html.twig` | `row row-cols-1 row-cols-md-2`: billing + shipping address cards |
| `order/form-tracking.php` | `order/form-tracking.html.twig` | Extend `layouts/form.html.twig`; order ID + email fields |
| `order/order-again.php` | `order/order-again.html.twig` | `btn btn-outline-primary` |
---
### Phase 8 -- Email Templates
Email templates require special treatment: no external CSS, no Bootstrap JS, inline styles only.
**Strategy:** Use Bootstrap's colour palette as CSS custom properties mapped to inline styles. Prefix all classes with `wc-email-` to avoid email client conflicts (per architecture decision in CLAUDE.md).
#### 8.1 Email Shell
| WooCommerce Template | Theme Override | Purpose |
| -------------------- | -------------- | ------- |
| `emails/email-header.php` | `emails/email-header.html.twig` | Table-based header with logo, store name |
| `emails/email-footer.php` | `emails/email-footer.html.twig` | Table-based footer with store info |
| `emails/email-styles.php` | `emails/email-styles.html.twig` | Inline CSS aligned to Bootstrap colour palette |
#### 8.2 Email Content Components
| WooCommerce Template | Theme Override | Purpose |
| -------------------- | -------------- | ------- |
| `emails/email-order-details.php` | `emails/email-order-details.html.twig` | Order items table |
| `emails/email-order-items.php` | `emails/email-order-items.html.twig` | Single order item row |
| `emails/email-customer-details.php` | `emails/email-customer-details.html.twig` | Customer info section |
| `emails/email-addresses.php` | `emails/email-addresses.html.twig` | Billing + shipping addresses |
| `emails/email-downloads.php` | `emails/email-downloads.html.twig` | Downloads table |
#### 8.3 Transactional Emails (Priority Order)
High-traffic emails first:
1. `emails/customer-processing-order.php` -- Order confirmation
2. `emails/customer-completed-order.php` -- Order shipped/complete
3. `emails/customer-on-hold-order.php` -- Awaiting payment
4. `emails/customer-new-account.php` -- Welcome email
5. `emails/customer-reset-password.php` -- Password reset
6. `emails/customer-invoice.php` -- Invoice/receipt
7. `emails/customer-note.php` -- Order note
8. `emails/customer-refunded-order.php` -- Refund notification
9. `emails/customer-cancelled-order.php` -- Cancellation
10. `emails/customer-failed-order.php` -- Payment failure
Admin emails:
11. `emails/admin-new-order.php` -- New order notification
12. `emails/admin-cancelled-order.php` -- Cancellation alert
13. `emails/admin-failed-order.php` -- Payment failure alert
#### 8.4 Plain Text Emails
The `emails/plain/` directory contains plain-text variants. These need minimal conversion -- mainly ensuring consistent formatting and branding text. Lower priority; can be done as a batch after HTML emails.
---
### Phase 9 -- Supplementary Templates
Lower-traffic pages and edge cases.
#### 9.1 Brands
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `brands/brand-description.php` | `brands/brand-description.html.twig` | `<div class="mb-4">` with prose |
| `brands/taxonomy-product_brand.php` | `brands/taxonomy-product_brand.html.twig` | Extend `layouts/archive.html.twig` |
| `brands/shortcodes/brands-a-z.php` | `brands/shortcodes/brands-a-z.html.twig` | Letter nav + `row` grid of brand cards |
| `brands/shortcodes/single-brand.php` | `brands/shortcodes/single-brand.html.twig` | Brand card |
#### 9.2 Auth (OAuth)
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `auth/form-login.php` | `auth/form-login.html.twig` | Extend `layouts/form.html.twig` |
| `auth/form-grant-access.php` | `auth/form-grant-access.html.twig` | `card` with scope list + approve/deny buttons |
#### 9.3 Back-in-Stock
| WooCommerce Template | Theme Override | Bootstrap 5 Mapping |
| -------------------- | -------------- | -------------------- |
| `single-product/back-in-stock-form.php` | `single-product/back-in-stock-form.html.twig` | `card card-body` with email `input-group` |
---
## CSS Strategy
### File: `assets/css/wc-bootstrap.css`
Additions needed per phase:
| Phase | CSS Additions |
| ----- | ------------- |
| 1 | Quantity input-group sizing; notice icon spacing |
| 2 | Product card hover lift effect; sale badge positioning; star rating colours |
| 3 | Product gallery thumbnail grid; tab content transitions; variation swatches |
| 4 | Cart item thumbnail sizing; mini-cart offcanvas width |
| 5 | Checkout sticky sidebar offset; payment method description animation |
| 6 | Account navigation active state; order status badge colours |
| 8 | Email inline styles (separate `wc-email-*` classes, no Bootstrap dependency) |
### WooCommerce CSS Overrides
WooCommerce ships its own CSS that conflicts with Bootstrap. Key overrides:
```css
/* Reset WooCommerce grid in favour of Bootstrap */
.woocommerce ul.products { display: contents; }
/* Ensure Bootstrap form styles take precedence */
.woocommerce .form-row input.input-text { /* inherit from .form-control */ }
/* Price styling consistency */
.woocommerce .price del { @extend .text-decoration-line-through, .text-body-secondary; }
.woocommerce .price ins { text-decoration: none; @extend .text-danger, .fw-bold; }
```
### Dark Mode
All templates use Bootstrap's adaptive utilities (`bg-body-tertiary`, `text-body-secondary`, `border-*`) which automatically adapt via `data-bs-theme="dark"`. No manual dark-mode CSS should be needed except for:
- Custom sale badge colours
- Star rating fill colour
- Email templates (separate palette)
---
## JavaScript Strategy
Minimal custom JS. Bootstrap 5 JS (from parent theme) handles:
- Offcanvas (mini-cart, mobile sidebar)
- Collapse (shipping calculator, coupon form, login form)
- Tabs (product tabs)
- Modal (image lightbox, if replacing PhotoSwipe)
- Tooltips/popovers (optional)
- Alert dismiss (notices)
Custom JS needed for:
| Feature | Location | Purpose |
| ------- | -------- | ------- |
| Quantity +/- buttons | `assets/js/quantity.js` | Increment/decrement input value, trigger `change` event for WC JS |
| Variation swatches | `assets/js/variations.js` | Sync Bootstrap form-select with WC variation JS (if needed) |
| Mini-cart AJAX | `assets/js/mini-cart.js` | Update offcanvas cart count after add-to-cart |
| Sticky cart totals | Inline or CSS-only | `position: sticky; top: 1rem` on checkout/cart sidebar |
---
## Template Naming Convention
All theme overrides mirror the plugin's directory structure exactly:
```txt
woocommerce/templates/cart/cart.php
--> wc-bootstrap/templates/cart/cart.html.twig
woocommerce/templates/single-product/tabs/tabs.php
--> wc-bootstrap/templates/single-product/tabs/tabs.html.twig
```
---
## Component Reuse Map
Common Bootstrap patterns extracted into reusable Twig components:
| Component | File | Used By |
| --------- | ---- | ------- |
| Product card | `components/card.html.twig` | Archive, related, cross-sells, up-sells |
| Pagination | `components/pagination.html.twig` | Archive, orders, downloads |
| Price display | `components/price.html.twig` | Loop, single product, cart, order details |
| Star rating | `components/rating.html.twig` | Loop, single product |
| Address card | `components/address-card.html.twig` | My Account addresses, order details, checkout |
| Status badge | `components/status-badge.html.twig` | Orders list, view order |
| Quantity input | `components/quantity-input.html.twig` | Cart, single product add-to-cart |
| Form field | `components/form-field.html.twig` | Checkout, account forms, address forms |
---
## Order Status Badge Colours
| Status | Bootstrap Class |
| ------ | --------------- |
| Pending | `bg-warning text-dark` |
| Processing | `bg-info text-dark` |
| On Hold | `bg-secondary` |
| Completed | `bg-success` |
| Cancelled | `bg-danger` |
| Refunded | `bg-dark` |
| Failed | `bg-danger` |
---
## Accessibility Checklist
Every template must meet:
- [ ] Semantic HTML (`<nav>`, `<main>`, `<article>`, `<section>`, `<aside>`)
- [ ] ARIA labels on interactive elements (`aria-label`, `aria-expanded`, `aria-current`)
- [ ] `sr-only` / `visually-hidden` for icon-only buttons
- [ ] Focus-visible styles (Bootstrap default)
- [ ] Keyboard navigation for custom widgets (quantity buttons, variation selects)
- [ ] Colour contrast ratio >= 4.5:1 (Bootstrap defaults meet this)
- [ ] Form labels associated with inputs (`for`/`id` pairs)
- [ ] Error messages linked to fields via `aria-describedby`
---
## Testing Strategy
Since Docker is not yet available, testing follows a manual / staged approach:
### Per-Template Checklist
1. **Visual review** -- Load page in browser, verify Bootstrap 5 styling applied
2. **Responsive check** -- Test at 320px, 768px, 1024px, 1440px breakpoints
3. **Dark mode** -- Toggle `data-bs-theme="dark"`, verify all elements adapt
4. **Functionality** -- All forms submit, AJAX works, WC JS hooks fire
5. **Accessibility** -- Tab through page, check screen reader output
6. **No regressions** -- Existing WooCommerce features still work (add to cart, checkout flow, etc.)
### Integration Test Flow
Once multiple phases are complete, test the full customer journey:
1. Browse shop archive --> filter --> view product
2. Select variation --> add to cart --> view cart
3. Update quantity --> apply coupon --> proceed to checkout
4. Fill billing/shipping --> select payment --> place order
5. View thank-you page --> check email
6. Log in to My Account --> view order --> manage addresses
---
## Estimated Template Count by Phase
| Phase | Templates | Priority |
| ----- | --------- | -------- |
| 1 -- Global & Notices | 9 | Critical |
| 2 -- Archive & Loop | 15 | Critical |
| 3 -- Single Product | 21 | Critical |
| 4 -- Cart | 9 | High |
| 5 -- Checkout | 12 | High |
| 6 -- My Account | 17 | Medium |
| 7 -- Order Details | 5 | Medium |
| 8 -- Emails | ~30 | Low |
| 9 -- Supplementary | 7 | Low |
| **Total** | **~125** | |
New reusable components to create: **8**
**Note:** Not all 233 WooCommerce templates need overrides. Block templates (`templates/templates/`, `parts/`), block-notice variants, and email plain-text variants are excluded or deferred. The ~125 count covers all customer-facing PHP templates that produce visible HTML.
---
## Dependencies & Blockers
| Dependency | Status | Impact |
| ---------- | ------ | ------ |
| Docker environment | Not set up | Cannot test WP-CLI, i18n extraction; use local PHP server |
| WooCommerce-to-Twig bridge | Implemented | `TemplateOverride` intercepts `wc_get_template()` via before/after hooks; `WooCommerceExtension` provides ~50 Twig functions and 7 filters |
| Parent theme Twig functions | Available | `__()`, `esc_html()`, `esc_attr()`, etc. registered by `TwigService` |
| Bootstrap 5 CSS/JS | Available | Loaded by parent theme `wp-bootstrap` |
| Composer autoloader | Available | PSR-4 for `WcBootstrap\` namespace |
---
## File Checklist
Track completion per file. Mark with `[x]` when done.
### Phase 1 -- Global & Notices
- [x] `global/wrapper-start.html.twig`
- [x] `global/wrapper-end.html.twig`
- [x] `global/breadcrumb.html.twig`
- [x] `global/sidebar.html.twig`
- [x] `global/quantity-input.html.twig`
- [x] `global/form-login.html.twig`
- [x] `notices/notice.html.twig`
- [x] `notices/error.html.twig`
- [x] `notices/success.html.twig`
### Phase 2 -- Archive & Loop
- [x] `archive-product.html.twig`
- [x] `content-product.html.twig`
- [x] `content-product-cat.html.twig`
- [x] `product-searchform.html.twig`
- [x] `loop/loop-start.html.twig`
- [x] `loop/loop-end.html.twig`
- [x] `loop/header.html.twig`
- [x] `loop/result-count.html.twig`
- [x] `loop/orderby.html.twig`
- [x] `loop/pagination.html.twig`
- [x] `loop/no-products-found.html.twig`
- [x] `loop/add-to-cart.html.twig`
- [x] `loop/price.html.twig`
- [x] `loop/rating.html.twig`
- [x] `loop/sale-flash.html.twig`
### Phase 3 -- Single Product
- [x] `single-product/product-image.html.twig`
- [x] `single-product/title.html.twig`
- [x] `single-product/price.html.twig`
- [x] `single-product/short-description.html.twig`
- [x] `single-product/meta.html.twig`
- [x] `single-product/rating.html.twig`
- [x] `single-product/stock.html.twig`
- [x] `single-product/sale-flash.html.twig`
- [x] `single-product/share.html.twig`
- [x] `single-product/product-attributes.html.twig`
- [x] `single-product/related.html.twig`
- [x] `single-product/up-sells.html.twig`
- [x] `single-product/add-to-cart/simple.html.twig`
- [x] `single-product/add-to-cart/variable.html.twig`
- [x] `single-product/add-to-cart/grouped.html.twig`
- [x] `single-product/add-to-cart/external.html.twig`
- [x] `single-product/add-to-cart/variation.html.twig`
- [x] `single-product/add-to-cart/variation-add-to-cart-button.html.twig`
- [x] `single-product/tabs/tabs.html.twig`
- [x] `single-product/tabs/description.html.twig`
- [x] `single-product/tabs/additional-information.html.twig`
### Phase 4 -- Cart
- [x] `cart/cart.html.twig`
- [x] `cart/cart-empty.html.twig`
- [x] `cart/cart-item-data.html.twig`
- [x] `cart/cart-totals.html.twig`
- [x] `cart/cart-shipping.html.twig`
- [x] `cart/cross-sells.html.twig`
- [x] `cart/mini-cart.html.twig`
- [x] `cart/proceed-to-checkout-button.html.twig`
- [x] `cart/shipping-calculator.html.twig`
### Phase 5 -- Checkout
- [x] `checkout/form-checkout.html.twig`
- [x] `checkout/form-billing.html.twig`
- [x] `checkout/form-shipping.html.twig`
- [x] `checkout/form-coupon.html.twig`
- [x] `checkout/form-login.html.twig`
- [x] `checkout/review-order.html.twig`
- [x] `checkout/payment.html.twig`
- [x] `checkout/payment-method.html.twig`
- [x] `checkout/terms.html.twig`
- [x] `checkout/thankyou.html.twig`
- [x] `checkout/order-received.html.twig`
- [x] `checkout/cart-errors.html.twig`
### Phase 6 -- My Account
- [x] `myaccount/my-account.html.twig`
- [x] `myaccount/navigation.html.twig`
- [x] `myaccount/dashboard.html.twig`
- [x] `myaccount/orders.html.twig`
- [x] `myaccount/view-order.html.twig`
- [x] `myaccount/my-address.html.twig`
- [x] `myaccount/form-edit-address.html.twig`
- [x] `myaccount/form-edit-account.html.twig`
- [x] `myaccount/downloads.html.twig`
- [x] `myaccount/payment-methods.html.twig`
- [x] `myaccount/form-add-payment-method.html.twig`
- [x] `myaccount/form-login.html.twig`
- [x] `myaccount/form-lost-password.html.twig`
- [x] `myaccount/form-reset-password.html.twig`
- [x] `myaccount/lost-password-confirmation.html.twig`
### Phase 7 -- Order Details
- [x] `order/order-details.html.twig`
- [x] `order/order-details-item.html.twig`
- [x] `order/order-details-customer.html.twig`
- [x] `order/form-tracking.html.twig`
- [x] `order/order-again.html.twig`
### Phase 8 -- Emails (SKIPPED)
Skipped: WooCommerce email templates use `wc_get_template_html()` which bypasses
the Twig rendering pipeline. Email overrides would require traditional PHP files
in `woocommerce/emails/`, breaking the Twig-only pattern. Default WooCommerce
email templates are sufficient; email customization can be handled via plugins
(e.g., Kadence WooCommerce Email Designer) or the WooCommerce block email editor.
### Phase 9 -- Supplementary
- [x] `brands/brand-description.html.twig`
- [x] `brands/taxonomy-product_brand.html.twig`
- [x] `brands/shortcodes/brands-a-z.html.twig`
- [x] `brands/shortcodes/single-brand.html.twig`
- [x] `auth/form-login.html.twig`
- [x] `auth/form-grant-access.html.twig`
- [x] `single-product/back-in-stock-form.html.twig`
### Reusable Components
- [x] `components/price.html.twig`
- [x] `components/rating.html.twig`
- [x] `components/address-card.html.twig`
- [x] `components/status-badge.html.twig`
- [x] `components/quantity-input.html.twig`
- [x] `components/form-field.html.twig`

View File

@@ -57,7 +57,9 @@ The bridge hooks into WooCommerce's `woocommerce_before_template_part` and `wooc
wc-bootstrap/ wc-bootstrap/
├── assets/ ├── assets/
│ ├── css/wc-bootstrap.css # Bootstrap override styles │ ├── css/wc-bootstrap.css # Bootstrap override styles
│ └── js/quantity.js # Quantity +/- button handler │ └── js/
│ ├── product-gallery.js # Thumbnail click-to-swap gallery handler
│ └── quantity.js # Quantity +/- button handler
├── inc/ ├── inc/
│ ├── TemplateOverride.php # WC template interception (before/after hooks) │ ├── TemplateOverride.php # WC template interception (before/after hooks)
│ └── Twig/ │ └── Twig/
@@ -68,7 +70,7 @@ wc-bootstrap/
│ ├── layouts/ # Page-type layouts (account, archive, form, page, single) │ ├── layouts/ # Page-type layouts (account, archive, form, page, single)
│ ├── archive-product.html.twig │ ├── archive-product.html.twig
│ ├── content-product.html.twig │ ├── content-product.html.twig
│ ├── content-product-cat.html.twig │ ├── content-product_cat.html.twig
│ ├── product-searchform.html.twig │ ├── product-searchform.html.twig
│ ├── auth/ │ ├── auth/
│ ├── brands/ │ ├── brands/
@@ -141,8 +143,8 @@ for po in languages/wc-bootstrap-*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
Releases are automated via Gitea Actions. Push a tag matching `vX.X.X` to trigger a release build. Releases are automated via Gitea Actions. Push a tag matching `vX.X.X` to trigger a release build.
```bash ```bash
git tag -a v0.1.3 -m "Version 0.1.3 - Add theme screenshot" git tag -a v0.1.5 -m "Version 0.1.5 - Fix 10 known bugs"
git push origin v0.1.3 git push origin v0.1.5
``` ```
## License ## License

182
SETUP.md
View File

@@ -1,182 +0,0 @@
# WP Bootstrap Child Theme Template -- Setup Guide
This template creates a Bootstrap 5 child theme for the `wp-bootstrap` parent theme
that overrides a WordPress plugin's Twig templates with Bootstrap 5 markup.
## Placeholders Reference
Search and replace these placeholders across all files when instantiating a new project:
### Project Identity
| Placeholder | Description | Example |
| --- | --- | --- |
| `WooCommerce Bootstrap` | Human-readable theme name | `WP JobRoom Theme` |
| `wc-bootstrap` | WordPress theme directory name (kebab-case) | `wp-jobroom-theme` |
| `wc-bootstrap` | WordPress text domain for i18n | `wp-jobroom-theme` |
| `WC_BOOTSTRAP` | PHP constant prefix (UPPER_SNAKE_CASE) | `WP_JOBROOM_THEME` |
| `wc_bootstrap` | PHP function prefix (lower_snake_case) | `wp_jobroom_theme` |
| `WcBootstrap` | PSR-4 PHP namespace | `WPJobroomTheme` |
| `magdev` | Composer vendor name | `magdev` |
### Plugin (upstream dependency)
| Placeholder | Description | Example |
| --- | --- | --- |
| `WooCommerce` | Human-readable plugin name | `WP JobRoom` |
| `woocommerce` | Plugin directory/handle name | `wp-jobroom` |
| `woocommerce` | Plugin text domain | `wp-jobroom` |
| `Magdev\Woocommerce` | Plugin PHP namespace | `Magdev\WpJobroom` |
| `https://github.com/woocommerce/woocommerce.git` | Plugin repository URL | `https://src.example.com/user/wp-myplugin` |
| `woocommerce_render_page` | Plugin's render page filter name | `wp_jobroom_render_page` |
| `woocommerce_is_theme_wrapped` | Plugin's is-wrapped filter name | `wp_jobroom_is_theme_wrapped` |
### Author & Repository
| Placeholder | Description | Example |
| --- | --- | --- |
| `Marco Grätsch` | Author's full name | `Marco Graetsch` |
| `magdev3.0@gmail.com` | Author's email | `magdev3.0@gmail.com` |
| `https://src.bundespruefstelle.ch/magdev` | Author's website URL | `https://src.example.com/user` |
| `ssh://git@src.bundespruefstelle.ch:2022/magdev/wc-bootstrap.git` | Theme repository URL | `https://src.example.com/user/wp-myplugin-theme` |
| `ssh://git@src.bundespruefstelle.ch:2022/magdev/wc-bootstrap.git/issues` | Issues tracker URL | `https://src.example.com/user/wp-myplugin-theme/issues` |
| `https://src.bundespruefstelle.ch/magdev/wp-bootstrap` | Parent theme repository URL | `https://src.example.com/user/wp-bootstrap` |
### Infrastructure
| Placeholder | Description | Example |
| --- | --- | --- |
| `woocommerce-wordpress` | Docker container name for WP-CLI | `myplugin-wordpress` |
## Files to Customize
After replacing placeholders, these files need project-specific customization:
### Required Changes
1. **`inc/TemplateOverride.php`** -- Update the `use` import to match your plugin's
actual Template class path (line: `use Magdev\Woocommerce\Frontend\Template;`)
2. **`functions.php`** -- Review and adapt:
- CSS dependency array in `enqueue_styles()` -- add plugin style handles
- Filter hooks -- match your plugin's actual filter names
- Remove or adapt features you don't need (sticky header, register page, etc.)
3. **`composer.json`** -- Verify namespace mapping matches your `inc/` directory structure
4. **`style.css`** -- Update the header to match your theme's specifics
5. **`.gitea/workflows/release.yml`** -- Update theme name in release title
### Optional Additions
6. **`inc/ProfileMenu.php`** -- Create if your plugin has a navigation menu that needs
Bootstrap 5 conversion (use wp-jobroom-theme's ProfileMenu.php as reference)
7. **`assets/css/theme-overrides.css`** -- Rename to `wc-bootstrap.css` and add
plugin-specific CSS class overrides mapping to Bootstrap 5
8. **`templates/`** -- Add template overrides mirroring your plugin's template structure
## Architecture Overview
```txt
wp-bootstrap (parent theme, Bootstrap 5 FSE + Twig rendering)
+-- wc-bootstrap (child theme, overrides plugin Twig templates with Bootstrap 5)
+-- woocommerce (plugin, provides post types, logic, base Twig templates)
```
### Template Override Flow
1. Plugin registers Twig `FilesystemLoader` with its `templates/` directory
2. Child theme's `TemplateOverride` hooks `init` at priority 20 (after plugin at 0)
3. `prependPath()` adds child theme's `templates/` before plugin's
4. Twig resolves templates: child theme first, plugin as fallback
### Page Rendering Flow
1. Plugin's Router catches a request and renders plugin content via Twig
2. Plugin fires `woocommerce_render_page` filter with pre-rendered HTML
3. Child theme's `render_page()` intercepts, delegates to parent theme's TwigService
4. Parent theme wraps content in its page shell (header, footer, navigation)
5. `_theme_wrapped` context flag tells plugin templates to suppress their own wrapper
### CSS Cascade
```txt
1. wp-bootstrap (parent) -- Bootstrap 5 framework
2. woocommerce -- Plugin's custom CSS
3. wc-bootstrap-style -- Child theme style.css (metadata)
4. wc-bootstrap-overrides -- Bootstrap 5 overrides for plugin classes
```
## Plugin Requirements
For this template to work, the plugin must:
1. **Use Twig with `FilesystemLoader`** -- for template overriding via `prependPath()`
2. **Expose a singleton Template class** -- so the child theme can access the Twig environment
3. **Fire a render page filter** -- so the child theme can delegate rendering to the parent theme
4. **Fire an is-wrapped filter** -- so plugin templates know to suppress their outer wrapper
5. **Register its styles with a known handle** -- for CSS dependency chain ordering
## Quick Start
```bash
# 1. Clone/copy this template
cp -r wp-theme-template/ wp-content/themes/my-new-theme/
# 2. Replace all placeholders (example using sed)
cd wp-content/themes/my-new-theme/
find . -type f \( -name "*.php" -o -name "*.md" -o -name "*.css" -o -name "*.json" -o -name "*.yml" -o -name "*.twig" \) \
-exec sed -i 's/WooCommerce Bootstrap/My Plugin Theme/g' {} +
# ... repeat for all placeholders
# 3. Rename the CSS override file
mv assets/css/theme-overrides.css assets/css/my-plugin-theme.css
# 4. Install dependencies
composer install
# 5. Initialize git
git init && git checkout -b dev
git add . && git commit -m "Initial theme scaffold"
# 6. Start overriding plugin templates in templates/
```
## Common Patterns
### Adding a New Template Override
1. Find the plugin's template in `plugins/woocommerce/templates/path/file.html.twig`
2. Create the same path in `themes/wc-bootstrap/templates/path/file.html.twig`
3. Convert HTML to Bootstrap 5 components
4. Preserve all context variables and block names from the original
5. Preserve plugin JS-bound CSS classes (repeater fields, interactive widgets)
### Layout Hierarchy
```txt
base.html.twig -- Notifications, breadcrumbs, container wrapping
+-- layouts/page.html.twig -- Standard content pages
+-- layouts/form.html.twig -- Auth and edit forms (centered card)
+-- layouts/single.html.twig -- Detail pages (8+4 with sidebar)
+-- layouts/archive.html.twig -- Search/list pages (3+9 with filters)
+-- layouts/account.html.twig -- User account pages
```
### Bootstrap 5 Component Mappings
| Plugin Pattern | Bootstrap 5 Equivalent |
| --- | --- |
| Custom button classes | `btn btn-primary`, `btn-outline-*` |
| Custom alert/notification | `alert alert-*` with `alert-dismissible` |
| Custom grid system | `row`, `col-*`, `row-cols-*` |
| Custom form fields | `form-control`, `form-floating`, `form-select` |
| Custom cards | `card`, `card-body`, `card-title` |
| Custom modal/dialog | `modal`, `modal-dialog`, `modal-content` |
| Custom dropdown | `dropdown`, `dropdown-menu`, `data-bs-toggle` |
| Custom tabs | `nav-tabs`, `tab-content`, `tab-pane` |
| Custom pagination | `pagination`, `page-item`, `page-link` |
| Custom breadcrumb | `breadcrumb`, `breadcrumb-item` |

View File

@@ -228,18 +228,12 @@
line-height: 2.5; line-height: 2.5;
} }
.woocommerce-ordering select { /* Ordering select — Bootstrap's .form-select handles all styling.
display: inline-block; Remove duplicate rules that conflict with Bootstrap's dropdown arrow.
padding: 0.375rem 2.25rem 0.375rem 0.75rem; WooCommerce's woocommerce-layout.css sets background-image on selects;
font-size: 0.875rem; ensure Bootstrap's chevron wins via appearance: none. */
font-weight: 400; .woocommerce-ordering .form-select {
line-height: 1.5; appearance: none;
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
appearance: auto;
} }
/* ========================================================================== /* ==========================================================================
@@ -477,6 +471,19 @@
color: #fff; color: #fff;
} }
/* Variable product attribute selectors — woocommerce.css sets:
.woocommerce div.product form.cart .variations select { background: url(...) no-repeat }
at specificity (0,4,3). We must match or exceed that.
Override both background-color (white fallback) and background-image (black SVG chevron)
so Bootstrap's dark-mode form-select styling takes effect. */
[data-bs-theme="dark"] .woocommerce div.product form.cart .variations select,
[data-bs-theme="dark"] .woocommerce div.product .variations .form-select {
background-color: var(--bs-body-bg);
background-image: var(--bs-form-select-bg-img);
color: var(--bs-body-color);
border-color: var(--bs-border-color);
}
/* Checkout form focus color for dark mode */ /* Checkout form focus color for dark mode */
[data-bs-theme="dark"] .woocommerce-checkout .form-row input.input-text:focus, [data-bs-theme="dark"] .woocommerce-checkout .form-row input.input-text:focus,
[data-bs-theme="dark"] .woocommerce-checkout .form-row textarea:focus, [data-bs-theme="dark"] .woocommerce-checkout .form-row textarea:focus,

View File

@@ -0,0 +1,56 @@
/**
* Product Gallery Thumbnail Click Handler
*
* Swaps the main product image when a gallery thumbnail is clicked.
* The first thumbnail is the main product image (always included in the strip).
* Also handles gallery fade-in (WooCommerce sets opacity: 0 by default).
*
* @package WcBootstrap
* @since 0.1.5
*/
(function () {
'use strict';
function initGallery() {
var gallery = document.querySelector('.woocommerce-product-gallery');
if (!gallery) return;
// Fade in the gallery (WooCommerce expects JS to set opacity: 1).
gallery.style.opacity = '1';
var mainImageContainer = gallery.querySelector('.woocommerce-product-gallery__image');
if (!mainImageContainer) return;
var mainImage = mainImageContainer.querySelector('img');
if (!mainImage) return;
var thumbs = gallery.querySelectorAll('.wc-gallery-thumb');
if (!thumbs.length) return;
thumbs.forEach(function (thumb) {
thumb.style.cursor = 'pointer';
thumb.addEventListener('click', function () {
var fullSrc = this.getAttribute('data-full-src');
if (!fullSrc) return;
mainImage.setAttribute('src', fullSrc);
// Update active state.
thumbs.forEach(function (t) {
t.style.opacity = '0.6';
t.classList.remove('border-primary', 'active');
t.classList.add('border');
});
this.style.opacity = '1';
this.classList.add('border-primary', 'active');
this.classList.remove('border');
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initGallery);
} else {
initGallery();
}
})();

View File

@@ -130,6 +130,17 @@ function wc_bootstrap_enqueue_scripts(): void {
$theme_version, $theme_version,
true true
); );
// Product gallery thumbnail handler for single product pages.
if ( function_exists( 'is_product' ) && is_product() ) {
wp_enqueue_script(
'wc-bootstrap-gallery',
get_stylesheet_directory_uri() . '/assets/js/product-gallery.js',
array(),
$theme_version,
true
);
}
} }
add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_scripts' ); add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_scripts' );
@@ -278,7 +289,7 @@ add_action( 'widgets_init', 'wc_bootstrap_register_sidebars' );
* @since 0.1.0 * @since 0.1.0
*/ */
function wc_bootstrap_loop_columns(): int { function wc_bootstrap_loop_columns(): int {
return 4; return 3;
} }
add_filter( 'loop_shop_columns', 'wc_bootstrap_loop_columns' ); add_filter( 'loop_shop_columns', 'wc_bootstrap_loop_columns' );

View File

@@ -37,6 +37,9 @@ class WooCommerceExtension extends AbstractExtension {
new TwigFilter( 'esc_attr', 'esc_attr', [ 'is_safe' => [ 'html' ] ] ), new TwigFilter( 'esc_attr', 'esc_attr', [ 'is_safe' => [ 'html' ] ] ),
new TwigFilter( 'esc_url', 'esc_url', [ 'is_safe' => [ 'html' ] ] ), new TwigFilter( 'esc_url', 'esc_url', [ 'is_safe' => [ 'html' ] ] ),
// Slug/sanitize filters.
new TwigFilter( 'sanitize_title', 'sanitize_title' ),
// Text processing filters. // Text processing filters.
new TwigFilter( 'wpautop', 'wpautop', [ 'is_safe' => [ 'html' ] ] ), new TwigFilter( 'wpautop', 'wpautop', [ 'is_safe' => [ 'html' ] ] ),
new TwigFilter( 'wp_kses_post', 'wp_kses_post', [ 'is_safe' => [ 'html' ] ] ), new TwigFilter( 'wp_kses_post', 'wp_kses_post', [ 'is_safe' => [ 'html' ] ] ),
@@ -107,6 +110,10 @@ class WooCommerceExtension extends AbstractExtension {
// Dynamic function calls. // Dynamic function calls.
new TwigFunction( 'call_user_func', [ $this, 'callUserFunc' ], [ 'is_safe' => [ 'html' ] ] ), new TwigFunction( 'call_user_func', [ $this, 'callUserFunc' ], [ 'is_safe' => [ 'html' ] ] ),
new TwigFunction( 'fn', [ $this, 'callFunction' ] ), new TwigFunction( 'fn', [ $this, 'callFunction' ] ),
// Product loop helpers (set global $product for WC hooks in Twig loops).
new TwigFunction( 'wc_setup_product_data', [ $this, 'setupProductData' ] ),
new TwigFunction( 'wp_reset_postdata', 'wp_reset_postdata' ),
]; ];
} }
@@ -248,6 +255,8 @@ class WooCommerceExtension extends AbstractExtension {
'wc_review_ratings_enabled', 'wc_review_ratings_enabled',
'wc_get_product_category_list', 'wc_get_product_category_list',
'wc_get_product_tag_list', 'wc_get_product_tag_list',
'woocommerce_page_title',
'wc_get_customer_available_downloads',
]; ];
/** /**
@@ -275,6 +284,25 @@ class WooCommerceExtension extends AbstractExtension {
return $name( ...$args ); return $name( ...$args );
} }
/**
* Set up global product data for WC hook-based rendering in Twig loops.
*
* WooCommerce hooks (woocommerce_before_shop_loop_item, etc.) read from
* the global $product. When iterating products in Twig (related, upsells),
* the global must be updated before rendering each product card.
*
* @param \WC_Product $product Product object.
* @return string Empty string (Twig requires a return value).
*/
public function setupProductData( \WC_Product $product ): string {
$GLOBALS['product'] = $product;
$post = get_post( $product->get_id() );
if ( $post ) {
setup_postdata( $GLOBALS['post'] = $post );
}
return '';
}
/** /**
* Capture wc_print_notices() output. * Capture wc_print_notices() output.
* *

View File

@@ -7,7 +7,7 @@ Description: A Bootstrap 5 child theme for WP Bootstrap that overrides all WooCo
Requires at least: 6.7 Requires at least: 6.7
Tested up to: 6.7 Tested up to: 6.7
Requires PHP: 8.3 Requires PHP: 8.3
Version: 0.1.4 Version: 0.1.5
License: GNU General Public License v2 or later License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html License URI: http://www.gnu.org/licenses/gpl-2.0.html
Template: wp-bootstrap Template: wp-bootstrap

View File

@@ -2,11 +2,8 @@
# Product Archive Header (Bootstrap 5 Override) # Product Archive Header (Bootstrap 5 Override)
# #
# Renders the archive page title and optional description. # Renders the archive page title and optional description.
# # Calls woocommerce_page_title() directly (matching the PHP template)
# Expected context: # because wc_get_template('loop/header.php') passes no context args.
# show_page_title - Whether to display the title (boolean, filtered)
# page_title - Archive page title string
# archive_description - Optional archive description HTML
# #
# WooCommerce PHP equivalent: loop/header.php # WooCommerce PHP equivalent: loop/header.php
# #
@@ -15,9 +12,9 @@
#} #}
<header class="woocommerce-products-header mb-4"> <header class="woocommerce-products-header mb-4">
{% if show_page_title is not defined or show_page_title %} {% if apply_filters('woocommerce_show_page_title', true) %}
<h1 class="woocommerce-products-header__title page-title mb-2"> <h1 class="woocommerce-products-header__title page-title mb-2">
{{ page_title|default('')|esc_html }} {{ fn('woocommerce_page_title', false)|esc_html }}
</h1> </h1>
{% endif %} {% endif %}

View File

@@ -12,6 +12,6 @@
# @since 0.1.0 # @since 0.1.0
#} #}
{% set cols = columns|default(4) %} {% set cols = columns|default(3) %}
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-{{ cols }} g-4 products"> <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-{{ cols }} g-4 products">

View File

@@ -9,6 +9,7 @@
# @since 0.1.0 # @since 0.1.0
#} #}
{# Match PHP template: use WC()->customer->get_downloadable_products() #}
{% set downloads = fn('WC').customer.get_downloadable_products() %} {% set downloads = fn('WC').customer.get_downloadable_products() %}
{% set has_downloads = downloads is not empty %} {% set has_downloads = downloads is not empty %}

View File

@@ -4,11 +4,11 @@
# Add-to-cart form for grouped products: table of child products with quantities. # Add-to-cart form for grouped products: table of child products with quantities.
# #
# Expected context: # Expected context:
# product - WC_Product_Grouped object # product - WC_Product_Grouped object (global, injected by TemplateOverride)
# grouped_products - Array of child WC_Product objects # grouped_products - Array of child WC_Product objects
# grouped_product_columns - Array of column definitions #
# quantites_required - Whether quantities are required # Note: quantites_required and show_add_to_cart_button are computed inside the
# show_add_to_cart_button - Whether to show the submit button # loop (matching WooCommerce's PHP template behavior), not passed as context.
# #
# WooCommerce PHP equivalent: single-product/add-to-cart/grouped.php # WooCommerce PHP equivalent: single-product/add-to-cart/grouped.php
# #
@@ -21,19 +21,48 @@
<form class="cart grouped_form" action="{{ product.get_permalink()|esc_url }}" method="post" enctype="multipart/form-data"> <form class="cart grouped_form" action="{{ product.get_permalink()|esc_url }}" method="post" enctype="multipart/form-data">
<div class="table-responsive mb-4"> <div class="table-responsive mb-4">
<table class="woocommerce-grouped-product-list group_table table table-borderless align-middle"> <table class="woocommerce-grouped-product-list group_table table table-borderless align-middle">
{% set quantites_required = false %}
{% set show_add_to_cart_button = false %}
{{ do_action('woocommerce_grouped_product_list_before') }} {{ do_action('woocommerce_grouped_product_list_before') }}
{% for grouped_product in grouped_products %} {% for grouped_product in grouped_products %}
{% set child_id = grouped_product.get_id() %} {% set child_id = grouped_product.get_id() %}
<tr id="product-{{ child_id }}" class="{{ grouped_product.get_stock_status() }}">
{# Set up global product data for each child (matches PHP's setup_postdata). #}
{{ wc_setup_product_data(grouped_product) }}
{% if grouped_product.is_purchasable() and not grouped_product.has_options() %}
{% set quantites_required = true %}
{% endif %}
{% if grouped_product.is_in_stock() %}
{% set show_add_to_cart_button = true %}
{% endif %}
<tr id="product-{{ child_id }}" class="woocommerce-grouped-product-list-item {{ grouped_product.get_stock_status() }}">
<td class="woocommerce-grouped-product-list-item__quantity" style="width: 140px;"> <td class="woocommerce-grouped-product-list-item__quantity" style="width: 140px;">
{% if grouped_product.is_purchasable() and grouped_product.is_in_stock() %} {% if not grouped_product.is_purchasable() or grouped_product.has_options() or not grouped_product.is_in_stock() %}
{# Non-purchasable, has options (variable), or out-of-stock: show view link #}
{% if grouped_product.is_visible() %}
<a href="{{ grouped_product.get_permalink()|esc_url }}" class="btn btn-sm btn-outline-secondary">
{{ __('View product') }}
</a>
{% endif %}
{% elseif grouped_product.is_sold_individually() %}
<div class="form-check">
<input type="checkbox"
name="quantity[{{ child_id }}]"
value="1"
class="form-check-input wc-grouped-product-add-to-cart-checkbox"
id="quantity-{{ child_id }}" />
</div>
{% else %}
{% include 'global/quantity-input.html.twig' with { {% include 'global/quantity-input.html.twig' with {
input_id: 'quantity_' ~ child_id, input_id: 'quantity_' ~ child_id,
input_name: 'quantity[' ~ child_id ~ ']', input_name: 'quantity[' ~ child_id ~ ']',
input_value: 0, input_value: '',
min_value: 0, min_value: 0,
max_value: grouped_product.get_max_purchase_quantity()|default(0), max_value: grouped_product.get_max_purchase_quantity(),
step: 1, step: 1,
placeholder: '0', placeholder: '0',
inputmode: 'numeric', inputmode: 'numeric',
@@ -46,7 +75,7 @@
</td> </td>
<td class="woocommerce-grouped-product-list-item__label"> <td class="woocommerce-grouped-product-list-item__label">
<label for="quantity_{{ child_id }}"> <label for="product-{{ child_id }}">
{% if grouped_product.is_visible() %} {% if grouped_product.is_visible() %}
<a href="{{ grouped_product.get_permalink()|esc_url }}" class="text-decoration-none"> <a href="{{ grouped_product.get_permalink()|esc_url }}" class="text-decoration-none">
{{ grouped_product.get_name()|esc_html }} {{ grouped_product.get_name()|esc_html }}
@@ -65,14 +94,17 @@
</tr> </tr>
{% endfor %} {% endfor %}
{{ wp_reset_postdata() }}
{{ do_action('woocommerce_grouped_product_list_after') }} {{ do_action('woocommerce_grouped_product_list_after') }}
</table> </table>
</div> </div>
{% if show_add_to_cart_button is not defined or show_add_to_cart_button %} <input type="hidden" name="add-to-cart" value="{{ product.get_id() }}" />
{% if quantites_required and show_add_to_cart_button %}
{{ do_action('woocommerce_before_add_to_cart_button') }} {{ do_action('woocommerce_before_add_to_cart_button') }}
<input type="hidden" name="add-to-cart" value="{{ product.get_id() }}" />
<button type="submit" class="btn btn-primary btn-lg single_add_to_cart_button"> <button type="submit" class="btn btn-primary btn-lg single_add_to_cart_button">
{{ product.single_add_to_cart_text() }} {{ product.single_add_to_cart_text() }}
</button> </button>

View File

@@ -24,22 +24,26 @@
action="{{ product.get_permalink()|esc_url }}" action="{{ product.get_permalink()|esc_url }}"
method="post" method="post"
enctype="multipart/form-data" enctype="multipart/form-data"
{{ variations_attr|default('')|raw }}> data-product_id="{{ product.get_id() }}"
data-product_variations="{{ available_variations|json_encode|esc_attr }}">
{{ do_action('woocommerce_before_variations_form') }} {{ do_action('woocommerce_before_variations_form') }}
{% if available_variations is defined %} {% if available_variations is not same as(false) %}
{# Variation attribute selectors #} {# Variation attribute selectors.
WC PHP uses sanitize_title() on attribute names for name/data-attribute_name
so they match the lowercase keys in the variation data JSON. #}
<div class="variations mb-4"> <div class="variations mb-4">
{% for attribute_name, options in attributes %} {% for attribute_name, options in attributes %}
{% set sanitized_name = attribute_name|sanitize_title %}
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold" for="{{ attribute_name|esc_attr }}"> <label class="form-label fw-semibold" for="{{ sanitized_name|esc_attr }}">
{{ wc_attribute_label(attribute_name)|esc_html }} {{ wc_attribute_label(attribute_name)|esc_html }}
</label> </label>
<select id="{{ attribute_name|esc_attr }}" <select id="{{ sanitized_name|esc_attr }}"
class="form-select" class="form-select"
name="attribute_{{ attribute_name|esc_attr }}" name="attribute_{{ sanitized_name|esc_attr }}"
data-attribute_name="attribute_{{ attribute_name|esc_attr }}"> data-attribute_name="attribute_{{ sanitized_name|esc_attr }}">
<option value="">{{ __('Choose an option') }}</option> <option value="">{{ __('Choose an option') }}</option>
{% if options is iterable %} {% if options is iterable %}
{% for option in options %} {% for option in options %}
@@ -54,25 +58,22 @@
{% endfor %} {% endfor %}
</div> </div>
{{ do_action('woocommerce_after_variations_table') }} {# Reset link — WC JS toggles visibility based on attribute selection #}
<a class="reset_variations btn btn-link btn-sm p-0 text-decoration-none" href="#" style="visibility: hidden;">
{# Reset link #}
<div class="reset_variations_wrapper mb-3" style="display: none;">
<a class="reset_variations btn btn-link btn-sm p-0 text-decoration-none" href="#">
{{ __('Clear') }} {{ __('Clear') }}
</a> </a>
</div> <div class="reset_variations_alert screen-reader-text" role="alert" aria-live="polite" aria-relevant="all"></div>
{# Single variation display + add-to-cart button #} {{ do_action('woocommerce_after_variations_table') }}
{# Single variation display + add-to-cart button.
The woocommerce_single_variation hook outputs:
- priority 10: empty .single_variation div (JS populates via underscore template)
- priority 20: .variations_button div with quantity + add-to-cart button
No manual wrapper divs — the hooks handle the full DOM structure. #}
<div class="single_variation_wrap"> <div class="single_variation_wrap">
{{ do_action('woocommerce_before_single_variation') }} {{ do_action('woocommerce_before_single_variation') }}
<div class="woocommerce-variation single_variation"></div>
<div class="woocommerce-variation-add-to-cart variations_button">
{{ do_action('woocommerce_single_variation') }} {{ do_action('woocommerce_single_variation') }}
</div>
{{ do_action('woocommerce_after_single_variation') }} {{ do_action('woocommerce_after_single_variation') }}
</div> </div>
{% else %} {% else %}
@@ -86,3 +87,4 @@
</form> </form>
{{ do_action('woocommerce_after_add_to_cart_form') }} {{ do_action('woocommerce_after_add_to_cart_form') }}

View File

@@ -37,8 +37,7 @@
{{ do_action('woocommerce_after_add_to_cart_quantity') }} {{ do_action('woocommerce_after_add_to_cart_quantity') }}
<button type="submit" <button type="submit"
class="btn btn-primary btn-lg single_add_to_cart_button" class="btn btn-primary btn-lg single_add_to_cart_button disabled wc-variation-selection-needed">
disabled>
{{ product.single_add_to_cart_text() }} {{ product.single_add_to_cart_text() }}
</button> </button>
</div> </div>

View File

@@ -16,8 +16,11 @@
# @since 0.1.0 # @since 0.1.0
#} #}
{% set cols = columns|default(4) %} {# Compute image data from the product object (PHP template does this locally). #}
{% set has_images = post_thumbnail_id is defined and post_thumbnail_id %} {% set post_thumbnail_id = product.get_image_id() %}
{% set gallery_image_ids = product.get_gallery_image_ids() %}
{% set cols = apply_filters('woocommerce_product_thumbnails_columns', 4) %}
{% set has_images = post_thumbnail_id %}
{% set gallery_classes = 'woocommerce-product-gallery woocommerce-product-gallery--columns-' ~ cols %} {% set gallery_classes = 'woocommerce-product-gallery woocommerce-product-gallery--columns-' ~ cols %}
{% if has_images %} {% if has_images %}
{% set gallery_classes = gallery_classes ~ ' woocommerce-product-gallery--with-images' %} {% set gallery_classes = gallery_classes ~ ' woocommerce-product-gallery--with-images' %}
@@ -28,34 +31,37 @@
<div class="{{ gallery_classes }}" data-columns="{{ cols }}" style="opacity: 0; transition: opacity .25s ease-in-out;"> <div class="{{ gallery_classes }}" data-columns="{{ cols }}" style="opacity: 0; transition: opacity .25s ease-in-out;">
<div class="woocommerce-product-gallery__wrapper"> <div class="woocommerce-product-gallery__wrapper">
{# Main product image #} {# Main product image #}
{% if main_image_html is defined and main_image_html %} {% if post_thumbnail_id %}
<div class="woocommerce-product-gallery__image mb-3">
{{ main_image_html|raw }}
</div>
{% elseif post_thumbnail_id is defined and post_thumbnail_id %}
<div class="woocommerce-product-gallery__image mb-3"> <div class="woocommerce-product-gallery__image mb-3">
<img src="{{ wp_get_attachment_url(post_thumbnail_id)|esc_url }}" <img src="{{ wp_get_attachment_url(post_thumbnail_id)|esc_url }}"
class="img-fluid rounded" class="img-fluid rounded wp-post-image"
alt="{{ product.get_name()|esc_attr }}" /> alt="{{ product.get_name()|esc_attr }}" />
</div> </div>
{% else %} {% else %}
<div class="woocommerce-product-gallery__image mb-3"> <div class="woocommerce-product-gallery__image woocommerce-product-gallery__image--placeholder mb-3">
<img src="{{ wc_placeholder_img_src()|esc_url }}" <img src="{{ wc_placeholder_img_src()|esc_url }}"
class="img-fluid rounded" class="img-fluid rounded wp-post-image"
alt="{{ __('Placeholder')|esc_attr }}" /> alt="{{ __('Awaiting product image')|esc_attr }}" />
</div> </div>
{% endif %} {% endif %}
{# Thumbnail gallery strip #} {# Thumbnail gallery strip — includes main image so user can switch back.
{% if gallery_image_ids is defined and gallery_image_ids|length > 0 %} Build a combined list: main image first, then gallery images.
<div class="row row-cols-{{ columns|default(4) }} g-2"> Skip any IDs that don't resolve to a valid attachment URL. #}
{% for image_id in gallery_image_ids %} {% if gallery_image_ids|length > 0 and post_thumbnail_id %}
{% set all_thumb_ids = [post_thumbnail_id]|merge(gallery_image_ids) %}
<div class="row row-cols-{{ cols }} g-2 mt-2">
{% for image_id in all_thumb_ids %}
{% set thumb_url = wp_get_attachment_url(image_id) %}
{% if thumb_url %}
<div class="col"> <div class="col">
<img src="{{ wp_get_attachment_url(image_id)|esc_url }}" <img src="{{ thumb_url|esc_url }}"
class="img-fluid rounded border cursor-pointer wc-gallery-thumb" class="img-fluid rounded border wc-gallery-thumb{% if loop.first %} border-primary active{% endif %}"
alt="{{ product.get_name()|esc_attr }}" alt="{{ product.get_name()|esc_attr }}"
data-full-src="{{ wp_get_attachment_url(image_id)|esc_url }}" /> data-full-src="{{ thumb_url|esc_url }}"
style="{% if loop.first %}opacity: 1{% else %}opacity: 0.6{% endif %}" />
</div> </div>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}

View File

@@ -0,0 +1,13 @@
{#
# Product Thumbnails (Bootstrap 5 Override)
#
# Intentionally empty — the product-image.html.twig template renders its own
# thumbnail gallery strip using Bootstrap grid. This override suppresses the
# default WC output from woocommerce_show_product_thumbnails() which would
# render additional full-size gallery images below the thumbnail row.
#
# WooCommerce PHP equivalent: single-product/product-thumbnails.php
#
# @package WcBootstrap
# @since 0.1.5
#}

View File

@@ -23,9 +23,11 @@
{{ woocommerce_product_loop_start() }} {{ woocommerce_product_loop_start() }}
{% for product in related_products %} {% for related_product in related_products %}
{% include 'content-product.html.twig' with { product: product } %} {{ wc_setup_product_data(related_product) }}
{% include 'content-product.html.twig' %}
{% endfor %} {% endfor %}
{{ wp_reset_postdata() }}
{{ woocommerce_product_loop_end() }} {{ woocommerce_product_loop_end() }}
</section> </section>

View File

@@ -23,9 +23,11 @@
{{ woocommerce_product_loop_start() }} {{ woocommerce_product_loop_start() }}
{% for product in upsells %} {% for upsell in upsells %}
{% include 'content-product.html.twig' with { product: product } %} {{ wc_setup_product_data(upsell) }}
{% include 'content-product.html.twig' %}
{% endfor %} {% endfor %}
{{ wp_reset_postdata() }}
{{ woocommerce_product_loop_end() }} {{ woocommerce_product_loop_end() }}
</section> </section>

View File

@@ -16,6 +16,12 @@
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
// Render breadcrumbs. The woocommerce_before_main_content hook is not fired here
// (to avoid the content wrapper), so breadcrumbs must be called directly.
// TemplateOverride will intercept global/breadcrumb.php and render the Bootstrap
// breadcrumb Twig template.
woocommerce_breadcrumb();
// Fire structured data hook (normally on woocommerce_before_main_content at priority 30). // Fire structured data hook (normally on woocommerce_before_main_content at priority 30).
do_action( 'woocommerce_shop_loop_header' ); do_action( 'woocommerce_shop_loop_header' );