/** * Ajax Search Customization Tab * Templates, Customize Design (accordion sections), Text Labels, and sticky preview */ import { useState } from 'react'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Switch } from '@/components/ui/switch'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; import { Palette, LayoutTemplate, Type, Square, Eye, Image, Tag, DollarSign, Star, Package, FileText, Search, MessageSquare, Copy, Check, Code, ChevronDown, Settings, Columns, Minus, Plus, X, } from 'lucide-react'; import { AjaxSearchSettings, SHADOW_OPTIONS, TEMPLATE_PRESETS } from '../config'; import { ProIndicator } from '@/components/pro/pro-indicator'; import { GlobalColorPicker, resolveGlobalColor } from '@/components/ui/global-color-picker'; import { GlobalTypographyPicker, resolveGlobalTypography } from '@/components/ui/global-typography-picker'; import { LivePreview as LivePreviewContainer } from '@/components/ui/live-preview'; import { cn } from '@/lib/utils'; interface CustomizationTabProps { settings: AjaxSearchSettings; setSettings: React.Dispatch>; isPro: boolean; } /** Compact number input with +/- buttons — replaces Slider */ function NumberStepper({ label, value, onChange, min, max, step = 1, unit = 'px', }: { label: string; value: number; onChange: (v: number) => void; min: number; max: number; step?: number; unit?: string; }) { const clamp = (v: number) => Math.min(max, Math.max(min, v)); return (
{ const parsed = parseInt(e.target.value, 10); if (!isNaN(parsed)) onChange(clamp(parsed)); }} className="h-8 w-14 text-center text-sm border-x bg-transparent outline-none" />
); } const SEARCH_PREVIEW_SHADOWS: Record = { none: 'none', light: '0 1px 2px rgb(3 7 18 / 0.06)', medium: '0 8px 16px rgb(3 7 18 / 0.14)', heavy: '0 18px 32px rgb(3 7 18 / 0.18)', }; function AjaxSearchPreview({ settings }: { settings: AjaxSearchSettings }) { const primaryColor = resolveGlobalColor(settings.primaryColor, 'primary', '#00226b'); const textColor = resolveGlobalColor(settings.textColor, 'text', '#333333'); const backgroundColor = resolveGlobalColor(settings.backgroundColor, 'background', '#ffffff'); const borderColor = resolveGlobalColor(settings.inputBorderColor, 'border', '#dddddd'); const isClassic = settings.template === 'classic'; const isModal = settings.layout === 'modal'; const borderRadius = settings.layout === 'fullwidth' || isClassic ? 0 : settings.borderRadius; const boxShadow = isClassic ? 'none' : SEARCH_PREVIEW_SHADOWS[settings.boxShadow]; const fontFamily = resolveGlobalTypography(settings.fontFamily, 'body', 'inherit'); const effectiveBorderColor = isClassic ? primaryColor : borderColor; const inputStyle: React.CSSProperties = { minHeight: `${settings.inputHeight}px`, color: textColor, backgroundColor, border: `${settings.inputBorderWidth}px solid ${effectiveBorderColor}`, borderRadius: `${borderRadius}px`, boxShadow, fontFamily, fontSize: `${settings.inputFontSize}px`, }; const surfaceStyle: React.CSSProperties = { color: textColor, backgroundColor, border: `${settings.inputBorderWidth}px solid ${effectiveBorderColor}`, borderRadius: `${borderRadius}px`, boxShadow, fontFamily, fontSize: `${settings.fontSize}px`, }; const resultsStyle: React.CSSProperties = isModal ? { ...surfaceStyle, border: 0, borderRadius: 0, boxShadow: 'none' } : surfaceStyle; const searchInputStyle: React.CSSProperties = isModal ? { ...inputStyle, boxShadow: 'none' } : inputStyle; const results = (
{isModal && (
3 results for “sample”
)}
{['Sample Product', 'Another Product'].map((title, index) => (
{settings.showImage && (
{index === 0 && settings.showSaleBadge && ( Sale )}
)}
{title}
{(settings.showCategories || settings.showSku || settings.showRating || settings.showStock) && (
{settings.showCategories && {index === 0 ? 'Electronics' : 'Clothing'}} {settings.showSku && SKU: 12345} {settings.showRating && ★ 4.5} {settings.showStock && ( In Stock )}
)} {settings.showPrice && (
{index === 0 ? ( <> $99 $79 ) : '$49'}
)} {settings.showDescription && index === 0 && (

This is a sample product description.

)}
))}
{settings.viewAllText} (3)
); const searchField = (
{settings.placeholderText}
{isModal && ( )} {settings.showSearchButton && !isModal && ( )}
); if (isModal) { return (
{settings.placeholderText}
{searchField}
{results}
); } return (
{searchField}
{results}
); } export function AjaxSearchCustomizationTab({ settings, setSettings, isPro }: CustomizationTabProps) { const [copied, setCopied] = useState(false); const shortcode = '[swift_commerce_search]'; const [expandedSections, setExpandedSections] = useState>({ columns: false, colors: false, fieldStyle: false, borderEffects: false, typography: false, productInfo: false, }); const toggleSection = (section: string) => { setExpandedSections(prev => ({ ...prev, [section]: !prev[section] })); }; const copyShortcode = async () => { try { await navigator.clipboard.writeText(shortcode); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { const textArea = document.createElement('textarea'); textArea.value = shortcode; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); setCopied(true); setTimeout(() => setCopied(false), 2000); } }; return (
{/* Left Column - Settings */}
{/* Templates */} Templates Choose a design starting point. Customize further below. { const preset = TEMPLATE_PRESETS[value]; if (preset) { setSettings((prev) => ({ ...prev, ...preset, template: value as AjaxSearchSettings['template'] })); } }} className="grid grid-cols-3 gap-3" > {/* Modern Template */}