Skip to content

Honor embed format and custom size in the compose editor#5548

Open
FadhlanR wants to merge 4 commits into
mainfrom
cs-12112-fitted-isolated-format-custom-size-embeds-edit
Open

Honor embed format and custom size in the compose editor#5548
FadhlanR wants to merge 4 commits into
mainfrom
cs-12112-fitted-isolated-format-custom-size-embeds-edit

Conversation

@FadhlanR

@FadhlanR FadhlanR commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

In the field's edit (compose) view, embedded cards/files didn't honor a BFM directive's format or custom size — most visibly fitted (and isolated) width/height was ignored, so the embed rendered at the wrong height. The saved views and the edit "preview" sub-mode rendered sizes correctly, so the bug was specific to the CodeMirror compose editor.

Fixes CS-12112.

Root cause

The format/size specifier (::card[url | fitted w:400 h:300], | isolated, …) was dropped at three points in the compose pipeline:

  • CardWidget never recorded the directive's format/size — toDOM emitted only id/kind/ref-type.
  • The target notifier hardcoded format: kind === 'inline' ? 'atom' : 'embedded'.
  • The live-preview template rendered <RefComponent @format=… /> with no sizing, and CardWidgetTarget.format couldn't even represent fitted/isolated.

Fix

Thread format/width/height through the widget → DOM data-attrs → target → template chain, reusing the existing parseBfmSizeSpec / bfmRefFormatAndSize helpers so the compose editor matches the saved and preview markdown renderers exactly — including overflow: hidden for fitted. Applies to both :card and :file embeds.

Two equality guards also had to learn about the size fields so a size-only edit (e.g. w:400w:600) actually re-renders: CardWidget.eq (else CodeMirror reuses the stale DOM element) and the _applyTargets structural-identity skip check (else the Glimmer re-render is skipped).

Testing

  • pnpm lint:types clean; pnpm lint:js clean.
  • New codemirror-context integration tests cover block fitted with explicit size, block isolated, atom/embedded defaults, and file-embed parity. Full module 51/51.
  • codemirror embed toolbar 8/8 and RichMarkdownField 25/25 — no regressions.

Screenshot

Before

Screenshot 2026-07-21 at 19 59 18

After

Screenshot 2026-07-21 at 19 48 57

🤖 Generated with Claude Code

The CodeMirror compose editor dropped a BFM embed directive's format and
size specifier: `CardWidget` never recorded them, the target notifier
hardcoded `atom`/`embedded`, and the live-preview template rendered the
card with no sizing. So `::card[url | fitted w:400 h:300]` and `| isolated`
rendered at the wrong size in compose, while the saved view and edit
preview rendered them correctly.

Thread format/width/height through the widget -> DOM data-attrs -> target
-> template chain, reusing `parseBfmSizeSpec` / `bfmRefFormatAndSize` so
the compose editor matches the saved and preview markdown renderers
(including `overflow: hidden` for fitted). Applies to both card and file
embeds. `CardWidget.eq` and the target skip-check now account for the size
fields so a size-only edit re-renders.

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

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

127 tests   127 ✅  8m 10s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit de2c330.

Realm Server Test Results

    1 files  ±0      1 suites  ±0   12m 11s ⏱️ -7s
1 913 tests ±0  1 913 ✅ ±0  0 💤 ±0  0 ❌ ±0 
1 992 runs  ±0  1 992 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit de2c330. ± Comparison against earlier commit ec1d83d.

FadhlanR and others added 3 commits July 21, 2026 20:22
Route inline non-atom embeds (embedded/fitted/isolated) through a
chrome-less inline-block slot (codemirror-card-slot--inline-embed) instead
of the atom pill, so a sized or isolated inline card occupies its requested
footprint the same way the saved and preview markdown renderers do. The
atom pill path is unchanged. Type CardWidget.format and
parseCardWidgetContent as BfmRefFormat.

Add compose-editor coverage: block fitted and prefix-form (@-RRI) previews
resolve and size their cards, inline embeds flow by format (atom pill vs
sized inline-block), and a custom fitted chooser selection inserts a
directive carrying its size rather than a bare `fitted`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "selecting a fitted format inserts a directive carrying its size" test
asserts that choosing the Custom-size fitted option inserts a directive
with explicit dimensions. Selecting Custom without entering a width/height
currently serializes to a bare `| fitted` (selectFormat seeds dimensions
only for the named fitted variants, not for Custom), so the test pins
behavior that is not yet implemented and does not belong on this branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Selecting the Custom-size fitted option seeds no width/height, so inserting
immediately would emit a size-less bare `::card[url | fitted]`. Gate the
insert CTA on EmbedFormatSelection.hasValidSize — always true for the named
formats, and for Custom only once at least one dimension parses — and guard
the insert action so a disabled CTA can't fire. Cover the disabled state
and the sized insert with an acceptance test.

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

@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: de2c330d3d

ℹ️ 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".

if (this.category !== 'custom') {
return true;
}
return this.width !== undefined || this.height !== undefined;

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 Keep valid bare fitted embeds actionable

When the chooser opens in edit mode for an existing ::card[url | fitted] directive, deriveFormatSeeds treats that valid bare fitted spec as category === 'custom' with empty width/height, so this returns false and the pane disables the Done/Accept button. The markdown parser/renderer explicitly supports bare fitted with no size attributes, so users editing existing/manual content in that format cannot accept the unchanged embed unless they add an arbitrary dimension; please only block blank Custom for new inserts or preserve seeded bare-fitted edit state as valid.

Useful? React with 👍 / 👎.

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

[Claude Code 🤖] Reviewed for parity with the saved/preview markdown renderers. The fix's stated contract is that the compose editor should size and format embeds exactly as the saved view and edit-preview already do, so I traced the widget → data-boxel-bfm-* attr → bfmRefFormatAndSize → target → template chain against rendered-markdown.gts and verified each hop.

Bottom line: the threading is correct and the twin parity genuinely holds. One non-blocking regression in the edit-mode CTA — already flagged by the automated reviewer, now verified with a concrete repro.

What lands right:

  • The widget → DOM attrs → target → template chain reuses the same helpers the render side uses, so fitted / isolated / explicit-size embeds now derive identically in compose and in saved views (verified hop by hop — see the confirmation on codemirror-context.ts).
  • Both re-render guards are needed and correct: CardWidget.eq (format/width/height) forces CM6 to rebuild the widget DOM on a size-only edit, and the _applyTargets structural skip-check (format/style, guarded by the length check) forces the Glimmer re-render. Without both, a w:400 → w:600 edit would silently no-op.
  • The new integration tests exercise the real resolved-card path, not just target shape — the fitted slot's computed width / height / overflow are asserted on the rendered element.

Recommendation (non-blocking):

  1. Keep a bare ::card[url | fitted] editable — opening the chooser on one currently disables "Done" because the seed lands in the custom category with no dimensions. Gate the disable on dirtiness (or scope it to new inserts) and add the edit-path test. Detail in the thread on format-selection.ts.

Adjacent, out of scope:

  • CardWidgetTarget is declared twice — in codemirror-editor.gts (base) and codemirror-context.ts (host) — and this change extends both in lockstep. They're structurally identical; worth collapsing to one exported type before they drift, but not this PR's job.
  • The compose editor's loading/unresolved fallback (codemirror-card-fallback) carries no size style, whereas rendered-markdown.gts sizes its loading shimmer — so a block fitted embed doesn't reserve its footprint until the card resolves. Minor, transient, pre-existing structure; flagging only because the PR's goal is renderer parity.

Generated by Claude Code

Comment on lines +286 to +291
get hasValidSize(): boolean {
if (this.category !== 'custom') {
return true;
}
return this.width !== undefined || this.height !== undefined;
}

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.

[Claude Code 🤖] Editing an existing bare ::card[url | fitted] disables the "Done" CTA, so the unchanged embed can't be accepted. The automated reviewer flagged this; I traced it end to end and it reproduces.

The path. Bare fitted is a first-class supported form — parseBfmSizeSpec('fitted') returns { format: 'fitted' }, and serializeBfmSizeSpec emits bare fitted for a dimensionless fitted spec. When the pencil opens the chooser on such a directive, codemirror-editor.gts calls editEmbed({ sizeSpec: 'fitted', kind: 'block' }), and deriveFormatSeeds('fitted', 'block') takes the fitted default branch → { format: 'custom', width: undefined, height: undefined, kind: 'block' }. The constructed EmbedFormatSelection therefore has category === 'custom' with empty widthInput/heightInput, so hasValidSize returns falsepane.gts's ctaDisabled is true → the "Done" button is disabled. The user can't accept the embed they opened without either typing a dimension (which silently rewrites the directive) or removing it.

Scope: regression introduced here — before this guard the CTA was always enabled, so the edit round-trip worked. It's narrow (only bare fitted; named variants and explicit W×H seed dimensions and are unaffected), so non-blocking — but it does break a valid edit round-trip.

A way out. The guard wants to block a new Custom insert left empty, not a seeded embed opened unchanged; dirtiness distinguishes the two — e.g. in pane.gts:

private get ctaDisabled(): boolean {
  return !this.args.selection.hasValidSize && this.args.selection.isDirty;
}

An unchanged bare-fitted edit has isDirty === false, so "Done" stays enabled and re-serializes the same | fitted; a fresh Custom-with-no-dims pick is dirty (selection moved off atom) and stays blocked — so the acceptance test added here still passes. Alternatively, scope the block to new inserts / preserve seeded bare-fitted as valid, as the automated reviewer suggested.

Coverage. The new acceptance test covers the insert path (pick Custom → disabled → enter dims → enabled). The gap is the edit path: open the chooser on an existing ::card[url | fitted] and assert "Done" is enabled and accepting re-inserts the bare fitted.


Generated by Claude Code

Comment on lines +946 to +951
let style =
format === 'fitted'
? sizeStyle
? `${sizeStyle}; overflow: hidden`
: 'overflow: hidden'
: undefined;

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.

[Claude Code 🤖] Confirming this is correct, and documenting the invariant it now depends on. This block is the compose-editor twin of the saved/preview renderer in operator-mode/preview-panel/rendered-markdown.gts, and I verified the two match hop for hop: the same bfmRefFormatAndSize(..., kind === 'inline' ? 'atom' : 'embedded') call, and the identical fitted-style formula (${sizeStyle}; overflow: hidden, else overflow: hidden). The atom-vs-embed class split added in codemirror-editor.gts mirrors markdown-bfm-card-slot--inline / --inline-embed there as well. That parity is the whole point of the fix and the thing a future edit must preserve — the two derivations have no shared home, so a change to one silently drifts from the other. Extracting a single shared helper ("derive slot format + resolved style from BFM attrs") would remove the drift risk, but that's a follow-up, not this PR.

Also confirming htmlSafe(target.style) in the template is safe here: style is assembled only from bfmRefFormatAndSize, which admits width/height only after /^\d+%$/ / /^\d+$/ gates, so no free-form text reaches the style attribute.


Generated by Claude Code

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