x
This commit is contained in:
1 parent
46968782b6
commit
08045eabff
18 files changed
+943
-104
No files matched your search
@@ -33,6 +33,15 @@ export default function ProductCarouselForm({
|
||||
}: ProductCarouselFormProps) {
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (editingSection?.payload && !editingSection.payload.type) {
|
||||
setEditingSection(prev => prev ? {
|
||||
...prev,
|
||||
payload: { ...(prev.payload || {}), type: 'featured' }
|
||||
} : null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateFlag = (field: 'showPrice' | 'showAddToCart', value: boolean) => {
|
||||
setEditingSection({
|
||||
...editingSection,
|
||||
@@ -104,6 +113,61 @@ export default function ProductCarouselForm({
|
||||
className="admin-input w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Card Layout Style</label>
|
||||
<select
|
||||
value={editingSection.payload?.cardStyle || 'standard'}
|
||||
onChange={(e) =>
|
||||
setEditingSection(prev => prev ? {
|
||||
...prev,
|
||||
payload: { ...(prev.payload || {}), cardStyle: e.target.value }
|
||||
} : null)
|
||||
}
|
||||
className="admin-input w-full bg-slate-900 border-white/10"
|
||||
>
|
||||
<option value="standard">Standard (Retail with Add to Cart)</option>
|
||||
<option value="minimal_swatch">Minimal Swatch (Editorial with Color Dots)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">View All Link (URL)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editingSection.payload?.viewAllLink || ''}
|
||||
onChange={(e) =>
|
||||
setEditingSection(prev => prev ? {
|
||||
...prev,
|
||||
payload: { ...(prev.payload || {}), viewAllLink: e.target.value }
|
||||
} : null)
|
||||
}
|
||||
className="admin-input w-full"
|
||||
placeholder="e.g. /shop or /category/1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">View All Label</label>
|
||||
<input
|
||||
type="text"
|
||||
value={getLocalizedValue(editingSection.payload, 'viewAllText')}
|
||||
onChange={(e) => updateLocalizedValue('viewAllText', e.target.value)}
|
||||
className="admin-input w-full"
|
||||
placeholder="e.g. View All / ดูทั้งหมด"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Badge Text (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={getLocalizedValue(editingSection.payload, 'badgeText')}
|
||||
onChange={(e) => updateLocalizedValue('badgeText', e.target.value)}
|
||||
className="admin-input w-full"
|
||||
placeholder="e.g. NEW / แนะนำ"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Palette, X, Pipette, Check } from 'lucide-react'
|
||||
|
||||
interface ColorSwatchPickerProps {
|
||||
value?: string | null
|
||||
onChange: (color: string) => void
|
||||
onRemove?: () => void
|
||||
buttonLabel?: string
|
||||
size?: 'sm' | 'md'
|
||||
}
|
||||
|
||||
const PRESET_COLORS = [
|
||||
{ name: 'Black', hex: '#18181B' },
|
||||
{ name: 'White', hex: '#FFFFFF' },
|
||||
{ name: 'Charcoal', hex: '#374151' },
|
||||
{ name: 'Slate', hex: '#64748B' },
|
||||
{ name: 'Red', hex: '#EF4444' },
|
||||
{ name: 'Crimson', hex: '#991B1B' },
|
||||
{ name: 'Orange', hex: '#F97316' },
|
||||
{ name: 'Amber', hex: '#F59E0B' },
|
||||
{ name: 'Emerald', hex: '#10B981' },
|
||||
{ name: 'Sky', hex: '#0EA5E9' },
|
||||
{ name: 'Blue', hex: '#2563EB' },
|
||||
{ name: 'Indigo', hex: '#6366F1' },
|
||||
{ name: 'Violet', hex: '#8B5CF6' },
|
||||
{ name: 'Rose', hex: '#F43F5E' },
|
||||
{ name: 'Rose Gold', hex: '#E0A99E' },
|
||||
{ name: 'Gold', hex: '#D4AF37' },
|
||||
]
|
||||
|
||||
export default function ColorSwatchPicker({
|
||||
value,
|
||||
onChange,
|
||||
onRemove,
|
||||
buttonLabel = 'Color',
|
||||
size = 'md',
|
||||
}: ColorSwatchPickerProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [tempHex, setTempHex] = useState(value || '#18181B')
|
||||
const popoverRef = useRef<HTMLDivElement>(null)
|
||||
const nativeColorInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (value) setTempHex(value)
|
||||
}, [value])
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const handleSelectPreset = (hex: string) => {
|
||||
setTempHex(hex)
|
||||
onChange(hex)
|
||||
}
|
||||
|
||||
const handleApplyHex = (hex: string) => {
|
||||
let clean = hex.trim()
|
||||
if (clean && !clean.startsWith('#')) {
|
||||
clean = `#${clean}`
|
||||
}
|
||||
setTempHex(clean)
|
||||
if (/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(clean)) {
|
||||
onChange(clean)
|
||||
}
|
||||
}
|
||||
|
||||
const isLightColor = (hex?: string | null) => {
|
||||
if (!hex) return false
|
||||
const color = hex.replace('#', '')
|
||||
if (color.length !== 6) return false
|
||||
const r = parseInt(color.substring(0, 2), 16)
|
||||
const g = parseInt(color.substring(2, 4), 16)
|
||||
const b = parseInt(color.substring(4, 6), 16)
|
||||
const brightness = (r * 299 + g * 587 + b * 114) / 1000
|
||||
return brightness > 180
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative inline-block" ref={popoverRef}>
|
||||
{value ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`group relative flex items-center gap-2 rounded-xl border border-white/20 bg-night-950 hover:border-cyan-glow/50 transition-all ${
|
||||
size === 'sm' ? 'p-1 text-xs' : 'px-2 py-1.5 text-xs'
|
||||
}`}
|
||||
title={`Color: ${value}`}
|
||||
>
|
||||
<div
|
||||
className={`rounded-lg border border-white/20 shrink-0 shadow-inner flex items-center justify-center ${
|
||||
size === 'sm' ? 'w-6 h-6' : 'w-7 h-7'
|
||||
}`}
|
||||
style={{ backgroundColor: value }}
|
||||
>
|
||||
<div
|
||||
className={`w-1.5 h-1.5 rounded-full ${
|
||||
isLightColor(value) ? 'bg-black/60' : 'bg-white/70'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-mono text-[11px] text-slate-300 group-hover:text-cyan-glow">
|
||||
{value.toUpperCase()}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{onRemove && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
className="p-1 rounded-lg text-slate-500 hover:text-rose-400 hover:bg-rose-500/10 transition-colors"
|
||||
title="Remove color"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`shrink-0 flex items-center gap-1.5 rounded-xl border border-dashed border-white/20 hover:border-cyan-glow/50 text-slate-400 hover:text-cyan-glow bg-white/[0.02] font-medium transition-all ${
|
||||
size === 'sm' ? 'px-2 py-1 text-[11px]' : 'px-2.5 py-1.5 text-xs'
|
||||
}`}
|
||||
title="Pick color"
|
||||
>
|
||||
<Palette className={size === 'sm' ? 'w-3 h-3 text-cyan-400/80' : 'w-3.5 h-3.5 text-cyan-400/80'} />
|
||||
<span className="whitespace-nowrap">{buttonLabel}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Popover */}
|
||||
{isOpen && (
|
||||
<div className="absolute z-50 mt-2 left-0 w-64 p-3 rounded-2xl bg-night-900 border border-white/15 shadow-2xl backdrop-blur-xl space-y-3">
|
||||
<div className="flex items-center justify-between border-b border-white/10 pb-2">
|
||||
<span className="text-[11px] font-bold uppercase tracking-wider text-slate-300 flex items-center gap-1.5">
|
||||
<Palette className="w-3.5 h-3.5 text-cyan-glow" />
|
||||
Select Color
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="p-1 text-slate-500 hover:text-white rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Quick Preset Palette */}
|
||||
<div>
|
||||
<div className="text-[10px] text-slate-500 font-semibold mb-1.5 uppercase tracking-wider">Presets</div>
|
||||
<div className="grid grid-cols-8 gap-1.5">
|
||||
{PRESET_COLORS.map((preset) => {
|
||||
const isSelected = value?.toUpperCase() === preset.hex.toUpperCase()
|
||||
return (
|
||||
<button
|
||||
key={preset.hex}
|
||||
type="button"
|
||||
onClick={() => handleSelectPreset(preset.hex)}
|
||||
title={`${preset.name} (${preset.hex})`}
|
||||
className={`w-6 h-6 rounded-lg border flex items-center justify-center transition-all hover:scale-110 ${
|
||||
isSelected
|
||||
? 'border-cyan-glow ring-2 ring-cyan-glow/40'
|
||||
: 'border-white/20 hover:border-white/50'
|
||||
}`}
|
||||
style={{ backgroundColor: preset.hex }}
|
||||
>
|
||||
{isSelected && (
|
||||
<Check
|
||||
className={`w-3 h-3 ${isLightColor(preset.hex) ? 'text-black' : 'text-white'}`}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Color Input & Hex */}
|
||||
<div className="pt-2 border-t border-white/10 space-y-2">
|
||||
<div className="text-[10px] text-slate-500 font-semibold uppercase tracking-wider">Custom Shade</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
ref={nativeColorInputRef}
|
||||
type="color"
|
||||
value={tempHex.startsWith('#') && tempHex.length === 7 ? tempHex : '#18181B'}
|
||||
onChange={(e) => {
|
||||
setTempHex(e.target.value)
|
||||
onChange(e.target.value)
|
||||
}}
|
||||
className="sr-only"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => nativeColorInputRef.current?.click()}
|
||||
className="w-9 h-9 rounded-xl border border-white/20 shrink-0 flex items-center justify-center shadow-inner hover:scale-105 transition-all"
|
||||
style={{ backgroundColor: tempHex }}
|
||||
title="Click to open color spectrum picker"
|
||||
>
|
||||
<Pipette
|
||||
className={`w-4 h-4 ${isLightColor(tempHex) ? 'text-black' : 'text-white'}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={tempHex}
|
||||
onChange={(e) => handleApplyHex(e.target.value)}
|
||||
placeholder="#000000"
|
||||
maxLength={7}
|
||||
className="w-full px-3 py-1.5 bg-night-950 border border-white/15 focus:border-cyan-glow/50 rounded-xl outline-none text-xs font-mono text-white transition-all shadow-inner uppercase"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="pt-2 border-t border-white/10 flex items-center justify-between">
|
||||
{value && onRemove && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onRemove()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className="text-[11px] text-rose-400 hover:text-rose-300 hover:underline"
|
||||
>
|
||||
Clear Color
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="ml-auto px-3 py-1 bg-cyan-glow/10 hover:bg-cyan-glow/20 border border-cyan-glow/30 text-cyan-glow rounded-lg text-xs font-semibold transition-colors"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -930,10 +930,11 @@ export default function PageBuilderPage() {
|
||||
defaultPayload = {
|
||||
title: 'Featured Collection',
|
||||
description: 'Hand-picked products for this campaign.',
|
||||
collectionId: '',
|
||||
type: 'featured',
|
||||
limit: 6,
|
||||
showPrice: true,
|
||||
showAddToCart: true,
|
||||
cardStyle: 'standard',
|
||||
};
|
||||
break;
|
||||
case 'PRODUCT_MAIN':
|
||||
|
||||
@@ -28,6 +28,7 @@ import MDEditor from '@uiw/react-md-editor'
|
||||
import MediaLibraryModal from '../components/MediaLibraryModal'
|
||||
import ContentLanguageTabs from '../components/ContentLanguageTabs'
|
||||
import ProductTranslationModal from '../components/products/ProductTranslationModal'
|
||||
import ColorSwatchPicker from '../components/products/ColorSwatchPicker'
|
||||
|
||||
type EditableAttribute = {
|
||||
id?: number
|
||||
@@ -129,7 +130,7 @@ export default function ProductEditPage() {
|
||||
const [name, setName] = useState<Record<string, string>>({})
|
||||
const [description, setDescription] = useState<Record<string, string>>({})
|
||||
const [details, setDetails] = useState<Record<string, string>>({})
|
||||
const [options, setOptions] = useState<{name: Record<string, string>, values: (Record<string, string> & { imageUrl?: string })[]}[]>([])
|
||||
const [options, setOptions] = useState<{name: Record<string, string>, values: (Record<string, string> & { imageUrl?: string; colorCode?: string })[]}[]>([])
|
||||
const [variants, setVariants] = useState<Partial<Variant>[]>([])
|
||||
const [images, setImages] = useState<EditableProductImage[]>([])
|
||||
const [titleGraphicUrl, setTitleGraphicUrl] = useState<string>('')
|
||||
@@ -259,7 +260,7 @@ export default function ProductEditPage() {
|
||||
setVariants(updated)
|
||||
}
|
||||
|
||||
const regenerateVariants = (newOptions: {name: Record<string, string>, values: (Record<string, string> & { imageUrl?: string })[]}[]) => {
|
||||
const regenerateVariants = (newOptions: {name: Record<string, string>, values: (Record<string, string> & { imageUrl?: string; colorCode?: string })[]}[]) => {
|
||||
setVariants(prevVariants => {
|
||||
const validOptions = newOptions.filter(o => o.name && (o.name['th'] || o.name['en']) && o.values.length > 0)
|
||||
if (validOptions.length === 0) {
|
||||
@@ -284,13 +285,15 @@ export default function ProductEditPage() {
|
||||
const existing = prevVariants.find(v => v.option1 === option1 && v.option2 === option2 && v.option3 === option3)
|
||||
const opt1Val = newOptions[0]?.values.find(v => (v['th'] || v['en'] || Object.values(v)[0]) === option1)
|
||||
const swatchUrl = opt1Val?.imageUrl || null
|
||||
const swatchColor = opt1Val?.colorCode || null
|
||||
if (existing) {
|
||||
return {
|
||||
...existing,
|
||||
imageUrl: existing.imageUrl || swatchUrl || null,
|
||||
colorCode: existing.colorCode || swatchColor || null,
|
||||
}
|
||||
}
|
||||
return { option1, option2, option3, price: 0, stock: 0, sku: '', barcode: '', imageUrl: swatchUrl }
|
||||
return { option1, option2, option3, price: 0, stock: 0, sku: '', barcode: '', imageUrl: swatchUrl, colorCode: swatchColor }
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -414,6 +417,7 @@ export default function ProductEditPage() {
|
||||
sku: v.sku || '',
|
||||
barcode: v.barcode || '',
|
||||
imageUrl: v.imageUrl || null,
|
||||
colorCode: v.colorCode || null,
|
||||
})),
|
||||
images: images.filter(img => img.url).map((img, idx) => ({
|
||||
...(img.id ? { id: img.id } : {}),
|
||||
@@ -1165,6 +1169,41 @@ export default function ProductEditPage() {
|
||||
<span className="text-[11px] whitespace-nowrap">รูป 1:1</span>
|
||||
</button>
|
||||
)}
|
||||
<ColorSwatchPicker
|
||||
value={val.colorCode}
|
||||
size="sm"
|
||||
onChange={(color) => {
|
||||
const newOpts = [...options]
|
||||
newOpts[idx].values[vIdx] = { ...newOpts[idx].values[vIdx], colorCode: color }
|
||||
setOptions(newOpts)
|
||||
const valName = val[activeLang] || val['th'] || val['en'] || ''
|
||||
if (valName) {
|
||||
setVariants(prev => prev.map(variant => {
|
||||
const optMatch = idx === 0 ? variant.option1 : idx === 1 ? variant.option2 : variant.option3
|
||||
if (optMatch === valName) {
|
||||
return { ...variant, colorCode: color }
|
||||
}
|
||||
return variant
|
||||
}))
|
||||
}
|
||||
}}
|
||||
onRemove={() => {
|
||||
const newOpts = [...options]
|
||||
const { colorCode: _, ...rest } = newOpts[idx].values[vIdx]
|
||||
newOpts[idx].values[vIdx] = rest
|
||||
setOptions(newOpts)
|
||||
const valName = val[activeLang] || val['th'] || val['en'] || ''
|
||||
if (valName) {
|
||||
setVariants(prev => prev.map(variant => {
|
||||
const optMatch = idx === 0 ? variant.option1 : idx === 1 ? variant.option2 : variant.option3
|
||||
if (optMatch === valName) {
|
||||
return { ...variant, colorCode: null }
|
||||
}
|
||||
return variant
|
||||
}))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newOpts = [...options]
|
||||
@@ -1231,7 +1270,7 @@ export default function ProductEditPage() {
|
||||
)) : (
|
||||
<th className="py-3 px-4 font-bold">Variant</th>
|
||||
)}
|
||||
<th className="py-3 px-4 font-bold">Swatch (1:1)</th>
|
||||
<th className="py-3 px-4 font-bold">Swatch (Image / Color)</th>
|
||||
<th className="py-3 px-4 font-bold">Price (THB)</th>
|
||||
<th className="py-3 px-4 font-bold">Stock</th>
|
||||
<th className="py-3 px-4 font-bold">SKU</th>
|
||||
@@ -1251,38 +1290,50 @@ export default function ProductEditPage() {
|
||||
<td className="py-3 px-4 text-sm font-medium text-white">{v.option1 || 'Default'}</td>
|
||||
)}
|
||||
<td className="py-3 px-4">
|
||||
{v.imageUrl ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative w-10 h-10 rounded-lg overflow-hidden border border-white/15 bg-black/60 shrink-0">
|
||||
<img src={v.imageUrl} alt="Variant swatch" className="w-full h-full object-cover" />
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 1:1 Image */}
|
||||
{v.imageUrl ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="relative w-8 h-8 rounded-lg overflow-hidden border border-white/15 bg-black/60 shrink-0">
|
||||
<img src={v.imageUrl} alt="Variant swatch" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openMediaModal((url) => handleUpdateVariant(idx, 'imageUrl', url))}
|
||||
className="text-[10px] text-cyan-glow hover:underline text-left leading-none"
|
||||
>
|
||||
Change
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleUpdateVariant(idx, 'imageUrl', '')}
|
||||
className="text-[10px] text-rose-400 hover:underline text-left leading-none"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openMediaModal((url) => handleUpdateVariant(idx, 'imageUrl', url))}
|
||||
className="text-[10px] text-cyan-glow hover:underline text-left"
|
||||
>
|
||||
Change
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleUpdateVariant(idx, 'imageUrl', '')}
|
||||
className="text-[10px] text-rose-400 hover:underline text-left"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openMediaModal((url) => handleUpdateVariant(idx, 'imageUrl', url))}
|
||||
className="px-2.5 py-1.5 rounded-lg border border-dashed border-white/15 hover:border-cyan-glow/40 bg-white/5 hover:bg-white/10 text-[11px] text-slate-400 hover:text-cyan-glow flex items-center gap-1.5 transition-all"
|
||||
>
|
||||
<ImagePlus className="w-3.5 h-3.5" />
|
||||
<span>1:1 Image</span>
|
||||
</button>
|
||||
)}
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openMediaModal((url) => handleUpdateVariant(idx, 'imageUrl', url))}
|
||||
className="px-2 py-1 rounded-lg border border-dashed border-white/15 hover:border-cyan-glow/40 bg-white/5 hover:bg-white/10 text-[11px] text-slate-400 hover:text-cyan-glow flex items-center gap-1 transition-all"
|
||||
title="Attach 1:1 image"
|
||||
>
|
||||
<ImagePlus className="w-3.5 h-3.5" />
|
||||
<span>1:1</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Color Swatch */}
|
||||
<ColorSwatchPicker
|
||||
value={v.colorCode}
|
||||
size="sm"
|
||||
onChange={(color) => handleUpdateVariant(idx, 'colorCode', color)}
|
||||
onRemove={() => handleUpdateVariant(idx, 'colorCode', null)}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<input
|
||||
|
||||
Reference in new issue
Block a user