Files
klz-cables.com/scripts/pdf/react-pdf/components/DenseTable.tsx
2026-01-14 18:49:33 +01:00

114 lines
4.0 KiB
TypeScript

import * as React from 'react';
import { Text, View } from '@react-pdf/renderer';
import type { DatasheetVoltageTable } from '../../model/types';
import { styles } from '../styles';
function clamp(n: number, min: number, max: number): number {
return Math.max(min, Math.min(max, n));
}
export function DenseTable(props: {
table: Pick<DatasheetVoltageTable, 'columns' | 'rows'>;
firstColLabel: string;
}): React.ReactElement {
const cols = props.table.columns;
const rows = props.table.rows;
const noWrapHeader = (label: string): string => {
const raw = String(label || '').trim();
if (!raw) return '';
// 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
// consumes the full content width.
// Goal:
// - keep the designation column *not too wide*
// - guarantee enough width for data headers when there is available space
const cfgMin = 0.18;
const cfgMax = 0.30;
let cfgPct = cols.length >= 14 ? 0.22 : cols.length >= 12 ? 0.24 : cols.length >= 10 ? 0.26 : 0.30;
cfgPct = clamp(cfgPct, cfgMin, cfgMax);
const minDataPct = cols.length >= 14 ? 0.045 : cols.length >= 12 ? 0.05 : cols.length >= 10 ? 0.055 : 0.06;
// If the initial cfgPct leaves too little width per data column, reduce cfgPct.
const cfgPctMaxForMinData = 1 - cols.length * minDataPct;
if (Number.isFinite(cfgPctMaxForMinData)) {
cfgPct = Math.min(cfgPct, cfgPctMaxForMinData);
cfgPct = clamp(cfgPct, cfgMin, cfgMax);
}
const cfgW = `${(cfgPct * 100).toFixed(4)}%`;
const dataTotal = 1 - cfgPct;
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%.
if (idx === cols.length - 1) {
const used = each * Math.max(0, cols.length - 1);
const remainder = Math.max(0, dataTotal - used);
return `${(remainder * 100).toFixed(4)}%`;
}
return `${(each * 100).toFixed(4)}%`;
});
return (
<View style={styles.tableWrap}>
<View style={styles.tableHeader} wrap={false}>
<View style={{ width: cfgW }}>
<Text
style={[
styles.tableHeaderCell,
styles.tableHeaderCellCfg,
cols.length ? styles.tableHeaderCellDivider : null,
]}
wrap={false}
>
{noWrapHeader(props.firstColLabel)}
</Text>
</View>
{cols.map((c, idx) => {
const isLast = idx === cols.length - 1;
return (
<View key={c.key} style={{ width: dataWs[idx] }}>
<Text
style={[styles.tableHeaderCell, !isLast ? styles.tableHeaderCellDivider : null]}
wrap={false}
>
{noWrapHeader(c.label)}
</Text>
</View>
);
})}
</View>
{rows.map((r, ri) => (
<View
key={`${r.configuration}-${ri}`}
style={[styles.tableRow, ri % 2 === 0 ? styles.tableRowAlt : null]}
wrap={false}
>
<View style={{ width: cfgW }}>
<Text style={[styles.tableCell, styles.tableCellCfg, cols.length ? styles.tableCellDivider : null]}>{r.configuration}</Text>
</View>
{r.cells.map((cell, ci) => {
const isLast = ci === r.cells.length - 1;
return (
<View key={`${cols[ci]?.key || ci}`} style={{ width: dataWs[ci] }}>
<Text style={[styles.tableCell, !isLast ? styles.tableCellDivider : null]}>{cell}</Text>
</View>
);
})}
</View>
))}
</View>
);
}