mdact 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.gitkeep ADDED
File without changes
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # mdact
2
+
3
+ mdact is a markdown-to-React library plus a reference blog you can host on GitHub Pages or Vercel. Drop `.md` files into `content/` and the app renders them with the custom mdact parser.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install mdact
9
+ ```
10
+
11
+ ## Local development
12
+
13
+ ```bash
14
+ npm install
15
+ npm run dev
16
+ ```
17
+
18
+ ## Setup guide
19
+
20
+ 1. Create new markdown files in `content/` with frontmatter.
21
+ 2. Use `import.meta.glob` (see `src/lib/content.ts`) to load posts.
22
+ 3. Render `post.body` with the mdact renderer.
23
+
24
+ ```md
25
+ ---
26
+ title: My New Post
27
+ summary: A short teaser.
28
+ date: 2024-05-25
29
+ ---
30
+
31
+ # My New Post
32
+
33
+ Write markdown here.
34
+ ```
35
+
36
+ ## Current features
37
+
38
+ - Headings (H1–H3)
39
+ - Paragraphs and blockquotes
40
+ - Lists
41
+ - Fenced code blocks with language labels
42
+ - Inline formatting for **bold**, *italic*, and `inline code`
43
+
44
+ ## Deploying
45
+
46
+ - **GitHub Pages**: Build with `npm run build`, then deploy the `dist/` folder.
47
+ - **Vercel**: Import the repo, set the build command to `npm run build`, and output to `dist`.
@@ -0,0 +1,19 @@
1
+ ---
2
+ title: Building the mdact Parser
3
+ summary: Notes on the handcrafted renderer that powers mdact.
4
+ date: 2024-05-22
5
+ ---
6
+
7
+ # Building the mdact parser
8
+
9
+ This post explains the architecture of the markdown parser inside mdact.
10
+
11
+ ## Inline formatting
12
+
13
+ You can write **bold**, *italic*, or `inline code` without relying on external libraries.
14
+
15
+ ## Lists
16
+
17
+ - One idea
18
+ - Another idea
19
+ - A third idea
@@ -0,0 +1,21 @@
1
+ ---
2
+ title: Hello mdact
3
+ summary: A first entry showing the mdact markdown pipeline.
4
+ date: 2024-05-20
5
+ ---
6
+
7
+ # Hello mdact
8
+
9
+ Welcome to the mdact blog. This file lives in the `content/` folder and is parsed by the mdact markdown-to-React library.
10
+
11
+ ## Why this approach
12
+
13
+ - Markdown stays in Git.
14
+ - The renderer is local and hackable.
15
+ - Vercel/GitHub Pages can host the static output.
16
+
17
+ ```ts
18
+ export const greeting = "Hello from mdact";
19
+ ```
20
+
21
+ > Keep writing. The parser will turn this into components.
@@ -0,0 +1,44 @@
1
+ ---
2
+ title: mdact Docs — Install, Setup, Features
3
+ summary: Everything you need to ship a markdown-powered blog with mdact.
4
+ date: 2024-05-24
5
+ ---
6
+
7
+ # mdact docs
8
+
9
+ mdact is a lightweight markdown-to-React toolkit plus a reference blog you can host on GitHub Pages or Vercel.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install mdact
15
+ ```
16
+
17
+ ## Setup guide
18
+
19
+ 1. Create a `content/` folder in your repo.
20
+ 2. Add markdown files with frontmatter for title, summary, and date.
21
+ 3. Load the posts with `import.meta.glob` (see `src/lib/content.ts`).
22
+ 4. Render the body with the mdact renderer.
23
+
24
+ ```ts
25
+ import { renderMarkdown } from "mdact";
26
+
27
+ export const PostBody = ({ markdown }: { markdown: string }) => {
28
+ return <article>{renderMarkdown(markdown)}</article>;
29
+ };
30
+ ```
31
+
32
+ ## Current features
33
+
34
+ - Headings (H1–H3)
35
+ - Paragraphs and blockquotes
36
+ - Lists
37
+ - Fenced code blocks with language labels
38
+ - Inline formatting for **bold**, *italic*, and `inline code`
39
+
40
+ ## What's next
41
+
42
+ - Custom component slots
43
+ - Tables and images
44
+ - Theme tokens for fast rebranding
package/index.html ADDED
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>mdact</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "mdact",
3
+ "private": false,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.2.0",
13
+ "react-dom": "^18.2.0",
14
+ "react-router-dom": "^6.22.3"
15
+ },
16
+ "devDependencies": {
17
+ "@types/react": "^18.2.66",
18
+ "@types/react-dom": "^18.2.22",
19
+ "@vitejs/plugin-react": "^4.2.1",
20
+ "typescript": "^5.4.2",
21
+ "vite": "^5.2.0"
22
+ }
23
+ }
@@ -0,0 +1,58 @@
1
+ import { ReactNode, useEffect, useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+
4
+ const BlogLayout = ({ children }: { children: ReactNode }) => {
5
+ const [isDark, setIsDark] = useState(false);
6
+
7
+ useEffect(() => {
8
+ document.body.classList.toggle("theme-dark", isDark);
9
+ }, [isDark]);
10
+
11
+ return (
12
+ <div className="app-shell">
13
+ <header className="site-header">
14
+ <div>
15
+ <Link to="/" className="logo">
16
+ mdact
17
+ </Link>
18
+ <p className="tagline">
19
+ A markdown-to-React toolkit and blog starter you can publish anywhere.
20
+ </p>
21
+ </div>
22
+ <div className="header-actions">
23
+ <button
24
+ type="button"
25
+ className="ghost-link"
26
+ onClick={() => setIsDark((prev) => !prev)}
27
+ >
28
+ {isDark ? "Light mode" : "Dark mode"}
29
+ </button>
30
+ <a
31
+ href="https://www.npmjs.com/package/mdact"
32
+ target="_blank"
33
+ rel="noreferrer"
34
+ className="header-link"
35
+ >
36
+ Install mdact
37
+ </a>
38
+ <a
39
+ href="https://github.com/"
40
+ target="_blank"
41
+ rel="noreferrer"
42
+ className="ghost-link secondary-link"
43
+ >
44
+ View repo
45
+ </a>
46
+ </div>
47
+ </header>
48
+ <main className="main-content">{children}</main>
49
+ <footer className="site-footer">
50
+ <p>
51
+ mdact turns markdown into React components so you can publish blogs that feel native.
52
+ </p>
53
+ </footer>
54
+ </div>
55
+ );
56
+ };
57
+
58
+ export default BlogLayout;
@@ -0,0 +1,32 @@
1
+ import { Link } from "react-router-dom";
2
+ import { getPosts } from "../lib/content";
3
+
4
+ const BlogList = () => {
5
+ const posts = getPosts().sort((a, b) => b.date.localeCompare(a.date));
6
+
7
+ return (
8
+ <section className="blog-list">
9
+ <h1>mdact showcase</h1>
10
+ <p className="intro">
11
+ This site is a living demo of the <strong>mdact</strong> library: drop markdown files
12
+ into <code>content/</code> and they render as styled React pages.
13
+ </p>
14
+ <div className="post-grid">
15
+ {posts.map((post) => (
16
+ <article key={post.slug} className="post-card">
17
+ <div>
18
+ <p className="post-date">{post.date}</p>
19
+ <h2>{post.title}</h2>
20
+ <p>{post.summary}</p>
21
+ </div>
22
+ <Link to={`/post/${post.slug}`} className="read-link">
23
+ Read more →
24
+ </Link>
25
+ </article>
26
+ ))}
27
+ </div>
28
+ </section>
29
+ );
30
+ };
31
+
32
+ export default BlogList;
@@ -0,0 +1,36 @@
1
+ import { Link, useParams } from "react-router-dom";
2
+ import { getPostBySlug } from "../lib/content";
3
+ import { renderMarkdown } from "../lib/mdParser";
4
+
5
+ const BlogPost = () => {
6
+ const { slug } = useParams();
7
+ const post = slug ? getPostBySlug(slug) : undefined;
8
+
9
+ if (!post) {
10
+ return (
11
+ <section className="blog-post">
12
+ <h1>Post not found</h1>
13
+ <p>We couldn't locate that markdown file.</p>
14
+ <Link to="/" className="read-link">
15
+ ← Back to posts
16
+ </Link>
17
+ </section>
18
+ );
19
+ }
20
+
21
+ return (
22
+ <section className="blog-post">
23
+ <Link to="/" className="read-link">
24
+ ← Back to posts
25
+ </Link>
26
+ <header className="post-header">
27
+ <p className="post-date">{post.date}</p>
28
+ <h1>{post.title}</h1>
29
+ <p className="post-summary">{post.summary}</p>
30
+ </header>
31
+ {renderMarkdown(post.body)}
32
+ </section>
33
+ );
34
+ };
35
+
36
+ export default BlogPost;
@@ -0,0 +1,61 @@
1
+ export type BlogPost = {
2
+ slug: string;
3
+ title: string;
4
+ summary: string;
5
+ date: string;
6
+ body: string;
7
+ };
8
+
9
+ type FrontMatter = {
10
+ title?: string;
11
+ summary?: string;
12
+ date?: string;
13
+ };
14
+
15
+ const markdownFiles = import.meta.glob("../../content/*.md", {
16
+ as: "raw",
17
+ eager: true,
18
+ });
19
+
20
+ const parseFrontMatter = (raw: string): { frontMatter: FrontMatter; body: string } => {
21
+ if (!raw.startsWith("---")) {
22
+ return { frontMatter: {}, body: raw };
23
+ }
24
+
25
+ const end = raw.indexOf("---", 3);
26
+ if (end === -1) {
27
+ return { frontMatter: {}, body: raw };
28
+ }
29
+
30
+ const frontMatterRaw = raw.slice(3, end).trim();
31
+ const body = raw.slice(end + 3).trim();
32
+ const frontMatter = frontMatterRaw
33
+ .split("\n")
34
+ .map((line) => line.split(":"))
35
+ .reduce<FrontMatter>((acc, [key, ...rest]) => {
36
+ if (!key) return acc;
37
+ acc[key.trim() as keyof FrontMatter] = rest.join(":").trim();
38
+ return acc;
39
+ }, {});
40
+
41
+ return { frontMatter, body };
42
+ };
43
+
44
+ export const getPosts = (): BlogPost[] => {
45
+ return Object.entries(markdownFiles).map(([path, raw]) => {
46
+ const slug = path.split("/").pop()?.replace(".md", "") ?? "";
47
+ const { frontMatter, body } = parseFrontMatter(raw as string);
48
+
49
+ return {
50
+ slug,
51
+ title: frontMatter.title ?? slug,
52
+ summary: frontMatter.summary ?? "",
53
+ date: frontMatter.date ?? "",
54
+ body,
55
+ };
56
+ });
57
+ };
58
+
59
+ export const getPostBySlug = (slug: string): BlogPost | undefined => {
60
+ return getPosts().find((post) => post.slug === slug);
61
+ };
@@ -0,0 +1,163 @@
1
+ import React from "react";
2
+
3
+ type Block =
4
+ | { type: "heading"; level: number; text: string }
5
+ | { type: "paragraph"; text: string }
6
+ | { type: "code"; language: string; text: string }
7
+ | { type: "list"; items: string[] }
8
+ | { type: "blockquote"; text: string };
9
+
10
+ const inlinePattern = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g;
11
+
12
+ const renderInline = (text: string): React.ReactNode[] => {
13
+ const parts: React.ReactNode[] = [];
14
+ let lastIndex = 0;
15
+
16
+ text.replace(inlinePattern, (match, _group, offset) => {
17
+ if (offset > lastIndex) {
18
+ parts.push(text.slice(lastIndex, offset));
19
+ }
20
+
21
+ if (match.startsWith("**")) {
22
+ parts.push(<strong key={`${offset}-bold`}>{match.slice(2, -2)}</strong>);
23
+ } else if (match.startsWith("*")) {
24
+ parts.push(<em key={`${offset}-italic`}>{match.slice(1, -1)}</em>);
25
+ } else if (match.startsWith("`")) {
26
+ parts.push(<code key={`${offset}-code`} className="inline-code">{match.slice(1, -1)}</code>);
27
+ }
28
+
29
+ lastIndex = offset + match.length;
30
+ return match;
31
+ });
32
+
33
+ if (lastIndex < text.length) {
34
+ parts.push(text.slice(lastIndex));
35
+ }
36
+
37
+ return parts;
38
+ };
39
+
40
+ export const parseMarkdown = (markdown: string): Block[] => {
41
+ const lines = markdown.split("\n");
42
+ const blocks: Block[] = [];
43
+ let currentParagraph: string[] = [];
44
+ let currentList: string[] = [];
45
+ let inCodeBlock = false;
46
+ let codeLanguage = "";
47
+ let codeLines: string[] = [];
48
+
49
+ const flushParagraph = () => {
50
+ if (currentParagraph.length > 0) {
51
+ blocks.push({ type: "paragraph", text: currentParagraph.join(" ") });
52
+ currentParagraph = [];
53
+ }
54
+ };
55
+
56
+ const flushList = () => {
57
+ if (currentList.length > 0) {
58
+ blocks.push({ type: "list", items: currentList });
59
+ currentList = [];
60
+ }
61
+ };
62
+
63
+ const flushCode = () => {
64
+ if (codeLines.length > 0) {
65
+ blocks.push({ type: "code", language: codeLanguage, text: codeLines.join("\n") });
66
+ codeLines = [];
67
+ codeLanguage = "";
68
+ }
69
+ };
70
+
71
+ lines.forEach((line) => {
72
+ if (line.startsWith("```")) {
73
+ if (!inCodeBlock) {
74
+ flushParagraph();
75
+ flushList();
76
+ inCodeBlock = true;
77
+ codeLanguage = line.replace("```", "").trim();
78
+ return;
79
+ }
80
+
81
+ inCodeBlock = false;
82
+ flushCode();
83
+ return;
84
+ }
85
+
86
+ if (inCodeBlock) {
87
+ codeLines.push(line);
88
+ return;
89
+ }
90
+
91
+ if (line.trim() === "") {
92
+ flushParagraph();
93
+ flushList();
94
+ return;
95
+ }
96
+
97
+ if (line.startsWith("#")) {
98
+ flushParagraph();
99
+ flushList();
100
+ const level = Math.min(line.match(/^#+/)?.[0].length ?? 1, 3);
101
+ blocks.push({ type: "heading", level, text: line.replace(/^#+\s*/, "") });
102
+ return;
103
+ }
104
+
105
+ if (line.startsWith(">")) {
106
+ flushParagraph();
107
+ flushList();
108
+ blocks.push({ type: "blockquote", text: line.replace(/^>\s*/, "") });
109
+ return;
110
+ }
111
+
112
+ if (line.match(/^[-*]\s+/)) {
113
+ flushParagraph();
114
+ currentList.push(line.replace(/^[-*]\s+/, ""));
115
+ return;
116
+ }
117
+
118
+ currentParagraph.push(line.trim());
119
+ });
120
+
121
+ flushParagraph();
122
+ flushList();
123
+ flushCode();
124
+
125
+ return blocks;
126
+ };
127
+
128
+ export const renderMarkdown = (markdown: string): React.ReactElement => {
129
+ const blocks = parseMarkdown(markdown);
130
+
131
+ return (
132
+ <div className="markdown-content">
133
+ {blocks.map((block, index) => {
134
+ switch (block.type) {
135
+ case "heading":
136
+ if (block.level === 1) return <h1 key={index}>{renderInline(block.text)}</h1>;
137
+ if (block.level === 2) return <h2 key={index}>{renderInline(block.text)}</h2>;
138
+ return <h3 key={index}>{renderInline(block.text)}</h3>;
139
+ case "paragraph":
140
+ return <p key={index}>{renderInline(block.text)}</p>;
141
+ case "code":
142
+ return (
143
+ <pre key={index}>
144
+ <code data-language={block.language}>{block.text}</code>
145
+ </pre>
146
+ );
147
+ case "list":
148
+ return (
149
+ <ul key={index}>
150
+ {block.items.map((item, itemIndex) => (
151
+ <li key={`${index}-${itemIndex}`}>{renderInline(item)}</li>
152
+ ))}
153
+ </ul>
154
+ );
155
+ case "blockquote":
156
+ return <blockquote key={index}>{renderInline(block.text)}</blockquote>;
157
+ default:
158
+ return null;
159
+ }
160
+ })}
161
+ </div>
162
+ );
163
+ };
package/src/main.tsx ADDED
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import { BrowserRouter } from "react-router-dom";
4
+ import App from "./pages/App";
5
+ import "./styles/global.css";
6
+
7
+ ReactDOM.createRoot(document.getElementById("root")!).render(
8
+ <React.StrictMode>
9
+ <BrowserRouter>
10
+ <App />
11
+ </BrowserRouter>
12
+ </React.StrictMode>
13
+ );
@@ -0,0 +1,17 @@
1
+ import { Route, Routes } from "react-router-dom";
2
+ import BlogLayout from "../components/BlogLayout";
3
+ import BlogList from "../components/BlogList";
4
+ import BlogPost from "../components/BlogPost";
5
+
6
+ const App = () => {
7
+ return (
8
+ <BlogLayout>
9
+ <Routes>
10
+ <Route path="/" element={<BlogList />} />
11
+ <Route path="/post/:slug" element={<BlogPost />} />
12
+ </Routes>
13
+ </BlogLayout>
14
+ );
15
+ };
16
+
17
+ export default App;
@@ -0,0 +1,258 @@
1
+ :root {
2
+ font-family: "Inter", "Segoe UI", system-ui, sans-serif;
3
+ color: #1f2937;
4
+ background: #f9fafb;
5
+ }
6
+
7
+ * {
8
+ box-sizing: border-box;
9
+ }
10
+
11
+ body {
12
+ margin: 0;
13
+ background: #f9fafb;
14
+ }
15
+
16
+ body.theme-dark {
17
+ background: #0f172a;
18
+ color: #e2e8f0;
19
+ }
20
+
21
+ a {
22
+ color: inherit;
23
+ text-decoration: none;
24
+ }
25
+
26
+ code {
27
+ font-family: "JetBrains Mono", "SFMono-Regular", monospace;
28
+ }
29
+
30
+ .app-shell {
31
+ min-height: 100vh;
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 2rem;
35
+ padding: 2rem 6vw 3rem;
36
+ }
37
+
38
+ .site-header {
39
+ display: flex;
40
+ justify-content: space-between;
41
+ align-items: center;
42
+ border-bottom: 1px solid #e5e7eb;
43
+ padding-bottom: 1.5rem;
44
+ gap: 1.5rem;
45
+ flex-wrap: wrap;
46
+ }
47
+
48
+ body.theme-dark .site-header {
49
+ border-bottom-color: rgba(148, 163, 184, 0.25);
50
+ }
51
+
52
+ .logo {
53
+ font-size: 1.5rem;
54
+ font-weight: 700;
55
+ }
56
+
57
+ .tagline {
58
+ margin: 0.4rem 0 0;
59
+ color: #6b7280;
60
+ }
61
+
62
+ body.theme-dark .tagline {
63
+ color: #94a3b8;
64
+ }
65
+
66
+ .header-actions {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 0.75rem;
70
+ }
71
+
72
+ .header-link {
73
+ padding: 0.6rem 1.1rem;
74
+ border-radius: 999px;
75
+ background: #111827;
76
+ color: #f9fafb;
77
+ font-weight: 600;
78
+ font-size: 0.9rem;
79
+ }
80
+
81
+ .ghost-link {
82
+ padding: 0.6rem 1rem;
83
+ border-radius: 999px;
84
+ border: 1px solid #e5e7eb;
85
+ font-weight: 600;
86
+ font-size: 0.9rem;
87
+ background: transparent;
88
+ }
89
+
90
+ .secondary-link {
91
+ color: #111827;
92
+ }
93
+
94
+ body.theme-dark .ghost-link {
95
+ border-color: rgba(148, 163, 184, 0.4);
96
+ color: #e2e8f0;
97
+ }
98
+
99
+ body.theme-dark .secondary-link {
100
+ color: #e2e8f0;
101
+ }
102
+
103
+ .main-content {
104
+ flex: 1;
105
+ }
106
+
107
+ .blog-list h1 {
108
+ font-size: 2.4rem;
109
+ margin-bottom: 0.4rem;
110
+ }
111
+
112
+ .intro {
113
+ color: #6b7280;
114
+ max-width: 640px;
115
+ }
116
+
117
+ body.theme-dark .intro {
118
+ color: #94a3b8;
119
+ }
120
+
121
+ .post-grid {
122
+ display: grid;
123
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
124
+ gap: 1.5rem;
125
+ margin-top: 2rem;
126
+ }
127
+
128
+ .post-card {
129
+ background: white;
130
+ border-radius: 1.2rem;
131
+ padding: 1.5rem;
132
+ box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
133
+ display: flex;
134
+ flex-direction: column;
135
+ justify-content: space-between;
136
+ gap: 1.5rem;
137
+ }
138
+
139
+ body.theme-dark .post-card {
140
+ background: #111827;
141
+ box-shadow: 0 12px 28px rgba(15, 23, 42, 0.45);
142
+ }
143
+
144
+ .post-card h2 {
145
+ margin: 0.4rem 0 0.6rem;
146
+ }
147
+
148
+ .post-date {
149
+ font-size: 0.85rem;
150
+ color: #9ca3af;
151
+ text-transform: uppercase;
152
+ letter-spacing: 0.08em;
153
+ }
154
+
155
+ body.theme-dark .post-date {
156
+ color: #94a3b8;
157
+ }
158
+
159
+ .read-link {
160
+ font-weight: 600;
161
+ color: #2563eb;
162
+ }
163
+
164
+ body.theme-dark .read-link {
165
+ color: #93c5fd;
166
+ }
167
+
168
+ .blog-post {
169
+ max-width: 760px;
170
+ }
171
+
172
+ .post-header {
173
+ margin: 1.5rem 0 2rem;
174
+ }
175
+
176
+ .post-summary {
177
+ color: #6b7280;
178
+ font-size: 1.05rem;
179
+ }
180
+
181
+ body.theme-dark .post-summary {
182
+ color: #94a3b8;
183
+ }
184
+
185
+ .markdown-content h1 {
186
+ font-size: 2.3rem;
187
+ margin-bottom: 1rem;
188
+ }
189
+
190
+ .markdown-content h2 {
191
+ font-size: 1.6rem;
192
+ margin-top: 2rem;
193
+ }
194
+
195
+ .markdown-content h3 {
196
+ font-size: 1.3rem;
197
+ margin-top: 1.5rem;
198
+ }
199
+
200
+ .markdown-content p {
201
+ line-height: 1.7;
202
+ color: #374151;
203
+ }
204
+
205
+ body.theme-dark .markdown-content p {
206
+ color: #cbd5f5;
207
+ }
208
+
209
+ .markdown-content ul {
210
+ padding-left: 1.2rem;
211
+ line-height: 1.7;
212
+ }
213
+
214
+ .markdown-content pre {
215
+ background: #111827;
216
+ color: #f9fafb;
217
+ padding: 1.2rem;
218
+ border-radius: 0.9rem;
219
+ overflow-x: auto;
220
+ }
221
+
222
+ body.theme-dark .markdown-content pre {
223
+ background: #0b1120;
224
+ }
225
+
226
+ .inline-code {
227
+ background: #e5e7eb;
228
+ padding: 0.1rem 0.3rem;
229
+ border-radius: 0.3rem;
230
+ }
231
+
232
+ body.theme-dark .inline-code {
233
+ background: rgba(148, 163, 184, 0.2);
234
+ color: #e2e8f0;
235
+ }
236
+
237
+ .markdown-content blockquote {
238
+ border-left: 4px solid #60a5fa;
239
+ padding-left: 1rem;
240
+ color: #4b5563;
241
+ margin: 1.5rem 0;
242
+ }
243
+
244
+ body.theme-dark .markdown-content blockquote {
245
+ color: #cbd5f5;
246
+ border-left-color: #38bdf8;
247
+ }
248
+
249
+ .site-footer {
250
+ border-top: 1px solid #e5e7eb;
251
+ padding-top: 1.5rem;
252
+ color: #6b7280;
253
+ }
254
+
255
+ body.theme-dark .site-footer {
256
+ border-top-color: rgba(148, 163, 184, 0.25);
257
+ color: #94a3b8;
258
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "Bundler",
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "noEmit": true,
12
+ "jsx": "react-jsx",
13
+ "strict": true
14
+ },
15
+ "include": ["src"]
16
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ });