import React, {FC, useEffect, useRef, useState} from "react"; import {TreePreconditions} from "./TreePreconditions"; import {useAtom, useAtomValue, useSetAtom} from "jotai/index"; import { AddNewTreePopupWindowIsOpen, MobileOptionsPanelOpenAtom, rightSidebarWidthAtom, sidebarEdgeOffsetAtom } from "./atoms"; import {useCopyToClipboard, useMeasure} from "react-use"; import {__, CouponsPlus} from "../globals"; import {Button} from "./cards/Button"; import {GroveExporter} from "./export/GroveExporter"; import {useNodesStore} from "./store"; import {ExportableDataFormatPreset} from "./export/AppImporter"; import {TreePredates} from "./TreePredates"; import {NewTreeButton} from "./NewTreeButton"; import {AppExporter} from "./export/AppExporter"; import {ExportImportTarget} from "./export/Exporters"; import {SaveButton} from "./SaveButton"; import {AutoApplyButton} from "./AutoApplyButton"; import {GoToStoreButton} from "./GoToStoreButton"; import toast from "react-hot-toast"; import {useIsMobile} from "./hooks"; import classNames from "classnames"; import {nanoid} from "nanoid"; import AutosizeInput from 'react-input-autosize'; export type TreeSidebarRightProps = {} export const TreeSidebarRight: FC = ({}) => { const [sidebarWidth, setSidebarWidth] = useAtom(rightSidebarWidthAtom) const edgeOffset = useAtomValue(sidebarEdgeOffsetAtom) const [ref, {width}] = useMeasure() const isMobile = useIsMobile() const [isOptionsPanelOpen, setIsOptionsPanelOpen] = useAtom(MobileOptionsPanelOpenAtom) const hasSelectedPreconditions = useNodesStore(({preconditionsID, testableRelations}) => { if (!preconditionsID) { return false } return (testableRelations.find(({id}) => id === preconditionsID)?.children?.length || 0) > 0 }) const hasSelectedPredates = useNodesStore(({predatesID, testableRelations}) => { if (!predatesID) { return false } return (testableRelations.find(({id}) => id === predatesID)?.children?.length || 0) > 0 }) const shouldRenderPredatesFirst = hasSelectedPredates && !hasSelectedPreconditions const preconditionAndPredateSections = shouldRenderPredatesFirst ? [, ] : [, ] const desktopPreconditionsPredatesMaxHeight = `calc(100vh - ${edgeOffset * 2 + (CouponsPlus.environment === 'development' ? 190 : 152)}px)` useEffect(() => { if (width !== sidebarWidth) { setSidebarWidth(width) } }, [width, setSidebarWidth]) // Mobile: Top floating controls with coupon code if (isMobile) { return ( ) } // Desktop: Original sidebar return
{CouponsPlus.implementations.engine.trees && }
{CouponsPlus.environment === 'development' &&
} {false &&

Add an action to save

} {preconditionAndPredateSections}
; }; function exportGroveToConsole(target: ExportImportTarget = 'preset', exportInPieces: boolean = false) { const state = useNodesStore.getState(); const appExporter = new AppExporter(state, new GroveExporter(state)) let exportedData = appExporter.export(target) as ExportableDataFormatPreset; console.log(`exported [${target}]:`) if (exportInPieces) { console.log('Exporting in pieces:') Object.keys(exportedData).forEach(key => { console.log(`${key}:`, JSON.stringify(exportedData[key as keyof typeof exportedData] || '')) }) } else { const jsonString = JSON.stringify(exportedData); console.log(jsonString) console.log(exportedData) // Copy to clipboard navigator.clipboard.writeText(jsonString).then(() => { console.log('Copied to clipboard!') toast.success('Export copied to clipboard!') }).catch(err => { console.error('Failed to copy to clipboard:', err) toast.error('Failed to copy to clipboard') }) } } // Mobile top bar component with all controls type MobileTopBarProps = { isOptionsPanelOpen: boolean; setIsOptionsPanelOpen: (open: boolean) => void; shouldRenderPredatesFirst: boolean; } const MobileTopBar: FC = ({isOptionsPanelOpen, setIsOptionsPanelOpen, shouldRenderPredatesFirst}) => { const [inputValue, setInputValue] = useState((CouponsPlus.state as any).code || '') const [, copyToClipboard] = useCopyToClipboard() const inputRef = useRef(null) const edgeOffset = useAtomValue(sidebarEdgeOffsetAtom) const openNewTreePopup = useSetAtom(AddNewTreePopupWindowIsOpen) const preconditionAndPredateSections = shouldRenderPredatesFirst ? [, ] : [, ] useEffect(() => { const wpInput = document.querySelector('input[name="post_title"]') if (wpInput) { wpInput.value = inputValue } }, [inputValue]) const handleGenerate = () => { setInputValue(nanoid(8)) } const handleCopy = () => { if (inputValue) { copyToClipboard(inputValue) toast.success(__('Copied!')) } } return ( <> {/* Floating top controls - VisionOS-like dark blurred pills (mobile-only) */}
{/* Inner centered wrapper prevents overflow on narrow screens */}
{/* Left side - Coupon code pill (light bluish-gray blurred, no border) */}
setInputValue(e.target.value)} inputStyle={{ backgroundColor: 'transparent', border: 'none', outline: 'none', fontSize: '13px', fontWeight: 600, letterSpacing: '0.02em', color: 'rgba(16, 40, 66, 0.95)', minWidth: '56px', maxWidth: '100px', }} />
{/* Right side - Floating light bluish-gray pill controls (no borders/shadows) */}
{/* New Tree + button */} {CouponsPlus.implementations.engine.trees && } {/* Options toggle button */} {/* AutoApply button */}
{/* Save button - separate floating pill (light bluish-gray blurred, no border/shadow) */}
{/* Options panel */} {isOptionsPanelOpen && ( <> {/* Backdrop */}
setIsOptionsPanelOpen(false)} /> {/* Panel */}
Options
{preconditionAndPredateSections}
)} ) } type ScrollableSidebarGroupsProps = { maxHeight: string; className?: string; children: React.ReactNode; } const ScrollableSidebarGroups: FC = ({maxHeight, className, children}) => { const scrollableGroupsRef = useRef(null) const [showFade, setShowFade] = useState(false) const updateFadeVisibility = React.useCallback(() => { const container = scrollableGroupsRef.current if (!container) { return } const hasVerticalOverflow = container.scrollHeight > container.clientHeight + 1 const hasMoreItemsBelow = container.scrollTop + container.clientHeight < container.scrollHeight - 1 setShowFade(hasVerticalOverflow && hasMoreItemsBelow) }, []) useEffect(() => { updateFadeVisibility() }, [children, maxHeight, updateFadeVisibility]) useEffect(() => { const container = scrollableGroupsRef.current if (!container) { return } updateFadeVisibility() container.addEventListener('scroll', updateFadeVisibility, {passive: true}) window.addEventListener('resize', updateFadeVisibility) return () => { container.removeEventListener('scroll', updateFadeVisibility) window.removeEventListener('resize', updateFadeVisibility) } }, [updateFadeVisibility]) return
{children}
{showFade &&
}
}