Skip to content
Open

Release #4587

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
d04caf2
feat(dashboard): add recommended plugins in performance tab
harshitarora-in Aug 10, 2026
5d0663d
fix: hide promoted plugin cards after successful activation
Copilot Aug 11, 2026
3506b5f
Sync branch [skip ci]
pirate-bot Aug 12, 2026
981caec
Sync branch [skip ci]
pirate-bot Aug 12, 2026
d9f9a83
fix: enhance Playwright tests
girishpanchal30 Aug 12, 2026
0dbd5ce
fix: enhance Playwright tests
girishpanchal30 Aug 13, 2026
b62b6ea
fix: enhance Playwright tests
girishpanchal30 Aug 14, 2026
859eb81
fix: simplify request handling
girishpanchal30 Aug 14, 2026
08b8f91
fix: enhance Playwright tests
girishpanchal30 Aug 14, 2026
ee905b6
Improved handling of post content ordering (#4575)
girishpanchal30 Aug 17, 2026
8207c0e
Merge pull request #4581 from Codeinwp/bugfix/e2e
selul Aug 18, 2026
e5ac5bc
refactor(dashboard): render performance plugins inside the tab content
harshitarora-in Aug 18, 2026
809a825
Merge branch 'development' into feat/performance-recommended-plugins
harshitarora-in Aug 18, 2026
fdda1d5
Protected registered meta keys in block editor sidebar (#4584)
girishpanchal30 Aug 19, 2026
f336f90
Checked walker class existence (#4586)
girishpanchal30 Aug 19, 2026
262e1d6
Prevent duplicate footer menu IDs (#4568)
girishpanchal30 Aug 20, 2026
f714386
Added class existence checks to prevent errors (#4588)
girishpanchal30 Aug 21, 2026
d7a71a2
Enhanced color sanitization to handle non-string values (#4591)
girishpanchal30 Aug 24, 2026
fdb1cc5
Improved component resolution handling (#4592)
girishpanchal30 Aug 24, 2026
bfad0e8
Merge branch 'master' into development
selul Aug 24, 2026
9677869
Fix capitalization of 'WordPress' in readme
selul Aug 24, 2026
116d718
chore: fix WordPress spelling flagged by Theme Check and update theme…
selul Aug 24, 2026
3904282
chore: update themeisle-sdk to 3.3.60 with Theme Check spelling fixes
selul Aug 24, 2026
8b23b67
fix: restore mobile cart after AJAX updates (#4589)
girishpanchal30 Aug 24, 2026
79a19be
Applied disabled styling to the title in the editor. (#4594)
girishpanchal30 Aug 24, 2026
4abf9c8
chore: exclude composer installed.json and SDK changelog from dist zip
selul Aug 24, 2026
1dce6a0
Improved dropdown positioning (#4590)
girishpanchal30 Aug 24, 2026
8dba348
Preserved HTML markup in excerpts (#4600)
girishpanchal30 Aug 27, 2026
01a2a33
refactor(dashboard): use ControlWrap for the recommended plugins section
harshitarora-in Aug 27, 2026
12efc00
fix(dashboard): guard plugin lookup and set explicit button type
harshitarora-in Aug 29, 2026
5bfd03f
Sync branch [skip ci]
pirate-bot Aug 31, 2026
38126e4
Handled WP Error responses to prevent fatal (#4595)
girishpanchal30 Aug 31, 2026
9f709dd
Improved post element ordering (#4597)
girishpanchal30 Aug 31, 2026
4ba5373
chore(deps): bump codeinwp/themeisle-sdk from 3.3.60 to 3.3.61 (#4608)
dependabot[bot] Aug 31, 2026
34838c5
ci: skip PR-comment job on Dependabot PRs (no secrets, always fails)
selul Sep 2, 2026
ba3c49b
ci: revert Dependabot gate on PR-comment job (org Dependabot secret c…
selul Sep 2, 2026
1be59bf
Fixed transparent header retaining a shadow line (#4609)
stefan-cotitosu Sep 4, 2026
23af558
Handle array input in logo sanitizer (#4607) (#4614)
selul Sep 5, 2026
19d34b3
Fix invalid Google Fonts catalog handling (#4604) (#4616)
selul Sep 5, 2026
e34411b
Handle array values in Mods::to_json (#4610) (#4615)
selul Sep 5, 2026
e2f847f
fix: add form label typography fallbacks (#4618)
selul Sep 5, 2026
e338270
Guard missing scroll-to-top options class (#4606) (#4617)
selul Sep 5, 2026
bb41a36
Merge branch 'development' into feat/performance-recommended-plugins
harshitarora-in Sep 9, 2026
a914d48
Merge pull request #4577 from Codeinwp/feat/performance-recommended-p…
harshitarora-in Sep 9, 2026
e7a40e6
fix: guard responsive container width calculations (#4621)
selul Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .distignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ header-footer-grid/package.json
header-footer-grid/package-lock.json
header-footer-grid/rollup.config.js
vendor/codeinwp/ti-onboarding/assets/vue
vendor/codeinwp/themeisle-sdk/CHANGELOG.md
vendor/composer/installed.json
vendor/codeinwp/ti-about-page/assets/scss
vendor/codeinwp/ti-about-page/assets/scss
vendor/codeinwp/ti-onboarding/assets/scss
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ jobs:
envs: "sample-data"
# - specs: "woo-visual-regression"
# envs: "woo-sample"
- specs: "editor"
envs: "sample-data"
runs-on: ubuntu-22.04
env:
ZIP_URL: "https://verti-artifacts.s3.amazonaws.com/${{ github.event.pull_request.base.repo.name }}-${{ needs.dev-zip.outputs.branch-name }}-${{ needs.dev-zip.outputs.git-sha-8 }}/neve.zip"
Expand All @@ -150,5 +152,7 @@ jobs:
if: failure()
uses: actions/upload-artifact@v4
with:
name: traces-${{ matrix.specs }}-${{ matrix.envs }}
path: ./test-results/**
if-no-files-found: ignore
retention-days: 1
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ New Features
- Integrated installation of Login Customizer, Cookie Notice, Duplicate Page, and Custom Fonts/Scripts modules
- New Maintenance and Coming Soon custom layouts [PRO]
- Admin Dashboard Customizer module to personalize the WordPress admin experience (Menu, Admin Bar, Custom Pages) [Agency]
- Included premium WP Landing Kit wordpress plugin [Agency]
- Included premium WP Landing Kit WordPress plugin [Agency]

Enhancements

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/* global neveDash */
import { useEffect, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import cn from 'classnames';
import { LoaderCircle, LucidePuzzle, LucideRocket } from 'lucide-react';

import Pill from '../../Common/Pill';
import Toast from '../../Common/Toast';
import TransitionInOut from '../../Common/TransitionInOut';
import ControlWrap from '../../Controls/ControlWrap';
import usePluginActions from '../../../Hooks/usePluginActions';
import {
NEVE_HIDE_PLUGINS,
NEVE_PLUGIN_ICON_MAP,
} from '../../../utils/constants';

const PROMOTED_SLUGS = ['optimole-wp', 'wp-cloudflare-page-cache'];

// How long the "Active" pill stays up before the card removes itself.
const ACTIVE_PILL_TIMEOUT = 1500;

const PluginCard = ({ slug, data, onDismiss }) => {
const ICON = NEVE_PLUGIN_ICON_MAP[slug] || LucidePuzzle;
// Titles and descriptions are already localized in inc/admin/dashboard/main.php.
const { title, description } = data;

const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);

const { doPluginAction, loading, buttonText } = usePluginActions(
slug,
true
);

useEffect(() => {
if (!success) {
return;
}

// Only `success` belongs in the deps: onDismiss is recreated on every
// parent render and would restart a countdown already in progress.
const timeoutId = window.setTimeout(() => {
onDismiss(slug);
}, ACTIVE_PILL_TIMEOUT);

return () => window.clearTimeout(timeoutId);
}, [success]);

const handleClick = async () => {
setError(null);

window.tiTrk?.with('neve').set(slug, {
feature: 'performance-plugin-promo',
featureComponent: slug,
});

const result = await doPluginAction();

if (result.success) {
setSuccess(true);

return;
}

setError(result.error);
};

return (
<div
className="flex flex-col h-full p-4 bg-white rounded-lg border border-gray-100"
data-plugin={slug}
>
<div className="flex gap-3 items-center">
<ICON className="size-6 text-blue-500 shrink-0" />
<h4 className="text-sm font-medium text-gray-900">{title}</h4>

{success && (
<div className="ml-auto">
<TransitionInOut show>
<Pill type="success">{__('Active', 'neve')}</Pill>
</TransitionInOut>
</div>
)}
</div>

{/* grow keeps the CTAs aligned when descriptions wrap to different heights */}
<p className="grow text-sm leading-relaxed text-gray-600 mt-2">
{description}
</p>

{!success && (
<button
type="button"
onClick={handleClick}
disabled={loading}
className={cn(
'mt-3 self-start text-blue-600 hover:text-blue-700 text-sm font-medium flex items-center',
{ 'opacity-75 cursor-not-allowed': loading }
)}
>
{loading && (
<LoaderCircle size={14} className="animate-spin mr-2" />
)}
<span>{buttonText}</span>
</button>
)}

{error && (
<div className="mt-2">
<Toast type="error" dismiss={setError} message={error} />
</div>
)}
</div>
);
};

const PerformancePlugins = () => {
// Snapshot: a card leaves this list when it asks to, not when the store
// changes, so the "Active" pill outlives the activation request.
const [visibleSlugs, setVisibleSlugs] = useState(() =>
PROMOTED_SLUGS.filter((slug) => {
// Super Page Cache is dropped server side when SPC Pro is
// installed, so a promoted slug can be missing entirely.
const plugin = neveDash.plugins?.[slug];

return plugin && plugin.cta !== 'deactivate';
})
);

if (NEVE_HIDE_PLUGINS || visibleSlugs.length < 1) {
return null;
}

const handleDismiss = (dismissed) =>
setVisibleSlugs((current) =>
current.filter((slug) => slug !== dismissed)
);

return (
<ControlWrap
label={__('Recommended Plugins', 'neve')}
icon={LucideRocket}
>
<div
className={cn('grid gap-4', {
'md:grid-cols-2': visibleSlugs.length > 1,
})}
>
{visibleSlugs.map((slug) => (
<PluginCard
key={slug}
slug={slug}
data={neveDash.plugins[slug]}
onDismiss={handleDismiss}
/>
))}
</div>
</ControlWrap>
);
};

export default PerformancePlugins;
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ToggleControl from '../../Controls/ToggleControl';
import Button from '../../Common/Button';
import useLicenseData from '../../../Hooks/useLicenseData';
import OptionGroup from './OptionGroup';
import PerformancePlugins from './PerformancePlugins';

const LOCAL_HOSTING_OPTION = 'enable_local_fonts';

Expand Down Expand Up @@ -114,6 +115,8 @@ export default () => {
/>

{(isLicenseValid && <ProModuleSettings />) || <DummySettings />}

<PerformancePlugins />
</div>
</>
);
Expand Down
42 changes: 32 additions & 10 deletions assets/apps/metabox/src/components/MetaFieldsManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,39 @@ class MetaFieldsManager extends Component {
});
}

addCSSToHead(css, styleID = 'neve-meta-editor-style') {
const head = document.head;
const hasStyle = document.getElementById(styleID);
if (hasStyle) {
hasStyle.innerHTML = css;
return false;
/**
* Documents the editor styles need to be added to.
*
* Since the block editor renders inside an iframe, styles added to the
* top document alone don't reach the content. Older setups without the
* iframe keep working through the top document.
*
* @return {Document[]} Target documents.
*/
getStyleTargets() {
const targets = [document];
const canvas = document.querySelector('iframe[name="editor-canvas"]');
const canvasDocument = canvas && canvas.contentDocument;

if (canvasDocument && canvasDocument.head) {
targets.push(canvasDocument);
}
const style = document.createElement('style');
style.setAttribute('id', styleID);
style.innerHTML = css;
head.appendChild(style);

return targets;
}

addCSSToHead(css, styleID = 'neve-meta-editor-style') {
this.getStyleTargets().forEach((doc) => {
const hasStyle = doc.getElementById(styleID);
if (hasStyle) {
hasStyle.innerHTML = css;
return;
}
const style = doc.createElement('style');
style.setAttribute('id', styleID);
style.innerHTML = css;
doc.head.appendChild(style);
});
}

updateTitleVisibility() {
Expand Down
50 changes: 30 additions & 20 deletions assets/js/src/frontend/navigation.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* global NeveProperties menuCalcEvent CustomEvent */
/* global menuCalcEvent CustomEvent */
/* jshint esversion: 6 */
import {
toggleClass,
Expand Down Expand Up @@ -29,39 +29,49 @@ export const initNavigation = () => {
* Reposition drop downs in case they go off screen.
*/
export const repositionDropdowns = () => {
const { isRTL } = NeveProperties;
const dropDowns = document.querySelectorAll(
'.sub-menu, .minimal .nv-nav-search'
);

if (dropDowns.length === 0) return;
if (!dropDowns.length) return;

const windowWidth = window.innerWidth;

// How far a dropdown runs past the viewport: negative past the left edge,
// positive past the right edge, zero when it fits.
const getOverflow = (bounding) => {
if (bounding.left < 0) {
return bounding.left;
}

return bounding.right > windowWidth ? bounding.right - windowWidth : 0;
};

dropDowns.forEach((dropDown) => {
let bounding = dropDown.getBoundingClientRect(),
rightDist = bounding.left;
const style = dropDown.style;

if (rightDist < 0) {
dropDown.style.right = isRTL ? '-100%' : 'auto';
dropDown.style.left = isRTL ? 'auto' : 0;
}
// Drop what an earlier pass applied, so the dropdown is measured where
// the stylesheet puts it and stale offsets do not pile up.
style.right = style.left = style.transform = '';

if (rightDist + bounding.width >= windowWidth) {
dropDown.style.right = isRTL ? 0 : '100%';
dropDown.style.left = 'auto';
let overflow = getOverflow(dropDown.getBoundingClientRect());

if (!overflow) {
return;
}

// Recalculate bounding after we've made adjustments.
bounding = dropDown.getBoundingClientRect();
rightDist = bounding.left;
const edge = dropDown.matches('.sub-menu .sub-menu') ? '100%' : '0px';

style.right = overflow < 0 ? 'auto' : edge;
style.left = overflow < 0 ? edge : 'auto';

// Wider than the space on the side it opens towards, offset it to fit.
overflow = getOverflow(dropDown.getBoundingClientRect());

if (rightDist < 0 || rightDist + bounding.width >= windowWidth) {
// Calculate how much should we offset the dropdown to make it fit.
dropDown.style.transform =
if (overflow) {
style.transform =
'translateX(' +
(isRTL ? '-' : '') +
(Math.abs(rightDist) + 20) +
(overflow < 0 ? 20 - overflow : -overflow - 20) +
'px)';
}
});
Expand Down
2 changes: 1 addition & 1 deletion assets/scss/components/hfg/frontend/layout/_general.scss
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,6 @@ $desktop: map-get($gl-devices-list, desktop);
grid-template-columns: 1fr auto 1fr;
}

.hfg_header.site-header {
.hfg_header.site-header:not(.neve-transparent-header) {
box-shadow: 0 -1px 3px rgba(0, 0, 0, 0.1);
}
6 changes: 3 additions & 3 deletions assets/scss/components/main/_extends.scss
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@

%nv-form-labels {
font-weight: var(--formlabelfontweight, var(--bodyfontweight));
text-transform: var(--formlabeltexttransform);
letter-spacing: var(--formlabelletterspacing);
line-height: var(--formlabellineheight);
text-transform: var(--formlabeltexttransform, none);
letter-spacing: var(--formlabelletterspacing, normal);
line-height: var(--formlabellineheight, var(--bodylineheight));
font-size: var(--formlabelfontsize, var(--bodyfontsize));
}

Expand Down
7 changes: 7 additions & 0 deletions bin/envs/cli-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ init_environment(){
echo "Installing Neve theme from $NEVE_LOCATION"
wp --allow-root theme install --activate $NEVE_LOCATION
wp --allow-root option update fresh_site 0

# Activating Neve remaps widgets by sidebar id, and 'blog-sidebar' does not
# match the previous theme's, so the default widgets can be left unassigned.
if [ "$(wp --allow-root widget list blog-sidebar --format=count)" = "0" ]; then
echo "Populating blog-sidebar"
wp --allow-root widget add block blog-sidebar --content='<!-- wp:search /-->'
fi
echo "Installing Theme API Plugin"
wp --allow-root plugin install https://github.com/codeinwp/wp-thememods-api/archive/refs/heads/main.zip --force --activate
}
Expand Down
7 changes: 5 additions & 2 deletions bin/ss-pages.test.stub
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ describe( 'Starter Sites VR - {{site}}', () => {
let frontpage = "{{site}}"
cy.visit(frontpage );
cy.captureDocument();
cy.get( '#nv-primary-navigation-main' ).then( $headerMenu => {
cy.get( '[id^="nv-primary-navigation"]' ).then( $headerMenu => {
[ ...$headerMenu.find( '.menu-item a' ) ].forEach( $url => {
let url = $url.href;
if(url.includes("#")){
Expand All @@ -13,8 +13,11 @@ describe( 'Starter Sites VR - {{site}}', () => {
if(frontpage.replace(/\/*$/, "") === url.replace(/\/*$/, "")){
return;
}
if(pages.includes(url)){
return;
}

pages.push( $url.href );
pages.push( url );
} );
} );
} );
Expand Down
Loading
Loading