@roottale/cms-renderer-next 0.11.0 → 0.13.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/dist/mui.js ADDED
@@ -0,0 +1,62 @@
1
+ // src/mui.ts
2
+ function parseRadiusPx(raw) {
3
+ const v = raw.trim();
4
+ const num = Number.parseFloat(v);
5
+ if (Number.isNaN(num)) return void 0;
6
+ if (v.endsWith("rem")) return num * 16;
7
+ if (v.endsWith("em")) return num * 16;
8
+ return num;
9
+ }
10
+ function isPlainObject(x) {
11
+ return typeof x === "object" && x !== null && !Array.isArray(x);
12
+ }
13
+ function deepMerge(a, b) {
14
+ const out = { ...a };
15
+ for (const [k, bv] of Object.entries(b)) {
16
+ const av = out[k];
17
+ out[k] = isPlainObject(av) && isPlainObject(bv) ? deepMerge(av, bv) : bv;
18
+ }
19
+ return out;
20
+ }
21
+ function rootTaleThemeToMui(theme, overrides) {
22
+ const c = theme?.colors ?? {};
23
+ const f = theme?.fonts ?? {};
24
+ const r = theme?.radius ?? {};
25
+ const palette = {};
26
+ if (c.primary) {
27
+ palette.primary = {
28
+ main: c.primary,
29
+ ...c.primaryForeground ? { contrastText: c.primaryForeground } : {}
30
+ };
31
+ }
32
+ if (c.background) palette.background = { default: c.background };
33
+ if (c.foreground || c.mutedForeground) {
34
+ palette.text = {
35
+ ...c.foreground ? { primary: c.foreground } : {},
36
+ ...c.mutedForeground ? { secondary: c.mutedForeground } : {}
37
+ };
38
+ }
39
+ if (c.border) palette.divider = c.border;
40
+ const typography = {};
41
+ if (f.body) typography.fontFamily = f.body;
42
+ if (f.display) {
43
+ typography.h1 = { fontFamily: f.display };
44
+ typography.h2 = { fontFamily: f.display };
45
+ typography.h3 = { fontFamily: f.display };
46
+ typography.h4 = { fontFamily: f.display };
47
+ typography.h5 = { fontFamily: f.display };
48
+ typography.h6 = { fontFamily: f.display };
49
+ }
50
+ const base = {};
51
+ if (Object.keys(palette).length > 0) base.palette = palette;
52
+ if (Object.keys(typography).length > 0) base.typography = typography;
53
+ if (r.md) {
54
+ const radiusPx = parseRadiusPx(r.md);
55
+ if (radiusPx !== void 0) base.shape = { borderRadius: radiusPx };
56
+ }
57
+ return overrides ? deepMerge(base, overrides) : base;
58
+ }
59
+ export {
60
+ rootTaleThemeToMui
61
+ };
62
+ //# sourceMappingURL=mui.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mui.ts"],"sourcesContent":["/**\n * `@roottale/cms-renderer-next/mui` — `RootTaleTheme`(admin /design 에서 fetch 하는\n * 런타임 테마 객체) → MUI `ThemeOptions` 호환 객체 브리지.\n *\n * 목적(ADR-0040 §Decisions, P0.5 RFC §③): MUI 기반 고객 사이트(예: kjmtax)가\n * 자체 하드코딩 palette 를 버리고 admin /design 토큰을 **단일 SSoT** 로 쓰도록.\n * 탈-MUI 가 아니라 *이중 토큰 제거 + 공존 충돌 최소화* 가 목적이다.\n *\n * 설계:\n * - **런타임 MUI 의존 없음**. 반환값은 MUI `ThemeOptions` 에 구조적으로 assignable 한\n * 평범한 객체 — `@mui/material` 의 `createTheme()` 에 그대로 넘긴다. 이 패키지는\n * MUI-free 를 유지(비-MUI 고객사 roottale-web 은 이 서브패스를 import 하지 않음).\n * - 의미가 명확한 매핑만 수행. 모호한 토큰(`muted` 배경 등)은 CSS 변수\n * (`<RootTaleThemeProvider>`) 소비로 남긴다 — MUI 에 억지 매핑하지 않는다.\n *\n * ```ts\n * import { createTheme } from \"@mui/material/styles\";\n * import { fetchTheme } from \"@roottale/cms-client/server\";\n * import { rootTaleThemeToMui } from \"@roottale/cms-renderer-next/mui\";\n *\n * const rt = await fetchTheme({ apiKey: process.env.ROOTTALE_API_KEY! });\n * const theme = createTheme(rootTaleThemeToMui(rt, {\n * // 브랜드 고유값은 overrides 로 (deep-merge, overrides 우선).\n * palette: { secondary: { main: \"#0B1F49\" } },\n * }));\n * ```\n */\nimport type { RootTaleTheme } from \"@roottale/cms-client/server\";\n\n/** MUI `ThemeOptions` 의 구조적 부분집합 — `createTheme()` 에 assignable. */\nexport interface RootTaleMuiThemeOptions {\n palette?: {\n primary?: { main: string; contrastText?: string };\n background?: { default?: string };\n text?: { primary?: string; secondary?: string };\n divider?: string;\n [k: string]: unknown;\n };\n typography?: {\n fontFamily?: string;\n h1?: { fontFamily?: string };\n h2?: { fontFamily?: string };\n h3?: { fontFamily?: string };\n h4?: { fontFamily?: string };\n h5?: { fontFamily?: string };\n h6?: { fontFamily?: string };\n [k: string]: unknown;\n };\n shape?: { borderRadius?: number };\n [k: string]: unknown;\n}\n\n/** CSS 길이(\"8px\" / \"0.5rem\" / \"6\")를 MUI shape.borderRadius(px number)로 파싱. */\nfunction parseRadiusPx(raw: string): number | undefined {\n const v = raw.trim();\n const num = Number.parseFloat(v);\n if (Number.isNaN(num)) return undefined;\n if (v.endsWith(\"rem\")) return num * 16;\n if (v.endsWith(\"em\")) return num * 16;\n // \"8px\" | \"8\" | \"0\" 모두 px 로 간주.\n return num;\n}\n\nfunction isPlainObject(x: unknown): x is Record<string, unknown> {\n return typeof x === \"object\" && x !== null && !Array.isArray(x);\n}\n\n/** plain object 깊은 병합. `b`(overrides) 우선. 배열/원시값은 b 가 덮어씀. */\nfunction deepMerge<T extends Record<string, unknown>>(a: T, b: Record<string, unknown>): T {\n const out: Record<string, unknown> = { ...a };\n for (const [k, bv] of Object.entries(b)) {\n const av = out[k];\n out[k] = isPlainObject(av) && isPlainObject(bv) ? deepMerge(av, bv) : bv;\n }\n return out as T;\n}\n\n/**\n * `RootTaleTheme` → MUI `ThemeOptions` 호환 객체.\n *\n * 매핑:\n * - `colors.primary` → `palette.primary.main`, `colors.primaryForeground` → `…contrastText`\n * - `colors.background` → `palette.background.default`\n * - `colors.foreground` → `palette.text.primary`, `colors.mutedForeground` → `…secondary`\n * - `colors.border` → `palette.divider`\n * - `fonts.body` → `typography.fontFamily`, `fonts.display` → `typography.h1..h6.fontFamily`\n * - `radius.md` → `shape.borderRadius`(px)\n *\n * `colors.muted` 는 명확한 MUI 대응이 없어 의도적으로 미매핑(CSS 변수로 소비).\n *\n * @param theme admin /design 테마(빈/부분 객체 허용)\n * @param overrides 브랜드 고유값(deep-merge, overrides 우선)\n */\nexport function rootTaleThemeToMui(\n theme: RootTaleTheme | null | undefined,\n overrides?: RootTaleMuiThemeOptions,\n): RootTaleMuiThemeOptions {\n const c = theme?.colors ?? {};\n const f = theme?.fonts ?? {};\n const r = theme?.radius ?? {};\n\n const palette: NonNullable<RootTaleMuiThemeOptions[\"palette\"]> = {};\n if (c.primary) {\n palette.primary = {\n main: c.primary,\n ...(c.primaryForeground ? { contrastText: c.primaryForeground } : {}),\n };\n }\n if (c.background) palette.background = { default: c.background };\n if (c.foreground || c.mutedForeground) {\n palette.text = {\n ...(c.foreground ? { primary: c.foreground } : {}),\n ...(c.mutedForeground ? { secondary: c.mutedForeground } : {}),\n };\n }\n if (c.border) palette.divider = c.border;\n\n const typography: NonNullable<RootTaleMuiThemeOptions[\"typography\"]> = {};\n if (f.body) typography.fontFamily = f.body;\n if (f.display) {\n typography.h1 = { fontFamily: f.display };\n typography.h2 = { fontFamily: f.display };\n typography.h3 = { fontFamily: f.display };\n typography.h4 = { fontFamily: f.display };\n typography.h5 = { fontFamily: f.display };\n typography.h6 = { fontFamily: f.display };\n }\n\n const base: RootTaleMuiThemeOptions = {};\n if (Object.keys(palette).length > 0) base.palette = palette;\n if (Object.keys(typography).length > 0) base.typography = typography;\n if (r.md) {\n const radiusPx = parseRadiusPx(r.md);\n if (radiusPx !== undefined) base.shape = { borderRadius: radiusPx };\n }\n\n return overrides ? deepMerge(base, overrides as Record<string, unknown>) : base;\n}\n"],"mappings":";AAqDA,SAAS,cAAc,KAAiC;AACtD,QAAM,IAAI,IAAI,KAAK;AACnB,QAAM,MAAM,OAAO,WAAW,CAAC;AAC/B,MAAI,OAAO,MAAM,GAAG,EAAG,QAAO;AAC9B,MAAI,EAAE,SAAS,KAAK,EAAG,QAAO,MAAM;AACpC,MAAI,EAAE,SAAS,IAAI,EAAG,QAAO,MAAM;AAEnC,SAAO;AACT;AAEA,SAAS,cAAc,GAA0C;AAC/D,SAAO,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,MAAM,QAAQ,CAAC;AAChE;AAGA,SAAS,UAA6C,GAAM,GAA+B;AACzF,QAAM,MAA+B,EAAE,GAAG,EAAE;AAC5C,aAAW,CAAC,GAAG,EAAE,KAAK,OAAO,QAAQ,CAAC,GAAG;AACvC,UAAM,KAAK,IAAI,CAAC;AAChB,QAAI,CAAC,IAAI,cAAc,EAAE,KAAK,cAAc,EAAE,IAAI,UAAU,IAAI,EAAE,IAAI;AAAA,EACxE;AACA,SAAO;AACT;AAkBO,SAAS,mBACd,OACA,WACyB;AACzB,QAAM,IAAI,OAAO,UAAU,CAAC;AAC5B,QAAM,IAAI,OAAO,SAAS,CAAC;AAC3B,QAAM,IAAI,OAAO,UAAU,CAAC;AAE5B,QAAM,UAA2D,CAAC;AAClE,MAAI,EAAE,SAAS;AACb,YAAQ,UAAU;AAAA,MAChB,MAAM,EAAE;AAAA,MACR,GAAI,EAAE,oBAAoB,EAAE,cAAc,EAAE,kBAAkB,IAAI,CAAC;AAAA,IACrE;AAAA,EACF;AACA,MAAI,EAAE,WAAY,SAAQ,aAAa,EAAE,SAAS,EAAE,WAAW;AAC/D,MAAI,EAAE,cAAc,EAAE,iBAAiB;AACrC,YAAQ,OAAO;AAAA,MACb,GAAI,EAAE,aAAa,EAAE,SAAS,EAAE,WAAW,IAAI,CAAC;AAAA,MAChD,GAAI,EAAE,kBAAkB,EAAE,WAAW,EAAE,gBAAgB,IAAI,CAAC;AAAA,IAC9D;AAAA,EACF;AACA,MAAI,EAAE,OAAQ,SAAQ,UAAU,EAAE;AAElC,QAAM,aAAiE,CAAC;AACxE,MAAI,EAAE,KAAM,YAAW,aAAa,EAAE;AACtC,MAAI,EAAE,SAAS;AACb,eAAW,KAAK,EAAE,YAAY,EAAE,QAAQ;AACxC,eAAW,KAAK,EAAE,YAAY,EAAE,QAAQ;AACxC,eAAW,KAAK,EAAE,YAAY,EAAE,QAAQ;AACxC,eAAW,KAAK,EAAE,YAAY,EAAE,QAAQ;AACxC,eAAW,KAAK,EAAE,YAAY,EAAE,QAAQ;AACxC,eAAW,KAAK,EAAE,YAAY,EAAE,QAAQ;AAAA,EAC1C;AAEA,QAAM,OAAgC,CAAC;AACvC,MAAI,OAAO,KAAK,OAAO,EAAE,SAAS,EAAG,MAAK,UAAU;AACpD,MAAI,OAAO,KAAK,UAAU,EAAE,SAAS,EAAG,MAAK,aAAa;AAC1D,MAAI,EAAE,IAAI;AACR,UAAM,WAAW,cAAc,EAAE,EAAE;AACnC,QAAI,aAAa,OAAW,MAAK,QAAQ,EAAE,cAAc,SAAS;AAAA,EACpE;AAEA,SAAO,YAAY,UAAU,MAAM,SAAoC,IAAI;AAC7E;","names":[]}
package/dist/server.d.ts CHANGED
@@ -31,24 +31,24 @@ declare function RootTalePostCard(props: RootTalePostCardProps): ReactElement;
31
31
  /**
32
32
  * `@roottale/cms-renderer/toc` — Tiptap doc → TOC entries + heading id injection.
33
33
  *
34
- * Server-only utility. Walks a Tiptap JSON doc, extracts H2/H3 headings, and
34
+ * Server-only utility. Walks a Tiptap JSON doc, extracts H1-H4 headings, and
35
35
  * derives stable kebab-case ids (Korean-safe). The companion `attachHeadingIds`
36
- * mutates a shallow clone of the doc so renderers can emit `<h2 id="...">`
37
- * anchors that match what `<RootTaleTableOfContents>` links to.
36
+ * mutates a shallow clone of the doc so renderers can emit heading anchors
37
+ * that match what `<RootTaleTableOfContents>` links to.
38
38
  *
39
39
  * Internal ancestor: `roottale-internal/packages/cms-renderer/src/toc.ts`
40
40
  * (HTML-string based, vendored snapshot). Ported to Tiptap-JSON because the
41
41
  * platform renderer no longer round-trips through HTML before render.
42
42
  */
43
43
  interface TocEntry {
44
- level: 2 | 3;
44
+ level: 2 | 3 | 4;
45
45
  text: string;
46
46
  id: string;
47
47
  }
48
48
  declare function headingToId(text: string): string;
49
49
  declare function extractToc(doc: Record<string, unknown>): TocEntry[];
50
50
  /**
51
- * Returns a shallow-cloned Tiptap doc where each H2/H3 carries the matching
51
+ * Returns a shallow-cloned Tiptap doc where each H1-H4 carries the matching
52
52
  * `attrs.id` from `entries` (same order as `extractToc` returns). Existing
53
53
  * `id` attrs are overwritten so renderer + TOC always agree on anchors.
54
54
  */
@@ -232,7 +232,7 @@ type RootTaleBlogPostProps = {
232
232
  notFoundElement?: ReactElement;
233
233
  /**
234
234
  * When true, prepends an auto-derived `<RootTaleTableOfContents>` before
235
- * the article body (H2/H3 headings only). Default: false — keep markup
235
+ * the article body (H2-H4 headings only). Default: false — keep markup
236
236
  * stable for customers who already control their own layout.
237
237
  */
238
238
  showTableOfContents?: boolean;
package/dist/server.js CHANGED
@@ -16,14 +16,18 @@ function RootTalePostCard(props) {
16
16
  const showDate = display ? display.showDate : true;
17
17
  const showAuthor = display ? display.showAuthor : false;
18
18
  const authorName = post.authorName;
19
- const meta = [
20
- showDate ? formatPublishedDate(post.publishedAt) : null,
21
- showAuthor && authorName ? `\uC791\uC131\uC790 ${authorName}` : null
22
- ].filter(Boolean).join(" \xB7 ");
19
+ const publishedDate = showDate ? formatPublishedDate(post.publishedAt) : "";
23
20
  return /* @__PURE__ */ jsx("article", { "data-roottale-cms": "card", className: "rt-cms-card", children: /* @__PURE__ */ jsxs("a", { className: "rt-cms-card-link", href: href(post), children: [
24
- meta ? /* @__PURE__ */ jsx("p", { className: "rt-cms-meta", children: meta }) : null,
21
+ publishedDate || showAuthor && authorName ? /* @__PURE__ */ jsxs("p", { className: "rt-cms-meta", children: [
22
+ publishedDate ? /* @__PURE__ */ jsx("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "date", children: publishedDate }) : null,
23
+ showAuthor && authorName ? /* @__PURE__ */ jsxs("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "author", children: [
24
+ "\uC791\uC131\uC790 ",
25
+ authorName
26
+ ] }) : null
27
+ ] }) : null,
25
28
  /* @__PURE__ */ jsx("h2", { className: "rt-cms-title", children: post.title }),
26
- post.excerpt ? /* @__PURE__ */ jsx("p", { className: "rt-cms-excerpt", children: post.excerpt }) : null
29
+ post.excerpt ? /* @__PURE__ */ jsx("p", { className: "rt-cms-excerpt", children: post.excerpt }) : null,
30
+ /* @__PURE__ */ jsx("span", { className: "rt-cms-card-action", "aria-hidden": "true", children: "\uC77D\uAE30" })
27
31
  ] }) });
28
32
  }
29
33
  function defaultHref(post) {
@@ -44,8 +48,11 @@ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
44
48
  function RootTaleTableOfContents(props) {
45
49
  const { entries, title = "\uBAA9\uCC28" } = props;
46
50
  if (entries.length === 0) return null;
47
- return /* @__PURE__ */ jsxs2("nav", { "data-roottale-cms": "toc", className: "rt-cms-toc", "aria-label": title, children: [
48
- title ? /* @__PURE__ */ jsx2("p", { className: "rt-cms-toc-title", children: title }) : null,
51
+ return /* @__PURE__ */ jsx2("nav", { "data-roottale-cms": "toc", className: "rt-cms-toc", "aria-label": title, children: /* @__PURE__ */ jsxs2("details", { className: "rt-cms-toc-details", open: true, children: [
52
+ /* @__PURE__ */ jsxs2("summary", { className: "rt-cms-toc-summary", children: [
53
+ title ? /* @__PURE__ */ jsx2("span", { className: "rt-cms-toc-title", children: title }) : null,
54
+ /* @__PURE__ */ jsx2("span", { className: "rt-cms-toc-toggle", "aria-hidden": "true" })
55
+ ] }),
49
56
  /* @__PURE__ */ jsx2("ol", { className: "rt-cms-toc-list", children: entries.map((entry) => /* @__PURE__ */ jsx2(
50
57
  "li",
51
58
  {
@@ -54,10 +61,15 @@ function RootTaleTableOfContents(props) {
54
61
  },
55
62
  entry.id
56
63
  )) })
57
- ] });
64
+ ] }) });
58
65
  }
59
66
 
60
67
  // src/toc.ts
68
+ function tocLevel(rawLevel) {
69
+ if (rawLevel < 1 || rawLevel > 4) return null;
70
+ if (rawLevel < 3) return 2;
71
+ return rawLevel === 3 ? 3 : 4;
72
+ }
61
73
  function headingToId(text) {
62
74
  return text.trim().toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
63
75
  }
@@ -74,14 +86,15 @@ function extractToc(doc) {
74
86
  for (const node of content) {
75
87
  if (node?.type !== "heading") continue;
76
88
  const rawLevel = Number(node.attrs?.level ?? 2);
77
- if (rawLevel !== 2 && rawLevel !== 3) continue;
89
+ const level = tocLevel(rawLevel);
90
+ if (level === null) continue;
78
91
  const text = textOf(node).trim();
79
92
  if (!text) continue;
80
93
  const base = headingToId(text) || "heading";
81
94
  const count = idCounts.get(base) ?? 0;
82
95
  const id = count === 0 ? base : `${base}-${count}`;
83
96
  idCounts.set(base, count + 1);
84
- entries.push({ level: rawLevel, text, id });
97
+ entries.push({ level, text, id });
85
98
  }
86
99
  return entries;
87
100
  }
@@ -92,7 +105,7 @@ function attachHeadingIds(doc, entries) {
92
105
  const nextContent = content.map((node) => {
93
106
  if (node?.type !== "heading") return node;
94
107
  const rawLevel = Number(node.attrs?.level ?? 2);
95
- if (rawLevel !== 2 && rawLevel !== 3) return node;
108
+ if (rawLevel < 1 || rawLevel > 4) return node;
96
109
  if (!textOf(node).trim()) return node;
97
110
  const entry = entries[cursor++];
98
111
  if (!entry) return node;
@@ -314,12 +327,13 @@ function RootTaleLeadForm(props) {
314
327
  }
315
328
 
316
329
  // src/block-to-react.tsx
330
+ import { sanitizeHtml } from "@roottale/cms-core";
317
331
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
318
332
  function isString(value) {
319
333
  return typeof value === "string";
320
334
  }
321
335
  function htmlContent(html) {
322
- return { __html: html };
336
+ return { __html: sanitizeHtml(html) };
323
337
  }
324
338
  function renderInner(blocks) {
325
339
  return blocks.map((b, i) => renderBlock(b, i));
@@ -340,18 +354,15 @@ function renderBlock(block, key = 0) {
340
354
  }
341
355
  case "core/heading": {
342
356
  const levelRaw = block.attributes.level;
343
- const level = typeof levelRaw === "number" && levelRaw >= 1 && levelRaw <= 6 ? levelRaw : 2;
357
+ const level = typeof levelRaw === "number" && levelRaw >= 2 && levelRaw <= 4 ? levelRaw : 2;
344
358
  const text = isString(block.attributes.content) ? block.attributes.content : block.rawHtml ?? "";
345
359
  const props = {
346
360
  "data-block-id": dataBlockId,
347
361
  dangerouslySetInnerHTML: htmlContent(text)
348
362
  };
349
- if (level === 1) return /* @__PURE__ */ jsx5("h1", { ...props }, key);
350
363
  if (level === 2) return /* @__PURE__ */ jsx5("h2", { ...props }, key);
351
364
  if (level === 3) return /* @__PURE__ */ jsx5("h3", { ...props }, key);
352
- if (level === 4) return /* @__PURE__ */ jsx5("h4", { ...props }, key);
353
- if (level === 5) return /* @__PURE__ */ jsx5("h5", { ...props }, key);
354
- return /* @__PURE__ */ jsx5("h6", { ...props }, key);
365
+ return /* @__PURE__ */ jsx5("h4", { ...props }, key);
355
366
  }
356
367
  case "core/image": {
357
368
  const src = isString(block.attributes.url) ? block.attributes.url : "";
@@ -513,10 +524,14 @@ async function RootTaleBlogPost(props) {
513
524
  const toc = display.showTableOfContents ? extractToc(post.bodyJson) : [];
514
525
  const renderDoc = toc.length > 0 ? attachHeadingIds(post.bodyJson, toc) : post.bodyJson;
515
526
  const authorName = post.authorName;
516
- const meta = [
517
- display.showDate ? formatPublishedDate2(post.publishedAt) : null,
518
- display.showAuthor && authorName ? `\uC791\uC131\uC790 ${authorName}` : null
519
- ].filter(Boolean).join(" \xB7 ");
527
+ const authorImageUrl = post.authorImageUrl;
528
+ const postAuthorBio = post.authorBio;
529
+ const publishedDate = display.showDate ? formatPublishedDate2(post.publishedAt) : "";
530
+ const showTopAuthor = display.showAuthor && Boolean(authorName);
531
+ const displayAuthorName = display.authorProfileName ?? authorName;
532
+ const authorBio = display.authorProfileBio ?? postAuthorBio ?? display.authorCardDescription;
533
+ const authorImageUrlForCard = display.authorProfileImageUrl ?? authorImageUrl;
534
+ const showAuthorCard = display.showAuthorCard && Boolean(displayAuthorName);
520
535
  return /* @__PURE__ */ jsxs6(
521
536
  "article",
522
537
  {
@@ -524,17 +539,48 @@ async function RootTaleBlogPost(props) {
524
539
  className: "rt-cms-article",
525
540
  style: themeStyle ?? void 0,
526
541
  children: [
527
- /* @__PURE__ */ jsxs6("header", { children: [
528
- meta ? /* @__PURE__ */ jsx6("p", { className: "rt-cms-meta", children: meta }) : null,
542
+ /* @__PURE__ */ jsxs6("header", { className: "rt-cms-post-header", children: [
543
+ publishedDate || showTopAuthor ? /* @__PURE__ */ jsxs6("p", { className: "rt-cms-meta", children: [
544
+ publishedDate ? /* @__PURE__ */ jsx6("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "date", children: publishedDate }) : null,
545
+ showTopAuthor ? /* @__PURE__ */ jsxs6("span", { className: "rt-cms-meta-item", "data-rt-cms-meta": "author", children: [
546
+ "\uC791\uC131\uC790 ",
547
+ authorName
548
+ ] }) : null
549
+ ] }) : null,
529
550
  /* @__PURE__ */ jsx6("h1", { className: "rt-cms-title", children: post.title }),
530
551
  post.excerpt ? /* @__PURE__ */ jsx6("p", { className: "rt-cms-excerpt", children: post.excerpt }) : null
531
552
  ] }),
532
553
  toc.length > 0 ? /* @__PURE__ */ jsx6(RootTaleTableOfContents, { entries: toc, title: display.tocTitle }) : null,
533
- /* @__PURE__ */ jsx6(RenderTiptap, { doc: renderDoc })
554
+ /* @__PURE__ */ jsx6("div", { "data-roottale-cms": "body", className: "rt-cms-body", children: /* @__PURE__ */ jsx6(RenderTiptap, { doc: renderDoc }) }),
555
+ showAuthorCard && displayAuthorName ? /* @__PURE__ */ jsxs6("section", { className: "rt-cms-author-card", "aria-label": "\uC791\uC131\uC790", children: [
556
+ authorImageUrlForCard ? /* @__PURE__ */ jsx6(
557
+ "img",
558
+ {
559
+ className: "rt-cms-author-card__image",
560
+ src: authorImageUrlForCard,
561
+ alt: "",
562
+ loading: "lazy"
563
+ }
564
+ ) : /* @__PURE__ */ jsx6("div", { className: "rt-cms-author-card__avatar", "aria-hidden": "true", children: authorInitials(displayAuthorName) }),
565
+ /* @__PURE__ */ jsxs6("div", { className: "rt-cms-author-card__body", children: [
566
+ /* @__PURE__ */ jsx6("p", { className: "rt-cms-author-card__eyebrow", children: display.authorCardEyebrow }),
567
+ /* @__PURE__ */ jsx6("p", { className: "rt-cms-author-card__name", children: displayAuthorName }),
568
+ authorBio ? /* @__PURE__ */ jsx6("p", { className: "rt-cms-author-card__description", children: authorBio }) : null
569
+ ] })
570
+ ] }) : null
534
571
  ]
535
572
  }
536
573
  );
537
574
  }
575
+ function authorInitials(name) {
576
+ const trimmed = name.trim();
577
+ if (!trimmed) return "";
578
+ const parts = trimmed.split(/\s+/).filter(Boolean);
579
+ if (parts.length >= 2) {
580
+ return `${parts[0]?.[0] ?? ""}${parts[1]?.[0] ?? ""}`.toUpperCase();
581
+ }
582
+ return [...trimmed].slice(0, 2).join("").toUpperCase();
583
+ }
538
584
  async function resolveThemeStyle(input) {
539
585
  const { themeProp, apiKey, baseUrl } = input;
540
586
  if (themeProp === null) return null;
@@ -604,13 +650,14 @@ function renderNode(node, key) {
604
650
  case "paragraph":
605
651
  return /* @__PURE__ */ jsx6("p", { style: alignStyle(node), children }, key);
606
652
  case "heading": {
607
- const level = Math.min(Math.max(Number(node.attrs?.level ?? 2), 1), 3);
653
+ const rawLevel = Number(node.attrs?.level ?? 2);
654
+ const level = Math.min(Math.max(Number.isFinite(rawLevel) ? rawLevel : 2, 2), 4);
608
655
  const idAttr = node.attrs?.id;
609
656
  const id = typeof idAttr === "string" && idAttr.length > 0 ? idAttr : void 0;
610
657
  const style = alignStyle(node);
611
- if (level === 1) return /* @__PURE__ */ jsx6("h1", { id, style, children }, key);
612
658
  if (level === 2) return /* @__PURE__ */ jsx6("h2", { id, style, children }, key);
613
- return /* @__PURE__ */ jsx6("h3", { id, style, children }, key);
659
+ if (level === 3) return /* @__PURE__ */ jsx6("h3", { id, style, children }, key);
660
+ return /* @__PURE__ */ jsx6("h4", { id, style, children }, key);
614
661
  }
615
662
  case "bulletList":
616
663
  return /* @__PURE__ */ jsx6("ul", { children }, key);