From 24cce7bbdf6c2e76d7e32dc13511e248a10fc857 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Wed, 15 Apr 2026 21:18:56 +0200 Subject: [PATCH 01/15] Phase 2: dual-load progressplanner/wp-admin-ui behind feature flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the extracted admin-ui kit as a composer dependency (vcs repo, dev-main) and wires a shadow admin page that boots AdminUI alongside the existing progress-planner admin page. Behind the PROGRESS_PLANNER_USE_ADMIN_UI_PKG constant — off by default, so production behavior is unchanged. - composer.json: require progressplanner/wp-admin-ui dev-main - progress-planner.php: conditionally load composer autoload + boot shadow page when the flag is defined-and-truthy - classes/admin/class-admin-ui-pkg-shadow.php: shadow page + smoke-test widget rendering via the kit Verified: with flag off, PHP syntax + composer valid, no behavior change. With flag on, shadow page at ?page=progress-planner-adminui renders a gauge inside a proper widget wrapper, confirming the kit autoloads and works inside progress-planner's PHP environment. Phase 3 note: the shadow page sets host_assets_path to a non-existent dir because progress-planner's own asset file headers use qualified 'progress-planner/...' dep strings that the kit's enqueuer can't resolve yet. That gets fixed in Phase 3 when we set the kit's asset_prefix to 'progress-planner' — then legacy headers will round-trip cleanly. --- classes/admin/class-admin-ui-pkg-shadow.php | 92 +++++++++++++++++++++ composer.json | 9 ++ composer.lock | 55 +++++++++++- progress-planner.php | 20 +++++ 4 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 classes/admin/class-admin-ui-pkg-shadow.php diff --git a/classes/admin/class-admin-ui-pkg-shadow.php b/classes/admin/class-admin-ui-pkg-shadow.php new file mode 100644 index 000000000..6d5f3a8b6 --- /dev/null +++ b/classes/admin/class-admin-ui-pkg-shadow.php @@ -0,0 +1,92 @@ +get_ui__branding()->get_admin_menu_icon( true ); + + $config = new Config( + 'progress-planner', + 'progress-planner-adminui', + 'prplpkg', + 'progress_planner', + 'progress-planner/v1', + 'PRPL (kit shadow)', + 'Dashboard (kit shadow)', + new Branding( + 'Progress Planner (kit shadow)', + 'Dashboard (kit shadow)', + $branding_svg, + 'Progress Planner — kit shadow page' + ), + $package_root . '/assets', + \plugin_dir_url( PROGRESS_PLANNER_FILE ) . 'vendor/progressplanner/wp-admin-ui/assets', + // For Phase 2 we deliberately point host_assets_path at a non- + // existent directory so the kit resolves every asset against the + // package only. Progress-planner's own assets/ dir has files + // with legacy `progress-planner/` prefixed Dependencies headers + // that the kit's enqueuer can't resolve — that gets fixed in + // Phase 3 when we refactor those headers. + PROGRESS_PLANNER_DIR . '/_no_host_assets_phase2', + \constant( 'PROGRESS_PLANNER_URL' ) . '/_no_host_assets_phase2', + $package_root . '/views' + ); + + $ui = AdminUI::boot( $config ); + $ui->add_widget( new Admin_UI_Pkg_Shadow_Widget( $ui ) ); + } +} + +/** + * Trivial smoke-test widget. Confirms the kit's Widget base + AssetEnqueuer + * work when loaded inside progress-planner's PHP + asset environment. + */ +final class Admin_UI_Pkg_Shadow_Widget extends Widget { + + protected $id = 'kit-shadow'; + + public function enqueue_styles(): void { + // No widget-specific CSS — kit defaults only. + } + + public function enqueue_scripts(): void { + $this->ui->enqueuer()->enqueue_script( 'web-components/prpl-gauge' ); + } + + public function render(): void { + $this->enqueue_scripts(); + ?> +
+
+

Kit shadow smoke test

+

If you can see a gauge below, progressplanner/wp-admin-ui autoloaded and rendered inside progress-planner's PHP environment.

+
+ +
+
+
+ =7.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6", + "szepeviktor/phpstan-wordpress": "^1.3" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "ProgressPlanner\\AdminUI\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "ProgressPlanner\\AdminUI\\Tests\\": "tests/phpunit/" + } + }, + "license": [ + "GPL-3.0-or-later" + ], + "description": "Reusable admin UI kit for WordPress plugins — CSS tokens, web components, page framework.", + "homepage": "https://github.com/progressplanner/wp-admin-ui", + "support": { + "source": "https://github.com/ProgressPlanner/wp-admin-ui/tree/main", + "issues": "https://github.com/ProgressPlanner/wp-admin-ui/issues" + }, + "time": "2026-04-15T19:10:21+00:00" + } + ], "packages-dev": [ { "name": "antecedent/patchwork", @@ -9392,7 +9437,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": { + "progressplanner/wp-admin-ui": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": {}, @@ -9400,5 +9447,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/progress-planner.php b/progress-planner.php index 5fa2c26a6..392c4ca5e 100644 --- a/progress-planner.php +++ b/progress-planner.php @@ -28,6 +28,26 @@ require_once PROGRESS_PLANNER_DIR . '/autoload.php'; +// Load the Composer autoloader if present — currently only used by the +// optional wp-admin-ui package integration (Phase 2 dual-load). +if ( \file_exists( PROGRESS_PLANNER_DIR . '/vendor/autoload.php' ) ) { + require_once PROGRESS_PLANNER_DIR . '/vendor/autoload.php'; +} + +// Dual-load the extracted admin-ui kit behind a feature flag so we can +// validate it boots cleanly in the host context without changing any +// existing behavior. Off by default — define the constant to true in +// wp-config.php to see the shadow page at +// /wp-admin/admin.php?page=progress-planner-adminui. +if ( + \defined( 'PROGRESS_PLANNER_USE_ADMIN_UI_PKG' ) + && \constant( 'PROGRESS_PLANNER_USE_ADMIN_UI_PKG' ) + && \class_exists( \ProgressPlanner\AdminUI\AdminUI::class ) +) { + require_once PROGRESS_PLANNER_DIR . '/classes/admin/class-admin-ui-pkg-shadow.php'; + \add_action( 'plugins_loaded', [ \Progress_Planner\Admin\Admin_UI_Pkg_Shadow::class, 'boot' ], 20 ); +} + if ( ! \function_exists( 'progress_planner' ) ) { /** * Get the progress planner instance. From 74f90b9b52203530a89ab52729c8816d4a1dc2e9 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Wed, 15 Apr 2026 21:24:29 +0200 Subject: [PATCH 02/15] Phase 3A: delegate Admin\Enqueue to kit's AssetEnqueuer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Progress_Planner\Admin\Enqueue is now a thin adapter around the kit: - enqueue_style() fully delegates to the kit enqueuer. - enqueue_script() delegates non-vendor handles to the kit; vendor scripts (particles-confetti, driver) keep their special handling because their fixed path/version doesn't fit the kit's generic {prefix}/{handle} resolution. - Vendor handles are registered with the kit as externals with a lazy resolver callback, so when another script declares them as deps, the vendor file is wp_enqueue_script()'d on demand. - Progress-planner-specific methods (localize_script() with its per-handle switch, get_localized_strings(), get_badge_urls(), maybe_empty_session_storage()) stay here — they're domain logic, not asset mechanics. - get_file_details() removed; the kit owns resolution now. The kit's asset_prefix is 'progress-planner' so existing legacy headers (Dependencies: progress-planner/foo) round-trip cleanly without needing file edits. Requires the kit's register_external() accepting a resolver callback (progressplanner/wp-admin-ui@dev-main). --- classes/admin/class-enqueue.php | 193 ++++++++++++++++---------------- composer.lock | 8 +- 2 files changed, 101 insertions(+), 100 deletions(-) diff --git a/classes/admin/class-enqueue.php b/classes/admin/class-enqueue.php index 20c045c4e..96504bc4d 100644 --- a/classes/admin/class-enqueue.php +++ b/classes/admin/class-enqueue.php @@ -2,29 +2,38 @@ /** * Assets class. * + * Since Phase 3A of the wp-admin-ui extraction, this class is a thin + * adapter around {@see \ProgressPlanner\AdminUI\Assets\AssetEnqueuer}. + * Script/style resolution, dependency parsing, and mtime versioning + * all live in the kit now. This class only retains: + * + * - Progress Planner-specific vendor-script handling (particles-confetti, + * driver.js) that doesn't fit the kit's generic {prefix}/{handle} model. + * - `localize_script()` with its domain-specific switch (badges, tasks, + * celebrate), which will keep living here for the foreseeable future. + * - `maybe_empty_session_storage()` — a Progress Planner admin-head shim. + * * @package Progress_Planner */ namespace Progress_Planner\Admin; use Progress_Planner\Badges\Monthly; +use ProgressPlanner\AdminUI\Assets\AssetEnqueuer; /** * Enqueue class. */ class Enqueue { - /** - * Have the scripts been registered? - * - * @var boolean - */ - protected static $scripts_registered = false; - /** * Vendor scripts. * - * @var array + * Internal file-path stem → { WP handle, hard-coded version }. These + * don't live at /assets/js/{handle}.js so they can't go through the + * kit's generic resolver. + * + * @var array */ const VENDOR_SCRIPTS = [ 'vendor/tsparticles.confetti.bundle.min' => [ @@ -38,14 +47,18 @@ class Enqueue { ]; /** - * Enqueued assets. + * Shared kit enqueuer — single instance per Enqueue instance. * - * @var array + * @var AssetEnqueuer|null */ - protected $enqueued_assets = [ - 'js' => [], - 'css' => [], - ]; + private $kit_enqueuer = null; + + /** + * Vendor handles already enqueued (guards against double enqueue). + * + * @var array + */ + private $registered_vendors = []; /** * Init. @@ -56,6 +69,46 @@ public function init() { \add_action( 'admin_head', [ $this, 'maybe_empty_session_storage' ], 1 ); } + /** + * Lazily build the kit enqueuer. + * + * handle_prefix is set to 'progress-planner' so existing `Dependencies: + * progress-planner/foo` file headers across the plugin round-trip + * cleanly through the kit's resolver. + * + * version_strategy delegates to Progress_Planner\Base::get_file_version() + * so debug-mode behavior is preserved. + */ + private function get_kit_enqueuer(): AssetEnqueuer { + if ( null === $this->kit_enqueuer ) { + $this->kit_enqueuer = new AssetEnqueuer( + 'progress-planner', + [ + [ + 'path' => \constant( 'PROGRESS_PLANNER_DIR' ) . '/assets', + 'url' => \constant( 'PROGRESS_PLANNER_URL' ) . '/assets', + ], + ], + static function ( $file_path ) { + return \progress_planner()->get_file_version( $file_path ); + } + ); + + // Register vendor bundles as external handles with a resolver. + // When the kit encounters them as a dep of another script, the + // resolver fires and we wp_enqueue_script() the vendor file — + // keeping loads lazy. + foreach ( self::VENDOR_SCRIPTS as $file_handle => $vendor_meta ) { + $resolver = function () use ( $file_handle, $vendor_meta ): void { + $this->enqueue_vendor_script( $file_handle, $vendor_meta ); + }; + $this->kit_enqueuer->register_external( $vendor_meta['handle'], $resolver ); + } + } + + return $this->kit_enqueuer; + } + /** * Enqueue script. * @@ -70,27 +123,26 @@ public function init() { * @return void */ public function enqueue_script( $handle, $localize_data = [] ) { - $file_details = $this->get_file_details( 'js', $handle ); - if ( empty( $file_details ) ) { - return; + $lookup = $handle; + if ( \str_starts_with( $lookup, 'progress-planner/' ) ) { + $lookup = \substr( $lookup, \strlen( 'progress-planner/' ) ); } - $this->enqueued_assets['js'][] = $file_details['handle']; - $final_dependencies = []; - - // Enqueue the script dependencies. - foreach ( $file_details['dependencies'] as $dependency ) { - if ( ! \in_array( $dependency, $this->enqueued_assets['js'], true ) ) { - $this->enqueue_script( $dependency ); - $final_dependencies[] = $dependency; + // Vendor scripts use a fixed path/version — enqueue them directly. + foreach ( self::VENDOR_SCRIPTS as $file_handle => $vendor_meta ) { + if ( $vendor_meta['handle'] === $lookup || $file_handle === $lookup ) { + $this->enqueue_vendor_script( $file_handle, $vendor_meta ); + $this->localize_script( $vendor_meta['handle'], $localize_data ); + return; } } - // Enqueue the stylesheet. - \wp_enqueue_script( $file_details['handle'], $file_details['file_url'], $final_dependencies, $file_details['version'], true ); + // Everything else goes through the kit. + $this->get_kit_enqueuer()->enqueue_script( $handle ); - // Localize the script. - $this->localize_script( $file_details['handle'], $localize_data ); + // Pass the kit-prefixed handle to localize_script() so existing + // switch-cases match (they already use 'progress-planner/xxx'). + $this->localize_script( 'progress-planner/' . $lookup, $localize_data ); } /** @@ -101,79 +153,28 @@ public function enqueue_script( $handle, $localize_data = [] ) { * @return void */ public function enqueue_style( $handle ) { - $file_details = $this->get_file_details( 'css', $handle ); - if ( empty( $file_details ) ) { - return; - } - - $this->enqueued_assets['css'][] = $file_details['handle']; - $final_dependencies = []; - - // Enqueue the script dependencies. - foreach ( $file_details['dependencies'] as $dependency ) { - if ( ! \in_array( $dependency, $this->enqueued_assets['css'], true ) ) { - $this->enqueue_style( $dependency ); - } - } - // Enqueue the stylesheet. - \wp_enqueue_style( $file_details['handle'], $file_details['file_url'], $final_dependencies, $file_details['version'] ); + $this->get_kit_enqueuer()->enqueue_style( $handle ); } /** - * Get file details. - * - * @param string $context The context of the file ( `css` or `js` ). - * @param string $handle The handle of the file. + * Enqueue a vendor script (plain wp_enqueue_script, no dep resolution). * - * @return array + * @param string $file_handle The internal file stem, e.g. 'vendor/driver.js.iife'. + * @param array{handle:string,version:string} $vendor_meta Handle + version. */ - public function get_file_details( $context, $handle ) { - if ( \str_starts_with( $handle, 'progress-planner/' ) ) { - $handle = \str_replace( 'progress-planner/', '', $handle ); - } - - if ( 'js' === $context ) { - foreach ( self::VENDOR_SCRIPTS as $vendor_script_handle => $vendor_script ) { - if ( $vendor_script['handle'] === $handle ) { - $handle = $vendor_script_handle; - break; - } - } - } - // The file path. - $file_path = \constant( 'PROGRESS_PLANNER_DIR' ) . "/assets/{$context}/{$handle}.{$context}"; - - // If the file does not exist, bail early. - if ( ! \file_exists( $file_path ) ) { - return []; + private function enqueue_vendor_script( $file_handle, $vendor_meta ): void { + if ( isset( $this->registered_vendors[ $vendor_meta['handle'] ] ) ) { + return; } - - // The file URL. - $file_url = \constant( 'PROGRESS_PLANNER_URL' ) . "/assets/{$context}/{$handle}.{$context}"; - - // The handle. - $handle = 'js' === $context && isset( self::VENDOR_SCRIPTS[ $handle ] ) - ? self::VENDOR_SCRIPTS[ $handle ]['handle'] - : 'progress-planner/' . $handle; - - // The version. - $version = 'js' === $context && isset( self::VENDOR_SCRIPTS[ $handle ] ) - ? self::VENDOR_SCRIPTS[ $handle ]['version'] - : \progress_planner()->get_file_version( $file_path ); - - // The dependencies. - $headers = \get_file_data( $file_path, [ 'dependencies' => 'Dependencies' ] ); - $dependencies = isset( $headers['dependencies'] ) - ? \array_filter( \array_map( 'trim', \explode( ',', $headers['dependencies'] ) ) ) - : []; - - return [ - 'file_path' => $file_path, - 'file_url' => $file_url, - 'handle' => $handle, - 'version' => $version, - 'dependencies' => $dependencies, - ]; + $this->registered_vendors[ $vendor_meta['handle'] ] = true; + + \wp_enqueue_script( + $vendor_meta['handle'], + \constant( 'PROGRESS_PLANNER_URL' ) . "/assets/js/{$file_handle}.js", + [], + $vendor_meta['version'], + true + ); } /** diff --git a/composer.lock b/composer.lock index a9a02b85b..6a0c9cf94 100644 --- a/composer.lock +++ b/composer.lock @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "git@github.com:ProgressPlanner/wp-admin-ui.git", - "reference": "0cddd0b71320d4f9e6e52ca3c68ab380227329dd" + "reference": "2715e570061731bc74956409a5edd3eb8da0a44e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ProgressPlanner/wp-admin-ui/zipball/0cddd0b71320d4f9e6e52ca3c68ab380227329dd", - "reference": "0cddd0b71320d4f9e6e52ca3c68ab380227329dd", + "url": "https://api.github.com/repos/ProgressPlanner/wp-admin-ui/zipball/2715e570061731bc74956409a5edd3eb8da0a44e", + "reference": "2715e570061731bc74956409a5edd3eb8da0a44e", "shasum": "" }, "require": { @@ -48,7 +48,7 @@ "source": "https://github.com/ProgressPlanner/wp-admin-ui/tree/main", "issues": "https://github.com/ProgressPlanner/wp-admin-ui/issues" }, - "time": "2026-04-15T19:10:21+00:00" + "time": "2026-04-15T19:23:34+00:00" } ], "packages-dev": [ From 884f00ba7396bdae9b13d93881e784d1fb94b506 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Wed, 15 Apr 2026 21:25:29 +0200 Subject: [PATCH 03/15] Phase 3B: bridge Progress_Planner\UI\Branding to the kit's Branding VO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Progress_Planner\UI\Branding::to_kit_branding() which returns a \ProgressPlanner\AdminUI\Branding populated with currently-resolved SaaS values (menu name, submenu name, admin menu icon SVG, logo HTML, and custom CSS). The SaaS-driven Progress_Planner\UI\Branding class keeps all its existing behavior — remote fetches, hostname sniffing, widget filtering — and this method is purely an output bridge. Kit code (which performs no remote calls) consumes the VO. The shadow page now uses to_kit_branding() instead of constructing its own Branding, exercising the bridge end-to-end. --- classes/admin/class-admin-ui-pkg-shadow.php | 12 ++++------ classes/ui/class-branding.php | 26 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/classes/admin/class-admin-ui-pkg-shadow.php b/classes/admin/class-admin-ui-pkg-shadow.php index 6d5f3a8b6..b505ce550 100644 --- a/classes/admin/class-admin-ui-pkg-shadow.php +++ b/classes/admin/class-admin-ui-pkg-shadow.php @@ -12,7 +12,6 @@ namespace Progress_Planner\Admin; use ProgressPlanner\AdminUI\AdminUI; -use ProgressPlanner\AdminUI\Branding; use ProgressPlanner\AdminUI\Config; use ProgressPlanner\AdminUI\Widgets\Widget; @@ -25,7 +24,9 @@ final class Admin_UI_Pkg_Shadow { public static function boot(): void { $package_root = PROGRESS_PLANNER_DIR . '/vendor/progressplanner/wp-admin-ui'; - $branding_svg = \progress_planner()->get_ui__branding()->get_admin_menu_icon( true ); + // Hand off the SaaS-resolved branding VO from progress-planner to + // the kit — verifies the Phase 3B branding bridge works end-to-end. + $branding = \progress_planner()->get_ui__branding()->to_kit_branding(); $config = new Config( 'progress-planner', @@ -35,12 +36,7 @@ public static function boot(): void { 'progress-planner/v1', 'PRPL (kit shadow)', 'Dashboard (kit shadow)', - new Branding( - 'Progress Planner (kit shadow)', - 'Dashboard (kit shadow)', - $branding_svg, - 'Progress Planner — kit shadow page' - ), + $branding, $package_root . '/assets', \plugin_dir_url( PROGRESS_PLANNER_FILE ) . 'vendor/progressplanner/wp-admin-ui/assets', // For Phase 2 we deliberately point host_assets_path at a non- diff --git a/classes/ui/class-branding.php b/classes/ui/class-branding.php index aea8576d0..7014bd961 100644 --- a/classes/ui/class-branding.php +++ b/classes/ui/class-branding.php @@ -324,4 +324,30 @@ public function get_seo_plugin_recommendation_slug(): string { ? 'wordpress-seo' : $this->get_api_data()['seo_plugin_recommendation_slug']; } + + /** + * Export a kit-compatible {@see \ProgressPlanner\AdminUI\Branding} VO + * populated with the currently-resolved SaaS values. + * + * Kit code (the wp-admin-ui package) takes a plain Branding VO and does + * no remote calls of its own — this method is the handoff point. + */ + public function to_kit_branding(): \ProgressPlanner\AdminUI\Branding { + \ob_start(); + $this->the_logo(); + $logo_html = (string) \ob_get_clean(); + + return new \ProgressPlanner\AdminUI\Branding( + $this->get_admin_menu_name(), + $this->get_admin_submenu_name(), + $this->get_admin_menu_icon( true ), + $logo_html, + '#dd324f', // --prpl-color-button-primary (kit default; SaaS doesn't currently override). + null, + '#ffffff', + '#38296d', // --prpl-color-headings. + '#faa310', // --prpl-background-monthly. + $this->get_custom_css() + ); + } } From c85cf766b58efacd96f3412002938a3a55ccbb73 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Wed, 15 Apr 2026 21:28:31 +0200 Subject: [PATCH 04/15] Bump wp-admin-ui to pick up AssetEnqueuer cycle-safe deps fix --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index 6a0c9cf94..4bb8c661f 100644 --- a/composer.lock +++ b/composer.lock @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "git@github.com:ProgressPlanner/wp-admin-ui.git", - "reference": "2715e570061731bc74956409a5edd3eb8da0a44e" + "reference": "b293a882e0aaab71c8c3c0b20ae21575e8fdae73" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ProgressPlanner/wp-admin-ui/zipball/2715e570061731bc74956409a5edd3eb8da0a44e", - "reference": "2715e570061731bc74956409a5edd3eb8da0a44e", + "url": "https://api.github.com/repos/ProgressPlanner/wp-admin-ui/zipball/b293a882e0aaab71c8c3c0b20ae21575e8fdae73", + "reference": "b293a882e0aaab71c8c3c0b20ae21575e8fdae73", "shasum": "" }, "require": { @@ -48,7 +48,7 @@ "source": "https://github.com/ProgressPlanner/wp-admin-ui/tree/main", "issues": "https://github.com/ProgressPlanner/wp-admin-ui/issues" }, - "time": "2026-04-15T19:23:34+00:00" + "time": "2026-04-15T19:27:46+00:00" } ], "packages-dev": [ From d1242fa75085f19ea3c78b4e4593a55fbe004254 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Wed, 15 Apr 2026 21:33:02 +0200 Subject: [PATCH 05/15] Phase 4: Widget base extends kit's Widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Progress_Planner\Admin\Widgets\Widget now extends ProgressPlanner\AdminUI\Widgets\Widget. Rendering mechanics (wrapper div, CSS class naming, view loading) live in the kit; progress-planner- specific additions stay here: - range/frequency getters (bound to the existing - ${ this.options.dataArgs[ key ].label } - `; - - /** - * Get the filters label. - * - * @return {string} The filters label. - */ - getCheckboxesFiltersLabel = () => - '' === this.options.filtersLabel - ? '' - : `${ this.options.filtersLabel }`; - - /** - * Generate the SVG for the chart. - * - * @return {string} The SVG HTML for the chart. - */ - getSvgHTML = () => - ` - ${ this.getXAxisLineHTML() } - ${ this.getYAxisLineHTML() } - ${ this.getXAxisLabelsAndRulersHTML() } - ${ this.getYAxisLabelsAndRulersHTML() } - ${ this.getPolyLinesHTML() } - `; - - /** - * Get the poly lines for the SVG. - * - * @return {string} The poly lines. - */ - getPolyLinesHTML = () => - Object.keys( this.data ) - .map( ( key ) => this.getPolylineHTML( key ) ) - .join( '' ); - - /** - * Get a single polyline. - * - * @param {string} key - The key of the data. - * - * @return {string} The polyline. - */ - getPolylineHTML = ( key ) => { - if ( ! this.options.showCharts.includes( key ) ) { - return ''; - } - - const polylinePoints = []; - let xCoordinate = this.options.axisOffset * 3; - this.data[ key ].forEach( ( item ) => { - polylinePoints.push( [ - xCoordinate, - this.calcYCoordinate( item.score ), - ] ); - xCoordinate += this.getXDistanceBetweenPoints(); - } ); - - return ``; - }; - - /** - * Get the number of steps for the Y axis. - * - * Choose between 3, 4, or 5 steps. - * The result should be the number that when used as a divisor, - * produces integer values for the Y labels - or at least as close as possible. - * - * @return {number} The number of steps. - */ - getYLabelsStepsDivider = () => { - const maxValuePadded = this.getMaxValuePadded(); - - const stepsRemainders = { - 4: maxValuePadded % 4, - 5: maxValuePadded % 5, - 3: maxValuePadded % 3, - }; - // Get the smallest remainder. - const smallestRemainder = Math.min( - ...Object.values( stepsRemainders ) - ); - - // Get the key of the smallest remainder. - const smallestRemainderKey = Object.keys( stepsRemainders ).find( - ( key ) => stepsRemainders[ key ] === smallestRemainder - ); - return smallestRemainderKey; - }; - - /** - * Get the Y labels. - * - * @return {number[]} The Y labels. - */ - getYLabels = () => { - const maxValuePadded = this.getMaxValuePadded(); - const yLabelsStepsDivider = this.getYLabelsStepsDivider(); - const yLabelsStep = maxValuePadded / yLabelsStepsDivider; - const yLabels = []; - if ( 100 === maxValuePadded || 15 > maxValuePadded ) { - for ( let i = 0; i <= yLabelsStepsDivider; i++ ) { - yLabels.push( parseInt( yLabelsStep * i ) ); - } - } else { - // Round the values to the nearest 10. - for ( let i = 0; i <= yLabelsStepsDivider; i++ ) { - yLabels.push( - Math.min( - maxValuePadded, - Math.round( yLabelsStep * i, -1 ) - ) - ); - } - } - - return yLabels; - }; - - /** - * Get the X axis line. - * - * @return {string} The X axis line. - */ - getXAxisLineHTML = () => - ``; - - /** - * Get the Y axis line. - * - * @return {string} The Y axis line. - */ - getYAxisLineHTML = () => - ``; - - /** - * Get the X axis labels and rulers. - * - * @return {string} The X axis labels and rulers. - */ - getXAxisLabelsAndRulersHTML = () => { - let html = ''; - let labelXCoordinate = 0; - const dataLength = - this.data[ Object.keys( this.data )[ 0 ] ].length; - const labelsXDivider = Math.round( dataLength / 6 ); - let i = 0; - Object.keys( this.data ).forEach( ( key ) => { - this.data[ key ].forEach( ( item ) => { - labelXCoordinate = - this.getXDistanceBetweenPoints() * i + - this.options.axisOffset * 2; - ++i; - - // Only allow up to 6 labels to prevent overlapping. - // If there are more than 6 labels, find the alternate labels. - if ( - 6 < dataLength && - 1 !== i && - ( i - 1 ) % labelsXDivider !== 0 - ) { - return; - } - - html += `${ item.label }`; - - // Draw the ruler. - if ( 1 !== i ) { - html += ``; - } - } ); - } ); - - return html; - }; - - /** - * Get the distance between the points in the X axis. - * - * @return {number} The distance between the points in the X axis. - */ - getXDistanceBetweenPoints = () => - Math.round( - ( this.options.height * this.options.aspectRatio - - 3 * this.options.axisOffset ) / - ( this.data[ Object.keys( this.data )[ 0 ] ].length - 1 ) - ); - - /** - * Get the Y axis labels and rulers. - * - * @return {string} The Y axis labels and rulers. - */ - getYAxisLabelsAndRulersHTML = () => { - // Y-axis labels and rulers. - let yLabelCoordinate = 0; - let iYLabel = 0; - let html = ''; - this.getYLabels().forEach( ( yLabel ) => { - yLabelCoordinate = this.calcYCoordinate( yLabel ); - - html += `${ yLabel }`; - - // Draw the ruler. - if ( 0 !== iYLabel ) { - html += ``; - } - - ++iYLabel; - } ); - - return html; - }; - - /** - * Get the max value from the data. - * - * @return {number} The max value. - */ - getMaxValue = () => - Object.keys( this.data ).reduce( ( max, key ) => { - if ( this.options.showCharts.includes( key ) ) { - return Math.max( - max, - this.data[ key ].reduce( - ( _max, item ) => Math.max( _max, item.score ), - 0 - ) - ); - } - return max; - }, 0 ); - - /** - * Get the max value padded. - * - * @return {number} The max value padded. - */ - getMaxValuePadded = () => { - const max = this.getMaxValue(); - const maxValue = 100 > max && 70 < max ? 100 : max; - return Math.max( - 100 === maxValue ? 100 : parseInt( maxValue * 1.1 ), - 1 - ); - }; - - /** - * Add event listeners to the checkboxes. - */ - addCheckboxesEventListeners = () => - // Add event listeners to the checkboxes. - this.querySelectorAll( 'input[type="checkbox"]' ).forEach( - ( checkbox ) => { - checkbox.addEventListener( 'change', ( e ) => { - const el = e.target; - const parentEl = el.parentElement; - const checkboxColorEl = parentEl.querySelector( - '.prpl-chart-line-checkbox-color' - ); - if ( el.checked ) { - this.options.showCharts.push( - el.getAttribute( 'name' ) - ); - checkboxColorEl.style.backgroundColor = - parentEl.dataset.color; - } else { - this.options.showCharts = - this.options.showCharts.filter( - ( chart ) => - chart !== el.getAttribute( 'name' ) - ); - checkboxColorEl.style.backgroundColor = - 'transparent'; - } - - // Update the chart. - this.querySelector( '.svg-container' ).innerHTML = - this.getSvgHTML(); - } ); - } - ); - - /** - * Calculate the Y coordinate for a given value. - * - * @param {number} value - The value. - * - * @return {number} The Y coordinate. - */ - calcYCoordinate = ( value ) => { - const maxValuePadded = this.getMaxValuePadded(); - const multiplier = - ( this.options.height - this.options.axisOffset * 2 ) / - this.options.height; - const yCoordinate = - ( maxValuePadded - value * multiplier ) * - ( this.options.height / maxValuePadded ) - - this.options.axisOffset; - return yCoordinate - this.options.strokeWidth / 2; - }; - } -); diff --git a/assets/js/web-components/prpl-gauge-progress-controller.js b/assets/js/web-components/prpl-gauge-progress-controller.js deleted file mode 100644 index 9c5a59449..000000000 --- a/assets/js/web-components/prpl-gauge-progress-controller.js +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Web Component: prpl-gauge-progress-controller - * - * A web component that controls the progress of a gauge and its progress bars. - * - * Dependencies: progress-planner/web-components/prpl-gauge, progress-planner/web-components/prpl-badge-progress-bar - */ - -// eslint-disable-next-line no-unused-vars -class PrplGaugeProgressController { - constructor( gauge, ...progressBars ) { - this.gauge = gauge; - this.progressBars = progressBars; // array, can be empty. - - this.addListeners(); - } - - /** - * Add listeners to the gauge and progress bars. - */ - addListeners() { - // Monthy badge gauge updated. - // Update the gauge and bars side elements (elements there are not part of the component), for example: the points counter. - document.addEventListener( 'prpl-gauge-update', ( event ) => { - if ( - 'prpl-gauge-ravi' !== event.detail.element.getAttribute( 'id' ) - ) { - return; - } - - // Update the monthly badge gauge points counter. - this.updateGaugePointsCounter( event.detail.value ); - - // Mark badge as (not)completed, in the a Monthly badges widgets (both on page and in the popover), if we reached the max points. - this.maybeUpdateBadgeCompletedStatus( - event.detail.badgeId, - event.detail.value, - event.detail.max - ); - - // Update remaining points side elements for all progress bars, for example: "20 more points to go" text. - this.updateBarsRemainingPoints(); - } ); - - // Progress bar for the previous month badge updated. - // Updates the gauge and bars side elements (elements there are not part of the component), for example: "20 more points to go" text. - document.addEventListener( - 'prlp-badge-progress-bar-update', - ( event ) => { - // Update the remaining points. - const remainingPointsEl = event.detail.element; - - const remainingPointsElWrapper = remainingPointsEl.closest( - '.prpl-previous-month-badge-progress-bar-wrapper' - ); - - if ( remainingPointsElWrapper ) { - // Update the progress bars points number. - const badgePointsNumberEl = - remainingPointsElWrapper.querySelector( - '.prpl-widget-previous-ravi-points-number' - ); - - if ( badgePointsNumberEl ) { - badgePointsNumberEl.textContent = - event.detail.points + 'pt'; - } - - // Mark badge as (not)completed, in the a Monthly badges widgets (both on page and in the popover), if we reached the max points. - this.maybeUpdateBadgeCompletedStatus( - event.detail.badgeId, - event.detail.points, - event.detail.maxPoints - ); - - // Update remaining points text for all progress bars, for example: "20 more points to go". - this.updateBarsRemainingPoints(); - - // Maybe remove the completed progress bar. - this.maybeRemoveCompletedBarFromDom( - event.detail.badgeId, - event.detail.points, - event.detail.maxPoints - ); - } - } - ); - } - - /** - * Update the monthly badge gauge points counter. - * - * @param {number} value The value. - */ - updateGaugePointsCounter( value ) { - // Update the points counter. - const pointsCounter = document.getElementById( - 'prpl-widget-content-ravi-points-number' - ); - - if ( pointsCounter ) { - pointsCounter.textContent = parseInt( value ) + 'pt'; - } - } - - /** - * Update the remaining points display for all progress bars based on current gauge and progress bar values. - * For example: "11 more points to go" text. - */ - updateBarsRemainingPoints() { - const currentGaugeValue = this.gaugeValue; - - for ( let i = 0; i < this.progressBars.length; i++ ) { - const bar = this.progressBars[ i ]; - - // Calculate remaining points for this bar - let remainingPoints = 0; - if ( currentGaugeValue < this.gaugeMax ) { - // Calculate the threshold for this progress bar - // First bar starts at gauge max (10), second at gauge max + first bar max (20), etc. - const barThreshold = - this.gaugeMax + ( i + 1 ) * this._barMaxPoints( bar ); - - // Gauge is not full yet, show points needed to reach this bar - remainingPoints = barThreshold - currentGaugeValue; - } else { - // Gauge is full, show remaining points in this specific bar - for ( let j = 0; j <= i; j++ ) { - remainingPoints += - this._barMaxPoints( this.progressBars[ j ] ) - - this._barValue( this.progressBars[ j ] ); - } - } - - // Ensure remaining points is never negative - remainingPoints = Math.max( 0, remainingPoints ); - - // Update the display - const parentWrapper = bar.closest( - '.prpl-previous-month-badge-progress-bar-wrapper' - ); - - if ( parentWrapper ) { - const numberEl = parentWrapper.querySelector( '.number' ); - if ( numberEl ) { - numberEl.textContent = remainingPoints; - } - } - } - } - - /** - * Maybe update the badge completed status. - * This sets the complete attribute on the badge element and toggles visibility of the ! icon. - * - * @param {string} badgeId The badge id. - * @param {number} value The value. - * @param {number} max The max. - */ - maybeUpdateBadgeCompletedStatus( badgeId, value, max ) { - if ( ! badgeId ) { - return; - } - - // See if the badge is completed or not, this is used as attribute value. - const badgeCompleted = - parseInt( value ) >= parseInt( max ) ? 'true' : 'false'; - - // If the badge was completed we need to select all badges with the same badge-id which are marked as not completed. - // And vice versa. - const badgeSelector = `prpl-badge[complete="${ - 'true' === badgeCompleted ? 'false' : 'true' - }"][badge-id="${ badgeId }"]`; - - // We have multiple badges, one in widget and the other in the popover. - document - .querySelectorAll( - `.prpl-badge-row-wrapper .prpl-badge ${ badgeSelector }` - ) - ?.forEach( ( badge ) => { - badge.setAttribute( 'complete', badgeCompleted ); - } ); - } - - /** - * Maybe remove the completed bar. - * - * @param {string} badgeId The badge id. - * @param {number} value The value. - * @param {number} max The max. - */ - maybeRemoveCompletedBarFromDom( badgeId, value, max ) { - if ( ! badgeId ) { - return; - } - - // If the previous month badge is completed, remove the progress bar. - if ( value >= parseInt( max ) ) { - // Remove the previous month badge progress bar. - document - .querySelector( - `.prpl-previous-month-badge-progress-bar-wrapper[data-badge-id="${ badgeId }"]` - ) - ?.remove(); - - // If there are no more progress bars, remove the previous month badge progress bar wrapper. - if ( - ! document.querySelector( - '.prpl-previous-month-badge-progress-bar-wrapper' - ) - ) { - document - .querySelector( - '.prpl-previous-month-badge-progress-bars-wrapper' - ) - ?.remove(); - } - } - } - - /** - * Get the gauge value. - */ - get gaugeValue() { - return parseInt( this.gauge.value ) || 0; - } - - /** - * Set the gauge value. - * - * @param {number} v The value. - */ - set gaugeValue( v ) { - this.gauge.value = v; - } - - /** - * Get the gauge max. - */ - get gaugeMax() { - return parseInt( this.gauge.max ) || 10; - } - - /** - * Get the bar value. - * - * @param {number} bar The bar. - * @return {number} The value. - */ - _barValue( bar ) { - return parseInt( bar.points ) || 0; - } - - /** - * Set the bar value. - * - * @param {number} bar The bar. - * @param {number} v The value. - */ - _setBarValue( bar, v ) { - bar.points = v; - } - - /** - * Get the bar max points. - * - * @param {number} bar The bar. - * @return {number} The max points. - */ - _barMaxPoints( bar ) { - return parseInt( bar.maxPoints ) || 10; - } - - /** - * Increase the gauge and progress bars. - * This method is used to sync the gauge and progress bars. - * - * @param {number} amount The amount. - */ - increase( amount = 1 ) { - let remaining = amount; - - // Fill gauge first - const gaugeSpace = this.gaugeMax - this.gaugeValue; - const toGauge = Math.min( remaining, gaugeSpace ); - this.gaugeValue += toGauge; - remaining -= toGauge; - - // Fill progress bars in order - for ( const bar of this.progressBars ) { - if ( remaining <= 0 ) { - break; - } - const barSpace = parseInt( bar.maxPoints ) - this._barValue( bar ); - - const toBar = Math.min( remaining, barSpace ); - - this._setBarValue( bar, this._barValue( bar ) + toBar ); - remaining -= toBar; - } - } - - /** - * Decrease the gauge and progress bars. - * This method is used to sync the gauge and progress bars. - * - * @param {number} amount The amount. - */ - decrease( amount = 1 ) { - // Convert negative amount to positive. - if ( 0 > amount ) { - amount = -amount; - } - - let remaining = amount; - - // Decrease progress bars first, in reverse order - for ( let i = this.progressBars.length - 1; i >= 0; i-- ) { - if ( remaining <= 0 ) { - break; - } - const bar = this.progressBars[ i ]; - const barVal = this._barValue( bar ); - const fromBar = Math.min( remaining, barVal ); - this._setBarValue( bar, barVal - fromBar ); - remaining -= fromBar; - } - - // Decrease gauge last - if ( remaining > 0 ) { - this.gaugeValue -= remaining; - } - } -} diff --git a/assets/js/web-components/prpl-tooltip.js b/assets/js/web-components/prpl-tooltip.js deleted file mode 100644 index c0b3e8fdd..000000000 --- a/assets/js/web-components/prpl-tooltip.js +++ /dev/null @@ -1,138 +0,0 @@ -/* global customElements, HTMLElement, prplL10n */ -/* - * Tooltip - * - * A web component to display a tooltip. - * - * Dependencies: progress-planner/l10n - */ -/* eslint-disable camelcase */ - -/** - * Register the custom web component. - */ -customElements.define( - 'prpl-tooltip', - class extends HTMLElement { - // constructor() { - // // Get parent class properties - // super(); - // } - - /** - * Connected callback. - */ - connectedCallback() { - // Find the elements inside - const contentSlot = this.querySelector( 'slot[name="content"]' ); - const openSlot = this.querySelector( 'slot[name="open"]' ); - const openIconSlot = this.querySelector( 'slot[name="open-icon"]' ); - const closeSlot = this.querySelector( 'slot[name="close"]' ); - const closeIconSlot = this.querySelector( - 'slot[name="close-icon"]' - ); - - // Create tooltip container - const tooltipContent = document.createElement( 'div' ); - tooltipContent.className = 'prpl-tooltip'; - tooltipContent.setAttribute( 'data-tooltip-content', '' ); - tooltipContent.setAttribute( 'role', 'tooltip' ); - tooltipContent.setAttribute( 'aria-hidden', 'true' ); - // Generate a unique ID for the tooltip. - const tooltipId = - 'prpl-tooltip-' + Math.random().toString( 36 ).substr( 2, 9 ); - tooltipContent.setAttribute( 'id', tooltipId ); - - // Move content inside the tooltip container - while ( contentSlot?.childNodes.length ) { - tooltipContent.appendChild( contentSlot.childNodes[ 0 ] ); - } - contentSlot?.remove(); // Remove slot element - - // Find the open button (or create a default one) - let openButton = openSlot?.firstElementChild; - if ( ! openButton ) { - openButton = document.createElement( 'button' ); - openButton.type = 'button'; - openButton.className = 'prpl-info-icon'; - openButton.innerHTML = - openIconSlot?.innerHTML || - ` - - - ${ prplL10n( 'info' ) } - - `; - } - - // Add data attribute to the open button. - openButton.setAttribute( 'data-tooltip-action', 'open-tooltip' ); - // Connect button to tooltip for screen readers. - openButton.setAttribute( 'aria-describedby', tooltipId ); - - openSlot?.remove(); // Remove slot element - openIconSlot?.remove(); // Remove slot element - - // Find the close button (or create a default one) - let closeButton = closeSlot?.firstElementChild; - if ( ! closeButton ) { - closeButton = document.createElement( 'button' ); - closeButton.type = 'button'; - closeButton.className = 'prpl-tooltip-close'; - closeButton.setAttribute( - 'data-tooltip-action', - 'close-tooltip' - ); - closeButton.innerHTML = - closeIconSlot?.innerHTML || - ` - - ${ prplL10n( 'close' ) } - `; - } - closeSlot?.remove(); // Remove slot element - closeIconSlot?.remove(); // Remove slot element - - // Append elements to the component - this.appendChild( openButton ); - tooltipContent.appendChild( closeButton ); - this.appendChild( tooltipContent ); - - // Add event listeners - this.addListeners(); - } - - /** - * Add listeners to the item. - */ - addListeners = () => { - const thisObj = this, - openTooltipButton = thisObj.querySelector( - 'button[data-tooltip-action="open-tooltip"]' - ), - closeTooltipButton = thisObj.querySelector( - 'button[data-tooltip-action="close-tooltip"]' - ); - - // Open the tooltip. - openTooltipButton?.addEventListener( 'click', () => { - const tooltip = thisObj.querySelector( - '[data-tooltip-content]' - ); - tooltip.setAttribute( 'data-tooltip-visible', 'true' ); - tooltip.removeAttribute( 'aria-hidden' ); - } ); - - // Close the tooltip. - closeTooltipButton?.addEventListener( 'click', () => { - const tooltip = thisObj.querySelector( - '[data-tooltip-content]' - ); - tooltip.removeAttribute( 'data-tooltip-visible' ); - tooltip.setAttribute( 'aria-hidden', 'true' ); - } ); - }; - } -); - -/* eslint-enable camelcase */ diff --git a/classes/admin/class-enqueue.php b/classes/admin/class-enqueue.php index eb1bbe197..121a32cf3 100644 --- a/classes/admin/class-enqueue.php +++ b/classes/admin/class-enqueue.php @@ -84,10 +84,20 @@ private function get_kit_enqueuer(): AssetEnqueuer { $this->kit_enqueuer = new AssetEnqueuer( 'progress-planner', [ + // Host first so progress-planner's own files still win + // (they're supersets of the kit's layout/tokens). [ 'path' => \constant( 'PROGRESS_PLANNER_DIR' ) . '/assets', 'url' => \constant( 'PROGRESS_PLANNER_URL' ) . '/assets', ], + // Package fallback — lets the kit supply any generic + // file progress-planner doesn't ship locally, and makes + // Phase 3C/6 deletions possible (files fall through to + // the kit's copies). + [ + 'path' => \constant( 'PROGRESS_PLANNER_DIR' ) . '/vendor/progressplanner/wp-admin-ui/assets', + 'url' => \plugin_dir_url( \constant( 'PROGRESS_PLANNER_FILE' ) ) . 'vendor/progressplanner/wp-admin-ui/assets', + ], ], static function ( $file_path ) { return \progress_planner()->get_file_version( $file_path ); From 7c8f948a1ef6c5890ce4b2c436ddf523318a67c6 Mon Sep 17 00:00:00 2001 From: Joost de Valk Date: Wed, 15 Apr 2026 22:30:27 +0200 Subject: [PATCH 10/15] =?UTF-8?q?Remove=20PROGRESS=5FPLANNER=5FUSE=5FADMIN?= =?UTF-8?q?=5FUI=5FPKG=20flag=20=E2=80=94=20kit=20always=20renders=20the?= =?UTF-8?q?=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flag was the Phase-5 safety net for switching progress-planner to the extracted wp-admin-ui kit's page rendering. With the kit verified working in both flag states across all the Phase 1-6 commits, drop the toggle. Changes: - progress-planner.php: unconditional composer autoload + unconditional Admin_UI_Kit_Integration boot (was flag-gated). - Admin_UI_Instance: remove kit_renders_page(); boot kit with register_page=true always. - Admin_UI_Kit_Integration::boot(): remove the flag early-return. - Admin\Page: strip the legacy page ownership — no more add_page(), render_page(), enqueue_scripts(), enqueue_styles(), or get_notification_counter(). enqueue_assets() always runs the widget-only path (kit handles tokens + layout + masonry). Kept: get_widgets()/get_widget(), maybe_enqueue_focus_el_script(), remove_admin_notices(), clear_activity_scores_cache(), admin_footer(). - Deleted views/legacy-admin-page.php + views/legacy-admin-page-header.php (only the flag-off path used them). --- classes/admin/class-admin-ui-instance.php | 29 +-- .../admin/class-admin-ui-kit-integration.php | 24 +-- classes/admin/class-page.php | 204 ++---------------- progress-planner.php | 26 +-- views/legacy-admin-page-header.php | 79 ------- views/legacy-admin-page.php | 52 ----- 6 files changed, 41 insertions(+), 373 deletions(-) delete mode 100644 views/legacy-admin-page-header.php delete mode 100644 views/legacy-admin-page.php diff --git a/classes/admin/class-admin-ui-instance.php b/classes/admin/class-admin-ui-instance.php index bf535e652..9e997d3b4 100644 --- a/classes/admin/class-admin-ui-instance.php +++ b/classes/admin/class-admin-ui-instance.php @@ -3,20 +3,11 @@ * Singleton accessor for the kit's AdminUI instance used inside * progress-planner. * - * Two boot modes, keyed off {@see PROGRESS_PLANNER_USE_ADMIN_UI_PKG}: - * - * - Off (default, Phase 4-style): register_page = false. The kit is used - * only as a back-end for the Widget base class and AssetEnqueuer. - * progress-planner's own Admin\Page class renders the admin page and - * enqueues assets. No behavior change. - * - * - On (Phase 5): register_page = true. The kit's PageRegistrar takes - * over the top-level admin menu and renders views/admin-page.php from - * the kit (with progress-planner views/host overrides where present). - * Progress-planner's Admin\Page::add_page() short-circuits, and - * progress-planner-specific UI (notification counter, welcome/privacy - * gate, tour, subscribe, JS templates) attaches via the kit's - * {asset_prefix}_admin_ui_* filters/actions. + * The kit owns the top-level admin page: its PageRegistrar registers the + * menu and renders views/admin-page.php. Progress-planner-specific UI + * (notification counter, welcome/privacy gate, tour, subscribe popover, + * JS templates) attaches via {@see Admin_UI_Kit_Integration} on the + * kit's {asset_prefix}_admin_ui_* filters/actions. * * @package Progress_Planner */ @@ -36,14 +27,6 @@ final class Admin_UI_Instance { */ private static $instance = null; - /** - * Whether the kit should render the admin page itself. - */ - public static function kit_renders_page(): bool { - return \defined( 'PROGRESS_PLANNER_USE_ADMIN_UI_PKG' ) - && (bool) \constant( 'PROGRESS_PLANNER_USE_ADMIN_UI_PKG' ); - } - public static function get(): AdminUI { if ( null !== self::$instance ) { return self::$instance; @@ -71,7 +54,7 @@ public static function get(): AdminUI { 'manage_options', \progress_planner()->get_ui__branding()->get_admin_submenu_position(), true, // show_range_filter — matches progress-planner's existing header. - self::kit_renders_page() + true // register_page — the kit owns the admin menu. ); self::$instance = AdminUI::boot( $config ); diff --git a/classes/admin/class-admin-ui-kit-integration.php b/classes/admin/class-admin-ui-kit-integration.php index 0ad793f99..129805162 100644 --- a/classes/admin/class-admin-ui-kit-integration.php +++ b/classes/admin/class-admin-ui-kit-integration.php @@ -1,17 +1,13 @@ get_ui__branding()->get_admin_submenu_name(), - \progress_planner()->get_ui__branding()->get_admin_menu_name() . $this->get_notification_counter(), - 'manage_options', - $page_identifier, - '__return_empty_string', - \progress_planner()->get_ui__branding()->get_admin_menu_icon(), - \progress_planner()->get_ui__branding()->get_admin_submenu_position() - ); - - \add_submenu_page( - $page_identifier, - \progress_planner()->get_ui__branding()->get_admin_submenu_name(), - \progress_planner()->get_ui__branding()->get_admin_submenu_name() . $this->get_notification_counter(), - 'manage_options', - $page_identifier, - [ $this, 'render_page' ], - ); - - // Wipe notification bits from hooks. - // phpcs:ignore WordPress.WP.GlobalVariablesOverride -- This is a deliberate action. - $admin_page_hooks[ $page_identifier ] = $page_identifier; - } - - /** - * Returns the notification count in HTML format. - * - * @return string The notification count in HTML format. - */ - protected function get_notification_counter() { - $notification_count = \wp_count_posts( 'prpl_recommendations' )->pending; - - if ( 0 === $notification_count ) { - return ''; - } - - /* translators: Hidden accessibility text; %s: number of notifications. */ - $notifications = \sprintf( \_n( '%s pending celebration', '%s pending celebrations', $notification_count, 'progress-planner' ), \number_format_i18n( $notification_count ) ); - - return \sprintf( '%2$s', $notification_count, $notifications ); - } - - /** - * Render the admin page. + * Enqueue widget-specific scripts/styles on the kit's admin page. * - * @return void - */ - public function render_page() { - \progress_planner()->the_view( 'legacy-admin-page.php' ); - } - - /** - * Enqueue scripts and styles. + * The kit handles tokens + layout + masonry; this method only + * enqueues the bits specific to progress-planner's widgets. * * @param string $hook The current admin page. * @@ -170,27 +108,6 @@ public function enqueue_assets( $hook ) { return; } - // When the kit renders the page it already enqueues its base tokens + - // layout CSS + grid-masonry via PageRegistrar, and the legacy - // privacy-gate + welcome/onboard branch is served by the kit's hooks - // (see Admin_UI_Kit_Integration). We still enqueue widget-specific - // scripts/styles needed for widgets rendered inside the kit view. - if ( Admin_UI_Instance::kit_renders_page() ) { - $this->enqueue_widget_assets(); - return; - } - - $this->enqueue_scripts(); - $this->enqueue_styles(); - } - - /** - * Enqueue just the scripts/styles needed for widgets when the kit - * renders the page. The kit handles tokens + layout + masonry itself. - * - * @return void - */ - private function enqueue_widget_assets(): void { $default_localization_data = [ 'name' => 'progressPlanner', 'data' => [ @@ -226,47 +143,6 @@ private function enqueue_widget_assets(): void { } } - /** - * Enqueue scripts. - * - * @return void - */ - public function enqueue_scripts() { - $current_screen = \get_current_screen(); - if ( ! $current_screen ) { - return; - } - - if ( 'toplevel_page_progress-planner' === $current_screen->id ) { - $default_localization_data = [ - 'name' => 'progressPlanner', - 'data' => [ - 'onboardNonceURL' => \progress_planner()->get_utils__onboard()->get_remote_url( 'get-nonce' ), - 'onboardAPIUrl' => \progress_planner()->get_utils__onboard()->get_remote_url( 'onboard' ), - 'ajaxUrl' => \admin_url( 'admin-ajax.php' ), - 'nonce' => \wp_create_nonce( 'progress_planner' ), - ], - ]; - - if ( true === \progress_planner()->is_privacy_policy_accepted() ) { - \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-gauge' ); - \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-badge-progress-bar' ); - \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-chart-bar' ); - \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-chart-line' ); - \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-big-counter' ); - \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-tooltip' ); - \progress_planner()->get_admin__enqueue()->enqueue_script( 'header-filters', $default_localization_data ); - \progress_planner()->get_admin__enqueue()->enqueue_script( 'settings', $default_localization_data ); - \progress_planner()->get_admin__enqueue()->enqueue_script( 'grid-masonry' ); - \progress_planner()->get_admin__enqueue()->enqueue_script( 'upgrade-tasks' ); - } else { - \progress_planner()->get_admin__enqueue()->enqueue_script( 'onboard', $default_localization_data ); - } - - \progress_planner()->get_admin__enqueue()->enqueue_script( 'external-link-accessibility-helper' ); - } - } - /** * Enqueue the focus element script. * @@ -332,41 +208,6 @@ public function maybe_enqueue_focus_el_script( $hook ) { \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/focus-element' ); } - /** - * Enqueue styles. - * - * @return void - */ - public function enqueue_styles() { - $current_screen = \get_current_screen(); - if ( ! $current_screen ) { - return; - } - - \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/variables-color' ); - \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/admin' ); - if ( ! static::$branding_inline_styles_added ) { - \wp_add_inline_style( 'progress-planner/admin', \progress_planner()->get_ui__branding()->get_custom_css() ); - static::$branding_inline_styles_added = true; - } - \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/web-components/prpl-tooltip' ); - \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/web-components/prpl-install-plugin' ); - - if ( 'toplevel_page_progress-planner' === $current_screen->id ) { - // Enqueue ugprading (onboarding) tasks styles, these are needed both when privacy policy is accepted and when it is not. - \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/upgrade-tasks' ); - } - - $prpl_privacy_policy_accepted = \progress_planner()->is_privacy_policy_accepted(); - if ( ! $prpl_privacy_policy_accepted ) { - // Enqueue welcome styles. - \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/welcome' ); - - // Enqueue onboarding styles. - \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/onboard' ); - } - } - /** * Remove all admin notices when the user is on the Progress Planner page. * @@ -377,13 +218,7 @@ public function remove_admin_notices() { if ( ! $current_screen ) { return; } - if ( ! \in_array( - $current_screen->id, - [ - 'toplevel_page_progress-planner', - ], - true - ) ) { + if ( 'toplevel_page_progress-planner' !== $current_screen->id ) { return; } @@ -408,7 +243,8 @@ public function clear_activity_scores_cache( $activity ) { } /** - * Add a custom admin footer. + * Add a custom admin footer — positions the notification bubble on + * the top-level menu item. * * @return void */ diff --git a/progress-planner.php b/progress-planner.php index b4539efba..dfe4592b2 100644 --- a/progress-planner.php +++ b/progress-planner.php @@ -27,26 +27,14 @@ \define( 'PROGRESS_PLANNER_URL', \untrailingslashit( \plugin_dir_url( __FILE__ ) ) ); require_once PROGRESS_PLANNER_DIR . '/autoload.php'; +require_once PROGRESS_PLANNER_DIR . '/vendor/autoload.php'; -// Load the Composer autoloader if present — currently only used by the -// optional wp-admin-ui package integration (Phase 2 dual-load). -if ( \file_exists( PROGRESS_PLANNER_DIR . '/vendor/autoload.php' ) ) { - require_once PROGRESS_PLANNER_DIR . '/vendor/autoload.php'; -} - -// Dual-load the extracted admin-ui kit behind a feature flag so we can -// validate it boots cleanly in the host context without changing any -// existing behavior. Off by default — define the constant to true in -// wp-config.php to see the shadow page at -// /wp-admin/admin.php?page=progress-planner-adminui. -if ( - \defined( 'PROGRESS_PLANNER_USE_ADMIN_UI_PKG' ) - && \constant( 'PROGRESS_PLANNER_USE_ADMIN_UI_PKG' ) - && \class_exists( \ProgressPlanner\AdminUI\AdminUI::class ) -) { - require_once PROGRESS_PLANNER_DIR . '/classes/admin/class-admin-ui-kit-integration.php'; - \add_action( 'plugins_loaded', [ \Progress_Planner\Admin\Admin_UI_Kit_Integration::class, 'boot' ], 20 ); -} +// The wp-admin-ui kit owns the admin page. Admin_UI_Kit_Integration wires +// progress-planner's widgets + UI bits (notification counter, welcome +// gate, tour button, subscribe popover, JS templates) into the kit's +// action/filter hooks. +require_once PROGRESS_PLANNER_DIR . '/classes/admin/class-admin-ui-kit-integration.php'; +\add_action( 'plugins_loaded', [ \Progress_Planner\Admin\Admin_UI_Kit_Integration::class, 'boot' ], 20 ); if ( ! \function_exists( 'progress_planner' ) ) { /** diff --git a/views/legacy-admin-page-header.php b/views/legacy-admin-page-header.php deleted file mode 100644 index 1a6f75343..000000000 --- a/views/legacy-admin-page-header.php +++ /dev/null @@ -1,79 +0,0 @@ - -
- - -
- - get_license_key() ) { - \progress_planner()->get_ui__popover()->the_popover( 'subscribe-form' )->render_button( - '', - \progress_planner()->get_asset( 'images/register_icon.svg' ) . '' . \esc_html__( 'Subscribe', 'progress-planner' ) . '' - ); - // Render the subscribe form popover. - \progress_planner()->get_ui__popover()->the_popover( 'subscribe-form' )->render(); - } - ?> -
- - - - -
-
-
diff --git a/views/legacy-admin-page.php b/views/legacy-admin-page.php deleted file mode 100644 index 8c5ccde5c..000000000 --- a/views/legacy-admin-page.php +++ /dev/null @@ -1,52 +0,0 @@ -is_privacy_policy_accepted(); -?> - -
- - -

- the_view( 'legacy-admin-page-header.php' ); ?> -
- get_admin__page()->get_widgets() as $prpl_admin_widget ) : ?> - render(); ?> - -
- - - - the_view( 'welcome.php' ); ?> - -
- - - - -the_view( 'js-templates/suggested-task.html' ); ?> From 1db545a671e1ec379c86d7b90f8f0fc4dca89114 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 16 Apr 2026 09:47:24 +0200 Subject: [PATCH 11/15] Fix minor style issues: add PHPDoc, remove double blank line, fix PHPCS warnings Co-Authored-By: Claude Opus 4.6 (1M context) --- classes/admin/class-admin-ui-instance.php | 7 +++++++ classes/admin/class-enqueue.php | 6 +++--- classes/ui/class-branding.php | 1 - 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/classes/admin/class-admin-ui-instance.php b/classes/admin/class-admin-ui-instance.php index 9e997d3b4..ffee2ae08 100644 --- a/classes/admin/class-admin-ui-instance.php +++ b/classes/admin/class-admin-ui-instance.php @@ -23,10 +23,17 @@ final class Admin_UI_Instance { /** + * The single kit AdminUI instance, or null before first access. + * * @var AdminUI|null */ private static $instance = null; + /** + * Get (or create) the kit AdminUI instance for progress-planner. + * + * @return AdminUI + */ public static function get(): AdminUI { if ( null !== self::$instance ) { return self::$instance; diff --git a/classes/admin/class-enqueue.php b/classes/admin/class-enqueue.php index 121a32cf3..333ef4a22 100644 --- a/classes/admin/class-enqueue.php +++ b/classes/admin/class-enqueue.php @@ -72,7 +72,7 @@ public function init() { /** * Lazily build the kit enqueuer. * - * handle_prefix is set to 'progress-planner' so existing `Dependencies: + * Handle_prefix is set to 'progress-planner' so existing `Dependencies: * progress-planner/foo` file headers across the plugin round-trip * cleanly through the kit's resolver. * @@ -178,8 +178,8 @@ public function enqueue_style( $handle ) { /** * Enqueue a vendor script (plain wp_enqueue_script, no dep resolution). * - * @param string $file_handle The internal file stem, e.g. 'vendor/driver.js.iife'. - * @param array{handle:string,version:string} $vendor_meta Handle + version. + * @param string $file_handle The internal file stem, e.g. 'vendor/driver.js.iife'. + * @param array{handle:string,version:string} $vendor_meta Handle + version. */ private function enqueue_vendor_script( $file_handle, $vendor_meta ): void { if ( isset( $this->registered_vendors[ $vendor_meta['handle'] ] ) ) { diff --git a/classes/ui/class-branding.php b/classes/ui/class-branding.php index 837db29fb..1a96ddd48 100644 --- a/classes/ui/class-branding.php +++ b/classes/ui/class-branding.php @@ -337,7 +337,6 @@ public function to_kit_branding(): \ProgressPlanner\AdminUI\Branding { $this->the_logo(); $logo_html = (string) \ob_get_clean(); - // Pass null for every color override — progress-planner's own // variables-color.css owns these tokens and the kit's inline CSS // overrides would clobber them (e.g. --prpl-background-monthly From 5afec8d6feb582748228d3fa0775a557d795c32b Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 16 Apr 2026 09:48:49 +0200 Subject: [PATCH 12/15] Fix dashboard widgets calling removed Page::enqueue_styles() Replace with direct enqueue of the base stylesheets (variables-color + admin) that the dashboard widgets need on the WP Dashboard screen. Co-Authored-By: Claude Opus 4.6 (1M context) --- classes/admin/class-dashboard-widget-score.php | 5 +++-- classes/admin/class-dashboard-widget-todo.php | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/classes/admin/class-dashboard-widget-score.php b/classes/admin/class-dashboard-widget-score.php index d6b6509cb..515e2bd6f 100644 --- a/classes/admin/class-dashboard-widget-score.php +++ b/classes/admin/class-dashboard-widget-score.php @@ -36,8 +36,9 @@ protected function get_title() { * @return void */ public function render_widget() { - // Enqueue stylesheets. - \progress_planner()->get_admin__page()->enqueue_styles(); + // Enqueue base stylesheets (variables + admin layout). + \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/variables-color' ); + \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/admin' ); \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-gauge' ); $suggested_tasks_widget = \progress_planner()->get_admin__page()->get_widget( 'suggested-tasks' ); diff --git a/classes/admin/class-dashboard-widget-todo.php b/classes/admin/class-dashboard-widget-todo.php index 8fd9629ec..ba01d7991 100644 --- a/classes/admin/class-dashboard-widget-todo.php +++ b/classes/admin/class-dashboard-widget-todo.php @@ -37,7 +37,9 @@ protected function get_title() { * @return void */ public function render_widget() { - \progress_planner()->get_admin__page()->enqueue_styles(); + // Enqueue base stylesheets (variables + admin layout). + \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/variables-color' ); + \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/admin' ); $todo_widget = \progress_planner()->get_admin__page()->get_widget( 'todo' ); if ( $todo_widget ) { From 84eb43076d1342edc809130c7574df30a0b08528 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 16 Apr 2026 09:51:51 +0200 Subject: [PATCH 13/15] Add branding inline styles to dashboard widgets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The removed Page::enqueue_styles() also attached branding CSS overrides via wp_add_inline_style — restore that for WP Dashboard widgets. Co-Authored-By: Claude Opus 4.6 (1M context) --- classes/admin/class-dashboard-widget-score.php | 3 ++- classes/admin/class-dashboard-widget-todo.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/classes/admin/class-dashboard-widget-score.php b/classes/admin/class-dashboard-widget-score.php index 515e2bd6f..c6474150e 100644 --- a/classes/admin/class-dashboard-widget-score.php +++ b/classes/admin/class-dashboard-widget-score.php @@ -36,9 +36,10 @@ protected function get_title() { * @return void */ public function render_widget() { - // Enqueue base stylesheets (variables + admin layout). + // Enqueue base stylesheets (variables + admin layout + branding overrides). \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/variables-color' ); \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/admin' ); + \wp_add_inline_style( 'progress-planner/admin', \progress_planner()->get_ui__branding()->get_custom_css() ); \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-gauge' ); $suggested_tasks_widget = \progress_planner()->get_admin__page()->get_widget( 'suggested-tasks' ); diff --git a/classes/admin/class-dashboard-widget-todo.php b/classes/admin/class-dashboard-widget-todo.php index ba01d7991..c86fafc25 100644 --- a/classes/admin/class-dashboard-widget-todo.php +++ b/classes/admin/class-dashboard-widget-todo.php @@ -37,9 +37,10 @@ protected function get_title() { * @return void */ public function render_widget() { - // Enqueue base stylesheets (variables + admin layout). + // Enqueue base stylesheets (variables + admin layout + branding overrides). \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/variables-color' ); \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/admin' ); + \wp_add_inline_style( 'progress-planner/admin', \progress_planner()->get_ui__branding()->get_custom_css() ); $todo_widget = \progress_planner()->get_admin__page()->get_widget( 'todo' ); if ( $todo_widget ) { From 26164389109ac680cd4af6a94ad378b773dac186 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 16 Apr 2026 09:54:03 +0200 Subject: [PATCH 14/15] Add tooltip CSS to score dashboard widget The suggested-task template uses tooltip-actions classes, so the tooltip stylesheet is needed on the WP Dashboard. Co-Authored-By: Claude Opus 4.6 (1M context) --- classes/admin/class-dashboard-widget-score.php | 1 + 1 file changed, 1 insertion(+) diff --git a/classes/admin/class-dashboard-widget-score.php b/classes/admin/class-dashboard-widget-score.php index c6474150e..1b84db91c 100644 --- a/classes/admin/class-dashboard-widget-score.php +++ b/classes/admin/class-dashboard-widget-score.php @@ -40,6 +40,7 @@ public function render_widget() { \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/variables-color' ); \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/admin' ); \wp_add_inline_style( 'progress-planner/admin', \progress_planner()->get_ui__branding()->get_custom_css() ); + \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/web-components/prpl-tooltip' ); \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-gauge' ); $suggested_tasks_widget = \progress_planner()->get_admin__page()->get_widget( 'suggested-tasks' ); From 44c90aadc2d78ee31faae195191169c903f36108 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Thu, 16 Apr 2026 10:00:57 +0200 Subject: [PATCH 15/15] Update Playground to use kit's header_before action The old progress_planner_admin_page_header_before action no longer fires since the kit owns the admin page header. Switch to the kit's equivalent progress-planner_admin_ui_header_before hook. Co-Authored-By: Claude Opus 4.6 (1M context) --- classes/utils/class-playground.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/utils/class-playground.php b/classes/utils/class-playground.php index 988043db8..01ef02eff 100644 --- a/classes/utils/class-playground.php +++ b/classes/utils/class-playground.php @@ -47,7 +47,7 @@ public function register_hooks() { ); \update_option( 'progress_planner_demo_data_generated', true ); } - \add_action( 'progress_planner_admin_page_header_before', [ $this, 'show_header_notice' ] ); + \add_action( 'progress-planner_admin_ui_header_before', [ $this, 'show_header_notice' ] ); \add_action( 'wp_ajax_progress_planner_hide_onboarding', [ $this, 'hide_onboarding' ] ); \add_action( 'wp_ajax_progress_planner_show_onboarding', [ $this, 'show_onboarding' ] );