Implement Phase 1: global templates and notices (Bootstrap 5)

Add 9 Twig template overrides for WooCommerce's global and notice templates:
- global/wrapper-start, wrapper-end: conditional container with _theme_wrapped
- global/breadcrumb: Bootstrap breadcrumb component with aria-current
- global/sidebar: offcanvas-lg for mobile, standard aside for desktop
- global/quantity-input: input-group with +/- buttons
- global/form-login: responsive form with form-control, form-check
- notices/notice, error, success: Bootstrap alert-dismissible with icons

Supporting changes:
- assets/js/quantity.js: +/- button handler respecting min/max/step
- assets/css/wc-bootstrap.css: WooCommerce notice fallback styles, spinner removal
- functions.php: register quantity.js script

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 10:19:10 +01:00
parent 9592b8cae5
commit 01b807a769
14 changed files with 477 additions and 16 deletions

18
PLAN.md
View File

@@ -519,15 +519,15 @@ Track completion per file. Mark with `[x]` when done.
### Phase 1 -- Global & Notices
- [ ] `global/wrapper-start.html.twig`
- [ ] `global/wrapper-end.html.twig`
- [ ] `global/breadcrumb.html.twig`
- [ ] `global/sidebar.html.twig`
- [ ] `global/quantity-input.html.twig`
- [ ] `global/form-login.html.twig`
- [ ] `notices/notice.html.twig`
- [ ] `notices/error.html.twig`
- [ ] `notices/success.html.twig`
- [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

View File

@@ -37,19 +37,56 @@
*/
/* ==========================================================================
Notification Overrides
Map plugin notification classes to Bootstrap alert styles.
WooCommerce Notice Overrides
Map WooCommerce notice classes to Bootstrap alert styles as fallback
when notices are rendered outside our Twig templates.
========================================================================== */
/* Example: Map plugin .my-notification to Bootstrap alert
.my-notification {
.woocommerce-info,
.woocommerce-message,
.woocommerce-error {
position: relative;
padding: 1rem 1rem;
padding: 1rem 3rem 1rem 1rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: var(--bs-border-radius);
}
*/
.woocommerce-info {
color: var(--bs-info-text-emphasis);
background-color: var(--bs-info-bg-subtle);
border-color: var(--bs-info-border-subtle);
}
.woocommerce-message {
color: var(--bs-success-text-emphasis);
background-color: var(--bs-success-bg-subtle);
border-color: var(--bs-success-border-subtle);
}
.woocommerce-error {
color: var(--bs-danger-text-emphasis);
background-color: var(--bs-danger-bg-subtle);
border-color: var(--bs-danger-border-subtle);
list-style: none;
padding-left: 1rem;
}
/* ==========================================================================
Quantity Input
Sizing for the Bootstrap input-group quantity widget.
========================================================================== */
.quantity.input-group .form-control {
/* Remove number input spinners (browser default) */
-moz-appearance: textfield;
}
.quantity.input-group .form-control::-webkit-outer-spin-button,
.quantity.input-group .form-control::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* ==========================================================================
Dark Mode Overrides

39
assets/js/quantity.js Normal file
View File

@@ -0,0 +1,39 @@
/**
* Quantity Input +/- Button Handler
*
* Handles increment/decrement for Bootstrap 5 quantity input groups.
* Respects min, max, and step attributes on the input element.
* Triggers 'change' event so WooCommerce JS picks up the new value.
*
* @package WcBootstrap
* @since 0.1.0
*/
(function () {
'use strict';
function handleQuantityClick(e) {
var button = e.target.closest('.wc-qty-minus, .wc-qty-plus');
if (!button) return;
var target = button.getAttribute('data-target');
var input = target ? document.querySelector(target) : button.closest('.quantity').querySelector('input');
if (!input) return;
var currentVal = parseFloat(input.value) || 0;
var min = parseFloat(input.getAttribute('min')) || 0;
var max = parseFloat(input.getAttribute('max')) || Infinity;
var step = parseFloat(input.getAttribute('step')) || 1;
if (button.classList.contains('wc-qty-minus')) {
var newVal = currentVal - step;
input.value = newVal >= min ? newVal : min;
} else {
var newVal = currentVal + step;
input.value = max !== Infinity && newVal > max ? max : newVal;
}
input.dispatchEvent(new Event('change', { bubbles: true }));
}
document.addEventListener('click', handleQuantityClick);
})();

View File

@@ -94,6 +94,25 @@ function wc_bootstrap_enqueue_styles(): void {
}
add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_styles' );
/**
* Enqueue child theme scripts.
*
* @since 0.1.0
*/
function wc_bootstrap_enqueue_scripts(): void {
$theme_version = wp_get_theme()->get( 'Version' );
// Quantity +/- button handler for Bootstrap input-group widget.
wp_enqueue_script(
'wc-bootstrap-quantity',
get_stylesheet_directory_uri() . '/assets/js/quantity.js',
array(),
$theme_version,
true
);
}
add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_scripts' );
/**
* Handle plugin page rendering via plugin render filter.
*

View File

@@ -1,6 +1,6 @@
/*
Theme Name: WooCommerce Bootstrap
Theme URI: ssh://git@src.bundespruefstelle.ch:2022/magdev/wc-bootstrap.git
Theme URI: https://src.bundespruefstelle.ch:2022/magdev/wc-bootstrap
Author: Marco Grätsch
Author URI: https://src.bundespruefstelle.ch/magdev
Description: A Bootstrap 5 child theme for WP Bootstrap that overrides all WooCommerce plugin templates with modern, responsive Bootstrap 5 markup and styling.

View File

@@ -0,0 +1,37 @@
{#
# Shop Breadcrumb (Bootstrap 5 Override)
#
# Replaces WooCommerce's breadcrumb with Bootstrap 5 breadcrumb component.
# Skipped when parent theme is wrapping (base.html.twig handles breadcrumbs).
#
# Expected context (from WooCommerce woocommerce_breadcrumb()):
# breadcrumb - Array of [label, url] tuples
# wrap_before - Opening HTML (ignored, we use Bootstrap markup)
# wrap_after - Closing HTML (ignored)
# before - Before each item (ignored)
# after - After each item (ignored)
# delimiter - Between items (ignored, Bootstrap uses CSS)
#
# WooCommerce PHP equivalent: global/breadcrumb.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{% if breadcrumb is defined and breadcrumb|length > 0 %}
<nav aria-label="{{ __('Breadcrumb') }}">
<ol class="breadcrumb">
{% for crumb in breadcrumb %}
{% if not loop.last and crumb[1] is defined and crumb[1] %}
<li class="breadcrumb-item">
<a href="{{ crumb[1]|esc_url }}">{{ crumb[0]|esc_html }}</a>
</li>
{% else %}
<li class="breadcrumb-item active" aria-current="page">
{{ crumb[0]|esc_html }}
</li>
{% endif %}
{% endfor %}
</ol>
</nav>
{% endif %}

View File

@@ -0,0 +1,89 @@
{#
# Global Login Form (Bootstrap 5 Override)
#
# Inline login form used on checkout and other pages (not the My Account login).
# Can be initially hidden and toggled via a link.
#
# Expected context (from WooCommerce wc_get_template('global/form-login.php')):
# hidden - Whether form is initially hidden (boolean)
# message - Optional message to display above the form
# redirect - URL to redirect after login
#
# WooCommerce PHP equivalent: global/form-login.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{% if not is_user_logged_in() %}
<form class="woocommerce-form woocommerce-form-login login{% if hidden %} d-none{% endif %}" method="post">
{{ do_action('woocommerce_login_form_start') }}
{% if message %}
<div class="mb-3">
{{ message|wpautop }}
</div>
{% endif %}
<div class="row g-3 mb-3">
<div class="col-sm-6">
<label for="username" class="form-label">
{{ __('Username or email') }}&nbsp;<span class="text-danger" aria-hidden="true">*</span>
<span class="visually-hidden">{{ __('Required') }}</span>
</label>
<input type="text"
class="form-control"
name="username"
id="username"
autocomplete="username"
required
aria-required="true" />
</div>
<div class="col-sm-6">
<label for="password" class="form-label">
{{ __('Password') }}&nbsp;<span class="text-danger" aria-hidden="true">*</span>
<span class="visually-hidden">{{ __('Required') }}</span>
</label>
<input type="password"
class="form-control"
name="password"
id="password"
autocomplete="current-password"
required
aria-required="true" />
</div>
</div>
{{ do_action('woocommerce_login_form') }}
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div class="form-check">
<input class="form-check-input"
type="checkbox"
name="rememberme"
id="rememberme"
value="forever" />
<label class="form-check-label" for="rememberme">
{{ __('Remember me') }}
</label>
</div>
{{ wp_nonce_field('woocommerce-login', 'woocommerce-login-nonce') }}
<input type="hidden" name="redirect" value="{{ redirect|esc_url }}" />
<button type="submit"
class="btn btn-primary"
name="login"
value="{{ __('Login') }}">
{{ __('Login') }}
</button>
</div>
<p class="mt-3 mb-0">
<a href="{{ wp_lostpassword_url()|esc_url }}">{{ __('Lost your password?') }}</a>
</p>
{{ do_action('woocommerce_login_form_end') }}
</form>
{% endif %}

View File

@@ -0,0 +1,74 @@
{#
# Quantity Input (Bootstrap 5 Override)
#
# Replaces WooCommerce's quantity input with a Bootstrap 5 input-group
# featuring decrement/increment buttons.
#
# Expected context (from WooCommerce woocommerce_quantity_input()):
# input_id - Input element ID
# input_name - Input element name attribute
# input_value - Current quantity value
# min_value - Minimum allowed quantity
# max_value - Maximum allowed quantity (0 = no max)
# step - Step increment
# placeholder - Placeholder text
# inputmode - Input mode (e.g., 'numeric')
# classes - CSS class(es) for the input
# readonly - Whether input is read-only
# type - Input type attribute
# args - Additional args (product_name for accessible label)
# autocomplete - Autocomplete attribute value
#
# WooCommerce PHP equivalent: global/quantity-input.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{% set label = args.product_name is defined and args.product_name
? __('%s quantity')|format(args.product_name)
: __('Quantity')
%}
<div class="quantity input-group input-group-sm" style="max-width: 140px;">
{{ do_action('woocommerce_before_quantity_input_field') }}
<label class="visually-hidden" for="{{ input_id|esc_attr }}">{{ label|esc_attr }}</label>
{% if not readonly %}
<button type="button"
class="btn btn-outline-secondary wc-qty-minus"
aria-label="{{ __('Decrease quantity') }}"
data-target="#{{ input_id|esc_attr }}">
<i class="bi bi-dash" aria-hidden="true"></i>
</button>
{% endif %}
<input type="{{ type|default('number')|esc_attr }}"
{% if readonly %}readonly="readonly"{% endif %}
id="{{ input_id|esc_attr }}"
class="form-control text-center {{ classes is iterable ? classes|join(' ')|esc_attr : classes|default('')|esc_attr }}"
name="{{ input_name|esc_attr }}"
value="{{ input_value|esc_attr }}"
aria-label="{{ __('Product quantity')|esc_attr }}"
min="{{ min_value|esc_attr }}"
{% if max_value is defined and max_value > 0 %}max="{{ max_value|esc_attr }}"{% endif %}
{% if not readonly %}
step="{{ step|esc_attr }}"
placeholder="{{ placeholder|esc_attr }}"
inputmode="{{ inputmode|default('numeric')|esc_attr }}"
autocomplete="{{ autocomplete|default('on')|esc_attr }}"
{% endif %}
/>
{% if not readonly %}
<button type="button"
class="btn btn-outline-secondary wc-qty-plus"
aria-label="{{ __('Increase quantity') }}"
data-target="#{{ input_id|esc_attr }}">
<i class="bi bi-plus" aria-hidden="true"></i>
</button>
{% endif %}
{{ do_action('woocommerce_after_quantity_input_field') }}
</div>

View File

@@ -0,0 +1,40 @@
{#
# Shop Sidebar (Bootstrap 5 Override)
#
# Renders the WooCommerce shop sidebar using Bootstrap 5 offcanvas for mobile
# and a standard aside column for desktop.
#
# Expected context:
# sidebar_content - Pre-rendered sidebar widget HTML (from get_sidebar('shop'))
#
# WooCommerce PHP equivalent: global/sidebar.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{% if sidebar_content is defined and sidebar_content %}
{# Mobile: offcanvas trigger button (visible below lg breakpoint) #}
<button class="btn btn-outline-secondary d-lg-none mb-3"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#shopSidebar"
aria-controls="shopSidebar">
<i class="bi bi-funnel" aria-hidden="true"></i>
{{ __('Filters') }}
</button>
{# Mobile: offcanvas panel #}
<div class="offcanvas offcanvas-start offcanvas-lg"
tabindex="-1"
id="shopSidebar"
aria-labelledby="shopSidebarLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="shopSidebarLabel">{{ __('Filters') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="{{ __('Close') }}"></button>
</div>
<div class="offcanvas-body">
{{ sidebar_content|raw }}
</div>
</div>
{% endif %}

View File

@@ -0,0 +1,18 @@
{#
# Content Wrapper End (Bootstrap 5 Override)
#
# Closes the wrapper divs opened by wrapper-start.html.twig.
#
# WooCommerce PHP equivalent: global/wrapper-end.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{% set _wrapped = _theme_wrapped is defined and _theme_wrapped %}
{% if not _wrapped %}
</main>
</div>
</div>
{% endif %}

View File

@@ -0,0 +1,20 @@
{#
# Content Wrapper Start (Bootstrap 5 Override)
#
# Replaces WooCommerce's theme-specific wrapper divs with Bootstrap 5 layout.
# When the parent theme wraps the page (_theme_wrapped), this outputs nothing
# since base.html.twig already provides the container.
#
# WooCommerce PHP equivalent: global/wrapper-start.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{% set _wrapped = _theme_wrapped is defined and _theme_wrapped %}
{% if not _wrapped %}
<div class="container my-4">
<div id="primary" class="content-area">
<main id="main" class="site-main" role="main">
{% endif %}

View File

@@ -0,0 +1,34 @@
{#
# Error Notice (Bootstrap 5 Override)
#
# Displays WooCommerce error notices as a Bootstrap 5 danger alert.
# Multiple errors are shown as a list within a single alert.
#
# Expected context:
# notices - Array of notice objects, each with:
# notice - Notice HTML content (pre-sanitized via wc_kses_notice)
# data - Optional data attributes string
#
# WooCommerce PHP equivalent: notices/error.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{% if notices is defined and notices|length > 0 %}
<div class="alert alert-danger alert-dismissible fade show woocommerce-error" role="alert">
<i class="bi bi-exclamation-triangle me-2" aria-hidden="true"></i>
{% if notices|length == 1 %}
{{ notices[0].notice|raw }}
{% else %}
<ul class="mb-0 ps-3">
{% for notice in notices %}
<li {{ notice.data|default('')|raw }}>
{{ notice.notice|raw }}
</li>
{% endfor %}
</ul>
{% endif %}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('Close') }}"></button>
</div>
{% endif %}

View File

@@ -0,0 +1,27 @@
{#
# Info Notice (Bootstrap 5 Override)
#
# Displays WooCommerce info/neutral notices as Bootstrap 5 alerts.
#
# Expected context:
# notices - Array of notice objects, each with:
# notice - Notice HTML content (pre-sanitized via wc_kses_notice)
# data - Optional data attributes string
#
# WooCommerce PHP equivalent: notices/notice.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{% if notices is defined and notices|length > 0 %}
{% for notice in notices %}
<div class="alert alert-info alert-dismissible fade show woocommerce-info"
{{ notice.data|default('')|raw }}
role="status">
<i class="bi bi-info-circle me-2" aria-hidden="true"></i>
{{ notice.notice|raw }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('Close') }}"></button>
</div>
{% endfor %}
{% endif %}

View File

@@ -0,0 +1,27 @@
{#
# Success Notice (Bootstrap 5 Override)
#
# Displays WooCommerce success notices as Bootstrap 5 success alerts.
#
# Expected context:
# notices - Array of notice objects, each with:
# notice - Notice HTML content (pre-sanitized via wc_kses_notice)
# data - Optional data attributes string
#
# WooCommerce PHP equivalent: notices/success.php
#
# @package WcBootstrap
# @since 0.1.0
#}
{% if notices is defined and notices|length > 0 %}
{% for notice in notices %}
<div class="alert alert-success alert-dismissible fade show woocommerce-message"
{{ notice.data|default('')|raw }}
role="alert">
<i class="bi bi-check-circle me-2" aria-hidden="true"></i>
{{ notice.notice|raw }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('Close') }}"></button>
</div>
{% endfor %}
{% endif %}