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 }` - : ``; - 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 ) => - ``; - - /** - * 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( + '%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; + } + ?> + + 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( '%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(); - ?> -
-
- the_view( "page-widgets/{$this->id}.php" ); ?> -
-
- 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 @@ - -
- - -
- - 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/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' ); ?>