chore: version packages - #431
Open
blove wants to merge 1 commit into
Open
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
blove
enabled auto-merge (squash)
August 15, 2026 03:51
Contributor
Vercel preview readyPreview: https://pretable-61ui2deb7-cacheplane.vercel.app Updated automatically by the |
github-actions
Bot
force-pushed
the
changeset-release/main
branch
14 times, most recently
from
August 16, 2026 02:48
141ef19 to
dccb86e
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 16, 2026 03:28
dccb86e to
fd5cc1f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@pretable/core@0.11.0
Minor Changes
Eviction: the focused cell and the selection anchor survive their rows being (#427)
released, exactly as a selection range already does — and DOM focus never falls
to
<body>.The cursor.
reconcileIndexedFocusre-seated to the nearest surviving rowwhenever the focused row was absent. Under eviction that silently moved the
user's cursor: scroll away, scroll back, and focus had migrated. That rule was
written when an absent row could only mean a deleted one. It now reads the same
discriminator the selection does —
resultMeta.window— through the sameprovenDeletedRow:The anchor.
anchor = ranges[0].startfired on visibility alone, so anevicted anchor migrated to the first range's start. The anchor is the fixed end
of the next gesture — a shift-click extends straight from that address — so
for an upward selection (anchor at the range's end) or a cmd-clicked second
range, the following shift-click extended from the wrong end and deselected what
the user had. It is now retained when merely evicted and reassigned only on a
proven deletion.
DOM focus. When the cursor's cell is unmounted — an evicted row, or an
ordinary scroll past the virtualization window — focus is parked on the grid's
scroll viewport rather than being dropped to
<body>, so the keyboard keepsworking and a screen reader stays inside the grid. The cell takes focus back the
moment its row is rendered again, and arrow keys resume from there rather than
from wherever the viewport is parked. Proven in a real browser
(
apps/bench/tests/eviction.spec.ts), with a kill switch that strips the windowand asserts the cursor is lost — jsdom has no opinion about where focus goes
when its element unmounts.
Local mode — a grid with no window — is unchanged in every branch.
Eviction, finished: an arrow key no longer loses an evicted cursor, and one (#446)
deleted row no longer takes a whole selection with it.
The keyboard. The cursor survives its row being released — but
moveIndexedFocusreconciled two-argument, so it could not tell an evicted rowfrom a deleted one and dropped the cursor on the very next keystroke. That is
precisely the state a user is in: they scrolled away from the cell, then pressed
a key. The eviction context is now threaded through the move and through the
store's
moveFocus.A row-axis move from a cursor whose row is not loaded is refused — the
cursor holds where the user left it — while the column axis still answers,
because it never needed the row. The alternatives were both worse: jumping to
the nearest loaded row teleports the cursor across however many rows were
released, and moving to the adjacent dataset position cannot be expressed at
all, because a focus ref addresses a cursor by row identity and the engine
cannot name a row it has never loaded. A positional cursor that requests its own
row is a real feature and a product decision; it is deliberately not smuggled in
here.
The selection. Retention was per range: reconciliation dropped a range as
soon as either endpoint was proven deleted, and collapsed it onto the survivor
when one endpoint was still loaded. So a range whose start was genuinely deleted
while its end was merely evicted was discarded whole — an 81-row selection
reporting 0, or 1, with 80 of those rows still loaded and painted.
The spec states the rule per row, and
datasetRowSpanis what makes thatexpressible: the proven-deleted rows prune and the span around them narrows.
A deletion shifts everything after it down one, so whichever end was removed the
survivors land on
lo … hi - 1. The deleted endpoint's identity is replacedby the row that now holds the narrowed boundary, so no deleted row id lingers in
a live selection — where the anchor reassignment would hand it straight back.
Fail-closed throughout: a range with no readable span, an emptied span, or a
narrowed boundary that is not loaded is still dropped exactly as before.
Local mode — a grid with no window — is byte-for-byte unchanged in every branch.
Patch Changes
processing: { filter: "external" }now stops the engine applying (#447)query.filters, without changing anything the grid reports.Declaring external filter authority used to change nothing about which rows were
drawn: the engine went on re-applying the published filters to whatever rows it
was handed. That is idempotent while the rows and the query agree — the server
answered the same query, so re-selecting changed nothing — which is why it went
unnoticed. It stops being idempotent the moment they disagree, and the lifecycle
guarantees they will: during
dataState.phase === "stale"the loaded rows answerthe PREVIOUS query while the grid already holds the NEW one, so the engine
filtered the old window by the new filter and the reader watched rows vanish and
return. The same mechanism was reproducible in the
errorphase — same rows,same failed request,
contains "fail"emptying the body whilenotContains "fail"kept every row.Under external authority the consumer owns which records exist, so re-applying
the query was the grid overruling the authority it had just been told it does not
have.
Nothing that is REPORTED moves. The funnel still shows the active filter,
onQueryChangestill publishes it, the query in the snapshot is byte-for-bytewhat it was, and
aria-sortis untouched. A filter naming a column that does notexist is still rejected. Only the record selection stops.
Two consequences worth naming:
aggregateFilteredRows, thefiltered population under suppression is the whole loaded window, because the
server already chose it. Groups whose rows all failed the local filter now
appear with their totals instead of disappearing.
sort: "external"still lets the engineorder the rows it was given; only filtering is suppressed.
Rows mode only. A consumer who supplies their own model through
model=alreadydecides what goes into the query and can omit the filters themselves, so the
surface never moves that model's authority.
The server-controlled data surface is no longer marked experimental. External (#426)
filter/sort authority (
PretableProcessingAuthority,PretableProcessingOptions),the
dataStatelifecycle (PretableDataState,PretableBodyStateKind) andresult metadata (
PretableMatchingTotal,PretableResultMeta) shipped acrossfive releases, are locked behind the API-surface gate and carry e2e coverage, so
the
@experimentalhedge on their TSDoc has been dropped. Their types andbehavior are unchanged.
@pretable/react@0.11.0
Minor Changes
Eviction: the focused cell and the selection anchor survive their rows being (#427)
released, exactly as a selection range already does — and DOM focus never falls
to
<body>.The cursor.
reconcileIndexedFocusre-seated to the nearest surviving rowwhenever the focused row was absent. Under eviction that silently moved the
user's cursor: scroll away, scroll back, and focus had migrated. That rule was
written when an absent row could only mean a deleted one. It now reads the same
discriminator the selection does —
resultMeta.window— through the sameprovenDeletedRow:The anchor.
anchor = ranges[0].startfired on visibility alone, so anevicted anchor migrated to the first range's start. The anchor is the fixed end
of the next gesture — a shift-click extends straight from that address — so
for an upward selection (anchor at the range's end) or a cmd-clicked second
range, the following shift-click extended from the wrong end and deselected what
the user had. It is now retained when merely evicted and reassigned only on a
proven deletion.
DOM focus. When the cursor's cell is unmounted — an evicted row, or an
ordinary scroll past the virtualization window — focus is parked on the grid's
scroll viewport rather than being dropped to
<body>, so the keyboard keepsworking and a screen reader stays inside the grid. The cell takes focus back the
moment its row is rendered again, and arrow keys resume from there rather than
from wherever the viewport is parked. Proven in a real browser
(
apps/bench/tests/eviction.spec.ts), with a kill switch that strips the windowand asserts the cursor is lost — jsdom has no opinion about where focus goes
when its element unmounts.
Local mode — a grid with no window — is unchanged in every branch.
Eviction, finished: an arrow key no longer loses an evicted cursor, and one (#446)
deleted row no longer takes a whole selection with it.
The keyboard. The cursor survives its row being released — but
moveIndexedFocusreconciled two-argument, so it could not tell an evicted rowfrom a deleted one and dropped the cursor on the very next keystroke. That is
precisely the state a user is in: they scrolled away from the cell, then pressed
a key. The eviction context is now threaded through the move and through the
store's
moveFocus.A row-axis move from a cursor whose row is not loaded is refused — the
cursor holds where the user left it — while the column axis still answers,
because it never needed the row. The alternatives were both worse: jumping to
the nearest loaded row teleports the cursor across however many rows were
released, and moving to the adjacent dataset position cannot be expressed at
all, because a focus ref addresses a cursor by row identity and the engine
cannot name a row it has never loaded. A positional cursor that requests its own
row is a real feature and a product decision; it is deliberately not smuggled in
here.
The selection. Retention was per range: reconciliation dropped a range as
soon as either endpoint was proven deleted, and collapsed it onto the survivor
when one endpoint was still loaded. So a range whose start was genuinely deleted
while its end was merely evicted was discarded whole — an 81-row selection
reporting 0, or 1, with 80 of those rows still loaded and painted.
The spec states the rule per row, and
datasetRowSpanis what makes thatexpressible: the proven-deleted rows prune and the span around them narrows.
A deletion shifts everything after it down one, so whichever end was removed the
survivors land on
lo … hi - 1. The deleted endpoint's identity is replacedby the row that now holds the narrowed boundary, so no deleted row id lingers in
a live selection — where the anchor reassignment would hand it straight back.
Fail-closed throughout: a range with no readable span, an emptied span, or a
narrowed boundary that is not loaded is still dropped exactly as before.
Local mode — a grid with no window — is byte-for-byte unchanged in every branch.
Patch Changes
Data-honesty checks now read every input from one commit, and the engine-sort (#435)
rule finally runs.
A narrowing query no longer accuses you of a broken total.
rowsandresultMeta.totalarrive together, but the row model ingests rows in a layouteffect — after the render that already read the new total. The contiguous-window
check therefore compared a new total against the previous query's row count:
filter 480 rows down to 120 and it reported that 120 records "cannot be a
contiguous window", then settled at the right
aria-rowcounta render later.Because these warnings fire once per page load, that spurious first one
permanently disarmed the check for the rest of the session — the real defect. In
rows mode the loaded count now comes from the
rowsthe consumer just handedover, and the "no total supplied" fallback counts the same records; explicit-model
mode still reads the model, which has no such skew and whose
rowsprop is anempty array rather than an absent one.
processing: { filter: "external", sort: "engine" }over a partial window nowwarns. The rule was written, unit-tested, and never called from a render.
Sorting a server-selected window locally presents the wrong sample under a
truthful-looking
aria-sort, and it fires only where that is provable: an exactresultMeta.totalcounting more records than the grid holds. Wiring it dependedon the fix above — the same one-render skew made an ordinary widening query look
like a partial window.
Settled behaviour is unchanged: the same counts, the same scope answers, and the
same warning for a
resultMeta.totalthat really is inconsistent with the rows.processing: { filter: "external" }now stops the engine applying (#447)query.filters, without changing anything the grid reports.Declaring external filter authority used to change nothing about which rows were
drawn: the engine went on re-applying the published filters to whatever rows it
was handed. That is idempotent while the rows and the query agree — the server
answered the same query, so re-selecting changed nothing — which is why it went
unnoticed. It stops being idempotent the moment they disagree, and the lifecycle
guarantees they will: during
dataState.phase === "stale"the loaded rows answerthe PREVIOUS query while the grid already holds the NEW one, so the engine
filtered the old window by the new filter and the reader watched rows vanish and
return. The same mechanism was reproducible in the
errorphase — same rows,same failed request,
contains "fail"emptying the body whilenotContains "fail"kept every row.Under external authority the consumer owns which records exist, so re-applying
the query was the grid overruling the authority it had just been told it does not
have.
Nothing that is REPORTED moves. The funnel still shows the active filter,
onQueryChangestill publishes it, the query in the snapshot is byte-for-bytewhat it was, and
aria-sortis untouched. A filter naming a column that does notexist is still rejected. Only the record selection stops.
Two consequences worth naming:
aggregateFilteredRows, thefiltered population under suppression is the whole loaded window, because the
server already chose it. Groups whose rows all failed the local filter now
appear with their totals instead of disappearing.
sort: "external"still lets the engineorder the rows it was given; only filtering is suppressed.
Rows mode only. A consumer who supplies their own model through
model=alreadydecides what goes into the query and can omit the filters themselves, so the
surface never moves that model's authority.
Stop segmenting graphemes for text that cannot need it. Grapheme-accurate (#428)
counting made
prepareText— which runs per wrapped cell on the row-heightestimate path — 97% segmentation by cost, charged twice per string: once for
the whole text and again per token. ASCII cannot form a multi-code-unit
grapheme cluster, so such text now counts by code-unit length and segments by
character; CRLF, the sole exception, still takes the segmenter, as does any
text carrying a character outside ASCII. On the S2
hypothesisscrollbenchmark this returns
scroll_frame_p95_msfrom 31.8/32.4 to 17.2/18.0 —four 120Hz ticks per scroll step back to two — with no predicted line count
and no estimate changed.
Stop recomputing the estimator's line count on every measurement. The row (#432)
layout controller asks
predictRowLineCountto classify each measured data rowfor the height calibration, and the estimator had already computed that number
for the same row from the same inputs — so the calibration path re-prepared and
re-laid out every wrapped cell in the grid once per commit. The count is now
stored on the estimate's existing cache entry and read back. Measured on the S2
hypothesisscroll benchmark, time underpredictRowLineCountfalls from17.3ms (3.27% of the run) to 1.1ms (0.22%). No estimate, line count or rendered
row count changes.
The server-controlled data surface is no longer marked experimental. External (#426)
filter/sort authority (
PretableProcessingAuthority,PretableProcessingOptions),the
dataStatelifecycle (PretableDataState,PretableBodyStateKind) andresult metadata (
PretableMatchingTotal,PretableResultMeta) shipped acrossfive releases, are locked behind the API-surface gate and carry e2e coverage, so
the
@experimentalhedge on their TSDoc has been dropped. Their types andbehavior are unchanged.
telemetry.windowGapnow judges the viewport against geometry from a single (#445)commit, which fixes a false negative and a false positive that had the same
cause.
The boundary is the plan's, not arithmetic over it. The end of the loaded
window was reconstructed as
totalHeight - trailingRows * rowHeight: a pixeltotal published by the last row layout plan, minus a row count derived fresh
from
resultMetaevery render. The row layout controller does not replan on aresultMeta-only change, so those two halves could describe different states ofthe world. A
totalthat GREW pushed the boundary further away and hid thedefect; a
totalthat SHRANK moved it the other way, andwindowGapwentsilently absent for a viewport still genuinely past the loaded rows — until any
scroll or row change triggered a replan. The plan already publishes both honest
halves of that boundary (
leadingHeightand the loaded rows' own height), sothe judgement now reads them directly and reconstructs nothing. When the plan is
current the two expressions are the same number by construction, so no answer
that was already correct has changed.
No gap is reported while the layout does not describe the rows. At mount
there is no geometry — the boundary sits at pixel 0 — so every viewport, on a
grid nobody had scrolled, read as past its own window. That covers three states
that all publish
totalHeight: 0: before the first plan, a first block of rowslanding against the plan for an empty grid, and the render where
rowshavebeen handed over but the row model has not ingested them yet. A gap is now
reported only once the plan, the row model and the rows you supplied agree on
how many records are loaded. On the windowing docs example, mount went from four
requests to one — the example's own.
Absence of a gap has always meant "no signal" rather than "the viewport is
inside the window", and that is unchanged: a handler that can tell from its own
state that the reader is past the loaded block should still act on what it
knows.
Updated dependencies [
762bcb0,076a36f,305f8f4,f37fa1c]:@pretable/stream-adapter@0.11.0
@pretable/ui@0.11.0
@pretable/app-bench@0.0.33
Patch Changes
7be784c,762bcb0,076a36f,305f8f4,f8c0699,7cd69d5,f37fa1c,f831631]:@pretable/app-website@0.0.33
Patch Changes
7be784c,762bcb0,076a36f,305f8f4,f8c0699,7cd69d5,f37fa1c,f831631]: