import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { AdvancedTab } from "./tabs/Advanced"; import { ChipsTab } from "./tabs/Chips"; import { GeneralTab } from "./tabs/General"; import { GettingStartedTab } from "./tabs/GettingStarted"; import { TooltipsTab } from "./tabs/Tooltips"; import { TypographyTab } from "./tabs/Typography"; import { useSearchIndex } from "./hooks/useSearchIndex"; import { coerceInt } from "./constants"; import { baseTabs } from "./baseTabs"; import { BASE_LAYOUT_LABELS, BASE_STYLE_LABELS } from "./previewUtils"; import { Preview, SearchContext, SearchResultsView } from "./components"; import type { SearchableFieldData } from "./components"; import type { RequiresToggle } from "./settingsSchema"; import type { Settings, TabId, ToastState } from "./types"; export type { Settings } from "./types"; const BASE_TAB_IDS = new Set([ "general", "chips", "typography", "tooltips", "advanced", ]); type AdminSectionId = "getting-started" | TabId; type NavIconProps = { className?: string }; export interface AppProps { data?: Record; } function BookIcon({ className = "b3-wvs-w-4 b3-wvs-h-4" }: NavIconProps) { return ( ); } function GearIcon({ className = "b3-wvs-w-4 b3-wvs-h-4" }: NavIconProps) { return ( ); } function SwatchIcon({ className = "b3-wvs-w-4 b3-wvs-h-4" }: NavIconProps) { return ( ); } function TypeIcon({ className = "b3-wvs-w-4 b3-wvs-h-4" }: NavIconProps) { return ( ); } function TooltipIcon({ className = "b3-wvs-w-4 b3-wvs-h-4" }: NavIconProps) { return ( ); } function WrenchIcon({ className = "b3-wvs-w-4 b3-wvs-h-4" }: NavIconProps) { return ( ); } const NAV_ICONS: Record> = { "getting-started": BookIcon, general: GearIcon, chips: SwatchIcon, typography: TypeIcon, tooltips: TooltipIcon, advanced: WrenchIcon, }; function evaluateToggleState( settings: Settings, requiresToggle?: RequiresToggle, ): boolean { if (!requiresToggle) { return true; } const settingValue = settings[requiresToggle.settingKey as keyof Settings]; const expectedValue = requiresToggle.enabledValue === undefined ? true : requiresToggle.enabledValue; switch (requiresToggle.operator ?? "equals") { case "greaterThan": return Number(settingValue) > Number(expectedValue); case "notEquals": return settingValue !== expectedValue; case "equals": default: return settingValue === expectedValue; } } function App({ data: providedData }: AppProps) { const data = providedData ?? (window as any).b3WvsData ?? {}; const restUrl = data.rest_url || ""; const nonce = data.nonce || ""; const adminColor = data.admin_color || "#4f46e5"; const adminUrls = data.admin_urls ?? {}; const usageStats = data.usage_stats ?? { terms_total: 0, attribute_taxonomies_configured: 0, products_with_overrides: 0, by_type: { color: 0, button: 0 }, }; const [settings, setSettings] = useState( () => (data.settings ?? {}) as Settings, ); const [activeSection, setActiveSection] = useState(() => { if (typeof window === "undefined") { return "getting-started"; } const hash = window.location.hash.replace(/^#/, ""); if (hash === "getting-started") { return "getting-started"; } return BASE_TAB_IDS.has(hash) ? (hash as TabId) : "getting-started"; }); const [activeGeneralSubTab, setActiveGeneralSubTab] = useState< "layout" | "size" | "options" >("layout"); const [toast, setToast] = useState({ show: false, type: "success", message: "", }); const [saving, setSaving] = useState(false); const [resetting, setResetting] = useState(false); const [previewSelectedIndex, setPreviewSelectedIndex] = useState(0); const [searchQuery, setSearchQuery] = useState(""); const [highlightedFieldId, setHighlightedFieldId] = useState( null, ); const toastTimeoutRef = useRef(null); const highlightTimeoutRef = useRef(null); // ── Nav slider animation state ── const navRef = useRef(null); const navBtnRefs = useRef>({}); const [navSliderStyle, setNavSliderStyle] = useState({ height: 0, width: 0, transform: "translate(0,0)", opacity: 0, }); const updateNavSlider = useCallback(() => { const activeEl = navBtnRefs.current[activeSection]; const container = navRef.current; if (!activeEl || !container) return; try { const aRect = activeEl.getBoundingClientRect(); const cRect = container.getBoundingClientRect(); if (aRect.height === 0 || cRect.height === 0) return; setNavSliderStyle({ height: aRect.height, width: aRect.width, transform: `translate(${aRect.left - cRect.left}px,${aRect.top - cRect.top}px)`, opacity: 1, }); } catch { /* ignore measurement errors during resize/zoom */ } }, [activeSection]); useEffect(() => { requestAnimationFrame(updateNavSlider); }, [updateNavSlider]); useEffect(() => { let timer: number; const onResize = () => { clearTimeout(timer); timer = window.setTimeout( () => requestAnimationFrame(updateNavSlider), 50, ); }; window.addEventListener("resize", onResize, { passive: true }); const fallback = setTimeout(updateNavSlider, 150); return () => { window.removeEventListener("resize", onResize); clearTimeout(fallback); clearTimeout(timer); }; }, [updateNavSlider]); const { fields, registerField, unregisterField, search } = useSearchIndex({ useSchema: true, }); useEffect(() => { let cancelled = false; async function loadSettings() { if (!restUrl) { return; } try { const response = await fetch(`${restUrl}/settings`, { method: "GET", headers: { "X-WP-Nonce": nonce }, credentials: "same-origin", }); if (!response.ok) { throw new Error("GET settings failed"); } const json = (await response.json()) as | Settings | { settings?: Settings }; const resolved = json && typeof json === "object" && "settings" in json ? json.settings : (json as Settings); if (!cancelled && resolved && typeof resolved === "object") { setSettings(resolved); } } catch { // Ignore; localized settings are already present. } } void loadSettings(); return () => { cancelled = true; }; }, [nonce, restUrl]); useEffect(() => { return () => { if (toastTimeoutRef.current) { window.clearTimeout(toastTimeoutRef.current); } if (highlightTimeoutRef.current) { window.clearTimeout(highlightTimeoutRef.current); } }; }, []); const searchResults = useMemo(() => { if (!searchQuery.trim()) { return []; } return search(searchQuery); }, [search, searchQuery]); const navItems = useMemo( () => [ { id: "getting-started" as AdminSectionId, label: "Getting Started", Icon: BookIcon, }, ...baseTabs.map((tab) => ({ id: tab.id as AdminSectionId, label: tab.label, Icon: NAV_ICONS[tab.id as AdminSectionId] ?? GearIcon, })), ], [], ); const previewShowLabels = settings.show_labels !== false; const previewShowLabelsColors = settings.show_labels_colors !== false; const previewEnableTooltips = settings.enable_tooltips !== false; const previewLayout = "wrap"; const previewSelectorStyle = "underline"; const previewLayoutPaddingTop = coerceInt(settings.layout_padding_top, 5); const previewLayoutPaddingRight = coerceInt(settings.layout_padding_right, 5); const previewLayoutPaddingBottom = coerceInt( settings.layout_padding_bottom, 5, ); const previewLayoutPaddingLeft = coerceInt(settings.layout_padding_left, 5); const previewLabelFontSize = coerceInt(settings.label_font_size, 14); const previewLabelTextColor = typeof settings.label_text_color === "string" ? settings.label_text_color : ""; const previewSwatchWidth = coerceInt(settings.swatch_width, 32); const previewSwatchHeight = coerceInt(settings.swatch_height, 32); const previewSwatchGap = coerceInt(settings.swatch_gap, 12); const attributesAdminUrl = typeof adminUrls.attributes === "string" ? adminUrls.attributes : "#"; const productsAdminUrl = typeof adminUrls.products === "string" ? adminUrls.products : "#"; function setActiveSectionWithHash(section: AdminSectionId) { setActiveSection(section); if (typeof window !== "undefined") { const url = new URL(window.location.href); url.hash = section; window.history.replaceState( null, "", `${url.pathname}${url.search}${url.hash}`, ); } } function showToast(payload: { type: ToastState["type"]; message: string; durationMs?: number; }) { setToast({ show: true, type: payload.type, message: payload.message }); const durationMs = typeof payload.durationMs === "number" && payload.durationMs > 300 ? payload.durationMs : 2800; if (toastTimeoutRef.current) { window.clearTimeout(toastTimeoutRef.current); } toastTimeoutRef.current = window.setTimeout(() => { setToast((current) => ({ ...current, show: false })); toastTimeoutRef.current = null; }, durationMs); } function highlightField(fieldId: string) { setHighlightedFieldId(fieldId); if (highlightTimeoutRef.current) { window.clearTimeout(highlightTimeoutRef.current); } highlightTimeoutRef.current = window.setTimeout(() => { setHighlightedFieldId(null); highlightTimeoutRef.current = null; }, 3000); window.setTimeout(() => { const element = document.getElementById(fieldId); element?.scrollIntoView({ behavior: "smooth", block: "center" }); }, 100); } function navigateToField(target: { id: string; tabId: string; subTab?: string; }) { if (target.tabId === "general" && target.subTab) { setActiveGeneralSubTab(target.subTab as "layout" | "size" | "options"); } setSearchQuery(""); setActiveSectionWithHash(target.tabId as AdminSectionId); highlightField(target.id); } function handleSelectSearchResult(result: SearchableFieldData) { if ( result.requiresToggle && !evaluateToggleState(settings, result.requiresToggle) ) { const toggleTarget = fields.find( (field) => field.id === result.requiresToggle?.toggleFieldId, ); if (toggleTarget) { showToast({ type: "info", message: result.requiresToggle.message ?? "Enable the parent setting first.", durationMs: 3200, }); navigateToField({ id: toggleTarget.id, tabId: toggleTarget.tabId, subTab: toggleTarget.subTab, }); return; } } navigateToField({ id: result.id, tabId: result.tabId, subTab: result.subTab, }); } async function handleSave() { if (!restUrl) { return; } setSaving(true); try { const response = await fetch(`${restUrl}/settings`, { method: "POST", headers: { "Content-Type": "application/json", "X-WP-Nonce": nonce, }, credentials: "same-origin", body: JSON.stringify(settings), }); if (!response.ok) { throw new Error("Save failed"); } const json = (await response.json()) as { settings?: Settings }; if (json.settings) { setSettings(json.settings); } showToast({ type: "success", message: "Settings saved" }); } catch { showToast({ type: "error", message: "Failed to save settings" }); } finally { setSaving(false); } } async function handleResetToDefaults() { if (!restUrl) { return; } const confirmed = window.confirm( "Reset all settings to their default values?", ); if (!confirmed) { return; } setResetting(true); try { const response = await fetch(`${restUrl}/settings/reset`, { method: "POST", headers: { "X-WP-Nonce": nonce }, credentials: "same-origin", }); if (!response.ok) { throw new Error("Reset failed"); } const json = (await response.json()) as { settings?: Settings }; if (json.settings) { setSettings(json.settings); } showToast({ type: "success", message: "Settings reset to defaults" }); } catch { showToast({ type: "error", message: "Failed to reset settings" }); } finally { setResetting(false); } } const searchContextValue = useMemo( () => ({ registerField, unregisterField, highlightedFieldId, }), [highlightedFieldId, registerField, unregisterField], ); const isSearching = searchQuery.trim().length > 0; const showSaveReset = activeSection !== "getting-started" || isSearching; return (

B3 Swatches Settings

{showSaveReset ? ( <> ) : null}
{isSearching ? (

Search Results

) : null}
{isSearching ? ( setSearchQuery("")} isToggleEnabled={(result) => evaluateToggleState(settings, result.requiresToggle) } /> ) : activeSection === "getting-started" ? ( ) : ( <> {activeSection === "general" ? ( ) : null} {activeSection === "chips" ? ( ) : null} {activeSection === "typography" ? ( ) : null} {activeSection === "tooltips" ? ( ) : null} {activeSection === "advanced" ? ( ) : null}
)}
); } export default App;