From 9ce21578f26c16e5ba4bcb6ec3b6ceed4bd478cd Mon Sep 17 00:00:00 2001 From: Jasper Frumau Date: Sun, 24 May 2026 15:08:33 +0700 Subject: [PATCH 1/8] feat: add prompt templates to AI integration and admin UI Add six built-in prompt templates (Homepage, About, Services, Contact, Landing, Portfolio) to AiIntegration::get_prompt_templates(), filterable via the waygate_prompt_templates hook. Add a Quick Template dropdown to the admin form that pre-fills the description textarea on selection. A confirmation prompt fires if the textarea already has content, then resets the select so re-selection still works. Description hint now notes [placeholder] substitution. --- includes/class-admin.php | 96 +++++++++++++++++++++++----- includes/class-ai-integration.php | 103 ++++++++++++++++++++++++------ 2 files changed, 163 insertions(+), 36 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 5a1f82e..af30482 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -1,45 +1,62 @@ 'You do not have permission to create pages.' ]; + $result = array( 'error' => 'You do not have permission to create pages.' ); } elseif ( ! $text_gen_supported ) { - $result = [ 'error' => 'Text generation is not supported by the configured AI provider.' ]; + $result = array( 'error' => 'Text generation is not supported by the configured AI provider.' ); } else { $description = sanitize_textarea_field( wp_unslash( $_POST['description'] ?? '' ) ); if ( empty( $description ) ) { - $result = [ 'error' => 'Please describe the page you want to create.' ]; + $result = array( 'error' => 'Please describe the page you want to create.' ); } else { $result = AiIntegration::generate_page( $description ); } @@ -50,10 +67,10 @@ public static function render_page(): void { usort( $all_patterns, fn( $a, $b ) => strcmp( $a['slug'], $b['slug'] ) ); // Build unique category list (strip namespace prefix for display/filtering). - $all_categories = []; + $all_categories = array(); foreach ( $all_patterns as $p ) { foreach ( $p['categories'] as $cat ) { - $parts = explode( '/', $cat ); + $parts = explode( '/', $cat ); $all_categories[ end( $parts ) ] = true; } } @@ -100,6 +117,20 @@ function ( $p ) use ( $selected_category ) { + + + + @@ -119,6 +150,19 @@ class="large-text" +
@@ -181,6 +225,12 @@ function ( $c ) {

Error: ' . esc_html( $result['error'] ) . '

'; @@ -264,17 +322,25 @@ private static function result_notice( array $result ): void { ID, '_waygate_reasoning', true ); @@ -288,7 +354,7 @@ public static function render_meta_box( \WP_Post $post ): void { if ( self::is_dev() ) { $patterns_json = get_post_meta( $post->ID, '_waygate_patterns', true ); $generated_at = get_post_meta( $post->ID, '_waygate_generated_at', true ); - $patterns = $patterns_json ? json_decode( $patterns_json, true ) : []; + $patterns = $patterns_json ? json_decode( $patterns_json, true ) : array(); if ( $generated_at ) { echo '

Generated: ' . esc_html( $generated_at ) . '

'; diff --git a/includes/class-ai-integration.php b/includes/class-ai-integration.php index 7353bb4..4cf6316 100644 --- a/includes/class-ai-integration.php +++ b/includes/class-ai-integration.php @@ -1,15 +1,76 @@ + */ + private static array $prompt_templates = array( + 'homepage' => array( + 'label' => 'Homepage', + 'description' => 'Hero, features grid, testimonials, and CTA', + 'prompt' => 'Create a homepage for a [industry] business with a hero section, features grid, customer testimonials, and a call-to-action section', + ), + 'about' => array( + 'label' => 'About Page', + 'description' => 'Team bios, company story, and mission', + 'prompt' => 'Create an about page for a [industry] company with team member cards, company history, core values, and a contact form', + ), + 'services' => array( + 'label' => 'Services Page', + 'description' => 'Services listing with benefits and pricing CTA', + 'prompt' => 'Create a services page for a [industry] business showcasing our main services with descriptions, key benefits, and a pricing call-to-action', + ), + 'contact' => array( + 'label' => 'Contact Page', + 'description' => 'Contact form, location, and team info', + 'prompt' => 'Create a contact page with a contact form, office location details, a brief team introduction, and social media links', + ), + 'landing' => array( + 'label' => 'Landing Page', + 'description' => 'Conversion-focused with hero and strong CTA', + 'prompt' => 'Create a landing page for [product or service] with a compelling hero section, key benefits, social proof, and a strong call-to-action', + ), + 'portfolio' => array( + 'label' => 'Portfolio / Work', + 'description' => 'Work showcase with case studies and CTA', + 'prompt' => 'Create a portfolio page for a [industry] studio showcasing selected projects, client logos, a brief process overview, and a hire-us CTA', + ), + ); + + /** + * Returns all prompt templates, allowing third parties to add their own via the filter. + * + * @return array + */ + public static function get_prompt_templates(): array { + return apply_filters( 'waygate_prompt_templates', self::$prompt_templates ); + } + + /** + * Registers the Mistral provider on the init hook. + */ public static function init(): void { - add_action( 'init', [ self::class, 'register_mistral_provider' ], 6 ); + add_action( 'init', array( self::class, 'register_mistral_provider' ), 6 ); } + /** + * Returns true when a configured AI provider supports text generation. + */ public static function is_text_generation_supported(): bool { if ( ! function_exists( 'wp_ai_client_prompt' ) ) { return false; @@ -57,25 +118,25 @@ public static function generate_page( string $description ): array { $pattern_detail .= "- {$p['slug']} | {$p['title']} | {$cats}\n"; } - $schema = [ + $schema = array( 'type' => 'object', - 'properties' => [ - 'title' => [ + 'properties' => array( + 'title' => array( 'type' => 'string', 'description' => 'Suggested page title.', - ], - 'patterns' => [ + ), + 'patterns' => array( 'type' => 'array', - 'items' => [ 'type' => 'string' ], + 'items' => array( 'type' => 'string' ), 'description' => 'Ordered list of Elayne pattern slugs to assemble the page.', - ], - 'reasoning' => [ + ), + 'reasoning' => array( 'type' => 'string', 'description' => 'One sentence explaining pattern choices.', - ], - ], - 'required' => [ 'title', 'patterns', 'reasoning' ], - ]; + ), + ), + 'required' => array( 'title', 'patterns', 'reasoning' ), + ); $prompt = <<generate_text(); } catch ( \Throwable $e ) { - return [ 'error' => 'AI request failed: ' . $e->getMessage() ]; + return array( 'error' => 'AI request failed: ' . $e->getMessage() ); } if ( is_wp_error( $raw ) ) { - return [ 'error' => 'AI provider error: ' . $raw->get_error_message() ]; + return array( 'error' => 'AI provider error: ' . $raw->get_error_message() ); } if ( ! is_string( $raw ) ) { - return [ 'error' => 'AI returned an unexpected type: ' . gettype( $raw ) ]; + return array( 'error' => 'AI returned an unexpected type: ' . gettype( $raw ) ); } $data = json_decode( $raw, true ); if ( ! is_array( $data ) || empty( $data['patterns'] ) ) { - return [ 'error' => 'AI returned an unexpected response. Raw output: ' . esc_html( substr( $raw, 0, 300 ) ) ]; + return array( 'error' => 'AI returned an unexpected response. Raw output: ' . esc_html( substr( $raw, 0, 300 ) ) ); } $post_id = PatternLab::create_page( $data['title'] ?? $description, $data['patterns'], 'draft' ); if ( is_wp_error( $post_id ) ) { - return [ 'error' => $post_id->get_error_message() ]; + return array( 'error' => $post_id->get_error_message() ); } $reasoning = $data['reasoning'] ?? ''; @@ -143,13 +204,13 @@ public static function generate_page( string $description ): array { update_post_meta( $post_id, '_waygate_patterns', wp_json_encode( $data['patterns'] ) ); update_post_meta( $post_id, '_waygate_generated_at', current_time( 'mysql' ) ); - return [ + return array( 'title' => $data['title'] ?? $description, 'patterns' => $data['patterns'], 'pattern_count' => count( $data['patterns'] ), 'reasoning' => $reasoning, 'edit_url' => get_edit_post_link( $post_id, 'raw' ), 'view_url' => get_permalink( $post_id ), - ]; + ); } } From b2751313dff498741473339811d8351386421435 Mon Sep 17 00:00:00 2001 From: Jasper Frumau Date: Sun, 24 May 2026 15:08:52 +0700 Subject: [PATCH 2/8] chore: add phpcs.xml config and fix all coding standard violations Add phpcs.xml to declare WordPress standard and suppress the InvalidClassFileName sniff (WordPress convention uses hyphens in file names for multi-word classes, e.g. class-pattern-lab.php). Fix pre-existing violations across all plugin files: - Add missing file, class, and method doc comments - Convert short array syntax to long form (phpcbf auto-fix) - Fix Yoda condition in Admin::render_page() - Add sanitize_textarea_field() to textarea echo in admin form - Add @package tag to waygate.php file header Update CLAUDE.md to use the bare `vendor/bin/phpcs` command so phpcs.xml is picked up automatically. --- CLAUDE.md | 2 +- includes/class-abilities-api.php | 111 +++++++++++++++++++------------ includes/class-pattern-lab.php | 29 +++++--- phpcs.xml | 12 ++++ waygate.php | 10 +-- 5 files changed, 106 insertions(+), 58 deletions(-) create mode 100644 phpcs.xml diff --git a/CLAUDE.md b/CLAUDE.md index fa784e0..f27722b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ find . -name "*.php" -not -path "*/vendor/*" -exec php -l {} \; **Run PHP CodeSniffer:** ```bash -vendor/bin/phpcs --standard=WordPress includes/ waygate.php +vendor/bin/phpcs ``` **Run PHPUnit:** diff --git a/includes/class-abilities-api.php b/includes/class-abilities-api.php index 31d44ba..d3caeed 100644 --- a/includes/class-abilities-api.php +++ b/includes/class-abilities-api.php @@ -1,15 +1,29 @@ __( 'List Elayne Patterns', 'waygate' ), 'description' => __( 'Returns all available Elayne block patterns with slug, title, categories and keywords.', 'waygate' ), 'category' => 'content', - 'input_schema' => [ + 'input_schema' => array( 'type' => 'object', - 'properties' => [ - 'category' => [ + 'properties' => array( + 'category' => array( 'type' => 'string', 'description' => 'Optional category filter, e.g. "hero", "features", "cta".', - ], - ], - ], - 'output_schema' => [ + ), + ), + ), + 'output_schema' => array( 'type' => 'array', - 'items' => [ + 'items' => array( 'type' => 'object', - 'properties' => [ - 'slug' => [ 'type' => 'string' ], - 'title' => [ 'type' => 'string' ], - 'categories' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], - 'keywords' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], - ], - ], - ], - 'execute_callback' => function ( array $params = [] ): array { + 'properties' => array( + 'slug' => array( 'type' => 'string' ), + 'title' => array( 'type' => 'string' ), + 'categories' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'keywords' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + ), + ), + ), + 'execute_callback' => function ( array $params = array() ): array { $patterns = PatternLab::get_patterns(); if ( ! empty( $params['category'] ) ) { @@ -55,39 +75,39 @@ public static function register_abilities(): void { return $patterns; }, 'permission_callback' => fn() => current_user_can( 'edit_posts' ), - 'meta' => [ + 'meta' => array( 'show_in_rest' => true, - 'annotations' => [ 'readonly' => true ], - ], - ] + 'annotations' => array( 'readonly' => true ), + ), + ) ); wp_register_ability( 'elayne/create-page', - [ + array( 'label' => __( 'Create Page from Elayne Patterns', 'waygate' ), 'description' => __( 'Creates a new WordPress draft page assembled from Elayne block patterns.', 'waygate' ), 'category' => 'content', - 'input_schema' => [ + 'input_schema' => array( 'type' => 'object', - 'properties' => [ - 'title' => [ + 'properties' => array( + 'title' => array( 'type' => 'string', 'description' => 'Page title.', - ], - 'patterns' => [ + ), + 'patterns' => array( 'type' => 'array', - 'items' => [ 'type' => 'string' ], + 'items' => array( 'type' => 'string' ), 'description' => 'Ordered list of Elayne pattern slugs.', - ], - 'status' => [ + ), + 'status' => array( 'type' => 'string', - 'enum' => [ 'draft', 'publish' ], + 'enum' => array( 'draft', 'publish' ), 'default' => 'draft', - ], - ], - 'required' => [ 'title', 'patterns' ], - ], + ), + ), + 'required' => array( 'title', 'patterns' ), + ), 'execute_callback' => function ( array $params ): array { $result = PatternLab::create_page( $params['title'], @@ -96,22 +116,25 @@ public static function register_abilities(): void { ); if ( is_wp_error( $result ) ) { - return [ 'success' => false, 'error' => $result->get_error_message() ]; + return array( + 'success' => false, + 'error' => $result->get_error_message(), + ); } - return [ + return array( 'success' => true, 'page_id' => $result, 'edit_url' => get_edit_post_link( $result, 'raw' ), 'view_url' => get_permalink( $result ), - ]; + ); }, 'permission_callback' => fn() => current_user_can( 'publish_pages' ), - 'meta' => [ + 'meta' => array( 'show_in_rest' => true, - 'annotations' => [ 'idempotent' => true ], - ], - ] + 'annotations' => array( 'idempotent' => true ), + ), + ) ); } } diff --git a/includes/class-pattern-lab.php b/includes/class-pattern-lab.php index e0e5310..4f89f81 100644 --- a/includes/class-pattern-lab.php +++ b/includes/class-pattern-lab.php @@ -1,11 +1,22 @@ get_all_registered(); - $prefixes = apply_filters( 'waygate_pattern_prefixes', [ 'elayne/' ] ); - $patterns = []; + $prefixes = apply_filters( 'waygate_pattern_prefixes', array( 'elayne/' ) ); + $patterns = array(); foreach ( $all as $p ) { if ( empty( $p['slug'] ) ) { @@ -37,13 +48,13 @@ public static function get_patterns(): array { continue; } - $patterns[] = [ + $patterns[] = array( 'slug' => $p['slug'], 'title' => $p['title'] ?? '', 'description' => $p['description'] ?? '', - 'categories' => $p['categories'] ?? [], - 'keywords' => $p['keywords'] ?? [], - ]; + 'categories' => $p['categories'] ?? array(), + 'keywords' => $p['keywords'] ?? array(), + ); } return $patterns; @@ -80,12 +91,12 @@ public static function create_page( string $title, array $pattern_slugs, string } return wp_insert_post( - [ + array( 'post_title' => sanitize_text_field( $title ), 'post_content' => $content, - 'post_status' => in_array( $status, [ 'draft', 'publish' ], true ) ? $status : 'draft', + 'post_status' => in_array( $status, array( 'draft', 'publish' ), true ) ? $status : 'draft', 'post_type' => 'page', - ], + ), true ); } diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..5f06427 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,12 @@ + + + Waygate PHPCS configuration + + includes/ + waygate.php + + + + + + diff --git a/waygate.php b/waygate.php index 2bc21c5..071e72e 100644 --- a/waygate.php +++ b/waygate.php @@ -12,6 +12,8 @@ * Domain Path: /languages * Requires at least: 7.0 * Requires PHP: 8.3 + * + * @package Imagewize\Waygate */ defined( 'ABSPATH' ) || exit; @@ -25,7 +27,7 @@ require_once WAYGATE_PLUGIN_DIR . 'includes/class-ai-integration.php'; require_once WAYGATE_PLUGIN_DIR . 'includes/class-admin.php'; -add_action( 'plugins_loaded', [ 'Imagewize\\Waygate\\PatternLab', 'init' ] ); -add_action( 'plugins_loaded', [ 'Imagewize\\Waygate\\AiIntegration', 'init' ] ); -add_action( 'plugins_loaded', [ 'Imagewize\\Waygate\\AbilitiesApi', 'init' ] ); -add_action( 'plugins_loaded', [ 'Imagewize\\Waygate\\Admin', 'init' ] ); +add_action( 'plugins_loaded', array( 'Imagewize\\Waygate\\PatternLab', 'init' ) ); +add_action( 'plugins_loaded', array( 'Imagewize\\Waygate\\AiIntegration', 'init' ) ); +add_action( 'plugins_loaded', array( 'Imagewize\\Waygate\\AbilitiesApi', 'init' ) ); +add_action( 'plugins_loaded', array( 'Imagewize\\Waygate\\Admin', 'init' ) ); From bf9b80785e13fd619d351e7b701799426c5c280d Mon Sep 17 00:00:00 2001 From: Jasper Frumau Date: Sun, 24 May 2026 15:09:03 +0700 Subject: [PATCH 3/8] test: add prompt template unit tests and extend bootstrap stubs Add five tests for AiIntegration::get_prompt_templates(): - returns an array - has all six expected template keys - each template has non-empty label, description, and prompt fields - filter hook allows third parties to add templates - homepage template prompt contains [industry] placeholder Add remove_all_filters() stub to the test bootstrap so filter teardown in tests works without a full WordPress environment. --- tests/Unit/AiIntegrationTest.php | 56 ++++++++++++++++++++++++++++++++ tests/bootstrap.php | 4 +++ 2 files changed, 60 insertions(+) diff --git a/tests/Unit/AiIntegrationTest.php b/tests/Unit/AiIntegrationTest.php index 3609f77..d48a5f7 100644 --- a/tests/Unit/AiIntegrationTest.php +++ b/tests/Unit/AiIntegrationTest.php @@ -12,4 +12,60 @@ public function test_is_text_generation_not_supported_when_wp_client_missing(): $this->assertFalse( function_exists( 'wp_ai_client_prompt' ) ); $this->assertFalse( AiIntegration::is_text_generation_supported() ); } + + public function test_get_prompt_templates_returns_array(): void { + $templates = AiIntegration::get_prompt_templates(); + $this->assertIsArray( $templates ); + } + + public function test_get_prompt_templates_has_expected_keys(): void { + $templates = AiIntegration::get_prompt_templates(); + $this->assertArrayHasKey( 'homepage', $templates ); + $this->assertArrayHasKey( 'about', $templates ); + $this->assertArrayHasKey( 'services', $templates ); + $this->assertArrayHasKey( 'contact', $templates ); + $this->assertArrayHasKey( 'landing', $templates ); + $this->assertArrayHasKey( 'portfolio', $templates ); + } + + public function test_each_template_has_required_fields(): void { + foreach ( AiIntegration::get_prompt_templates() as $key => $tpl ) { + $this->assertArrayHasKey( 'label', $tpl, "Template '{$key}' missing 'label'" ); + $this->assertArrayHasKey( 'description', $tpl, "Template '{$key}' missing 'description'" ); + $this->assertArrayHasKey( 'prompt', $tpl, "Template '{$key}' missing 'prompt'" ); + $this->assertNotEmpty( $tpl['label'], "Template '{$key}' has empty 'label'" ); + $this->assertNotEmpty( $tpl['description'], "Template '{$key}' has empty 'description'" ); + $this->assertNotEmpty( $tpl['prompt'], "Template '{$key}' has empty 'prompt'" ); + } + } + + public function test_get_prompt_templates_is_filterable(): void { + // Simulate a third-party adding a template via the filter. + $extra = array( + 'label' => 'FAQ', + 'description' => 'Frequently asked questions', + 'prompt' => 'Create an FAQ page for a [industry] business', + ); + + // apply_filters is not available in unit tests; call the filter manually. + add_filter( + 'waygate_prompt_templates', + function ( array $templates ) use ( $extra ): array { + $templates['faq'] = $extra; + return $templates; + } + ); + + $templates = AiIntegration::get_prompt_templates(); + + remove_all_filters( 'waygate_prompt_templates' ); + + $this->assertArrayHasKey( 'faq', $templates ); + $this->assertSame( $extra['label'], $templates['faq']['label'] ); + } + + public function test_homepage_template_prompt_contains_placeholder(): void { + $templates = AiIntegration::get_prompt_templates(); + $this->assertStringContainsString( '[industry]', $templates['homepage']['prompt'] ); + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index a3d9d3d..7dc32c1 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -22,6 +22,10 @@ function apply_filters( string $tag, mixed $value, mixed ...$args ): mixed { return $value; } +function remove_all_filters( string $tag ): void { + unset( $GLOBALS['wp_filters'][ $tag ] ); +} + function sanitize_text_field( string $str ): string { return trim( strip_tags( $str ) ); } From 8c4783026ceb943021b4f0828134114e144583cf Mon Sep 17 00:00:00 2001 From: Jasper Frumau Date: Sun, 24 May 2026 15:09:19 +0700 Subject: [PATCH 4/8] docs: mark Phase 1 complete and set Phase 2 as next in roadmap All four Phase 1 items are confirmed shipped as of 2026-05-24: feature detection, ability annotations, generic pattern prefix filter, and category filter dropdown in admin UI. Update Implementation Priority to start Phase 2 with prompt templates (now done), followed by REST API, client-side abilities, image generation, and batch page creation. --- docs/ROADMAP.md | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ba2e6fb..5e55304 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,7 +1,7 @@ # Waygate Roadmap & Improvement Ideas -> **Document Version**: 1.0.0 -> **Last Updated**: 2026-05-22 +> **Document Version**: 1.1.0 +> **Last Updated**: 2026-05-24 > **Status**: Draft This document outlines potential improvements and feature additions for Waygate, based on WordPress 7.0 AI Client and Abilities API capabilities. @@ -73,9 +73,9 @@ Based on [Introducing the AI Client in WordPress 7.0](https://make.wordpress.org ## Improvement Ideas -### Phase 1: Quick Wins (Low Effort, High Impact) +### Phase 1: Quick Wins ✅ Complete -#### 1. Add Feature Detection +#### 1. Add Feature Detection ✅ **File**: `includes/class-ai-integration.php` **File**: `includes/class-admin.php` @@ -88,7 +88,7 @@ $image_gen_supported = $builder->is_supported_for_image_generation(); **Benefit**: Hide AI features when no compatible provider is configured. -#### 2. Add Ability Annotations +#### 2. Add Ability Annotations ✅ **File**: `includes/class-abilities-api.php` ```php @@ -121,7 +121,7 @@ wp_register_ability( **Benefit**: Better REST API behavior, clearer ability semantics. -#### 3. Support Generic Pattern Prefixes +#### 3. Support Generic Pattern Prefixes ✅ **File**: `includes/class-pattern-lab.php` ```php @@ -134,14 +134,14 @@ public static function get_patterns(): array { **Benefit**: Works with any block theme, not just Elayne. -#### 4. Add Pattern Category Filter in Admin UI +#### 4. Add Pattern Category Filter in Admin UI ✅ **File**: `includes/class-admin.php` Add a dropdown to filter patterns by category in the catalog table. **Benefit**: Easier pattern discovery for users. -### Phase 2: Medium Effort +### Phase 2: Medium Effort ← **Next Up** #### 5. Add Image Generation for Pattern Previews **New file**: `includes/class-image-generator.php` @@ -446,21 +446,23 @@ public static function track_pattern_usage( string $pattern_slug ): void { ### Recommended Order -1. Feature detection, ability annotations, generic prefixes — *Phase 1* (2–3 days with tests) -2. REST API, client-side abilities, prompt templates — *Phase 2* (3–5 days) -3. Image generation for previews — *Phase 2* (2–3 days) -4. Batch page creation — *Phase 2* (1–2 days) -5. Cost tracking, pattern popularity — *Phase 3* (2–3 days) -6. Advanced features based on user feedback — *Phase 3, speculative* +1. ~~Feature detection, ability annotations, generic prefixes — *Phase 1*~~ ✅ Done +2. Prompt templates — *Phase 2* (1–2 days) ← **Start here** +3. REST API endpoints — *Phase 2* (2–3 days) +4. Client-side abilities for editor integration — *Phase 2* (2–3 days) +5. Image generation for previews — *Phase 2* (2–3 days) +6. Batch page creation — *Phase 2* (1–2 days) +7. Cost tracking, pattern popularity — *Phase 3* (2–3 days) +8. Advanced features based on user feedback — *Phase 3, speculative* -### Quick Start (2–3 days with tests) +### Quick Start — Phase 1 Complete ✅ -> The four items below are low-effort code changes, but "1 day" assumes no test coverage. If you're writing or updating PHPUnit tests alongside each change (recommended), budget 2–3 days. +All four Phase 1 items shipped as of 2026-05-24: -- Add feature detection -- Add ability annotations -- Add generic pattern prefix filter -- Add category filter to admin UI +- ~~Add feature detection~~ ✅ +- ~~Add ability annotations~~ ✅ +- ~~Add generic pattern prefix filter~~ ✅ +- ~~Add category filter to admin UI~~ ✅ --- @@ -574,3 +576,4 @@ const abilities = getAbilities(); | Date | Author | Change | |------|--------|--------| | 2026-05-22 | Initial draft | Created roadmap document | +| 2026-05-24 | Jasper Frumau | Marked Phase 1 complete; Phase 2 is next | From d49ecc42c7d8eb4409c90304b16413d2bdc7e6fe Mon Sep 17 00:00:00 2001 From: Jasper Frumau Date: Sun, 24 May 2026 15:14:22 +0700 Subject: [PATCH 5/8] chore: add .vibe tooling config to repository Track Vibe CLI project configuration (.vibe/) alongside the existing .claude/ tooling. Excluded from Composer archive via composer.json so it does not ship to end users via Packagist. --- .vibe/config.toml | 186 ++++++++++++++++++++++++++++++++++++++++++ .vibe/prompts/vibe.md | 86 +++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 .vibe/config.toml create mode 100644 .vibe/prompts/vibe.md diff --git a/.vibe/config.toml b/.vibe/config.toml new file mode 100644 index 0000000..6d57383 --- /dev/null +++ b/.vibe/config.toml @@ -0,0 +1,186 @@ +active_model = "mistral-medium-3.5" +vim_keybindings = false +disable_welcome_banner_animation = false +displayed_workdir = "" +auto_compact_threshold = 200000 +context_warnings = false +textual_theme = "catppuccin-mocha" +instructions = "" +system_prompt_id = "vibe" +include_commit_signature = true +include_model_info = true +include_project_context = true +include_prompt_detail = true +enable_update_checks = true +api_timeout = 720.0 +tool_paths = [] +mcp_servers = [] +enabled_tools = [] +disabled_tools = [] + +[[providers]] +name = "mistral" +api_base = "https://api.mistral.ai/v1" +api_key_env_var = "MISTRAL_API_KEY" +api_style = "openai" +backend = "mistral" + +[[providers]] +name = "llamacpp" +api_base = "http://127.0.0.1:8080/v1" +api_key_env_var = "" +api_style = "openai" +backend = "generic" + +[[models]] +name = "mistral-vibe-cli-latest" +provider = "mistral" +alias = "mistral-medium-3.5" +temperature = 1.0 +input_price = 1.5 +output_price = 7.5 +thinking = "high" + +[[models]] +name = "devstral-small-latest" +provider = "mistral" +alias = "devstral-small" +temperature = 0.2 +input_price = 0.1 +output_price = 0.3 + +[[models]] +name = "devstral" +provider = "llamacpp" +alias = "local" +temperature = 0.2 +input_price = 0.0 +output_price = 0.0 + +[project_context] +max_chars = 40000 +default_commit_count = 5 +max_doc_bytes = 32768 +truncation_buffer = 1000 +max_depth = 3 +max_files = 1000 +max_dirs_per_level = 20 +timeout_seconds = 2.0 + +[session_logging] +save_dir = "/Users/jasperfrumau/.vibe/logs/session" +session_prefix = "session" +enabled = true + +[tools.search_replace] +permission = "ask" +allowlist = [] +denylist = [] +max_content_size = 100000 +create_backup = false +fuzzy_threshold = 0.9 + +[tools.bash] +permission = "ask" +allowlist = [ + "cat", + "echo", + "file", + "find", + "git diff", + "git log", + "git status", + "head", + "ls", + "pwd", + "stat", + "tail", + "tree", + "uname", + "wc", + "which", + "whoami", +] +denylist = [ + "gdb", + "pdb", + "passwd", + "nano", + "vim", + "vi", + "emacs", + "bash -i", + "sh -i", + "zsh -i", + "fish -i", + "dash -i", + "screen", + "tmux", +] +max_output_bytes = 16000 +default_timeout = 30 +denylist_standalone = [ + "ipython", + "bash", + "sh", + "nohup", + "vi", + "vim", + "emacs", + "nano", + "su", +] + +[tools.grep] +permission = "always" +allowlist = [] +denylist = [] +max_output_bytes = 64000 +default_max_matches = 100 +default_timeout = 60 +exclude_patterns = [ + ".venv/", + "venv/", + ".env/", + "env/", + "node_modules/", + ".git/", + "__pycache__/", + ".pytest_cache/", + ".mypy_cache/", + ".tox/", + ".nox/", + ".coverage/", + "htmlcov/", + "dist/", + "build/", + ".idea/", + ".vscode/", + "*.egg-info", + "*.pyc", + "*.pyo", + "*.pyd", + ".DS_Store", + "Thumbs.db", +] +codeignore_file = ".vibeignore" + +[tools.read_file] +permission = "always" +allowlist = [] +denylist = [] +max_read_bytes = 64000 +max_state_history = 10 + +[tools.todo] +permission = "always" +allowlist = [] +denylist = [] +max_todos = 100 + +[tools.write_file] +permission = "ask" +allowlist = [] +denylist = [] +max_write_bytes = 64000 +create_parent_dirs = true diff --git a/.vibe/prompts/vibe.md b/.vibe/prompts/vibe.md new file mode 100644 index 0000000..8bd3881 --- /dev/null +++ b/.vibe/prompts/vibe.md @@ -0,0 +1,86 @@ +# Custom Instructions for Vibe + +## Primary Directive +Always follow the rules and conventions in the project's `CLAUDE.md` file. This is your authoritative source for all project-specific guidance about Waygate. + +## Project Overview +Waygate is a standalone WordPress plugin (`imagewize/waygate`) that provides AI-powered pattern page builder capabilities for the Elayne block theme. It integrates with the WordPress Abilities API (WP 7.0+) and WordPress AI Client to generate pages from natural-language descriptions. + +- **Namespace:** `Imagewize\Waygate` +- **Requires:** PHP 8.3+, WordPress 7.0+ +- **All classes** live in `includes/` with PSR-4 autoloading + +## Before Responding +1. **Review CLAUDE.md** for: + - Project architecture and file structure + - Class responsibilities (PatternLab, AbilitiesApi, AiIntegration, Admin) + - Initialization order + - Code style and naming conventions + - Git commit guidelines + +2. **Check Current Context**: + - Verify you're working in the plugin directory + - Confirm the current branch and git status + - Review recent commits for relevant changes + +3. **Adhere to Key Principles**: + - Never mention AI tools (Claude, Mistral, Vibe) in commit messages + - Use conventional commit prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:` + - Make atomic commits: each commit is one logical change that builds/passes on its own + - All PHP code must pass `vendor/bin/phpcs --standard=WordPress` + +## Commands +**Install dependencies:** +```bash +composer install +``` + +**PHP syntax check:** +```bash +find . -name "*.php" -not -path "*/vendor/*" -exec php -l {} \; +``` + +**Run PHP CodeSniffer:** +```bash +vendor/bin/phpcs --standard=WordPress includes/ waygate.php +``` + +**Run PHPUnit:** +```bash +vendor/bin/phpunit --configuration phpunit.xml +``` + +## Architecture +- **PatternLab** (`includes/class-pattern-lab.php`) — Data layer for pattern querying and page composition +- **AbilitiesApi** (`includes/class-abilities-api.php`) — Registers `elayne/list-patterns` and `elayne/create-page` abilities +- **AiIntegration** (`includes/class-ai-integration.php`) — Orchestrates AI page generation with Mistral provider +- **Admin** (`includes/class-admin.php`) — WordPress admin UI at **Tools → Waygate** + +**Initialization Order:** +``` +plugins_loaded → PatternLab::init() + → AiIntegration::init() + → AbilitiesApi::init() + → Admin::init() +``` + +`PatternLab` must init first since other classes depend on its pattern data. + +## Response Guidelines +- Be concise and technical +- Reference specific files and line numbers +- Use markdown formatting for code and structure +- Prioritize verification over assumptions +- When unsure, ask for clarification before acting + +## Version Bump Checklist +When releasing a new version, update the version string in **three places**: +1. `waygate.php` line 8 — `* Version:` plugin header +2. `waygate.php` line 19 — `define( 'WAYGATE_VERSION', ... )` constant +3. `CHANGELOG.md` — add a new `## [x.y.z] - YYYY-MM-DD` section at the top + +## NO Mistral Vibe Attribution in Any Commits +- Do NOT include "Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe " attribution +- Keep all commits professional and attribution-free +- This applies to ALL files and directories in the entire repository +- Follow standard git commit message format From 1df2c96b89c6b1faa3a1b4a2433e823b10371ae4 Mon Sep 17 00:00:00 2001 From: Jasper Frumau Date: Sun, 24 May 2026 15:14:30 +0700 Subject: [PATCH 6/8] chore: bump version to 0.6.0 Update Version header and WAYGATE_VERSION constant in waygate.php. Add .vibe/ to composer.json archive excludes. --- composer.json | 2 +- waygate.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index a58d99f..32f094e 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "../../plugins/{$name}/": ["type:wordpress-plugin"] }, "archive": { - "exclude": ["docs/", "tests/", ".github/", "*.md", "!README.md"] + "exclude": ["docs/", "tests/", ".github/", ".vibe/", "*.md", "!README.md"] } }, "config": { diff --git a/waygate.php b/waygate.php index 071e72e..37a3cb2 100644 --- a/waygate.php +++ b/waygate.php @@ -3,7 +3,7 @@ * Plugin Name: Waygate * Plugin URI: https://github.com/imagewize/waygate * Description: AI-powered pattern page builder for the Elayne block theme. Lists registered patterns, creates pages from pattern slugs, and integrates with WordPress AI Client for natural-language page generation. - * Version: 0.5.0 + * Version: 0.6.0 * Author: Jasper Frumau * Author URI: https://imagewize.com * License: GPL-2.0-or-later @@ -18,7 +18,7 @@ defined( 'ABSPATH' ) || exit; -define( 'WAYGATE_VERSION', '0.5.0' ); +define( 'WAYGATE_VERSION', '0.6.0' ); define( 'WAYGATE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'WAYGATE_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); From c0e4d214873437e3ab8073d6eeae1f62a022593c Mon Sep 17 00:00:00 2001 From: Jasper Frumau Date: Sun, 24 May 2026 15:14:43 +0700 Subject: [PATCH 7/8] docs: update CHANGELOG and README for v0.6.0 Add 0.6.0 release notes covering prompt templates, Quick Template dropdown, waygate_prompt_templates filter, new unit tests, and phpcs.xml. Bump version badge in README and add prompt templates to the feature list. --- CHANGELOG.md | 12 ++++++++++++ README.md | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c328b00..2d197f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to Waygate will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.6.0] - 2026-05-24 + +### Added +- Six built-in prompt templates (Homepage, About, Services, Contact, Landing Page, Portfolio) in `AiIntegration::get_prompt_templates()`; filterable via the `waygate_prompt_templates` hook so third-party themes/plugins can add or remove templates +- **Quick Template** dropdown in the admin AI generation form — selecting a template pre-fills the description textarea; a confirmation dialog fires when the textarea already has content +- `[placeholder]` substitution note added to the description field hint +- PHPUnit tests for `get_prompt_templates()` and the `waygate_prompt_templates` filter +- `phpcs.xml` PHP CodeSniffer configuration committed so `vendor/bin/phpcs` works without arguments + +### Changed +- All PHP files now pass WPCS coding standards (resolved via `phpcs.xml` ruleset) + ## [0.5.0] - 2026-05-24 ### Added diff --git a/README.md b/README.md index 6036c76..a44b90f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Waygate lets you assemble WordPress pages from block patterns — manually or via a natural-language AI prompt powered by the WordPress AI Client (WordPress 7.0+). Works with any block theme; [Elayne](https://github.com/imagewize/elayne) is the primary supported theme. -> **Beta** — v0.5.0. Use on staging/development sites; not yet recommended for production. +> **Beta** — v0.6.0. Use on staging/development sites; not yet recommended for production. --- @@ -14,6 +14,7 @@ Waygate lets you assemble WordPress pages from block patterns — manually or vi - **AI page generation** — Describe the page you want; the AI picks patterns and creates a draft - **AI reasoning** — The AI's one-sentence explanation of its pattern choices is shown after generation and persisted as post meta on the created page - **Developer debug info** — When `WP_ENV=development`, the page editor sidebar and the generation notice also show the ordered pattern slugs and generation timestamp +- **Prompt templates** — Six built-in page templates (Homepage, About, Services, Contact, Landing Page, Portfolio) pre-fill the AI prompt; extend via the `waygate_prompt_templates` filter - **Feature detection** — AI form is hidden automatically when no provider supports text generation - **Abilities API** — Exposes `elayne/list-patterns` and `elayne/create-page` abilities for WP 7.0+ - **Multi-provider** — Works with Mistral, Claude, OpenAI, or Gemini via WP AI Client From 3ec1fa2d81b158e73311df6ea38c521b4999d74a Mon Sep 17 00:00:00 2001 From: Jasper Frumau Date: Sun, 24 May 2026 15:28:17 +0700 Subject: [PATCH 8/8] refactor: rename classes to WordPress underscore convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pattern_Lab, Abilities_API, AI_Integration replace PatternLab, AbilitiesApi, AiIntegration to comply with WordPress coding standards and unblock WordPress.org submission. Removes the InvalidClassFileName PHPCS exclusion — renamed classes pass cleanly. --- .vibe/prompts/vibe.md | 20 ++++++++++---------- CHANGELOG.md | 4 +++- CLAUDE.md | 14 +++++++------- includes/class-abilities-api.php | 6 +++--- includes/class-admin.php | 12 ++++++------ includes/class-ai-integration.php | 6 +++--- includes/class-pattern-lab.php | 2 +- phpcs.xml | 5 +---- tests/Unit/AiIntegrationTest.php | 14 +++++++------- tests/Unit/PatternLabTest.php | 20 ++++++++++---------- waygate.php | 6 +++--- 11 files changed, 54 insertions(+), 55 deletions(-) diff --git a/.vibe/prompts/vibe.md b/.vibe/prompts/vibe.md index 8bd3881..6b7cb82 100644 --- a/.vibe/prompts/vibe.md +++ b/.vibe/prompts/vibe.md @@ -8,12 +8,12 @@ Waygate is a standalone WordPress plugin (`imagewize/waygate`) that provides AI- - **Namespace:** `Imagewize\Waygate` - **Requires:** PHP 8.3+, WordPress 7.0+ -- **All classes** live in `includes/` with PSR-4 autoloading +- **All classes** live in `includes/` with classmap autoloading ## Before Responding 1. **Review CLAUDE.md** for: - Project architecture and file structure - - Class responsibilities (PatternLab, AbilitiesApi, AiIntegration, Admin) + - Class responsibilities (Pattern_Lab, Abilities_API, AI_Integration, Admin) - Initialization order - Code style and naming conventions - Git commit guidelines @@ -42,7 +42,7 @@ find . -name "*.php" -not -path "*/vendor/*" -exec php -l {} \; **Run PHP CodeSniffer:** ```bash -vendor/bin/phpcs --standard=WordPress includes/ waygate.php +vendor/bin/phpcs ``` **Run PHPUnit:** @@ -51,20 +51,20 @@ vendor/bin/phpunit --configuration phpunit.xml ``` ## Architecture -- **PatternLab** (`includes/class-pattern-lab.php`) — Data layer for pattern querying and page composition -- **AbilitiesApi** (`includes/class-abilities-api.php`) — Registers `elayne/list-patterns` and `elayne/create-page` abilities -- **AiIntegration** (`includes/class-ai-integration.php`) — Orchestrates AI page generation with Mistral provider +- **Pattern_Lab** (`includes/class-pattern-lab.php`) — Data layer for pattern querying and page composition +- **Abilities_API** (`includes/class-abilities-api.php`) — Registers `elayne/list-patterns` and `elayne/create-page` abilities +- **AI_Integration** (`includes/class-ai-integration.php`) — Orchestrates AI page generation with Mistral provider - **Admin** (`includes/class-admin.php`) — WordPress admin UI at **Tools → Waygate** **Initialization Order:** ``` -plugins_loaded → PatternLab::init() - → AiIntegration::init() - → AbilitiesApi::init() +plugins_loaded → Pattern_Lab::init() + → AI_Integration::init() + → Abilities_API::init() → Admin::init() ``` -`PatternLab` must init first since other classes depend on its pattern data. +`Pattern_Lab` must init first since other classes depend on its pattern data. ## Response Guidelines - Be concise and technical diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d197f9..90b9eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.6.0] - 2026-05-24 ### Added -- Six built-in prompt templates (Homepage, About, Services, Contact, Landing Page, Portfolio) in `AiIntegration::get_prompt_templates()`; filterable via the `waygate_prompt_templates` hook so third-party themes/plugins can add or remove templates +- Six built-in prompt templates (Homepage, About, Services, Contact, Landing Page, Portfolio) in `AI_Integration::get_prompt_templates()`; filterable via the `waygate_prompt_templates` hook so third-party themes/plugins can add or remove templates - **Quick Template** dropdown in the admin AI generation form — selecting a template pre-fills the description textarea; a confirmation dialog fires when the textarea already has content - `[placeholder]` substitution note added to the description field hint - PHPUnit tests for `get_prompt_templates()` and the `waygate_prompt_templates` filter - `phpcs.xml` PHP CodeSniffer configuration committed so `vendor/bin/phpcs` works without arguments ### Changed +- Renamed plugin classes to WordPress underscore convention: `Pattern_Lab`, `Abilities_API`, `AI_Integration` (previously `PatternLab`, `AbilitiesApi`, `AiIntegration`) to comply with WordPress coding standards and allow WordPress.org submission +- Removed `WordPress.Files.FileName.InvalidClassFileName` PHPCS exclusion from `phpcs.xml` — the renamed classes now pass the sniff cleanly - All PHP files now pass WPCS coding standards (resolved via `phpcs.xml` ruleset) ## [0.5.0] - 2026-05-24 diff --git a/CLAUDE.md b/CLAUDE.md index f27722b..ae9166b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,28 +38,28 @@ All classes live in `includes/` with PSR-4 autoloading (`Imagewize\Waygate` → ### Class Responsibilities -**`PatternLab`** (`includes/class-pattern-lab.php`) — The data layer. Queries WordPress's registered block patterns to extract those belonging to the active Elayne theme, returning structured metadata (slug, title, description, categories, keywords). Also handles composing `wp:pattern` blocks into a page and calling `wp_insert_post`. +**`Pattern_Lab`** (`includes/class-pattern-lab.php`) — The data layer. Queries WordPress's registered block patterns to extract those belonging to the active Elayne theme, returning structured metadata (slug, title, description, categories, keywords). Also handles composing `wp:pattern` blocks into a page and calling `wp_insert_post`. -**`AbilitiesApi`** (`includes/class-abilities-api.php`) — Registers two WordPress Abilities (WP 7.0+ feature): +**`Abilities_API`** (`includes/class-abilities-api.php`) — Registers two WordPress Abilities (WP 7.0+ feature): - `elayne/list-patterns` — lists available patterns, optionally filtered by category; requires `edit_posts` - `elayne/create-page` — creates a draft page from an ordered pattern slug list; requires `publish_pages` Each ability has a JSON input/output schema for capability validation. -**`AiIntegration`** (`includes/class-ai-integration.php`) — Orchestrates AI page generation. Registers a Mistral provider with the WP AI Client registry, then in `generate_page()` builds a prompt from all available patterns, sends it to the AI (with fallback chain: Mistral → Claude → OpenAI → Gemini), parses the JSON response, and delegates to `PatternLab::create_page()`. Enforces layout constraints: 3–7 patterns, hero first, CTA last, no consecutive grid patterns. +**`AI_Integration`** (`includes/class-ai-integration.php`) — Orchestrates AI page generation. Registers a Mistral provider with the WP AI Client registry, then in `generate_page()` builds a prompt from all available patterns, sends it to the AI (with fallback chain: Mistral → Claude → OpenAI → Gemini), parses the JSON response, and delegates to `Pattern_Lab::create_page()`. Enforces layout constraints: 3–7 patterns, hero first, CTA last, no consecutive grid patterns. **`Admin`** (`includes/class-admin.php`) — WordPress admin UI at **Tools → Waygate**. Renders status indicators (WP AI Client, Abilities API, Mistral provider, Elayne patterns), the AI generation form, and a searchable pattern catalog. Handles POST form submission with nonce verification and displays success/error notices. ### Initialization Order ``` -plugins_loaded → PatternLab::init() - → AiIntegration::init() - → AbilitiesApi::init() +plugins_loaded → Pattern_Lab::init() + → AI_Integration::init() + → Abilities_API::init() → Admin::init() ``` -`PatternLab` must init first since the other classes depend on its pattern data. +`Pattern_Lab` must init first since the other classes depend on its pattern data. ### External Dependencies (optional) diff --git a/includes/class-abilities-api.php b/includes/class-abilities-api.php index d3caeed..0fea4ab 100644 --- a/includes/class-abilities-api.php +++ b/includes/class-abilities-api.php @@ -12,7 +12,7 @@ /** * Registers the elayne/list-patterns and elayne/create-page abilities. */ -class AbilitiesApi { +class Abilities_API { /** * Hooks into the Abilities API init action. @@ -63,7 +63,7 @@ public static function register_abilities(): void { ), ), 'execute_callback' => function ( array $params = array() ): array { - $patterns = PatternLab::get_patterns(); + $patterns = Pattern_Lab::get_patterns(); if ( ! empty( $params['category'] ) ) { $cat = 'elayne/' . sanitize_key( $params['category'] ); @@ -109,7 +109,7 @@ public static function register_abilities(): void { 'required' => array( 'title', 'patterns' ), ), 'execute_callback' => function ( array $params ): array { - $result = PatternLab::create_page( + $result = Pattern_Lab::create_page( $params['title'], $params['patterns'], $params['status'] ?? 'draft' diff --git a/includes/class-admin.php b/includes/class-admin.php index af30482..6a830ce 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -40,7 +40,7 @@ public static function register_menu(): void { */ public static function render_page(): void { $ai_available = function_exists( 'wp_ai_client_prompt' ); - $text_gen_supported = AiIntegration::is_text_generation_supported(); + $text_gen_supported = AI_Integration::is_text_generation_supported(); $result = null; if ( @@ -58,12 +58,12 @@ public static function render_page(): void { if ( empty( $description ) ) { $result = array( 'error' => 'Please describe the page you want to create.' ); } else { - $result = AiIntegration::generate_page( $description ); + $result = AI_Integration::generate_page( $description ); } } } - $all_patterns = PatternLab::get_patterns(); + $all_patterns = Pattern_Lab::get_patterns(); usort( $all_patterns, fn( $a, $b ) => strcmp( $a['slug'], $b['slug'] ) ); // Build unique category list (strip namespace prefix for display/filtering). @@ -122,7 +122,7 @@ function ( $p ) use ( $selected_category ) {