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 = ({
{ ); }; +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) { @@ -355,6 +408,7 @@ window.wp.customize.bind('ready', () => { initBlogPageFocus(); initSearchCustomizer(); initLocalGoogleFonts(); + initStyleBookButton(); previewScrollToTopChanges(); }); 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 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/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/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/inc/core/core_loader.php b/inc/core/core_loader.php index f82bb0c665..462bcceab9 100644 --- a/inc/core/core_loader.php +++ b/inc/core/core_loader.php @@ -97,6 +97,7 @@ private function define_modules() { 'Views\Content_None', 'Views\Content_404', 'Views\Breadcrumbs', + 'Views\Style_Book', 'Views\Scroll_To_Top', 'Views\Layouts\Layout_Container', 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', + ], + ]; + + ?> + +