Allow external materials/brands to be used when adding spool (#455)

* feat(spool-form): enhance brand and material selection in color section

* feat(spool-form): change input field behavoiur when deleting all chars, fix scroll menu theming

* feat(spool-form): add available materials to SpoolFormModal and FilamentSection

* refactor(ColorSection): remove unnecessary hasCatalogMatch variable

* feat(SpoolFormModal): add bilateral brand and material filtering logic

* feat(ColorCatalogSettings): improve input field sizes and layout for color and material selection

* Refactor colorCatalog settings and dependencies, allow custom materials, fix ml-12 marging, show catalog colors on brand/material selection

* Refactor ColorCatalogSettings and ColorSection components; remove  onBlur logic in FilamentSection

* Prioritize exact matches in search results

* Added inventory.useCustomMaterial to locales

* Remove unused filteredCatalogColors logic and update button rendering; update types file to import Printer and SpoolKProfile

* Fix indentation for useCustomMaterial

* Refactor showCatalogSection logic to only depend on matched catalog colors

* Remove extra newline before brand dropdown

* Clean up ColorSection.tsx by removing blank lines

Removed unnecessary blank lines in ColorSection.tsx.

* Ensure material property is a string in ColorSection component

---------

Co-authored-by: MartinNYHC <mz@v8w.de>
This commit is contained in:
Keybored
2026-02-20 18:34:24 +01:00
committed by GitHub
co-authored by MartinNYHC
parent 6ab48fa338
commit 8b081dbd9a
12 changed files with 296 additions and 49 deletions
@@ -383,37 +383,37 @@ export function ColorCatalogSettings() {
<div className="flex items-center gap-2">
<input
type="color"
className="w-10 h-10 rounded cursor-pointer border border-bambu-dark-tertiary"
className="w-20 h-10 rounded cursor-pointer border border-bambu-dark-tertiary"
value={formHexColor}
onChange={(e) => setFormHexColor(e.target.value)}
/>
<input
type="text"
className="flex-1 px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
className="w-32 px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
placeholder="#FFFFFF"
value={formHexColor}
onChange={(e) => setFormHexColor(e.target.value)}
/>
</div>
<input
type="text"
className="px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
placeholder={t('settings.colorCatalog.materialOptional')}
value={formMaterial}
onChange={(e) => setFormMaterial(e.target.value)}
/>
<div className="flex gap-2">
<div className="flex items-center gap-1">
<input
type="text"
className="flex-1 max-w-lg min-w-[200px] px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
placeholder={t('settings.colorCatalog.materialOptional')}
value={formMaterial}
onChange={(e) => setFormMaterial(e.target.value)}
/>
<button
onClick={handleAdd}
disabled={saving}
className="flex-1 px-3 py-2 bg-bambu-green text-white rounded-lg hover:bg-bambu-green/80 flex items-center justify-center gap-1"
className="w-24 px-3 py-2 bg-bambu-green text-white rounded-lg hover:bg-bambu-green/80 flex items-center justify-center gap-3"
>
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
{t('common.add')}
</button>
<button
onClick={() => { setShowAddForm(false); resetForm(); }}
className="p-2 rounded-lg text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary"
className="p-2 ml-2 rounded-lg text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary"
>
<X className="w-4 h-4" />
</button>
+85 -5
View File
@@ -8,7 +8,8 @@ import { Button } from './Button';
import { useToast } from '../contexts/ToastContext';
import type { SpoolFormData, PrinterWithCalibrations, ColorPreset } from './spool-form/types';
import { defaultFormData, validateForm } from './spool-form/types';
import { buildFilamentOptions, extractBrandsFromPresets, findPresetOption, loadRecentColors, saveRecentColor } from './spool-form/utils';
import { buildFilamentOptions, extractBrandsFromPresets, findPresetOption, loadRecentColors, parsePresetName, saveRecentColor } from './spool-form/utils';
import { MATERIALS } from './spool-form/constants';
import { FilamentSection } from './spool-form/FilamentSection';
import { ColorSection } from './spool-form/ColorSection';
import { AdditionalSection } from './spool-form/AdditionalSection';
@@ -149,10 +150,88 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
);
// Extract brands from presets
const availableBrands = useMemo(
() => extractBrandsFromPresets(cloudPresets, localPresets),
[cloudPresets, localPresets],
);
const baseAvailableBrands = useMemo(() => {
const presetBrands = extractBrandsFromPresets(cloudPresets, localPresets);
const catalogBrands = colorCatalog
.map(entry => entry.manufacturer?.trim())
.filter((brand): brand is string => !!brand);
const brandSet = new Set<string>([...presetBrands, ...catalogBrands]);
return Array.from(brandSet).sort((a, b) => a.localeCompare(b));
}, [cloudPresets, localPresets, colorCatalog]);
const baseAvailableMaterials = useMemo(() => {
const catalogMaterials = colorCatalog
.map(entry => entry.material?.trim())
.filter((material): material is string => !!material);
const materialSet = new Set<string>([...MATERIALS, ...catalogMaterials]);
return Array.from(materialSet).sort((a, b) => a.localeCompare(b));
}, [colorCatalog]);
const brandMaterialPairs = useMemo(() => {
const pairs: Array<{ brand: string; material: string }> = [];
for (const entry of colorCatalog) {
const brand = entry.manufacturer?.trim();
const material = entry.material?.trim();
if (brand && material) pairs.push({ brand, material });
}
for (const preset of cloudPresets) {
const parsed = parsePresetName(preset.name);
if (parsed.brand && parsed.material) {
pairs.push({ brand: parsed.brand, material: parsed.material });
}
}
for (const preset of localPresets) {
const parsed = parsePresetName(preset.name);
const brand = preset.filament_vendor?.trim() || parsed.brand;
const material = parsed.material;
if (brand && material) {
pairs.push({ brand, material });
}
}
return pairs;
}, [cloudPresets, colorCatalog, localPresets]);
const brandToMaterials = useMemo(() => {
const map = new Map<string, Set<string>>();
for (const pair of brandMaterialPairs) {
const brandKey = pair.brand.toLowerCase();
const materialKey = pair.material.toLowerCase();
if (!map.has(brandKey)) map.set(brandKey, new Set());
map.get(brandKey)!.add(materialKey);
}
return map;
}, [brandMaterialPairs]);
const materialToBrands = useMemo(() => {
const map = new Map<string, Set<string>>();
for (const pair of brandMaterialPairs) {
const brandKey = pair.brand.toLowerCase();
const materialKey = pair.material.toLowerCase();
if (!map.has(materialKey)) map.set(materialKey, new Set());
map.get(materialKey)!.add(brandKey);
}
return map;
}, [brandMaterialPairs]);
const availableBrands = useMemo(() => {
if (!formData.material) return baseAvailableBrands;
const materialKey = formData.material.toLowerCase();
const brandKeys = materialToBrands.get(materialKey);
if (!brandKeys || brandKeys.size === 0) return baseAvailableBrands;
return baseAvailableBrands.filter(brand => brandKeys.has(brand.toLowerCase()));
}, [baseAvailableBrands, formData.material, materialToBrands]);
const availableMaterials = useMemo(() => {
if (!formData.brand) return baseAvailableMaterials;
const brandKey = formData.brand.toLowerCase();
const materialKeys = brandToMaterials.get(brandKey);
if (!materialKeys || materialKeys.size === 0) return baseAvailableMaterials;
return baseAvailableMaterials.filter(material => materialKeys.has(material.toLowerCase()));
}, [baseAvailableMaterials, formData.brand, brandToMaterials]);
// Find selected preset option
const selectedPresetOption = useMemo(
@@ -434,6 +513,7 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
selectedPresetOption={selectedPresetOption}
filamentOptions={filamentOptions}
availableBrands={availableBrands}
availableMaterials={availableMaterials}
/>
{errors.slicer_filament && (
<p className="mt-1 text-xs text-red-400">{errors.slicer_filament}</p>
@@ -178,6 +178,8 @@ export function AdditionalSection({
const { showToast } = useToast();
const [measuredInput, setMeasuredInput] = useState('');
const [isMeasuredFocused, setIsMeasuredFocused] = useState(false);
const [remainingInput, setRemainingInput] = useState('');
const [isRemainingFocused, setIsRemainingFocused] = useState(false);
const remainingWeight = Math.max(0, formData.label_weight - formData.weight_used);
const measuredDefault = formData.core_weight + remainingWeight;
@@ -188,6 +190,12 @@ export function AdditionalSection({
}
}, [isMeasuredFocused, measuredDefault]);
useEffect(() => {
if (!isRemainingFocused) {
setRemainingInput(String(remainingWeight));
}
}, [isRemainingFocused, remainingWeight]);
return (
<div className="space-y-4">
{/* Empty Spool Weight */}
@@ -206,12 +214,24 @@ export function AdditionalSection({
<div className="relative flex-1">
<input
type="number"
value={remainingWeight}
value={remainingInput}
min={0}
max={formData.label_weight}
onFocus={() => setIsRemainingFocused(true)}
onChange={(e) => {
const remaining = parseInt(e.target.value) || 0;
updateField('weight_used', Math.max(0, formData.label_weight - remaining));
setRemainingInput(e.target.value);
}}
onBlur={() => {
setIsRemainingFocused(false);
const raw = remainingInput.trim();
const remaining = Number(raw);
if (!raw || !Number.isFinite(remaining) || remaining < 0 || remaining > formData.label_weight) {
setRemainingInput(String(remainingWeight));
return;
}
const rounded = Math.round(remaining);
updateField('weight_used', Math.max(0, formData.label_weight - rounded));
setRemainingInput(String(rounded));
}}
className="w-full px-3 py-2 pr-7 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:outline-none focus:border-bambu-green"
/>
@@ -1,7 +1,7 @@
import { useState, useMemo } from 'react';
import { Search, Clock, ChevronDown, ChevronUp } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { ColorSectionProps } from './types';
import type { ColorSectionProps, CatalogDisplayColor } from './types';
import { QUICK_COLORS, ALL_COLORS } from './constants';
export function ColorSection({
@@ -32,7 +32,7 @@ export function ColorSection({
// Filter catalog colors by the selected brand + material + subtype
// Brand matching is word-based: "mz - Bambu" matches "Bambu Lab" because both contain "Bambu"
// Material matching: try exact "PETG Basic" first, fall back to base material "PETG" prefix
const matchedCatalogColors = useMemo(() => {
const matchedCatalogColors = useMemo<CatalogDisplayColor[]>(() => {
if (catalogColors.length === 0) return [];
const brand = formData.brand?.trim();
const material = formData.material?.toLowerCase().trim();
@@ -51,6 +51,19 @@ export function ColorSection({
return brandWords.some(w => mfrLower.includes(w));
};
// If only brand is provided, return all colors for that manufacturer
if (brand && !material) {
const byBrand = catalogColors.filter(c => brandMatches(c.manufacturer));
if (byBrand.length > 0) {
return byBrand.map(c => ({
name: c.color_name,
hex: c.hex_color.replace('#', '').substring(0, 6),
manufacturer: c.manufacturer,
material: typeof c.material === 'string' ? c.material : undefined,
}));
}
}
// Build the combined material+subtype string to match catalog entries
const fullMaterial = material && subtype ? `${material} ${subtype}` : '';
@@ -64,6 +77,8 @@ export function ColorSection({
return exact.map(c => ({
name: c.color_name,
hex: c.hex_color.replace('#', '').substring(0, 6),
manufacturer: c.manufacturer,
material: typeof c.material === 'string' ? c.material : undefined,
}));
}
// Try without trailing "+" (e.g. "PLA Silk+" -> "PLA Silk")
@@ -77,6 +92,8 @@ export function ColorSection({
return normMatch.map(c => ({
name: c.color_name,
hex: c.hex_color.replace('#', '').substring(0, 6),
manufacturer: c.manufacturer,
material: typeof c.material === 'string' ? c.material : undefined,
}));
}
}
@@ -92,6 +109,8 @@ export function ColorSection({
return byMaterial.map(c => ({
name: c.color_name,
hex: c.hex_color.replace('#', '').substring(0, 6),
manufacturer: c.manufacturer,
material: typeof c.material === 'string' ? c.material : undefined,
}));
}
}
@@ -99,14 +118,20 @@ export function ColorSection({
return [];
}, [catalogColors, formData.brand, formData.material, formData.subtype]);
const hasCatalogMatch = matchedCatalogColors.length > 0;
// Search within matched catalog colors
const filteredCatalogColors = useMemo(() => {
const catalogSearchResults = useMemo<CatalogDisplayColor[]>(() => {
if (!colorSearch) return matchedCatalogColors;
if (matchedCatalogColors.length === 0) return [];
const q = colorSearch.toLowerCase();
return matchedCatalogColors.filter(c => c.name.toLowerCase().includes(q));
}, [matchedCatalogColors, colorSearch]);
const matches = matchedCatalogColors.filter(c =>
c.name.toLowerCase().includes(q) ||
(c.manufacturer?.toLowerCase().includes(q) ?? false) ||
(c.material?.toLowerCase().includes(q) ?? false),
);
return matches;
}, [colorSearch, matchedCatalogColors]);
// Only show catalog section if there are matched catalog colors
const showCatalogSection = matchedCatalogColors.length > 0;
// Fallback hardcoded colors for search/expand
const filteredFallbackColors = useMemo(() => {
@@ -165,16 +190,16 @@ export function ColorSection({
</div>
{/* Color Swatches */}
{hasCatalogMatch ? (
{showCatalogSection ? (
/* Catalog colors matching selected brand/material */
<div className="space-y-1.5">
<span className="text-xs text-bambu-gray">
{colorSearch ? t('inventory.searchResults') : `${formData.brand}${formData.material ? ` ${formData.material}` : ''}`}
</span>
<div className="flex flex-wrap gap-1.5">
{filteredCatalogColors.map(color => (
{catalogSearchResults.map(color => (
<button
key={`${color.hex}-${color.name}`}
key={`${color.hex}-${color.name}-${color.manufacturer ?? ''}`}
type="button"
onClick={() => selectColor(color.hex, color.name)}
className={`w-6 h-6 rounded border-2 transition-all hover:scale-110 hover:z-20 relative group ${
@@ -183,14 +208,24 @@ export function ColorSection({
: 'border-bambu-dark-tertiary'
}`}
style={{ backgroundColor: `#${color.hex}` }}
title={color.name}
title={
color.manufacturer && color.material
? `${color.name} (${color.manufacturer} — ${color.material})`
: color.manufacturer
? `${color.name} (${color.manufacturer})`
: color.name
}
>
<span className="absolute -bottom-7 left-1/2 -translate-x-1/2 px-2 py-0.5 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-20 shadow-lg text-white">
{color.name}
{color.manufacturer && color.material
? `${color.name} (${color.manufacturer} — ${color.material})`
: color.manufacturer
? `${color.name} (${color.manufacturer})`
: color.name}
</span>
</button>
))}
{filteredCatalogColors.length === 0 && (
{catalogSearchResults.length === 0 && (
<p className="text-sm text-bambu-gray py-1">{t('inventory.noColorsFound')}</p>
)}
</div>
@@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useMemo } from 'react';
import { Search, Loader2, ChevronDown, Cloud, CloudOff } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { FilamentSectionProps, FilamentOption } from './types';
import { MATERIALS, KNOWN_VARIANTS } from './constants';
import { KNOWN_VARIANTS } from './constants';
import { parsePresetName } from './utils';
export function FilamentSection({
@@ -15,16 +15,22 @@ export function FilamentSection({
selectedPresetOption,
filamentOptions,
availableBrands,
availableMaterials,
}: FilamentSectionProps) {
const { t } = useTranslation();
const [presetDropdownOpen, setPresetDropdownOpen] = useState(false);
const [brandDropdownOpen, setBrandDropdownOpen] = useState(false);
const [subtypeDropdownOpen, setSubtypeDropdownOpen] = useState(false);
const [materialDropdownOpen, setMaterialDropdownOpen] = useState(false);
const [brandSearch, setBrandSearch] = useState('');
const [subtypeSearch, setSubtypeSearch] = useState('');
const [materialSearch, setMaterialSearch] = useState('');
const [labelInput, setLabelInput] = useState(String(formData.label_weight));
const [isLabelFocused, setIsLabelFocused] = useState(false);
const presetRef = useRef<HTMLDivElement>(null);
const brandRef = useRef<HTMLDivElement>(null);
const subtypeRef = useRef<HTMLDivElement>(null);
const materialRef = useRef<HTMLDivElement>(null);
// Close dropdowns on outside click
useEffect(() => {
@@ -32,6 +38,9 @@ export function FilamentSection({
if (presetRef.current && !presetRef.current.contains(e.target as Node)) {
setPresetDropdownOpen(false);
}
if (materialRef.current && !materialRef.current.contains(e.target as Node)) {
setMaterialDropdownOpen(false);
}
if (brandRef.current && !brandRef.current.contains(e.target as Node)) {
setBrandDropdownOpen(false);
}
@@ -57,7 +66,15 @@ export function FilamentSection({
const filteredBrands = useMemo(() => {
if (!brandSearch) return availableBrands;
const search = brandSearch.toLowerCase();
return availableBrands.filter(b => b.toLowerCase().includes(search));
const filtered = availableBrands.filter(b => b.toLowerCase().includes(search));
// Sort: exact match first, then others
return filtered.sort((a, b) => {
const aExact = a.toLowerCase() === search;
const bExact = b.toLowerCase() === search;
if (aExact && !bExact) return -1;
if (!aExact && bExact) return 1;
return a.localeCompare(b);
});
}, [availableBrands, brandSearch]);
const filteredVariants = useMemo(() => {
@@ -66,6 +83,26 @@ export function FilamentSection({
return KNOWN_VARIANTS.filter(v => v.toLowerCase().includes(search));
}, [subtypeSearch]);
const filteredMaterials = useMemo(() => {
if (!materialSearch) return availableMaterials;
const search = materialSearch.toLowerCase();
const filtered = availableMaterials.filter(m => m.toLowerCase().includes(search));
// Sort: exact match first, then others
return filtered.sort((a, b) => {
const aExact = a.toLowerCase() === search;
const bExact = b.toLowerCase() === search;
if (aExact && !bExact) return -1;
if (!aExact && bExact) return 1;
return a.localeCompare(b);
});
}, [materialSearch, availableMaterials]);
useEffect(() => {
if (!isLabelFocused) {
setLabelInput(String(formData.label_weight));
}
}, [formData.label_weight, isLabelFocused]);
// Handle preset selection
const handlePresetSelect = (option: FilamentOption) => {
updateField('slicer_filament', option.code);
@@ -147,18 +184,62 @@ export function FilamentSection({
{/* Material */}
<div>
<label className="block text-sm font-medium text-bambu-gray mb-1">{t('inventory.material')} *</label>
<select
value={formData.material}
onChange={(e) => updateField('material', e.target.value)}
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:outline-none focus:border-bambu-green"
>
<option value="">{t('inventory.selectMaterial')}</option>
{MATERIALS.map((m) => (
<option key={m} value={m}>{m}</option>
))}
</select>
<div className="relative" ref={materialRef}>
<input
type="text"
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm placeholder:text-bambu-gray/50 focus:outline-none focus:border-bambu-green"
placeholder={t('inventory.selectMaterial')}
value={materialDropdownOpen ? materialSearch : formData.material}
onChange={(e) => {
setMaterialSearch(e.target.value);
setMaterialDropdownOpen(true);
}}
onFocus={() => {
setMaterialDropdownOpen(true);
setMaterialSearch('');
}}
/>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray/50 pointer-events-none" />
{materialDropdownOpen && (
<div className="absolute z-50 w-full mt-1 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg shadow-lg max-h-48 overflow-y-auto">
{filteredMaterials.length === 0 ? (
<div className="px-3 py-2 text-sm text-bambu-gray">{t('inventory.noResults')}</div>
) : (
filteredMaterials.map((material) => (
<button
key={material}
type="button"
className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary ${
formData.material === material ? 'bg-bambu-green/10 text-bambu-green' : 'text-white'
}`}
onClick={() => {
updateField('material', material);
setMaterialDropdownOpen(false);
setMaterialSearch('');
}}
>
{material}
</button>
))
)}
{/* Allow custom material */}
{materialSearch && !filteredMaterials.includes(materialSearch) && (
<button
type="button"
className="w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary text-bambu-green border-t border-bambu-dark-tertiary"
onClick={() => {
updateField('material', materialSearch);
setMaterialDropdownOpen(false);
setMaterialSearch('');
}}
>
{t('inventory.useCustomMaterial', { material: materialSearch })}
</button>
)}
</div>
)}
</div>
</div>
{/* Brand (dropdown with search) */}
<div>
<label className="block text-sm font-medium text-bambu-gray mb-1">{t('inventory.brand')} *</label>
@@ -284,13 +365,28 @@ export function FilamentSection({
<div className="relative">
<input
type="number"
value={formData.label_weight}
onChange={(e) => updateField('label_weight', parseInt(e.target.value) || 0)}
className="w-full px-3 py-2 pr-7 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:outline-none focus:border-bambu-green"
value={labelInput}
min={0}
onFocus={() => setIsLabelFocused(true)}
onChange={(e) => setLabelInput(e.target.value)}
onBlur={() => {
setIsLabelFocused(false);
const raw = labelInput.trim();
const next = Number(raw);
if (!raw || !Number.isFinite(next) || next < 0) {
setLabelInput(String(formData.label_weight));
return;
}
const rounded = Math.round(next);
updateField('label_weight', rounded);
setLabelInput(String(rounded));
}}
/>
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-xs text-bambu-gray">g</span>
</div>
</div>
</div>
);
}
@@ -1,5 +1,14 @@
import type { Printer, SpoolKProfile } from '../../api/client';
// Catalog color display type (moved from component)
export interface CatalogDisplayColor {
name: string;
hex: string;
manufacturer?: string;
material?: string;
}
// Form data structure
export interface SpoolFormData {
material: string;
@@ -79,6 +88,7 @@ export interface FilamentSectionProps extends SectionProps {
selectedPresetOption?: FilamentOption;
filamentOptions: FilamentOption[];
availableBrands: string[];
availableMaterials: string[];
}
// Color section props
+1
View File
@@ -2552,6 +2552,7 @@ export default {
brand: 'Marke',
searchBrand: 'Marke suchen...',
useCustomBrand: '"{{brand}}" verwenden',
useCustomMaterial: 'Benutzerdefiniertes Material verwenden: {{material}}',
colorName: 'Farbname',
colorNamePlaceholder: 'Jade White, Fire Red...',
color: 'Farbe',
+1
View File
@@ -2552,6 +2552,7 @@ export default {
brand: 'Brand',
searchBrand: 'Search brand...',
useCustomBrand: 'Use "{{brand}}"',
useCustomMaterial: 'Use custom material: {{material}}',
colorName: 'Color Name',
colorNamePlaceholder: 'Jade White, Fire Red...',
color: 'Color',
+1
View File
@@ -2540,6 +2540,7 @@ export default {
brand: 'Marque',
searchBrand: 'Chercher marque...',
useCustomBrand: 'Utiliser "{{brand}}"',
useCustomMaterial: 'Utiliser un matériau personnalisé : {{material}}',
colorName: 'Nom de couleur',
colorNamePlaceholder: 'Jade White, Fire Red...',
color: 'Couleur',
+1
View File
@@ -2357,6 +2357,7 @@ export default {
brand: 'Marchio',
searchBrand: 'Cerca marchio...',
useCustomBrand: 'Usa "{{brand}}"',
useCustomMaterial: 'Usa materiale personalizzato: {{material}}',
colorName: 'Nome Colore',
colorNamePlaceholder: 'Jade White, Fire Red...',
color: 'Colore',
+1
View File
@@ -2479,6 +2479,7 @@ export default {
brand: 'ブランド',
searchBrand: 'ブランドを検索...',
useCustomBrand: '「{{brand}}」を使用',
useCustomMaterial: 'カスタム素材を使用: {{material}}',
colorName: '色名',
colorNamePlaceholder: 'Jade White, Fire Red...',
color: '色',
+1
View File
@@ -2552,6 +2552,7 @@ export default {
brand: 'Marca',
searchBrand: 'Pesquisar marca...',
useCustomBrand: 'Usar "{{brand}}"',
useCustomMaterial: 'Usar material personalizado: {{material}}',
colorName: 'Nome da Cor',
colorNamePlaceholder: 'Jade White, Fire Red...',
color: 'Cor',