Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 257 additions & 0 deletions .ai/plans/2026-05-28-structured-intrinsic-mdx-elements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
# Structured Intrinsic MDX Elements Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Parse lowercase intrinsic MDX/HTML as structured Studio blocks so native wrappers do not hide registered child components.

**Architecture:** Add a `mdxIntrinsicElement` TipTap node beside `mdxComponent` and keep `mdxRawJsx` only as the unsupported fallback. The MDX AST parser will convert lowercase elements into intrinsic nodes when attributes are representable, the runtime editor will render them with block chrome, and the Markdown serializer will emit lowercase HTML tags.

**Tech Stack:** Bun, TypeScript, TipTap 3, MDX mdast parser, React node views.

---

### Task 1: Parser Contract Tests
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix heading level jump to keep markdown lint-clean.

Line 13 jumps from an H1 section to H3 (###) without an intermediate H2, which triggers MD001. Change it to ## (or insert a missing ## parent heading).

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 13-13: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.ai/plans/2026-05-28-structured-intrinsic-mdx-elements.md at line 13, The
heading "### Task 1: Parser Contract Tests" jumps from an H1 to H3 causing
MD001; change that heading to an H2 by replacing "### Task 1: Parser Contract
Tests" with "## Task 1: Parser Contract Tests" (or alternatively add a parent H2
above it), ensuring the heading level sequence is H1 → H2 → H3 for downstream
sections.


**Files:**
- Modify: `packages/studio/src/lib/markdown-pipeline.test.ts`

- [ ] **Step 1: Add a failing test for lowercase wrappers containing components**

Add a test that parses:

```mdx
<div style={{display: "flex", gap: "2rem"}}>
<Hero headlineLead="AI-native CMS for" />
<FeatureGrid headingLead="Built for teams that" />
</div>
```

Expected JSON shape:

```ts
assert.equal(parsed.content?.[0]?.type, "mdxIntrinsicElement");
assert.equal(parsed.content?.[0]?.attrs?.tagName, "div");
assert.equal(parsed.content?.[0]?.content?.[0]?.type, "mdxComponent");
assert.equal(parsed.content?.[0]?.content?.[0]?.attrs?.componentName, "Hero");
assert.equal(parsed.content?.[0]?.content?.[1]?.type, "mdxComponent");
assert.equal(
parsed.content?.[0]?.content?.[1]?.attrs?.componentName,
"FeatureGrid",
);
```

- [ ] **Step 2: Add round-trip coverage for native form elements**

Add a test that round-trips:

```mdx
<form name="contact">
<label>
Name
<input type="text" name="name" required />
</label>
<button type="submit">Send</button>
</form>
```

Expected: `roundTripMarkdown(source).markdown` contains `<form`, `<label>`, `<input`, and `<button`, and does not contain `mdxRawJsx`.

- [ ] **Step 3: Run the focused tests and verify failure**

Run:

```bash
bun test packages/studio/src/lib/markdown-pipeline.test.ts
```

Expected: the new intrinsic-element tests fail because lowercase elements still parse as `mdxRawJsx`.

### Task 2: Intrinsic Node Extension

**Files:**
- Create: `packages/studio/src/lib/mdx-intrinsic-element-extension.ts`
- Modify: `packages/studio/src/lib/editor-extensions.ts`

- [ ] **Step 1: Implement `MdxIntrinsicElementExtension`**

Create a TipTap node named `mdxIntrinsicElement` with:

```ts
group: "block";
content: "block*";
isolating: true;
selectable: true;
draggable: true;
```

Attributes:

```ts
tagName: { default: "" };
props: { default: {} };
isVoid: { default: false };
```

Markdown rendering should serialize:

```mdx
<tagName prop="value" />
```

for void nodes and:

```mdx
<tagName prop="value">
children
</tagName>
```

for wrapper nodes, using the existing `serializeMdxJsxAttributes` helper.

- [ ] **Step 2: Register the extension**

Update `createEditorExtensions` so it includes `MdxIntrinsicElementExtension` before `MdxRawJsxExtension` and exposes an override slot like the existing `mdxComponent` and `mdxRawJsx` slots.

- [ ] **Step 3: Run the focused tests**

Run:

```bash
bun test packages/studio/src/lib/markdown-pipeline.test.ts
```

Expected: tests still fail until the parser converts lowercase AST nodes to the new node type.

### Task 3: Parser Conversion

**Files:**
- Modify: `packages/studio/src/lib/mdx-markdown-parser.ts`

- [ ] **Step 1: Convert lowercase MDX AST elements to intrinsic nodes**

Change the non-uppercase branch in `convertMdxJsxElementNode` so lowercase element names with parseable attributes return:

```ts
{
type: "mdxIntrinsicElement",
attrs: {
tagName: name,
props,
isVoid,
},
content,
}
```

Continue returning `mdxRawJsx` when attribute parsing fails or the name is neither uppercase nor lowercase intrinsic syntax.

- [ ] **Step 2: Preserve void semantics**

Treat MDX self-closing syntax and known HTML void elements as void. Void intrinsic nodes must not carry parsed children.

- [ ] **Step 3: Run the focused tests**

Run:

```bash
bun test packages/studio/src/lib/markdown-pipeline.test.ts
```

Expected: parser and round-trip tests pass.

### Task 4: Runtime Editor Rendering

**Files:**
- Create: `packages/studio/src/lib/runtime-ui/components/editor/mdx-intrinsic-element-node-view.tsx`
- Modify: `packages/studio/src/lib/runtime-ui/components/editor/tiptap-editor.tsx`
- Modify: `packages/studio/src/lib/runtime-ui/components/editor/mdx-component-node-view.tsx`

- [ ] **Step 1: Add an intrinsic node view**

Render intrinsic nodes with the existing `MdxComponentNodeFrame`, using the label `tagName`, the same collapse/duplicate/delete chrome, and `NodeViewContent` for non-void children.

- [ ] **Step 2: Render parseable intrinsic HTML as inert native preview**

For non-void intrinsic elements, wrap the editable content in `createElement(tagName, safeProps, editableSlot)` when safe to do so. For void elements, render `createElement(tagName, safeProps)` inside a `contentEditable={false}` preview surface.

- [ ] **Step 3: Register the node view**

Update `TipTapEditor` so `mdxIntrinsicElement` uses `ReactNodeViewRenderer(MdxIntrinsicElementNodeView)`.

- [ ] **Step 4: Run runtime UI tests affected by component node views**

Run:

```bash
bun test packages/studio/src/lib/runtime-ui/components/editor/mdx-component-collapse.test.tsx packages/studio/src/lib/runtime-ui/components/editor/mdx-component-selection.test.ts packages/studio/src/lib/markdown-pipeline.test.ts
```

Expected: tests pass.

### Task 5: Selection and Props Panel

**Files:**
- Modify: `packages/studio/src/lib/runtime-ui/components/editor/mdx-component-selection.ts`
- Modify: `packages/studio/src/lib/runtime-ui/components/editor/mdx-props-panel.tsx`
- Modify: `packages/studio/src/lib/runtime-ui/components/editor/visual-composition-commands.ts`

- [ ] **Step 1: Generalize selected block metadata**

Allow `getSelectedMdxComponent` and `updateSelectedMdxComponentProps` to work with `mdxIntrinsicElement` nodes by returning a selection with `kind: "component" | "intrinsic"`.

- [ ] **Step 2: Show intrinsic props in the side panel**

Update `MdxPropsPanel` so intrinsic selections show the lowercase tag name and a `VisualStyleInspector`-compatible style editor when the `style` prop exists or can be added. Avoid the unregistered component warning for intrinsic nodes.

- [ ] **Step 3: Keep existing component commands scoped**

Only `mdxComponent` nodes should use catalog-only operations such as wrap-in-Box and host component validation. Intrinsic nodes may support duplicate/delete/collapse, but not catalog validation.

- [ ] **Step 4: Run targeted selection and panel tests**

Run:

```bash
bun test packages/studio/src/lib/runtime-ui/components/editor/mdx-component-selection.test.ts packages/studio/src/lib/runtime-ui/components/editor/visual-style-inspector.test.ts packages/studio/src/lib/runtime-ui/pages/content-document-page.test.tsx
```

Expected: tests pass after updating expectations for intrinsic selections.

### Task 6: Changeset, Quality, Commit, Push

**Files:**
- Create via CLI: `.changeset/*.md`

- [ ] **Step 1: Create a changeset**

Run:

```bash
bun run changeset
```

Select `@mdcms/studio`, patch release, and describe the Studio intrinsic HTML editor support.

- [ ] **Step 2: Run validation**

Run:

```bash
bun run format:check
bun run check
bun test packages/studio/src/lib/markdown-pipeline.test.ts
```

Expected: all commands pass.

- [ ] **Step 3: Commit and push**

Run:

```bash
git add docs/specs/SPEC-007-editor-mdx-and-collaboration.md docs/specs/SPEC-014-ai-assisted-studio-editing.md .ai/research/2026-05-28-structured-intrinsic-mdx-elements-design.md .ai/plans/2026-05-28-structured-intrinsic-mdx-elements.md packages/studio/src .changeset
git commit -m "feat(studio): structure intrinsic mdx elements"
git push -u origin codex/structured-intrinsic-mdx-elements
```

Expected: branch is pushed with the spec, implementation, tests, and changeset.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Structured Intrinsic MDX Elements Design

## Decision

Studio will treat parseable lowercase intrinsic MDX/HTML elements as structured
visual blocks instead of raw JSX preview islands. Tags such as `div`, `section`,
`form`, `label`, `input`, and `button` remain valid MDX, but the editor surface
represents them as block nodes with a tag name, props, void status, and editable
children.

Raw JSX preservation remains only for syntax Studio cannot safely represent as
structured data. The fallback must be explicit unsupported content, not a
sanitized partial HTML preview that hides the wrapper tag while showing escaped
children.

## Rationale

The current raw JSX island behavior preserves valid MDX but creates a confusing
editing model. A lowercase wrapper like `<div>` can swallow nested registered
components, causing the wrapper to render as invisible HTML while nested
PascalCase components appear as escaped text. Editors should see all document
structure as blocks when it is parseable, including native HTML elements.

## Editor Contract

- Uppercase MDX elements continue to parse as `mdxComponent` nodes.
- Parseable lowercase MDX/HTML elements parse as `mdxIntrinsicElement` nodes.
- Intrinsic nodes store `tagName`, `props`, and `isVoid`.
- Intrinsic nodes can contain parsed Markdown, registered components, built-ins,
and other intrinsic nodes when the source element is not void.
- Intrinsic nodes serialize back to lowercase MDX/HTML tags.
- Intrinsic nodes are not inserted from the default component palette in this
pass.
- Intrinsic nodes do not use catalog validation because their contract is native
HTML syntax, not host component metadata.
- Unparseable JSX remains preserved as unsupported raw content.

## Implementation Shape

The parser should use the MDX AST that already exposes lowercase elements and
their children. Instead of returning `mdxRawJsx` for every non-uppercase element,
it should parse attributes and return `mdxIntrinsicElement` for lowercase tag
names when attributes are representable. The existing raw JSX extension stays as
the fallback for cases that cannot be represented.

The new node extension should mirror the block affordances of `mdxComponent`
where practical: block group, selectable, isolating, optional content for
non-void elements, Markdown parse/render hooks, and an editor node view that
shows the tag label and nested content. The side panel can initially reuse the
existing prop editing path by exposing the selected node's props and tag name.

## Validation

Targeted tests should prove that:

- `<div><Hero /></div>` parses into an intrinsic `div` node containing an
`mdxComponent` `Hero` child.
- Nested intrinsic elements round-trip through Markdown without becoming raw
preview text.
- Native form syntax still round-trips, including void controls such as
`<input />`.
- Existing raw fallback tests continue to pass for unsupported/unparseable
syntax.
5 changes: 5 additions & 0 deletions .changeset/silly-months-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mdcms/studio": patch
---

Render lowercase MDX/HTML elements as structured Studio blocks.
25 changes: 18 additions & 7 deletions docs/specs/SPEC-007-editor-mdx-and-collaboration.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,13 +256,24 @@ deterministic error when `config.components` declares `Box`, `Text`, `Image`, or
`style` prop in the extracted catalog; built-in support does not imply a
universal wrapper or style injection layer for host components.

Raw lowercase MDX/HTML elements such as `<div>`, `<form>`, `<label>`,
`<input>`, and `<button>` remain valid advanced MDX authoring syntax. They are
not catalog components and are not shown as first-class visual composition
blocks. Studio preserves them as raw MDX islands, renders an inert preview where
possible, and serializes them back to their original MDX source. Built-ins are
therefore the supported visual-editing primitives, not the only HTML that can
exist in a document.
Lowercase intrinsic MDX/HTML elements such as `<div>`, `<form>`, `<label>`,
`<input>`, and `<button>` remain valid advanced MDX authoring syntax. When their
attributes can be represented as Studio props, Studio parses them into
first-class visual composition blocks with their tag name, props, void status,
and editable children. Intrinsic blocks are not catalog components, are not
shown in the insert component palette by default, and do not participate in host
component prop-schema validation. They still render in the canvas with the same
block affordances as MDX components so editors never see a partial raw HTML
preview as the normal editing surface.

Studio preserves intrinsic HTML semantics during serialization: an intrinsic
`div` block serializes as `<div>`, a `form` block serializes as `<form>`, and
void intrinsic elements such as `<input />` serialize as self-closing lowercase
HTML. Built-ins are the supported visual-editing primitives for common layout
and inline content, while intrinsic blocks cover valid native HTML semantics and
legacy MDX content. Raw MDX islands are a last-resort preservation fallback only
for unparseable or unsupported syntax; they must be clearly labeled as
unsupported content rather than rendered as a half-raw inline preview.
Comment on lines +259 to +276
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resolve contract conflict on lowercase intrinsic handling.

This new section says parseable lowercase intrinsic HTML should be structured blocks, but the later “Children / Nested Content” section still states raw lowercase HTML remains raw-island content. Please align both sections to one behavior to avoid conflicting implementation guidance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/specs/SPEC-007-editor-mdx-and-collaboration.md` around lines 259 - 276,
The spec contains a contract conflict between the "Lowercase intrinsic MDX/HTML
elements" section (which treats parseable lowercase HTML as structured intrinsic
blocks) and the "Children / Nested Content" section (which claims lowercase HTML
remains raw-island content); pick and document a single behavior and update the
"Children / Nested Content" text to match: declare that parseable lowercase
intrinsic elements (e.g., div, form, label, input, button) are parsed into
intrinsic blocks with props/void/editable children and only
unparseable/unsupported syntax is preserved as a raw MDX island; make sure to
reference the terms "intrinsic blocks" and "raw MDX islands" in the revised
paragraph so implementers know which path to follow.


The legacy `/` slash menu lists only host-registered components and is
positioned inline near the active cursor. Built-ins remain hidden from that
Expand Down
11 changes: 6 additions & 5 deletions docs/specs/SPEC-014-ai-assisted-studio-editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,12 +526,13 @@ Rules:
does not create different validation semantics; it only distinguishes
MDCMS-provided components from host-registered components for Studio
discoverability.
- Lowercase intrinsic MDX/HTML elements are valid raw MDX output. They are not
- Lowercase intrinsic MDX/HTML elements are valid MDX output. They are not
catalog components, so the catalog validator does not require them to be
registered. The assistant should prefer catalog components and built-ins when
the user asks for visually editable composition, and use raw intrinsic HTML
only when the requested result needs native HTML semantics such as forms or
inputs.
registered. When Studio can parse their attributes, they render as structured
intrinsic element blocks with editable children rather than raw HTML preview
text. The assistant should prefer catalog components and built-ins when the
user asks for common visual composition, and use raw intrinsic HTML only when
the requested result needs native HTML semantics such as forms or inputs.
- Inline styles are valid only through first-class `style` props. Style values
must be flat objects whose values are strings or numbers.

Expand Down
Loading
Loading