datasheets
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/datasheets_v2.zip
Normal file
BIN
public/datasheets_v2.zip
Normal file
Binary file not shown.
@@ -16,13 +16,6 @@ type Assets = {
|
|||||||
qrDataUrl: string | null;
|
qrDataUrl: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function chunk<T>(arr: T[], size: number): T[][] {
|
|
||||||
if (size <= 0) return [arr];
|
|
||||||
const out: T[][] = [];
|
|
||||||
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DatasheetDocument(props: { model: DatasheetModel; assets: Assets }): React.ReactElement {
|
export function DatasheetDocument(props: { model: DatasheetModel; assets: Assets }): React.ReactElement {
|
||||||
const { model, assets } = props;
|
const { model, assets } = props;
|
||||||
const headerTitle = model.labels.datasheet;
|
const headerTitle = model.labels.datasheet;
|
||||||
@@ -30,13 +23,6 @@ export function DatasheetDocument(props: { model: DatasheetModel; assets: Assets
|
|||||||
// Dense tables require compact headers (no wrapping). Use standard abbreviations.
|
// 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 & QS' : 'Cores & CS';
|
||||||
|
|
||||||
const tablePages: Array<{ table: DatasheetVoltageTable; rows: DatasheetVoltageTable['rows'] }> =
|
|
||||||
model.voltageTables.flatMap(t => {
|
|
||||||
const perPage = 30;
|
|
||||||
const chunks = chunk(t.rows, perPage);
|
|
||||||
return chunks.map(rows => ({ table: t, rows }));
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
<Page size="A4" style={styles.page}>
|
<Page size="A4" style={styles.page}>
|
||||||
@@ -67,18 +53,28 @@ export function DatasheetDocument(props: { model: DatasheetModel; assets: Assets
|
|||||||
) : null}
|
) : null}
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
{tablePages.map((p, index) => (
|
{/*
|
||||||
<Page key={`${p.table.voltageLabel}-${index}`} size="A4" style={styles.page}>
|
Render all voltage sections in a single flow so React-PDF can paginate naturally.
|
||||||
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} />
|
This avoids hard page breaks that waste remaining whitespace at the bottom of a page.
|
||||||
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
|
*/}
|
||||||
|
<Page size="A4" style={styles.page}>
|
||||||
|
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} />
|
||||||
|
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
|
||||||
|
|
||||||
<Section title={`${model.labels.crossSection} — ${p.table.voltageLabel}`}>
|
{model.voltageTables.map((t: DatasheetVoltageTable) => (
|
||||||
{p.table.metaItems.length ? <KeyValueGrid items={p.table.metaItems} /> : null}
|
<View key={t.voltageLabel}>
|
||||||
</Section>
|
<Section
|
||||||
|
title={`${model.labels.crossSection} — ${t.voltageLabel}`}
|
||||||
|
// Prevent orphaned voltage headings at page bottom; let the rest flow.
|
||||||
|
minPresenceAhead={140}
|
||||||
|
>
|
||||||
|
{t.metaItems.length ? <KeyValueGrid items={t.metaItems} /> : null}
|
||||||
|
</Section>
|
||||||
|
|
||||||
<DenseTable table={{ columns: p.table.columns, rows: p.rows }} firstColLabel={firstColLabel} />
|
<DenseTable table={{ columns: t.columns, rows: t.rows }} firstColLabel={firstColLabel} />
|
||||||
</Page>
|
</View>
|
||||||
))}
|
))}
|
||||||
</Document>
|
</Page>
|
||||||
);
|
</Document>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,69 @@ function clamp(n: number, min: number, max: number): number {
|
|||||||
return Math.max(min, Math.min(max, n));
|
return Math.max(min, Math.min(max, n));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normTextForMeasure(v: unknown): string {
|
||||||
|
return String(v ?? '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function textLen(v: unknown): number {
|
||||||
|
return normTextForMeasure(v).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function distributeWithMinMax(weights: number[], total: number, minEach: number, maxEach: number): number[] {
|
||||||
|
const n = weights.length;
|
||||||
|
if (!n) return [];
|
||||||
|
|
||||||
|
const mins = Array.from({ length: n }, () => minEach);
|
||||||
|
const maxs = Array.from({ length: n }, () => maxEach);
|
||||||
|
|
||||||
|
// If mins don't fit, scale them down proportionally.
|
||||||
|
const minSum = mins.reduce((a, b) => a + b, 0);
|
||||||
|
if (minSum > total) {
|
||||||
|
const k = total / minSum;
|
||||||
|
return mins.map(m => m * k);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = mins.slice();
|
||||||
|
let remaining = total - minSum;
|
||||||
|
let remainingIdx = Array.from({ length: n }, (_, i) => i);
|
||||||
|
|
||||||
|
// Distribute remaining proportionally, respecting max constraints.
|
||||||
|
// Loop is guaranteed to terminate because each iteration either:
|
||||||
|
// - removes at least one index due to hitting max, or
|
||||||
|
// - exhausts `remaining`.
|
||||||
|
while (remaining > 1e-9 && remainingIdx.length) {
|
||||||
|
const wSum = remainingIdx.reduce((acc, i) => acc + Math.max(0, weights[i] || 0), 0);
|
||||||
|
if (wSum <= 1e-9) {
|
||||||
|
// No meaningful weights: distribute evenly.
|
||||||
|
const even = remaining / remainingIdx.length;
|
||||||
|
for (const i of remainingIdx) result[i] += even;
|
||||||
|
remaining = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextIdx: number[] = [];
|
||||||
|
for (const i of remainingIdx) {
|
||||||
|
const w = Math.max(0, weights[i] || 0);
|
||||||
|
const add = (w / wSum) * remaining;
|
||||||
|
const capped = Math.min(result[i] + add, maxs[i]);
|
||||||
|
const used = capped - result[i];
|
||||||
|
result[i] = capped;
|
||||||
|
remaining -= used;
|
||||||
|
if (result[i] + 1e-9 < maxs[i]) nextIdx.push(i);
|
||||||
|
}
|
||||||
|
remainingIdx = nextIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numerical guard: force exact sum by adjusting the last column.
|
||||||
|
const sum = result.reduce((a, b) => a + b, 0);
|
||||||
|
const drift = total - sum;
|
||||||
|
if (Math.abs(drift) > 1e-9) result[result.length - 1] += drift;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export function DenseTable(props: {
|
export function DenseTable(props: {
|
||||||
table: Pick<DatasheetVoltageTable, 'columns' | 'rows'>;
|
table: Pick<DatasheetVoltageTable, 'columns' | 'rows'>;
|
||||||
firstColLabel: string;
|
firstColLabel: string;
|
||||||
@@ -15,50 +78,68 @@ export function DenseTable(props: {
|
|||||||
const cols = props.table.columns;
|
const cols = props.table.columns;
|
||||||
const rows = props.table.rows;
|
const rows = props.table.rows;
|
||||||
|
|
||||||
const noWrapHeader = (label: string): string => {
|
const headerText = (label: string): string => {
|
||||||
const raw = String(label || '').trim();
|
// Table headers must NEVER wrap into a second line.
|
||||||
if (!raw) return '';
|
// react-pdf can wrap on spaces, so we replace whitespace with NBSP.
|
||||||
|
return String(label || '').replace(/\s+/g, '\u00A0').trim();
|
||||||
// Ensure the header never wraps into a second line.
|
|
||||||
// - Remove whitespace break opportunities (NBSP)
|
|
||||||
// NOTE: Avoid inserting zero-width joiners between letters.
|
|
||||||
// Some PDF viewers render them with spacing/odd glyph behavior.
|
|
||||||
// This is intentionally aggressive because broken headers destroy scanability.
|
|
||||||
return raw.replace(/\s+/g, '\u00A0');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Column widths: use explicit percentages (no rounding gaps) so the table always
|
// Column widths: use explicit percentages (no rounding gaps) so the table always
|
||||||
// consumes the full content width.
|
// consumes the full content width.
|
||||||
// Goal:
|
// Goal:
|
||||||
// - keep the designation column *not too wide*
|
// - keep the designation column *not too wide*
|
||||||
// - guarantee enough width for data headers when there is available space
|
// - distribute data columns by estimated content width (header + cells)
|
||||||
const cfgMin = 0.18;
|
// so columns better fit their data
|
||||||
const cfgMax = 0.30;
|
// Make first column denser so numeric columns get more room.
|
||||||
|
// (Long designations can still wrap in body if needed, but table scanability
|
||||||
|
// benefits more from wider data columns.)
|
||||||
|
const cfgMin = 0.14;
|
||||||
|
const cfgMax = 0.23;
|
||||||
|
|
||||||
let cfgPct = cols.length >= 14 ? 0.22 : cols.length >= 12 ? 0.24 : cols.length >= 10 ? 0.26 : 0.30;
|
// A content-based heuristic.
|
||||||
|
// React-PDF doesn't expose a reliable text-measurement API at render time,
|
||||||
|
// so we approximate width by string length (compressed via sqrt to reduce outliers).
|
||||||
|
const cfgContentLen = Math.max(textLen(props.firstColLabel), ...rows.map(r => textLen(r.configuration)), 8);
|
||||||
|
const dataContentLens = cols.map((c, ci) => {
|
||||||
|
const headerL = textLen(c.label);
|
||||||
|
let cellMax = 0;
|
||||||
|
for (const r of rows) cellMax = Math.max(cellMax, textLen(r.cells[ci]));
|
||||||
|
// Slightly prioritize the header (scanability) over a single long cell.
|
||||||
|
return Math.max(headerL * 1.15, cellMax, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use mostly-linear weights so long headers get noticeably more space.
|
||||||
|
const cfgWeight = cfgContentLen * 1.05;
|
||||||
|
const dataWeights = dataContentLens.map(l => l);
|
||||||
|
const dataWeightSum = dataWeights.reduce((a, b) => a + b, 0);
|
||||||
|
const rawCfgPct = dataWeightSum > 0 ? cfgWeight / (cfgWeight + dataWeightSum) : 0.28;
|
||||||
|
let cfgPct = clamp(rawCfgPct, cfgMin, cfgMax);
|
||||||
|
|
||||||
|
// Ensure a minimum per-data-column width; if needed, shrink cfgPct.
|
||||||
|
// These floors are intentionally generous. Too-narrow columns are worse than a
|
||||||
|
// slightly narrower first column for scanability.
|
||||||
|
const minDataPct = cols.length >= 14 ? 0.045 : cols.length >= 12 ? 0.05 : cols.length >= 10 ? 0.055 : 0.06;
|
||||||
|
const cfgPctMaxForMinData = 1 - cols.length * minDataPct;
|
||||||
|
if (Number.isFinite(cfgPctMaxForMinData)) cfgPct = Math.min(cfgPct, cfgPctMaxForMinData);
|
||||||
cfgPct = clamp(cfgPct, cfgMin, cfgMax);
|
cfgPct = clamp(cfgPct, cfgMin, cfgMax);
|
||||||
|
|
||||||
const minDataPct = cols.length >= 14 ? 0.045 : cols.length >= 12 ? 0.05 : cols.length >= 10 ? 0.055 : 0.06;
|
const dataTotal = Math.max(0, 1 - cfgPct);
|
||||||
// If the initial cfgPct leaves too little width per data column, reduce cfgPct.
|
const maxDataPct = Math.min(0.24, Math.max(minDataPct * 2.8, dataTotal * 0.55));
|
||||||
const cfgPctMaxForMinData = 1 - cols.length * minDataPct;
|
const dataPcts = distributeWithMinMax(dataWeights, dataTotal, minDataPct, maxDataPct);
|
||||||
if (Number.isFinite(cfgPctMaxForMinData)) {
|
|
||||||
cfgPct = Math.min(cfgPct, cfgPctMaxForMinData);
|
|
||||||
cfgPct = clamp(cfgPct, cfgMin, cfgMax);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cfgW = `${(cfgPct * 100).toFixed(4)}%`;
|
const cfgW = `${(cfgPct * 100).toFixed(4)}%`;
|
||||||
const dataTotal = 1 - cfgPct;
|
const dataWs = dataPcts.map((p, idx) => {
|
||||||
const each = cols.length ? dataTotal / cols.length : dataTotal;
|
|
||||||
const dataWs = cols.map((_, idx) => {
|
|
||||||
// Keep the last column as the remainder so percentages sum to exactly 100%.
|
// Keep the last column as the remainder so percentages sum to exactly 100%.
|
||||||
if (idx === cols.length - 1) {
|
if (idx === dataPcts.length - 1) {
|
||||||
const used = each * Math.max(0, cols.length - 1);
|
const used = dataPcts.slice(0, -1).reduce((a, b) => a + b, 0);
|
||||||
const remainder = Math.max(0, dataTotal - used);
|
const remainder = Math.max(0, dataTotal - used);
|
||||||
return `${(remainder * 100).toFixed(4)}%`;
|
return `${(remainder * 100).toFixed(4)}%`;
|
||||||
}
|
}
|
||||||
return `${(each * 100).toFixed(4)}%`;
|
return `${(p * 100).toFixed(4)}%`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const headerFontSize = cols.length >= 14 ? 5.7 : cols.length >= 12 ? 5.9 : cols.length >= 10 ? 6.2 : 6.6;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.tableWrap}>
|
<View style={styles.tableWrap}>
|
||||||
<View style={styles.tableHeader} wrap={false}>
|
<View style={styles.tableHeader} wrap={false}>
|
||||||
@@ -67,11 +148,12 @@ export function DenseTable(props: {
|
|||||||
style={[
|
style={[
|
||||||
styles.tableHeaderCell,
|
styles.tableHeaderCell,
|
||||||
styles.tableHeaderCellCfg,
|
styles.tableHeaderCellCfg,
|
||||||
|
{ fontSize: headerFontSize, paddingHorizontal: 3 },
|
||||||
cols.length ? styles.tableHeaderCellDivider : null,
|
cols.length ? styles.tableHeaderCellDivider : null,
|
||||||
]}
|
]}
|
||||||
wrap={false}
|
wrap={false}
|
||||||
>
|
>
|
||||||
{noWrapHeader(props.firstColLabel)}
|
{headerText(props.firstColLabel)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{cols.map((c, idx) => {
|
{cols.map((c, idx) => {
|
||||||
@@ -79,10 +161,14 @@ export function DenseTable(props: {
|
|||||||
return (
|
return (
|
||||||
<View key={c.key} style={{ width: dataWs[idx] }}>
|
<View key={c.key} style={{ width: dataWs[idx] }}>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.tableHeaderCell, !isLast ? styles.tableHeaderCellDivider : null]}
|
style={[
|
||||||
|
styles.tableHeaderCell,
|
||||||
|
{ fontSize: headerFontSize, paddingHorizontal: 3 },
|
||||||
|
!isLast ? styles.tableHeaderCellDivider : null,
|
||||||
|
]}
|
||||||
wrap={false}
|
wrap={false}
|
||||||
>
|
>
|
||||||
{noWrapHeader(c.label)}
|
{headerText(c.label)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -94,15 +180,31 @@ export function DenseTable(props: {
|
|||||||
key={`${r.configuration}-${ri}`}
|
key={`${r.configuration}-${ri}`}
|
||||||
style={[styles.tableRow, ri % 2 === 0 ? styles.tableRowAlt : null]}
|
style={[styles.tableRow, ri % 2 === 0 ? styles.tableRowAlt : null]}
|
||||||
wrap={false}
|
wrap={false}
|
||||||
|
// If the row doesn't fit, move the whole row to the next page.
|
||||||
|
// This prevents page breaks mid-row.
|
||||||
|
minPresenceAhead={16}
|
||||||
>
|
>
|
||||||
<View style={{ width: cfgW }}>
|
<View style={{ width: cfgW }} wrap={false}>
|
||||||
<Text style={[styles.tableCell, styles.tableCellCfg, cols.length ? styles.tableCellDivider : null]}>{r.configuration}</Text>
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.tableCell,
|
||||||
|
styles.tableCellCfg,
|
||||||
|
// Denser first column: slightly smaller type + tighter padding.
|
||||||
|
{ fontSize: 6.2, paddingHorizontal: 3 },
|
||||||
|
cols.length ? styles.tableCellDivider : null,
|
||||||
|
]}
|
||||||
|
wrap={false}
|
||||||
|
>
|
||||||
|
{r.configuration}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{r.cells.map((cell, ci) => {
|
{r.cells.map((cell, ci) => {
|
||||||
const isLast = ci === r.cells.length - 1;
|
const isLast = ci === r.cells.length - 1;
|
||||||
return (
|
return (
|
||||||
<View key={`${cols[ci]?.key || ci}`} style={{ width: dataWs[ci] }}>
|
<View key={`${cols[ci]?.key || ci}`} style={{ width: dataWs[ci] }} wrap={false}>
|
||||||
<Text style={[styles.tableCell, !isLast ? styles.tableCellDivider : null]}>{cell}</Text>
|
<Text style={[styles.tableCell, !isLast ? styles.tableCellDivider : null]} wrap={false}>
|
||||||
|
{cell}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function KeyValueGrid(props: { items: KeyValueItem[] }): React.ReactEleme
|
|||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
key={`${left.label}-${rowIndex}`}
|
key={`${left.label}-${rowIndex}`}
|
||||||
style={[styles.kvRow, isLast ? styles.kvRowLast : null]}
|
style={[styles.kvRow, rowIndex % 2 === 0 ? styles.kvRowAlt : null, isLast ? styles.kvRowLast : null]}
|
||||||
wrap={false}
|
wrap={false}
|
||||||
>
|
>
|
||||||
<View style={[styles.kvCell, { width: '23%' }]}>
|
<View style={[styles.kvCell, { width: '23%' }]}>
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ export function Section(props: {
|
|||||||
title: string;
|
title: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
boxed?: boolean;
|
boxed?: boolean;
|
||||||
|
minPresenceAhead?: number;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const boxed = props.boxed ?? true;
|
const boxed = props.boxed ?? true;
|
||||||
return (
|
return (
|
||||||
<View style={boxed ? styles.section : styles.sectionPlain}>
|
<View style={boxed ? styles.section : styles.sectionPlain} minPresenceAhead={props.minPresenceAhead}>
|
||||||
<Text style={styles.sectionTitle}>{props.title}</Text>
|
<Text style={styles.sectionTitle}>{props.title}</Text>
|
||||||
{props.children}
|
{props.children}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export const styles = StyleSheet.create({
|
|||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: COLORS.lightGray,
|
borderBottomColor: COLORS.lightGray,
|
||||||
},
|
},
|
||||||
|
kvRowAlt: { backgroundColor: COLORS.almostWhite },
|
||||||
kvRowLast: { borderBottomWidth: 0 },
|
kvRowLast: { borderBottomWidth: 0 },
|
||||||
kvCell: { paddingVertical: 6, paddingHorizontal: 8 },
|
kvCell: { paddingVertical: 6, paddingHorizontal: 8 },
|
||||||
// Visual separator between (label,value) pairs in the 4-col KV grid.
|
// Visual separator between (label,value) pairs in the 4-col KV grid.
|
||||||
@@ -114,7 +115,7 @@ export const styles = StyleSheet.create({
|
|||||||
kvLabelText: { fontSize: 8.5, fontWeight: 700, color: COLORS.mediumGray },
|
kvLabelText: { fontSize: 8.5, fontWeight: 700, color: COLORS.mediumGray },
|
||||||
kvValueText: { fontSize: 9.5, color: COLORS.darkGray },
|
kvValueText: { fontSize: 9.5, color: COLORS.darkGray },
|
||||||
|
|
||||||
tableWrap: { width: '100%', borderWidth: 1, borderColor: COLORS.lightGray },
|
tableWrap: { width: '100%', borderWidth: 1, borderColor: COLORS.lightGray, marginBottom: 14 },
|
||||||
tableHeader: {
|
tableHeader: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
Reference in New Issue
Block a user