diff --git a/.ai/plans/2026-05-28-structured-intrinsic-mdx-elements.md b/.ai/plans/2026-05-28-structured-intrinsic-mdx-elements.md new file mode 100644 index 00000000..2196220c --- /dev/null +++ b/.ai/plans/2026-05-28-structured-intrinsic-mdx-elements.md @@ -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 + +**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 +
+ + +
+``` + +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 +
+ + +
+``` + +Expected: `roundTripMarkdown(source).markdown` contains ``, ` +``` + +for void nodes and: + +```mdx + +children + +``` + +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. diff --git a/.ai/research/2026-05-28-structured-intrinsic-mdx-elements-design.md b/.ai/research/2026-05-28-structured-intrinsic-mdx-elements-design.md new file mode 100644 index 00000000..8974c9d7 --- /dev/null +++ b/.ai/research/2026-05-28-structured-intrinsic-mdx-elements-design.md @@ -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 `
` 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: + +- `
` 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 + ``. +- Existing raw fallback tests continue to pass for unsupported/unparseable + syntax. diff --git a/.changeset/silly-months-kneel.md b/.changeset/silly-months-kneel.md new file mode 100644 index 00000000..468a72ba --- /dev/null +++ b/.changeset/silly-months-kneel.md @@ -0,0 +1,5 @@ +--- +"@mdcms/studio": patch +--- + +Render lowercase MDX/HTML elements as structured Studio blocks. diff --git a/docs/specs/SPEC-007-editor-mdx-and-collaboration.md b/docs/specs/SPEC-007-editor-mdx-and-collaboration.md index 0cde9049..9a36a753 100644 --- a/docs/specs/SPEC-007-editor-mdx-and-collaboration.md +++ b/docs/specs/SPEC-007-editor-mdx-and-collaboration.md @@ -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 `
`, `
`, `