diff --git a/.github/workflows/diff-translations.yml b/.github/workflows/diff-translations.yml index fb545b3aaa..16f6e24866 100644 --- a/.github/workflows/diff-translations.yml +++ b/.github/workflows/diff-translations.yml @@ -5,71 +5,48 @@ on: pull_request: types: [opened, edited, synchronize, ready_for_review] branches: - - "development" - - "new/**" + - development + - master jobs: translation: runs-on: ubuntu-latest steps: - - name: Checkout Ref Base + - name: Checkout Base Branch uses: actions/checkout@v4 with: - path: neve-head + ref: ${{ github.base_ref }} + path: neve-base - name: Setup node 16 uses: actions/setup-node@v4 with: node-version: 16.x - - name: FRESH Makepot BASE + - name: Build POT for Base Branch run: | - cd neve-head + cd neve-base ls languages/ composer install --no-dev --prefer-dist --no-progress --no-suggest yarn install --frozen-lockfile yarn run build ls languages/ - - name: Checkout Ref Head - uses: actions/checkout@v2 + - name: Checkout PR Branch (Head) + uses: actions/checkout@v4 with: - ref: development - path: neve-base - - name: FRESH Makepot HEAD + path: neve-head + - name: Build POT for PR Branch run: | - cd neve-base + cd neve-head ls languages/ composer install --no-dev --prefer-dist --no-progress --no-suggest yarn install --frozen-lockfile yarn run build ls languages/ - - name: Find Comment - uses: peter-evans/find-comment@v1 - id: find_coomment - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: "pirate-bot" - body-includes: PR has POT difference - - name: Install PODiff - run: | - curl -o podiff.gz ftp://download.gnu.org.ua/pub/releases/podiff/podiff-1.3.tar.gz - tar -xf podiff.gz - cd podiff-1.3 - make - mkdir -p $GITHUB_WORKSPACE/bin - mv ./podiff $GITHUB_WORKSPACE/bin - echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH - cd .. - - name: Run Podiff - id: translation_status - run: | - ${GITHUB_WORKSPACE}/neve-head/bin/pot-diff.sh ./neve-base/languages/neve.pot ./neve-head/languages/neve.pot $PERCENT_TRESHOLD - - name: Step require review - if: steps.translation_status.outputs.has_pot_diff != 'success' - uses: Automattic/action-required-review@v2 + - name: Compare POT files + uses: Codeinwp/action-i18n-string-reviewer@main with: - requirements: | - - name: Everything else - paths: unmatched - teams: - - "admin-neve" - status: Has translation changes, a review admin-neve team is required - token: ${{ secrets.BOT_TOKEN }} + fail-on-changes: 'true' + openrouter-key: ${{ secrets.OPEN_ROUTER_API_KEY }} + openrouter-model: 'google/gemini-2.5-flash' + base-pot-file: 'neve-base/languages/neve.pot' + target-pot-file: 'neve-head/languages/neve.pot' + github-token: ${{ secrets.BOT_TOKEN }} diff --git a/assets/apps/components/src/Controls/ColorControl.js b/assets/apps/components/src/Controls/ColorControl.js index 9461310745..98a147ef11 100644 --- a/assets/apps/components/src/Controls/ColorControl.js +++ b/assets/apps/components/src/Controls/ColorControl.js @@ -19,6 +19,7 @@ import classnames from 'classnames'; const ColorPickerFix = lazy(() => import('../Common/ColorPickerFix')); const ColorControl = ({ + slug = null, label, selectedColor, onChange, @@ -52,7 +53,8 @@ const ColorControl = ({ }; const isGlobal = selectedColor && selectedColor.indexOf('var') > -1; - const defaultGradient = 'linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)'; + const defaultGradient = + 'linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)'; const handleClear = () => { onChange(defaultValue || ''); @@ -63,7 +65,10 @@ const ColorControl = ({ const wrapClasses = classnames([ 'neve-control-header', 'neve-color-component', - { 'allows-global': !disableGlobal }, + { + 'allows-global': !disableGlobal, + [`neve-color-slug-${slug}`]: !!slug, + }, ]); const [gradient, setGradient] = useState(selectedColor); @@ -159,7 +164,9 @@ const ColorControl = ({
= ({ value, hidden, portalMount }) => { setMobileOverlayDismissed(true); }} > - {__('Enable', 'neve-pro-addon')} + {__('Enable', 'neve')}
)} diff --git a/assets/apps/customizer-controls/src/builder/components/ComponentsPopover.tsx b/assets/apps/customizer-controls/src/builder/components/ComponentsPopover.tsx index e64e400c2d..3b66cdf1c7 100644 --- a/assets/apps/customizer-controls/src/builder/components/ComponentsPopover.tsx +++ b/assets/apps/customizer-controls/src/builder/components/ComponentsPopover.tsx @@ -80,6 +80,16 @@ const ComponentsPopover: React.FC = ({ }); }; + const getFilteredUpsells = () => { + if (!searchQuery) { + return upsells; + } + + return upsells.filter(({ name }) => { + return name.toLowerCase().includes(searchQuery); + }); + }; + const renderItem = (item: ItemInterface, idx: number) => { if (!item.id) { return null; @@ -113,10 +123,15 @@ const ComponentsPopover: React.FC = ({ const renderItems = () => { const themeItems = getSidebarItems(); const boosterItems = getSidebarItems(true); + const filteredUpsells = getFilteredUpsells(); let noComponents = null; - if (themeItems.length === 0 && boosterItems.length === 0) { + if ( + themeItems.length === 0 && + boosterItems.length === 0 && + filteredUpsells.length === 0 + ) { noComponents = (
@@ -156,13 +171,13 @@ const ComponentsPopover: React.FC = ({ )} )} - {boosterItems.length < 1 && upsells.length > 0 && ( + {boosterItems.length < 1 && filteredUpsells.length > 0 && ( <>

{__('PRO', 'neve')} {__('Components', 'neve')}

- {upsells.map(({ name, icon }, idx) => { + {filteredUpsells.map(({ name, icon }, idx) => { return renderUpsell(idx, icon, name); })}
diff --git a/assets/apps/customizer-controls/src/common/useKeyboardSorting.js b/assets/apps/customizer-controls/src/common/useKeyboardSorting.js new file mode 100644 index 0000000000..3f70bbb319 --- /dev/null +++ b/assets/apps/customizer-controls/src/common/useKeyboardSorting.js @@ -0,0 +1,84 @@ +import { useCallback, useRef, useEffect } from '@wordpress/element'; + +/** + * Custom hook for keyboard-based sorting of list items. + * + * @param {number} index - Current item index in the list + * @param {number} totalItems - Total number of items in the list + * @param {Function} onMove - Callback function to move item (receives fromIndex, toIndex) + * @param {boolean} isActive - External state for whether keyboard mode is active + * @param {Function} setIsActive - Function to set the active state + * @return {Object} Hook state and handlers + */ +const useKeyboardSorting = ( + index, + totalItems, + onMove, + isActive, + setIsActive +) => { + const handleRef = useRef(null); + const previousIndexRef = useRef(index); + + // Handle keyboard events + const handleKeyDown = useCallback( + (e) => { + if (e.key === ' ' || e.key === 'Spacebar') { + e.preventDefault(); + setIsActive(!isActive); + return; + } + + if (!isActive) { + return; + } + + if (e.key === 'ArrowUp' && index > 0) { + e.preventDefault(); + const newIndex = index - 1; + previousIndexRef.current = index; + onMove(index, newIndex); + } + + if (e.key === 'ArrowDown' && index < totalItems - 1) { + e.preventDefault(); + const newIndex = index + 1; + previousIndexRef.current = index; + onMove(index, newIndex); + } + if (e.key === 'Escape') { + e.preventDefault(); + setIsActive(false); + } + }, + [isActive, index, totalItems, onMove, setIsActive] + ); + + const handleBlur = useCallback(() => { + setIsActive(false); + }, [setIsActive]); + + useEffect(() => { + if ( + isActive && + handleRef.current && + previousIndexRef.current !== index + ) { + // Small delay to ensure DOM has updated + window.requestAnimationFrame(() => { + if (handleRef.current) { + handleRef.current.focus(); + } + }); + } + previousIndexRef.current = index; + }, [index, isActive]); + + return { + handleRef, + handleKeyDown, + handleBlur, + }; +}; + +export default useKeyboardSorting; diff --git a/assets/apps/customizer-controls/src/controls.js b/assets/apps/customizer-controls/src/controls.js index 83b4a5e6f8..2bb9ee80d6 100644 --- a/assets/apps/customizer-controls/src/controls.js +++ b/assets/apps/customizer-controls/src/controls.js @@ -168,6 +168,59 @@ const initSearchCustomizer = () => { ); }; +const initStyleBookButton = () => { + const headerContainer = document.getElementById('customize-header-actions'); + + if (!headerContainer) { + return; + } + + // Initialize the Style Book state if it doesn't exist + if (!wp.customize.state.has('neveStyleBookOpen')) { + wp.customize.state.create('neveStyleBookOpen', false); + } + + // Create the Style Book button + const button = document.createElement('button'); + button.name = 'neve-style-book'; + button.id = 'neve-style-book'; + button.className = 'button-secondary button'; + button.title = __('Style Book', 'neve'); + button.innerHTML = ` + + ${__('Style Book', 'neve')} + `; + + // Add click handler + button.addEventListener('click', (e) => { + e.preventDefault(); + + // Toggle the state in customizer + const currentState = wp.customize.state('neveStyleBookOpen').get(); + const newState = !currentState; + wp.customize.state('neveStyleBookOpen').set(newState); + + // Send message to preview + wp.customize.previewer.send('neve-toggle-style-book', newState); + }); + + // Append to header container + headerContainer.appendChild(button); + + // Restore state when preview is ready + wp.customize.previewer.bind('ready', () => { + const currentState = wp.customize.state('neveStyleBookOpen').get(); + if (currentState) { + wp.customize.previewer.send('neve-restore-style-book-state', true); + } + }); + + // Listen for state changes from preview + wp.customize.previewer.bind('neve-style-book-state-changed', (newState) => { + wp.customize.state('neveStyleBookOpen').set(newState); + }); +}; + const initCustomPagesFocus = () => { const { sectionsFocus } = window.NeveReactCustomize; if (sectionsFocus !== undefined) { @@ -297,6 +350,50 @@ const checkHasElementorTemplates = () => { } }; +/** + * Find the Scroll to top button within the customizer preview. + */ +function findScrollToTopBtn() { + const iframeElement = document.querySelector('#customize-preview iframe'); + + if (!iframeElement) { + return; + } + + const scrollToTopBtn = + iframeElement.contentWindow.document.querySelector('#scroll-to-top'); + + return scrollToTopBtn; +} + +/** + * Show the Scroll to Top button as soon as the user enters the section in Customizer. + */ +function previewScrollToTopChanges() { + wp.customize.section('neve_scroll_to_top', (section) => { + section.expanded.bind((isExpanded) => { + const scrollToTopBtn = findScrollToTopBtn(); + + if (!scrollToTopBtn) { + return; + } + + // If Scroll to top customizer section is expanded + if (isExpanded) { + wp.customize.previewer.bind('ready', () => { + wp.customize.previewer.send('nv-opened-stt', true); + }); + scrollToTopBtn.style.visibility = 'visible'; + scrollToTopBtn.style.opacity = '1'; + } else { + // Hide the button when we leave the section + scrollToTopBtn.style.visibility = 'hidden'; + scrollToTopBtn.style.opacity = '0'; + } + }); + }); +} + window.wp.customize.bind('ready', () => { initStarterContentNotice(); initDocSection(); @@ -311,6 +408,8 @@ window.wp.customize.bind('ready', () => { initBlogPageFocus(); initSearchCustomizer(); initLocalGoogleFonts(); + initStyleBookButton(); + previewScrollToTopChanges(); }); window.HFG = { diff --git a/assets/apps/customizer-controls/src/customizer-search/MainSearch.tsx b/assets/apps/customizer-controls/src/customizer-search/MainSearch.tsx index 496bf5c33d..d7b4fc777a 100644 --- a/assets/apps/customizer-controls/src/customizer-search/MainSearch.tsx +++ b/assets/apps/customizer-controls/src/customizer-search/MainSearch.tsx @@ -1,6 +1,5 @@ import { createPortal } from '@wordpress/element'; import React, { useState } from 'react'; -import SearchToggle from './SearchToggle'; import SearchComponent, { Control } from './SearchComponent'; import SearchResults from './SearchResults'; @@ -20,23 +19,13 @@ type MainSearchProps = { * @class */ const MainSearch: React.FC = ({ search, button, results }) => { - const [isOpened, setIsOpened] = useState(false); const [query, setQuery] = useState(''); const [matchResults, setMatchResults] = useState([] as Control[]); return ( <> - {createPortal( - { - setIsOpened(!isOpened); - }} - />, - button - )} {createPortal( void; matchResults: Control[]; @@ -75,7 +74,6 @@ type SearchComponentProps = { * @class */ const SearchComponent: React.FC = ({ - isOpened, search, setSearch, matchResults, @@ -128,24 +126,6 @@ const SearchComponent: React.FC = ({ }); }, []); - /** - * This `useEffect()` is being used to listen for the toggleEvent - * from `SearchToggle` component. - */ - useEffect(() => { - if (isOpened) { - document - .getElementById('neve-customize-search-field') - ?.classList.add('visible'); - document.getElementById('nv-customizer-search-input')?.focus(); - } else { - document - .getElementById('neve-customize-search-field') - ?.classList.remove('visible'); - clearField(); - } - }, [isOpened]); - useEffect(() => { if (search === '') { if (customizerPanels?.classList.contains('search-not-found')) { @@ -217,15 +197,12 @@ const SearchComponent: React.FC = ({ return ( <> - - {__('Search', 'neve') + - ' ' + - __('Settings', 'neve').toLowerCase()} - void; -}; - -/** - * Search Toggle. - * - * @param {SearchToggleProps} SearchToggleProps - * @class - */ -const SearchToggle: React.FC = ({ onToggle }) => { - return ( - <> - - - ); -}; - -export default SearchToggle; diff --git a/assets/apps/customizer-controls/src/global-colors/PaletteColors.js b/assets/apps/customizer-controls/src/global-colors/PaletteColors.js index e7a7f9dbd8..4e5f9e8195 100644 --- a/assets/apps/customizer-controls/src/global-colors/PaletteColors.js +++ b/assets/apps/customizer-controls/src/global-colors/PaletteColors.js @@ -52,6 +52,7 @@ const PaletteColors = ({ values, defaults, save }) => { ( - - - -); +const Handle = ({ handleRef, onKeyDown, onBlur, isActive }) => { + return ( + + + + ); +}; const Item = ({ item, @@ -37,9 +52,22 @@ const Item = ({ components, allowsToggle = true, locked = false, + itemIndex, + totalItems, + onMove, + isKeyboardActive, + onKeyboardActiveChange, }) => { const label = components[item.id]?.label || components[item.id]; + const { handleRef, handleKeyDown, handleBlur } = useKeyboardSorting( + itemIndex, + totalItems, + onMove, + isKeyboardActive, + onKeyboardActiveChange + ); + const hasControls = useMemo(() => { return ( !!components[item.id]?.controls && @@ -99,7 +127,12 @@ const Item = ({ ) : ( - + )} {label} @@ -330,6 +363,13 @@ Item.propTypes = { item: PropTypes.object.isRequired, onToggle: PropTypes.func.isRequired, allowsToggle: PropTypes.bool.isRequired, + itemIndex: PropTypes.number.isRequired, + totalItems: PropTypes.number.isRequired, + onMove: PropTypes.func.isRequired, + components: PropTypes.object.isRequired, + locked: PropTypes.bool, + isKeyboardActive: PropTypes.bool.isRequired, + onKeyboardActiveChange: PropTypes.func.isRequired, className: PropTypes.string, disabled: PropTypes.bool, }; @@ -342,6 +382,9 @@ const Ordering = ({ locked = [], allowsToggle = true, }) => { + // Track active item by stable id, not by index or object reference. + const activeItemIdRef = useRef(null); + const [, forceUpdate] = useState({}); const handleToggle = (item) => { const newValue = value.map((e) => { if (e.id === item) { @@ -365,6 +408,13 @@ const Ordering = ({ onUpdate(updatedValue); }; + const handleMove = (fromIndex, toIndex) => { + const newValue = [...value]; + const [movedItem] = newValue.splice(fromIndex, 1); + newValue.splice(toIndex, 0, movedItem); + // Active id persists because item.id does not change. + handleChange(newValue); + }; value = value.filter((element) => { return components.hasOwnProperty(element.id); }); @@ -386,7 +436,7 @@ const Ordering = ({ .filter((element) => { return components.hasOwnProperty(element.id); }) - .map((item) => ( + .map((item, index) => ( { + activeItemIdRef.current = active + ? item.id + : null; + forceUpdate({}); + }} /> ))} diff --git a/assets/apps/customizer-controls/src/repeater/Repeater.js b/assets/apps/customizer-controls/src/repeater/Repeater.js index e5e185bd2c..66daf9663a 100644 --- a/assets/apps/customizer-controls/src/repeater/Repeater.js +++ b/assets/apps/customizer-controls/src/repeater/Repeater.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { Button } from '@wordpress/components'; import { ReactSortable } from 'react-sortablejs'; import { __ } from '@wordpress/i18n'; +import { useRef, useState } from '@wordpress/element'; const Repeater = ({ label, @@ -15,6 +16,20 @@ const Repeater = ({ }) => { const itemFields = Object.keys(newItemFields).length > 0 ? newItemFields : fields; + // Track active item by a stable key (slug or generated) independent of index. + const activeItemKeyRef = useRef(null); + const [, forceUpdate] = useState({}); + + // Ensure each existing item has a stable internal key for keyboard tracking (preserved across moves). + value.forEach((obj) => { + if (!obj.__kbKey) { + obj.__kbKey = + obj.slug || + 'kb-' + + Date.now().toString(36) + + Math.random().toString(36).slice(2, 8); + } + }); const handleToggle = (index) => { const newValue = [...value]; @@ -56,6 +71,12 @@ const Repeater = ({ newItem[field] = itemFields[field].default; } + // Assign stable key before pushing so initial activation works. + newItem.__kbKey = + newItem.slug || + 'kb-' + + Date.now().toString(36) + + Math.random().toString(36).slice(2, 8); newValue.push(newItem); onUpdate(newValue); }; @@ -72,18 +93,24 @@ const Repeater = ({ onUpdate(newValue); }; + const handleMove = (fromIndex, toIndex) => { + const newValue = [...value]; + const [movedItem] = newValue.splice(fromIndex, 1); + newValue.splice(toIndex, 0, movedItem); + setList(newValue); + }; const setList = (l) => { + const allowed = [ + ...Object.keys(itemFields), + 'title', + 'visibility', + 'blocked', + 'slug', + '__kbKey', // preserve stable keyboard key + ]; const final = l.map((i) => { Object.keys(i).forEach((k) => { - if ( - ![ - ...Object.keys(itemFields), - 'title', - 'visibility', - 'blocked', - 'slug', - ].includes(k) - ) { + if (!allowed.includes(k)) { delete i[k]; } }); @@ -112,6 +139,7 @@ const Repeater = ({ handle=".handle" > {value.map((val, index) => { + const reactKey = val.__kbKey; return ( handleContentChange(index, newItemValue) } onRemove={handleRemove} index={index} + totalItems={value.length} + onMove={handleMove} + isKeyboardActive={ + activeItemKeyRef.current === val.__kbKey + } + onKeyboardActiveChange={(active) => { + activeItemKeyRef.current = active + ? val.__kbKey + : null; + forceUpdate({}); + }} /> ); })} diff --git a/assets/apps/customizer-controls/src/repeater/RepeaterItem.js b/assets/apps/customizer-controls/src/repeater/RepeaterItem.js index 7a61dbfe0e..df4138239a 100644 --- a/assets/apps/customizer-controls/src/repeater/RepeaterItem.js +++ b/assets/apps/customizer-controls/src/repeater/RepeaterItem.js @@ -7,21 +7,30 @@ import classnames from 'classnames'; import PropTypes from 'prop-types'; import { VisibilityIcon, HiddenIcon } from '../common'; +import useKeyboardSorting from '../common/useKeyboardSorting'; import RepeaterItemContent from './RepeaterItemContent'; -const Handle = () => ( - - - -); +const Handle = ({ handleRef, onKeyDown, onBlur, isActive }) => { + return ( + + + + ); +}; const RepeaterItem = ({ fields, @@ -31,9 +40,21 @@ const RepeaterItem = ({ onToggle, onContentChange, onRemove, + totalItems, + onMove, + isKeyboardActive, + onKeyboardActiveChange, }) => { const [expanded, setExpanded] = useState(false); + const { handleRef, handleKeyDown, handleBlur } = useKeyboardSorting( + itemIndex, + totalItems, + onMove, + isKeyboardActive, + onKeyboardActiveChange + ); + const itemLabel = () => { let label = __('Item', 'neve'); if (value[itemIndex].title) { @@ -60,7 +81,12 @@ const RepeaterItem = ({ })} >
- + {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' - )} -
-
-
-
- - -
-
-
- ); -}; - 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' + )} +

+
+ +
+
+ + {/* 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}

+
+ +
+ ); +}; + +// 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 && ( - + )}
); 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 && ( + + )} +
{(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 && ( + + )} +
{ 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' + )} +
+
+
+
+ + +
+
+
+ ); +}; + 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 ( +
+
+ + +
+
+
+ ); +}; 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 ( -
-
-
-
-
- ); - })} + {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 = ''; + // 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', + ], + ]; + + ?> + + 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(), + ], }); });