diff --git a/CHANGELOG.md b/CHANGELOG.md index 9232024..b4daccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. +## [0.1.7] - 2026-03-29 + +### Added + +- **Product category tree sidebar** on archive/shop and single product pages: hierarchical navigation with collapsible sub-levels up to 3 levels deep, using Bootstrap 5 list-group and collapse components +- **`wc_bootstrap_get_category_tree()`** (`functions.php`): Builds nested category array with `is_active`/`is_ancestor` flags for current page context, parent-indexed lookup for O(n) tree construction +- **Category tree Twig templates** (`global/category-tree.html.twig`, `global/category-tree-node.html.twig`): Recursive rendering with "All products" link, product count badges, chevron toggle buttons, and `aria-current="page"` accessibility +- **Single product sidebar** (`woocommerce/single-product.php`): Added responsive sidebar layout (`col-lg-3` + `col-lg-9`) with offcanvas on mobile, matching the archive page pattern +- **Category tree CSS** (`wc-bootstrap.css`): Hover states, active highlighting (primary color), chevron rotation animation, nested indentation, dark mode compatible via Bootstrap CSS variables + +### Changed + +- **Archive sidebar always visible** (`woocommerce/archive-product.php`): Sidebar now renders when categories exist, not only when widgets are configured. Category tree renders above any widgets. + ## [0.1.6] - 2026-03-01 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 48944ff..eb6f706 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -345,10 +345,34 @@ The child theme inherits from `wp-bootstrap` via WordPress `Template: wp-bootstr ## Version History -Current version: **v0.1.6** +Current version: **v0.1.7** ## Session History +### 2026-03-29 — v0.1.7 Add Product Category Tree Sidebar + +**Scope:** Added a hierarchical product category tree sidebar to archive/shop and single product pages, with collapsible sub-levels up to 3 levels deep. + +**Files created (2):** + +- `templates/global/category-tree.html.twig` — Main category tree template with "All products" link and Bootstrap list-group wrapper +- `templates/global/category-tree-node.html.twig` — Recursive node template with collapsible children, count badges, chevron toggle buttons + +**Files modified (5):** + +- `functions.php` — Added `wc_bootstrap_get_category_tree()`: builds hierarchical category array using parent-indexed lookup map, with `is_active`/`is_ancestor` flags based on current query; supports up to 3 nesting levels +- `woocommerce/archive-product.php` — Sidebar now renders when categories exist (not just when widgets are configured); category tree rendered above widgets via TwigService +- `woocommerce/single-product.php` — Added responsive sidebar layout (`col-lg-3` + `col-lg-9`) with offcanvas on mobile, matching archive page pattern; wraps product content in grid +- `assets/css/wc-bootstrap.css` — Category tree styles: hover states, active highlighting (primary color), chevron rotation animation, nested list indentation, dark mode compatible via Bootstrap CSS variables +- `style.css` — Version bump 0.1.6 → 0.1.7 + +**Key decisions:** + +- **Programmatic tree over widget:** Category tree renders from `get_terms()` data, not dependent on widgets being configured in WP admin. Widgets still render below the tree if configured. +- **Recursive Twig `{% include %}` for nesting:** `category-tree-node.html.twig` includes itself for child categories, keeping the template DRY and handling arbitrary depth (capped at 3 by PHP). +- **`is_ancestor` auto-expand:** When viewing a subcategory, all ancestor categories auto-expand their collapse panels (`show` class + `aria-expanded="true"`), so the user sees the full path. +- **Shared sidebar pattern:** Both archive and single product pages use the same offcanvas-lg responsive pattern — full sidebar on desktop, slide-out panel on mobile. + ### 2026-03-01 — v0.1.6 Add PHPUnit Test Suite **Scope:** Added PHPUnit test infrastructure with Brain\Monkey for WordPress function mocking, covering both PHP classes (`TemplateOverride`, `WooCommerceExtension`). Added test job to Gitea CI pipeline. diff --git a/assets/css/wc-bootstrap.css b/assets/css/wc-bootstrap.css index 64742f1..48b5b4f 100644 --- a/assets/css/wc-bootstrap.css +++ b/assets/css/wc-bootstrap.css @@ -644,3 +644,50 @@ header.sticky-top.is-stuck { .woocommerce-product-gallery--without-images { opacity: 1 !important; } + +/* ========================================================================== + Category Tree Sidebar + Hierarchical product category navigation with collapsible sub-levels. + ========================================================================== */ + +.category-tree { + font-size: 0.9rem; +} + +.category-tree .list-group-item { + background-color: transparent; +} + +.category-tree-link { + color: var(--bs-body-color); + transition: background-color 0.15s ease, color 0.15s ease; +} + +.category-tree-link:hover { + background-color: var(--bs-tertiary-bg); + color: var(--bs-body-color); +} + +.category-tree-link.active { + background-color: var(--bs-primary); + color: #fff; +} + +.category-tree-link.active .badge { + background-color: rgba(255, 255, 255, 0.2) !important; + color: #fff !important; +} + +/* Rotate chevron icon when collapsed/expanded */ +.category-tree-toggle .bi { + transition: transform 0.2s ease; +} + +.category-tree-toggle[aria-expanded="false"] .bi { + transform: rotate(-90deg); +} + +/* Nested list indentation */ +.category-tree .list-group-flush .list-group-flush { + padding-left: 0.75rem; +} diff --git a/functions.php b/functions.php index 9a7c7ee..be9b665 100644 --- a/functions.php +++ b/functions.php @@ -293,6 +293,91 @@ function wc_bootstrap_loop_columns(): int { } add_filter( 'loop_shop_columns', 'wc_bootstrap_loop_columns' ); +/** + * Build a hierarchical product category tree up to a given depth. + * + * Returns a nested array of category objects with children, suitable for + * rendering a multi-level navigation sidebar. Each node contains the + * WP_Term object plus a 'children' array and an 'is_active'/'is_ancestor' + * flag based on the current query. + * + * @since 0.1.7 + * + * @param int $max_depth Maximum nesting depth (1 = top-level only, 3 = three levels). + * @return array Hierarchical category tree. + */ +function wc_bootstrap_get_category_tree( int $max_depth = 3 ): array { + $terms = get_terms( [ + 'taxonomy' => 'product_cat', + 'hide_empty' => true, + 'orderby' => 'name', + 'order' => 'ASC', + ] ); + + if ( is_wp_error( $terms ) || empty( $terms ) ) { + return []; + } + + // Determine the currently viewed category (if any). + $current_term_id = 0; + $ancestor_ids = []; + + if ( is_product_category() ) { + $queried = get_queried_object(); + if ( $queried instanceof \WP_Term ) { + $current_term_id = $queried->term_id; + $ancestor_ids = get_ancestors( $current_term_id, 'product_cat', 'taxonomy' ); + } + } elseif ( is_product() ) { + // On single product pages, highlight the first assigned category. + global $product; + if ( $product ) { + $cat_ids = $product->get_category_ids(); + if ( ! empty( $cat_ids ) ) { + $current_term_id = $cat_ids[0]; + $ancestor_ids = get_ancestors( $current_term_id, 'product_cat', 'taxonomy' ); + } + } + } + + // Index terms by parent for efficient tree building. + $by_parent = []; + foreach ( $terms as $term ) { + $by_parent[ $term->parent ][] = $term; + } + + /** + * Recursively build tree nodes from the parent-indexed map. + */ + $build = function ( int $parent_id, int $depth ) use ( &$build, $by_parent, $max_depth, $current_term_id, $ancestor_ids ): array { + if ( $depth > $max_depth || ! isset( $by_parent[ $parent_id ] ) ) { + return []; + } + + $nodes = []; + foreach ( $by_parent[ $parent_id ] as $term ) { + $children = $build( $term->term_id, $depth + 1 ); + $is_active = ( $term->term_id === $current_term_id ); + $is_ancestor = in_array( $term->term_id, $ancestor_ids, true ); + + $nodes[] = [ + 'term_id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'count' => $term->count, + 'url' => get_term_link( $term ), + 'is_active' => $is_active, + 'is_ancestor' => $is_ancestor, + 'children' => $children, + ]; + } + + return $nodes; + }; + + return $build( 0, 1 ); +} + /** * Remove WooCommerce's default sidebar hook. * diff --git a/style.css b/style.css index ace1e2a..d9e13de 100644 --- a/style.css +++ b/style.css @@ -7,7 +7,7 @@ Description: A Bootstrap 5 child theme for WP Bootstrap that overrides all WooCo Requires at least: 6.7 Tested up to: 6.7 Requires PHP: 8.3 -Version: 0.1.6 +Version: 0.1.7 License: GNU General Public License v2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html Template: wp-bootstrap diff --git a/templates/global/category-tree-node.html.twig b/templates/global/category-tree-node.html.twig new file mode 100644 index 0000000..8295c5c --- /dev/null +++ b/templates/global/category-tree-node.html.twig @@ -0,0 +1,46 @@ +{# + # Single Category Tree Node (recursive) + # + # Renders one category with optional collapsible children. + # Included recursively by category-tree.html.twig. + # + # Expected context: + # node - Category node from wc_bootstrap_get_category_tree() + # level - Current nesting depth (1-3) + # + # @package WcBootstrap + # @since 0.1.7 + #} + +