-
Notifications
You must be signed in to change notification settings - Fork 0
Review 5353
#5353 · rubyycheung · internal · draft
Head reviewed c0c6941c9b4b (+2777/−156, 9 files)
Verdict comment — posted
2026-08-24 UTC, with three inlines. Comment rather than request-changes because it
is a draft; the review says the safe-area half would be taken today.
Two passes. Pass one found every one of the changes Cindy blocks on and drew no conclusion from any of them. That contrast is why this page exists, and it is below.
As stated in the PR: a toast against a notched phone's raw viewport edge, plus padding and wrapping that break at narrow widths. That is the whole of the written problem — six runtime changes ship against it.
Safe-area gutters and a width constraint; padding, alignment and wrapping; CSS-native entry/exit motion — and then four more, each changing behaviour for every existing consumer:
| decision | traces to a stated problem? |
|---|---|
| safe-area gutters + width constraint | yes — the notch |
| layout / padding / motion polish | yes, same |
| one toast at a time below 640px, the rest queued | no |
isAutoHide false whenever endContent is set |
no |
swipe-to-dismiss, 'swipe' added to ToastDismissReason
|
no |
the addToast announce moved into an effect |
no |
Six decisions, four of them piggybacked — the rule this produced is R29.
Everyone on the next release, no opt-in. Four of the six change what an existing
addToast call does.
~ ToastDismissReason: + 'swipe' (public, core barrel) · types.ts:15
~ Toast.isAutoHide default → false whenever endContent is renderable · ToastViewport.tsx:520
'swipe' widens a union the library hands the consumer, permanently. Pass one
argued for it — "a genuine third member of an existing enumeration" — a rationale
the PR itself never offered. Cindy: "a swipe is arguably also manual, so why the
third classification — what does it gain".
-
Render — the announce moved from an imperative call to an effect that sets
state: +1 render pass per visible-toast change, plus
announcedIdsRef, an unboundedSetexisting only to suppress the re-runs the effect created. ·ToastViewport.tsx:395 -
Listeners — a
ResizeObserverand awindowresize listener watching the same number; one is redundant. - Size — +2777 lines. Cindy: "2777 new LOC is a smell for sure." ~480 of it is runtime, under ~2,300 of stories, tests and docs, and that ratio is what let four unargued behaviour changes arrive looking like housekeeping.
comment — take the safe-area half; the other four need their reasons written down, and two need fixing wherever they land.
1. Four runtime changes with no problem statement anywhere in 2,777 lines
→ a reviewer cannot weigh a change whose reason is not written
· ToastViewport.tsx:390, :520, :395 · Toast.tsx:100 · types.ts:15
2. At 375px a persistent toast never leaves the head of the queue
→ five fired, one shown at 20s; main shows three · ToastViewport.tsx:390
3. `touch-action: none` kills page scroll under the toast
→ a finger that lands on the toast cannot scroll the page · Toast.tsx
Thanks — the safe-area and wrapping work is the right fix and I'd take that half today. Commenting rather than blocking only because it's a draft.
Six runtime changes ship here and only the safe-area one has a problem written down. These four each change behaviour for every existing consumer, and I couldn't find the sentence saying what was wrong before:
- one toast at a time below 640px, the rest queued (
ToastViewport.tsx:390)isAutoHidenow false wheneverendContentis set (:520)- swipe-to-dismiss, and
'swipe'added toToastDismissReason(Toast.tsx:100,types.ts:15)- the
addToastannounce moved into an effect (:395)Two I'd want fixed wherever they land: at 375px a persistent toast never leaves the head of the queue — five fired, one shown at 20s, main shows three — and
touch-action: nonekills page scroll under the toast.Could you write up the problem each of those four solves, and split out any that want their own discussion? Happy with several problems in one PR — I just can't weigh a change I can't see the reason for.
[Reviewed by Robohands]
Inlines:
ToastViewport.tsx:520— Any toast withendContentis persistent now. Why was the old default wrong?
ToastViewport.tsx:395— Hmm, was theaddToastannounce not enough? The comment you deleted said why it lived there.
Toast.tsx:100— BottomSheet keeps this on the handle and does the handoff in a non-passive touchmove —useSheetGestures.ts:1186.
Both passes read the same diff. The findings did not change; the conclusions did.
| the change | pass one | pass two |
|---|---|---|
isAutoHide default flips |
listed accurately in API and IMPACT | "Why was the old default wrong?" — a finding |
'swipe' joins the union |
OSSIFICATION argued for it | "what does it gain" — a finding |
| announce becomes an effect | PERFORMANCE called it "the smell" | +1 render pass, priced, and the Set named — a finding |
| six decisions | named mid-review, then dropped | the closing ask |
| the closing line | a design question about swipe direction | write up the four problems, split what needs it |
Three things this cost, each now a rule:
- The loop blocks on what it can measure breaking and merely describes what it cannot. All three of Cindy's objections were changes the PR made and never justified; pass one found all three and drew no conclusion → R1j.
-
Worse than a miss: pass one built the author's case. On
'swipe'it reasoned its way to a justification the PR never offered. An unargued change with a reviewer-supplied rationale is strictly worse than an unargued change, because the reviewer has laundered it. - The closing ask is the verdict. Pass one named the split mid-review and then closed on a design question — so the author would have answered the design question and the split would have evaporated.
And the correction Cindy made to the first draft of R29 itself: "I actually don't mind more than one problem per PR, but all problems and solutions should be recorded, not piggybacked." Plural problems are fine; unrecorded ones are not.
Effects and listeners got their own rules from the same review — R18e (an effect that sets state costs a render pass; price it) and R18f (a listener is fine when scoped to the moment it is needed).
#5352, same author, same day: ~50 runtime lines, 2 decisions, merged within the day. This one: ~480 runtime lines, 6 decisions, round two. See Review-Loop-2026-08-23.
Verdict request-changes. Run for the 0.5.0 cut at 11:00 PT; recommendation
was do not include in this cut. Read-only — nothing posted.
Banked at astryx/release_blocks kt-o26i.
Frames: assets/pr-5353.
All four unrecorded changes are gone from this head and both defects moved to
#5375 /
#5376. types.ts is -5/+1 — a
prettier reflow of ToastPosition, no union member touched. Every one of the six
"Problem and rationale" bullets now traces to a change and back. R29's remedy
worked: the author wrote the problems down and split what needed splitting, and
the count going 6 → 6 while the findings went to zero is the point — plural
problems are fine, unrecorded ones are not.
-
Toast.positionis public surface that positions nothing ·Toast.tsx:159. Only picks the motion drift sign and the transform-origin corner; same name and type asToastViewport.position(ToastViewport.tsx:143), which really does place things. Dropping it and letting the wrapper set--_toast-enter-yand--_toast-origin— custom properties inherit — reproduces the entry drift, the scale and the RTL origin exactly, at −50/+32 lines and no public API. Built in a scratch worktree, typechecked, measured, reverted. -
The retune is tuned single-line-only ·
Toast.tsx:109.align-items: centerunconditionally moves Undo and × off the first line on any wrapped toast (× centre 406→434 at 900px, 586→624 at 375px, against first-line mids of 414 and 594). Banner does the opposite deliberately —flex-startwithheaderCenteredonly when single-line (Banner.tsx:230, :250, :572) — and keeps the −4px end-area margin this drops.
paddingBlock 16→12 folded into (2) under R30 rather than standing as a
third finding: on a single-line toast the 8px it frees is taken straight back by
dropping that −4px margin, so the box is 52px before and after. The headline
change of the changeset is invisible in the common case, and visible only on the
multi-line toast whose controls the same decision displaces.
It is present at the previously reviewed head c0c6941c9b4b:200. Round one's API
slot listed ToastDismissReason: + 'swipe' and the isAutoHide default and not
this. A round that is looking for unrecorded behaviour changes can walk past a
new public prop — the API slot has to be enumerated from the source, not from
the diff's loudest lines.
The first draft came in at approve-with-nits — internal author, nothing broken, escalate the taste call. The independent critic (blind to the code and to the reviewer's reasoning) failed it on R1h (a measured, reachable visual change is a regression, and the bucket has no exception) and R1g/R1g-api (a prop a consumer can set to something untrue), and called the class "not arguable". It was right and the verdict changed.
R2 passed on the first draft — 23 words against a 30 cap, inlines 12/13/5. First run in the kit's history where length was not the failure. What failed instead was structure: twelve missing slots, and an unstructured "what I verified" list standing in for them. Worth watching whether that trade is a pattern — a draft that is short because it dropped the slots is not a short draft.
Three catches from the critic that changed the evidence rather than the wording:
-
R14f —
max(16px, env(…))had been measured only at the default. Driving the gutter token to0pxon both builds returns0pxon both, becauseenv()is 0 in Chromium. So the theme axis is unchanged, which is a stronger and more honest answer than "inert on every surface I can test". -
R2l — the
--_toast-enter-yalternative was proposed, not built. Building it turned an opinion into a measurement. -
R14h, self-inflicted — the review nearly reported that this PR would break
main's new
apiContractDrifttest, which #4315 added this morning. #5382 deleted that test the same day. A confident merge-hazard claim, killed by the check that was supposed to confirm it.
env(safe-area-inset-*) is 0 in Chromium including device emulation, so the
safe-area path — the PR's own headline — is inert on every surface the loop can
reach. The author cites an iOS simulator; the loop cannot. Said plainly rather
than assumed, per the brief.