Add custom color picker, move website link to store items, and fix sidebar logo aspect.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
592540cc7e
commit
00f2e0f729
@@ -3,10 +3,10 @@ import { createPortal } from 'react-dom'
|
||||
import { Plus, Search, X } from 'lucide-react'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { useT } from '../i18n/useT'
|
||||
import type { BusinessMessageKey } from '../i18n/messages'
|
||||
import { COLOR_PRESETS } from '../data/colorPresets'
|
||||
import { COLOR_PRESETS, getColorHex, normalizeColorName } from '../data/colorPresets'
|
||||
import type { Variation, VariationType } from '../types/variation'
|
||||
import { SIZE_OPTIONS, normalizeColorNames } from '../types/variation'
|
||||
import { SIZE_OPTIONS } from '../types/variation'
|
||||
import { localizeVariationOptionLabel } from '../utils/localizeVariationOption'
|
||||
import { Tooltip } from './Tooltip'
|
||||
import styles from './VariationsModal.module.css'
|
||||
|
||||
@@ -18,15 +18,42 @@ interface AddVariationModalProps {
|
||||
initial?: Variation | null
|
||||
}
|
||||
|
||||
interface SelectedColor {
|
||||
label: string
|
||||
hex: string
|
||||
}
|
||||
|
||||
const ANIMATION_MS = 220
|
||||
const DEFAULT_CUSTOM_HEX = '#64748B'
|
||||
|
||||
function seedCustomValues(values: string[]) {
|
||||
const trimmed = values.map((value) => value.trim()).filter(Boolean)
|
||||
return trimmed.length ? trimmed : ['']
|
||||
}
|
||||
|
||||
function colorLabelKey(name: string): BusinessMessageKey {
|
||||
return `categories.variations.color.${name}` as BusinessMessageKey
|
||||
function normalizeHex(value: string): string {
|
||||
const trimmed = value.trim()
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) return trimmed.toUpperCase()
|
||||
if (/^[0-9a-fA-F]{6}$/.test(trimmed)) return `#${trimmed.toUpperCase()}`
|
||||
return DEFAULT_CUSTOM_HEX
|
||||
}
|
||||
|
||||
function colorsFromInitial(initial: Variation): SelectedColor[] {
|
||||
return initial.values
|
||||
.map((raw, index) => {
|
||||
const label = raw.trim()
|
||||
if (!label) return null
|
||||
const canonical = normalizeColorName(label)
|
||||
const provided = initial.colorHexes?.[index]
|
||||
const hex =
|
||||
(provided && /^#[0-9a-fA-F]{6}$/.test(provided.trim())
|
||||
? provided.trim().toUpperCase()
|
||||
: null) ??
|
||||
getColorHex(canonical) ??
|
||||
DEFAULT_CUSTOM_HEX
|
||||
return { label: canonical, hex }
|
||||
})
|
||||
.filter((item): item is SelectedColor => item !== null)
|
||||
}
|
||||
|
||||
export function AddVariationModal({ open, onClose, onSubmit, initial = null }: AddVariationModalProps) {
|
||||
@@ -38,10 +65,13 @@ export function AddVariationModal({ open, onClose, onSubmit, initial = null }: A
|
||||
const [closing, setClosing] = useState(false)
|
||||
const [type, setType] = useState<VariationType>('color')
|
||||
const [customName, setCustomName] = useState('')
|
||||
const [selectedColors, setSelectedColors] = useState<string[]>([])
|
||||
const [selectedColors, setSelectedColors] = useState<SelectedColor[]>([])
|
||||
const [selectedSizes, setSelectedSizes] = useState<string[]>([])
|
||||
const [customValues, setCustomValues] = useState<string[]>([''])
|
||||
const [colorQuery, setColorQuery] = useState('')
|
||||
const [customColorName, setCustomColorName] = useState('')
|
||||
const [customColorHex, setCustomColorHex] = useState(DEFAULT_CUSTOM_HEX)
|
||||
const [customColorError, setCustomColorError] = useState('')
|
||||
const valueInputRefs = useRef<(HTMLInputElement | null)[]>([])
|
||||
const pendingFocusIndex = useRef<number | null>(null)
|
||||
|
||||
@@ -56,12 +86,13 @@ export function AddVariationModal({ open, onClose, onSubmit, initial = null }: A
|
||||
setMounted(true)
|
||||
setClosing(false)
|
||||
setColorQuery('')
|
||||
setCustomColorName('')
|
||||
setCustomColorHex(DEFAULT_CUSTOM_HEX)
|
||||
setCustomColorError('')
|
||||
if (initial) {
|
||||
setType(initial.type)
|
||||
setCustomName(initial.type === 'custom' ? initial.name : '')
|
||||
setSelectedColors(
|
||||
initial.type === 'color' ? normalizeColorNames(initial.values) : [],
|
||||
)
|
||||
setSelectedColors(initial.type === 'color' ? colorsFromInitial(initial) : [])
|
||||
setSelectedSizes(initial.type === 'size' ? [...initial.values] : [])
|
||||
setCustomValues(
|
||||
initial.type === 'custom' ? seedCustomValues(initial.values) : [''],
|
||||
@@ -92,26 +123,67 @@ export function AddVariationModal({ open, onClose, onSubmit, initial = null }: A
|
||||
return () => document.removeEventListener('keydown', onKey)
|
||||
}, [mounted, closing, onClose])
|
||||
|
||||
const selectedColorLabels = useMemo(
|
||||
() => new Set(selectedColors.map((color) => color.label.toLowerCase())),
|
||||
[selectedColors],
|
||||
)
|
||||
|
||||
const filteredColors = useMemo(() => {
|
||||
const q = colorQuery.trim().toLowerCase()
|
||||
if (!q) return COLOR_PRESETS
|
||||
return COLOR_PRESETS.filter((color) => {
|
||||
const en = color.name.toLowerCase()
|
||||
const fa = t(colorLabelKey(color.name)).toLowerCase()
|
||||
const fa = localizeVariationOptionLabel(color.name, t).toLowerCase()
|
||||
return en.includes(q) || fa.includes(q)
|
||||
})
|
||||
}, [colorQuery, t])
|
||||
|
||||
if (!mounted) return null
|
||||
|
||||
function toggleOption(value: string, selected: string[], setSelected: (v: string[]) => void) {
|
||||
setSelected(
|
||||
selected.includes(value)
|
||||
? selected.filter((item) => item !== value)
|
||||
: [...selected, value],
|
||||
function togglePresetColor(name: string, hex: string) {
|
||||
setSelectedColors((prev) => {
|
||||
const exists = prev.some((item) => item.label === name)
|
||||
if (exists) return prev.filter((item) => item.label !== name)
|
||||
return [...prev, { label: name, hex }]
|
||||
})
|
||||
setCustomColorError('')
|
||||
}
|
||||
|
||||
function removeSelectedColor(label: string) {
|
||||
setSelectedColors((prev) => prev.filter((item) => item.label !== label))
|
||||
}
|
||||
|
||||
function toggleSize(value: string) {
|
||||
setSelectedSizes((prev) =>
|
||||
prev.includes(value) ? prev.filter((item) => item !== value) : [...prev, value],
|
||||
)
|
||||
}
|
||||
|
||||
function addCustomColor() {
|
||||
const label = customColorName.trim()
|
||||
if (!label) {
|
||||
setCustomColorError(t('categories.variations.customColorNameRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
const hex = normalizeHex(customColorHex)
|
||||
const key = label.toLowerCase()
|
||||
const canonical = normalizeColorName(label)
|
||||
const alreadySelected =
|
||||
selectedColorLabels.has(key) ||
|
||||
selectedColorLabels.has(canonical.toLowerCase())
|
||||
|
||||
if (alreadySelected) {
|
||||
setCustomColorError(t('categories.variations.customColorExists'))
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedColors((prev) => [...prev, { label, hex }])
|
||||
setCustomColorName('')
|
||||
setCustomColorHex(DEFAULT_CUSTOM_HEX)
|
||||
setCustomColorError('')
|
||||
}
|
||||
|
||||
function updateCustomValue(index: number, value: string) {
|
||||
setCustomValues((prev) => prev.map((v, i) => (i === index ? value : v)))
|
||||
}
|
||||
@@ -142,7 +214,12 @@ export function AddVariationModal({ open, onClose, onSubmit, initial = null }: A
|
||||
e.preventDefault()
|
||||
|
||||
if (type === 'color') {
|
||||
onSubmit({ name: t('categories.variations.type.color'), type: 'color', values: selectedColors })
|
||||
onSubmit({
|
||||
name: t('categories.variations.type.color'),
|
||||
type: 'color',
|
||||
values: selectedColors.map((color) => color.label),
|
||||
colorHexes: selectedColors.map((color) => color.hex),
|
||||
})
|
||||
} else if (type === 'size') {
|
||||
onSubmit({ name: t('categories.variations.type.size'), type: 'size', values: selectedSizes })
|
||||
} else {
|
||||
@@ -224,27 +301,22 @@ export function AddVariationModal({ open, onClose, onSubmit, initial = null }: A
|
||||
</div>
|
||||
{selectedColors.length > 0 ? (
|
||||
<div className={styles.selectedColors}>
|
||||
{selectedColors.map((color) => {
|
||||
const preset = COLOR_PRESETS.find((item) => item.name === color)
|
||||
return (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
className={`${styles.chip} ${styles.chipSelected}`}
|
||||
onClick={() =>
|
||||
toggleOption(color, selectedColors, setSelectedColors)
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={styles.colorSwatch}
|
||||
style={{ backgroundColor: preset?.hex ?? '#94a3b8' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t(colorLabelKey(color))}
|
||||
<X size={12} aria-hidden="true" />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{selectedColors.map((color) => (
|
||||
<button
|
||||
key={color.label}
|
||||
type="button"
|
||||
className={`${styles.chip} ${styles.chipSelected}`}
|
||||
onClick={() => removeSelectedColor(color.label)}
|
||||
>
|
||||
<span
|
||||
className={styles.colorSwatch}
|
||||
style={{ backgroundColor: color.hex }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{localizeVariationOptionLabel(color.label, t)}
|
||||
<X size={12} aria-hidden="true" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.chipGrid}>
|
||||
@@ -256,22 +328,67 @@ export function AddVariationModal({ open, onClose, onSubmit, initial = null }: A
|
||||
key={color.name}
|
||||
type="button"
|
||||
className={`${styles.chip} ${
|
||||
selectedColors.includes(color.name) ? styles.chipSelected : ''
|
||||
selectedColorLabels.has(color.name.toLowerCase())
|
||||
? styles.chipSelected
|
||||
: ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
toggleOption(color.name, selectedColors, setSelectedColors)
|
||||
}
|
||||
onClick={() => togglePresetColor(color.name, color.hex)}
|
||||
>
|
||||
<span
|
||||
className={styles.colorSwatch}
|
||||
style={{ backgroundColor: color.hex }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t(colorLabelKey(color.name))}
|
||||
{localizeVariationOptionLabel(color.name, t)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.customColorBlock}>
|
||||
<span className={styles.customColorTitle}>
|
||||
{t('categories.variations.addCustomColor')}
|
||||
</span>
|
||||
<div className={styles.customColorRow}>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.customColorNameInput}
|
||||
value={customColorName}
|
||||
onChange={(e) => {
|
||||
setCustomColorName(e.target.value)
|
||||
if (customColorError) setCustomColorError('')
|
||||
}}
|
||||
placeholder={t('categories.variations.customColorNamePlaceholder')}
|
||||
aria-label={t('categories.variations.customColorName')}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
addCustomColor()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label className={styles.customColorPickerLabel}>
|
||||
<span className={styles.srOnly}>{t('categories.variations.customColorHex')}</span>
|
||||
<input
|
||||
type="color"
|
||||
className={styles.customColorPicker}
|
||||
value={customColorHex}
|
||||
onChange={(e) => setCustomColorHex(normalizeHex(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.addValueBtn}
|
||||
onClick={addCustomColor}
|
||||
>
|
||||
<Plus size={14} />
|
||||
{t('categories.variations.addCustomColorBtn')}
|
||||
</button>
|
||||
</div>
|
||||
{customColorError ? (
|
||||
<p className={styles.customColorError}>{customColorError}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -284,7 +401,7 @@ export function AddVariationModal({ open, onClose, onSubmit, initial = null }: A
|
||||
key={size}
|
||||
type="button"
|
||||
className={`${styles.chip} ${selectedSizes.includes(size) ? styles.chipSelected : ''}`}
|
||||
onClick={() => toggleOption(size, selectedSizes, setSelectedSizes)}
|
||||
onClick={() => toggleSize(size)}
|
||||
>
|
||||
{size}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user