├── .gitattributes ├── src ├── images │ ├── ollie-video-menu.webp │ ├── ollie-video-mobile.webp │ ├── ollie-video-dropdown.webp │ └── ollie-video-menu-designer.webp ├── utils │ └── template-utils.js ├── blocks │ └── mega-menu │ │ ├── block.json │ │ ├── style.scss │ │ ├── edit.scss │ │ ├── index.js │ │ ├── view.scss │ │ ├── render.php │ │ ├── edit.js │ │ └── view.js ├── mobile-menu │ ├── mobile-menu-color-controls.js │ └── navigation-edit.js ├── components │ ├── TemplateHelpText.js │ ├── TemplateSelector.js │ ├── MenuDesignerGuide.js │ └── TemplatePreviewModal.js └── hooks │ └── useTemplateCreation.js ├── .editorconfig ├── .gitignore ├── package.json ├── languages └── ollie-menu-designer.pot ├── includes ├── omd-preview.php └── omd-mobile-menu-filter.php ├── ollie-menu-designer.php ├── .github └── workflows │ └── create-release.yml ├── CLAUDE.md ├── README.md ├── readme.txt └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/images/ollie-video-menu.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OllieWP/ollie-menu-designer/HEAD/src/images/ollie-video-menu.webp -------------------------------------------------------------------------------- /src/images/ollie-video-mobile.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OllieWP/ollie-menu-designer/HEAD/src/images/ollie-video-mobile.webp -------------------------------------------------------------------------------- /src/images/ollie-video-dropdown.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OllieWP/ollie-menu-designer/HEAD/src/images/ollie-video-dropdown.webp -------------------------------------------------------------------------------- /src/images/ollie-video-menu-designer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OllieWP/ollie-menu-designer/HEAD/src/images/ollie-video-menu-designer.webp -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # It's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods. 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | 22 | # Logs and databases # 23 | ###################### 24 | logs 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | ehthumbs.db 37 | Thumbs.db 38 | 39 | # NPM # 40 | ####### 41 | node_modules/ 42 | 43 | # Claude # 44 | ############ 45 | .claude/ 46 | Sites 47 | 48 | # Zip Scripts # 49 | ############### 50 | zip-plugin.sh 51 | zip 52 | *.zip 53 | 54 | # Config Files # 55 | ############### 56 | package-lock.json 57 | .editorconfig 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ollie-menu-designer", 3 | "version": "0.2.0", 4 | "description": "Design custom mega menus and mobile menus for WordPress.", 5 | "author": "Mike McAlister", 6 | "license": "GPL-3.0-or-later", 7 | "main": "build/index.js", 8 | "scripts": { 9 | "build": "wp-scripts build --experimental-modules", 10 | "format": "wp-scripts format", 11 | "lint:css": "wp-scripts lint-style", 12 | "lint:js": "wp-scripts lint-js", 13 | "lint:js:src": "wp-scripts lint-js ./src", 14 | "lint:js:src:fix": "wp-scripts lint-js ./src --fix", 15 | "start": "wp-scripts start --experimental-modules", 16 | "packages-update": "wp-scripts packages-update", 17 | "update-pot": "wp i18n make-pot . languages/menu-designer.pot --exclude=src,build,node_modules", 18 | "plugin-zip": "npm run plugin-zip && copyfiles --verbose menu-designer.zip _playground/ && rm menu-designer.zip" 19 | }, 20 | "devDependencies": { 21 | "@wordpress/scripts": "^30.27.0", 22 | "copyfiles": "^2.4.1" 23 | }, 24 | "dependencies": { 25 | "@wordpress/icons": "^11.1.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/template-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Template utility functions and constants 3 | */ 4 | 5 | // Constants 6 | export const PREVIEW_WRAPPER_CLASS = '.mega-menu-preview-wrapper'; 7 | export const DEFAULT_IFRAME_HEIGHT = '600px'; 8 | export const MODAL_HEADER_OFFSET = 120; 9 | export const IFRAME_MEASURE_HEIGHT = '2000px'; 10 | export const REM_TO_PX = 16; 11 | 12 | /** 13 | * Calculate CSS clamp() value in pixels 14 | * @param {number} min - Minimum value in rem 15 | * @param {number} max - Maximum value in rem 16 | * @param {number} viewportRatio - Viewport ratio (e.g., 0.04 for 4vw) 17 | * @returns {number} Calculated padding in pixels 18 | */ 19 | export const calculateClampValue = ( min, max, viewportRatio ) => { 20 | const minPx = min * REM_TO_PX; 21 | const maxPx = max * REM_TO_PX; 22 | const viewportPx = window.innerWidth * viewportRatio; 23 | return Math.max( minPx, Math.min( viewportPx, maxPx ) ); 24 | }; 25 | 26 | /** 27 | * Get site URL with correct protocol 28 | * @param {string} url - Site URL 29 | * @returns {string} URL with correct protocol 30 | */ 31 | export const getSecureUrl = ( url ) => { 32 | if ( ! url ) return ''; 33 | return window.location.protocol === 'https:' 34 | ? url.replace( /^http:\/\//, 'https://' ) 35 | : url; 36 | }; -------------------------------------------------------------------------------- /languages/ollie-menu-designer.pot: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Mike McAlister 2 | # This file is distributed under the same license as the Ollie Menu Designer plugin. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: Ollie Menu Designer 0.1.7\n" 6 | "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/ollie-menu-designer\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: LANGUAGE \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "POT-Creation-Date: 2025-08-29T04:05:10+00:00\n" 13 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 | "X-Generator: WP-CLI 2.4.0\n" 15 | "X-Domain: ollie-menu-designer\n" 16 | 17 | #. Plugin Name of the plugin 18 | msgid "Ollie Menu Designer" 19 | msgstr "" 20 | 21 | #. Description of the plugin 22 | msgid "Design stunning mobile navigation and dropdown menus in minutes using the native WordPress block editor — no coding required." 23 | msgstr "" 24 | 25 | #. Author of the plugin 26 | msgid "Mike McAlister" 27 | msgstr "" 28 | 29 | #: includes/preview.php:23 30 | msgid "You do not have permission to preview menus." 31 | msgstr "" 32 | 33 | #: includes/preview.php:31 34 | msgid "No menu specified." 35 | msgstr "" 36 | 37 | #: includes/preview.php:77 38 | msgid "Template part not found." 39 | msgstr "" 40 | 41 | #: ollie-menu-designer.php:51 42 | msgid "Menu templates are used to create dropdown menus and mobile menus." 43 | msgstr "" 44 | 45 | #: ollie-menu-designer.php:53 46 | msgid "Menu" 47 | msgstr "" 48 | -------------------------------------------------------------------------------- /src/blocks/mega-menu/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 3, 4 | "name": "ollie/mega-menu", 5 | "version": "0.1.0", 6 | "title": "Dropdown Menu", 7 | "category": "design", 8 | "description": "Add a unique drop down menu to your navigation.", 9 | "parent": [ "core/navigation" ], 10 | "example": {}, 11 | "attributes": { 12 | "label": { 13 | "type": "string" 14 | }, 15 | "description": { 16 | "type": "string" 17 | }, 18 | "title": { 19 | "type": "string" 20 | }, 21 | "menuSlug": { 22 | "type": "string" 23 | }, 24 | "showOnHover": { 25 | "type": "boolean", 26 | "default": false 27 | }, 28 | "url": { 29 | "type": "string" 30 | }, 31 | "disableWhenCollapsed": { 32 | "type": "boolean" 33 | }, 34 | "collapsedUrl": { 35 | "type": "string" 36 | }, 37 | "justifyMenu": { 38 | "type": "string" 39 | }, 40 | "width": { 41 | "type": "string" 42 | }, 43 | "customWidth": { 44 | "type": "number", 45 | "default": 600 46 | }, 47 | "topSpacing": { 48 | "type": "number", 49 | "default": 0 50 | } 51 | }, 52 | "supports": { 53 | "html": false, 54 | "interactivity": true, 55 | "reusable": false, 56 | "typography": { 57 | "fontSize": true, 58 | "lineHeight": true, 59 | "__experimentalFontFamily": true, 60 | "__experimentalFontWeight": true, 61 | "__experimentalFontStyle": true, 62 | "__experimentalTextTransform": true, 63 | "__experimentalTextDecoration": true, 64 | "__experimentalLetterSpacing": true, 65 | "__experimentalDefaultControls": { 66 | "fontSize": true 67 | } 68 | }, 69 | "__experimentalSlashInserter": true 70 | }, 71 | "textdomain": "menu-designer", 72 | "editorScript": "file:./index.js", 73 | "editorStyle": "file:./index.css", 74 | "style": "file:./style-index.css", 75 | "render": "file:./render.php", 76 | "viewScriptModule": "file:./view.js", 77 | "viewStyle": "file:./index.css" 78 | } 79 | -------------------------------------------------------------------------------- /src/blocks/mega-menu/style.scss: -------------------------------------------------------------------------------- 1 | // Styles for both the Editor and the front end. 2 | 3 | // Screen reader only text 4 | .sr-only { 5 | position: absolute; 6 | width: 1px; 7 | height: 1px; 8 | padding: 0; 9 | margin: -1px; 10 | overflow: hidden; 11 | clip: rect(0, 0, 0, 0); 12 | white-space: nowrap; 13 | border: 0; 14 | } 15 | 16 | .wp-block-ollie-mega-menu { 17 | .wp-block-ollie-mega-menu__toggle { 18 | background-color: initial; 19 | border: none; 20 | color: currentColor; 21 | cursor: pointer; 22 | font-family: inherit; 23 | font-size: inherit; 24 | font-style: inherit; 25 | font-weight: inherit; 26 | line-height: inherit; 27 | padding: 0; 28 | text-align: left; 29 | text-transform: inherit; 30 | 31 | .wp-block-ollie-mega-menu__toggle-icon { 32 | align-self: center; 33 | display: inline-block; 34 | font-size: inherit; 35 | height: 0.6em; 36 | line-height: 0; 37 | margin-left: 0.25em; 38 | padding: 0; 39 | width: 0.6em; 40 | 41 | svg { 42 | stroke: currentColor; 43 | display: inline-block; 44 | height: inherit; 45 | margin-top: 0.075em; 46 | width: inherit; 47 | transition: all .125s linear; 48 | } 49 | } 50 | 51 | &[aria-expanded=true] { 52 | .wp-block-ollie-mega-menu__toggle-icon { 53 | svg { 54 | transform: rotate(180deg); 55 | } 56 | } 57 | 58 | &~.wp-block-ollie-mega-menu__menu-container { 59 | opacity: 1; 60 | overflow: visible; 61 | visibility: visible; 62 | } 63 | } 64 | } 65 | } 66 | 67 | .wp-block-navigation:has(.wp-block-ollie-mega-menu) .wp-block-navigation__submenu-icon { 68 | svg { 69 | transition: all .125s linear; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/blocks/mega-menu/edit.scss: -------------------------------------------------------------------------------- 1 | // Editor specific styles. 2 | .ollie-mega-menu__layout-panel { 3 | .components-h-stack { 4 | .components-base-control, 5 | .components-toggle-group-control, 6 | .components-base-control__field { 7 | margin-bottom: 0; 8 | } 9 | } 10 | 11 | // When justification is disabled for wide/full width 12 | .block-editor-hooks__flex-layout-justification-controls.is-disabled { 13 | // Apply opacity to left and right options 14 | &> div:first-child, 15 | &> div:last-child { 16 | opacity: 0.5; 17 | cursor: not-allowed; 18 | } 19 | } 20 | } 21 | 22 | .ollie-mega-menu__layout-help { 23 | font-size: 12px; 24 | color: rgb(117, 117, 117); 25 | margin-top: 8px !important; 26 | } 27 | 28 | .menu-designer-guide { 29 | width: 800px; 30 | max-height: none !important; 31 | 32 | .components-modal__header .components-button:hover svg { 33 | fill: currentColor; 34 | } 35 | 36 | .components-guide__container { 37 | padding: 40px; 38 | } 39 | 40 | h2 + p { 41 | margin-top: 0; 42 | } 43 | 44 | .menu-designer-guide-video { 45 | position: relative; 46 | padding-bottom: 56.25%; 47 | height: 0; 48 | overflow: hidden; 49 | max-width: 100%; 50 | margin-top: 10px; 51 | border-radius: 8px; 52 | 53 | iframe { 54 | position: absolute; 55 | top: 0; 56 | left: 0; 57 | width: 100%; 58 | height: 100%; 59 | } 60 | } 61 | 62 | .components-guide__page-control { 63 | order: 10; 64 | display: none; 65 | } 66 | 67 | .components-guide__footer { 68 | margin-bottom: 0; 69 | margin-top: 30px; 70 | } 71 | 72 | .components-guide__forward-button, 73 | .components-guide__finish-button { 74 | right: 0; 75 | } 76 | 77 | .components-guide__back-button { 78 | left: 0; 79 | } 80 | } 81 | 82 | .menu-designer-guide-buttons { 83 | margin-bottom: 16px; 84 | background: color-mix(in srgb, var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)) 4%, #0000); 85 | padding: 3px 0px 3px 10px; 86 | border-radius: 5px; 87 | 88 | button.components-button:first-child { 89 | padding-left: 0; 90 | gap: 8px; 91 | text-decoration: none; 92 | justify-content: start !important; 93 | 94 | &:hover { 95 | text-decoration: underline; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/blocks/mega-menu/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { registerBlockType } from '@wordpress/blocks'; 5 | import { addFilter } from '@wordpress/hooks'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import './style.scss'; 11 | import './view.scss'; 12 | import Edit from './edit'; 13 | import metadata from './block.json'; 14 | import '../../mobile-menu/navigation-edit'; 15 | 16 | const megaMenuIcon = ( 17 | 24 | 25 | 26 | ); 27 | 28 | /** 29 | * Every block starts by registering a new block type definition. 30 | * 31 | * @see https://developer.wordpress.org/block-editor/developers/block-api/#registering-a-block 32 | */ 33 | registerBlockType( metadata.name, { 34 | icon: megaMenuIcon, 35 | edit: Edit, 36 | } ); 37 | 38 | /** 39 | * Make the Mega Menu Block available to Navigation blocks. 40 | * 41 | * @since 0.1.0 42 | * 43 | * @param {Object} blockSettings The original settings of the block. 44 | * @param {string} blockName The name of the block being modified. 45 | * @return {Object} The modified settings for the Navigation block or the original settings for other blocks. 46 | */ 47 | const addToNavigation = ( blockSettings, blockName ) => { 48 | if ( blockName === 'core/navigation' ) { 49 | return { 50 | ...blockSettings, 51 | allowedBlocks: [ 52 | ...( blockSettings.allowedBlocks ?? [] ), 53 | 'ollie/mega-menu', 54 | ], 55 | }; 56 | } 57 | return blockSettings; 58 | }; 59 | addFilter( 60 | 'blocks.registerBlockType', 61 | 'ollie-mega-menu-add-to-navigation', 62 | addToNavigation 63 | ); 64 | -------------------------------------------------------------------------------- /src/mobile-menu/mobile-menu-color-controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | import { 6 | withColors, 7 | __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, 8 | __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, 9 | } from '@wordpress/block-editor'; 10 | 11 | const MobileMenuColorControls = ({ 12 | clientId, 13 | mobileMenuBackgroundColor, 14 | mobileIconBackgroundColor, 15 | mobileIconColor, 16 | setMobileMenuBackgroundColor, 17 | setMobileIconBackgroundColor, 18 | setMobileIconColor, 19 | }) => { 20 | const colorSettings = [ 21 | { 22 | value: mobileMenuBackgroundColor?.color, 23 | onChange: setMobileMenuBackgroundColor, 24 | label: __( 'Mobile menu background', 'ollie-menu-designer' ), 25 | resetAllFilter: () => ({ 26 | mobileMenuBackgroundColor: undefined, 27 | customMobileMenuBackgroundColor: undefined, 28 | }), 29 | }, 30 | { 31 | value: mobileIconBackgroundColor?.color, 32 | onChange: setMobileIconBackgroundColor, 33 | label: __( 'Mobile toggle background', 'ollie-menu-designer' ), 34 | resetAllFilter: () => ({ 35 | mobileIconBackgroundColor: undefined, 36 | customMobileIconBackgroundColor: undefined, 37 | }), 38 | }, 39 | { 40 | value: mobileIconColor?.color, 41 | onChange: setMobileIconColor, 42 | label: __( 'Mobile toggle icon', 'ollie-menu-designer' ), 43 | resetAllFilter: () => ({ 44 | mobileIconColor: undefined, 45 | customMobileIconColor: undefined, 46 | }), 47 | }, 48 | ]; 49 | 50 | const colorGradientSettings = useMultipleOriginColorsAndGradients(); 51 | 52 | if ( ! colorGradientSettings.hasColorsOrGradients ) { 53 | return null; 54 | } 55 | 56 | return ( 57 | <> 58 | { colorSettings.map( 59 | ( { onChange, label, value, resetAllFilter } ) => ( 60 | 77 | ) 78 | ) } 79 | 80 | ); 81 | }; 82 | 83 | export default withColors( 84 | { mobileMenuBackgroundColor: 'color' }, 85 | { mobileIconBackgroundColor: 'color' }, 86 | { mobileIconColor: 'color' } 87 | )( MobileMenuColorControls ); 88 | -------------------------------------------------------------------------------- /src/components/TemplateHelpText.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __, sprintf } from '@wordpress/i18n'; 5 | import { createInterpolateElement } from '@wordpress/element'; 6 | 7 | /** 8 | * Template Help Text Component 9 | * Generates help text with links for template creation 10 | * 11 | * @param {Object} props Component props 12 | * @param {boolean} props.hasTemplates - Whether templates exist 13 | * @param {string} props.templateArea - Template area name 14 | * @param {string} props.siteUrl - Site URL for links 15 | * @param {string} props.adminUrl - Admin URL for links 16 | * @param {Function} props.onCreateClick - Callback when create is clicked 17 | * @param {boolean} props.isCreating - Whether template is being created 18 | */ 19 | export default function TemplateHelpText( { 20 | hasTemplates, 21 | templateArea, 22 | siteUrl, 23 | adminUrl, 24 | onCreateClick, 25 | isCreating = false, 26 | } ) { 27 | if ( hasTemplates ) { 28 | return createInterpolateElement( 29 | sprintf( 30 | __( 'Select a menu to use as a dropdown or create a new one in the Site Editor.', 'ollie-menu-designer' ) 31 | ), 32 | { 33 | create: ( 34 | { 37 | e.preventDefault(); 38 | if ( ! isCreating ) { 39 | onCreateClick(); 40 | } 41 | } } 42 | style={{ 43 | textDecoration: 'underline', 44 | cursor: isCreating ? 'default' : 'pointer', 45 | opacity: isCreating ? 0.6 : 1 46 | }} 47 | /> 48 | ), 49 | editor: ( 50 | 56 | ), 57 | } 58 | ); 59 | } 60 | 61 | // Special case for menu templates 62 | const noTemplatesMessage = templateArea === 'menu' 63 | ? __( 'No menus found. Create your first menu.', 'ollie-menu-designer' ) 64 | : sprintf( 65 | __( 'No %s templates found. Create your first template.', 'ollie-menu-designer' ), 66 | templateArea 67 | ); 68 | 69 | return createInterpolateElement( 70 | noTemplatesMessage, 71 | { 72 | a: ( 73 | { 76 | e.preventDefault(); 77 | if ( ! isCreating ) { 78 | onCreateClick(); 79 | } 80 | } } 81 | style={{ 82 | textDecoration: 'underline', 83 | cursor: isCreating ? 'default' : 'pointer', 84 | opacity: isCreating ? 0.6 : 1 85 | }} 86 | /> 87 | ), 88 | } 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /includes/omd-preview.php: -------------------------------------------------------------------------------- 1 | ' . wp_strip_all_tags( $styles ) . ''; 42 | } 43 | } 44 | }, 20 ); 45 | 46 | // Set up a minimal HTML page for preview 47 | ?> 48 | 49 | > 50 | 51 | 52 | 53 | 54 | 69 | 70 | > 71 |
72 | ' . esc_html__( 'Template part not found.', 'ollie-menu-designer' ) . '

'; 78 | } else { 79 | // Use the content property which contains the raw block content 80 | $content = $template_part->content; 81 | 82 | // Render the blocks - this will process all blocks including inline styles 83 | echo do_blocks( $content ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Block content is properly escaped by do_blocks 84 | } 85 | ?> 86 |
87 | 92 | 93 | 94 | is_block_editor() ) { 55 | return; 56 | } 57 | 58 | // Provide correct URLs for multisite environments 59 | ?> 60 | 66 | 'menu', 87 | 'area_tag' => 'div', 88 | 'description' => __( 'Menu templates are used to create dropdown menus and mobile menus.', 'ollie-menu-designer' ), 89 | 'icon' => 'layout', 90 | 'label' => __( 'Menu', 'ollie-menu-designer' ), 91 | ); 92 | 93 | return $areas; 94 | } 95 | 96 | add_filter( 'default_wp_template_part_areas', 'omd_template_part_areas' ); 97 | 98 | add_action( 'plugins_loaded', function () { 99 | // Include preview functionality 100 | require_once plugin_dir_path( __FILE__ ) . 'includes/omd-preview.php'; 101 | 102 | // Include mobile menu functionality 103 | require_once plugin_dir_path( __FILE__ ) . 'includes/omd-mobile-menu-filter.php'; 104 | } ); 105 | -------------------------------------------------------------------------------- /src/hooks/useTemplateCreation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __, sprintf } from '@wordpress/i18n'; 5 | import { useState } from '@wordpress/element'; 6 | import { useDispatch } from '@wordpress/data'; 7 | 8 | /** 9 | * Custom hook for creating template parts 10 | * 11 | * @param {Object} options Configuration options 12 | * @param {string} options.templateArea - The template area (e.g., 'menu', 'header', 'footer') 13 | * @param {string} options.baseSlug - Base slug for the template (e.g., 'mobile-menu', 'dropdown-menu') 14 | * @param {string} options.baseTitle - Base title for the template (e.g., 'Mobile Menu', 'Dropdown Menu') 15 | * @param {Array} options.existingTemplates - Array of existing templates to check for duplicates 16 | * @param {string} options.currentTheme - Current theme slug 17 | * @param {Function} options.onSuccess - Callback when template is created successfully 18 | * @returns {Object} Object containing createTemplate function and isCreating state 19 | */ 20 | export default function useTemplateCreation( { 21 | templateArea, 22 | baseSlug, 23 | baseTitle, 24 | existingTemplates = [], 25 | currentTheme, 26 | onSuccess = () => {}, 27 | } ) { 28 | const [ isCreating, setIsCreating ] = useState( false ); 29 | const { saveEntityRecord } = useDispatch( 'core' ); 30 | 31 | /** 32 | * Generate a unique slug and title 33 | */ 34 | const generateUniqueSlugAndTitle = () => { 35 | // Count existing templates with our base slug pattern 36 | const existingCount = existingTemplates?.filter( t => { 37 | if ( !t.area || t.area === templateArea ) { 38 | return t.slug === baseSlug || t.slug.startsWith( `${baseSlug}-` ); 39 | } 40 | return false; 41 | } ).length || 0; 42 | 43 | let slug = baseSlug; 44 | let displayNumber = existingCount + 1; 45 | 46 | // If this is not the first template, add a number 47 | if ( existingCount > 0 ) { 48 | // Find the next available slug 49 | let counter = existingCount; 50 | do { 51 | counter++; 52 | slug = `${baseSlug}-${counter}`; 53 | } while ( existingTemplates?.find( 54 | t => t.slug === slug && ( !t.area || t.area === templateArea ) 55 | ) ); 56 | displayNumber = counter; 57 | } 58 | 59 | // Generate the title 60 | const title = existingCount > 0 61 | ? sprintf( __( '%s %d', 'ollie-menu-designer' ), baseTitle, displayNumber ) 62 | : baseTitle; 63 | 64 | return { slug, title }; 65 | }; 66 | 67 | /** 68 | * Create a new template part 69 | */ 70 | const createTemplate = async () => { 71 | if ( isCreating ) return; 72 | 73 | setIsCreating( true ); 74 | 75 | try { 76 | const { slug, title } = generateUniqueSlugAndTitle(); 77 | 78 | const newTemplate = await saveEntityRecord( 'postType', 'wp_template_part', { 79 | slug: slug, 80 | theme: currentTheme || 'theme', 81 | type: 'wp_template_part', 82 | area: templateArea, 83 | title: { 84 | raw: title, 85 | rendered: title 86 | }, 87 | content: '', 88 | status: 'publish', 89 | } ); 90 | 91 | if ( newTemplate && newTemplate.id ) { 92 | // Call success callback with the new template 93 | onSuccess( newTemplate ); 94 | 95 | // Small delay to ensure the template is fully saved 96 | setTimeout( () => { 97 | // Navigate to the new template in the site editor 98 | const editUrl = `${window.menuDesignerData.adminUrl}site-editor.php?postId=${encodeURIComponent( newTemplate.id )}&postType=wp_template_part&canvas=edit`; 99 | window.open( editUrl, '_blank' ); 100 | }, 500 ); 101 | } else { 102 | console.error( 'Template was created but no ID was returned' ); 103 | } 104 | } catch ( error ) { 105 | console.error( 'Error creating template:', error ); 106 | alert( __( 'Failed to create template. Please try again.', 'ollie-menu-designer' ) ); 107 | } finally { 108 | setIsCreating( false ); 109 | } 110 | }; 111 | 112 | return { 113 | createTemplate, 114 | isCreating, 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /src/blocks/mega-menu/view.scss: -------------------------------------------------------------------------------- 1 | // Front-end specific styles. 2 | 3 | body:has(.wp-block-ollie-mega-menu__menu-container.menu-width-full) { 4 | overflow-x: hidden; 5 | } 6 | 7 | .wp-block-ollie-mega-menu__menu-container { 8 | height: auto; 9 | left: -1px; 10 | opacity: 0; 11 | overflow: hidden; 12 | position: absolute; 13 | top: 40px; 14 | transition: opacity .1s linear; 15 | visibility: hidden; 16 | z-index: 100; 17 | color: initial; 18 | background-color: initial; 19 | 20 | &.menu-width-content { 21 | max-width: var(--wp--style--global--content-size); 22 | width: var(--wp--style--global--content-size); 23 | } 24 | 25 | &.menu-width-wide { 26 | max-width: var(--wp--style--global--wide-size); 27 | width: var(--wp--style--global--wide-size); 28 | } 29 | 30 | &.menu-width-full { 31 | max-width: 100vw; 32 | width: 100vw; 33 | } 34 | 35 | .menu-container__close-button { 36 | align-items: center; 37 | -webkit-backdrop-filter: blur(16px) saturate(180%); 38 | backdrop-filter: blur(16px) saturate(180%); 39 | background-color: #ffffffba; 40 | border: none; 41 | border-radius: 999999px; 42 | cursor: pointer; 43 | display: flex; 44 | justify-content: center; 45 | opacity: 0; 46 | padding: 4px; 47 | position: absolute; 48 | right: 12px; 49 | text-align: center; 50 | top: 12px; 51 | transition: opacity .2s ease; 52 | z-index: 100; 53 | 54 | // Show the close button when focused (for keyboard navigation) 55 | &:focus { 56 | opacity: 1; 57 | } 58 | } 59 | 60 | // Show the close button when the mega menu is hovered. 61 | &:hover { 62 | .menu-container__close-button { 63 | opacity: 1; 64 | } 65 | } 66 | 67 | // This ensures navigation menu inside of mega menus display correctly. 68 | .is-responsive { 69 | display: flex; 70 | } 71 | } 72 | 73 | .wp-block-navigation__responsive-container.is-menu-open .wp-block-ollie-mega-menu:has(.wp-block-ollie-mega-menu__toggle[aria-expanded="true"]) .menu-container__close-button { 74 | opacity: 1; 75 | visibility: visible; 76 | } 77 | 78 | @media (min-width: 600px) { 79 | // Hide close button when using hover mode 80 | .wp-block-navigation-item[data-wp-context*='"showOnHover": true'] { 81 | .menu-container__close-button { 82 | display: none; 83 | } 84 | } 85 | } 86 | 87 | // Change the position of the menu depending on the menu alignment (override Navigation block). 88 | .wp-block-navigation { 89 | .wp-block-ollie-mega-menu__menu-container { 90 | &.menu-justified-left { 91 | left: -1px; 92 | right: unset; 93 | } 94 | 95 | &.menu-justified-right { 96 | left: unset; 97 | right: -1px; 98 | } 99 | 100 | &.menu-justified-center { 101 | right: unset; 102 | 103 | &.menu-width-content { 104 | left: calc( ( -1 * var(--wp--style--global--content-size) / 2 ) + 50% ); 105 | } 106 | } 107 | } 108 | } 109 | 110 | // If there is a collapsed link, start hidden. 111 | .wp-block-navigation__responsive-container:not(.is-menu-open) { 112 | .wp-block-ollie-mega-menu__collapsed-link { 113 | display: none; 114 | } 115 | } 116 | 117 | // If a navigation modal is open, check if the mega menu should be displayed. 118 | .wp-block-navigation__responsive-container.is-menu-open { 119 | .wp-block-ollie-mega-menu { 120 | &.disable-menu-when-collapsed { 121 | &:not(.has-collapsed-link) { 122 | display: none; 123 | } 124 | 125 | .wp-block-ollie-mega-menu__collapsed-link { 126 | display: inherit; 127 | } 128 | 129 | .wp-block-ollie-mega-menu__toggle, 130 | .wp-block-ollie-mega-menu__menu-container { 131 | display: none; 132 | } 133 | } 134 | } 135 | 136 | .wp-block-ollie-mega-menu__menu-container { 137 | position: fixed; 138 | top: 0 !important; 139 | left: 0 !important; 140 | width: 100% !important; 141 | height: 100% !important; 142 | z-index: 100; 143 | opacity: 1; 144 | overflow-x: hidden !important; 145 | overflow-y: scroll !important; 146 | } 147 | 148 | .admin-bar & .wp-block-ollie-mega-menu__menu-container { 149 | top: 46px !important; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release from Readme 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 2 16 | 17 | - name: Check if version changed 18 | id: version_check 19 | run: | 20 | # Get current version from readme.txt 21 | CURRENT_VERSION=$(grep -Po 'Stable tag: \K(.*)' readme.txt) 22 | echo "Current version: $CURRENT_VERSION" 23 | 24 | # Get previous version from readme.txt in the previous commit 25 | git checkout HEAD~1 readme.txt 26 | PREVIOUS_VERSION=$(grep -Po 'Stable tag: \K(.*)' readme.txt || echo "0.0.0") 27 | git checkout HEAD readme.txt 28 | echo "Previous version: $PREVIOUS_VERSION" 29 | 30 | # Compare versions 31 | if [ "$CURRENT_VERSION" = "$PREVIOUS_VERSION" ]; then 32 | echo "Version has not changed. Skipping release." 33 | echo "VERSION_CHANGED=false" >> $GITHUB_ENV 34 | else 35 | echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION" 36 | echo "VERSION_CHANGED=true" >> $GITHUB_ENV 37 | fi 38 | 39 | - name: Set up Node.js 40 | if: env.VERSION_CHANGED == 'true' 41 | uses: actions/setup-node@v3 42 | with: 43 | node-version: '18' 44 | 45 | - name: Install dependencies 46 | if: env.VERSION_CHANGED == 'true' 47 | run: npm install 48 | 49 | - name: Build plugin 50 | if: env.VERSION_CHANGED == 'true' 51 | run: npm run build 52 | 53 | - name: Extract version and changelog 54 | if: env.VERSION_CHANGED == 'true' 55 | id: extract_info 56 | run: | 57 | # Extract stable tag 58 | STABLE_TAG=$(grep -Po 'Stable tag: \K(.*)' readme.txt) 59 | echo "STABLE_TAG=$STABLE_TAG" >> $GITHUB_ENV 60 | 61 | # Extract latest changelog entry 62 | CHANGELOG_TITLE=$(grep -Po '= [0-9.]+ .*=' readme.txt | head -1) 63 | 64 | # Get all bullet points under the latest changelog entry 65 | CHANGELOG_START_LINE=$(grep -n "$CHANGELOG_TITLE" readme.txt | cut -d ':' -f 1) 66 | NEXT_SECTION_LINE=$(tail -n +$((CHANGELOG_START_LINE+1)) readme.txt | grep -n "=" | head -1 | cut -d ':' -f 1) 67 | 68 | if [ -z "$NEXT_SECTION_LINE" ]; then 69 | # If there's no next section, read until end of changelog section 70 | NEXT_SECTION_LINE=$(tail -n +$((CHANGELOG_START_LINE+1)) readme.txt | grep -n "==" | head -1 | cut -d ':' -f 1) 71 | fi 72 | 73 | if [ -z "$NEXT_SECTION_LINE" ]; then 74 | # If still no section found, read next 20 lines 75 | NEXT_SECTION_LINE=20 76 | fi 77 | 78 | CHANGELOG_CONTENT=$(tail -n +$((CHANGELOG_START_LINE+1)) readme.txt | head -n $((NEXT_SECTION_LINE-1))) 79 | 80 | # Format the release notes 81 | RELEASE_NOTES="${CHANGELOG_TITLE}\n${CHANGELOG_CONTENT}" 82 | echo "RELEASE_NOTES<> $GITHUB_ENV 83 | echo -e "$RELEASE_NOTES" >> $GITHUB_ENV 84 | echo "EOF" >> $GITHUB_ENV 85 | 86 | - name: Create ZIP archive 87 | if: env.VERSION_CHANGED == 'true' 88 | run: | 89 | # Create the zip file directly, excluding development files 90 | zip -r menu-designer.zip . \ 91 | -x "*.git*" \ 92 | -x "*.github*" \ 93 | -x "node_modules/*" \ 94 | -x "src/*" \ 95 | -x "Sites/*" \ 96 | -x "*.log" \ 97 | -x ".DS_Store" \ 98 | -x "package-lock.json" \ 99 | -x "*.sh" \ 100 | -x "CLAUDE.md" \ 101 | -x ".editorconfig" \ 102 | -x ".gitignore" \ 103 | -x ".gitattributes" \ 104 | -x ".claude/*" 105 | 106 | - name: Create Release 107 | if: env.VERSION_CHANGED == 'true' 108 | id: create_release 109 | uses: softprops/action-gh-release@v1 110 | with: 111 | tag_name: v${{ env.STABLE_TAG }} 112 | name: Menu Designer v${{ env.STABLE_TAG }} 113 | body: ${{ env.RELEASE_NOTES }} 114 | draft: false 115 | prerelease: false 116 | files: menu-designer.zip 117 | env: 118 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 119 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | Menu Designer is a WordPress Block Plugin that enhances WordPress navigation with custom mega menus and mobile menus. The plugin provides: 8 | 9 | - **Mega Menu Block**: Adds expandable mega menu functionality to navigation blocks 10 | - **Mobile Menu Feature**: Allows custom template parts to replace default mobile navigation 11 | 12 | The plugin uses modern WordPress block development practices with React, the WordPress Block Editor APIs, and the WordPress Interactivity API for frontend behavior. 13 | 14 | ## Development Commands 15 | 16 | ```bash 17 | # Start development server with hot reloading 18 | npm start 19 | 20 | # Build for production 21 | npm run build 22 | 23 | # Lint JavaScript files 24 | npm run lint:js 25 | npm run lint:js:src # Lint only src directory 26 | npm run lint:js:src:fix # Auto-fix linting issues in src 27 | 28 | # Lint CSS/SCSS files 29 | npm run lint:css 30 | 31 | # Format code 32 | npm run format 33 | 34 | # Generate translation files 35 | npm run update-pot 36 | 37 | # Create plugin zip (for distribution) 38 | npm run plugin-zip 39 | ``` 40 | 41 | ## Architecture and Key Components 42 | 43 | ### Plugin Structure 44 | ``` 45 | src/ 46 | ├── blocks/ 47 | │ └── mega-menu/ # Mega menu block component 48 | ├── scripts/ 49 | │ └── mobile-menu/ # Mobile menu navigation extension 50 | ├── shared/ # Shared components (TemplateSelector) 51 | ├── block.json # Block metadata 52 | ├── index.js # Main entry point 53 | ├── render.php # Server-side rendering 54 | ├── view.js # Frontend interactivity 55 | └── *.scss # Styles 56 | ``` 57 | 58 | ### Block Registration Flow 59 | 1. **PHP Entry** (`mega-menu-block.php`): Registers the block server-side and adds a custom "menu" template part area 60 | 2. **JavaScript Entry** (`src/index.js`): Registers the block client-side and integrates it with navigation blocks 61 | 3. **Block Definition** (`src/block.json`): Defines block metadata, attributes, and dependencies 62 | 63 | ### Frontend Rendering 64 | - **Server-side**: `src/render.php` handles PHP rendering using WordPress template parts 65 | - **Client-side**: `src/view.js` implements interactivity using WordPress Interactivity API for menu behaviors (open/close, keyboard navigation, focus management) 66 | 67 | ### Editor Interface 68 | - **Mega Menu Block**: `src/blocks/mega-menu/edit.js` provides the block editor UI 69 | - **Mobile Menu**: `src/scripts/mobile-menu/navigation-edit.js` extends navigation block settings 70 | - **Shared Components**: `src/shared/TemplateSelector.js` used by both features 71 | 72 | ### Key Features Implementation 73 | - **Template Part Integration**: Both mega menu and mobile menu content use WordPress template parts, allowing flexible design through the Site Editor 74 | - **Interactivity API**: Uses WordPress's modern approach for frontend JavaScript with stores, actions, and callbacks 75 | - **Responsive Behavior**: Includes edge detection and mobile menu handling with optional disable for mobile 76 | - **Mobile Menu Replacement**: Automatically replaces default navigation with custom template on mobile devices 77 | 78 | ## Important Development Notes 79 | 80 | 1. **ES Modules**: The build process uses `--experimental-modules` flag for ES module support in the WordPress scripts build tool 81 | 82 | 2. **Block Attributes**: 83 | - **Mega Menu Block**: 84 | - `menuSlug`: Template part slug reference 85 | - `width`: Menu width (content/wide/full/custom) 86 | - `justifyMenu`: Alignment (left/center/right) 87 | - `disableWhenCollapsed`: Mobile behavior toggle 88 | - `collapsedUrl`: Alternative URL for mobile devices 89 | - `showOnHover`: Open menu on mouse hover 90 | - `topSpacing`: Space between toggle and menu 91 | - **Navigation Block Extension**: 92 | - `mobileMenuSlug`: Template part for mobile menu 93 | 94 | 3. **Navigation Block Integration**: The plugin extends WordPress navigation blocks by adding the mega menu as a navigation item variation 95 | 96 | 4. **Focus Management**: The view.js implements comprehensive keyboard navigation and focus trapping for accessibility 97 | 98 | 5. **Styling**: Uses SCSS with separate files for editor (`edit.scss`) and frontend (`style.scss`) styles 99 | 100 | ## Development Principles 101 | 102 | - Always use WordPress core components first when creating components, adding UI to the block editor, settings to blocks, etc. 103 | - Always use WordPress core development standards and modern tooling -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ollie Menu Designer 2 | 3 | ### Create beautiful, content-rich mobile menus and dropdown menus in WordPress using the power of the block editor. 4 | 5 | [![Ollie Menu Designer Screenshot](https://olliewp.com/wp-content/uploads/2025/08/menu-designer-readme.webp)](https://olliewp.com/menu-designer) 6 | 7 | [![License](https://img.shields.io/badge/license-GPL--3.0%2B-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.html) 8 | [![WordPress](https://img.shields.io/badge/WordPress-6.0%2B-blue.svg)](https://wordpress.org/) 9 | [![PHP](https://img.shields.io/badge/PHP-7.4%2B-purple.svg)](https://php.net) 10 | 11 | ## Overview 12 | 13 | Create stunning, content-rich navigation menus using the WordPress block editor. [Ollie Menu Designer](https://olliewp.com/menu-designer) lets you build beautiful dropdown menus and mobile navigation with images, buttons, call-to-actions, and any other blocks – giving you the same creative freedom you have when designing your pages. 14 | 15 | Menu Designer puts you in complete control of how your menus look and function. Best of all, if you're using the [free Ollie theme](https://olliewp.com/download/), you'll get access to a collection of beautifully pre-designed menu templates to help you get started quickly. 16 | 17 | ## Features 18 | 19 | - **Visual Design** - Build your menus in the Site Editor with live preview 20 | - **Block-Based** - Use any WordPress block to create rich menu content 21 | - **Responsive** - Responsive patterns, intelligent edge detection, and optinal fallback URLs for mobile 22 | - **Smart Positioning** - Multiple alignment options and settings to customize dropdown positioning 23 | - **Accessible** - Follows best practices for dropdowns and mobile menus 24 | - **Performance First** - Menu assets load only when needed. 25 | 26 | ## Getting Started 27 | 28 | [![Ollie Menu Designer Tutorial](https://olliewp.com/wp-content/uploads/2025/08/menu-designer-tutorial-readme.webp)](https://youtu.be/UXWOafpBn38) 29 | 30 | [Check out our complete video walkthrough on YouTube](https://youtu.be/UXWOafpBn38) to learn how to create beautiful dropdown menus and mobile navigation with Ollie Menu Designer. 31 | 32 | ### Installation 33 | 34 | 1. Download the latest release or clone this repository 35 | 3. Activate the plugin through the WordPress admin 36 | 4. Start adding mobile menus and dropdown menus 37 | 38 | ### Adding Mobile Menus with Ollie Menu Designer 39 | 40 | Watch the [full video tutorial](https://youtu.be/UXWOafpBn38) for a detailed walkthrough. 41 | 42 | 1. Navigate to Appearance → Editor → Patterns and edit your Header template part 43 | 2. Click the Navigation block 44 | 3. In the Navigation block Settings tab, find the Mobile Menu panel 45 | 4. Click "Create a new one" or select from existing mobile menu templates 46 | 5. Choose from pre-designed Ollie patterns or build custom with blocks 47 | 6. Save your menu and select it in the Mobile Menu panel 48 | 7. Customize background colors and menu icon in the mobile menu settings 49 | 50 | ### Adding Dropdown Menus with Ollie Menu Designer 51 | 52 | Watch the [full video tutorial](https://youtu.be/UXWOafpBn38) for a detailed walkthrough. 53 | 54 | 1. Navigate to Appearance → Editor → Patterns and edit your Header template part 55 | 2. Click Add Block and search for "Dropdown Menu" 56 | 3. Name your dropdown and position it in your navigation 57 | 4. In the Dropdown Menu block Settings tab, find the dropdown menu panel 58 | 5. Click "Create a new one" or select from existing dropdown menu templates 59 | 6. Choose from pre-designed Ollie patterns or build custom with blocks 60 | 7. Save your menu and select it in the dropdown menu panel 61 | 8. Configure additional customization settings 62 | 63 | ## Adding Starter Patterns 64 | 65 | You can create custom starter patterns for menu templates to give users quick starting points for their menus. This is especially useful for theme developers who want to provide pre-designed menu layouts. 66 | 67 | ### How to Add Starter Patterns 68 | 69 | 1. Create a `/patterns` folder in your theme or plugin directory 70 | 2. Add your pattern files (PHP format) to this folder 71 | 3. In each pattern file, ensure you include the following in your pattern header: 72 | 73 | ```php 74 | /** 75 | * Title: My Mobile Menu Pattern 76 | * Slug: mytheme/my-mobile-pattern 77 | * Categories: menu 78 | * Block Types: core/template-part/menu 79 | */ 80 | ``` 81 | 82 | The key requirement is the `Block Types: core/template-part/menu` line - this ensures your pattern appears as an option when creating new menu template parts. 83 | 84 | ## License 85 | 86 | Menu Designer is licensed under the GPL v3 or later - see the [LICENSE](LICENSE) file for details. 87 | 88 | ## Credits 89 | 90 | Built with love by the [OllieWP](https://olliewp.com) team. Shout out to [Nick Diego](https://x.com/nickmdiego) who created the initial [proof of concept](https://github.com/ndiego/mega-menu-block) that Menu Designer is inspired by. 91 | -------------------------------------------------------------------------------- /src/components/TemplateSelector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | import { decodeEntities } from '@wordpress/html-entities'; 6 | import { useEntityRecords } from '@wordpress/core-data'; 7 | import { useSelect } from '@wordpress/data'; 8 | import { useState } from '@wordpress/element'; 9 | import { 10 | Button, 11 | ComboboxControl, 12 | __experimentalSpacer as Spacer, 13 | __experimentalHStack as HStack, 14 | } from '@wordpress/components'; 15 | import { seen, edit } from '@wordpress/icons'; 16 | 17 | /** 18 | * Internal dependencies 19 | */ 20 | import useTemplateCreation from '../hooks/useTemplateCreation'; 21 | import TemplatePreviewModal from '../components/TemplatePreviewModal'; 22 | import TemplateHelpText from '../components/TemplateHelpText'; 23 | import { getSecureUrl } from '../utils/template-utils'; 24 | 25 | /** 26 | * Template selector component for menu templates 27 | * 28 | * @param {Object} props Component props 29 | * @param {string} props.value Selected template slug 30 | * @param {Function} props.onChange Callback when template changes 31 | * @param {string} props.templateArea Template area (default: 'menu') 32 | * @param {string} props.label Label for the selector 33 | * @param {string} props.help Help text for the selector 34 | * @param {Object} props.previewOptions Preview modal options (width, customWidth, etc.) 35 | * @param {string} props.previewBackgroundColor Background color for preview 36 | */ 37 | export default function TemplateSelector( { 38 | value, 39 | onChange, 40 | templateArea = 'menu', 41 | label = __( 'Dropdown Menu', 'ollie-menu-designer' ), 42 | help = null, 43 | previewOptions = {}, 44 | previewBackgroundColor = null, 45 | } ) { 46 | const [ isPreviewOpen, setIsPreviewOpen ] = useState( false ); 47 | 48 | // Get site data and editor settings 49 | const { siteUrl, currentTheme, layout } = useSelect( ( select ) => { 50 | const { getSite, getCurrentTheme } = select( 'core' ); 51 | const editorSettings = select( 'core/editor' ).getEditorSettings(); 52 | return { 53 | siteUrl: getSite()?.url, 54 | currentTheme: getCurrentTheme()?.stylesheet, 55 | layout: editorSettings?.__experimentalFeatures?.layout, 56 | }; 57 | }, [] ); 58 | 59 | // Use multisite-aware URLs from localized data 60 | const actualSiteUrl = window.menuDesignerData?.siteUrl || siteUrl || window.location.origin; 61 | const adminUrl = window.menuDesignerData?.adminUrl || `${actualSiteUrl}/wp-admin/`; 62 | 63 | const secureSiteUrl = getSecureUrl( actualSiteUrl ); 64 | 65 | // Fetch all template parts 66 | const { hasResolved, records } = useEntityRecords( 67 | 'postType', 68 | 'wp_template_part', 69 | { per_page: -1 } 70 | ); 71 | 72 | // Filter templates by area 73 | const templateOptions = hasResolved && records 74 | ? records 75 | .filter( ( item ) => item.area === templateArea ) 76 | .map( ( item ) => ( { 77 | label: decodeEntities( item.title.rendered ), 78 | value: item.slug, 79 | } ) ) 80 | : []; 81 | 82 | const hasTemplates = templateOptions.length > 0; 83 | const isValidSelection = ! value || templateOptions.some( ( option ) => option.value === value ); 84 | 85 | // Use the shared template creation hook 86 | const baseSlug = templateArea === 'menu' ? 'dropdown-menu' : templateArea; 87 | const baseTitle = templateArea === 'menu' ? __( 'Dropdown Menu', 'ollie-menu-designer' ) : templateArea; 88 | 89 | const { createTemplate: createNewTemplate, isCreating } = useTemplateCreation( { 90 | templateArea, 91 | baseSlug, 92 | baseTitle, 93 | existingTemplates: records, 94 | currentTheme, 95 | onSuccess: ( newTemplate ) => { 96 | // Update the selected value when template is created 97 | onChange( newTemplate.slug ); 98 | }, 99 | } ); 100 | 101 | /** 102 | * Get current template label 103 | */ 104 | const getCurrentTemplateLabel = () => { 105 | const template = templateOptions.find( ( option ) => option.value === value ); 106 | return template?.label || value; 107 | }; 108 | 109 | return ( 110 | <> 111 | 125 | ) } 126 | /> 127 | 128 | { value && isValidSelection && ( 129 | <> 130 | 131 | 132 | 139 | 147 | 148 | 149 | 150 | ) } 151 | 152 | setIsPreviewOpen( false ) } 155 | templateSlug={ value } 156 | templateLabel={ getCurrentTemplateLabel() } 157 | siteUrl={ secureSiteUrl } 158 | previewOptions={ previewOptions } 159 | backgroundColor={ previewBackgroundColor } 160 | layout={ layout } 161 | /> 162 | 163 | ); 164 | } 165 | 166 | -------------------------------------------------------------------------------- /src/components/MenuDesignerGuide.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | import { useState, useEffect } from '@wordpress/element'; 6 | import { Button, Guide, __experimentalHStack as HStack } from '@wordpress/components'; 7 | import { closeSmall } from '@wordpress/icons'; 8 | 9 | /** 10 | * Ollie Menu Designer Guide Component 11 | * 12 | * Displays a guide modal with tutorial information about the Ollie Menu Designer plugin. 13 | * Can be used in both mega menu and mobile menu settings. 14 | * 15 | * @param {Object} props - Component props 16 | * @param {string} props.buttonText - Text for the button that opens the guide 17 | * @param {string} props.buttonStyle - Additional styles for the button 18 | * @return {Element} Guide button and modal 19 | */ 20 | export default function MenuDesignerGuide( { 21 | buttonText = __( 'Menu Designer Guide', 'ollie-menu-designer' ), 22 | buttonStyle = {} 23 | } ) { 24 | const [ isGuideOpen, setIsGuideOpen ] = useState( false ); 25 | const [ isDismissed, setIsDismissed ] = useState( false ); 26 | 27 | // Check localStorage on mount to see if guide was previously dismissed 28 | useEffect( () => { 29 | const dismissed = localStorage.getItem( 'ollieMenuDesignerGuideDismissed' ); 30 | if ( dismissed === 'true' ) { 31 | setIsDismissed( true ); 32 | } 33 | }, [] ); 34 | 35 | // Handle dismissing the guide 36 | const handleDismiss = () => { 37 | setIsDismissed( true ); 38 | localStorage.setItem( 'ollieMenuDesignerGuideDismissed', 'true' ); 39 | }; 40 | 41 | // Don't render anything if dismissed 42 | if ( isDismissed ) { 43 | return null; 44 | } 45 | 46 | return ( 47 | <> 48 | 54 | 64 |