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 +21 -0
- package/README.md +138 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +167 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +172 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +76 -0
- package/sanity.json +8 -0
- package/src/components/Body.tsx +26 -0
- package/src/components/Doc.tsx +64 -0
- package/src/components/Errors.tsx +14 -0
- package/src/components/SideNav.tsx +34 -0
- package/src/components/index.ts +3 -0
- package/src/hooks/useDocRouter.ts +67 -0
- package/src/index.ts +48 -0
- package/src/renderer.ts +12 -0
- package/src/types/index.ts +26 -0
- package/src/util/index.ts +26 -0
- package/v2-incompatible.js +11 -0
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
|
+

|
|
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.
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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,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,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
|
+
})
|
package/src/renderer.ts
ADDED
|
@@ -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
|
+
})
|