@xeplr/ui-table 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * config: { onClick?: (row) => void, hrefKey?: string, target?: string }
3
+ *
4
+ * If `hrefKey` is set, renders an <a>. Otherwise a clickable span that calls onClick(row).
5
+ */
6
+ export default function link(config) {
7
+ var onClick = config && config.onClick;
8
+ var hrefKey = config && config.hrefKey;
9
+ var target = config && config.target;
10
+ return function cell(ctx) {
11
+ var label = ctx.getValue();
12
+ if (label === null || label === undefined || label === '') return null;
13
+ if (hrefKey) {
14
+ var href = ctx.row.original[hrefKey];
15
+ return <a className="xeplr-r-link" href={href} target={target}>{label}</a>;
16
+ }
17
+ return (
18
+ <span
19
+ className="xeplr-r-link"
20
+ onClick={function() { if (onClick) onClick(ctx.row.original); }}
21
+ >{label}</span>
22
+ );
23
+ };
24
+ }
@@ -0,0 +1,35 @@
1
+ import { initials, avatarTint } from './_helpers.js';
2
+
3
+ /**
4
+ * config: { max?: number, nameKey?: string, imgKey?: string, imgPrefix?: string }
5
+ * Value: Array<{ [nameKey]: string, [imgKey]?: string, ... }>
6
+ */
7
+ export default function memberChips(config) {
8
+ var max = (config && config.max) || 3;
9
+ var nameKey = (config && config.nameKey) || 'name';
10
+ var imgKey = config && config.imgKey;
11
+ var imgPrefix = (config && config.imgPrefix) || '';
12
+ return function cell(ctx) {
13
+ var value = ctx.getValue();
14
+ if (!Array.isArray(value) || value.length === 0) return null;
15
+ var visible = value.slice(0, max);
16
+ var overflow = Math.max(value.length - max, 0);
17
+ return (
18
+ <div className="xeplr-r-chips">
19
+ {visible.map(function(item, i) {
20
+ var name = item && item[nameKey] ? item[nameKey] : '';
21
+ var img = imgKey && item ? item[imgKey] : null;
22
+ return (
23
+ <span key={i} className="xeplr-r-chip">
24
+ {img
25
+ ? <img className="xeplr-r-chip-avatar xeplr-r-chip-avatar-img" src={imgPrefix + img} alt="" />
26
+ : <span className="xeplr-r-chip-avatar" style={{ background: avatarTint(name) }}>{initials(name)}</span>}
27
+ <span className="xeplr-r-chip-name">{name}</span>
28
+ </span>
29
+ );
30
+ })}
31
+ {overflow > 0 && <span className="xeplr-r-chip-more">+{overflow}</span>}
32
+ </div>
33
+ );
34
+ };
35
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * config: { map: { [value]: { bg?, color?, label? } }, default?: { bg?, color? } }
3
+ */
4
+ export default function statusBadge(config) {
5
+ var map = (config && config.map) || {};
6
+ var fallback = (config && config.default) || { bg: '#232540', color: '#b8bbe0' };
7
+ return function cell(ctx) {
8
+ var v = ctx.getValue();
9
+ if (v === null || v === undefined || v === '') return null;
10
+ var entry = map[v] || fallback;
11
+ var label = entry.label || String(v);
12
+ var style = { background: entry.bg, color: entry.color, borderColor: entry.border || entry.bg };
13
+ return <span className="xeplr-r-badge" style={style}>{label}</span>;
14
+ };
15
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * config: { variant?: 'gold'|'muted'|'blue', max?: number }
3
+ * Value: string[] (or string — split by comma)
4
+ */
5
+ export default function tags(config) {
6
+ var variant = (config && config.variant) || 'gold';
7
+ var max = config && config.max;
8
+ return function cell(ctx) {
9
+ var value = ctx.getValue();
10
+ if (!value) return null;
11
+ var list = Array.isArray(value) ? value : String(value).split(',').map(function(s) { return s.trim(); }).filter(Boolean);
12
+ var visible = max ? list.slice(0, max) : list;
13
+ var overflow = max ? Math.max(list.length - max, 0) : 0;
14
+ return (
15
+ <div className="xeplr-r-tags">
16
+ {visible.map(function(t, i) {
17
+ return <span key={i} className={'xeplr-r-tag xeplr-r-tag-' + variant}>{t}</span>;
18
+ })}
19
+ {overflow > 0 && <span className="xeplr-r-tag-more">+{overflow}</span>}
20
+ </div>
21
+ );
22
+ };
23
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * config: { subKey?: string, primaryClass?: string }
3
+ */
4
+ export default function twoLine(config) {
5
+ var subKey = config && config.subKey;
6
+ return function cell(ctx) {
7
+ var primary = ctx.getValue();
8
+ var sub = subKey ? ctx.row.original[subKey] : null;
9
+ return (
10
+ <div className="xeplr-r-twoline">
11
+ <div className="xeplr-r-twoline-primary">{primary}</div>
12
+ {sub && <div className="xeplr-r-twoline-sub">{sub}</div>}
13
+ </div>
14
+ );
15
+ };
16
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * The table's style vocabulary, and the rule for resolving it.
3
+ *
4
+ * This is the "clear interface" the whole styling feature hangs off: one
5
+ * registry naming every style property a table cell can take, and one pure
6
+ * function that decides which layer's value wins. No React, no DOM, no
7
+ * persistence — a host supplies layers (theme defaults, workspace theme,
8
+ * widget theme, per-column/row/cell overrides) and gets back a CSS object.
9
+ *
10
+ * THREE THINGS WORTH KNOWING BEFORE CHANGING ANY OF THIS:
11
+ *
12
+ * 1. Properties are addressed by PATH, not name — 'border.top.width', not
13
+ * 'borderWidth'. Merging happens per leaf path, which is what stops a
14
+ * column setting border.bottom.color from wiping a cell's border.top.width.
15
+ *
16
+ * 2. `border.all` is expanded into the four sides at FLATTEN time, inside its
17
+ * own layer. That ordering is load-bearing. If it were expanded after
18
+ * merging, a theme that set border.top.width would survive a column that
19
+ * set border.all.width — the user would set the column's border to thin
20
+ * and watch one edge stay thick.
21
+ *
22
+ * 3. `margin` is deliberately absent. A <td> is not a block box and discards
23
+ * margin entirely; offering the control would be offering a lie. Margin
24
+ * belongs to the table as a whole, which a host lays out itself.
25
+ */
26
+
27
+ var LENGTH_UNITS = ['px', 'em', 'rem'];
28
+
29
+ var FONT_FAMILY_OPTIONS = [
30
+ ['inherit', 'Inherit'], ['sans-serif', 'Sans'], ['serif', 'Serif'], ['monospace', 'Mono']
31
+ ];
32
+ var FONT_WEIGHT_OPTIONS = [['400', 'Regular'], ['500', 'Medium'], ['600', 'Semibold'], ['700', 'Bold']];
33
+ var BORDER_STYLE_OPTIONS = [
34
+ ['none', 'None'], ['solid', 'Solid'], ['dashed', 'Dashed'], ['dotted', 'Dotted'], ['double', 'Double']
35
+ ];
36
+
37
+ var TEXT_FIELDS = [
38
+ { key: 'text.color', label: 'Text color', type: 'color', css: 'color' },
39
+ { key: 'text.fontFamily', label: 'Font', type: 'select', css: 'fontFamily', options: FONT_FAMILY_OPTIONS },
40
+ { key: 'text.fontSize', label: 'Size', type: 'length', css: 'fontSize', units: LENGTH_UNITS },
41
+ { key: 'text.fontWeight', label: 'Weight', type: 'select', css: 'fontWeight', options: FONT_WEIGHT_OPTIONS },
42
+ { key: 'text.fontStyle', label: 'Style', type: 'select', css: 'fontStyle', options: [['normal', 'Normal'], ['italic', 'Italic']] },
43
+ { key: 'text.textAlign', label: 'Align', type: 'select', css: 'textAlign', options: [['left', 'Left'], ['center', 'Center'], ['right', 'Right'], ['justify', 'Justify']] },
44
+ { key: 'text.verticalAlign', label: 'Vertical', type: 'select', css: 'verticalAlign', options: [['top', 'Top'], ['middle', 'Middle'], ['bottom', 'Bottom']] },
45
+ { key: 'text.textTransform', label: 'Case', type: 'select', css: 'textTransform', options: [['none', 'None'], ['uppercase', 'UPPER'], ['lowercase', 'lower'], ['capitalize', 'Capitalize']] },
46
+ { key: 'text.textDecoration', label: 'Decoration', type: 'select', css: 'textDecoration', options: [['none', 'None'], ['underline', 'Underline'], ['line-through', 'Strikethrough']] },
47
+ { key: 'text.lineHeight', label: 'Line height', type: 'number', css: 'lineHeight', min: 0.8, max: 3, step: 0.1 },
48
+ { key: 'text.letterSpacing', label: 'Letter spacing', type: 'length', css: 'letterSpacing', units: LENGTH_UNITS },
49
+ // Wrapping is a style choice a report designer genuinely makes per column —
50
+ // a code column reads better on one line even if it clips.
51
+ { key: 'text.whiteSpace', label: 'Wrapping', type: 'select', css: 'whiteSpace', options: [['normal', 'Wrap'], ['nowrap', 'No wrap'], ['pre-wrap', 'Preserve']] }
52
+ ];
53
+
54
+ var FILL_FIELDS = [
55
+ { key: 'fill.background', label: 'Background', type: 'color', css: 'background' }
56
+ ];
57
+
58
+ var BOX_FIELDS = [
59
+ { key: 'box.padding.top', label: 'Padding top', type: 'length', css: 'paddingTop', units: LENGTH_UNITS },
60
+ { key: 'box.padding.right', label: 'Padding right', type: 'length', css: 'paddingRight', units: LENGTH_UNITS },
61
+ { key: 'box.padding.bottom', label: 'Padding bottom', type: 'length', css: 'paddingBottom', units: LENGTH_UNITS },
62
+ { key: 'box.padding.left', label: 'Padding left', type: 'length', css: 'paddingLeft', units: LENGTH_UNITS },
63
+ { key: 'box.height', label: 'Height', type: 'length', css: 'height', units: LENGTH_UNITS }
64
+ ];
65
+
66
+ /** 'all' is a shorthand the user edits; it never reaches CSS (see note 2). */
67
+ export var BORDER_SIDES = ['top', 'right', 'bottom', 'left'];
68
+
69
+ function borderFieldsFor(side) {
70
+ var prefix = 'border.' + side + '.';
71
+ var cap = side === 'all' ? null : side.charAt(0).toUpperCase() + side.slice(1);
72
+ return [
73
+ { key: prefix + 'width', label: 'Width', type: 'length', units: LENGTH_UNITS, css: cap && 'border' + cap + 'Width', side: side },
74
+ { key: prefix + 'style', label: 'Style', type: 'select', options: BORDER_STYLE_OPTIONS, css: cap && 'border' + cap + 'Style', side: side },
75
+ { key: prefix + 'color', label: 'Color', type: 'color', css: cap && 'border' + cap + 'Color', side: side }
76
+ ];
77
+ }
78
+
79
+ var BORDER_FIELDS = [{ key: 'border.radius', label: 'Radius', type: 'length', css: 'borderRadius', units: LENGTH_UNITS }]
80
+ .concat(borderFieldsFor('all'))
81
+ .concat(BORDER_SIDES.reduce(function(acc, side) { return acc.concat(borderFieldsFor(side)); }, []));
82
+
83
+ /**
84
+ * The whole vocabulary, grouped for panel generation. A host builds its panel
85
+ * from this and nothing else — adding a stylable property means adding one
86
+ * entry here, and it appears everywhere with no other change.
87
+ */
88
+ export var STYLE_GROUPS = [
89
+ { key: 'text', label: 'Text', fields: TEXT_FIELDS },
90
+ { key: 'fill', label: 'Fill', fields: FILL_FIELDS },
91
+ { key: 'box', label: 'Spacing & size', fields: BOX_FIELDS },
92
+ // Sides are collapsed behind an expander in the UI: 'all' by default, with
93
+ // each side openable to override just that edge.
94
+ { key: 'border', label: 'Border', fields: BORDER_FIELDS, expandable: BORDER_SIDES }
95
+ ];
96
+
97
+ export var STYLE_FIELDS = STYLE_GROUPS.reduce(function(acc, g) { return acc.concat(g.fields); }, []);
98
+
99
+ var FIELD_BY_PATH = {};
100
+ STYLE_FIELDS.forEach(function(f) { FIELD_BY_PATH[f.key] = f; });
101
+
102
+ export function styleField(path) { return FIELD_BY_PATH[path] || null; }
103
+
104
+ // ── Values ────────────────────────────────────────────────────────────────
105
+
106
+ /**
107
+ * A length is stored as { value, unit } so the unit survives a round trip and
108
+ * the panel can show it. Bare numbers mean px; strings pass through untouched
109
+ * (so a host can store 'auto' or a var() reference).
110
+ */
111
+ export function cssLength(value) {
112
+ if (value === null || value === undefined || value === '') return null;
113
+ if (typeof value === 'number') return value + 'px';
114
+ if (typeof value === 'string') return value;
115
+ if (typeof value.value !== 'number' && !value.value) return null;
116
+ return value.value + (value.unit || 'px');
117
+ }
118
+
119
+ // ── Flatten ───────────────────────────────────────────────────────────────
120
+
121
+ function walk(node, prefix, out) {
122
+ Object.keys(node).forEach(function(key) {
123
+ var value = node[key];
124
+ var path = prefix ? prefix + '.' + key : key;
125
+ // A {value, unit} pair is a leaf, not a branch to descend into.
126
+ var isLeaf = value === null || typeof value !== 'object' ||
127
+ Object.prototype.hasOwnProperty.call(value, 'value');
128
+ if (isLeaf) out[path] = value;
129
+ else walk(value, path, out);
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Nested style object -> flat { path: value }, with border.all expanded into
135
+ * the four sides. An explicit side inside the SAME object beats 'all'; see
136
+ * note 2 at the top for why that expansion has to happen here.
137
+ */
138
+ export function flattenStyle(style) {
139
+ var flat = {};
140
+ if (!style) return flat;
141
+ walk(style, '', flat);
142
+
143
+ ['width', 'style', 'color'].forEach(function(part) {
144
+ var all = flat['border.all.' + part];
145
+ if (all === undefined) return;
146
+ BORDER_SIDES.forEach(function(side) {
147
+ var path = 'border.' + side + '.' + part;
148
+ if (flat[path] === undefined) flat[path] = all;
149
+ });
150
+ });
151
+ delete flat['border.all.width'];
152
+ delete flat['border.all.style'];
153
+ delete flat['border.all.color'];
154
+ return flat;
155
+ }
156
+
157
+ // ── Resolve ───────────────────────────────────────────────────────────────
158
+
159
+ /**
160
+ * Decide, per property, which layer wins.
161
+ *
162
+ * @param {Array<{id, style, rank?, at?}>} layers - Low precedence first.
163
+ * `rank` defaults to array position. Layers that genuinely compete — a cell,
164
+ * row and column override all landing on the same cell — share one rank and
165
+ * are separated by `at` (a timestamp), so the most recent edit wins.
166
+ *
167
+ * The timestamp is a TIEBREAK, not a winner-takes-all: it is consulted per
168
+ * property. A column setting a background and a cell setting a font weight
169
+ * both apply, whichever was edited last. Only when both set the SAME
170
+ * property does recency decide. Whole-record precedence would mean
171
+ * restyling a column silently discarded deliberate per-cell work.
172
+ *
173
+ * @returns {object} { path: { value, source, at } }
174
+ */
175
+ export function resolveStyle(layers) {
176
+ var resolved = {};
177
+ (layers || []).forEach(function(layer, index) {
178
+ if (!layer || !layer.style) return;
179
+ var rank = layer.rank === undefined ? index : layer.rank;
180
+ var at = layer.at === undefined ? null : layer.at;
181
+ var flat = flattenStyle(layer.style);
182
+
183
+ Object.keys(flat).forEach(function(path) {
184
+ var value = flat[path];
185
+ if (value === null || value === undefined || value === '') return;
186
+ var held = resolved[path];
187
+ if (held) {
188
+ if (rank < held.rank) return;
189
+ if (rank === held.rank && at !== null && held.at !== null && at < held.at) return;
190
+ if (rank === held.rank && at === null && held.at !== null) return;
191
+ }
192
+ resolved[path] = { value: value, source: layer.id, at: at, rank: rank };
193
+ });
194
+ });
195
+ return resolved;
196
+ }
197
+
198
+ // ── Emit ──────────────────────────────────────────────────────────────────
199
+
200
+ /**
201
+ * Resolved paths -> a React style object.
202
+ *
203
+ * Accepts either the output of resolveStyle or a plain { path: value } map,
204
+ * so a caller with a single style object can skip resolution entirely.
205
+ */
206
+ export function toCssProperties(resolved) {
207
+ var css = {};
208
+ if (!resolved) return css;
209
+
210
+ Object.keys(resolved).forEach(function(path) {
211
+ var field = FIELD_BY_PATH[path];
212
+ if (!field || !field.css) return; // unknown path, or border.all — never emitted
213
+ var entry = resolved[path];
214
+ var value = entry && typeof entry === 'object' && Object.prototype.hasOwnProperty.call(entry, 'source')
215
+ ? entry.value : entry;
216
+ if (value === null || value === undefined || value === '') return;
217
+ css[field.css] = field.type === 'length' ? cssLength(value) : value;
218
+ });
219
+
220
+ // A border width with no style renders nothing at all — CSS defaults
221
+ // border-style to none. Someone setting "3px" and seeing no border would
222
+ // reasonably call that broken, so an unstated style means solid.
223
+ BORDER_SIDES.forEach(function(side) {
224
+ var cap = side.charAt(0).toUpperCase() + side.slice(1);
225
+ if (css['border' + cap + 'Width'] && !css['border' + cap + 'Style']) {
226
+ css['border' + cap + 'Style'] = 'solid';
227
+ }
228
+ });
229
+
230
+ return css;
231
+ }
232
+
233
+ /** The common case: layers in, CSS out. */
234
+ export function resolveCellCss(layers) {
235
+ return toCssProperties(resolveStyle(layers));
236
+ }
@@ -0,0 +1,280 @@
1
+ import { useState, useRef, useLayoutEffect, useCallback } from 'react';
2
+ import { measureColumnDemand, allocateColumnWidths, resolvePercentWidths } from './columnWidths.js';
3
+
4
+ /**
5
+ * Content-based column widths — the DOM half. See columnWidths.js for the
6
+ * allocation rule itself; this file only supplies it with real numbers.
7
+ *
8
+ * "Use the font scale" is handled by measuring rather than configuring: the
9
+ * font is read off an actual rendered <td>/<th>, so if a host shrinks the type
10
+ * (a compact theme, say) every column narrows to match with no width setting
11
+ * anywhere. The same goes for cell padding — it's read from the DOM, not
12
+ * duplicated as a constant that would drift the first time the CSS changes.
13
+ */
14
+
15
+ // Sentinel handed to the pure measurer in place of a font string, meaning
16
+ // "whichever of the body's fonts renders this widest" — see measureWidest.
17
+ var BODY_FONTS = '@@body-fonts@@';
18
+ var MAX_BODY_FONTS = 3;
19
+
20
+ var DEFAULTS = {
21
+ sampleRows: 200,
22
+ minWidth: 40,
23
+ maxWidth: 420,
24
+ maxFloor: 220
25
+ };
26
+
27
+ // Canvas measureText is exact for proportional fonts and costs microseconds.
28
+ // One canvas is shared across every table on the page; it's never attached to
29
+ // the document, so it holds a single backing store regardless of table count.
30
+ var sharedCanvas = null;
31
+ function textMeasurer() {
32
+ if (typeof document === 'undefined') return null;
33
+ if (!sharedCanvas) sharedCanvas = document.createElement('canvas');
34
+ var ctx = sharedCanvas.getContext('2d');
35
+ if (!ctx) return null;
36
+ var cache = new Map();
37
+ return function measure(text, font) {
38
+ if (!text) return 0;
39
+ var cacheKey = font + '' + text;
40
+ var hit = cache.get(cacheKey);
41
+ if (hit !== undefined) return hit;
42
+ ctx.font = font;
43
+ var width = ctx.measureText(text).width;
44
+ cache.set(cacheKey, width);
45
+ return width;
46
+ };
47
+ }
48
+
49
+ function fontOf(style) {
50
+ return (style.fontStyle && style.fontStyle !== 'normal' ? style.fontStyle + ' ' : '') +
51
+ (style.fontWeight || 400) + ' ' +
52
+ (style.fontSize || '14px') + ' ' +
53
+ (style.fontFamily || 'sans-serif');
54
+ }
55
+
56
+ /**
57
+ * Hosts routinely rebuild their column array inline on every render, so the
58
+ * effect below cannot use array identity to decide whether to re-measure —
59
+ * it would re-measure forever. This is the real question: has anything that
60
+ * could change a width changed?
61
+ *
62
+ * Cell FORMATTING is deliberately not part of this (the host's columnText is
63
+ * usually a fresh closure each render too). Hosts signal that with sizingKey.
64
+ */
65
+ function signatureOf(columns, containerWidth, sizingKey, overrides) {
66
+ var parts = [containerWidth, columns.length];
67
+ for (var i = 0; i < columns.length; i++) {
68
+ parts.push(columns[i].accessor, columns[i].header || '');
69
+ }
70
+ parts.push(typeof sizingKey === 'object' ? JSON.stringify(sizingKey) : String(sizingKey));
71
+ // Overrides MUST be in here. They change widths, and this signature is the
72
+ // only thing deciding whether the effect below does anything at all — leave
73
+ // them out and setting a width is a no-op with no error and no clue.
74
+ // Stringified rather than identity-compared because a host will rebuild the
75
+ // object inline, exactly as it does the column array.
76
+ parts.push(overrides ? JSON.stringify(overrides) : '');
77
+ return parts.join('');
78
+ }
79
+
80
+ /** Widths that came out identical must not be published — that's a render loop. */
81
+ function sameSizing(a, b) {
82
+ if (!a || !b || a.tableWidth !== b.tableWidth || a.widths.length !== b.widths.length) return false;
83
+ for (var i = 0; i < a.widths.length; i++) {
84
+ if (a.widths[i] !== b.widths[i]) return false;
85
+ }
86
+ return true;
87
+ }
88
+
89
+ function horizontalPadding(el) {
90
+ if (!el) return 0;
91
+ var style = window.getComputedStyle(el);
92
+ return parseFloat(style.paddingLeft || 0) + parseFloat(style.paddingRight || 0) +
93
+ parseFloat(style.borderLeftWidth || 0) + parseFloat(style.borderRightWidth || 0);
94
+ }
95
+
96
+ /**
97
+ * @param {object} opts
98
+ * @param {boolean} opts.enabled - Off by default; hosts opt in
99
+ * @param {Array} opts.data - Rows to sample. The FULL set, not the
100
+ * current page: widths measured per page would jump every time you paged.
101
+ * @param {Array} opts.columns - [{ accessor, header, ... }]
102
+ * @param {Function} [opts.columnText] - (row, column) => displayed string.
103
+ * Defaults to the raw value; hosts that format their cells should pass their
104
+ * formatter, or currency/date columns get measured as bare numbers.
105
+ * @param {*} [opts.sizingKey] - Changing this re-measures. For font or
106
+ * theme changes, which nothing else can observe.
107
+ * @param {object} [opts.columnWidths] - { columnIndex: percent }. Those
108
+ * columns are held at that share of the available width and never measured;
109
+ * everything else divides what's left. PERCENT, not px, so a pin keeps its
110
+ * proportion when the container resizes. BY INDEX, not by key — see
111
+ * pinnedWidth() in columnWidths.js.
112
+ * @param {Function} [opts.onSizing] - Called with the full decision record.
113
+ * @returns {{ scrollRef, tableRef, sizing }}
114
+ */
115
+ export default function useColumnWidths(opts) {
116
+ var enabled = !!opts.enabled;
117
+ var data = opts.data;
118
+ var columns = opts.columns;
119
+ var sizingKey = opts.sizingKey;
120
+ var onSizing = opts.onSizing;
121
+ var sampleRows = opts.sampleRows || DEFAULTS.sampleRows;
122
+ var overrides = opts.columnWidths;
123
+
124
+ var scrollRef = useRef(null);
125
+ var tableRef = useRef(null);
126
+ var lastWidthRef = useRef(0);
127
+ var signatureRef = useRef(null);
128
+ var dataRef = useRef(null);
129
+ var sizingRef = useRef(null);
130
+ var [containerWidth, setContainerWidth] = useState(0);
131
+ var [sizing, setSizing] = useState(null);
132
+
133
+ var columnText = opts.columnText;
134
+ var textOf = useCallback(function(row, column) {
135
+ if (columnText) return columnText(row, column);
136
+ var value = row ? row[column.accessor] : null;
137
+ return value === null || value === undefined ? '' : String(value);
138
+ }, [columnText]);
139
+
140
+ // ── Track the available width ──
141
+ // clientWidth of the scroll port, deliberately: overflow-x means a horizontal
142
+ // scrollbar eats height, not width, so this number can't oscillate with the
143
+ // scrollbar it produces.
144
+ useLayoutEffect(function() {
145
+ if (!enabled) return undefined;
146
+ var el = scrollRef.current;
147
+ if (!el || typeof ResizeObserver === 'undefined') return undefined;
148
+
149
+ function read() {
150
+ var width = el.clientWidth;
151
+ // Sub-pixel churn from zoom/fractional layout would otherwise re-run the
152
+ // whole measure pass on every frame during a window drag.
153
+ if (Math.abs(width - lastWidthRef.current) < 2) return;
154
+ lastWidthRef.current = width;
155
+ setContainerWidth(width);
156
+ }
157
+
158
+ read();
159
+ var observer = new ResizeObserver(read);
160
+ observer.observe(el);
161
+ return function() { observer.disconnect(); };
162
+ }, [enabled]);
163
+
164
+ // ── Measure and allocate ──
165
+ useLayoutEffect(function() {
166
+ if (!enabled || !containerWidth || !columns || !columns.length) {
167
+ if (sizingRef.current) { sizingRef.current = null; setSizing(null); }
168
+ return;
169
+ }
170
+ var measure = textMeasurer();
171
+ var table = tableRef.current;
172
+ if (!measure || !table) return;
173
+
174
+ // Cheap bail-out before the expensive part: measuring 200 rows across
175
+ // every column on a render that changed nothing is pure waste.
176
+ var signature = signatureOf(columns, containerWidth, sizingKey, overrides);
177
+ if (signature === signatureRef.current && data === dataRef.current && sizingRef.current) return;
178
+ signatureRef.current = signature;
179
+ dataRef.current = data;
180
+
181
+ // Fonts and padding come from real cells so this stays in step with the
182
+ // stylesheet automatically. Before the first row renders there's no <td>,
183
+ // so the header cell stands in — one pass slightly off, corrected as soon
184
+ // as rows arrive and the effect re-runs.
185
+ var bodyCell = table.querySelector('tbody .xeplr-table-td');
186
+ var headCell = table.querySelector('thead .xeplr-table-header-cell');
187
+ var headerFont = fontOf(window.getComputedStyle(headCell || bodyCell || table));
188
+
189
+ // Rows are not guaranteed to share a font. A host that styles a total row
190
+ // bold, or bands alternate rows differently, makes some rows wider than
191
+ // others at the same character count — sizing off row one alone would
192
+ // leave exactly those rows wrapping. So collect the distinct fonts in play
193
+ // and give every column enough width for the widest of them.
194
+ var bodyFonts = [];
195
+ var sampleCells = table.querySelectorAll('tbody tr .xeplr-table-td:first-child');
196
+ for (var s = 0; s < sampleCells.length && bodyFonts.length < MAX_BODY_FONTS; s++) {
197
+ var font = fontOf(window.getComputedStyle(sampleCells[s]));
198
+ if (bodyFonts.indexOf(font) === -1) bodyFonts.push(font);
199
+ }
200
+ if (!bodyFonts.length) bodyFonts.push(headerFont);
201
+
202
+ // measureColumnDemand deals in one font per call, which keeps it pure and
203
+ // trivially testable; the multi-font case is resolved here instead.
204
+ var measureWidest = function(text, font) {
205
+ if (font !== BODY_FONTS) return measure(text, font);
206
+ var widest = 0;
207
+ for (var f = 0; f < bodyFonts.length; f++) {
208
+ var width = measure(text, bodyFonts[f]);
209
+ if (width > widest) widest = width;
210
+ }
211
+ return widest;
212
+ };
213
+
214
+ // Header chrome: the label shares its row with the sort caret and the
215
+ // filter trigger, so the text needs less room than the cell does.
216
+ var headerPadding = horizontalPadding(headCell);
217
+ var trigger = table.querySelector('thead .xeplr-table-filter-trigger');
218
+ if (trigger) headerPadding += trigger.offsetWidth + 4;
219
+ else headerPadding += 16; // sort caret
220
+
221
+ // The table's own structural columns — expand arrow, select checkbox, row
222
+ // actions — aren't content columns and don't compete for space. Their
223
+ // width is reserved off the top, so the content columns divide what's
224
+ // actually left. Read from the DOM because two of them are sized in CSS
225
+ // and the third shrink-wraps whatever buttons the host supplied.
226
+ var extraWidths = {};
227
+ var reserved = 0;
228
+ ['expand', 'checkbox', 'actions'].forEach(function(kind) {
229
+ var el = table.querySelector('thead .xeplr-table-th-' + kind);
230
+ if (!el) return;
231
+ var width = Math.ceil(el.offsetWidth);
232
+ extraWidths[kind] = width;
233
+ reserved += width;
234
+ });
235
+
236
+ var rows = data && data.length > sampleRows ? data.slice(0, sampleRows) : (data || []);
237
+
238
+ // Percentages resolve against `available` — the container minus the
239
+ // structural columns — so 100 means "all the room the columns have".
240
+ // The conversion itself is pure and lives in columnWidths.js; this is
241
+ // just the only place that knows what `available` is.
242
+ //
243
+ // A percentage is also why this survives a resize: containerWidth is in
244
+ // the signature, so the effect re-runs and a pinned column re-resolves to
245
+ // the right px rather than staying at yesterday's.
246
+ var available = containerWidth - reserved;
247
+ var pxOverrides = resolvePercentWidths(overrides, available);
248
+
249
+ var demands = measureColumnDemand({
250
+ columns: columns,
251
+ rows: rows,
252
+ textOf: textOf,
253
+ headerOf: function(column) { return column.header || column.accessor; },
254
+ keyOf: function(column) { return column.accessor; },
255
+ measure: measureWidest,
256
+ bodyFont: BODY_FONTS,
257
+ headerFont: headerFont,
258
+ cellPadding: horizontalPadding(bodyCell) || 25,
259
+ headerPadding: headerPadding,
260
+ minWidth: opts.minColumnWidth || DEFAULTS.minWidth,
261
+ maxWidth: opts.maxColumnWidth || DEFAULTS.maxWidth,
262
+ maxFloor: opts.maxFloorWidth || DEFAULTS.maxFloor,
263
+ overrides: pxOverrides
264
+ });
265
+
266
+ var next = allocateColumnWidths(demands, containerWidth - reserved);
267
+ next.extraWidths = extraWidths;
268
+ next.tableWidth += reserved;
269
+
270
+ if (sameSizing(next, sizingRef.current)) return;
271
+ sizingRef.current = next;
272
+ setSizing(next);
273
+ if (onSizing) onSizing(next);
274
+ // State is tracked through refs above, so it is deliberately absent here.
275
+ // eslint-disable-next-line react-hooks/exhaustive-deps
276
+ }, [enabled, containerWidth, data, columns, textOf, sampleRows, sizingKey, overrides,
277
+ opts.minColumnWidth, opts.maxColumnWidth, opts.maxFloorWidth]);
278
+
279
+ return { scrollRef: scrollRef, tableRef: tableRef, sizing: enabled ? sizing : null };
280
+ }