/** * Product Addons Customization Tab * * Template-based appearance customization with Stacked, Grid, and Inline layouts. * - NumberStepper replaces all Slider controls for compact sizing inputs * - Google Fonts searchable picker in Typography (Pro) */ import { useState } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Label } from "@/components/ui/label" import { Button } from "@/components/ui/button" import { Switch } from "@/components/ui/switch" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible" import { LivePreview } from "@/components/ui/live-preview" import { Check, ChevronDown, LayoutTemplate, Minus, Palette, Plus, RotateCcw } from "lucide-react" import { ProIndicator } from "@/components/pro/pro-indicator" import { AddonCustomizationSettings, ProductAddonsSettings, quickStartPresets, } from "../../config" import { GlobalTypographyPicker } from "@/components/ui/global-typography-picker" import { GlobalColorPicker } from "@/components/ui/global-color-picker" import { ProductAddonsPreview } from "../product-addons-preview" interface CustomizationTabProps { customization: AddonCustomizationSettings settings: ProductAddonsSettings isPro: boolean updateCustomization: (key: K, value: AddonCustomizationSettings[K]) => void updateSettings: (key: K, value: ProductAddonsSettings[K]) => void resetCustomization: () => void } /* ------------------------------------------------------------------ */ /* Reusable inline helpers */ /* ------------------------------------------------------------------ */ /** 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" />
) } /** Segmented control for visual toggles */ function SegmentedControl({ value, onChange, options, }: { value: T onChange: (v: T) => void options: { value: T; label: string }[] }) { return (
{options.map(opt => ( ))}
) } /* ------------------------------------------------------------------ */ /* Main component */ /* ------------------------------------------------------------------ */ export function CustomizationTab({ customization, settings: _settings, isPro, updateCustomization, updateSettings: _updateSettings, resetCustomization, }: CustomizationTabProps) { const [expandedSections, setExpandedSections] = useState>({ colors: false, layout: false, typography: false, spacing: false, prices: false, images: false, }) const template = customization.template || 'stacked' const applyPreset = (presetId: string) => { const preset = quickStartPresets.find(p => p.id === presetId) if (preset) { Object.entries(preset.customization).forEach(([key, value]) => { updateCustomization( key as keyof AddonCustomizationSettings, value as AddonCustomizationSettings[keyof AddonCustomizationSettings] ) }) } } const toggleSection = (section: string) => { setExpandedSections(prev => ({ ...prev, [section]: !prev[section] })) } /* ------------------------------------------------------------------ */ /* Render */ /* ------------------------------------------------------------------ */ return (
{/* Left Column */}
{/* Template Selection */} Layout Template Choose how addon options are displayed to customers
{/* Stacked */} {/* Grid */} {/* Inline */}
{/* Option Columns — shown when Grid template is selected */} {template === 'grid' && (
updateCustomization('optionColumns', Number(v))} options={[ { value: '1', label: '1 Column' }, { value: '2', label: '2 Columns' }, { value: '3', label: '3 Columns' }, { value: '4', label: '4 Columns' }, ]} />
)}
{/* Customize Design */}
Customize Design {isPro ? 'Fine-tune every aspect of your addon appearance' : 'Customize the colors of your addon appearance'}
{/* Quick Start Presets */}
Quick start: {quickStartPresets.map(preset => ( ))}
{/* Colors — Free */} toggleSection('colors')}> Colors
updateCustomization('containerBackground', v)} globalTokenId="background" /> updateCustomization('containerBorderColor', v)} globalTokenId="accent" featureDefault="#e5e7eb" />
updateCustomization('titleColor', v)} globalTokenId="text" /> updateCustomization('descriptionColor', v)} />
updateCustomization('labelColor', v)} globalTokenId="text" /> updateCustomization('priceColor', v)} globalTokenId="primary" featureDefault="#00226b" />
updateCustomization('inputBorderColor', v)} globalTokenId="accent" featureDefault="#d1d5db" /> updateCustomization('inputFocusBorderColor', v)} globalTokenId="primary" />
updateCustomization('requiredIndicatorColor', v)} globalTokenId="error" featureDefault="#ef4444" /> updateCustomization('totalPriceColor', v)} globalTokenId="text" />
updateCustomization('inputBackgroundColor', v)} globalTokenId="background" /> updateCustomization('totalPriceBackground', v)} />
updateCustomization('imageOptionSelectedBorderColor', v)} globalTokenId="primary" />
{/* Upgrade nudge for Free users */} {!isPro && (

For full customization control including typography, spacing, borders, and more, upgrade to Swift Commerce Pro.

)} {/* Layout & Display — Pro only */} {isPro && ( toggleSection('layout')}> Layout & Display
{template === 'grid' && ( <>
updateCustomization('optionCardStyle', v)} options={[ { value: 'bordered', label: 'Bordered' }, { value: 'filled', label: 'Filled' }, ]} />
)}
updateCustomization('showDescriptions', checked)} />
)} {/* Typography — Pro only */} {isPro && ( toggleSection('typography')}> Typography
{/* Font Family — with global typography support */} updateCustomization('fontFamily', value)} globalTokenId="body" featureDefault="inherit" /> {/* Title */}
updateCustomization('titleFontSize', v)} min={12} max={24} />
{/* Description */}
updateCustomization('descriptionFontSize', v)} min={10} max={18} />
{/* Label */}
updateCustomization('labelFontSize', v)} min={10} max={18} />
)} {/* Spacing & Dimensions — Pro only */} {isPro && ( toggleSection('spacing')}> Spacing & Dimensions
updateCustomization('containerBorderRadius', v)} min={0} max={24} /> updateCustomization('containerPadding', v)} min={0} max={40} step={2} /> updateCustomization('inputBorderRadius', v)} min={0} max={16} />
)} {/* Price Styles — Pro only */} {isPro && ( toggleSection('prices')}> Price Styles
updateCustomization('priceFontSize', v)} min={10} max={20} />
updateCustomization('totalPriceFontSize', v)} min={14} max={24} />
)} {/* Image Options — Pro only */} {isPro && ( toggleSection('images')}> Image Options
updateCustomization('imageOptionSize', v)} min={40} max={120} step={4} /> updateCustomization('imageOptionBorderRadius', v)} min={0} max={24} /> updateCustomization('imageOptionSelectedBorderWidth', v)} min={1} max={6} />
)}
{/* Right Column — Sticky Preview */} Reset } >
) }