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
27 times, most recently
from
August 17, 2026 04:44
62dab49 to
801144c
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
6 times, most recently
from
August 23, 2026 00:39
11ff03f to
a9eec1f
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 24, 2026 17:59
a9eec1f to
3b4ab2b
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 25, 2026 03:54
3b4ab2b to
e96cbe4
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
Column visibility:
hiddenon column config,setColumnVisibleon the grid (#486)model.
PretableGridUiColumnandPretableGridUiColumnLayoutgain an optionalhiddenflag, and the grid model gainssetColumnVisible(columnId, visible).A hidden column stays in the column model — its width, pin state, and relative
order are preserved — but is excluded from the drawn order the renderer and
span-resolving consumers see.
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.
Eviction: two publicly-reachable correctness fixes, both of which contradicted (#460)
what the docs promised.
An evicted selection no longer paints rows the reader never selected.
datasetKeyidentifies the QUERY, not the population — deliberately, andthe docs tell consumers to keep it
stable while they page within one result. So an insert or a delete made by
someone else, upstream of a selection whose own rows are unloaded, arrived with
the key unchanged and silently re-filled the remembered dataset positions with
different rows. Reproduced through
<PretableSurface>with the honesty gatefully passing: a
row-1..row-8selection, both endpoints evicted, five rowsprepended to the same result, and the returning window painted five rows
selected — four of which had not existed when the user selected — while the
eight they did choose painted nothing.
A span now records the population's SIZE alongside its key
(
PretableIndexedDatasetRowSpan.datasetTotal, from the exactresultMeta.total.countthe gate already requires), and a mismatch fails closedexactly as a key mismatch does: nothing paints from the span, and
getCellSelectionSummary()reportsverified: falseuntil a window coveringboth endpoints re-measures it. A proven deletion is the one allowance — a total
short by exactly the rows the engine watched vanish is accounted for, so
endpoint narrowing still works. What this does not catch is a change that
leaves the size identical;
eviction.mdxnow says so rather than promisingotherwise.
ɵPretableIndexedSelectionWindow.datasetTotalis required, not optional: thegate that builds a window cannot pass without an exact total, so an optional
field would only be a way to fail open by omission.
One closed-gate revision no longer destroys the selection and the cursor.
With
resultMeta.totalreporting{kind: "estimate"}for a single render — anin-flight count query, a backend that stops counting past 10k — or one revision
of
processing.sort: "engine", a window slide dropped every range and emptiedthe cursor irrecoverably. Restoring the exact total brought neither back.
Uncontrolled consumers only; a controlled one was accidentally immune because
the
state.selectionecho re-supplied what the engine had discarded.A null window was two different situations sharing one representation. The
engine is now told which:
windowedsays whether the consumer publishesresultMeta.windowat all, independent of any gate, so a windowed grid with nowindow this revision means "I cannot verify", not "those rows were deleted" —
and it holds the selection and the cursor byte-for-byte instead of asserting a
deletion it could not have observed. Local mode, where an absent row genuinely
is a deleted row, is unchanged in every branch and pinned by tests that run the
same fixture both ways.
CreateGridUiCoreOptions.getSelectionWindowis replaced bygetWindowing,which returns both facts from one read so they cannot describe different
instants.
processing: { sort: "external" }now suppresses local sorting, the way (#467)filter: "external"suppresses local filtering since fix(react,row-model): external filter authority actually suppresses local filtering #447.It previously suppressed nothing: the declaration was read in two advisory
places and the engine went on applying
query.sort. That left the consumer whodeclared it worse off than one who did not — declaring external sort authority
silences the partial-window warning and unlocks the full population as
aria-rowcount, while the local re-sort it silences the warning about keptrunning.
Suppression changes what is APPLIED, never what is REPORTED:
aria-sort,onQueryChangeand the snapshot'squeryare untouched. A consumer holding acomplete window who legitimately sorts locally is unaffected — they declare
"engine", which is the default.Windowed spacers are sized from what rows have measured, not from the default (#465)
row height.
A windowed grid reserves the unloaded regions as spacers, and
getWindowSpacersreports those regions as row counts — how many rows sit before and after the
loaded window. The controller turned a count into pixels by multiplying by
defaultRowHeight. Its own comment said so: "Row counts, not pixel heights."That is the region's real height only on a grid whose rows are all the default
height. On a grid whose rows wrap it is a systematic understatement of the whole
scroll extent, by the ratio between a wrapped row and the unwrapped default —
and the retained-measurement cache, which knows exactly what those rows were
worth, was never consulted for geometry at all. It is keyed by row identity
while the spacer arrives as a count, so the two systems had no way to meet.
The controller now prices a spacer's rows at
RowHeightIndex.getMeasuredHeightMean()— the mean of every height the DOM hasreported, the retained heights of evicted rows included — falling back to
defaultRowHeightuntil something has been measured. A grid that has measurednothing, and every grid with no window at all, is byte-for-byte unchanged.
It remains an estimate: a count cannot say which rows are out there, so the
extent tracks the result's size without reproducing its height. The docs
previously claimed the spacer "reproduces the region's height precisely" where
retained heights were exact, which the code could not do and now does not claim.
eviction.mdxandwindowing.mdxsay what it actually computes, and that theviewport anchor is what absorbs the residual.
The mean is aggregated structurally — every hash node in the persistent height
index carries the sum of its values beside the count it already carried — rather
than threaded as a running total through
measure,apply, retention evictionand the cooperative replacement builder, so a copy-on-write rebuild cannot leave
it stale.
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.
Eviction: two publicly-reachable correctness fixes, both of which contradicted (#460)
what the docs promised.
An evicted selection no longer paints rows the reader never selected.
datasetKeyidentifies the QUERY, not the population — deliberately, andthe docs tell consumers to keep it
stable while they page within one result. So an insert or a delete made by
someone else, upstream of a selection whose own rows are unloaded, arrived with
the key unchanged and silently re-filled the remembered dataset positions with
different rows. Reproduced through
<PretableSurface>with the honesty gatefully passing: a
row-1..row-8selection, both endpoints evicted, five rowsprepended to the same result, and the returning window painted five rows
selected — four of which had not existed when the user selected — while the
eight they did choose painted nothing.
A span now records the population's SIZE alongside its key
(
PretableIndexedDatasetRowSpan.datasetTotal, from the exactresultMeta.total.countthe gate already requires), and a mismatch fails closedexactly as a key mismatch does: nothing paints from the span, and
getCellSelectionSummary()reportsverified: falseuntil a window coveringboth endpoints re-measures it. A proven deletion is the one allowance — a total
short by exactly the rows the engine watched vanish is accounted for, so
endpoint narrowing still works. What this does not catch is a change that
leaves the size identical;
eviction.mdxnow says so rather than promisingotherwise.
ɵPretableIndexedSelectionWindow.datasetTotalis required, not optional: thegate that builds a window cannot pass without an exact total, so an optional
field would only be a way to fail open by omission.
One closed-gate revision no longer destroys the selection and the cursor.
With
resultMeta.totalreporting{kind: "estimate"}for a single render — anin-flight count query, a backend that stops counting past 10k — or one revision
of
processing.sort: "engine", a window slide dropped every range and emptiedthe cursor irrecoverably. Restoring the exact total brought neither back.
Uncontrolled consumers only; a controlled one was accidentally immune because
the
state.selectionecho re-supplied what the engine had discarded.A null window was two different situations sharing one representation. The
engine is now told which:
windowedsays whether the consumer publishesresultMeta.windowat all, independent of any gate, so a windowed grid with nowindow this revision means "I cannot verify", not "those rows were deleted" —
and it holds the selection and the cursor byte-for-byte instead of asserting a
deletion it could not have observed. Local mode, where an absent row genuinely
is a deleted row, is unchanged in every branch and pinned by tests that run the
same fixture both ways.
CreateGridUiCoreOptions.getSelectionWindowis replaced bygetWindowing,which returns both facts from one read so they cannot describe different
instants.
processing: { sort: "external" }now suppresses local sorting, the way (#467)filter: "external"suppresses local filtering since fix(react,row-model): external filter authority actually suppresses local filtering #447.It previously suppressed nothing: the declaration was read in two advisory
places and the engine went on applying
query.sort. That left the consumer whodeclared it worse off than one who did not — declaring external sort authority
silences the partial-window warning and unlocks the full population as
aria-rowcount, while the local re-sort it silences the warning about keptrunning.
Suppression changes what is APPLIED, never what is REPORTED:
aria-sort,onQueryChangeand the snapshot'squeryare untouched. A consumer holding acomplete window who legitimately sorts locally is unaffected — they declare
"engine", which is the default.PageUp/PageDowngo through the engine, so an evicted cursor holds instead (#453)of being teleported.
The grid's page keys were the last place the surface still resolved a movement
itself.
handleSurfaceKeyDownasked the LOADED snapshot for the cursor's indexand read
indexOf's-1as "base the step at row 0" — a sentinel that meanstwo unrelated things. It means "the cursor is on the header, or there is none",
where basing at row 0 is deliberate; and it means "this ref did not resolve",
which is exactly what an evicted cursor returns. The two collapsed into one
branch, so a page key pressed while the cursor's row was released teleported it
a page into the loaded window — across however many rows had been let go — and
Shift+PageDowndragged the user's selection along with it, into a range withno dataset span left to count.
The branch now calls
moveFocus, which already modelspage-up/page-down,already receives the loaded window, and already refuses a row-axis move from a
cursor it cannot place. That is the same rule an arrow key follows, reached
through the same code, rather than a second implementation that has to remember
it. The surface still measures the step — a page is a screen's worth of the body
viewport, in rendered rows, which the engine cannot know — and hands it over on
every press.
A refused move now leaves the selection alone as well as the cursor. There
is nothing new to extend to, and extending to the evicted cursor itself rewrote
whatever range the user had into
anchor → a row the grid cannot place: onscreen, a keystroke that appeared to do nothing while quietly discarding the
span the selection is counted by. This applies to
Shift+Arrowas well, whichhad the same hole.
Three smaller behaviours change with the delegation, all of them the engine's
existing answer replacing the surface's divergent one:
PageDownfrom a column header lands on row 0. It used to land a pagebelow row 0, which no other key did —
ArrowDownfrom the header has alwaysmeant "the row below the header".
PageUpfrom a column header holds on the header. It used to drop thecursor into the body, where
ArrowUpthere is a no-op.PageDownwith no cursor at all seeds one at the first cell rather than apage into the grid, and never on the row-checkbox column.
Local mode — no
resultMeta.window— is otherwise unchanged: with no windownothing is ever retained, so no cursor can reach the refusal, and the page step
is measured and applied exactly as before.
Tool panel: a rail-and-pane shell on
PretableSurface, on by default, opening (#486)with a columns section.
The rail is a strip of section tabs docked at the grid's right edge, inside the
card; selecting a tab opens a 264px pane between the body viewport and the
rail. The rail borrows the header's surface and the pane the toolbar's, so the
panel reads as chrome, not content. It ships enabled —
toolPanel={false}removes it — and
PretableToolPanelConfigdrives the open section either way:activeSection/onActiveSectionChangecontrolled,defaultActiveSectionuncontrolled. The
<Pretable>preset passes the prop through, which retiresits documented "no configuration UI" limitation.
The columns section lists every column, subgrouped by pin state: a checkbox
toggles visibility (the engine's new
hiddenflag andsetColumnVisible,released alongside in
@pretable/core, so width, pin state and relative ordersurvive a round trip), a search box filters the list, "Reset columns" restores
the mount-time configuration, and a per-row kebab menu offers the three pin
placements. Rows reorder by dragging the grip or with Shift+ArrowUp/Down on it;
Escape abandons an in-flight drag or keyboard move without touching the engine.
In
@pretable/ui, the card chrome — border, radius, shadow — moves up from thescroll viewport onto a layout wrapper that encloses viewport, pane and rail, so
the docked panel sits inside the card rather than bolted onto it; the boxes
inside surrender their own copies and meet at hairlines. A grid rendered
without the panel paints identically to before.
Windowed spacers are sized from what rows have measured, not from the default (#465)
row height.
A windowed grid reserves the unloaded regions as spacers, and
getWindowSpacersreports those regions as row counts — how many rows sit before and after the
loaded window. The controller turned a count into pixels by multiplying by
defaultRowHeight. Its own comment said so: "Row counts, not pixel heights."That is the region's real height only on a grid whose rows are all the default
height. On a grid whose rows wrap it is a systematic understatement of the whole
scroll extent, by the ratio between a wrapped row and the unwrapped default —
and the retained-measurement cache, which knows exactly what those rows were
worth, was never consulted for geometry at all. It is keyed by row identity
while the spacer arrives as a count, so the two systems had no way to meet.
The controller now prices a spacer's rows at
RowHeightIndex.getMeasuredHeightMean()— the mean of every height the DOM hasreported, the retained heights of evicted rows included — falling back to
defaultRowHeightuntil something has been measured. A grid that has measurednothing, and every grid with no window at all, is byte-for-byte unchanged.
It remains an estimate: a count cannot say which rows are out there, so the
extent tracks the result's size without reproducing its height. The docs
previously claimed the spacer "reproduces the region's height precisely" where
retained heights were exact, which the code could not do and now does not claim.
eviction.mdxandwindowing.mdxsay what it actually computes, and that theviewport anchor is what absorbs the residual.
The mean is aggregated structurally — every hash node in the persistent height
index carries the sum of its values beside the count it already carried — rather
than threaded as a running total through
measure,apply, retention evictionand the cooperative replacement builder, so a copy-on-write rebuild cannot leave
it stale.
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 [
693f01e,762bcb0,076a36f,2a4cd7a,305f8f4,3124591,f37fa1c,693f01e,01a7d60]:@pretable/ui@0.11.0
Minor Changes
Tool panel: a rail-and-pane shell on
PretableSurface, on by default, opening (#486)with a columns section.
The rail is a strip of section tabs docked at the grid's right edge, inside the
card; selecting a tab opens a 264px pane between the body viewport and the
rail. The rail borrows the header's surface and the pane the toolbar's, so the
panel reads as chrome, not content. It ships enabled —
toolPanel={false}removes it — and
PretableToolPanelConfigdrives the open section either way:activeSection/onActiveSectionChangecontrolled,defaultActiveSectionuncontrolled. The
<Pretable>preset passes the prop through, which retiresits documented "no configuration UI" limitation.
The columns section lists every column, subgrouped by pin state: a checkbox
toggles visibility (the engine's new
hiddenflag andsetColumnVisible,released alongside in
@pretable/core, so width, pin state and relative ordersurvive a round trip), a search box filters the list, "Reset columns" restores
the mount-time configuration, and a per-row kebab menu offers the three pin
placements. Rows reorder by dragging the grip or with Shift+ArrowUp/Down on it;
Escape abandons an in-flight drag or keyboard move without touching the engine.
In
@pretable/ui, the card chrome — border, radius, shadow — moves up from thescroll viewport onto a layout wrapper that encloses viewport, pane and rail, so
the docked panel sits inside the card rather than bolted onto it; the boxes
inside surrender their own copies and meet at hairlines. A grid rendered
without the panel paints identically to before.
@pretable/stream-adapter@0.11.0
@pretable/app-bench@0.0.33
Patch Changes
7be784c,762bcb0,076a36f,2a4cd7a,305f8f4,3124591,f8c0699,5dfdf20,7cd69d5,f37fa1c,693f01e,f831631,01a7d60]:@pretable/app-website@0.0.33
Patch Changes
693f01e,7be784c,762bcb0,076a36f,2a4cd7a,305f8f4,3124591,f8c0699,5dfdf20,7cd69d5,f37fa1c,693f01e,f831631,01a7d60]:@pretable-internal/renderer-dom@0.0.1
Patch Changes
693f01e,762bcb0,076a36f,2a4cd7a,305f8f4,3124591,f37fa1c,01a7d60]: