-
+
{itemLabel()}
@@ -122,6 +148,10 @@ RepeaterItem.propTypes = {
onToggle: PropTypes.func.isRequired,
onContentChange: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired,
+ totalItems: PropTypes.number.isRequired,
+ onMove: PropTypes.func.isRequired,
+ isKeyboardActive: PropTypes.bool.isRequired,
+ onKeyboardActiveChange: PropTypes.func.isRequired,
};
export default RepeaterItem;
diff --git a/assets/apps/customizer-controls/src/scss/_customizer-search.scss b/assets/apps/customizer-controls/src/scss/_customizer-search.scss
index f0af3805c9..35e01d914f 100644
--- a/assets/apps/customizer-controls/src/scss/_customizer-search.scss
+++ b/assets/apps/customizer-controls/src/scss/_customizer-search.scss
@@ -1,27 +1,10 @@
#neve-customize-search {
- position: absolute;
- top: 40px;
- right: 0;
- width: 40px;
- height: 28px;
- overflow: hidden;
- z-index: 1000;
-
- button {
- text-decoration: none;
- &:focus:not(:disabled) {
- box-shadow: none;
- outline: none;
- }
- }
+ display: none;
}
#neve-customize-search-field {
- display: none;
- &.visible {
- display: block;
- }
+ display: block;
.accordion-section {
background-color: #fff;
@@ -37,7 +20,6 @@
.nv-search-wrap {
display: flex;
align-items: center;
- margin-top: 6px;
}
.nv-customizer-search-input {
diff --git a/assets/apps/customizer-controls/src/scss/_general.scss b/assets/apps/customizer-controls/src/scss/_general.scss
index a6fca330ef..cb506ae541 100644
--- a/assets/apps/customizer-controls/src/scss/_general.scss
+++ b/assets/apps/customizer-controls/src/scss/_general.scss
@@ -86,9 +86,12 @@
#customize-control-neve_pro_global_header_settings_main_shortcut p,
#customize-control-neve_pro_global_header_settings_bottom_shortcut p {
margin: 0;
+ background-color: #fff;
+ padding: 10px;
a {
cursor: pointer;
+ color: #007cba;
}
}
@@ -157,3 +160,42 @@
width: 100%;
}
}
+
+// Style Book button styles
+#customize-header-actions {
+ #neve-style-book {
+ display: block;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 48px;
+ width: 45px;
+ margin-top: 0 !important;
+ padding: 0;
+ background: #f0f0f1;
+ border: none;
+ border-radius: 0;
+ border-top: 4px solid #f0f0f1;
+ border-right: 1px solid #dcdcde;
+ color: #3c434a;
+ fill: #3c434a;
+ stroke: #3c434a;
+ text-align: center;
+ cursor: pointer;
+ transition: color 0.15s ease-in-out, border-color 0.15s ease-in-out, background 0.15s ease-in-out;
+
+ .dashicons {
+ font-size: 22px;
+ line-height: 1.2;
+ width: 22px;
+ height: 22px;
+ }
+
+ &:hover,
+ &:focus {
+ color: #0073aa;
+ border-top-color: #0073aa;
+ background: #fff;
+ }
+ }
+}
diff --git a/assets/apps/customizer-controls/src/scss/_ordering.scss b/assets/apps/customizer-controls/src/scss/_ordering.scss
index 1031f0d803..c02d0062fa 100644
--- a/assets/apps/customizer-controls/src/scss/_ordering.scss
+++ b/assets/apps/customizer-controls/src/scss/_ordering.scss
@@ -58,6 +58,16 @@ $icon-bright: #00a0d2;
fill: $gray-dark;
background-color: $gray-light;
}
+
+ &:focus {
+ outline: 2px solid $icon;
+ outline-offset: -2px;
+ }
+
+ &.keyboard-active {
+ background-color: $icon-bright;
+ fill: #fff;
+ }
}
diff --git a/assets/apps/customizer-controls/src/scss/_repeater.scss b/assets/apps/customizer-controls/src/scss/_repeater.scss
index 57c74436db..c229bbb733 100644
--- a/assets/apps/customizer-controls/src/scss/_repeater.scss
+++ b/assets/apps/customizer-controls/src/scss/_repeater.scss
@@ -17,6 +17,18 @@ $text-color: #50575e;
margin: 0;
}
+ .handle {
+ &:focus {
+ outline: 2px solid #0073aa;
+ outline-offset: -2px;
+ }
+
+ &.keyboard-active {
+ background-color: #00a0d2;
+ fill: #fff;
+ }
+ }
+
.icon-control {
height: 30px;
margin-bottom: 10px;
diff --git a/assets/apps/customizer-controls/src/typography-extra/LocalGoogleFonts.js b/assets/apps/customizer-controls/src/typography-extra/LocalGoogleFonts.js
index ac499ecadd..7f6ba54fcd 100644
--- a/assets/apps/customizer-controls/src/typography-extra/LocalGoogleFonts.js
+++ b/assets/apps/customizer-controls/src/typography-extra/LocalGoogleFonts.js
@@ -20,7 +20,7 @@ const initLocalGoogleFonts = () => {
return;
}
- const toggleControl =
+ const localFontsToggle =
new wp.customize.controlConstructor.neve_toggle_control(
NeveReactCustomize.localGoogleFonts.key,
{
@@ -32,7 +32,24 @@ const initLocalGoogleFonts = () => {
}
);
- render(
, section.container[0]);
+ const preloadFontsToggle =
+ new wp.customize.controlConstructor.neve_toggle_control(
+ NeveReactCustomize.preloadFonts.key,
+ {
+ section: section.id,
+ label: __('Preload fonts', 'neve'),
+ setting: NeveReactCustomize.preloadFonts.key,
+ priority: 6,
+ }
+ );
+
+ render(
+ <>
+
+
+ >,
+ section.container[0]
+ );
};
export { initLocalGoogleFonts };
diff --git a/assets/apps/dashboard/src/Components/App.js b/assets/apps/dashboard/src/Components/App.js
index c533454b7e..74734e0323 100644
--- a/assets/apps/dashboard/src/Components/App.js
+++ b/assets/apps/dashboard/src/Components/App.js
@@ -45,7 +45,9 @@ const App = () => {
{tabs[currentTab].render(setTab)}
- {!['starter-sites', 'settings'].includes(currentTab) && (
+ {!['starter-sites', 'settings', 'launch-progress'].includes(
+ currentTab
+ ) && (
diff --git a/assets/apps/dashboard/src/Components/Content/AvailableModule.js b/assets/apps/dashboard/src/Components/Content/AvailableModule.js
new file mode 100644
index 0000000000..06a3fc262a
--- /dev/null
+++ b/assets/apps/dashboard/src/Components/Content/AvailableModule.js
@@ -0,0 +1,213 @@
+/* global neveDash */
+import { __ } from '@wordpress/i18n';
+import {
+ NEVE_AVAILABLE_MODULES_ICON_MAP,
+ NEVE_STORE,
+} from '../../utils/constants';
+import { ArrowRight, LoaderCircle, LucideSettings } from 'lucide-react';
+import Card from '../../Layout/Card';
+import { useState } from '@wordpress/element';
+import { useDispatch, useSelect } from '@wordpress/data';
+import Toggle from '../Common/Toggle';
+import { send } from '../../utils/rest';
+
+const Toast = ({ message }) => {
+ return (
+
+ {message}
+
+ );
+};
+
+const ModuleToggle = ({
+ slug,
+ moduleData,
+ setMessage,
+ isActive,
+ setIsActive,
+ isInstalled,
+ setIsInstalled,
+}) => {
+ const [loading, setLoading] = useState(false);
+ const { changeModuleStatus, setObfxModuleStatus, setToast } =
+ useDispatch(NEVE_STORE);
+ const { moduleStatus } = useSelect((select) => {
+ const { getObfxModuleStatus } = select(NEVE_STORE);
+
+ return {
+ moduleStatus: getObfxModuleStatus(slug) || false,
+ };
+ });
+
+ const { api } = neveDash;
+ const { title } = moduleData;
+ const toastMessage = {
+ installing: __('Installing', 'neve'),
+ activating: __('Activating', 'neve'),
+ };
+
+ const handleToggle = async (value) => {
+ try {
+ setLoading(true);
+ changeModuleStatus(slug, value);
+
+ let isPluginActive = true;
+ // Handle plugin installation or activation
+ if (!isInstalled) {
+ setMessage(toastMessage.installing);
+ isPluginActive = false;
+ } else if (!isActive) {
+ setMessage(toastMessage.activating);
+ isPluginActive = false;
+ }
+
+ if (!isPluginActive) {
+ await send(api + 'activate-plugin', {
+ slug: 'themeisle-companion',
+ }).then((res) => {
+ if (res.success) {
+ setIsInstalled(true);
+ setIsActive(true);
+ }
+ });
+ }
+
+ // Fire the send method after install/activate or immediately if both are done
+ const response = await send(api + 'activate-module', {
+ slug,
+ value,
+ });
+
+ setObfxModuleStatus(slug, response.success ? value : !value);
+ setToast(
+ response.success
+ ? (value
+ ? __('Module Activated', 'neve')
+ : __('Module Deactivated.', 'neve')) + ` (${title})`
+ : response.data
+ );
+ } catch (error) {
+ setToast(
+ __(
+ 'Something went wrong. Please reload the page and try again.',
+ 'neve'
+ )
+ );
+ } finally {
+ setLoading(false);
+ setMessage('');
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+const AvailableModuleCard = ({
+ moduleData,
+ slug,
+ setMessage,
+ isActive,
+ setIsActive,
+ isInstalled,
+ setIsInstalled,
+}) => {
+ const { title, description } = moduleData;
+ const CardIcon = NEVE_AVAILABLE_MODULES_ICON_MAP[slug] || LucideSettings;
+
+ return (
+ }
+ title={title}
+ className="bg-white p-6 rounded-lg shadow-sm"
+ afterTitle={
+
+ }
+ id={`module-${slug}`}
+ >
+
+ {description}
+
+ {!isActive ? (
+
+ {__(
+ 'This feature is part of OrbitFox plugin, built by the Neve team. Enabling the toggle will automatically install and activate the plugin.',
+ 'neve'
+ )}
+
+ ) : (
+
+ {__('Settings', 'neve')}
+
+
+ )}
+
+ );
+};
+
+export default () => {
+ const [message, setMessage] = useState('');
+ const [isInstalled, setIsInstalled] = useState(
+ neveDash.orbitFox.isInstalled
+ );
+ const [isActive, setIsActive] = useState(neveDash.orbitFox.isActive);
+
+ return (
+ <>
+
+
+
+ {__('Available Modules', 'neve')}
+
+
+
+ {Object.entries(neveDash.availableModules).map(
+ ([slug, moduleData]) => (
+
+ )
+ )}
+
+
+ {message && (
+
+
+ {message}
+ >
+ }
+ />
+ )}
+ >
+ );
+};
diff --git a/assets/apps/dashboard/src/Components/Content/FreePro.js b/assets/apps/dashboard/src/Components/Content/FreePro.js
index 33c7226692..e664e548a8 100644
--- a/assets/apps/dashboard/src/Components/Content/FreePro.js
+++ b/assets/apps/dashboard/src/Components/Content/FreePro.js
@@ -2,17 +2,10 @@
import { __ } from '@wordpress/i18n';
-import {
- CheckCircle2,
- XCircle,
- HelpCircle,
- ArrowRight,
- BookOpen,
-} from 'lucide-react';
+import { CheckCircle2, XCircle, HelpCircle } from 'lucide-react';
import Card from '../../Layout/Card';
import Tooltip from '../Common/Tooltip';
-import Button from '../Common/Button';
import TransitionWrapper from '../Common/TransitionWrapper';
const FreeProCard = () => (
@@ -80,63 +73,12 @@ const FreeProCard = () => (
);
-const UpsellCard = () => {
- return (
-
-
-
- {__('Need help deciding?', 'neve')}
-
-
-
- {__(
- 'Our support team is happy to answer your questions about specific Pro features and help you determine if they match your needs.',
- 'neve'
- )}
-
-
-
- {__(
- 'Average response time: ~8 hours during business days',
- 'neve'
- )}
-
-
-
-
-
- {__('View Pro Plans', 'neve')}
-
-
-
- {__('Contact Support', 'neve')}
-
-
-
-
-
- );
-};
-
export default () => {
return (
-
-
-
);
};
diff --git a/assets/apps/dashboard/src/Components/Content/LaunchProgress.js b/assets/apps/dashboard/src/Components/Content/LaunchProgress.js
new file mode 100644
index 0000000000..693d0a3caa
--- /dev/null
+++ b/assets/apps/dashboard/src/Components/Content/LaunchProgress.js
@@ -0,0 +1,656 @@
+/* global neveDash */
+import { __ } from '@wordpress/i18n';
+import { useState, useEffect } from '@wordpress/element';
+import { LucideExternalLink, LucideCheck } from 'lucide-react';
+import Card from '../../Layout/Card';
+import Button from '../Common/Button';
+import TransitionWrapper from '../Common/TransitionWrapper';
+import cn from 'classnames';
+import apiFetch from '@wordpress/api-fetch';
+
+/**
+ * Initialize step state with auto-detection and saved progress
+ *
+ * @param {Array} steps - Array of step objects
+ * @param {string} sectionKey - Section key (identity, content, performance)
+ * @param {Object} savedProgress - Saved progress from server
+ * @param {Object} autoDetections - Auto-detection overrides by index
+ * @return {Array} Initialized state array
+ */
+const initializeStepState = (
+ steps,
+ sectionKey,
+ savedProgress,
+ autoDetections = {}
+) => {
+ return steps.map((step, index) => {
+ // Auto-detection overrides saved false values
+ if (autoDetections[index]) {
+ return true;
+ }
+
+ // Use saved progress if available
+ if (
+ savedProgress[sectionKey] &&
+ savedProgress[sectionKey][index] !== undefined
+ ) {
+ return savedProgress[sectionKey][index];
+ }
+
+ return step.completed;
+ });
+};
+
+const { plugins } = neveDash;
+
+const activeTPC =
+ plugins['templates-patterns-collection'] &&
+ plugins['templates-patterns-collection'].cta === 'deactivate';
+
+const LaunchProgress = () => {
+ // Get checks from neveDash
+ const checks = neveDash.launchProgress || {};
+ const autoDetected = checks.autoDetected || {};
+ const savedProgress = checks.savedProgress || {};
+
+ const [stepsState, setStepsState] = useState({
+ identity: initializeStepState(
+ identitySteps,
+ 'identity',
+ savedProgress,
+ {
+ 1: autoDetected.hasLogo,
+ 3: autoDetected.hasFavicon,
+ }
+ ),
+ content: initializeStepState(contentSteps, 'content', savedProgress),
+ performance: initializeStepState(
+ performanceSteps,
+ 'performance',
+ savedProgress,
+ {
+ 0: autoDetected.hasCustomPermalink,
+ 1: autoDetected.hasSeoPlugin,
+ 3: autoDetected.hasPrivacyPage,
+ }
+ ),
+ });
+
+ // Save progress whenever it changes
+ useEffect(() => {
+ const timeoutId = setTimeout(() => {
+ // Update neveDash object to keep it in sync
+ if (neveDash.launchProgress) {
+ neveDash.launchProgress.savedProgress = stepsState;
+ }
+
+ apiFetch({
+ path: '/nv/v1/dashboard/launch-progress',
+ method: 'POST',
+ data: { progress: stepsState },
+ }).catch((error) => {
+ // eslint-disable-next-line no-console
+ console.error('Failed to save progress:', error);
+ });
+ }, 500); // Debounce for 500ms
+
+ return () => clearTimeout(timeoutId);
+ }, [stepsState]);
+
+ // Calculate total steps and completed steps
+ const allCompleted = [
+ ...stepsState.identity,
+ ...stepsState.content,
+ ...stepsState.performance,
+ ];
+ const completedSteps = allCompleted.filter((completed) => completed).length;
+ const totalSteps = allCompleted.length;
+ const progressPercentage = (completedSteps / totalSteps) * 100;
+
+ // Trigger confetti when all steps are completed
+ useEffect(() => {
+ if (completedSteps === totalSteps && totalSteps > 0) {
+ triggerConfetti();
+ }
+ }, [completedSteps, totalSteps]);
+
+ return (
+
+ {/* Skip Setup Banner */}
+
+
+
+
+
+ ⚡
+
+
+ {__(
+ 'Import ready-made websites with a single click',
+ 'neve'
+ )}
+
+
+
+ {__(
+ 'Explore a vast library of pre-designed sites within Neve. Visit our constantly growing collection of demos to find the perfect starting point for your project.',
+ 'neve'
+ )}
+
+
+
+ {__('Starter Sites', 'neve')}
+
+
+
+
+ {/* Progress Bar */}
+
+ {completedSteps === totalSteps && totalSteps > 0 ? (
+ // Completion message
+
+
+
+ 🎉
+
+ {__(
+ 'Congratulations! Your Website is Ready for Launch!',
+ 'neve'
+ )}
+
+
+ {__(
+ "You've completed all essential setup steps. Take your site to the next level with Pro features!",
+ 'neve'
+ )}
+
+ {!neveDash.isValidLicense && (
+
+ {__('View Pro Plans', 'neve')}
+
+ )}
+
+ ) : (
+ // Progress tracking
+ <>
+
+
+
+
+ 🚀
+
+ {__('Launch Progress', 'neve')}
+
+
+ {__(
+ 'Complete these essential steps to launch your website',
+ 'neve'
+ )}
+
+
+
+
+ {completedSteps}/{totalSteps}
+
+
+ {__('Steps Completed', 'neve')}
+
+
+
+ {/* Progress Bar */}
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ );
+};
+
+const StepSection = ({
+ title,
+ steps,
+ sectionKey,
+ stepsState,
+ setStepsState,
+ startIndex = 1,
+}) => {
+ const stepsCount = steps.length;
+
+ return (
+
+
+
{title}
+
+ {stepsCount} {__('Steps', 'neve')}
+
+
+
+ {steps.map((step, index) => (
+ {
+ setStepsState((prev) => ({
+ ...prev,
+ [sectionKey]: prev[sectionKey].map(
+ (completed, i) =>
+ i === index ? !completed : completed
+ ),
+ }));
+ }}
+ />
+ ))}
+
+
+ );
+};
+
+const StepItem = ({ step, index, isCompleted, onToggle }) => {
+ const checkboxClasses = cn(
+ 'size-6 rounded border-2 flex items-center justify-center cursor-pointer transition-all shrink-0',
+ {
+ 'bg-blue-600 border-blue-600': isCompleted,
+ 'bg-white border-gray-300 hover:border-gray-400': !isCompleted,
+ }
+ );
+
+ const handleToggle = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onToggle();
+ };
+
+ const handleKeyDown = (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ e.stopPropagation();
+ onToggle();
+ }
+ };
+
+ const handleButtonClick = (e) => {
+ e.stopPropagation();
+
+ if (!isCompleted) {
+ onToggle();
+ }
+
+ if (step.link.startsWith('http')) {
+ window.open(step.link, '_blank', 'noopener,noreferrer');
+ } else {
+ window.location.href = step.link;
+ }
+ };
+
+ const handleRowClick = () => {
+ onToggle();
+ };
+
+ const handleRowKeyDown = (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onToggle();
+ }
+ };
+
+ return (
+
+
+ {isCompleted && (
+
+ )}
+
+
+ {index}
+
+
+
+ {step.title}
+
+
{step.description}
+
+
+ {__('Set Up', 'neve')}
+
+
+
+ );
+};
+
+// Step data
+const urls = neveDash.launchProgressUrls || {};
+
+const identitySteps = [
+ {
+ title: __('Site Title', 'neve') + ' & ' + __('Site Tagline', 'neve'),
+ description: __(
+ 'Replace "Just Another WordPress Site" with your brand name and description',
+ 'neve'
+ ),
+ link: urls.siteIdentity,
+ completed: false,
+ },
+ {
+ title: __('Upload Logo', 'neve'),
+ description: __(
+ 'Add your custom logo to the header for a professional look',
+ 'neve'
+ ),
+ link: urls.logo,
+ completed: false,
+ },
+ {
+ title: __('Set Colors', 'neve'),
+ description: __(
+ "Customize your site's color scheme to match your brand identity",
+ 'neve'
+ ),
+ link: urls.colors,
+ completed: false,
+ },
+ {
+ title: __('Add Site Icon (Favicon)', 'neve'),
+ description: __(
+ 'Display your brand in browser tabs, bookmarks, and mobile home screens',
+ 'neve'
+ ),
+ link: urls.favicon,
+ completed: false,
+ },
+];
+
+const contentSteps = [
+ {
+ title: __('Create Your Homepage', 'neve'),
+ description: __(
+ 'Add compelling content that tells visitors what you do and why it matters',
+ 'neve'
+ ),
+ link: urls.homepage,
+ completed: false,
+ },
+ {
+ title:
+ __('About', 'neve') +
+ ' & ' +
+ __('Contact', 'neve') +
+ ' ' +
+ __('Pages', 'neve'),
+ description: __(
+ 'Create essential pages so visitors can learn about you and get in touch',
+ 'neve'
+ ),
+ link: urls.pages,
+ completed: false,
+ },
+ {
+ title: __('Navigation Menu', 'neve'),
+ description: __(
+ 'Make it easy for visitors to find their way around your website',
+ 'neve'
+ ),
+ link: urls.menus,
+ completed: false,
+ },
+ {
+ title: __('Footer', 'neve'),
+ description: __(
+ 'Add copyright info, social links, and contact details to your footer',
+ 'neve'
+ ),
+ link: urls.footer,
+ completed: false,
+ },
+];
+
+const performanceSteps = [
+ {
+ title: __('Set Permalink Structure', 'neve'),
+ description: __(
+ 'Configure SEO-friendly URLs (recommended: Post name)',
+ 'neve'
+ ),
+ link: urls.permalinks,
+ completed: false,
+ },
+ {
+ title: __('Install SEO Plugin', 'neve'),
+ description: __(
+ 'Optimize your site for search engines with Yoast SEO or RankMath',
+ 'neve'
+ ),
+ link: urls.plugins,
+ completed: false,
+ },
+ {
+ title: __('Test Site Speed', 'neve'),
+ description: __(
+ 'Check your website speed and performance using free testing tools',
+ 'neve'
+ ),
+ link: urls.speedTest,
+ completed: false,
+ },
+ {
+ title: __('Create Privacy Policy Page', 'neve'),
+ description: __(
+ 'Meet legal requirements with essential privacy and terms pages',
+ 'neve'
+ ),
+ link: urls.privacyPolicy,
+ completed: false,
+ },
+];
+
+/**
+ * Trigger confetti animation
+ */
+const triggerConfetti = () => {
+ const duration = 3000;
+ const animationEnd = Date.now() + duration;
+ const defaults = {
+ startVelocity: 30,
+ spread: 360,
+ ticks: 60,
+ zIndex: 999999,
+ };
+
+ const randomInRange = (min, max) => Math.random() * (max - min) + min;
+
+ const interval = setInterval(() => {
+ const timeLeft = animationEnd - Date.now();
+
+ if (timeLeft <= 0) {
+ return clearInterval(interval);
+ }
+
+ const particleCount = 50 * (timeLeft / duration);
+
+ // Create confetti from two origins
+ createConfetti(
+ Object.assign({}, defaults, {
+ particleCount,
+ origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
+ })
+ );
+ createConfetti(
+ Object.assign({}, defaults, {
+ particleCount,
+ origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
+ })
+ );
+ }, 250);
+};
+
+/**
+ * Create confetti particles
+ *
+ * @param {Object} options - Confetti options
+ */
+const createConfetti = (options) => {
+ const canvas = document.createElement('canvas');
+ canvas.style.position = 'fixed';
+ canvas.style.top = '0';
+ canvas.style.left = '0';
+ canvas.style.width = '100%';
+ canvas.style.height = '100%';
+ canvas.style.pointerEvents = 'none';
+ canvas.style.zIndex = options.zIndex || 999999;
+ document.body.appendChild(canvas);
+
+ const ctx = canvas.getContext('2d');
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+
+ const particles = [];
+ const colors = ['#3b82f6', '#8b5cf6', '#ec4899', '#10b981', '#f59e0b'];
+
+ const randomInRange = (min, max) => Math.random() * (max - min) + min;
+
+ // Create particles
+ for (let i = 0; i < options.particleCount; i++) {
+ particles.push({
+ x: canvas.width * options.origin.x,
+ y: canvas.height * options.origin.y,
+ angle: randomInRange(0, 360),
+ velocity: options.startVelocity + randomInRange(-5, 5),
+ color: colors[Math.floor(Math.random() * colors.length)],
+ size: randomInRange(5, 10),
+ rotation: randomInRange(0, 360),
+ rotationSpeed: randomInRange(-10, 10),
+ gravity: 0.5,
+ decay: 0.95,
+ tick: 0,
+ });
+ }
+
+ // Animate particles
+ const animate = () => {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ let activeParticles = 0;
+
+ particles.forEach((particle) => {
+ if (particle.tick < options.ticks) {
+ activeParticles++;
+ particle.tick++;
+ particle.velocity *= particle.decay;
+ particle.x +=
+ Math.cos((particle.angle * Math.PI) / 180) *
+ particle.velocity;
+ particle.y +=
+ Math.sin((particle.angle * Math.PI) / 180) *
+ particle.velocity +
+ particle.gravity;
+ particle.rotation += particle.rotationSpeed;
+
+ ctx.save();
+ ctx.translate(particle.x, particle.y);
+ ctx.rotate((particle.rotation * Math.PI) / 180);
+ ctx.fillStyle = particle.color;
+ ctx.fillRect(
+ -particle.size / 2,
+ -particle.size / 2,
+ particle.size,
+ particle.size
+ );
+ ctx.restore();
+ }
+ });
+
+ if (activeParticles > 0) {
+ window.requestAnimationFrame(animate);
+ } else {
+ document.body.removeChild(canvas);
+ }
+ };
+
+ animate();
+};
+
+export default LaunchProgress;
diff --git a/assets/apps/dashboard/src/Components/Content/ModuleGrid.js b/assets/apps/dashboard/src/Components/Content/ModuleGrid.js
index 9dd00a7a9f..2902ea6bb3 100644
--- a/assets/apps/dashboard/src/Components/Content/ModuleGrid.js
+++ b/assets/apps/dashboard/src/Components/Content/ModuleGrid.js
@@ -2,7 +2,12 @@
import { useDispatch, useSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
-import { LoaderCircle, LucideCheck, LucideSettings } from 'lucide-react';
+import {
+ LoaderCircle,
+ LucideCheck,
+ LucideSettings,
+ LucideExternalLink,
+} from 'lucide-react';
import useLicenseData from '../../Hooks/useLicenseData';
import Card from '../../Layout/Card';
@@ -137,12 +142,14 @@ const ModulesHeader = () => {
: __('Neve Pro Modules', 'neve')}
{!isLicenseValid && (
-
+
+ {__('Get Neve Pro', 'neve')}
+
+
)}
);
diff --git a/assets/apps/dashboard/src/Components/Content/Settings/GeneralTabContent.js b/assets/apps/dashboard/src/Components/Content/Settings/GeneralTabContent.js
index 4a1c828892..75f9e67fd8 100644
--- a/assets/apps/dashboard/src/Components/Content/Settings/GeneralTabContent.js
+++ b/assets/apps/dashboard/src/Components/Content/Settings/GeneralTabContent.js
@@ -7,6 +7,7 @@ import {
LucideMonitorDown,
LucideTags,
LucideType,
+ LucideExternalLink,
} from 'lucide-react';
import useLicenseData from '../../../Hooks/useLicenseData';
import { NEVE_HAS_VALID_PRO } from '../../../utils/constants';
@@ -16,6 +17,7 @@ import OptionGroup from './OptionGroup';
import ControlWrap from '../../Controls/ControlWrap';
import Toggle from '../../Common/Toggle';
import Select from '../../Common/Select';
+import Button from '../../Common/Button';
const DUMMY_SETTINGS_ARGS = {
enable_featured_image_taxonomy: {
@@ -185,9 +187,22 @@ export default () => {
return (
<>
-
- {__('General Settings', 'neve')}
-
+
+
+ {__('General Settings', 'neve')}
+
+
+ {!isLicenseValid && (
+
+ {__('Get Neve Pro', 'neve')}
+
+
+ )}
+
{(isLicenseValid &&
) ||
}
diff --git a/assets/apps/dashboard/src/Components/Content/Settings/ManageModulesTabContent.js b/assets/apps/dashboard/src/Components/Content/Settings/ManageModulesTabContent.js
index 8a077600b0..8248c15ff1 100644
--- a/assets/apps/dashboard/src/Components/Content/Settings/ManageModulesTabContent.js
+++ b/assets/apps/dashboard/src/Components/Content/Settings/ManageModulesTabContent.js
@@ -1,5 +1,11 @@
+import AvailableModule from '../AvailableModule';
import ModuleGrid from '../ModuleGrid';
export default () => {
- return
;
+ return (
+ <>
+
+
+ >
+ );
};
diff --git a/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js b/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js
index 536e468d8a..1efe58f04e 100644
--- a/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js
+++ b/assets/apps/dashboard/src/Components/Content/Settings/PerformanceTabContent.js
@@ -1,7 +1,14 @@
/* global neveDash */
import { __ } from '@wordpress/i18n';
-import { LucideCode, LucideSmile, LucideText, LucideZap } from 'lucide-react';
+import {
+ LucideCode,
+ LucideSmile,
+ LucideText,
+ LucideZap,
+ LucideExternalLink,
+} from 'lucide-react';
import ToggleControl from '../../Controls/ToggleControl';
+import Button from '../../Common/Button';
import useLicenseData from '../../../Hooks/useLicenseData';
import OptionGroup from './OptionGroup';
@@ -78,9 +85,22 @@ export default () => {
return (
<>
-
- {__('Performance Settings', 'neve')}
-
+
+
+ {__('Performance Settings', 'neve')}
+
+
+ {!isLicenseValid && (
+
+ {__('Get Neve Pro', 'neve')}
+
+
+ )}
+
{
const [toast, setToast] = useState('');
const [toastType, setToastType] = useState('success');
- const { valid, expiration } = license;
+ const { valid, expiration, tier } = license;
const { whiteLabel, strings } = neveDash;
const { licenseCardHeading, licenseCardDescription } = strings;
const isValid = 'valid' === valid;
@@ -70,6 +70,17 @@ const LicenseCard = () => {
return statusLabelMap[status];
};
+ const getPlanLabel = () => {
+ const planLabel = {
+ 1: __('Personal', 'neve'),
+ 2: __('Business', 'neve'),
+ 3: __('Agency', 'neve'),
+ };
+
+ return planLabel[tier] || null;
+ };
+ const planLabel = getPlanLabel();
+
return (
@@ -124,6 +135,11 @@ const LicenseCard = () => {
)}
{isOrWasValid && (
+ {planLabel && (
+
+ {planLabel}
+
+ )}
(
@@ -107,6 +109,54 @@ const ContributingCard = () => {
);
};
+const UpsellCard = () => {
+ return (
+
+
+
+ {__('Need help deciding?', 'neve')}
+
+
+
+ {__(
+ 'Our support team is happy to answer your questions about specific Pro features and help you determine if they match your needs.',
+ 'neve'
+ )}
+
+
+
+ {__(
+ 'Average response time: ~8 hours during business days',
+ 'neve'
+ )}
+
+
+
+
+
+ {__('View Pro Plans', 'neve')}
+
+
+
+ {__('Contact Support', 'neve')}
+
+
+
+
+
+ );
+};
+
const CommunityCard = () => {
return (
@@ -127,12 +177,21 @@ const CommunityCard = () => {
};
const Sidebar = () => {
+ const { currentTab } = useSelect((select) => {
+ const { getTab } = select(NEVE_STORE);
+ return {
+ currentTab: getTab(),
+ };
+ });
+
return (
{NEVE_HAS_PRO &&
}
{NEVE_HAS_PRO &&
}
+ {currentTab === 'free-pro' &&
}
+
{!NEVE_IS_WHITELABEL &&
}
{!NEVE_HAS_PRO &&
}
diff --git a/assets/apps/dashboard/src/Components/Header.js b/assets/apps/dashboard/src/Components/Header.js
index d39bfd3ec2..ca7818cae6 100644
--- a/assets/apps/dashboard/src/Components/Header.js
+++ b/assets/apps/dashboard/src/Components/Header.js
@@ -6,7 +6,11 @@ import { Fragment, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
-import { LucideBookOpen, LucideFileText } from 'lucide-react';
+import {
+ LucideBookOpen,
+ LucideFileText,
+ LucideExternalLink,
+} from 'lucide-react';
import useLicenseData from '../Hooks/useLicenseData';
import Container from '../Layout/Container';
import { NEVE_IS_WHITELABEL, NEVE_STORE } from '../utils/constants';
@@ -107,6 +111,8 @@ const HeaderTopBar = ({ currentTab, setTab }) => {
};
const Navigation = ({ setTab, currentTab }) => {
+ const { isLicenseValid } = useLicenseData();
+
return (
@@ -115,13 +121,22 @@ const Navigation = ({ setTab, currentTab }) => {
if (!label) {
return null;
}
+ // Hide "Get Neve Pro" tab for users with valid licenses
+ if (slug === 'get-neve-pro' && isLicenseValid) {
+ return null;
+ }
const itemClasses = cn([
'relative px-4 py-3 font-medium border-b-2',
{
'text-blue-600 border-blue-600':
- currentTab === slug,
+ currentTab === slug &&
+ slug !== 'get-neve-pro',
'border-transparent text-gray-600 hover:text-gray-900 transition-colors duration-150':
- currentTab !== slug,
+ currentTab !== slug &&
+ slug !== 'get-neve-pro',
+ 'border-transparent text-blue-600 transition-colors duration-150':
+ currentTab !== slug &&
+ slug === 'get-neve-pro',
},
]);
@@ -136,6 +151,9 @@ const Navigation = ({ setTab, currentTab }) => {
if (!url) {
linkProps.onClick = handleLinkClick;
+ } else {
+ linkProps.target = '_blank';
+ linkProps.rel = 'noopener noreferrer';
}
return (
@@ -144,7 +162,15 @@ const Navigation = ({ setTab, currentTab }) => {
key={slug}
className={itemClasses}
>
- {label}
+
+ {label}
+ {url && (
+
+ )}
+
);
})}
diff --git a/assets/apps/dashboard/src/store/actions.js b/assets/apps/dashboard/src/store/actions.js
index ab6f81dc71..802451a46e 100644
--- a/assets/apps/dashboard/src/store/actions.js
+++ b/assets/apps/dashboard/src/store/actions.js
@@ -50,4 +50,10 @@ export default {
payload: loggerStatus,
};
},
+ setObfxModuleStatus(slug, value) {
+ return {
+ type: 'SET_OBFX_MODULE_STATUS',
+ payload: { slug, value },
+ };
+ },
};
diff --git a/assets/apps/dashboard/src/store/reducer.js b/assets/apps/dashboard/src/store/reducer.js
index d2cd911b7c..e9f5ec628e 100644
--- a/assets/apps/dashboard/src/store/reducer.js
+++ b/assets/apps/dashboard/src/store/reducer.js
@@ -8,6 +8,7 @@ const initialState = {
currentTab: 'start',
license: neveDash.pro ? neveDash.license : {},
notifications: neveDash.notifications || {},
+ obfxModuleStatus: neveDash.orbitFox?.data?.module_status || {},
};
const hash = getTabHash();
@@ -73,6 +74,16 @@ const reducer = (state = initialState, action) => {
neve_logger_flag: action.payload,
},
};
+ case 'SET_OBFX_MODULE_STATUS':
+ return {
+ ...state,
+ obfxModuleStatus: {
+ ...state.obfxModuleStatus,
+ [action.payload.slug]: {
+ active: action.payload.value,
+ },
+ },
+ };
}
return state;
};
diff --git a/assets/apps/dashboard/src/store/selectors.js b/assets/apps/dashboard/src/store/selectors.js
index cb576148e4..c49ad370d5 100644
--- a/assets/apps/dashboard/src/store/selectors.js
+++ b/assets/apps/dashboard/src/store/selectors.js
@@ -25,4 +25,15 @@ export default {
return shownNotifications;
},
+ getObfxModuleStatus: (state, slug) => {
+ if (!state.obfxModuleStatus) {
+ return false;
+ }
+
+ if (state.obfxModuleStatus[slug]) {
+ return state.obfxModuleStatus[slug]?.active || false;
+ }
+
+ return false;
+ },
};
diff --git a/assets/apps/dashboard/src/utils/common.js b/assets/apps/dashboard/src/utils/common.js
index a145c5e438..0ebe8f1f6a 100644
--- a/assets/apps/dashboard/src/utils/common.js
+++ b/assets/apps/dashboard/src/utils/common.js
@@ -6,6 +6,7 @@ import Welcome from '../Components/Content/Welcome';
import FreePro from '../Components/Content/FreePro';
import Settings from '../Components/Content/Settings';
import Changelog from '../Components/Content/Changelog';
+import LaunchProgress from '../Components/Content/LaunchProgress';
import { __ } from '@wordpress/i18n';
@@ -18,6 +19,10 @@ const tabs = {
label: __('Starter Sites', 'neve'),
render: () => ,
},
+ 'launch-progress': {
+ label: __('Launch Progress', 'neve'),
+ render: () => ,
+ },
'free-pro': {
label: __('Free vs Pro', 'neve'),
render: () => ,
@@ -29,8 +34,18 @@ const tabs = {
changelog: {
render: () => ,
},
+ 'get-neve-pro': {
+ label: __('Get Neve Pro', 'neve'),
+ url: neveDash.upgradeURLModules,
+ external: true,
+ },
};
+// Conditionally remove launch-progress tab if not a new user
+if (Boolean(neveDash.showLaunchProgress) === false) {
+ delete tabs['launch-progress'];
+}
+
const { plugins } = neveDash;
const activeTPC =
plugins['templates-patterns-collection'] &&
@@ -68,7 +83,7 @@ const getTabHash = () => {
hash = hash.substring(1);
- if (!tabs[hash]?.render) {
+ if (!tabs[hash]?.render && !tabs[hash]?.url) {
return null;
}
diff --git a/assets/apps/dashboard/src/utils/constants.js b/assets/apps/dashboard/src/utils/constants.js
index f9ca5aa4f4..3e695cfed1 100644
--- a/assets/apps/dashboard/src/utils/constants.js
+++ b/assets/apps/dashboard/src/utils/constants.js
@@ -10,9 +10,11 @@ import {
LucideGraduationCap,
LucideImage,
LucideLayoutTemplate,
+ LucideLock,
LucideNewspaper,
LucidePalette,
LucidePanelRightDashed,
+ LucidePanelsTopLeft,
LucidePanelTopDashed,
LucidePin,
LucideRss,
@@ -22,6 +24,7 @@ import {
LucideShoppingCart,
LucideTimer,
LucideToyBrick,
+ LucideType,
LucideTypeOutline,
} from 'lucide-react';
@@ -67,3 +70,10 @@ export const NEVE_PLUGIN_ICON_MAP = {
'hyve-lite': LucideBotMessageSquare,
// 'sparks'
};
+
+export const NEVE_AVAILABLE_MODULES_ICON_MAP = {
+ 'login-customizer': LucideLock,
+ 'custom-fonts': LucideType,
+ 'policy-notice': LucideShield,
+ 'post-duplicator': LucidePanelsTopLeft,
+};
diff --git a/assets/apps/metabox/src/components/controls/SortableItems.js b/assets/apps/metabox/src/components/controls/SortableItems.js
index af3ba3cecf..358a47af16 100644
--- a/assets/apps/metabox/src/components/controls/SortableItems.js
+++ b/assets/apps/metabox/src/components/controls/SortableItems.js
@@ -1,10 +1,90 @@
import { withDispatch } from '@wordpress/data';
import { ReactSortable } from 'react-sortablejs';
-import { Button } from '@wordpress/components';
+import { Button, Tooltip } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
+import { useRef, useState } from '@wordpress/element';
+import useKeyboardSorting from '../../../../customizer-controls/src/common/useKeyboardSorting';
+
+const SortableItem = ({
+ item,
+ index,
+ total,
+ elements,
+ toggle,
+ onMove,
+ activeItemIdRef,
+ forceUpdate,
+}) => {
+ const { id, visible } = item;
+ const isActive = activeItemIdRef.current === id;
+ const { handleRef, handleKeyDown, handleBlur } = useKeyboardSorting(
+ index,
+ total,
+ (from, to) => onMove(from, to),
+ isActive,
+ (active) => {
+ activeItemIdRef.current = active ? id : null;
+ forceUpdate({});
+ }
+ );
+
+ return (
+
+
+
toggle(id)}
+ />
+ {elements[id]}
+
+
+ e.preventDefault()}
+ onKeyDown={handleKeyDown}
+ onBlur={handleBlur}
+ tabIndex={0}
+ type="button"
+ >
+
+
+
+
+
+
+ );
+};
const SortableItems = (props) => {
const { value, elements, updateElement, toggle } = props;
+ const activeItemIdRef = useRef(null);
+ const [, forceUpdate] = useState({});
+
+ const handleMove = (fromIndex, toIndex) => {
+ if (fromIndex === toIndex) {
+ return;
+ }
+ const newValue = [...value];
+ const [moved] = newValue.splice(fromIndex, 1);
+ newValue.splice(toIndex, 0, moved);
+ updateElement(newValue);
+ };
+
return (
{
list={value}
setList={updateElement}
handle=".ti-sortable-handle"
- animation="300"
+ animation={300}
>
- {value.map((item) => {
- const { id, visible } = item;
- return (
-
-
-
{
- toggle(id);
- }}
- />
-
- {elements[id]}
-
-
-
-
-
-
- );
- })}
+ {value.map((item, index) => (
+
+ ))}
);
diff --git a/assets/apps/metabox/src/editor.scss b/assets/apps/metabox/src/editor.scss
index b10ea5442a..4927d1da09 100644
--- a/assets/apps/metabox/src/editor.scss
+++ b/assets/apps/metabox/src/editor.scss
@@ -99,14 +99,44 @@
flex-basis: 20%;
cursor: move;
- .components-button {
- color: #bfbfbf;
+ // Enhanced custom handle button styling
+ .ti-sortable-handle-btn {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid #d5dadf;
+ background: #f6f7f7;
+ border-radius: 4px;
+ color: #6c6c6c;
+ cursor: move;
+ padding: 0;
+ transition: background .15s ease, box-shadow .15s ease, color .15s ease;
+ }
- &:hover, &:focus {
- color: #bfbfbf;
- box-shadow: none;
- background: none;
- }
+ .ti-sortable-handle-btn:hover {
+ background: #ebedef;
+ color: #444444;
+ }
+
+ .ti-sortable-handle-btn:focus {
+ outline: none;
+ background: #ffffff;
+ box-shadow: 0 0 0 2px #2271b1;
+ color: #1e1e1e;
+ }
+
+ &.keyboard-active .ti-sortable-handle-btn {
+ background: #2271b1;
+ color: #ffffff;
+ box-shadow: 0 0 0 2px #2271b1;
+ border-color: #2271b1;
+ }
+
+ .ti-sortable-handle-btn .dashicons {
+ font-size: 18px;
+ line-height: 1;
}
}
diff --git a/assets/js/src/customizer-preview/app.js b/assets/js/src/customizer-preview/app.js
index 15c683cfcf..19a30ae343 100644
--- a/assets/js/src/customizer-preview/app.js
+++ b/assets/js/src/customizer-preview/app.js
@@ -25,6 +25,26 @@ function handleResponsiveRadioButtons(args, nextValue) {
});
}
+window.wp.customize.bind('ready', () => {
+ previewScrollToTopChanges();
+});
+
+/**
+ * Preview scroll to top changes made in customizer.
+ */
+function previewScrollToTopChanges() {
+ wp.customize.preview.bind('nv-opened-stt', (show) => {
+ if (show) {
+ const scrollToTopBtn = document.querySelector('#scroll-to-top');
+ if (!scrollToTopBtn) {
+ return;
+ }
+ scrollToTopBtn.style.visibility = 'visible';
+ scrollToTopBtn.style.opacity = '1';
+ }
+ });
+}
+
/**
* Run JS on preview-ready.
*/
@@ -91,6 +111,217 @@ wp.customize.bind('preview-ready', function () {
selector + '{font-family: ' + parsedFontFamily + ' ;}'
);
});
+
+ // Handle Style Book toggle
+ wp.customize.preview.bind('neve-toggle-style-book', function (isOpen) {
+ const styleBookContent = document.getElementById('nv-sb-container');
+
+ if (!styleBookContent) {
+ return;
+ }
+
+ // If state is explicitly passed, use it; otherwise toggle current state
+ const shouldOpen =
+ typeof isOpen === 'boolean'
+ ? isOpen
+ : styleBookContent.style.display === 'none';
+
+ if (shouldOpen) {
+ styleBookContent.style.display = 'block';
+ document.body.classList.add('nv-sb-open');
+ } else {
+ styleBookContent.style.display = 'none';
+ document.body.classList.remove('nv-sb-open');
+ }
+ });
+
+ // Handle Style Book state restoration
+ wp.customize.preview.bind(
+ 'neve-restore-style-book-state',
+ function (isOpen) {
+ const styleBookContent = document.getElementById('nv-sb-container');
+
+ if (!styleBookContent) {
+ return;
+ }
+
+ if (isOpen) {
+ styleBookContent.style.display = 'block';
+ document.body.classList.add('nv-sb-open');
+ }
+ }
+ );
+
+ // Handle Style Book close button
+ document.addEventListener('click', function (e) {
+ if (
+ e.target.closest('.neve-style-book-close') ||
+ e.target.closest('.nv-sb-close-btn')
+ ) {
+ const styleBookContent = document.getElementById('nv-sb-container');
+ if (styleBookContent) {
+ styleBookContent.style.display = 'none';
+ document.body.classList.remove('nv-sb-open');
+ // Notify customizer controls about state change
+ wp.customize.preview.send(
+ 'neve-style-book-state-changed',
+ false
+ );
+ }
+ }
+
+ // Close when clicking outside the modal
+ if (e.target.classList.contains('neve-style-book-overlay')) {
+ const styleBookContent = document.getElementById('nv-sb-container');
+ if (styleBookContent) {
+ styleBookContent.style.display = 'none';
+ document.body.classList.remove('nv-sb-open');
+ // Notify customizer controls about state change
+ wp.customize.preview.send(
+ 'neve-style-book-state-changed',
+ false
+ );
+ }
+ }
+ });
+
+ // Handle Style Book clickable items navigation
+ document.addEventListener('click', function (e) {
+ // Skip navigation if clicking inside form fields (input, textarea, select)
+ if (e.target.matches('input, textarea, select')) {
+ e.stopPropagation();
+ return;
+ }
+
+ // Handle click on any Style Book builder-item-focus element
+ const styleBookItem = e.target.closest(
+ '#nv-sb-container .builder-item-focus'
+ );
+
+ if (styleBookItem) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ // Prioritize data-control if it exists, otherwise use data-section
+ const controlId = styleBookItem.getAttribute('data-control');
+ const sectionId = styleBookItem.getAttribute('data-section');
+
+ if (
+ window.parent &&
+ window.parent.wp &&
+ window.parent.wp.customize
+ ) {
+ try {
+ // If data-control is specified, handle the control
+ if (controlId) {
+ // Check if this is a color control (starts with neve-color-slug-)
+ if (controlId.startsWith('neve-color-slug-')) {
+ // Color controls don't have a control object, just find by class
+ const section =
+ window.parent.wp.customize.section(sectionId);
+ if (
+ section &&
+ typeof section.focus === 'function'
+ ) {
+ section.focus();
+
+ setTimeout(() => {
+ const colorControl =
+ window.parent.document.querySelector(
+ '.' + controlId
+ );
+ if (colorControl) {
+ const colorButton =
+ colorControl.querySelector(
+ '.components-button'
+ );
+ if (colorButton) {
+ colorButton.click();
+ }
+ }
+ }, 100);
+ }
+ return;
+ }
+
+ // Regular controls (accordions, buttons, etc.)
+ const control =
+ window.parent.wp.customize.control(controlId);
+
+ // Try to focus if the control has a focus method
+ if (control && typeof control.focus === 'function') {
+ control.focus();
+ }
+
+ // Handle accordion expansion after focusing
+ setTimeout(() => {
+ const controlElement =
+ window.parent.document.getElementById(
+ 'customize-control-' + controlId
+ );
+ if (controlElement) {
+ // Close all other expanded accordions in the same section
+ const section =
+ controlElement.closest('.control-section');
+ if (section) {
+ section
+ .querySelectorAll(
+ '.customize-control.expanded'
+ )
+ .forEach((accordion) => {
+ if (
+ accordion.id !==
+ 'customize-control-' + controlId
+ ) {
+ accordion.classList.remove(
+ 'expanded'
+ );
+ }
+ });
+ }
+
+ // Expand the target accordion if not already expanded
+ if (
+ !controlElement.classList.contains(
+ 'expanded'
+ )
+ ) {
+ const heading =
+ controlElement.querySelector(
+ '.neve-customizer-heading'
+ );
+ if (heading) {
+ heading.click();
+ }
+ }
+ }
+ }, 100);
+ return;
+ } // If data-section is specified or control focus failed, focus on section
+ if (sectionId) {
+ const section =
+ window.parent.wp.customize.section(sectionId);
+ if (section && typeof section.focus === 'function') {
+ section.focus();
+ }
+ }
+ } catch (error) {
+ // Fallback: Try to expand the section if focusing fails
+ try {
+ if (sectionId) {
+ const section =
+ window.parent.wp.customize.section(sectionId);
+ if (section && section.expanded) {
+ section.expanded(true);
+ }
+ }
+ } catch (fallbackError) {
+ // Silent fallback - navigation failed
+ }
+ }
+ }
+ }
+ });
});
/**
diff --git a/assets/js/src/scroll-to-top.js b/assets/js/src/scroll-to-top.js
new file mode 100644
index 0000000000..3672abe1bd
--- /dev/null
+++ b/assets/js/src/scroll-to-top.js
@@ -0,0 +1,97 @@
+/*global neveScrollOffset*/
+
+function scrollTopSafe(to) {
+ let i = window.scrollY;
+ to = parseInt(to);
+ const scrollInterval = setInterval(function () {
+ if (i < to + 20) i -= 1;
+ else if (i < to + 40) i -= 6;
+ else if (i < to + 80) i -= 16;
+ else if (i < to + 160) i -= 36;
+ else if (i < to + 200) i -= 48;
+ else if (i < to + 300) i -= 80;
+ else i -= 120;
+ window.scroll(0, i);
+ if (i <= to) clearInterval(scrollInterval);
+ }, 15);
+}
+
+function runScroll() {
+ const smoothScrollFeature =
+ 'scrollBehavior' in document.documentElement.style;
+ if (!smoothScrollFeature) {
+ scrollTopSafe(0);
+ } else {
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ }
+ const content = document.getElementById('content');
+ const scrollButton = document.getElementById('scroll-to-top');
+ if (content) {
+ scrollButton.blur();
+ content.focus();
+ }
+}
+function scrollToTop() {
+ const element = document.getElementById('scroll-to-top');
+ if (!element) {
+ return false;
+ }
+
+ element.addEventListener('click', function () {
+ runScroll();
+ });
+
+ element.addEventListener('keydown', function (event) {
+ if (event.key === 'Enter') {
+ runScroll();
+ }
+ });
+
+ window.addEventListener('scroll', function () {
+ const yScrollPos = window.scrollY;
+ const offset = neveScrollOffset.offset;
+
+ if (yScrollPos > offset) {
+ element.style.visibility = 'visible';
+ element.style.opacity = '1';
+ }
+ if (yScrollPos <= offset) {
+ element.style.opacity = '0';
+ element.style.visibility = 'hidden';
+ }
+
+ // Change scroll to top position if there is a sticky add to cart in place.
+ const stickyAddToCart = document.querySelector(
+ '.sticky-add-to-cart-bottom'
+ );
+ if (stickyAddToCart) {
+ element.style.bottom = '30px';
+
+ // Try to get Neve's sticky footer. If it doesn't exist try to get Elementor's.
+ let stickyFooter = document.querySelector('.hfg_footer');
+ if (
+ !stickyFooter ||
+ !stickyFooter.classList.contains('has-sticky-rows')
+ ) {
+ stickyFooter = document.querySelector(
+ '.elementor-location-footer .elementor-sticky'
+ );
+ }
+ const footerHeight = stickyFooter ? stickyFooter.offsetHeight : 0;
+
+ if (
+ stickyAddToCart.classList.contains('sticky-add-to-cart--active')
+ ) {
+ element.style.bottom =
+ stickyAddToCart.offsetHeight + footerHeight + 10 + 'px';
+ }
+ }
+ });
+}
+
+window.addEventListener('load', function () {
+ scrollToTop();
+});
diff --git a/assets/js/src/shop/app.js b/assets/js/src/shop/app.js
index cd9740586f..32143062f7 100644
--- a/assets/js/src/shop/app.js
+++ b/assets/js/src/shop/app.js
@@ -1,95 +1,158 @@
/* jshint esversion: 6 */
-/* global CustomEvent */
+/* global jQuery, CustomEvent */
+/* global neveShopSlider */
import { tns } from 'tiny-slider/src/tiny-slider';
-/**
- * Init shop.
- */
-function initShop() {
- if (document.body.classList.contains('woocommerce')) {
- handleShopSidebar();
+(function ($) {
+ /**
+ * Init shop.
+ */
+ function initShop() {
+ const $body = $('body');
+ if ($body.hasClass('woocommerce')) {
+ handleShopSidebar();
+ }
+
+ const countExclusive = $('.exclusive-products li.product').length;
+
+ if ($body.hasClass('nv-exclusive') && countExclusive > 4) {
+ handleExclusiveSlider();
+ }
+
+ if (
+ '1' !== neveShopSlider.isSparkActive &&
+ $body.hasClass('single-product')
+ ) {
+ handleGallerySlider();
+ }
}
- const countExclusive = document.querySelectorAll(
- '.exclusive-products li.product'
- ).length;
+ /**
+ * Add prev and next
+ *
+ * @param {Node} targetNode
+ * @param {Node} slider
+ * @param {string} vertical
+ */
+ function addNextPrev(targetNode, slider, vertical = false) {
+ const $next = $(' ')
+ .addClass('next dashicons')
+ .addClass(
+ 'dashicons-arrow-' + (vertical ? 'down' : 'right') + '-alt2'
+ );
+ const $prev = $(' ')
+ .addClass('prev dashicons')
+ .addClass(
+ 'dashicons-arrow-' + (vertical ? 'up' : 'left') + '-alt2'
+ );
+
+ $prev.on('click', () => slider.goTo('prev'));
- if (
- document.body.classList.contains('nv-exclusive') &&
- countExclusive > 4
- ) {
- handleExclusiveSlider();
+ $next.on('click', () => slider.goTo('next'));
+
+ const $target = $(targetNode);
+ $prev.insertBefore($target);
+ $next.insertAfter($target);
}
-}
-
-/**
- * Handle the shop sidebar.
- */
-function handleShopSidebar() {
- const sidebar = document.querySelector('.shop-sidebar');
- if (sidebar === null) {
- return;
+
+ /**
+ * Handle the shop sidebar.
+ */
+ function handleShopSidebar() {
+ const $sidebar = $('.shop-sidebar');
+ if (0 === $sidebar.length) {
+ return;
+ }
+ const $html = $('html');
+ const $toggles = $('.nv-sidebar-toggle');
+ $toggles.each(function () {
+ $(this).on('click', function (e) {
+ e.preventDefault();
+ $sidebar.toggleClass('sidebar-open');
+ $html.toggleClass('menu-openend');
+ });
+ });
}
- const html = document.querySelector('html');
- const toggles = document.querySelectorAll('.nv-sidebar-toggle') || [];
- toggles.forEach((toggle) => {
- toggle.addEventListener('click', function (e) {
- e.preventDefault();
- sidebar.classList.toggle('sidebar-open');
- html.classList.toggle('menu-openend');
+
+ /**
+ * Handle Exclusive Products Slider
+ */
+ function handleExclusiveSlider() {
+ const $items = $('ul.exclusive-products');
+
+ if (0 === $items.length) return;
+
+ const responsive = {
+ 0: { items: 2, gutter: 21 },
+ 768: { items: 4, gutter: 27 },
+ 1200: { items: 4, gutter: 30 },
+ };
+
+ const slider = tns({
+ container: 'ul.exclusive-products',
+ slideBy: 1,
+ arrowKeys: true,
+ loop: true,
+ autoplay: true,
+ items: 4,
+ edgePadding: 0,
+ autoplayButtonOutput: false,
+ autoplayHoverPause: true,
+ speed: 1000,
+ autoplayTimeout: 3000,
+ autoplayButton: false,
+ controls: false,
+ navPosition: 'bottom',
+ navContainer: '.dots-nav',
+ navAsThumbnails: true,
+ responsive,
});
- });
-}
-
-/**
- * Handle Exclusive Products Slider
- */
-function handleExclusiveSlider() {
- const items = document.querySelector('ul.exclusive-products');
-
- if (items === null) return false;
-
- const responsive = {
- 0: { items: 2, gutter: 21 },
- 768: { items: 4, gutter: 27 },
- 1200: { items: 4, gutter: 30 },
- };
-
- const slider = tns({
- container: 'ul.exclusive-products',
- slideBy: 1,
- arrowKeys: true,
- loop: true,
- autoplay: true,
- items: 4,
- edgePadding: 0,
- autoplayButtonOutput: false,
- autoplayHoverPause: true,
- speed: 1000,
- autoplayTimeout: 3000,
- autoplayButton: false,
- controls: false,
- navPosition: 'bottom',
- navContainer: '.dots-nav',
- navAsThumbnails: true,
- responsive,
- });
- // [If Sparks Variation Swatches is enabled and ] Initialize Sparks Variation Swatches for cloned products.
- if (document.body.classList.contains('sparks-vs-shop-attribute')) {
- slider.events.on('transitionEnd', () => {
- document.dispatchEvent(
- new CustomEvent('sparksVSNeedsInit', {
- detail: {
- container: '.products.exclusive',
- },
- })
- );
+ // [If Sparks Variation Swatches is enabled and ] Initialize Sparks Variation Swatches for cloned products.
+ if ($('body').hasClass('sparks-vs-shop-attribute')) {
+ slider.events.on('transitionEnd', () => {
+ document.dispatchEvent(
+ new CustomEvent('sparksVSNeedsInit', {
+ detail: { container: '.products.exclusive' },
+ })
+ );
+ });
+ }
+ }
+
+ /**
+ * Handle Gallery Image Slider
+ */
+ function handleGallerySlider() {
+ const $galleryNav = $('ol.flex-control-nav');
+
+ if (0 === $galleryNav.length) return;
+
+ const isDesktop = window.innerWidth >= 992;
+
+ const slider = tns({
+ container: 'ol.flex-control-nav',
+ items: 4,
+ axis: isDesktop ? 'vertical' : 'horizontal',
+ slideBy: 'page',
+ rewind: true,
+ loop: false,
+ nav: false,
+ controls: false,
+ mouseDrag: true,
});
+
+ addNextPrev(
+ $('.woocommerce-product-gallery .tns-inner')[0],
+ slider,
+ isDesktop
+ );
}
-}
-/**
- * Run JS on load.
- */
-window.addEventListener('load', initShop);
+ /**
+ * Run JS on load.
+ */
+ $(function () {
+ initShop();
+ });
+})(jQuery);
diff --git a/assets/scss/components/compat/woocommerce/_sidebar.scss b/assets/scss/components/compat/woocommerce/_sidebar.scss
index 4249c25ce4..da52a01c80 100644
--- a/assets/scss/components/compat/woocommerce/_sidebar.scss
+++ b/assets/scss/components/compat/woocommerce/_sidebar.scss
@@ -56,3 +56,95 @@
}
}
}
+
+body:not(.nv-left-gallery):not(.sp-slider-gallery):not([class*="related-products-columns-"]) {
+
+ .tns-ovh {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ }
+
+ .tns-inner {
+ overflow: hidden;
+ }
+
+ .tns-visually-hidden {
+ display: none;
+ }
+
+ /* Make WooCommerce gallery vertical on desktop */
+ @media (min-width: 992px) {
+
+ div.product {
+
+ .onsale {
+ left: 110px;
+ }
+
+ div.images {
+ display: flex;
+ flex-direction: row-reverse;
+ gap: 10px;
+
+ .tns-ovh {
+ width: 100px;
+ flex-direction: column;
+ position: relative;
+
+ .dashicons {
+ position: absolute;
+ z-index: 1;
+ color: var(--nv-text-color);
+ width: 100px;
+ text-align: center;
+
+ &.prev {
+ top: 0;
+ }
+
+ &.next {
+ bottom: 0;
+ }
+
+ &:hover {
+ background-color: var(--nv-site-bg);
+ }
+ }
+ }
+
+ .flex-viewport {
+ width: calc(100% - 100px);
+ }
+
+ .flex-control-nav {
+ display: flex;
+ flex-direction: column;
+ width: 100px;
+ margin-top: -5px;
+
+ li {
+ width: 100px;
+ }
+ }
+ }
+ }
+ }
+
+ /* On mobile, keep horizontal layout */
+ @media (max-width: 991px) {
+
+ div.product {
+
+ div.images {
+
+ .flex-control-nav {
+ flex-direction: row;
+ width: auto;
+ max-height: none;
+ display: flex;
+ }
+ }
+ }
+ }
+}
diff --git a/assets/scss/customizer-preview.scss b/assets/scss/customizer-preview.scss
index 6cee8648e7..4feef276b0 100644
--- a/assets/scss/customizer-preview.scss
+++ b/assets/scss/customizer-preview.scss
@@ -1,3 +1,6 @@
+@import "components/main/variables";
+@import "components/main/extends";
+
/* Customize Preview */
.edit-row-action {
top: 0;
@@ -78,9 +81,6 @@
.customize-partial-edit-shortcut {
display: none;
}
-
- .builder-item-focus {
- }
}
.footer--row {
@@ -118,3 +118,489 @@
top:unset !important;
}
}
+
+/* Prevent body scroll when Style Book is open */
+body.nv-sb-open {
+ overflow: hidden;
+}
+
+/* Style Book Modal */
+#nv-sb-container {
+ margin: 0 auto;
+ padding: 40px 20px;
+ width: 100%;
+ height: 100%;
+ position: fixed;
+ z-index: 999999;
+ overflow: scroll;
+ background: gray;
+}
+
+/* Close button in top right */
+.nv-sb-close-btn {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ width: 40px;
+ height: 40px;
+ background: rgba(0, 0, 0, 0.8);
+ border: none;
+ border-radius: 50%;
+ color: white;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000000;
+ transition: background-color 0.2s ease;
+
+ .dashicons {
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ color: white;
+ }
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.9);
+ }
+
+ &:focus {
+ outline: 2px solid white;
+ outline-offset: 2px;
+ background: rgba(0, 0, 0, 0.9);
+ }
+}
+
+.nv-sb-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 30px;
+ margin: 0 auto;
+ max-width: 956px;
+ margin-bottom: 30px;
+}
+
+.nv-sb-two-col-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 30px;
+}
+
+.nv-sb-section {
+ background: var(--nv-light-bg);
+ padding: 35px;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+}
+
+.nv-sb-section.nv-sb-full-section {
+ padding: 40px;
+}
+
+.nv-sb-section-title {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-bottom: 20px;
+ color: var(--nv-text-color);
+}
+
+/* Style Book - Generic clickable items */
+#nv-sb-container .builder-item-focus {
+ position: relative;
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ outline: 1px solid #0073aa;
+ outline-offset: -1px;
+ }
+}
+
+/* Color Palette */
+.nv-sb-color-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 12px;
+}
+
+.nv-sb-color-swatch {
+ border-radius: 6px;
+ box-shadow: 0px 2px 4px color-mix(in srgb, var(--nv-text-color) 20%, transparent);
+ background: var(--nv-light-bg);
+}
+
+.nv-sb-color-box {
+ height: 70px;
+ border-radius: 6px 6px 0 0;
+}
+
+.nv-sb-color-info {
+ padding: 10px;
+}
+
+.nv-sb-color-name {
+ font-weight: 600;
+ font-size: 0.85rem;
+ margin-bottom: 3px;
+}
+
+/* Typography */
+.nv-sb-typography-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 40px;
+ align-items: start;
+ overflow: hidden; /* Prevent grid overflow */
+}
+
+.nv-sb-type-sample {
+ margin-bottom: 30px;
+ overflow: hidden; /* Prevent heading overflow */
+}
+
+.nv-sb-alphabet {
+ line-height: 1.8;
+ word-wrap: break-word;
+ word-break: break-all; /* Break long character sequences */
+ overflow-wrap: break-word;
+ margin: 20px 0;
+ overflow: hidden; /* Prevent alphabet overflow */
+}
+
+/* Typography text content */
+.nv-sb-typography-grid p {
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ hyphens: auto;
+ line-height: 1.6;
+ overflow: hidden; /* Prevent paragraph overflow */
+}
+
+/* Heading elements in typography */
+.nv-sb-type-sample h1,
+.nv-sb-type-sample h2,
+.nv-sb-type-sample h3,
+.nv-sb-type-sample h4,
+.nv-sb-type-sample h5,
+.nv-sb-type-sample h6 {
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Buttons */
+.nv-sb-button-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15px;
+ margin-bottom: 25px;
+}
+
+ .nv-sb-btn-primary {
+ @extend %nv-button-primary;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-decoration: none;
+ display: inline-block;
+ }
+
+ .nv-sb-btn-secondary {
+ @extend %nv-button-secondary;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-decoration: none;
+ display: inline-block;
+ }
+
+/* Form Elements */
+.nv-sb-form-container {
+ max-width: 600px;
+}
+
+.nv-sb-form-group {
+ margin-bottom: 20px;
+ position: relative;
+ cursor: pointer;
+ padding: 15px;
+ border-radius: 6px;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: rgba(0, 115, 170, 0.05);
+ outline: 1px solid #0073aa;
+ outline-offset: -1px;
+ }
+
+ > label {
+ display: block;
+ margin-bottom: 8px;
+ }
+
+ input[type="text"],
+ input[type="email"],
+ input[type="password"],
+ input[type="url"],
+ input[type="tel"],
+ input[type="number"],
+ select,
+ textarea {
+ width: 100%;
+ font-family: inherit;
+ /* Let theme styles handle colors, padding, borders, etc. */
+ }
+
+ textarea {
+ min-height: 100px;
+ resize: vertical;
+ }
+
+ select {
+ cursor: pointer;
+ /* Let theme handle select styling */
+ }
+}
+
+.nv-sb-checkbox-group,
+.nv-sb-radio-group {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.nv-sb-checkbox-label,
+.nv-sb-radio-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: normal !important;
+ margin-bottom: 0 !important;
+ cursor: pointer;
+ padding: 8px 0;
+
+ input[type="checkbox"],
+ input[type="radio"] {
+ width: auto !important;
+ margin: 0;
+ /* Let theme handle input styling */
+ }
+}
+
+
+/* Full Width Section */
+.nv-sb-full-section {
+ grid-column: 1 / -1;
+}
+
+/* Responsive */
+/* Large tablets and small desktops */
+@media (max-width: 1024px) {
+ #nv-sb-container {
+ padding: 30px 15px;
+ }
+
+ .nv-sb-grid {
+ gap: 25px;
+ max-width: 100%;
+ padding: 0 15px;
+ }
+
+ .nv-sb-section {
+ padding: 25px;
+ }
+
+ .nv-sb-section.nv-sb-full-section {
+ padding: 30px;
+ }
+
+ .nv-sb-typography-grid {
+ grid-template-columns: 1fr;
+ gap: 30px;
+ }
+}
+
+/* Large tablets and small desktops */
+@media (max-width: 840px) {
+ .nv-sb-color-grid {
+ gap: 10px;
+ }
+
+ .nv-sb-typography-grid {
+ gap: 25px;
+ }
+}
+
+/* Tablets */
+@media (max-width: 768px) {
+ #nv-sb-container {
+ padding: 20px 10px;
+ }
+
+ .nv-sb-grid {
+ gap: 20px;
+ padding: 0 10px;
+ }
+
+ .nv-sb-two-col-grid {
+ grid-template-columns: 1fr;
+ gap: 20px;
+ }
+
+ .nv-sb-section {
+ padding: 20px;
+ }
+
+ .nv-sb-section.nv-sb-full-section {
+ padding: 25px;
+ }
+
+ .nv-sb-section-title {
+ font-size: 1.3rem;
+ margin-bottom: 15px;
+ }
+
+ .nv-sb-color-grid {
+ grid-template-columns: repeat(3, 1fr);
+ gap: 8px;
+ }
+
+ .nv-sb-color-box {
+ height: 55px;
+ }
+
+ .nv-sb-color-info {
+ padding: 6px;
+ }
+
+ .nv-sb-color-name {
+ font-size: 0.8rem;
+ }
+
+ .nv-sb-button-group {
+ flex-direction: column;
+ gap: 10px;
+ }
+}
+
+/* Small tablets and large phones */
+@media (max-width: 600px) {
+ .nv-sb-color-grid {
+ grid-template-columns: repeat(3, 1fr);
+ gap: 6px;
+ }
+
+ .nv-sb-color-box {
+ height: 45px;
+ }
+
+ .nv-sb-color-info {
+ padding: 5px;
+ }
+
+ .nv-sb-color-name {
+ font-size: 0.75rem;
+ }
+}
+
+/* Mobile phones */
+@media (max-width: 480px) {
+ .nv-sb-close-btn {
+ top: 15px;
+ right: 15px;
+ width: 35px;
+ height: 35px;
+
+ .dashicons {
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ }
+ }
+
+ .nv-sb-section {
+ padding: 15px;
+ }
+
+ .nv-sb-section.nv-sb-full-section {
+ padding: 20px;
+ }
+
+ .nv-sb-section-title {
+ font-size: 1.2rem;
+ margin-bottom: 12px;
+ }
+
+ .nv-sb-color-grid {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 8px;
+ }
+
+ .nv-sb-color-box {
+ height: 50px;
+ }
+
+ .nv-sb-color-info {
+ padding: 8px;
+ }
+
+ .nv-sb-color-name {
+ font-size: 0.8rem;
+ }
+
+ .nv-sb-typography-grid {
+ gap: 15px;
+ overflow: visible; /* Allow content to flow naturally on mobile */
+ }
+
+ /* Allow headings to wrap on mobile */
+ .nv-sb-type-sample h1,
+ .nv-sb-type-sample h2,
+ .nv-sb-type-sample h3,
+ .nv-sb-type-sample h4,
+ .nv-sb-type-sample h5,
+ .nv-sb-type-sample h6 {
+ white-space: normal;
+ text-overflow: unset;
+ }
+
+ .nv-sb-alphabet {
+ font-size: 1.3rem;
+ line-height: 1.5;
+ margin: 15px 0;
+ word-break: break-word; /* More aggressive breaking on mobile */
+ }
+
+ .nv-sb-btn-primary,
+ .nv-sb-btn-secondary {
+ padding: 10px 20px;
+ font-size: 0.9rem;
+ }
+
+ /* Form Elements on Mobile */
+ .nv-sb-form-group {
+ padding: 12px;
+ margin-bottom: 15px;
+
+ > label {
+ font-size: 0.9rem;
+ margin-bottom: 6px;
+ }
+
+ textarea {
+ min-height: 80px;
+ }
+ }
+
+ .nv-sb-checkbox-group,
+ .nv-sb-radio-group {
+ gap: 8px;
+ }
+
+ .nv-sb-checkbox-label,
+ .nv-sb-radio-label {
+ padding: 6px 0;
+ font-size: 0.9rem;
+ }
+}
diff --git a/composer.json b/composer.json
index 16953a4ef1..1ea7a1b515 100644
--- a/composer.json
+++ b/composer.json
@@ -40,7 +40,7 @@
"phpcs": "phpcs --standard=phpcs.xml -s --runtime-set testVersion 7.0-",
"lint": "composer run-script phpcs",
"phpcs-i": "phpcs -i",
- "phpstan": "phpstan analyse",
+ "phpstan": "phpstan analyse --memory-limit 2G",
"post-install-cmd": [
"[ ! -z \"$GITHUB_ACTIONS\" ] && yarn run bump-vendor || true"
],
diff --git a/composer.lock b/composer.lock
index 9364ceee8f..37857d6c15 100644
--- a/composer.lock
+++ b/composer.lock
@@ -8,16 +8,16 @@
"packages": [
{
"name": "codeinwp/themeisle-sdk",
- "version": "3.3.48",
+ "version": "3.3.50",
"source": {
"type": "git",
"url": "https://github.com/Codeinwp/themeisle-sdk.git",
- "reference": "0727d2cf2fc9bfb81b42968aeaf2bf4e340f021e"
+ "reference": "3c1f8dfc2390e667bbc086c5d660900a7985efa6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/0727d2cf2fc9bfb81b42968aeaf2bf4e340f021e",
- "reference": "0727d2cf2fc9bfb81b42968aeaf2bf4e340f021e",
+ "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/3c1f8dfc2390e667bbc086c5d660900a7985efa6",
+ "reference": "3c1f8dfc2390e667bbc086c5d660900a7985efa6",
"shasum": ""
},
"require-dev": {
@@ -36,16 +36,16 @@
"homepage": "https://themeisle.com"
}
],
- "description": "ThemeIsle SDK",
+ "description": "Themeisle SDK.",
"homepage": "https://github.com/Codeinwp/themeisle-sdk",
"keywords": [
"wordpress"
],
"support": {
"issues": "https://github.com/Codeinwp/themeisle-sdk/issues",
- "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.48"
+ "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.50"
},
- "time": "2025-08-11T16:47:24+00:00"
+ "time": "2025-11-25T19:36:35+00:00"
},
{
"name": "wptt/webfont-loader",
@@ -694,5 +694,5 @@
"platform-overrides": {
"php": "7.0"
},
- "plugin-api-version": "2.6.0"
+ "plugin-api-version": "2.9.0"
}
diff --git a/e2e-tests/fixtures/customizer/scroll-to-top/scroll-to-top-setup.json b/e2e-tests/fixtures/customizer/scroll-to-top/scroll-to-top-setup.json
new file mode 100644
index 0000000000..5df11f6b0e
--- /dev/null
+++ b/e2e-tests/fixtures/customizer/scroll-to-top/scroll-to-top-setup.json
@@ -0,0 +1,28 @@
+{
+ "general": {
+ "neve_scroll_to_top_side": "left",
+ "neve_scroll_to_top_label": "Go up",
+ "neve_scroll_to_top_offset": 100,
+ "neve_scroll_to_top_padding": {
+ "desktop": { "top": 10, "right": 12, "bottom": 10, "left": 12 },
+ "tablet": { "top": 6, "right": 8, "bottom": 6, "left": 8 },
+ "mobile": { "top": 10, "right": 12, "bottom": 10, "left": 12 },
+ "desktop-unit": "px",
+ "tablet-unit": "px",
+ "mobile-unit": "px"
+ },
+ "neve_scroll_to_top_border_radius": 100,
+ "neve_scroll_to_top_icon_color": "#ff0000",
+ "neve_scroll_to_top_icon_hover_color": "#ff0000",
+ "neve_scroll_to_top_background_color": "#ffffff",
+ "neve_scroll_to_top_background_hover_color": "#ffffff",
+ "neve_scroll_to_top_type": "icon",
+ "neve_scroll_to_top_image": 0,
+ "neve_scroll_to_top_on_mobile": false
+ },
+ "icon-check": {
+ "neve_scroll_to_top_side": "left",
+ "neve_scroll_to_top_icon_size": "{ \"mobile\": \"100\", \"tablet\": \"50\", \"desktop\": \"100\" }",
+ "neve_scroll_to_top_on_mobile": false
+ }
+}
\ No newline at end of file
diff --git a/e2e-tests/specs/customizer/hfg/hfg-logo-component.spec.ts b/e2e-tests/specs/customizer/hfg/hfg-logo-component.spec.ts
index 5466148015..ff8b6d5d29 100644
--- a/e2e-tests/specs/customizer/hfg/hfg-logo-component.spec.ts
+++ b/e2e-tests/specs/customizer/hfg/hfg-logo-component.spec.ts
@@ -128,7 +128,7 @@ test.describe('Logo Component palette', function () {
await expect(await siteLogo.getAttribute('src')).toBe(logos[1]?.url);
- await page.locator('.icon > svg > path').click();
+ await page.getByRole('link', { name: 'Palette Switch' }).click();
await expect(await siteLogo.getAttribute('src')).toBe(logos[0]?.url);
});
diff --git a/e2e-tests/specs/customizer/hfg/hfg-palette-switch-component.spec.ts b/e2e-tests/specs/customizer/hfg/hfg-palette-switch-component.spec.ts
index 0983dc41c1..05e70c3c06 100644
--- a/e2e-tests/specs/customizer/hfg/hfg-palette-switch-component.spec.ts
+++ b/e2e-tests/specs/customizer/hfg/hfg-palette-switch-component.spec.ts
@@ -28,7 +28,7 @@ test.describe('Palette Switch component', function () {
);
}
- await page.locator('.icon > svg > path').click();
+ await page.getByRole('link', { name: 'Palette Switch' }).click();
for (let i = 0; i < count; i++) {
await expect(headerElements.nth(i)).toHaveCSS(
diff --git a/e2e-tests/specs/customizer/scroll-to-top/scroll-to-top.spec.ts b/e2e-tests/specs/customizer/scroll-to-top/scroll-to-top.spec.ts
new file mode 100644
index 0000000000..8f53409880
--- /dev/null
+++ b/e2e-tests/specs/customizer/scroll-to-top/scroll-to-top.spec.ts
@@ -0,0 +1,190 @@
+import { test, expect } from '@playwright/test';
+import { setCustomizeSettings, scrollTo, visitAdminPage } from '../../../utils';
+import data from '../../../fixtures/customizer/scroll-to-top/scroll-to-top-setup.json';
+
+test.describe( 'Scroll to top', function () {
+ test( 'Checks the position', async function ( { page, request, baseURL } ) {
+ await setCustomizeSettings( 'stt-left', data.general, {
+ request,
+ baseURL,
+ } );
+ await page.goto( '/hello-world/?test_name=stt-left' );
+ await scrollTo( page, 'bottom' );
+ const scrollToTopBtn = await page.locator( '#scroll-to-top' );
+ await expect( scrollToTopBtn ).toHaveCSS( 'left', '20px' );
+
+ await setCustomizeSettings(
+ 'stt-right',
+ { neve_scroll_to_top_side: 'right' },
+ {
+ request,
+ baseURL,
+ }
+ );
+ await page.goto( '/hello-world/?test_name=stt-right' );
+ await scrollTo( page, 'bottom' );
+ await expect( scrollToTopBtn ).toHaveCSS( 'right', '20px' );
+ await scrollToTopBtn.click();
+ await page.waitForTimeout( 2000 );
+ const isAtTop = await page.evaluate( () => {
+ return window.scrollY === 0;
+ } );
+ await expect( isAtTop ).toBeTruthy();
+ } );
+
+ test( 'Checks scroll to top general settings', async function ( {
+ page,
+ request,
+ baseURL,
+ } ) {
+ await setCustomizeSettings( 'stt-general', data.general, {
+ request,
+ baseURL,
+ } );
+
+ await page.goto( '/hello-world/?test_name=stt-general' );
+ const sttButton = await page.locator( '#scroll-to-top' );
+
+ // Checks label
+ await page.evaluate( () => {
+ window.scrollTo( 0, document.body.scrollHeight );
+ } );
+ await expect( sttButton ).toBeVisible();
+ await expect(
+ await sttButton.getByText( /Go up/ ).first()
+ ).toBeVisible();
+
+ // Checks offset
+ await scrollTo( page, 80 );
+ await page.waitForTimeout( 500 );
+ await expect( sttButton ).not.toBeVisible();
+ await scrollTo( page, 110 );
+ await page.waitForTimeout( 500 );
+ await expect( sttButton ).toBeVisible();
+
+ // Checks button padding
+ await scrollTo( page, 'bottom' );
+ await expect( sttButton ).toHaveCSS( 'padding', '10px 12px' );
+
+ await page.setViewportSize( { width: 768, height: 1024 } );
+ await expect( sttButton ).toHaveCSS( 'padding', '6px 8px' );
+
+ await page.setViewportSize( { width: 375, height: 812 } );
+ await expect( sttButton ).toHaveCSS( 'padding', '10px 12px' );
+
+ // Checks border radius
+ await scrollTo( page, 'bottom' );
+ await expect( sttButton ).toHaveCSS( 'border-radius', '100px' );
+
+ // Checks colors
+ await scrollTo( page, 'bottom' );
+ await expect( sttButton ).toHaveCSS( 'color', 'rgb(255, 0, 0)' );
+ await expect( sttButton ).toHaveCSS(
+ 'background-color',
+ 'rgb(255, 255, 255)'
+ );
+
+ await sttButton.hover();
+ await expect( sttButton ).toHaveCSS(
+ 'background-color',
+ 'rgb(255, 255, 255)'
+ );
+ await expect( sttButton ).toHaveCSS( 'color', 'rgb(255, 0, 0)' );
+ } );
+
+ test( 'Checks the icon type', async function ( {
+ page,
+ request,
+ baseURL,
+ } ) {
+ const iconTypeData = Object.assign( {}, data.general );
+
+ // Get the id of the first image to be able to apply it.
+ await visitAdminPage( page, 'upload.php', '' );
+ await page.locator( '.attachment' ).first().click();
+ const urlString = page.url();
+ const url = new URL( urlString );
+ const imageId = url.searchParams.get( 'item' ) || '';
+
+ iconTypeData.neve_scroll_to_top_type = 'image';
+ iconTypeData.neve_scroll_to_top_image = parseInt( imageId );
+
+ await setCustomizeSettings( 'stt-icon-check', iconTypeData, {
+ request,
+ baseURL,
+ } );
+
+ await page.goto( '/hello-world/?test_name=stt-icon-check' );
+ await scrollTo( page, 'bottom' );
+
+ const scrollToTopImage = await page.locator(
+ '#scroll-to-top .scroll-to-top-image'
+ );
+ await expect( await scrollToTopImage.count() ).toBeGreaterThan( 0 );
+
+ await expect( scrollToTopImage ).toHaveCSS(
+ 'background-image',
+ /spectacles.gif/
+ );
+
+ await setCustomizeSettings( 'stt-icon-check2', data.general, {
+ request,
+ baseURL,
+ } );
+ await page.goto( '/hello-world/?test_name=stt-icon-check2' );
+ await scrollTo( page, 'bottom' );
+ await expect(
+ await page.locator( '#scroll-to-top svg' ).count()
+ ).toEqual( 1 );
+ } );
+
+ test( 'Checks hiding on mobile', async function ( {
+ page,
+ request,
+ baseURL,
+ } ) {
+ const hidingData = Object.assign( {}, data.general );
+ hidingData.neve_scroll_to_top_on_mobile = true;
+
+ await setCustomizeSettings( 'stt-check-hiding', hidingData, {
+ request,
+ baseURL,
+ } );
+
+ await page.goto( '/hello-world/?test_name=stt-check-hiding' ); // iphone-x
+
+ const sttButton = await page.locator( '#scroll-to-top' );
+
+ await page.setViewportSize( { width: 375, height: 812 } );
+ await scrollTo( page, 'bottom' );
+ await expect( sttButton ).not.toBeVisible();
+
+ await page.setViewportSize( { width: 1440, height: 900 } );
+ await scrollTo( page, 'bottom' );
+ await expect( sttButton ).toBeVisible();
+ } );
+
+ test( 'Checks icon size', async function ( { page, request, baseURL } ) {
+ await setCustomizeSettings( 'stt-check-icon3', data[ 'icon-check' ], {
+ request,
+ baseURL,
+ } );
+
+ await page.goto( '/hello-world/?test_name=stt-check-icon3' );
+ await scrollTo( page, 'bottom' );
+
+ const sttIcon = await page.locator(
+ '#scroll-to-top .scroll-to-top-icon'
+ );
+ await expect( sttIcon ).toHaveCSS( 'width', '100px' );
+ await expect( sttIcon ).toHaveCSS( 'height', '100px' );
+
+ await page.setViewportSize( { width: 768, height: 1024 } );
+ await expect( sttIcon ).toHaveCSS( 'width', '50px' );
+ await expect( sttIcon ).toHaveCSS( 'height', '50px' );
+
+ await page.setViewportSize( { width: 375, height: 812 } );
+ await expect( sttIcon ).toHaveCSS( 'width', '100px' );
+ await expect( sttIcon ).toHaveCSS( 'height', '100px' );
+ } );
+} );
diff --git a/e2e-tests/specs/customizer/style-book/style-book.spec.ts b/e2e-tests/specs/customizer/style-book/style-book.spec.ts
new file mode 100644
index 0000000000..8650b37cdd
--- /dev/null
+++ b/e2e-tests/specs/customizer/style-book/style-book.spec.ts
@@ -0,0 +1,125 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Style Book Modal', () => {
+ test.beforeEach(async ({ page }) => {
+ // First, try to go to customizer
+ await page.goto('/wp-admin/customize.php');
+
+ // Wait for customizer to fully load
+ await page.waitForSelector('.wp-full-overlay-sidebar', { state: 'visible' });
+
+ // Wait a bit more for all scripts to initialize
+ await page.waitForTimeout(1000);
+
+ // Open Style Book for all tests
+ await page.getByRole('button', { name: ' Style Book' }).click();
+
+ // Wait for Style Book to appear in the iframe
+ await page
+ .frameLocator('iframe[name="customize-preview-0"]')
+ .locator('#nv-sb-container')
+ .waitFor({ state: 'visible' });
+ });
+
+ test('should open Style Book modal when button is clicked', async ({ page }) => {
+ // Check that the Style Book modal appears (already opened in beforeEach)
+ const styleBookModal = page.frameLocator('iframe[name="customize-preview-0"]').locator('#nv-sb-container');
+ await expect(styleBookModal).toBeVisible();
+
+ // Check that the modal has the correct background overlay
+ await expect(styleBookModal).toHaveCSS('position', 'fixed');
+ await expect(styleBookModal).toHaveCSS('z-index', '999999');
+ });
+
+ test('should display all main sections in Style Book', async ({ page }) => {
+ // Check for all main sections
+ const iframe = page.frameLocator('iframe[name="customize-preview-0"]');
+ await expect(iframe.locator('.nv-sb-section-title', { hasText: 'Palette Colors' })).toBeVisible();
+ await expect(iframe.locator('.nv-sb-section-title', { hasText: 'Typography' })).toBeVisible();
+ await expect(iframe.locator('.nv-sb-section-title', { hasText: 'Buttons' })).toBeVisible();
+ await expect(iframe.locator('.nv-sb-section-title', { hasText: 'Form Fields' })).toBeVisible();
+ });
+
+ test('should display color swatches with correct structure', async ({ page }) => {
+ // Check color grid exists
+ const iframe = page.frameLocator('iframe[name="customize-preview-0"]');
+ await expect(iframe.locator('.nv-sb-color-grid')).toBeVisible();
+
+ // Check that color swatches are present
+ const colorSwatches = iframe.locator('.nv-sb-color-swatch');
+ await expect(colorSwatches).toHaveCount(9); // We have 9 color variables defined
+
+ // Check first color swatch structure
+ const firstSwatch = colorSwatches.first();
+ await expect(firstSwatch.locator('.nv-sb-color-box')).toBeVisible();
+ await expect(firstSwatch.locator('.nv-sb-color-name')).toBeVisible();
+
+ // Verify color swatch has clickable class
+ await expect(firstSwatch).toHaveClass(/builder-item-focus/);
+ });
+
+ test('should display typography elements with headings', async ({ page }) => {
+ // Check typography grid exists (Style Book already opened in beforeEach)
+ const iframe = page.frameLocator('iframe[name="customize-preview-0"]');
+ await expect(iframe.locator('.nv-sb-typography-grid')).toBeVisible();
+
+ // Check all heading levels are present and clickable
+ for (let i = 1; i <= 6; i++) {
+ const heading = iframe.locator(`.nv-sb-type-sample h${i}.builder-item-focus`);
+ await expect(heading).toBeVisible();
+ await expect(heading).toContainText(`Heading ${i}`);
+ }
+
+ // Check paragraph text is present and clickable
+ const paragraphs = iframe.locator('p.builder-item-focus');
+ await expect(paragraphs).toHaveCount(2); // We have 2 paragraphs
+ });
+
+ test('should display form elements with proper structure', async ({ page }) => {
+ // Check form container exists (Style Book already opened in beforeEach)
+ const iframe = page.frameLocator('iframe[name="customize-preview-0"]');
+ await expect(iframe.locator('.nv-sb-form')).toBeVisible();
+
+ // Check individual form elements
+ await expect(iframe.locator('input[type="text"]')).toBeVisible();
+ await expect(iframe.locator('textarea')).toBeVisible();
+ await expect(iframe.locator('select')).toBeVisible();
+ await expect(iframe.locator('.nv-sb-btn-primary')).toBeVisible();
+ });
+
+ test('should display buttons with proper styling', async ({ page }) => {
+ // Check button group exists (Style Book already opened in beforeEach)
+ const iframe = page.frameLocator('iframe[name="customize-preview-0"]');
+ await expect(iframe.locator('.nv-sb-button-group')).toBeVisible();
+
+ // Check both button types are present
+ const primaryBtn = iframe.locator('.nv-sb-btn-primary.builder-item-focus');
+ const secondaryBtn = iframe.locator('.nv-sb-btn-secondary.builder-item-focus');
+
+ await expect(primaryBtn).toBeVisible();
+ await expect(secondaryBtn).toBeVisible();
+
+ await expect(primaryBtn).toContainText('Primary Button');
+ await expect(secondaryBtn).toContainText('Secondary Button');
+ });
+
+ test('should close Style Book when close button is clicked', async ({ page }) => {
+ // Click close button (Style Book already opened in beforeEach)
+ const iframe = page.frameLocator('iframe[name="customize-preview-0"]');
+ await iframe.locator('.nv-sb-close-btn').click();
+
+ // Check that modal is hidden
+ await expect(iframe.locator('#nv-sb-container')).toBeHidden();
+ });
+
+ test('should navigate to customizer sections when elements are clicked', async ({ page }) => {
+ // Click on a color swatch (should navigate to colors section) - Style Book already opened in beforeEach
+ const iframe = page.frameLocator('iframe[name="customize-preview-0"]');
+ const colorSwatch = iframe.locator('.nv-sb-color-swatch.builder-item-focus').first();
+ await colorSwatch.click();
+
+ // Check that we're still in the customizer (Style Book should close and focus section)
+ await page.waitForTimeout(500); // Give time for navigation
+ await expect( page.getByRole('heading', { name: 'Customizing ▸ Global Colors & Background' }).getByText('Customizing ▸ Global') ).toBeVisible();
+ });
+});
diff --git a/functions.php b/functions.php
index 2cf1aca20f..092715b34c 100644
--- a/functions.php
+++ b/functions.php
@@ -164,3 +164,48 @@ function() {
);
add_filter( 'themeisle_sdk_enable_telemetry', '__return_true' );
+add_filter(
+ 'themeisle_sdk_labels',
+ function ( $labels ) {
+ if ( isset( $labels['about_us'] ) ) {
+ $labels['about_us'] = array_merge(
+ $labels['about_us'],
+ array(
+ 'title' => __( 'About Us', 'neve' ),
+ 'heroHeader' => __( 'Our Story', 'neve' ),
+ )
+ );
+ }
+ if ( isset( $labels['dashboard_widget'] ) ) {
+ $labels['dashboard_widget'] = array_merge(
+ $labels['dashboard_widget'],
+ array(
+ 'title' => __( 'WordPress Guides/Tutorials', 'neve' ),
+ /* translators: %s: product name */
+ 'popular' => __( 'Popular %s', 'neve' ),
+ 'install' => __( 'Install', 'neve' ),
+ /* translators: %s: product name */
+ 'powered' => __( 'Powered by %s', 'neve' ),
+ )
+ );
+ }
+ if ( isset( $labels['compatibilities'] ) ) {
+ $labels['compatibilities'] = array_merge(
+ $labels['compatibilities'],
+ array(
+ /* translators: %s: product name, %s: requirement name %s: update link start, %s: update link end, %s: requirement name %s: requirement type(theme/plugin) */
+ 'notice' => __( '%1$s requires a newer version of %2$s. Please %3$supdate%4$s %5$s %6$s to the latest version.', 'neve' ),
+ /* translators: %s: product name, %s: requirement name %s: update link start, %s: update link end, %s: requirement name %s: requirement type(theme/plugin) */
+ 'notice2' => __( '%1$s update requires a newer version of %2$s. Please %3$supdate%4$s %5$s %6$s.', 'neve' ),
+ /* translators: $1: Bold start, $2: Bold end, $3: theme name, $4: plugin name */
+ 'notice_theme' => __( '%1$sWarning:%2$s This theme has not been tested with your current version of %1$s%3$s%2$s. Please update %3$s plugin.', 'neve' ),
+ /* translators: $1: Bold start, $2: Bold end, $3: Product name, $4: product type(theme/plugin) */
+ 'notice_plugin' => __( '%1$sWarning:%2$s This plugin has not been tested with your current version of %1$s%3$s%2$s. Please update %3$s %4$s.', 'neve' ),
+ 'theme' => __( 'theme', 'neve' ),
+ 'plugin' => __( 'plugin', 'neve' ),
+ )
+ );
+ }
+ return $labels;
+ }
+);
diff --git a/globals/google-fonts.php b/globals/google-fonts.php
index 16d08e5c5b..946e670da1 100644
--- a/globals/google-fonts.php
+++ b/globals/google-fonts.php
@@ -1,11 +1,11 @@
array( '300', '400', '500', '600', '700', '800',),
+ '42dot Sans' => array(),
'ABeeZee' => array( '400', '400italic',),
'ADLaM Display' => array( '400',),
'AR One Sans' => array( '400', '500', '600', '700',),
@@ -20,8 +20,10 @@
'Adamina' => array( '400',),
'Advent Pro' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Afacad' => array( '400', '500', '600', '700', '400italic', '500italic', '600italic', '700italic',),
+ 'Afacad Flux' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Agbalumo' => array( '400',),
'Agdasima' => array( '400', '700',),
+ 'Agu Display' => array( '400',),
'Aguafina Script' => array( '400',),
'Akatab' => array( '400', '500', '600', '700', '800', '900',),
'Akaya Kanadaka' => array( '400',),
@@ -29,6 +31,7 @@
'Akronim' => array( '400',),
'Akshar' => array( '300', '400', '500', '600', '700',),
'Aladin' => array( '400',),
+ 'Alan Sans' => array( '300', '400', '500', '600', '700', '800', '900',),
'Alata' => array( '400',),
'Alatsi' => array( '400',),
'Albert Sans' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
@@ -60,6 +63,7 @@
'Alumni Sans Collegiate One' => array( '400', '400italic',),
'Alumni Sans Inline One' => array( '400', '400italic',),
'Alumni Sans Pinstripe' => array( '400', '400italic',),
+ 'Alumni Sans SC' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Amarante' => array( '400',),
'Amaranth' => array( '400', '700', '400italic', '700italic',),
'Amatic SC' => array( '400', '700',),
@@ -69,6 +73,8 @@
'Amiri Quran' => array( '400',),
'Amita' => array( '400', '700',),
'Anaheim' => array( '400', '500', '600', '700', '800',),
+ 'Ancizar Sans' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
+ 'Ancizar Serif' => array( '300', '400', '500', '600', '700', '800', '900', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Andada' => array(),
'Andada Pro' => array( '400', '500', '600', '700', '800', '400italic', '500italic', '600italic', '700italic', '800italic',),
'Andika' => array( '400', '700', '400italic', '700italic',),
@@ -120,8 +126,10 @@
'Asap' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Asap Condensed' => array( '200', '300', '400', '500', '600', '700', '800', '900', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Asar' => array( '400',),
+ 'Asimovian' => array( '400',),
'Asset' => array( '400',),
'Assistant' => array( '200', '300', '400', '500', '600', '700', '800',),
+ 'Asta Sans' => array( '300', '400', '500', '600', '700', '800',),
'Astloch' => array( '400', '700',),
'Asul' => array( '400', '700',),
'Athiti' => array( '200', '300', '400', '500', '600', '700',),
@@ -142,6 +150,9 @@
'Azeret Mono' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'B612' => array( '400', '700', '400italic', '700italic',),
'B612 Mono' => array( '400', '700', '400italic', '700italic',),
+ 'BBH Sans Bartle' => array( '400',),
+ 'BBH Sans Bogle' => array( '400',),
+ 'BBH Sans Hegarty' => array( '400',),
'BIZ UDGothic' => array( '400', '700',),
'BIZ UDMincho' => array( '400', '700',),
'BIZ UDPGothic' => array( '400', '700',),
@@ -149,6 +160,7 @@
'Babylonica' => array( '400',),
'Bacasime Antique' => array( '400',),
'Bad Script' => array( '400',),
+ 'Badeen Display' => array( '400',),
'Bagel Fat One' => array( '400',),
'Bahiana' => array( '400',),
'Bahianita' => array( '400',),
@@ -185,7 +197,7 @@
'Barrio' => array( '400',),
'Basic' => array( '400',),
'Baskervville' => array( '400', '500', '600', '700', '400italic', '500italic', '600italic', '700italic',),
- 'Baskervville SC' => array( '400',),
+ 'Baskervville SC' => array( '400', '500', '600', '700',),
'Battambang' => array( '100', '300', '400', '700', '900',),
'Baumans' => array( '400',),
'Bayon' => array( '400',),
@@ -208,12 +220,15 @@
'Beth Ellen' => array( '400',),
'Bevan' => array( '400', '400italic',),
'BhuTuka Expanded One' => array( '400',),
- 'Big Shoulders Display' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
- 'Big Shoulders Inline Display' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
- 'Big Shoulders Inline Text' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
- 'Big Shoulders Stencil Display' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
- 'Big Shoulders Stencil Text' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
- 'Big Shoulders Text' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Big Shoulders' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Big Shoulders Display' => array(),
+ 'Big Shoulders Inline' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Big Shoulders Inline Display' => array(),
+ 'Big Shoulders Inline Text' => array(),
+ 'Big Shoulders Stencil' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Big Shoulders Stencil Display' => array(),
+ 'Big Shoulders Stencil Text' => array(),
+ 'Big Shoulders Text' => array(),
'Bigelow Rules' => array( '400',),
'Bigshot One' => array( '400',),
'Bilbo' => array( '400',),
@@ -223,6 +238,18 @@
'Birthstone' => array( '400',),
'Birthstone Bounce' => array( '400', '500',),
'Biryani' => array( '200', '300', '400', '600', '700', '800', '900',),
+ 'Bitcount' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Bitcount Grid Double' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Bitcount Grid Double Ink' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Bitcount Grid Single' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Bitcount Grid Single Ink' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Bitcount Ink' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Bitcount Prop Double' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Bitcount Prop Double Ink' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Bitcount Prop Single' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Bitcount Prop Single Ink' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Bitcount Single' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Bitcount Single Ink' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Bitter' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Black And White Picture' => array( '400',),
'Black Han Sans' => array( '400',),
@@ -234,6 +261,7 @@
'Bodoni Moda' => array( '400', '500', '600', '700', '800', '900', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Bodoni Moda SC' => array( '400', '500', '600', '700', '800', '900', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Bokor' => array( '400',),
+ 'Boldonse' => array( '400',),
'Bona Nova' => array( '400', '700', '400italic',),
'Bona Nova SC' => array( '400', '700', '400italic',),
'Bonbon' => array( '400',),
@@ -252,7 +280,7 @@
'Bubblegum Sans' => array( '400',),
'Bubbler One' => array( '400',),
'Buda' => array( '300',),
- 'Buenard' => array( '400', '700',),
+ 'Buenard' => array( '400', '500', '600', '700',),
'Bungee' => array( '400',),
'Bungee Hairline' => array( '400',),
'Bungee Inline' => array( '400',),
@@ -262,6 +290,7 @@
'Bungee Tint' => array( '400',),
'Butcherman' => array( '400',),
'Butterfly Kids' => array( '400',),
+ 'Bytesized' => array( '400',),
'Cabin' => array( '400', '500', '600', '700', '400italic', '500italic', '600italic', '700italic',),
'Cabin Condensed' => array( '400', '500', '600', '700',),
'Cabin Sketch' => array( '400', '700',),
@@ -319,6 +348,9 @@
'Chewy' => array( '400',),
'Chicle' => array( '400',),
'Chilanka' => array( '400',),
+ 'Chiron GoRound TC' => array( '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Chiron Hei HK' => array( '200', '300', '400', '500', '600', '700', '800', '900', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
+ 'Chiron Sung HK' => array( '200', '300', '400', '500', '600', '700', '800', '900', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Chivo' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Chivo Mono' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Chocolate Classical Sans' => array( '400',),
@@ -357,6 +389,8 @@
'Cormorant SC' => array( '300', '400', '500', '600', '700',),
'Cormorant Unicase' => array( '300', '400', '500', '600', '700',),
'Cormorant Upright' => array( '300', '400', '500', '600', '700',),
+ 'Cossette Texte' => array( '400', '700',),
+ 'Cossette Titre' => array( '400', '700',),
'Courgette' => array( '400',),
'Courier Prime' => array( '400', '700', '400italic', '700italic',),
'Cousine' => array( '400', '700', '400italic', '700italic',),
@@ -429,13 +463,20 @@
'Edu AU VIC WA NT Guides' => array( '400', '500', '600', '700',),
'Edu AU VIC WA NT Hand' => array( '400', '500', '600', '700',),
'Edu AU VIC WA NT Pre' => array( '400', '500', '600', '700',),
+ 'Edu NSW ACT Cursive' => array( '400', '500', '600', '700',),
'Edu NSW ACT Foundation' => array( '400', '500', '600', '700',),
+ 'Edu NSW ACT Hand Pre' => array( '400', '500', '600', '700',),
'Edu QLD Beginner' => array( '400', '500', '600', '700',),
+ 'Edu QLD Hand' => array( '400', '500', '600', '700',),
'Edu SA Beginner' => array( '400', '500', '600', '700',),
+ 'Edu SA Hand' => array( '400', '500', '600', '700',),
'Edu TAS Beginner' => array( '400', '500', '600', '700',),
'Edu VIC WA NT Beginner' => array( '400', '500', '600', '700',),
+ 'Edu VIC WA NT Hand' => array( '400', '500', '600', '700',),
+ 'Edu VIC WA NT Hand Pre' => array( '400', '500', '600', '700',),
'El Messiri' => array( '400', '500', '600', '700',),
'Electrolize' => array( '400',),
+ 'Elms Sans' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Elsie' => array( '400', '900',),
'Elsie Swash Caps' => array( '400', '900',),
'Emblema One' => array( '400',),
@@ -451,11 +492,14 @@
'Enriqueta' => array( '400', '500', '600', '700',),
'Ephesis' => array( '400',),
'Epilogue' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
+ 'Epunda Sans' => array( '300', '400', '500', '600', '700', '800', '900', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
+ 'Epunda Slab' => array( '300', '400', '500', '600', '700', '800', '900', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Erica One' => array( '400',),
'Esteban' => array( '400',),
'Estonia' => array( '400',),
'Euphoria Script' => array( '400',),
'Ewert' => array( '400',),
+ 'Exile' => array( '400',),
'Exo' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Exo 2' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Expletus Sans' => array( '400', '500', '600', '700', '400italic', '500italic', '600italic', '700italic',),
@@ -547,6 +591,7 @@
'Geostar Fill' => array( '400',),
'Germania One' => array( '400',),
'Gideon Roman' => array( '400',),
+ 'Gidole' => array( '400',),
'Gidugu' => array( '400',),
'Gilda Display' => array( '400',),
'Girassol' => array( '400',),
@@ -561,6 +606,8 @@
'Gochi Hand' => array( '400',),
'Goldman' => array( '400', '700',),
'Golos Text' => array( '400', '500', '600', '700', '800', '900',),
+ 'Google Sans Code' => array( '300', '400', '500', '600', '700', '800', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic',),
+ 'Google Sans Flex' => array( '400',),
'Gorditas' => array( '400', '700',),
'Gothic A1' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Gotu' => array( '400',),
@@ -596,7 +643,7 @@
'Handjet' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Handlee' => array( '400',),
'Hanken Grotesk' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
- 'Hanuman' => array( '100', '300', '400', '700', '900',),
+ 'Hanuman' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Happy Monkey' => array( '400',),
'Harmattan' => array( '400', '500', '600', '700',),
'Headland One' => array( '400',),
@@ -621,6 +668,7 @@
'Host Grotesk' => array( '300', '400', '500', '600', '700', '800', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic',),
'Hubballi' => array( '400',),
'Hubot Sans' => array( '200', '300', '400', '500', '600', '700', '800', '900', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
+ 'Huninn' => array( '400',),
'Hurricane' => array( '400',),
'IBM Plex Mono' => array( '100', '200', '300', '400', '500', '600', '700', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic',),
'IBM Plex Sans' => array( '100', '200', '300', '400', '500', '600', '700', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic',),
@@ -643,6 +691,7 @@
'IM Fell French Canon SC' => array( '400',),
'IM Fell Great Primer' => array( '400', '400italic',),
'IM Fell Great Primer SC' => array( '400',),
+ 'Iansui' => array( '400',),
'Ibarra Real Nova' => array( '400', '500', '600', '700', '400italic', '500italic', '600italic', '700italic',),
'Iceberg' => array( '400',),
'Iceland' => array( '400',),
@@ -661,6 +710,7 @@
'Inspiration' => array( '400',),
'Instrument Sans' => array( '400', '500', '600', '700', '400italic', '500italic', '600italic', '700italic',),
'Instrument Serif' => array( '400', '400italic',),
+ 'Intel One Mono' => array( '300', '400', '500', '600', '700', '300italic', '400italic', '500italic', '600italic', '700italic',),
'Inter' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Inter Tight' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Irish Grover' => array( '400',),
@@ -726,6 +776,8 @@
'Kapakana' => array( '300', '400',),
'Karantina' => array( '300', '400', '700',),
'Karla' => array( '200', '300', '400', '500', '600', '700', '800', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic',),
+ 'Karla Tamil Inclined' => array( '400', '700',),
+ 'Karla Tamil Upright' => array( '400', '700',),
'Karma' => array( '300', '400', '500', '600', '700',),
'Katibeh' => array( '400',),
'Kaushan Script' => array( '400',),
@@ -735,6 +787,7 @@
'Kdam Thmor' => array(),
'Kdam Thmor Pro' => array( '400',),
'Keania One' => array( '400',),
+ 'Kedebideri' => array( '400', '500', '600', '700', '800', '900',),
'Kelly Slab' => array( '400',),
'Kenia' => array( '400',),
'Khand' => array( '300', '400', '500', '600', '700',),
@@ -767,6 +820,7 @@
'Kumar One Outline' => array( '400',),
'Kumbh Sans' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Kurale' => array( '400',),
+ 'LXGW Marker Gothic' => array( '400',),
'LXGW WenKai Mono TC' => array( '300', '400', '700',),
'LXGW WenKai TC' => array( '300', '400', '700',),
'La Belle Aurore' => array( '400',),
@@ -796,6 +850,12 @@
'Lexend Peta' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Lexend Tera' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Lexend Zetta' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Libertinus Keyboard' => array( '400',),
+ 'Libertinus Math' => array( '400',),
+ 'Libertinus Mono' => array( '400',),
+ 'Libertinus Sans' => array( '400', '700', '400italic',),
+ 'Libertinus Serif' => array( '400', '600', '700', '400italic', '600italic', '700italic',),
+ 'Libertinus Serif Display' => array( '400',),
'Libre Barcode 128' => array( '400',),
'Libre Barcode 128 Text' => array( '400',),
'Libre Barcode 39' => array( '400',),
@@ -803,7 +863,7 @@
'Libre Barcode 39 Extended Text' => array( '400',),
'Libre Barcode 39 Text' => array( '400',),
'Libre Barcode EAN13 Text' => array( '400',),
- 'Libre Baskerville' => array( '400', '700', '400italic',),
+ 'Libre Baskerville' => array( '400', '500', '600', '700', '400italic', '500italic', '600italic', '700italic',),
'Libre Bodoni' => array( '400', '500', '600', '700', '400italic', '500italic', '600italic', '700italic',),
'Libre Caslon Display' => array( '400',),
'Libre Caslon Text' => array( '400', '700', '400italic',),
@@ -864,6 +924,7 @@
'Manrope' => array( '200', '300', '400', '500', '600', '700', '800',),
'Mansalva' => array( '400',),
'Manuale' => array( '300', '400', '500', '600', '700', '800', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic',),
+ 'Manufacturing Consent' => array( '400',),
'Marcellus' => array( '400',),
'Marcellus SC' => array( '400',),
'Marck Script' => array( '400',),
@@ -876,6 +937,7 @@
'Martel Sans' => array( '200', '300', '400', '600', '700', '800', '900',),
'Martian Mono' => array( '100', '200', '300', '400', '500', '600', '700', '800',),
'Marvel' => array( '400', '700', '400italic', '700italic',),
+ 'Matangi' => array( '300', '400', '500', '600', '700', '800', '900',),
'Mate' => array( '400', '400italic',),
'Mate SC' => array( '400',),
'Matemasie' => array( '400',),
@@ -897,10 +959,11 @@
'Meera Inimai' => array( '400',),
'Megrim' => array( '400',),
'Meie Script' => array( '400',),
+ 'Menbere' => array( '100', '200', '300', '400', '500', '600', '700',),
'Meow Script' => array( '400',),
'Merienda' => array( '300', '400', '500', '600', '700', '800', '900',),
'Merienda One' => array(),
- 'Merriweather' => array( '300', '400', '700', '900', '300italic', '400italic', '700italic', '900italic',),
+ 'Merriweather' => array( '300', '400', '500', '600', '700', '800', '900', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Merriweather Sans' => array( '300', '400', '500', '600', '700', '800', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic',),
'Metal' => array( '400',),
'Metal Mania' => array( '400',),
@@ -915,7 +978,7 @@
'Mina' => array( '400', '700',),
'Mingzat' => array( '400',),
'Miniver' => array( '400',),
- 'Miriam Libre' => array( '400', '700',),
+ 'Miriam Libre' => array( '400', '500', '600', '700',),
'Mirza' => array( '400', '500', '600', '700',),
'Miss Fajardose' => array( '400',),
'Mitr' => array( '200', '300', '400', '500', '600', '700',),
@@ -929,6 +992,9 @@
'Moirai One' => array( '400',),
'Molengo' => array( '400',),
'Molle' => array( '400italic',),
+ 'Momo Signature' => array( '400',),
+ 'Momo Trust Display' => array( '400',),
+ 'Momo Trust Sans' => array( '200', '300', '400', '500', '600', '700', '800',),
'Mona Sans' => array( '200', '300', '400', '500', '600', '700', '800', '900', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Monda' => array( '400', '500', '600', '700',),
'Monofett' => array( '400',),
@@ -951,6 +1017,8 @@
'Moulpali' => array( '400',),
'Mountains of Christmas' => array( '400', '700',),
'Mouse Memoirs' => array( '400',),
+ 'Mozilla Headline' => array( '200', '300', '400', '500', '600', '700',),
+ 'Mozilla Text' => array( '200', '300', '400', '500', '600', '700',),
'Mr Bedfort' => array( '400',),
'Mr Dafoe' => array( '400',),
'Mr De Haviland' => array( '400',),
@@ -977,6 +1045,7 @@
'Nanum Myeongjo' => array( '400', '700', '800',),
'Nanum Pen Script' => array( '400',),
'Narnoor' => array( '400', '500', '600', '700', '800',),
+ 'Nata Sans' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'National Park' => array( '200', '300', '400', '500', '600', '700', '800',),
'Neonderthaw' => array( '400',),
'Nerko One' => array( '400',),
@@ -991,7 +1060,7 @@
'Niramit' => array( '200', '300', '400', '500', '600', '700', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic',),
'Nixie One' => array( '400',),
'Nobile' => array( '400', '500', '700', '400italic', '500italic', '700italic',),
- 'Nokora' => array( '100', '300', '400', '700', '900',),
+ 'Nokora' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Norican' => array( '400',),
'Nosifer' => array( '400',),
'Notable' => array( '400',),
@@ -1119,7 +1188,8 @@
'Noto Sans Pahawh Hmong' => array( '400',),
'Noto Sans Palmyrene' => array( '400',),
'Noto Sans Pau Cin Hau' => array( '400',),
- 'Noto Sans Phags Pa' => array( '400',),
+ 'Noto Sans Phags Pa' => array(),
+ 'Noto Sans PhagsPa' => array( '400',),
'Noto Sans Phoenician' => array( '400',),
'Noto Sans Psalter Pahlavi' => array( '400',),
'Noto Sans Rejang' => array( '400',),
@@ -1136,11 +1206,13 @@
'Noto Sans Sora Sompeng' => array( '400', '500', '600', '700',),
'Noto Sans Soyombo' => array( '400',),
'Noto Sans Sundanese' => array( '400', '500', '600', '700',),
+ 'Noto Sans Sunuwar' => array( '400',),
'Noto Sans Syloti Nagri' => array( '400',),
'Noto Sans Symbols' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Noto Sans Symbols 2' => array( '400',),
'Noto Sans Syriac' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Noto Sans Syriac Eastern' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Noto Sans Syriac Western' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Noto Sans TC' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Noto Sans Tagalog' => array( '400',),
'Noto Sans Tagbanwa' => array( '400',),
@@ -1204,6 +1276,7 @@
'Noto Serif Telugu' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Noto Serif Thai' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Noto Serif Tibetan' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Noto Serif Todhri' => array( '400',),
'Noto Serif Toto' => array( '400', '500', '600', '700',),
'Noto Serif Vithkuqi' => array( '400', '500', '600', '700',),
'Noto Serif Yezidi' => array( '400', '500', '600', '700',),
@@ -1268,6 +1341,7 @@
'Palette Mosaic' => array( '400',),
'Pangolin' => array( '400',),
'Paprika' => array( '400',),
+ 'Parastoo' => array( '400', '500', '600', '700',),
'Parisienne' => array( '400',),
'Parkinsans' => array( '300', '400', '500', '600', '700', '800',),
'Passero One' => array( '400',),
@@ -1303,57 +1377,112 @@
'Playfair Display' => array( '400', '500', '600', '700', '800', '900', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Playfair Display SC' => array( '400', '700', '900', '400italic', '700italic', '900italic',),
'Playpen Sans' => array( '100', '200', '300', '400', '500', '600', '700', '800',),
+ 'Playpen Sans Arabic' => array( '100', '200', '300', '400', '500', '600', '700', '800',),
+ 'Playpen Sans Deva' => array( '100', '200', '300', '400', '500', '600', '700', '800',),
+ 'Playpen Sans Hebrew' => array( '100', '200', '300', '400', '500', '600', '700', '800',),
+ 'Playpen Sans Thai' => array( '100', '200', '300', '400', '500', '600', '700', '800',),
'Playwrite AR' => array( '100', '200', '300', '400',),
+ 'Playwrite AR Guides' => array( '400',),
'Playwrite AT' => array( '100', '200', '300', '400', '100italic', '200italic', '300italic', '400italic',),
+ 'Playwrite AT Guides' => array( '400', '400italic',),
'Playwrite AU NSW' => array( '100', '200', '300', '400',),
+ 'Playwrite AU NSW Guides' => array( '400',),
'Playwrite AU QLD' => array( '100', '200', '300', '400',),
+ 'Playwrite AU QLD Guides' => array( '400',),
'Playwrite AU SA' => array( '100', '200', '300', '400',),
+ 'Playwrite AU SA Guides' => array( '400',),
'Playwrite AU TAS' => array( '100', '200', '300', '400',),
+ 'Playwrite AU TAS Guides' => array( '400',),
'Playwrite AU VIC' => array( '100', '200', '300', '400',),
+ 'Playwrite AU VIC Guides' => array( '400',),
'Playwrite BE VLG' => array( '100', '200', '300', '400',),
+ 'Playwrite BE VLG Guides' => array( '400',),
'Playwrite BE WAL' => array( '100', '200', '300', '400',),
+ 'Playwrite BE WAL Guides' => array( '400',),
'Playwrite BR' => array( '100', '200', '300', '400',),
+ 'Playwrite BR Guides' => array( '400',),
'Playwrite CA' => array( '100', '200', '300', '400',),
+ 'Playwrite CA Guides' => array( '400',),
'Playwrite CL' => array( '100', '200', '300', '400',),
+ 'Playwrite CL Guides' => array( '400',),
'Playwrite CO' => array( '100', '200', '300', '400',),
+ 'Playwrite CO Guides' => array( '400',),
'Playwrite CU' => array( '100', '200', '300', '400',),
+ 'Playwrite CU Guides' => array( '400',),
'Playwrite CZ' => array( '100', '200', '300', '400',),
+ 'Playwrite CZ Guides' => array( '400',),
'Playwrite DE Grund' => array( '100', '200', '300', '400',),
+ 'Playwrite DE Grund Guides' => array( '400',),
'Playwrite DE LA' => array( '100', '200', '300', '400',),
+ 'Playwrite DE LA Guides' => array( '400',),
'Playwrite DE SAS' => array( '100', '200', '300', '400',),
+ 'Playwrite DE SAS Guides' => array( '400',),
'Playwrite DE VA' => array( '100', '200', '300', '400',),
+ 'Playwrite DE VA Guides' => array( '400',),
'Playwrite DK Loopet' => array( '100', '200', '300', '400',),
+ 'Playwrite DK Loopet Guides' => array( '400',),
'Playwrite DK Uloopet' => array( '100', '200', '300', '400',),
+ 'Playwrite DK Uloopet Guides' => array( '400',),
'Playwrite ES' => array( '100', '200', '300', '400',),
'Playwrite ES Deco' => array( '100', '200', '300', '400',),
+ 'Playwrite ES Deco Guides' => array( '400',),
+ 'Playwrite ES Guides' => array( '400',),
'Playwrite FR Moderne' => array( '100', '200', '300', '400',),
+ 'Playwrite FR Moderne Guides' => array( '400',),
'Playwrite FR Trad' => array( '100', '200', '300', '400',),
+ 'Playwrite FR Trad Guides' => array( '400',),
'Playwrite GB J' => array( '100', '200', '300', '400', '100italic', '200italic', '300italic', '400italic',),
+ 'Playwrite GB J Guides' => array( '400', '400italic',),
'Playwrite GB S' => array( '100', '200', '300', '400', '100italic', '200italic', '300italic', '400italic',),
+ 'Playwrite GB S Guides' => array( '400', '400italic',),
'Playwrite HR' => array( '100', '200', '300', '400',),
+ 'Playwrite HR Guides' => array( '400',),
'Playwrite HR Lijeva' => array( '100', '200', '300', '400',),
+ 'Playwrite HR Lijeva Guides' => array( '400',),
'Playwrite HU' => array( '100', '200', '300', '400',),
+ 'Playwrite HU Guides' => array( '400',),
'Playwrite ID' => array( '100', '200', '300', '400',),
+ 'Playwrite ID Guides' => array( '400',),
'Playwrite IE' => array( '100', '200', '300', '400',),
+ 'Playwrite IE Guides' => array( '400',),
'Playwrite IN' => array( '100', '200', '300', '400',),
+ 'Playwrite IN Guides' => array( '400',),
'Playwrite IS' => array( '100', '200', '300', '400',),
+ 'Playwrite IS Guides' => array( '400',),
'Playwrite IT Moderna' => array( '100', '200', '300', '400',),
+ 'Playwrite IT Moderna Guides' => array( '400',),
'Playwrite IT Trad' => array( '100', '200', '300', '400',),
+ 'Playwrite IT Trad Guides' => array( '400',),
'Playwrite MX' => array( '100', '200', '300', '400',),
+ 'Playwrite MX Guides' => array( '400',),
'Playwrite NG Modern' => array( '100', '200', '300', '400',),
+ 'Playwrite NG Modern Guides' => array( '400',),
'Playwrite NL' => array( '100', '200', '300', '400',),
+ 'Playwrite NL Guides' => array( '400',),
'Playwrite NO' => array( '100', '200', '300', '400',),
+ 'Playwrite NO Guides' => array( '400',),
'Playwrite NZ' => array( '100', '200', '300', '400',),
+ 'Playwrite NZ Guides' => array( '400',),
'Playwrite PE' => array( '100', '200', '300', '400',),
+ 'Playwrite PE Guides' => array( '400',),
'Playwrite PL' => array( '100', '200', '300', '400',),
+ 'Playwrite PL Guides' => array( '400',),
'Playwrite PT' => array( '100', '200', '300', '400',),
+ 'Playwrite PT Guides' => array( '400',),
'Playwrite RO' => array( '100', '200', '300', '400',),
+ 'Playwrite RO Guides' => array( '400',),
'Playwrite SK' => array( '100', '200', '300', '400',),
+ 'Playwrite SK Guides' => array( '400',),
'Playwrite TZ' => array( '100', '200', '300', '400',),
+ 'Playwrite TZ Guides' => array( '400',),
'Playwrite US Modern' => array( '100', '200', '300', '400',),
+ 'Playwrite US Modern Guides' => array( '400',),
'Playwrite US Trad' => array( '100', '200', '300', '400',),
+ 'Playwrite US Trad Guides' => array( '400',),
'Playwrite VN' => array( '100', '200', '300', '400',),
+ 'Playwrite VN Guides' => array( '400',),
'Playwrite ZA' => array( '100', '200', '300', '400',),
+ 'Playwrite ZA Guides' => array( '400',),
'Plus Jakarta Sans' => array( '200', '300', '400', '500', '600', '700', '800', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic',),
'Pochaevsk' => array( '400',),
'Podkova' => array( '400', '500', '600', '700', '800',),
@@ -1364,6 +1493,7 @@
'Poly' => array( '400', '400italic',),
'Pompiere' => array( '400',),
'Ponnala' => array( '400',),
+ 'Ponomar' => array( '400',),
'Pontano Sans' => array( '300', '400', '500', '600', '700',),
'Poor Story' => array( '400',),
'Poppins' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
@@ -1444,7 +1574,7 @@
'Righteous' => array( '400',),
'Risque' => array( '400',),
'Road Rage' => array( '400',),
- 'Roboto' => array( '100', '300', '400', '500', '700', '900', '100italic', '300italic', '400italic', '500italic', '700italic', '900italic',),
+ 'Roboto' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Roboto Condensed' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Roboto Flex' => array( '400',),
'Roboto Mono' => array( '100', '200', '300', '400', '500', '600', '700', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic',),
@@ -1502,7 +1632,8 @@
'Rye' => array( '400',),
'STIX Two Math' => array(),
'STIX Two Text' => array( '400', '500', '600', '700', '400italic', '500italic', '600italic', '700italic',),
- 'SUSE' => array( '100', '200', '300', '400', '500', '600', '700', '800',),
+ 'SUSE' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
+ 'SUSE Mono' => array( '100', '200', '300', '400', '500', '600', '700', '800', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic',),
'Sacramento' => array( '400',),
'Sahitya' => array( '400', '700',),
'Sail' => array( '400',),
@@ -1524,6 +1655,7 @@
'Sarpanch' => array( '400', '500', '600', '700', '800', '900',),
'Sassy Frass' => array( '400',),
'Satisfy' => array( '400',),
+ 'Savate' => array( '200', '300', '400', '500', '600', '700', '800', '900', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Sawarabi Gothic' => array( '400',),
'Sawarabi Mincho' => array( '400',),
'Scada' => array( '400', '700', '400italic', '700italic',),
@@ -1531,6 +1663,7 @@
'Scheherazade New' => array( '400', '500', '600', '700',),
'Schibsted Grotesk' => array( '400', '500', '600', '700', '800', '900', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Schoolbell' => array( '400',),
+ 'Science Gothic' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
'Scope One' => array( '400',),
'Seaweed Script' => array( '400',),
'Secular One' => array( '400',),
@@ -1569,8 +1702,10 @@
'Single Day' => array( '400',),
'Sintony' => array( '400', '700',),
'Sirin Stencil' => array( '400',),
+ 'Sirivennela' => array( '400',),
'Six Caps' => array( '400',),
'Sixtyfour' => array( '400',),
+ 'Sixtyfour Convergence' => array( '400',),
'Skranji' => array( '400', '700',),
'Slabo 13px' => array( '400',),
'Slabo 27px' => array( '400',),
@@ -1607,7 +1742,7 @@
'Space Mono' => array( '400', '700', '400italic', '700italic',),
'Spartan' => array(),
'Special Elite' => array( '400',),
- 'Special Gothic' => array( '400',),
+ 'Special Gothic' => array( '400', '500', '600', '700',),
'Special Gothic Condensed One' => array( '400',),
'Special Gothic Expanded One' => array( '400',),
'Spectral' => array( '200', '300', '400', '500', '600', '700', '800', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic',),
@@ -1624,6 +1759,9 @@
'Sriracha' => array( '400',),
'Srisakdi' => array( '400', '700',),
'Staatliches' => array( '400',),
+ 'Stack Sans Headline' => array( '200', '300', '400', '500', '600', '700',),
+ 'Stack Sans Notch' => array( '200', '300', '400', '500', '600', '700',),
+ 'Stack Sans Text' => array( '200', '300', '400', '500', '600', '700',),
'Stalemate' => array( '400',),
'Stalinist One' => array( '400',),
'Stardos Stencil' => array( '400', '700',),
@@ -1632,6 +1770,7 @@
'Stint Ultra Condensed' => array( '400',),
'Stint Ultra Expanded' => array( '400',),
'Stoke' => array( '300', '400',),
+ 'Story Script' => array( '400',),
'Strait' => array( '400',),
'Style Script' => array( '400',),
'Stylish' => array( '400',),
@@ -1651,6 +1790,8 @@
'Syne' => array( '400', '500', '600', '700', '800',),
'Syne Mono' => array( '400',),
'Syne Tactile' => array( '400',),
+ 'TASA Explorer' => array( '400', '500', '600', '700', '800',),
+ 'TASA Orbiter' => array( '400', '500', '600', '700', '800',),
'Tac One' => array( '400',),
'Tagesschrift' => array( '400',),
'Tai Heritage Pro' => array( '400', '700',),
@@ -1672,6 +1813,7 @@
'The Girl Next Door' => array( '400',),
'The Nautigal' => array( '400', '700',),
'Tienne' => array( '400', '700', '900',),
+ 'TikTok Sans' => array( '300', '400', '500', '600', '700', '800', '900',),
'Tillana' => array( '400', '500', '600', '700', '800',),
'Tilt Neon' => array( '400',),
'Tilt Prism' => array( '400',),
@@ -1687,6 +1829,7 @@
'Tiro Kannada' => array( '400', '400italic',),
'Tiro Tamil' => array( '400', '400italic',),
'Tiro Telugu' => array( '400', '400italic',),
+ 'Tirra' => array( '400', '500', '600', '700', '800', '900',),
'Titan One' => array( '400',),
'Titillium Web' => array( '200', '300', '400', '600', '700', '900', '200italic', '300italic', '400italic', '600italic', '700italic',),
'Tomorrow' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
@@ -1721,6 +1864,7 @@
'Unkempt' => array( '400', '700',),
'Unlock' => array( '400',),
'Unna' => array( '400', '700', '400italic', '700italic',),
+ 'UoqMunThenKhung' => array( '400',),
'Updock' => array( '400',),
'Urbanist' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900', '100italic', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'VT323' => array( '400',),
@@ -1730,6 +1874,7 @@
'Varta' => array( '300', '400', '500', '600', '700',),
'Vast Shadow' => array( '400',),
'Vazirmatn' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900',),
+ 'Vend Sans' => array( '300', '400', '500', '600', '700', '300italic', '400italic', '500italic', '600italic', '700italic',),
'Vesper Libre' => array( '400', '500', '700', '900',),
'Viaoda Libre' => array( '400',),
'Vibes' => array( '400',),
@@ -1744,6 +1889,8 @@
'Vollkorn SC' => array( '400', '600', '700', '900',),
'Voltaire' => array( '400',),
'Vujahday Script' => array( '400',),
+ 'WDXL Lubrifont JP N' => array( '400',),
+ 'WDXL Lubrifont SC' => array( '400',),
'WDXL Lubrifont TC' => array( '400',),
'Waiting for the Sunrise' => array( '400',),
'Wallpoet' => array( '400',),
@@ -1794,6 +1941,9 @@
'ZCOOL QingKe HuangYou' => array( '400',),
'ZCOOL XiaoWei' => array( '400',),
'Zain' => array( '200', '300', '400', '700', '800', '900', '300italic', '400italic',),
+ 'Zalando Sans' => array( '200', '300', '400', '500', '600', '700', '800', '900', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
+ 'Zalando Sans Expanded' => array( '200', '300', '400', '500', '600', '700', '800', '900', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
+ 'Zalando Sans SemiExpanded' => array( '200', '300', '400', '500', '600', '700', '800', '900', '200italic', '300italic', '400italic', '500italic', '600italic', '700italic', '800italic', '900italic',),
'Zen Antique' => array( '400',),
'Zen Antique Soft' => array( '400',),
'Zen Dots' => array( '400',),
diff --git a/header-footer-grid/Core/Builder/Footer.php b/header-footer-grid/Core/Builder/Footer.php
index d66f8daa67..d36c6e8522 100644
--- a/header-footer-grid/Core/Builder/Footer.php
+++ b/header-footer-grid/Core/Builder/Footer.php
@@ -318,6 +318,18 @@ protected function get_upsell_components() {
'icon' => 'images-alt',
'name' => __( 'Payment icons', 'neve' ),
],
+ [
+ 'icon' => 'nametag',
+ 'name' => __( 'Copyright', 'neve' ),
+ ],
+ [
+ 'icon' => 'embed-generic',
+ 'name' => __( 'Custom Layouts', 'neve' ),
+ ],
+ [
+ 'icon' => 'welcome-widgets-menus',
+ 'name' => __( 'Widget Area', 'neve' ),
+ ],
[
'icon' => 'share',
'name' => __( 'Social Icons', 'neve' ),
diff --git a/header-footer-grid/Core/Builder/Header.php b/header-footer-grid/Core/Builder/Header.php
index cfd1a21a0c..975c271ffe 100644
--- a/header-footer-grid/Core/Builder/Header.php
+++ b/header-footer-grid/Core/Builder/Header.php
@@ -548,6 +548,14 @@ protected function get_upsell_components() {
}
return [
+ [
+ 'icon' => 'code-standards',
+ 'name' => __( 'Search Form', 'neve' ),
+ ],
+ [
+ 'icon' => 'email',
+ 'name' => __( 'Contact', 'neve' ),
+ ],
[
'icon' => 'welcome-write-blog',
'name' => __( 'HTML', 'neve' ) . ' 2',
@@ -556,10 +564,26 @@ protected function get_upsell_components() {
'icon' => 'embed-generic',
'name' => __( 'Custom Layouts', 'neve' ),
],
+ [
+ 'icon' => 'minus',
+ 'name' => __( 'Divider element', 'neve' ),
+ ],
[
'icon' => 'share',
'name' => __( 'Social Icons', 'neve' ),
],
+ [
+ 'icon' => 'menu',
+ 'name' => __( 'Primary Menu', 'neve' ),
+ ],
+ [
+ 'icon' => 'admin-links',
+ 'name' => __( 'Button', 'neve' ),
+ ],
+ [
+ 'icon' => 'welcome-widgets-menus',
+ 'name' => __( 'Widget Area', 'neve' ),
+ ],
];
}
diff --git a/inc/admin/dashboard/main.php b/inc/admin/dashboard/main.php
index 2924fc5e30..03b0be0de7 100755
--- a/inc/admin/dashboard/main.php
+++ b/inc/admin/dashboard/main.php
@@ -364,6 +364,14 @@ private function get_localization() {
'canActivatePlugins' => current_user_can( 'activate_plugins' ),
'rootUrl' => get_site_url(),
'sparksActive' => defined( 'SPARKS_WC_VERSION' ) ? 'yes' : 'no',
+ 'api' => esc_url( rest_url( '/nv/v1/dashboard/' ) ),
+ 'availableModules' => $this->get_available_modules(),
+ 'orbitFox' => array(
+ 'isInstalled' => file_exists( WP_PLUGIN_DIR . '/themeisle-companion/themeisle-companion.php' ),
+ 'isActive' => class_exists( 'Orbit_Fox' ),
+ 'activationUrl' => $this->plugin_helper->get_plugin_action_link( 'themeisle-companion' ),
+ 'data' => class_exists( 'Orbit_Fox' ) ? get_option( 'obfx_data' ) : array(),
+ ),
];
if ( defined( 'NEVE_PRO_PATH' ) ) {
@@ -394,6 +402,44 @@ private function get_localization() {
$lang_code = isset( $available_languages[ $language ] ) ? 'de' : 'en';
$data['lang'] = $lang_code;
+ // Launch Progress checks
+ $launch_progress_data = $this->get_launch_progress_checks();
+ $data['showLaunchProgress'] = $launch_progress_data['showLaunchProgress'];
+ $data['launchProgress'] = [
+ 'autoDetected' => $launch_progress_data['autoDetected'],
+ 'savedProgress' => $launch_progress_data['savedProgress'],
+ ];
+
+ $screen = get_current_screen();
+ if ( ! isset( $screen->id ) ) {
+ return $data;
+ }
+
+ $theme = $this->theme_args;
+ $theme_page = ! empty( $theme['template'] ) ? $theme['template'] . '-welcome' : $theme['slug'] . '-welcome';
+
+ // Check if front page exists
+ $page_on_front = get_option( 'page_on_front' );
+ $homepage_url = $page_on_front ? admin_url( 'post.php?post=' . $page_on_front . '&action=edit' ) : admin_url( 'edit.php?post_type=page' );
+
+ // Launch Progress step URLs
+ $data['launchProgressUrls'] = [
+ 'upgradeURL' => apply_filters( 'neve_upgrade_link_from_child_theme_filter', tsdk_translate_link( tsdk_utmify( 'https://themeisle.com/themes/neve/upgrade/', 'getpronow', 'launchprogress' ) ) ),
+ 'starterSites' => admin_url( 'admin.php?page=' . $theme_page . '#starter-sites' ),
+ 'siteIdentity' => add_query_arg( [ 'autofocus[section]' => 'title_tagline' ], admin_url( 'customize.php' ) ),
+ 'logo' => add_query_arg( [ 'autofocus[control]' => 'custom_logo' ], admin_url( 'customize.php' ) ),
+ 'colors' => add_query_arg( [ 'autofocus[section]' => 'neve_colors_background_section' ], admin_url( 'customize.php' ) ),
+ 'favicon' => add_query_arg( [ 'autofocus[control]' => 'site_icon' ], admin_url( 'customize.php' ) ),
+ 'homepage' => $homepage_url,
+ 'pages' => admin_url( 'edit.php?post_type=page' ),
+ 'menus' => admin_url( 'nav-menus.php' ),
+ 'footer' => add_query_arg( [ 'autofocus[panel]' => 'hfg_footer' ], admin_url( 'customize.php' ) ),
+ 'permalinks' => admin_url( 'options-permalink.php' ),
+ 'plugins' => admin_url( 'plugin-install.php?s=seo&tab=search' ),
+ 'speedTest' => 'https://pagespeed.web.dev/analysis?url=' . urlencode( get_site_url() ),
+ 'privacyPolicy' => admin_url( 'options-privacy.php' ),
+ ];
+
return $data;
}
@@ -534,6 +580,66 @@ private function get_customizer_shortcuts() {
];
}
+ /**
+ * Get launch progress checks.
+ *
+ * @return array{
+ * showLaunchProgress: bool,
+ * autoDetected: array{
+ * hasLogo: bool,
+ * hasFavicon: bool,
+ * hasCustomPermalink: bool,
+ * hasSeoPlugin: bool,
+ * hasPrivacyPage: bool
+ * },
+ * savedProgress: array>
+ * }
+ */
+ private function get_launch_progress_checks() {
+ // Check if we should show the Launch Progress tab
+ $show_launch_progress = get_option( \Neve\Core\Admin::$launch_progress_option );
+ if ( false === $show_launch_progress ) {
+ $install_time = get_option( 'neve_install' );
+ if ( ! empty( $install_time ) ) {
+ $one_week_ago = time() - WEEK_IN_SECONDS;
+ $show_launch_progress = ( intval( $install_time ) > $one_week_ago ) ? 'yes' : 'no';
+ update_option( \Neve\Core\Admin::$launch_progress_option, $show_launch_progress, false );
+ } else {
+ $show_launch_progress = 'no';
+ }
+ }
+
+ $has_logo = (bool) get_theme_mod( 'custom_logo' );
+ $has_favicon = (bool) get_site_icon_url();
+ $permalink_structure = get_option( 'permalink_structure' );
+
+ // Check if SEO plugin is active (Yoast, RankMath, or AIOSEO)
+ $has_seo_plugin = (
+ class_exists( 'WPSEO_Options' ) || // Yoast SEO
+ class_exists( 'RankMath' ) || // RankMath
+ function_exists( 'aioseo' ) // All in One SEO
+ );
+
+ // Check if privacy policy page exists
+ $privacy_page_id = (int) get_option( 'wp_page_for_privacy_policy' );
+ $has_privacy_page = $privacy_page_id > 0 && get_post_status( $privacy_page_id ) === 'publish';
+
+ // Get saved progress from option
+ $saved_progress = get_option( 'neve_launch_progress', [] );
+
+ return [
+ 'showLaunchProgress' => ( $show_launch_progress === 'yes' ),
+ 'autoDetected' => [
+ 'hasLogo' => $has_logo,
+ 'hasFavicon' => $has_favicon,
+ 'hasCustomPermalink' => ! empty( $permalink_structure ),
+ 'hasSeoPlugin' => $has_seo_plugin,
+ 'hasPrivacyPage' => $has_privacy_page,
+ ],
+ 'savedProgress' => $saved_progress,
+ ];
+ }
+
/**
* Get doc link.
*
@@ -645,8 +751,11 @@ private function get_free_pro_features() {
private function get_modules() {
$plugins = array(
'hfg_module' => array(
- 'nicename' => __( 'Header Booster', 'neve' ),
- 'description' => __( 'Create unique sticky & transparent headers that adapt to scroll. Perfect for modern, immersive websites.', 'neve' ),
+ 'nicename' => __( 'Header Booster', 'neve' ),
+ 'description' => __( 'Create unique sticky & transparent headers that adapt to scroll. Perfect for modern, immersive websites.', 'neve' ),
+ 'documentation' => array(
+ 'url' => 'https://docs.themeisle.com/article/1057-header-booster-documentation?utm_source=wpadmin&utm_medium=welcomepage&utm_campaign=headerbooster&utm_content=neve',
+ ),
),
'woocommerce_booster' => array(
'nicename' => __( 'WooCommerce Booster', 'neve' ),
@@ -660,32 +769,46 @@ private function get_modules() {
'condition' => class_exists( 'Easy_Digital_Downloads' ),
),
'blog_pro' => array(
- 'nicename' => __( 'Blog Booster', 'neve' ),
- 'description' => __( 'Advanced layouts, reading time estimates, and social sharing to keep readers engaged longer.', 'neve' ),
+ 'nicename' => __( 'Blog Booster', 'neve' ),
+ 'description' => __( 'Advanced layouts, reading time estimates, and social sharing to keep readers engaged longer.', 'neve' ),
+ 'documentation' => array(
+ 'url' => 'https://docs.themeisle.com/article/1059-blog-booster-documentation?utm_source=wpadmin&utm_medium=welcomepage&utm_campaign=blogbooster&utm_content=neve',
+ ),
),
'post_type_enhancements' => array(
- 'nicename' => __( 'Post types enhancements', 'neve' ),
- 'description' => __( 'Extend Neve\'s powerful features to custom post types. Create unique layouts for portfolios, testimonials, and more.', 'neve' ),
- ),
- 'scroll_to_top' => array(
- 'nicename' => __( 'Scroll To Top', 'neve' ),
- 'description' => __( 'Add a customizable scroll-to-top button that appears exactly when needed. Style it to match your brand.', 'neve' ),
+ 'nicename' => __( 'Post types enhancements', 'neve' ),
+ 'description' => __( 'Extend Neve\'s powerful features to custom post types. Create unique layouts for portfolios, testimonials, and more.', 'neve' ),
+ 'documentation' => array(
+ 'url' => 'https://docs.themeisle.com/article/1505-neve-post-type-enhancements-module?utm_source=wpadmin&utm_medium=welcomepage&utm_campaign=postenhancements&utm_content=neve',
+ ),
),
'performance_features' => array(
- 'nicename' => __( 'Performance', 'neve' ),
- 'description' => __( 'Optimize core vitals, enable lazy loading, and minify resources for lightning-fast load times.', 'neve' ),
+ 'nicename' => __( 'Performance', 'neve' ),
+ 'description' => __( 'Optimize core vitals, enable lazy loading, and minify resources for lightning-fast load times.', 'neve' ),
+ 'documentation' => array(
+ 'url' => 'https://docs.themeisle.com/article/1366-performance-module-documentation?utm_source=wpadmin&utm_medium=welcomepage&utm_campaign=performancemodule&utm_content=neve',
+ ),
),
'block_editor_booster' => array(
- 'nicename' => __( 'Block Editor Booster', 'neve' ),
- 'description' => __( 'Advanced Gutenberg blocks designed specifically for Neve. Build faster with pre-styled patterns.', 'neve' ),
+ 'nicename' => __( 'Block Editor Booster', 'neve' ),
+ 'description' => __( 'Advanced Gutenberg blocks designed specifically for Neve. Build faster with pre-styled patterns.', 'neve' ),
+ 'documentation' => array(
+ 'url' => 'https://docs.themeisle.com/article/1473-neve-block-editor-booster-module?utm_source=wpadmin&utm_medium=welcomepage&utm_campaign=blockeditorbooster&utm_content=neve',
+ ),
),
'white_label' => array(
- 'nicename' => __( 'White Label', 'neve' ),
- 'description' => __( 'Rebrand Neve as your own. Change theme name, author, and links to match your agency identity.', 'neve' ),
+ 'nicename' => __( 'White Label', 'neve' ),
+ 'description' => __( 'Rebrand Neve as your own. Change theme name, author, and links to match your agency identity.', 'neve' ),
+ 'documentation' => array(
+ 'url' => 'https://docs.themeisle.com/article/1061-white-label-module-documentation?utm_source=wpadmin&utm_medium=welcomepage&utm_campaign=whitelabel&utm_content=neve',
+ ),
),
'custom_layouts' => array(
- 'nicename' => __( 'Custom Layouts', 'neve' ),
- 'description' => __( 'Create conditional headers, footers, and content blocks. Perfect for custom landing pages and marketing campaigns.', 'neve' ),
+ 'nicename' => __( 'Custom Layouts', 'neve' ),
+ 'description' => __( 'Create conditional headers, footers, and content blocks. Perfect for custom landing pages and marketing campaigns.', 'neve' ),
+ 'documentation' => array(
+ 'url' => 'https://docs.themeisle.com/article/1062-custom-layouts-module?utm_source=wpadmin&utm_medium=welcomepage&utm_campaign=customlayouts&utm_content=neve',
+ ),
),
'elementor_booster' => array(
'nicename' => __( 'Elementor Booster', 'neve' ),
@@ -698,16 +821,32 @@ private function get_modules() {
'condition' => class_exists( 'LifterLMS' ),
),
'typekit_fonts' => array(
- 'nicename' => __( 'Typekit Fonts', 'neve' ),
- 'description' => __( 'Access premium Adobe fonts directly in your theme. Add professional typography to any element.', 'neve' ),
+ 'nicename' => __( 'Typekit Fonts', 'neve' ),
+ 'description' => __( 'Access premium Adobe fonts directly in your theme. Add professional typography to any element.', 'neve' ),
+ 'documentation' => array(
+ 'url' => 'https://docs.themeisle.com/article/1085-typekit-fonts-documentation?utm_source=wpadmin&utm_medium=welcomepage&utm_campaign=typekitfonts&utm_content=neve',
+ ),
),
'custom_sidebars' => array(
- 'nicename' => __( 'Custom Sidebars', 'neve' ),
- 'description' => __( 'Create unique sidebar layouts for different sections. Show relevant content based on user context.', 'neve' ),
+ 'nicename' => __( 'Custom Sidebars', 'neve' ),
+ 'description' => __( 'Create unique sidebar layouts for different sections. Show relevant content based on user context.', 'neve' ),
+ 'documentation' => array(
+ 'url' => 'https://docs.themeisle.com/article/1770-custom-sidebars-module-documentation?utm_source=wpadmin&utm_medium=welcomepage&utm_campaign=customsidebars&utm_content=neve',
+ ),
),
'access_restriction' => array(
- 'nicename' => __( 'Content restriction', 'neve' ),
- 'description' => __( 'Create members-only content areas. Control access by user roles, logged-in status, or custom rules.', 'neve' ),
+ 'nicename' => __( 'Content restriction', 'neve' ),
+ 'description' => __( 'Create members-only content areas. Control access by user roles, logged-in status, or custom rules.', 'neve' ),
+ 'documentation' => array(
+ 'url' => 'https://docs.themeisle.com/article/1863-content-restriction-module-documentation?utm_source=wpadmin&utm_medium=welcomepage&utm_campaign=contentrestriction&utm_content=neve',
+ ),
+ ),
+ 'dashboard_customizer' => array(
+ 'nicename' => __( 'WP Dashboard Customizer', 'neve' ),
+ 'description' => __( 'Create or modify the WordPress dashboard. Customize the admin pages, admin menu, and admin bar.', 'neve' ),
+ 'documentation' => array(
+ 'url' => 'https://docs.themeisle.com/article/2408-wp-dashboard-customizer-module-documentation?utm_source=wpadmin&utm_medium=welcomepage&utm_campaign=dashboardcustomizer&utm_content=neve',
+ ),
),
);
@@ -824,6 +963,34 @@ private function get_external_plugins_data() {
return $plugins;
}
+ /**
+ * Get available modules.
+ *
+ * @return array
+ */
+ private function get_available_modules() {
+ $modules = array(
+ 'login-customizer' => array(
+ 'title' => __( 'Login Customizer', 'neve' ),
+ 'description' => __( 'Customize your WordPress login page with branding and styling options.', 'neve' ),
+ ),
+ 'custom-fonts' => array(
+ 'title' => __( 'Custom Fonts/Scripts', 'neve' ),
+ 'description' => __( 'Add custom fonts and scripts to your website easily.', 'neve' ),
+ ),
+ 'policy-notice' => array(
+ 'title' => __( 'Cookie Notice', 'neve' ),
+ 'description' => __( 'Display a customizable cookie consent notice for GDPR compliance.', 'neve' ),
+ ),
+ 'post-duplicator' => array(
+ 'title' => __( 'Duplicate Page', 'neve' ),
+ 'description' => __( 'Quickly duplicate posts, pages, and custom post types.', 'neve' ),
+ ),
+ );
+
+ return apply_filters( 'neve_available_modules', $modules );
+ }
+
/**
* Renders the custom layout header section in the admin dashboard for Custom Layouts
*
diff --git a/inc/compatibility/elementor.php b/inc/compatibility/elementor.php
index b258907c83..d2dca99088 100644
--- a/inc/compatibility/elementor.php
+++ b/inc/compatibility/elementor.php
@@ -80,6 +80,7 @@ public function init() {
*/
add_filter( 'neve_pro_run_wc_view', array( $this, 'suspend_woo_customizations' ), 10, 2 );
add_action( 'elementor/theme/register_locations', array( $this, 'register_elementor_locations' ) );
+ add_filter( 'elementor/document/config', array( $this, 'elementor_document_config' ), 10, 2 );
}
/**
@@ -567,4 +568,26 @@ public function suspend_woo_customizations( $should_load, $class_name ) {
return ! $elementor_overrides;
}
+
+ /**
+ * Allow Post Content widget to be shown in the panel for neve_custom_layouts post type.
+ *
+ * @param array $data The original data that needs to be saved.
+ * @param int $post_id The ID of the post for which the data is being saved.
+ *
+ * @return array The modified data with the additional configuration.
+ */
+ public function elementor_document_config( $data, $post_id ) {
+ if ( 'neve_custom_layouts' === get_post_type( $post_id ) ) {
+ $data['panel'] = array(
+ 'widgets_settings' => array(
+ 'theme-post-content' => array(
+ 'show_in_panel' => true,
+ ),
+ ),
+ );
+ }
+
+ return $data;
+ }
}
diff --git a/inc/core/admin.php b/inc/core/admin.php
index dca7974ea0..27e97823a1 100644
--- a/inc/core/admin.php
+++ b/inc/core/admin.php
@@ -22,6 +22,13 @@
class Admin {
use Theme_Info;
+ /**
+ * Launch progress option key.
+ *
+ * @var string
+ */
+ public static $launch_progress_option = 'neve_show_launch_progress';
+
/**
* Dismiss notice key.
*
@@ -303,6 +310,67 @@ public function register_rest_routes() {
],
]
);
+
+ register_rest_route(
+ 'nv/v1/dashboard',
+ '/activate-plugin',
+ array(
+ 'methods' => \WP_REST_Server::EDITABLE,
+ 'callback' => [ $this, 'activate_plugin' ],
+ 'permission_callback' => function() {
+ return ( current_user_can( 'install_plugins' ) && current_user_can( 'activate_plugins' ) );
+ },
+ 'args' => array(
+ 'slug' => array(
+ 'required' => true,
+ 'sanitize_callback' => 'sanitize_key',
+ ),
+ ),
+ )
+ );
+
+ register_rest_route(
+ 'nv/v1/dashboard',
+ '/launch-progress',
+ [
+ 'methods' => \WP_REST_Server::EDITABLE,
+ 'callback' => [ $this, 'save_launch_progress' ],
+ 'permission_callback' => function() {
+ return current_user_can( 'manage_options' );
+ },
+ 'args' => array(
+ 'progress' => array(
+ 'required' => true,
+ 'sanitize_callback' => function( $value ) {
+ return is_array( $value ) ? $value : [];
+ },
+ ),
+ ),
+ ]
+ );
+
+ register_rest_route(
+ 'nv/v1/dashboard',
+ '/activate-module',
+ [
+ 'methods' => \WP_REST_Server::EDITABLE,
+ 'callback' => [ $this, 'activate_module' ],
+ 'permission_callback' => function() {
+ return current_user_can( 'manage_options' );
+ },
+ 'args' => array(
+ 'slug' => array(
+ 'required' => true,
+ 'sanitize_callback' => 'sanitize_key',
+ ),
+ 'value' => array(
+ 'required' => true,
+ 'sanitize_callback' => 'rest_sanitize_boolean',
+ 'validate_callback' => 'rest_validate_request_arg',
+ ),
+ ),
+ ]
+ );
}
/**
@@ -324,6 +392,168 @@ public function get_plugin_state( \WP_REST_Request $request ) {
);
}
+ /**
+ * Activate a plugin via REST API.
+ *
+ * @param \WP_REST_Request> $request Request details.
+ *
+ * @return void
+ */
+ public function activate_plugin( $request ) {
+ $slug = $request->get_param( 'slug' );
+
+ if ( empty( $slug ) ) {
+ return;
+ }
+
+ $plugin_helper = new Plugin_Helper();
+ $path = $plugin_helper->get_plugin_path( $slug );
+
+ if ( ! file_exists( WP_PLUGIN_DIR . '/' . $path ) ) {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+ require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
+
+ /** @var object|\WP_Error $api */
+ $api = plugins_api(
+ 'plugin_information',
+ array(
+
+ 'slug' => $slug,
+ 'fields' => array( 'sections' => false ),
+ )
+ );
+
+ if ( is_wp_error( $api ) ) {
+ wp_send_json_error( array( 'message' => $api->get_error_message() ) );
+ }
+
+ if ( ! isset( $api->download_link ) ) {
+ wp_send_json_error( array( 'message' => __( 'Invalid action', 'neve' ) ) );
+ }
+
+ $skin = new \WP_Ajax_Upgrader_Skin();
+ $upgrader = new \Plugin_Upgrader( $skin );
+ $result = $upgrader->install( $api->download_link );
+
+ if ( is_wp_error( $result ) ) {
+ wp_send_json_error( array( 'message' => $result->get_error_message() ) );
+ }
+
+ if ( $skin->get_errors()->has_errors() && is_wp_error( $skin->result ) ) {
+ if ( 'folder_exists' !== $skin->result->get_error_code() ) {
+ wp_send_json_error( array( 'message' => $skin->result->get_error_message() ) );
+ }
+ }
+
+ if ( ! $result ) {
+ global $wp_filesystem;
+
+ $status = [
+ 'message' => __( 'Invalid action', 'neve' ),
+ ];
+
+ if ( $wp_filesystem instanceof \WP_Filesystem_Base && $wp_filesystem->errors->has_errors() ) {
+ $status['message'] = esc_html( $wp_filesystem->errors->get_error_message() );
+ }
+
+ wp_send_json_error( $status );
+ }
+ }
+
+ $result = activate_plugin( $path );
+
+ if ( is_wp_error( $result ) ) {
+ wp_send_json_error( array( 'message' => $result->get_error_message() ) );
+ }
+
+ wp_send_json_success( array( 'message' => __( 'Module Activated', 'neve' ) ) );
+ }
+
+ /**
+ * Activate Orbit Fox module.
+ *
+ * @param \WP_REST_Request> $request Request details.
+ * @return void
+ */
+ public function activate_module( $request ) {
+ $module_slug = $request->get_param( 'slug' );
+ $module_value = $request->get_param( 'value' );
+
+ if ( ! class_exists( 'Orbit_Fox_Global_Settings' ) ) {
+ wp_send_json_error( __( 'Invalid action', 'neve' ) );
+ }
+
+ $settings = new \Orbit_Fox_Global_Settings();
+ $modules = $settings::$instance->module_objects;
+
+ if ( ! isset( $modules[ $module_slug ] ) ) {
+ wp_send_json_error( __( 'Invalid action', 'neve' ) );
+ }
+
+ $response = $modules[ $module_slug ]->set_status( 'active', $module_value );
+
+ wp_send_json_success( $module_value ? __( 'Module Activated', 'neve' ) : __( 'Module Deactivated.', 'neve' ) );
+ }
+
+ /**
+ * Save launch progress state.
+ *
+ * @param \WP_REST_Request> $request The request object.
+ * @return \WP_REST_Response
+ */
+ public function save_launch_progress( $request ) {
+ $progress = $request->get_param( 'progress' );
+
+ if ( ! is_array( $progress ) ) {
+ return new \WP_REST_Response(
+ [
+ 'success' => false,
+ 'message' => 'Invalid progress data',
+ ],
+ 400
+ );
+ }
+
+ // Validate progress structure
+ $valid_keys = [ 'identity', 'content', 'performance' ];
+ foreach ( $valid_keys as $key ) {
+ if ( ! isset( $progress[ $key ] ) || ! is_array( $progress[ $key ] ) ) {
+ return new \WP_REST_Response(
+ [
+ 'success' => false,
+ 'message' => 'Invalid progress structure',
+ ],
+ 400
+ );
+ }
+
+ // Validate all values are booleans
+ foreach ( $progress[ $key ] as $value ) {
+ if ( ! is_bool( $value ) ) {
+ return new \WP_REST_Response(
+ [
+ 'success' => false,
+ 'message' => 'Progress values must be boolean',
+ ],
+ 400
+ );
+ }
+ }
+ }
+
+ // Save to option
+ update_option( 'neve_launch_progress', $progress, false );
+
+ return new \WP_REST_Response(
+ [
+ 'success' => true,
+ 'message' => 'Progress saved',
+ ],
+ 200
+ );
+ }
+
/**
* Drop `Background` submenu item.
*/
diff --git a/inc/core/core_loader.php b/inc/core/core_loader.php
index 3279533399..462bcceab9 100644
--- a/inc/core/core_loader.php
+++ b/inc/core/core_loader.php
@@ -97,6 +97,8 @@ private function define_modules() {
'Views\Content_None',
'Views\Content_404',
'Views\Breadcrumbs',
+ 'Views\Style_Book',
+ 'Views\Scroll_To_Top',
'Views\Layouts\Layout_Container',
'Views\Layouts\Layout_Sidebar',
diff --git a/inc/core/front_end.php b/inc/core/front_end.php
index aac92b9835..05232dc1d2 100644
--- a/inc/core/front_end.php
+++ b/inc/core/front_end.php
@@ -16,6 +16,7 @@
use Neve\Core\Settings\Mods;
use Neve\Core\Dynamic_Css;
use Neve\Core\Traits\Theme_Mods;
+use Neve\Customizer\Options\Scroll_To_Top;
/**
* Front end handler class.
@@ -86,6 +87,8 @@ public function setup_theme() {
add_filter( 'theme_mod_background_color', '__return_empty_string' );
$this->add_woo_support();
add_filter( 'neve_dynamic_style_output', array( $this, 'css_global_custom_colors' ), PHP_INT_MAX, 2 );
+
+ add_filter( 'neve_dynamic_style_output', array( $this, 'css_scroll_to_top' ), 99, 2 );
}
/**
@@ -462,9 +465,16 @@ private function add_scripts() {
}
if ( class_exists( 'WooCommerce', false ) && is_woocommerce() ) {
- wp_register_script( 'neve-shop-script', NEVE_ASSETS_URL . 'js/build/modern/shop.js', array(), NEVE_VERSION, true );
+ wp_register_script( 'neve-shop-script', NEVE_ASSETS_URL . 'js/build/modern/shop.js', array( 'jquery', 'wc-single-product' ), NEVE_VERSION, true );
wp_enqueue_script( 'neve-shop-script' );
wp_script_add_data( 'neve-shop-script', 'async', true );
+ wp_localize_script(
+ 'neve-shop-script',
+ 'neveShopSlider',
+ array(
+ 'isSparkActive' => is_plugin_active( 'sparks-for-woocommerce/sparks-for-woocommerce.php' ),
+ )
+ );
}
if ( $this->should_load_comments_reply() ) {
@@ -587,6 +597,9 @@ public function get_strings() {
'switch_skin' => __( 'Switch Skin', 'neve' ),
'dismiss' => __( 'Dismiss', 'neve' ),
'rollback' => __( 'Roll Back', 'neve' ),
+ 'scroll_to_top_desc' => __( 'Add a customizable scroll-to-top button that appears exactly when needed. Style it to match your brand.', 'neve' ),
+ /* translators: %s - Module name for the upsell */
+ 'upsell' => __( 'Unlock %s with the Pro version.', 'neve' ),
];
}
@@ -610,6 +623,89 @@ public function css_global_custom_colors( $current_styles, $context ) {
return $current_styles;
}
+ /**
+ * Add module css.
+ *
+ * @param string $css Current CSS style.
+ * @param string $context Current context.
+ *
+ * @return string Altered CSS.
+ */
+ public function css_scroll_to_top( $css, $context = 'frontend' ) {
+ if ( ! Scroll_To_Top::is_enabled() ) {
+ return $css;
+ }
+
+ if ( $context !== 'frontend' ) {
+ return $css;
+ }
+
+ $scroll_to_top_css = '.scroll-to-top {' . ( is_rtl() ? 'left: 20px;' : 'right: 20px;' ) . '
+ border: none;
+ position: fixed;
+ bottom: 30px;
+ display: none;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
+ align-items: center;
+ justify-content: center;
+ z-index: 999;
+ }
+ @supports (-webkit-overflow-scrolling: touch) {
+ .scroll-to-top {
+ bottom: 74px;
+ }
+ }
+ .scroll-to-top.image {
+ background-position: center;
+ }
+ .scroll-to-top .scroll-to-top-image {
+ width: 100%;
+ height: 100%;
+ }
+ .scroll-to-top .scroll-to-top-label {
+ margin: 0;
+ padding: 5px;
+ }
+ .scroll-to-top:hover {
+ text-decoration: none;
+ }
+ .scroll-to-top.scroll-to-top-left {' . ( is_rtl() ? 'right: 20px; left: unset;' : 'left: 20px; right: unset;' ) . '}
+ .scroll-to-top.scroll-show-mobile {
+ display: flex;
+ }
+ @media (min-width: 960px) {
+ .scroll-to-top {
+ display: flex;
+ }
+ }';
+
+ $scroll_to_top_css .= '.scroll-to-top {
+ color: var(--color);
+ padding: var(--padding);
+ border-radius: var(--borderradius);
+ background: var(--bgcolor);
+ }
+
+ .scroll-to-top:hover, .scroll-to-top:focus {
+ color: var(--hovercolor);
+ background: var(--hoverbgcolor);
+ }
+
+ .scroll-to-top-icon, .scroll-to-top.image .scroll-to-top-image {
+ width: var(--size);
+ height: var(--size);
+ }
+
+ .scroll-to-top-image {
+ background-image: var(--bgimage);
+ background-size: cover;
+ }';
+
+ return $css . $scroll_to_top_css;
+ }
+
/**
* Fix script translations language directory.
*
diff --git a/inc/core/settings/config.php b/inc/core/settings/config.php
index b3b9b5db6a..ab5ba5369a 100644
--- a/inc/core/settings/config.php
+++ b/inc/core/settings/config.php
@@ -94,6 +94,7 @@ class Config {
const MODS_POST_TYPE_VSPACING = 'content_vspacing';
const OPTION_LOCAL_GOOGLE_FONTS_HOSTING = 'nv_pro_enable_local_fonts';
+ const MODS_PRELOAD_FONTS = 'neve_preload_fonts';
const OPTION_POSTS_PER_PAGE = 'posts_per_page';
const MODS_TPOGRAPHY_FONT_PAIRS = 'neve_font_pairs';
diff --git a/inc/core/styles/frontend.php b/inc/core/styles/frontend.php
index 84a56509dd..f355571d94 100644
--- a/inc/core/styles/frontend.php
+++ b/inc/core/styles/frontend.php
@@ -9,8 +9,10 @@
use Neve\Core\Settings\Config;
use Neve\Core\Settings\Mods;
+use Neve\Core\Styles\Css_Prop;
use Neve\Customizer\Defaults\Layout;
use Neve\Customizer\Defaults\Single_Post;
+use Neve\Customizer\Options\Scroll_To_Top;
/**
* Class Generator for Frontend.
@@ -54,6 +56,7 @@ public function __construct() {
$this->setup_header_style();
$this->setup_single_post_style();
$this->setup_content_vspacing();
+ $this->setup_scroll_to_top();
}
/**
@@ -1058,4 +1061,108 @@ private function setup_content_vspacing() {
];
}
}
+
+ /**
+ * Setup scroll to top styles.
+ *
+ * @return void
+ */
+ private function setup_scroll_to_top() {
+ if ( ! Scroll_To_Top::is_enabled() ) {
+ return;
+ }
+
+ // Add CSS variables to root
+ $rules = $this->get_scroll_to_top_rules();
+ $this->_subscribers[] = [
+ Dynamic_Selector::KEY_SELECTOR => '.scroll-to-top',
+ Dynamic_Selector::KEY_RULES => $rules,
+ ];
+ }
+
+ /**
+ * Get scroll to top CSS variables rules.
+ *
+ * @return array>
+ */
+ private function get_scroll_to_top_rules() {
+ $rules = [
+ '--color' => [
+ Dynamic_Selector::META_KEY => 'neve_scroll_to_top_icon_color',
+ Dynamic_Selector::META_DEFAULT => empty( Mods::get( 'neve_scroll_to_top_icon_color', 'var(--nv-text-dark-bg)' ) ) ? 'transparent' : 'var(--nv-text-dark-bg)',
+ ],
+ '--padding' => [
+ Dynamic_Selector::META_KEY => 'neve_scroll_to_top_padding',
+ Dynamic_Selector::META_IS_RESPONSIVE => true,
+ Dynamic_Selector::META_SUFFIX => 'responsive_unit',
+ 'directional-prop' => Config::CSS_PROP_PADDING,
+ Dynamic_Selector::META_DEFAULT => array(
+ 'desktop' => array(
+ 'top' => 8,
+ 'right' => 10,
+ 'bottom' => 8,
+ 'left' => 10,
+ ),
+ 'tablet' => array(
+ 'top' => 8,
+ 'right' => 10,
+ 'bottom' => 8,
+ 'left' => 10,
+ ),
+ 'mobile' => array(
+ 'top' => 8,
+ 'right' => 10,
+ 'bottom' => 8,
+ 'left' => 10,
+ ),
+ 'desktop-unit' => 'px',
+ 'tablet-unit' => 'px',
+ 'mobile-unit' => 'px',
+ ),
+ ],
+ '--borderradius' => [
+ Dynamic_Selector::META_KEY => 'neve_scroll_to_top_border_radius',
+ Dynamic_Selector::META_DEFAULT => 3,
+ Dynamic_Selector::META_SUFFIX => 'px',
+ ],
+ '--bgcolor' => [
+ Dynamic_Selector::META_KEY => 'neve_scroll_to_top_background_color',
+ Dynamic_Selector::META_DEFAULT => empty( Mods::get( 'neve_scroll_to_top_background_color', 'var(--nv-primary-accent)' ) ) ? 'transparent' : 'var(--nv-primary-accent)',
+ ],
+ '--hovercolor' => [
+ Dynamic_Selector::META_KEY => 'neve_scroll_to_top_icon_hover_color',
+ Dynamic_Selector::META_DEFAULT => empty( Mods::get( 'neve_scroll_to_top_icon_hover_color', 'var(--nv-text-dark-bg)' ) ) ? 'transparent' : 'var(--nv-text-dark-bg)',
+ ],
+ '--hoverbgcolor' => [
+ Dynamic_Selector::META_KEY => 'neve_scroll_to_top_background_hover_color',
+ Dynamic_Selector::META_DEFAULT => empty( Mods::get( 'neve_scroll_to_top_background_hover_color', 'var(--nv-primary-accent)' ) ) ? 'transparent' : 'var(--nv-primary-accent)',
+ ],
+ '--size' => [
+ Dynamic_Selector::META_KEY => 'neve_scroll_to_top_icon_size',
+ Dynamic_Selector::META_DEFAULT => '{ "mobile": "16", "tablet": "16", "desktop": "16" }',
+ Dynamic_Selector::META_IS_RESPONSIVE => true,
+ Dynamic_Selector::META_FILTER => function ( $css_prop, $value, $meta, $device ) {
+ $value = (int) $value;
+ if ( $value > 0 ) {
+ $unit_suffix = Css_Prop::get_suffix_responsive( $meta, $device );
+ return sprintf( '%s:%s;', $css_prop, $value . $unit_suffix );
+ }
+ return '';
+ },
+ ],
+ ];
+
+ $type = Mods::get( 'neve_scroll_to_top_type', 'icon' );
+
+ if ( $type === 'image' ) {
+ $rules['--bgimage'] = [
+ Dynamic_Selector::META_KEY => 'neve_scroll_to_top_image',
+ Dynamic_Selector::META_FILTER => function ( $css_prop, $value, $meta, $device ) {
+ return sprintf( '--bgimage:url(%s);', wp_get_attachment_url( $value ) );
+ },
+ ];
+ }
+
+ return $rules;
+ }
}
diff --git a/inc/customizer/loader.php b/inc/customizer/loader.php
index 605ad804d2..4d6267813c 100644
--- a/inc/customizer/loader.php
+++ b/inc/customizer/loader.php
@@ -83,6 +83,7 @@ private function define_modules() {
'Customizer\Options\Layout_Single_Page',
'Customizer\Options\Layout_Single_Product',
'Customizer\Options\Layout_Sidebar',
+ 'Customizer\Options\Scroll_To_Top',
'Customizer\Options\Typography',
'Customizer\Options\Colors_Background',
'Customizer\Options\Checkout',
@@ -144,6 +145,9 @@ public function enqueue_customizer_controls() {
'learnMore' => apply_filters( 'neve_external_link', 'https://docs.themeisle.com/article/1349-how-to-load-neve-fonts-locally', esc_html__( 'Learn more', 'neve' ) ),
'key' => Config::OPTION_LOCAL_GOOGLE_FONTS_HOSTING,
),
+ 'preloadFonts' => array(
+ 'key' => Config::MODS_PRELOAD_FONTS,
+ ),
'fontPairs' => get_theme_mod( Config::MODS_TPOGRAPHY_FONT_PAIRS, Config::$typography_default_pairs ),
'allowedGlobalCustomColor' => Colors_Background::CUSTOM_COLOR_LIMIT,
'constants' => [
@@ -291,6 +295,14 @@ public function register_setting_local_gf( $wp_customize ) {
'default' => false,
]
);
+
+ $wp_customize->add_setting(
+ Config::MODS_PRELOAD_FONTS,
+ [
+ 'sanitize_callback' => 'neve_sanitize_checkbox',
+ 'default' => false,
+ ]
+ );
}
/**
diff --git a/inc/customizer/options/scroll_to_top.php b/inc/customizer/options/scroll_to_top.php
new file mode 100644
index 0000000000..790f007bb0
--- /dev/null
+++ b/inc/customizer/options/scroll_to_top.php
@@ -0,0 +1,762 @@
+
+ * Created on: 2019-02-06
+ *
+ * @package Neve Pro Addon
+ */
+
+namespace Neve\Customizer\Options;
+
+use Neve\Customizer\Base_Customizer;
+use Neve\Customizer\Types\Control;
+use Neve\Customizer\Types\Section;
+
+/**
+ * Class Scroll_To_Top
+ *
+ * @package Neve_Pro\Customizer\Options
+ */
+class Scroll_To_Top extends Base_Customizer {
+ /**
+ * The minimum value of some customizer controls is 0 to able to allow usability relative to CSS units.
+ * That can be removed after the https://github.com/Codeinwp/neve/issues/3609 issue is handled.
+ *
+ * That is defined here against the usage of old Neve versions, Base_Customizer class of the stable Neve version already has the RELATIVE_CSS_UNIT_SUPPORTED_MIN_VALUE constant.
+ */
+ const RELATIVE_CSS_UNIT_SUPPORTED_MIN_VALUE = 0;
+
+ /**
+ * Base initialization.
+ *
+ * @return void
+ */
+ public function init() {
+ parent::init();
+ add_action( 'wp_head', array( $this, 'live_refresh_scripts' ) );
+ }
+
+ /**
+ * Live refresh for scroll to top controls.
+ *
+ * @return void
+ */
+ public function live_refresh_scripts() {
+ if ( ! is_customize_preview() ) {
+ return;
+ }
+ ?>
+
+ scroll_to_top_section();
+ $this->scroll_to_top_options();
+ $this->scroll_to_top_style_controls();
+ }
+
+ /**
+ * Register customizer section for the module
+ *
+ * @return void
+ */
+ private function scroll_to_top_section() {
+ $this->add_section(
+ new Section(
+ 'neve_scroll_to_top',
+ array(
+ 'priority' => 80,
+ 'title' => esc_html__( 'Scroll To Top', 'neve' ),
+ 'panel' => 'neve_layout',
+ )
+ )
+ );
+
+ }
+
+ /**
+ * Register option toggle in customizer
+ *
+ * @return void
+ */
+ private function scroll_to_top_options() {
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_status',
+ array(
+ 'sanitize_callback' => array( $this, 'sanitize_module_status' ),
+ 'default' => '1',
+ ),
+ array(
+ 'label' => esc_html__( 'Enable', 'neve' ),
+ 'section' => 'neve_scroll_to_top',
+ 'type' => 'neve_toggle_control',
+ 'priority' => 5,
+ )
+ )
+ );
+
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_general',
+ array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ array(
+ 'label' => esc_html__( 'General', 'neve' ),
+ 'section' => 'neve_scroll_to_top',
+ 'priority' => 10,
+ 'class' => 'scroll-to-top-general',
+ 'accordion' => true,
+ 'expanded' => true,
+ 'controls_to_wrap' => 6,
+ 'active_callback' => array( $this, 'is_module_enabled' ),
+ ),
+ 'Neve\Customizer\Controls\Heading'
+ )
+ );
+
+ /**
+ * Button side
+ */
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_side',
+ array(
+ 'default' => 'right',
+ 'sanitize_callback' => array( $this, 'sanitize_scroll_to_top_side' ),
+ 'transport' => $this->selective_refresh,
+ ),
+ array(
+ 'label' => esc_html__( 'Choose Side', 'neve' ),
+ 'section' => 'neve_scroll_to_top',
+ 'priority' => 20,
+ 'type' => 'select',
+ 'choices' => array(
+ 'left' => esc_html__( 'Left', 'neve' ),
+ 'right' => esc_html__( 'Right', 'neve' ),
+ ),
+ 'active_callback' => array( $this, 'is_module_enabled' ),
+ )
+ )
+ );
+
+ /**
+ * Scroll to top type
+ */
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_type',
+ array(
+ 'default' => 'icon',
+ 'sanitize_callback' => array( $this, 'sanitize_scroll_to_top_type' ),
+ ),
+ array(
+ 'label' => esc_html__( 'Type', 'neve' ),
+ 'section' => 'neve_scroll_to_top',
+ 'priority' => 30,
+ 'type' => 'select',
+ 'choices' => array(
+ 'icon' => esc_html__( 'Icon', 'neve' ),
+ 'image' => esc_html__( 'Image', 'neve' ),
+ ),
+ 'active_callback' => array( $this, 'is_module_enabled' ),
+ )
+ )
+ );
+
+ /**
+ * Scroll to top icon
+ */
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_icon',
+ array(
+ 'sanitize_callback' => 'wp_filter_nohtml_kses',
+ 'default' => 'stt-icon-style-1',
+ ),
+ array(
+ 'label' => esc_html__( 'Icon', 'neve' ),
+ 'section' => 'neve_scroll_to_top',
+ 'priority' => 35,
+ 'active_callback' => array( $this, 'is_icon_type_control' ),
+ 'is_for' => 'scroll_to_top',
+ 'large_buttons' => false,
+ 'type' => 'neve_radio_buttons_control',
+ ),
+ '\Neve\Customizer\Controls\React\Radio_Buttons'
+ )
+ );
+
+ /**
+ * Image button
+ */
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_image',
+ array(
+ 'sanitize_callback' => 'absint',
+ ),
+ array(
+ 'label' => esc_html__( 'Image', 'neve' ),
+ 'section' => 'neve_scroll_to_top',
+ 'priority' => 40,
+ 'active_callback' => array( $this, 'is_image_type_control' ),
+ 'flex_height' => true,
+ 'flex_width' => true,
+ ),
+ '\WP_Customize_Cropped_Image_Control'
+ )
+ );
+
+ /*
+ * Label
+ */
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_label',
+ array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ 'transport' => $this->selective_refresh,
+ ),
+ array(
+ 'priority' => 50,
+ 'section' => 'neve_scroll_to_top',
+ 'label' => esc_html__( 'Label', 'neve' ),
+ 'type' => 'text',
+ 'active_callback' => array( $this, 'is_module_enabled' ),
+ )
+ )
+ );
+
+ /**
+ * Offset
+ */
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_offset',
+ array(
+ 'sanitize_callback' => 'absint',
+ 'default' => 0,
+ ),
+ array(
+ 'label' => esc_html__( 'Offset (px)', 'neve' ),
+ 'section' => 'neve_scroll_to_top',
+ 'step' => 1,
+ 'input_attr' => array(
+ 'min' => 0,
+ 'max' => 1000,
+ 'default' => 0,
+ ),
+ 'input_attrs' => array(
+ 'min' => 0,
+ 'max' => 1000,
+ 'defaultVal' => 0,
+ ),
+ 'priority' => 60,
+ 'active_callback' => array( $this, 'is_module_enabled' ),
+ ),
+ class_exists( 'Neve\Customizer\Controls\React\Range' ) ? 'Neve\Customizer\Controls\React\Range' : 'Neve\Customizer\Controls\Range'
+ )
+ );
+
+ /**
+ * Hide on mobile
+ */
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_on_mobile',
+ array(
+ 'sanitize_callback' => 'neve_sanitize_checkbox',
+ 'default' => false,
+ 'transport' => $this->selective_refresh,
+ ),
+ array(
+ 'label' => esc_html__( 'Hide on mobile', 'neve' ),
+ 'section' => 'neve_scroll_to_top',
+ 'type' => 'neve_toggle_control',
+ 'priority' => 70,
+ 'active_callback' => array( $this, 'is_module_enabled' ),
+ ),
+ 'Neve\Customizer\Controls\Checkbox'
+ )
+ );
+ }
+
+ /**
+ * Add style controls for Scroll to top module.
+ *
+ * @return void
+ */
+ private function scroll_to_top_style_controls() {
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_style',
+ array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ array(
+ 'label' => esc_html__( 'Style', 'neve' ),
+ 'section' => 'neve_scroll_to_top',
+ 'priority' => 80,
+ 'class' => 'scroll-to-top-accordion',
+ 'accordion' => true,
+ 'expanded' => false,
+ 'controls_to_wrap' => 3,
+ 'active_callback' => array( $this, 'is_module_enabled' ),
+ ),
+ 'Neve\Customizer\Controls\Heading'
+ )
+ );
+
+ $default_padding_values = array(
+ 'desktop' => array(
+ 'top' => 8,
+ 'right' => 10,
+ 'bottom' => 8,
+ 'left' => 10,
+ ),
+ 'tablet' => array(
+ 'top' => 8,
+ 'right' => 10,
+ 'bottom' => 8,
+ 'left' => 10,
+ ),
+ 'mobile' => array(
+ 'top' => 8,
+ 'right' => 10,
+ 'bottom' => 8,
+ 'left' => 10,
+ ),
+ 'desktop-unit' => 'px',
+ 'tablet-unit' => 'px',
+ 'mobile-unit' => 'px',
+ );
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_padding',
+ array(
+ 'default' => $default_padding_values,
+ 'transport' => $this->selective_refresh,
+ ),
+ array(
+ 'label' => __( 'Padding', 'neve' ),
+ 'sanitize_callback' => array( $this, 'sanitize_spacing_array' ),
+ 'section' => 'neve_scroll_to_top',
+ 'input_attrs' => array(
+ 'units' => array( 'px', 'em', 'rem' ),
+ ),
+ 'default' => $default_padding_values,
+ 'priority' => 90,
+ 'live_refresh_selector' => true,
+ 'live_refresh_css_prop' => array(
+ 'cssVar' => array(
+ 'vars' => '--padding',
+ 'selector' => '#scroll-to-top',
+ 'responsive' => true,
+ ),
+ 'responsive' => true,
+ 'directional' => true,
+ 'template' =>
+ '#scroll-to-top {
+ padding-top: {{value.top}};
+ padding-right: {{value.right}};
+ padding-bottom: {{value.bottom}};
+ padding-left: {{value.left}};
+ }',
+ ),
+ 'active_callback' => array( $this, 'is_module_enabled' ),
+ ),
+ '\Neve\Customizer\Controls\React\Spacing'
+ )
+ );
+
+ /**
+ * Icon size
+ */
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_icon_size',
+ array(
+ 'sanitize_callback' => 'neve_sanitize_range_value',
+ 'default' => '{ "mobile": "16", "tablet": "16", "desktop": "16" }',
+ 'transport' => $this->selective_refresh,
+ ),
+ array(
+ 'label' => esc_html__( 'Icon Size', 'neve' ),
+ 'section' => 'neve_scroll_to_top',
+ 'media_query' => true,
+ 'step' => 1,
+ 'input_attr' => array(
+ 'mobile' => array(
+ 'min' => 10,
+ 'max' => 100,
+ 'default' => 16,
+ ),
+ 'tablet' => array(
+ 'min' => 10,
+ 'max' => 100,
+ 'default' => 16,
+ ),
+ 'desktop' => array(
+ 'min' => 10,
+ 'max' => 100,
+ 'default' => 16,
+ ),
+ ),
+ 'input_attrs' => array(
+ 'step' => 1,
+ 'min' => self::RELATIVE_CSS_UNIT_SUPPORTED_MIN_VALUE,
+ 'max' => 100,
+ 'defaultVal' => array(
+ 'mobile' => 16,
+ 'tablet' => 16,
+ 'desktop' => 16,
+ 'suffix' => [
+ 'mobile' => 'px',
+ 'tablet' => 'px',
+ 'desktop' => 'px',
+ ],
+ ),
+ 'units' => array( 'px', 'em', 'rem' ),
+ ),
+ 'priority' => 100,
+ 'live_refresh_selector' => true,
+ 'live_refresh_css_prop' => array(
+ 'cssVar' => array(
+ 'vars' => '--size',
+ 'selector' => '.scroll-to-top-icon, .scroll-to-top-image',
+ 'responsive' => true,
+ 'suffix' => 'px',
+ ),
+ 'responsive' => true,
+ 'template' => 'body .scroll-to-top.icon .scroll-to-top-icon, body .scroll-to-top.image .scroll-to-top-image {
+ width: {{value}}px;
+ height: {{value}}px;
+ }',
+ ),
+ 'active_callback' => array( $this, 'is_module_enabled' ),
+ ),
+ class_exists( 'Neve\Customizer\Controls\React\Responsive_Range', false ) ? 'Neve\Customizer\Controls\React\Responsive_Range' : 'Neve\Customizer\Controls\Range'
+ )
+ );
+
+ /**
+ * Button border radius
+ */
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_border_radius',
+ array(
+ 'sanitize_callback' => 'absint',
+ 'default' => 3,
+ 'transport' => $this->selective_refresh,
+ ),
+ array(
+ 'label' => esc_html__( 'Border Radius', 'neve' ),
+ 'section' => 'neve_scroll_to_top',
+ 'step' => 1,
+ 'input_attr' => array(
+ 'min' => 0,
+ 'max' => 200,
+ 'default' => 3,
+ ),
+ 'input_attrs' => array(
+ 'min' => 0,
+ 'max' => 200,
+ 'defaultVal' => 3,
+ ),
+ 'priority' => 110,
+ 'live_refresh_selector' => true,
+ 'live_refresh_css_prop' => array(
+ 'cssVar' => array(
+ 'fallback' => '0',
+ 'vars' => '--borderradius',
+ 'selector' => '.scroll-to-top',
+ 'suffix' => 'px',
+ ),
+ 'template' => 'body .scroll-to-top {
+ border-radius: {{value}}px;
+ }',
+ 'fallback' => '0',
+ ),
+ 'active_callback' => array( $this, 'is_module_enabled' ),
+ ),
+ class_exists( 'Neve\Customizer\Controls\React\Range' ) ? 'Neve\Customizer\Controls\React\Range' : 'Neve\Customizer\Controls\Range'
+ )
+ );
+
+ /**
+ * Colors heading
+ */
+ $this->add_control(
+ new Control(
+ 'neve_scroll_to_top_colors',
+ array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ array(
+ 'label' => esc_html__( 'Color', 'neve' ),
+ 'section' => 'neve_scroll_to_top',
+ 'priority' => 110,
+ 'class' => 'scroll-top-colors-accordion',
+ 'accordion' => true,
+ 'expanded' => false,
+ 'controls_to_wrap' => 4,
+ 'active_callback' => array( $this, 'is_module_enabled' ),
+ ),
+ 'Neve\Customizer\Controls\Heading'
+ )
+ );
+
+ $color_controls = array(
+ 'neve_scroll_to_top_icon_color' => array(
+ 'default' => 'var(--nv-text-dark-bg)',
+ 'priority' => 120,
+ 'label' => esc_html__( 'Color', 'neve' ),
+ 'live_refresh_css_prop' => array(
+ 'cssVar' => array(
+ 'vars' => '--color',
+ 'selector' => '.scroll-to-top',
+ ),
+ 'template' => '
+ body .scroll-to-top {
+ color: {{value}};
+ }',
+ ),
+ ),
+ 'neve_scroll_to_top_icon_hover_color' => array(
+ 'default' => 'var(--nv-text-dark-bg)',
+ 'priority' => 130,
+ 'label' => esc_html__( 'Hover Color', 'neve' ),
+ 'live_refresh_css_prop' => array(
+ 'cssVar' => array(
+ 'vars' => '--hovercolor',
+ 'selector' => '.scroll-to-top:hover',
+ ),
+ 'template' => '
+ body .scroll-to-top:hover {
+ color: {{value}};
+ }',
+ ),
+ ),
+ 'neve_scroll_to_top_background_color' => array(
+ 'default' => 'var(--nv-primary-accent)',
+ 'priority' => 140,
+ 'label' => esc_html__( 'Background Color', 'neve' ),
+ 'input_attrs' => [
+ 'allow_gradient' => true,
+ ],
+ 'live_refresh_css_prop' => array(
+ 'cssVar' => array(
+ 'vars' => '--bgcolor',
+ 'selector' => '.scroll-to-top',
+ ),
+ 'template' => '
+ body .scroll-to-top {
+ background: {{value}};
+ }',
+ ),
+ ),
+ 'neve_scroll_to_top_background_hover_color' => array(
+ 'default' => 'var(--nv-primary-accent)',
+ 'priority' => 150,
+ 'label' => esc_html__( 'Background Hover Color', 'neve' ),
+ 'input_attrs' => [
+ 'allow_gradient' => true,
+ ],
+ 'live_refresh_css_prop' => array(
+ 'cssVar' => array(
+ 'vars' => '--hoverbgcolor',
+ 'selector' => '.scroll-to-top:hover',
+ ),
+ 'template' => '
+ body .scroll-to-top:hover {
+ background: {{value}};
+ }',
+ ),
+ ),
+ );
+
+ /**
+ * Color controls
+ */
+ foreach ( $color_controls as $control_id => $control_properties ) {
+ $this->add_control(
+ new Control(
+ $control_id,
+ array(
+ 'sanitize_callback' => 'neve_sanitize_colors',
+ 'default' => $control_properties['default'],
+ 'transport' => $this->selective_refresh,
+ ),
+ array(
+ 'label' => $control_properties['label'],
+ 'section' => 'neve_scroll_to_top',
+ 'priority' => $control_properties['priority'],
+ 'input_attrs' => isset( $control_properties['input_attrs'] ) ? $control_properties['input_attrs'] : [],
+ 'live_refresh_selector' => true,
+ 'live_refresh_css_prop' => $control_properties['live_refresh_css_prop'],
+ 'active_callback' => array( $this, 'is_module_enabled' ),
+ ),
+ '\Neve\Customizer\Controls\React\Color'
+ )
+ );
+ }
+ }
+
+ /**
+ * Active callback for controls that are available only if scroll to top is an image
+ *
+ * @return bool
+ */
+ public function is_image_type_control() {
+ if ( ! $this->is_module_enabled() ) {
+ return false;
+ }
+
+ return get_theme_mod( 'neve_scroll_to_top_type', 'icon' ) === 'image';
+ }
+
+ /**
+ * Active callback for controls that are available only if scroll to top is an icon
+ *
+ * @return bool
+ */
+ public function is_icon_type_control() {
+ if ( ! $this->is_module_enabled() ) {
+ return false;
+ }
+
+ return get_theme_mod( 'neve_scroll_to_top_type', 'icon' ) === 'icon';
+ }
+
+ /**
+ * Sanitize scroll to top type
+ *
+ * @param string $value - value of the control.
+ *
+ * @return string
+ */
+ public function sanitize_scroll_to_top_type( $value ) {
+ $allowed_values = array( 'icon', 'image' );
+ if ( ! in_array( $value, $allowed_values, true ) ) {
+ return 'icon';
+ }
+
+ return esc_html( $value );
+ }
+
+ /**
+ * Sanitize scroll to top side
+ *
+ * @param string $value - value of the control.
+ *
+ * @return string
+ */
+ public function sanitize_scroll_to_top_side( $value ) {
+ $allowed_values = array( 'left', 'right' );
+ if ( ! in_array( $value, $allowed_values, true ) ) {
+ return 'right';
+ }
+
+ return esc_html( $value );
+ }
+
+ /**
+ * Check if Scroll to Top is enabled.
+ *
+ * @return bool
+ */
+ public static function is_enabled() {
+ // Check old option first for backward compatibility.
+ $old_value = get_option( 'nv_pro_scroll_to_top_status', null );
+ $is_enabled = false;
+
+ if ( null !== $old_value ) {
+ // Option exists — use it and migrate to the new theme_mod for future.
+ $is_enabled = $old_value === '1';
+
+ set_theme_mod( 'neve_scroll_to_top_status', $old_value );
+ delete_option( 'nv_pro_scroll_to_top_status' );
+ } else {
+ // Otherwise, use the new theme_mod.
+ $is_enabled = get_theme_mod( 'neve_scroll_to_top_status', '1' ) === '1';
+ }
+
+ /**
+ * Filter to allow conditional loading of the scroll to top feature.
+ *
+ * @param bool $is_enabled Whether the scroll to top feature is enabled.
+ */
+ return apply_filters( 'neve_scroll_to_top_is_enabled', $is_enabled );
+ }
+
+ /**
+ * Active callback for scroll to top controls.
+ *
+ * @return bool
+ */
+ public function is_module_enabled() {
+ return self::is_enabled();
+ }
+
+ /**
+ * Sanitize module status. The toggle in neve options returns '1' or '' so our control should return the same thing.
+ *
+ * @param bool|string $value Current value.
+ *
+ * @return string
+ */
+ public function sanitize_module_status( $value ) {
+ if ( $value === true ) {
+ return '1';
+ }
+ if ( $value === false ) {
+ return '';
+ }
+ return $value;
+ }
+
+}
diff --git a/inc/customizer/options/upsells.php b/inc/customizer/options/upsells.php
index 768b876672..668f5d3168 100644
--- a/inc/customizer/options/upsells.php
+++ b/inc/customizer/options/upsells.php
@@ -28,13 +28,6 @@ class Upsells extends Base_Customizer {
*/
private $upsell_url = '';
- /**
- * Scroll to top upsell url
- *
- * @var string
- */
- private $stt_upsell_url = '';
-
/**
* Init function
*
@@ -45,8 +38,7 @@ public function init() {
return;
}
- $this->stt_upsell_url = esc_url_raw( apply_filters( 'neve_upgrade_link_from_child_theme_filter', $this->get_upgrade_url( 'scrolltotop' ) ) );
- $this->upsell_url = esc_url_raw( apply_filters( 'neve_upgrade_link_from_child_theme_filter', $this->get_upgrade_url( 'learnmorebtn' ) ) );
+ $this->upsell_url = esc_url_raw( apply_filters( 'neve_upgrade_link_from_child_theme_filter', $this->get_upgrade_url( 'learnmorebtn' ) ) );
parent::init();
@@ -408,20 +400,6 @@ private function section_upsells() {
)
);
}
-
- $this->add_section(
- new Section(
- 'neve_scroll_to_top_upsell',
- array(
- 'priority' => 80,
- 'title' => esc_html__( 'Scroll To Top', 'neve' ),
- 'cta' => esc_html__( 'PRO', 'neve' ),
- 'url' => $this->stt_upsell_url,
- 'panel' => 'neve_layout',
- ),
- 'Neve\Customizer\Controls\React\Upsell_Section'
- )
- );
}
/**
@@ -443,30 +421,6 @@ private function control_upsells() {
)
);
-
- /*
- * Deactivated.
- *
- * @since 4.1.0
- */
- $this->add_control(
- new Control(
- 'neve_scroll_to_top_cta_control',
- [ 'sanitize_callback' => 'sanitize_text_field' ],
- [
- /* translators: Module name for the upsell. */
- 'text' => sprintf( __( 'Unlock %s with the Pro version.', 'neve' ), __( 'Scroll To Top', 'neve' ) ),
- 'button_text' => esc_html__( 'Get the PRO version!', 'neve' ),
- 'section' => 'neve_scroll_to_top_upsell',
- 'priority' => PHP_INT_MIN,
- 'link' => $this->get_upgrade_url( 'scrolltotop' ),
- 'class' => 'column-layout',
- 'use_primary' => 'true',
- ],
- 'Neve\Customizer\Controls\Simple_Upsell'
- )
- );
-
$hfg_header = 'hfg_header';
$hfg_footer = 'hfg_footer';
diff --git a/inc/views/font_manager.php b/inc/views/font_manager.php
index cc9d8b36d3..46769b35d3 100644
--- a/inc/views/font_manager.php
+++ b/inc/views/font_manager.php
@@ -93,6 +93,7 @@ public function init() {
add_action( 'enqueue_block_editor_assets', array( $this, 'register_google_fonts' ), 200 );
add_action( 'enqueue_block_editor_assets', array( $this, 'do_editor_styles_google_fonts' ), 210 );
add_action( 'wp_print_styles', array( $this, 'load_external_fonts_locally' ), PHP_INT_MAX );
+ add_filter( 'style_loader_tag', array( $this, 'add_rel_preload' ), 10, 2 );
}
/**
@@ -251,6 +252,46 @@ private function enqueue_google_font( $font, $weights = [], $skip_enqueue = fals
wp_enqueue_style( 'neve-google-font-' . str_replace( ' ', '-', strtolower( $font ) ), $url, array(), NEVE_VERSION );
}
+ /**
+ * Add onload, rel and as attributes for Google Font stylesheets.
+ * Implements lazy loading with preload for better performance.
+ *
+ * @param string $html Current html code.
+ * @param string $handle Current script handle.
+ *
+ * @return string
+ */
+ public function add_rel_preload( $html, $handle ) {
+ if ( is_admin() ) {
+ return $html;
+ }
+
+ $preload_enabled = get_theme_mod( Config::MODS_PRELOAD_FONTS, false );
+
+ /**
+ * Filters whether fonts should be preloaded.
+ *
+ * @param bool $preload_enabled Whether fonts should be preloaded. Default value is false.
+ *
+ * @since 3.9.0
+ */
+ $should_preload = apply_filters( 'neve_preload_fonts', $preload_enabled );
+
+ if ( ! (bool) $should_preload ) {
+ return $html;
+ }
+
+ // Only preload Google Font stylesheets
+ if ( strpos( $handle, 'neve-google-font-' ) === 0 ) {
+ // Lazy load with JS, but also add noscript in case no JS
+ $no_script = '' . $html . ' ';
+ // Add onload, rel="preload", as="style", and put together with noscript
+ $html = str_replace( 'rel=\'stylesheet\'', 'rel="preload" as="style" onload="this.rel=\'stylesheet\';"', $html ) . $no_script;
+ }
+
+ return $html;
+ }
+
/**
* Load Google Fonts locally.
*
diff --git a/inc/views/scroll_to_top.php b/inc/views/scroll_to_top.php
new file mode 100644
index 0000000000..9eb007fe6d
--- /dev/null
+++ b/inc/views/scroll_to_top.php
@@ -0,0 +1,189 @@
+';
+
+ // We use 2 `amp-animation` elements to trigger the visibility of the button. The first one is for making the button visible
+ echo '
+
+
+ ';
+
+ echo '
+
+
+
+
+ ';
+ }
+
+ /**
+ * Enqueue module scripts
+ *
+ * @return void
+ */
+ public function enqueue_scripts() {
+ if ( ! Scroll_To_Top_Options::is_enabled() ) {
+ return;
+ }
+
+ if ( neve_is_amp() ) {
+ return;
+ }
+
+ wp_register_script(
+ 'neve-scroll-to-top',
+ NEVE_ASSETS_URL . 'js/build/modern/scroll-to-top.js',
+ array(),
+ NEVE_VERSION,
+ true
+ );
+
+ wp_enqueue_script( 'neve-scroll-to-top' );
+
+ wp_script_add_data( 'neve-scroll-to-top', 'async', true );
+
+ wp_localize_script( 'neve-scroll-to-top', 'neveScrollOffset', $this->localize_scroll() );
+ }
+
+ /**
+ * Send offset to the JS object
+ *
+ * @return array
+ */
+ private function localize_scroll() {
+ return array(
+ 'offset' => get_theme_mod( 'neve_scroll_to_top_offset', 0 ),
+ );
+ }
+
+ /**
+ * Display scroll to top button
+ *
+ * @return void
+ */
+ public function render_button() {
+ if ( ! Scroll_To_Top_Options::is_enabled() ) {
+ return;
+ }
+
+ $position = get_theme_mod( 'neve_scroll_to_top_side', 'right' );
+ $hide_on_mobile = get_theme_mod( 'neve_scroll_to_top_on_mobile', false );
+ $type = get_theme_mod( 'neve_scroll_to_top_type', 'icon' );
+ $label = get_theme_mod( 'neve_scroll_to_top_label' );
+ $image = get_theme_mod( 'neve_scroll_to_top_image' );
+ $icon = get_theme_mod( 'neve_scroll_to_top_icon', 'stt-icon-style-1' );
+
+ $extra_class = sprintf( 'scroll-to-top-%s %s', $position, ( ( ! $hide_on_mobile ) ? ' scroll-show-mobile ' : '' ) );
+ $extra_class .= $type;
+
+ $amp = neve_is_amp() ? 'on="tap:neve_body.scrollTo(duration=200)"' : '';
+
+ echo '';
+ }
+
+ /**
+ * Get SVG icon for scroll to top button.
+ *
+ * @param string $icon_style The icon style identifier.
+ * @return string SVG icon markup.
+ */
+ private function get_icon_svg( $icon_style ) {
+ $icons = array(
+ 'stt-icon-style-1' => ' ',
+ 'stt-icon-style-2' => ' ',
+ 'stt-icon-style-3' => ' ',
+ 'stt-icon-style-4' => ' ',
+ 'stt-icon-style-5' => ' ',
+ 'stt-icon-style-6' => ' ',
+ );
+
+ return isset( $icons[ $icon_style ] ) ? $icons[ $icon_style ] : $icons['stt-icon-style-1'];
+ }
+}
diff --git a/inc/views/style_book.php b/inc/views/style_book.php
new file mode 100644
index 0000000000..195d766258
--- /dev/null
+++ b/inc/views/style_book.php
@@ -0,0 +1,191 @@
+ [
+ 'name' => __( 'Primary Accent', 'neve' ),
+ 'section' => 'neve_colors_background_section',
+ ],
+ 'nv-secondary-accent' => [
+ 'name' => __( 'Secondary Accent', 'neve' ),
+ 'section' => 'neve_colors_background_section',
+ ],
+ 'nv-site-bg' => [
+ 'name' => __( 'Site Background', 'neve' ),
+ 'section' => 'neve_colors_background_section',
+ ],
+ 'nv-light-bg' => [
+ 'name' => __( 'Light Background', 'neve' ),
+ 'section' => 'neve_colors_background_section',
+ ],
+ 'nv-dark-bg' => [
+ 'name' => __( 'Dark Background', 'neve' ),
+ 'section' => 'neve_colors_background_section',
+ ],
+ 'nv-text-color' => [
+ 'name' => __( 'Text Color', 'neve' ),
+ 'section' => 'neve_colors_background_section',
+ ],
+ 'nv-text-dark-bg' => [
+ 'name' => __( 'Text Dark Background', 'neve' ),
+ 'section' => 'neve_colors_background_section',
+ ],
+ 'nv-c-1' => [
+ 'name' => __( 'Extra Color 1', 'neve' ),
+ 'section' => 'neve_colors_background_section',
+ ],
+ 'nv-c-2' => [
+ 'name' => __( 'Extra Color 2', 'neve' ),
+ 'section' => 'neve_colors_background_section',
+ ],
+ ];
+
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ $color_data ) : ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ 'neve_h1_accordion_wrap',
+ 2 => 'neve_h2_accordion_wrap',
+ 3 => 'neve_h3_accordion_wrap',
+ 4 => 'neve_h4_accordion_wrap',
+ 5 => 'neve_h5_accordion_wrap',
+ 6 => 'neve_h6_accordion_wrap',
+ ];
+
+ foreach ( $headings as $level => $control_id ) :
+ ?>
+ class="builder-item-focus" data-section="" data-control="">
+
+ >
+
+
+
+
+
+
+ get( 'Description' ) ); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.5%, last 2 versions, Firefox ESR, not dead"
- ]
+ targets: {
+ browsers: [
+ '> 0.5%, last 2 versions, Firefox ESR, not dead',
+ ],
},
- "useBuiltIns": "usage",
- "corejs": 3,
- "exclude": [
- 'es.regexp.exec',
- 'es.string.split',
- ]
- }
+ useBuiltIns: 'usage',
+ corejs: 3,
+ exclude: ['es.regexp.exec', 'es.string.split'],
+ },
],
],
};
const ROLLUP_MODERN = {
exclude: 'node_modules/**',
babelrc: false,
- presets:
+ presets: [
[
- [
- "@babel/env",
- {
- "targets": ["defaults",
- "not ie >= 0"],
- "debug": true,
- "useBuiltIns": "usage",
- "corejs": 3,
- "exclude": [
- "es.string.split",
- 'web.dom-collections.iterator'
- ]
- }
- ]
-
+ '@babel/env',
+ {
+ targets: ['defaults', 'not ie >= 0'],
+ debug: true,
+ useBuiltIns: 'usage',
+ corejs: 3,
+ exclude: ['es.string.split', 'web.dom-collections.iterator'],
+ },
+ ],
],
};
-let all_coverage = {
+const all_coverage = {
'assets/js/build/all/metabox.js': 'assets/js/src/metabox.js',
'assets/js/build/all/gutenberg.js': 'assets/js/src/gutenberg.js',
- 'assets/js/build/all/customizer-preview.js': ['assets/js/src/customizer-preview/app.js'],
- 'assets/js/build/all/customizer-controls.js': ['./assets/customizer/js/*.js']
+ 'assets/js/build/all/customizer-preview.js': [
+ 'assets/js/src/customizer-preview/app.js',
+ ],
+ 'assets/js/build/all/customizer-controls.js': [
+ './assets/customizer/js/*.js',
+ ],
},
__export = [],
modern = {
'assets/js/build/modern/shop.js': 'assets/js/src/shop/app.js',
'assets/js/build/modern/frontend.js': 'assets/js/src/frontend/app.js',
+ 'assets/js/build/modern/scroll-to-top.js':
+ 'assets/js/src/scroll-to-top.js',
};
Object.keys(all_coverage).forEach(function (item) {
@@ -67,15 +64,15 @@ Object.keys(all_coverage).forEach(function (item) {
output: {
file: item,
format: 'iife',
- sourceMap: 'inline'
+ sourceMap: 'inline',
},
plugins: [
multi(),
resolve(),
commonjs(),
babel(ROLLUP_LEGACY),
- uglify()
- ]
+ uglify(),
+ ],
});
});
Object.keys(modern).forEach(function (item) {
@@ -84,15 +81,15 @@ Object.keys(modern).forEach(function (item) {
output: {
file: item,
format: 'iife',
- sourceMap: 'inline'
+ sourceMap: 'inline',
},
plugins: [
multi(),
resolve(),
commonjs(),
babel(ROLLUP_MODERN),
- terser()
- ]
+ terser(),
+ ],
});
});