You've already forked wc-bootstrap
Initial theme scaffold from wp-theme-template
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
210
.gitea/workflows/release.yml
Normal file
210
.gitea/workflows/release.yml
Normal 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
32
.gitignore
vendored
Normal 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
17
CHANGELOG.md
Normal 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
311
CLAUDE.md
Normal 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
68
README.md
Normal 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
182
SETUP.md
Normal 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` |
|
||||||
85
assets/css/wc-bootstrap.css
Normal file
85
assets/css/wc-bootstrap.css
Normal 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
0
assets/js/.gitkeep
Normal file
25
composer.json
Normal file
25
composer.json
Normal 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
180
functions.php
Normal 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
76
inc/TemplateOverride.php
Normal 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
0
languages/.gitkeep
Normal file
16
style.css
Normal file
16
style.css
Normal 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
76
templates/base.html.twig
Normal 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 %}
|
||||||
36
templates/components/card.html.twig
Normal file
36
templates/components/card.html.twig
Normal 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>
|
||||||
49
templates/components/pagination.html.twig
Normal file
49
templates/components/pagination.html.twig
Normal 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">«</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">…</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">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
28
templates/layouts/account.html.twig
Normal file
28
templates/layouts/account.html.twig
Normal 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 %}
|
||||||
61
templates/layouts/archive.html.twig
Normal file
61
templates/layouts/archive.html.twig
Normal 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 %}
|
||||||
98
templates/layouts/form.html.twig
Normal file
98
templates/layouts/form.html.twig
Normal 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 %}
|
||||||
32
templates/layouts/page.html.twig
Normal file
32
templates/layouts/page.html.twig
Normal 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 %}
|
||||||
53
templates/layouts/single.html.twig
Normal file
53
templates/layouts/single.html.twig
Normal 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 %}
|
||||||
Reference in New Issue
Block a user