concertina 1.0.0 → 1.1.0
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.
- package/README.md +26 -0
- package/dist/core/data-worker.js +2 -1
- package/dist/core/index.cjs +22 -11
- package/dist/core/index.d.cts +4 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +22 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -609,6 +609,32 @@ computedWidth = fixedWidth ?? (maxContentChars × charWidthHint + CELL_H_PADDING
|
|
|
609
609
|
|
|
610
610
|
`CELL_H_PADDING` is 16 px. A 14 px monospace font uses `charWidthHint: 8`. Widths are resolved once at `INIT` and re-sent with every `WINDOW_UPDATE`.
|
|
611
611
|
|
|
612
|
+
### DOM-traced pitch
|
|
613
|
+
|
|
614
|
+
The worker computes a `rowHeight` from `rowHeightHint` at `INIT` time, but real rows may be taller due to padding, borders, or font metrics that differ from the hint. **Pitch** lets the main thread override the worker's row height with a DOM-measured value.
|
|
615
|
+
|
|
616
|
+
```tsx
|
|
617
|
+
// Measure actual row height from the DOM (e.g. from a ghost/warmup row)
|
|
618
|
+
const [measuredPitch, setMeasuredPitch] = useState(0);
|
|
619
|
+
const ghostRef = useCallback((el) => {
|
|
620
|
+
if (el) setMeasuredPitch(el.getBoundingClientRect().height);
|
|
621
|
+
}, []);
|
|
622
|
+
|
|
623
|
+
// Push to the store — VirtualChamber and scroll handler read it automatically
|
|
624
|
+
useEffect(() => {
|
|
625
|
+
if (measuredPitch > 0) store.setPitch(measuredPitch);
|
|
626
|
+
}, [measuredPitch, store]);
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
When `pitch > 0`, VirtualChamber uses it instead of `layout.rowHeight` for:
|
|
630
|
+
- **Spacer height**: `totalRows × pitch` (scrollbar range)
|
|
631
|
+
- **Pool node height**: each pool `<div>` gets `height: pitch`
|
|
632
|
+
- **translateY**: row positioning uses `rowIndex × pitch`
|
|
633
|
+
- **Scroll handler**: `SET_WINDOW` start row = `Math.floor(scrollTop / pitch)`
|
|
634
|
+
- **scrollToRow**: programmatic scroll uses `row × pitch`
|
|
635
|
+
|
|
636
|
+
When `pitch` is `0` (the default), all math falls back to the worker's `layout.rowHeight`. This means existing code that doesn't call `setPitch` continues to work unchanged.
|
|
637
|
+
|
|
612
638
|
---
|
|
613
639
|
|
|
614
640
|
## License
|
package/dist/core/data-worker.js
CHANGED
package/dist/core/index.cjs
CHANGED
|
@@ -74,7 +74,8 @@ var INITIAL_STORE_STATE = {
|
|
|
74
74
|
window: null,
|
|
75
75
|
backpressure: { strategy: "NOMINAL", queueDepth: 0, avgRenderMs: asMs(0) },
|
|
76
76
|
totalRows: 0,
|
|
77
|
-
error: null
|
|
77
|
+
error: null,
|
|
78
|
+
pitch: 0
|
|
78
79
|
};
|
|
79
80
|
|
|
80
81
|
// src/core/atomic-store.ts
|
|
@@ -127,6 +128,10 @@ var AtomicStore = class {
|
|
|
127
128
|
setStatus(status, error) {
|
|
128
129
|
this.merge({ status, error: error ?? this.state.error });
|
|
129
130
|
}
|
|
131
|
+
/** Set DOM-traced row pitch. 0 resets to Worker-computed rowHeight. */
|
|
132
|
+
setPitch(px) {
|
|
133
|
+
if (this.state.pitch !== px) this.merge({ pitch: px });
|
|
134
|
+
}
|
|
130
135
|
merge(patch) {
|
|
131
136
|
const next = { ...this.state, ...patch };
|
|
132
137
|
this.state = next;
|
|
@@ -228,14 +233,16 @@ function useStabilityOrchestrator(options) {
|
|
|
228
233
|
const el = containerRef_internal.current;
|
|
229
234
|
const lay = layoutRef.current;
|
|
230
235
|
if (!el || !lay || lay.rowHeight === 0) return;
|
|
231
|
-
const
|
|
236
|
+
const p = store.getState().pitch;
|
|
237
|
+
const rh = p > 0 ? p : lay.rowHeight;
|
|
238
|
+
const startRow = asRowIndex(Math.floor(el.scrollTop / rh));
|
|
232
239
|
const rowCount = lay.viewportRows + overscanRows * 2;
|
|
233
240
|
workerRef.current?.postMessage({
|
|
234
241
|
type: "SET_WINDOW",
|
|
235
242
|
startRow,
|
|
236
243
|
rowCount
|
|
237
244
|
});
|
|
238
|
-
}, [overscanRows]);
|
|
245
|
+
}, [overscanRows, store]);
|
|
239
246
|
const containerRef = (0, import_react2.useCallback)(
|
|
240
247
|
(el) => {
|
|
241
248
|
if (containerRef_internal.current) {
|
|
@@ -322,13 +329,15 @@ function useStabilityOrchestrator(options) {
|
|
|
322
329
|
const el = containerRef_internal.current;
|
|
323
330
|
const lay = layoutRef.current;
|
|
324
331
|
if (!el || !lay) return;
|
|
325
|
-
|
|
332
|
+
const p = store.getState().pitch;
|
|
333
|
+
const rh = p > 0 ? p : lay.rowHeight;
|
|
334
|
+
el.scrollTo({ top: row * rh, behavior: "smooth" });
|
|
326
335
|
workerRef.current?.postMessage({
|
|
327
336
|
type: "SET_WINDOW",
|
|
328
337
|
startRow: asRowIndex(Math.max(0, row - overscanRows)),
|
|
329
338
|
rowCount: lay.viewportRows + overscanRows * 2
|
|
330
339
|
});
|
|
331
|
-
}, [overscanRows]);
|
|
340
|
+
}, [overscanRows, store]);
|
|
332
341
|
return (0, import_react2.useMemo)(
|
|
333
342
|
() => ({ containerRef, store, ingest, scrollToRow }),
|
|
334
343
|
[containerRef, store, ingest, scrollToRow]
|
|
@@ -428,7 +437,7 @@ function buildRowProxy(accessors, schema, localRow) {
|
|
|
428
437
|
}
|
|
429
438
|
};
|
|
430
439
|
}
|
|
431
|
-
function buildPoolAssignments(win,
|
|
440
|
+
function buildPoolAssignments(win, rowHeight, poolSize) {
|
|
432
441
|
const assignments = [];
|
|
433
442
|
for (let i = 0; i < win.rowCount; i++) {
|
|
434
443
|
const rowIndex = win.startRow + i;
|
|
@@ -436,7 +445,7 @@ function buildPoolAssignments(win, layout, poolSize) {
|
|
|
436
445
|
poolSlot: asPoolSlot(i % poolSize),
|
|
437
446
|
rowIndex: asRowIndex(rowIndex),
|
|
438
447
|
localIndex: i,
|
|
439
|
-
y: rowIndex *
|
|
448
|
+
y: rowIndex * rowHeight
|
|
440
449
|
});
|
|
441
450
|
}
|
|
442
451
|
return assignments;
|
|
@@ -445,14 +454,16 @@ var OVERSCAN_ROWS = 3;
|
|
|
445
454
|
function VirtualChamber({ store, renderRow, containerRef, className, style }) {
|
|
446
455
|
const layout = useAtomicSlice(store, (s) => s.layout);
|
|
447
456
|
const win = useAtomicSlice(store, (s) => s.window);
|
|
457
|
+
const pitch = useAtomicSlice(store, (s) => s.pitch);
|
|
448
458
|
const accessors = (0, import_react3.useMemo)(
|
|
449
459
|
() => win ? buildAccessors(win.buffer, win.rowCount) : null,
|
|
450
460
|
[win]
|
|
451
461
|
);
|
|
452
462
|
const poolSize = layout ? layout.viewportRows + OVERSCAN_ROWS * 2 : 0;
|
|
463
|
+
const rh = (pitch && pitch > 0 ? pitch : layout?.rowHeight) ?? 0;
|
|
453
464
|
const assignments = (0, import_react3.useMemo)(
|
|
454
|
-
() => win &&
|
|
455
|
-
[win,
|
|
465
|
+
() => win && rh > 0 ? buildPoolAssignments(win, rh, poolSize) : [],
|
|
466
|
+
[win, rh, poolSize]
|
|
456
467
|
);
|
|
457
468
|
if (!layout) return null;
|
|
458
469
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
@@ -471,7 +482,7 @@ function VirtualChamber({ store, renderRow, containerRef, className, style }) {
|
|
|
471
482
|
"div",
|
|
472
483
|
{
|
|
473
484
|
"aria-hidden": true,
|
|
474
|
-
style: { height: layout.
|
|
485
|
+
style: { height: layout.totalRows * rh, pointerEvents: "none" }
|
|
475
486
|
}
|
|
476
487
|
),
|
|
477
488
|
accessors !== null && assignments.map(({ poolSlot, rowIndex, localIndex, y }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
@@ -484,7 +495,7 @@ function VirtualChamber({ store, renderRow, containerRef, className, style }) {
|
|
|
484
495
|
top: 0,
|
|
485
496
|
left: 0,
|
|
486
497
|
right: 0,
|
|
487
|
-
height:
|
|
498
|
+
height: rh,
|
|
488
499
|
transform: `translateY(${y}px)`,
|
|
489
500
|
willChange: "transform",
|
|
490
501
|
contain: "layout style"
|
package/dist/core/index.d.cts
CHANGED
|
@@ -136,6 +136,8 @@ interface StoreState {
|
|
|
136
136
|
readonly backpressure: BackpressureState;
|
|
137
137
|
readonly totalRows: number;
|
|
138
138
|
readonly error: string | null;
|
|
139
|
+
/** DOM-traced row pitch in px. 0 = use layout.rowHeight from Worker. */
|
|
140
|
+
readonly pitch: number;
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
/**
|
|
@@ -156,6 +158,8 @@ declare class AtomicStore {
|
|
|
156
158
|
dispatch(event: WorkerEvent): void;
|
|
157
159
|
/** Transition stream lifecycle status. */
|
|
158
160
|
setStatus(status: StreamStatus, error?: string): void;
|
|
161
|
+
/** Set DOM-traced row pitch. 0 resets to Worker-computed rowHeight. */
|
|
162
|
+
setPitch(px: number): void;
|
|
159
163
|
private merge;
|
|
160
164
|
}
|
|
161
165
|
type EqualityFn<T> = (a: T, b: T) => boolean;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -136,6 +136,8 @@ interface StoreState {
|
|
|
136
136
|
readonly backpressure: BackpressureState;
|
|
137
137
|
readonly totalRows: number;
|
|
138
138
|
readonly error: string | null;
|
|
139
|
+
/** DOM-traced row pitch in px. 0 = use layout.rowHeight from Worker. */
|
|
140
|
+
readonly pitch: number;
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
/**
|
|
@@ -156,6 +158,8 @@ declare class AtomicStore {
|
|
|
156
158
|
dispatch(event: WorkerEvent): void;
|
|
157
159
|
/** Transition stream lifecycle status. */
|
|
158
160
|
setStatus(status: StreamStatus, error?: string): void;
|
|
161
|
+
/** Set DOM-traced row pitch. 0 resets to Worker-computed rowHeight. */
|
|
162
|
+
setPitch(px: number): void;
|
|
159
163
|
private merge;
|
|
160
164
|
}
|
|
161
165
|
type EqualityFn<T> = (a: T, b: T) => boolean;
|
package/dist/core/index.js
CHANGED
|
@@ -42,7 +42,8 @@ var INITIAL_STORE_STATE = {
|
|
|
42
42
|
window: null,
|
|
43
43
|
backpressure: { strategy: "NOMINAL", queueDepth: 0, avgRenderMs: asMs(0) },
|
|
44
44
|
totalRows: 0,
|
|
45
|
-
error: null
|
|
45
|
+
error: null,
|
|
46
|
+
pitch: 0
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
// src/core/atomic-store.ts
|
|
@@ -95,6 +96,10 @@ var AtomicStore = class {
|
|
|
95
96
|
setStatus(status, error) {
|
|
96
97
|
this.merge({ status, error: error ?? this.state.error });
|
|
97
98
|
}
|
|
99
|
+
/** Set DOM-traced row pitch. 0 resets to Worker-computed rowHeight. */
|
|
100
|
+
setPitch(px) {
|
|
101
|
+
if (this.state.pitch !== px) this.merge({ pitch: px });
|
|
102
|
+
}
|
|
98
103
|
merge(patch) {
|
|
99
104
|
const next = { ...this.state, ...patch };
|
|
100
105
|
this.state = next;
|
|
@@ -195,14 +200,16 @@ function useStabilityOrchestrator(options) {
|
|
|
195
200
|
const el = containerRef_internal.current;
|
|
196
201
|
const lay = layoutRef.current;
|
|
197
202
|
if (!el || !lay || lay.rowHeight === 0) return;
|
|
198
|
-
const
|
|
203
|
+
const p = store.getState().pitch;
|
|
204
|
+
const rh = p > 0 ? p : lay.rowHeight;
|
|
205
|
+
const startRow = asRowIndex(Math.floor(el.scrollTop / rh));
|
|
199
206
|
const rowCount = lay.viewportRows + overscanRows * 2;
|
|
200
207
|
workerRef.current?.postMessage({
|
|
201
208
|
type: "SET_WINDOW",
|
|
202
209
|
startRow,
|
|
203
210
|
rowCount
|
|
204
211
|
});
|
|
205
|
-
}, [overscanRows]);
|
|
212
|
+
}, [overscanRows, store]);
|
|
206
213
|
const containerRef = useCallback2(
|
|
207
214
|
(el) => {
|
|
208
215
|
if (containerRef_internal.current) {
|
|
@@ -289,13 +296,15 @@ function useStabilityOrchestrator(options) {
|
|
|
289
296
|
const el = containerRef_internal.current;
|
|
290
297
|
const lay = layoutRef.current;
|
|
291
298
|
if (!el || !lay) return;
|
|
292
|
-
|
|
299
|
+
const p = store.getState().pitch;
|
|
300
|
+
const rh = p > 0 ? p : lay.rowHeight;
|
|
301
|
+
el.scrollTo({ top: row * rh, behavior: "smooth" });
|
|
293
302
|
workerRef.current?.postMessage({
|
|
294
303
|
type: "SET_WINDOW",
|
|
295
304
|
startRow: asRowIndex(Math.max(0, row - overscanRows)),
|
|
296
305
|
rowCount: lay.viewportRows + overscanRows * 2
|
|
297
306
|
});
|
|
298
|
-
}, [overscanRows]);
|
|
307
|
+
}, [overscanRows, store]);
|
|
299
308
|
return useMemo(
|
|
300
309
|
() => ({ containerRef, store, ingest, scrollToRow }),
|
|
301
310
|
[containerRef, store, ingest, scrollToRow]
|
|
@@ -397,7 +406,7 @@ function buildRowProxy(accessors, schema, localRow) {
|
|
|
397
406
|
}
|
|
398
407
|
};
|
|
399
408
|
}
|
|
400
|
-
function buildPoolAssignments(win,
|
|
409
|
+
function buildPoolAssignments(win, rowHeight, poolSize) {
|
|
401
410
|
const assignments = [];
|
|
402
411
|
for (let i = 0; i < win.rowCount; i++) {
|
|
403
412
|
const rowIndex = win.startRow + i;
|
|
@@ -405,7 +414,7 @@ function buildPoolAssignments(win, layout, poolSize) {
|
|
|
405
414
|
poolSlot: asPoolSlot(i % poolSize),
|
|
406
415
|
rowIndex: asRowIndex(rowIndex),
|
|
407
416
|
localIndex: i,
|
|
408
|
-
y: rowIndex *
|
|
417
|
+
y: rowIndex * rowHeight
|
|
409
418
|
});
|
|
410
419
|
}
|
|
411
420
|
return assignments;
|
|
@@ -414,14 +423,16 @@ var OVERSCAN_ROWS = 3;
|
|
|
414
423
|
function VirtualChamber({ store, renderRow, containerRef, className, style }) {
|
|
415
424
|
const layout = useAtomicSlice(store, (s) => s.layout);
|
|
416
425
|
const win = useAtomicSlice(store, (s) => s.window);
|
|
426
|
+
const pitch = useAtomicSlice(store, (s) => s.pitch);
|
|
417
427
|
const accessors = useMemo2(
|
|
418
428
|
() => win ? buildAccessors(win.buffer, win.rowCount) : null,
|
|
419
429
|
[win]
|
|
420
430
|
);
|
|
421
431
|
const poolSize = layout ? layout.viewportRows + OVERSCAN_ROWS * 2 : 0;
|
|
432
|
+
const rh = (pitch && pitch > 0 ? pitch : layout?.rowHeight) ?? 0;
|
|
422
433
|
const assignments = useMemo2(
|
|
423
|
-
() => win &&
|
|
424
|
-
[win,
|
|
434
|
+
() => win && rh > 0 ? buildPoolAssignments(win, rh, poolSize) : [],
|
|
435
|
+
[win, rh, poolSize]
|
|
425
436
|
);
|
|
426
437
|
if (!layout) return null;
|
|
427
438
|
return /* @__PURE__ */ jsxs(
|
|
@@ -440,7 +451,7 @@ function VirtualChamber({ store, renderRow, containerRef, className, style }) {
|
|
|
440
451
|
"div",
|
|
441
452
|
{
|
|
442
453
|
"aria-hidden": true,
|
|
443
|
-
style: { height: layout.
|
|
454
|
+
style: { height: layout.totalRows * rh, pointerEvents: "none" }
|
|
444
455
|
}
|
|
445
456
|
),
|
|
446
457
|
accessors !== null && assignments.map(({ poolSlot, rowIndex, localIndex, y }) => /* @__PURE__ */ jsx(
|
|
@@ -453,7 +464,7 @@ function VirtualChamber({ store, renderRow, containerRef, className, style }) {
|
|
|
453
464
|
top: 0,
|
|
454
465
|
left: 0,
|
|
455
466
|
right: 0,
|
|
456
|
-
height:
|
|
467
|
+
height: rh,
|
|
457
468
|
transform: `translateY(${y}px)`,
|
|
458
469
|
willChange: "transform",
|
|
459
470
|
contain: "layout style"
|