Skip to content

fix(shell): track renderer readiness per iframe so concurrent surfaces stop clobbering each other - #252

Merged
mikzat merged 6 commits into
a2ui-project:mainfrom
mikzat:per-frame-renderer-readiness
Oct 8, 2026
Merged

mikzat merged 6 commits into
a2ui-project:mainfrom
mikzat:per-frame-renderer-readiness

Conversation

@mikzat

@mikzat mikzat commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Description

The /a2a route mounts several renderer iframes at once: one per inline chat surface plus the side canvas. HostCommunication tracked renderer readiness with a single global flag, reset that flag and wiped the whole outbound queue whenever any iframe registered, and sendMessage read the flag inside the components' effects. Opening the canvas therefore briefly flipped every frame back to "not ready", re-ran every RenderedFrame effect, and queued and replayed the inline surfaces' payloads. A2aChatView compounded this by also sending the canvas payload with no target, which goes to the most recently registered iframe.

Observable symptoms on /a2a against an agent whose surface contains a Canvas card:

  • First canvas open: the canvas content is drawn inside the inline chat bubble for roughly 600 ms (until the canvas iframe finishes its handshake), after which the inline surface is recreated and loses its data model (DATA_MODEL_CHANGE resets to {}).
  • Canvas already open, newer turn arrives: when a later turn mounts its own inline frame and the user then opens that turn's Canvas card, no new canvas iframe mounts, the readiness flag never toggles, and the newer inline surface is overwritten with the canvas content for good.
  • Every canvas open also costs a burst of redundant RENDER_A2UI/SET_THEME dispatches to frames that did not change.

What changes

Readiness is now recorded per guest window:

  • A message is posted as soon as its own frame has announced RENDERER_READY (or sent an MCP_REQUEST), regardless of siblings that are still loading, and a frame's RENDERER_READY flushes only the messages addressed to it (plus untargeted ones, as before). Messages for frames that are still handshaking stay queued instead of being replayed at the newest frame.
  • registerIframe discards only that frame's previous readiness and queued messages; frames that are already up keep both. registerIframe(null) still clears everything.
  • unregisterIframe withdraws the frame's readiness and drops its queued messages, and does so even when the element has already left the DOM (its contentWindow is recorded while it is attached).
  • A target the service does not know about (an explicit, unregistered window) keeps the previous single-flag behaviour.
  • sendMessage reads readiness through untracked(), so a frame's handshake cannot become a dependency of the component effects that call it.
  • The handshake theme goes only to the frame that announced itself. sendTheme() (theme switches) posts to each registered frame that is up and skips frames still handshaking, which receive the current theme on their own RENDERER_READY; the direct postMessage loop is gone. An MCP_REQUEST from a frame marks it ready and flushes its queued messages, the same as a handshake.
  • The two untargeted sendRenderA2UI effects in A2aChatView are removed; the canvas RenderedFrame already sends the payload to its own iframe and re-sends it on that iframe's RENDERER_READY/A2UI_CATALOG.

One existing spec changed meaning: it expected a message queued for frame A to be delivered when a different frame B became ready. It now expects delivery on frame A's own handshake, since a renderer that has not announced itself drops the message anyway. The specs assert on what each guest window receives via postMessage rather than on the service's private queue.

No issue was filed for this beforehand.

Verification

Rebased onto main at 5b10985.

Pre-launch Checklist

  • I signed the CLA.
  • I read the Contributors Guide.
  • I read the Style Guide.
  • I have added updates to the CHANGELOG. (The changelog has not been maintained per PR in this repo; happy to add an entry if wanted.)
  • I updated/added relevant documentation. (Not applicable: internal service behaviour, no public API change.)
  • My code changes (if any) have tests.
  • If my branch is on fork, I have verified that scripts/e2e_test.sh passes. (Not run: it needs a GEMINI_API_KEY, which I do not have in this environment. The Playwright suite including the new journey was run directly.)

If you need help, consider asking for advice on the discussion board.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the HostCommunication service to support multiple concurrent iframe renderers by tracking readiness per window and managing queued messages independently. The review feedback highlights two issues in host-communication.ts: a potential bug in isTargetReady where a nullish target could cause messages to be silently dropped if another iframe is already ready, and an unconditional call to set the renderer readiness signal to true in markWindowReady even when the source window is null.

@mikzat
mikzat marked this pull request as ready for review October 5, 2026 17:50
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

⚡ A2UI Composer PR Preview

Your automated preview is successfully live (commit 39aebe6):
👉 Launch PR Preview

Note: This environment will be wiped automatically when the PR is merged or closed.

@jgindin jgindin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the diagnosis is spot on and per-window readiness is a clean way to fix it. Nothing blocking from me; a few suggestions are inline.

One general point, since it has no natural line to attach to: there's no Playwright coverage for the multi-frame scenario. The repo's testing guidelines ask for user journeys to be covered by Playwright (yarn e2e-headless in shell/). The headless-Chrome before/after in the description is convincing, but it's a manual check. A local stub A2A agent like the one you used for that check (one inline surface plus one Canvas card per turn) could turn the scenario into an e2e test that needs no GEMINI_API_KEY. The checks would be: open canvas #1, send turn 2 while it's open, open canvas #2, then confirm each inline frame keeps its own content. That would guard against this regressing.

mikzat added 4 commits October 7, 2026 14:58
…s stop clobbering each other

The /a2a route mounts several renderer iframes at once: one per inline chat
surface plus the side canvas. HostCommunication tracked readiness with a
single global flag, reset it and wiped the whole outbound queue whenever any
iframe registered, and sendMessage read that flag inside the components'
effects. Opening the canvas therefore briefly flipped every frame back to
"not ready", re-ran every RenderedFrame effect, and queued and replayed the
inline surfaces' payloads. A2aChatView compounded this by also sending the
canvas payload with no target, which goes to the most recently registered
iframe: on first open that is still the inline frame, so the canvas content
is drawn inline for roughly 600 ms and the inline surface is then recreated
(losing its data model); when the canvas is already open and a newer turn
has mounted its own inline frame, that inline surface is overwritten for
good.

Readiness is now recorded per guest window. A message is posted as soon as
its own frame has announced RENDERER_READY, regardless of siblings that are
still loading, and a frame's RENDERER_READY flushes only the messages
addressed to it (plus untargeted ones, as before). Registering a frame
discards only that frame's previous readiness and queued messages, and
unregisterIframe releases the window and its queued messages even when the
element already left the DOM. The duplicate untargeted sendRenderA2UI
effects in A2aChatView are removed; the canvas RenderedFrame already sends
its payload to its own iframe. The one spec that expected a frame's queued
message to be delivered when a *different* frame became ready now expects
delivery on that frame's own handshake, since a renderer that has not
announced itself drops the message anyway.
…re sourceless RENDERER_READY

Follow-up to the per-frame readiness tracking, addressing two review
comments.

An untargeted send resolves to the default (most recently registered)
frame. When that frame's element is registered but momentarily has no
window, `isTargetReady` previously fell through to the overall readiness
flag, so with a ready sibling the message was handed to `sendMessage`
with no window to post to and dropped. The default is now resolved up
front and handled like an explicit target, so the message waits for the
frame to announce itself.

A RENDERER_READY without a source window can no longer flip the overall
readiness flag, keeping it consistent with the per-window bookkeeping.
Real guest frames always carry a source; only synthetic events lack one.
…ad readiness untracked in sendMessage

Follow-ups from review:

- RENDERER_READY now sends SET_THEME to the frame that announced itself
  instead of calling sendTheme(), which broadcast to every frame on every
  handshake (and, for the default frame, through the queue as well, so a
  second frame's handshake re-sent the theme to the first). sendTheme()
  itself now posts to each registered frame that is up and skips frames
  still handshaking, since those receive the current theme on their own
  RENDERER_READY; a queued copy would only arrive as a duplicate. The
  direct-postMessage loop is gone; everything goes through sendMessage.
- MCP_REQUEST marks the source frame ready and flushes the queue, the same
  as a handshake, so a frame whose RENDERER_READY was missed does not wait
  for some other frame's handshake to receive its queued messages.
- sendMessage reads readiness through untracked(). Callers run inside
  component effects (RenderedFrame's payload and theme effects); readiness
  must not become a dependency of those effects or every frame's handshake
  would re-run them and re-send their payloads.
- Docs: isRendererReady is described as the aggregate it is,
  resolveTargetWindow names the default frame precisely, and the
  registerIframe/unregisterIframe comments say why untargeted queued
  messages are dropped on unregister.
- Specs assert on what each window receives via postMessage instead of
  reaching into the private buffer, and cover the effect-dependency,
  per-frame theme, and MCP_REQUEST flush cases. The A2aChatView spec now
  asserts the canvas payload reaches the side-canvas iframe and that no
  canvas payload is ever sent without a target.
Adds e2e/a2a-multi-frame-surfaces.e2e.ts. A stubbed agent answers each
turn with a surface whose root Column holds an inline Text and a Canvas
card (autoOpen: false). The test sends turn one, opens its canvas, sends
turn two, opens that canvas, and then checks every frame's document:
each inline frame shows its own "INLINE SURFACE #n" and the side canvas
shows "CANVAS CONTENT a2ui-project#2".

On main the last step fails: the second inline frame shows "CANVAS
CONTENT a2ui-project#2", because the canvas payload is dispatched to whichever frame
registered last. On this branch it passes (3/3 repeats).

connectMockAgent/setupMockA2aRoutes take an optional per-request
responder so a test can script turn-specific replies; the existing
keyword routing is unchanged when it is omitted. sseEvent is exported for
building those replies.
@mikzat
mikzat force-pushed the per-frame-renderer-readiness branch from 656faf2 to 9bcf649 Compare October 7, 2026 22:29
@mikzat

mikzat commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Rebased onto main (5b10985) and pushed two follow-up commits; inline replies are on each thread.

On the Playwright point: added shell/e2e/a2a-multi-frame-surfaces.e2e.ts. The agent is a Playwright route stub (via connectMockAgent, which now takes an optional per-request responder), so it runs under the regular yarn e2e setup with no GEMINI_API_KEY. Each turn returns a surface with an inline Text ("INLINE SURFACE #n") and a Canvas card ("CANVAS CONTENT #n", autoOpen: false). Steps: turn 1 → open its canvas → turn 2 while the canvas stays open → open turn 2's canvas, then read each frame's document.

Commits: a5ddf05 (handshake theme only to the announcing frame, untracked readiness read in sendMessage, MCP_REQUEST flush, doc and spec changes from the threads) and 9bcf649 (the e2e journey). Full vitest run: 80 files / 1708 tests.

The two items I left as follow-ups (double RENDER_A2UI on handshake, readiness across an iframe reload) I can file as issues if you'd like them tracked.

…s return-type convention

- A RENDERER_READY without a source window already did not mark anything
  ready, but still went on to send the handshake theme untargeted, which
  queued a stray SET_THEME for the default frame. The whole branch now
  requires a source window. The spec for this case now drives the
  detached default frame through its real handshake afterwards and
  checks it receives the theme exactly once; without the guard it
  received two.
- Reuse envelope.sourceWindow instead of computing the source a second
  time.
- The private methods added in this PR carry explicit return types like
  the rest of the file.
- The e2e journey's first-open assertions are documented as a
  steady-state check: the brief flash the old code showed until the
  canvas frame's handshake is not something a retrying assertion can pin
  down; the permanent variant of the misrouting is what the test guards.
@mikzat

mikzat commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

One more small commit (6fd5952) from a self-review pass, nothing new in behaviour:

  • A RENDERER_READY without a source window is now a complete no-op. It already did not mark anything ready, but it still sent the handshake theme untargeted, which queued a stray SET_THEME for the default frame. The spec for that case now walks the detached default through its real handshake afterwards and checks it gets the theme once (two without the guard).
  • envelope.sourceWindow is reused instead of recomputed; the private methods this PR adds have explicit return types like the rest of the file.
  • The e2e's first-open assertions are documented as a steady-state check; the permanent misrouting is what the test guards.

The canvas payload dispatch specs located the side canvas iframe with a
By.css query. A2aChatViewHarness now exposes the RenderedFrame inside the
side canvas viewport, RenderedFrameHarness hands out its guest iframe, and
the specs resolve the element from there (via
TestbedHarnessEnvironment.getNativeElement, since the assertion is about
which element the host posts to).
@mikzat
mikzat merged commit 79bf175 into a2ui-project:main Oct 8, 2026
13 checks passed
jerelvelarde added a commit to jerelvelarde/a2ui-composer that referenced this pull request Oct 9, 2026
…mo frames

a2ui-project#252 tracks renderer readiness per guest window, which makes most of
the HostCommunication additions this PR used to work around the global
readiness flag unnecessary:

- sendToFrame is removed. DemoCard sends its demo with sendRenderA2UI
  addressed to its own iframe; HostCommunication marks the frame ready
  before subscribers see its RENDERER_READY, so the payload is posted
  at once. DemosCatalog sends GET_DEMOS with sendMessage addressed to
  the coordinator, and only after the coordinator reported ready. A
  request sent earlier would now be held and posted on that handshake,
  in addition to the one the handshake sends.
- messageStreamFor is removed. DemoCard subscribes to messageStream$
  and already ignores envelopes from other windows.
- unregisterSecondaryIframe is removed in favour of unregisterIframe,
  which also withdraws the frame's readiness and queued messages and
  never leaves the default target on an unregistered frame.
- registerSecondaryIframe is replaced by registering with
  DefaultTarget.KEEP. Registering a frame otherwise makes it the
  default target: the frame that messages sent without a target go
  to. CatalogManagement answers every frame's RENDERER_READY with an
  untargeted GET_CATALOG, so with each card taking over as it mounted,
  the request would go to the newest card and be dropped when the
  next one registered, ending in the catalog watchdog error. KEEP
  leaves the coordinator as the default; everything else about the
  registration is unchanged.

The origin-parsing helper this PR extracted is dropped too, so
HostCommunication now differs from main only by DefaultTarget.
jerelvelarde added a commit to jerelvelarde/a2ui-composer that referenced this pull request Oct 16, 2026
…mo frames

a2ui-project#252 tracks renderer readiness per guest window, which makes most of
the HostCommunication additions this PR used to work around the global
readiness flag unnecessary:

- sendToFrame is removed. DemoCard sends its demo with sendRenderA2UI
  addressed to its own iframe; HostCommunication marks the frame ready
  before subscribers see its RENDERER_READY, so the payload is posted
  at once. DemosCatalog sends GET_DEMOS with sendMessage addressed to
  the coordinator, and only after the coordinator reported ready. A
  request sent earlier would now be held and posted on that handshake,
  in addition to the one the handshake sends.
- messageStreamFor is removed. DemoCard subscribes to messageStream$
  and already ignores envelopes from other windows.
- unregisterSecondaryIframe is removed in favour of unregisterIframe,
  which also withdraws the frame's readiness and queued messages and
  never leaves the default target on an unregistered frame.
- registerSecondaryIframe is replaced by registering with
  DefaultTarget.KEEP. Registering a frame otherwise makes it the
  default target: the frame that messages sent without a target go
  to. CatalogManagement answers every frame's RENDERER_READY with an
  untargeted GET_CATALOG, so with each card taking over as it mounted,
  the request would go to the newest card and be dropped when the
  next one registered, ending in the catalog watchdog error. KEEP
  leaves the coordinator as the default; everything else about the
  registration is unchanged.

The origin-parsing helper this PR extracted is dropped too, so
HostCommunication now differs from main only by DefaultTarget.
jerelvelarde added a commit to jerelvelarde/a2ui-composer that referenced this pull request Oct 16, 2026
…mo frames

a2ui-project#252 tracks renderer readiness per guest window, which makes most of
the HostCommunication additions this PR used to work around the global
readiness flag unnecessary:

- sendToFrame is removed. DemoCard sends its demo with sendRenderA2UI
  addressed to its own iframe; HostCommunication marks the frame ready
  before subscribers see its RENDERER_READY, so the payload is posted
  at once. DemosCatalog sends GET_DEMOS with sendMessage addressed to
  the coordinator, and only after the coordinator reported ready. A
  request sent earlier would now be held and posted on that handshake,
  in addition to the one the handshake sends.
- messageStreamFor is removed. DemoCard subscribes to messageStream$
  and already ignores envelopes from other windows.
- unregisterSecondaryIframe is removed in favour of unregisterIframe,
  which also withdraws the frame's readiness and queued messages and
  never leaves the default target on an unregistered frame.
- registerSecondaryIframe is replaced by registering with
  DefaultTarget.KEEP. Registering a frame otherwise makes it the
  default target: the frame that messages sent without a target go
  to. CatalogManagement answers every frame's RENDERER_READY with an
  untargeted GET_CATALOG, so with each card taking over as it mounted,
  the request would go to the newest card and be dropped when the
  next one registered, ending in the catalog watchdog error. KEEP
  leaves the coordinator as the default; everything else about the
  registration is unchanged.

The origin-parsing helper this PR extracted is dropped too, so
HostCommunication now differs from main only by DefaultTarget.
jgindin pushed a commit that referenced this pull request Oct 19, 2026
* feat(demos): add an interactive bento demo wall

Browse live examples from the selected renderer in a responsive bento
grid, then open one in Composer to edit its A2UI. Renderers serve their
demos over the bridge (GET_DEMOS / getDemos, returning a name,
description, and A2UI messages for each), mirroring
GET_COMPONENT_USAGES, so the shell stays renderer-agnostic.

- Angular, Lit, and React provide the same four shared showcases and
  43 pinned catalog examples; shared JSON sources are validated before
  generating renderer modules.
- Bundled adapters report when their first surface has rendered, so the
  wall reveals real content without a fixed measurement wait; older
  external adapters keep the measurement fallback, and a renderer that
  never finishes reports an error.
- Sandboxed frames mount lazily with a cap, replies are filtered by
  source window, and wide previews are fitted without clipping.
- Open in Composer loads the example through the shared-design route.

* fix(bridge): coalesce SURFACE_RESIZE bursts and drop repeated reports

The demo protocol's readiness reports (contentReady false, then true)
add to the reports a renderer sends while its first surface settles.
With React's layout passes that burst exceeded the host's feedback-loop
cadence bound. The bridge now sends the first report of a burst at once,
coalesces the rest into one trailing report per 16ms carrying the latest
size, and drops a report identical to the last one sent. A readiness
change still differs from the last report, so it is always delivered.

The demo e2e now waits for the workspace preview to settle before
editing an opened demo, and compares card row positions to within half
a pixel instead of exactly.

* refactor(demos): pass loggers through the validator and URL helpers, as #245 does

#245 routes shell logging through ErrorLogger and makes CrossFrameValidator
take a logger. The demo card, the demo frame dispatch, and the new message
checks now pass theirs, and buildRendererUrl takes the caller's logger.

* refactor(bridge): name the resize steps and pass sandbox options as one object

Addresses review feedback on the bridge:
- Rename flushSurfaceResize to sendSurfaceResize, and document the three
  steps a SURFACE_RESIZE report goes through (measure, coalesce, send) and
  what `force` is for.
- Explain the 16ms coalescing window (one 60 Hz frame), why the measurement
  after every render isn't forced while the one after a reset is, and why
  renderers without the readiness hook still get measured.
- Rename RendererConfig.whenSurfaceRendered to onInitialRender, and describe
  renderers without it instead of calling them "legacy".
- A2uiSandboxConnection takes the AngularSandboxOptions object rather than
  four positional parameters, so argument order no longer matters.
- Fix the type errors in the new bridge specs.

* refactor(demos): share one generated demo list across the sample renderers

Addresses review feedback on the sample demos:
- The generator writes one samples/shared/demos/demos.json instead of three
  identical demos.ts modules, and the Angular, Lit, and React samples import
  it through samples/shared/demos/index.ts.
- JSON carries no license header, so the generator no longer copies one out
  of bridge-message.ts; index.ts has an ordinary header that addlicense
  checks like any other file.
- The README explains how the four hand-written showcase demos relate to the
  generated list: they lead it, followed by the 43 upstream basic-catalog
  examples.

* refactor(demos): use enums for card state and rewrite the comments that were hard to follow

DemoCardState and MeasurementPhase are enums now, and the template reads a
hasError flag instead of comparing strings. Fields no longer carry their type in
the name (stateSignal, cardHeightSignal, coordinatorSignal, mountedKeysSignal).

Rewrote the demo catalog, card measurement and bento layout comments in plain
terms: what the coordinator iframe is, what a height report is, which window
or iframe each sentence means, and the bug the 32px floor guards against.

* refactor(demos): use per-frame readiness from #252 for demo frames

#252 tracks renderer readiness per guest window, which makes most of
the HostCommunication additions this PR used to work around the global
readiness flag unnecessary:

- sendToFrame is removed. DemoCard sends its demo with sendRenderA2UI
  addressed to its own iframe; HostCommunication marks the frame ready
  before subscribers see its RENDERER_READY, so the payload is posted
  at once. DemosCatalog sends GET_DEMOS with sendMessage addressed to
  the coordinator, and only after the coordinator reported ready. A
  request sent earlier would now be held and posted on that handshake,
  in addition to the one the handshake sends.
- messageStreamFor is removed. DemoCard subscribes to messageStream$
  and already ignores envelopes from other windows.
- unregisterSecondaryIframe is removed in favour of unregisterIframe,
  which also withdraws the frame's readiness and queued messages and
  never leaves the default target on an unregistered frame.
- registerSecondaryIframe is replaced by registering with
  DefaultTarget.KEEP. Registering a frame otherwise makes it the
  default target: the frame that messages sent without a target go
  to. CatalogManagement answers every frame's RENDERER_READY with an
  untargeted GET_CATALOG, so with each card taking over as it mounted,
  the request would go to the newest card and be dropped when the
  next one registered, ending in the catalog watchdog error. KEEP
  leaves the coordinator as the default; everything else about the
  registration is unchanged.

The origin-parsing helper this PR extracted is dropped too, so
HostCommunication now differs from main only by DefaultTarget.

* test(demos): drop try blocks and fixed waits from the demo tests

- The Lit and Angular bootstrap tests register their cleanup with
  onTestFinished instead of try/finally.
- The demos e2e init script only touches the Composer page's storage,
  so it no longer needs a try/catch for the renderer iframes.
- The bridge's GET_DEMOS tests wait for the DEMOS reply with vi.waitFor
  instead of sleeping 10ms.
- The demo wall spec's test IntersectionObserver queues the targets it
  observes and delivers them when the test asks, instead of on a 0ms
  timer.

* fix(demos): show one Flight status card on the demo wall

The wall showed two Flight cards: the "Flight status" showcase and the
specification's own "Flight Status" basic-catalog example. The generator
now drops a specification example when a showcase has the same name,
ignoring case, so a showcase replaces the example it's modelled on. The
wall has 46 demos instead of 47.

* feat(demos): fill the demo wall's rows without empty cards

The wall paired neighboring demos two to a row in reading order, which
left cards mostly empty in two ways.

A row is as tall as its taller card, so a short demo beside a tall one
was stretched to the tall one's height: Interactive Button sat in a card
three times its height beside Incremental's list, and four other rows
did the same. Now, when a pair's shorter tile leaves a gap, the tiles
that follow stack under it, in its column, while each brings the column
closer to the tall tile's height, up to three tiles; the tall tile spans
their rows. Sparse grid auto-placement puts each stacked card in the
next row beside the tall one, so reading order and keyboard order are
unchanged.

A demo that overflowed the narrower half of a row took a full row, even
when it fit the wider half, and so did the neighbor left without a
partner: Live Invitation Builder and Task Card need a little more than
the narrower half, and Movie Card and Music Player were left alone next
to them, so all four filled the full width with demos far narrower. A demo now
takes the wider half when it needs it, and a full row only when it
needs more than that. DemoCard reports the narrowest card that shows
its demo unscaled (minimumCardWidth, replacing needsWideLayout), and
the wall reads its own width and gap from the resize observer it
already has.

A tile still left without a partner, before a demo that needs a full
row or at the end of the wall, joins the row before it, three to a row,
when that row is a plain pair and all three demos fit a third of it.
Otherwise it takes a full row as before.

A tile that hasn't been measured yet is paired as before and never
stacked. On the Angular sample at 1440px, the emptiest card went from
about 30% filled to about 60%, and the only demo on a full row is Task
Card, whose content is wider than any card, as before.

* fix(demos): stop a demo that is always wider than its frame from shrinking

A card scales a demo that overflows its frame and widens the frame to
the content width it recorded, so the demo fits. A demo that is always
wider than its frame by the same amount then reported a wider width from
the widened frame, and the card recorded that too: the sample Task Card
is about 65px wider than any frame, and its recorded width grew from
1,347px to 1,919px in one session while its preview shrank.

Once a frame is scaled, the card now records a wider width only while
the demo converges: while its overflow shrinks, or repeats within 3px
(widths are rounded up to whole pixels), up to 20 times in a row. React's
invitation builder, which lays out wider in a wider frame, still settles,
overflowing by 45, 34, 26 and so on down to 3px. Task Card is scaled once
and stays put.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants