-
Notifications
You must be signed in to change notification settings - Fork 0
Review 5214
freddymeta · collaborator (write, head on facebook/astryx) · #5214
Four rounds. Rounds 1–3 were never recorded here — reconstructed from the GitHub API on 2026-08-25.
Three blockers, all since fixed: private --_* vars rejected by astryx theme build; only px overrides survived; box and ring agreed only at mount.
Claimed the PR themed the canvas and the canvas was gone. False: the author had merged main and reimplemented on the SVG ring at 15:37, six hours before we asked. Retracted in a comment at 23:29 the same day; the review state itself was not cleared until 2026-08-25 18:34, which left the PR BLOCKED for ~20 hours after we knew the block was wrong.
The 23:29 correction carried one real finding: --spinner-rail-width: 0 painted nothing, against the story's own caption.
Two comments posted in Cindy's voice decided a first-of-kind public API question: first that --spinner-color was unnecessary because currentColor covers it, then a reversal — "Keep all four vars." The question had been escalated to her at 23:29 on 24 Aug and was answered by the loop four hours later, unasked.
The 03:40 reasoning is sound (currentColor resolves against a host cascade a theme author does not control; six input hosts set no color), and it corrected a wrong earlier steer. It was still not the loop's call: R33 puts new public surface in front of a person, and R27 says the merge/permanence decision is not the reviewer's. A ruling the other way is now a fourth reversal on this PR.
Head d18ef06c9d2402d6546addd4070472b37a1d18c8. Wait from his push: 10h 22m.
Everything from rounds 1–3 is closed, including the rail-0 finding — answered rather than code-fixed: the behaviour is deliberate, the doc entry now states it in both locales, and --spinner-track-color: transparent serves the "hide only the track" reading without touching geometry. R14d has no clause for a finding answered rather than fixed; this is the shape.
Default parity verified at every size, shade and the labelled case — frames at assets/pr-5214.
registerSpinnerVars() runs from the ring's ref callback (Spinner.tsx:161), so both CSS.registerProperty calls land at the first spinner mount. Registering an inherited property with an initial-value invalidates style document-wide, and a spinner appears on an already-rendered page with someone already waiting.
| document | as pushed | call removed | module scope |
|---|---|---|---|
| ~1,800 elements | 4.1 ms | 0.8 ms | 0.6 ms |
| ~8,400 elements | 10.3 ms | 0.8 ms | 0.7 ms |
Confirmed three ways: reverting the line in the same session; the cost tracking document size (2.2 / 29 / 73 / 171 ms at 142 / 2k / 10k / 40k elements — the signature of a document-wide invalidation, not of the spinner); and a module-scope positive control that removes the cost while rail 0 still draws its 28×28 box. Recalc count is flat at 8–11 either way, which is why R18c's count rule cannot carry this one and the duration has to.
- Inline-size precedence flips: a caller's
style={{width}}now beats the box. Declared, correctly tiered[breaking], migration line present. -
--spinner-color/--spinner-track-colorremain the system's first public per-component paint override. Cindy's, still open despite the loop having answered it.
#5410 split the compiled CSS into astryx-base and the product layer, so a component declaration now loses to @layer astryx-theme. The four "inert public vars" claimed twice in this thread — --button-focus-offset, --button-icon-only-aspect, --tree-list-indent, --tree-list-row-gap — are reachable again. That item is closed.
Risk class: needs judgement — new public surface, a declared behaviour change on shipped surface, a measured perf regression. Never merges unattended.
Versions: Review Loop unversioned (pre-1.0) · Component Audit Rubric 1.10
Head: d18ef06c · Verdict: request changes
Problem. Spinner theming now has the right SVG shape and all four semantic public variables, but theme registration still happens when the first spinner mounts, adding work to the moment a loading indicator appears.
Solution. Move the static registration to module initialization; keep the four variables, SVG behavior, rail-zero semantics, flex-shrink split, and sizing-precedence changeset.
Evidence. Real-browser A/B measured 21.7 ms first-mount recalculation versus 0.7 ms at module scope; defaults changed zero pixels and 119 targeted tests passed.
Review as posted:
Thanks — I previously asked for the SVG rebase, all four vars, rail-0 semantics, the flex-shrink split, and a separate sizing-precedence changeset; those are resolved.
One thing still blocks: first-mount registration adds a 21.7 ms style recalculation on a populated page while its loading indicator appears. The same registration takes 0.7 ms at module scope with zero changed pixels. Could you move it there?
[Reviewed by Robohands]
Versions: Review Loop 1.4.0 · Component Audit Rubric 1.12
Lane: full · Head: 42ae22fca045bef8627f833f78957a34967e0a6a · Verdict: approve
freddymeta pushed the one-parent fix at 05:15:15Z, 28 minutes after our changes-requested at
04:47:35Z. This round reviews that commit and reconciles everything older against it.
The 04:47 CHANGES_REQUESTED is still open and it is ours. Its single ask, quoted: "first-mount
registration adds a 21.7 ms style recalculation on a populated page while its loading indicator
appears… Could you move it there?" Satisfied. Every earlier ask — public var names,
rem/em/calc() reaching the ring, box and ring in step, rail-0 semantics, the flex-shrink
split (#5484), a separate sizing-precedence
changeset — was already closed at round 4 and stays closed. This round extends; it contradicts
nothing. Only the same account can clear the review state, so the PR stays blocked until it is.
The canvas findings are dead and are not revisited: the ring has been SVG since 1a41110.
A theme author cannot make the spinner theirs — size and shade resolve to geometry and
colours baked into the component, so they get the library's 28px accent ring or they hand-roll a
loading indicator that drifts from the system's motion and accessible name. The problem this head
adds: registering the geometry properties from the ring's ref callback made the person already
waiting pay for it, because an inherited property with an initial-value invalidates style
document-wide at the moment a spinner appears on a rendered page.
- four public vars on the
spinnertarget, read by the SVG ring - the box is sized from the same two values, which flips inline-size precedence — declared
[breaking], own changeset - the private
<length>pair registers at module evaluation, not at first mount — our round-4 ask
Three is over the >2 smell, but 1 and 2 cannot ship apart (the box has to read what the ring reads) and 3 is ours. Nothing traces to an unstated problem, so there is no split to ask for.
Invisible until a theme opts in — no shipped theme sets these, so no end user sees a pixel move
on the next release. A person waiting on a loading indicator on an upgraded app gets the spinner
without the browser re-resolving the whole page's style first. A builder who wrote
<Spinner style={{width: 40}} /> now gets 40px where the component's size used to win; their
code does not change and its meaning does, which is what the [breaking] changeset is for.
No API change. SpinnerProps.size already accepted 'xl' on main (SIZES.xl), so the doc
line going from 'sm' | 'md' | 'lg' to 'sm' | 'md' | 'lg' | 'xl' in both locales is repair of a
stale doc, not new surface.
spinner: {
'size:xl': {'--spinner-diameter': '2.5rem', '--spinner-rail-width': '0.375rem'},
'shade:subtle': {'--spinner-track-color': 'transparent'},
base: {'--spinner-color': 'var(--color-brand)'},
}<Spinner size="xl" /> // unchanged — what it resolves to is now the theme'sNo new target. Four public vars on the existing one:
+ --spinner-diameter → the drawn ring's diameter, on .astryx-spinner (public, permanent)
+ --spinner-rail-width → stroke width of ring and track (public, permanent)
+ --spinner-color → the moving arc's stroke (public, permanent)
+ --spinner-track-color → the track's stroke (public, permanent)
Documented in en and zh, all four, both blocks.
Rounds 3, 4 and 5 all treated the colour pair as the system's first public per-component paint
override. At this head, against this main, that is no longer true.
git grep over every .doc.mjs on origin/main returns five per-component public vars:
--button-focus-offset, --button-icon-only-aspect, --tree-list-indent, --tree-list-row-gap
— and --selectable-card-ring-color, which merged in
#5551 at 07:10 PT today, two and a half hours
after our last review. It is public, documented at SelectableCard.doc.mjs:41, declared at
SelectableCard.tsx:61, and sits on the same VARS_WITHOUT_DERIVED_MAPPING list for the same
reason the Spinner four do. Its doc sentence is the identical argument: "no token the component
could pick is guaranteed to contrast with a fill it cannot know."
So the geometry pair matches four landed members and the colour pair matches one. Both are new instances of an existing idea rather than a new concept, and they pass on citation. This is a correction of fact, not a ruling — the direction the author already has is unchanged and nothing is reopened.
- API — no.
- Visual — no, driven rather than asserted. Both arms, both stories, every sensor matched except Build; the pixel delta between arms is smaller than the same-arm control (below).
-
Theme — no. Existing targets and tokens resolve. The four public vars stay unregistered
on both arms (all read
""on<html>), which is the invariant the reachability guard needs.
Far side of the bound: the registered <length>'s initial-value: 0px inside
calc(diameter + 2 × rail) was driven past with rails of 0.125rem–0.375rem; every box came
out at exactly diameter + 2 × rail. Nothing grew — default boxes are 14/20/24/36 on both arms.
Zero effects added, removed or moved. No listener, no observer, no forced reflow, no dependency. The commit deletes a module-scope boolean and moves one idempotent call.
The block, resolved — measured as a state, not a clock. A registered inherited <length>
with an initial-value makes every element report a value; an unregistered custom property
nobody declares resolves to "". Carrier is a Button story, because Button imports Spinner
for its loading state and renders none at rest, which separates imported from mounted.
| arm | spinners in DOM |
--_spinner-ring-diameter on <html>
|
four public vars |
|---|---|---|---|
before d18ef06 — ref callback |
0 |
"" — not registered |
"" ×4 |
after 42ae22f — module scope |
0 |
"0px" — registered |
"" ×4 |
Reproduction: probe-kit/spinner-var-registration-timing.cjs,
ARM=<arm> SB_PORT=6411 STORY=core-button--primary; arms switched by git checkout in one
worktree, so the arm delta is the reviewed commit itself.
No fourth millisecond figure was taken. Three already sit in this thread on three different document sizes, a clock is not a legitimate assertion anyway, and the state read settles the same question deterministically. Not measured: the residual cost when a lazily code-split chunk evaluates onto an already-rendered page — bounded above by the old behaviour, so not a regression, but unquantified.
VISUAL CHECK: manual frames required
WHY: an unchanged-pixels claim over style-resolution work; and the visual gate, though green on
this head, compared 2 shots and left 12 without a baseline — neither themed story is
covered, so the endpoints this feature exists for are outside what the gate watches.
Arms are two checked-out commits in one worktree, so the arm delta is the reviewed PR diff (3 files, +81/−11, no unrelated change; inspected before the pixels were).
Sensor receipt — only Build differs across a pair:
| Sensor | Expected | Observed | Pass? |
|---|---|---|---|
| Build |
d18ef06… / 42ae22f…, one worktree |
as expected per arm | ✅ |
| Story |
core-spinner--sizes · core-spinner--themed-geometry
|
same | ✅ |
| Theme | preview initialGlobals.astryxTheme = 'neutral'; themed story nests spinner-themed-geometry
|
same | ✅ |
| Color mode | light |
light |
✅ |
| Direction | ltr |
ltr |
✅ |
| Viewport/media | 900×400 @1, forced-colors off, reduced-motion off, fine pointer, hover | same | ✅ |
| Rendered state | 4 × role="status" / aria-label="Loading"; ring box/r/stroke-width authored from SIZES and the story's own theme before opening the page |
exact match, both arms | ✅ |
| Subject geometry | 4 × .astryx-spinner, visible, non-zero, in viewport |
same | ✅ |
| Settled render | fonts loaded; animations frozen at currentTime 0 and paused — an intentional recorded state, because the ring rotates; no page error |
runningAnimations: 0 |
✅ |
| Image | 900×400 PNG, non-blank | 6.0–12.2 KB | ✅ |
sizes [{box:14,r:5,sw:2},{box:20,r:7,sw:3},{box:24,r:9,sw:3},{box:36,r:14,sw:4}]
themed [{box:20,r:8,sw:2},{box:32,r:12,sw:4},{box:42,r:16,sw:5},{box:52,r:20,sw:6}]
All eight triples were written into expected from the contract before the page was opened, and
none was loosened. box = diameter + 2 × rail holds in all eight.
| what it shows | before | after |
|---|---|---|
| defaults, four sizes | ![]() |
![]() |
| themed geometry, four sizes | ![]() |
![]() |
The frames are not byte-identical, and the control is why that is not a finding.
| pair | differing px of 360,000 | max channel delta |
|---|---|---|
| control — same arm, two launches, sizes | 862 | 47 |
| control — same arm, two launches, themed | 506 | 12 |
arms — d18ef06 vs 42ae22f, sizes |
349 | 13 |
arms — d18ef06 vs 42ae22f, themed |
506 | 12 |
The same build against itself differs more than the two arms do, so the cross-arm delta is inside the harness's own launch-to-launch antialiasing noise. Without the control this would have been a confident, wrong finding.
One honest limit: the image viewer in this run returned only metadata for every PNG, so the frames were inspected as per-pixel luminance maps of the subject region instead. Both arms show the same track, the same arc sector at the same radius and the same stroke thickness, on the default xl and the themed xl; the only differences are single-pixel shifts on the arc's antialiased edge.
needs judgement — new permanent public theme surface, and a declared behaviour change on shipped surface. Not low-risk, and it does not merge unattended.
approve
1. [not blocking] the branch will not merge
→ the author has a green PR that sits unmergeable; #5551 appended
--selectable-card-ring-color to the same Set this PR appends four names to
· packages/core/src/theme/derivedVarRegistry.test.ts:293-302
Both sides simply appended to VARS_WITHOUT_DERIVED_MAPPING; both entries survive the merge.
GitHub reports mergeable: false / dirty and git merge-tree returns one content conflict in
that file and nothing else. No [BLOCKS] line, which is the test that this is not
request-changes.
GOAL: met — the stated problem was registration landing at first mount. Driven on both arms:
"" before, "0px" after, with zero spinners in the DOM.
AUTHOR CAN PROCEED: yes — merge main and keep both sides of the appended list. Acceptance:
mergeable returns to clean and test stays green. No decision was withheld from him.
WORST OUTCOME: "A spinner is the loading indicator, so that lands on a page that is already rendered, with someone already waiting" — our own round-4 sentence, and it no longer describes the code at this head. No slot contains a sentence about a user or builder being harmed.
Judgement needed: none. Both var pairs match landed members; the class is established by citation rather than settled by taste.
Thanks — module scope is it: registration lands at import with no spinner mounted, and defaults are pixel-identical. Needs a merge with main; #5551 touched the same list.
[Reviewed by Robohands]
27 words. Collaborator bucket, so no Discord clause and no full-review link.
Not posted — this round was drafted read-only and nothing was published to the PR.



