Skip to content

Markdown embed chooser modal (CS-11675/76/77)#5346

Draft
FadhlanR wants to merge 8 commits into
mainfrom
cs-11675-11676-11677-markdown-embed-chooser-modal
Draft

Markdown embed chooser modal (CS-11675/76/77)#5346
FadhlanR wants to merge 8 commits into
mainfrom
cs-11675-11676-11677-markdown-embed-chooser-modal

Conversation

@FadhlanR

@FadhlanR FadhlanR commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Overview

Lets an author embed a card or file reference into a markdown field directly from the CodeMirror editor, choosing the target and its display size/format through a single combined chooser. Covers Linear CS-11675, CS-11676, and CS-11677.

A markdown embed reference is written as a link with an optional size/format specifier after a | (e.g. the target URL plus a fitted WxH, or atom/embedded/isolated). The editor renders these inline, and the toolbar lets you add a new one at the cursor or edit the one under it.

Changes by area

Embed-reference parsing — runtime-common

  • bfm-card-references.ts — parses/serializes the size-format spec (BfmSizeSpec: atom | fitted | isolated | embedded, with optional width/height), resolves reference URLs against the doc's base, and provides a marked tokenizer extension. Code fences and inline code spans are stripped before extraction so references inside code aren't matched.

Editor integration — base/codemirror-editor.gts + host/app/lib

  • Adds an Add-embed / Edit-embed toolbar to the markdown editor.
  • codemirror-context.ts tracks the embed reference under the caret so the toolbar knows whether to offer Add (no ref at cursor) or Edit (ref at cursor), and which target/spec to seed.

Chooser modal + service — host/app/components/markdown-embed-chooser + service

  • markdown-embed-chooser.ts service is the single entry point (chooseCardOrFile / editEmbed); it holds one in-flight request and resolves a Deferred with the picked { refType, url, bfm }, a { remove: true }, or undefined (cancel). A new request cancels any open one.
  • modal.gts — the modal shell, mounted once in the operator-mode container and driven by the service via a global bridge (mirrors the existing card/file chooser pattern).
  • tabs.gts / tab-pills.gts / tab-panel.gts — a two-tab (Cards / Files) layout. Tabs are a segmented pill control above each tab's search bar; both panels stay mounted so each keeps its search query, highlighted row, scroll position, and the pane's size/format selection across a switch.
  • pane.gts — the right-hand preview pane: shows the selected target and lets the user choose the display format and dimensions, emitting the BFM spec on insert.
  • Edit mode — opening on an existing reference seeds the matching tab with the placed target and its size/placement and offers Replace / Remove.

Mini choosers — card-chooser/mini, file-chooser/mini, card-search

  • The left column of each tab reuses the compact MiniCardChooser / MiniFileChooser primitives (card search results vs. a workspace file tree).
  • Both choosers render their chrome and list rows at a shared 14px scale so toggling tabs doesn't jump in size, and the card chooser's search-bar focus outline is no longer clipped by the results list below it. (card-search/panel-content.gts + sheet-results.gts carry the matching mini-variant tweaks.)

Tests

  • Acceptance: markdown-embed-chooser-test.gts — end-to-end add/edit flow from the editor toolbar through the modal.
  • Integration: markdown-embed-chooser-modal-test.gts, codemirror-embed-toolbar-test.gts, codemirror-editor-test.gts.
  • Unit: bfm-card-references-test.ts.

The chooser font-scale harmonization and focus-outline fix were verified live in the browser via the component freestyle page.

docs/cs-11675-11676-11677-markdown-embed-chooser-modal-plan.md is a scratch planning doc and should be dropped before merge.

🤖 Generated with Claude Code

FadhlanR and others added 7 commits June 26, 2026 21:47
A tabbed modal that wraps the existing mini choosers and the
MarkdownEmbedPreviewPane from #5303. Both tabs stay mounted so each tab's
search query, highlighted row, scroll position, and pane W×H survive a
switch. Driven by a new `markdown-embed-chooser` service exposing
`chooseCardOrFile({ defaultTab })` (edit-mode `editEmbed` follows in
CS-11676). Pane teach: render with `@target` undefined so layout doesn't
jump before a row is picked, CTA disabled until then.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When opened via `editEmbed({ url, refType, sizeSpec })` the matching tab
starts in `current` mode showing the placed target with Replace / Remove
buttons; the other tab still mounts its mini chooser per Zeplin 08B.
Replace flips the tab back to the chooser. Remove resolves the modal's
deferred with `{ remove: true }`. The pane gains edit-mode preload args
(initialFormat / initialWidth / initialHeight / initialKind) plus a
`ctaLabelOverride` and an `onDirtyChange` callback the parent uses to
flip the CTA between DONE and ACCEPT.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `extractBfmRefRanges(markdown)` (runtime-common) returns one
  `[from, to]` per BFM directive, skipping code blocks; powers in-editor
  range tracking and in-place replacement.
- `codemirror-context.ts` `SelectionInfo` gains a `currentRef` field; the
  doc-wide range scan re-runs on `docChanged` and the cursor is tested
  against the cached ranges on every selection update.
- `codemirror-editor.gts` toolbar gets a new left-most slot: an Add-embed
  popover (Add a card / Add a file) when the cursor is outside any
  directive, an Edit pencil when it's inside. Add inserts the returned
  BFM at the cursor (block placement gets surrounding newlines); Edit
  replaces the matched range, Remove deletes it.
- The combined chooser modal registers a global bridge
  `_CARDSTACK_MARKDOWN_EMBED_CHOOSER`; `chooseMarkdownEmbed` /
  `editMarkdownEmbed` in runtime-common dispatch to it — mirrors the
  `chooseCard` / `chooseFile` pattern so the base editor reaches the
  host modal without a direct import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end coverage of the toolbar → modal → editor round-trip:
- Toolbar surfaces an Add-embed popover (Add a card / Add a file) when
  the cursor is outside a directive.
- Picking a card from the popover lands `:card[url]` at the cursor.
- Placing the cursor inside a directive swaps the toolbar to the Edit
  pencil; Remove on the edit modal deletes the directive in place.

Loads CodeMirrorEditor through the loader (virtual-URL module) and
injects it via precompileTemplate's scope so the integration test can
mount the base-realm editor alongside the host-side modal in the same
render pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drives the full operator-mode flow: interact submode → open a Note card
with a RichMarkdownField → switch to edit → click Add embed in the
markdown toolbar → pick a card from the combined chooser modal → assert
the editor's source now carries the inserted `:card[url]` directive.
Second test asserts cursor-inside-directive swaps the toolbar to the
Edit pencil and opens the modal on the current-target tile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- externals.ts: shim `@cardstack/runtime-common/bfm-card-references` as
  an async module so the base-realm editor (and anything else that
  reaches the subpath through the loader) resolves it via the virtual
  network — the loader has no way to fetch it otherwise.
- codemirror-editor.gts: drop `disabled={{not this.toolbarEnabled}}`
  from the Add-embed and Edit-embed buttons; both open modals that own
  their own state, so editor focus isn't a prerequisite.
- pane.gts: new `initialTargetUrl` arg. Without it, in edit mode the
  dirty check briefly flips true once the resolved target instance
  lands (constructor records `undefined`, then `args.target?.id` changes
  to the URL), making the CTA read ACCEPT before the user edits
  anything. Tab-panel passes the URL through verbatim.
- Tests: switch from `editor.cmView.view` (not a public CM6 surface) to
  `cmContext.EditorView.findFromDOM(editor)`; fix one unit-test
  assertion where the block-ref form needed to sit at the start of a
  line for the `^::` regex to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the card/file tab control from a full-width strip into a segmented
pill control (tab-pills.gts) that sits at the top of each panel's left
search column, directly above its search bar. Both tabpanels stay mounted
so each tab keeps its search query, highlighted row, scroll position, and
the pane's size/format selection across a switch.

Harmonize the two mini choosers so toggling tabs no longer jumps in scale:
both now render their chrome and list rows at the shared 14px
(--boxel-font-sm) size — the file tree (was 12px), the card chooser
baseline, and the mini "Search Results" summary (was 16px). Each list
keeps its own natural weight.

Give the card chooser's search header a small bottom inset so the bar's
2px focus outline is no longer painted over by the results list below it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FadhlanR FadhlanR force-pushed the cs-11675-11676-11677-markdown-embed-chooser-modal branch from 288a7e6 to d3c567d Compare June 26, 2026 14:48
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files  ±  0      1 suites  ±0   2h 6m 5s ⏱️ + 3m 55s
3 256 tests +159  3 240 ✅ +158  15 💤 +1  1 ❌ ±0 
3 275 runs  +160  3 259 ✅ +159  15 💤 +1  1 ❌ ±0 

Results for commit 9e4f2ba. ± Comparison against earlier commit d3c567d.

For more details on these errors, see this check.

Realm Server Test Results

    1 files  ±0      1 suites  ±0   8m 48s ⏱️ - 1m 24s
1 663 tests ±0  1 663 ✅ ±0  0 💤 ±0  0 ❌ ±0 
1 742 runs  ±0  1 742 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 9e4f2ba. ± Comparison against earlier commit d3c567d.

Narrow the embed-chooser resolution before reading `bfm`: a negated `&&`
condition doesn't narrow the union, so split the `remove` check into its
own block. And read a card's display title via `cardTitle` (the base
CardDef `title` field was replaced by the cardInfo-derived `cardTitle`).

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

1 participant