@jarenjs/collection 0.83.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,3 @@
1
+ /** Public headless collection geometry and vnode projection. */
2
+ export { createCollection } from './collection.js';
3
+ export { createCollectionInteraction } from './interaction.js';
@@ -0,0 +1,92 @@
1
+ /** Identity-based interaction. Range intent stores endpoint keys in a declared query/snapshot. */
2
+ /** @param {any} options */
3
+ export declare function createCollectionInteraction(options: any): {
4
+ state: () => {
5
+ focus: any;
6
+ pending: any;
7
+ selection: {
8
+ mode: string;
9
+ keys: never[];
10
+ ranges: never[];
11
+ exclusions: never[];
12
+ query: any;
13
+ snapshot: any;
14
+ };
15
+ };
16
+ selected: (key: any) => boolean;
17
+ focusIndex: (index: any, column?: number) => {
18
+ state: string;
19
+ reason: string;
20
+ focus?: undefined;
21
+ index?: undefined;
22
+ column?: undefined;
23
+ } | {
24
+ reason?: undefined;
25
+ state: string;
26
+ focus: null;
27
+ index?: undefined;
28
+ column?: undefined;
29
+ } | {
30
+ reason?: undefined;
31
+ focus?: undefined;
32
+ state: string;
33
+ index: any;
34
+ column: number;
35
+ } | {
36
+ reason?: undefined;
37
+ focus?: undefined;
38
+ key: string;
39
+ index: any;
40
+ column: number;
41
+ state: string;
42
+ };
43
+ cancelPending(): void;
44
+ update(next: any): {
45
+ state: string;
46
+ reason: string;
47
+ focus?: undefined;
48
+ index?: undefined;
49
+ column?: undefined;
50
+ } | {
51
+ reason?: undefined;
52
+ state: string;
53
+ focus: null;
54
+ index?: undefined;
55
+ column?: undefined;
56
+ } | {
57
+ reason?: undefined;
58
+ focus?: undefined;
59
+ state: string;
60
+ index: any;
61
+ column: number;
62
+ } | {
63
+ state: string;
64
+ };
65
+ toggle: (key: any) => {
66
+ state: string;
67
+ reason: string;
68
+ } | {
69
+ reason?: undefined;
70
+ state: string;
71
+ };
72
+ selectAll(): void;
73
+ clear(): void;
74
+ restore(intent: any): {
75
+ state: string;
76
+ reason: string;
77
+ } | {
78
+ reason?: undefined;
79
+ state: string;
80
+ };
81
+ key(event: any): {
82
+ state: string;
83
+ reason: string;
84
+ } | {
85
+ reason?: undefined;
86
+ state: string;
87
+ } | {
88
+ state: string;
89
+ key: any;
90
+ column: any;
91
+ };
92
+ };
@@ -0,0 +1,110 @@
1
+ # Collection contract
2
+
3
+ ## Engine and rendering
4
+
5
+ `createCollection(options)` from `@jarenjs/collection` needs `count`, `keyAt(index)`,
6
+ `getItem(index)` and `renderCell(row,column,index)`. Keys are unique stable strings;
7
+ `null` denotes an unloaded placeholder. Duplicate loaded keys throw `TypeError`.
8
+ Accessors visit only the credited viewport and pins. The engine imports core/view
9
+ only and has no DOM, app, database or worker import.
10
+
11
+ Geometry options include `rowSize` (44), `columnSize` (160), `columnCount` (1),
12
+ `overscan` (2), `columnOverscan` (1), `maxRows` (128), `maxColumns` (32) and
13
+ `maxCells` (840). Row and column windows are independent. `rowPins`, `columnPins`,
14
+ `rowPinBudget` (2) and `columnPinBudget` (2) include focus pins in the same limits.
15
+ Pinned rows/columns remain at the leading viewport edge. With base windows R/C,
16
+ row/column mounts are at most R+rowPins and C+columnPins; all cells must also fit
17
+ `maxCells`. Credit exhaustion returns an empty explicit refusal, never a partially
18
+ rendered success. Hidden viewports have no rows, cells or focus pins.
19
+
20
+ The row and column axes independently accept `maxMeasurements`, `measurementBytes`,
21
+ `maxColumnMeasurements` and `columnMeasurementBytes`. Eviction reverts a size to
22
+ its estimate. `measure(index,key,size,axis='row')` preserves the stored keyed row
23
+ anchor, including compensation for evicted measurements. `update(next)` reindexes
24
+ retained measurements via `indexOf(key)`; it must run after the host changes source
25
+ order. Changing `rowSize` or `columnSize` invalidates corresponding measurements.
26
+
27
+ `viewport({top,left,width,height})`, `layout()`, `view(interaction,id)`, `stats()`,
28
+ `position()`, `options()`, `pin(rows,columns)`, `snapshot()`, `restore(anchor)` and
29
+ idempotent `dispose()` form the controller API. `scrollToOffset`, `scrollToIndex`
30
+ and `scrollToKey` return outcomes; missing key lookup returns `unsupported-seek`.
31
+ The optional column argument of `scrollToIndex` scrolls both axes. Indices locate
32
+ rows in the current query and are never persisted as entity identity. A missing
33
+ anchor key falls back to its clamped previous position; a changed query starts at
34
+ zero. A new snapshot re-resolves the stable key in the same query.
35
+
36
+ ## DOM ownership and accessibility
37
+
38
+ Import `mountCollection`, `createCollectionWidget` and `mountProviderCollection`
39
+ from `@jarenjs/collection/component`, and link `@jarenjs/collection/styles/collection.css`.
40
+ `mountCollection(host, options)` owns its element, scroll listener, keyboard/click/
41
+ focus/composition handlers, resize observers and frames. `requestFrame`,
42
+ `cancelFrame` and `observe(element, callback) => unsubscribe` can be injected by the
43
+ host. `dispose` is adapted to the existing widget `unmount` hook, preserving the
44
+ renderer lifecycle. Failed mounts clean acquired resources too.
45
+
46
+ ```js
47
+ import { createCollectionWidget } from '@jarenjs/collection/component';
48
+ const widgets = { collection: createCollectionWidget({
49
+ count: rows.length, keyAt: (i) => rows[i].id, indexOf: (key) => index.get(key) ?? -1,
50
+ getItem: (i) => rows[i], renderCell: (row, column) => columns[column].render(row),
51
+ columnCount: columns.length, label: 'Catalog'
52
+ }) };
53
+ const vnode = ['jaren-widget', { name: 'collection', props: { height: 440 } }];
54
+ ```
55
+
56
+ The DOM adapter's conservative `maxExtent` defaults to 8,000,000 CSS pixels per
57
+ axis; larger extents return `error / scroll-extent` before reaching CSS layout.
58
+ The pure engine can compute larger ranges; this is an explicit browser refusal.
59
+ `direction: 'rtl'` normalizes negative browser scroll offsets and aligns columns
60
+ with logical inline positions. `height` controls the viewport; its host controls
61
+ width. `measured:true` observes content; `measureRow(element,index)` can supply
62
+ host-specific height measurement. The default measures the tallest cell content
63
+ plus cell padding. `maxMeasurementWork` (64) bounds measured rows per pass; resize
64
+ notifications coalesce into one frame. The same keyed renderer handles controlled
65
+ inputs, so scroll and measurement patches preserve node identity and selection.
66
+
67
+ `role` is `grid` by default or `listbox`. Grid cells carry logical row/column
68
+ indices; list options carry logical position/set size. `totalKnown:false` exposes
69
+ unknown total as -1 rather than loaded count. `label` names the collection.
70
+ `createCollectionInteraction(options)` is the headless counterpart: arrows,
71
+ Home/End (Ctrl for the whole grid), PageUp/Down, Space selection, Enter activation
72
+ and Escape return focus. Arrow direction follows RTL. Editing targets and
73
+ composition bypass collection shortcuts. Browser-native editing keys retain their
74
+ normal caret behavior.
75
+
76
+ `focusIndex` realizes a loaded key or records a pending transient index while
77
+ retaining existing focus. The DOM exposes `aria-activedescendant` only after the
78
+ referenced element exists. Loading exposes `aria-busy`; source refusal clears
79
+ pending realization. `removedKeys` tells interaction to resolve a removal fallback.
80
+ `onRealize`, `onActivate`, `returnFocus`, `onIntent` and `onChange` are host hooks;
81
+ only bounded observations and JSON intent need enter app state.
82
+
83
+ Selection contains stable `keys`, or query/snapshot-scoped `mode:'all'` with
84
+ `exclusions`, or one range with stable `fromKey`/`toKey` endpoints. Shift ranges
85
+ survive asynchronous endpoint realization. `maxSelectedKeys` (4096) bounds explicit
86
+ keys/exclusions. `state`, `selected`, `toggle`, `selectAll`, `clear`, `restore`,
87
+ `cancelPending` and `update` expose the model. Range membership for rendered rows
88
+ uses the host's current key lookup; exports resolve endpoints over the full ordered
89
+ snapshot. Key selection survives eviction and query changes; scoped all/range
90
+ selection cannot be exported against another identity. Authoritative writes must
91
+ still recheck membership/revision.
92
+
93
+ ## Data and complete output
94
+
95
+ `mountProviderCollection(host,coordinator,options)` uses exactly the same controller
96
+ and interaction. The host injects `createCollectionCoordinator(provider)` from
97
+ `@jarenjs/app`. The component requests missing visible ranges, publishes loading/
98
+ refusal state, and preserves edit pages within the coordinator's existing credits.
99
+ Eviction cannot destroy an active editor; if its pinned page prevents admission,
100
+ the coordinator returns `pinned-page-credits` until the host releases the pin.
101
+
102
+ The mounted provider handle exposes `mounted`, `coordinator`, `next`, `snapshot`,
103
+ `output`, `print` and async idempotent `dispose`. `next` is the continuation
104
+ operation for unknown-total sequential sources. Printing and exporting use the same
105
+ complete snapshot stream, with the host supplying a transactional sink; neither
106
+ reads mounted DOM. The source must advertise `completeExport`. See the normative
107
+ [app provider contract](../../../packages/app/docs/COLLECTION-PROVIDER.md).
108
+
109
+ Actual assistive technology, physical touch and native OS IME qualification remain
110
+ separate from automated browser evidence; see [measurements](MEASUREMENTS.md).
@@ -0,0 +1,62 @@
1
+ # Collection evidence
2
+
3
+ The retained reference fixtures and budgets remain unchanged. The installed
4
+ Node/Bun consumer runs both fixed and measured profiles using only public package
5
+ exports, without a retained virtualizer dependency. The reference remains in the
6
+ benchmark workspace for differential checks and future downstream qualification.
7
+
8
+ <!--fact:collection.measurements-->
9
+
10
+ Measured on v24.19.0, linux/x64, AMD Ryzen 9 5900HX with Radeon Graphics.
11
+
12
+ | Consumer | Rows | Reference range ms | Native range/view/interaction p95 ms | Cells | Cached rows / bytes | Measurements / accounted bytes | Heap MiB | Teardown ms |
13
+ |---|---:|---:|---:|---:|---|---|---:|---:|
14
+ | catalog | 10000 | 0.020 | 1.450 | 170 | 256 / 9732 | 256 / 8192 | 19.49 | 0.177 |
15
+ | archive-stock | 75000 | 8.080 | 0.307 | 160 | 256 / 10244 | 256 / 8448 | 92.43 | 0.047 |
16
+
17
+ The component and coordinator browser bundle is 16815 gzip bytes. Reference range calls do less work than native vnode and interaction calls; the comparison deliberately publishes that cost rather than claiming equal workloads.
18
+
19
+ <!--/fact-->
20
+
21
+ The Node measurements include range calculation, vnode projection and headless
22
+ interaction. The reference timing is range-only and is therefore cheaper work.
23
+ The historical adoption report also includes other engines; its heap and RSS
24
+ cannot be treated as isolated virtualizer memory. Measurement bytes account for
25
+ UTF-8 keys and numeric payloads; runtime heap separately includes Map/array overhead.
26
+
27
+ The browser matrix covers keyed anchor preservation, both axes, RTL, pinned rows,
28
+ resize, CSS zoom, synthetic touch events, keyboard selection, pending realization,
29
+ editing/composition events, extent limits, source examples and disposal. The
30
+ near-ceiling fixture uses lazy row access. It never allocates the entire logical
31
+ source in the browser. Browser script timings cover synchronous DOM reconciliation;
32
+ paint scheduling is separate. Physical touch devices, actual assistive technology,
33
+ native OS IME sessions and real downstream cutover remain unqualified.
34
+
35
+ <!--fact:collection.browser-->
36
+
37
+ Linux container, Node v24.20.0, 4 browser workers. Each sample set contains 40 synchronous viewport changes after the near-ceiling check.
38
+
39
+ | Engine / version | DOM interaction p95 ms | Earlier full-matrix p95 ms | Peak cells | CSS extent / final offset | Remaining resources |
40
+ |---|---:|---:|---:|---|---:|
41
+ | chromium 149.0.7827.55 | 10.30 | 8.60 | 170 | 7920000 / 7919560 | 0 |
42
+ | firefox 151.0 | 11.00 | 12.00 | 170 | 7920000 / 7919560 | 0 |
43
+ | webkit 26.5 | 15.00 | 17.00 | 170 | 7920000 / 7919560 | 0 |
44
+
45
+ The earlier loaded WebKit sample exceeded the fixed-profile 16 ms target. Browser latency varies with concurrent load; these measurements do not establish a universal frame-time guarantee.
46
+
47
+ <!--/fact-->
48
+
49
+ Reproduce the portable and Node measurements with `npm run test:packed` and
50
+ `npm run benchmark:collection`. After `npm run website:build`, run
51
+ `COLLECTION_MEASURE=1 npx playwright test -c packages/website/playwright.config.js packages/website/e2e/collection.spec.js packages/website/e2e/lifecycle.spec.js --workers=4`
52
+ on the qualified browser host, then `npm run docs:derive`. Ordinary gates omit the
53
+ measurement environment variable and leave committed timings unchanged. The earlier
54
+ full-matrix sample is retained separately so rerunning does not erase a measured loss.
55
+ True peak heap remains unmeasured; sampled heap and process peak RSS are reported.
56
+
57
+ The browser's conservative CSS extent ceiling refuses larger extents before they
58
+ reach layout. The pure range engine supports larger logical counts. Source-backed
59
+ sequential providers retain unknown totals and refuse index/key jumps and complete
60
+ export. Complete export is qualified for immutable resident array sources and
61
+ bounded complete SQLite resident snapshots. The browser download example uses a
62
+ finite output spool and refuses an oversized export before making a download.
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@jarenjs/collection",
3
+ "private": false,
4
+ "version": "0.83.2",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./dist/types/index.d.ts",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types/index.d.ts",
12
+ "default": "./src/index.js"
13
+ },
14
+ "./component": {
15
+ "types": "./dist/types/component/index.d.ts",
16
+ "default": "./src/component/index.js"
17
+ },
18
+ "./package.json": "./package.json",
19
+ "./styles/collection.css": "./styles/collection.css"
20
+ },
21
+ "files": [
22
+ "src/",
23
+ "dist/types/",
24
+ "styles/",
25
+ "docs/",
26
+ "ARCHITECTURE.md"
27
+ ],
28
+ "description": "Bounded virtual lists and grids with keyed interaction and injected data providers",
29
+ "author": "joham",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/jklarenbeek/jarenjs.git",
33
+ "directory": "components/collection"
34
+ },
35
+ "license": "MIT",
36
+ "engines": {
37
+ "node": ">=24"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org/"
42
+ },
43
+ "keywords": [
44
+ "jaren",
45
+ "collection",
46
+ "virtual",
47
+ "grid"
48
+ ],
49
+ "scripts": {
50
+ "build": "npm run build:types",
51
+ "build:types": "tsc -p tsconfig.json",
52
+ "prepack": "npm run build:types"
53
+ },
54
+ "dependencies": {
55
+ "@jarenjs/core": "^0.83.2",
56
+ "@jarenjs/view": "^0.83.2",
57
+ "@jarenjs/app": "^0.83.2"
58
+ }
59
+ }
@@ -0,0 +1,134 @@
1
+ //@ts-check
2
+ import { createVirtualAxis, virtualIndices } from '@jarenjs/core/virtual';
3
+
4
+ /** Stable DOM identity; JSON escaping also supports lone surrogate code units in JSON keys.
5
+ * @param {string} id @param {string} key @param {number} [column] */
6
+ export function collectionItemId(id, key, column) {
7
+ return `${id}-r-${encodeURIComponent(JSON.stringify(key))}${column === undefined ? '' : `-c-${column}`}`;
8
+ }
9
+
10
+ /**
11
+ * Create a source-independent collection. Accessors run only for credited mounted rows.
12
+ * Measurements and pins have their own finite credits. Oversized DOM extents refuse explicitly.
13
+ * @param {any} options
14
+ */
15
+ export function createCollection(options) {
16
+ let config = { rowSize: 44, overscan: 2, columnCount: 1, columnSize: 160, columnOverscan: 1,
17
+ maxRows: 128, maxColumns: 32, maxCells: 840, rowPinBudget: 2, columnPinBudget: 2,
18
+ maxExtent: Number.MAX_SAFE_INTEGER, query: '', snapshot: '', ...options };
19
+ for (const name of ['maxRows', 'maxColumns', 'maxCells', 'rowPinBudget', 'columnPinBudget'])
20
+ if (!Number.isSafeInteger(config[name]) || config[name] < 0) throw new RangeError(`Invalid ${name}`);
21
+ const rowAxis = createVirtualAxis({ count: config.count, estimateSize: config.rowSize,
22
+ maxMeasurements: config.maxMeasurements, maxBytes: config.measurementBytes });
23
+ const columnAxis = createVirtualAxis({ count: config.columnCount, estimateSize: config.columnSize,
24
+ maxMeasurements: config.maxColumnMeasurements, maxBytes: config.columnMeasurementBytes });
25
+ let viewport = { top: 0, left: 0, width: 0, height: 0 };
26
+ let anchor = null, disposed = false;
27
+ let focusPins = { rows: [], columns: [] };
28
+ const keyAt = (index) => config.keyAt(index);
29
+ const refusal = (state, reason) => ({ state, reason, rows: [], columns: [] });
30
+ function layout() {
31
+ if (disposed) return refusal('error', 'disposed');
32
+ const visible = viewport.height > 0 && viewport.width > 0;
33
+ const rows = rowAxis.range({ viewport: visible ? viewport.height : 0, offset: viewport.top, overscan: config.overscan });
34
+ const columns = columnAxis.range({ viewport: visible ? viewport.width : 0, offset: viewport.left, overscan: config.columnOverscan });
35
+ if (rows.extent > config.maxExtent || columns.extent > config.maxExtent) return refusal('error', 'scroll-extent');
36
+ if (rows.end - rows.start > config.maxRows || columns.end - columns.start > config.maxColumns)
37
+ return refusal('budget-exhausted', 'dom-credits');
38
+ const rowIndices = virtualIndices(rows, visible ? [...(config.rowPins ?? []), ...focusPins.rows] : [], config.count, config.rowPinBudget);
39
+ const columnIndices = virtualIndices(columns, visible ? [...(config.columnPins ?? []), ...focusPins.columns] : [], config.columnCount, config.columnPinBudget);
40
+ if (rowIndices.state !== 'ready' || columnIndices.state !== 'ready') return refusal('budget-exhausted', 'pin-credits');
41
+ if (rowIndices.indices.length * columnIndices.indices.length > config.maxCells) return refusal('budget-exhausted', 'dom-credits');
42
+ const seen = new Set();
43
+ const mounted = rowIndices.indices.map((index) => {
44
+ const key = keyAt(index);
45
+ if (key != null && (typeof key !== 'string' || seen.has(key))) throw new TypeError('Collection keys must be unique strings');
46
+ if (key != null) seen.add(key);
47
+ return { index, key, start: rowAxis.position(index), size: rowAxis.size(index), pinned: (config.rowPins ?? []).includes(index) };
48
+ });
49
+ return { state: 'ready', rowRange: rows, columnRange: columns, rows: mounted,
50
+ columns: columnIndices.indices.map((index) => ({ index, start: columnAxis.position(index), size: columnAxis.size(index),
51
+ pinned: (config.columnPins ?? []).includes(index) })) };
52
+ }
53
+ function saveAnchor() { anchor = rowAxis.anchor(viewport.top, keyAt, config.query); }
54
+ function restoreAnchor() {
55
+ const restored = rowAxis.restore(anchor, config.indexOf, config.query);
56
+ viewport.top = Math.min(restored.offset, Math.max(0, rowAxis.extent() - viewport.height));
57
+ return viewport.top;
58
+ }
59
+ function scrollToOffset(offset) {
60
+ if (disposed) return { state: 'error', reason: 'disposed' };
61
+ viewport.top = offset;
62
+ const result = layout();
63
+ if (result.state !== 'ready') return result;
64
+ viewport.top = result.rowRange.offset; saveAnchor();
65
+ return { state: 'ready', offset: viewport.top, left: viewport.left };
66
+ }
67
+ return {
68
+ rowAxis, columnAxis, layout,
69
+ options() { return config; },
70
+ position() { return { ...viewport }; },
71
+ viewport(next) { viewport = { ...viewport, ...next }; const result = layout();
72
+ if (result.state === 'ready') { viewport.top = result.rowRange.offset; viewport.left = result.columnRange.offset; saveAnchor(); }
73
+ return result; },
74
+ update(next) {
75
+ config = { ...config, ...next };
76
+ rowAxis.update({ count: config.count, estimateSize: config.rowSize, indexOf: config.indexOf });
77
+ columnAxis.update({ count: config.columnCount, estimateSize: config.columnSize });
78
+ restoreAnchor(); saveAnchor(); return layout();
79
+ },
80
+ measure(index, key, size, axis = 'row') {
81
+ const engine = axis === 'column' ? columnAxis : rowAxis;
82
+ const result = engine.measure(index, key, size);
83
+ if (result.state === 'ready') { restoreAnchor(); saveAnchor(); }
84
+ return { ...result, offset: viewport.top };
85
+ },
86
+ pin(rows = [], columns = []) { focusPins = { rows, columns }; return layout(); },
87
+ restore(value) { anchor = value; const offset = restoreAnchor(); saveAnchor(); return scrollToOffset(offset); },
88
+ snapshot() { return anchor ? { ...anchor, snapshot: config.snapshot } : null; },
89
+ scrollToOffset,
90
+ /** @param {number} index @param {number} [column] */
91
+ scrollToIndex(index, column) {
92
+ if (!Number.isSafeInteger(index) || index < 0 || index >= config.count) return { state: 'error', reason: 'invalid-index' };
93
+ if (column !== undefined) viewport.left = columnAxis.position(Math.max(0, Math.min(config.columnCount - 1, column)));
94
+ return scrollToOffset(rowAxis.position(index));
95
+ },
96
+ scrollToKey(key) {
97
+ if (!config.indexOf) return { state: 'error', reason: 'unsupported-seek' };
98
+ return this.scrollToIndex(config.indexOf(key));
99
+ },
100
+ view(interaction = null, id = 'collection') {
101
+ const result = layout();
102
+ const grid = config.role !== 'listbox';
103
+ const selection = (key) => interaction?.selected(key) ?? false;
104
+ const active = interaction?.state().focus;
105
+ const rows = result.rows.map((row) => {
106
+ const rowKey = JSON.stringify(row.key == null ? [0, row.index] : [1, row.key]);
107
+ const rowId = row.key == null ? `${id}-p-${row.index}` : collectionItemId(id, row.key);
108
+ const rowTop = row.pinned ? Math.max(row.start, viewport.top) : row.start;
109
+ return ['div', { key: rowKey, 'data-row': row.index, 'data-key': row.key,
110
+ role: grid ? 'row' : 'option', id: grid ? undefined : rowId,
111
+ 'aria-rowindex': grid ? row.index + 1 : undefined,
112
+ 'aria-posinset': grid ? undefined : row.index + 1,
113
+ 'aria-setsize': grid ? undefined : config.totalKnown === false ? -1 : config.count,
114
+ 'aria-selected': grid ? undefined : selection(row.key), class: row.pinned ? 'jc-row jc-pin' : 'jc-row', style: {
115
+ position: 'absolute', top: `${rowTop}px`, height: `${row.size}px`, width: '100%', zIndex: row.pinned ? 2 : 0 } },
116
+ ...result.columns.map((column) => ['div', { key: column.index, class: column.pinned ? 'jc-cell jc-pin' : 'jc-cell',
117
+ role: grid ? 'gridcell' : undefined,
118
+ id: grid ? `${rowId}-c-${column.index}` : undefined,
119
+ 'data-column': column.index, 'aria-colindex': grid ? column.index + 1 : undefined,
120
+ 'aria-selected': grid ? selection(row.key) : undefined,
121
+ 'data-active': active?.key === row.key && active?.column === column.index,
122
+ style: { position: 'absolute', [config.direction === 'rtl' ? 'right' : 'left']:
123
+ `${column.pinned ? Math.max(column.start, viewport.left) : column.start}px`,
124
+ width: `${column.size}px`, height: '100%', zIndex: column.pinned ? 1 : 0 } },
125
+ ['div', { class: 'jc-content' }, row.key == null ? (config.loadingLabel ?? 'Loading…') : config.renderCell(config.getItem(row.index), column.index, row.index)]])];
126
+ });
127
+ return ['div', { class: 'jc-surface', style: { position: 'relative', height: `${result.rowRange?.extent ?? 0}px`,
128
+ width: `${result.columnRange?.extent ?? 0}px` } }, ...rows];
129
+ },
130
+ stats() { const result = layout(); return { rows: result.rows.length, columns: result.columns.length,
131
+ cells: result.rows.length * result.columns.length, rowMeasurements: rowAxis.stats(), columnMeasurements: columnAxis.stats() }; },
132
+ dispose() { disposed = true; rowAxis.dispose(); columnAxis.dispose(); anchor = null; focusPins = { rows: [], columns: [] }; },
133
+ };
134
+ }