Skip to content

Render the broken-ref visual in the markdown-embed chooser#5444

Open
FadhlanR wants to merge 8 commits into
mainfrom
broken-card-file-rendering
Open

Render the broken-ref visual in the markdown-embed chooser#5444
FadhlanR wants to merge 8 commits into
mainfrom
broken-card-file-rendering

Conversation

@FadhlanR

@FadhlanR FadhlanR commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Linear: CS-11678: Broken card / broken file rendering

Problem

A :card[...] / :file[...] reference that no longer resolves (deleted, moved, no permission) rendered the same empty "Search for a … & preview its format here" placeholder as a not-yet-picked slot. The chooser's loadTarget collapsed "still loading / nothing picked" and "resolution failed" into one selectedTarget = undefined, so a broken ref was indistinguishable from an empty pane — in the chooser preview and in the Edit modal.

Change

The chooser preview pane and the Edit modal now render the same rich broken-ref visual that broken linksTo / linksToMany field values already use: the placeholder box with a type/file label plus the reveal overlay carrying the URL, a copy button, and error diagnostics.

To reach that component from host with a plain static import (rather than a runtime module load), BrokenLinkTemplate is relocated from base to @cardstack/boxel-ui — the shared component package both base and host already consume, and whose scoped CSS is already shipped in the host bundle. Its three outside dependencies are decoupled so boxel-ui takes on no dependency on runtime-common (which would invert the existing runtime-common → boxel-ui edge into a cycle):

  • cardTypeName → a caller-supplied @typeName arg (base card sites pass the card type name; the file chooser passes the filename; falls back to 'Card').
  • SerializedError → a local BrokenLinkErrorDoc interface listing only the fields the overlay reads (SerializedError is a structural superset, so base callers pass one unchanged).
  • base ViewCardFn → a local BrokenLinkViewFn.

base (visual parity, no behavior change)

The five call sites (card-api, links-to-editor, links-to-many-component) switch to the boxel-ui import and pass @typeName={{cardTypeName reference}} — reproducing the label the component previously computed internally, so the shipped linksTo / linksToMany broken UI is unchanged.

host chooser (the ticket)

tab-panel tracks a selectedError, mounts the pane on target-or-error, and threads a broken state (url / state / type name / error doc) through pane into MarkdownEmbedPreview, which renders BrokenLinkTemplate in place of the embed. The Edit modal's existing Remove / Replace tile is the fix/remove affordance; there is no "Open anyway" in the chooser. The inline document render (rendered-markdown.gts) is left as-is.

Tests

  • New boxel-ui test-app suite for the relocated component (box + @typeName, reveal overlay + copy, not-found vs error) — passing locally (4/4).
  • broken-link-template-test re-pointed at the boxel-ui import + @typeName.
  • Broken-state cases added to the chooser suites: markdown-embed-preview (404 + error, card vs file label, no resolved embed), markdown-embed-preview-pane (broken preview surfaces; CTA still serializes the URL), markdown-embed-chooser-modal (Edit-mode broken preload shows the broken preview + Remove/Replace, no empty placeholder).
  • realm-indexing-test dep snapshots: dropped the two now-stale @cardstack/base/default-templates/broken-link-template lines (the link-off icon stays — still reachable via the boxel-ui bundle).
  • linksto-broken-link-placeholder-test / linksto-many-broken-link-placeholder-test are the regression guard for the shipped linksTo UI (unchanged type-name value).

Verified locally: glint/type-check clean (boxel-ui addon, host, test-app), eslint + template-lint clean, boxel-ui test-app suite green. Host integration tests rely on CI (they need the realm-server dev stack).

A `:card[...]` / `:file[...]` reference that no longer resolves (deleted,
moved, no permission) showed the same empty "search & preview" placeholder as
a not-yet-picked slot: the chooser collapsed "still loading" and "resolution
failed" into one undefined target. The chooser preview pane and the Edit modal
now render the rich broken-ref visual that broken linksTo / linksToMany field
values already use — the placeholder box with the type/file label plus the
reveal overlay carrying the URL, copy button, and error diagnostics.

To make that component reachable from host with a plain static import (rather
than a runtime module load), relocate `BrokenLinkTemplate` from base to
`@cardstack/boxel-ui`, the shared component package both base and host already
consume. Its three outside dependencies are decoupled so boxel-ui takes on no
dependency on runtime-common (which would invert the existing
runtime-common -> boxel-ui edge into a cycle):

- `cardTypeName` -> a caller-supplied `@typeName` arg (base card sites pass the
  card type name; the file chooser passes the filename). Falls back to 'Card'.
- `SerializedError` -> a local `BrokenLinkErrorDoc` interface listing only the
  fields the overlay reads (SerializedError is a structural superset).
- base `ViewCardFn` -> a local `BrokenLinkViewFn`.

The five base call sites switch to the boxel-ui import and pass
`@typeName={{cardTypeName reference}}`, reproducing the label the component
previously computed internally — no visual change to the shipped linksTo UI.

In the chooser, tab-panel threads a broken state (url / state / type name /
error doc) through the pane into MarkdownEmbedPreview, which renders
BrokenLinkTemplate in place of the embed. The Edit modal's existing
Remove / Replace tile is the fix/remove affordance; there is no "Open anyway"
here. The inline document render (rendered-markdown.gts) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files      1 suites   2h 55m 31s ⏱️
3 480 tests 3 465 ✅ 15 💤 0 ❌
3 499 runs  3 484 ✅ 15 💤 0 ❌

Results for commit 8ed7e3e.

Realm Server Test Results

    1 files      1 suites   11m 18s ⏱️
1 816 tests 1 816 ✅ 0 💤 0 ❌
1 895 runs  1 895 ✅ 0 💤 0 ❌

Results for commit 8ed7e3e.

FadhlanR and others added 4 commits July 9, 2026 14:45
Every boxel-ui component ships a usage.gts consumed by the component explorer;
add one for the relocated BrokenLinkTemplate (interactive controls for the
state, format, type name, errorDoc fields, and the optional viewCard handler)
and regenerate the auto-generated usage registry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A fitted broken ref rendered at the template's 40px min-height because the
chooser passed no dimensions — the box collapsed to a thin bar instead of the
tile the real fitted embed previews. Thread the selected width/height onto the
broken template (reusing the same bfmRefFormatAndSize the embed uses), so the
fitted broken placeholder occupies the picked footprint. The size string omits
`overflow: hidden` on purpose: the broken template's root must not clip, or the
reveal overlay would be cut off — the inner box does its own clipping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In edit mode the tile above Replace / Remove falls back to the raw reference
URL when there's no title to show (a broken ref, or a card without a title).
When that reference lives in the realm the user currently has open, collapse it
to its realm-relative path — a shorter, less noisy label. References outside the
current realm keep their absolute URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The broken-ref overlay headline is now driven by a `@noun` arg on
BrokenLinkTemplate ("Linked card not found" vs "Linked file not found"),
defaulting to 'card' so the linksTo/linksToMany field visuals are
unchanged. The markdown-embed chooser threads 'file' for `:file[...]`
refs so a broken file no longer reads as a card.

Tighten the chooser label test to assert the exact realm-relative path
for an in-realm ref, and add a case for an out-of-realm ref keeping its
absolute URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FadhlanR FadhlanR marked this pull request as ready for review July 10, 2026 04:06
@FadhlanR FadhlanR requested a review from a team July 10, 2026 04:07

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61da84f011

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sizeStyle={{this.sizeStyle}}
/>
{{else if @brokenUrl}}
<BrokenLinkTemplate

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve inline placement for broken previews

When the chooser is editing/preloading a broken inline ref with a non-atom format, this branch renders BrokenLinkTemplate directly instead of going through the Embed wrapper that receives @kind. BrokenLinkTemplate roots as a block-level placeholder for embedded/fitted/isolated formats, so an inline directive such as :card[url | embedded] is previewed as a block even though the CTA preserves it as inline; mirror the inline wrapper/classes used for resolved embeds so broken refs preview the same placement they will serialize.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖] Fixed in 013c0c5. Broken refs now render through a BrokenEmbed placement wrapper that mirrors the resolved-embed inline slot, so an inline non-atom broken ref previews inline-block (matching how it serializes) instead of a block. The wrapper adds no card-frame and never sets overflow: hidden, so the reveal overlay stays unclipped. Added a preview test covering the inline placement.

Thread the editing document's own URL (the codemirror editor's
cardReferenceBaseUrl) through the chooser — dispatch, service, modal,
tab-panel, and pane — so a picked or edited ref serializes as a
`../`-relative directive, matching the format-picker insertion path and
the `../`-relative storage convention. The current-target tile label
relativizes against the same document URL rather than the realm root, so
the fallback URL label reads as the same form that gets inserted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

This PR makes unresolved :card[...] / :file[...] references in the markdown-embed chooser visually distinct from an “empty/not-yet-picked” slot by rendering the shared broken-reference UI (the same treatment used by linksTo / linksToMany). It also adds support for relativizing inserted BFM refs against the current document URL, aligning chooser insertion with the format-picker behavior.

Changes:

  • Relocate/re-expose BrokenLinkTemplate via @cardstack/boxel-ui and update base + host call sites to pass the new caller-supplied @typeName (and optional noun).
  • Teach the host markdown-embed chooser to preserve and render a “broken ref” state (url + error payload) instead of collapsing it into the empty placeholder.
  • Add documentBaseUrl plumbing so chooser insertion (and labels) can emit ../-relative BFM refs where applicable, with updated integration/acceptance coverage.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/runtime-common/bfm-card-references.ts Extends chooser bridge types/options to include documentBaseUrl.
packages/host/app/services/markdown-embed-chooser.ts Threads documentBaseUrl through chooser requests (choose + edit).
packages/host/app/components/markdown-embed-chooser/modal.gts Passes documentBaseUrl to both tabs so labels/insertion can be relativized.
packages/host/app/components/markdown-embed-chooser/tab-panel.gts Tracks selectedError to distinguish broken refs from “nothing selected” and supplies broken-ref args to the pane.
packages/host/app/components/markdown-embed-chooser/pane.gts Serializes inserted BFM refs relative to documentBaseUrl; supports broken-ref preview + insertion.
packages/host/app/components/markdown-embed-chooser/preview/index.gts Renders BrokenLinkTemplate when brokenUrl is present and no resolved target exists; adds fitted sizing for broken placeholders.
packages/base/codemirror-editor.gts Supplies documentBaseUrl into chooser calls from the editor context.
packages/base/card-api.gts Switches broken-link rendering to the boxel-ui component and passes caller-derived @typeName.
packages/base/links-to-editor.gts Uses boxel-ui BrokenLinkTemplate and passes @typeName.
packages/base/links-to-many-component.gts Uses boxel-ui BrokenLinkTemplate and passes @typeName at all broken-link render sites.
packages/boxel-ui/addon/src/components/broken-link/index.gts Refactors broken-link component to remove runtime-common/base dependencies; adds @typeName/@noun and local error/view types.
packages/boxel-ui/addon/src/components.ts Re-exports BrokenLinkTemplate and its public types from boxel-ui.
packages/boxel-ui/addon/src/usage.ts Registers BrokenLink usage panel in freestyle usage index.
packages/boxel-ui/addon/src/components/broken-link/usage.gts Adds a freestyle usage page for the broken-link component.
packages/boxel-ui/test-app/tests/integration/components/broken-link-test.gts New test-app coverage for broken-link rendering and overlay behaviors.
packages/host/tests/integration/components/broken-link-template-test.gts Updates host integration test to import the boxel-ui component and pass the new @typeName.
packages/host/tests/integration/components/markdown-embed-preview-test.gts Adds coverage for broken-ref preview rendering (404 vs error, card vs file noun/type label, fitted sizing).
packages/host/tests/integration/components/markdown-embed-preview-pane-test.gts Adds coverage for relative ref insertion and broken-ref preview + CTA serialization.
packages/host/tests/integration/components/markdown-embed-chooser-modal-test.gts Adds edit-mode broken preload coverage + relative insertion result coverage.
packages/host/tests/acceptance/markdown-embed-chooser-test.gts Updates acceptance expectation to the new document-relative inserted ref form.
packages/host/tests/integration/realm-indexing-test.gts Updates dependency snapshots to remove the now-relocated base broken-link template module entry.
Comments suppressed due to low confidence (1)

packages/boxel-ui/addon/src/components/broken-link/index.gts:50

  • noun is documented/used as an enum-like value ('card' | 'file') but is typed as string, which loses type safety/autocomplete and allows unexpected values to flow into user-visible copy (headline strings). Consider narrowing it to a union type (and updating any callers/types accordingly).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +163 to +172
// The broken template requires a state + error doc; default them so the arg
// types stay optional at this boundary (the parent only sets them alongside
// `brokenUrl`, but the correlation isn't expressible in the type).
private get brokenState(): BrokenLinkState {
return this.args.brokenState ?? 'error';
}

private get brokenErrorDoc(): BrokenLinkErrorDoc {
return this.args.errorDoc ?? {};
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖] Done in 013c0c5 — the fallback returns a module-level frozen EMPTY_ERROR_DOC constant instead of a fresh {} each render.

@burieberry burieberry 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.

I made a couple comments. Other than those, can you make sure all css properties properly match the designs: https://app.zeplin.io/project/6a1f141256365a9e08e74147/screen/6a1f14c4aadcc7b409bc2366
I also recommend using our context button for the x on the overlay.

// context.
class WarningIcon extends GlimmerComponent<{
Element: SVGElement;
const WarningIcon: TOC<{

@burieberry burieberry Jul 10, 2026

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.

can you get a version of this where the fill and stroke properties are already in the svg? Then put it through image optimizer or compress it, and place it in icons in boxel-ui raw icons

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖] Done in 013c0c5. Added raw-icons/warning-triangle-filled.svg with fill/stroke baked in per-path (SVGO drops a root fill, so the colour lives on each path), ran it through the icon pipeline (pnpm rebuild:icons → SVGO + committed src/icons/warning-triangle-filled.gts), and the component imports that instead of the inline SVG. The triangle uses the design amber #ffa515.

Comment on lines +14 to +15
@tracked typeName = 'Author';
@tracked noun = 'card';

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.

noun is a very vague property name. how about calling noun -> itemType and calling typeName -> displayName? If there's no displayName, you can fall back to the capitalized itemType. If there's any way of knowing the itemType from the payload, we could use that too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖] Renamed in 013c0c5: nounitemType (now typed 'card' | 'file') and typeNamedisplayName. When displayName is absent the label falls back to the capitalized itemType ("Card" / "File"). itemType is caller-supplied from the payload — linksTo sites pass 'card'; the BFM chooser derives card-vs-file from the ref type.

- Rename BrokenLink args: noun→itemType ('card'|'file' union) and
  typeName→displayName, with the label falling back to the capitalized
  itemType.
- Move the warning triangle to a baked-fill boxel-ui raw icon
  (warning-triangle-filled) instead of an inline two-tone SVG.
- Use ContextButton (@ICON='close') for the reveal-overlay close control.
- Align the broken-ref CSS to the design: 5px radii on the atom pill and
  error boxes, --boxel-border-color hairlines.
- Preview an inline broken ref inline (not as a block) via a placement
  wrapper mirroring the resolved-embed inline slot.
- Reuse a stable empty errorDoc object in the preview instead of a fresh {}.
- Resolve document-relative refs to their absolute URL before loading the
  edit-mode preview, so the pencil-opened chooser renders the card instead
  of a blank pane.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FadhlanR

FadhlanR commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

[Claude Code 🤖] Thanks for the review — all addressed in 013c0c5:

CSS vs the designs — went through the broken-ref styling against the Zeplin screen. Most of it already used --boxel-* tokens; the real corrections were the atom pill + error-box radii → --boxel-border-radius-2xs (5px) and the hairlines → --boxel-border-color (≈ the design's #d1d1d1, vs the lighter --boxel-200 before). Panel width/height keep the design's fixed px (350 wide, 155–600 tall). One deliberate departure from tokens: the warning triangle bakes the design amber #ffa515, since a two-tone icon can't ride a single colour var.

Context button — the overlay × is now <ContextButton @icon='close'>.

Scope note — the full V2 error panel (scroll / copy-link / placement fallbacks) is a much larger feature than this PR; happy to file a follow-up ticket for it rather than pull it in here.

While verifying the pencil-edit flow live I also found and fixed two bugs (same root cause): editing a card ref opened the chooser with a blank preview and threw a Glimmer backtracking assertion. The document-relative ref (../Type/id) wasn't resolved to an absolute URL before store.get, so it fast-failed mid-render. It's now resolved first, with a regression test that reproduces the assertion when the fix is reverted.

Screen.Recording.2026-07-13.at.13.43.44.mov

…ering

# Conflicts:
#	packages/base/codemirror-editor.gts
@FadhlanR FadhlanR requested a review from burieberry July 13, 2026 10:08
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.

3 participants