Skip to content

Review 4158

Cindy Zhang edited this page Aug 27, 2026 · 1 revision

Review 4158

PR

#4158 feat(lab): Image — content image with loading, error fallback, and preview (RFC #4094) by AKnassa (bucket: external contributor — not in ENGOWNERS/DESIGNOWNERS; gh api .../collaborators/AKnassa/permissionread)

HEAD REVIEWED

58906ddc74162818c14c737b7232fd6f88af4f3b

Refreshed at review time: the aged head a4e2b81d4923bab316dcf633b1d54f1144a63f36 (2026-07-21) was CONFLICTING against main 2458b03b35a45512b996b0c2c764d9530a36a052. One conflict, in packages/lab/src/index.ts: TransferList (main) and Image (this branch) were independent additions at the same insertion point. Both exports kept; no product, API or behaviour choice was made. Merge commit pushed non-force to AKnassa/astryx ak-4094-image; PR now MERGEABLE. Every claim below is at the merged head.

PRIOR REVIEW (R1e)

None. pulls/4158/reviews and pulls/4158/comments are both empty; the only issue comments are the Vercel bot and the PR Analysis Report. Nothing to extend or contradict.

VERSIONS

LOOP VERSION: 1.6.0 AUDIT RUBRIC: 1.13

LANE

LANE: full WHY: new public component, new exports, a new theme target, a new Effect, an intended visual surface, and the PR arrived conflicting. Fast requires none of those.

STEP 0 — is this safe to run?

Clean. gh pr diff --name-only at the merged head is 5 added files plus one barrel line: component, doc, test, story, index. No package.json script, no dependency, no lockfile entry, no .github/workflows/**, no new scripts//tools//.husky/ file, no env/~/.npmrc/~/.ssh read, no build-time network call. Installed and built.

PROBLEM

WHY 1: a builder placing a content image hand-writes the same <img> plus fill/cap/radius CSS, and gets no loading, error or zoom behaviour with it. WHY 2: so every surface skips the parts that are work — the box is not reserved, a broken source leaves a torn frame, and there is no view-larger — because each one costs its own code. WHY 3: the system exists so a common surface arrives correct by default; when the correct version is opt-in per call site, the default the product ships is the incorrect one.

USER-FACING PROBLEM: someone opening a page with a slow or broken remote image sees the layout jump as it lands, or a torn frame where it failed, because the builder had no first-party way to get the reserved box and the fallback without writing them. PROBLEM SEVERITY: missing capability — the RFC counts 99 raw <img> uses in this repo, 16 sharing an identical style block. Demonstrated in-repo, not asserted.

VERDICT: clear

SOLUTION

The component watches whether the picture it was told to show has arrived, failed, or is still coming. While it is still coming and a shape was declared, it holds that shape and shows a placeholder block, so nothing around it moves. If the picture fails it tries the spare address, and if that fails too it draws a neutral stand-in that still says what the picture was. Responsibility sits with the image itself rather than the page, so every surface gets the same three behaviours without writing them.

SOLUTION (3 decisions · ~300 runtime lines of 1831)

  1. loading and error are keyed to the exact source string, not to booleans, so swapping src/fallbackSrc re-attempts instead of reusing a stale verdict — serves WHY 1/2
  2. ratio selects a reserved AspectRatio box with fit; omitting it keeps intrinsic size capped by maxWidth — serves WHY 1
  3. hasPreview wraps the image in a button that opens a Lightbox, always named even for alt="" — serves the RFC's zoom case

BURDEN: low — one Effect, no listener, no observer, no timer, no shared-system coupling, no context. Public surface is one component and 11 props. The three "adjust state during render" prev-prop comparisons are React's own documented pattern, not a state machine. BURDEN MATCH: proportionate — three decisions that must ship together (a loading state with no error state is not the capability), against a demonstrated missing capability.

VERDICT: clear

ARCHITECTURE

OWNER: Image (lab) — the content-image concept; no existing owner claims it. TIER 1: reuses AspectRatio, Skeleton, Lightbox, mergeProps, mergeRefs, themeProps, radiusVars/colorVars. Nothing in the contract is rebuilt locally. TIER 2: none — no size scale, no status, no semantic icon (the placeholder glyph is inline decorative SVG, aria-hidden). SEAMS: the fallback render prop; the Lightbox's own top-layer portal; xstyle/className/style passthrough; consumer-controlled hasPreview. BEHAVIOR UNIT: inline — one Effect, no coordination between Effects, so the mechanical two-Effect threshold does not fire.

seam driven result
built-in placeholder works — 03-error-and-rescue.png; role="img" + aria-label present, aria-hidden for alt="" (probe 2 decorative placeholder)
custom fallback node works — Image.test.tsx:483 renders the consumer node; probe D shows the default path it replaces
Lightbox portal / top layer works — dialog opens in the top layer, Close focused, Escape returns focus to the trigger (probe E, F)
source fails under an OPEN preview works — dialog closes, focus re-anchors on the frame, tabindex=-1 retained (probe G focus anchor)
hasPreview flipped off under an OPEN preview fails — dialog closes and focus lands on <body> (probe 3 after flip-off: frameTabindex=null active=BODY activeIsBody=true)

The last row is the finding. Image.tsx:303-308 names this exact case — "When its subtree unmounts mid-open (source failure, hasPreview flip) … the removed dialog drops keyboard focus to <body>. Reset the state and re-anchor focus on the frame" — and the frame is only focusable while hasPreview is true (Image.tsx:437, tabIndex={hasPreview ? -1 : undefined}). On the flip, React removes the attribute in the same commit that unmounts the dialog, so rootRef.current?.focus() at :314 is a no-op. The failure branch, which keeps hasPreview true, works. Placement is right; the anchor's lifetime is not.

VERDICT: BLOCKS — the component's own stated focus guarantee does not hold on one of the two branches it names

IMPACT

Reaches nobody on main today — new lab export, canary-only, no consumer. It reaches (a) a builder adopting Image, who gets the reserved box, the fallback chain and a named zoom trigger for free; (b) a keyboard or screen-reader user of any surface that adopts it with a consumer-controlled hasPreview, who loses their place in the page if that prop goes false while the preview is open; (c) a screen-reader user in a non-English locale on a decorative previewable image, who hears an English trigger name.

Newly visible gap in a paired surface: none. Skeleton, AspectRatio and Lightbox are used as published.

VERDICT: note — real reach is limited to adopters; lab, canary, no consumer yet

API

The smallest real call site, from the PR's own story:

<Image src={SAMPLE} alt="Forest valley under fog" ratio={16 / 9} radius="container" />
change public? class doc'd? verdict
+ Image component barrel (packages/lab/src/index.ts) new concept, no sibling en+zh+dense ok
+ src: string, alt: string (both required) barrel matches img en+zh ok
+ ratio?: number, fit?: 'cover' | 'contain' barrel AspectRatio.ratio en+zh ok
+ radius?: 'none' | 'inner' | 'element' | 'container' | 'full' barrel radius token roles en+zh ok
+ maxWidth?: SizeValue barrel SizeValue reused en+zh ok
+ fallbackSrc?: string, fallback?: ReactNode barrel new pair en+zh ok
+ loading?: 'lazy' | 'eager' barrel native attr passthrough en+zh ok
+ hasPreview?: boolean, previewCaption?: ReactNode, previewLabel?: string barrel has* boolean convention en+zh ok
+ ImageProps, ImageFit, ImageRadius types barrel ok

R1g-api walk — can a builder set it to something untrue? fit is ignored without ratio, and both the prop doc and the dense doc say so; it is inert rather than lying. fallbackSrc === src and fallbackSrc === '' are both normalised to absent at Image.tsx:258, with a test at :504 proving the wedged-skeleton case that would otherwise follow. alt is required rather than optional, so decorative is an explicit "" and not an omission. Nothing here asks the consumer for something the component already knows.

OSSIFICATION: every line is a class, not a single case — a content image with a ratio, a fallback and a zoom is the shape the RFC counted 99 times. Cost of being wrong is bounded: lab is canary-only and published under @canary, so an export can still be withdrawn without a [breaking] changeset. No genuinely new system concept is introduced; radius roles, SizeValue and the has* boolean are all existing vocabulary.

VERDICT: clear

THEMING

One new target, astryx-image, via themeProps('image', {radius}) at Image.tsx:440 — painting element is the root frame <div>, which owns borderRadius: var(--_image-radius) and the overflow: clip. Naming matches <component-kebab>; radius reflects as data-radius and as a variant class, consistent with the other 20 lab targets. One private var, --_image-radius, defaulted to --radius-none at :33 and switched by radiusStyles, mirroring the --_card-radius pattern. Documented in both docs.theming and docsZh.theming with private: true and a derived entry. No existing target or value changes. No raw colour, no light-dark(), no hardcoded radius — colorVars/radiusVars throughout; the repo's no-raw-color and no-light-dark-outside-theme rules report nothing on this file.

One inconsistency, not a defect: fit is a visual prop and is emitted as a hand-written data-fit on the inner <img> (Image.tsx:359) rather than through the target, so theming.targets[0].visualProps lists only radius while the DOM also carries data-fit. A theme author reading the doc will not know data-fit is there.

VERDICT: note — data-fit is undocumented theme-adjacent surface emitted outside the target

BREAKING

BEHAVIOR: no — every file is new except one barrel line, which adds an export beside TransferList. No existing state, default or conditional is touched. Confirmed by the diff: A A A A A plus M packages/lab/src/index.ts. API: no — additive export only. pnpm -F @astryxdesign/lab exec vitest run src/Image is 59/59 green at the merged head, and the full local pnpm run build exits 0. VISUAL: yes, by construction — a new rendering component. Growth check does not apply to existing surface; nothing that already rendered grew. Frames below. THEME: no — one target added, none changed or removed.

VERDICT: clear

PERFORMANCE & RESOURCES

EFFECTS: one added, zero changed, zero moved, zero deleted.

+ useEffect  [isPreviewForceClosed]   Image.tsx:311   reads nothing, calls focus()
Effect + deps external system why render/handler cannot do it measured render cost lifetime + cleanup focused test
useEffect [isPreviewForceClosed] Image.tsx:311 DOM focus the trigger is a prop change or a load error, not an event on this component; focus must move after the dialog is out of the tree flat with N — recalcs 29/27/32 at N=1/10/20 vs 39/18/32 for a raw <img> grid; the body runs only in the force-close path nothing attached; no listener, timer, rAF or observer, so nothing to clean up. document listener count is 17 at N=1, 10 and 20 — constant, not per instance (raw grid: 8) Image.test.tsx:760 covers the source-failure branch only; no test covers the hasPreview branch, which is the branch that fails

RENDER: no reachable regression. The effect sets state only when a preview is force-closed, so the common path costs one registration per instance. LISTENERS/OBSERVERS: none added by Image. Census above proves the constant 17 is the harness plus the Lightbox dismissal stack, not per-instance growth. LAYOUT: no forced synchronous reflow — nothing reads offsetWidth/getBoundingClientRect in render or in the effect. AspectRatio reserves the box in CSS. BUNDLE: no new dependency. The PR Analysis Report on the PR carries the size delta.

One measured cost, stated as a count and not a clock. hasPreview mounts the Lightbox whenever hasPreview && activeSrc != null (Image.tsx:467), independent of isPreviewOpen, so every previewable image carries a closed <dialog> and a second full-size <img> that nobody opened:

arm frames <img> total closed <dialog> DOM nodes JS listeners image requests served
raw <img> grid, N=20 0 20 0 167 223 20
Image hasPreview, N=20 20 40 20 487 434 20
Image hasPreview, N=1 1 2 1 145 203 1

Requests are equal at 20, so there is no extra network cost — the second element resolves from cache. What scales is DOM: at N=20 the gallery the docs advertise carries 20 hidden dialogs and 20 extra decoded elements (dialogImgComplete=true, naturalWidth=1200). I did not measure resident memory, so I am not claiming a memory figure. Reproduction: ~/astryx/review-4158-probes/perf.cjs and requests.cjs against Probe/Image4158Perf (probe stories, not part of the PR).

This is the cost of a capability rather than the same outcome reached more expensively — a raw <img> has no preview at all — so it is a note, not a block. It is worth saying because mounting on open would cost nothing when closed.

VERDICT: note — previewable images mount a closed dialog and a second element each; DOM 167 → 487 at N=20, no extra requests

VISUAL EVIDENCE

VISUAL CHECK: manual frames required WHY: a new rendering component — pixels are intended to exist where none did. visual-acceptance is PENDING on this head and pr-a11y/pr-rtl are SKIPPED, so no gate covers the endpoints; every visible state below was driven locally at the exact head.

All frames: real Chromium, storybook dev on port 6241 in the exact-head worktree, every image response served locally (a deterministic 1200×800 gradient) because the shipped stories point at picsum.photos, which this host does not reach. Every frame was opened and looked at.

The three states the PR exists for

Loading (ratio box) Loaded Error: placeholder, and fallbackSrc rescue
Loading Loaded Error

The skeleton fills the reserved 16:9 box at the container radius and the frame does not move when the source lands — same box, both frames. In the error pair the two boxes are identical in size, so a failed source costs no layout.

Preview: the ring, and the open dialog

Trigger focused by keyboard Preview open
Focus ring Preview open

The ring is drawn on the un-clipped frame, not inside the overflow: clip box, and follows the frame radius — outlineStyle none → solid, 2px, offset 2px, in both colour modes (rgb(38,38,38) light, rgb(235,235,235) dark). This is the claim Image.tsx:39-49 makes, and it holds.

Direction, and the ratio-less path

RTL LTR (control) Intrinsic, settled
RTL LTR Intrinsic

RTL reverses the grid order and nothing else: all three frames 197×197 in both directions, cover/contain/full preserved. The intrinsic frame caps at maxWidth={240} and centres inside the Card.

Every difference labelled, per the intentional/unintentional rule:

difference verdict source
skeleton at container radius, no corners of its own intentional Image.tsx:455-457 comment, and the frame's own --_image-radius + overflow: clip
contain shows no visible corner rounding intentional the frame is transparent and the letterboxed image does not reach the corners; inherent to contain, not a defect
focus ring on the frame rather than the button intentional quoted from the PR body: "shows its focus ring on the un-clipped frame"
ratio-less frame renders nothing while in flight unintentional as documented — see below the PR body says "shows a skeleton while it loads" with no qualification

The one frame the sensor refused, and why that is the evidence

06 has no receipt. The harness fails closed on geometry, and it refused:

Geometry sensor failed: {"x":330,"y":260,"width":240,"height":0,"insideViewport":true}

In ratio-less layout the in-flight frame is 240×0 — no reserved height and no skeleton — while still reporting aria-busy="true" (probe 4 intrinsic in-flight: w=240 h=0 busy=true skeletons=0). The illustrative frame shows the Card collapsed to a hairline: Intrinsic in flight

Skeleton is rendered only inside the ratio != null branch (Image.tsx:451-458), so the ratio-less path advertises a busy state with nothing to perceive, and the box jumps from 0 to full height when the source lands. The docs' first best practice already steers to ratio for exactly this — "Set ratio so the box is reserved and layout never shifts" — so the behaviour is intended; what is not qualified is usage.description, which promises "a skeleton while it loads" for both paths. Image.test.tsx:212 locks the current behaviour in.

SENSOR RECEIPT: build 58906ddc74162818c14c737b7232fd6f88af4f3b (asserted by captureWithSensors against the worktree HEAD, all 8 receipted frames) · stories lab-image--showcase, --error-fallback, --with-preview, --intrinsic-max-width, --ratio-and-fit · theme neutral · colour mode light · direction ltr (frame 08: rtl, via Storybook's direction global, asserted) · viewport 900×520 @1 · media forcedColors:false reducedMotion:false coarsePointer:false hover:true · semantic state asserted per frame as busy/skeleton/img/placeholder/trigger/dialog/focusRing · geometry visible and inside viewport · fonts loaded · zero page and Storybook errors, except frame 03 where one net::ERR_FAILED was filtered because the deliberately failed source is that frame's subject (1 filtered, 0 remaining — banked in the probe output). Receipts: https://raw.githubusercontent.com/cixzhang/astryx/assets/pr-4158/<frame>.png.sensors.json. Exact-head link: https://github.com/facebook/astryx/blob/58906ddc74162818c14c737b7232fd6f88af4f3b/packages/lab/src/Image/Image.tsx

VERDICT: note — every intended state renders correctly; the ratio-less in-flight frame reserves nothing while claiming aria-busy, and the usage text does not qualify the skeleton promise

REMEDY SEARCH

REMEDY SEARCH: not triggered — author owns HOW; acceptance criteria are sufficient

The one blocking behaviour (focus to <body> on a hasPreview flip) has a settled owner, a settled public shape and no uncovered visual treatment. Nothing here is a system or design choice.

A11Y & I18N

Driven in real Chromium at the exact head, not read off the source.

check result how
A1 accessible name on every interactive element pass — checked by me the trigger takes its name from the inner alt; with alt="" Chromium's own AX tree computes View larger image from the attribute (probe 1 AX name)
A2 APG pattern named pass — checked by me it is a button that opens a dialog; aria-haspopup="dialog" present, and the dialog is Lightbox's existing implementation, not a reinvention
A3 state exposed via ARIA, not colour alone pass — checked by me aria-busy on the frame while loading; role="img" + aria-label on the error placeholder
A4 full keyboard support pass — checked by me Tab reaches the trigger, Enter and Space both open, Escape closes (probe F tabbed/entered/escaped/spaced)
A5 focus never dropped to <body> FAIL — checked by me hasPreview off with the preview open → activeIsBody=true (probe 3 after flip-off). Source-failure branch passes: focus re-anchors on the frame (probe G focus anchor, tabindex=-1)
A8/A9 nothing hover-only pass — checked by me the trigger is a real <button>, reachable by Tab; nothing is revealed on hover
A13 entry/exit animation guarded pass — checked by me under prefers-reduced-motion: reduce the skeleton's animationName is none and document.getAnimations() running count is 0 (probe I reduced-motion)
A14 painted state survives forced colors pass — checked by me under forced-colors: active the placeholder paints bg rgb(255,255,255), color rgb(0,0,0), glyph still laid out (probe 6 forced-colors)
A15 focus ring visible in every theme, on the owner pass — checked by me frame 04; solid 2px in both light and dark, on the frame rather than inside the clipped media
A16 / I1 AT strings through useTranslator() FAIL — checked by me 'View larger image' is a literal at Image.tsx:389. packages/lab siblings TransferList.tsx:416, TransferListSelector.tsx:214 and ListInput.tsx:666 all call useTranslator(), and Lightbox.tsx does too; core/locales/en.json already carries 20 @astryx.transferList.* keys. No @astryx.image.* key exists
A18 axe green without new baseline entries auto-covered, not run pr-a11y is SKIPPED on this head; the diff adds no baseline entry (.github/a11y-baseline.json untouched)
I4/I5/I6 key shape, en.json entry, ICU n/a — no catalog key is added, which is the A16 finding above
I8–I12 logical properties and values, mirroring pass — checked by me insetInlineStart, marginInline only; no physical inset, no translate, no directional glyph. RTL frame 08 against LTR control 09: identical geometry, order reversed

Two failures. The A5 one is the block: a keyboard user whose preview is closed out from under them lands on <body> and their next Tab restarts at the top of the document. The A16 one does not strand anyone — the button still has a name — but it is the only user-facing string the component emits and it cannot be translated.

VERDICT: BLOCKS — focus reaches <body> on the hasPreview branch the code claims to handle

JUDGEMENT

slot verdict
PROBLEM clear
SOLUTION clear
ARCHITECTURE BLOCKS — stated focus guarantee fails on the hasPreview branch
IMPACT note — lab/canary, no consumer yet
API clear
THEMING note — data-fit emitted outside the documented target
BREAKING clear
PERFORMANCE note — closed dialog + second element per previewable image; DOM 167 → 487 at N=20
VISUAL note — ratio-less in-flight frame reserves nothing while claiming aria-busy
A11Y & I18N BLOCKS — focus reaches <body>; and the one AT string is untranslatable

Plus two required checks red at this head, both established by running them locally:

  • buildbuild-storybookTypecheck lab docs: Image.doc.mjs(3,152,284) error TS2307: Cannot find module '../../core/src/docs-types'. The module has never existed. The repo's spelling is import('@astryxdesign/cli/authoring')1277 uses across the repo against these 3, and TranslationDoc is spelled ComponentTranslationDoc. Rewriting the three annotations makes pnpm -F @astryxdesign/lab run typecheck:docs exit 0; verified in the worktree and reverted.
  • lint: Image.tsx:82 error @astryx/disabled-cursor. It is the only error in the repo — the other 87 problems are pre-existing warnings in files this PR does not touch. Applying the shape the rule itself prints makes eslint packages/lab/src/Image/Image.tsx exit 0; verified and reverted.

Both are main moving under an aged PR, not author error. At the original head a4e2b81d (2026-07-21) Lint and CI both completed success. packages/lab's typecheck:docs landed 2026-07-27 in #4435, six days later; @astryx/disabled-cursor landed 2026-08-22 in #5335, a month later. Neither gate ever ran on this code.

7d — do the findings share a parent? The two red checks do: this PR was green in July and two gates arrived since. That is the review's frame and the reason the tone is "here are the three anchors" rather than "this needs work". The focus defect is not a child of it — it is an independent behaviour gap that has been there since July and that no gate would have caught, because it is a keyboard path with no test.

GOAL: partly met — the three behaviours the PR set out to add all work, driven and framed above; the component's own stated focus guarantee holds on one of the two branches it names, and two required checks are red. DISPOSITION:

  • focus to <body> on the hasPreview flip → blocks now; acceptance criterion below
  • Image.doc.mjs module path → blocks now (required build); exact replacement cited
  • Image.tsx:82 cursor guard → blocks now (required lint); rule prints the shape
  • 'View larger image' untranslatable → follow-up, stated as a nit in the review
  • data-fit outside the documented target → accepted for lab; not worth a round trip on a canary component
  • closed dialog per previewable image → accepted as the capability's cost; no extra requests, flat listener count
  • ratio-less in-flight box reserves nothing → accepted; the docs already steer to ratio, though usage.description overpromises

ADVICE: proven existing pattern — @astryxdesign/cli/authoring (1277 landed uses) for the doc types; the @astryx/disabled-cursor rule's own printed shape for the cursor. For the focus defect, outcome criteria only, no implementation: after hasPreview goes false while the preview is open, focus must land on an element this component owns, and document.activeElement must not be <body>; a focused test must cover that branch as Image.test.tsx:760 covers the failure branch. AUTHOR CAN PROCEED: yes — all three asks are outcome-stated with an existing pattern or a printed rule; nothing waits on a human decision. WORST OUTCOME: "A keyboard user viewing a preview has it closed out from under them by a prop change and lands on <body>, so their next Tab restarts at the top of the page." → request changes

JUDGEMENT NEEDED: none

request changes

  1. Image.doc.mjs:3,152,284 names a module that does not exist, so build-storybook fails → any maintainer trying to merge this is blocked by a red required check; the repo spells it @astryxdesign/cli/authoring · packages/lab/src/Image/Image.doc.mjs:3
  2. hasPreview turned off while the preview is open drops focus to <body> → a keyboard user mid-preview loses their place in the page and Tab restarts at the top; the frame stops being focusable in the same commit that unmounts the dialog · packages/lab/src/Image/Image.tsx:437
  3. cursor: 'zoom-in' is missing the disabled guard the repo's lint rule requires → lint is red, and it is the only error in the repo · packages/lab/src/Image/Image.tsx:82

REVIEW

Thanks — keying the fallback chain off which source failed, rather than a boolean, is the part most versions of this get wrong.

This was green when you pushed in July. I've merged main in to clear the conflict, and two gates that landed since now fail on your files:

  • Image.doc.mjs:3,152,284import('../../core/src/docs-types') doesn't resolve. Everywhere else uses @astryxdesign/cli/authoring, with ComponentTranslationDoc for the dense one. build-storybook red.
  • Image.tsx:82cursor: 'zoom-in' wants the disabled guard the rule prints. lint red.

The behavioural one: turn hasPreview off while the preview is open and focus lands on <body>. The comment at Image.tsx:303 says the frame catches this, but tabIndex={-1} is already gone by the time focus() runs. The source-failure half works.

'View larger image' could go through useTranslator() like TransferList does, whenever.

I can push the two one-liners — do you want to take the focus one?

INLINE (0–3, only when genuinely line-anchored)

  • packages/lab/src/Image/Image.doc.mjs:3/** @type {import('../../core/src/docs-types').ComponentDoc} */ — this path doesn't resolve; everywhere else uses @astryxdesign/cli/authoring.
  • packages/lab/src/Image/Image.tsx:437tabIndex={hasPreview ? -1 : undefined} — the anchor disappears on the same render the flip force-closes on.
  • packages/lab/src/Image/Image.tsx:82cursor: 'zoom-in', — needs the disabled guard; the rule prints the shape.

EVIDENCE I DID NOT SPEND

  • 59/59 focused Vitest green at the merged head, and the full local pnpm run build exits 0 — the failures are the two gates only.
  • Effect cost is flat with N (recalcs 29/27/32 at N=1/10/20) and document listeners are constant at 17 — no leak.
  • Reduced motion and forced colors both pass, driven; the skeleton stops animating and the placeholder stays visible.

TIME

TIME total 27m setup 4m worktree + conflict resolve + push; fast-install 60s; warm main reused: yes reading 6m rules, diff, composed core components, prior-review check measuring 11m 4 probes (behaviour, states, frames, perf/requests), 8 receipted frames, 2 local gate reproductions writing 6m waste 1m --reporter=basic and -F lab vitest both wrong for this repo's vitest layout; page.accessibility gone from this Playwright — used CDP instead

WHAT I COULD NOT VERIFY

  • Resident memory for the per-instance closed dialogs; I measured element and node counts, not bytes, so no memory claim is made.
  • pr-a11y and pr-rtl are SKIPPED on this head, so CI's axe result is unavailable; the a11y rows above are all driven locally instead.

What changed before posting

Not posted — drafted read-only and handed to Cindy.

Clone this wiki locally