@blogmcp/mdx-render-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/MdxContent.d.ts +29 -0
- package/dist/MdxContent.js +51 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +43 -0
- package/dist/marked.d.ts +34 -0
- package/dist/marked.js +15 -0
- package/dist/registry.d.ts +33 -0
- package/dist/registry.js +88 -0
- package/dist/render/alert/Alert.d.ts +13 -0
- package/dist/render/alert/Alert.js +24 -0
- package/dist/render/audioplayer/AudioPlayerDefault.d.ts +6 -0
- package/dist/render/audioplayer/AudioPlayerDefault.js +75 -0
- package/dist/render/badge/Badge.d.ts +11 -0
- package/dist/render/badge/Badge.js +12 -0
- package/dist/render/button/Button.d.ts +14 -0
- package/dist/render/button/Button.js +20 -0
- package/dist/render/callout/CalloutDefault.d.ts +11 -0
- package/dist/render/callout/CalloutDefault.js +19 -0
- package/dist/render/callout/CalloutMinimal.d.ts +10 -0
- package/dist/render/callout/CalloutMinimal.js +16 -0
- package/dist/render/card/Card.d.ts +9 -0
- package/dist/render/card/Card.js +17 -0
- package/dist/render/codeblock/CodeBlockDefault.d.ts +14 -0
- package/dist/render/codeblock/CodeBlockDefault.js +56 -0
- package/dist/render/codeblock/language-labels.d.ts +8 -0
- package/dist/render/codeblock/language-labels.js +42 -0
- package/dist/render/gallery/GalleryGrid.d.ts +11 -0
- package/dist/render/gallery/GalleryGrid.js +106 -0
- package/dist/render/gallery/GalleryLightbox.d.ts +18 -0
- package/dist/render/gallery/GalleryLightbox.js +154 -0
- package/dist/render/gallery/gallery.d.ts +29 -0
- package/dist/render/gallery/gallery.js +56 -0
- package/dist/render/icon/Icon.d.ts +11 -0
- package/dist/render/icon/Icon.js +18 -0
- package/dist/render/index.d.ts +25 -0
- package/dist/render/index.js +26 -0
- package/dist/render/tooltip/Tooltip.d.ts +13 -0
- package/dist/render/tooltip/Tooltip.js +15 -0
- package/dist/render/youtube/YouTubeDefault.d.ts +11 -0
- package/dist/render/youtube/YouTubeDefault.js +53 -0
- package/dist/render/youtube/youtube.d.ts +8 -0
- package/dist/render/youtube/youtube.js +13 -0
- package/dist/server-render-map.d.ts +54 -0
- package/dist/server-render-map.js +56 -0
- package/package.json +77 -0
- package/src/MdxContent.tsx +105 -0
- package/src/index.ts +37 -0
- package/src/marked.ts +49 -0
- package/src/registry.ts +131 -0
- package/src/render/alert/Alert.tsx +32 -0
- package/src/render/audioplayer/AudioPlayerDefault.tsx +106 -0
- package/src/render/badge/Badge.tsx +24 -0
- package/src/render/button/Button.tsx +49 -0
- package/src/render/callout/CalloutDefault.tsx +34 -0
- package/src/render/callout/CalloutMinimal.tsx +25 -0
- package/src/render/card/Card.tsx +21 -0
- package/src/render/codeblock/CodeBlockDefault.tsx +82 -0
- package/src/render/codeblock/language-labels.ts +46 -0
- package/src/render/gallery/GalleryGrid.tsx +143 -0
- package/src/render/gallery/GalleryLightbox.tsx +201 -0
- package/src/render/gallery/gallery.ts +90 -0
- package/src/render/icon/Icon.tsx +25 -0
- package/src/render/index.ts +26 -0
- package/src/render/tooltip/Tooltip.tsx +38 -0
- package/src/render/youtube/YouTubeDefault.tsx +58 -0
- package/src/render/youtube/youtube.ts +17 -0
- package/src/server-render-map.ts +120 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @blogmcp/mdx-render-react
|
|
2
|
+
|
|
3
|
+
**Status: scaffold only.** Implementation deferred until a React consumer exists (blog or md-editor React port).
|
|
4
|
+
|
|
5
|
+
This package mirrors [`@blogmcp/mdx-render-vue`](../mdx-render-vue/) against the shared
|
|
6
|
+
[`@blogmcp/mdx-schema`](../mdx-schema/) contract. Authoring stays identical across frameworks
|
|
7
|
+
(same MDX syntax, same metadata, same MCP tool output) — only the renderer swaps.
|
|
8
|
+
|
|
9
|
+
## Contract to implement
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { componentMetadata } from '@blogmcp/mdx-schema'
|
|
13
|
+
import type { ComponentType } from 'react'
|
|
14
|
+
|
|
15
|
+
export interface ReactComponentRegistration {
|
|
16
|
+
metadata: ComponentMetadata // from @blogmcp/mdx-schema
|
|
17
|
+
render: Record<string, () => Promise<{ default: ComponentType<any> }>>
|
|
18
|
+
edit: () => Promise<{ default: ComponentType<any> }>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const reactRegistry: Record<string, ReactComponentRegistration>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Implementation mirrors [`packages/mdx-render-vue/src/registry.ts`](../mdx-render-vue/src/registry.ts)
|
|
25
|
+
— same keys (`Callout`, `YouTube`, `Gallery`, `CodeBlock`, `AudioPlayer`), same variants, React
|
|
26
|
+
components instead of Vue SFCs. Prefer Ark UI React primitives where Vue uses Ark UI Vue; share
|
|
27
|
+
Tailwind classes verbatim.
|
|
28
|
+
|
|
29
|
+
## When to implement
|
|
30
|
+
|
|
31
|
+
When the first React consumer appears (blog-react or editor-react). Until then, keep the package
|
|
32
|
+
empty so the schema contract proves framework-neutral by construction.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { ImageHtmlRenderer, SanitizeFn } from '@blogmcp/sdk';
|
|
3
|
+
export interface MdxContentProps {
|
|
4
|
+
/** Raw MDX/markdown source (frontmatter allowed; it is stripped). */
|
|
5
|
+
content: string;
|
|
6
|
+
/** Optional custom image HTML renderer (IPX, Cloudinary, …). */
|
|
7
|
+
imageRenderer?: ImageHtmlRenderer;
|
|
8
|
+
/**
|
|
9
|
+
* Optional HTML sanitizer override. Defaults to `sanitizeHtml` from
|
|
10
|
+
* `@blogmcp/sanitize` (DOMPurify with our strict config). Pass `null`
|
|
11
|
+
* to opt out for trusted-self surfaces.
|
|
12
|
+
*/
|
|
13
|
+
sanitize?: SanitizeFn | null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Renders MDX content as a tree of React Server Components.
|
|
17
|
+
*
|
|
18
|
+
* Mirrors the Vue blog renderer (KbMdxContent.vue):
|
|
19
|
+
* - strips frontmatter
|
|
20
|
+
* - fast-paths pure markdown via `marked` → dangerouslySetInnerHTML
|
|
21
|
+
* - otherwise splits via `extractMdxComponents`, rendering html chunks via
|
|
22
|
+
* `marked` and components via `serverRenderMap[name]`
|
|
23
|
+
* - recurses into component children by rendering a nested <MdxContent>
|
|
24
|
+
* (React equivalent of Vue's raw-string default slot)
|
|
25
|
+
* - unknown component → the same yellow fallback box the Vue renderer shows
|
|
26
|
+
*
|
|
27
|
+
* Async Server Component: safe to `await` in RSC; works synchronously in tests.
|
|
28
|
+
*/
|
|
29
|
+
export default function MdxContent({ content, imageRenderer, sanitize }: MdxContentProps): Promise<ReactNode>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as n } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment as h } from "react";
|
|
3
|
+
import { stripFrontmatter as u, HAS_MDX_RE as f, extractMdxComponents as x } from "@blogmcp/mdx-schema/composables/parsing";
|
|
4
|
+
import { createMarked as y } from "./marked.js";
|
|
5
|
+
import { wrappedServerRenderMap as w } from "./server-render-map.js";
|
|
6
|
+
async function v({ content: a, imageRenderer: c, sanitize: l }) {
|
|
7
|
+
const m = y({ imageRenderer: c, sanitize: l });
|
|
8
|
+
if (!a)
|
|
9
|
+
return /* @__PURE__ */ n("div", { className: "mdx-content" });
|
|
10
|
+
const s = u(a);
|
|
11
|
+
if (!f.test(s)) {
|
|
12
|
+
const t = await m.parse(s);
|
|
13
|
+
return /* @__PURE__ */ n("div", { className: "mdx-content", dangerouslySetInnerHTML: { __html: t } });
|
|
14
|
+
}
|
|
15
|
+
const o = x(s);
|
|
16
|
+
if (o.length === 1 && o[0].type === "html") {
|
|
17
|
+
const t = await m.parse(o[0].content);
|
|
18
|
+
return /* @__PURE__ */ n("div", { className: "mdx-content", dangerouslySetInnerHTML: { __html: t } });
|
|
19
|
+
}
|
|
20
|
+
const r = [];
|
|
21
|
+
for (let t = 0; t < o.length; t++) {
|
|
22
|
+
const e = o[t];
|
|
23
|
+
if (e.type === "html") {
|
|
24
|
+
const p = await m.parse(e.content);
|
|
25
|
+
r.push(/* @__PURE__ */ n("div", { dangerouslySetInnerHTML: { __html: p } }, t));
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const d = w[e.name];
|
|
29
|
+
if (!d) {
|
|
30
|
+
r.push(
|
|
31
|
+
/* @__PURE__ */ n(
|
|
32
|
+
"div",
|
|
33
|
+
{
|
|
34
|
+
className: "bg-yellow-50 border border-yellow-200 rounded-lg p-4 my-4 text-yellow-800",
|
|
35
|
+
children: `Unknown component: ${e.name}`
|
|
36
|
+
},
|
|
37
|
+
t
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const i = e.children ? await v({ content: e.children, imageRenderer: c, sanitize: l }) : void 0;
|
|
43
|
+
r.push(
|
|
44
|
+
/* @__PURE__ */ n(d, { ...e.props, children: i }, t)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return /* @__PURE__ */ n("div", { className: "mdx-content", children: r.map((t, e) => /* @__PURE__ */ n(h, { children: t }, e)) });
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
v as default
|
|
51
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @blogmcp/mdx-render-react
|
|
3
|
+
*
|
|
4
|
+
* React renderer for MDX components. Implements the same contract as
|
|
5
|
+
* @blogmcp/mdx-render-vue against the shared @blogmcp/mdx-schema. Authoring
|
|
6
|
+
* (MDX syntax, metadata, MCP tool output) is identical across frameworks —
|
|
7
|
+
* only the renderer swaps.
|
|
8
|
+
*
|
|
9
|
+
* The blog/RSC path uses `MdxContent` (async Server Component) + the static
|
|
10
|
+
* `serverRenderMap`. `reactRegistry` mirrors the Vue registry for future
|
|
11
|
+
* tooling (lazy variant resolution, an eventual React md-editor).
|
|
12
|
+
*/
|
|
13
|
+
export { default as MdxContent } from './MdxContent';
|
|
14
|
+
export type { MdxContentProps } from './MdxContent';
|
|
15
|
+
export { serverRenderMap } from './server-render-map';
|
|
16
|
+
export { reactRegistry, componentMetadata, getComponentMetadata, getComponentNames, getComponentVariants, getRenderComponentAsync, type ReactComponentRegistration, } from './registry';
|
|
17
|
+
export { createMarked, marked, defaultImageHtml } from './marked';
|
|
18
|
+
export type { ImageHtmlRenderer } from './marked';
|
|
19
|
+
export * from './render/index';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { default as o } from "./MdxContent.js";
|
|
2
|
+
import { serverRenderMap as r } from "./server-render-map.js";
|
|
3
|
+
import { getComponentVariants as l, getRenderComponentAsync as m, reactRegistry as d } from "./registry.js";
|
|
4
|
+
import { createMarked as u, marked as n } from "./marked.js";
|
|
5
|
+
import { default as s } from "./render/callout/CalloutDefault.js";
|
|
6
|
+
import { default as g } from "./render/callout/CalloutMinimal.js";
|
|
7
|
+
import { default as c } from "./render/youtube/YouTubeDefault.js";
|
|
8
|
+
import { default as y } from "./render/gallery/GalleryGrid.js";
|
|
9
|
+
import { default as k } from "./render/codeblock/CodeBlockDefault.js";
|
|
10
|
+
import { default as B } from "./render/audioplayer/AudioPlayerDefault.js";
|
|
11
|
+
import { default as G } from "./render/button/Button.js";
|
|
12
|
+
import { default as T } from "./render/card/Card.js";
|
|
13
|
+
import { default as v } from "./render/alert/Alert.js";
|
|
14
|
+
import { default as N } from "./render/badge/Badge.js";
|
|
15
|
+
import { default as V } from "./render/icon/Icon.js";
|
|
16
|
+
import { default as h } from "./render/tooltip/Tooltip.js";
|
|
17
|
+
import { componentMetadata as q, getComponentMetadata as w, getComponentNames as z } from "@blogmcp/mdx-schema/metadata";
|
|
18
|
+
import { defaultImageHtml as F } from "@blogmcp/sdk";
|
|
19
|
+
export {
|
|
20
|
+
v as Alert,
|
|
21
|
+
B as AudioPlayerDefault,
|
|
22
|
+
N as Badge,
|
|
23
|
+
G as Button,
|
|
24
|
+
s as CalloutDefault,
|
|
25
|
+
g as CalloutMinimal,
|
|
26
|
+
T as Card,
|
|
27
|
+
k as CodeBlockDefault,
|
|
28
|
+
y as GalleryGrid,
|
|
29
|
+
V as Icon,
|
|
30
|
+
o as MdxContent,
|
|
31
|
+
h as Tooltip,
|
|
32
|
+
c as YouTubeDefault,
|
|
33
|
+
q as componentMetadata,
|
|
34
|
+
u as createMarked,
|
|
35
|
+
F as defaultImageHtml,
|
|
36
|
+
w as getComponentMetadata,
|
|
37
|
+
z as getComponentNames,
|
|
38
|
+
l as getComponentVariants,
|
|
39
|
+
m as getRenderComponentAsync,
|
|
40
|
+
n as marked,
|
|
41
|
+
d as reactRegistry,
|
|
42
|
+
r as serverRenderMap
|
|
43
|
+
};
|
package/dist/marked.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configured `marked` instance for the React renderer.
|
|
3
|
+
*
|
|
4
|
+
* Reuses the SDK's `createMarkedInstance` so the image/link renderers, table
|
|
5
|
+
* wrap, and `th scope` post-processing match the Vue blog (KbMdxContent.vue)
|
|
6
|
+
* exactly. Consumers may pass a custom image renderer (IPX, Cloudinary, etc.)
|
|
7
|
+
* and an optional HTML sanitizer.
|
|
8
|
+
*
|
|
9
|
+
* Sanitization is wired by default via `@blogmcp/sanitize` — the reference
|
|
10
|
+
* renderer ships safe-by-default. Callers can opt out by passing `null`
|
|
11
|
+
* (intentional, grep-able) for trusted-self surfaces or when a downstream
|
|
12
|
+
* renderer applies its own sanitization.
|
|
13
|
+
*/
|
|
14
|
+
import { defaultImageHtml, type ImageHtmlRenderer, type SanitizeFn } from '@blogmcp/sdk';
|
|
15
|
+
import { sanitizeHtml } from '@blogmcp/sanitize';
|
|
16
|
+
import type { Marked } from 'marked';
|
|
17
|
+
export { defaultImageHtml, sanitizeHtml };
|
|
18
|
+
export type { ImageHtmlRenderer, SanitizeFn };
|
|
19
|
+
export interface CreateMarkedOptions {
|
|
20
|
+
/** Custom image renderer. Defaults to plain <img> with lazy loading. */
|
|
21
|
+
imageRenderer?: ImageHtmlRenderer;
|
|
22
|
+
/**
|
|
23
|
+
* HTML sanitizer applied as the last step of `marked.parse()`. Defaults
|
|
24
|
+
* to `sanitizeHtml` from `@blogmcp/sanitize` (DOMPurify with our strict
|
|
25
|
+
* config). Pass `null` to opt out — only do this for trusted-self surfaces
|
|
26
|
+
* or when a downstream renderer applies its own sanitization. The opt-out
|
|
27
|
+
* is explicit and grep-able.
|
|
28
|
+
*/
|
|
29
|
+
sanitize?: SanitizeFn | null;
|
|
30
|
+
}
|
|
31
|
+
/** Create a configured Marked instance (sanitize-on by default). */
|
|
32
|
+
export declare function createMarked(opts?: CreateMarkedOptions): Marked;
|
|
33
|
+
/** Shared default instance for the common case (no custom options). */
|
|
34
|
+
export declare const marked: Marked;
|
package/dist/marked.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defaultImageHtml as a, createMarkedInstance as i } from "@blogmcp/sdk";
|
|
2
|
+
import { defaultImageHtml as s } from "@blogmcp/sdk";
|
|
3
|
+
import { sanitizeHtml as n } from "@blogmcp/sanitize";
|
|
4
|
+
import { sanitizeHtml as z } from "@blogmcp/sanitize";
|
|
5
|
+
function m(e = {}) {
|
|
6
|
+
const t = e.imageRenderer ?? a, r = e.sanitize === null ? void 0 : e.sanitize ?? n;
|
|
7
|
+
return i({ imageRenderer: t, sanitize: r });
|
|
8
|
+
}
|
|
9
|
+
const l = m();
|
|
10
|
+
export {
|
|
11
|
+
m as createMarked,
|
|
12
|
+
s as defaultImageHtml,
|
|
13
|
+
l as marked,
|
|
14
|
+
z as sanitizeHtml
|
|
15
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React MDX Component Registry.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `@blogmcp/mdx-render-vue/src/registry.ts` against the shared schema.
|
|
5
|
+
* Attaches React-specific lazy render loaders to the framework-agnostic
|
|
6
|
+
* metadata published by `@blogmcp/mdx-schema`. Schema is the single source of
|
|
7
|
+
* truth for component descriptions, prop shapes, and MCP-facing examples.
|
|
8
|
+
*
|
|
9
|
+
* This registry is for future tooling (dynamic resolution, editor previews).
|
|
10
|
+
* The blog/RSC render path uses the static `serverRenderMap` instead.
|
|
11
|
+
*/
|
|
12
|
+
import { componentMetadata, getComponentMetadata, getComponentNames, type ComponentMetadata } from '@blogmcp/mdx-schema/metadata';
|
|
13
|
+
export { componentMetadata, getComponentMetadata, getComponentNames };
|
|
14
|
+
export interface ReactComponentRegistration {
|
|
15
|
+
metadata: ComponentMetadata;
|
|
16
|
+
/** Render variants: variant name → async loader. Must include 'default'. */
|
|
17
|
+
render: Record<string, () => Promise<{
|
|
18
|
+
default: unknown;
|
|
19
|
+
}>>;
|
|
20
|
+
/**
|
|
21
|
+
* Optional edit component loader. The blog only needs `render`; an editor
|
|
22
|
+
* (React md-editor port) would populate this later.
|
|
23
|
+
*/
|
|
24
|
+
edit?: () => Promise<{
|
|
25
|
+
default: unknown;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
/** Merged registry: schema metadata + React wiring, keyed by component name. */
|
|
29
|
+
export declare const reactRegistry: Record<string, ReactComponentRegistration>;
|
|
30
|
+
/** Get available render variant names for a component. */
|
|
31
|
+
export declare function getComponentVariants(name: string): string[];
|
|
32
|
+
/** Async resolver — loads only the requested render component. */
|
|
33
|
+
export declare function getRenderComponentAsync(name: string, variant?: string): Promise<unknown | undefined>;
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { componentMetadata as d } from "@blogmcp/mdx-schema/metadata";
|
|
2
|
+
import { componentMetadata as f, getComponentMetadata as s, getComponentNames as g } from "@blogmcp/mdx-schema/metadata";
|
|
3
|
+
const i = {
|
|
4
|
+
Callout: {
|
|
5
|
+
render: {
|
|
6
|
+
default: () => import("./render/callout/CalloutDefault.js"),
|
|
7
|
+
minimal: () => import("./render/callout/CalloutMinimal.js")
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
YouTube: {
|
|
11
|
+
render: {
|
|
12
|
+
default: () => import("./render/youtube/YouTubeDefault.js")
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
Gallery: {
|
|
16
|
+
render: {
|
|
17
|
+
default: () => import("./render/gallery/GalleryGrid.js"),
|
|
18
|
+
grid: () => import("./render/gallery/GalleryGrid.js")
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
CodeBlock: {
|
|
22
|
+
render: {
|
|
23
|
+
default: () => import("./render/codeblock/CodeBlockDefault.js")
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
AudioPlayer: {
|
|
27
|
+
render: {
|
|
28
|
+
default: () => import("./render/audioplayer/AudioPlayerDefault.js")
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
Button: {
|
|
32
|
+
render: {
|
|
33
|
+
default: () => import("./render/button/Button.js")
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
Card: {
|
|
37
|
+
render: {
|
|
38
|
+
default: () => import("./render/card/Card.js")
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
Alert: {
|
|
42
|
+
render: {
|
|
43
|
+
default: () => import("./render/alert/Alert.js")
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
Badge: {
|
|
47
|
+
render: {
|
|
48
|
+
default: () => import("./render/badge/Badge.js")
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
Icon: {
|
|
52
|
+
render: {
|
|
53
|
+
default: () => import("./render/icon/Icon.js")
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
Tooltip: {
|
|
57
|
+
render: {
|
|
58
|
+
default: () => import("./render/tooltip/Tooltip.js")
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}, o = Object.fromEntries(
|
|
62
|
+
d.map((e) => {
|
|
63
|
+
const r = i[e.name];
|
|
64
|
+
if (!r)
|
|
65
|
+
throw new Error(
|
|
66
|
+
`[mdx-render-react] No React wiring registered for component "${e.name}". Add an entry to reactWiring in registry.ts or remove the schema entry.`
|
|
67
|
+
);
|
|
68
|
+
return [e.name, { metadata: e, ...r }];
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
function u(e) {
|
|
72
|
+
const r = o[e];
|
|
73
|
+
return r ? Object.keys(r.render) : [];
|
|
74
|
+
}
|
|
75
|
+
async function p(e, r = "default") {
|
|
76
|
+
const t = o[e];
|
|
77
|
+
if (!t) return;
|
|
78
|
+
const n = t.render[r] || t.render.default;
|
|
79
|
+
return n ? (await n()).default : void 0;
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
f as componentMetadata,
|
|
83
|
+
s as getComponentMetadata,
|
|
84
|
+
g as getComponentNames,
|
|
85
|
+
u as getComponentVariants,
|
|
86
|
+
p as getRenderComponentAsync,
|
|
87
|
+
o as reactRegistry
|
|
88
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type AlertSeverity } from '@blogmcp/mdx-schema/styles';
|
|
3
|
+
export interface AlertProps {
|
|
4
|
+
severity?: AlertSeverity;
|
|
5
|
+
title?: string;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Inline notice — flatter/smaller than Callout. Mirrors Vue Alert.vue:
|
|
10
|
+
* severity → wrap classes + iconify icon + icon color (shared table),
|
|
11
|
+
* optional bold title, children inside a `prose-sm` body.
|
|
12
|
+
*/
|
|
13
|
+
export default function Alert({ severity, title, children }: AlertProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsxs as o, jsx as s } from "react/jsx-runtime";
|
|
2
|
+
import { Icon as l } from "@iconify/react";
|
|
3
|
+
import { alertStyles as r } from "@blogmcp/mdx-schema/styles";
|
|
4
|
+
function d({ severity: n = "info", title: i, children: a }) {
|
|
5
|
+
const e = r.severity[n];
|
|
6
|
+
return /* @__PURE__ */ o(
|
|
7
|
+
"div",
|
|
8
|
+
{
|
|
9
|
+
className: [r.base, e.wrap].join(" "),
|
|
10
|
+
role: "alert",
|
|
11
|
+
"data-mdx-component": "Alert",
|
|
12
|
+
children: [
|
|
13
|
+
/* @__PURE__ */ s(l, { icon: e.icon, className: ["mt-0.5 shrink-0", e.iconClass].join(" "), width: 18, height: 18 }),
|
|
14
|
+
/* @__PURE__ */ o("div", { className: "min-w-0 flex-1", children: [
|
|
15
|
+
i ? /* @__PURE__ */ s("div", { className: "mb-0.5 font-semibold", children: i }) : null,
|
|
16
|
+
/* @__PURE__ */ s("div", { className: "prose-sm", children: a })
|
|
17
|
+
] })
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
d as default
|
|
24
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AudioPlayerProps } from '@blogmcp/mdx-schema';
|
|
2
|
+
/**
|
|
3
|
+
* Audio player with a collapsible, lazy-loaded transcript. Client island —
|
|
4
|
+
* mirrors Vue AudioPlayerDefault.vue + useAudioPlayer.ts behaviour and classes.
|
|
5
|
+
*/
|
|
6
|
+
export default function AudioPlayerDefault({ slug, title, showTranscription }: AudioPlayerProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as n, jsx as r, Fragment as w } from "react/jsx-runtime";
|
|
3
|
+
import { useState as o, useCallback as x } from "react";
|
|
4
|
+
function j({ slug: e, title: l, showTranscription: h = !1 }) {
|
|
5
|
+
const y = !!l, f = l || e || "Audio", g = e ? `/api/media/file/${e}` : "", c = e ? `/api/media/transcription/${e}` : "", [t, s] = o(null), [d, b] = o(null), [v, m] = o(!1), [p, N] = o(!1), u = x(async () => {
|
|
6
|
+
if (!(!e || t !== null)) {
|
|
7
|
+
m(!0);
|
|
8
|
+
try {
|
|
9
|
+
const i = await fetch(c);
|
|
10
|
+
if (i.ok) {
|
|
11
|
+
const a = await i.json();
|
|
12
|
+
s(a.fullText || a.full_text || ""), b(a.summary || null);
|
|
13
|
+
} else
|
|
14
|
+
s("");
|
|
15
|
+
} catch {
|
|
16
|
+
s("");
|
|
17
|
+
} finally {
|
|
18
|
+
m(!1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}, [e, c, t]), k = x(() => {
|
|
22
|
+
N((i) => {
|
|
23
|
+
const a = !i;
|
|
24
|
+
return a && t === null && u(), a;
|
|
25
|
+
});
|
|
26
|
+
}, [u, t]);
|
|
27
|
+
return /* @__PURE__ */ n("div", { className: "audio-player my-6 rounded-lg bg-gray-50 p-5 border border-gray-200", "data-mdx-component": "AudioPlayer", children: [
|
|
28
|
+
/* @__PURE__ */ n("div", { className: "flex items-center gap-3 mb-3", children: [
|
|
29
|
+
/* @__PURE__ */ r("svg", { className: "w-5 h-5 text-gray-500 flex-shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ r(
|
|
30
|
+
"path",
|
|
31
|
+
{
|
|
32
|
+
strokeLinecap: "round",
|
|
33
|
+
strokeLinejoin: "round",
|
|
34
|
+
strokeWidth: 2,
|
|
35
|
+
d: "M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"
|
|
36
|
+
}
|
|
37
|
+
) }),
|
|
38
|
+
y ? /* @__PURE__ */ r("h3", { className: "text-base font-semibold text-gray-800 m-0", children: f }) : null
|
|
39
|
+
] }),
|
|
40
|
+
/* @__PURE__ */ r("audio", { src: g, controls: !0, preload: "metadata", className: "w-full" }),
|
|
41
|
+
h ? /* @__PURE__ */ n("div", { className: "mt-4", children: [
|
|
42
|
+
/* @__PURE__ */ n(
|
|
43
|
+
"button",
|
|
44
|
+
{
|
|
45
|
+
type: "button",
|
|
46
|
+
className: "flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 transition-colors cursor-pointer bg-transparent border-0 p-0",
|
|
47
|
+
onClick: k,
|
|
48
|
+
children: [
|
|
49
|
+
/* @__PURE__ */ r(
|
|
50
|
+
"svg",
|
|
51
|
+
{
|
|
52
|
+
className: ["w-4 h-4 transition-transform", p ? "rotate-90" : ""].join(" ").trim(),
|
|
53
|
+
fill: "none",
|
|
54
|
+
stroke: "currentColor",
|
|
55
|
+
viewBox: "0 0 24 24",
|
|
56
|
+
children: /* @__PURE__ */ r("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" })
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
"Transcription"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
p ? /* @__PURE__ */ r("div", { className: "mt-2 p-3 bg-white rounded border border-gray-200 text-sm text-gray-700 leading-relaxed max-h-64 overflow-y-auto", children: v ? /* @__PURE__ */ r("p", { className: "text-gray-400 italic", children: "Loading transcription..." }) : t ? /* @__PURE__ */ n(w, { children: [
|
|
64
|
+
d ? /* @__PURE__ */ n("div", { className: "mb-3 pb-3 border-b border-gray-200", children: [
|
|
65
|
+
/* @__PURE__ */ r("div", { className: "text-xs uppercase tracking-wide text-gray-400 mb-1", children: "Summary" }),
|
|
66
|
+
/* @__PURE__ */ r("div", { className: "text-gray-800", children: d })
|
|
67
|
+
] }) : null,
|
|
68
|
+
/* @__PURE__ */ r("div", { className: "whitespace-pre-wrap", children: t })
|
|
69
|
+
] }) : /* @__PURE__ */ r("p", { className: "text-gray-400 italic", children: "No transcription available." }) }) : null
|
|
70
|
+
] }) : null
|
|
71
|
+
] });
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
j as default
|
|
75
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type BadgeVariant, type BadgeColor } from '@blogmcp/mdx-schema/styles';
|
|
2
|
+
export interface BadgeProps {
|
|
3
|
+
label: string;
|
|
4
|
+
color?: BadgeColor;
|
|
5
|
+
variant?: BadgeVariant;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Small label / tag. Mirrors Vue Badge.vue — a single <span> with the
|
|
9
|
+
* shared base + variant×color class table.
|
|
10
|
+
*/
|
|
11
|
+
export default function Badge({ label, color, variant }: BadgeProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as r } from "react/jsx-runtime";
|
|
2
|
+
import { badgeStyles as a } from "@blogmcp/mdx-schema/styles";
|
|
3
|
+
function m({ label: o, color: t = "neutral", variant: e = "soft" }) {
|
|
4
|
+
const n = [
|
|
5
|
+
a.base,
|
|
6
|
+
a.variantColor[e][t]
|
|
7
|
+
].join(" ");
|
|
8
|
+
return /* @__PURE__ */ r("span", { className: n, "data-mdx-component": "Badge", children: o });
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
m as default
|
|
12
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ButtonVariant, type ButtonColor, type ButtonSize } from '@blogmcp/mdx-schema/styles';
|
|
2
|
+
export interface ButtonProps {
|
|
3
|
+
label: string;
|
|
4
|
+
to?: string;
|
|
5
|
+
variant?: ButtonVariant;
|
|
6
|
+
color?: ButtonColor;
|
|
7
|
+
size?: ButtonSize;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Action button / CTA link. Mirrors Vue Button.vue:
|
|
11
|
+
* renders <a> when `to` is set, else <button type="button">.
|
|
12
|
+
* Class tables come from the shared @blogmcp/mdx-schema/styles module.
|
|
13
|
+
*/
|
|
14
|
+
export default function Button({ label, to, variant, color, size, }: ButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as i } from "react/jsx-runtime";
|
|
2
|
+
import { buttonStyles as t } from "@blogmcp/mdx-schema/styles";
|
|
3
|
+
import { sanitizeUrl as d } from "@blogmcp/sanitize";
|
|
4
|
+
function p({
|
|
5
|
+
label: o,
|
|
6
|
+
to: n,
|
|
7
|
+
variant: a = "solid",
|
|
8
|
+
color: m = "primary",
|
|
9
|
+
size: s = "md"
|
|
10
|
+
}) {
|
|
11
|
+
const r = [
|
|
12
|
+
t.base,
|
|
13
|
+
t.size[s],
|
|
14
|
+
t.variantColor[a][m]
|
|
15
|
+
].join(" "), e = n ? d(n) : void 0;
|
|
16
|
+
return e ? /* @__PURE__ */ i("a", { href: e, className: r, "data-mdx-component": "Button", children: o }) : /* @__PURE__ */ i("button", { type: "button", className: r, "data-mdx-component": "Button", children: o });
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
p as default
|
|
20
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { CalloutType } from '@blogmcp/mdx-schema';
|
|
3
|
+
export interface CalloutProps {
|
|
4
|
+
type?: CalloutType;
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Standard callout (icon + accent left border). Mirrors Vue CalloutDefault.vue.
|
|
9
|
+
* Color table + icon SVG path data come from the shared styles module.
|
|
10
|
+
*/
|
|
11
|
+
export default function CalloutDefault({ type, children }: CalloutProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as e, jsxs as i } from "react/jsx-runtime";
|
|
2
|
+
import { calloutStyles as o } from "@blogmcp/mdx-schema/styles";
|
|
3
|
+
function d({ type: s, children: a }) {
|
|
4
|
+
const l = s || "info", t = o.default.type[l], n = o.iconPaths[l];
|
|
5
|
+
return /* @__PURE__ */ e(
|
|
6
|
+
"div",
|
|
7
|
+
{
|
|
8
|
+
className: [o.default.base, t.bg, t.border, t.text].join(" "),
|
|
9
|
+
"data-mdx-component": "Callout",
|
|
10
|
+
children: /* @__PURE__ */ i("div", { className: "flex items-start gap-3", children: [
|
|
11
|
+
/* @__PURE__ */ e("div", { className: "flex-shrink-0 mt-0.5", children: /* @__PURE__ */ e("svg", { className: ["w-5 h-5", t.iconColor].join(" "), fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ e("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: n }) }) }),
|
|
12
|
+
/* @__PURE__ */ e("div", { className: "flex-1 text-sm leading-relaxed", children: a })
|
|
13
|
+
] })
|
|
14
|
+
}
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
d as default
|
|
19
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { CalloutType } from '@blogmcp/mdx-schema';
|
|
3
|
+
export interface CalloutProps {
|
|
4
|
+
type?: CalloutType;
|
|
5
|
+
children?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Icon-less, flatter callout. Mirrors Vue CalloutMinimal.vue.
|
|
9
|
+
*/
|
|
10
|
+
export default function CalloutMinimal({ type, children }: CalloutProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as t } from "react/jsx-runtime";
|
|
2
|
+
import { calloutStyles as o } from "@blogmcp/mdx-schema/styles";
|
|
3
|
+
function r({ type: a, children: l }) {
|
|
4
|
+
const i = a || "info", e = o.minimal.type[i];
|
|
5
|
+
return /* @__PURE__ */ t(
|
|
6
|
+
"div",
|
|
7
|
+
{
|
|
8
|
+
className: [o.minimal.base, e.bg, e.border, e.text].join(" "),
|
|
9
|
+
"data-mdx-component": "Callout",
|
|
10
|
+
children: /* @__PURE__ */ t("div", { className: "text-sm leading-relaxed", children: l })
|
|
11
|
+
}
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
r as default
|
|
16
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface CardProps {
|
|
3
|
+
title?: string;
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Bordered content block with optional title. Mirrors Vue Card.vue.
|
|
8
|
+
*/
|
|
9
|
+
export default function Card({ title, children }: CardProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsxs as d, jsx as a } from "react/jsx-runtime";
|
|
2
|
+
function t({ title: r, children: e }) {
|
|
3
|
+
return /* @__PURE__ */ d(
|
|
4
|
+
"section",
|
|
5
|
+
{
|
|
6
|
+
className: "mdx-card rounded-lg border border-gray-200 bg-white p-5 shadow-sm",
|
|
7
|
+
"data-mdx-component": "Card",
|
|
8
|
+
children: [
|
|
9
|
+
r ? /* @__PURE__ */ a("h3", { className: "mb-3 text-lg font-semibold text-gray-900", children: r }) : null,
|
|
10
|
+
/* @__PURE__ */ a("div", { className: "text-gray-700", children: e })
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
t as default
|
|
17
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { CodeBlockProps as SchemaCodeBlockProps } from '@blogmcp/mdx-schema';
|
|
3
|
+
export interface CodeBlockProps extends SchemaCodeBlockProps {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Syntax-highlighted code block. Client island — mirrors Vue CodeBlockDefault.vue
|
|
8
|
+
* + useCodeBlock.ts. The header (language label + optional title) and the
|
|
9
|
+
* `<pre><code>` block render as the initial DOM exactly as before; the copy
|
|
10
|
+
* button now copies the code via `navigator.clipboard.writeText` and shows a
|
|
11
|
+
* transient "Copied!" state (2s), toggling the same Tailwind classes as Vue.
|
|
12
|
+
* `code-block` + `data-mdx-component="CodeBlock"` are unchanged.
|
|
13
|
+
*/
|
|
14
|
+
export default function CodeBlockDefault({ language, title, children }: CodeBlockProps): import("react/jsx-runtime").JSX.Element;
|