diff --git a/src/editor/components/Blocks.js b/src/editor/components/Blocks.js
new file mode 100644
index 0000000..36db903
--- /dev/null
+++ b/src/editor/components/Blocks.js
@@ -0,0 +1,48 @@
+import { __ } from '@wordpress/i18n';
+import { useState, useContext } from '@wordpress/element';
+
+import Search from './Search';
+import BlocksItem from './BlocksItem';
+import EditorContext from '../context/EditorContext';
+import { getCoreBlocks } from '../../utils/block-helpers';
+
+/**
+ * Blocks tab menu component
+ */
+const Blocks = () => {
+ const { themeConfig, schema } = useContext( EditorContext );
+ const [ searchValue, setSearchValue ] = useState();
+
+ return (
+
+ { __( 'Blocks', 'themer' ) }
+
+ { __(
+ 'Customise the appearance of specific blocks for the whole site.',
+ 'themer'
+ ) }
+
+
+ { getCoreBlocks( undefined, themeConfig, schema )?.map(
+ ( block ) => {
+ if (
+ searchValue?.length > 0 &&
+ ! block.toLowerCase().includes( searchValue )
+ ) {
+ return false;
+ }
+
+ return (
+
+ );
+ }
+ ) }
+
+ );
+};
+
+export default Blocks;
diff --git a/src/editor/components/BlocksItem.js b/src/editor/components/BlocksItem.js
new file mode 100644
index 0000000..8ad9156
--- /dev/null
+++ b/src/editor/components/BlocksItem.js
@@ -0,0 +1,36 @@
+import Border from './StylesBorder';
+import getThemeOption from '../../utils/get-theme-option';
+
+/**
+ * Individual block item
+ *
+ * @param {Object} props Component props
+ * @param {string} props.block Block name
+ * @param {Object} props.themeConfig Theme JSON
+ */
+const BlocksItem = ( { block, themeConfig } ) => {
+ if ( ! block ) {
+ return;
+ }
+
+ const blockSelector = [ 'styles', 'blocks', block ];
+ const hasBorderStyles = getThemeOption(
+ [ ...blockSelector, 'border' ].join( '.' ),
+ themeConfig
+ );
+
+ return (
+
+ { block }
+
+ { hasBorderStyles && (
+
+ ) }
+
+
+ );
+};
+
+export default BlocksItem;
diff --git a/src/editor/components/Colours.js b/src/editor/components/Colours.js
new file mode 100644
index 0000000..c7ba6ff
--- /dev/null
+++ b/src/editor/components/Colours.js
@@ -0,0 +1,6 @@
+/**
+ * Colours tab menu component
+ */
+const Colours = () =>
Colours Tab
;
+
+export default Colours;
diff --git a/src/editor/components/ComponentWrapper.js b/src/editor/components/ComponentWrapper.js
index be82459..31c5891 100644
--- a/src/editor/components/ComponentWrapper.js
+++ b/src/editor/components/ComponentWrapper.js
@@ -1,4 +1,4 @@
-import ThemerComponent from './fields/ThemerComponent';
+import ThemerComponent from './ThemerComponent';
/**
* Wrapper for app
diff --git a/src/editor/components/CustomBlocks.js b/src/editor/components/CustomBlocks.js
new file mode 100644
index 0000000..b1d5c21
--- /dev/null
+++ b/src/editor/components/CustomBlocks.js
@@ -0,0 +1,6 @@
+/**
+ * Custom Blocks tab menu component
+ */
+const CustomBlock = () => Custom Block Tab
;
+
+export default CustomBlock;
diff --git a/src/editor/components/Layout.js b/src/editor/components/Layout.js
new file mode 100644
index 0000000..796dccb
--- /dev/null
+++ b/src/editor/components/Layout.js
@@ -0,0 +1,6 @@
+/**
+ * Layout tab menu component
+ */
+const Layout = () => Layout Tab
;
+
+export default Layout;
diff --git a/src/editor/components/fields/Preview.js b/src/editor/components/Preview.js
similarity index 100%
rename from src/editor/components/fields/Preview.js
rename to src/editor/components/Preview.js
diff --git a/src/editor/components/fields/ResponsiveButton.js b/src/editor/components/ResponsiveButton.js
similarity index 100%
rename from src/editor/components/fields/ResponsiveButton.js
rename to src/editor/components/ResponsiveButton.js
diff --git a/src/editor/components/Search.js b/src/editor/components/Search.js
new file mode 100644
index 0000000..0725cff
--- /dev/null
+++ b/src/editor/components/Search.js
@@ -0,0 +1,24 @@
+/**
+ * Search component
+ *
+ * @param {Object} props Component props
+ * @param {Function} props.setValue Input on change function
+ * @param {string} props.placeholder Placeholder attribute value
+ */
+const Search = ( { setValue, placeholder = 'Search' } ) => {
+ const handleSearch = ( event ) => {
+ setValue( event?.target?.value?.toLowerCase().trim() );
+ };
+
+ return (
+
+ handleSearch( event ) }
+ placeholder={ placeholder }
+ />
+
+ );
+};
+
+export default Search;
diff --git a/src/editor/components/StylesBorder.js b/src/editor/components/StylesBorder.js
new file mode 100644
index 0000000..85526e9
--- /dev/null
+++ b/src/editor/components/StylesBorder.js
@@ -0,0 +1,43 @@
+/* eslint-disable @wordpress/no-unsafe-wp-apis */
+
+import { set } from 'lodash';
+import { __ } from '@wordpress/i18n';
+import { useContext } from '@wordpress/element';
+import { __experimentalBorderBoxControl as BorderBoxControl } from '@wordpress/components';
+
+import getThemeOption from '../../utils/get-theme-option';
+import EditorContext from '../context/EditorContext';
+import StylesContext from '../context/StylesContext';
+
+/**
+ * Reusable border control style component
+ *
+ * @param {Object} props Component props
+ * @param {string} props.selector Property target selector
+ */
+const Border = ( { selector } ) => {
+ const { themeConfig } = useContext( EditorContext );
+ const { setUserConfig } = useContext( StylesContext );
+ const value = getThemeOption( selector, themeConfig );
+ const colors = getThemeOption(
+ 'settings.color.palette.theme',
+ themeConfig
+ );
+
+ const onChange = ( newValue ) => {
+ let config = structuredClone( themeConfig );
+ config = set( config, selector, newValue );
+ setUserConfig( config );
+ };
+
+ return (
+
+ );
+};
+
+export default Border;
diff --git a/src/editor/components/StylesColour.js b/src/editor/components/StylesColour.js
new file mode 100644
index 0000000..9ffe3db
--- /dev/null
+++ b/src/editor/components/StylesColour.js
@@ -0,0 +1,8 @@
+/**
+ * Reusable colour control style component
+ */
+const Colour = () => {
+ return Colour Component
;
+};
+
+export default Colour;
diff --git a/src/editor/components/StylesDimensions.js b/src/editor/components/StylesDimensions.js
new file mode 100644
index 0000000..e26f6a2
--- /dev/null
+++ b/src/editor/components/StylesDimensions.js
@@ -0,0 +1,8 @@
+/**
+ * Reusable dimensions control style component
+ */
+const Dimensions = () => {
+ return Dimensions Component
;
+};
+
+export default Dimensions;
diff --git a/src/editor/components/StylesFilter.js b/src/editor/components/StylesFilter.js
new file mode 100644
index 0000000..db61844
--- /dev/null
+++ b/src/editor/components/StylesFilter.js
@@ -0,0 +1,8 @@
+/**
+ * Reusable filter control style component
+ */
+const Filter = () => {
+ return Filter Component
;
+};
+
+export default Filter;
diff --git a/src/editor/components/StylesOutline.js b/src/editor/components/StylesOutline.js
new file mode 100644
index 0000000..044826b
--- /dev/null
+++ b/src/editor/components/StylesOutline.js
@@ -0,0 +1,8 @@
+/**
+ * Reusable outline control style component
+ */
+const Outline = () => {
+ return Outline Component
;
+};
+
+export default Outline;
diff --git a/src/editor/components/StylesShadow.js b/src/editor/components/StylesShadow.js
new file mode 100644
index 0000000..1dd4b69
--- /dev/null
+++ b/src/editor/components/StylesShadow.js
@@ -0,0 +1,8 @@
+/**
+ * Reusable shadow control style component
+ */
+const Shadow = () => {
+ return Shadow Component
;
+};
+
+export default Shadow;
diff --git a/src/editor/components/StylesSpacing.js b/src/editor/components/StylesSpacing.js
new file mode 100644
index 0000000..4e91f73
--- /dev/null
+++ b/src/editor/components/StylesSpacing.js
@@ -0,0 +1,8 @@
+/**
+ * Reusable spacing control style component
+ */
+const Spacing = () => {
+ return Spacing Component
;
+};
+
+export default Spacing;
diff --git a/src/editor/components/StylesTypography.js b/src/editor/components/StylesTypography.js
new file mode 100644
index 0000000..84ba5fa
--- /dev/null
+++ b/src/editor/components/StylesTypography.js
@@ -0,0 +1,8 @@
+/**
+ * Reusable typography control style component
+ */
+const Typography = () => {
+ return Typography Component
;
+};
+
+export default Typography;
diff --git a/src/editor/components/ThemerComponent.js b/src/editor/components/ThemerComponent.js
new file mode 100644
index 0000000..d75b9e6
--- /dev/null
+++ b/src/editor/components/ThemerComponent.js
@@ -0,0 +1,226 @@
+import { mergeWith, isEmpty } from 'lodash';
+import { Button, Spinner, TabPanel } from '@wordpress/components';
+import { useSelect, dispatch } from '@wordpress/data';
+import { useEffect, useState, useMemo } from '@wordpress/element';
+import apiFetch from '@wordpress/api-fetch';
+
+import Blocks from './Blocks';
+import Layout from './Layout';
+import Colours from './Colours';
+import Preview from './Preview';
+import Typography from './Typography';
+import CustomBlocks from './CustomBlocks';
+import ButtonExport from './ButtonExport';
+import ResponsiveButton from './ResponsiveButton';
+import EditorContext from '../context/EditorContext';
+import StylesContext from '../context/StylesContext';
+import fetchSchema from '../../utils/schema-helpers';
+
+/**
+ * main component
+ */
+const ThemerComponent = () => {
+ const [ previewCss, setPreviewCss ] = useState( '' );
+ const [ previewSize, setPreviewSize ] = useState();
+ const [ schema, setSchema ] = useState( {} );
+
+ const setUserConfig = ( config ) => {
+ dispatch( 'core' ).editEntityRecord(
+ 'root',
+ 'globalStyles',
+ globalStylesId,
+ config
+ );
+ };
+
+ const { globalStylesId, baseConfig, userConfig } = useSelect(
+ ( select ) => {
+ const {
+ __experimentalGetCurrentGlobalStylesId,
+ __experimentalGetCurrentThemeBaseGlobalStyles,
+ getEditedEntityRecord,
+ } = select( 'core' );
+
+ const currentGlobalStylesId =
+ __experimentalGetCurrentGlobalStylesId();
+
+ return {
+ globalStylesId: currentGlobalStylesId, // eslint-disable no-underscore-dangle -- require underscore dangle for experimental functions
+ baseConfig: __experimentalGetCurrentThemeBaseGlobalStyles(), // eslint-disable no-underscore-dangle -- require underscore dangle for experimental functions
+ userConfig: getEditedEntityRecord(
+ 'root',
+ 'globalStyles',
+ currentGlobalStylesId
+ ),
+ };
+ }
+ );
+
+ /**
+ * Returns merged base and user configs
+ */
+ const themeConfig = useMemo( () => {
+ if ( isEmpty( userConfig ) ) {
+ return baseConfig;
+ }
+ const merged = mergeWith( {}, baseConfig, userConfig );
+ return merged;
+ }, [ userConfig, baseConfig ] );
+
+ /**
+ * Fetch new preview CSS whenever config is changed
+ */
+ useEffect( () => {
+ const updatePreviewCss = async () => {
+ const res = await apiFetch( {
+ path: '/themer/v1/styles',
+ method: 'POST',
+ data: themeConfig,
+ } );
+ if ( res ) {
+ setPreviewCss( res );
+ }
+ };
+ if ( themeConfig ) {
+ updatePreviewCss();
+ }
+ }, [ themeConfig, setPreviewCss ] );
+
+ /**
+ * TODO: For demo purpose only, this should be refactored and
+ * implemented into the processing of the schema file task
+ */
+ useEffect( () => {
+ ( async () => {
+ const schemaJson = await fetchSchema();
+ setSchema( schemaJson );
+ } )();
+ }, [] );
+
+ /**
+ * saves edited entity data
+ */
+ const save = async () => {
+ try {
+ await dispatch( 'core' ).saveEditedEntityRecord(
+ 'root',
+ 'globalStyles',
+ globalStylesId
+ );
+ } catch ( err ) {
+ // eslint-disable-next-line no-console
+ console.log( err );
+ }
+ };
+
+ /**
+ * resets updated theme db data back to original theme.json
+ */
+ const reset = () => {
+ dispatch( 'core' ).editEntityRecord(
+ 'root',
+ 'globalStyles',
+ globalStylesId,
+ baseConfig
+ );
+ };
+
+ if ( ! themeConfig || ! previewCss ) {
+ return (
+ <>
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ { ( tab ) => {
+ switch ( tab?.name ) {
+ case 'colours':
+ return ;
+ case 'layout':
+ return ;
+ case 'blocks':
+ return ;
+ case 'custom-blocks':
+ return ;
+ case 'typography':
+ default:
+ return ;
+ }
+ } }
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ThemerComponent;
diff --git a/src/editor/components/Typography.js b/src/editor/components/Typography.js
new file mode 100644
index 0000000..89a1137
--- /dev/null
+++ b/src/editor/components/Typography.js
@@ -0,0 +1,6 @@
+/**
+ * Typography tab menu component
+ */
+const Typography = () => Typography Tab
;
+
+export default Typography;
diff --git a/src/editor/components/fields/ComponentMap.js b/src/editor/components/fields/ComponentMap.js
deleted file mode 100644
index f896b95..0000000
--- a/src/editor/components/fields/ComponentMap.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import { TextControl, ColorPicker } from '@wordpress/components';
-import { useSelect } from '@wordpress/data';
-
-import FontPicker from './Components/FontPicker';
-import SpacingControl from './Components/SpacingControl';
-
-/**
- * Returns appropriate component depending on field type
- *
- * @param {Object} props
- * @param {string} props.label
- * @param {string} props.value
- * @param {Function} props.onChange
- */
-const ComponentMap = ( { label, value, onChange } ) => {
- const { currentThemeBaseGlobalStyles } = useSelect( ( select ) => {
- return {
- currentThemeBaseGlobalStyles:
- select(
- 'core'
- ).__experimentalGetCurrentThemeBaseGlobalStyles(),
- };
- } );
-
- const colorPickerArray = [ 'background', 'text' ];
- const fontPickerArray = [
- 'fontFamily',
- 'fontSize',
- 'lineHeight',
- 'textDecoration',
- ];
- const blockGapArray = [ 'blockGap', 'top', 'right', 'bottom', 'left' ];
-
- switch ( true ) {
- case colorPickerArray.includes( label ):
- return (
- onChange( val ) }
- />
- );
- case fontPickerArray.includes( label ):
- return (
-
- onChange( val ) }
- base={ currentThemeBaseGlobalStyles }
- />
-
- );
- case blockGapArray.includes( label ):
- return (
- onChange( val ) }
- base={ currentThemeBaseGlobalStyles }
- />
- );
- default:
- return (
- onChange( val ) }
- />
- );
- }
-};
-
-export default ComponentMap;
diff --git a/src/editor/components/fields/Components/FontPicker.js b/src/editor/components/fields/Components/FontPicker.js
deleted file mode 100644
index ac4c4ac..0000000
--- a/src/editor/components/fields/Components/FontPicker.js
+++ /dev/null
@@ -1,136 +0,0 @@
-/**
- * This component requires use of experimental apis
- */
-
-/* eslint-disable @wordpress/no-unsafe-wp-apis */
-
-import {
- FontSizePicker,
- SelectControl,
- Button,
- __experimentalInputControl as InputControl,
- __experimentalToggleGroupControl as ToggleGroup,
- __experimentalToggleGroupControlOptionIcon as ToggleIcon,
-} from '@wordpress/components';
-
-/**
- * returns component for font options
- *
- * @param {Object} props
- * @param {Object} props.base
- * @param {string} props.id
- * @param {string|string[]} props.value
- * @param {Function} props.onChange
- */
-const FontPicker = ( { base, id, value, onChange } ) => {
- /**
- * returns preset font sizes from theme.json
- */
- const getFontSizes = () => {
- const sizes = base?.settings?.typography?.fontSizes?.theme;
- return sizes;
- };
-
- /**
- * returns preset font families from theme.json
- */
- const getFontFamilies = () => {
- const fonts = base?.settings?.typography?.fontFamilies?.theme;
- const result = [];
- fonts.forEach( ( item ) => {
- // eslint-disable-next-line no-param-reassign -- remove '-' from slug to use as title
- item.slug = item.slug.replace( /\s+/g, '-' );
- result.push( {
- value: item.slug,
- label: item.name,
- } );
- } );
- return result;
- };
-
- /**
- * handles line height incremental input
- *
- * @param {number} val
- * @param {string} dir
- */
- const getLineHeight = ( val, dir ) => {
- let increment;
- if ( dir === 'minus' ) {
- increment = -0.1;
- } else increment = 0.1;
- const number = parseFloat( val );
- const result = parseFloat( number + increment )
- .toFixed( 1 )
- .toString();
- return result;
- };
-
- switch ( id ) {
- case 'fontSize':
- return (
- onChange( val ) }
- />
- );
- case 'fontFamily':
- return (
- onChange( val ) }
- />
- );
- case 'lineHeight':
- return (
- onChange( val ) }
- suffix={
- <>
- {
- onChange( getLineHeight( value, 'plus' ) );
- } }
- />
- {
- onChange( getLineHeight( value, 'minus' ) );
- } }
- />
- >
- }
- />
- );
- case 'textDecoration':
- return (
- <>
- {
- onChange( val );
- } }
- >
-
-
-
-
- >
- );
- default:
- return null;
- }
-};
-
-export default FontPicker;
diff --git a/src/editor/components/fields/Components/SpacingControl.js b/src/editor/components/fields/Components/SpacingControl.js
deleted file mode 100644
index bfbb89f..0000000
--- a/src/editor/components/fields/Components/SpacingControl.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * This component requires use of experimental apis
- */
-
-/* eslint-disable @wordpress/no-unsafe-wp-apis */
-
-import { useState } from '@wordpress/element';
-import {
- Button,
- RangeControl,
- __experimentalUnitControl as UnitControl,
-} from '@wordpress/components';
-
-/**
- * renders component for managing spacing elements
- *
- * @param {Object} props
- * @param {Object} props.value
- * @param {Function} props.onChange
- * @param {Object} props.base
- */
-const SpacingControl = ( { value, onChange, base } ) => {
- const [ toggle, setToggle ] = useState( isNaN( Array.from( value )[ 0 ] ) );
-
- /**
- * returns default markers for spacing component
- *
- * @param {number} val
- */
- const getRangeMarks = ( val ) => {
- const marks = base?.settings?.spacing?.spacingSizes?.theme;
- const result = [];
- marks.forEach( ( item ) => {
- result.push( {
- value: item.size,
- label: item.name,
- } );
- } );
- if ( val ) {
- return result[ val ].value;
- }
-
- return result;
- };
-
- return (
- <>
- setToggle( ! toggle ) }
- isPressed={ ! toggle }
- />
- { toggle && (
- <>
- onChange( getRangeMarks( val ) ) }
- />
- >
- ) }
- { ! toggle && (
- <>
- {
- onChange( val );
- } }
- />
- >
- ) }
- >
- );
-};
-
-export default SpacingControl;
diff --git a/src/editor/components/fields/Field.js b/src/editor/components/fields/Field.js
deleted file mode 100644
index e28f54d..0000000
--- a/src/editor/components/fields/Field.js
+++ /dev/null
@@ -1,73 +0,0 @@
-/* eslint no-underscore-dangle: 0 */
-
-import { set, merge } from 'lodash';
-import { useState } from '@wordpress/element';
-import { select, dispatch } from '@wordpress/data';
-
-import ComponentMap from './ComponentMap';
-
-/**
- * main component
- *
- * @param {Object} props
- * @param {string} props.value
- * @param {string} props.path
- * @param {string} props.id
- */
-const Field = ( { value, path, id } ) => {
- /**
- * gets ID for global styles
- */
- const getGlobalStylesId = () =>
- select( 'core' ).__experimentalGetCurrentGlobalStylesId();
- const [ text, setText ] = useState( value );
- const context = { ...{} };
-
- /**
- * updates entity record on field edit
- *
- * @param {*} newValue
- */
- const edit = ( newValue ) => {
- const current = {
- ...select( 'core' ).getEditedEntityRecord(
- 'root',
- 'globalStyles',
- getGlobalStylesId()
- ),
- };
- const updated = set( context, path, newValue );
- const newObj = merge( current, updated );
- dispatch( 'core' ).editEntityRecord(
- 'root',
- 'globalStyles',
- getGlobalStylesId(),
- {
- styles: newObj.styles || {},
- settings: newObj.settings || {},
- }
- );
- };
-
- /**
- * gets field path and value and passes to edit
- *
- * @param {Event} e Change event.
- */
- const onChange = ( e ) => {
- setText( e );
- edit( e );
- };
- return (
- <>
- { id }
- onChange( val ) }
- />
- >
- );
-};
-
-export default Field;
diff --git a/src/editor/components/fields/Fields.js b/src/editor/components/fields/Fields.js
deleted file mode 100644
index 28a81f4..0000000
--- a/src/editor/components/fields/Fields.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import Field from './Field';
-
-/**
- * loops over theme config and renders fields
- *
- * @param {Object} props
- * @param {Object} props.sourceObject
- * @param {string} props.path
- * @param {boolean} props.child
- */
-const Fields = ( { sourceObject, path = '', child } ) => {
- return Object.entries( sourceObject ).map( ( [ key, value ] ) => {
- if ( path.charAt( 0 ) === '.' ) {
- path = path.substring( 1 );
- }
- const currentPath = `${ path }.${ key }`;
-
- /**
- * TODO: Handle settings
- */
- if ( key === 'settings' ) {
- return null;
- }
-
- /**
- * If we encounter an unknown object, recursively call the function again using it's value
- */
- if ( typeof value === 'object' && value !== null ) {
- return (
-
- );
- }
- /**
- * If we encounter a string, render a field
- */
- if ( typeof value === 'string' ) {
- return (
-
- );
- }
- return null;
- } );
-};
-
-export default Fields;
diff --git a/src/editor/components/fields/ThemerComponent.js b/src/editor/components/fields/ThemerComponent.js
deleted file mode 100644
index a0299d8..0000000
--- a/src/editor/components/fields/ThemerComponent.js
+++ /dev/null
@@ -1,163 +0,0 @@
-import { mergeWith, isEmpty } from 'lodash';
-import { Button, Spinner, TabPanel } from '@wordpress/components';
-import { useSelect, dispatch } from '@wordpress/data';
-import { useEffect, useState, useMemo } from '@wordpress/element';
-import apiFetch from '@wordpress/api-fetch';
-
-import Preview from './Preview';
-import Fields from './Fields';
-import ResponsiveButton from './ResponsiveButton';
-import ButtonExport from '../ButtonExport';
-
-/**
- * main component
- */
-const ThemerComponent = () => {
- const [ previewCss, setPreviewCss ] = useState( '' );
- const [ previewSize, setPreviewSize ] = useState();
-
- const { globalStylesId, baseConfig, userConfig } = useSelect(
- ( select ) => {
- const {
- __experimentalGetCurrentGlobalStylesId,
- __experimentalGetCurrentThemeBaseGlobalStyles,
- getEditedEntityRecord,
- } = select( 'core' );
-
- const currentGlobalStylesId =
- __experimentalGetCurrentGlobalStylesId();
-
- return {
- globalStylesId: currentGlobalStylesId, // eslint-disable no-underscore-dangle -- require underscore dangle for experimental functions
- baseConfig: __experimentalGetCurrentThemeBaseGlobalStyles(), // eslint-disable no-underscore-dangle -- require underscore dangle for experimental functions
- userConfig: getEditedEntityRecord(
- 'root',
- 'globalStyles',
- currentGlobalStylesId
- ),
- };
- }
- );
-
- /**
- * Returns merged base and user configs
- */
- const themeConfig = useMemo( () => {
- if ( isEmpty( userConfig ) ) {
- return baseConfig;
- }
- const merged = mergeWith( {}, baseConfig, userConfig );
- return merged;
- }, [ userConfig, baseConfig ] );
-
- /**
- * Fetch new preview CSS whenever config is changed
- */
- useEffect( () => {
- const updatePreviewCss = async () => {
- const res = await apiFetch( {
- path: '/themer/v1/styles',
- method: 'POST',
- data: themeConfig,
- } );
- if ( res ) {
- setPreviewCss( res );
- }
- };
- if ( themeConfig ) {
- updatePreviewCss();
- }
- }, [ themeConfig, setPreviewCss ] );
-
- /**
- * saves edited entity data
- */
- const save = async () => {
- dispatch( 'core' ).undo();
- try {
- await dispatch( 'core' ).saveEditedEntityRecord(
- 'root',
- 'globalStyles',
- globalStylesId
- );
- } catch ( err ) {
- // eslint-disable-next-line no-console
- console.log( err );
- }
- };
-
- /**
- * resets updated theme db data back to original theme.json
- */
- const reset = () => {
- dispatch( 'core' ).editEntityRecord(
- 'root',
- 'globalStyles',
- globalStylesId,
- baseConfig
- );
- };
-
- if ( ! themeConfig || ! previewCss ) {
- return (
- <>
-
- >
- );
- }
-
- return (
- <>
-
- reset() } text="Reset" />
- save() } text="Save" />
-
-
-
-
- { ( tab ) => (
- <>
- { tab.title }
-
- >
- ) }
-
-
-
-
- >
- );
-};
-
-export default ThemerComponent;
diff --git a/src/editor/context/EditorContext.js b/src/editor/context/EditorContext.js
new file mode 100644
index 0000000..0fb0b0d
--- /dev/null
+++ b/src/editor/context/EditorContext.js
@@ -0,0 +1,6 @@
+import { createContext } from '@wordpress/element';
+
+/**
+ * Sets a default context for use as state management across the plugin
+ */
+export default createContext();
diff --git a/src/editor/context/StylesContext.js b/src/editor/context/StylesContext.js
new file mode 100644
index 0000000..0fb0b0d
--- /dev/null
+++ b/src/editor/context/StylesContext.js
@@ -0,0 +1,6 @@
+import { createContext } from '@wordpress/element';
+
+/**
+ * Sets a default context for use as state management across the plugin
+ */
+export default createContext();
diff --git a/src/editor/styles/components/blocks.scss b/src/editor/styles/components/blocks.scss
new file mode 100644
index 0000000..2735121
--- /dev/null
+++ b/src/editor/styles/components/blocks.scss
@@ -0,0 +1,37 @@
+.themer--blocks-item-component {
+ padding: 8px 32px;
+
+ > summary {
+ display: flex;
+ cursor: pointer;
+ list-style: none;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ > summary::after {
+ content: "+";
+ }
+
+ &[open] > summary::after {
+ content: "-";
+ }
+
+ > summary svg {
+ width: 24px;
+ margin: 0 6px 0 0;
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ .themer--blocks-item-component--styles {
+ gap: 16px;
+ display: flex;
+ padding: 8px 32px;
+ flex-direction: column;
+ }
+
+ > .themer--blocks-item-component--styles > * {
+ flex: 1 1 auto;
+ }
+}
diff --git a/src/editor/styles/components/search.scss b/src/editor/styles/components/search.scss
new file mode 100644
index 0000000..2634092
--- /dev/null
+++ b/src/editor/styles/components/search.scss
@@ -0,0 +1,9 @@
+.themer--search-component {
+ > input {
+ border: 0;
+ width: 100%;
+ border-radius: 0;
+ padding: 8px 16px;
+ background: #f0f0f0;
+ }
+}
diff --git a/src/editor/styles/styles.scss b/src/editor/styles/styles.scss
index 414500c..9af1335 100644
--- a/src/editor/styles/styles.scss
+++ b/src/editor/styles/styles.scss
@@ -1,4 +1,8 @@
-@import './base.scss';
-@import './themerPreview.scss';
-@import './navigator.scss';
-@import './components/fontPicker.scss';
+@import "./base.scss";
+@import "./themerPreview.scss";
+@import "./navigator.scss";
+@import "./components/fontPicker.scss";
+
+// components
+@import "./components/search";
+@import "./components/blocks";
diff --git a/src/utils/block-helpers.js b/src/utils/block-helpers.js
new file mode 100644
index 0000000..5a24201
--- /dev/null
+++ b/src/utils/block-helpers.js
@@ -0,0 +1,49 @@
+import { select } from '@wordpress/data';
+
+/**
+ * Returns a list of core blocks that are in the theme.json schema
+ * and also have styles set in theme.json
+ *
+ * @param {Object} themeConfig Theme JSON
+ * @param {Object} schema Theme schema JSON
+ *
+ * @return {Array} Core blocks
+ */
+const getCoreBlocksFromSchema = ( themeConfig, schema ) => {
+ const schemaBlocks = Object.keys(
+ schema?.definitions?.stylesBlocksPropertiesComplete?.properties ?? {}
+ );
+ const themeJSONBlocks = Object.keys( themeConfig?.styles?.blocks ?? {} );
+
+ return schemaBlocks?.filter( ( block ) =>
+ themeJSONBlocks?.includes( block )
+ );
+};
+
+/**
+ * Returns a list of registered core blocks
+ *
+ * @param {number} mode Mode of operation
+ * 0: Use core store as data source
+ * 1: Use schema and theme.json as data source
+ * @param {Object} themeConfig Theme JSON
+ * @param {Object} schema Theme schema JSON
+ *
+ * @return {Array} Core blocks
+ */
+export const getCoreBlocks = ( mode = 0, themeConfig = {}, schema = {} ) => {
+ switch ( mode ) {
+ case 1:
+ return select( 'core/blocks' ).getBlockTypes();
+ case 0:
+ default:
+ return getCoreBlocksFromSchema( themeConfig, schema );
+ }
+};
+
+/**
+ * Returns a list of registered custom blocks
+ *
+ * @return {Array} Custom blocks
+ */
+export const getCustomBlocks = () => [];
diff --git a/src/utils/component-map.js b/src/utils/component-map.js
new file mode 100644
index 0000000..5dcbd45
--- /dev/null
+++ b/src/utils/component-map.js
@@ -0,0 +1,9 @@
+/**
+ * Style properties and their corresponding React components
+ */
+export const styleComponentMap = {};
+
+/**
+ * Settings properties and their corresponding React components
+ */
+export const settingComponentMap = {};
diff --git a/src/utils/get-theme-option.js b/src/utils/get-theme-option.js
new file mode 100644
index 0000000..df3b2a1
--- /dev/null
+++ b/src/utils/get-theme-option.js
@@ -0,0 +1,14 @@
+/**
+ * Returns the value of the specified selector from the base object
+ *
+ * @param {string} selector Property target selector
+ * @param {Object} base Theme settings
+ *
+ * @return {*} Value of selector
+ */
+const getThemeOption = ( selector, base ) => {
+ const selectorArray = selector.split( '.' );
+ return selectorArray.reduce( ( acc, curr ) => acc?.[ curr ], base );
+};
+
+export default getThemeOption;
diff --git a/src/utils/schema-helpers.js b/src/utils/schema-helpers.js
new file mode 100644
index 0000000..96534f8
--- /dev/null
+++ b/src/utils/schema-helpers.js
@@ -0,0 +1,57 @@
+import { styleComponentMap } from './component-map';
+
+/**
+ * Maps style properties to React components
+ *
+ * @param {Object} properties Theme style allowed properties
+ *
+ * @return {Object} Mapped components
+ */
+export const generateStyleComponents = ( properties ) => {
+ if ( ! properties ) {
+ return {};
+ }
+
+ const components = {};
+
+ for ( const property in properties ) {
+ const style = styleComponentMap?.[ property ];
+
+ if ( ! style ) {
+ continue;
+ }
+
+ components[ property ] = style;
+ }
+
+ return components;
+};
+
+/**
+ * Fetch theme.json schema data
+ *
+ * @return {Object} theme.json schema json
+ */
+const fetchSchema = async () => {
+ let schema = {};
+ const url =
+ 'https://raw.githubusercontent.com/WordPress/gutenberg/trunk/schemas/json/theme.json';
+
+ try {
+ const response = await fetch( url );
+
+ if ( ! response?.ok ) {
+ throw new Error(
+ `${ response?.status } ${ response?.statusText }`
+ );
+ }
+
+ schema = await response.json();
+ } catch ( error ) {
+ console.error( error ); // eslint-disable-line no-console -- Output of caught error
+ }
+
+ return schema ?? {};
+};
+
+export default fetchSchema;