Markdown embed chooser: show a size-matched card placeholder while a pick loads#5602
Markdown embed chooser: show a size-matched card placeholder while a pick loads#5602FadhlanR wants to merge 2 commits into
Conversation
The right preview pane showed the "nothing selected" placeholder while a picked card/file was still loading, so a slow (cold) load read as "no selection" rather than "loading". Add a loading state (rendered when the load task is running, in place of the empty placeholder), and route an unexpected load rejection to the existing broken-ref visual so the pane can't spin forever. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Preview deploymentsHost Test Results 1 files 1 suites 3h 0m 48s ⏱️ Results for commit 3cfa8a1. For more details on these errors, see this check. Realm Server Test Results 1 files ± 0 1 suites ±0 13m 33s ⏱️ + 2m 18s Results for commit 3cfa8a1. ± Comparison against earlier commit 0dba7cc. |
…lder Instead of blanking the whole preview pane with a centered spinner, keep the pane (format selector + controls) mounted while a pick resolves and show a card-shaped placeholder in the embed slot: the resolved embed's footprint (fitted W×H, inline embedded/isolated footprint, or atom pill) with a small centered spinner. It reads the shared selection live, so changing the format resizes the placeholder. The CTA is disabled until the instance (or an error) lands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FadhlanR
left a comment
There was a problem hiding this comment.
[Claude Code 🤖] Reviewed for correctness of the loading state machine, the twin render paths in the preview component, and the concurrency handling in the load task — that's where a "keep the pane mounted while a pick resolves" change is most likely to go wrong.
Bottom line: no blocking issues. The change is sound and lands cleanly. One inert CSS line worth dropping (inline), plus a couple of coverage notes below. Safe to merge as-is.
What lands right
showPane = hasPreview || isLoadingcloses the flicker gap correctly.loadTargetclearsselectedTarget/selectedErrorsynchronously before its firstawait, andisLoading(loadTarget.isRunning) is already true by the time any render runs — so a superseding pick transitions resolved → loading placeholder with no intermediate frame of the empty placeholder or a stale embed. On completion,selectedTargetis set inside the task body before it returns (noawaitin between), sohasPreviewis true beforeisRunningflips false — no empty-placeholder flash on the way out either.- The
didCancel(e)rethrow is correct and matches the house convention.store.getreturnsPromise<T | CardErrorJSONAPI>— it resolves its own load failures rather than throwing them — so the non-cancel catch branch genuinely only fires on an unexpected throw, and routing that to the broken-ref visual is the right call (no infinite spinner). The rethrow-cancellation-inside-the-task-body shape mirrorsresources/search.ts(if (didCancel(err)) throw err;), so it's consistent with how the rest of the host handles restartable-task supersession. target?made optional +{{#if @target}}guard on both the inline (<span>) and block (<div>) render paths — the loading branch was added to both twins, so inline/block placement stays symmetric.Embedis module-local, so there are no external callers to break.- The size-matched footprint reuses the existing
sizeStyle/frame logic, so the placeholder tracks the live format/size selection — exactly the design goal.
Coverage notes (non-blocking, follow-up)
- The headline behavior — that the placeholder is size-matched (carries the resolved embed's footprint/format, not just a bare spinner) — isn't asserted; the tests only check that
[data-test-markdown-embed-preview-loading]exists. Consider asserting the loading element also carries the fittedstyle/frame class for the picked format so a future refactor can't silently drop the footprint. - The trickiest added logic — a superseding pick canceling an in-flight load (the
didCancelrethrow path) — has no test. A test that gates pick A, fires pick B before A resolves, then releases both and asserts B's target wins with no error state would pin that contract. ctaDisabledreturning true while@loadinghas no direct assertion; cheap to add alongside test 1.
Adjacent, out of scope
The working tree also has uncommitted edits to two experiments-realm JSON files and an untracked docs/cs-*-plan.md; none are in this PR's diff, just flagging so they don't get swept into a later commit — the plan doc in particular shouldn't land in merged history.
| /* Small enough to sit inside an atom pill, unobtrusive in larger frames. */ | ||
| .markdown-embed-preview__spinner { | ||
| --boxel-loading-indicator-size: 1.25rem; | ||
| } |
There was a problem hiding this comment.
[Claude Code 🤖] Non-blocking cleanup. This override is a no-op: LoadingIndicator sizes itself from var(--boxel-loading-indicator-size, var(--boxel-icon-sm)), and --boxel-icon-sm is defined as 1.25rem in boxel-ui's variables.css — so setting --boxel-loading-indicator-size: 1.25rem here just re-states the default. The rendered spinner is the same size with or without this rule (and the class='markdown-embed-preview__spinner' that carries it). Either drop both, or — if a distinct size was intended — pick a value that actually differs from the default (compare operator-mode/workspace-chooser/workspace.gts, which sets 2rem for a genuine override). The comment (“small enough to sit inside an atom pill”) also reads as if it shrinks the spinner, which it doesn't.
Background and Goal
In the markdown embed chooser modal, the right preview pane showed the empty "Search for a card/file & preview its format here" placeholder while a picked card/file was still resolving — so a slow (cold) load read as "nothing selected" rather than "loading". Reported in CS-12321.
Now, while a pick resolves, the pane stays mounted (format selector + placement/size controls) and its embed slot shows a size-matched card placeholder — the resolved embed's footprint (fitted W×H, inline embedded/isolated footprint, or atom pill) with a small centered spinner — which swaps to the real embed once it loads. The placeholder reads the shared format/size selection live, so changing the format resizes it. The CTA is disabled until the instance (or an error) lands.
Where to start
packages/host/app/components/markdown-embed-chooser/preview/index.gts— theEmbedprimitive renders a spinner in place of the card body when@loading, reusing the existingsizeStyle/footprint logic so the placeholder matches the eventual embed. Taggeddata-test-markdown-embed-preview-loading(and it omits the resolveddata-test-markdown-embed-previewwhile loading).packages/host/app/components/markdown-embed-chooser/pane.gts— forwards@loadingto the preview and holds the CTA disabled while loading.packages/host/app/components/markdown-embed-chooser/tab-panel.gts— renders the pane whenever a pick is loading, resolved, or errored (hasPreview || isLoading) so there's no jump to a bare spinner; the empty placeholder shows only before anything is picked.loadTargetis wrapped so a superseding pick's cancelation re-throws (newer load wins) while an unexpected rejection routes to the existing broken-ref visual instead of spinning forever.Tests
Two integration tests in
markdown-embed-chooser-modal-test.gts:Notes
Scope is the loading state only — Replace and Remove are unchanged from
main(Remove keeps its existing remove-and-close behavior).Screen Recording
Screen.Recording.2026-07-24.at.19.46.34.mov
🤖 Generated with Claude Code