6 Commits

Author SHA1 Message Date
e3e9b9f2be fix: make page title <h1> conditional to prevent double headings (v1.0.3)
All checks were successful
Create Release Package / PHP Lint (push) Successful in 1m3s
Create Release Package / Build Release (push) Successful in 1m41s
When plugins inject content via TwigService with empty post.title,
the theme's <h1> is now skipped. Prevents duplicate headings on
plugin-rendered pages that provide their own titles.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-11 09:54:16 +01:00
702c0c35f4 fix: add title-tag theme support for proper <title> output (v1.0.2)
All checks were successful
Create Release Package / PHP Lint (push) Successful in 1m14s
Create Release Package / Build Release (push) Successful in 1m42s
Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-10 16:00:10 +01:00
3620d9b1d1 v1.0.1 - Integrate Bootstrap Icons web font
All checks were successful
Create Release Package / PHP Lint (push) Successful in 55s
Create Release Package / Build Release (push) Successful in 1m35s
Add bootstrap-icons npm package with SCSS import and font file copy
build step. All 2,000+ icons available via CSS classes (bi bi-*) in
both frontend and block editor.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-09 09:28:26 +01:00
5268289782 v1.0.0 - Release: widget area, documentation refresh
All checks were successful
Create Release Package / PHP Lint (push) Successful in 50s
Create Release Package / Build Release (push) Successful in 1m14s
- Register sidebar widget area via register_sidebar()
- Render WordPress widgets in Twig sidebar with fallback to built-in content
- Update README.md with accurate feature counts and descriptions
- Update translation files with widget area strings
- Bump version to 1.0.0

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-08 18:43:09 +01:00
4c808a992a v0.3.3 - Fix style variation bridge to read from theme palette origin
All checks were successful
Create Release Package / PHP Lint (push) Successful in 50s
Create Release Package / Build Release (push) Successful in 1m16s
WordPress puts active variation colors in the 'theme' palette origin,
not 'custom'. Detection now compares theme origin against base defaults.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-08 18:12:52 +01:00
d6731cca47 v0.3.2 - Fix dark mode conflicts with WordPress global styles
All checks were successful
Create Release Package / PHP Lint (push) Successful in 59s
Create Release Package / Build Release (push) Successful in 1m25s
Fix dark mode body colors overridden by WordPress theme.json styles.color,
add broad dark mode rules for plugin form elements, fix footer-columns
template, and add style variation bridge function.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-08 17:33:12 +01:00
27 changed files with 17318 additions and 74 deletions

View File

@@ -2,6 +2,55 @@
All notable changes to this project will be documented in this file.
## [1.0.2] - 2026-02-10
### Fixed
- Missing HTML `<title>` tag on all pages — theme never declared `add_theme_support('title-tag')`, so WordPress's `_wp_render_title_tag()` hook was inactive during `wp_head()` output in Twig templates
## [1.0.1] - 2026-02-09
### Added
- Bootstrap Icons web font integration — all 2,000+ icons available via `<i class="bi bi-*"></i>` CSS classes
- `copy:icons` build step to copy icon font files (`.woff`, `.woff2`) from `node_modules` to `assets/fonts/`
- Bootstrap Icons SCSS imported in both frontend and editor stylesheets for icon support in templates and block editor
## [1.0.0] - 2026-02-08
### Added
- Sidebar widget area (`primary-sidebar`) registered via `register_sidebar()` — manageable in Appearance > Widgets
- Widget area rendering in Twig sidebar with fallback to built-in content (recent posts, search, tags) when no widgets assigned
- Widget area description strings added to all translation files (en_US, de_CH, fr_FR)
### Changed
- Updated README.md with accurate feature counts (15 style variations, 41 patterns, 3 translations)
- Added documentation for style variation bridge, widget areas, RTL support, and accessibility features
## [0.3.3] - 2026-02-08
### Fixed
- Style variation colors not applied to Bootstrap frontend — bridge function checked wrong palette origin (`custom` instead of `theme`)
- Variation detection now compares `theme` origin against base theme.json defaults instead of looking for slugs in `custom` origin
## [0.3.2] - 2026-02-08
### Fixed
- Dark mode body colors overridden by WordPress global styles (`styles.color` in `theme.json` generated conflicting `body` CSS)
- Dark mode styling for plugin-generated form elements (`select`, `input`, `textarea`) that lack Bootstrap classes
- Footer columns template used hardcoded `bg-dark text-light` instead of semantic `bg-body-tertiary`
- Style variation bridge function ran with default palette when no variation was active, causing unnecessary CSS overrides
### Changed
- Removed `styles.color` from `theme.json` to prevent WordPress from generating body background/text CSS that conflicts with Bootstrap dark mode
- Added `!important` override in `_custom.scss` for `html[data-bs-theme="dark"] body` to ensure Bootstrap dark mode takes precedence
- Added broad dark mode rules for native form elements in `_custom.scss`
## [0.3.1] - 2026-02-08
### Added

View File

@@ -34,7 +34,7 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
Current version is **v0.3.1** - Style Variations. Next milestone is **v1.0.0 - Release**. See `PLAN.md` for details.
Current version is **v1.0.2**. See `PLAN.md` for details.
## Technical Stack
@@ -193,6 +193,102 @@ Build steps (in order):
## Session History
### Session 11 — v1.0.3 Conditional Page Title (2026-02-11)
**Completed:** Made `<h1>` on page template conditional to prevent double headings when plugins provide their own titles.
**What was fixed:**
- `views/pages/page.html.twig` now wraps `<h1>{{ post.title }}</h1>` in `{% if post.title is not empty %}` guard
- When a plugin passes empty `post.title` via `render_via_theme_twig()`, the theme's `<h1>` is skipped
- Prevents duplicate headings on pages where plugin templates render their own `<h1>` with richer context (icons, badges, meta)
**Key learnings:**
- Plugins that delegate rendering to the parent theme via `TwigService` should be able to opt out of the theme's `<h1>` by passing empty `post.title`
- The `is not empty` Twig test correctly handles both `null` and empty string `''`
### Session 10 — v1.0.2 Title Tag Fix (2026-02-10)
**Completed:** Fixed missing HTML `<title>` tag on all pages rendered by the theme's Twig pipeline.
**What was fixed:**
- Added `add_theme_support('title-tag')` to `wp_bootstrap_setup()` in `functions.php`
**Root cause:**
- The theme's `base.html.twig` calls `{{ wp_head() }}` which fires the `wp_head` action
- WordPress hooks `_wp_render_title_tag()` to `wp_head` at priority 1, which outputs the `<title>` tag
- However, this hook only fires when the theme declares `add_theme_support('title-tag')`
- The theme never made this declaration, so `wp_head()` output included styles and scripts but no `<title>` element
- All pages rendered by `TemplateController` (via `base.html.twig`) were affected
**Key learnings:**
- `add_theme_support('title-tag')` is required even for themes that render `wp_head()` via Twig — WordPress does not output `<title>` without it
- The absence of a `<title>` tag is invisible in the rendered page but affects SEO, browser tab display, and bookmarking
- This support declaration has been standard since WordPress 4.1 and should always be included in `after_setup_theme`
### Session 9 — v1.0.1 Bootstrap Icons (2026-02-09)
**Completed:** Bootstrap Icons web font integration via SCSS build pipeline.
**What was built:**
- Added `bootstrap-icons` npm dependency (v1.13.1)
- Imported Bootstrap Icons SCSS in both `style.scss` and `editor-style.scss`
- Added `$bootstrap-icons-font-src` variable override in `_variables.scss` to point `@font-face` at `assets/fonts/`
- Added `copy:icons` npm script to copy `.woff`/`.woff2` font files from `node_modules` to `assets/fonts/`
- Updated `build` script to include `copy:icons` step
**Key learnings:**
- Bootstrap Icons SCSS uses `$bootstrap-icons-font-src` to allow overriding the `@font-face` `src` declaration — set it before the import to control font file paths
- The existing `--load-path=node_modules` Sass flag resolves `@import "bootstrap-icons/font/bootstrap-icons"` without any extra configuration
- Font files (`.woff2` at 131KB, `.woff` at 176KB) are small enough to serve as web fonts without performance concern
### Session 8 — v1.0.0 Release (2026-02-08)
**Completed:** Sidebar widget area registration, Twig widget rendering with fallback, documentation refresh, v1.0.0 release.
**What was built:**
- `register_sidebar()` for `primary-sidebar` widget area with Bootstrap-styled wrapper markup
- Widget area rendering in `ContextBuilder::getSidebarData()` via `ob_start()` + `dynamic_sidebar()` with fallback to built-in content
- Twig sidebar template conditional: renders WordPress widgets when assigned, falls back to recent posts/search/tags otherwise
- Updated README.md with accurate feature counts (15 variations, 41 patterns, 3 translations, accessibility, RTL, widget area)
- Updated all translation files (.pot, de_CH.po, fr_FR.po) with widget area strings
**Key learnings:**
- `is_active_sidebar()` returns true only when widgets are assigned to the area, making it the right condition for fallback logic
- `dynamic_sidebar()` outputs widget HTML directly, so `ob_start()`/`ob_get_clean()` is needed to capture it for Twig
- Widget area `before_widget`/`after_widget` markup should use Bootstrap utility classes (`widget mb-4`) for consistent spacing
- Widget title markup (`before_title`/`after_title`) should match existing sidebar heading styles (`sidebar-heading h6 text-uppercase fw-semibold`)
### Session 7 — v0.3.2/v0.3.3 Dark Mode & Style Variation Bridge (2026-02-08)
**Completed:** Fixed dark mode rendering conflicts between WordPress global styles and Bootstrap, fixed form element styling in dark mode, bridged style variation colors to Bootstrap CSS custom properties, fixed variation detection to read from correct palette origin.
**What was built:**
- Style variation bridge function (`wp_bootstrap_variation_colors`) that maps WordPress palette colors to Bootstrap CSS custom properties via `wp_enqueue_scripts`
- Helper functions for color manipulation: `wp_bootstrap_hex_to_rgb()`, `wp_bootstrap_build_surface_css()`, `wp_bootstrap_mix_hex()`, `wp_bootstrap_hex_to_rgb_array()`, `wp_bootstrap_relative_luminance()`
- Variation detection comparing `theme` origin palette against hardcoded base defaults (base, contrast, primary)
- Dark mode body override in `_custom.scss` using `!important` to defeat WordPress global styles specificity
- Broad dark mode rules for all native form elements (`select`, `input`, `textarea`) to catch plugin-generated controls
- Fixed `footer-columns.html.twig` to use semantic `bg-body-tertiary` instead of hardcoded `bg-dark text-light`
**Key learnings:**
- WordPress puts style variation colors in the `theme` palette origin, NOT `custom` -- `wp_get_global_settings(['color', 'palette', 'theme'])` returns the base theme.json merged with the active variation
- The `custom` palette origin contains user manual edits from the Site Editor, but its data structure may lack expected `slug`/`color` keys
- To detect an active variation, compare `theme` origin colors against known base theme.json defaults rather than checking for slugs in `custom`
- WordPress `theme.json` `styles.color` generates `body { background-color: var(--wp--preset--color--base) }` directly on `body`, which overrides inherited CSS variables from `html[data-bs-theme="dark"]` -- removing `styles.color` from theme.json is the cleanest fix
- CSS variables defined directly on `body` beat inherited values from `html` due to specificity, requiring `!important` on `html[data-bs-theme="dark"] body` to ensure Bootstrap dark mode works
- Plugin-generated form elements (e.g., `<select class="jr-search-form__filter-select">`) lack Bootstrap classes and need explicit dark mode styling via element selectors
### Session 6 — v0.3.1 Style Variations (2026-02-08)
**Completed:** Added 8 new style variations (4 light, 4 dark) to the Design Editor.

11
PLAN.md
View File

@@ -82,13 +82,12 @@ node_modules/bootstrap/dist/js/ → copyfiles → assets/js/bootstrap.bundle.min
- [x] Additional translations
- [x] Documentation
### v1.0.0 - Release
### v1.0.0 - Release (Complete)
- [ ] All features complete and tested
- [ ] WordPress.org theme review compliance
- [ ] Comprehensive documentation
- [ ] Full test coverage
- [ ] Update `README.md`
- [x] All features complete and tested
- [x] Widget area registration and Twig rendering with fallback
- [x] Comprehensive documentation (README refresh, updated translations)
- [x] Code quality audit passed (no TODO/FIXME, proper escaping, no security issues)
## Bootstrap 5 Integration Strategy

View File

@@ -7,15 +7,18 @@ A modern WordPress Block Theme built from scratch with Bootstrap 5. Features res
- **Bootstrap 5 Frontend** -- Proper Bootstrap 5 HTML (navbar, cards, pagination, grid) rendered via Twig templates
- **Dark Mode** -- Toggle with localStorage persistence and `prefers-color-scheme` support
- **Full Site Editing** -- Compatible with the WordPress Site Editor for admin editing
- **Block Patterns** -- 30+ patterns across 10 categories (hero, features, CTA, testimonials, pricing, contact, text, layout, components, navigation)
- **Style Variations** -- 15 color schemes (7 light, 7 dark, plus default) with live Design Editor customization
- **Block Patterns** -- 41 patterns across 11 categories (hero, features, CTA, testimonials, pricing, contact, text, layout, components, navigation, pages)
- **Bootstrap Icons** -- 2,000+ icons available via CSS classes (`bi bi-*`)
- **Block Styles** -- 17 custom styles mapping Bootstrap components to WordPress blocks
- **Style Variations** -- 4 color schemes: Ocean, Forest, Sunset, Midnight
- **Custom Templates** -- Landing (no header/footer), full-width, hero, sidebar page templates
- **Header/Footer Variations** -- Centered, transparent, minimal, multi-column variants
- **Header/Footer Variations** -- Default, centered, transparent headers; default, minimal, multi-column footers
- **Navigation Styles** -- Dark navbar, offcanvas mobile navigation
- **Design Editor** -- Full compatibility with the WordPress Site Editor
- **Widget Area** -- Sidebar widget area manageable via WordPress admin, with built-in fallback
- **Accessibility** -- Skip-to-content link, ARIA labels, `aria-current` on active items, screen reader announcements
- **RTL Support** -- Right-to-left language support with logical CSS properties
- **Translation Ready** -- Full i18n support with `en_US`, `de_CH`, and `fr_FR` translations
- **Responsive** -- Mobile-first design with Bootstrap's responsive grid
- **Translation Ready** -- Full i18n support with `en_US` and `de_CH` translations
## Requirements
@@ -67,8 +70,10 @@ Activate the theme in **Appearance > Themes** in the WordPress admin.
1. `copy:js` -- Copy Bootstrap JS bundle from `node_modules` to `assets/js/`
2. `copy:theme-js` -- Copy theme JS (dark-mode.js) from `src/js/` to `assets/js/`
3. `scss` -- Compile SCSS (`src/scss/`) to CSS (`assets/css/`)
4. `postcss` -- Autoprefixer + cssnano minification to `assets/css/style.min.css`
3. `copy:icons` -- Copy Bootstrap Icons font files (`.woff`, `.woff2`) to `assets/fonts/`
4. `scss` -- Compile SCSS (`src/scss/`) to CSS (`assets/css/`)
5. `scss:rtl` -- Compile RTL stylesheet (`assets/css/rtl.css`)
6. `postcss` -- Autoprefixer + cssnano minification to `assets/css/style.min.css`
## Architecture
@@ -81,6 +86,10 @@ The theme uses a dual-rendering approach:
The `TemplateController` intercepts frontend requests and renders the appropriate Twig template with data gathered by `ContextBuilder`. FSE templates remain untouched for the WordPress admin editor.
### Style Variation Bridge
WordPress style variation colors are bridged to Bootstrap CSS custom properties at runtime. When a variation is selected in the Design Editor, the theme reads the active palette via `wp_get_global_settings()` and outputs inline CSS that overrides Bootstrap's compiled defaults. This works for both light and dark mode.
### Key PHP Classes
| Class | Purpose |
@@ -99,6 +108,10 @@ Register menus in **Appearance > Menus**:
If no menu is assigned, the primary location falls back to listing published pages.
### Widget Areas
The theme registers a **Sidebar** widget area. When widgets are assigned via **Appearance > Widgets**, they replace the default sidebar content. When no widgets are assigned, the sidebar displays recent posts, a search form, and a tag cloud.
### Project Structure
```txt

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@@ -23,6 +23,9 @@ if ( file_exists( get_template_directory() . '/vendor/autoload.php' ) ) {
*/
if ( ! function_exists( 'wp_bootstrap_setup' ) ) :
function wp_bootstrap_setup() {
// Add support for automatic document title tag.
add_theme_support( 'title-tag' );
// Add support for post formats.
add_theme_support( 'post-formats', array(
'aside', 'audio', 'chat', 'gallery', 'image',
@@ -44,6 +47,26 @@ if ( ! function_exists( 'wp_bootstrap_setup' ) ) :
endif;
add_action( 'after_setup_theme', 'wp_bootstrap_setup' );
/**
* Register widget areas.
*
* @since 1.0.0
*/
if ( ! function_exists( 'wp_bootstrap_register_sidebars' ) ) :
function wp_bootstrap_register_sidebars() {
register_sidebar( array(
'name' => __( 'Sidebar', 'wp-bootstrap' ),
'id' => 'primary-sidebar',
'description' => __( 'Add widgets here to appear in the sidebar.', 'wp-bootstrap' ),
'before_widget' => '<div id="%1$s" class="widget mb-4 %2$s">',
'after_widget' => '</div>',
'before_title' => '<h3 class="sidebar-heading h6 text-uppercase fw-semibold">',
'after_title' => '</h3>',
) );
}
endif;
add_action( 'widgets_init', 'wp_bootstrap_register_sidebars' );
/**
* Enqueue theme scripts and styles.
*/
@@ -134,6 +157,280 @@ if ( ! function_exists( 'wp_bootstrap_rtl_styles' ) ) :
endif;
add_action( 'wp_enqueue_scripts', 'wp_bootstrap_rtl_styles', 20 );
/**
* Bridge WordPress style variation colors to Bootstrap CSS custom properties.
*
* Reads the active color palette (theme.json + variation + user overrides)
* and outputs inline CSS for both light and dark modes so the dark-mode toggle
* switches between two variation-aware color schemes.
*
* Theme colors (primaryinfo) go into :root and apply in both modes.
* Surface colors (body-bg, tertiary-bg, etc.) are computed separately for
* [data-bs-theme=light] and [data-bs-theme=dark].
*
* For light palettes: light mode uses base/contrast; dark mode uses dark/light slugs.
* For dark palettes: dark mode uses base/contrast; light mode swaps them.
*
* @since 0.3.2
*/
if ( ! function_exists( 'wp_bootstrap_variation_colors' ) ) :
function wp_bootstrap_variation_colors() {
// Read the theme origin palette — this contains the base theme.json
// colors merged with the active style variation (if any).
$theme_palette = wp_get_global_settings( array( 'color', 'palette', 'theme' ) );
$colors = array();
if ( ! empty( $theme_palette ) && is_array( $theme_palette ) ) {
foreach ( $theme_palette as $entry ) {
if ( ! empty( $entry['slug'] ) && ! empty( $entry['color'] ) ) {
$colors[ $entry['slug'] ] = $entry['color'];
}
}
}
// Compare against base theme.json defaults to detect an active variation.
// WordPress puts variation colors in the 'theme' origin, not 'custom'.
$base_defaults = array(
'base' => '#ffffff',
'contrast' => '#212529',
'primary' => '#0d6efd',
);
$is_default = true;
foreach ( $base_defaults as $slug => $default_color ) {
if ( ! empty( $colors[ $slug ] ) && strtolower( $colors[ $slug ] ) !== $default_color ) {
$is_default = false;
break;
}
}
// No variation active — let Bootstrap's compiled CSS handle both modes.
if ( $is_default ) {
return;
}
if ( empty( $colors['base'] ) || empty( $colors['contrast'] ) ) {
return;
}
// Theme colors apply in both modes via :root.
$theme_slugs = array(
'primary' => '--bs-primary',
'secondary' => '--bs-secondary',
'success' => '--bs-success',
'danger' => '--bs-danger',
'warning' => '--bs-warning',
'info' => '--bs-info',
);
$root_css = '';
foreach ( $theme_slugs as $slug => $var ) {
if ( ! empty( $colors[ $slug ] ) ) {
$hex = esc_attr( $colors[ $slug ] );
$rgb = wp_bootstrap_hex_to_rgb( $colors[ $slug ] );
$root_css .= "{$var}:{$hex};";
if ( $rgb ) {
$root_css .= "{$var}-rgb:{$rgb};";
}
}
}
// Link colors from primary (both modes).
if ( ! empty( $colors['primary'] ) ) {
$primary_rgb = wp_bootstrap_hex_to_rgb( $colors['primary'] );
$root_css .= '--bs-link-color:' . esc_attr( $colors['primary'] ) . ';';
$root_css .= '--bs-link-color-rgb:' . $primary_rgb . ';';
$root_css .= '--bs-link-hover-color:' . esc_attr( $colors['primary'] ) . ';';
$root_css .= '--bs-link-hover-color-rgb:' . $primary_rgb . ';';
}
// Determine if this is a dark palette (base luminance < contrast luminance).
$is_dark = wp_bootstrap_relative_luminance( $colors['base'] )
< wp_bootstrap_relative_luminance( $colors['contrast'] );
// Resolve light-mode and dark-mode base colors.
if ( $is_dark ) {
// Dark palette: dark mode is native, light mode swaps base↔contrast.
$light_bg = $colors['contrast'];
$light_fg = $colors['base'];
$dark_bg = $colors['base'];
$dark_fg = $colors['contrast'];
} else {
// Light palette: light mode is native, dark mode uses dark/light slugs.
$light_bg = $colors['base'];
$light_fg = $colors['contrast'];
$dark_bg = $colors['dark'] ?? '#212529';
$dark_fg = $colors['light'] ?? '#f8f9fa';
}
// Also set light/dark slug variables for utilities like bg-light, bg-dark.
if ( ! empty( $colors['light'] ) ) {
$root_css .= '--bs-light:' . esc_attr( $colors['light'] ) . ';';
$root_css .= '--bs-light-rgb:' . wp_bootstrap_hex_to_rgb( $colors['light'] ) . ';';
}
if ( ! empty( $colors['dark'] ) ) {
$root_css .= '--bs-dark:' . esc_attr( $colors['dark'] ) . ';';
$root_css .= '--bs-dark-rgb:' . wp_bootstrap_hex_to_rgb( $colors['dark'] ) . ';';
}
// Build surface CSS for a given bg/fg pair.
$light_css = wp_bootstrap_build_surface_css( $light_bg, $light_fg, false );
$dark_css = wp_bootstrap_build_surface_css( $dark_bg, $dark_fg, true );
$css = ':root{' . $root_css . '}'
. '[data-bs-theme=light]{' . $light_css . '}'
. '[data-bs-theme=dark]{' . $dark_css . '}';
// Attach after the compiled stylesheet so variation values override
// Bootstrap's hardcoded dark-mode defaults via source order.
wp_add_inline_style( 'wp-bootstrap-style', $css );
}
endif;
add_action( 'wp_enqueue_scripts', 'wp_bootstrap_variation_colors', 30 );
/**
* Build Bootstrap surface CSS variables for a given background/foreground pair.
*
* Computes body-bg, body-color, tertiary-bg, secondary-bg, secondary-color,
* emphasis-color, and border-color — mirroring how Bootstrap derives them.
*
* @since 0.3.2
*
* @param string $bg Background hex color.
* @param string $fg Foreground (text) hex color.
* @param bool $is_dark Whether this is for dark mode.
* @return string CSS declarations (no selector).
*/
if ( ! function_exists( 'wp_bootstrap_build_surface_css' ) ) :
function wp_bootstrap_build_surface_css( $bg, $fg, $is_dark ) {
$bg_rgb = wp_bootstrap_hex_to_rgb( $bg );
$fg_rgb = wp_bootstrap_hex_to_rgb( $fg );
$css = '--bs-body-bg:' . esc_attr( $bg ) . ';';
$css .= '--bs-body-bg-rgb:' . $bg_rgb . ';';
$css .= '--bs-body-color:' . esc_attr( $fg ) . ';';
$css .= '--bs-body-color-rgb:' . $fg_rgb . ';';
if ( $is_dark ) {
$secondary_bg = wp_bootstrap_mix_hex( $fg, $bg, 0.16 );
$tertiary_bg = wp_bootstrap_mix_hex( $fg, $bg, 0.08 );
$border_color = wp_bootstrap_mix_hex( $fg, $bg, 0.24 );
$emphasis = '#FFFFFF';
} else {
$secondary_bg = wp_bootstrap_mix_hex( $fg, $bg, 0.08 );
$tertiary_bg = wp_bootstrap_mix_hex( $fg, $bg, 0.04 );
$border_color = wp_bootstrap_mix_hex( $fg, $bg, 0.16 );
$emphasis = '#000000';
}
$css .= '--bs-secondary-color:rgba(' . $fg_rgb . ',0.75);';
$css .= '--bs-secondary-bg:' . $secondary_bg . ';';
$css .= '--bs-secondary-bg-rgb:' . wp_bootstrap_hex_to_rgb( $secondary_bg ) . ';';
$css .= '--bs-tertiary-color:rgba(' . $fg_rgb . ',0.5);';
$css .= '--bs-tertiary-bg:' . $tertiary_bg . ';';
$css .= '--bs-tertiary-bg-rgb:' . wp_bootstrap_hex_to_rgb( $tertiary_bg ) . ';';
$css .= '--bs-emphasis-color:' . $emphasis . ';';
$css .= '--bs-emphasis-color-rgb:' . wp_bootstrap_hex_to_rgb( $emphasis ) . ';';
$css .= '--bs-border-color:' . $border_color . ';';
return $css;
}
endif;
/**
* Convert a hex color string to an RGB triplet string.
*
* @since 0.3.2
*
* @param string $hex Hex color (e.g. "#0d6efd" or "0d6efd").
* @return string RGB triplet (e.g. "13,110,253") or empty string on failure.
*/
if ( ! function_exists( 'wp_bootstrap_hex_to_rgb' ) ) :
function wp_bootstrap_hex_to_rgb( $hex ) {
$hex = ltrim( $hex, '#' );
if ( strlen( $hex ) === 3 ) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
}
if ( strlen( $hex ) !== 6 ) {
return '';
}
return hexdec( substr( $hex, 0, 2 ) ) . ','
. hexdec( substr( $hex, 2, 2 ) ) . ','
. hexdec( substr( $hex, 4, 2 ) );
}
endif;
/**
* Mix two hex colors by a given weight.
*
* @since 0.3.2
*
* @param string $color1 Hex color to mix in.
* @param string $color2 Base hex color.
* @param float $weight Weight of color1 (0.0 to 1.0).
* @return string Resulting hex color.
*/
if ( ! function_exists( 'wp_bootstrap_mix_hex' ) ) :
function wp_bootstrap_mix_hex( $color1, $color2, $weight ) {
$c1 = wp_bootstrap_hex_to_rgb_array( $color1 );
$c2 = wp_bootstrap_hex_to_rgb_array( $color2 );
if ( ! $c1 || ! $c2 ) {
return $color2;
}
$r = (int) round( $c1[0] * $weight + $c2[0] * ( 1 - $weight ) );
$g = (int) round( $c1[1] * $weight + $c2[1] * ( 1 - $weight ) );
$b = (int) round( $c1[2] * $weight + $c2[2] * ( 1 - $weight ) );
return sprintf( '#%02x%02x%02x', max( 0, min( 255, $r ) ), max( 0, min( 255, $g ) ), max( 0, min( 255, $b ) ) );
}
endif;
/**
* Convert a hex color to an array of [r, g, b] integers.
*
* @since 0.3.2
*
* @param string $hex Hex color.
* @return array|false Array of [r, g, b] or false on failure.
*/
if ( ! function_exists( 'wp_bootstrap_hex_to_rgb_array' ) ) :
function wp_bootstrap_hex_to_rgb_array( $hex ) {
$hex = ltrim( $hex, '#' );
if ( strlen( $hex ) === 3 ) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
}
if ( strlen( $hex ) !== 6 ) {
return false;
}
return array(
hexdec( substr( $hex, 0, 2 ) ),
hexdec( substr( $hex, 2, 2 ) ),
hexdec( substr( $hex, 4, 2 ) ),
);
}
endif;
/**
* Compute relative luminance of a hex color (0.0 = black, 1.0 = white).
*
* @since 0.3.2
*
* @param string $hex Hex color.
* @return float Relative luminance.
*/
if ( ! function_exists( 'wp_bootstrap_relative_luminance' ) ) :
function wp_bootstrap_relative_luminance( $hex ) {
$rgb = wp_bootstrap_hex_to_rgb_array( $hex );
if ( ! $rgb ) {
return 0.0;
}
$channels = array();
foreach ( $rgb as $val ) {
$val /= 255;
$channels[] = ( $val <= 0.03928 ) ? $val / 12.92 : pow( ( $val + 0.055 ) / 1.055, 2.4 );
}
return 0.2126 * $channels[0] + 0.7152 * $channels[1] + 0.0722 * $channels[2];
}
endif;
/**
* Enqueue Bootstrap JS in the block editor for interactive previews.
*

View File

@@ -387,12 +387,26 @@ class ContextBuilder
/**
* Get sidebar widget data.
*
* If the 'primary-sidebar' widget area has widgets assigned,
* their rendered HTML is returned. Otherwise, fallback data
* (recent posts, tags) is provided for the default Twig sidebar.
*/
private function getSidebarData(): array
{
$widgets_active = is_active_sidebar( 'primary-sidebar' );
$widgets_html = '';
if ( $widgets_active ) {
ob_start();
dynamic_sidebar( 'primary-sidebar' );
$widgets_html = ob_get_clean();
}
return [
'recent_posts' => $this->getSidebarRecentPosts(),
'tags' => $this->getSidebarTags(),
'widgets_active' => $widgets_active,
'widgets_html' => $widgets_html,
'recent_posts' => $this->getSidebarRecentPosts(),
'tags' => $this->getSidebarTags(),
];
}

View File

@@ -906,6 +906,14 @@ msgstr "Hauptnavigation"
msgid "Footer navigation"
msgstr "Fussnavigation"
#: functions.php
msgid "Sidebar"
msgstr "Seitenleiste"
#: functions.php
msgid "Add widgets here to appear in the sidebar."
msgstr "Widgets hier hinzufuegen, um sie in der Seitenleiste anzuzeigen."
#: views/partials/sidebar.html.twig
msgid "Blog sidebar"
msgstr "Blog-Seitenleiste"

View File

@@ -904,6 +904,14 @@ msgstr "Navigation principale"
msgid "Footer navigation"
msgstr "Navigation du pied de page"
#: functions.php
msgid "Sidebar"
msgstr "Barre latérale"
#: functions.php
msgid "Add widgets here to appear in the sidebar."
msgstr "Ajoutez des widgets ici pour les afficher dans la barre latérale."
#: views/partials/sidebar.html.twig
msgid "Blog sidebar"
msgstr "Barre latérale du blog"

View File

@@ -2,7 +2,7 @@
# This file is distributed under the same license as the WP Bootstrap theme.
msgid ""
msgstr ""
"Project-Id-Version: WP Bootstrap 0.3.0\n"
"Project-Id-Version: WP Bootstrap 1.0.0\n"
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-bootstrap/issues\n"
"POT-Creation-Date: 2026-02-08 00:00+0000\n"
"MIME-Version: 1.0\n"
@@ -903,6 +903,14 @@ msgstr ""
msgid "Footer navigation"
msgstr ""
#: functions.php
msgid "Sidebar"
msgstr ""
#: functions.php
msgid "Add widgets here to appear in the sidebar."
msgstr ""
#: views/partials/sidebar.html.twig
msgid "Blog sidebar"
msgstr ""

23
package-lock.json generated
View File

@@ -1,16 +1,17 @@
{
"name": "wp-bootstrap",
"version": "0.1.0",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wp-bootstrap",
"version": "0.1.0",
"version": "1.0.0",
"license": "GPL-2.0-or-later",
"dependencies": {
"@popperjs/core": "^2.11",
"bootstrap": "^5.3"
"bootstrap": "^5.3",
"bootstrap-icons": "^1.13.1"
},
"devDependencies": {
"autoprefixer": "^10.4",
@@ -259,6 +260,22 @@
"@popperjs/core": "^2.11.8"
}
},
"node_modules/bootstrap-icons": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz",
"integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "wp-bootstrap",
"version": "0.3.1",
"version": "1.0.1",
"description": "WordPress Theme built with Bootstrap 5",
"author": "Marco Graetsch <magdev3.0@gmail.com>",
"license": "GPL-2.0-or-later",
@@ -13,16 +13,17 @@
"node": ">=20.0.0"
},
"dependencies": {
"@popperjs/core": "^2.11",
"bootstrap": "^5.3",
"@popperjs/core": "^2.11"
"bootstrap-icons": "^1.13.1"
},
"devDependencies": {
"sass": "^1.97",
"autoprefixer": "^10.4",
"copyfiles": "^2.4",
"cssnano": "^7.1",
"postcss": "^8.5",
"postcss-cli": "^11",
"autoprefixer": "^10.4",
"cssnano": "^7.1",
"copyfiles": "^2.4"
"sass": "^1.97"
},
"scripts": {
"scss": "sass src/scss/style.scss:assets/css/style.css src/scss/editor-style.scss:assets/css/editor-style.css --load-path=node_modules",
@@ -31,7 +32,8 @@
"postcss": "postcss assets/css/style.css --use autoprefixer cssnano -o assets/css/style.min.css --no-map",
"copy:js": "copyfiles -f node_modules/bootstrap/dist/js/bootstrap.bundle.min.js node_modules/bootstrap/dist/js/bootstrap.bundle.min.js.map assets/js/",
"copy:theme-js": "copyfiles -f src/js/dark-mode.js assets/js/",
"build": "npm run copy:js && npm run copy:theme-js && npm run scss && npm run scss:rtl && npm run postcss",
"copy:icons": "copyfiles -f node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2 assets/fonts/",
"build": "npm run copy:js && npm run copy:theme-js && npm run copy:icons && npm run scss && npm run scss:rtl && npm run postcss",
"watch": "npm run copy:js && npm run scss:watch",
"dev": "npm run watch"
}

View File

@@ -36,8 +36,25 @@
}
}
// Offcanvas navigation dark mode compatibility
// Force Bootstrap dark mode body colors past any WordPress global styles.
// WordPress may output body { background-color: ...; color: ...; } via
// global-styles that overrides Bootstrap's variable-based body styling.
html[data-bs-theme="dark"] body {
background-color: var(--bs-body-bg) !important;
color: var(--bs-body-color) !important;
}
// Dark mode for all form elements — catches plugin-generated controls
// that lack Bootstrap's .form-select / .form-control classes.
[data-bs-theme="dark"] {
select,
input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="reset"]),
textarea {
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
border-color: var(--bs-border-color);
}
.offcanvas {
--bs-offcanvas-bg: var(--bs-body-bg);
}

View File

@@ -44,3 +44,7 @@ $enable-dark-mode: true;
// Enable reduced motion
$enable-reduced-motion: true;
// Bootstrap Icons font path (points to copied files in assets/fonts/)
$bootstrap-icons-font-src: url("../fonts/bootstrap-icons.woff2") format("woff2"),
url("../fonts/bootstrap-icons.woff") format("woff");

View File

@@ -15,8 +15,11 @@
// 4. WordPress block compatibility
@import "wordpress";
// 5. Custom styles (dark mode overrides, block styles, etc.)
// 5. Bootstrap Icons
@import "bootstrap-icons/font/bootstrap-icons";
// 6. Custom styles (dark mode overrides, block styles, etc.)
@import "custom";
// 6. Editor-specific overrides
// 7. Editor-specific overrides
@import "editor-overrides";

View File

@@ -15,5 +15,8 @@
// 4. WordPress block compatibility
@import "wordpress";
// 5. Custom styles
// 5. Bootstrap Icons
@import "bootstrap-icons/font/bootstrap-icons";
// 6. Custom styles
@import "custom";

View File

@@ -7,7 +7,7 @@ Description: A modern WordPress Block Theme built from scratch with Bootstrap 5.
Requires at least: 6.7
Tested up to: 6.7
Requires PHP: 8.3
Version: 0.3.1
Version: 1.0.3
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: wp-bootstrap

View File

@@ -271,10 +271,6 @@
"useRootPaddingAwareAlignments": true
},
"styles": {
"color": {
"background": "var:preset|color|base",
"text": "var:preset|color|contrast"
},
"spacing": {
"blockGap": "1.5rem",
"padding": {

View File

@@ -10,7 +10,9 @@
</figure>
{% endif %}
<h1>{{ post.title }}</h1>
{% if post.title is not empty %}
<h1>{{ post.title }}</h1>
{% endif %}
<div class="post-content">
{{ post.content|raw }}

View File

@@ -1,4 +1,4 @@
<footer class="bg-dark text-light mt-auto">
<footer class="bg-body-tertiary mt-auto">
<div class="container py-5">
<div class="row">
<div class="col-lg-4 mb-4 mb-lg-0">

View File

@@ -1,44 +1,48 @@
<aside aria-label="{{ __('Blog sidebar') }}">
{% if sidebar.recent_posts is defined and sidebar.recent_posts|length > 0 %}
{% if sidebar.widgets_active %}
{{ sidebar.widgets_html|raw }}
{% else %}
{% if sidebar.recent_posts is defined and sidebar.recent_posts|length > 0 %}
<div class="mb-4">
<h3 class="sidebar-heading h6 text-uppercase fw-semibold">
{{ __('Recent Posts') }}
</h3>
<ul class="list-unstyled">
{% for post in sidebar.recent_posts %}
<li class="mb-2">
<a href="{{ post.url }}" class="text-decoration-none">{{ post.title }}</a>
<br>
<small class="text-body-secondary">{{ post.date }}</small>
</li>
{% endfor %}
</ul>
</div>
<hr>
{% endif %}
<div class="mb-4">
<h3 class="sidebar-heading h6 text-uppercase fw-semibold">
{{ __('Recent Posts') }}
{{ __('Search') }}
</h3>
<ul class="list-unstyled">
{% for post in sidebar.recent_posts %}
<li class="mb-2">
<a href="{{ post.url }}" class="text-decoration-none">{{ post.title }}</a>
<br>
<small class="text-body-secondary">{{ post.date }}</small>
</li>
{% endfor %}
</ul>
{% include 'partials/search-form.html.twig' %}
</div>
<hr>
{% endif %}
<div class="mb-4">
<h3 class="sidebar-heading h6 text-uppercase fw-semibold">
{{ __('Search') }}
</h3>
{% include 'partials/search-form.html.twig' %}
</div>
<hr>
{% if sidebar.tags is defined and sidebar.tags|length > 0 %}
<div class="mb-4">
<h3 class="sidebar-heading h6 text-uppercase fw-semibold">
{{ __('Tags') }}
</h3>
<div>
{% for tag in sidebar.tags %}
<a href="{{ tag.url }}" class="badge bg-secondary text-decoration-none me-1 mb-1">
{{ tag.name }}
</a>
{% endfor %}
{% if sidebar.tags is defined and sidebar.tags|length > 0 %}
<div class="mb-4">
<h3 class="sidebar-heading h6 text-uppercase fw-semibold">
{{ __('Tags') }}
</h3>
<div>
{% for tag in sidebar.tags %}
<a href="{{ tag.url }}" class="badge bg-secondary text-decoration-none me-1 mb-1">
{{ tag.name }}
</a>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% endif %}
</aside>