Initial theme scaffold from wp-theme-template

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 09:42:35 +01:00
commit 399354b7d2
21 changed files with 1635 additions and 0 deletions

View File

@@ -0,0 +1,210 @@
name: Create Release Package
on:
push:
tags:
- 'v*'
jobs:
lint:
name: PHP Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, xml, zip
tools: composer:v2
- name: PHP Syntax Check
run: |
find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*" -print0 | xargs -0 -n1 php -l
build-release:
name: Build Release
runs-on: ubuntu-latest
needs: [lint]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, xml, zip, intl, gettext
tools: composer:v2
- name: Get version from tag
id: version
run: |
VERSION=${GITHUB_REF_NAME#v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Building version: $VERSION"
- name: Validate composer.json
run: composer validate --no-check-lock --no-check-all
- name: Install Composer dependencies (production)
run: |
composer config platform.php 8.3.0
composer install --no-dev --optimize-autoloader --no-interaction
- name: Install gettext
run: apt-get update && apt-get install -y gettext
- name: Compile translations
run: |
for po in languages/*.po; do
if [ -f "$po" ]; then
mo="${po%.po}.mo"
echo "Compiling $po to $mo"
msgfmt -o "$mo" "$po"
fi
done
- name: Verify theme version matches tag
run: |
THEME_VERSION=$(grep -oP "Version:\s*\K[0-9]+\.[0-9]+\.[0-9]+" style.css | head -1)
TAG_VERSION=${{ steps.version.outputs.version }}
if [ "$THEME_VERSION" != "$TAG_VERSION" ]; then
echo "Error: Theme version ($THEME_VERSION) does not match tag version ($TAG_VERSION)"
exit 1
fi
echo "Version verified: $THEME_VERSION"
- name: Build release package
run: |
VERSION=${{ steps.version.outputs.version }}
THEME_NAME="wc-bootstrap"
# Stage files into a properly named directory for the zip archive.
WORKSPACE_DIR="$(pwd)"
STAGING_DIR=$(mktemp -d)
cp -a . "${STAGING_DIR}/${THEME_NAME}"
cd "${STAGING_DIR}/${THEME_NAME}"
rm -rf .git .gitea .github .vscode .claude releases node_modules
rm -f CLAUDE.md PLAN.md composer.json composer.lock .gitignore .editorconfig
find . -name '*.log' -o -name '*.po~' -o -name '*.bak' -o -name '.DS_Store' | xargs rm -f 2>/dev/null || true
cd "${STAGING_DIR}"
mkdir -p "${WORKSPACE_DIR}/releases"
zip -r "${WORKSPACE_DIR}/releases/${THEME_NAME}-${VERSION}.zip" "${THEME_NAME}"
cd "${WORKSPACE_DIR}"
rm -rf "${STAGING_DIR}"
echo "Created: releases/${THEME_NAME}-${VERSION}.zip"
ls -lh "releases/${THEME_NAME}-${VERSION}.zip"
- name: Generate checksums
run: |
VERSION=${{ steps.version.outputs.version }}
THEME_NAME="wc-bootstrap"
cd releases
sha256sum "${THEME_NAME}-${VERSION}.zip" > "${THEME_NAME}-${VERSION}.zip.sha256"
echo "SHA256:"
cat "${THEME_NAME}-${VERSION}.zip.sha256"
- name: Verify package structure
run: |
set +o pipefail
VERSION=${{ steps.version.outputs.version }}
THEME_NAME="wc-bootstrap"
echo "Package contents (first 50 entries):"
unzip -l "releases/${THEME_NAME}-${VERSION}.zip" | head -50 || true
# Verify main style.css file exists
if unzip -l "releases/${THEME_NAME}-${VERSION}.zip" | grep -q "${THEME_NAME}/style.css"; then
echo "Main style.css file: OK"
else
echo "ERROR: Main style.css file not found!"
exit 1
fi
# Verify functions.php exists
if unzip -l "releases/${THEME_NAME}-${VERSION}.zip" | grep -q "${THEME_NAME}/functions.php"; then
echo "functions.php file: OK"
else
echo "ERROR: functions.php file not found!"
exit 1
fi
- name: Extract changelog for release notes
id: changelog
run: |
VERSION=${{ steps.version.outputs.version }}
NOTES=$(sed -n "/^## \[${VERSION}\]/,/^## \[/p" CHANGELOG.md | sed '$ d' | tail -n +2)
if [ -z "$NOTES" ]; then
NOTES="Release version ${VERSION}"
fi
echo "$NOTES" > release_notes.txt
echo "Release notes extracted"
- name: Create Gitea Release
env:
GITEA_TOKEN: ${{ secrets.SRC_GITEA_TOKEN }}
run: |
VERSION=${{ steps.version.outputs.version }}
TAG_NAME=${{ github.ref_name }}
THEME_NAME="wc-bootstrap"
PRERELEASE="false"
if [[ "$TAG_NAME" == *-* ]] || [[ "$VERSION" == 0.* ]]; then
PRERELEASE="true"
fi
BODY=$(cat release_notes.txt)
# Check if release already exists and delete it
EXISTING_RELEASE=$(curl -s \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG_NAME}")
EXISTING_ID=$(echo "$EXISTING_RELEASE" | jq -r '.id // empty')
if [ -n "$EXISTING_ID" ] && [ "$EXISTING_ID" != "null" ]; then
echo "Deleting existing release ID: $EXISTING_ID"
curl -s -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${EXISTING_ID}"
echo "Existing release deleted"
fi
# Create release via Gitea API
RELEASE_RESPONSE=$(curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"WooCommerce Bootstrap ${VERSION}\", \"body\": $(echo "$BODY" | jq -Rs .), \"draft\": false, \"prerelease\": ${PRERELEASE}}" \
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases")
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
if [ "$RELEASE_ID" == "null" ] || [ -z "$RELEASE_ID" ]; then
echo "Failed to create release:"
echo "$RELEASE_RESPONSE"
exit 1
fi
echo "Created release ID: $RELEASE_ID"
# Upload release assets
for file in "releases/${THEME_NAME}-${VERSION}.zip" "releases/${THEME_NAME}-${VERSION}.zip.sha256"; do
if [ -f "$file" ]; then
FILENAME=$(basename "$file")
echo "Uploading $FILENAME..."
curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$file" \
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${FILENAME}"
echo "Uploaded $FILENAME"
fi
done
echo "Release created: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG_NAME}"

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Symlinks
wp-core
# Dependencies
node_modules/
vendor/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# npm
npm-debug.log
# Backup files
*.bak
*.po~
# Compiled translations (built by CI/CD release workflow)
*.mo
# Claude local settings
.claude/settings.local.json
# Build artifacts (releases directory)
releases/

17
CHANGELOG.md Normal file
View File

@@ -0,0 +1,17 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.1.0] - 2026-02-28
### Added
- Initial theme scaffold
- Bootstrap 5 base templates (base, page, form, single, archive, account)
- Template override mechanism via Twig `prependPath()`
- Parent theme rendering delegation via render page filter
- Theme wrapping support (`_theme_wrapped` context flag)
- CSS override stylesheet for plugin-to-Bootstrap class mapping
- Sticky header scroll shadow behavior
- CI/CD release workflow (Gitea Actions)
- Translation support (`.pot` template ready)

311
CLAUDE.md Normal file
View File

@@ -0,0 +1,311 @@
# WooCommerce Bootstrap
**Author:** Marco Grätsch
**Author URL:** <https://src.bundespruefstelle.ch/magdev>
**Author Email:** <magdev3.0@gmail.com>
**Repository URL:** <ssh://git@src.bundespruefstelle.ch:2022/magdev/wc-bootstrap.git>
**Issues URL:** <ssh://git@src.bundespruefstelle.ch:2022/magdev/wc-bootstrap.git/issues>
## Project Overview
This child theme of wp-bootstrap (`../wp-bootstrap/` or <https://src.bundespruefstelle.ch/magdev/wp-bootstrap>) extends wp-bootstrap and adds overrides for all theme files of the WooCommerce plugin (`../../plugins/woocommerce/` or <https://github.com/woocommerce/woocommerce.git>). All theme files from the plugin are converted to Bootstrap 5 structures and styles.
## Technical Stack
- **Language:** PHP 8.3.x
- **Framework:** Latest WordPress Theme API
- **Template Engine:** Twig 3.0 (via Composer)
- **WordPress Base Theme:** wp-bootstrap
- **Frontend:** Bootstrap 5 JavaScript & Vanilla JavaScript
- **Styling:** Bootstrap 5 & Custom CSS (if necessary)
- **Dependency Management:** Composer (PHP), npm (JS/CSS)
- **Internationalization:** WordPress i18n (.pot/.po/.mo files)
### Security Best Practices
- All user inputs are sanitized (integers for quantities/prices)
- Nonce verification on form submissions
- Output escaping in templates (`esc_attr`, `esc_html`, `esc_js`)
- Direct file access prevention via `ABSPATH` check
- XSS-safe DOM construction in JavaScript (no `innerHTML` with user data)
- `json_encode|raw` in JS context is correct (not XSS) -- Twig auto-escaping would break JSON
- `styles|raw` in PDF exports is backend-generated static CSS, documented with comments
### Translation Ready
All user-facing strings use:
```php
__('Text to translate', 'wc-bootstrap')
_e('Text to translate', 'wc-bootstrap')
```
Text domain: `wc-bootstrap`
#### Text Domain Strategy
<!-- Choose ONE of these strategies based on your project needs: -->
**Option A -- Plugin domain (recommended when plugin provides Twig functions):**
All strings in theme templates use the plugin's `'woocommerce'` domain. This is necessary when the plugin registers `__()` / `_n()` as Twig functions with a default domain, because `load_child_theme_textdomain()` does not resolve at Twig render time.
**Rule:** Always use `'woocommerce'` as the text domain in all Twig templates and PHP files.
**Option B -- Theme domain (when theme handles its own Twig rendering):**
Theme templates use the theme's own `'wc-bootstrap'` domain. Ensure `load_child_theme_textdomain()` is called before Twig rendering.
#### Available Translations
<!-- List your supported locales here -->
- `en_US` -- English (base language, .pot template)
- `de_DE` -- German (Germany, formal)
- `de_CH` -- German (Switzerland, formal)
- `fr_FR` -- French (France)
Translation file naming convention: `wc-bootstrap-{locale}.po` (e.g., `wc-bootstrap-de_CH.po`)
Compiled `.mo` files are built by the CI/CD pipeline during releases. For local development:
```bash
for po in languages/wc-bootstrap-*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
```
#### Updating Translations (Fast JSON Workflow)
##### Step 1 -- Regenerate `.pot` files
wp-bootstrap (PHP sources only):
```bash
docker exec woocommerce-wordpress wp i18n make-pot \
/var/www/html/wp-content/themes/wp-bootstrap \
/var/www/html/wp-content/themes/wp-bootstrap/languages/wp-bootstrap.pot \
--allow-root
```
Child theme (PHP + Twig). Use a Twig extractor script that scans `.html.twig` files for `__()`, `_e()`, `_n()`, `_x()` calls, generates `twig-extracted.pot`, then merges with WP-CLI PHP extraction:
```bash
# Inside languages/ directory:
python3 twig-extractor.py # generates twig-extracted.pot
docker exec woocommerce-wordpress wp i18n make-pot \
/var/www/html/wp-content/themes/wc-bootstrap \
/var/www/html/wp-content/themes/wc-bootstrap/languages/php-extracted.pot \
--allow-root
msgcat --use-first php-extracted.pot twig-extracted.pot \
-o wc-bootstrap.pot
rm php-extracted.pot
```
##### Step 2 -- Merge new strings into all `.po` files
```bash
for locale in de_CH de_DE fr_FR; do
msgmerge --update --backup=none --no-fuzzy-matching \
wc-bootstrap-${locale}.po wc-bootstrap.pot
done
```
##### Step 3 -- Translate and apply
Extract untranslated strings per locale into JSON, translate (via AI or manual), then apply with `languages/patch-po.py`.
German variant derivation rules:
- `de_CH` from `de_DE`: replace `ss` where Swiss German uses `ss` instead of `ß`
- `de_DE_informal` from `de_DE`: `Sie` -> `du`, `Ihr` -> `dein`, etc.
- `de_CH_informal`: combine both transformations
##### Step 4 -- Validate and compile
```bash
for po in languages/wc-bootstrap-*.po; do msgfmt --statistics "$po" -o /dev/null; done
for po in languages/wc-bootstrap-*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
```
### Important Git Notes
- Default branch while development is `dev`
- Create releases from branch `main` after merging branch `dev`
- Tags should use format `vX.X.X` (e.g., `v1.1.22`), start with v0.1.0
- Use annotated tags (`-a`) not lightweight tags
- **ALWAYS push tags to origin** -- CI/CD triggers on tag push
- Commit messages should follow the established format with Claude Code attribution
- `.claude/settings.local.json` changes are typically local-only (stash before rebasing)
### CRITICAL -- Release Workflow
On every new version, ALWAYS execute this complete workflow:
```bash
# 1. Commit changes to dev branch
git add <files>
git commit -m "Description of changes (vX.X.X)"
# 2. Merge dev to main
git checkout main
git merge dev --no-edit
# 3. Create annotated tag
git tag -a vX.X.X -m "Version X.X.X - Brief description"
# 4. Push everything to origin
git push origin dev main vX.X.X
# 5. Switch back to dev for continued development
git checkout dev
```
Never skip any of these steps. The release is not complete until all branches and the tag are pushed to origin.
### CRITICAL -- Cross-Project Release Order
When a release touches both the plugin and the theme, ALWAYS release in this order:
1. **Theme first** -- full release workflow (commit -> merge -> tag -> push)
2. **Plugin second** -- update plugin's Dockerfile/config to reference the new theme version, then full release workflow
Reason: the plugin's build references the theme version. If the plugin is released first with a new theme version number, the CI build will attempt to download a theme release that does not yet exist and fail.
---
## For AI Assistants
When starting a new session on this project:
1. Read this CLAUDE.md file first
2. Semantic versioning follows the `MAJOR.MINOR.BUGFIX` pattern
3. Check git log for recent changes
4. Verify you're on the `dev` branch before making changes
5. Run `git submodule update --init --recursive` if lib/ is empty (only if submodules present)
6. Run `composer install` if vendor/ is missing
7. Test changes before committing
8. Follow commit message format with Claude Code attribution
9. Update this session history section with learnings
10. Never commit backup files (`*.po~`, `*.bak`, etc.) - check `git status` before committing
11. Follow markdown linting rules (see below)
Always refer to this document when starting work on this project.
### Markdown Linting Rules
1. **MD031**: Blank lines before and after fenced code blocks
2. **MD056**: Table separators must have matching column counts
3. **MD009**: No trailing spaces
4. **MD012**: No multiple consecutive blank lines
5. **MD040**: Fenced code blocks should have a language specified
6. **MD032**: Lists surrounded by blank lines
7. **MD034**: Bare URLs in angle brackets or markdown link syntax
8. **MD013**: Disabled (line length)
9. **MD024**: `siblings_only` mode
## Known Pitfalls & Key Learnings
Recurring bugs and non-obvious behaviours discovered across sessions. **Read this before starting any task.**
### Template Override Mechanism
- Plugin's `Template` class uses Twig `FilesystemLoader`. The `inc/TemplateOverride.php` hooks `init` at priority 20 (after plugin init at 0) and uses `prependPath()` to give child theme templates priority.
- Plugin templates in `../../plugins/woocommerce/templates/` are overridden by matching files in `templates/`.
### Twig Compatibility
- **`|values` filter does NOT exist in Twig 3.x** -- use loop-based sum instead. Twig 4.x only.
- **Twig autoescape + WordPress escape filters = double encoding** -- register all `esc_*` filters with `['is_safe' => ['html']]` option in the plugin's `Template.php`.
- **`wp i18n make-pot` does NOT scan Twig templates** -- any string used exclusively in `.html.twig` files must be manually added to the `.pot` file.
- **`#, fuzzy` silently skips translations at runtime** -- always remove fuzzy flags after verifying translations.
### Bootstrap 5 vs Plugin CSS Conflicts
- Plugin CSS may define its own grid systems that override Bootstrap's flexbox `row row-cols-*`. Add theme CSS overrides as needed.
- **CSS dependency chain**: `woocommerce` -> child theme overrides. Ensures correct cascade.
- jQuery `.show()`/`.hide()` cannot override Bootstrap `!important` (`d-none`). Toggle both class and inline style.
- `overflow: visible !important` on `.wp-block-navigation__container` is essential for dropdowns inside block theme navigation.
### Double Heading Prevention
- Parent theme (`wp-bootstrap`) conditionally skips its `<h1>` when `post.title` is empty.
- Plugin passes empty `post.title` to parent theme's Twig system.
- Child theme layout templates always render their own `<h1>` with `page_title` context.
### Theme Wrapping (`_wrapped` / `_theme_wrapped`)
- `_wrapped` is set in `base.html.twig` when `_theme_wrapped` is true (parent theme provides page shell).
- **Structural blocks** (wrapper, breadcrumbs, sidebar, `<h1>`) are suppressed when wrapped.
- **Functional blocks** (`{% block scripts %}`) must ALWAYS render -- they contain AJAX handlers, JS initialization etc.
### Plugin JS Compatibility
- Theme must preserve plugin's JS-bound CSS class names (e.g., repeater fields, interactive widgets).
- Theme uses plugin's global JS objects for AJAX URLs, nonces, and i18n strings.
- Bootstrap 5 `data-bs-toggle` attributes replace plugin's custom dropdown mechanisms.
### Security
- Security audit checklist (OWASP Top-10): open redirect validation, `|esc_url`/`|esc_attr`/`|esc_js` on all dynamic output, `|wp_kses_post` for rich text, no `innerHTML` with dynamic data.
## Multi-Project Architecture
This child theme is part of a three-project ecosystem. Each project is a separate git repo with its own `CLAUDE.md` and should be worked on in **separate Claude Code sessions**.
### Projects
| Project | Type | Location | Repository |
| ------- | ---- | -------- | ---------- |
| woocommerce | Plugin | `wp-content/plugins/woocommerce/` | <https://github.com/woocommerce/woocommerce.git> |
| wp-bootstrap | Parent Theme | `wp-content/themes/wp-bootstrap/` | <https://src.bundespruefstelle.ch/magdev/wp-bootstrap> |
| wc-bootstrap | Child Theme | `wp-content/themes/wc-bootstrap/` | <ssh://git@src.bundespruefstelle.ch:2022/magdev/wc-bootstrap.git> |
### Dependency Chain
```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)
```
### Cross-Project Workflow
1. **One session per project.** Each session reads its own `CLAUDE.md` and stays within its own git repo.
2. **Plugin changes first.** When a change affects templates consumed by the theme, make the plugin change first, then switch to the theme session.
3. **Communicate changes explicitly.** When switching sessions, describe what changed upstream (new variables, renamed classes, new templates, etc.).
4. **Shared Docker environment.** All three projects are bind-mounted into the same Docker container, so changes are visible live without rebuilds.
### Template Contract (Plugin -> Theme)
The child theme overrides plugin Twig templates by prepending its own `templates/` directory to the Twig `FilesystemLoader` via `TemplateOverride` class (hooks `init` priority 20, after plugin init at priority 0).
#### Twig Template Locations
- **Plugin base templates:** `wp-content/plugins/woocommerce/templates/`
- **Theme overrides:** `wp-content/themes/wc-bootstrap/templates/` (mirrors plugin structure)
- **Resolution order:** Theme templates take priority over plugin templates (Twig `prependPath`)
### Theme Contract (Parent -> Child)
The child theme inherits from `wp-bootstrap` via WordPress `Template: wp-bootstrap` declaration.
- **Bootstrap 5:** CSS framework and JS loaded by parent theme
- **Bootstrap Icons:** Web font available via parent theme's SCSS build
- **Twig rendering:** Parent theme's `TwigService` handles frontend rendering
- **Dark mode:** Parent handles via `data-bs-theme` attribute; child theme inherits
- **Style variations:** Color palettes available from parent, bridged to Bootstrap CSS custom properties
## Architecture Decisions
- **Edit forms** use 3+9 column layout: `col-lg-3` sticky sidebar (progress indicator, section nav) + `col-lg-9` card-based sections.
- **Detail pages** use 8+4 column layout with sticky sidebar.
- **Cards** use `<article class="card h-100">` with `stretched-link`.
- **Form layout** uses centered `col-lg-8 col-xl-7` with card + shadow for auth forms.
- **Mobile patterns**: offcanvas for search filters; navbar uses Bootstrap `offcanvas-lg` responsive pattern.
- **Email templates**: use custom CSS class prefix (not Bootstrap) to avoid email client conflicts.
- **PDF exports**: inline styles aligned with Bootstrap color palette (no external CSS in PDF renderers).
## Version History
Current version: **v0.1.0**
## Session History
<!-- AI assistants: document key learnings and session outcomes here -->

68
README.md Normal file
View File

@@ -0,0 +1,68 @@
# WooCommerce Bootstrap
A WordPress child theme of [WP Bootstrap](https://src.bundespruefstelle.ch/magdev/wp-bootstrap) that overrides all [WooCommerce](https://github.com/woocommerce/woocommerce.git) plugin templates with Bootstrap 5 structures and styling.
## Requirements
- WordPress 6.7+
- PHP 8.3+
- [WP Bootstrap](https://src.bundespruefstelle.ch/magdev/wp-bootstrap) theme (parent)
- [WooCommerce](https://github.com/woocommerce/woocommerce.git) plugin
## Installation
1. Install and activate the parent theme `wp-bootstrap`
2. Install and activate the `woocommerce` plugin
3. Upload `wc-bootstrap` to `wp-content/themes/`
4. Run `composer install` in the theme directory
5. Activate the theme in WordPress Admin > Appearance > Themes
## What This Theme Does
The WooCommerce plugin ships with its own Twig templates using custom CSS classes. This child theme overrides those templates to use Bootstrap 5 components, ensuring visual consistency with the WP Bootstrap parent theme.
### Key Features
- Bootstrap 5 markup for all plugin templates
- Responsive design inheriting WP Bootstrap's grid system
- Dark mode support via WP Bootstrap's theme toggle
- Translation-ready
## Development
### Directory Structure
```txt
wc-bootstrap/
├── assets/css/ # Custom CSS overrides
├── assets/js/ # Custom JavaScript
├── inc/ # PHP classes (PSR-4)
├── languages/ # Translation files
├── templates/ # Bootstrap 5 Twig template overrides
├── composer.json
├── functions.php
└── style.css
```
### Building Translations
```bash
for po in languages/wc-bootstrap-*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
```
## Releases
Releases are automated via Gitea Actions. Push a tag matching `vX.X.X` to trigger a release build.
```bash
git tag -a v0.1.0 -m "Version 0.1.0 - Initial release"
git push origin v0.1.0
```
## License
GPL-2.0-or-later
## Author
Marco Grätsch - <https://src.bundespruefstelle.ch/magdev>

182
SETUP.md Normal file
View File

@@ -0,0 +1,182 @@
# 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

@@ -0,0 +1,85 @@
/**
* WooCommerce Bootstrap - Bootstrap 5 Overrides
*
* Provides Bootstrap 5 styling for any plugin CSS classes
* that may be injected by the plugin's JavaScript or inline markup.
*
* CSS dependency chain (lowest to highest priority):
* 1. wp-bootstrap (parent theme)
* 2. woocommerce (plugin styles)
* 3. wc-bootstrap-style (child theme style.css)
* 4. wc-bootstrap-overrides (this file)
*
* @package WcBootstrap
* @since 0.1.0
*/
/* ==========================================================================
Button Overrides
Map plugin button classes to Bootstrap button styles.
========================================================================== */
/* Example: Map plugin .my-button to Bootstrap styling
.my-button {
display: inline-block;
font-weight: 400;
line-height: 1.5;
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
border-radius: var(--bs-border-radius);
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}
*/
/* ==========================================================================
Notification Overrides
Map plugin notification classes to Bootstrap alert styles.
========================================================================== */
/* Example: Map plugin .my-notification to Bootstrap alert
.my-notification {
position: relative;
padding: 1rem 1rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: var(--bs-border-radius);
}
*/
/* ==========================================================================
Dark Mode Overrides
Fix any plugin elements that don't adapt to Bootstrap's dark mode.
========================================================================== */
/* Bootstrap 5 dark mode uses data-bs-theme="dark" attribute on <html> */
[data-bs-theme="dark"] {
/* Example overrides for dark mode compatibility */
}
/* ==========================================================================
Sticky Header
Shadow effect when header is in stuck position.
========================================================================== */
header.sticky-top.is-stuck {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.2s ease;
}
[data-bs-theme="dark"] header.sticky-top.is-stuck {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
/* ==========================================================================
Block Navigation Fix
Required for dropdown menus inside WordPress block navigation.
========================================================================== */
.wp-block-navigation__container {
overflow: visible !important;
}

0
assets/js/.gitkeep Normal file
View File

25
composer.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "magdev/wc-bootstrap",
"description": "WordPress child theme for WP Bootstrap with WooCommerce template overrides using Bootstrap 5",
"type": "wordpress-theme",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Marco Grätsch",
"email": "magdev3.0@gmail.com",
"homepage": "https://src.bundespruefstelle.ch/magdev"
}
],
"require": {
"php": ">=8.3"
},
"autoload": {
"psr-4": {
"WcBootstrap\\": "inc/"
}
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
}
}

180
functions.php Normal file
View File

@@ -0,0 +1,180 @@
<?php
/**
* WooCommerce Bootstrap functions and definitions.
*
* Child theme of WP Bootstrap that overrides WooCommerce plugin templates
* with Bootstrap 5 structures and styling.
*
* @package WcBootstrap
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Define theme constants.
*
* CRITICAL: WordPress reads the version from TWO places:
* 1. style.css header "Version:" — WordPress uses THIS for admin display
* 2. This PHP constant — used internally by the theme
* Both MUST be updated on every release.
*/
define( 'WC_BOOTSTRAP_VERSION', '0.1.0' );
define( 'WC_BOOTSTRAP_PATH', get_stylesheet_directory() . '/' );
define( 'WC_BOOTSTRAP_URL', get_stylesheet_directory_uri() . '/' );
/**
* Load Composer autoloader if present.
*/
if ( file_exists( WC_BOOTSTRAP_PATH . 'vendor/autoload.php' ) ) {
require_once WC_BOOTSTRAP_PATH . 'vendor/autoload.php';
}
/**
* Sets up theme defaults and registers support for various WordPress features.
*/
function wc_bootstrap_setup(): void {
// Make theme available for translation.
load_child_theme_textdomain( 'wc-bootstrap', WC_BOOTSTRAP_PATH . 'languages' );
}
add_action( 'after_setup_theme', 'wc_bootstrap_setup' );
/**
* Register plugin template overrides.
*
* Prepends the child theme's templates/ directory to the plugin's Twig loader,
* so child theme templates take priority over plugin templates.
*/
function wc_bootstrap_register_template_override(): void {
$override = new \WcBootstrap\TemplateOverride();
$override->register();
}
add_action( 'after_setup_theme', 'wc_bootstrap_register_template_override' );
/**
* Enqueue child theme styles.
*
* Loads parent theme stylesheet first, then child theme overrides.
* CSS cascade order:
* 1. wp-bootstrap (parent)
* 2. woocommerce (plugin styles)
* 3. wc-bootstrap-style (child theme style.css)
* 4. wc-bootstrap-overrides (plugin CSS overrides)
*/
function wc_bootstrap_enqueue_styles(): void {
$theme_version = wp_get_theme()->get( 'Version' );
// Enqueue parent theme stylesheet.
wp_enqueue_style(
'wp-bootstrap-style',
get_template_directory_uri() . '/assets/css/style.min.css',
array(),
wp_get_theme( 'wp-bootstrap' )->get( 'Version' )
);
// Enqueue child theme stylesheet.
wp_enqueue_style(
'wc-bootstrap-style',
get_stylesheet_directory_uri() . '/style.css',
array( 'wp-bootstrap-style' ),
$theme_version
);
// Enqueue plugin Bootstrap override styles.
// Depend on plugin stylesheets so overrides always load after plugin CSS.
wp_enqueue_style(
'wc-bootstrap-overrides',
get_stylesheet_directory_uri() . '/assets/css/wc-bootstrap.css',
array( 'wc-bootstrap-style', 'woocommerce' ),
$theme_version
);
}
add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_styles' );
/**
* Handle plugin page rendering via plugin render filter.
*
* Delegates page rendering to the parent theme's TwigService so that plugin pages
* share the same page shell (header, footer, layout) as native WordPress pages.
* Falls back to letting the plugin handle rendering if the parent theme is not available.
*
* @param bool $rendered Whether the page has been rendered.
* @param string $content Pre-rendered plugin HTML content.
* @param array $context Plugin template context.
* @return bool True if rendering was handled, false to let plugin use fallback.
*
* @since 0.1.0
*/
function wc_bootstrap_render_page( bool $rendered, string $content, array $context ): bool {
if ( ! class_exists( '\WPBootstrap\Twig\TwigService' )
|| ! class_exists( '\WPBootstrap\Template\ContextBuilder' ) ) {
return false; // Can't render, let plugin use its own fallback
}
$context_builder = new \WPBootstrap\Template\ContextBuilder();
$theme_context = $context_builder->build();
$twig = \WPBootstrap\Twig\TwigService::getInstance();
// Inject plugin content as the page post content so page.html.twig renders it
// inside the standard content block. Title is empty so the parent theme does not
// render its own <h1> — plugin templates handle their own headings.
$theme_context['post'] = array_merge(
$theme_context['post'] ?? [],
[
'content' => $content,
'title' => '',
'thumbnail' => '',
]
);
echo $twig->render( 'pages/page.html.twig', $theme_context );
return true;
}
add_filter( 'woocommerce_render_page', 'wc_bootstrap_render_page', 10, 3 );
/**
* Signal to the plugin that its content will be wrapped by the parent theme.
*
* When the parent theme's TwigService is available, the plugin templates should skip
* their outer wrapper elements (breadcrumbs, sidebar, page shell) to avoid double-wrapping.
*
* @param bool $wrapped Whether content is theme-wrapped.
* @return bool
*
* @since 0.1.0
*/
function wc_bootstrap_is_wrapped( bool $wrapped ): bool {
if ( class_exists( '\WPBootstrap\Twig\TwigService' ) ) {
return true;
}
return $wrapped;
}
add_filter( 'woocommerce_is_theme_wrapped', 'wc_bootstrap_is_wrapped' );
/**
* Add sticky header scroll shadow behavior.
*
* Toggles an 'is-stuck' class on the navbar when the page is scrolled,
* adding a subtle box-shadow to visually separate the header from content.
*
* @since 0.1.0
*/
function wc_bootstrap_sticky_header_script(): void {
?>
<script>
(function() {
var header = document.querySelector('header.sticky-top');
if (!header) return;
var onScroll = function() {
header.classList.toggle('is-stuck', window.scrollY > 0);
};
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
})();
</script>
<?php
}
add_action( 'wp_footer', 'wc_bootstrap_sticky_header_script' );

76
inc/TemplateOverride.php Normal file
View File

@@ -0,0 +1,76 @@
<?php
/**
* Template Override
*
* Hooks into the plugin's Twig FilesystemLoader to prepend
* the child theme's templates/ directory, allowing template overrides.
*
* @package WcBootstrap
* @since 0.1.0
*/
namespace WcBootstrap;
// IMPORTANT: Update these imports to match your plugin's actual class names.
use Magdev\Woocommerce\Frontend\Template;
use Twig\Loader\FilesystemLoader;
class TemplateOverride {
/**
* Path to the child theme's templates directory.
*
* @var string
*/
private string $template_path;
/**
* Constructor.
*/
public function __construct() {
$this->template_path = WC_BOOTSTRAP_PATH . 'templates';
}
/**
* Register the template override with WordPress hooks.
*
* Must be called after the plugin's Template singleton is initialized
* (plugin inits at 'init' priority 0).
*
* @return void
*/
public function register(): void {
add_action( 'init', [ $this, 'override_template_paths' ], 20 );
}
/**
* Prepend the child theme's templates directory to the Twig loader.
*
* This makes Twig look in the child theme's templates/ first,
* falling back to the plugin's templates/ if not found.
*
* @return void
*/
public function override_template_paths(): void {
if ( ! class_exists( Template::class ) ) {
return;
}
if ( ! is_dir( $this->template_path ) ) {
return;
}
try {
$twig = Template::get_instance()->get_twig();
$loader = $twig->getLoader();
if ( $loader instanceof FilesystemLoader ) {
$loader->prependPath( $this->template_path );
}
} catch ( \Exception $e ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'WooCommerce Bootstrap: Failed to register template override - ' . $e->getMessage() );
}
}
}
}

0
languages/.gitkeep Normal file
View File

16
style.css Normal file
View File

@@ -0,0 +1,16 @@
/*
Theme Name: WooCommerce Bootstrap
Theme URI: ssh://git@src.bundespruefstelle.ch:2022/magdev/wc-bootstrap.git
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.
Requires at least: 6.7
Tested up to: 6.7
Requires PHP: 8.3
Version: 0.1.0
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Template: wp-bootstrap
Text Domain: wc-bootstrap
Tags: one-column, custom-colors, custom-menu, custom-logo, editor-style, featured-images, full-site-editing, translation-ready, wide-blocks, block-styles, accessibility-ready
*/

76
templates/base.html.twig Normal file
View File

@@ -0,0 +1,76 @@
{#
# Base Template (Bootstrap 5 Override)
#
# Overrides the plugin's base.html.twig with Bootstrap 5 components.
# Provides the basic structure and block definitions.
#
# When _theme_wrapped is true, the parent theme already provides the page
# shell (header, footer, container). The outer wrapper, breadcrumbs,
# sidebar, and head blocks are skipped to avoid double-wrapping.
# The scripts block is always rendered because child templates use it for
# page-specific inline JavaScript (e.g. AJAX form handlers).
#
# @package WcBootstrap
# @since 0.1.0
#}
{% set _wrapped = _theme_wrapped is defined and _theme_wrapped %}
{% if not _wrapped %}{% block head %}{% endblock %}{% endif %}
{% if not _wrapped %}<div class="container my-4">{% endif %}
{% block notifications %}
{% if notifications is defined and notifications|length > 0 %}
{% for notification in notifications %}
<div class="alert alert-{{ notification.type|default('info') }} alert-dismissible fade show" role="alert">
{{ notification.message|wp_kses_post }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('Close') }}"></button>
</div>
{% endfor %}
{% endif %}
{% if flash_messages is defined %}
{% for type, messages in flash_messages %}
{% for message in messages %}
<div class="alert alert-{{ type }} alert-dismissible fade show" role="alert">
{{ message|wp_kses_post }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('Close') }}"></button>
</div>
{% endfor %}
{% endfor %}
{% endif %}
{% endblock %}
{% if not _wrapped %}<main>{% endif %}
{% block breadcrumbs %}
{% if not _wrapped and breadcrumbs is defined and breadcrumbs|length > 0 %}
<nav aria-label="{{ __('Breadcrumb') }}">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ home_url() }}">{{ __('Home') }}</a>
</li>
{% for crumb in breadcrumbs %}
{% if crumb.url %}
<li class="breadcrumb-item">
<a href="{{ crumb.url|esc_url }}">{{ crumb.label|esc_html }}</a>
</li>
{% else %}
<li class="breadcrumb-item active" aria-current="page">{{ crumb.label|esc_html }}</li>
{% endif %}
{% endfor %}
</ol>
</nav>
{% endif %}
{% endblock %}
{% block content %}{% endblock %}
{% if not _wrapped %}</main>{% endif %}
{% block sidebar %}{% endblock %}
{% if not _wrapped %}</div>{% endif %}
{% block scripts %}{% endblock %}

View File

@@ -0,0 +1,36 @@
{#
# Card Component (Bootstrap 5)
#
# Reusable card component for list/grid display.
#
# Expected context:
# post.title - Card title
# post.permalink - Card link URL
# post.excerpt - Card description text
# post.thumbnail - Optional thumbnail URL
#
# @package WcBootstrap
# @since 0.1.0
#}
<article class="card h-100 shadow-sm">
{% if post.thumbnail is defined and post.thumbnail %}
<img src="{{ post.thumbnail|esc_url }}" class="card-img-top" alt="{{ post.title|esc_attr }}">
{% endif %}
<div class="card-body">
<h3 class="card-title h5">
<a href="{{ post.permalink|esc_url }}" class="stretched-link text-decoration-none">
{{ post.title|esc_html }}
</a>
</h3>
{% if post.excerpt is defined and post.excerpt %}
<p class="card-text text-body-secondary">{{ post.excerpt|wp_kses_post }}</p>
{% endif %}
{% block card_meta %}{% endblock %}
</div>
{% block card_footer %}{% endblock %}
</article>

View File

@@ -0,0 +1,49 @@
{#
# Pagination Component (Bootstrap 5)
#
# Renders pagination navigation for search/archive pages.
#
# Expected context:
# current_page - Current page number (1-based)
# max_pages - Total number of pages
#
# @package WcBootstrap
# @since 0.1.0
#}
{% if max_pages is defined and max_pages > 1 %}
<nav aria-label="{{ __('Page navigation') }}" class="mt-4">
<ul class="pagination justify-content-center">
{# Previous button #}
<li class="page-item{% if current_page <= 1 %} disabled{% endif %}">
<a class="page-link" href="?page={{ current_page - 1 }}" aria-label="{{ __('Previous') }}">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{# Page numbers #}
{% for i in 1..max_pages %}
{% if i == current_page %}
<li class="page-item active" aria-current="page">
<span class="page-link">{{ i }}</span>
</li>
{% elseif i == 1 or i == max_pages or (i >= current_page - 2 and i <= current_page + 2) %}
<li class="page-item">
<a class="page-link" href="?page={{ i }}">{{ i }}</a>
</li>
{% elseif i == current_page - 3 or i == current_page + 3 %}
<li class="page-item disabled">
<span class="page-link">&hellip;</span>
</li>
{% endif %}
{% endfor %}
{# Next button #}
<li class="page-item{% if current_page >= max_pages %} disabled{% endif %}">
<a class="page-link" href="?page={{ current_page + 1 }}" aria-label="{{ __('Next') }}">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>
{% endif %}

View File

@@ -0,0 +1,28 @@
{#
# Account Layout (Bootstrap 5 Override)
#
# Layout for user account/settings pages.
#
# @package WcBootstrap
# @since 0.1.0
#}
{% extends "base.html.twig" %}
{% block content %}
<div class="mb-4">
{% block account_header %}
<header class="mb-4">
<h1>{{ page_title|default('')|esc_html }}</h1>
{% block account_description %}
{% if page_description is defined %}
<p class="lead text-body-secondary">{{ page_description|wp_kses_post }}</p>
{% endif %}
{% endblock %}
</header>
{% endblock %}
{% block account_content %}{% endblock %}
</div>
{% endblock %}

View File

@@ -0,0 +1,61 @@
{#
# Archive/Search Layout (Bootstrap 5 Override)
#
# Layout for search results and archive pages.
# 3+9 column split: filter sidebar + results grid.
#
# @package WcBootstrap
# @since 0.1.0
#}
{% extends "base.html.twig" %}
{% block content %}
<div class="mb-4">
{% block archive_header %}
<header class="mb-4">
<h1>{{ page_title|default(__('Search Results'))|esc_html }}</h1>
{% if results_count is defined %}
<p class="text-body-secondary">
{{ _n('%d result found', '%d results found', results_count)|format(results_count) }}
</p>
{% endif %}
</header>
{% endblock %}
<div class="row">
{% block archive_sidebar %}
<aside class="col-lg-3 mb-4 mb-lg-0">
{% block search_filters %}{% endblock %}
</aside>
{% endblock %}
<div class="col-lg-9">
{% block archive_results %}
{% if results is defined and results|length > 0 %}
<div class="row row-cols-1 row-cols-md-2 g-4">
{% for post in results %}
<div class="col">
{% block result_card %}
{% include 'components/card.html.twig' with {post: post} %}
{% endblock %}
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info" role="alert">
<p class="mb-0">{{ __('No results found. Try adjusting your filters.') }}</p>
</div>
{% endif %}
{% endblock %}
{% block pagination %}
{% if max_pages is defined and max_pages > 1 %}
{% include 'components/pagination.html.twig' %}
{% endif %}
{% endblock %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,98 @@
{#
# Form Layout (Bootstrap 5 Override)
#
# Layout for registration and editing forms.
# Uses a centered card layout with consistent Bootstrap 5 styling.
#
# @package WcBootstrap
# @since 0.1.0
#}
{% extends "base.html.twig" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8 col-xl-7">
{% block form_header %}
<header class="text-center mb-4">
<h1 class="mb-2">{{ page_title|esc_html }}</h1>
{% block form_description %}
{% if form_description is defined %}
<p class="lead text-body-secondary">{{ form_description|wp_kses_post }}</p>
{% endif %}
{% endblock %}
</header>
{% endblock %}
{% block form_content %}
<div class="card shadow-sm">
<div class="card-body p-4">
<form method="post" action="{{ form_action|default('') }}" enctype="multipart/form-data" novalidate>
{{ wp_nonce_field(nonce_action|default('form_nonce'), nonce_name|default('_wpnonce')) }}
{% if form_type is defined %}
<input type="hidden" name="form_type" value="{{ form_type|esc_attr }}">
{% endif %}
{% if post_id is defined %}
<input type="hidden" name="post_id" value="{{ post_id }}">
{% endif %}
{% block form_errors %}
{% if errors is defined and errors|length > 0 %}
<div class="alert alert-danger" role="alert">
<h2 class="alert-heading h6">{{ __('Please correct the following errors:') }}</h2>
<ul class="mb-0">
{% for error in errors %}
<li>{{ error|esc_html }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock %}
{% block form_fields %}{% endblock %}
{% block form_actions %}
<div class="d-grid gap-2 mt-4">
<button type="submit" class="btn btn-primary btn-lg">
{{ submit_label|default(__('Submit')) }}
</button>
{% if cancel_url is defined %}
<a href="{{ cancel_url|esc_url }}" class="btn btn-outline-secondary btn-lg">
{{ __('Cancel') }}
</a>
{% endif %}
</div>
{% endblock %}
</form>
</div>
{% block form_footer %}{% endblock %}
</div>
{% endblock %}
{% block form_sidebar %}{% endblock %}
</div>
</div>
{% endblock %}
{% block scripts %}
{{ parent() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
var form = document.querySelector('form');
if (form) {
form.addEventListener('submit', function(e) {
var submitBtn = form.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>' + submitBtn.textContent;
}
});
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,32 @@
{#
# Page Layout (Bootstrap 5 Override)
#
# Layout for standard content pages.
#
# @package WcBootstrap
# @since 0.1.0
#}
{% extends "base.html.twig" %}
{% block content %}
<div class="mb-4">
{% block page_header %}
<header class="mb-4">
<h1>{{ page_title|default('')|esc_html }}</h1>
{% block page_description %}
{% if page_description is defined %}
<p class="lead text-body-secondary">{{ page_description|wp_kses_post }}</p>
{% endif %}
{% endblock %}
</header>
{% endblock %}
{% block page_content %}
<div>
{% block content_inner %}{% endblock %}
</div>
{% endblock %}
</div>
{% endblock %}

View File

@@ -0,0 +1,53 @@
{#
# Single Post Layout (Bootstrap 5 Override)
#
# Layout for single detail pages.
# Supports an optional sidebar via the article_sidebar block.
# When no sidebar content is provided, content spans full width.
#
# @package WcBootstrap
# @since 0.1.0
#}
{% extends "base.html.twig" %}
{% block content %}
<article>
{% block article_header %}
<header class="d-flex flex-wrap align-items-start gap-3 mb-4">
{% if has_thumbnail %}
<div class="flex-shrink-0">
<img src="{{ thumbnail }}" alt="{{ title|esc_attr }}" class="rounded img-fluid" style="max-width: 150px;">
</div>
{% endif %}
<div class="flex-grow-1">
<h1>{{ title|esc_html }}</h1>
{% block article_meta %}{% endblock %}
</div>
{% block article_actions %}{% endblock %}
</header>
{% endblock %}
<div class="row g-4">
<div class="{% block content_col_class %}col-lg-8{% endblock %}">
{% block article_content %}
<div>
{{ original_content|default('')|wp_kses_post }}
</div>
{% endblock %}
</div>
{% set _sidebar_html %}{% block article_sidebar %}{% endblock %}{% endset %}
{% if _sidebar_html|trim %}
<aside class="col-lg-4">
{{ _sidebar_html|raw }}
</aside>
{% endif %}
</div>
{% block article_footer %}{% endblock %}
</article>
{% endblock %}