hd-garden 0.3.13 → 0.4.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.
Files changed (58) hide show
  1. package/app/a2/[slug]/page.tsx +61 -0
  2. package/app/a2/page.tsx +27 -0
  3. package/app/blog/[...path]/page.tsx +33 -22
  4. package/app/blog/page.tsx +6 -7
  5. package/app/columns/[column]/[...path]/page.tsx +33 -25
  6. package/app/columns/page.tsx +38 -52
  7. package/app/components/ArticleMeasure.tsx +95 -0
  8. package/app/components/ArticleTitle.tsx +20 -0
  9. package/app/components/BlogArchive.tsx +106 -102
  10. package/app/components/MarkdownArticle.tsx +145 -66
  11. package/app/components/MermaidDiagram.tsx +44 -71
  12. package/app/components/SiteFooter.tsx +3 -7
  13. package/app/components/SiteHeader.tsx +55 -41
  14. package/app/components/TableOfContents.tsx +3 -2
  15. package/app/components/TagIndex.tsx +97 -0
  16. package/app/components/garden/GardenBlock.tsx +45 -0
  17. package/app/components/garden/blocks.tsx +530 -0
  18. package/app/components/garden/chrome.tsx +350 -0
  19. package/app/components/garden/sandbox-message.ts +16 -0
  20. package/app/garden-modules.d.ts +2 -0
  21. package/app/globals.css +17 -12
  22. package/app/layout.tsx +9 -0
  23. package/app/lib/a2-meta.ts +22 -0
  24. package/app/lib/a2-preview.ts +50 -0
  25. package/app/lib/content.ts +48 -5
  26. package/app/lib/markdown-plugins.ts +2 -0
  27. package/app/lib/remark-a2.mjs +208 -0
  28. package/app/lib/remark-garden.mjs +88 -0
  29. package/app/page.tsx +122 -93
  30. package/app/styles/a2.css +1383 -0
  31. package/app/styles/shell.css +438 -0
  32. package/app/styles/tokens.css +111 -0
  33. package/app/tags/page.tsx +28 -0
  34. package/app/ui-parity/page.tsx +3 -2
  35. package/package.json +1 -1
  36. package/scripts/build-content.mjs +11 -1
  37. package/scripts/check-post.mjs +18 -4
  38. package/scripts/new-post.mjs +3 -0
  39. package/site.config.yaml +1 -1
  40. package/templates/garden-blocks.md +128 -0
  41. package/templates/garden-snippets.plist +121 -0
  42. package/ui/garden/catalog.mjs +196 -0
  43. package/ui/garden/check.mjs +103 -0
  44. package/ui/garden/enhance.mjs +89 -0
  45. package/ui/garden/parse.mjs +198 -0
  46. package/ui/garden/snippets.mjs +109 -0
  47. package/ui/garden/title.mjs +37 -0
  48. package/ui/garden/word-count.mjs +266 -0
  49. package/ui/host.mjs +45 -28
  50. package/ui/parity-fixtures.mjs +1 -1
  51. package/ui/preview/dist/garden-ui.css +81 -77
  52. package/ui/preview/dist/garden-ui.esm.js +124 -89
  53. package/ui/preview/dist/garden-ui.iife.js +124 -89
  54. package/ui/prompt/rules.md +17 -1
  55. package/ui/react/CatalogPage.tsx +12 -0
  56. package/ui/react/components/display.tsx +1 -1
  57. package/ui/react/components/media.tsx +2 -2
  58. package/ui/react/ui.css +81 -77
@@ -4,17 +4,38 @@ import { useEffect, useId, useState } from "react";
4
4
 
5
5
  let mermaidRenderQueue: Promise<unknown> = Promise.resolve();
6
6
 
7
+ function token(name: string) {
8
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
9
+ }
10
+
7
11
  function renderMermaid(source: string, id: string) {
8
12
  const task = mermaidRenderQueue.then(async () => {
9
13
  const mermaid = (await import("mermaid")).default;
14
+ const dark = document.documentElement.getAttribute("data-theme") === "dark";
15
+ const prepared = source
16
+ .replace(/^\s*classDef .*$/gm, "")
17
+ .replace(/^\s*style .*$/gm, "");
10
18
  mermaid.initialize({
11
19
  startOnLoad: false,
12
- theme: "neutral",
13
- fontFamily: "var(--font-mono)",
14
20
  securityLevel: "strict",
15
21
  suppressErrorRendering: true,
22
+ theme: "base",
23
+ fontFamily: token("--sans") || "-apple-system, PingFang SC, sans-serif",
24
+ flowchart: { useMaxWidth: true, htmlLabels: false },
25
+ themeVariables: {
26
+ darkMode: dark,
27
+ background: token("--paper"),
28
+ primaryColor: token("--card"),
29
+ primaryBorderColor: token("--rule-strong"),
30
+ primaryTextColor: token("--ink"),
31
+ lineColor: token("--muted"),
32
+ secondaryColor: token("--well"),
33
+ tertiaryColor: token("--well"),
34
+ fontSize: "14px",
35
+ edgeLabelBackground: token("--paper"),
36
+ },
16
37
  });
17
- return mermaid.render(id, source);
38
+ return mermaid.render(id, prepared);
18
39
  });
19
40
  mermaidRenderQueue = task.then(
20
41
  () => undefined,
@@ -25,93 +46,45 @@ function renderMermaid(source: string, id: string) {
25
46
 
26
47
  export function MermaidDiagram({ source }: { source: string }) {
27
48
  const reactId = useId();
28
- const [view, setView] = useState<"render" | "code">("render");
29
49
  const [svg, setSvg] = useState("");
30
- const [error, setError] = useState(false);
31
- const panelId = `mermaid-panel-${reactId.replaceAll(":", "")}`;
50
+ const [theme, setTheme] = useState("light");
51
+
52
+ useEffect(() => {
53
+ const sync = () => setTheme(document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light");
54
+ sync();
55
+ const observer = new MutationObserver(sync);
56
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
57
+ return () => observer.disconnect();
58
+ }, []);
32
59
 
33
60
  useEffect(() => {
34
61
  let active = true;
35
62
  const draw = async () => {
36
- setError(false);
63
+ setSvg("");
37
64
  try {
38
- const id = `mermaid-${reactId.replaceAll(":", "")}`;
65
+ const id = `mermaid-${reactId.replaceAll(":", "")}-${theme}`;
39
66
  const result = await renderMermaid(source, id);
40
67
  if (active) setSvg(result.svg);
41
68
  } catch {
42
- if (active) setError(true);
69
+ if (active) setSvg("");
43
70
  }
44
71
  };
45
72
  void draw();
46
73
  return () => {
47
74
  active = false;
48
75
  };
49
- }, [reactId, source]);
76
+ }, [reactId, source, theme]);
50
77
 
51
78
  return (
52
79
  <figure
53
- className="mermaid-block language-mermaid"
54
- data-mermaid-theme="neutral"
80
+ className={`g-mermaid mermaid-block language-mermaid${svg ? " ok" : ""}`}
81
+ data-g="mermaid"
82
+ data-mermaid-theme="a2"
55
83
  >
56
- <figcaption className="mermaid-toolbar">
57
- <span>Mermaid</span>
58
- <span
59
- className="mermaid-view-switcher"
60
- role="tablist"
61
- aria-label="切换 Mermaid 显示方式"
62
- >
63
- <button
64
- type="button"
65
- role="tab"
66
- aria-selected={view === "render"}
67
- aria-controls={panelId}
68
- tabIndex={view === "render" ? 0 : -1}
69
- onClick={() => setView("render")}
70
- >
71
- 图表
72
- </button>
73
- <button
74
- type="button"
75
- role="tab"
76
- aria-selected={view === "code"}
77
- aria-controls={panelId}
78
- tabIndex={view === "code" ? 0 : -1}
79
- onClick={() => setView("code")}
80
- >
81
- Code
82
- </button>
83
- </span>
84
- </figcaption>
85
- {view === "render" ? (
86
- <div
87
- className="mermaid-canvas"
88
- id={panelId}
89
- role="tabpanel"
90
- aria-live="polite"
91
- >
92
- {svg ? (
93
- <div
94
- className="mermaid-svg"
95
- dangerouslySetInnerHTML={{ __html: svg }}
96
- />
97
- ) : null}
98
- {!svg && !error ? (
99
- <span className="mermaid-loading">正在绘制图表…</span>
100
- ) : null}
101
- {error ? (
102
- <span className="mermaid-error">图表渲染失败,请检查语法。</span>
103
- ) : null}
104
- </div>
105
- ) : (
106
- <pre
107
- className="mermaid-code"
108
- id={panelId}
109
- role="tabpanel"
110
- data-language="MERMAID"
111
- >
112
- <code>{source}</code>
113
- </pre>
114
- )}
84
+ {svg ? <div className="g-mermaid-out mermaid-svg" dangerouslySetInnerHTML={{ __html: svg }} /> : null}
85
+ <pre className="code mermaid-src">
86
+ <code>{source}</code>
87
+ </pre>
115
88
  </figure>
116
89
  );
117
90
  }
@@ -3,18 +3,14 @@ import { githubUrl, siteConfig } from "../site.config";
3
3
 
4
4
  export function SiteFooter() {
5
5
  return (
6
- <footer className="site-footer">
7
- <div className="footer-inner">
6
+ <footer className="sh-footer">
7
+ <div className="sh-foot">
8
8
  <p>
9
9
  © {new Date().getFullYear()} {siteConfig.author.name} · {siteConfig.footer}
10
10
  <span className="footer-framework"> · 基于 <a href="https://github.com/hedon954/garden" target="_blank" rel="noreferrer">Garden</a> 构建</span>
11
11
  </p>
12
12
  <div>
13
- <a
14
- href={githubUrl}
15
- aria-label="GitHub"
16
- rel="me"
17
- >
13
+ <a href={githubUrl} aria-label="GitHub" rel="me">
18
14
  <GithubLogo size={19} />
19
15
  GitHub
20
16
  </a>
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
4
5
  import {
5
6
  useCallback,
6
7
  useEffect,
@@ -10,21 +11,15 @@ import {
10
11
  type KeyboardEvent,
11
12
  } from "react";
12
13
  import Fuse, { type FuseResultMatch } from "fuse.js";
13
- import {
14
- Cactus,
15
- MagnifyingGlass,
16
- Moon,
17
- Sun,
18
- X,
19
- } from "@phosphor-icons/react";
14
+ import { MagnifyingGlass, X } from "@phosphor-icons/react";
20
15
  import { searchRecords } from "../lib/content";
21
16
  import { siteConfig } from "../site.config";
22
17
 
23
18
  const nav = [
24
- { href: "/", label: "首页" },
25
- { href: "/blog", label: "博文" },
19
+ { href: "/blog", label: "文章" },
26
20
  { href: "/thoughts", label: "随想" },
27
21
  { href: "/columns", label: "专栏" },
22
+ { href: "/tags", label: "标签" },
28
23
  { href: "/about", label: "关于" },
29
24
  ];
30
25
 
@@ -141,6 +136,8 @@ function searchPreview(
141
136
  }
142
137
 
143
138
  export function SiteHeader() {
139
+ const pathname = usePathname() || "/";
140
+ const [menuOpen, setMenuOpen] = useState(false);
144
141
  const [searchOpen, setSearchOpen] = useState(false);
145
142
  const [query, setQuery] = useState("");
146
143
  const [dark, setDark] = useState(false);
@@ -176,6 +173,11 @@ export function SiteHeader() {
176
173
  return () => window.cancelAnimationFrame(frame);
177
174
  }, []);
178
175
 
176
+ useEffect(() => {
177
+ const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
178
+ document.documentElement.dataset.mod = isMac ? "mac" : "other";
179
+ }, []);
180
+
179
181
  useEffect(() => {
180
182
  const onKey = (event: KeyboardEvent) => {
181
183
  if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
@@ -272,49 +274,61 @@ export function SiteHeader() {
272
274
  window.localStorage.setItem("theme", next ? "dark" : "light");
273
275
  };
274
276
 
277
+ const current = (href: string) => {
278
+ if (href === "/blog") {
279
+ return pathname === "/blog" || pathname.startsWith("/blog/") || pathname.startsWith("/a2/");
280
+ }
281
+ return pathname === href || pathname.startsWith(`${href}/`);
282
+ };
283
+
275
284
  return (
276
285
  <>
277
- <header className="site-header">
278
- <div className="header-inner">
279
- <Link href="/" className="brand" aria-label={`${siteConfig.name} 首页`}>
280
- <span className="brand-mark">
281
- <Cactus size={34} weight="thin" aria-hidden="true" />
282
- </span>
283
- <span>
284
- <strong>{siteConfig.name}</strong>
285
- <small>{siteConfig.tagline}</small>
286
- </span>
287
- </Link>
288
-
289
- <nav className="main-nav" aria-label="主导航">
286
+ <header className="sh-header">
287
+ <div className="sh-bar">
288
+ <Link href="/" className="sh-word">{siteConfig.name}</Link>
289
+ <nav className="sh-nav" aria-label="站点导航">
290
290
  {nav.map((item) => (
291
- <Link key={item.href} href={item.href}>
291
+ <Link key={item.href} href={item.href} aria-current={current(item.href) ? "page" : undefined}>
292
292
  {item.label}
293
293
  </Link>
294
294
  ))}
295
295
  </nav>
296
-
297
- <div className="header-actions">
298
- <button
299
- ref={searchTriggerRef}
300
- className="icon-button search-trigger"
301
- onClick={openSearch}
302
- aria-label="搜索文章"
303
- aria-expanded={searchOpen}
304
- aria-controls="site-search-dialog"
305
- >
306
- <MagnifyingGlass size={22} weight="regular" />
307
- <span className="key-hint">⌘K</span>
296
+ <div className="sh-tools">
297
+ <button ref={searchTriggerRef} className="sh-search" type="button" onClick={openSearch} aria-label="搜索文章" aria-expanded={searchOpen} aria-controls="site-search-dialog">
298
+ <svg viewBox="0 0 20 20" aria-hidden="true">
299
+ <circle cx="9" cy="9" r="5.8" />
300
+ <path d="M13.3 13.3 17 17" />
301
+ </svg>
302
+ <span className="lbl">搜索文章</span>
303
+ <kbd><span className="kbd-mod kbd-mac">⌘</span><span className="kbd-mod kbd-other">Ctrl</span>{" "}K</kbd>
308
304
  </button>
309
- <button
310
- className="icon-button"
311
- onClick={toggleTheme}
312
- aria-label={dark ? "切换到浅色模式" : "切换到深色模式"}
313
- >
314
- {dark ? <Sun size={23} /> : <Moon size={23} />}
305
+ <button className="sh-icon sh-search-m" type="button" onClick={openSearch} aria-label="搜索文章">
306
+ <svg viewBox="0 0 20 20" aria-hidden="true">
307
+ <circle cx="9" cy="9" r="5.8" />
308
+ <path d="M13.3 13.3 17 17" />
309
+ </svg>
310
+ </button>
311
+ <button className="sh-icon sh-theme" type="button" onClick={toggleTheme} aria-label={dark ? "切换到浅色模式" : "切换到深色模式"}>
312
+ <svg className="i-moon" viewBox="0 0 20 20" aria-hidden="true">
313
+ <path d="M16.5 12.2A6.8 6.8 0 0 1 7.8 3.5a6.8 6.8 0 1 0 8.7 8.7Z" />
314
+ </svg>
315
+ <svg className="i-sun" viewBox="0 0 20 20" aria-hidden="true">
316
+ <circle cx="10" cy="10" r="3.6" />
317
+ <path d="M10 1.8v2M10 16.2v2M1.8 10h2M16.2 10h2M4.2 4.2l1.4 1.4M14.4 14.4l1.4 1.4M4.2 15.8l1.4-1.4M14.4 5.6l1.4-1.4" />
318
+ </svg>
319
+ </button>
320
+ <button className="sh-icon sh-menu-btn" type="button" aria-expanded={menuOpen} aria-controls="sh-menu" aria-label={menuOpen ? "关闭菜单" : "打开菜单"} onClick={() => setMenuOpen((open) => !open)}>
321
+ {menuOpen ? <X size={18} /> : <span aria-hidden="true">☰</span>}
315
322
  </button>
316
323
  </div>
317
324
  </div>
325
+ <nav className={`sh-menu${menuOpen ? " is-open" : ""}`} id="sh-menu" aria-label="站点导航(移动端)">
326
+ {nav.map((item) => (
327
+ <Link key={item.href} href={item.href} aria-current={current(item.href) ? "page" : undefined} onClick={() => setMenuOpen(false)}>
328
+ {item.label}
329
+ </Link>
330
+ ))}
331
+ </nav>
318
332
  </header>
319
333
 
320
334
  {searchOpen && (
@@ -11,7 +11,7 @@ import {
11
11
  } from "react";
12
12
  import { List, X } from "@phosphor-icons/react";
13
13
 
14
- type Heading = { depth: number; text: string; id: string };
14
+ type Heading = { depth: number; text: string; id: string; no?: string };
15
15
 
16
16
  type TocNode = Heading & { children: TocNode[] };
17
17
 
@@ -354,6 +354,7 @@ export function TableOfContents({
354
354
  onFocus={() => prefetchHeadingLayout(node.id)}
355
355
  aria-current={activeId === node.id ? "location" : undefined}
356
356
  >
357
+ {node.no ? <span className="t-no">{node.no}</span> : null}
357
358
  {node.text}
358
359
  </a>
359
360
  {node.children.length > 0 ? (
@@ -407,7 +408,7 @@ export function TableOfContents({
407
408
  onClick={() => closeMobileToc(true)}
408
409
  />
409
410
  <aside
410
- className="toc"
411
+ className="toc toc-rail"
411
412
  id={panelId}
412
413
  aria-label={label}
413
414
  data-mobile-open={mobileOpen}
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
5
+ import { useMemo } from "react";
6
+ import { postHref, type ContentEntry } from "../lib/content";
7
+
8
+ function day(value: string) {
9
+ const date = new Date(value);
10
+ if (Number.isNaN(date.getTime())) return value.slice(0, 10);
11
+ return new Intl.DateTimeFormat("en-CA", {
12
+ timeZone: "Asia/Shanghai",
13
+ year: "numeric",
14
+ month: "2-digit",
15
+ day: "2-digit",
16
+ }).format(date);
17
+ }
18
+
19
+ function joinTags(names: string[]) {
20
+ if (names.length <= 1) return names[0] ?? "";
21
+ return `${names.slice(0, -1).join("、")}和${names[names.length - 1]}`;
22
+ }
23
+
24
+ export function TagIndex({ posts }: { posts: ContentEntry[] }) {
25
+ const router = useRouter();
26
+ const pathname = usePathname();
27
+ const params = useSearchParams();
28
+ const tags = useMemo(() => {
29
+ const counts = new Map<string, number>();
30
+ for (const post of posts) {
31
+ for (const tag of post.tags ?? []) counts.set(tag, (counts.get(tag) ?? 0) + 1);
32
+ }
33
+ return [...counts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0], "zh"));
34
+ }, [posts]);
35
+ const requested = params.get("t") ?? "";
36
+ const active = tags.some(([tag]) => tag === requested) ? requested : "";
37
+ const common = tags.filter(([, count]) => count > 1);
38
+ const once = tags.filter(([, count]) => count === 1);
39
+ const visible = posts
40
+ .filter((post) => active && (post.tags ?? []).includes(active))
41
+ .slice()
42
+ .sort((left, right) => Date.parse(right.date) - Date.parse(left.date));
43
+ const choose = (tag: string) => {
44
+ const next = active === tag ? "" : tag;
45
+ router.replace(next ? `${pathname}?t=${encodeURIComponent(next)}` : pathname, { scroll: false });
46
+ };
47
+
48
+ return (
49
+ <div className="sh-listgrid" id="sh-tags">
50
+ <div>
51
+ <ul className="sh-tagcloud" aria-label="常用标签">
52
+ {common.map(([tag, count]) => (
53
+ <li key={tag}>
54
+ <button type="button" className="sh-chip" data-tag={tag} aria-pressed={active === tag} onClick={() => choose(tag)}>
55
+ {tag}<span className="n">{count}</span>
56
+ </button>
57
+ </li>
58
+ ))}
59
+ </ul>
60
+ {once.length ? (
61
+ <details className="sh-once" open={once.some(([tag]) => tag === active) || undefined}>
62
+ <summary>只出现一次的标签<span className="num">{once.length}</span><svg viewBox="0 0 14 14" aria-hidden="true"><path d="M3.5 5.5 7 9l3.5-3.5" /></svg></summary>
63
+ <ul className="sh-tagcloud">
64
+ {once.map(([tag]) => (
65
+ <li key={tag}>
66
+ <button type="button" className="sh-chip" data-tag={tag} aria-pressed={active === tag} onClick={() => choose(tag)}>
67
+ {tag}
68
+ </button>
69
+ </li>
70
+ ))}
71
+ </ul>
72
+ </details>
73
+ ) : null}
74
+ <section className="sh-tagresult" id="sh-tagresult" aria-live="polite">
75
+ <h2 id="sh-tag-h">{active ? `“${active}”的 ${visible.length} 篇文章` : "选择一个标签"}</h2>
76
+ <p className="hint" hidden={Boolean(active)}>
77
+ {common.length ? `还没有选择标签。常用的有 ${joinTags(common.slice(0, 4).map(([tag]) => tag))}。` : "还没有选择标签。"}
78
+ </p>
79
+ <ol className="sh-rows">
80
+ {visible.map((post) => (
81
+ <li className="sh-row" key={post.path}>
82
+ <time className="num" dateTime={day(post.date)}>{day(post.date)}</time>
83
+ <h3><Link href={postHref(post)}>{post.title}</Link></h3>
84
+ {post.description ? <p>{post.description}</p> : null}
85
+ {post.topic ? <span className="cat">{post.topic}</span> : null}
86
+ </li>
87
+ ))}
88
+ </ol>
89
+ </section>
90
+ </div>
91
+ <aside className="sh-rail" aria-label="说明">
92
+ <h2>标签与分类</h2>
93
+ <p>分类回答“这篇属于哪一块”,每篇一个;标签回答“提到了什么”,每篇可以有多个。</p>
94
+ </aside>
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import { readGardenPayload } from "../../lib/remark-garden.mjs";
4
+ import {
5
+ GardenCard,
6
+ GardenChart,
7
+ GardenCompare,
8
+ GardenFigure,
9
+ GardenHtml,
10
+ GardenIdea,
11
+ GardenInteractive,
12
+ GardenLegacy,
13
+ GardenStat,
14
+ GardenTimeline,
15
+ GardenUnknown,
16
+ } from "./blocks";
17
+
18
+ export function GardenBlock({ dataPayload }: { dataPayload?: string }) {
19
+ const block = readGardenPayload(dataPayload);
20
+ if (!block) return null;
21
+ if (block.legacy && block.suggestion) return <GardenLegacy suggestion={String(block.suggestion)} />;
22
+ if (!block.name) return <GardenIdea text={String(block.idea || block.children || "")} />;
23
+ if (block.error) return <GardenUnknown name={String(block.name)} />;
24
+ const props = (block.props ?? {}) as Record<string, unknown>;
25
+ switch (block.name) {
26
+ case "card":
27
+ return <GardenCard props={props} />;
28
+ case "stat":
29
+ return <GardenStat props={props} />;
30
+ case "compare":
31
+ return <GardenCompare props={props} />;
32
+ case "timeline":
33
+ return <GardenTimeline props={props} />;
34
+ case "chart":
35
+ return <GardenChart props={props} />;
36
+ case "figure":
37
+ return <GardenFigure props={props}>{String(block.children ?? "")}</GardenFigure>;
38
+ case "html":
39
+ return <GardenHtml props={props}>{String(block.children ?? "")}</GardenHtml>;
40
+ case "interactive":
41
+ return <GardenInteractive block={{ id: block.id, props, children: block.children }} />;
42
+ default:
43
+ return <GardenUnknown name={String(block.name)} />;
44
+ }
45
+ }