@driftime/sanity-plugin-handbook 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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +299 -0
  3. package/dist/index.cjs +1866 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +143 -0
  6. package/dist/index.d.ts +143 -0
  7. package/dist/index.js +1870 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +92 -0
  10. package/src/blocks/callout/block.tsx +56 -0
  11. package/src/blocks/callout/content.tsx +39 -0
  12. package/src/blocks/callout/preview.tsx +15 -0
  13. package/src/blocks/code/block.tsx +18 -0
  14. package/src/blocks/code/content.tsx +18 -0
  15. package/src/blocks/code/preview.tsx +17 -0
  16. package/src/blocks/horizontal-rule/block.tsx +5 -0
  17. package/src/blocks/horizontal-rule/content.tsx +8 -0
  18. package/src/blocks/horizontal-rule/preview.tsx +5 -0
  19. package/src/blocks/image/block.tsx +23 -0
  20. package/src/blocks/image/content.tsx +31 -0
  21. package/src/blocks/image/preview.tsx +23 -0
  22. package/src/blocks/video/block.tsx +21 -0
  23. package/src/blocks/video/content.tsx +37 -0
  24. package/src/blocks/video/preview.tsx +21 -0
  25. package/src/components/content-panel.tsx +58 -0
  26. package/src/components/decorators.tsx +53 -0
  27. package/src/components/document-types.tsx +49 -0
  28. package/src/components/field.tsx +528 -0
  29. package/src/components/guide-content.tsx +117 -0
  30. package/src/components/guides.tsx +37 -0
  31. package/src/components/pane-header.tsx +26 -0
  32. package/src/components/sidebar-group.tsx +30 -0
  33. package/src/components/sidebar-tab.tsx +78 -0
  34. package/src/contexts/handbook-context.tsx +84 -0
  35. package/src/getting-started/document-types.tsx +104 -0
  36. package/src/getting-started/how-to-use.tsx +93 -0
  37. package/src/getting-started/overview.tsx +64 -0
  38. package/src/hooks/use-container-width.ts +41 -0
  39. package/src/hooks/use-handbook-configuration.ts +74 -0
  40. package/src/hooks/use-is-handbook-editor.ts +18 -0
  41. package/src/index.ts +145 -0
  42. package/src/layouts/content.tsx +13 -0
  43. package/src/layouts/sidebar.tsx +15 -0
  44. package/src/layouts/tool.tsx +205 -0
  45. package/src/lib/handbook-structure.ts +46 -0
  46. package/src/lib/icons.ts +18 -0
  47. package/src/lib/utils.ts +69 -0
  48. package/src/schemas/annotations/link.ts +19 -0
  49. package/src/schemas/blocks/callout.ts +69 -0
  50. package/src/schemas/blocks/code.ts +46 -0
  51. package/src/schemas/blocks/horizontal-rule.ts +31 -0
  52. package/src/schemas/blocks/image.ts +40 -0
  53. package/src/schemas/blocks/video.ts +34 -0
  54. package/src/schemas/guide-content.ts +58 -0
  55. package/src/schemas/types/guide.ts +48 -0
  56. package/src/schemas/types/handbook.ts +85 -0
  57. package/src/types.ts +101 -0
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@driftime/sanity-plugin-handbook",
3
+ "version": "0.1.0",
4
+ "description": "Schema-driven documentation and editorial guides, built right into Sanity Studio.",
5
+ "keywords": [
6
+ "sanity",
7
+ "sanity-plugin"
8
+ ],
9
+ "homepage": "https://github.com/driftime/sanity-plugin-handbook",
10
+ "bugs": {
11
+ "url": "https://github.com/driftime/sanity-plugin-handbook/issues"
12
+ },
13
+ "license": "MIT",
14
+ "author": {
15
+ "name": "Kenny Heard",
16
+ "email": "kenny@driftime.com",
17
+ "url": "https://driftime.com"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+ssh://git@github.com/driftime/sanity-plugin-handbook.git"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.cjs",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "source": "./src/index.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.cjs",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "exports": {
43
+ ".": {
44
+ "import": "./dist/index.js",
45
+ "require": "./dist/index.cjs",
46
+ "default": "./dist/index.js"
47
+ },
48
+ "./package.json": "./package.json"
49
+ }
50
+ },
51
+ "scripts": {
52
+ "check": "oxfmt --check && oxlint",
53
+ "fix": "oxfmt && oxlint --fix",
54
+ "dev": "plugin-kit link-watch",
55
+ "watch": "pkg-utils watch --strict",
56
+ "build": "plugin-kit verify-package --silent && pkg-utils build --strict --clean --check"
57
+ },
58
+ "dependencies": {
59
+ "lucide-react": "1.7.0"
60
+ },
61
+ "devDependencies": {
62
+ "@sanity/browserslist-config": "1.0.5",
63
+ "@sanity/pkg-utils": "10.4.13",
64
+ "@sanity/plugin-kit": "4.0.20",
65
+ "@sanity/ui": "3.1.14",
66
+ "@types/node": "25.5.2",
67
+ "@types/react": "19.2.14",
68
+ "oxfmt": "0.44.0",
69
+ "oxlint": "1.59.0",
70
+ "oxlint-tsgolint": "0.20.0",
71
+ "react": "19.2.4",
72
+ "react-dom": "19.2.4",
73
+ "sanity": "5.20.0",
74
+ "typescript": "6.0.2"
75
+ },
76
+ "peerDependencies": {
77
+ "@sanity/ui": "2 || 3",
78
+ "react": "18 || 19",
79
+ "sanity": "3 || 4 || 5"
80
+ },
81
+ "browserslist": "extends @sanity/browserslist-config",
82
+ "engines": {
83
+ "node": ">=18"
84
+ },
85
+ "sanityPlugin": {
86
+ "verifyPackage": {
87
+ "sanityV2Json": false,
88
+ "eslintImports": false,
89
+ "scripts": false
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,56 @@
1
+ import { PortableText } from "@portabletext/react";
2
+ import type { PortableTextBlock } from "@portabletext/react";
3
+ import type { ReactNode } from "react";
4
+
5
+ import type { CalloutVariant } from "./content";
6
+ import { CalloutContent } from "./content";
7
+
8
+ interface HandbookCalloutValue {
9
+ /** Document type identifier. */
10
+ _type: "handbook.callout";
11
+ /** Visual style and intent of the callout. */
12
+ variant: CalloutVariant;
13
+ /** Portable Text content displayed inside the callout. */
14
+ body: PortableTextBlock[];
15
+ }
16
+
17
+ export function CalloutBlock({ value }: { value: HandbookCalloutValue }) {
18
+ return (
19
+ <div style={{ marginBlock: "2rem" }}>
20
+ <CalloutContent variant={value.variant}>
21
+ <PortableText
22
+ value={value.body}
23
+ components={{
24
+ block: { normal: ({ children }) => <>{children}</> },
25
+ marks: {
26
+ strong: ({ children }) => <strong>{children}</strong>,
27
+ em: ({ children }) => <em>{children}</em>,
28
+ code: ({ children }) => (
29
+ <code
30
+ style={{
31
+ padding: "0.125rem 0.25rem",
32
+ fontSize: "0.875em",
33
+ backgroundColor: "var(--card-code-bg-color)",
34
+ borderRadius: "0.125rem",
35
+ }}
36
+ >
37
+ {children}
38
+ </code>
39
+ ),
40
+ link: ({ children, value: mark }: { children: ReactNode; value?: { href?: string } }) => (
41
+ <a
42
+ href={mark?.href}
43
+ target="_blank"
44
+ rel="noopener noreferrer"
45
+ style={{ color: "var(--card-fg-color)" }}
46
+ >
47
+ {children}
48
+ </a>
49
+ ),
50
+ },
51
+ }}
52
+ />
53
+ </CalloutContent>
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,39 @@
1
+ import { Card, Flex, Text } from "@sanity/ui";
2
+ import { InfoIcon, LightbulbIcon, TriangleAlertIcon } from "lucide-react";
3
+ import type { ReactNode } from "react";
4
+
5
+ import { defaultIconProps } from "../../lib/icons";
6
+
7
+ export type CalloutVariant = "tip" | "info" | "warning";
8
+
9
+ interface CalloutContentProps {
10
+ /** Visual style variant determining the icon and tone. */
11
+ variant: CalloutVariant;
12
+ /** Content displayed inside the callout. */
13
+ children: ReactNode;
14
+ }
15
+
16
+ /** Icon and tone mapping for each callout variant. */
17
+ export const variantConfig = {
18
+ tip: { icon: LightbulbIcon, tone: "positive" },
19
+ info: { icon: InfoIcon, tone: "primary" },
20
+ warning: { icon: TriangleAlertIcon, tone: "caution" },
21
+ } as const;
22
+
23
+ export function CalloutContent({ variant, children }: CalloutContentProps) {
24
+ const config = variantConfig[variant];
25
+ const Icon = config.icon;
26
+
27
+ return (
28
+ <Card padding={4} radius={2} tone={config.tone}>
29
+ <Flex align="flex-start" gap={3}>
30
+ <div style={{ flexShrink: 0, marginTop: "-2px", fontSize: "16px", color: "var(--card-icon-color)" }}>
31
+ <Icon {...defaultIconProps} />
32
+ </div>
33
+ <Text size={1} style={{ color: "var(--card-icon-color)" }}>
34
+ {children}
35
+ </Text>
36
+ </Flex>
37
+ </Card>
38
+ );
39
+ }
@@ -0,0 +1,15 @@
1
+ import type { PreviewProps } from "sanity";
2
+
3
+ import type { CalloutVariant } from "./content";
4
+ import { CalloutContent } from "./content";
5
+
6
+ interface CalloutPreviewProps extends PreviewProps {
7
+ /** Visual style variant for the callout. */
8
+ variant?: CalloutVariant;
9
+ /** Plain text content extracted from the callout body. */
10
+ text?: string;
11
+ }
12
+
13
+ export function CalloutPreview({ variant, text }: CalloutPreviewProps) {
14
+ return <CalloutContent variant={variant ?? "tip"}>{text}</CalloutContent>;
15
+ }
@@ -0,0 +1,18 @@
1
+ import { CodeContent } from "./content";
2
+
3
+ interface HandbookCodeValue {
4
+ /** Document type identifier. */
5
+ _type: "handbook.code";
6
+ /** Source code to display. */
7
+ code: string;
8
+ /** Programming language for syntax highlighting. */
9
+ language?: string;
10
+ }
11
+
12
+ export function CodeBlock({ value }: { value: HandbookCodeValue }) {
13
+ return (
14
+ <div style={{ marginBlock: "1.5rem" }}>
15
+ <CodeContent code={value.code} language={value.language} />
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,18 @@
1
+ import { Card, Code } from "@sanity/ui";
2
+
3
+ interface CodeContentProps {
4
+ /** Source code to display. */
5
+ code: string;
6
+ /** Programming language for syntax highlighting. */
7
+ language?: string;
8
+ }
9
+
10
+ export function CodeContent({ code, language }: CodeContentProps) {
11
+ return (
12
+ <Card padding={4} radius={2} tone="transparent" style={{ overflowX: "auto" }}>
13
+ <Code size={1} language={language}>
14
+ {code}
15
+ </Code>
16
+ </Card>
17
+ );
18
+ }
@@ -0,0 +1,17 @@
1
+ import type { PreviewProps } from "sanity";
2
+
3
+ import { isDefined } from "../../lib/utils";
4
+ import { CodeContent } from "./content";
5
+
6
+ interface CodePreviewProps extends PreviewProps {
7
+ /** Source code to display. */
8
+ code?: string;
9
+ /** Programming language for syntax highlighting. */
10
+ language?: string;
11
+ }
12
+
13
+ export function CodePreview({ code, language }: CodePreviewProps) {
14
+ if (!isDefined(code)) return null;
15
+
16
+ return <CodeContent code={code} language={language} />;
17
+ }
@@ -0,0 +1,5 @@
1
+ import { HorizontalRuleContent } from "./content";
2
+
3
+ export function HorizontalRuleBlock() {
4
+ return <HorizontalRuleContent marginBlock="2.5rem" />;
5
+ }
@@ -0,0 +1,8 @@
1
+ interface HorizontalRuleContentProps {
2
+ /** Vertical spacing above and below the rule. */
3
+ marginBlock?: string;
4
+ }
5
+
6
+ export function HorizontalRuleContent({ marginBlock = "0" }: HorizontalRuleContentProps) {
7
+ return <hr style={{ marginBlock, border: "none", borderTop: "1px solid var(--card-border-color)" }} />;
8
+ }
@@ -0,0 +1,5 @@
1
+ import { HorizontalRuleContent } from "./content";
2
+
3
+ export function HorizontalRulePreview() {
4
+ return <HorizontalRuleContent marginBlock="0.5rem" />;
5
+ }
@@ -0,0 +1,23 @@
1
+ import { isDefined } from "../../lib/utils";
2
+ import { ImageContent } from "./content";
3
+
4
+ interface HandbookImageValue {
5
+ /** Document type identifier. */
6
+ _type: "handbook.image";
7
+ /** Resolved image asset containing the URL. */
8
+ asset?: { url?: string };
9
+ /** Text displayed beneath the image. */
10
+ caption?: string;
11
+ /** Alternative text for screen readers. */
12
+ alt?: string;
13
+ }
14
+
15
+ export function ImageBlock({ value }: { value: HandbookImageValue }) {
16
+ if (!isDefined(value.asset?.url)) return null;
17
+
18
+ return (
19
+ <div style={{ marginBlock: "1.5rem" }}>
20
+ <ImageContent url={value.asset.url} alt={value.alt} caption={value.caption} />
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,31 @@
1
+ import { Text } from "@sanity/ui";
2
+
3
+ import { isDefined } from "../../lib/utils";
4
+
5
+ interface ImageContentProps {
6
+ /** Image source URL. */
7
+ url: string;
8
+ /** Alternative text for screen readers. */
9
+ alt?: string;
10
+ /** Text displayed beneath the image. */
11
+ caption?: string;
12
+ }
13
+
14
+ export function ImageContent({ url, alt, caption }: ImageContentProps) {
15
+ return (
16
+ <>
17
+ <img
18
+ src={url}
19
+ alt={alt ?? ""}
20
+ style={{ display: "block", maxWidth: "100%", borderRadius: "0.25rem", overflow: "hidden" }}
21
+ />
22
+ {isDefined(caption) && (
23
+ <div style={{ marginBlockStart: "0.75rem" }}>
24
+ <Text size={1} muted style={{ fontStyle: "italic" }}>
25
+ {caption}
26
+ </Text>
27
+ </div>
28
+ )}
29
+ </>
30
+ );
31
+ }
@@ -0,0 +1,23 @@
1
+ import type { PreviewProps } from "sanity";
2
+
3
+ import { isDefined } from "../../lib/utils";
4
+ import { ImageContent } from "./content";
5
+
6
+ interface ImagePreviewProps extends PreviewProps {
7
+ /** Resolved image URL from the asset. */
8
+ url?: string;
9
+ /** Alternative text for screen readers. */
10
+ alt?: string;
11
+ /** Text displayed beneath the image. */
12
+ caption?: string;
13
+ }
14
+
15
+ export function ImagePreview({ url, alt, caption }: ImagePreviewProps) {
16
+ if (!isDefined(url)) return null;
17
+
18
+ return (
19
+ <div style={{ padding: "0.5rem" }}>
20
+ <ImageContent url={url} alt={alt} caption={caption} />
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,21 @@
1
+ import { isDefined } from "../../lib/utils";
2
+ import { VideoContent } from "./content";
3
+
4
+ interface HandbookVideoValue {
5
+ /** Document type identifier. */
6
+ _type: "handbook.video";
7
+ /** Resolved video asset containing the URL. */
8
+ asset?: { url?: string };
9
+ /** Text displayed beneath the video. */
10
+ caption?: string;
11
+ }
12
+
13
+ export function VideoBlock({ value }: { value: HandbookVideoValue }) {
14
+ if (!isDefined(value.asset?.url)) return null;
15
+
16
+ return (
17
+ <div style={{ marginBlock: "1.5rem" }}>
18
+ <VideoContent url={value.asset.url} caption={value.caption} />
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,37 @@
1
+ import { Text } from "@sanity/ui";
2
+
3
+ import { isDefined } from "../../lib/utils";
4
+
5
+ interface VideoContentProps {
6
+ /** Video source URL. */
7
+ url: string;
8
+ /** Text displayed beneath the video. */
9
+ caption?: string;
10
+ }
11
+
12
+ export function VideoContent({ url, caption }: VideoContentProps) {
13
+ return (
14
+ <>
15
+ <video
16
+ src={url}
17
+ controls
18
+ style={{
19
+ display: "block",
20
+ maxWidth: "100%",
21
+ borderRadius: "0.25rem",
22
+ overflow: "hidden",
23
+ WebkitTransform: "translateZ(0)",
24
+ }}
25
+ >
26
+ <track kind="captions" label={caption ?? "Video"} />
27
+ </video>
28
+ {isDefined(caption) && (
29
+ <div style={{ marginBlockStart: "0.75rem" }}>
30
+ <Text size={1} muted style={{ fontStyle: "italic" }}>
31
+ {caption}
32
+ </Text>
33
+ </div>
34
+ )}
35
+ </>
36
+ );
37
+ }
@@ -0,0 +1,21 @@
1
+ import type { PreviewProps } from "sanity";
2
+
3
+ import { isDefined } from "../../lib/utils";
4
+ import { VideoContent } from "./content";
5
+
6
+ interface VideoPreviewProps extends PreviewProps {
7
+ /** Resolved video URL from the asset. */
8
+ url?: string;
9
+ /** Text displayed beneath the video. */
10
+ caption?: string;
11
+ }
12
+
13
+ export function VideoPreview({ url, caption }: VideoPreviewProps) {
14
+ if (!isDefined(url)) return null;
15
+
16
+ return (
17
+ <div style={{ padding: "0.5rem" }}>
18
+ <VideoContent url={url} caption={caption} />
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,58 @@
1
+ import { Box, Heading, Stack, Text } from "@sanity/ui";
2
+ import type { ReactNode } from "react";
3
+
4
+ import { useHandbookContext } from "../contexts/handbook-context";
5
+ import { isDefined } from "../lib/utils";
6
+ import { PaneHeader } from "./pane-header";
7
+
8
+ interface ContentPanelProps {
9
+ /** Unique identifier linking this panel to its sidebar tab. */
10
+ id: string;
11
+ /** Heading displayed at the top of the panel. */
12
+ title: string;
13
+ /** Optional description shown beneath the heading. */
14
+ description?: string;
15
+ /** Panel body content. */
16
+ children: ReactNode;
17
+ }
18
+
19
+ export function ContentPanel({ id, title, description, children }: ContentPanelProps) {
20
+ const { activeTab } = useHandbookContext();
21
+
22
+ if (activeTab !== id) return null;
23
+
24
+ return (
25
+ <div
26
+ id={`panel-${id}`}
27
+ aria-labelledby={id}
28
+ role="tabpanel"
29
+ style={{ display: "flex", flexDirection: "column", height: "100%" }}
30
+ >
31
+ <PaneHeader title={title} />
32
+ <div style={{ flex: 1, overflowY: "auto" }}>
33
+ <Box
34
+ style={{
35
+ maxWidth: "640px",
36
+ boxSizing: "border-box",
37
+ marginInline: "auto",
38
+ paddingInline: "20px",
39
+ paddingBlockStart: "32px",
40
+ paddingBlockEnd: "220px",
41
+ }}
42
+ >
43
+ <Stack marginBottom={6} space={5}>
44
+ <Heading as="h2" size={4}>
45
+ {title}
46
+ </Heading>
47
+ {isDefined(description) && (
48
+ <Text size={1} muted>
49
+ {description}
50
+ </Text>
51
+ )}
52
+ </Stack>
53
+ {children}
54
+ </Box>
55
+ </div>
56
+ </div>
57
+ );
58
+ }
@@ -0,0 +1,53 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ interface DecoratorProps {
4
+ /** Text content wrapped by the decorator. */
5
+ children: ReactNode;
6
+ }
7
+
8
+ export function ParagraphDecorator({ children }: DecoratorProps) {
9
+ return <span style={{ fontSize: "0.8125rem" }}>{children}</span>;
10
+ }
11
+
12
+ export function HeadingDecorator({ children }: DecoratorProps) {
13
+ return <span style={{ fontSize: "0.8125rem", fontWeight: "500" }}>{children}</span>;
14
+ }
15
+
16
+ export function BlockquoteDecorator({ children }: DecoratorProps) {
17
+ return (
18
+ <div
19
+ style={{
20
+ paddingLeft: "0.75em",
21
+ fontSize: "0.8125rem",
22
+ fontStyle: "italic",
23
+ borderLeft: "2px solid var(--card-border-color)",
24
+ }}
25
+ >
26
+ {children}
27
+ </div>
28
+ );
29
+ }
30
+
31
+ export function BulletListDecorator({ children }: DecoratorProps) {
32
+ return <div style={{ marginTop: "-0.25em", fontSize: "0.8125rem" }}>{children}</div>;
33
+ }
34
+
35
+ export function NumberListDecorator({ children }: DecoratorProps) {
36
+ return <div style={{ marginTop: "-0.25em", fontSize: "0.8125rem" }}>{children}</div>;
37
+ }
38
+
39
+ export function InlineCodeDecorator({ children }: DecoratorProps) {
40
+ return (
41
+ <code
42
+ style={{
43
+ padding: "0.125rem 0.25rem",
44
+ fontSize: "0.8125rem",
45
+ color: "var(--card-muted-fg-color)",
46
+ backgroundColor: "var(--card-code-bg-color)",
47
+ borderRadius: "0.125rem",
48
+ }}
49
+ >
50
+ {children}
51
+ </code>
52
+ );
53
+ }
@@ -0,0 +1,49 @@
1
+ import { useHandbookContext } from "../contexts/handbook-context";
2
+ import { convertCase } from "../lib/utils";
3
+ import { ContentPanel } from "./content-panel";
4
+ import { Field } from "./field";
5
+ import { SidebarGroup } from "./sidebar-group";
6
+ import { SidebarTab } from "./sidebar-tab";
7
+
8
+ export function DocumentTypesTabs() {
9
+ const { groups } = useHandbookContext();
10
+
11
+ return groups.map((group) => {
12
+ const { title, documents } = group;
13
+
14
+ return (
15
+ <SidebarGroup key={title} heading={title}>
16
+ {documents.map((document) => {
17
+ const effectiveTitle = document.handbook?.title ?? document.title ?? convertCase(document.name, "title");
18
+ const icon = typeof document.icon === "function" ? document.icon : undefined;
19
+
20
+ return <SidebarTab key={document.name} id={document.name} label={effectiveTitle} icon={icon} />;
21
+ })}
22
+ </SidebarGroup>
23
+ );
24
+ });
25
+ }
26
+
27
+ export function DocumentTypesPanels() {
28
+ const { groups } = useHandbookContext();
29
+
30
+ return groups.map((group) => {
31
+ const { documents } = group;
32
+
33
+ return documents.map((document) => {
34
+ const effectiveTitle = document.handbook?.title ?? document.title ?? convertCase(document.name, "title");
35
+ const effectiveDescription =
36
+ document.handbook?.description ?? (typeof document.description === "string" ? document.description : undefined);
37
+
38
+ return (
39
+ <ContentPanel key={document.name} id={document.name} title={effectiveTitle} description={effectiveDescription}>
40
+ <div style={{ display: "flex", flexDirection: "column", gap: "3.25rem" }}>
41
+ {document.fields.map((field) => (
42
+ <Field key={field.name} field={field} />
43
+ ))}
44
+ </div>
45
+ </ContentPanel>
46
+ );
47
+ });
48
+ });
49
+ }