/**
* WordPress dependencies
*/
import { registerPlugin } from "@wordpress/plugins";
import { __ } from "@wordpress/i18n";
import { useEffect, useState, useCallback, useMemo, useRef } from "@wordpress/element";
import { dispatch, useSelect, useDispatch, createReduxStore, register } from "@wordpress/data";
import "./editor.scss";
import { PanelBody, Button, Notice, __experimentalPaletteEdit as PaletteEdit } from "@wordpress/components";
import apiFetch from "@wordpress/api-fetch";
import domReady from "@wordpress/dom-ready";
import { SPToggleGroupControl } from "../components";
import { DefaultPresetAttr, generateRootCSS } from "./default-settings";
import { breakpointDefault, globalTypographyDefault, shadowDefaultList } from "./defaultConstant";
import { SmartPostShowLogoIcon } from "../icons/icons";
import TypographySettings from "./typography/typography";
import ShadowSettings from "./shadow/shadow";
import Breakpoint from "./breakpoint/breakpoint";
import CustomCss from "./custom-css/customCss";
import { googleFonts } from "../controls/controls";
import { useEditorSaveStatus } from "../hooks/useEditorSaveStatus";
// ============================================================================
// Constants & Helpers
// ============================================================================
const colorStacks = DefaultPresetAttr("colorStacks");
const COLOR_KEYS = [
{ name: "Light (Text)", slug: "light-text" },
{ name: "Background", slug: "background" },
{ name: "Primary Light", slug: "primary-light" },
{ name: "Primary", slug: "primary" },
{ name: "Primary Dark", slug: "primary-dark" },
{ name: "Secondary", slug: "secondary" },
{ name: "Dark 2 (Text)", slug: "dark-2-text" },
{ name: "Dark (Text)", slug: "dark-text" },
{ name: "Black", slug: "black" },
];
const mapColorStackToPreset = (stackColors) => {
return stackColors.map((color, index) => ({
name: COLOR_KEYS[index]?.name || `color-${index}`,
slug: COLOR_KEYS[index]?.slug || `color-${index}`,
color,
}));
};
function normalizeGradient(gradient) {
if (!gradient) {
return "";
}
return gradient
.toLowerCase()
.replace(/\s+/g, "")
.replace(/(\d+)\.0+deg/g, "$1deg");
}
// ============================================================================
// Default Settings
// ============================================================================
const DEFAULT_SETTINGS = {
colors: {
defaultColors: mapColorStackToPreset(colorStacks?.color_type_1),
selectedColorStack: "color_type_1",
presetColors: [],
customColors: [],
defaultGradient: [
{
name: "Ocean Breeze",
gradient: "linear-gradient(46deg,rgb(0,97,255) 4%,rgb(96,239,255) 93%)",
slug: "ocean_breeze",
},
{
name: "Cosmic Fusion",
gradient: "linear-gradient(46deg,rgb(232,28,255) 4%,rgb(64,201,255) 93%)",
slug: "cosmic_fusion",
},
{
name: "Sunset Blaze",
gradient: "linear-gradient(46deg,rgb(255,15,123) 4%,rgb(248,155,41) 93%)",
slug: "sunset_blaze",
},
{
name: "Dreamy Violet",
gradient: "linear-gradient(46deg,rgb(211,151,250) 4%,rgb(131,100,232) 93%)",
slug: "dreamy_violet",
},
{
name: "Peach Bloom",
gradient: "linear-gradient(46deg,rgb(246,213,247) 4%,rgb(251,233,215) 93%)",
slug: "peach_bloom",
},
{
name: "Aqua Depths",
gradient: "linear-gradient(46deg,rgb(0,173,181) 4%,rgb(9,95,92) 93%)",
slug: "aqua_depths",
},
{
name: "Midnight Steel",
gradient: "linear-gradient(46deg,rgb(36,55,72) 4%,rgb(75,116,159) 93%)",
slug: "midnight_steel",
},
],
customGradients: [],
},
typography: globalTypographyDefault,
shadow: shadowDefaultList,
breakpoint: breakpointDefault,
customCss: "",
rootcss:
":root{ --sp-smart-breakpoint-tablet: 1023px; --sp-smart-breakpoint-mobile: 767px; --smart-post-light-text: #FAFAFA; --smart-post-background: #FFFFFF; --smart-post-primary-light: #EBEBEB; --smart-post-primary: #999999; --smart-post-primary-dark: #1D1D1D; --smart-post-secondary: #0054FB; --smart-post-dark-2-text: #3E3E3E; --smart-post-dark-text: #0A0A0A; --smart-post-black: #000000;} :root { --smart-post-shadow-subtle-1dp: 0px 1px 2px 0px rgba(0, 0, 0, 0.12); --smart-post-shadow-light-2dp: 0px 2px 4px 0px rgba(0, 0, 0, 0.14); --smart-post-shadow-medium-4dp: 0px 4px 6px 0px rgba(0, 0, 0, 0.16); --smart-post-shadow-strong-8dp: 0px 8px 18px 0px rgba(0, 0, 0, 0.18); --smart-post-shadow-deep-12dp: 0px 12px 17px 0px rgba(0, 0, 0, 0.20); --smart-post-shadow-sharp-4dp: 4px 4px 0px 0px rgba(0, 0, 0, 0.25);}",
};
// ============================================================================
// Redux Store
// ============================================================================
const smartpostStore = createReduxStore("smartpost/global-settings", {
reducer: (state = DEFAULT_SETTINGS, action) => {
switch (action.type) {
case "SET_SETTINGS":
return {
...state,
...action.settings,
_isDirty: false,
};
case "UPDATE_SETTING":
const isComputedUpdate =
action.isComputed ||
(action.category === "typography" &&
(action.key === "typographyCss" || action.key === "fontList")) ||
(action.category === "shadow" && action.key === "shadowRootCSS") ||
action.key === "rootcss";
return {
...state,
[action.category]: {
...state[action.category],
[action.key]: action.value,
},
_isDirty: isComputedUpdate ? state._isDirty : true,
};
case "ADD_CUSTOM_COLOR":
return {
...state,
colors: {
...state.colors,
customColors: [...state.colors.customColors, action.color],
},
_isDirty: true,
};
case "ADD_DEFAULT_COLOR":
return {
...state,
colors: {
...state.colors,
// Changed from defaultColor to defaultColors.
defaultColors: [...state.colors.defaultColors, action.color],
},
_isDirty: true,
};
case "SET_ROOT_CSS":
return {
...state,
rootcss: action.css,
_isDirty: state._isDirty,
};
case "SET_CUSTOM_CSS":
return {
...state,
customCss: action.css,
_isDirty: true,
};
case "REMOVE_CUSTOM_COLOR":
return {
...state,
colors: {
...state.colors,
customColors: state.colors.customColors.filter((_, i) => i !== action.index),
},
_isDirty: true,
};
case "UPDATE_PRESET_COLORS":
return {
...state,
colors: {
...state.colors,
presetColors: action.colors,
},
_isDirty: true,
};
case "SET_SELECTED_COLOR_STACK":
return {
...state,
colors: {
...state.colors,
selectedColorStack: action.stackKey,
},
_isDirty: true,
};
default:
return state;
}
},
actions: {
setSettings: (settings) => ({ type: "SET_SETTINGS", settings }),
updateSetting: (category, key, value) => ({
type: "UPDATE_SETTING",
category,
key,
value,
}),
addCustomColor: (color) => ({ type: "ADD_CUSTOM_COLOR", color }),
addDefaultColor: (color) => ({ type: "ADD_DEFAULT_COLOR", color }),
removeCustomColor: (index) => ({
type: "REMOVE_CUSTOM_COLOR",
index,
}),
setRootCSS: (css) => ({ type: "SET_ROOT_CSS", css }),
setCustomCSS: (css) => ({ type: "SET_CUSTOM_CSS", css }),
updatePresetColors: (colors) => ({
type: "UPDATE_PRESET_COLORS",
colors,
}),
setSelectedColorStack: (stackKey) => ({
type: "SET_SELECTED_COLOR_STACK",
stackKey,
}),
},
selectors: {
getSettings: (state) => state,
getSetting: (state, category, key) => state[category]?.[key],
getCategory: (state, category) => state[category] || {},
getSelectedColorStack: (state) => state.colors?.selectedColorStack,
getPresetColors: (state) => state.colors?.presetColors || [],
getCustomColors: (state) => state.colors?.customColors || [],
getDefaultColors: (state) => state.colors?.defaultColors || [],
getDefaultGradient: (state) => state.colors?.defaultGradient || [],
getCustomGradients: (state) => state.colors?.customGradients || [],
getRootCSS: (state) => state.rootcss || "",
getCustomCSS: (state) => state.customCss || "",
getAllColors: (state) => state.colors || [],
isDirty: (state) => state._isDirty || false,
},
});
register(smartpostStore);
// ============================================================================
// API Functions
// ============================================================================
const loadGlobalSettings = async () => {
try {
const fetchedSettings = await apiFetch({
path: "/smart-post/v2/get-global-settings",
});
// Ensure we have a selected color stack
const colorStacks = DefaultPresetAttr("colorStacks");
const firstStackKey = Object.keys(colorStacks)[0];
if (!fetchedSettings.colors?.selectedColorStack && firstStackKey) {
// Changed from undefined 'settings' to 'fetchedSettings'
fetchedSettings.colors = fetchedSettings.colors || {};
fetchedSettings.colors.selectedColorStack = firstStackKey;
fetchedSettings.colors.presetColors = mapColorStackToPreset(colorStacks[firstStackKey]);
}
if (!fetchedSettings.colors.defaultGradient) {
fetchedSettings.colors.defaultGradient = DEFAULT_SETTINGS?.colors?.defaultGradient;
}
dispatch("smartpost/global-settings").setSettings(fetchedSettings);
return fetchedSettings;
} catch (error) {
console.log("Failed to load settings:", error);
// Initialize with first color stack as fallback.
const colorStacks = DefaultPresetAttr("colorStacks");
const firstStackKey = Object.keys(colorStacks)[0];
const fallbackSettings = { ...DEFAULT_SETTINGS };
if (firstStackKey) {
fallbackSettings.colors.selectedColorStack = firstStackKey;
fallbackSettings.colors.presetColors = mapColorStackToPreset(colorStacks[firstStackKey]);
}
dispatch("smartpost/global-settings").setSettings(fallbackSettings);
return fallbackSettings;
}
};
const saveGlobalSettings = async (settings) => {
try {
const updated = await apiFetch({
path: "/smart-post/v2/global-settings",
method: "POST",
data: settings,
});
return updated;
} catch (error) {
console.log("Failed to save SmartPost settings:", error);
// Return error object instead of throwing to allow graceful handling.
return { error: true, message: error.message };
}
};
// ============================================================================
// CSS Generation & Updates
// ============================================================================
const generateAllRootCSS = (settings) => {
const cssParts = [];
const globalFontFamily = settings?.typography?.fontList?.filter(item => {
const [name] = item.split(":");
return name.trim() !== "";
});
if (globalFontFamily) {
const fontList = globalFontFamily?.map(
(item) => `family=${item?.replaceAll(/[:\s]+/g, (match) => (match === ":" ? ":wght@" : "+"))}&`
);
const fontsCss = fontList?.length > 0 ? `@import url('https://fonts.googleapis.com/css2?${fontList.join("")}display=swap');` : "";
cssParts.push(fontsCss);
}
if (settings?.typography?.typographyCss) {
cssParts.push(settings.typography.typographyCss);
}
if (settings?.rootcss) {
cssParts.push(settings.rootcss);
}
if (settings?.shadow?.shadowRootCSS) {
cssParts.push(settings.shadow.shadowRootCSS);
}
if (settings?.customCss) {
cssParts.push(settings.customCss);
}
return cssParts.join(" ");
};
// const updateRootCSS = (settings) => {
// const googleFonts = "";
// const css = generateAllRootCSS(settings);
// const existingTag = document.getElementById("sp-smart-post-global-root-inline-css");
// let styleTag = document.getElementById("sp-smart-post-global-root-inline-css-extra");
// if (!styleTag) {
// styleTag = document.createElement("style");
// styleTag.id = "sp-smart-post-global-root-inline-css-extra";
// if (existingTag && existingTag.parentNode) {
// // Insert new style tag immediately after the existing one
// existingTag.parentNode.insertBefore(styleTag, existingTag.nextSibling);
// }
// }
// styleTag.innerHTML = css;
// };
const updateRootCSS = (settings) => {
const googleFonts = ""; // keep this if needed
const css = generateAllRootCSS(settings);
const existingTag = document.getElementById("sp-smart-post-global-root-inline-css");
let styleTag = document.getElementById("sp-smart-post-global-root-inline-css-extra");
// Insert CSS in Main Document
if (!styleTag) {
styleTag = document.createElement("style");
styleTag.id = "sp-smart-post-global-root-inline-css-extra";
if (existingTag && existingTag.parentNode) {
existingTag.parentNode.insertBefore(styleTag, existingTag.nextSibling);
} else {
document.head.appendChild(styleTag);
}
}
styleTag.innerHTML = css;
// ---------------------------------------------
// ADD CSS ALSO INSIDE THE GUTENBERG IFRAME
// ---------------------------------------------
const applyCssToIframe = (iframe) => {
if (!iframe || !iframe.contentDocument) return;
const iframeDoc = iframe.contentDocument;
let iframeStyleTag = iframeDoc.getElementById("sp-smart-post-global-root-inline-css-extra");
if (!iframeStyleTag) {
iframeStyleTag = iframeDoc.createElement("style");
iframeStyleTag.id = "sp-smart-post-global-root-inline-css-extra";
iframeDoc.head?.appendChild(iframeStyleTag);
}
iframeStyleTag.innerHTML = css;
};
// Detect Gutenberg iframe (current WP versions)
// Gutenberg canvas iframe usually looks like:
// iframe[name="editor-canvas"] OR div.editor-styles-wrapper iframe
const iframeSelectors = ['iframe[name="editor-canvas"]', ".editor-styles-wrapper iframe", "iframe.editor-canvas"];
iframeSelectors.forEach((selector) => {
const iframe = document.querySelector(selector);
if (iframe) applyCssToIframe(iframe);
});
// If iframe loads later (React re-renders), observe and reapply
const observer = new MutationObserver(() => {
iframeSelectors.forEach((selector) => {
const iframe = document.querySelector(selector);
if (iframe) applyCssToIframe(iframe);
});
});
observer.observe(document.body, { childList: true, subtree: true });
};
// ============================================================================
// Color Settings Component
// ============================================================================
const ColorSettings = () => {
const {
selectedColorStack,
presetColors,
customColors,
defaultGradient: storedDefaultGradient,
customGradients,
allColors,
breakPoint,
} = useSelect((select) => {
const store = select("smartpost/global-settings");
return {
selectedColorStack: store.getSelectedColorStack(),
presetColors: store.getPresetColors(),
customColors: store.getCustomColors(),
defaultGradient: store.getDefaultGradient(),
customGradients: store.getCustomGradients(),
allColors: store.getAllColors(),
breakPoint: store.getCategory("breakpoint"),
};
}, []);
const { updateSetting, setSelectedColorStack, updatePresetColors, setRootCSS } =
useDispatch("smartpost/global-settings");
const [selectedTab, setSelectedTab] = useState("solid");
// Memoize base gradient to prevent recreating on every render
const baseGradient = useMemo(() => DEFAULT_SETTINGS?.colors?.defaultGradient, []);
// Use fallback if default gradient is empty.
const defaultGradient = !storedDefaultGradient?.length ? baseGradient : storedDefaultGradient;
const handleColorStackSelection = useCallback(
(stackKey, stackColors) => {
setSelectedColorStack(stackKey);
const mappedPresetColors = mapColorStackToPreset(stackColors);
updatePresetColors(mappedPresetColors);
},
[setSelectedColorStack, updatePresetColors]
);
const popoverProps = useMemo(
() => ({
placement: "bottom",
offset: 36,
shift: true,
flip: true,
}),
[]
);
// Better dependency management for CSS generation
useEffect(() => {
if (allColors) {
const generatedCSS = generateRootCSS(allColors, breakPoint);
setRootCSS(generatedCSS);
}
}, [allColors, breakPoint, setRootCSS]);
const tabs = useMemo(
() => [
{
name: "solid",
label: __("Solid", "post-carousel"),
value: "solid",
},
{
name: "gradient",
label: __("Gradient", "post-carousel"),
value: "gradient",
},
],
[]
);
return (
{__("Color Palettes", "post-carousel")}
{ __( "Gradients", "post-carousel" ) }
*/}