sanity-plugin-tnd-docs 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 The New Dynamic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # sanity-plugin-tnd-docs
2
+
3
+ > A Sanity Studio v3 plugin for displaying markdown documentation directly in your studio.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install sanity-plugin-tnd-docs
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Add the plugin to your `sanity.config.ts` (or .js):
14
+
15
+ ```typescript
16
+ import { defineConfig } from 'sanity'
17
+ import { tndDocs } from 'sanity-plugin-tnd-docs'
18
+
19
+ // Import your markdown files using Vite's import.meta.glob
20
+ const docs = import.meta.glob('/docs/**/*.md', { eager: true, query: '?raw' import: 'default'})
21
+
22
+ export default defineConfig({
23
+ // ...
24
+ plugins: [
25
+ tndDocs({
26
+ documents: docs
27
+ })
28
+ ]
29
+ })
30
+ ```
31
+
32
+ ## Configuration Options
33
+
34
+ | Option | Type | Required | Default | Description |
35
+ |--------|------|----------|---------|-------------|
36
+ | `documents` | `Record<string, string>` | Yes | - | Markdown files loaded via `import.meta.glob` |
37
+ | `name` | `string` | No | `'tnd-docs'` | Internal name for the tool. Will determine route in studio |
38
+ | `title` | `string` | No | `'Documentation'` | Display title in Sanity Studio |
39
+
40
+ ## Setting Up Your Documentation Files
41
+
42
+ ### 1. Create a docs folder in your project
43
+
44
+ ```
45
+ your-sanity-project/
46
+ ├── docs/
47
+ │ ├── getting-started.md
48
+ │ ├── endpoints.md
49
+ │ ├── authentication.md
50
+ │ └── guides.md
51
+ ```
52
+
53
+ ### 2. Import markdown files
54
+
55
+ The plugin uses Vite's `import.meta.glob` to load markdown files at build time. This must be done in your `sanity.config.ts`:
56
+
57
+ ```typescript
58
+ // Load all .md files from the /docs directory
59
+ const docs = import.meta.glob('/docs/**/*.md', {
60
+ eager: true,
61
+ query: '?raw',
62
+ import: 'default'
63
+ })
64
+ ```
65
+
66
+ ## Example: Complete Configuration
67
+
68
+ ```typescript
69
+ import { defineConfig } from 'sanity'
70
+ import { structureTool } from 'sanity/structure'
71
+ import { tndDocs } from 'sanity-plugin-tnd-docs'
72
+
73
+ // Load documentation files
74
+ const docs = import.meta.glob('/docs/**/*.md', { eager: true, as: 'raw' })
75
+
76
+ export default defineConfig({
77
+ name: 'default',
78
+ title: 'My Project',
79
+
80
+ projectId: 'your-project-id',
81
+ dataset: 'production',
82
+
83
+ plugins: [
84
+ structureTool(),
85
+ tndDocs({
86
+ title: 'Project Docs',
87
+ documents: docs
88
+ })
89
+ ],
90
+
91
+ schema: {
92
+ types: [/* your schemas */]
93
+ }
94
+ })
95
+ ```
96
+
97
+ ⚠️ **The glob pattern must be a literal string** - you cannot use variables:
98
+
99
+ ```typescript
100
+ // ✅ Correct
101
+ const docs = import.meta.glob('/docs/**/*.md', { eager: true, as: 'raw' })
102
+
103
+ // ❌ Incorrect - will not work
104
+ const path = '/docs/**/*.md'
105
+ const docs = import.meta.glob(path, { eager: true, as: 'raw' })
106
+ ```
107
+
108
+ ## Markdown Syntax
109
+
110
+ ### Useful frontmatter
111
+
112
+ Currently the only frontmatter needed is:
113
+
114
+ | Name | Type | Required | Description |
115
+ |--------|------|----------|-------------|
116
+ | `title` | `number` | No | Will be used to populate the title of the entry in the side navigation. If missing, the path will be used |
117
+ | `weight` | `string` | No | Will be used to sort the entries in the side navigation |
118
+ | `description` | `string` | No | Will be printed in small text in the navigation entry |
119
+
120
+ ## Images
121
+
122
+ Sanity will copy the static directory as is, so you should use it to store your md images.
123
+
124
+ ```md
125
+ ![This is the title attribute](/static/uploads/title-formating.png "This is a caption")
126
+ ```
127
+
128
+ ## License
129
+
130
+ [MIT](LICENSE) © The New Dynamic
131
+
132
+ ## Develop & test
133
+
134
+ This plugin uses [@sanity/plugin-kit](https://github.com/sanity-io/plugin-kit)
135
+ with default configuration for build & watch scripts.
136
+
137
+ See [Testing a plugin in Sanity Studio](https://github.com/sanity-io/plugin-kit#testing-a-plugin-in-sanity-studio)
138
+ on how to run this plugin with hotreload in the studio.
@@ -0,0 +1,16 @@
1
+ import { Plugin as Plugin_2 } from "sanity"
2
+
3
+ declare interface DocConfig {
4
+ name?: string
5
+ title?: string
6
+ documents: Record<string, string>
7
+ }
8
+
9
+ /**
10
+ * TND Documentation plugin for Sanity Studio
11
+ *
12
+ * @public
13
+ */
14
+ export declare const tndDocs: Plugin_2<DocConfig>
15
+
16
+ export {}
@@ -0,0 +1,16 @@
1
+ import { Plugin as Plugin_2 } from "sanity"
2
+
3
+ declare interface DocConfig {
4
+ name?: string
5
+ title?: string
6
+ documents: Record<string, string>
7
+ }
8
+
9
+ /**
10
+ * TND Documentation plugin for Sanity Studio
11
+ *
12
+ * @public
13
+ */
14
+ export declare const tndDocs: Plugin_2<DocConfig>
15
+
16
+ export {}
package/dist/index.js ADDED
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: !0 });
3
+ var icons = require("@sanity/icons"), marked = require("marked"), react = require("react"), sanity = require("sanity"), jsxRuntime = require("react/jsx-runtime"), ui = require("@sanity/ui");
4
+ function parseFrontmatter(markdown) {
5
+ const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/, match = markdown.match(frontmatterRegex);
6
+ if (!match)
7
+ return { frontmatter: {}, content: markdown };
8
+ const frontmatterText = match[1] ?? "", content = match[2] ?? "", frontmatter = {};
9
+ return frontmatterText.split(`
10
+ `).forEach((line) => {
11
+ const colonIndex = line.indexOf(":");
12
+ if (colonIndex > 0) {
13
+ const key = line.substring(0, colonIndex).trim(), value = line.substring(colonIndex + 1).trim();
14
+ frontmatter[key] = value;
15
+ }
16
+ }), { frontmatter, content };
17
+ }
18
+ function useDocRouter(documents) {
19
+ const processedFiles = react.useMemo(() => Object.entries(documents).map(([path, rawContent]) => {
20
+ const filename = path, { frontmatter, content } = parseFrontmatter(rawContent), html = marked.marked.parse(content);
21
+ return {
22
+ path,
23
+ filename,
24
+ weight: frontmatter.weight || 0,
25
+ description: frontmatter.description || void 0,
26
+ title: frontmatter.title || filename.replace(".md", ""),
27
+ content,
28
+ html,
29
+ frontmatter
30
+ };
31
+ }).sort((a, b) => a.weight - b.weight), [documents]), getInitialPath = () => window.location.hash.slice(1) || processedFiles[0]?.path || null, [currentPath, setCurrentPath] = react.useState(getInitialPath());
32
+ react.useEffect(() => {
33
+ const handleHashChange = () => {
34
+ const hash = window.location.hash.slice(1);
35
+ setCurrentPath(hash || processedFiles[0]?.path || null);
36
+ };
37
+ return window.addEventListener("hashchange", handleHashChange), () => window.removeEventListener("hashchange", handleHashChange);
38
+ }, []);
39
+ const navigate = (path) => {
40
+ window.location.hash = path, setCurrentPath(path);
41
+ }, currentFile = processedFiles.find((file) => file.path === currentPath) || null;
42
+ return {
43
+ currentPath,
44
+ currentFile,
45
+ // The full processed file object
46
+ navigate,
47
+ allFiles: processedFiles
48
+ // All processed files, sorted by weight
49
+ };
50
+ }
51
+ function Body({ file }) {
52
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
53
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 5, weight: "bold", style: { marginBottom: "1rem" }, children: file.title }),
54
+ /* @__PURE__ */ jsxRuntime.jsx(
55
+ "div",
56
+ {
57
+ className: "prose",
58
+ dangerouslySetInnerHTML: {
59
+ __html: file.html
60
+ },
61
+ style: {
62
+ maxWidth: "800px",
63
+ lineHeight: "1.6",
64
+ fontFamily: "system-ui, -apple-system, sans-serif"
65
+ }
66
+ }
67
+ )
68
+ ] });
69
+ }
70
+ function SideNav({ allFiles, currentFile, navigate }) {
71
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: allFiles.map((file) => /* @__PURE__ */ jsxRuntime.jsxs(
72
+ ui.Card,
73
+ {
74
+ padding: 3,
75
+ radius: 2,
76
+ shadow: 1,
77
+ style: {
78
+ cursor: "pointer",
79
+ backgroundColor: currentFile?.filename == file.filename ? "#FAFAF8" : "transparent"
80
+ },
81
+ onClick: () => navigate(file.path),
82
+ children: [
83
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "medium", children: file.title }),
84
+ file.description && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, style: { marginTop: ".4rem" }, muted: !0, children: file.description })
85
+ ]
86
+ },
87
+ file.filename
88
+ )) });
89
+ }
90
+ function Doc({ config }) {
91
+ const { currentFile, navigate, allFiles } = useDocRouter(config.documents);
92
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Box, { padding: 4, className: "here-you-go", children: [
93
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
94
+ .prose {
95
+ figure{
96
+ display: inline-block;
97
+ gap: 2;
98
+ margin: 0;
99
+ figcaption{
100
+ text-align: center;
101
+ }
102
+ }
103
+ img{
104
+ display:block;
105
+ max-width: 100%;
106
+ flex-grow: 0;
107
+ border: 1px solid hsl(228, 10%, 90%);
108
+ border-radius: 3px;
109
+ }
110
+ strong{
111
+ font-weight: 700;
112
+ }
113
+ }
114
+ .doc-grid {
115
+ display: grid;
116
+ grid-template-columns: 1fr;
117
+ gap: 20px;
118
+ }
119
+ @media (min-width: 768px) {
120
+ .doc-grid {
121
+ grid-template-columns: 250px 1fr;
122
+ }
123
+ }
124
+ ` }),
125
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 5, children: [
126
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 4, weight: "bold", children: config.title || "Documentation" }),
127
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "doc-grid", children: [
128
+ /* @__PURE__ */ jsxRuntime.jsx(SideNav, { navigate, currentFile, allFiles }),
129
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 4, radius: 2, shadow: 1, style: { backgroundColor: "#FAFAF8" }, children: currentFile ? /* @__PURE__ */ jsxRuntime.jsx(Body, { file: currentFile }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, children: "Select a file to view its contents" }) })
130
+ ] })
131
+ ] })
132
+ ] });
133
+ }
134
+ function Errors({ messages }) {
135
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { padding: 4, align: "center", justify: "center", style: { height: "100%", width: "100%" }, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "caution", padding: 4, border: !0, children: messages.map((m, index) => /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: m }, index)) }) });
136
+ }
137
+ const renderer = {
138
+ image(href, title, text) {
139
+ return `<div><figure>
140
+ <img src="${href}" alt="${text}" />
141
+ ${title ? `<figcaption>${title}</figcaption>` : ""}
142
+ </figure></div>`;
143
+ }
144
+ };
145
+ marked.marked.use({ renderer });
146
+ const tndDocs = sanity.definePlugin((config) => {
147
+ const errors = [];
148
+ if (!config)
149
+ errors.push("tnd-docs: Configuration is required");
150
+ else if (config) {
151
+ const missingFields = ["documents"].filter((field) => !config[field]);
152
+ missingFields.length > 0 && errors.push(`tnd-docs: Missing required configuration fields: ${missingFields.join(", ")}`);
153
+ }
154
+ return {
155
+ name: "tnd-docs",
156
+ tools: [
157
+ {
158
+ name: config && config.name || "tnd-docs",
159
+ title: "Documentation",
160
+ icon: icons.DocumentTextIcon,
161
+ component: () => errors.length ? react.createElement(Errors, { messages: errors }) : react.createElement(Doc, { config })
162
+ }
163
+ ]
164
+ };
165
+ });
166
+ exports.tndDocs = tndDocs;
167
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/util/index.ts","../src/hooks/useDocRouter.ts","../src/components/Body.tsx","../src/components/SideNav.tsx","../src/components/Doc.tsx","../src/components/Errors.tsx","../src/renderer.ts","../src/index.ts"],"sourcesContent":["export function parseFrontmatter(markdown: string): {\n frontmatter: Record<string, any>\n content: string\n} {\n const frontmatterRegex = /^---\\n([\\s\\S]*?)\\n---\\n([\\s\\S]*)$/\n const match = markdown.match(frontmatterRegex)\n\n if (!match) {\n return { frontmatter: {}, content: markdown }\n }\n\n const frontmatterText = match[1] ?? \"\"\n const content = match[2] ?? \"\"\n\n // Parse YAML-like frontmatter (simple key: value pairs)\n const frontmatter: Record<string, any> = {}\n frontmatterText.split(\"\\n\").forEach((line) => {\n const colonIndex = line.indexOf(\":\")\n if (colonIndex > 0) {\n const key = line.substring(0, colonIndex).trim()\n const value = line.substring(colonIndex + 1).trim()\n frontmatter[key] = value\n }\n })\n return { frontmatter, content }\n}\n","import { marked } from \"marked\"\nimport { useEffect, useMemo, useState } from \"react\"\n\nimport type { MarkdownFile } from \"../types\"\nimport { parseFrontmatter } from \"../util\" // Your existing parser\n\nexport function useDocRouter(documents: Record<string, string>) {\n // Process all markdown files once\n const processedFiles = useMemo(() => {\n return Object.entries(documents)\n .map(([path, rawContent]) => {\n const filename = path\n\n // Parse frontmatter and content\n const { frontmatter, content } = parseFrontmatter(rawContent)\n\n // Convert markdown to HTML\n const html = marked.parse(content) as string\n return {\n path,\n filename,\n weight: frontmatter[\"weight\"] || 0,\n description: frontmatter[\"description\"] || undefined,\n title: frontmatter[\"title\"] || filename.replace(\".md\", \"\"),\n content,\n html,\n frontmatter,\n } as MarkdownFile\n })\n .sort((a, b) => a.weight - b.weight) // Sort by weight\n }, [documents])\n\n // Get initial path from URL hash, or use first document\n const getInitialPath = () => {\n const hash = window.location.hash.slice(1)\n return hash || processedFiles[0]?.path || null\n }\n\n const [currentPath, setCurrentPath] = useState<string | null>(getInitialPath())\n\n // Listen for URL hash changes\n useEffect(() => {\n const handleHashChange = () => {\n const hash = window.location.hash.slice(1)\n setCurrentPath(hash || processedFiles[0]?.path || null)\n }\n\n window.addEventListener(\"hashchange\", handleHashChange)\n return () => window.removeEventListener(\"hashchange\", handleHashChange)\n }, []) // Empty array - only set up listener once\n\n // Function to navigate to a different document\n const navigate = (path: string) => {\n window.location.hash = path\n setCurrentPath(path)\n }\n\n // Get the current processed file\n const currentFile = processedFiles.find((file) => file.path === currentPath) || null\n\n return {\n currentPath,\n currentFile, // The full processed file object\n navigate,\n allFiles: processedFiles, // All processed files, sorted by weight\n }\n}\n","/* eslint-disable react/no-danger */\nimport { Text } from \"@sanity/ui\"\nimport type { JSX } from \"react\"\n\nimport type { MarkdownFile } from \"../types\"\n\nexport function Body({ file }: { file: MarkdownFile }): JSX.Element {\n return (\n <div>\n <Text size={5} weight=\"bold\" style={{ marginBottom: \"1rem\" }}>\n {file.title}\n </Text>\n <div\n className=\"prose\"\n dangerouslySetInnerHTML={{\n __html: file.html,\n }}\n style={{\n maxWidth: \"800px\",\n lineHeight: \"1.6\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n }}\n />\n </div>\n )\n}\n","import { Card, Stack, Text } from \"@sanity/ui\"\nimport { JSX } from \"react\"\n\nimport type { SideNavProps } from \"../types\"\n\nexport function SideNav({ allFiles, currentFile, navigate }: SideNavProps): JSX.Element {\n return (\n <Stack space={2}>\n {allFiles.map((file) => (\n <Card\n key={file.filename}\n padding={3}\n radius={2}\n shadow={1}\n style={{\n cursor: \"pointer\",\n backgroundColor: currentFile?.filename == file.filename ? \"#FAFAF8\" : \"transparent\",\n }}\n // eslint-disable-next-line react/jsx-no-bind\n onClick={() => navigate(file.path)}\n >\n <Text size={1} weight=\"medium\">\n {file.title}\n </Text>\n {file.description && (\n <Text size={0} style={{ marginTop: \".4rem\" }} muted>\n {file.description}\n </Text>\n )}\n </Card>\n ))}\n </Stack>\n )\n}\n","import { Box, Card, Stack, Text } from \"@sanity/ui\"\nimport type { JSX } from \"react\"\n\nimport { useDocRouter } from \"../hooks/useDocRouter\"\nimport type { DocProps } from \"../types\"\nimport { Body } from \"./Body\"\nimport { SideNav } from \"./SideNav\"\n\nexport function Doc({ config }: DocProps): JSX.Element {\n const { currentFile, navigate, allFiles } = useDocRouter(config.documents)\n\n const styles = `\n .prose {\n figure{\n display: inline-block;\n gap: 2;\n margin: 0;\n figcaption{\n text-align: center;\n }\n }\n img{\n display:block;\n max-width: 100%;\n flex-grow: 0;\n border: 1px solid hsl(228, 10%, 90%);\n border-radius: 3px;\n }\n strong{\n font-weight: 700;\n }\n }\n .doc-grid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 20px;\n }\n @media (min-width: 768px) {\n .doc-grid {\n grid-template-columns: 250px 1fr;\n }\n }\n`\n return (\n <Box padding={4} className=\"here-you-go\">\n <style>{styles}</style>\n <Stack space={5}>\n <Text size={4} weight=\"bold\">\n {config.title || \"Documentation\"}\n </Text>\n <div className=\"doc-grid\">\n <SideNav navigate={navigate} currentFile={currentFile} allFiles={allFiles} />\n <Card padding={4} radius={2} shadow={1} style={{ backgroundColor: \"#FAFAF8\" }}>\n {currentFile ? (\n <Body file={currentFile} />\n ) : (\n <Text muted>Select a file to view its contents</Text>\n )}\n </Card>\n </div>\n </Stack>\n </Box>\n )\n}\n","import { Card, Flex, Text } from \"@sanity/ui\"\nimport { JSX } from \"react\"\n\nexport function Errors({ messages }: { messages: string[] }): JSX.Element {\n return (\n <Flex padding={4} align=\"center\" justify=\"center\" style={{ height: \"100%\", width: \"100%\" }}>\n <Card tone=\"caution\" padding={4} border>\n {messages.map((m, index) => (\n <Text key={index}>{m}</Text>\n ))}\n </Card>\n </Flex>\n )\n}\n","import { type Renderer } from \"marked\"\n\nconst renderer: Partial<Renderer> = {\n image(href: string, title: string | null, text: string): string {\n return `<div><figure>\n <img src=\"${href}\" alt=\"${text}\" />\n ${title ? `<figcaption>${title}</figcaption>` : \"\"}\n </figure></div>`\n },\n}\n\nexport default renderer\n","/** @public */\nimport { DocumentTextIcon } from \"@sanity/icons\"\nimport { marked } from \"marked\"\nimport { createElement } from \"react\"\nimport { definePlugin } from \"sanity\"\n\nimport { Doc } from \"./components/Doc\"\nimport { Errors } from \"./components/Errors\"\nimport renderer from \"./renderer\"\nimport type { DocConfig } from \"./types\"\n//import './style.css'\nmarked.use({ renderer })\n\n/**\n * TND Documentation plugin for Sanity Studio\n *\n * @public\n */\nexport const tndDocs = definePlugin<DocConfig>((config) => {\n const errors: string[] = []\n\n if (!config) {\n errors.push(\"tnd-docs: Configuration is required\")\n } else if (config) {\n // Add specific required field checks\n // Example: if your config requires certain fields\n const requiredFields = [\"documents\"] // Replace with your actual required fields\n const missingFields = requiredFields.filter((field) => !config[field as keyof DocConfig])\n if (missingFields.length > 0) {\n errors.push(`tnd-docs: Missing required configuration fields: ${missingFields.join(\", \")}`)\n }\n }\n\n return {\n name: \"tnd-docs\",\n tools: [\n {\n name: (config && config.name) || \"tnd-docs\",\n title: \"Documentation\",\n icon: DocumentTextIcon,\n component: () =>\n errors.length\n ? createElement(Errors, { messages: errors })\n : createElement(Doc, { config }),\n },\n ],\n }\n})\n"],"names":["useMemo","marked","useState","useEffect","jsx","Text","Stack","jsxs","Card","Box","Flex","definePlugin","DocumentTextIcon","createElement"],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,172 @@
1
+ import { DocumentTextIcon } from "@sanity/icons";
2
+ import { marked } from "marked";
3
+ import { useMemo, useState, useEffect, createElement } from "react";
4
+ import { definePlugin } from "sanity";
5
+ import { jsxs, jsx } from "react/jsx-runtime";
6
+ import { Text, Stack, Card, Box, Flex } from "@sanity/ui";
7
+ function parseFrontmatter(markdown) {
8
+ const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/, match = markdown.match(frontmatterRegex);
9
+ if (!match)
10
+ return { frontmatter: {}, content: markdown };
11
+ const frontmatterText = match[1] ?? "", content = match[2] ?? "", frontmatter = {};
12
+ return frontmatterText.split(`
13
+ `).forEach((line) => {
14
+ const colonIndex = line.indexOf(":");
15
+ if (colonIndex > 0) {
16
+ const key = line.substring(0, colonIndex).trim(), value = line.substring(colonIndex + 1).trim();
17
+ frontmatter[key] = value;
18
+ }
19
+ }), { frontmatter, content };
20
+ }
21
+ function useDocRouter(documents) {
22
+ const processedFiles = useMemo(() => Object.entries(documents).map(([path, rawContent]) => {
23
+ const filename = path, { frontmatter, content } = parseFrontmatter(rawContent), html = marked.parse(content);
24
+ return {
25
+ path,
26
+ filename,
27
+ weight: frontmatter.weight || 0,
28
+ description: frontmatter.description || void 0,
29
+ title: frontmatter.title || filename.replace(".md", ""),
30
+ content,
31
+ html,
32
+ frontmatter
33
+ };
34
+ }).sort((a, b) => a.weight - b.weight), [documents]), getInitialPath = () => window.location.hash.slice(1) || processedFiles[0]?.path || null, [currentPath, setCurrentPath] = useState(getInitialPath());
35
+ useEffect(() => {
36
+ const handleHashChange = () => {
37
+ const hash = window.location.hash.slice(1);
38
+ setCurrentPath(hash || processedFiles[0]?.path || null);
39
+ };
40
+ return window.addEventListener("hashchange", handleHashChange), () => window.removeEventListener("hashchange", handleHashChange);
41
+ }, []);
42
+ const navigate = (path) => {
43
+ window.location.hash = path, setCurrentPath(path);
44
+ }, currentFile = processedFiles.find((file) => file.path === currentPath) || null;
45
+ return {
46
+ currentPath,
47
+ currentFile,
48
+ // The full processed file object
49
+ navigate,
50
+ allFiles: processedFiles
51
+ // All processed files, sorted by weight
52
+ };
53
+ }
54
+ function Body({ file }) {
55
+ return /* @__PURE__ */ jsxs("div", { children: [
56
+ /* @__PURE__ */ jsx(Text, { size: 5, weight: "bold", style: { marginBottom: "1rem" }, children: file.title }),
57
+ /* @__PURE__ */ jsx(
58
+ "div",
59
+ {
60
+ className: "prose",
61
+ dangerouslySetInnerHTML: {
62
+ __html: file.html
63
+ },
64
+ style: {
65
+ maxWidth: "800px",
66
+ lineHeight: "1.6",
67
+ fontFamily: "system-ui, -apple-system, sans-serif"
68
+ }
69
+ }
70
+ )
71
+ ] });
72
+ }
73
+ function SideNav({ allFiles, currentFile, navigate }) {
74
+ return /* @__PURE__ */ jsx(Stack, { space: 2, children: allFiles.map((file) => /* @__PURE__ */ jsxs(
75
+ Card,
76
+ {
77
+ padding: 3,
78
+ radius: 2,
79
+ shadow: 1,
80
+ style: {
81
+ cursor: "pointer",
82
+ backgroundColor: currentFile?.filename == file.filename ? "#FAFAF8" : "transparent"
83
+ },
84
+ onClick: () => navigate(file.path),
85
+ children: [
86
+ /* @__PURE__ */ jsx(Text, { size: 1, weight: "medium", children: file.title }),
87
+ file.description && /* @__PURE__ */ jsx(Text, { size: 0, style: { marginTop: ".4rem" }, muted: !0, children: file.description })
88
+ ]
89
+ },
90
+ file.filename
91
+ )) });
92
+ }
93
+ function Doc({ config }) {
94
+ const { currentFile, navigate, allFiles } = useDocRouter(config.documents);
95
+ return /* @__PURE__ */ jsxs(Box, { padding: 4, className: "here-you-go", children: [
96
+ /* @__PURE__ */ jsx("style", { children: `
97
+ .prose {
98
+ figure{
99
+ display: inline-block;
100
+ gap: 2;
101
+ margin: 0;
102
+ figcaption{
103
+ text-align: center;
104
+ }
105
+ }
106
+ img{
107
+ display:block;
108
+ max-width: 100%;
109
+ flex-grow: 0;
110
+ border: 1px solid hsl(228, 10%, 90%);
111
+ border-radius: 3px;
112
+ }
113
+ strong{
114
+ font-weight: 700;
115
+ }
116
+ }
117
+ .doc-grid {
118
+ display: grid;
119
+ grid-template-columns: 1fr;
120
+ gap: 20px;
121
+ }
122
+ @media (min-width: 768px) {
123
+ .doc-grid {
124
+ grid-template-columns: 250px 1fr;
125
+ }
126
+ }
127
+ ` }),
128
+ /* @__PURE__ */ jsxs(Stack, { space: 5, children: [
129
+ /* @__PURE__ */ jsx(Text, { size: 4, weight: "bold", children: config.title || "Documentation" }),
130
+ /* @__PURE__ */ jsxs("div", { className: "doc-grid", children: [
131
+ /* @__PURE__ */ jsx(SideNav, { navigate, currentFile, allFiles }),
132
+ /* @__PURE__ */ jsx(Card, { padding: 4, radius: 2, shadow: 1, style: { backgroundColor: "#FAFAF8" }, children: currentFile ? /* @__PURE__ */ jsx(Body, { file: currentFile }) : /* @__PURE__ */ jsx(Text, { muted: !0, children: "Select a file to view its contents" }) })
133
+ ] })
134
+ ] })
135
+ ] });
136
+ }
137
+ function Errors({ messages }) {
138
+ return /* @__PURE__ */ jsx(Flex, { padding: 4, align: "center", justify: "center", style: { height: "100%", width: "100%" }, children: /* @__PURE__ */ jsx(Card, { tone: "caution", padding: 4, border: !0, children: messages.map((m, index) => /* @__PURE__ */ jsx(Text, { children: m }, index)) }) });
139
+ }
140
+ const renderer = {
141
+ image(href, title, text) {
142
+ return `<div><figure>
143
+ <img src="${href}" alt="${text}" />
144
+ ${title ? `<figcaption>${title}</figcaption>` : ""}
145
+ </figure></div>`;
146
+ }
147
+ };
148
+ marked.use({ renderer });
149
+ const tndDocs = definePlugin((config) => {
150
+ const errors = [];
151
+ if (!config)
152
+ errors.push("tnd-docs: Configuration is required");
153
+ else if (config) {
154
+ const missingFields = ["documents"].filter((field) => !config[field]);
155
+ missingFields.length > 0 && errors.push(`tnd-docs: Missing required configuration fields: ${missingFields.join(", ")}`);
156
+ }
157
+ return {
158
+ name: "tnd-docs",
159
+ tools: [
160
+ {
161
+ name: config && config.name || "tnd-docs",
162
+ title: "Documentation",
163
+ icon: DocumentTextIcon,
164
+ component: () => errors.length ? createElement(Errors, { messages: errors }) : createElement(Doc, { config })
165
+ }
166
+ ]
167
+ };
168
+ });
169
+ export {
170
+ tndDocs
171
+ };
172
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../src/util/index.ts","../src/hooks/useDocRouter.ts","../src/components/Body.tsx","../src/components/SideNav.tsx","../src/components/Doc.tsx","../src/components/Errors.tsx","../src/renderer.ts","../src/index.ts"],"sourcesContent":["export function parseFrontmatter(markdown: string): {\n frontmatter: Record<string, any>\n content: string\n} {\n const frontmatterRegex = /^---\\n([\\s\\S]*?)\\n---\\n([\\s\\S]*)$/\n const match = markdown.match(frontmatterRegex)\n\n if (!match) {\n return { frontmatter: {}, content: markdown }\n }\n\n const frontmatterText = match[1] ?? \"\"\n const content = match[2] ?? \"\"\n\n // Parse YAML-like frontmatter (simple key: value pairs)\n const frontmatter: Record<string, any> = {}\n frontmatterText.split(\"\\n\").forEach((line) => {\n const colonIndex = line.indexOf(\":\")\n if (colonIndex > 0) {\n const key = line.substring(0, colonIndex).trim()\n const value = line.substring(colonIndex + 1).trim()\n frontmatter[key] = value\n }\n })\n return { frontmatter, content }\n}\n","import { marked } from \"marked\"\nimport { useEffect, useMemo, useState } from \"react\"\n\nimport type { MarkdownFile } from \"../types\"\nimport { parseFrontmatter } from \"../util\" // Your existing parser\n\nexport function useDocRouter(documents: Record<string, string>) {\n // Process all markdown files once\n const processedFiles = useMemo(() => {\n return Object.entries(documents)\n .map(([path, rawContent]) => {\n const filename = path\n\n // Parse frontmatter and content\n const { frontmatter, content } = parseFrontmatter(rawContent)\n\n // Convert markdown to HTML\n const html = marked.parse(content) as string\n return {\n path,\n filename,\n weight: frontmatter[\"weight\"] || 0,\n description: frontmatter[\"description\"] || undefined,\n title: frontmatter[\"title\"] || filename.replace(\".md\", \"\"),\n content,\n html,\n frontmatter,\n } as MarkdownFile\n })\n .sort((a, b) => a.weight - b.weight) // Sort by weight\n }, [documents])\n\n // Get initial path from URL hash, or use first document\n const getInitialPath = () => {\n const hash = window.location.hash.slice(1)\n return hash || processedFiles[0]?.path || null\n }\n\n const [currentPath, setCurrentPath] = useState<string | null>(getInitialPath())\n\n // Listen for URL hash changes\n useEffect(() => {\n const handleHashChange = () => {\n const hash = window.location.hash.slice(1)\n setCurrentPath(hash || processedFiles[0]?.path || null)\n }\n\n window.addEventListener(\"hashchange\", handleHashChange)\n return () => window.removeEventListener(\"hashchange\", handleHashChange)\n }, []) // Empty array - only set up listener once\n\n // Function to navigate to a different document\n const navigate = (path: string) => {\n window.location.hash = path\n setCurrentPath(path)\n }\n\n // Get the current processed file\n const currentFile = processedFiles.find((file) => file.path === currentPath) || null\n\n return {\n currentPath,\n currentFile, // The full processed file object\n navigate,\n allFiles: processedFiles, // All processed files, sorted by weight\n }\n}\n","/* eslint-disable react/no-danger */\nimport { Text } from \"@sanity/ui\"\nimport type { JSX } from \"react\"\n\nimport type { MarkdownFile } from \"../types\"\n\nexport function Body({ file }: { file: MarkdownFile }): JSX.Element {\n return (\n <div>\n <Text size={5} weight=\"bold\" style={{ marginBottom: \"1rem\" }}>\n {file.title}\n </Text>\n <div\n className=\"prose\"\n dangerouslySetInnerHTML={{\n __html: file.html,\n }}\n style={{\n maxWidth: \"800px\",\n lineHeight: \"1.6\",\n fontFamily: \"system-ui, -apple-system, sans-serif\",\n }}\n />\n </div>\n )\n}\n","import { Card, Stack, Text } from \"@sanity/ui\"\nimport { JSX } from \"react\"\n\nimport type { SideNavProps } from \"../types\"\n\nexport function SideNav({ allFiles, currentFile, navigate }: SideNavProps): JSX.Element {\n return (\n <Stack space={2}>\n {allFiles.map((file) => (\n <Card\n key={file.filename}\n padding={3}\n radius={2}\n shadow={1}\n style={{\n cursor: \"pointer\",\n backgroundColor: currentFile?.filename == file.filename ? \"#FAFAF8\" : \"transparent\",\n }}\n // eslint-disable-next-line react/jsx-no-bind\n onClick={() => navigate(file.path)}\n >\n <Text size={1} weight=\"medium\">\n {file.title}\n </Text>\n {file.description && (\n <Text size={0} style={{ marginTop: \".4rem\" }} muted>\n {file.description}\n </Text>\n )}\n </Card>\n ))}\n </Stack>\n )\n}\n","import { Box, Card, Stack, Text } from \"@sanity/ui\"\nimport type { JSX } from \"react\"\n\nimport { useDocRouter } from \"../hooks/useDocRouter\"\nimport type { DocProps } from \"../types\"\nimport { Body } from \"./Body\"\nimport { SideNav } from \"./SideNav\"\n\nexport function Doc({ config }: DocProps): JSX.Element {\n const { currentFile, navigate, allFiles } = useDocRouter(config.documents)\n\n const styles = `\n .prose {\n figure{\n display: inline-block;\n gap: 2;\n margin: 0;\n figcaption{\n text-align: center;\n }\n }\n img{\n display:block;\n max-width: 100%;\n flex-grow: 0;\n border: 1px solid hsl(228, 10%, 90%);\n border-radius: 3px;\n }\n strong{\n font-weight: 700;\n }\n }\n .doc-grid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 20px;\n }\n @media (min-width: 768px) {\n .doc-grid {\n grid-template-columns: 250px 1fr;\n }\n }\n`\n return (\n <Box padding={4} className=\"here-you-go\">\n <style>{styles}</style>\n <Stack space={5}>\n <Text size={4} weight=\"bold\">\n {config.title || \"Documentation\"}\n </Text>\n <div className=\"doc-grid\">\n <SideNav navigate={navigate} currentFile={currentFile} allFiles={allFiles} />\n <Card padding={4} radius={2} shadow={1} style={{ backgroundColor: \"#FAFAF8\" }}>\n {currentFile ? (\n <Body file={currentFile} />\n ) : (\n <Text muted>Select a file to view its contents</Text>\n )}\n </Card>\n </div>\n </Stack>\n </Box>\n )\n}\n","import { Card, Flex, Text } from \"@sanity/ui\"\nimport { JSX } from \"react\"\n\nexport function Errors({ messages }: { messages: string[] }): JSX.Element {\n return (\n <Flex padding={4} align=\"center\" justify=\"center\" style={{ height: \"100%\", width: \"100%\" }}>\n <Card tone=\"caution\" padding={4} border>\n {messages.map((m, index) => (\n <Text key={index}>{m}</Text>\n ))}\n </Card>\n </Flex>\n )\n}\n","import { type Renderer } from \"marked\"\n\nconst renderer: Partial<Renderer> = {\n image(href: string, title: string | null, text: string): string {\n return `<div><figure>\n <img src=\"${href}\" alt=\"${text}\" />\n ${title ? `<figcaption>${title}</figcaption>` : \"\"}\n </figure></div>`\n },\n}\n\nexport default renderer\n","/** @public */\nimport { DocumentTextIcon } from \"@sanity/icons\"\nimport { marked } from \"marked\"\nimport { createElement } from \"react\"\nimport { definePlugin } from \"sanity\"\n\nimport { Doc } from \"./components/Doc\"\nimport { Errors } from \"./components/Errors\"\nimport renderer from \"./renderer\"\nimport type { DocConfig } from \"./types\"\n//import './style.css'\nmarked.use({ renderer })\n\n/**\n * TND Documentation plugin for Sanity Studio\n *\n * @public\n */\nexport const tndDocs = definePlugin<DocConfig>((config) => {\n const errors: string[] = []\n\n if (!config) {\n errors.push(\"tnd-docs: Configuration is required\")\n } else if (config) {\n // Add specific required field checks\n // Example: if your config requires certain fields\n const requiredFields = [\"documents\"] // Replace with your actual required fields\n const missingFields = requiredFields.filter((field) => !config[field as keyof DocConfig])\n if (missingFields.length > 0) {\n errors.push(`tnd-docs: Missing required configuration fields: ${missingFields.join(\", \")}`)\n }\n }\n\n return {\n name: \"tnd-docs\",\n tools: [\n {\n name: (config && config.name) || \"tnd-docs\",\n title: \"Documentation\",\n icon: DocumentTextIcon,\n component: () =>\n errors.length\n ? createElement(Errors, { messages: errors })\n : createElement(Doc, { config }),\n },\n ],\n }\n})\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "sanity-plugin-tnd-docs",
3
+ "version": "1.0.0",
4
+ "description": "Adding documentation to your Sanity Studio using markdown files",
5
+ "keywords": [
6
+ "sanity",
7
+ "sanity-plugin",
8
+ "markdown",
9
+ "documentation"
10
+ ],
11
+ "license": "MIT",
12
+ "author": "Regis Philibert <login@regisphilibert.com>",
13
+ "sideEffects": false,
14
+ "type": "commonjs",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/theNewDynamic/sanity-plugin-tnd-docs"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "source": "./src/index.ts",
22
+ "import": "./dist/index.mjs",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "main": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "files": [
30
+ "dist",
31
+ "sanity.json",
32
+ "src",
33
+ "v2-incompatible.js"
34
+ ],
35
+ "scripts": {
36
+ "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
37
+ "format": "prettier --write --cache --ignore-unknown .",
38
+ "link-watch": "plugin-kit link-watch",
39
+ "lint": "eslint .",
40
+ "prepublishOnly": "npm run build",
41
+ "watch": "pkg-utils watch --strict"
42
+ },
43
+ "dependencies": {
44
+ "@sanity/incompatible-plugin": "^1.0.5",
45
+ "@sanity/icons": "^3.7.4",
46
+ "@sanity/ui": "^3.1.6",
47
+ "marked": "^11.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "@sanity/pkg-utils": "^10.2.1",
51
+ "@sanity/plugin-kit": "^4.0.20",
52
+ "@types/react": "^19.2.7",
53
+ "@typescript-eslint/eslint-plugin": "^8.50.0",
54
+ "@typescript-eslint/parser": "^8.50.0",
55
+ "eslint": "^8.57.1",
56
+ "eslint-config-prettier": "^10.1.8",
57
+ "eslint-config-sanity": "^7.1.4",
58
+ "eslint-plugin-prettier": "^5.5.4",
59
+ "eslint-plugin-react": "^7.37.5",
60
+ "eslint-plugin-react-hooks": "^7.0.1",
61
+ "prettier": "^3.7.4",
62
+ "prettier-plugin-packagejson": "^2.5.20",
63
+ "react": "^19.2.3",
64
+ "react-dom": "^19.2.3",
65
+ "sanity": "^4.21.1",
66
+ "styled-components": "^6.1.19",
67
+ "typescript": "^5.9.3"
68
+ },
69
+ "peerDependencies": {
70
+ "react": "^18",
71
+ "sanity": "^3"
72
+ },
73
+ "engines": {
74
+ "node": ">=18"
75
+ }
76
+ }
package/sanity.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "parts": [
3
+ {
4
+ "implements": "part:@sanity/base/sanity-root",
5
+ "path": "./v2-incompatible.js"
6
+ }
7
+ ]
8
+ }
@@ -0,0 +1,26 @@
1
+ /* eslint-disable react/no-danger */
2
+ import { Text } from "@sanity/ui"
3
+ import type { JSX } from "react"
4
+
5
+ import type { MarkdownFile } from "../types"
6
+
7
+ export function Body({ file }: { file: MarkdownFile }): JSX.Element {
8
+ return (
9
+ <div>
10
+ <Text size={5} weight="bold" style={{ marginBottom: "1rem" }}>
11
+ {file.title}
12
+ </Text>
13
+ <div
14
+ className="prose"
15
+ dangerouslySetInnerHTML={{
16
+ __html: file.html,
17
+ }}
18
+ style={{
19
+ maxWidth: "800px",
20
+ lineHeight: "1.6",
21
+ fontFamily: "system-ui, -apple-system, sans-serif",
22
+ }}
23
+ />
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,64 @@
1
+ import { Box, Card, Stack, Text } from "@sanity/ui"
2
+ import type { JSX } from "react"
3
+
4
+ import { useDocRouter } from "../hooks/useDocRouter"
5
+ import type { DocProps } from "../types"
6
+ import { Body } from "./Body"
7
+ import { SideNav } from "./SideNav"
8
+
9
+ export function Doc({ config }: DocProps): JSX.Element {
10
+ const { currentFile, navigate, allFiles } = useDocRouter(config.documents)
11
+
12
+ const styles = `
13
+ .prose {
14
+ figure{
15
+ display: inline-block;
16
+ gap: 2;
17
+ margin: 0;
18
+ figcaption{
19
+ text-align: center;
20
+ }
21
+ }
22
+ img{
23
+ display:block;
24
+ max-width: 100%;
25
+ flex-grow: 0;
26
+ border: 1px solid hsl(228, 10%, 90%);
27
+ border-radius: 3px;
28
+ }
29
+ strong{
30
+ font-weight: 700;
31
+ }
32
+ }
33
+ .doc-grid {
34
+ display: grid;
35
+ grid-template-columns: 1fr;
36
+ gap: 20px;
37
+ }
38
+ @media (min-width: 768px) {
39
+ .doc-grid {
40
+ grid-template-columns: 250px 1fr;
41
+ }
42
+ }
43
+ `
44
+ return (
45
+ <Box padding={4} className="here-you-go">
46
+ <style>{styles}</style>
47
+ <Stack space={5}>
48
+ <Text size={4} weight="bold">
49
+ {config.title || "Documentation"}
50
+ </Text>
51
+ <div className="doc-grid">
52
+ <SideNav navigate={navigate} currentFile={currentFile} allFiles={allFiles} />
53
+ <Card padding={4} radius={2} shadow={1} style={{ backgroundColor: "#FAFAF8" }}>
54
+ {currentFile ? (
55
+ <Body file={currentFile} />
56
+ ) : (
57
+ <Text muted>Select a file to view its contents</Text>
58
+ )}
59
+ </Card>
60
+ </div>
61
+ </Stack>
62
+ </Box>
63
+ )
64
+ }
@@ -0,0 +1,14 @@
1
+ import { Card, Flex, Text } from "@sanity/ui"
2
+ import { JSX } from "react"
3
+
4
+ export function Errors({ messages }: { messages: string[] }): JSX.Element {
5
+ return (
6
+ <Flex padding={4} align="center" justify="center" style={{ height: "100%", width: "100%" }}>
7
+ <Card tone="caution" padding={4} border>
8
+ {messages.map((m, index) => (
9
+ <Text key={index}>{m}</Text>
10
+ ))}
11
+ </Card>
12
+ </Flex>
13
+ )
14
+ }
@@ -0,0 +1,34 @@
1
+ import { Card, Stack, Text } from "@sanity/ui"
2
+ import { JSX } from "react"
3
+
4
+ import type { SideNavProps } from "../types"
5
+
6
+ export function SideNav({ allFiles, currentFile, navigate }: SideNavProps): JSX.Element {
7
+ return (
8
+ <Stack space={2}>
9
+ {allFiles.map((file) => (
10
+ <Card
11
+ key={file.filename}
12
+ padding={3}
13
+ radius={2}
14
+ shadow={1}
15
+ style={{
16
+ cursor: "pointer",
17
+ backgroundColor: currentFile?.filename == file.filename ? "#FAFAF8" : "transparent",
18
+ }}
19
+ // eslint-disable-next-line react/jsx-no-bind
20
+ onClick={() => navigate(file.path)}
21
+ >
22
+ <Text size={1} weight="medium">
23
+ {file.title}
24
+ </Text>
25
+ {file.description && (
26
+ <Text size={0} style={{ marginTop: ".4rem" }} muted>
27
+ {file.description}
28
+ </Text>
29
+ )}
30
+ </Card>
31
+ ))}
32
+ </Stack>
33
+ )
34
+ }
@@ -0,0 +1,3 @@
1
+ export { Doc } from "./Doc"
2
+ export { SideNav } from "./SideNav"
3
+ export { Body } from "./Body"
@@ -0,0 +1,67 @@
1
+ import { marked } from "marked"
2
+ import { useEffect, useMemo, useState } from "react"
3
+
4
+ import type { MarkdownFile } from "../types"
5
+ import { parseFrontmatter } from "../util" // Your existing parser
6
+
7
+ export function useDocRouter(documents: Record<string, string>) {
8
+ // Process all markdown files once
9
+ const processedFiles = useMemo(() => {
10
+ return Object.entries(documents)
11
+ .map(([path, rawContent]) => {
12
+ const filename = path
13
+
14
+ // Parse frontmatter and content
15
+ const { frontmatter, content } = parseFrontmatter(rawContent)
16
+
17
+ // Convert markdown to HTML
18
+ const html = marked.parse(content) as string
19
+ return {
20
+ path,
21
+ filename,
22
+ weight: frontmatter["weight"] || 0,
23
+ description: frontmatter["description"] || undefined,
24
+ title: frontmatter["title"] || filename.replace(".md", ""),
25
+ content,
26
+ html,
27
+ frontmatter,
28
+ } as MarkdownFile
29
+ })
30
+ .sort((a, b) => a.weight - b.weight) // Sort by weight
31
+ }, [documents])
32
+
33
+ // Get initial path from URL hash, or use first document
34
+ const getInitialPath = () => {
35
+ const hash = window.location.hash.slice(1)
36
+ return hash || processedFiles[0]?.path || null
37
+ }
38
+
39
+ const [currentPath, setCurrentPath] = useState<string | null>(getInitialPath())
40
+
41
+ // Listen for URL hash changes
42
+ useEffect(() => {
43
+ const handleHashChange = () => {
44
+ const hash = window.location.hash.slice(1)
45
+ setCurrentPath(hash || processedFiles[0]?.path || null)
46
+ }
47
+
48
+ window.addEventListener("hashchange", handleHashChange)
49
+ return () => window.removeEventListener("hashchange", handleHashChange)
50
+ }, []) // Empty array - only set up listener once
51
+
52
+ // Function to navigate to a different document
53
+ const navigate = (path: string) => {
54
+ window.location.hash = path
55
+ setCurrentPath(path)
56
+ }
57
+
58
+ // Get the current processed file
59
+ const currentFile = processedFiles.find((file) => file.path === currentPath) || null
60
+
61
+ return {
62
+ currentPath,
63
+ currentFile, // The full processed file object
64
+ navigate,
65
+ allFiles: processedFiles, // All processed files, sorted by weight
66
+ }
67
+ }
package/src/index.ts ADDED
@@ -0,0 +1,48 @@
1
+ /** @public */
2
+ import { DocumentTextIcon } from "@sanity/icons"
3
+ import { marked } from "marked"
4
+ import { createElement } from "react"
5
+ import { definePlugin } from "sanity"
6
+
7
+ import { Doc } from "./components/Doc"
8
+ import { Errors } from "./components/Errors"
9
+ import renderer from "./renderer"
10
+ import type { DocConfig } from "./types"
11
+ //import './style.css'
12
+ marked.use({ renderer })
13
+
14
+ /**
15
+ * TND Documentation plugin for Sanity Studio
16
+ *
17
+ * @public
18
+ */
19
+ export const tndDocs = definePlugin<DocConfig>((config) => {
20
+ const errors: string[] = []
21
+
22
+ if (!config) {
23
+ errors.push("tnd-docs: Configuration is required")
24
+ } else if (config) {
25
+ // Add specific required field checks
26
+ // Example: if your config requires certain fields
27
+ const requiredFields = ["documents"] // Replace with your actual required fields
28
+ const missingFields = requiredFields.filter((field) => !config[field as keyof DocConfig])
29
+ if (missingFields.length > 0) {
30
+ errors.push(`tnd-docs: Missing required configuration fields: ${missingFields.join(", ")}`)
31
+ }
32
+ }
33
+
34
+ return {
35
+ name: "tnd-docs",
36
+ tools: [
37
+ {
38
+ name: (config && config.name) || "tnd-docs",
39
+ title: "Documentation",
40
+ icon: DocumentTextIcon,
41
+ component: () =>
42
+ errors.length
43
+ ? createElement(Errors, { messages: errors })
44
+ : createElement(Doc, { config }),
45
+ },
46
+ ],
47
+ }
48
+ })
@@ -0,0 +1,12 @@
1
+ import { type Renderer } from "marked"
2
+
3
+ const renderer: Partial<Renderer> = {
4
+ image(href: string, title: string | null, text: string): string {
5
+ return `<div><figure>
6
+ <img src="${href}" alt="${text}" />
7
+ ${title ? `<figcaption>${title}</figcaption>` : ""}
8
+ </figure></div>`
9
+ },
10
+ }
11
+
12
+ export default renderer
@@ -0,0 +1,26 @@
1
+ export interface DocConfig {
2
+ name?: string
3
+ title?: string
4
+ documents: Record<string, string>
5
+ }
6
+
7
+ export interface MarkdownFile {
8
+ path: string
9
+ filename: string
10
+ weight: number
11
+ description?: string
12
+ title: string
13
+ content: string
14
+ html: string
15
+ frontmatter: Record<string, any>
16
+ }
17
+
18
+ export interface DocProps {
19
+ config: DocConfig
20
+ }
21
+
22
+ export interface SideNavProps {
23
+ allFiles: MarkdownFile[]
24
+ currentFile: MarkdownFile | null
25
+ navigate: (path: string) => void
26
+ }
@@ -0,0 +1,26 @@
1
+ export function parseFrontmatter(markdown: string): {
2
+ frontmatter: Record<string, any>
3
+ content: string
4
+ } {
5
+ const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/
6
+ const match = markdown.match(frontmatterRegex)
7
+
8
+ if (!match) {
9
+ return { frontmatter: {}, content: markdown }
10
+ }
11
+
12
+ const frontmatterText = match[1] ?? ""
13
+ const content = match[2] ?? ""
14
+
15
+ // Parse YAML-like frontmatter (simple key: value pairs)
16
+ const frontmatter: Record<string, any> = {}
17
+ frontmatterText.split("\n").forEach((line) => {
18
+ const colonIndex = line.indexOf(":")
19
+ if (colonIndex > 0) {
20
+ const key = line.substring(0, colonIndex).trim()
21
+ const value = line.substring(colonIndex + 1).trim()
22
+ frontmatter[key] = value
23
+ }
24
+ })
25
+ return { frontmatter, content }
26
+ }
@@ -0,0 +1,11 @@
1
+ const { showIncompatiblePluginDialog } = require("@sanity/incompatible-plugin")
2
+ const { name, version, sanityExchangeUrl } = require("./package.json")
3
+
4
+ export default showIncompatiblePluginDialog({
5
+ name: name,
6
+ versions: {
7
+ v3: version,
8
+ v2: undefined,
9
+ },
10
+ sanityExchangeUrl,
11
+ })