Merge branch 'main' into feature/strapi
This commit is contained in:
@@ -39,6 +39,7 @@ type MdxProduct = {
|
||||
categories: string[];
|
||||
images: string[];
|
||||
descriptionHtml: string;
|
||||
applicationHtml: string;
|
||||
};
|
||||
|
||||
type MdxIndex = Map<string, MdxProduct>; // key: normalized designation/title
|
||||
@@ -85,9 +86,10 @@ function buildMdxIndex(locale: 'en' | 'de'): MdxIndex {
|
||||
const images = Array.isArray(data.images) ? data.images.map((i: any) => normalizeValue(String(i))).filter(Boolean) : [];
|
||||
|
||||
const descriptionHtml = extractDescriptionFromMdxFrontmatter(data);
|
||||
const applicationHtml = normalizeValue(String(data?.application || ''));
|
||||
|
||||
const slug = path.basename(file, '.mdx');
|
||||
idx.set(normalizeExcelKey(title), { slug, title, sku, categories, images, descriptionHtml });
|
||||
idx.set(normalizeExcelKey(title), { slug, title, sku, categories, images, descriptionHtml, applicationHtml });
|
||||
}
|
||||
|
||||
return idx;
|
||||
@@ -183,6 +185,7 @@ async function loadProductsFromExcelAndMdx(locale: 'en' | 'de'): Promise<Product
|
||||
name: title,
|
||||
shortDescriptionHtml: '',
|
||||
descriptionHtml,
|
||||
applicationHtml: mdx?.applicationHtml || '',
|
||||
images: mdx?.images || [],
|
||||
featuredImage: (mdx?.images && mdx.images[0]) || null,
|
||||
sku: mdx?.sku || title,
|
||||
@@ -191,7 +194,12 @@ async function loadProductsFromExcelAndMdx(locale: 'en' | 'de'): Promise<Product
|
||||
locale,
|
||||
categories: (mdx?.categories || []).map(name => ({ name })),
|
||||
attributes: [],
|
||||
voltageType: data.voltageType,
|
||||
voltageType: (() => {
|
||||
const cats = (mdx?.categories || []).map(c => String(c));
|
||||
const isMV = cats.some(c => /medium[-\s]?voltage|mittelspannung/i.test(c));
|
||||
if (isMV && data.voltageType === 'high-voltage') return 'medium-voltage';
|
||||
return data.voltageType;
|
||||
})(),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -159,83 +159,121 @@ function guessColumnKey(row: ExcelRow, patterns: RegExp[]): string | null {
|
||||
}
|
||||
|
||||
function technicalFullLabel(args: { key: string; excelKey: string; locale: 'en' | 'de' }): string {
|
||||
const key = args.key;
|
||||
if (args.locale === 'de') {
|
||||
switch (key) {
|
||||
case 'cond_mat': return 'Leitermaterial';
|
||||
case 'cond_class': return 'Leiterklasse';
|
||||
case 'core_ins': return 'Aderisolation';
|
||||
case 'field_ctrl': return 'Feldsteuerung';
|
||||
case 'screen': return 'Schirm';
|
||||
case 'long_water': return 'Längswasserdichtigkeit';
|
||||
case 'trans_water': return 'Querwasserdichtigkeit';
|
||||
case 'sheath_mat': return 'Mantelmaterial';
|
||||
case 'sheath_color': return 'Mantelfarbe';
|
||||
case 'flame_ret': return 'Flammwidrigkeit';
|
||||
case 'uv_res': return 'UV-beständig';
|
||||
case 'max_cond_temp': return 'Max. zulässige Leitertemperatur';
|
||||
case 'out_temp_fixed': return 'Zul. Kabelaußentemperatur, fest verlegt';
|
||||
case 'out_temp_motion': return 'Zul. Kabelaußentemperatur, in Bewegung';
|
||||
case 'max_sc_temp_val': return 'Maximale Kurzschlußtemperatur';
|
||||
case 'max_sc_temp': return 'Maximale Kurzschlußtemperatur';
|
||||
case 'max_op_temp': return 'Max. zulässige Leitertemperatur';
|
||||
case 'min_store_temp': return 'Minimale Lagertemperatur';
|
||||
case 'min_lay_temp': return 'Mindesttemperatur Verlegung';
|
||||
case 'min_bend_fixed': return 'Min. Biegeradius, fest verlegt';
|
||||
case 'min_lay_temp_val': return 'Mindesttemperatur Verlegung';
|
||||
case 'meter_mark': return 'Metermarkierung';
|
||||
case 'partial_dis': return 'Teilentladung';
|
||||
case 'cap': return 'Kapazität';
|
||||
case 'X': return 'Reaktanz';
|
||||
case 'test_volt': return 'Prüfspannung';
|
||||
case 'rated_volt': return 'Nennspannung';
|
||||
case 'temp_range': return 'Temperaturbereich';
|
||||
case 'Wm': return 'Manteldicke';
|
||||
case 'Wi': return 'Isolationsdicke';
|
||||
case 'RI': return 'DC-Leiterwiderstand (20 °C)';
|
||||
case 'Ø': return 'Außen-Ø';
|
||||
case 'Rbv': return 'Biegeradius';
|
||||
case 'cpr': return 'CPR-Klasse';
|
||||
case 'flame': return 'Flammhemmend';
|
||||
case 'G': return 'Gewicht';
|
||||
case 'Fzv': return 'Zugkraft';
|
||||
case 'DI': return 'Durchmesser über Isolation';
|
||||
case 'Ik_cond': return 'Kurzschlussstrom Leiter';
|
||||
case 'Ik_screen': return 'Kurzschlussstrom Schirm';
|
||||
case 'D_screen': return 'Durchmesser über Schirm';
|
||||
case 'S_screen': return 'Metallischer Schirm';
|
||||
case 'cross_section': return 'Querschnitt';
|
||||
case 'shape': return 'Leiterform';
|
||||
case 'Ibl': return 'Strombelastbarkeit (Luft)';
|
||||
case 'Ibe': return 'Strombelastbarkeit (Erde)';
|
||||
case 'Cond': return 'Leitermaterial';
|
||||
}
|
||||
} else {
|
||||
switch (key) {
|
||||
case 'cond_mat': return 'Conductor material';
|
||||
case 'cond_class': return 'Conductor class';
|
||||
case 'core_ins': return 'Core insulation';
|
||||
case 'field_ctrl': return 'Field control';
|
||||
case 'screen': return 'Screen';
|
||||
case 'long_water': return 'Longitudinal water tightness';
|
||||
case 'trans_water': return 'Transverse water tightness';
|
||||
case 'sheath_mat': return 'Sheath material';
|
||||
case 'sheath_color': return 'Sheath color';
|
||||
case 'flame_ret': return 'Flame retardancy';
|
||||
case 'uv_res': return 'UV resistant';
|
||||
case 'max_cond_temp': return 'Max. permissible conductor temperature';
|
||||
case 'out_temp_fixed': return 'Permissible cable outer temperature, fixed';
|
||||
case 'out_temp_motion': return 'Permissible cable outer temperature, in motion';
|
||||
case 'max_sc_temp_val': return 'Maximum short-circuit temperature';
|
||||
case 'max_sc_temp': return 'Maximum short-circuit temperature';
|
||||
case 'max_op_temp': return 'Max. permissible conductor temperature';
|
||||
case 'min_store_temp': return 'Minimum storage temperature';
|
||||
case 'min_lay_temp': return 'Minimum laying temperature';
|
||||
case 'min_bend_fixed': return 'Min. bending radius, fixed';
|
||||
case 'min_lay_temp_val': return 'Minimum laying temperature';
|
||||
case 'meter_mark': return 'Meter marking';
|
||||
case 'partial_dis': return 'Partial discharge';
|
||||
case 'cap': return 'Capacitance';
|
||||
case 'X': return 'Reactance';
|
||||
case 'test_volt': return 'Test voltage';
|
||||
case 'rated_volt': return 'Rated voltage';
|
||||
case 'temp_range': return 'Operating temperature range';
|
||||
case 'Wm': return 'Sheath thickness';
|
||||
case 'Wi': return 'Insulation thickness';
|
||||
case 'RI': return 'DC resistance (20 °C)';
|
||||
case 'Ø': return 'Outer diameter';
|
||||
case 'Rbv': return 'Bending radius';
|
||||
case 'cpr': return 'CPR class';
|
||||
case 'flame': return 'Flame retardant';
|
||||
case 'G': return 'Weight';
|
||||
case 'Fzv': return 'Pulling force';
|
||||
case 'DI': return 'Diameter over insulation';
|
||||
case 'Ik_cond': return 'Short-circuit current conductor';
|
||||
case 'Ik_screen': return 'Short-circuit current screen';
|
||||
case 'D_screen': return 'Diameter over screen';
|
||||
case 'S_screen': return 'Metallic screen';
|
||||
case 'cross_section': return 'Cross-section';
|
||||
case 'shape': return 'Conductor shape';
|
||||
case 'Ibl': return 'Current rating (air)';
|
||||
case 'Ibe': return 'Current rating (ground)';
|
||||
case 'Cond': return 'Conductor material';
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for unmapped keys (should be rare if columnMapping is comprehensive)
|
||||
const raw = normalizeValue(args.excelKey);
|
||||
if (!raw) return '';
|
||||
|
||||
|
||||
if (args.locale === 'de') {
|
||||
return raw
|
||||
.replace(/\(approx\.?\)/gi, '(ca.)')
|
||||
.replace(/\bconductor material\b/gi, 'Leitermaterial')
|
||||
.replace(/\bconductor class\b/gi, 'Leiterklasse')
|
||||
.replace(/\bcore insulation\b/gi, 'Aderisolation')
|
||||
.replace(/\binsulation\b/gi, 'Aderisolation')
|
||||
.replace(/\bfield control\b/gi, 'Feldsteuerung')
|
||||
.replace(/\bscreen\b/gi, 'Schirm')
|
||||
.replace(/\blongitudinal water tightness\b/gi, 'Längswasserdichtigkeit')
|
||||
.replace(/\btransverse water tightness\b/gi, 'Querwasserdichtigkeit')
|
||||
.replace(/\bsheath material\b/gi, 'Mantelmaterial')
|
||||
.replace(/\bsheath color\b/gi, 'Mantelfarbe')
|
||||
.replace(/\bflame retardancy\b/gi, 'Flammwidrigkeit')
|
||||
.replace(/\buv resistant\b/gi, 'UV-bestandig')
|
||||
.replace(/\bmax\.? permissible conductor temperature\b/gi, 'Max. zulässige Leitertemperatur')
|
||||
.replace(/\bpermissible cable outer temperature, fixed\b/gi, 'Zul. Kabelaußentemperatur, fest verlegt')
|
||||
.replace(/\bpermissible cable outer temperature, in motion\b/gi, 'Zul. Kabelaußentemperatur, in Bewegung')
|
||||
.replace(/\bmaximum short-circuit temperature\b/gi, 'Maximale Kurzschlußtemperatur')
|
||||
.replace(/\bmin\.? bending radius, fixed\b/gi, 'Min. Biegeradius, fest verlegt')
|
||||
.replace(/\bminimum laying temperature\b/gi, 'Mindesttemperatur Verlegung')
|
||||
.replace(/\bmeter marking\b/gi, 'Metermarkierung')
|
||||
.replace(/\bpartial discharge\b/gi, 'Teilentladung')
|
||||
.replace(/\bcapacitance\b/gi, 'Kapazität')
|
||||
.replace(/\binductance\b/gi, 'Induktivität')
|
||||
.replace(/\breactance\b/gi, 'Reaktanz')
|
||||
.replace(/\btest voltage\b/gi, 'Prüfspannung')
|
||||
.replace(/\brated voltage\b/gi, 'Nennspannung')
|
||||
.replace(/\boperating temperature range\b/gi, 'Temperaturbereich')
|
||||
.replace(/\bminimum sheath thickness\b/gi, 'Manteldicke (min.)')
|
||||
.replace(/\bsheath thickness\b/gi, 'Manteldicke')
|
||||
.replace(/\bnominal insulation thickness\b/gi, 'Isolationsdicke (nom.)')
|
||||
.replace(/\binsulation thickness\b/gi, 'Isolationsdicke')
|
||||
.replace(/\bdc resistance at 20\s*°?c\b/gi, 'DC-Leiterwiderstand (20 °C)')
|
||||
.replace(/\bouter diameter(?: of cable)?\b/gi, 'Außen-Ø')
|
||||
.replace(/\bbending radius\b/gi, 'Biegeradius')
|
||||
.replace(/\bpackaging\b/gi, 'Verpackung')
|
||||
.replace(/\bce\s*-?conformity\b/gi, 'CE-Konformität');
|
||||
}
|
||||
|
||||
return raw
|
||||
.replace(/\bconductor material\b/gi, 'Conductor material')
|
||||
.replace(/\bconductor class\b/gi, 'Conductor class')
|
||||
.replace(/\bcore insulation\b/gi, 'Core insulation')
|
||||
.replace(/\binsulation\b/gi, 'Core insulation')
|
||||
.replace(/\bfield control\b/gi, 'Field control')
|
||||
.replace(/\bscreen\b/gi, 'Screen')
|
||||
.replace(/\blongitudinal water tightness\b/gi, 'Longitudinal water tightness')
|
||||
.replace(/\btransverse water tightness\b/gi, 'Transverse water tightness')
|
||||
.replace(/\bsheath material\b/gi, 'Sheath material')
|
||||
.replace(/\bsheath color\b/gi, 'Sheath color')
|
||||
.replace(/\bflame retardancy\b/gi, 'Flame retardancy')
|
||||
.replace(/\buv resistant\b/gi, 'UV resistant')
|
||||
.replace(/\bmax\.? permissible conductor temperature\b/gi, 'Max. permissible conductor temperature')
|
||||
.replace(/\bpermissible cable outer temperature, fixed\b/gi, 'Permissible cable outer temperature, fixed')
|
||||
.replace(/\bpermissible cable outer temperature, in motion\b/gi, 'Permissible cable outer temperature, in motion')
|
||||
.replace(/\bmaximum short-circuit temperature\b/gi, 'Maximum short-circuit temperature')
|
||||
.replace(/\bmin\.? bending radius, fixed\b/gi, 'Min. bending radius, fixed')
|
||||
.replace(/\bminimum laying temperature\b/gi, 'Minimum laying temperature')
|
||||
.replace(/\bmeter marking\b/gi, 'Meter marking')
|
||||
.replace(/\bpartial discharge\b/gi, 'Partial discharge')
|
||||
.replace(/\bcapacitance\b/gi, 'Capacitance')
|
||||
.replace(/\binductance\b/gi, 'Inductance')
|
||||
.replace(/\breactance\b/gi, 'Reactance')
|
||||
.replace(/\btest voltage\b/gi, 'Test voltage')
|
||||
.replace(/\brated voltage\b/gi, 'Rated voltage')
|
||||
.replace(/\boperating temperature range\b/gi, 'Operating temperature range')
|
||||
.replace(/\bminimum sheath thickness\b/gi, 'Sheath thickness (min.)')
|
||||
.replace(/\bsheath thickness\b/gi, 'Sheath thickness')
|
||||
.replace(/\bnominal insulation thickness\b/gi, 'Insulation thickness (nom.)')
|
||||
.replace(/\binsulation thickness\b/gi, 'Insulation thickness')
|
||||
.replace(/\bdc resistance at 20\s*°?c\b/gi, 'DC resistance (20 °C)')
|
||||
.replace(/\bouter diameter(?: of cable)?\b/gi, 'Outer diameter')
|
||||
.replace(/\bbending radius\b/gi, 'Bending radius')
|
||||
.replace(/\bpackaging\b/gi, 'Packaging')
|
||||
.replace(/\bce\s*-?conformity\b/gi, 'CE conformity');
|
||||
}
|
||||
@@ -253,17 +291,58 @@ function technicalValueTranslation(args: { label: string; value: string; locale:
|
||||
if (/^stranded$/i.test(v)) return 'mehrdrähtig';
|
||||
if (/^(\d+)xD$/i.test(v)) return v.replace(/^(\d+)xD$/i, '$1 facher Durchmesser');
|
||||
if (/^XLPE/i.test(v)) return v.replace(/^XLPE/i, 'VPE');
|
||||
return v;
|
||||
if (/^yes, with swelling tape$/i.test(v)) return 'ja, mit Quellvliess';
|
||||
if (/^yes, Al-tape$/i.test(v)) return 'ja, Al-Band';
|
||||
if (/^Polyethylene/i.test(v)) return v.replace(/^Polyethylene/i, 'Polyethylen');
|
||||
if (/^Class 2 stranded$/i.test(v)) return 'Klasse 2 mehrdrähtig';
|
||||
if (/^VPE DIX8$/i.test(v)) return 'VPE DIX8';
|
||||
if (/^inner and outer semiconducting layer made of semiconducting plastic - 3-fold extruded$/i.test(v)) return 'innere und äußere Leitschicht aus halbleitendem Kunststoff - 3-fach-extrudiert';
|
||||
if (/^copper wires \+ transverse conductive helix$/i.test(v)) return 'Kupferdrähte + Querleitwendel';
|
||||
if (/^Polyethylene DMP2$/i.test(v)) return 'Polyethylen DMP2';
|
||||
if (/^15 times diameter$/i.test(v)) return '15 facher Durchmesser';
|
||||
|
||||
// Fallback for partial matches or common terms
|
||||
return v
|
||||
.replace(/\bcopper\b/gi, 'Kupfer')
|
||||
.replace(/\baluminum\b/gi, 'Aluminium')
|
||||
.replace(/\bblack\b/gi, 'schwarz')
|
||||
.replace(/\bstranded\b/gi, 'mehrdrähtig')
|
||||
.replace(/\byes\b/gi, 'ja')
|
||||
.replace(/\bno\b/gi, 'nein')
|
||||
.replace(/\bPolyethylene\b/gi, 'Polyethylen')
|
||||
.replace(/\bXLPE\b/gi, 'VPE');
|
||||
}
|
||||
|
||||
if (args.locale === 'en') {
|
||||
if (/^ja$/i.test(v)) return 'yes';
|
||||
if (/^nein$/i.test(v)) return 'no';
|
||||
if (/^kupfer$/i.test(v)) return 'Copper';
|
||||
if (/^aluminium$/i.test(v)) return 'Aluminum';
|
||||
if (/^schwarz$/i.test(v)) return 'black';
|
||||
if (/^mehrdrähtig$/i.test(v)) return 'stranded';
|
||||
if (/^(\d+)xD$/i.test(v)) return v.replace(/^(\d+)xD$/i, '$1 times diameter');
|
||||
if (/^VPE/i.test(v)) return v.replace(/^VPE/i, 'XLPE');
|
||||
if (/^ja, mit Quellvliess$/i.test(v)) return 'yes, with swelling tape';
|
||||
if (/^ja, Al-Band$/i.test(v)) return 'yes, Al-tape';
|
||||
if (/^Polyethylen/i.test(v)) return v.replace(/^Polyethylen/i, 'Polyethylene');
|
||||
if (/^Klasse 2 mehrdrähtig$/i.test(v)) return 'Class 2 stranded';
|
||||
if (/^innere und äußere Leitschicht aus halbleitendem Kunststoff - 3-fach-extrudiert$/i.test(v)) return 'inner and outer semiconducting layer made of semiconducting plastic - 3-fold extruded';
|
||||
if (/^Kupferdrähte \+ Querleitwendel$/i.test(v)) return 'copper wires + transverse conductive helix';
|
||||
if (/^Polyethylen DMP2$/i.test(v)) return 'Polyethylene DMP2';
|
||||
if (/^15 facher Durchmesser$/i.test(v)) return '15 times diameter';
|
||||
|
||||
// Fallback for partial matches or common terms
|
||||
return v
|
||||
.replace(/\bkupfer\b/gi, 'Copper')
|
||||
.replace(/\baluminium\b/gi, 'Aluminum')
|
||||
.replace(/\bschwarz\b/gi, 'black')
|
||||
.replace(/\bmehrdrähtig\b/gi, 'stranded')
|
||||
.replace(/\bja\b/gi, 'yes')
|
||||
.replace(/\bnein\b/gi, 'no')
|
||||
.replace(/\bPolyethylen\b/gi, 'Polyethylene')
|
||||
.replace(/\bVPE\b/gi, 'XLPE');
|
||||
}
|
||||
|
||||
if (/^ja$/i.test(v)) return 'yes';
|
||||
if (/^nein$/i.test(v)) return 'no';
|
||||
if (/^kupfer$/i.test(v)) return 'Copper';
|
||||
if (/^aluminium$/i.test(v)) return 'Aluminum';
|
||||
if (/^schwarz$/i.test(v)) return 'black';
|
||||
if (/^mehrdrähtig$/i.test(v)) return 'stranded';
|
||||
if (/^(\d+)xD$/i.test(v)) return v.replace(/^(\d+)xD$/i, '$1 times diameter');
|
||||
if (/^VPE/i.test(v)) return v.replace(/^VPE/i, 'XLPE');
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -287,6 +366,26 @@ function metaFullLabel(args: { key: string; excelKey: string; locale: 'en' | 'de
|
||||
return 'CPR-Klasse';
|
||||
case 'flame':
|
||||
return 'Flammhemmend';
|
||||
case 'DI': return 'Durchmesser über Isolation';
|
||||
case 'RI': return 'DC-Leiterwiderstand (20 °C)';
|
||||
case 'Wi': return 'Isolationsdicke';
|
||||
case 'Wm': return 'Manteldicke';
|
||||
case 'Rbv': return 'Biegeradius';
|
||||
case 'Fzv': return 'Zugkraft';
|
||||
case 'G': return 'Gewicht';
|
||||
case 'Ik_cond': return 'Kurzschlussstrom Leiter';
|
||||
case 'Ik_screen': return 'Kurzschlussstrom Schirm';
|
||||
case 'Ø': return 'Außen-Ø';
|
||||
case 'cap': return 'Kapazität';
|
||||
case 'X': return 'Reaktanz';
|
||||
case 'rated_volt': return 'Nennspannung';
|
||||
case 'D_screen': return 'Durchmesser über Schirm';
|
||||
case 'S_screen': return 'Metallischer Schirm';
|
||||
case 'cross_section': return 'Querschnitt';
|
||||
case 'shape': return 'Leiterform';
|
||||
case 'Ibl': return 'Strombelastbarkeit (Luft)';
|
||||
case 'Ibe': return 'Strombelastbarkeit (Erde)';
|
||||
case 'Cond': return 'Leitermaterial';
|
||||
default:
|
||||
return formatExcelHeaderLabel(args.excelKey);
|
||||
}
|
||||
@@ -309,6 +408,26 @@ function metaFullLabel(args: { key: string; excelKey: string; locale: 'en' | 'de
|
||||
return 'CPR class';
|
||||
case 'flame':
|
||||
return 'Flame retardant';
|
||||
case 'DI': return 'Diameter over insulation';
|
||||
case 'RI': return 'DC resistance (20 °C)';
|
||||
case 'Wi': return 'Insulation thickness';
|
||||
case 'Wm': return 'Sheath thickness';
|
||||
case 'Rbv': return 'Bending radius';
|
||||
case 'Fzv': return 'Pulling force';
|
||||
case 'G': return 'Weight';
|
||||
case 'Ik_cond': return 'Short-circuit current conductor';
|
||||
case 'Ik_screen': return 'Short-circuit current screen';
|
||||
case 'Ø': return 'Outer diameter';
|
||||
case 'cap': return 'Capacitance';
|
||||
case 'X': return 'Reactance';
|
||||
case 'rated_volt': return 'Rated voltage';
|
||||
case 'D_screen': return 'Diameter over screen';
|
||||
case 'S_screen': return 'Metallic screen';
|
||||
case 'cross_section': return 'Cross-section';
|
||||
case 'shape': return 'Conductor shape';
|
||||
case 'Ibl': return 'Current rating (air)';
|
||||
case 'Ibe': return 'Current rating (ground)';
|
||||
case 'Cond': return 'Conductor material';
|
||||
default:
|
||||
return formatExcelHeaderLabel(args.excelKey);
|
||||
}
|
||||
@@ -403,7 +522,7 @@ function translateAbbreviation(abbrev: string, description: string, locale: 'en'
|
||||
case 'Cond':
|
||||
return 'Leiter';
|
||||
case 'shape':
|
||||
return 'Form';
|
||||
return 'Leiterform';
|
||||
case 'cap':
|
||||
return 'Kapazität';
|
||||
case 'X':
|
||||
@@ -701,7 +820,7 @@ function buildExcelModel(args: { product: ProductData; locale: 'en' | 'de' }): B
|
||||
'Mantelmaterial',
|
||||
'Mantelfarbe',
|
||||
'Flammwidrigkeit',
|
||||
'UV-bestandig',
|
||||
'UV-beständig',
|
||||
'Max. zulässige Leitertemperatur',
|
||||
'Zul. Kabelaußentemperatur, fest verlegt',
|
||||
'Zul. Kabelaußentemperatur, in Bewegung',
|
||||
@@ -871,6 +990,7 @@ function buildExcelModel(args: { product: ProductData; locale: 'en' | 'de' }): B
|
||||
}
|
||||
|
||||
function isMediumVoltageProduct(product: ProductData): boolean {
|
||||
if (product.voltageType === 'medium-voltage') return true;
|
||||
const hay = [product.slug, product.path, product.translationKey, ...(product.categories || []).map(c => c.name)]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
@@ -991,7 +1111,7 @@ function buildMediumVoltageCrossSectionTableFromNewExcel(args: {
|
||||
return {
|
||||
key: col.colKey,
|
||||
// Use the abbreviated title from the first row as the table header.
|
||||
label: normalizeValue(col.colKey),
|
||||
label: denseAbbrevLabel({ key: col.colKey, locale: args.locale, unit: col.unit }) || normalizeValue(col.colKey),
|
||||
get: (rowIndex: number) => {
|
||||
const srcRowIndex = indices[rowIndex];
|
||||
const raw = normalizeValue(String((mv.rows[srcRowIndex] as Record<string, unknown>)?.[col.colKey] ?? ''));
|
||||
@@ -1009,7 +1129,7 @@ function buildMediumVoltageCrossSectionTableFromNewExcel(args: {
|
||||
export function buildDatasheetModel(args: { product: ProductData; locale: 'en' | 'de' }): DatasheetModel {
|
||||
const labels = getLabels(args.locale);
|
||||
const categoriesLine = (args.product.categories || []).map(c => stripHtml(c.name)).join(' • ');
|
||||
const descriptionText = stripHtml(args.product.shortDescriptionHtml || args.product.descriptionHtml || '');
|
||||
const descriptionText = stripHtml(args.product.applicationHtml || '');
|
||||
const heroSrc = resolveMediaToLocalPath(args.product.featuredImage || args.product.images?.[0] || null);
|
||||
const productUrl = getProductUrl(args.product);
|
||||
|
||||
@@ -1053,22 +1173,71 @@ export function buildDatasheetModel(args: { product: ProductData; locale: 'en' |
|
||||
productUrl,
|
||||
},
|
||||
labels,
|
||||
technicalItems: [
|
||||
...(excelModel.ok ? excelModel.technicalItems : []),
|
||||
...(isMediumVoltageProduct(args.product)
|
||||
? args.locale === 'de'
|
||||
? [
|
||||
{ label: 'Prüfspannung 6/10 kV', value: '21 kV' },
|
||||
{ label: 'Prüfspannung 12/20 kV', value: '42 kV' },
|
||||
{ label: 'Prüfspannung 18/30 kV', value: '63 kV' },
|
||||
]
|
||||
: [
|
||||
{ label: 'Test voltage 6/10 kV', value: '21 kV' },
|
||||
{ label: 'Test voltage 12/20 kV', value: '42 kV' },
|
||||
{ label: 'Test voltage 18/30 kV', value: '63 kV' },
|
||||
]
|
||||
: []),
|
||||
],
|
||||
technicalItems: (() => {
|
||||
if (!isMediumVoltageProduct(args.product)) {
|
||||
return excelModel.ok ? excelModel.technicalItems : [];
|
||||
}
|
||||
|
||||
const pn = normalizeDesignation(args.product.name || '');
|
||||
const isAl = /^NA/.test(pn);
|
||||
const isFL = pn.includes('FL');
|
||||
const isF = !isFL && pn.includes('F');
|
||||
|
||||
const findExcelVal = (labelPart: string) => {
|
||||
const found = excelModel.technicalItems.find(it => it.label.toLowerCase().includes(labelPart.toLowerCase()));
|
||||
return found ? found.value : null;
|
||||
};
|
||||
|
||||
const items: KeyValueItem[] = [];
|
||||
if (args.locale === 'de') {
|
||||
items.push({ label: 'Leitermaterial', value: isAl ? 'Aluminium' : 'Kupfer' });
|
||||
items.push({ label: 'Leiterklasse', value: isAl ? 'Klasse 1' : 'Klasse 2 mehrdrähtig' });
|
||||
items.push({ label: 'Aderisolation', value: 'VPE DIX8' });
|
||||
items.push({ label: 'Feldsteuerung', value: 'innere und äußere Leitschicht aus halbleitendem Kunststoff - 3-fach-extrudiert' });
|
||||
items.push({ label: 'Schirm', value: 'Kupferdrähte + Querleitwendel' });
|
||||
items.push({ label: 'Längswasserdichtigkeit', value: (isF || isFL) ? 'ja, mit Quellvliess' : 'nein' });
|
||||
items.push({ label: 'Querwasserdichtigkeit', value: isFL ? 'ja, Al-Band' : 'nein' });
|
||||
items.push({ label: 'Mantelmaterial', value: 'Polyethylen DMP2' });
|
||||
items.push({ label: 'Mantelfarbe', value: 'schwarz' });
|
||||
items.push({ label: 'Flammwidrigkeit', value: 'nein' });
|
||||
items.push({ label: 'UV-beständig', value: 'ja' });
|
||||
items.push({ label: 'Max. zulässige Leitertemperatur', value: findExcelVal('Leitertemperatur') || '90°C' });
|
||||
items.push({ label: 'Zul. Kabelaußentemperatur, fest verlegt', value: findExcelVal('fest verlegt') || '70°C' });
|
||||
items.push({ label: 'Zul. Kabelaußentemperatur, in Bewegung', value: findExcelVal('in Bewegung') || '-20 °C bis +70 °C' });
|
||||
items.push({ label: 'Maximale Kurzschlußtemperatur', value: findExcelVal('Kurzschlußtemperatur') || '+250 °C' });
|
||||
items.push({ label: 'Min. Biegeradius, fest verlegt', value: findExcelVal('Biegeradius') || '15 facher Durchmesser' });
|
||||
items.push({ label: 'Mindesttemperatur Verlegung', value: findExcelVal('Verlegung') || '-5 °C' });
|
||||
items.push({ label: 'Metermarkierung', value: 'ja' });
|
||||
items.push({ label: 'Teilentladung', value: findExcelVal('Teilentladung') || '2 pC' });
|
||||
items.push({ label: 'Prüfspannung 6/10 kV', value: '21 kV' });
|
||||
items.push({ label: 'Prüfspannung 12/20 kV', value: '42 kV' });
|
||||
items.push({ label: 'Prüfspannung 18/30 kV', value: '63 kV' });
|
||||
} else {
|
||||
items.push({ label: 'Conductor material', value: isAl ? 'Aluminum' : 'Copper' });
|
||||
items.push({ label: 'Conductor class', value: isAl ? 'Class 1' : 'Class 2 stranded' });
|
||||
items.push({ label: 'Core insulation', value: 'XLPE DIX8' });
|
||||
items.push({ label: 'Field control', value: 'inner and outer semiconducting layer made of semiconducting plastic - 3-fold extruded' });
|
||||
items.push({ label: 'Screen', value: 'copper wires + transverse conductive helix' });
|
||||
items.push({ label: 'Longitudinal water tightness', value: (isF || isFL) ? 'yes, with swelling tape' : 'no' });
|
||||
items.push({ label: 'Transverse water tightness', value: isFL ? 'yes, Al-tape' : 'no' });
|
||||
items.push({ label: 'Sheath material', value: 'Polyethylene DMP2' });
|
||||
items.push({ label: 'Sheath color', value: 'black' });
|
||||
items.push({ label: 'Flame retardancy', value: 'no' });
|
||||
items.push({ label: 'UV resistant', value: 'yes' });
|
||||
items.push({ label: 'Max. permissible conductor temperature', value: findExcelVal('conductor temperature') || '90°C' });
|
||||
items.push({ label: 'Permissible cable outer temperature, fixed', value: findExcelVal('fixed') || '70°C' });
|
||||
items.push({ label: 'Permissible cable outer temperature, in motion', value: findExcelVal('in motion') || '-20 °C to +70 °C' });
|
||||
items.push({ label: 'Maximum short-circuit temperature', value: findExcelVal('short-circuit temperature') || '+250 °C' });
|
||||
items.push({ label: 'Min. bending radius, fixed', value: findExcelVal('bending radius') || '15 times diameter' });
|
||||
items.push({ label: 'Minimum laying temperature', value: findExcelVal('laying temperature') || '-5 °C' });
|
||||
items.push({ label: 'Meter marking', value: 'yes' });
|
||||
items.push({ label: 'Partial discharge', value: findExcelVal('Partial discharge') || '2 pC' });
|
||||
items.push({ label: 'Test voltage 6/10 kV', value: '21 kV' });
|
||||
items.push({ label: 'Test voltage 12/20 kV', value: '42 kV' });
|
||||
items.push({ label: 'Test voltage 18/30 kV', value: '63 kV' });
|
||||
}
|
||||
return items;
|
||||
})(),
|
||||
voltageTables,
|
||||
legendItems: crossSectionModel.legendItems || [],
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface ProductData {
|
||||
name: string;
|
||||
shortDescriptionHtml: string;
|
||||
descriptionHtml: string;
|
||||
applicationHtml: string;
|
||||
images: string[];
|
||||
featuredImage: string | null;
|
||||
sku: string;
|
||||
|
||||
@@ -54,7 +54,7 @@ export function getLabels(locale: 'en' | 'de') {
|
||||
return {
|
||||
en: {
|
||||
datasheet: 'Technical Datasheet',
|
||||
description: 'DESCRIPTION',
|
||||
description: 'APPLICATION',
|
||||
technicalData: 'TECHNICAL DATA',
|
||||
crossSection: 'Cross-sections/Voltage',
|
||||
sku: 'SKU',
|
||||
@@ -62,7 +62,7 @@ export function getLabels(locale: 'en' | 'de') {
|
||||
},
|
||||
de: {
|
||||
datasheet: 'Technisches Datenblatt',
|
||||
description: 'BESCHREIBUNG',
|
||||
description: 'ANWENDUNG',
|
||||
technicalData: 'TECHNISCHE DATEN',
|
||||
crossSection: 'Querschnitte/Spannung',
|
||||
sku: 'ARTIKELNUMMER',
|
||||
|
||||
@@ -21,61 +21,67 @@ export function DatasheetDocument(props: { model: DatasheetModel; assets: Assets
|
||||
const headerTitle = model.labels.datasheet;
|
||||
|
||||
// Dense tables require compact headers (no wrapping). Use standard abbreviations.
|
||||
const firstColLabel = model.locale === 'de' ? 'Adern & QS' : 'Cores & CS';
|
||||
const firstColLabel = model.locale === 'de' ? 'Adern & Querschnitt' : 'Cores & Cross-section';
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page size="A4" style={styles.page}>
|
||||
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} />
|
||||
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
|
||||
|
||||
<Text style={styles.h1}>{model.product.name}</Text>
|
||||
{model.product.categoriesLine ? <Text style={styles.subhead}>{model.product.categoriesLine}</Text> : null}
|
||||
|
||||
<View style={styles.heroBox}>
|
||||
{assets.heroDataUrl ? (
|
||||
<Image src={assets.heroDataUrl} style={styles.heroImage} />
|
||||
) : (
|
||||
<Text style={styles.noImage}>{model.labels.noImage}</Text>
|
||||
)}
|
||||
<View style={styles.hero}>
|
||||
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} isHero={true} />
|
||||
|
||||
<View style={styles.productRow}>
|
||||
<View style={styles.productInfoCol}>
|
||||
<View style={styles.productHero}>
|
||||
{model.product.categoriesLine ? <Text style={styles.productMeta}>{model.product.categoriesLine}</Text> : null}
|
||||
<Text style={styles.productName}>{model.product.name}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.productImageCol}>
|
||||
{assets.heroDataUrl ? (
|
||||
<Image src={assets.heroDataUrl} style={styles.heroImage} />
|
||||
) : (
|
||||
<Text style={styles.noImage}>{model.labels.noImage}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{model.product.descriptionText ? (
|
||||
<Section title={model.labels.description} minPresenceAhead={24}>
|
||||
<Text style={styles.body}>{model.product.descriptionText}</Text>
|
||||
</Section>
|
||||
) : null}
|
||||
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
|
||||
|
||||
{model.technicalItems.length ? (
|
||||
<Section title={model.labels.technicalData} minPresenceAhead={24}>
|
||||
<KeyValueGrid items={model.technicalItems} />
|
||||
</Section>
|
||||
) : null}
|
||||
<View style={styles.content}>
|
||||
{model.product.descriptionText ? (
|
||||
<Section title={model.labels.description} minPresenceAhead={24}>
|
||||
<Text style={styles.body}>{model.product.descriptionText}</Text>
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
{model.technicalItems.length ? (
|
||||
<Section title={model.labels.technicalData} minPresenceAhead={24}>
|
||||
<KeyValueGrid items={model.technicalItems} />
|
||||
</Section>
|
||||
) : null}
|
||||
</View>
|
||||
</Page>
|
||||
|
||||
{/*
|
||||
Render all voltage sections in a single flow so React-PDF can paginate naturally.
|
||||
This avoids hard page breaks that waste remaining whitespace at the bottom of a page.
|
||||
Each table section has break={false} to prevent breaking within individual tables,
|
||||
but the overall flow allows tables to move to the next page if needed.
|
||||
*/}
|
||||
<Page size="A4" style={styles.page}>
|
||||
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} />
|
||||
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
|
||||
|
||||
{model.voltageTables.map((t: DatasheetVoltageTable) => (
|
||||
<View key={t.voltageLabel} style={{ marginBottom: 14 }} break={false} minPresenceAhead={24}>
|
||||
<Text style={styles.sectionTitle}>{`${model.labels.crossSection} — ${t.voltageLabel}`}</Text>
|
||||
<View style={styles.content}>
|
||||
{model.voltageTables.map((t: DatasheetVoltageTable) => (
|
||||
<View key={t.voltageLabel} style={{ marginBottom: 14 }} break={false} minPresenceAhead={24}>
|
||||
<Text style={styles.sectionTitle}>{`${model.labels.crossSection} — ${t.voltageLabel}`}</Text>
|
||||
<View style={styles.sectionAccent} />
|
||||
<DenseTable table={{ columns: t.columns, rows: t.rows }} firstColLabel={firstColLabel} />
|
||||
</View>
|
||||
))}
|
||||
|
||||
<DenseTable table={{ columns: t.columns, rows: t.rows }} firstColLabel={firstColLabel} />
|
||||
</View>
|
||||
))}
|
||||
|
||||
{model.legendItems.length ? (
|
||||
<Section title={model.locale === 'de' ? 'ABKÜRZUNGEN' : 'ABBREVIATIONS'} minPresenceAhead={24}>
|
||||
<KeyValueGrid items={model.legendItems} />
|
||||
</Section>
|
||||
) : null}
|
||||
{model.legendItems.length ? (
|
||||
<Section title={model.locale === 'de' ? 'ABKÜRZUNGEN' : 'ABBREVIATIONS'} minPresenceAhead={24}>
|
||||
<KeyValueGrid items={model.legendItems} />
|
||||
</Section>
|
||||
) : null}
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
|
||||
@@ -14,9 +14,9 @@ export function Footer(props: { locale: 'en' | 'de'; siteUrl?: string }): React.
|
||||
|
||||
return (
|
||||
<View style={styles.footer} fixed>
|
||||
<Text>{siteUrl}</Text>
|
||||
<Text>{date}</Text>
|
||||
<Text render={({ pageNumber, totalPages }) => `${pageNumber}/${totalPages}`} />
|
||||
<Text style={styles.footerBrand}>KLZ CABLES</Text>
|
||||
<Text style={styles.footerText}>{date}</Text>
|
||||
<Text style={styles.footerText} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,17 +3,16 @@ import { Image, Text, View } from '@react-pdf/renderer';
|
||||
|
||||
import { styles } from '../styles';
|
||||
|
||||
export function Header(props: { title: string; logoDataUrl?: string | null; qrDataUrl?: string | null }): React.ReactElement {
|
||||
export function Header(props: { title: string; logoDataUrl?: string | null; qrDataUrl?: string | null; isHero?: boolean }): React.ReactElement {
|
||||
const { isHero = false } = props;
|
||||
|
||||
return (
|
||||
<View style={styles.header} fixed>
|
||||
<View style={isHero ? styles.header : [styles.header, { paddingHorizontal: 0, backgroundColor: 'transparent', borderBottomWidth: 0, marginBottom: 24, paddingTop: 40 }]}>
|
||||
<View style={styles.headerLeft}>
|
||||
{props.logoDataUrl ? (
|
||||
<Image src={props.logoDataUrl} style={styles.logo} />
|
||||
) : (
|
||||
<View style={styles.brandFallback}>
|
||||
<Text style={styles.brandFallbackKlz}>KLZ</Text>
|
||||
<Text style={styles.brandFallbackCables}>Cables</Text>
|
||||
</View>
|
||||
<Text style={styles.brandFallback}>KLZ</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.headerRight}>
|
||||
|
||||
@@ -8,37 +8,25 @@ export function KeyValueGrid(props: { items: KeyValueItem[] }): React.ReactEleme
|
||||
const items = (props.items || []).filter(i => i.label && i.value);
|
||||
if (!items.length) return null;
|
||||
|
||||
// 4-column layout: (label, value, label, value)
|
||||
const rows: Array<[KeyValueItem, KeyValueItem | null]> = [];
|
||||
for (let i = 0; i < items.length; i += 2) {
|
||||
rows.push([items[i], items[i + 1] || null]);
|
||||
}
|
||||
|
||||
// 2-column layout: (label, value)
|
||||
return (
|
||||
<View style={styles.kvGrid}>
|
||||
{rows.map(([left, right], rowIndex) => {
|
||||
const isLast = rowIndex === rows.length - 1;
|
||||
const leftValue = left.unit ? `${left.value} ${left.unit}` : left.value;
|
||||
const rightValue = right ? (right.unit ? `${right.value} ${right.unit}` : right.value) : '';
|
||||
{items.map((item, rowIndex) => {
|
||||
const isLast = rowIndex === items.length - 1;
|
||||
const value = item.unit ? `${item.value} ${item.unit}` : item.value;
|
||||
|
||||
return (
|
||||
<View
|
||||
key={`${left.label}-${rowIndex}`}
|
||||
key={`${item.label}-${rowIndex}`}
|
||||
style={[styles.kvRow, rowIndex % 2 === 0 ? styles.kvRowAlt : null, isLast ? styles.kvRowLast : null]}
|
||||
wrap={false}
|
||||
minPresenceAhead={12}
|
||||
>
|
||||
<View style={[styles.kvCell, { width: '18%' }]}>
|
||||
<Text style={styles.kvLabelText}>{left.label}</Text>
|
||||
<View style={[styles.kvCell, { width: '50%' }]}>
|
||||
<Text style={styles.kvLabelText}>{item.label}</Text>
|
||||
</View>
|
||||
<View style={[styles.kvCell, styles.kvMidDivider, { width: '32%' }]}>
|
||||
<Text style={styles.kvValueText}>{leftValue}</Text>
|
||||
</View>
|
||||
<View style={[styles.kvCell, { width: '18%' }]}>
|
||||
<Text style={styles.kvLabelText}>{right?.label || ''}</Text>
|
||||
</View>
|
||||
<View style={[styles.kvCell, { width: '32%' }]}>
|
||||
<Text style={styles.kvValueText}>{rightValue}</Text>
|
||||
<View style={[styles.kvCell, { width: '50%' }]}>
|
||||
<Text style={styles.kvValueText}>{value}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -11,8 +11,9 @@ export function Section(props: {
|
||||
}): React.ReactElement {
|
||||
const boxed = props.boxed ?? true;
|
||||
return (
|
||||
<View style={boxed ? styles.section : styles.sectionPlain} minPresenceAhead={props.minPresenceAhead}>
|
||||
<View style={styles.section} minPresenceAhead={props.minPresenceAhead}>
|
||||
<Text style={styles.sectionTitle}>{props.title}</Text>
|
||||
<View style={styles.sectionAccent} />
|
||||
{props.children}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -5,146 +5,212 @@ import { Font, StyleSheet } from '@react-pdf/renderer';
|
||||
Font.registerHyphenationCallback(word => [word]);
|
||||
|
||||
export const COLORS = {
|
||||
navy: '#0E2A47',
|
||||
mediumGray: '#6B7280',
|
||||
darkGray: '#1F2933',
|
||||
lightGray: '#E6E9ED',
|
||||
almostWhite: '#F8F9FA',
|
||||
headerBg: '#F6F8FB',
|
||||
primary: '#001a4d',
|
||||
primaryDark: '#000d26',
|
||||
accent: '#82ed20',
|
||||
textPrimary: '#111827',
|
||||
textSecondary: '#4b5563',
|
||||
textLight: '#9ca3af',
|
||||
neutral: '#f8f9fa',
|
||||
border: '#e5e7eb',
|
||||
} as const;
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
page: {
|
||||
paddingTop: 54,
|
||||
paddingLeft: 54,
|
||||
paddingRight: 54,
|
||||
paddingBottom: 72,
|
||||
paddingTop: 0,
|
||||
paddingLeft: 30,
|
||||
paddingRight: 30,
|
||||
paddingBottom: 60,
|
||||
fontFamily: 'Helvetica',
|
||||
fontSize: 10,
|
||||
color: COLORS.darkGray,
|
||||
color: COLORS.textPrimary,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
|
||||
// Hero-style header
|
||||
hero: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
paddingTop: 30,
|
||||
paddingBottom: 0,
|
||||
paddingHorizontal: 0,
|
||||
marginBottom: 20,
|
||||
position: 'relative',
|
||||
borderBottomWidth: 0,
|
||||
borderBottomColor: COLORS.border,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: COLORS.headerBg,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.lightGray,
|
||||
marginBottom: 16,
|
||||
marginBottom: 24,
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
logo: { width: 110, height: 24, objectFit: 'contain' },
|
||||
brandFallback: { flexDirection: 'row', alignItems: 'baseline', gap: 6 },
|
||||
brandFallbackKlz: { fontSize: 18, fontWeight: 700, color: COLORS.navy },
|
||||
brandFallbackCables: { fontSize: 10, color: COLORS.mediumGray },
|
||||
logo: { width: 100, height: 22, objectFit: 'contain' },
|
||||
brandFallback: { fontSize: 20, fontWeight: 700, color: COLORS.primaryDark, letterSpacing: 1, textTransform: 'uppercase' },
|
||||
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
headerTitle: { fontSize: 9, fontWeight: 700, color: COLORS.navy, letterSpacing: 0.2 },
|
||||
qr: { width: 34, height: 34, objectFit: 'contain' },
|
||||
headerTitle: { fontSize: 9, fontWeight: 700, color: COLORS.primary, letterSpacing: 1.5, textTransform: 'uppercase' },
|
||||
qr: { width: 30, height: 30, objectFit: 'contain' },
|
||||
|
||||
productRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 20,
|
||||
},
|
||||
productInfoCol: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
productImageCol: {
|
||||
flex: 1,
|
||||
height: 120,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: COLORS.border,
|
||||
backgroundColor: '#FFFFFF',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
||||
productHero: {
|
||||
marginTop: 0,
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
productName: {
|
||||
fontSize: 24,
|
||||
fontWeight: 700,
|
||||
color: COLORS.primaryDark,
|
||||
marginBottom: 0,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
productMeta: {
|
||||
fontSize: 9,
|
||||
color: COLORS.textSecondary,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
marginBottom: 4,
|
||||
},
|
||||
|
||||
content: {
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
left: 54,
|
||||
right: 54,
|
||||
bottom: 36,
|
||||
paddingTop: 10,
|
||||
left: 30,
|
||||
right: 30,
|
||||
bottom: 30,
|
||||
paddingTop: 16,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: COLORS.lightGray,
|
||||
borderTopColor: COLORS.border,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: 8,
|
||||
color: COLORS.mediumGray,
|
||||
alignItems: 'center',
|
||||
},
|
||||
footerBrand: { fontSize: 9, fontWeight: 700, color: COLORS.primaryDark, textTransform: 'uppercase', letterSpacing: 1 },
|
||||
footerText: { fontSize: 8, color: COLORS.textLight, fontWeight: 500, textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
|
||||
h1: { fontSize: 18, fontWeight: 700, color: COLORS.navy, marginBottom: 6 },
|
||||
subhead: { fontSize: 10.5, color: COLORS.mediumGray, marginBottom: 14 },
|
||||
h1: { fontSize: 22, fontWeight: 700, color: COLORS.primaryDark, marginBottom: 8, textTransform: 'uppercase' },
|
||||
subhead: { fontSize: 10, fontWeight: 700, color: COLORS.textSecondary, marginBottom: 16, textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
|
||||
heroBox: {
|
||||
height: 110,
|
||||
height: 180,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: COLORS.lightGray,
|
||||
backgroundColor: COLORS.almostWhite,
|
||||
marginBottom: 16,
|
||||
borderColor: COLORS.border,
|
||||
backgroundColor: '#FFFFFF',
|
||||
marginBottom: 24,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
padding: 0,
|
||||
},
|
||||
heroImage: { width: '100%', height: '100%', objectFit: 'contain' },
|
||||
noImage: { fontSize: 8, color: COLORS.mediumGray, paddingHorizontal: 12 },
|
||||
noImage: { fontSize: 8, color: COLORS.textLight, textAlign: 'center' },
|
||||
|
||||
section: {
|
||||
borderWidth: 1,
|
||||
borderColor: COLORS.lightGray,
|
||||
padding: 14,
|
||||
marginBottom: 14,
|
||||
},
|
||||
sectionPlain: {
|
||||
paddingVertical: 2,
|
||||
marginBottom: 12,
|
||||
marginBottom: 10,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 10,
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: COLORS.navy,
|
||||
color: COLORS.primaryDark,
|
||||
marginBottom: 8,
|
||||
letterSpacing: 0.2,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: -0.2,
|
||||
},
|
||||
body: { fontSize: 10, lineHeight: 1.5, color: COLORS.darkGray },
|
||||
sectionAccent: {
|
||||
width: 30,
|
||||
height: 3,
|
||||
backgroundColor: COLORS.accent,
|
||||
marginBottom: 8,
|
||||
borderRadius: 1.5,
|
||||
},
|
||||
body: { fontSize: 10, lineHeight: 1.6, color: COLORS.textSecondary },
|
||||
|
||||
kvGrid: {
|
||||
width: '100%',
|
||||
borderWidth: 1,
|
||||
borderColor: COLORS.lightGray,
|
||||
borderColor: COLORS.border,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
kvRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.lightGray,
|
||||
borderBottomColor: COLORS.border,
|
||||
},
|
||||
kvRowAlt: { backgroundColor: COLORS.almostWhite },
|
||||
kvRowAlt: { backgroundColor: COLORS.neutral },
|
||||
kvRowLast: { borderBottomWidth: 0 },
|
||||
kvCell: { paddingVertical: 6, paddingHorizontal: 8 },
|
||||
// Visual separator between (label,value) pairs in the 4-col KV grid.
|
||||
// Matches the engineering-table look and improves scanability.
|
||||
kvCell: { paddingVertical: 3, paddingHorizontal: 12 },
|
||||
kvMidDivider: {
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: COLORS.lightGray,
|
||||
borderRightColor: COLORS.border,
|
||||
},
|
||||
kvLabelText: { fontSize: 8.5, fontWeight: 700, color: COLORS.mediumGray },
|
||||
kvValueText: { fontSize: 9.5, color: COLORS.darkGray },
|
||||
kvLabelText: { fontSize: 8, fontWeight: 700, color: COLORS.primaryDark, textTransform: 'uppercase', letterSpacing: 0.3 },
|
||||
kvValueText: { fontSize: 9, color: COLORS.textPrimary, fontWeight: 500 },
|
||||
|
||||
tableWrap: { width: '100%', borderWidth: 1, borderColor: COLORS.lightGray, marginBottom: 14 },
|
||||
tableWrap: {
|
||||
width: '100%',
|
||||
borderWidth: 1,
|
||||
borderColor: COLORS.border,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 16,
|
||||
},
|
||||
tableHeader: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: COLORS.neutral,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.lightGray,
|
||||
borderBottomColor: COLORS.border,
|
||||
},
|
||||
tableHeaderCell: {
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 6.6,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 6,
|
||||
fontSize: 7,
|
||||
fontWeight: 700,
|
||||
color: COLORS.navy,
|
||||
color: COLORS.primaryDark,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
tableHeaderCellCfg: {
|
||||
paddingHorizontal: 6,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
tableHeaderCellDivider: {
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: COLORS.lightGray,
|
||||
borderRightColor: COLORS.border,
|
||||
},
|
||||
tableRow: { width: '100%', flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.lightGray },
|
||||
tableRowAlt: { backgroundColor: COLORS.almostWhite },
|
||||
tableCell: { paddingVertical: 4, paddingHorizontal: 4, fontSize: 6.6, color: COLORS.darkGray },
|
||||
tableRow: { width: '100%', flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.border },
|
||||
tableRowAlt: { backgroundColor: '#FFFFFF' },
|
||||
tableCell: { paddingVertical: 6, paddingHorizontal: 6, fontSize: 7, color: COLORS.textSecondary, fontWeight: 500 },
|
||||
tableCellCfg: {
|
||||
paddingHorizontal: 6,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
tableCellDivider: {
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: COLORS.lightGray,
|
||||
borderRightColor: COLORS.border,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user