From e71609685931cf37ae43e8bac8f2f3d2394e4ff2 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Fri, 24 Jul 2026 19:18:05 +0700 Subject: [PATCH] =?UTF-8?q?Render=20BFM=20embeds=20across=20the=20full=20f?= =?UTF-8?q?ormat=20=C3=97=20placement=20matrix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Embedding a card or file in a markdown field only rendered correctly for the default format+placement combos. `isolated` collapsed to nothing (its default template lays out at height: 100%), inline `embedded` collapsed in a shrink-wrapping inline-block, and non-default combos were unverified. Add a shared `bfmResolvedEmbedStyle(format, kind, fittedSizeStyle)` helper that hands every non-atom resolved embed a definite footprint so it can no longer collapse: inline embedded 16rem×9.375rem, inline isolated 24rem×18.75rem, block isolated a growable `min-height: 18.75rem`, and fitted its requested dimensions plus `overflow: hidden`. Apply it in all four render surfaces — the saved MarkdownTemplate, the operator-mode preview panel, the CodeMirror source editor, and the embed chooser preview (which previously carried its own copy of this logic) — so footprints stay in lockstep everywhere. Plain `MarkdownField` (a StringField) has no relationship to resolve embeds, so its directives stay unresolved by design; documented at the class. Cover the full card and file × {atom, embedded, fitted, isolated} × {inline, block} matrix on the saved render path, plus a unit test for the helper and updated CodeMirror-editor footprint assertions. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/base/card-api.gts | 8 ++ packages/base/default-templates/markdown.gts | 17 +-- .../markdown-embed-chooser/preview/index.gts | 36 ++--- .../preview-panel/rendered-markdown.gts | 17 +-- packages/host/app/lib/codemirror-context.ts | 13 +- .../acceptance/markdown-file-def-test.gts | 52 +++++++ .../components/codemirror-editor-test.gts | 42 +++++- .../components/rich-markdown-field-test.gts | 127 ++++++++++++++++++ .../tests/unit/bfm-card-references-test.ts | 55 ++++++++ .../runtime-common/bfm-card-references.ts | 49 +++++++ 10 files changed, 365 insertions(+), 51 deletions(-) diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index 5adccbb7efe..cdbff4f60fd 100644 --- a/packages/base/card-api.gts +++ b/packages/base/card-api.gts @@ -2913,6 +2913,14 @@ export class CSSField extends TextAreaField { }; } +// A plain markdown StringField. BFM embed directives (`:card[…]` / `:file[…]`) +// in its content render as (correctly-sized) broken-link placeholders and do +// NOT resolve: a StringField is a bare string with no `id` to resolve relative +// refs against, no `linksToMany` storage to hold resolved instances, and no +// query to populate them at index time. Resolution is an index-time +// relationship concern — use `RichMarkdownField` (or a `.md` `MarkdownDef` +// file), which own the `linkedCards`/`linkedFiles` relationships, when embeds +// must render live. export class MarkdownField extends StringField { static displayName = 'Markdown'; static icon = MarkdownIcon; diff --git a/packages/base/default-templates/markdown.gts b/packages/base/default-templates/markdown.gts index 8a65f1c3ff2..2ae8f5be44d 100644 --- a/packages/base/default-templates/markdown.gts +++ b/packages/base/default-templates/markdown.gts @@ -10,6 +10,7 @@ import LinkOffIcon from '@cardstack/boxel-icons/link-off'; import { bfmRefFormatAndSize, + bfmResolvedEmbedStyle, buildWaiter, cardTypeName, fileNameFromUrl, @@ -271,14 +272,14 @@ export default class MarkDownTemplate extends GlimmerComponent<{ let format: CardSlotFormat = derived.format; let sizeStyle: string | undefined = derived.sizeStyle; - // Fitted slots carry an inline width/height plus `overflow: hidden` - // so the resolved instance occupies the requested footprint. - let resolvedStyle: ReturnType | undefined; - if (format === 'fitted') { - resolvedStyle = htmlSafe( - sizeStyle ? `${sizeStyle}; overflow: hidden` : 'overflow: hidden', - ); - } + // Non-atom resolved slots carry a footprint so the instance occupies + // a definite box instead of collapsing (isolated/inline-embedded + // default templates lay out at 100%). Fitted uses its requested + // dimensions; embedded/isolated get shared defaults. Same helper as + // the other render surfaces so footprints stay in lockstep. + let resolvedStyleRaw = bfmResolvedEmbedStyle(format, kind, sizeStyle); + let resolvedStyle: ReturnType | undefined = + resolvedStyleRaw ? htmlSafe(resolvedStyleRaw) : undefined; let resolvedUrl = resolveUrl(rawUrl, baseUrl); diff --git a/packages/host/app/components/markdown-embed-chooser/preview/index.gts b/packages/host/app/components/markdown-embed-chooser/preview/index.gts index 07cf88bdd32..ba2c99fba07 100644 --- a/packages/host/app/components/markdown-embed-chooser/preview/index.gts +++ b/packages/host/app/components/markdown-embed-chooser/preview/index.gts @@ -13,6 +13,7 @@ import { eq, not } from '@cardstack/boxel-ui/helpers'; import { bfmRefFormatAndSize, + bfmResolvedEmbedStyle, type BfmSizeSpec, } from '@cardstack/runtime-common/bfm-card-references'; @@ -266,38 +267,25 @@ export default class MarkdownEmbedPreview extends Component { return sizeStyle ? htmlSafe(sizeStyle) : undefined; } - // Fitted slots carry an inline width/height plus `overflow: hidden` so the - // instance occupies the requested footprint — derived through the same helper - // the live markdown renderer uses (`rendered-markdown.gts`). Inline embedded - // and isolated have no intrinsic inline width: the default template's - // `width/height: 100%` resolves against the inline-block wrapper, which is - // itself shrink-wrapping, and the box collapses. Give the wrapper a definite - // footprint that matches the live renderer's loading placeholders so the - // preview shows a real card body. + // Resolved-embed footprint, shared with the live markdown render surfaces + // (saved `MarkdownTemplate`, host preview panel, CodeMirror editor) through + // `bfmResolvedEmbedStyle`. Isolated and inline embedded have no intrinsic + // inline width — their default `width/height: 100%` resolves against a + // shrink-wrapping wrapper and the box collapses — so the helper hands back a + // definite footprint that matches the live renderers' loading placeholders. private get sizeStyle(): ReturnType | undefined { let { format } = this.args; + let fittedSizeStyle: string | undefined; if (format === 'fitted') { let { width, height } = this.args.sizeSpec ?? { format: 'fitted' }; - let { sizeStyle } = bfmRefFormatAndSize( + fittedSizeStyle = bfmRefFormatAndSize( 'fitted', width === undefined ? undefined : String(width), height === undefined ? undefined : String(height), - ); - return htmlSafe( - sizeStyle ? `${sizeStyle}; overflow: hidden` : 'overflow: hidden', - ); + ).sizeStyle; } - if ( - this.kind === 'inline' && - (format === 'embedded' || format === 'isolated') - ) { - let footprint = - format === 'isolated' - ? 'width: 24rem; height: 18.75rem' - : 'width: 16rem; height: 9.375rem'; - return htmlSafe(`${footprint}; overflow: hidden`); - } - return undefined; + let style = bfmResolvedEmbedStyle(format, this.kind, fittedSizeStyle); + return style ? htmlSafe(style) : undefined; }