diff --git a/assets/css/variables-color.css b/assets/css/variables-color.css
deleted file mode 100644
index 7ea93ae8d2..0000000000
--- a/assets/css/variables-color.css
+++ /dev/null
@@ -1,100 +0,0 @@
-:root {
-
- /* Background */
- --prpl-background: #f6f7f9;
- --prpl-background-banner: #f9b23c;
-
- /* Paper */
- --prpl-background-paper: #fff;
- --prpl-color-border: #d1d5db;
- --prpl-color-divider: var(--prpl-color-border);
- --prpl-color-shadow-paper: #000;
-
- /* Graph */
- --prpl-color-gauge-remain: #e1e3e7;
- --prpl-graph-color-1: #f43f5e;
- --prpl-graph-color-2: #faa310;
- --prpl-graph-color-3: #14b8a6;
- --prpl-graph-color-4: #534786;
-
- /* Table */
- --prpl-background-table: var(--prpl-background);
- --prpl-background-top-task: #fff9f0;
- --prpl-color-border-top-task: var(--prpl-color-monthly);
- --prpl-color-border-next-top-task: var(--prpl-graph-color-4);
- --prpl-color-selection-controls-inactive: #9ca3af;
- --prpl-color-selection-controls: var(--prpl-color-selection-controls-inactive); /* TBD */
- --prpl-color-ui-icon: #6b7280;
- --prpl-color-ui-icon-hover: var(--prpl-color-link);
- --prpl-color-ui-icon-hover-fill: var(--prpl-background-content-badge);
- --prpl-color-ui-icon-hover-delete: var(--prpl-color-alert-error);
- --prpl-background-point: var(--prpl-background-banner);
- --prpl-text-point: var(--prpl-color-headings);
- --prpl-background-point-inactive: var(--prpl-color-border);
- --prpl-text-point-inactive: var(--prpl-color-headings);
-
- /* Text */
- --prpl-color-text: #4b5563;
- --prpl-color-text-hover: var(--prpl-color-link);
- --prpl-color-headings: #38296d;
- --prpl-color-subheadings: var(--prpl-color-headings);
- --prpl-color-link: #1e40af;
- --prpl-color-link-hover: var(--prpl-color-text);
- --prpl-color-link-visited: var(--prpl-graph-color-4);
-
- /* Topics */
- --prpl-color-monthly: #faa310;
- --prpl-color-monthly-2: #faa310;
- --prpl-color-streak: var(--prpl-color-monthly);
- --prpl-color-content-badge: var(--prpl-color-monthly);
- --prpl-background-monthly: #fff9f0;
- --prpl-background-content: #f6f5fb;
- --prpl-background-activity: #f2faf9;
- --prpl-background-streak: #fff6f7;
- --prpl-background-content-badge: #effbfe;
-
- /* Alert success */
- --prpl-color-alert-success: #16a34a;
- --prpl-color-alert-success-text: #14532d;
- --prpl-background-alert-success: #f0fdf4;
-
- /* Alert error */
- --prpl-color-alert-error: #e73136;
- --prpl-color-alert-error-text: #7f1d1d;
- --prpl-background-alert-error: #fdeded;
-
- /* Alert warning */
- --prpl-color-alert-warning: #eab308;
- --prpl-color-alert-warning-text: #713f12;
- --prpl-background-alert-warning: #fefce8;
-
- /* Alert info */
- --prpl-color-alert-info: #2563eb;
- --prpl-color-alert-info-text: #1e3a8a;
- --prpl-background-alert-info: #eff6ff;
-
- /* Button */
- --prpl-color-button-primary: #dd324f;
- --prpl-color-button-primary-hover: #cf2441;
- --prpl-color-button-primary-shadow: var(--prpl-color-shadow-paper);
- --prpl-color-button-primary-border: none;
- --prpl-color-button-primary-text: var(--prpl-background-paper);
-
- /* Settings page */
- --prpl-color-setting-pages-icon: var(--prpl-color-monthly);
- --prpl-color-setting-posts-icon: var(--prpl-graph-color-4);
- --prpl-color-setting-login-icon: var(--prpl-graph-color-3);
- --prpl-background-setting-pages: var(--prpl-background-monthly);
- --prpl-background-setting-posts: var(--prpl-background-content);
- --prpl-background-setting-login: var(--prpl-background-activity);
- --prpl-color-border-settings: var(--prpl-color-border);
-
- /* Input field dropdown */
- --prpl-color-field-border: var(--prpl-color-border);
- --prpl-color-text-placeholder: var(--prpl-color-ui-icon);
- --prpl-color-text-dropdown: var(--prpl-color-text);
- --prpl-color-field-shadow: var(--prpl-color-shadow-paper);
-
- /* Badge */
- --prpl-color-icon-missed-badge: var(--prpl-color-alert-error);
-}
diff --git a/assets/css/web-components/prpl-install-plugin.css b/assets/css/web-components/prpl-install-plugin.css
deleted file mode 100644
index e237ce0aec..0000000000
--- a/assets/css/web-components/prpl-install-plugin.css
+++ /dev/null
@@ -1,54 +0,0 @@
-prpl-install-plugin {
-
- button {
- display: flex !important;
- align-items: center;
- justify-content: center;
- gap: 0.5rem;
- }
-
- .prpl-install-button-loader {
- display: none;
- width: 1rem;
- height: 1rem;
- border: 3px solid var(--prpl-color-link);
- border-bottom-color: transparent;
- border-radius: 50%;
- box-sizing: border-box;
- animation: install-button-rotation 1s linear infinite;
- }
-
- button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-
- .prpl-install-button-loader {
- display: block;
- }
- }
-
- .prpl-button-link {
- text-decoration: underline;
- color: var(--prpl-color-link);
- background: none;
- border: none;
- padding: 0;
- margin: 0;
- font-size: inherit;
- font-weight: inherit;
- line-height: inherit;
- text-align: inherit;
- cursor: pointer;
- }
-}
-
-@keyframes install-button-rotation {
-
- 0% {
- transform: rotate(0deg);
- }
-
- 100% {
- transform: rotate(360deg);
- }
-}
diff --git a/assets/css/web-components/prpl-tooltip.css b/assets/css/web-components/prpl-tooltip.css
deleted file mode 100644
index 40dee191e5..0000000000
--- a/assets/css/web-components/prpl-tooltip.css
+++ /dev/null
@@ -1,97 +0,0 @@
-.tooltip-actions {
- justify-content: flex-start;
- gap: 0.5em;
- display: flex;
- flex-wrap: wrap;
- position: relative;
-
- .icon {
- width: 1.25rem;
- height: 1.25rem;
- display: inline-block;
- vertical-align: bottom; /* align with the text */
- }
-}
-
-.prpl-tooltip {
- position: absolute;
- bottom: 0;
- left: 100%;
- transform: translate(-100%, calc(100% + 10px));
-
- padding: 0.75rem 1.5rem 0.75rem 0.75rem;
- width: 150px;
- background: var(--prpl-background-activity);
- border-radius: var(--prpl-border-radius);
- z-index: 2; /* above the gauges */
- visibility: hidden; /* hidden by default */
-
- font-size: 1rem;
- font-weight: 400;
- color: var(--prpl-color-text);
-
- &[data-tooltip-visible="true"] {
- visibility: visible;
- z-index: 10;
- }
-
- .close,
- .prpl-tooltip-close {
- position: absolute;
- top: 0;
- right: 0;
- padding: 0.1rem;
- line-height: 0;
- margin: 0;
- background: none;
- border: none;
- cursor: pointer;
- }
-
- /* Arrow */
- &::after {
- content: "";
- position: absolute;
- top: 0;
- right: 0;
- transform: translate(-10px, -10px) rotate(90deg);
-
- width: 0;
- height: 0;
- border-style: solid;
- border-width: 7.5px 10px 7.5px 0;
- border-color: transparent var(--prpl-background-activity) transparent transparent;
- }
-}
-
-prpl-tooltip {
- display: inline-flex;
- align-items: center;
- position: relative;
-
- .prpl-tooltip {
-
- p {
- margin-bottom: 0;
- }
-
- p:first-child {
- margin-top: 0;
- }
- }
-}
-
-.prpl-overlay {
- display: none;
-}
-
-body:has([data-tooltip-visible="true"]) .prpl-overlay {
- display: block !important;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 9;
- background-color: rgba(0, 0, 0, 0.5);
-}
diff --git a/assets/js/web-components/prpl-big-counter.js b/assets/js/web-components/prpl-big-counter.js
deleted file mode 100644
index 6bbbbe341a..0000000000
--- a/assets/js/web-components/prpl-big-counter.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/* global customElements, HTMLElement */
-
-/**
- * Register the custom web component.
- */
-customElements.define(
- 'prpl-big-counter',
- class extends HTMLElement {
- constructor( number, content, backgroundColor ) {
- // Get parent class properties
- super();
- number = number || this.getAttribute( 'number' );
- content = content || this.getAttribute( 'content' );
- backgroundColor =
- backgroundColor || this.getAttribute( 'background-color' );
- backgroundColor =
- backgroundColor || 'var(--prpl-background-content)';
-
- const el = this;
-
- this.innerHTML = `
-
-
-
${ number }
-
- ${ content }
-
-
- `;
-
- const resizeFont = () => {
- const element = el.querySelector( '.resize' );
- if ( ! element ) {
- return;
- }
-
- element.style.fontSize = '100%';
-
- let size = 100;
- while (
- element.clientWidth >
- el.querySelector( '.container-width' ).clientWidth
- ) {
- if ( size < 80 ) {
- element.style.fontSize = size + '%';
- element.style.width = '100%';
- break;
- }
- size -= 1;
- element.style.fontSize = size + '%';
- }
- };
-
- resizeFont();
- window.addEventListener( 'resize', resizeFont );
- }
- }
-);
diff --git a/assets/js/web-components/prpl-chart-bar.js b/assets/js/web-components/prpl-chart-bar.js
deleted file mode 100644
index 10eecd436e..0000000000
--- a/assets/js/web-components/prpl-chart-bar.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/* global customElements, HTMLElement */
-
-/**
- * Register the custom web component.
- */
-customElements.define(
- 'prpl-chart-bar',
- class extends HTMLElement {
- constructor( data = [] ) {
- // Get parent class properties
- super();
-
- if ( data.length === 0 ) {
- data = JSON.parse( this.getAttribute( 'data' ) );
- }
-
- const labelsDivider =
- data.length > 6 ? parseInt( data.length / 6 ) : 1;
-
- let html = ``;
- let i = 0;
- data.forEach( ( item ) => {
- html += `
`;
- html += `
`;
- // Only display up to 6 labels.
- html += `
`;
- html +=
- i % labelsDivider === 0
- ? `${ item.label } `
- : `${ item.label } `;
- html += ` `;
- html += `
`;
- i++;
- } );
- html += `
`;
-
- this.innerHTML = html;
-
- // Tweak labels styling to fix positioning when there are many items.
- if ( this.querySelectorAll( '.label.invisible' ).length > 0 ) {
- this.querySelectorAll( '.label-container' ).forEach(
- ( label ) => {
- const labelWidth =
- label.querySelector( '.label' ).offsetWidth;
- const labelElement = label.querySelector( '.label' );
- labelElement.style.display = 'block';
- labelElement.style.width = 0;
- const marginLeft =
- ( label.offsetWidth - labelWidth ) / 2;
- if ( labelElement.classList.contains( 'visible' ) ) {
- labelElement.style.marginLeft = `${ marginLeft }px`;
- }
- }
- );
- // Reduce the gap between items to avoid overflows.
- this.querySelector( '.chart-bar' ).style.gap =
- parseInt(
- Math.max(
- this.querySelector( '.label' ).offsetWidth / 4,
- 1
- )
- ) + 'px';
- }
- }
- }
-);
diff --git a/assets/js/web-components/prpl-chart-line.js b/assets/js/web-components/prpl-chart-line.js
deleted file mode 100644
index 5319da0c29..0000000000
--- a/assets/js/web-components/prpl-chart-line.js
+++ /dev/null
@@ -1,439 +0,0 @@
-/* global customElements, HTMLElement */
-
-/**
- * Register the custom web component.
- */
-customElements.define(
- 'prpl-chart-line',
- class extends HTMLElement {
- constructor( data = [], options = {} ) {
- // Get parent class properties
- super();
-
- // Set the object data.
- this.data =
- 0 === data.length
- ? JSON.parse( this.getAttribute( 'data' ) )
- : data;
-
- // Set the object options.
- this.options =
- 0 === Object.keys( options ).length
- ? JSON.parse( this.getAttribute( 'data-options' ) )
- : options;
-
- // Add default values to the options object.
- this.options = {
- aspectRatio: 2,
- height: 300,
- axisOffset: 16,
- strokeWidth: 4,
- dataArgs: {},
- showCharts: Object.keys( this.options.dataArgs ),
- axisColor: 'var(--prpl-color-border)',
- rulersColor: 'var(--prpl-color-border)',
- filtersLabel: '',
- ...this.options,
- };
-
- // Add the HTML to the element.
- this.innerHTML = `${ this.getCheckboxesHTML() }${ this.getSvgHTML() }
`;
-
- // Add event listeners for the checkboxes.
- this.addCheckboxesEventListeners();
- }
-
- /**
- * Get the checkboxes.
- *
- * @return {string} The checkboxes.
- */
- getCheckboxesHTML = () =>
- 1 >= Object.keys( this.options.dataArgs ).length
- ? ''
- : `${ this.getCheckboxesFiltersLabel() }${ Object.keys( this.options.dataArgs )
- .map( ( key ) => this.getCheckboxHTML( key ) )
- .join( '' ) }
`;
-
- /**
- * Get the HTML for a single checkbox.
- *
- * @param {string} key - The key of the data.
- *
- * @return {string} The checkbox HTML.
- */
- getCheckboxHTML = ( key ) =>
- `
-
-
- ${ 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 9c5a594497..0000000000
--- 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 c0b3e8fddf..0000000000
--- 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-admin-ui-instance.php b/classes/admin/class-admin-ui-instance.php
new file mode 100644
index 0000000000..ffee2ae08e
--- /dev/null
+++ b/classes/admin/class-admin-ui-instance.php
@@ -0,0 +1,70 @@
+get_ui__branding()->to_kit_branding();
+
+ $config = new Config(
+ 'progress-planner',
+ 'progress-planner',
+ 'progress-planner',
+ 'progress_planner',
+ 'progress-planner/v1',
+ $branding->name,
+ $branding->submenu_name,
+ $branding,
+ $package_root . '/assets',
+ \plugin_dir_url( \constant( 'PROGRESS_PLANNER_FILE' ) ) . 'vendor/progressplanner/wp-admin-ui/assets',
+ \constant( 'PROGRESS_PLANNER_DIR' ) . '/assets',
+ \constant( 'PROGRESS_PLANNER_URL' ) . '/assets',
+ $package_root . '/views',
+ \constant( 'PROGRESS_PLANNER_DIR' ) . '/views',
+ 'manage_options',
+ \progress_planner()->get_ui__branding()->get_admin_submenu_position(),
+ true, // show_range_filter — matches progress-planner's existing header.
+ true // register_page — the kit owns the admin menu.
+ );
+
+ self::$instance = AdminUI::boot( $config );
+ return self::$instance;
+ }
+}
diff --git a/classes/admin/class-admin-ui-kit-integration.php b/classes/admin/class-admin-ui-kit-integration.php
new file mode 100644
index 0000000000..129805162d
--- /dev/null
+++ b/classes/admin/class-admin-ui-kit-integration.php
@@ -0,0 +1,126 @@
+widgets() when rendering.
+ foreach ( \progress_planner()->get_admin__page()->get_widgets() as $prpl_widget ) {
+ $ui->add_widget( $prpl_widget );
+ }
+
+ $prefix = $ui->config()->asset_prefix; // 'progress-planner'.
+
+ \add_filter( $prefix . '_admin_ui_menu_title_suffix', [ self::class, 'menu_title_suffix' ] );
+ \add_action( $prefix . '_admin_ui_before_content', [ self::class, 'maybe_render_welcome' ] );
+ \add_action( $prefix . '_admin_ui_header_right', [ self::class, 'render_header_right' ] );
+ \add_action( $prefix . '_admin_ui_after_widgets', [ self::class, 'render_after_widgets' ] );
+ }
+
+ /**
+ * Append the pending-celebrations count bubble to the top-level menu title.
+ *
+ * @param string $suffix Existing suffix (other filter callbacks may have added to it).
+ */
+ public static function menu_title_suffix( $suffix ): string {
+ $count = (int) \wp_count_posts( 'prpl_recommendations' )->pending;
+ if ( 0 === $count ) {
+ return $suffix;
+ }
+
+ /* translators: Hidden accessibility text; %s: number of notifications. */
+ $a11y = \sprintf( \_n( '%s pending celebration', '%s pending celebrations', $count, 'progress-planner' ), \number_format_i18n( $count ) );
+
+ return $suffix . \sprintf(
+ '%1$d %2$s ',
+ $count,
+ $a11y
+ );
+ }
+
+ /**
+ * Render the welcome/privacy-policy gate before the widgets container
+ * when the user hasn't accepted the privacy policy yet.
+ */
+ public static function maybe_render_welcome(): void {
+ if ( true === \progress_planner()->is_privacy_policy_accepted() ) {
+ return;
+ }
+ \progress_planner()->the_view( 'welcome.php' );
+ }
+
+ /**
+ * Render the tour button + subscribe popover in the header's right column.
+ */
+ public static function render_header_right(): void {
+ if ( true !== \progress_planner()->is_privacy_policy_accepted() ) {
+ return;
+ }
+ ?>
+
+ the_asset( 'images/icon_tour.svg' ); ?>
+
+
+ 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' ) . ' '
+ );
+ \progress_planner()->get_ui__popover()->the_popover( 'subscribe-form' )->render();
+ }
+ }
+
+ /**
+ * Render the tooltip-overlay click handler + JS templates after the
+ * widgets container.
+ */
+ public static function render_after_widgets(): void {
+ if ( true !== \progress_planner()->is_privacy_policy_accepted() ) {
+ return;
+ }
+ ?>
+
+ the_view( 'js-templates/suggested-task.html' );
+
+ /**
+ * Legacy action preserved for third-party integrations.
+ *
+ * @since 1.1.1
+ */
+ \do_action( 'progress_planner_admin_page_after_widgets' );
+ }
+}
diff --git a/classes/admin/class-dashboard-widget-score.php b/classes/admin/class-dashboard-widget-score.php
index d6b6509cb9..1b84db91c7 100644
--- a/classes/admin/class-dashboard-widget-score.php
+++ b/classes/admin/class-dashboard-widget-score.php
@@ -36,8 +36,11 @@ 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 + 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_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' );
diff --git a/classes/admin/class-dashboard-widget-todo.php b/classes/admin/class-dashboard-widget-todo.php
index 8fd9629ecd..c86fafc25c 100644
--- a/classes/admin/class-dashboard-widget-todo.php
+++ b/classes/admin/class-dashboard-widget-todo.php
@@ -37,7 +37,10 @@ protected function get_title() {
* @return void
*/
public function render_widget() {
- \progress_planner()->get_admin__page()->enqueue_styles();
+ // 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 ) {
diff --git a/classes/admin/class-enqueue.php b/classes/admin/class-enqueue.php
index 20c045c4ec..333ef4a220 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,65 @@ 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',
+ [
+ // 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 );
+ },
+ // Fire progress-planner's localize_script() switch for every
+ // script the kit enqueues (including transitive deps), so
+ // wp_localize_script() payloads like prplL10nStrings,
+ // progressPlannerBadge, prplSuggestedTask get attached to
+ // l10n / prpl-badge / suggested-task no matter how they
+ // entered the enqueue chain.
+ function ( string $handle ): void {
+ $this->localize_script( $handle );
+ }
+ );
+
+ // 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 +142,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 +172,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.
+ * Enqueue a vendor script (plain wp_enqueue_script, no dep resolution).
*
- * @param string $context The context of the file ( `css` or `js` ).
- * @param string $handle The handle of the file.
- *
- * @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/classes/admin/class-page.php b/classes/admin/class-page.php
index 73d38c170f..94148cbc3a 100644
--- a/classes/admin/class-page.php
+++ b/classes/admin/class-page.php
@@ -1,6 +1,18 @@
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( '%1$d %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( '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.
*
@@ -162,48 +108,38 @@ public function enqueue_assets( $hook ) {
return;
}
- $this->enqueue_scripts();
- $this->enqueue_styles();
- }
+ $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' ),
+ ],
+ ];
- /**
- * Enqueue scripts.
- *
- * @return void
- */
- public function enqueue_scripts() {
- $current_screen = \get_current_screen();
- if ( ! $current_screen ) {
- return;
+ 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( 'upgrade-tasks' );
+ } else {
+ \progress_planner()->get_admin__enqueue()->enqueue_script( 'onboard', $default_localization_data );
}
- 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' );
+ \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' );
+ \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/upgrade-tasks' );
- \progress_planner()->get_admin__enqueue()->enqueue_script( 'external-link-accessibility-helper' );
+ if ( ! \progress_planner()->is_privacy_policy_accepted() ) {
+ \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/welcome' );
+ \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/onboard' );
}
}
@@ -272,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.
*
@@ -317,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;
}
@@ -348,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/classes/admin/widgets/class-badge-streak.php b/classes/admin/widgets/class-badge-streak.php
index 5ce16aac19..323795d41a 100644
--- a/classes/admin/widgets/class-badge-streak.php
+++ b/classes/admin/widgets/class-badge-streak.php
@@ -24,7 +24,7 @@ abstract class Badge_Streak extends Widget {
*
* @return void
*/
- public function enqueue_styles() {
+ public function enqueue_styles(): void {
\progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/page-widgets/badge-streak' );
}
diff --git a/classes/admin/widgets/class-challenge.php b/classes/admin/widgets/class-challenge.php
index 79fc75e87f..3d4a51653a 100644
--- a/classes/admin/widgets/class-challenge.php
+++ b/classes/admin/widgets/class-challenge.php
@@ -66,7 +66,7 @@ public function get_challenge() {
*
* @return void
*/
- public function render() {
+ public function render(): void {
if ( empty( $this->get_challenge() ) ) {
return;
}
diff --git a/classes/admin/widgets/class-todo.php b/classes/admin/widgets/class-todo.php
index bfc2903d2a..80c95ce23e 100644
--- a/classes/admin/widgets/class-todo.php
+++ b/classes/admin/widgets/class-todo.php
@@ -31,7 +31,7 @@ final class ToDo extends Widget {
*
* @return void
*/
- public function enqueue_styles() {
+ public function enqueue_styles(): void {
parent::enqueue_styles();
\wp_add_inline_style(
"progress-planner/page-widgets/{$this->id}",
diff --git a/classes/admin/widgets/class-widget.php b/classes/admin/widgets/class-widget.php
index 53b4c81a33..fa3b6162bf 100644
--- a/classes/admin/widgets/class-widget.php
+++ b/classes/admin/widgets/class-widget.php
@@ -2,11 +2,18 @@
/**
* Base class for widgets.
*
+ * Since Phase 4 of the wp-admin-ui extraction, this class extends the
+ * kit's Widget base. Rendering mechanics (wrapper div, CSS class naming,
+ * view loading) now live in the kit. Progress-planner-specific additions
+ * (range/frequency getters, stylesheet-dependency helper) stay here.
+ *
* @package Progress_Planner
*/
namespace Progress_Planner\Admin\Widgets;
+use ProgressPlanner\AdminUI\Widgets\Widget as Kit_Widget;
+use Progress_Planner\Admin\Admin_UI_Instance;
use Progress_Planner\Utils\Traits\Input_Sanitizer;
/**
@@ -14,7 +21,7 @@
*
* All widgets should extend this class.
*/
-abstract class Widget {
+abstract class Widget extends Kit_Widget {
use Input_Sanitizer;
@@ -34,12 +41,6 @@ abstract class Widget {
*/
protected $force_last_column = false;
- /**
- * Constructor.
- */
- public function __construct() {
- }
-
/**
* The widget ID.
*
@@ -48,67 +49,41 @@ public function __construct() {
protected $id;
/**
- * Get the widget ID.
- *
- * @return string
+ * Concrete widgets are instantiated via Base::__call() which passes an
+ * empty args array to the constructor. We forward the kit's required
+ * AdminUI dependency from the progress-planner singleton.
*/
- public function get_id() {
- return $this->id;
+ public function __construct() {
+ parent::__construct( Admin_UI_Instance::get() );
}
/**
* Get the widget range.
- *
- * @return string
*/
- public function get_range() {
+ public function get_range(): string {
return $this->get_sanitized_get( 'range', '-6 months' );
}
/**
* Get the widget frequency.
- *
- * @return string
*/
- public function get_frequency() {
+ public function get_frequency(): string {
return $this->get_sanitized_get( 'frequency', 'monthly' );
}
/**
- * Render the widget.
- *
- * @return void
- */
- public function render() {
- $this->enqueue_styles();
- $this->enqueue_scripts();
- ?>
-
- get_admin__enqueue()->enqueue_style( "progress-planner/page-widgets/{$this->id}" );
}
/**
- * Enqueue scripts.
- *
- * @return void
+ * Enqueue this widget's script (legacy path).
*/
- public function enqueue_scripts() {
+ public function enqueue_scripts(): void {
\progress_planner()->get_admin__enqueue()->enqueue_script( 'widgets/' . $this->id );
}
diff --git a/classes/ui/class-branding.php b/classes/ui/class-branding.php
index aea8576d01..1a96ddd48b 100644
--- a/classes/ui/class-branding.php
+++ b/classes/ui/class-branding.php
@@ -324,4 +324,35 @@ 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();
+
+ // 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
+ // defaulting to #faa310 instead of #fff9f0). SaaS-driven custom
+ // CSS still flows through via $custom_css.
+ return new \ProgressPlanner\AdminUI\Branding(
+ $this->get_admin_menu_name(),
+ $this->get_admin_submenu_name(),
+ $this->get_admin_menu_icon( true ),
+ $logo_html,
+ null,
+ null,
+ null,
+ null,
+ null,
+ $this->get_custom_css()
+ );
+ }
}
diff --git a/classes/utils/class-playground.php b/classes/utils/class-playground.php
index 988043db81..01ef02effe 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' ] );
diff --git a/composer.json b/composer.json
index 9261fb73d5..abb36b5d33 100644
--- a/composer.json
+++ b/composer.json
@@ -9,6 +9,15 @@
"email": "info@emilia.capital"
}
],
+ "repositories": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/ProgressPlanner/wp-admin-ui"
+ }
+ ],
+ "require": {
+ "progressplanner/wp-admin-ui": "^0.1.0"
+ },
"require-dev": {
"wp-coding-standards/wpcs": "^3.1",
"phpcompatibility/phpcompatibility-wp": "*",
diff --git a/composer.lock b/composer.lock
index 595196b7e0..43f5c81da2 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,52 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "ed426f84147000579a3ac5a541a79b90",
- "packages": [],
+ "content-hash": "d51deec4556a1c119126b79374650e94",
+ "packages": [
+ {
+ "name": "progressplanner/wp-admin-ui",
+ "version": "v0.1.1",
+ "source": {
+ "type": "git",
+ "url": "git@github.com:ProgressPlanner/wp-admin-ui.git",
+ "reference": "7ec599a9630417153e2df9c07a57032ec4c089e4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ProgressPlanner/wp-admin-ui/zipball/7ec599a9630417153e2df9c07a57032ec4c089e4",
+ "reference": "7ec599a9630417153e2df9c07a57032ec4c089e4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6",
+ "szepeviktor/phpstan-wordpress": "^1.3"
+ },
+ "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/v0.1.1",
+ "issues": "https://github.com/ProgressPlanner/wp-admin-ui/issues"
+ },
+ "time": "2026-04-15T19:57:22+00:00"
+ }
+ ],
"packages-dev": [
{
"name": "antecedent/patchwork",
@@ -9400,5 +9444,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 5fa2c26a6a..dfe4592b20 100644
--- a/progress-planner.php
+++ b/progress-planner.php
@@ -27,6 +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';
+
+// 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/admin-page-header.php b/views/admin-page-header.php
deleted file mode 100644
index 1a6f753433..0000000000
--- a/views/admin-page-header.php
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
diff --git a/views/admin-page.php b/views/admin-page.php
deleted file mode 100644
index 451f983fde..0000000000
--- a/views/admin-page.php
+++ /dev/null
@@ -1,52 +0,0 @@
-is_privacy_policy_accepted();
-?>
-
-
-
-
-
- the_view( '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' ); ?>