Repository: lubusIN/visual-blueprint-builder
Pull request #67: feat: (#60) add onboarding modal with quick start options for new blueprints
Author: login: Yogesh-purohit
Created: 2025-05-23T07:06:01Z | Merged: 2025-05-28T05:58:15Z

### Summary

This PR introduces an onboarding modal that appears when users start a new post with no existing blocks. The modal offers three entry points: JSON, Gallery, Start Blank or Skip, significantly improving the user experience for blueprint creation.

In addition, this PR decouples the BlueprintSidebarSettings logic by extracting schema preparation and configuration handling into a separate utility. This allows for cleaner data flow, especially between OpenJson, Gallery, and the sidebar. Now, when data is submitted via those components, it directly integrates into the blueprint sidebar without additional transformation layers.

### Related Issue

Closes #60

### Checklist

- [x] Onboarding modal added with three primary actions and skip option
- [x] Schema preparation logic moved out of BlueprintSidebarSettings
- [x] UX and code maintainability improved

### Screen Recording

https://github.com/user-attachments/assets/1fbd144a-5f2d-4793-a5cc-80279e9778ad

Shared a video showcasing the updated behavior after addressing all feedback

https://github.com/user-attachments/assets/05942158-ccd9-4373-91ae-39473e6048e8





Linked issue #60: onboarding: add modal for quick action items to start blueprint
Currently, we show a blank blueprint canvas and an option in the sidebar to allow opening/importing JSON or opening/importing from the blueprint gallery. Though this works pretty well, we can simplify this UX  by adding an onboarding modal.

This modal will be shown once during the creation of the blueprint and show the following options:
- Open BluePrint
- From Gallery
- Start Blank

Option to skip the modal

![Image](https://github.com/user-attachments/assets/7ffd6988-d1f6-4c4e-b293-b57118f5ffef)


Diff:
diff --git a/src/editor/blueprint-gallery.js b/src/editor/blueprint-gallery.js
index a9af2f3..a01eddd 100644
--- a/src/editor/blueprint-gallery.js
+++ b/src/editor/blueprint-gallery.js
@@ -14,17 +14,20 @@ import {
     __experimentalHeading as Heading,
     __experimentalGrid as Grid,
     __experimentalVStack as VStack,
+    Spinner,
 } from '@wordpress/components';
 
 /**
  * Internal dependencies
  */
-import { handleBlueprintData } from './utils';
+import { handleBlueprintData, useBlueprintData } from './utils';
 
-function Gallery({ onSubmitData }) {
+function Gallery({ label = 'Gallery', icon = null, handleClose }) {
+    const { updateBlueprintConfig } = useBlueprintData();
     const { createNotice } = useDispatch(noticesStore);
     const [isModalOpen, setModalOpen] = useState(false);
     const [blueprintList, setBlueprintList] = useState(null);
+    const [importingBlueprint, setImportingBlueprint] = useState(null);
 
     /**
      * Fetches the list of blueprints from the remote JSON file.
@@ -59,14 +62,16 @@ function Gallery({ onSubmitData }) {
         },
         steps: []
     };
-    
+
     const fetchBlueprintDetails = async (blueprintName) => {
+        setImportingBlueprint(blueprintName);
+
         const blueprintUrl = `https://raw.githubusercontent.com/WordPress/blueprints/trunk/${blueprintName}`;
         try {
             const response = await fetch(blueprintUrl);
             if (!response.ok) throw new Error(`Failed to fetch blueprint details: ${response.statusText}`);
             const data = await response.json();
-    
+
             // Ensure default values for required properties
             const validatedData = {
                 preferredVersions: data.preferredVersions || defaultValues.preferredVersions,
@@ -74,17 +79,20 @@ function Gallery({ onSubmitData }) {
                 steps: data.steps || defaultValues.steps,
                 ...data, // Merge in other properties from the fetched data
             };
-    
+
             // Merge the updated steps and other data with default values
             const mergedData = {
                 ...defaultValues,
                 ...validatedData,
             };
-            
             // Pass the processed data to the handler
-            handleBlueprintData(mergedData, createNotice, onSubmitData);
+            await handleBlueprintData(mergedData, createNotice, updateBlueprintConfig);
         } catch (error) {
             createNotice('error', __('Error fetching blueprint from Gallery', 'wp-playground-blueprint-editor') + `: ${error.message}`);
+        } finally {
+            setImportingBlueprint(null); // Stop loader
+            setModalOpen(false);
+            if (handleClose) handleClose();
         }
     };
 
@@ -93,9 +101,15 @@ function Gallery({ onSubmitData }) {
             {/* Open modal button */}
             <Button
                 className='blueprint_gallery_json'
-                __next40pxDefaultSize
-                onClick={() => setModalOpen(true)}>
-                {__('Gallery', 'wp-playground-blueprint-editor')}
+                onClick={() => setModalOpen(true)}
+                icon={icon}
+                style={{
+                    height: '100%',
+                    flexDirection: 'column',
+                    padding: '13px'
+                }}
+            >
+                {__(label, 'wp-playground-blueprint-editor')}
             </Button>
 
             {/* Blueprint gallery modal */}
@@ -112,7 +126,6 @@ function Gallery({ onSubmitData }) {
                             {Object.entries(blueprintList).map(([blueprintName, blueprintDetails], index) => (
                                 <Card
                                     key={index}
-                                    elevation={3}
                                 >
                                     <CardBody style={{ height: '100%', justifyContent: 'space-between', display: 'flex', flexDirection: 'column' }}>
                                         {/* Blueprint Info */}
@@ -125,23 +138,31 @@ function Gallery({ onSubmitData }) {
                                             </Text>
                                             <Text
                                                 lineHeight={'1.5em'}
-                                                size={15}   
+                                                size={15}
                                                 color='#777'
-                                                style={{wordBreak: 'break-word'}}
+                                                style={{ wordBreak: 'break-word' }}
                                             >
                                                 {blueprintDetails.description}
                                             </Text>
-                                            </VStack>
+                                        </VStack>
                                         {/* Action Button */}
                                         <Button
                                             variant="secondary"
                                             style={{
                                                 borderRadius: '4px',
-                                                alignSelf: 'flex-end',   
+                                                alignSelf: 'flex-end',
+                                                marginTop: '16px'
                                             }}
                                             onClick={() => fetchBlueprintDetails(blueprintName)}
+                                            disabled={importingBlueprint === blueprintName}
                                         >
-                                            {__('Import', 'wp-playground-blueprint-editor')}
+                                            {importingBlueprint === blueprintName ? (
+                                                <>
+                                                    <Spinner /> {' '} {__('Importing...', 'wp-playground-blueprint-editor')}
+                                                </>
+                                            ) : (
+                                                __('Import', 'wp-playground-blueprint-editor')
+                                            )}
                                         </Button>
                                     </CardBody>
                                 </Card>
diff --git a/src/editor/index.js b/src/editor/index.js
index f2a2133..20b377c 100644
--- a/src/editor/index.js
+++ b/src/editor/index.js
@@ -1,2 +1,3 @@
 import './block-appender';
-import './sidebar';
\ No newline at end of file
+import './sidebar';
+import './onboarding'
\ No newline at end of file
diff --git a/src/editor/onboarding.js b/src/editor/onboarding.js
new file mode 100644
index 0000000..0c48aa1
--- /dev/null
+++ b/src/editor/onboarding.js
@@ -0,0 +1,72 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useState, useEffect } from '@wordpress/element';
+import {
+    Button,
+    Modal,
+    __experimentalGrid as Grid,
+} from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { registerPlugin } from '@wordpress/plugins';
+import { code, gallery, plus } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import Gallery from './blueprint-gallery';
+import OpenJson from './open-json';
+
+const OnboardingModal = () => {
+    const [showModal, setShowModal] = useState(false);
+
+    const { isNewPost, blocks } = useSelect((select) => {
+        const editor = select('core/editor');
+        return {
+            isNewPost: editor.isCleanNewPost(),
+            blocks: editor.getBlocks(),
+        };
+    }, []);
+
+    useEffect(() => {
+        if (isNewPost && blocks.length === 0) {
+            setShowModal(true);
+        }
+    }, [isNewPost, blocks]);
+
+    const handleClose = () => setShowModal(false);
+
+    if (!showModal) return null;
+
+    return (
+        <Modal
+            title={__('Start Blueprint From', 'wp-playground-blueprint-editor')}
+            onRequestClose={handleClose}
+            shouldCloseOnClickOutside
+            shouldCloseOnEsc
+            size="medium"
+        >
+            <Grid columns={3} spacing={4}>
+                <OpenJson label='JSON' icon={code} handleClose={handleClose} />
+                <Gallery label='Gallery' icon={gallery} handleClose={handleClose} />
+                <Button
+                    onClick={handleClose}
+                    icon={plus}
+                    style={{
+                        boxShadow: 'inset 0 0 0 1px #ccc',
+                        height: '100%',
+                        flexDirection: 'column',
+                        padding: '13px'
+                    }}
+                >
+                    {__('Blank', 'wp-playground-blueprint-editor')}
+                </Button>
+            </Grid>
+        </Modal>
+    );
+};
+
+registerPlugin('wp-blueprint-onboarding', {
+    render: OnboardingModal,
+});
\ No newline at end of file
diff --git a/src/editor/open-json.js b/src/editor/open-json.js
index 748359a..d498603 100644
--- a/src/editor/open-json.js
+++ b/src/editor/open-json.js
@@ -1,24 +1,32 @@
 import { __ } from '@wordpress/i18n';
-import {  useDispatch } from '@wordpress/data';
-import { FormFileUpload, DropZone } from '@wordpress/components';
+import { useDispatch } from '@wordpress/data';
+import { FormFileUpload, DropZone, Spinner } from '@wordpress/components';
 import { store as noticesStore } from '@wordpress/notices';
-import {handleBlueprintData} from './utils'
+import { handleBlueprintData, useBlueprintData } from './utils'
+import { useState } from '@wordpress/element';
 
-const OpenJson = ({ onSubmitData }) => {
+const OpenJson = ({ label = 'Open', icon = null, handleClose }) => {
     const { createNotice } = useDispatch(noticesStore);
+    const { updateBlueprintConfig } = useBlueprintData();
+    const [isUploadingJson, setIsUploadingJson] = useState(false);
 
     // Process JSON file content
-    const processJsonFile = (file) => {
+    const processJsonFile = async (file) => {
         if (file.type === 'application/json') {
             const reader = new FileReader();
-            reader.onload = () => {
+            reader.onload = async () => {
                 try {
                     const jsonData = JSON.parse(reader.result);
-                    handleBlueprintData(jsonData , createNotice, onSubmitData); 
+                    setIsUploadingJson(true); // Show spinner
+                    await handleBlueprintData(jsonData, createNotice, updateBlueprintConfig);
+                    if (handleClose) handleClose();
                 }
                 catch (err) {
                     createNotice('error', __('Invalid JSON file.', 'wp-playground-blueprint-editor'));
                 }
+                finally {
+                    setIsUploadingJson(false); // Hide spinner
+                }
             };
             reader.readAsText(file);
         } else {
@@ -46,11 +54,24 @@ const OpenJson = ({ onSubmitData }) => {
     return (
         <FormFileUpload
             className='upload_blueprint_json'
-            __next40pxDefaultSize
             accept="application/json"
             onChange={handleFileSelection}
+            icon={null}
+            style={{
+                height: '100%',
+                flexDirection: 'column',
+                padding: '13px'
+            }}
         >
-            {__('Open', 'wp-playground-blueprint-editor')}
+            {!isUploadingJson && (<>
+                {icon && (
+                    <span style={{ width: 24, height: 24, display: 'inline-block' }}>
+                        {icon}
+                    </span>
+                )}
+                {__(label, 'wp-playground-blueprint-editor')}
+            </>)}
+            {isUploadingJson && (<Spinner style={{ margin: 0 }} />)}
             <DropZone
                 onFilesDrop={handleFileDrop}
                 accept="application/json"
diff --git a/src/editor/sidebar.js b/src/editor/sidebar.js
index f7e814f..f5f01c4 100644
--- a/src/editor/sidebar.js
+++ b/src/editor/sidebar.js
@@ -5,8 +5,7 @@ import { __ } from '@wordpress/i18n';
 import { registerPlugin } from '@wordpress/plugins';
 import { PluginDocumentSettingPanel, PluginPostStatusInfo } from '@wordpress/editor';
 import { copy, download, globe, code } from '@wordpress/icons';
-import { useCallback } from '@wordpress/element';
-import { useSelect, useDispatch } from '@wordpress/data';
+import { useDispatch } from '@wordpress/data';
 import { store as noticesStore } from '@wordpress/notices';
 import { useCopyToClipboard } from '@wordpress/compose';
 import { downloadBlob } from '@wordpress/blob';
@@ -30,57 +29,20 @@ import { PHP_VERSIONS, WP_VERSIONS, PLAYGROUND_BASE, PLAYGROUND_BUILDER_BASE, PL
 import Gallery from './blueprint-gallery';
 import SiteOptionsSettings from './site-options-settings';
 import PluginSettings from './plugins-settings';
+import { useBlueprintData } from './utils';
 
 /**
  * Main component for displaying blueprint sidebar setting.
  */
 function BlueprintSidebarSettings() {
-    const { createErrorNotice, createSuccessNotice } = useDispatch(noticesStore);
-    const { editPost } = useDispatch('core/editor');
-
-    const blocks = useSelect((select) => select('core/block-editor').getBlocks(), []);
-    const blueprint_config = useSelect((select) => {
-        return select('core/editor').getEditedPostAttribute('meta')['_blueprint_config'] || {}
-    });
-
-    const schema = {
-        $schema: PLAYGROUND_BLUEPRINT_SCHEMA_URL,
-        landingPage: blueprint_config.landing_page,
-        preferredVersions: {
-            php: blueprint_config.php_version,
-            wp: blueprint_config.wp_version
-        },
-        phpExtensionBundles: [blueprint_config.php_extension_bundles],
-        features: blueprint_config.networking ? { networking: true } : {},
-        login: blueprint_config.login,
-        siteOptions: blueprint_config.siteOptions,
-        extraLibraries: blueprint_config.extra_libraries,
-        plugins: blueprint_config.plugins,
-        steps: []
-    };
+    const { createNotice } = useDispatch(noticesStore);
+    const {
+        schema,
+        prepareSchema,
+        updateBlueprintConfig,
+        blueprint_config,
+    } = useBlueprintData();
 
-    /**
-     * Prepares the schema by extracting attributes from blocks.
-     * Blocks' metadata is excluded to keep the schema clean.
-     */
-    const prepareSchema = useCallback(() => {
-        const blockAttributes = blocks.map((block) => {
-            const { metadata, ...rest } = block.attributes; // Exclude metadata
-            return rest;
-        });
-        schema.steps = blockAttributes;
-        // Conditionally include the `login` property only if it's true
-        const cleanedSchema = {
-            ...schema,
-            login: blueprint_config.login ? blueprint_config.login : undefined,
-            siteOptions: blueprint_config.siteOptions && Object.keys(blueprint_config.siteOptions).length > 0
-                ? blueprint_config.siteOptions : undefined, // Include only if siteOptions is non-empty
-            extraLibraries: blueprint_config.extra_libraries && ['wp-cli'] || undefined,
-            plugins: blueprint_config.plugins && Object(blueprint_config.plugins).length > 0
-                ? blueprint_config.plugins : undefined, // Include only if plugins is non-empty
-        };
-        return JSON.stringify(cleanedSchema, null, 2); // Format the schema as a pretty JSON string
-    }, [blocks, schema, blueprint_config]);
     /**
      * Handles downloading the prepared schema as a JSON file.
      */
@@ -88,9 +50,9 @@ function BlueprintSidebarSettings() {
         try {
             const preparedSchema = prepareSchema();
             downloadBlob('playground-blueprint.json', preparedSchema, 'application/json');
-            createSuccessNotice(__('Blueprint downloaded successfully!', 'wp-playground-blueprint-editor'), { type: 'snackbar' });
+            createNotice('success', __('Blueprint downloaded successfully!', 'wp-playground-blueprint-editor'), { type: 'snackbar' });
         } catch (error) {
-            createErrorNotice(__('Failed to download Blueprint JSON.', 'wp-playground-blueprint-editor'));
+            createNotice('error', __('Failed to download Blueprint JSON.', 'wp-playground-blueprint-editor'));
         }
     };
 
@@ -99,44 +61,13 @@ function BlueprintSidebarSettings() {
      */
     const handleCopy = useCopyToClipboard(() => {
         if (!schema.steps.length) {
-            createErrorNotice(__('No Blueprint steps to copy!', 'wp-playground-blueprint-editor'));
+            createNotice('error', __('No Blueprint steps to copy!', 'wp-playground-blueprint-editor'));
             return ''; // Return empty string for invalid data
         }
-        createSuccessNotice(__('Blueprint schema copied to clipboard!', 'wp-playground-blueprint-editor'), { type: 'snackbar' });
+        createNotice('success', __('Blueprint schema copied to clipboard!', 'wp-playground-blueprint-editor'), { type: 'snackbar' });
         return prepareSchema();
     });
 
-    /**
-     * Updates the blueprint configuration stored in the post meta.
-     * @param {Object} updatedValues - Partial blueprint configuration to update.
-     */
-    const updateBlueprintConfig = (updatedValues) => {
-        editPost({ meta: { _blueprint_config: { ...blueprint_config, ...updatedValues } } });
-    };
-
-    /**
-     * Handles JSON data submission from the upload modal.
-     * Transforms the uploaded JSON to match the blueprint configuration format.
-     * @param {Object} data - Uploaded JSON data.
-     */
-    const handleJsonDataSubmit = (data) => {
-        if (!data) {
-            createErrorNotice(__('Failed to update Blueprint configuration.', 'wp-playground-blueprint-editor'));
-            return;
-        }
-        updateBlueprintConfig({
-            landing_page: data.landingPage,
-            php_version: data.preferredVersions.php,
-            wp_version: data.preferredVersions.wp,
-            php_extension_bundles: data.phpExtensionBundles,
-            networking: data.features.networking || false,
-            login: data.login || false,
-            siteOptions: data.siteOptions || undefined,
-            extra_libraries: data.extraLibraries || undefined,
-            plugins: data.plugins || undefined,
-        });
-        createSuccessNotice(__('Blueprint configuration updated successfully!', 'wp-playground-blueprint-editor'), { type: 'snackbar' });
-    };
 
     return (
         <>
@@ -144,10 +75,10 @@ function BlueprintSidebarSettings() {
                 <VStack spacing={5} style={{ width: '100%' }}>
                     <Flex>
                         <FlexBlock>
-                            <OpenJson onSubmitData={handleJsonDataSubmit} />
+                            <OpenJson />
                         </FlexBlock>
                         <FlexBlock>
-                            <Gallery onSubmitData={handleJsonDataSubmit} />
+                            <Gallery />
                         </FlexBlock>
                     </Flex>
                     <Toolbar style={{ justifyContent: 'space-between' }}>
diff --git a/src/editor/utils.js b/src/editor/utils.js
index d5b3280..8f98f2f 100644
--- a/src/editor/utils.js
+++ b/src/editor/utils.js
@@ -3,8 +3,9 @@
  */
 import { __ } from '@wordpress/i18n';
 import { createBlock } from "@wordpress/blocks";
-import { dispatch } from '@wordpress/data';
+import { dispatch, useSelect, useDispatch } from '@wordpress/data';
 import { PLAYGROUND_BLUEPRINT_SCHEMA_URL } from './constant';
+import { useCallback } from '@wordpress/element';
 
 /**
  * External dependencies
@@ -126,8 +127,10 @@ const validateAgainstSchema = async (data, createNotice) => {
  * jsonData - The parsed JSON data from the openJson and Gallery.
  * createNotice - Function to create notices in the WordPress admin.
  *  onSubmitData - Optional callback for additional data handling.
+ * @param {Function} updateBlueprintConfig - Function to update blueprint config in post meta.
  */
-export const handleBlueprintData = async (jsonData, createNotice, onSubmitData) => {
+
+export const handleBlueprintData = async (jsonData, createNotice, updateBlueprintConfig) => {
     if (!jsonData) {
         createNotice('error', __('Invalid blueprint schema.', 'wp-playground-blueprint-editor'));
         return;
@@ -138,14 +141,14 @@ export const handleBlueprintData = async (jsonData, createNotice, onSubmitData)
         const updatedData = updateDeprecatedFields(jsonData);
 
         // Validate updated JSON against schema
-        const isValid = await validateAgainstSchema(updatedData);
+        const isValid = await validateAgainstSchema(updatedData, createNotice);
         if (!isValid) {
             return;
         }
 
         const { meta, ...filteredData } = updatedData; // Exclude metadata
         const { steps } = filteredData;
-        
+
         const { validBlocks, invalidSteps } = validateBlueprintSteps(steps);
 
         if (validBlocks.length > 0) {
@@ -159,10 +162,110 @@ export const handleBlueprintData = async (jsonData, createNotice, onSubmitData)
                 .join(', ');
             createNotice('warning', __(`Some steps are invalid: ${errorDetails}.`, 'wp-playground-blueprint-editor'));
         }
+        handleJsonDataSubmit(filteredData, updateBlueprintConfig, createNotice);
 
-        if (onSubmitData) {
-            onSubmitData(filteredData);
-        }
     } catch (err) { return err; }
 };
 
+
+/**
+ * React hook to retrieve and manage the blueprint configuration from post meta.
+ * @returns {{
+ *   schema: Object,
+ *   prepareSchema: Function,
+ *   updateBlueprintConfig: Function,
+ *   blueprint_config: Object
+ * }}
+ */
+export const useBlueprintData = () => {
+    const { editPost } = useDispatch('core/editor');
+    const blocks = useSelect((select) => select('core/block-editor').getBlocks(), []);
+    const blueprint_config = useSelect((select) => {
+        return select('core/editor').getEditedPostAttribute('meta')['_blueprint_config'] || {};
+    });
+
+    const schema = {
+        $schema: PLAYGROUND_BLUEPRINT_SCHEMA_URL,
+        landingPage: blueprint_config.landing_page,
+        preferredVersions: {
+            php: blueprint_config.php_version,
+            wp: blueprint_config.wp_version,
+        },
+        phpExtensionBundles: [blueprint_config.php_extension_bundles],
+        features: blueprint_config.networking ? { networking: true } : {},
+        login: blueprint_config.login,
+        siteOptions: blueprint_config.siteOptions,
+        extraLibraries: blueprint_config.extra_libraries,
+        plugins: blueprint_config.plugins,
+        steps: [],
+    };
+
+    /**
+     * Prepares the schema by extracting attributes from blocks.
+     * Blocks' metadata is excluded to keep the schema clean.
+     */
+
+    const prepareSchema = useCallback(() => {
+        const blockAttributes = blocks.map((block) => {
+            const { metadata, ...rest } = block.attributes;
+            return rest;
+        });
+
+        schema.steps = blockAttributes;
+
+        const cleanedSchema = {
+            ...schema,
+            login: blueprint_config.login || undefined,
+            siteOptions: blueprint_config.siteOptions && Object.keys(blueprint_config.siteOptions).length > 0
+                ? blueprint_config.siteOptions : undefined,
+            extraLibraries: blueprint_config.extra_libraries && ['wp-cli'] || undefined,
+            plugins: blueprint_config.plugins && Object.keys(blueprint_config.plugins).length > 0
+                ? blueprint_config.plugins : undefined,
+        };
+
+        return JSON.stringify(cleanedSchema, null, 2);
+    }, [blocks, schema, blueprint_config]);
+
+    /**
+     * Updates the blueprint configuration stored in the post meta.
+     * @param {Object} updatedValues - Partial blueprint configuration to update.
+     */
+    const updateBlueprintConfig = (updatedValues) => {
+        editPost({ meta: { _blueprint_config: { ...blueprint_config, ...updatedValues } } });
+    };
+
+    return {
+        schema,
+        prepareSchema,
+        updateBlueprintConfig,
+        blueprint_config,
+    };
+};
+
+/**
+ * Updates the post meta with data from parsed JSON and triggers success/failure notices.
+ *
+ * @param {Object} data - Parsed JSON data from blueprint file.
+ * @param {Function} updateBlueprintConfig - Function to save new config.
+ * @param {Function} createNotice - Function to show admin notices.
+ */
+
+const handleJsonDataSubmit = (data, updateBlueprintConfig, createNotice) => {
+    if (!data) {
+        createNotice('error', __('Failed to update Blueprint configuration.', 'wp-playground-blueprint-editor'));
+        return;
+    }
+
+    updateBlueprintConfig({
+        landing_page: data.landingPage,
+        php_version: data.preferredVersions.php,
+        wp_version: data.preferredVersions.wp,
+        php_extension_bundles: data.phpExtensionBundles,
+        networking: data.features.networking || false,
+        login: data.login || false,
+        siteOptions: data.siteOptions || undefined,
+        extra_libraries: data.extraLibraries || undefined,
+        plugins: data.plugins || undefined,
+    });
+    createNotice('success', __('Blueprint configuration updated successfully!', 'wp-playground-blueprint-editor'), { type: 'snackbar' });
+};
\ No newline at end of file


Reviews:
user: login: ajitbohra
body: left feedback for copy changes
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T09:54:50Z
user: login: Verma-Punit
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T10:13:00Z
user: login: Yogesh-purohit
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T11:13:13Z
user: login: Yogesh-purohit
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T11:13:46Z
user: login: Yogesh-purohit
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T11:14:27Z
user: login: Yogesh-purohit
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T11:17:22Z
user: login: Verma-Punit
body: @Cy-Yaksh There seems to be no spacing between the content and the import button.

<img width="300" alt="Screenshot 2025-05-26 at 3 24 16 PM" src="https://github.com/user-attachments/assets/dd223d56-7386-4cb3-86ad-5232a3ff57b3" />
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T09:57:26Z
user: login: Verma-Punit
body: @Cy-Yaksh I think we can remove the skip button since we already have the blank button, and both serve the same purpose.

<img width="300" alt="Screenshot 2025-05-26 at 3 28 57 PM" src="https://github.com/user-attachments/assets/708f3870-0dd5-4a4b-bb6c-d2038fc0a745" />

state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T10:02:58Z
user: login: Verma-Punit
body: Looks Good 🚀
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T05:58:00Z

Review comments:
diff_hunk: @@ -0,0 +1,75 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useState, useEffect } from '@wordpress/element';
+import {
+    Button,
+    Modal,
+    __experimentalGrid as Grid,
+} from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { registerPlugin } from '@wordpress/plugins';
+import { code, gallery, plus } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import Gallery from './blueprint-gallery';
+import OpenJson from './open-json';
+
+const OnboardingModal = () => {
+    const [showModal, setShowModal] = useState(false);
+
+    const { isNewPost, blocks } = useSelect((select) => {
+        const editor = select('core/editor');
+        return {
+            isNewPost: editor.isCleanNewPost(),
+            blocks: editor.getBlocks(),
+        };
+    }, []);
+
+    useEffect(() => {
+        if (isNewPost && blocks.length === 0) {
+            setShowModal(true);
+        }
+    }, [isNewPost, blocks]);
+
+    const handleClose = () => setShowModal(false);
+
+    if (!showModal) return null;
+
+    return (
+        <Modal
+            title={__('Start Building Your Blueprint', 'wp-playground-blueprint-editor')}
+            onRequestClose={handleClose}
+            shouldCloseOnClickOutside
+            shouldCloseOnEsc
+            size="medium"
+        >
+            <Grid columns={3} spacing={4}>
+                <OpenJson label='Json' icon={code} onClick={handleClose} />
+                <Gallery label='Gallery' icon={gallery} onClick={handleClose} />
+                <Button
+                    onClick={handleClose}
+                    icon={plus}
+                    style={{
+                        boxShadow: 'inset 0 0 0 1px #ccc',
+                        height: '100%',
+                        flexDirection: 'column',
+                        padding: '13px'
+                    }}
+                >
+                    {__('Start Blank', 'wp-playground-blueprint-editor')}
path: src/editor/onboarding.js
user: login: ajitbohra
body: ```suggestion
                    {__('Blank', 'wp-playground-blueprint-editor')}
```
created_at: 2025-05-23T09:52:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useState, useEffect } from '@wordpress/element';
+import {
+    Button,
+    Modal,
+    __experimentalGrid as Grid,
+} from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { registerPlugin } from '@wordpress/plugins';
+import { code, gallery, plus } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import Gallery from './blueprint-gallery';
+import OpenJson from './open-json';
+
+const OnboardingModal = () => {
+    const [showModal, setShowModal] = useState(false);
+
+    const { isNewPost, blocks } = useSelect((select) => {
+        const editor = select('core/editor');
+        return {
+            isNewPost: editor.isCleanNewPost(),
+            blocks: editor.getBlocks(),
+        };
+    }, []);
+
+    useEffect(() => {
+        if (isNewPost && blocks.length === 0) {
+            setShowModal(true);
+        }
+    }, [isNewPost, blocks]);
+
+    const handleClose = () => setShowModal(false);
+
+    if (!showModal) return null;
+
+    return (
+        <Modal
+            title={__('Start Building Your Blueprint', 'wp-playground-blueprint-editor')}
path: src/editor/onboarding.js
user: login: ajitbohra
body: ```suggestion
            title={__('Start Blueprint From', 'wp-playground-blueprint-editor')}
```
created_at: 2025-05-23T09:53:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useState, useEffect } from '@wordpress/element';
+import {
+    Button,
+    Modal,
+    __experimentalGrid as Grid,
+} from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { registerPlugin } from '@wordpress/plugins';
+import { code, gallery, plus } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import Gallery from './blueprint-gallery';
+import OpenJson from './open-json';
+
+const OnboardingModal = () => {
+    const [showModal, setShowModal] = useState(false);
+
+    const { isNewPost, blocks } = useSelect((select) => {
+        const editor = select('core/editor');
+        return {
+            isNewPost: editor.isCleanNewPost(),
+            blocks: editor.getBlocks(),
+        };
+    }, []);
+
+    useEffect(() => {
+        if (isNewPost && blocks.length === 0) {
+            setShowModal(true);
+        }
+    }, [isNewPost, blocks]);
+
+    const handleClose = () => setShowModal(false);
+
+    if (!showModal) return null;
+
+    return (
+        <Modal
+            title={__('Start Building Your Blueprint', 'wp-playground-blueprint-editor')}
+            onRequestClose={handleClose}
+            shouldCloseOnClickOutside
+            shouldCloseOnEsc
+            size="medium"
+        >
+            <Grid columns={3} spacing={4}>
+                <OpenJson label='Json' icon={code} onClick={handleClose} />
path: src/editor/onboarding.js
user: login: ajitbohra
body: ```suggestion
                <OpenJson label='JSON' icon={code} onClick={handleClose} />
```
created_at: 2025-05-23T09:54:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useState, useEffect } from '@wordpress/element';
+import {
+    Button,
+    Modal,
+    __experimentalGrid as Grid,
+} from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { registerPlugin } from '@wordpress/plugins';
+import { code, gallery, plus } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import Gallery from './blueprint-gallery';
+import OpenJson from './open-json';
+
+const OnboardingModal = () => {
+    const [showModal, setShowModal] = useState(false);
+
+    const { isNewPost, blocks } = useSelect((select) => {
+        const editor = select('core/editor');
+        return {
+            isNewPost: editor.isCleanNewPost(),
+            blocks: editor.getBlocks(),
+        };
+    }, []);
+
+    useEffect(() => {
+        if (isNewPost && blocks.length === 0) {
+            setShowModal(true);
+        }
+    }, [isNewPost, blocks]);
+
+    const handleClose = () => setShowModal(false);
+
+    if (!showModal) return null;
+
+    return (
+        <Modal
+            title={__('Start Building Your Blueprint', 'wp-playground-blueprint-editor')}
+            onRequestClose={handleClose}
+            shouldCloseOnClickOutside
+            shouldCloseOnEsc
+            size="medium"
+        >
+            <Grid columns={3} spacing={4}>
+                <OpenJson label='Json' icon={code} onClick={handleClose} />
path: src/editor/onboarding.js
user: login: Verma-Punit
body: @Cy-Yaksh Use `handleClose={handleClose}` instead of `onClick={handleClose} ` to follow prop naming conventions.
created_at: 2025-05-23T10:11:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,75 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useState, useEffect } from '@wordpress/element';
+import {
+    Button,
+    Modal,
+    __experimentalGrid as Grid,
+} from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { registerPlugin } from '@wordpress/plugins';
+import { code, gallery, plus } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import Gallery from './blueprint-gallery';
+import OpenJson from './open-json';
+
+const OnboardingModal = () => {
+    const [showModal, setShowModal] = useState(false);
+
+    const { isNewPost, blocks } = useSelect((select) => {
+        const editor = select('core/editor');
+        return {
+            isNewPost: editor.isCleanNewPost(),
+            blocks: editor.getBlocks(),
+        };
+    }, []);
+
+    useEffect(() => {
+        if (isNewPost && blocks.length === 0) {
+            setShowModal(true);
+        }
+    }, [isNewPost, blocks]);
+
+    const handleClose = () => setShowModal(false);
+
+    if (!showModal) return null;
+
+    return (
+        <Modal
+            title={__('Start Building Your Blueprint', 'wp-playground-blueprint-editor')}
+            onRequestClose={handleClose}
+            shouldCloseOnClickOutside
+            shouldCloseOnEsc
+            size="medium"
+        >
+            <Grid columns={3} spacing={4}>
+                <OpenJson label='Json' icon={code} onClick={handleClose} />
+                <Gallery label='Gallery' icon={gallery} onClick={handleClose} />
+                <Button
+                    onClick={handleClose}
+                    icon={plus}
+                    style={{
+                        boxShadow: 'inset 0 0 0 1px #ccc',
+                        height: '100%',
+                        flexDirection: 'column',
+                        padding: '13px'
+                    }}
+                >
+                    {__('Start Blank', 'wp-playground-blueprint-editor')}
path: src/editor/onboarding.js
user: login: Yogesh-purohit
body: ✅ Changed to Blank as suggested
created_at: 2025-05-23T11:13:13Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useState, useEffect } from '@wordpress/element';
+import {
+    Button,
+    Modal,
+    __experimentalGrid as Grid,
+} from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { registerPlugin } from '@wordpress/plugins';
+import { code, gallery, plus } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import Gallery from './blueprint-gallery';
+import OpenJson from './open-json';
+
+const OnboardingModal = () => {
+    const [showModal, setShowModal] = useState(false);
+
+    const { isNewPost, blocks } = useSelect((select) => {
+        const editor = select('core/editor');
+        return {
+            isNewPost: editor.isCleanNewPost(),
+            blocks: editor.getBlocks(),
+        };
+    }, []);
+
+    useEffect(() => {
+        if (isNewPost && blocks.length === 0) {
+            setShowModal(true);
+        }
+    }, [isNewPost, blocks]);
+
+    const handleClose = () => setShowModal(false);
+
+    if (!showModal) return null;
+
+    return (
+        <Modal
+            title={__('Start Building Your Blueprint', 'wp-playground-blueprint-editor')}
path: src/editor/onboarding.js
user: login: Yogesh-purohit
body: ✅ Updated title to Start Blueprint From
created_at: 2025-05-23T11:13:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useState, useEffect } from '@wordpress/element';
+import {
+    Button,
+    Modal,
+    __experimentalGrid as Grid,
+} from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { registerPlugin } from '@wordpress/plugins';
+import { code, gallery, plus } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import Gallery from './blueprint-gallery';
+import OpenJson from './open-json';
+
+const OnboardingModal = () => {
+    const [showModal, setShowModal] = useState(false);
+
+    const { isNewPost, blocks } = useSelect((select) => {
+        const editor = select('core/editor');
+        return {
+            isNewPost: editor.isCleanNewPost(),
+            blocks: editor.getBlocks(),
+        };
+    }, []);
+
+    useEffect(() => {
+        if (isNewPost && blocks.length === 0) {
+            setShowModal(true);
+        }
+    }, [isNewPost, blocks]);
+
+    const handleClose = () => setShowModal(false);
+
+    if (!showModal) return null;
+
+    return (
+        <Modal
+            title={__('Start Building Your Blueprint', 'wp-playground-blueprint-editor')}
+            onRequestClose={handleClose}
+            shouldCloseOnClickOutside
+            shouldCloseOnEsc
+            size="medium"
+        >
+            <Grid columns={3} spacing={4}>
+                <OpenJson label='Json' icon={code} onClick={handleClose} />
path: src/editor/onboarding.js
user: login: Yogesh-purohit
body: Updated label to JSON
created_at: 2025-05-23T11:14:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useState, useEffect } from '@wordpress/element';
+import {
+    Button,
+    Modal,
+    __experimentalGrid as Grid,
+} from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { registerPlugin } from '@wordpress/plugins';
+import { code, gallery, plus } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import Gallery from './blueprint-gallery';
+import OpenJson from './open-json';
+
+const OnboardingModal = () => {
+    const [showModal, setShowModal] = useState(false);
+
+    const { isNewPost, blocks } = useSelect((select) => {
+        const editor = select('core/editor');
+        return {
+            isNewPost: editor.isCleanNewPost(),
+            blocks: editor.getBlocks(),
+        };
+    }, []);
+
+    useEffect(() => {
+        if (isNewPost && blocks.length === 0) {
+            setShowModal(true);
+        }
+    }, [isNewPost, blocks]);
+
+    const handleClose = () => setShowModal(false);
+
+    if (!showModal) return null;
+
+    return (
+        <Modal
+            title={__('Start Building Your Blueprint', 'wp-playground-blueprint-editor')}
+            onRequestClose={handleClose}
+            shouldCloseOnClickOutside
+            shouldCloseOnEsc
+            size="medium"
+        >
+            <Grid columns={3} spacing={4}>
+                <OpenJson label='Json' icon={code} onClick={handleClose} />
path: src/editor/onboarding.js
user: login: Yogesh-purohit
body: ✅ Replaced onClick with handleClose prop as per convention
created_at: 2025-05-23T11:17:22Z
author_association: COLLABORATOR

PR comments:
user: login: ajitbohra
created_at: 2025-05-26T10:12:26Z
body: Let's remove the skip. It feels redundant, as earlier pointed out by @Cy-Yaksh, and the same thoughts expressed by @Verma-Punit. 

 
author_association: MEMBER
user: login: Verma-Punit
created_at: 2025-05-26T10:14:17Z
body: @Cy-Yaksh As you can see, when we import, it takes a little bit of time. In the meantime, we can display a loading animation to avoid confusing the user. Currently, the transition feels a bit off.

https://github.com/user-attachments/assets/ec09c8ea-5608-474b-97b5-fdfdc938282f

author_association: CONTRIBUTOR
user: login: Yogesh-purohit
created_at: 2025-05-27T07:46:00Z
body: All the feedback has been addressed 

- [x] Fixed spacing between content and import button
- [x] Removed redundant “Skip” button from onboarding model
- [x] Added loading animation during module import
- [x] Ensured modal closes after “Create Block Theme” import (closes #70)


author_association: COLLABORATOR