From c2a1a45e1dea29475304b1d21e78f7a86f5c8fa2 Mon Sep 17 00:00:00 2001 From: woywro Date: Thu, 16 Apr 2026 15:32:31 +0200 Subject: [PATCH 1/3] feat(studio): rework inline MDX component block UI to lighter presentation --- .../editor/mdx-component-node-view.test.tsx | 74 +++++++++- .../editor/mdx-component-node-view.tsx | 130 ++++++++++++------ 2 files changed, 157 insertions(+), 47 deletions(-) diff --git a/packages/studio/src/lib/runtime-ui/components/editor/mdx-component-node-view.test.tsx b/packages/studio/src/lib/runtime-ui/components/editor/mdx-component-node-view.test.tsx index 551f0ea..aa103a5 100644 --- a/packages/studio/src/lib/runtime-ui/components/editor/mdx-component-node-view.test.tsx +++ b/packages/studio/src/lib/runtime-ui/components/editor/mdx-component-node-view.test.tsx @@ -28,10 +28,10 @@ test("MdxComponentNodeFrame renders wrapper component chrome with nested slot", assert.match(markup, /data-mdcms-mdx-component-kind="wrapper"/); assert.match(markup, /data-mdcms-mdx-preview-state="ready"/); assert.match(markup, /data-test-preview="ready"/); - assert.match(markup, />CalloutWrapper { @@ -47,8 +47,9 @@ test("MdxComponentNodeFrame renders void component chrome without child slot", ( assert.match(markup, /data-mdcms-mdx-component-frame="HeroBanner"/); assert.match(markup, /data-mdcms-mdx-component-kind="void"/); assert.match(markup, /data-mdcms-mdx-preview-state="empty"/); - assert.match(markup, /Local preview unavailable/); - assert.match(markup, /Self-closing component/); + assert.match(markup, /<HeroBanner \/>/); + assert.doesNotMatch(markup, /Local preview unavailable/); + assert.doesNotMatch(markup, /Self-closing component/); assert.doesNotMatch(markup, /data-test-slot="children"/); assert.doesNotMatch(markup, />VoidBody<\/strong>/); }); -test("MdxComponentNodeFrame renders 'Inner content' guidance for wrapper components", () => { +test("MdxComponentNodeFrame renders content-label data attribute for wrapper components", () => { const markup = renderToStaticMarkup( createElement( MdxComponentNodeFrame, @@ -90,8 +91,8 @@ test("MdxComponentNodeFrame renders 'Inner content' guidance for wrapper compone ); assert.match(markup, /data-mdcms-mdx-content-label="Callout"/); - assert.match(markup, />Inner contentInner content { @@ -106,3 +107,62 @@ test("MdxComponentNodeFrame does not render content label for void components", assert.doesNotMatch(markup, /data-mdcms-mdx-content-label/); }); + +test("MdxComponentNodeFrame renders action buttons when callbacks are provided", () => { + const markup = renderToStaticMarkup( + createElement(MdxComponentNodeFrame, { + componentName: "Alert", + isVoid: true, + propsSummary: "", + previewState: "empty", + onEditProps: () => {}, + onDelete: () => {}, + }), + ); + + assert.match(markup, /aria-label="Edit Alert props"/); + assert.match(markup, /aria-label="Delete Alert"/); +}); + +test("MdxComponentNodeFrame omits action buttons when callbacks are not provided", () => { + const markup = renderToStaticMarkup( + createElement(MdxComponentNodeFrame, { + componentName: "Alert", + isVoid: true, + propsSummary: "", + previewState: "empty", + }), + ); + + assert.doesNotMatch(markup, /aria-label="Edit Alert props"/); + assert.doesNotMatch(markup, /aria-label="Delete Alert"/); +}); + +test("MdxComponentNodeFrame applies selected styles when selected", () => { + const markup = renderToStaticMarkup( + createElement(MdxComponentNodeFrame, { + componentName: "Banner", + isVoid: true, + propsSummary: "", + selected: true, + previewState: "empty", + }), + ); + + assert.match(markup, /border-l-primary bg-accent-subtle/); +}); + +test("MdxComponentNodeFrame applies unselected styles when not selected", () => { + const markup = renderToStaticMarkup( + createElement(MdxComponentNodeFrame, { + componentName: "Banner", + isVoid: true, + propsSummary: "", + selected: false, + previewState: "empty", + }), + ); + + assert.match(markup, /border-l-primary\/20/); + assert.doesNotMatch(markup, /bg-accent-subtle/); +}); diff --git a/packages/studio/src/lib/runtime-ui/components/editor/mdx-component-node-view.tsx b/packages/studio/src/lib/runtime-ui/components/editor/mdx-component-node-view.tsx index 0a7b44d..5844561 100644 --- a/packages/studio/src/lib/runtime-ui/components/editor/mdx-component-node-view.tsx +++ b/packages/studio/src/lib/runtime-ui/components/editor/mdx-component-node-view.tsx @@ -12,7 +12,10 @@ import type { StudioMountContext } from "@mdcms/shared"; import type { ReactNodeViewProps } from "@tiptap/react"; import { NodeViewContent, NodeViewWrapper } from "@tiptap/react"; +import { GripVertical, Settings, Trash2 } from "lucide-react"; + import { isMdxExpressionValue } from "../../../mdx-component-extension.js"; +import { cn } from "../../lib/utils.js"; export function formatMdxComponentPropsSummary( props: Record | undefined, @@ -44,40 +47,88 @@ export function MdxComponentNodeFrame(props: { componentName: string; isVoid: boolean; propsSummary: string; + selected?: boolean; previewState?: "ready" | "empty" | "error"; previewSurface?: ReactNode; readOnly?: boolean; forbidden?: boolean; + onEditProps?: () => void; + onDelete?: () => void; children?: ReactNode; }) { return (
-
-
-
- {props.componentName} -
-
- {props.propsSummary} -
+ {/* Drag handle */} +
+ + + +
+ + {/* Chip row */} +
+ + {"<"}{props.componentName}{" />"} + + +
+ {props.forbidden ? ( + + Unavailable + + ) : props.readOnly ? ( + + Read-only + + ) : null} + {props.onEditProps ? ( + + ) : null} + {props.onDelete ? ( + + ) : null}
-
+ {/* Content area */} +
+ {/* Preview surface */}
{props.previewSurface} - {props.previewState === "empty" ? ( -

- Local preview unavailable. -

- ) : null} {props.previewState === "error" ? (

Preview failed to render. @@ -85,32 +136,15 @@ export function MdxComponentNodeFrame(props: { ) : null}

- {props.isVoid ? ( -

- Self-closing component -

- ) : ( -
-

- Inner content -

-

- Edit nested markdown directly in this block. -

+ {/* Wrapper children */} + {props.isVoid ? null : ( +
{props.children}
)} - - {props.forbidden ? ( -

- Editing is unavailable. -

- ) : props.readOnly ? ( -

Read-only preview.

- ) : null}
); @@ -224,6 +258,19 @@ export function MdxComponentNodeView( serializedPreviewProps, ]); + const handleEditProps = () => { + const pos = props.getPos(); + if (typeof pos === "number") { + props.editor.commands.setNodeSelection(pos); + } + }; + + const handleDelete = () => { + props.deleteNode(); + }; + + const isEditable = !props.readOnly && !props.forbidden; + return (
From 66c4b47edf6e2b54d979ad05b1cd0bf0292ce546 Mon Sep 17 00:00:00 2001 From: woywro Date: Thu, 16 Apr 2026 15:43:42 +0200 Subject: [PATCH 2/3] fix(studio): prevent focus loss in MDX props editor and block void component editing --- .../studio/src/lib/mdx-props-editor-host.tsx | 18 +++++++++--------- .../editor/mdx-component-node-view.tsx | 16 +++++++++------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/studio/src/lib/mdx-props-editor-host.tsx b/packages/studio/src/lib/mdx-props-editor-host.tsx index 746dee1..e7d8eb2 100644 --- a/packages/studio/src/lib/mdx-props-editor-host.tsx +++ b/packages/studio/src/lib/mdx-props-editor-host.tsx @@ -408,7 +408,7 @@ function renderAutoFormFieldControl(input: { return ( { const nextValue = event.currentTarget.value; @@ -557,7 +557,7 @@ function renderAutoFormFieldControl(input: { return (