handoff-app 1.1.2 → 1.1.3-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/Changelog.md +44 -0
- package/dist/cli.js +1 -1
- package/package.json +2 -1
- package/src/app/components/ComponentGuidelines.tsx +3 -1
- package/src/app/components/Markdown/MarkdownComponents.tsx +183 -50
- package/src/app/components/Navigation/AnchorNav.tsx +28 -28
- package/src/app/components/util/index.ts +93 -79
- package/src/app/css/index.css +4 -0
- package/src/app/pages/{[level1] → [...slug]}/index.tsx +24 -31
- package/src/app/pages/assets/fonts.tsx +3 -2
- package/src/app/pages/assets/index.tsx +3 -2
- package/src/app/pages/assets/logos.tsx +3 -2
- package/src/app/pages/foundations/colors.tsx +3 -2
- package/src/app/pages/foundations/effects.tsx +3 -2
- package/src/app/pages/foundations/icons/index.tsx +3 -2
- package/src/app/pages/foundations/index.tsx +3 -2
- package/src/app/pages/foundations/logo.tsx +3 -2
- package/src/app/pages/foundations/typography.tsx +3 -2
- package/src/app/pages/index.tsx +3 -2
- package/src/app/pages/system/component/[component]/index.tsx +3 -2
- package/src/app/pages/system/index.tsx +3 -2
- package/src/app/pages/system/tokens/foundations/colors.tsx +3 -2
- package/src/app/pages/system/tokens/foundations/effects.tsx +3 -2
- package/src/app/pages/system/tokens/foundations/typography.tsx +3 -2
- package/src/cli.ts +1 -1
- package/src/app/pages/[level1]/[level2]/index.tsx +0 -104
package/Changelog.md
CHANGED
|
@@ -6,6 +6,50 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
6
6
|
and this project adheres to
|
|
7
7
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## \[1.1.3] - 2026-03-27
|
|
10
|
+
|
|
11
|
+
This release reworks the markdown page rendering system with four improvements:
|
|
12
|
+
n-level deep routing, automatic table-of-contents generation, standardized code
|
|
13
|
+
block styling, and GitHub Flavored Markdown table support.
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- **N-level deep markdown routing** — Replaced the fixed two-level routing
|
|
18
|
+
(`[level1]` / `[level1]/[level2]`) with a single `[...slug]` catch-all route
|
|
19
|
+
that supports markdown pages at any directory depth. A new recursive
|
|
20
|
+
`buildCatchAllStaticPaths` function walks `config/docs` and `pages`
|
|
21
|
+
directories to collect `.md` files at any nesting level. The sidebar menu now
|
|
22
|
+
also recursively scans directories to surface deeply nested pages.
|
|
23
|
+
|
|
24
|
+
- **Automatic table of contents** — The existing but unused `PageTOC` component
|
|
25
|
+
is now wired into the catch-all markdown page. It scans rendered headings
|
|
26
|
+
(h1–h6) via a `MutationObserver` and populates the right-hand "On This Page"
|
|
27
|
+
anchor nav automatically. Fixed a timing bug where headings were scanned
|
|
28
|
+
before the markdown content was in the DOM, and fixed anchor IDs that were
|
|
29
|
+
incorrectly prefixed with `#` (producing `##id` in URLs).
|
|
30
|
+
|
|
31
|
+
- **Standardized code block styling** — Markdown fenced code blocks now render
|
|
32
|
+
with a toolbar header matching the site's `CodeHighlight` component: a
|
|
33
|
+
language icon, filename or language label, and a copy-to-clipboard button. A
|
|
34
|
+
new `remarkCodeMeta` remark plugin preserves the fence info string so metadata
|
|
35
|
+
like `` ```bash filename="deploy.sh" `` is parsed and displayed. The Prism
|
|
36
|
+
theme respects dark mode via `next-themes`, and registered languages now
|
|
37
|
+
include bash, css, scss, jsx, tsx, typescript, yaml, handlebars, and more.
|
|
38
|
+
|
|
39
|
+
- **Markdown table support** — Added `remark-gfm` to all `ReactMarkdown`
|
|
40
|
+
instances across the codebase (17 files). Pipe tables, strikethrough,
|
|
41
|
+
autolinks, and task lists now render correctly. Tables are styled via Tailwind
|
|
42
|
+
Typography's `.prose table` defaults.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- Deleted `src/app/pages/[level1]/index.tsx` and
|
|
47
|
+
`src/app/pages/[level1]/[level2]/index.tsx` (replaced by `[...slug]`).
|
|
48
|
+
- Removed `buildL1StaticPaths`, `buildL2StaticPaths`, and the `SubPageType`
|
|
49
|
+
interface from `src/app/components/util/index.ts`.
|
|
50
|
+
- `ComponentGuidelines` now uses `MarkdownComponents` and the remark plugins
|
|
51
|
+
for consistent rendering.
|
|
52
|
+
|
|
9
53
|
## \[1.1.2] - 2026-03-26
|
|
10
54
|
|
|
11
55
|
This release improves integration with downstream platforms (for example HubSpot) by letting
|
package/dist/cli.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handoff-app",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3-0",
|
|
4
4
|
"description": "Automated documentation toolchain for building client side documentation from figma",
|
|
5
5
|
"author ": {
|
|
6
6
|
"name": "Convertiv",
|
|
@@ -109,6 +109,7 @@
|
|
|
109
109
|
"react-scroll": "^1.8.9",
|
|
110
110
|
"react-syntax-highlighter": "^16.1.0",
|
|
111
111
|
"rehype-raw": "^7.0.0",
|
|
112
|
+
"remark-gfm": "^4.0.1",
|
|
112
113
|
"sass": "^1.85.0",
|
|
113
114
|
"tailwind-merge": "^2.5.5",
|
|
114
115
|
"tailwindcss": "^4.1.14",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import ReactMarkdown from 'react-markdown';
|
|
3
3
|
import rehypeRaw from 'rehype-raw';
|
|
4
|
+
import remarkGfm from 'remark-gfm';
|
|
5
|
+
import { MarkdownComponents, remarkCodeMeta } from './Markdown/MarkdownComponents';
|
|
4
6
|
|
|
5
7
|
export interface ComponentGuidelinesProps {
|
|
6
8
|
content: string;
|
|
@@ -10,7 +12,7 @@ export const ComponentGuidelines: React.FC<ComponentGuidelinesProps> = ({ conten
|
|
|
10
12
|
return (
|
|
11
13
|
<>
|
|
12
14
|
<div className="prose">
|
|
13
|
-
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{content}</ReactMarkdown>
|
|
15
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>{content}</ReactMarkdown>
|
|
14
16
|
</div>
|
|
15
17
|
</>
|
|
16
18
|
);
|
|
@@ -1,23 +1,56 @@
|
|
|
1
|
+
import { useTheme } from 'next-themes';
|
|
1
2
|
import React, { ReactElement } from 'react';
|
|
3
|
+
import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash';
|
|
4
|
+
import css from 'react-syntax-highlighter/dist/esm/languages/prism/css';
|
|
5
|
+
import handlebars from 'react-syntax-highlighter/dist/esm/languages/prism/handlebars';
|
|
2
6
|
import js from 'react-syntax-highlighter/dist/esm/languages/prism/javascript';
|
|
3
7
|
import json from 'react-syntax-highlighter/dist/esm/languages/prism/json';
|
|
8
|
+
import jsx from 'react-syntax-highlighter/dist/esm/languages/prism/jsx';
|
|
4
9
|
import markdown from 'react-syntax-highlighter/dist/esm/languages/prism/markdown';
|
|
5
10
|
import sass from 'react-syntax-highlighter/dist/esm/languages/prism/scss';
|
|
6
|
-
import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/
|
|
11
|
+
import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/tsx';
|
|
12
|
+
import typescript from 'react-syntax-highlighter/dist/esm/languages/prism/typescript';
|
|
7
13
|
import html from 'react-syntax-highlighter/dist/esm/languages/prism/xml-doc';
|
|
14
|
+
import yaml from 'react-syntax-highlighter/dist/esm/languages/prism/yaml';
|
|
8
15
|
import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light';
|
|
9
|
-
import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
16
|
+
import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
17
|
+
import CopyCode from '../CopyCode';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
Braces,
|
|
21
|
+
Code,
|
|
22
|
+
FileCode2,
|
|
23
|
+
FileText,
|
|
24
|
+
FileType,
|
|
25
|
+
Terminal,
|
|
26
|
+
} from 'lucide-react';
|
|
27
|
+
|
|
28
|
+
SyntaxHighlighter.registerLanguage('bash', bash);
|
|
29
|
+
SyntaxHighlighter.registerLanguage('sh', bash);
|
|
30
|
+
SyntaxHighlighter.registerLanguage('shell', bash);
|
|
31
|
+
SyntaxHighlighter.registerLanguage('zsh', bash);
|
|
32
|
+
SyntaxHighlighter.registerLanguage('css', css);
|
|
33
|
+
SyntaxHighlighter.registerLanguage('handlebars', handlebars);
|
|
34
|
+
SyntaxHighlighter.registerLanguage('hbs', handlebars);
|
|
35
|
+
SyntaxHighlighter.registerLanguage('javascript', js);
|
|
10
36
|
SyntaxHighlighter.registerLanguage('js', js);
|
|
11
37
|
SyntaxHighlighter.registerLanguage('json', json);
|
|
38
|
+
SyntaxHighlighter.registerLanguage('jsx', jsx);
|
|
12
39
|
SyntaxHighlighter.registerLanguage('markdown', markdown);
|
|
40
|
+
SyntaxHighlighter.registerLanguage('md', markdown);
|
|
13
41
|
SyntaxHighlighter.registerLanguage('sass', sass);
|
|
42
|
+
SyntaxHighlighter.registerLanguage('scss', sass);
|
|
14
43
|
SyntaxHighlighter.registerLanguage('tsx', tsx);
|
|
44
|
+
SyntaxHighlighter.registerLanguage('typescript', typescript);
|
|
45
|
+
SyntaxHighlighter.registerLanguage('ts', typescript);
|
|
15
46
|
SyntaxHighlighter.registerLanguage('html', html);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
47
|
+
SyntaxHighlighter.registerLanguage('xml', html);
|
|
48
|
+
SyntaxHighlighter.registerLanguage('yaml', yaml);
|
|
49
|
+
SyntaxHighlighter.registerLanguage('yml', yaml);
|
|
50
|
+
|
|
19
51
|
export type CustomRenderers = {
|
|
20
52
|
code: (props: any) => ReactElement;
|
|
53
|
+
pre: (props: any) => ReactElement;
|
|
21
54
|
h1: (props: any) => ReactElement;
|
|
22
55
|
h2: (props: any) => ReactElement;
|
|
23
56
|
h3: (props: any) => ReactElement;
|
|
@@ -27,32 +60,27 @@ export type CustomRenderers = {
|
|
|
27
60
|
paragraph: (props: { children: React.ReactNode }) => ReactElement;
|
|
28
61
|
};
|
|
29
62
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
function extractText(node: any): string {
|
|
64
|
+
if (typeof node === 'string') return node;
|
|
65
|
+
if (typeof node === 'number') return String(node);
|
|
66
|
+
if (Array.isArray(node)) return node.map(extractText).join('');
|
|
67
|
+
if (node?.props?.children) return extractText(node.props.children);
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const Headings = (element: any) => {
|
|
36
72
|
const { children, node } = element;
|
|
37
|
-
// Access actual (string) value of heading
|
|
38
73
|
if (children[0]) {
|
|
39
|
-
const heading = children[0];
|
|
40
74
|
const type = node.tagName || 'h6';
|
|
41
75
|
|
|
42
|
-
|
|
43
|
-
let anchor = heading.toString().toLowerCase();
|
|
44
|
-
|
|
45
|
-
// Clean anchor (replace special characters whitespaces).
|
|
46
|
-
// Alternatively, use encodeURIComponent() if you don't care about
|
|
47
|
-
// pretty anchor links
|
|
76
|
+
let anchor = extractText(children).toLowerCase();
|
|
48
77
|
anchor = anchor.replace(/[^a-zA-Z0-9 ]/g, '');
|
|
49
78
|
anchor = anchor.replace(/ /g, '-');
|
|
50
79
|
|
|
51
|
-
// Utility
|
|
52
80
|
const container = (children: React.ReactNode): React.ReactNode => (
|
|
53
81
|
<>
|
|
54
82
|
{children}
|
|
55
|
-
<a id={anchor} href={`#${anchor}`} className="doc-link"
|
|
83
|
+
<a id={anchor} href={`#${anchor}`} className="doc-link"></a>
|
|
56
84
|
</>
|
|
57
85
|
);
|
|
58
86
|
|
|
@@ -69,22 +97,147 @@ const Headings = (element) => {
|
|
|
69
97
|
return <h5 className="text-sm font-bold">{container(children)}</h5>;
|
|
70
98
|
case 'h6':
|
|
71
99
|
return <h6 className="text-xs font-bold">{container(children)}</h6>;
|
|
72
|
-
|
|
73
100
|
default:
|
|
74
|
-
return (
|
|
75
|
-
<h6 className="text-xs font-bold">
|
|
76
|
-
{JSON.stringify(type)}: {container(children)}
|
|
77
|
-
</h6>
|
|
78
|
-
);
|
|
101
|
+
return <h6 className="text-xs font-bold">{container(children)}</h6>;
|
|
79
102
|
}
|
|
80
103
|
} else {
|
|
81
104
|
return <h1>children</h1>;
|
|
82
105
|
}
|
|
83
106
|
};
|
|
84
107
|
|
|
108
|
+
const LANGUAGE_ICONS: Record<string, React.FC<{ className?: string }>> = {
|
|
109
|
+
bash: Terminal,
|
|
110
|
+
sh: Terminal,
|
|
111
|
+
shell: Terminal,
|
|
112
|
+
zsh: Terminal,
|
|
113
|
+
js: FileCode2,
|
|
114
|
+
javascript: FileCode2,
|
|
115
|
+
ts: FileCode2,
|
|
116
|
+
typescript: FileCode2,
|
|
117
|
+
jsx: FileCode2,
|
|
118
|
+
tsx: FileCode2,
|
|
119
|
+
css: FileType,
|
|
120
|
+
scss: FileType,
|
|
121
|
+
sass: FileType,
|
|
122
|
+
html: FileCode2,
|
|
123
|
+
xml: FileCode2,
|
|
124
|
+
json: Braces,
|
|
125
|
+
yaml: FileText,
|
|
126
|
+
yml: FileText,
|
|
127
|
+
markdown: FileText,
|
|
128
|
+
md: FileText,
|
|
129
|
+
handlebars: Code,
|
|
130
|
+
hbs: Code,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const LANGUAGE_LABELS: Record<string, string> = {
|
|
134
|
+
bash: 'Bash',
|
|
135
|
+
sh: 'Shell',
|
|
136
|
+
shell: 'Shell',
|
|
137
|
+
zsh: 'Zsh',
|
|
138
|
+
js: 'JavaScript',
|
|
139
|
+
javascript: 'JavaScript',
|
|
140
|
+
ts: 'TypeScript',
|
|
141
|
+
typescript: 'TypeScript',
|
|
142
|
+
jsx: 'JSX',
|
|
143
|
+
tsx: 'TSX',
|
|
144
|
+
css: 'CSS',
|
|
145
|
+
scss: 'SCSS',
|
|
146
|
+
sass: 'SASS',
|
|
147
|
+
html: 'HTML',
|
|
148
|
+
xml: 'XML',
|
|
149
|
+
json: 'JSON',
|
|
150
|
+
yaml: 'YAML',
|
|
151
|
+
yml: 'YAML',
|
|
152
|
+
markdown: 'Markdown',
|
|
153
|
+
md: 'Markdown',
|
|
154
|
+
handlebars: 'Handlebars',
|
|
155
|
+
hbs: 'Handlebars',
|
|
156
|
+
text: 'Code',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
function parseMeta(meta: string | undefined): Record<string, string> {
|
|
160
|
+
if (!meta) return {};
|
|
161
|
+
const result: Record<string, string> = {};
|
|
162
|
+
const regex = /(\w+)="([^"]+)"/g;
|
|
163
|
+
let match;
|
|
164
|
+
while ((match = regex.exec(meta)) !== null) {
|
|
165
|
+
result[match[1]] = match[2];
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function MarkdownCodeBlock(props: any) {
|
|
171
|
+
const { className, children, node, 'data-meta': dataMeta, ...rest } = props;
|
|
172
|
+
const { resolvedTheme } = useTheme();
|
|
173
|
+
const isDark = resolvedTheme === 'dark';
|
|
174
|
+
|
|
175
|
+
const langMatch = /language-(\w+)/.exec(className || '');
|
|
176
|
+
const isBlock = String(children).includes('\n');
|
|
177
|
+
|
|
178
|
+
if (!langMatch && !isBlock) {
|
|
179
|
+
return <code className={className} {...rest}>{children}</code>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const lang = langMatch ? langMatch[1] : 'text';
|
|
183
|
+
const meta = parseMeta(dataMeta);
|
|
184
|
+
const filename = meta.filename;
|
|
185
|
+
const codeString = String(children).replace(/\n$/, '');
|
|
186
|
+
const IconComponent = LANGUAGE_ICONS[lang] || Code;
|
|
187
|
+
const langLabel = LANGUAGE_LABELS[lang] || lang;
|
|
188
|
+
|
|
189
|
+
const theme = isDark ? { ...oneDark } : { ...oneLight };
|
|
190
|
+
theme['pre[class*="language-"]'] = {
|
|
191
|
+
...theme['pre[class*="language-"]'],
|
|
192
|
+
overflow: 'auto',
|
|
193
|
+
margin: '0',
|
|
194
|
+
borderRadius: '0 0 0.5rem 0.5rem',
|
|
195
|
+
background: isDark ? '#1e1e1e' : '#fafafa',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div className="not-prose my-4 min-w-0 max-w-full overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
|
200
|
+
<div className="flex items-center justify-between bg-gray-50 px-4 py-2 dark:bg-gray-800">
|
|
201
|
+
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
|
202
|
+
<IconComponent className="h-4 w-4" />
|
|
203
|
+
<span>{filename || langLabel}</span>
|
|
204
|
+
</div>
|
|
205
|
+
<CopyCode code={codeString} />
|
|
206
|
+
</div>
|
|
207
|
+
<SyntaxHighlighter
|
|
208
|
+
style={theme}
|
|
209
|
+
language={lang}
|
|
210
|
+
PreTag="div"
|
|
211
|
+
showLineNumbers={lang !== 'text'}
|
|
212
|
+
wrapLines={true}
|
|
213
|
+
useInlineStyles={true}
|
|
214
|
+
>
|
|
215
|
+
{codeString}
|
|
216
|
+
</SyntaxHighlighter>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
85
221
|
/**
|
|
86
|
-
*
|
|
222
|
+
* Remark plugin that preserves the code fence meta string
|
|
223
|
+
* (the text after the language identifier) as a hast data attribute.
|
|
87
224
|
*/
|
|
225
|
+
export function remarkCodeMeta() {
|
|
226
|
+
return (tree: any) => {
|
|
227
|
+
const walk = (node: any) => {
|
|
228
|
+
if (node.type === 'code' && node.meta) {
|
|
229
|
+
node.data = node.data || {};
|
|
230
|
+
node.data.hProperties = node.data.hProperties || {};
|
|
231
|
+
node.data.hProperties['data-meta'] = node.meta;
|
|
232
|
+
}
|
|
233
|
+
if (node.children) {
|
|
234
|
+
node.children.forEach(walk);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
walk(tree);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
88
241
|
export const MarkdownComponents: CustomRenderers = {
|
|
89
242
|
h1: Headings,
|
|
90
243
|
h2: Headings,
|
|
@@ -93,28 +246,8 @@ export const MarkdownComponents: CustomRenderers = {
|
|
|
93
246
|
h5: Headings,
|
|
94
247
|
h6: Headings,
|
|
95
248
|
paragraph: ({ children }) => <p>{children}</p>,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const match = /language-(\w+)/.exec(className || '');
|
|
99
|
-
// This was designed to flatten out children arrays, but I think its not needed
|
|
100
|
-
// if (props.children[0]) {
|
|
101
|
-
// props.children[0] = props.children[0].toString().trim();
|
|
102
|
-
// }
|
|
103
|
-
return match ? (
|
|
104
|
-
<SyntaxHighlighter
|
|
105
|
-
// @ts-ignore
|
|
106
|
-
style={oneLight}
|
|
107
|
-
language={match[1]}
|
|
108
|
-
PreTag="div"
|
|
109
|
-
showLineNumbers={true}
|
|
110
|
-
wrapLines={true}
|
|
111
|
-
useInlineStyles={true}
|
|
112
|
-
{...props}
|
|
113
|
-
></SyntaxHighlighter>
|
|
114
|
-
) : (
|
|
115
|
-
<code {...props} />
|
|
116
|
-
);
|
|
117
|
-
},
|
|
249
|
+
pre: ({ children }) => <>{children}</>,
|
|
250
|
+
code: MarkdownCodeBlock,
|
|
118
251
|
};
|
|
119
252
|
|
|
120
253
|
export default Headings;
|
|
@@ -18,45 +18,45 @@ interface TOCProps {
|
|
|
18
18
|
export function PageTOC({ body, title }: TOCProps) {
|
|
19
19
|
const [headers, setHeaders] = React.useState<{ id: string; title: string | null; level: number }[]>([]);
|
|
20
20
|
const router = useRouter();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
setHeaders(headers);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
const headersCallback = React.useCallback(getHeaders, [body]);
|
|
21
|
+
|
|
22
|
+
const scanHeaders = React.useCallback(() => {
|
|
23
|
+
if (!body.current) return;
|
|
24
|
+
const found = Array.from(body.current.querySelectorAll('h1, h2, h3, h4, h5, h6')).map((node) => ({
|
|
25
|
+
id: anchorSlugify(node?.textContent?.toString() ?? ''),
|
|
26
|
+
title: node.textContent,
|
|
27
|
+
level: parseInt(node.tagName[1]),
|
|
28
|
+
}));
|
|
29
|
+
setHeaders(found);
|
|
30
|
+
}, [body]);
|
|
35
31
|
|
|
36
32
|
useEffect(() => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
};
|
|
45
|
-
|
|
33
|
+
// Scan after paint to catch SSR-hydrated content
|
|
34
|
+
requestAnimationFrame(scanHeaders);
|
|
35
|
+
|
|
36
|
+
// Watch for dynamically inserted headings (client-rendered markdown)
|
|
37
|
+
const el = body.current;
|
|
38
|
+
if (!el) return;
|
|
39
|
+
const observer = new MutationObserver(scanHeaders);
|
|
40
|
+
observer.observe(el, { childList: true, subtree: true });
|
|
41
|
+
return () => observer.disconnect();
|
|
42
|
+
}, [body, scanHeaders]);
|
|
43
|
+
|
|
46
44
|
useEffect(() => {
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
const handler = () => requestAnimationFrame(scanHeaders);
|
|
46
|
+
router.events.on('routeChangeComplete', handler);
|
|
47
|
+
return () => router.events.off('routeChangeComplete', handler);
|
|
48
|
+
}, [router, scanHeaders]);
|
|
49
49
|
return (
|
|
50
50
|
<AnchorNav
|
|
51
51
|
title={title}
|
|
52
52
|
groups={headers.reduce((acc, header) => {
|
|
53
53
|
if (header.level === 1) {
|
|
54
|
-
acc.push({ [
|
|
54
|
+
acc.push({ [header.id]: header.title ?? '' });
|
|
55
55
|
} else {
|
|
56
56
|
if (acc.length === 0) {
|
|
57
|
-
acc.push({ [
|
|
57
|
+
acc.push({ [header.id]: header.title ?? '' });
|
|
58
58
|
} else {
|
|
59
|
-
acc[acc.length - 1][
|
|
59
|
+
acc[acc.length - 1][header.id] = header.title ?? '';
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
return acc;
|
|
@@ -7,13 +7,12 @@ import { Types as CoreTypes } from 'handoff-core';
|
|
|
7
7
|
import { groupBy, startCase, uniq } from 'lodash';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { ParsedUrlQuery } from 'querystring';
|
|
10
|
-
import { SubPageType } from '../../pages/[level1]/[level2]';
|
|
11
|
-
|
|
12
10
|
// Get the parsed url string type
|
|
13
11
|
export interface IParams extends ParsedUrlQuery {
|
|
14
|
-
slug: string;
|
|
12
|
+
slug: string | string[];
|
|
15
13
|
}
|
|
16
14
|
|
|
15
|
+
|
|
17
16
|
// Type for the metadata from frontmatter
|
|
18
17
|
export interface Metadata {
|
|
19
18
|
title: string;
|
|
@@ -118,95 +117,99 @@ export const pluralizeComponent = (singular: string): string => {
|
|
|
118
117
|
};
|
|
119
118
|
|
|
120
119
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
120
|
+
* Recursively collect all .md files from a directory, returning their
|
|
121
|
+
* path segments relative to the root (without the .md extension).
|
|
123
122
|
*/
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
123
|
+
const collectMarkdownPaths = (rootDir: string, relativeParts: string[] = []): string[][] => {
|
|
124
|
+
if (!fs.existsSync(rootDir)) return [];
|
|
125
|
+
const entries = fs.readdirSync(rootDir);
|
|
126
|
+
const results: string[][] = [];
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
const fullPath = path.join(rootDir, entry);
|
|
129
|
+
if (fs.lstatSync(fullPath).isDirectory()) {
|
|
130
|
+
results.push(...collectMarkdownPaths(fullPath, [...relativeParts, entry]));
|
|
131
|
+
} else if (entry.endsWith('.md') && entry !== 'index.md') {
|
|
132
|
+
results.push([...relativeParts, entry.replace('.md', '')]);
|
|
133
|
+
}
|
|
132
134
|
}
|
|
133
|
-
|
|
134
|
-
.filter((fileName) => {
|
|
135
|
-
if (fs.existsSync(path.join(docRoot, fileName))) return !fs.lstatSync(path.join(docRoot, fileName)).isDirectory();
|
|
136
|
-
if (fs.existsSync(path.join(pageRoot, fileName))) return !fs.lstatSync(path.join(pageRoot, fileName)).isDirectory();
|
|
137
|
-
return false;
|
|
138
|
-
})
|
|
139
|
-
.filter((fileName) => fileName.endsWith('.md'))
|
|
140
|
-
.map((fileName) => {
|
|
141
|
-
const path = fileName.replace('.md', '');
|
|
142
|
-
if (knownPaths.indexOf(path) < 0) {
|
|
143
|
-
return {
|
|
144
|
-
params: {
|
|
145
|
-
level1: path,
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
.filter(filterOutUndefined);
|
|
151
|
-
return paths;
|
|
135
|
+
return results;
|
|
152
136
|
};
|
|
153
137
|
|
|
154
138
|
/**
|
|
155
|
-
* Build static paths for
|
|
156
|
-
*
|
|
139
|
+
* Build catch-all static paths for all markdown pages at any depth.
|
|
140
|
+
* Excludes paths in knownPaths (those have dedicated route files).
|
|
157
141
|
*/
|
|
158
|
-
export const
|
|
142
|
+
export const buildCatchAllStaticPaths = () => {
|
|
159
143
|
const docRoot = path.resolve(process.env.HANDOFF_MODULE_PATH ?? '', 'config/docs');
|
|
160
|
-
const files = fs.readdirSync(docRoot);
|
|
161
144
|
const pageRoot = path.resolve(process.env.HANDOFF_WORKING_PATH ?? '', 'pages');
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
145
|
+
|
|
146
|
+
const docPaths = collectMarkdownPaths(docRoot);
|
|
147
|
+
const pagePaths = collectMarkdownPaths(pageRoot);
|
|
148
|
+
|
|
149
|
+
const seen = new Set<string>();
|
|
150
|
+
const allPaths: string[][] = [];
|
|
151
|
+
|
|
152
|
+
// Pages override docs (working copy wins)
|
|
153
|
+
for (const segments of [...pagePaths, ...docPaths]) {
|
|
154
|
+
const key = segments.join('/');
|
|
155
|
+
if (!seen.has(key)) {
|
|
156
|
+
seen.add(key);
|
|
157
|
+
allPaths.push(segments);
|
|
158
|
+
}
|
|
166
159
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
})
|
|
194
|
-
.filter(filterOutUndefined);
|
|
160
|
+
|
|
161
|
+
return allPaths
|
|
162
|
+
.filter((segments) => knownPaths.indexOf(segments.join('/')) < 0)
|
|
163
|
+
.map((segments) => ({ params: { slug: segments } }));
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Recursively build menu entries from .md files in a directory.
|
|
168
|
+
* Returns sub-section items with nested menu items for subdirectories.
|
|
169
|
+
*/
|
|
170
|
+
const buildMenuFromDirectory = (dirPath: string, urlPrefix: string): any[] => {
|
|
171
|
+
if (!fs.existsSync(dirPath)) return [];
|
|
172
|
+
const entries = fs.readdirSync(dirPath);
|
|
173
|
+
const items: any[] = [];
|
|
174
|
+
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const fullPath = path.join(dirPath, entry);
|
|
177
|
+
if (fs.lstatSync(fullPath).isDirectory()) {
|
|
178
|
+
const nestedItems = buildMenuFromDirectory(fullPath, `${urlPrefix}/${entry}`);
|
|
179
|
+
if (nestedItems.length > 0) {
|
|
180
|
+
items.push({
|
|
181
|
+
title: startCase(entry),
|
|
182
|
+
path: `${urlPrefix}/${entry}`,
|
|
183
|
+
menu: nestedItems,
|
|
184
|
+
});
|
|
195
185
|
}
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
|
|
186
|
+
} else if (entry.endsWith('.md') && entry !== 'index.md') {
|
|
187
|
+
const slug = entry.replace('.md', '');
|
|
188
|
+
const fullSlugPath = `${urlPrefix}/${slug}`.replace(/^\/+/, '');
|
|
189
|
+
if (knownPaths.indexOf(fullSlugPath) >= 0) continue;
|
|
190
|
+
|
|
191
|
+
const contents = fs.readFileSync(fullPath, 'utf-8');
|
|
192
|
+
const { data: metadata } = matter(contents);
|
|
193
|
+
if (metadata.enabled === false) continue;
|
|
194
|
+
|
|
195
|
+
items.push({
|
|
196
|
+
title: metadata.menuTitle ?? metadata.title ?? startCase(slug),
|
|
197
|
+
path: `/${fullSlugPath}`,
|
|
198
|
+
weight: metadata.weight ?? 0,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return items.sort((a, b) => (a.weight ?? 0) - (b.weight ?? 0));
|
|
199
204
|
};
|
|
200
205
|
|
|
201
206
|
/**
|
|
202
|
-
* Build the static menu for
|
|
207
|
+
* Build the static menu for rendering pages
|
|
203
208
|
* @returns SectionLink[]
|
|
204
209
|
*/
|
|
205
210
|
export const staticBuildMenu = () => {
|
|
206
|
-
// // Contents of docs
|
|
207
211
|
const docRoot = path.join(process.env.HANDOFF_MODULE_PATH ?? '', 'config/docs');
|
|
208
|
-
|
|
209
|
-
if(!fs.existsSync(docRoot)) {
|
|
212
|
+
if (!fs.existsSync(docRoot)) {
|
|
210
213
|
return [];
|
|
211
214
|
}
|
|
212
215
|
const files = fs.readdirSync(docRoot);
|
|
@@ -218,7 +221,6 @@ export const staticBuildMenu = () => {
|
|
|
218
221
|
list = list.concat(pages);
|
|
219
222
|
}
|
|
220
223
|
const sections: SectionLink[] = [];
|
|
221
|
-
// Build path tree
|
|
222
224
|
const custom = uniq(list)
|
|
223
225
|
.map((fileName: string) => {
|
|
224
226
|
let search = '';
|
|
@@ -243,19 +245,16 @@ export const staticBuildMenu = () => {
|
|
|
243
245
|
let subSections = [];
|
|
244
246
|
|
|
245
247
|
if (metadata.menu) {
|
|
246
|
-
// Build the submenu
|
|
247
248
|
subSections = Object.keys(metadata.menu)
|
|
248
249
|
.map((key) => {
|
|
249
250
|
const sub = metadata.menu[key];
|
|
250
251
|
if (sub.components) {
|
|
251
|
-
// The user wants to inject the component menu here
|
|
252
252
|
return {
|
|
253
253
|
title: sub.title,
|
|
254
254
|
menu: staticBuildComponentMenu(sub.components),
|
|
255
255
|
};
|
|
256
256
|
}
|
|
257
257
|
if (sub.tokens) {
|
|
258
|
-
// The user wants to inject the component menu here
|
|
259
258
|
return {
|
|
260
259
|
title: 'Tokens',
|
|
261
260
|
menu: staticBuildTokensMenu(),
|
|
@@ -266,6 +265,21 @@ export const staticBuildMenu = () => {
|
|
|
266
265
|
}
|
|
267
266
|
})
|
|
268
267
|
.filter(filterOutUndefined);
|
|
268
|
+
} else {
|
|
269
|
+
// Only auto-scan directories when no frontmatter menu is defined
|
|
270
|
+
const dirName = fileName.replace('.md', '');
|
|
271
|
+
const docDir = path.resolve(docRoot, dirName);
|
|
272
|
+
const pagesDir = path.resolve(workingPages, dirName);
|
|
273
|
+
const nestedFromDocs = buildMenuFromDirectory(docDir, `/${dirName}`);
|
|
274
|
+
const nestedFromPages = buildMenuFromDirectory(pagesDir, `/${dirName}`);
|
|
275
|
+
|
|
276
|
+
const seenPaths = new Set<string>();
|
|
277
|
+
for (const item of [...nestedFromPages, ...nestedFromDocs]) {
|
|
278
|
+
if (item.path && !seenPaths.has(item.path)) {
|
|
279
|
+
seenPaths.add(item.path);
|
|
280
|
+
subSections.push(item);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
269
283
|
}
|
|
270
284
|
|
|
271
285
|
let external: string | boolean = false;
|
package/src/app/css/index.css
CHANGED
|
@@ -1,56 +1,46 @@
|
|
|
1
1
|
import { GetStaticProps } from 'next';
|
|
2
2
|
import Head from 'next/head';
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
+
import { useRef } from 'react';
|
|
4
5
|
import ReactMarkdown from 'react-markdown';
|
|
5
6
|
import rehypeRaw from 'rehype-raw';
|
|
7
|
+
import remarkGfm from 'remark-gfm';
|
|
6
8
|
import Layout from '../../components/Layout/Main';
|
|
7
|
-
import { MarkdownComponents } from '../../components/Markdown/MarkdownComponents';
|
|
9
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../components/Markdown/MarkdownComponents';
|
|
10
|
+
import { PageTOC } from '../../components/Navigation/AnchorNav';
|
|
8
11
|
import HeadersType from '../../components/Typography/Headers';
|
|
9
12
|
import {
|
|
10
|
-
|
|
13
|
+
buildCatchAllStaticPaths,
|
|
11
14
|
DocumentationProps,
|
|
12
15
|
fetchDocPageMarkdown,
|
|
13
16
|
getClientRuntimeConfig,
|
|
14
|
-
IParams,
|
|
15
|
-
reduceSlugToString,
|
|
16
17
|
} from '../../components/util';
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
-
* Render all index pages
|
|
20
|
-
* @returns
|
|
21
|
-
*/
|
|
22
19
|
export async function getStaticPaths() {
|
|
23
20
|
return {
|
|
24
|
-
paths:
|
|
21
|
+
paths: buildCatchAllStaticPaths(),
|
|
25
22
|
fallback: false,
|
|
26
23
|
};
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
/**
|
|
30
|
-
* Get all the markdown data, build a menu tree, and then fetch the contents
|
|
31
|
-
* of the current page for rendering
|
|
32
|
-
*
|
|
33
|
-
* This is all done statically at build time
|
|
34
|
-
* @param context GetStaticProps
|
|
35
|
-
* @returns
|
|
36
|
-
*/
|
|
37
26
|
export const getStaticProps: GetStaticProps = (context) => {
|
|
38
|
-
|
|
39
|
-
const
|
|
27
|
+
const { slug } = context.params as { slug: string[] };
|
|
28
|
+
const dirParts = slug.slice(0, -1);
|
|
29
|
+
const file = slug[slug.length - 1];
|
|
30
|
+
const docPath = dirParts.length > 0 ? `docs/${dirParts.join('/')}/` : 'docs/';
|
|
31
|
+
const sectionId = `/${slug[0]}`;
|
|
32
|
+
|
|
40
33
|
return {
|
|
41
34
|
props: {
|
|
42
|
-
...fetchDocPageMarkdown(
|
|
35
|
+
...fetchDocPageMarkdown(docPath, file, sectionId).props,
|
|
43
36
|
config: getClientRuntimeConfig(),
|
|
44
37
|
},
|
|
45
38
|
};
|
|
46
39
|
};
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
* @returns
|
|
52
|
-
*/
|
|
53
|
-
export default function DocPage({ content, menu, metadata, current, config }: DocumentationProps) {
|
|
41
|
+
export default function DocCatchAllPage({ content, menu, metadata, current, config }: DocumentationProps) {
|
|
42
|
+
const bodyRef = useRef<HTMLDivElement>(null);
|
|
43
|
+
|
|
54
44
|
if (content) {
|
|
55
45
|
return (
|
|
56
46
|
<Layout config={config} menu={menu} current={current} metadata={metadata}>
|
|
@@ -58,12 +48,15 @@ export default function DocPage({ content, menu, metadata, current, config }: Do
|
|
|
58
48
|
<HeadersType.H1>{metadata.title}</HeadersType.H1>
|
|
59
49
|
<p className="text-lg leading-relaxed text-gray-600 dark:text-gray-300">{metadata.description}</p>
|
|
60
50
|
</div>
|
|
61
|
-
<div>
|
|
62
|
-
<div className="
|
|
63
|
-
<
|
|
64
|
-
{
|
|
65
|
-
|
|
51
|
+
<div className="lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_280px]">
|
|
52
|
+
<div className="min-w-0">
|
|
53
|
+
<div className="prose mb-10" ref={bodyRef}>
|
|
54
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
55
|
+
{content}
|
|
56
|
+
</ReactMarkdown>
|
|
57
|
+
</div>
|
|
66
58
|
</div>
|
|
59
|
+
<PageTOC body={bodyRef} title="On This Page" />
|
|
67
60
|
</div>
|
|
68
61
|
</Layout>
|
|
69
62
|
);
|
|
@@ -7,8 +7,9 @@ import path from 'path';
|
|
|
7
7
|
import * as React from 'react';
|
|
8
8
|
import ReactMarkdown from 'react-markdown';
|
|
9
9
|
import rehypeRaw from 'rehype-raw';
|
|
10
|
+
import remarkGfm from 'remark-gfm';
|
|
10
11
|
import Footer from '../../components/Footer';
|
|
11
|
-
import { MarkdownComponents } from '../../components/Markdown/MarkdownComponents';
|
|
12
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../components/Markdown/MarkdownComponents';
|
|
12
13
|
import Header from '../../components/old/Header';
|
|
13
14
|
import { fetchDocPageMarkdown, FontDocumentationProps, getClientRuntimeConfig, getTokens } from '../../components/util';
|
|
14
15
|
|
|
@@ -92,7 +93,7 @@ const FontsPage = ({ content, menu, metadata, current, customFonts, design, conf
|
|
|
92
93
|
))}
|
|
93
94
|
|
|
94
95
|
<div className="prose">
|
|
95
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
96
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
96
97
|
{content}
|
|
97
98
|
</ReactMarkdown>
|
|
98
99
|
</div>
|
|
@@ -3,8 +3,9 @@ import type { GetStaticProps } from 'next';
|
|
|
3
3
|
import Head from 'next/head';
|
|
4
4
|
import ReactMarkdown from 'react-markdown';
|
|
5
5
|
import rehypeRaw from 'rehype-raw';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
6
7
|
import Footer from '../../components/Footer';
|
|
7
|
-
import { MarkdownComponents } from '../../components/Markdown/MarkdownComponents';
|
|
8
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../components/Markdown/MarkdownComponents';
|
|
8
9
|
import NavLink from '../../components/NavLink';
|
|
9
10
|
import Header from '../../components/old/Header';
|
|
10
11
|
import { DocumentationProps, fetchDocPageMarkdown, getClientRuntimeConfig } from '../../components/util';
|
|
@@ -77,7 +78,7 @@ const AssetsPage = ({ content, menu, metadata, current, config }: DocumentationP
|
|
|
77
78
|
</div>
|
|
78
79
|
|
|
79
80
|
<div className="prose">
|
|
80
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
81
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
81
82
|
{content}
|
|
82
83
|
</ReactMarkdown>
|
|
83
84
|
</div>
|
|
@@ -3,8 +3,9 @@ import type { GetStaticProps } from 'next';
|
|
|
3
3
|
import Head from 'next/head';
|
|
4
4
|
import ReactMarkdown from 'react-markdown';
|
|
5
5
|
import rehypeRaw from 'rehype-raw';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
6
7
|
import Footer from '../../components/Footer';
|
|
7
|
-
import { MarkdownComponents } from '../../components/Markdown/MarkdownComponents';
|
|
8
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../components/Markdown/MarkdownComponents';
|
|
8
9
|
import Header from '../../components/old/Header';
|
|
9
10
|
import { DocumentationProps, fetchDocPageMarkdown, getClientRuntimeConfig } from '../../components/util';
|
|
10
11
|
|
|
@@ -57,7 +58,7 @@ const AssetsLogosPage = ({ content, menu, metadata, current, config }: Documenta
|
|
|
57
58
|
</div>
|
|
58
59
|
</div>
|
|
59
60
|
<div className="prose">
|
|
60
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
61
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
61
62
|
{content}
|
|
62
63
|
</ReactMarkdown>
|
|
63
64
|
</div>
|
|
@@ -3,10 +3,11 @@ import upperFirst from 'lodash/upperFirst';
|
|
|
3
3
|
import type { GetStaticProps } from 'next';
|
|
4
4
|
import ReactMarkdown from 'react-markdown';
|
|
5
5
|
import rehypeRaw from 'rehype-raw';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
6
7
|
import ColorGrid from '../..//components/Foundations/ColorGrid';
|
|
7
8
|
import { DownloadTokens } from '../../components/DownloadTokens';
|
|
8
9
|
import Layout from '../../components/Layout/Main';
|
|
9
|
-
import { MarkdownComponents } from '../../components/Markdown/MarkdownComponents';
|
|
10
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../components/Markdown/MarkdownComponents';
|
|
10
11
|
import AnchorNav from '../../components/Navigation/AnchorNav';
|
|
11
12
|
import PrevNextNav from '../../components/Navigation/PrevNextNav';
|
|
12
13
|
import HeadersType from '../../components/Typography/Headers';
|
|
@@ -89,7 +90,7 @@ const ColorsPage = ({
|
|
|
89
90
|
/>
|
|
90
91
|
|
|
91
92
|
<div className="prose">
|
|
92
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
93
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
93
94
|
{content}
|
|
94
95
|
</ReactMarkdown>
|
|
95
96
|
</div>
|
|
@@ -6,9 +6,10 @@ import type { GetStaticProps } from 'next';
|
|
|
6
6
|
import * as React from 'react';
|
|
7
7
|
import ReactMarkdown from 'react-markdown';
|
|
8
8
|
import rehypeRaw from 'rehype-raw';
|
|
9
|
+
import remarkGfm from 'remark-gfm';
|
|
9
10
|
import { DownloadTokens } from '../../components/DownloadTokens';
|
|
10
11
|
import Layout from '../../components/Layout/Main';
|
|
11
|
-
import { MarkdownComponents } from '../../components/Markdown/MarkdownComponents';
|
|
12
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../components/Markdown/MarkdownComponents';
|
|
12
13
|
import AnchorNav from '../../components/Navigation/AnchorNav';
|
|
13
14
|
import HeadersType from '../../components/Typography/Headers';
|
|
14
15
|
import * as util from '../../components/util';
|
|
@@ -128,7 +129,7 @@ const EffectsPage = ({
|
|
|
128
129
|
/>
|
|
129
130
|
|
|
130
131
|
<div className="prose">
|
|
131
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
132
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
132
133
|
{content}
|
|
133
134
|
</ReactMarkdown>
|
|
134
135
|
</div>
|
|
@@ -5,9 +5,10 @@ import Link from 'next/link';
|
|
|
5
5
|
import * as React from 'react';
|
|
6
6
|
import ReactMarkdown from 'react-markdown';
|
|
7
7
|
import rehypeRaw from 'rehype-raw';
|
|
8
|
+
import remarkGfm from 'remark-gfm';
|
|
8
9
|
import Footer from '../../../components/Footer';
|
|
9
10
|
import Layout from '../../../components/Layout/Main';
|
|
10
|
-
import { MarkdownComponents } from '../../../components/Markdown/MarkdownComponents';
|
|
11
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../../components/Markdown/MarkdownComponents';
|
|
11
12
|
import HeadersType from '../../../components/Typography/Headers';
|
|
12
13
|
import { buttonVariants } from '../../../components/ui/button';
|
|
13
14
|
import { Input } from '../../../components/ui/input';
|
|
@@ -99,7 +100,7 @@ const IconsPage = ({ content, menu, metadata, current, config, assets }: AssetDo
|
|
|
99
100
|
</div>
|
|
100
101
|
<hr className="mb-10" />
|
|
101
102
|
<div className="prose">
|
|
102
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
103
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
103
104
|
{content}
|
|
104
105
|
</ReactMarkdown>
|
|
105
106
|
</div>
|
|
@@ -2,9 +2,10 @@ import { Hexagon, LayoutPanelLeft, Palette, Shapes, Sun, TypeOutline } from 'luc
|
|
|
2
2
|
import type { GetStaticProps } from 'next';
|
|
3
3
|
import ReactMarkdown from 'react-markdown';
|
|
4
4
|
import rehypeRaw from 'rehype-raw';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
5
6
|
import CardsWithIcons from '../../components/cards/CardsWithIcons';
|
|
6
7
|
import Layout from '../../components/Layout/Main';
|
|
7
|
-
import { MarkdownComponents } from '../../components/Markdown/MarkdownComponents';
|
|
8
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../components/Markdown/MarkdownComponents';
|
|
8
9
|
import HeadersType from '../../components/Typography/Headers';
|
|
9
10
|
import { DocumentationProps, fetchDocPageMarkdown, getClientRuntimeConfig } from '../../components/util';
|
|
10
11
|
|
|
@@ -33,7 +34,7 @@ const DesignPage = ({ content, menu, metadata, current, config }: DocumentationP
|
|
|
33
34
|
<p className="text-lg leading-relaxed text-gray-600 dark:text-gray-300">{metadata.description}</p>
|
|
34
35
|
</div>
|
|
35
36
|
<div className="prose">
|
|
36
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
37
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
37
38
|
{content}
|
|
38
39
|
</ReactMarkdown>
|
|
39
40
|
</div>
|
|
@@ -6,9 +6,10 @@ import { AssetDocumentationProps, fetchDocPageMarkdown, getClientRuntimeConfig,
|
|
|
6
6
|
import { Download } from 'lucide-react';
|
|
7
7
|
import ReactMarkdown from 'react-markdown';
|
|
8
8
|
import rehypeRaw from 'rehype-raw';
|
|
9
|
+
import remarkGfm from 'remark-gfm';
|
|
9
10
|
import { DisplayLogo } from '../../components/Foundations/DisplayLogo';
|
|
10
11
|
import Layout from '../../components/Layout/Main';
|
|
11
|
-
import { MarkdownComponents } from '../../components/Markdown/MarkdownComponents';
|
|
12
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../components/Markdown/MarkdownComponents';
|
|
12
13
|
import HeadersType from '../../components/Typography/Headers';
|
|
13
14
|
import { buttonVariants } from '../../components/ui/button';
|
|
14
15
|
|
|
@@ -73,7 +74,7 @@ const LogoPage = ({ content, menu, metadata, current, config, assets }: AssetDoc
|
|
|
73
74
|
<hr />
|
|
74
75
|
<hr />
|
|
75
76
|
<div className="prose">
|
|
76
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
77
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
77
78
|
{content}
|
|
78
79
|
</ReactMarkdown>
|
|
79
80
|
</div>
|
|
@@ -3,10 +3,11 @@ import sortedUniq from 'lodash/sortedUniq';
|
|
|
3
3
|
import type * as next from 'next';
|
|
4
4
|
import ReactMarkdown from 'react-markdown';
|
|
5
5
|
import rehypeRaw from 'rehype-raw';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
6
7
|
import { DownloadTokens } from '../../components/DownloadTokens';
|
|
7
8
|
import TypographyExamples from '../../components/Foundations/TypographyExample';
|
|
8
9
|
import Layout from '../../components/Layout/Main';
|
|
9
|
-
import { MarkdownComponents } from '../../components/Markdown/MarkdownComponents';
|
|
10
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../components/Markdown/MarkdownComponents';
|
|
10
11
|
import AnchorNav, { anchorSlugify } from '../../components/Navigation/AnchorNav';
|
|
11
12
|
import HeadersType from '../../components/Typography/Headers';
|
|
12
13
|
import { fetchFoundationDocPageMarkdown, FoundationDocumentationProps, getClientRuntimeConfig, getTokens } from '../../components/util';
|
|
@@ -124,7 +125,7 @@ const Typography = ({
|
|
|
124
125
|
/>
|
|
125
126
|
|
|
126
127
|
<div className="prose">
|
|
127
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
128
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
128
129
|
{content}
|
|
129
130
|
</ReactMarkdown>
|
|
130
131
|
</div>
|
package/src/app/pages/index.tsx
CHANGED
|
@@ -5,9 +5,10 @@ import Link from 'next/link';
|
|
|
5
5
|
import { useRouter } from 'next/router';
|
|
6
6
|
import ReactMarkdown from 'react-markdown';
|
|
7
7
|
import rehypeRaw from 'rehype-raw';
|
|
8
|
+
import remarkGfm from 'remark-gfm';
|
|
8
9
|
import Footer from '../components/Footer';
|
|
9
10
|
import Layout from '../components/Layout/Main';
|
|
10
|
-
import { MarkdownComponents } from '../components/Markdown/MarkdownComponents';
|
|
11
|
+
import { MarkdownComponents, remarkCodeMeta } from '../components/Markdown/MarkdownComponents';
|
|
11
12
|
import HeadersType from '../components/Typography/Headers';
|
|
12
13
|
import { Button } from '../components/ui/button';
|
|
13
14
|
import { DocumentationProps, fetchDocPageMarkdown, getClientRuntimeConfig } from '../components/util';
|
|
@@ -41,7 +42,7 @@ const Home = ({ content, menu, metadata, config, current }: DocumentationProps)
|
|
|
41
42
|
</HeadersType.H1>
|
|
42
43
|
{content && (
|
|
43
44
|
<div className="text-lg max-w-4xl empty:hidden [&>*]:mt-6 [&>p]:font-light [&>h2:first-child]:mt-0 [&>h2:first-child]:font-normal sm:text-xl">
|
|
44
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
45
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
45
46
|
{content}
|
|
46
47
|
</ReactMarkdown>
|
|
47
48
|
</div>
|
|
@@ -6,11 +6,12 @@ import { startCase } from 'lodash';
|
|
|
6
6
|
import React, { useEffect, useState } from 'react';
|
|
7
7
|
import ReactMarkdown from 'react-markdown';
|
|
8
8
|
import rehypeRaw from 'rehype-raw';
|
|
9
|
+
import remarkGfm from 'remark-gfm';
|
|
9
10
|
import { ComponentPreview } from '../../../../components/Component/Preview';
|
|
10
11
|
import { HotReloadProvider } from '../../../../components/context/HotReloadProvider';
|
|
11
12
|
import { PreviewContextProvider } from '../../../../components/context/PreviewContext';
|
|
12
13
|
import Layout from '../../../../components/Layout/Main';
|
|
13
|
-
import { MarkdownComponents } from '../../../../components/Markdown/MarkdownComponents';
|
|
14
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../../../components/Markdown/MarkdownComponents';
|
|
14
15
|
import AnchorNav from '../../../../components/Navigation/AnchorNav';
|
|
15
16
|
import PrevNextNav from '../../../../components/Navigation/PrevNextNav';
|
|
16
17
|
import HeadersType from '../../../../components/Typography/Headers';
|
|
@@ -172,7 +173,7 @@ const GenericComponentPage = ({ menu, metadata, current, id, config, componentHo
|
|
|
172
173
|
<HeadersType.H1>{metadata.title}</HeadersType.H1>
|
|
173
174
|
<div className="flex flex-row justify-between gap-4 md:flex-col">
|
|
174
175
|
<div className="prose max-w-[800px] text-xl font-light leading-relaxed text-gray-600 dark:text-gray-300">
|
|
175
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
176
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
176
177
|
{metadata.description}
|
|
177
178
|
</ReactMarkdown>
|
|
178
179
|
</div>
|
|
@@ -5,9 +5,10 @@ import Image from 'next/image';
|
|
|
5
5
|
import { useEffect, useState } from 'react';
|
|
6
6
|
import ReactMarkdown from 'react-markdown';
|
|
7
7
|
import rehypeRaw from 'rehype-raw';
|
|
8
|
+
import remarkGfm from 'remark-gfm';
|
|
8
9
|
import { APIComponentList } from '../../components/Component/ComponentLists';
|
|
9
10
|
import Layout from '../../components/Layout/Main';
|
|
10
|
-
import { MarkdownComponents } from '../../components/Markdown/MarkdownComponents';
|
|
11
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../components/Markdown/MarkdownComponents';
|
|
11
12
|
import HeadersType from '../../components/Typography/Headers';
|
|
12
13
|
import { Button } from '../../components/ui/button';
|
|
13
14
|
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from '../../components/ui/drawer';
|
|
@@ -127,7 +128,7 @@ const ComponentsPage = ({ content, menu, metadata, current, config }: ComponentP
|
|
|
127
128
|
</div>
|
|
128
129
|
<div>
|
|
129
130
|
<div className="prose mb-10">
|
|
130
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
131
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
131
132
|
{content}
|
|
132
133
|
</ReactMarkdown>
|
|
133
134
|
</div>
|
|
@@ -3,8 +3,9 @@ import { groupBy, upperFirst } from 'lodash';
|
|
|
3
3
|
import type { GetStaticProps } from 'next';
|
|
4
4
|
import ReactMarkdown from 'react-markdown';
|
|
5
5
|
import rehypeRaw from 'rehype-raw';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
6
7
|
import Layout from '../../../../components/Layout/Main';
|
|
7
|
-
import { MarkdownComponents } from '../../../../components/Markdown/MarkdownComponents';
|
|
8
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../../../components/Markdown/MarkdownComponents';
|
|
8
9
|
import AnchorNav from '../../../../components/Navigation/AnchorNav';
|
|
9
10
|
import HeadersType from '../../../../components/Typography/Headers';
|
|
10
11
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../../../components/ui/table';
|
|
@@ -67,7 +68,7 @@ const ComponentsPage = ({ content, menu, metadata, current, config, design }: Fo
|
|
|
67
68
|
<div className="mt-10 lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_280px]">
|
|
68
69
|
<div>
|
|
69
70
|
<div className="prose">
|
|
70
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
71
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
71
72
|
{content}
|
|
72
73
|
</ReactMarkdown>
|
|
73
74
|
</div>
|
|
@@ -3,8 +3,9 @@ import { groupBy, upperFirst } from 'lodash';
|
|
|
3
3
|
import type { GetStaticProps } from 'next';
|
|
4
4
|
import ReactMarkdown from 'react-markdown';
|
|
5
5
|
import rehypeRaw from 'rehype-raw';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
6
7
|
import Layout from '../../../../components/Layout/Main';
|
|
7
|
-
import { MarkdownComponents } from '../../../../components/Markdown/MarkdownComponents';
|
|
8
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../../../components/Markdown/MarkdownComponents';
|
|
8
9
|
import AnchorNav from '../../../../components/Navigation/AnchorNav';
|
|
9
10
|
import HeadersType from '../../../../components/Typography/Headers';
|
|
10
11
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../../../components/ui/table';
|
|
@@ -61,7 +62,7 @@ const ComponentsPage = ({ content, menu, metadata, current, config, design }: Fo
|
|
|
61
62
|
<div className="mt-10 lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_280px]">
|
|
62
63
|
<div>
|
|
63
64
|
<div className="prose">
|
|
64
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
65
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
65
66
|
{content}
|
|
66
67
|
</ReactMarkdown>
|
|
67
68
|
</div>
|
|
@@ -4,8 +4,9 @@ import type { GetStaticProps } from 'next';
|
|
|
4
4
|
import React, { ReactElement } from 'react';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import rehypeRaw from 'rehype-raw';
|
|
7
|
+
import remarkGfm from 'remark-gfm';
|
|
7
8
|
import Layout from '../../../../components/Layout/Main';
|
|
8
|
-
import { MarkdownComponents } from '../../../../components/Markdown/MarkdownComponents';
|
|
9
|
+
import { MarkdownComponents, remarkCodeMeta } from '../../../../components/Markdown/MarkdownComponents';
|
|
9
10
|
import AnchorNav from '../../../../components/Navigation/AnchorNav';
|
|
10
11
|
import HeadersType from '../../../../components/Typography/Headers';
|
|
11
12
|
import { Table, TableBody, TableCell, TableRow } from '../../../../components/ui/table';
|
|
@@ -53,7 +54,7 @@ const ComponentsPage = ({ content, menu, metadata, current, config, design }: Fo
|
|
|
53
54
|
<div className="mt-10 lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_280px]">
|
|
54
55
|
<div>
|
|
55
56
|
<div className="prose">
|
|
56
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
57
|
+
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
57
58
|
{content}
|
|
58
59
|
</ReactMarkdown>
|
|
59
60
|
</div>
|
package/src/cli.ts
CHANGED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { GetStaticProps } from 'next';
|
|
2
|
-
import Head from 'next/head';
|
|
3
|
-
import Link from 'next/link';
|
|
4
|
-
import ReactMarkdown from 'react-markdown';
|
|
5
|
-
import rehypeRaw from 'rehype-raw';
|
|
6
|
-
import Layout from '../../../components/Layout/Main';
|
|
7
|
-
import { MarkdownComponents } from '../../../components/Markdown/MarkdownComponents';
|
|
8
|
-
import HeadersType from '../../../components/Typography/Headers';
|
|
9
|
-
import {
|
|
10
|
-
buildL2StaticPaths,
|
|
11
|
-
DocumentationProps,
|
|
12
|
-
fetchDocPageMarkdown,
|
|
13
|
-
getClientRuntimeConfig,
|
|
14
|
-
IParams,
|
|
15
|
-
reduceSlugToString,
|
|
16
|
-
} from '../../../components/util';
|
|
17
|
-
|
|
18
|
-
export interface SubPageType {
|
|
19
|
-
params: {
|
|
20
|
-
level1: string;
|
|
21
|
-
level2: string;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Render all index pages
|
|
26
|
-
* @returns
|
|
27
|
-
*/
|
|
28
|
-
export async function getStaticPaths() {
|
|
29
|
-
return {
|
|
30
|
-
paths: buildL2StaticPaths(),
|
|
31
|
-
fallback: false,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Get all the markdown data, build a menu tree, and then fetch the contents
|
|
36
|
-
* of the current page for rendering
|
|
37
|
-
*
|
|
38
|
-
* This is all done statically at build time
|
|
39
|
-
* @param context GetStaticProps
|
|
40
|
-
* @returns
|
|
41
|
-
*/
|
|
42
|
-
export const getStaticProps: GetStaticProps = async (context) => {
|
|
43
|
-
// Read current slug
|
|
44
|
-
const { level1 } = context.params as IParams;
|
|
45
|
-
let { level2 } = context.params as IParams;
|
|
46
|
-
if (!level2) {
|
|
47
|
-
level2 = '404';
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
props: {
|
|
51
|
-
...fetchDocPageMarkdown(`docs/${level1}/`, reduceSlugToString(level2), `/${level1}`).props,
|
|
52
|
-
config: getClientRuntimeConfig(),
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Render Docs page
|
|
59
|
-
* @param param0
|
|
60
|
-
* @returns
|
|
61
|
-
*/
|
|
62
|
-
export default function DocSubPage({ content, menu, metadata, current, config }: DocumentationProps) {
|
|
63
|
-
if (content) {
|
|
64
|
-
return (
|
|
65
|
-
<Layout config={config} menu={menu} current={current} metadata={metadata}>
|
|
66
|
-
<div className="flex flex-col gap-2 pb-7">
|
|
67
|
-
<HeadersType.H1>{metadata.title}</HeadersType.H1>
|
|
68
|
-
<p className="text-lg leading-relaxed text-gray-600 dark:text-gray-300">{metadata.description}</p>
|
|
69
|
-
</div>
|
|
70
|
-
<div>
|
|
71
|
-
<div className="prose mb-10">
|
|
72
|
-
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
73
|
-
{content}
|
|
74
|
-
</ReactMarkdown>
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
</Layout>
|
|
78
|
-
);
|
|
79
|
-
} else {
|
|
80
|
-
return (
|
|
81
|
-
<div className="flex min-h-screen flex-col items-center justify-center bg-white dark:bg-gray-900">
|
|
82
|
-
<Head>
|
|
83
|
-
<title>404 - Page Not Found</title>
|
|
84
|
-
<meta name="description" content="Page Not Found" />
|
|
85
|
-
</Head>
|
|
86
|
-
<div className="flex flex-col items-center">
|
|
87
|
-
<div className="mb-2 text-7xl font-bold text-gray-800 dark:text-white">404</div>
|
|
88
|
-
<h1 className="mb-4 text-2xl font-semibold text-gray-700 dark:text-gray-300">Oops! Page not found.</h1>
|
|
89
|
-
<p className="mb-6 max-w-md text-center text-gray-500 dark:text-gray-400">
|
|
90
|
-
Sorry, the page you are looking for does not exist or has been moved.
|
|
91
|
-
<br />
|
|
92
|
-
Please check the URL or return to the homepage.
|
|
93
|
-
</p>
|
|
94
|
-
<Link
|
|
95
|
-
href="/"
|
|
96
|
-
className="rounded-md bg-blue-600 px-6 py-2 font-medium text-white shadow-md transition-colors duration-200 hover:bg-blue-700"
|
|
97
|
-
>
|
|
98
|
-
Go Home
|
|
99
|
-
</Link>
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
}
|